Skip to content

[PE-7191] External wallet swap flow - #13276

Merged
faridsalau merged 16 commits into
mainfrom
jd/external-wallet-swap
Oct 21, 2025
Merged

[PE-7191] External wallet swap flow#13276
faridsalau merged 16 commits into
mainfrom
jd/external-wallet-swap

Conversation

@DejayJD

@DejayJD DejayJD commented Oct 20, 2025

Copy link
Copy Markdown
Contributor

Description

Wallet connection flow
2025-10-20 14 16 20

Swap Flow
2025-10-20 10 54 46

  • Adds UI for external wallet swaps
  • Updates useBuySellSwap hook to not handle the actual swap hook any more, this is now handled by web/mobile respectively
  • Aligns the input/output types of internal & external swap hooks

How Has This Been Tested?

✅ internal wallet swaps still work
✅ launchpad swaps still work
✅ mobile swaps still work
✅ tested swaps for USDC<>Coin, Audio<>Coin, and Coin<>Coin

@changeset-bot

changeset-bot Bot commented Oct 20, 2025

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ca2c984

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@DejayJD
DejayJD requested review from a team, dylanjeffers and faridsalau October 20, 2025 19:03

@dylanjeffers dylanjeffers left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super solid

prefix: string = '$'
): string => {
if (num === 0) return '$0.00'
if (num === 0) return `${prefix}0.00`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's this for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allows replacing the $ that gets prefixed to these strings - in this case removing it

@faridsalau faridsalau Oct 21, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still confused, seems like it's doing the same as before?

selectedPair: safeSelectedPair,
onClose
swapHookData: swapHookState,
handleSwap: (params) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleSwap: performSwap

selectedPair,
onClose: () => navigation.goBack()
swapHookData: swapHookState,
handleSwap: (params) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, is it the case the types are a bit different or something?

const externalWalletAccount = appkitModal.getAccount()
const internalSwapHook = useSwapCoins()
const externalSwapHook = useExternalWalletSwap()
const { mutateAsync: performSwap, ...swapHookState } =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sick

| typeof IconMetamask
| typeof IconSolana

const getWalletIcon = (): WalletIconComponent => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in your demo video, looks like this is showing audio icon when it should show phantom?


const tokenBalanceString = isUsingExternalWallet
? externalWalletTokenBalance
? formatCurrency(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a ton of helpers, but this still feels awk

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to follow on this:

  • this nesting is not the clearest to read, can we break up pls
  • we shouldn't default to showing 0 balances ever in case there's an error upstream. we should only show 0 as a balance if the user truly has 0 balance

border='default'
alignItems='center'
justifyContent='center'
css={(theme) => ({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in your demo the circle seems a bit too large compared to the icon itself

SwapStatus,
SwapTokensParams,
SwapTokensResult
} from '@audius/common/src/api/tan-query/jupiter/types'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not import from source

const outputAmount = result.outputAmount!
const outputAmount = result.outputAmount!.uiAmount
const newAmount = currentAmount + outputAmount
return new FixedDecimal(newAmount, oldBalance.decimalPlaces)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent job in this file

address: mintAddress,
decimals: LAUNCHPAD_COIN_DECIMALS
},
inputMint: TOKEN_LISTING_MAP.AUDIO.address,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so just to confirm, you were able to use the same external swap flow for both buy audio and external buy/sell/swap?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean the launchpad buy flow? Yes

Comment on lines +25 to +37
swapHookData: {
data?: SwapTokensResult
status: MutationStatus
error?: Error | null
}
// The swap is handled externally to allow for external wallet swaps
// Web and mobile use different services for these so we let each repo handle the logic
handleSwap: (params: {
inputMint: string
outputMint: string
amountUi: number
slippageBps: number
}) => void

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can make these their own types

import { useConnectExternalWallets } from 'hooks/useConnectExternalWallets'
import { env } from 'services/env'

const messages = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we have walletMessages.ts

Comment on lines +42 to +43
if (typeof window !== 'undefined' && window.ethereum) {
return IconMetamask

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the spec from design?

inputToken: { mint: string; symbol: string }
}) => {
const { data: currentUser } = useCurrentAccountUser()
const isAnonymousUser = !currentUser

@faridsalau faridsalau Oct 21, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: isLoggedOutUser feels more clear to me or isUnauthed tbh not bad to just use !currentUser everywhere isAnonymousUser is used. probably the most clear and is less chars haha

Comment on lines +70 to +79
} = useExternalWalletBalance({
walletAddress: externalWalletAccount?.address,
mint: inputToken.mint
})

const {
data: internalWalletTokenBalanceData,
isPending: isInternalWalletTokenBalanceLoading
} = useCoinBalance({
mint: inputToken.mint,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we not disable these if the other is being used?

? isExternalWalletTokenBalanceLoading
: isInternalWalletTokenBalanceLoading

const noCurrentWallet = isAnonymousUser && !isUsingExternalWallet

@faridsalau faridsalau Oct 21, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's do const hasUnauthedWallet = isAnonymousUser && isUsingExternalWallet and then we dont' have to do the negation on a negative below

? messages.builtInWalletNotAvailable
: messages.tradeWith}
</Text>
{/* Wallet pill */}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we have a pill component in harmony?

</Flex>
<TextLink
variant='visible'
href='#'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intentional?

}, [coins, coinsLoading])

// Get tokens that user owns (includes USDC if user has balance)
// TODO : modify this to account for external wallets

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#antiTodoComments 😿

const shouldShowError =
!!displayErrorMessage || (activeTab === 'buy' && !hasSufficientBalance)

const userHasNoWallet = !externalWalletAccount?.address && !currentUser

@faridsalau faridsalau Oct 21, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should try to use affirmative var names, so something like userHasWallet and using negation thing applies here as well

@@ -4,15 +4,19 @@ import {
TEMP_ARTIST_COINS_PAGE_SIZE,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var names

setCurrentStep(BuyModalStep.Loading)
}
if (swapError || swapData?.isError) {
if (swapError || swapData?.error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tested?

@@ -352,7 +356,7 @@ type AssetDetailProps = {

const BalanceSectionContent = ({ mint }: AssetDetailProps) => {
const { data: coin, isPending: coinsLoading } = useArtistCoin(mint)
const { data: tokenBalance, isPending: tokenBalanceLoading } = useCoinBalance(
const { data: tokenBalance, isLoading: tokenBalanceLoading } = useCoinBalance(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curious why we're using isLoading instead of isPending?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isPending = "wait until data shows up" - if there's no data ever (cause no user) isPending is permanently false. isLoading just means "is it trying to fetch data right now"

amountUi: number
walletAddress: string
isAMM: boolean
solanaConnection: any

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we avoid using any here

@faridsalau faridsalau Oct 21, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, there's a bit of overlap with these types. can we create a base types? maybe that's SwapTokenParams

Comment on lines +64 to +66
transaction: VersionedTransaction
inputAmount: { amount: number; uiAmount: number }
outputAmount: { amount: number; uiAmount: number }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: make into its own type

quoteResponse: firstQuote.quote,
userPublicKey: walletAddress,
dynamicSlippage: true,
useSharedAccounts: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have u tried with true?

Comment on lines +116 to +117
// Retry without shared accounts if it fails
swapRequest1.useSharedAccounts = false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's already false. also, we try 3 more times in internal flow, can perhaps try more times here?

})
} catch (e) {
// Retry without shared accounts if it fails
swapRequest2.useSharedAccounts = false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

}

// Convert instructions to TransactionInstructions
const firstSetupInstructions = convertJupiterInstructions(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we just use convertJupiterInstructions([...allInstructions]) instead of having to use it for each ix?


hookProgress.receivedQuote = true
let transaction: VersionedTransaction
let inputAmount: { amount: number; uiAmount: number }

@faridsalau faridsalau Oct 21, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should make a type for { amount: number; uiAmount: number } it's used all over the place, maybe SwapAmount or something

audioMint: env.WAUDIO_MINT_ADDRESS,
inputDecimals,
outputDecimals,
audioDecimals: 8, // AUDIO has 8 decimals

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use this from TOKEN_LISTING_MAP

cursor[bot]

This comment was marked as outdated.

@faridsalau
faridsalau merged commit d4f7649 into main Oct 21, 2025
4 of 5 checks passed
@faridsalau
faridsalau deleted the jd/external-wallet-swap branch October 21, 2025 18:39
// External wallet swaps require some extra params. These are unused for internal swaps
inputDecimals: swapTokens.inputTokenInfo!.decimals,
outputDecimals: swapTokens.outputTokenInfo!.decimals,
walletAddress: externalWalletAccount?.address as string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Token Info Nullability Causes Assertion Failures

Non-null assertions on swapTokens.inputTokenInfo!.decimals and swapTokens.outputTokenInfo!.decimals can cause runtime errors. This occurs because inputTokenInfo and outputTokenInfo can be null when currentTokenPair.baseToken or currentTokenPair.quoteToken are unavailable.

Fix in Cursor Fix in Web

dylanjeffers pushed a commit that referenced this pull request Oct 21, 2025
### Description
Wallet connection flow
![2025-10-20 14 16
20](https://github.com/user-attachments/assets/ae8847c5-5d67-4248-8a8c-695ade1c5673)


Swap Flow
![2025-10-20 10 54
46](https://github.com/user-attachments/assets/d94283f3-c764-41fc-b680-da0c4d8f4d0d)


- Adds UI for external wallet swaps
- Updates useBuySellSwap hook to not handle the actual swap hook any
more, this is now handled by web/mobile respectively
- Aligns the input/output types of internal & external swap hooks

### How Has This Been Tested?
✅ internal wallet swaps still work
✅ launchpad swaps still work
✅ mobile swaps still work
✅ tested swaps for USDC<>Coin, Audio<>Coin, and Coin<>Coin

---------

Co-authored-by: Farid Salau <faridsalau@gmail.com>
audius-infra pushed a commit that referenced this pull request Oct 25, 2025
[4b1ce10] [PE-7239] Fix issues due to stale local storage data (#13311) Dylan Jeffers
[7a88ddf] Artist coins page size 1000 (#13313) Reed
[3d246f6] Some additional analytics around wallets & launchpad (#13312) JD Francis
[f22659e] [PE-7238] Add analytics for external wallet swaps (#13309) JD Francis
[421a9e9] 💚🟢🐸🥬 (#13308) JD Francis
[ffb4af9] Fix wallet connect not triggering success in claim flow (#13305) JD Francis
[68fa1a4] [PE-7229] Fix referrer signup (#13306) Dylan Jeffers
[6ed86b5] Version Packages (#13254) github-actions[bot]
[b9b22cd] [PE-7204] Associate wallets on solana /relay calls (#13299) JD Francis
[ad01774] Revert "[PE-7211] Claim fees after connect" (#13303) JD Francis
[1819c1e] fix mobile ci (#13302) JD Francis
[afeceae] Fix AUDIO breakdown (#13298) Farid Salau
[68eace6] birdeye.so (#13301) Marcus Pasell
[30f3e66] [PE-7215] Add sol back into the list of tokens + Dont show 'add cash' (#13295) JD Francis
[6e74a41] Fix CI (#13297) Farid Salau
[e68b261] [PE-7212] Claim fees optimistic updates (#13290) Dylan Jeffers
[d1000f1] [PE-7211] Claim fees after connect (#13291) Dylan Jeffers
[f1f1e94] [PE-7210] Handle lowercase coin urls (#13284) Dylan Jeffers
[8eade23] [PE-7226] Fix issues with confirmation screen (#13294) JD Francis
[0b58a67] [PE-7198] Update mobile profile edit flow UI (#13271) Farid Salau
[dd17039] eth key exporter (#13293) alecsavvy
[a6d5e9d] Use production wAUDIO on stage (#13288) Marcus Pasell
[0bd603d] [PE-7213] Prevent claiming dust (#13289) Dylan Jeffers
[b4dde83] Add polling for audio balance changes (#13228) JD Francis
[315865f] Fix lint (#13285) Dylan Jeffers
[ee7ee7f] [PE-7209] External wallet coin dropdown (#13283) Dylan Jeffers
[d4f7649] [PE-7191] External wallet swap flow (#13276) JD Francis
[4a38e63] Rename asset to coin (#13275) Dylan Jeffers
[c3ffa17] Show DAMM V2 Fees in UI (#13280) Marcus Pasell
[172f129] Remove extra dbcpool arg (#13269) Ray Jacobson
[3dafa35] [PE-7199] Refactor buy/sell flow coin filtering logic  (#13272) Farid Salau
[c1d796d] [PE-7193] Announcement notifs link to artist coin explore (#13266) Reed
[3e561d2] [PE-7197] Update ArtistCoinHoverCard to include member vs creator  (#13270) Farid Salau
[50878b5] Small QA items (#13273) Farid Salau
[932fbdb] Remove search explore flag (#13261) Dylan Jeffers
[5da716e] [PE-7163] Redirect mispelled coins to 404 (#13274) Ray Jacobson
[6866949] Finalize mobile search explore (#13260) Dylan Jeffers
[0ec2f5f] [PE-7189] Add balance breakdown component  (#13262) Farid Salau
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants