Skip to content

Commit b6adfdc

Browse files
committed
fix: support solana token-2022 transfers in send flow (#4934)
1 parent 7fab812 commit b6adfdc

7 files changed

Lines changed: 243 additions & 18 deletions

File tree

.changeset/upset-fans-attack.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@reown/appkit-adapter-solana': patch
3+
'@reown/appkit-controllers': patch
4+
'pay-test-exchange': patch
5+
'@reown/appkit-adapter-bitcoin': patch
6+
'@reown/appkit-adapter-ethers': patch
7+
'@reown/appkit-adapter-ethers5': patch
8+
'@reown/appkit-adapter-wagmi': patch
9+
'@reown/appkit': patch
10+
'@reown/appkit-utils': patch
11+
'@reown/appkit-cdn': patch
12+
'@reown/appkit-cli': patch
13+
'@reown/appkit-codemod': patch
14+
'@reown/appkit-common': patch
15+
'@reown/appkit-core': patch
16+
'@reown/appkit-experimental': patch
17+
'@reown/appkit-pay': patch
18+
'@reown/appkit-polyfills': patch
19+
'@reown/appkit-scaffold-ui': patch
20+
'@reown/appkit-siwe': patch
21+
'@reown/appkit-siwx': patch
22+
'@reown/appkit-testing': patch
23+
'@reown/appkit-ui': patch
24+
'@reown/appkit-universal-connector': patch
25+
'@reown/appkit-wallet': patch
26+
'@reown/appkit-wallet-button': patch
27+
---
28+
29+
Fixed an issue where Solana token-2022 token transfers failed because the send flow used legacy transfer instructions

packages/adapters/solana/src/client.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { BaseWalletAdapter } from '@solana/wallet-adapter-base'
22
import type { Commitment, ConnectionConfig } from '@solana/web3.js'
3-
import { PublicKey, Connection as SolanaConnection } from '@solana/web3.js'
3+
import { PublicKey, SendTransactionError, Connection as SolanaConnection } from '@solana/web3.js'
44
import UniversalProvider from '@walletconnect/universal-provider'
55
import bs58 from 'bs58'
66

@@ -49,6 +49,13 @@ const IGNORED_CONNECTIONS_IDS: string[] = [
4949
CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT
5050
]
5151

52+
const TRANSACTION_ERROR_MAP = [
53+
{
54+
pattern: /Attempt to debit an account but found no record of a prior credit/iu,
55+
message: 'Not enough SOL to cover fees or rent'
56+
}
57+
]
58+
5259
export class SolanaAdapter extends AdapterBlueprint<SolanaProvider> {
5360
private connectionSettings: Commitment | ConnectionConfig
5461
public wallets?: BaseWalletAdapter[]
@@ -216,7 +223,19 @@ export class SolanaAdapter extends AdapterBlueprint<SolanaProvider> {
216223
value: Number.isNaN(Number(params.value)) ? 0 : Number(params.value)
217224
})
218225

219-
const result = await provider.sendTransaction(transaction, connection)
226+
const result = await provider.sendTransaction(transaction, connection).catch(error => {
227+
if (error instanceof SendTransactionError) {
228+
const errMessage = error?.transactionError?.message ?? error?.message ?? ''
229+
230+
for (const { pattern, message } of TRANSACTION_ERROR_MAP) {
231+
if (pattern.test(errMessage)) {
232+
throw new Error(message)
233+
}
234+
}
235+
}
236+
237+
throw error
238+
})
220239

221240
await new Promise<void>(resolve => {
222241
const interval = setInterval(async () => {
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { PublicKey } from '@solana/web3.js'
2+
import { beforeEach, describe, expect, it } from 'vitest'
3+
4+
import type { Provider } from '@reown/appkit-utils/solana'
5+
6+
import { createSPLTokenTransaction } from '../utils/createSPLTokenTransaction'
7+
import { mockConnection } from './mocks/Connection'
8+
import { TestConstants } from './util/TestConstants'
9+
10+
const mockProvider = () => {
11+
return {
12+
publicKey: new PublicKey(TestConstants.accounts[0].address)
13+
} as unknown as Provider
14+
}
15+
16+
const mockTokenMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' // USDC mint
17+
18+
let provider = mockProvider()
19+
let connection = mockConnection()
20+
21+
describe('createSPLTokenTransaction', () => {
22+
beforeEach(() => {
23+
provider = mockProvider()
24+
connection = mockConnection()
25+
})
26+
27+
it('should throw error when provider has no public key', async () => {
28+
const providerWithoutKey = { publicKey: null } as unknown as Provider
29+
30+
await expect(
31+
createSPLTokenTransaction({
32+
provider: providerWithoutKey,
33+
connection,
34+
to: TestConstants.accounts[1].address,
35+
amount: 10,
36+
tokenMint: mockTokenMint
37+
})
38+
).rejects.toThrow('No public key found')
39+
})
40+
41+
it('should throw error when amount is zero or negative', async () => {
42+
await expect(
43+
createSPLTokenTransaction({
44+
provider,
45+
connection,
46+
to: TestConstants.accounts[1].address,
47+
amount: 0,
48+
tokenMint: mockTokenMint
49+
})
50+
).rejects.toThrow('Amount must be greater than 0')
51+
52+
await expect(
53+
createSPLTokenTransaction({
54+
provider,
55+
connection,
56+
to: TestConstants.accounts[1].address,
57+
amount: -5,
58+
tokenMint: mockTokenMint
59+
})
60+
).rejects.toThrow('Amount must be greater than 0')
61+
})
62+
63+
it('should throw error for invalid recipient address format', async () => {
64+
await expect(
65+
createSPLTokenTransaction({
66+
provider,
67+
connection,
68+
to: 'invalid-address',
69+
amount: 10,
70+
tokenMint: mockTokenMint
71+
})
72+
).rejects.toThrow('Failed to create SPL token transaction')
73+
})
74+
75+
it('should throw error for invalid token mint format', async () => {
76+
await expect(
77+
createSPLTokenTransaction({
78+
provider,
79+
connection,
80+
to: TestConstants.accounts[1].address,
81+
amount: 10,
82+
tokenMint: 'invalid-mint'
83+
})
84+
).rejects.toThrow('Failed to create SPL token transaction')
85+
})
86+
87+
it('should throw error for empty recipient address', async () => {
88+
await expect(
89+
createSPLTokenTransaction({
90+
provider,
91+
connection,
92+
to: '',
93+
amount: 10,
94+
tokenMint: mockTokenMint
95+
})
96+
).rejects.toThrow('Invalid public key input')
97+
})
98+
99+
it('should throw error for null recipient address', async () => {
100+
await expect(
101+
createSPLTokenTransaction({
102+
provider,
103+
connection,
104+
to: null as any,
105+
amount: 10,
106+
tokenMint: mockTokenMint
107+
})
108+
).rejects.toThrow('Failed to create SPL token transaction')
109+
})
110+
111+
it('should throw error for empty token mint', async () => {
112+
await expect(
113+
createSPLTokenTransaction({
114+
provider,
115+
connection,
116+
to: TestConstants.accounts[1].address,
117+
amount: 10,
118+
tokenMint: ''
119+
})
120+
).rejects.toThrow('Invalid public key input')
121+
})
122+
123+
it('should throw error for null token mint', async () => {
124+
await expect(
125+
createSPLTokenTransaction({
126+
provider,
127+
connection,
128+
to: TestConstants.accounts[1].address,
129+
amount: 10,
130+
tokenMint: null as any
131+
})
132+
).rejects.toThrow('Failed to create SPL token transaction')
133+
})
134+
})

packages/adapters/solana/src/utils/createSPLTokenTransaction.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import {
2+
TOKEN_2022_PROGRAM_ID,
23
TOKEN_PROGRAM_ID,
34
TokenAccountNotFoundError,
45
createAssociatedTokenAccountInstruction,
5-
createTransferInstruction,
6+
createTransferCheckedInstruction,
67
getAccount,
78
getAssociatedTokenAddressSync,
89
getMint
910
} from '@solana/spl-token'
1011
import {
1112
ComputeBudgetProgram,
13+
Connection,
1214
PublicKey,
1315
Transaction,
1416
type TransactionInstruction
@@ -17,6 +19,24 @@ import {
1719
import { SPL_COMPUTE_BUDGET_CONSTANTS } from '@reown/appkit-utils/solana'
1820
import type { SPLTokenTransactionArgs } from '@reown/appkit-utils/solana'
1921

22+
async function getMintOwnerProgramId(connection: Connection, mint: PublicKey) {
23+
const info = await connection.getAccountInfo(mint)
24+
25+
if (!info) {
26+
throw new Error('Mint account not found')
27+
}
28+
29+
if (info.owner.equals(TOKEN_PROGRAM_ID)) {
30+
return TOKEN_PROGRAM_ID
31+
}
32+
33+
if (info.owner.equals(TOKEN_2022_PROGRAM_ID)) {
34+
return TOKEN_2022_PROGRAM_ID
35+
}
36+
37+
throw new Error('Unknown mint owner program')
38+
}
39+
2040
export async function createSPLTokenTransaction({
2141
provider,
2242
to,
@@ -27,30 +47,29 @@ export async function createSPLTokenTransaction({
2747
if (!provider.publicKey) {
2848
throw new Error('No public key found')
2949
}
30-
3150
if (amount <= 0) {
3251
throw new Error('Amount must be greater than 0')
3352
}
34-
3553
try {
3654
const fromPubkey = provider.publicKey
3755
const toPubkey = new PublicKey(to)
3856
const mintPubkey = new PublicKey(tokenMint)
3957

40-
const mintInfo = await getMint(connection, mintPubkey)
41-
const decimals = mintInfo.decimals
58+
const programId = await getMintOwnerProgramId(connection, mintPubkey)
4259

60+
const mintInfo = await getMint(connection, mintPubkey, undefined, programId)
61+
const decimals = mintInfo.decimals
4362
if (decimals < 0) {
4463
throw new Error('Invalid token decimals')
4564
}
4665

4766
const tokenAmount = Math.floor(amount * 10 ** decimals)
4867

49-
const fromTokenAccount = getAssociatedTokenAddressSync(mintPubkey, fromPubkey)
50-
const toTokenAccount = getAssociatedTokenAddressSync(mintPubkey, toPubkey)
68+
const fromTokenAccount = getAssociatedTokenAddressSync(mintPubkey, fromPubkey, false, programId)
69+
const toTokenAccount = getAssociatedTokenAddressSync(mintPubkey, toPubkey, false, programId)
5170

5271
try {
53-
const fromAccount = await getAccount(connection, fromTokenAccount)
72+
const fromAccount = await getAccount(connection, fromTokenAccount, undefined, programId)
5473
if (fromAccount.amount < BigInt(tokenAmount)) {
5574
throw new Error('Insufficient token balance')
5675
}
@@ -63,7 +82,7 @@ export async function createSPLTokenTransaction({
6382

6483
let shouldCreateATA = false
6584
try {
66-
await getAccount(connection, toTokenAccount)
85+
await getAccount(connection, toTokenAccount, undefined, programId)
6786
} catch (error) {
6887
if (error instanceof TokenAccountNotFoundError) {
6988
shouldCreateATA = true
@@ -87,18 +106,26 @@ export async function createSPLTokenTransaction({
87106

88107
if (shouldCreateATA) {
89108
instructions.push(
90-
createAssociatedTokenAccountInstruction(fromPubkey, toTokenAccount, toPubkey, mintPubkey)
109+
createAssociatedTokenAccountInstruction(
110+
fromPubkey,
111+
toTokenAccount,
112+
toPubkey,
113+
mintPubkey,
114+
programId
115+
)
91116
)
92117
}
93118

94119
instructions.push(
95-
createTransferInstruction(
120+
createTransferCheckedInstruction(
96121
fromTokenAccount,
122+
mintPubkey,
97123
toTokenAccount,
98124
fromPubkey,
99125
tokenAmount,
126+
decimals,
100127
[],
101-
TOKEN_PROGRAM_ID
128+
programId
102129
)
103130
)
104131

packages/controllers/src/controllers/SendController.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,10 +326,13 @@ const controller = {
326326

327327
if (
328328
SendController.state.token &&
329-
CoreHelperUtil.isCaipAddress(SendController.state.token.address)
329+
SendController.state.token.address !== ConstantsUtil.SOLANA_NATIVE_TOKEN_ADDRESS
330330
) {
331-
const [, , tokenMintSPLAddress] = SendController.state.token.address.split(':')
332-
tokenMint = tokenMintSPLAddress
331+
if (CoreHelperUtil.isCaipAddress(SendController.state.token.address)) {
332+
tokenMint = CoreHelperUtil.getPlainAddress(SendController.state.token.address)
333+
} else {
334+
tokenMint = SendController.state.token.address
335+
}
333336
}
334337

335338
await ConnectionController.sendTransaction({

packages/controllers/src/utils/ConstantsUtil.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export const ConstantsUtil = {
4444

4545
SECURE_SITE_FAVICON: `${SECURE_SITE}/images/favicon.png`,
4646

47+
SOLANA_NATIVE_TOKEN_ADDRESS: 'So11111111111111111111111111111111111111111',
48+
4749
RESTRICTED_TIMEZONES: [
4850
'ASIA/SHANGHAI',
4951
'ASIA/URUMQI',

packages/scaffold-ui/src/views/w3m-wallet-send-preview-view/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { LitElement, html } from 'lit'
22
import { state } from 'lit/decorators.js'
33

4+
import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common'
45
import {
56
ChainController,
67
EventsController,
@@ -162,7 +163,17 @@ export class W3mWalletSendPreviewView extends LitElement {
162163
SnackController.showSuccess('Transaction started')
163164
RouterController.replace('Account')
164165
} catch (error) {
165-
SnackController.showError('Failed to send transaction. Please try again.')
166+
let errMessage = 'Failed to send transaction. Please try again.'
167+
168+
// eslint-disable-next-line no-warning-comments
169+
// TODO: Remove this once we have a better way to handle errors for each adapter
170+
if (ChainController.state.activeChain === CommonConstantsUtil.CHAIN.SOLANA) {
171+
if (error instanceof Error) {
172+
errMessage = error.message
173+
}
174+
}
175+
176+
SnackController.showError(errMessage)
166177
// eslint-disable-next-line no-console
167178
console.error('SendController:sendToken - failed to send transaction', error)
168179

0 commit comments

Comments
 (0)