Skip to content

Commit 373b924

Browse files
committed
Add send tokens to audius users flow
1 parent 29a4bbd commit 373b924

8 files changed

Lines changed: 765 additions & 129 deletions

File tree

packages/common/src/api/tan-query/wallets/useSendCoins.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { useCoinBalance } from './useCoinBalance'
1818
export type SendCoinsParams = {
1919
recipientWallet: SolanaWalletAddress
2020
amount: bigint
21+
recipientEthAddress?: string // Optional: when sending to a user, provide their ETH address to derive user-bank ATA
2122
}
2223

2324
export type SendCoinsResult = {
@@ -49,7 +50,8 @@ export const useSendCoins = ({ mint }: { mint: string }) => {
4950
return useMutation({
5051
mutationFn: async ({
5152
recipientWallet,
52-
amount
53+
amount,
54+
recipientEthAddress
5355
}: SendCoinsParams): Promise<SendCoinsResult> => {
5456
try {
5557
const currentUser = walletAddresses?.currentUser
@@ -68,7 +70,8 @@ export const useSendCoins = ({ mint }: { mint: string }) => {
6870
amount: amount as any, // TODO: Fix type mismatch between bigint and AudioWei
6971
ethAddress: currentUser,
7072
sdk,
71-
mint: new PublicKey(mint) as any // TODO: Fix type mismatch between string and MintName | PublicKey
73+
mint: new PublicKey(mint) as any, // TODO: Fix type mismatch between string and MintName | PublicKey
74+
recipientEthAddress // Optional: when provided, derives user-bank ATA instead of regular ATA
7275
})
7376

7477
return {

packages/common/src/services/audius-backend/AudiusBackend.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -901,19 +901,33 @@ export const audiusBackend = ({
901901
amount,
902902
ethAddress,
903903
sdk,
904-
mint
904+
mint,
905+
recipientEthAddress
905906
}: {
906907
address: string
907908
amount: AudioWei
908909
ethAddress: string
909910
sdk: AudiusSdk
910911
mint: PublicKey
912+
recipientEthAddress?: string // When provided, derives user-bank ATA for the recipient
911913
}) {
912-
const tokenAccountAddress = await getOrCreateAssociatedTokenAccount({
913-
address,
914-
sdk,
915-
mint
916-
})
914+
let tokenAccountAddress: PublicKey
915+
916+
if (recipientEthAddress) {
917+
// When sending to a user, derive their user-bank ATA for this Solana mint
918+
// The user-bank is a PDA derived from their Ethereum address and the mint
919+
tokenAccountAddress = await sdk.services.claimableTokensClient.deriveUserBank({
920+
ethWallet: recipientEthAddress,
921+
mint
922+
})
923+
} else {
924+
// When sending to a Solana wallet address directly, use regular ATA logic
925+
tokenAccountAddress = await getOrCreateAssociatedTokenAccount({
926+
address,
927+
sdk,
928+
mint
929+
})
930+
}
917931

918932
const res = await transferTokens({
919933
destination: tokenAccountAddress,

packages/web/src/components/send-tokens-modal/SendTokensConfirmation.tsx

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
useCoinBalance,
66
transformArtistCoinToTokenInfo
77
} from '@audius/common/api'
8-
import { walletMessages } from '@audius/common/messages'
8+
import { User, SquareSizes } from '@audius/common/models'
99
import { FixedDecimal } from '@audius/fixed-decimal'
1010
import {
1111
Button,
@@ -14,24 +14,29 @@ import {
1414
Divider,
1515
Hint,
1616
Checkbox,
17-
useMedia,
18-
useTheme
17+
useTheme,
18+
Avatar
1919
} from '@audius/harmony'
2020

2121
import { CryptoBalanceSection } from 'components/buy-sell-modal/CryptoBalanceSection'
22+
import UserBadges from 'components/user-badges/UserBadges'
23+
import { useProfilePicture } from 'hooks/useProfilePicture'
2224

2325
interface SendTokensConfirmationProps {
2426
mint: string
2527
amount: bigint
2628
destinationAddress: string
29+
selectedUser: User | null
2730
onConfirm: () => void
2831
onBack: () => void
2932
onClose: () => void
3033
}
3134

3235
const messages = {
3336
sendTitle: 'SEND',
34-
amountToSend: 'Amount to Send',
37+
sending: 'Sending',
38+
toRecipient: 'To Recipient',
39+
recipient: 'Recipient',
3540
destinationAddress: 'Destination Address',
3641
reviewDetails: 'Review Details Carefully',
3742
reviewDescription:
@@ -47,12 +52,12 @@ const SendTokensConfirmation = ({
4752
mint,
4853
amount,
4954
destinationAddress,
55+
selectedUser,
5056
onConfirm,
5157
onBack
5258
}: SendTokensConfirmationProps) => {
5359
const { color } = useTheme()
5460
const [isConfirmed, setIsConfirmed] = useState(false)
55-
const { isMobile } = useMedia()
5661

5762
// Get token data and balance using the same hooks as ReceiveTokensModal
5863
const { data: coin } = useArtistCoin(mint)
@@ -63,6 +68,11 @@ const SendTokensConfirmation = ({
6368
})
6469
const tokenInfo = coin ? transformArtistCoinToTokenInfo(coin) : undefined
6570

71+
const profilePicture = useProfilePicture({
72+
userId: selectedUser?.user_id,
73+
size: SquareSizes.SIZE_150_BY_150
74+
})
75+
6676
const formatAmount = (amount: bigint) => {
6777
return new FixedDecimal(amount, tokenInfo?.decimals).toLocaleString(
6878
'en-US',
@@ -105,36 +115,73 @@ const SendTokensConfirmation = ({
105115

106116
<Divider orientation='horizontal' />
107117

108-
{/* Amount Info */}
109-
<Flex
110-
direction={isMobile ? 'column' : 'row'}
111-
justifyContent='space-between'
112-
gap='m'
113-
>
118+
{/* Sending Section */}
119+
<Flex column gap='s'>
114120
<Text variant='heading' size='s' color='subdued'>
115-
{messages.amountToSend}
116-
</Text>
117-
<Text variant='heading' size='s' css={{ wordBreak: 'break-all' }}>
118-
{walletMessages.minus}
119-
{formatAmount(amount)} ${tokenInfo.symbol}
121+
{messages.sending}
120122
</Text>
123+
<Flex alignItems='center' gap='s'>
124+
{/* Token logo would go here */}
125+
<Flex direction='column' gap='xs'>
126+
<Text variant='body' size='m' color='default' strength='strong'>
127+
{tokenInfo.name}
128+
</Text>
129+
<Text variant='heading' size='s' color='default'>
130+
{formatAmount(amount)} ${tokenInfo.symbol}
131+
</Text>
132+
</Flex>
133+
</Flex>
121134
</Flex>
122135

123136
<Divider orientation='horizontal' />
124137

125-
{/* Transfer Info */}
126-
<Flex column gap='m'>
138+
{/* To Recipient Section */}
139+
<Flex column gap='s'>
127140
<Text variant='heading' size='s' color='subdued'>
128-
{messages.destinationAddress}
129-
</Text>
130-
<Text
131-
variant='body'
132-
size='l'
133-
color='default'
134-
css={{ wordBreak: 'break-all' }}
135-
>
136-
{destinationAddress}
141+
{messages.toRecipient}
137142
</Text>
143+
{selectedUser ? (
144+
<Flex alignItems='center' gap='s'>
145+
<Avatar
146+
h={32}
147+
w={32}
148+
src={profilePicture}
149+
borderWidth='thin'
150+
css={{ flexShrink: 0 }}
151+
/>
152+
<Flex direction='column' flex={1} css={{ minWidth: 0 }}>
153+
<Flex alignItems='center' gap='xs' css={{ minWidth: 0 }}>
154+
<Text
155+
variant='body'
156+
size='m'
157+
color='default'
158+
ellipses
159+
strength='strong'
160+
>
161+
{selectedUser.name}
162+
</Text>
163+
<UserBadges userId={selectedUser.user_id} size='xs' inline />
164+
</Flex>
165+
<Text variant='body' size='s' color='subdued' ellipses>
166+
@{selectedUser.handle}
167+
</Text>
168+
</Flex>
169+
</Flex>
170+
) : (
171+
<Flex column gap='xs'>
172+
<Text variant='heading' size='s' color='subdued'>
173+
{messages.destinationAddress}
174+
</Text>
175+
<Text
176+
variant='body'
177+
size='m'
178+
color='default'
179+
css={{ wordBreak: 'break-all' }}
180+
>
181+
{destinationAddress}
182+
</Text>
183+
</Flex>
184+
)}
138185
</Flex>
139186

140187
{/* Review Details Hint */}

packages/web/src/components/send-tokens-modal/SendTokensFailure.tsx

Lines changed: 75 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
useCoinBalance,
44
transformArtistCoinToTokenInfo
55
} from '@audius/common/api'
6+
import { User, SquareSizes } from '@audius/common/models'
67
import { FixedDecimal } from '@audius/fixed-decimal'
78
import {
89
Button,
@@ -12,22 +13,27 @@ import {
1213
CompletionCheck,
1314
IconExternalLink,
1415
PlainButton,
15-
useMedia
16+
useMedia,
17+
Avatar
1618
} from '@audius/harmony'
1719

1820
import { CryptoBalanceSection } from 'components/buy-sell-modal/CryptoBalanceSection'
21+
import UserBadges from 'components/user-badges/UserBadges'
22+
import { useProfilePicture } from 'hooks/useProfilePicture'
1923

2024
interface SendTokensFailureProps {
2125
mint: string
2226
amount: bigint
2327
destinationAddress: string
28+
selectedUser: User | null
2429
error: string
2530
onTryAgain: () => void
2631
onClose: () => void
2732
}
2833

2934
const messages = {
3035
failed: 'Failed',
36+
recipient: 'Recipient',
3137
destinationAddress: 'Destination Address',
3238
viewOnSolana: 'View On Solana Block Explorer',
3339
transactionFailed: 'Your transaction failed to complete.',
@@ -39,6 +45,7 @@ const SendTokensFailure = ({
3945
mint,
4046
amount,
4147
destinationAddress,
48+
selectedUser,
4249
error,
4350
onTryAgain,
4451
onClose
@@ -56,6 +63,11 @@ const SendTokensFailure = ({
5663
? tokenBalance.balance.value
5764
: BigInt(0)
5865

66+
const profilePicture = useProfilePicture({
67+
userId: selectedUser?.user_id,
68+
size: SquareSizes.SIZE_150_BY_150
69+
})
70+
5971
const formatAmount = (amount: bigint) => {
6072
return new FixedDecimal(amount, tokenInfo?.decimals).toLocaleString(
6173
'en-US',
@@ -98,48 +110,77 @@ const SendTokensFailure = ({
98110

99111
<Divider orientation='horizontal' color='default' />
100112

101-
{/* Amount Info */}
102-
<Flex
103-
direction={isMobile ? 'column' : 'row'}
104-
gap='m'
105-
justifyContent='space-between'
106-
>
113+
{/* Failed Section */}
114+
<Flex column gap='s'>
107115
<Text variant='heading' size='s' color='subdued'>
108116
{messages.failed}
109117
</Text>
110-
<Text variant='heading' size='s' color='default'>
111-
-{formatAmount(amount)} ${tokenInfo.symbol}
112-
</Text>
118+
<Flex alignItems='center' gap='s'>
119+
{/* Token logo would go here */}
120+
<Flex direction='column' gap='xs'>
121+
<Text variant='body' size='m' color='default' strength='strong'>
122+
{tokenInfo.name}
123+
</Text>
124+
<Text variant='heading' size='s' color='default'>
125+
{formatAmount(amount)} ${tokenInfo.symbol}
126+
</Text>
127+
</Flex>
128+
</Flex>
113129
</Flex>
114130

115131
<Divider orientation='horizontal' color='default' />
116132

117-
{/* Address Container */}
118-
<Flex direction='column' gap='m'>
133+
{/* To Recipient Section */}
134+
<Flex direction='column' gap='s'>
119135
<Text variant='heading' size='s' color='subdued'>
120-
{messages.destinationAddress}
121-
</Text>
122-
<Text
123-
variant='body'
124-
size='m'
125-
color='default'
126-
css={{ wordBreak: 'break-all' }}
127-
>
128-
{destinationAddress}
136+
{messages.recipient}
129137
</Text>
130-
<PlainButton
131-
variant='subdued'
132-
css={{ alignSelf: 'flex-start' }}
133-
onClick={() => {
134-
window.open(
135-
`https://explorer.solana.com/address/${destinationAddress}`,
136-
'_blank'
137-
)
138-
}}
139-
iconRight={IconExternalLink}
140-
>
141-
{messages.viewOnSolana}
142-
</PlainButton>
138+
{selectedUser ? (
139+
<Flex alignItems='center' gap='s'>
140+
<Avatar
141+
h={32}
142+
w={32}
143+
src={profilePicture}
144+
borderWidth='thin'
145+
css={{ flexShrink: 0 }}
146+
/>
147+
<Flex direction='column' flex={1} css={{ minWidth: 0 }}>
148+
<Flex alignItems='center' gap='xs' css={{ minWidth: 0 }}>
149+
<Text variant='body' size='m' color='default' ellipses strength='strong'>
150+
{selectedUser.name}
151+
</Text>
152+
<UserBadges userId={selectedUser.user_id} size='xs' inline />
153+
</Flex>
154+
<Text variant='body' size='s' color='subdued' ellipses>
155+
@{selectedUser.handle}
156+
</Text>
157+
</Flex>
158+
</Flex>
159+
) : (
160+
<>
161+
<Text
162+
variant='body'
163+
size='m'
164+
color='default'
165+
css={{ wordBreak: 'break-all' }}
166+
>
167+
{destinationAddress}
168+
</Text>
169+
<PlainButton
170+
variant='subdued'
171+
css={{ alignSelf: 'flex-start' }}
172+
onClick={() => {
173+
window.open(
174+
`https://explorer.solana.com/address/${destinationAddress}`,
175+
'_blank'
176+
)
177+
}}
178+
iconRight={IconExternalLink}
179+
>
180+
{messages.viewOnSolana}
181+
</PlainButton>
182+
</>
183+
)}
143184
</Flex>
144185

145186
{/* Error Message */}

0 commit comments

Comments
 (0)