Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 39 additions & 22 deletions packages/common/src/services/audius-backend/AudiusBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
} from '../../store'
import { getErrorMessage, uuid, Maybe, Nullable } from '../../utils'

import { MintName, createUserBankIfNeeded } from './solana'
import { MintName } from './solana'

type DisplayEncoding = 'utf8' | 'hex'
type PhantomEvent = 'disconnect' | 'connect' | 'accountChanged'
Expand Down Expand Up @@ -990,47 +990,59 @@ export const audiusBackend = ({
mint: PublicKey
recipientEthAddress?: string // When provided, derives user-bank ATA for the recipient
}) {
let tokenAccountAddress: PublicKey

if (recipientEthAddress) {
// When sending to a user, ensure their user-bank account exists
// This will create it if needed (in a separate transaction)
tokenAccountAddress = await createUserBankIfNeeded(sdk, {
ethAddress: recipientEthAddress,
mint: mint as any,
recordAnalytics: () => {} // Analytics handled elsewhere
// When sending to a user by ETH address, derive their user bank and
// combine account creation (if needed) + transfer in a single tx.
const { userBank, instruction: createInstruction } =
await sdk.services.claimableTokensClient.createUserBankIfNeededInstruction(
{
ethWallet: recipientEthAddress,
mint: mint as any
}
)

const res = await transferTokens({
destination: userBank,
amount,
ethAddress,
sdk,
mint,
prefixInstructions: createInstruction ? [createInstruction] : []
})
return { res, error: null }
} else {
// When sending to a Solana wallet address directly, use regular ATA logic
tokenAccountAddress = await getOrCreateAssociatedTokenAccount({
const tokenAccountAddress = await getOrCreateAssociatedTokenAccount({
address,
sdk,
mint
})
}

const res = await transferTokens({
destination: tokenAccountAddress,
amount,
ethAddress,
sdk,
mint
})
return { res, error: null }
const res = await transferTokens({
destination: tokenAccountAddress,
amount,
ethAddress,
sdk,
mint
})
return { res, error: null }
}
}

async function transferTokens({
ethAddress,
destination,
amount,
sdk,
mint
mint,
prefixInstructions = []
}: {
ethAddress: string
destination: PublicKey
amount: AudioWei
sdk: AudiusSdkWithServices
mint: MintName | PublicKey
prefixInstructions?: import('@solana/web3.js').TransactionInstruction[]
}) {
console.info(
`Transferring ${amount.toString()} tokens with mint ${mint} to ${destination.toBase58()}`
Expand All @@ -1041,7 +1053,8 @@ export const audiusBackend = ({
amount,
ethWallet: ethAddress,
mint,
destination
destination,
instructionIndex: prefixInstructions.length
})
const transferInstruction =
await sdk.services.claimableTokensClient.createTransferInstruction({
Expand Down Expand Up @@ -1072,7 +1085,11 @@ export const audiusBackend = ({
}

const transaction = await sdk.services.solanaClient.buildTransaction({
instructions: [secpTransactionInstruction, transferInstruction],
instructions: [
...prefixInstructions,
secpTransactionInstruction,
transferInstruction
],
recentBlockhash
})
const signature =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,51 @@ export class ClaimableTokensClient {
}
}

/**
* Returns a create-user-bank instruction if the user bank does not yet exist,
* or null if it already does. Does NOT send a transaction — the caller is
* expected to include the instruction in its own transaction.
*/
async createUserBankIfNeededInstruction(
params: GetOrCreateUserBankRequest
): Promise<{
userBank: PublicKey
instruction: ReturnType<
typeof ClaimableTokensProgram.createAccountInstruction
> | null
}> {
const args = await parseParams(
'createUserBankIfNeededInstruction',
GetOrCreateUserBankSchema
)(params)
const {
ethWallet = await this.getDefaultWalletAddress(),
feePayer: feePayerOverride
} = args
const mint = parseMint(args.mint, this.preconfiguredMints)
const feePayer = feePayerOverride ?? (await this.client.getFeePayer())
const userBank = await this.deriveUserBank(args)

const userBankAccount =
await this.client.connection.getAccountInfo(userBank)
if (!userBankAccount) {
this.logger.debug(
`User bank ${userBank} does not exist. Returning create instruction.`
)
const instruction = ClaimableTokensProgram.createAccountInstruction({
ethAddress: ethWallet,
payer: feePayer,
mint,
authority: this.deriveAuthority(mint),
userBank,
programId: this.programId
})
return { userBank, instruction }
}
this.logger.debug(`User bank ${userBank} already exists.`)
return { userBank, instruction: null }
}

/**
* Creates a claimable tokens program transfer instruction using configured
* program ID, mint addresses, derived nonce, and derived authorities.
Expand Down
75 changes: 57 additions & 18 deletions packages/web/src/pages/oauth-pay-page/OAuthPayPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export const OAuthPayPage = () => {

const {
recipient,
handleUser,
handleLoading,
handleError,
amount,
mint,
tokenInfo,
Expand All @@ -51,6 +54,8 @@ export const OAuthPayPage = () => {
}
})

const recipientDisplayName = handleUser ? `@${handleUser.handle}` : undefined

const formatAmount = (amount: bigint | null) => {
if (!amount || !tokenInfo) return '0'
return new FixedDecimal(amount, tokenInfo.decimals).toLocaleString(
Expand All @@ -74,10 +79,12 @@ export const OAuthPayPage = () => {
!userHoldsMint ||
isSubmitting ||
!!queryParamsError ||
!!error
!!error ||
handleLoading ||
!!handleError

// Determine error message to show
const displayError = queryParamsError || error
const displayError = queryParamsError || handleError || error

// Auto-close success screen after 1 second
useEffect(() => {
Expand All @@ -100,7 +107,7 @@ export const OAuthPayPage = () => {
)
}

if (balanceLoading || !tokenInfo) {
if (balanceLoading || !tokenInfo || handleLoading) {
return (
<ContentWrapper display={display ?? 'popup'}>
<Flex p='4xl' alignItems='center' justifyContent='center'>
Expand Down Expand Up @@ -143,14 +150,30 @@ export const OAuthPayPage = () => {
<Text variant='heading' size='s' color='subdued'>
{messages.recipient}
</Text>
<Text
variant='body'
size='m'
color='default'
css={{ wordBreak: 'break-all' }}
>
{recipient}
</Text>
{recipientDisplayName ? (
<Flex direction='column' gap='xs'>
<Text variant='body' size='m' color='default'>
{recipientDisplayName}
</Text>
<Text
variant='body'
size='s'
color='subdued'
css={{ wordBreak: 'break-all' }}
>
{recipient}
</Text>
</Flex>
) : (
<Text
variant='body'
size='m'
color='default'
css={{ wordBreak: 'break-all' }}
>
{recipient}
</Text>
)}
<PlainButton
variant='subdued'
css={{ alignSelf: 'flex-start' }}
Expand Down Expand Up @@ -236,13 +259,29 @@ export const OAuthPayPage = () => {
<Text variant='heading' size='s' color='subdued'>
{messages.recipient}
</Text>
<Text
variant='body'
size='l'
css={{ wordBreak: 'break-all' }}
>
{recipient}
</Text>
{recipientDisplayName ? (
<Flex direction='column' gap='xs'>
<Text variant='body' size='l'>
{recipientDisplayName}
</Text>
<Text
variant='body'
size='s'
color='subdued'
css={{ wordBreak: 'break-all' }}
>
{recipient}
</Text>
</Flex>
) : (
<Text
variant='body'
size='l'
css={{ wordBreak: 'break-all' }}
>
{recipient}
</Text>
)}
</Flex>

<Flex direction='column' gap='xs'>
Expand Down
Loading
Loading