Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 027f82c

Browse files
docs: Add basic token-2022 wallet guide (#4325)
Co-authored-by: Dries Croons <[email protected]>
1 parent 069c4b0 commit 027f82c

File tree

1 file changed

+236
-1
lines changed

1 file changed

+236
-1
lines changed

docs/src/token-2022/wallet.md

Lines changed: 236 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,239 @@
22
title: Wallet Guide
33
---
44

5-
Coming soon!
5+
This guide is meant for wallet developers who want to support Token-2022.
6+
7+
Since wallets have very different internals for managing token account state
8+
and connections to blockchains, this guide will focus on the very specific changes
9+
required, without only vague mentions of code design.
10+
11+
## Motivation
12+
13+
Wallet developers are accustomed to only including one token program used for
14+
all tokens.
15+
16+
To properly support Token-2022, wallet developers must make code changes.
17+
18+
Important note: if you do not wish to support Token-2022, you do not need to do
19+
anything. The wallet will not load Token-2022 accounts, and transactions created
20+
by the wallet will fail loudly if using Token-2022 incorrectly.
21+
22+
Most likely, transactions will fail with `ProgramError::IncorrectProgramId`
23+
when trying to target the Token program with Token-2022 accounts.
24+
25+
## Prerequisites
26+
27+
When testing locally, be sure to use at least `solana-test-validator` version
28+
1.14.17, which includes the Token-2022 program by default. This comes bundled
29+
with version 2.3.0 of the `spl-token` CLI, which also supports Token-2022.
30+
31+
## Setup
32+
33+
You'll need some Token-2022 tokens for testing. First, create a mint with an
34+
extension. We'll use the "Mint Close Authority" extension:
35+
36+
```console
37+
$ spl-token -ul create-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb --enable-close
38+
Creating token E5SUrbnx7bMBp3bRdMWNCFS3FXp5VpvFDdNFp8rjrMLM under program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
39+
40+
Address: E5SUrbnx7bMBp3bRdMWNCFS3FXp5VpvFDdNFp8rjrMLM
41+
Decimals: 9
42+
43+
Signature: 2dYhT1M3dHjbGd9GFCFPXmHMtjujXBGhM8b5wBkx3mtUptQa5U9jjRTWHCEmUQnv8XLt2x5BHdbDUkZpNJFqfJn1
44+
```
45+
46+
The extension is important because it will test that your wallet properly handles
47+
larger mint accounts.
48+
49+
Next, create an account for your test wallet:
50+
51+
```console
52+
$ spl-token -ul create-account E5SUrbnx7bMBp3bRdMWNCFS3FXp5VpvFDdNFp8rjrMLM --owner <TEST_WALLET_ADDRESS> --fee-payer <FEE_PAYER_KEYPAIR>
53+
Creating account 4L45ZpFS6dqTyLMofmQZ9yuTqYvQrfCJfWL2xAjd5WDW
54+
55+
Signature: 5Cjvvzid7w2tNZojrWVCmZ2MFiezxxnWgJHLJKkvJNByZU2sLN97y85CghxHwPaVf5d5pJAcDV9R4N1MNigAbBMN
56+
```
57+
58+
With the `--owner` parameter, the new account is an associated token account,
59+
which includes the "Immutable Owner" account extension. This way, you'll also
60+
test larger token accounts.
61+
62+
Finally, mint some tokens:
63+
64+
```console
65+
$ spl-token -ul mint E5SUrbnx7bMBp3bRdMWNCFS3FXp5VpvFDdNFp8rjrMLM 100000 4L45ZpFS6dqTyLMofmQZ9yuTqYvQrfCJfWL2xAjd5WDW
66+
Minting 100000 tokens
67+
Token: E5SUrbnx7bMBp3bRdMWNCFS3FXp5VpvFDdNFp8rjrMLM
68+
Recipient: 4L45ZpFS6dqTyLMofmQZ9yuTqYvQrfCJfWL2xAjd5WDW
69+
70+
Signature: 43rsisVeLKjBCgLruwTFJXtGTBgwyfpLjwm44dY2YLHH9WJaazEvkyYGdq6omqs4thRfCS4G8z4KqzEGRP2xoMo9
71+
```
72+
73+
It's also helpful for your test wallet to have some SOL, so be sure to transfer some:
74+
75+
```console
76+
$ solana -ul transfer <TEST_WALLET_ADDRESS> 10 --allow-unfunded-recipient
77+
Signature: 5A4MbdMTgGiV7hzLesKbzmrPSCvYPG15e1bg3d7dViqMaPbZrdJweKSuY1BQAfq245RMMYeGudxyKQYkgKoGT1Ui
78+
```
79+
80+
Finally, you can save all of these accounts in a directory to be re-used for testing:
81+
82+
```console
83+
$ mkdir test-accounts
84+
$ solana -ul account --output-file test-accounts/token-account.json --output json 4L45ZpFS6dqTyLMofmQZ9yuTqYvQrfCJfWL2xAjd5WDW
85+
... output truncated ...
86+
$ solana -ul account --output-file test-accounts/mint.json --output json E5SUrbnx7bMBp3bRdMWNCFS3FXp5VpvFDdNFp8rjrMLM
87+
... output truncated ...
88+
$ solana -ul account --output-file test-accounts/wallet.json --output json <TEST_WALLET_ADDRESS>
89+
```
90+
91+
This way, whenever you want to restart your test validator, you can simply run:
92+
93+
```console
94+
$ solana-test-validator -r --account-dir test-accounts
95+
```
96+
97+
## Structure of this Guide
98+
99+
We'll go through the required code changes to support Token-2022 in your wallet,
100+
using only little code snippets. This work was done for the Backpack wallet in
101+
[PR #3976](https://github.com/coral-xyz/backpack/pull/3976),
102+
but as mentioned earlier, the actual code changes may look very different for
103+
your wallet.
104+
105+
## Part I: Fetch Token-2022 Accounts
106+
107+
In addition to normal Token accounts, your wallet must also fetch Token-2022
108+
accounts. Typically, wallets use the `getTokenAccountsByOwner` RPC endpoint once
109+
to fetch the accounts.
110+
111+
For Token-2022, you simply need to add one more call to get the additional accounts:
112+
113+
```typescript
114+
import { Connection, PublicKey } from '@solana/web3.js';
115+
116+
const TOKEN_PROGRAM_ID = new PublicKey(
117+
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
118+
);
119+
const TOKEN_2022_PROGRAM_ID = new PublicKey(
120+
'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'
121+
);
122+
const walletPublicKey = new PublicKey('11111111111111111111111111111111'); // insert your key
123+
const connection = new Connection('http://127.0.0.1:8899', 'confirmed');
124+
125+
const tokenAccounts = await connection.getTokenAccountsByOwner(
126+
walletPublicKey, { programId: TOKEN_PROGRAM_ID }
127+
);
128+
const token2022Accounts = await connection.getTokenAccountsByOwner(
129+
walletPublicKey, { programId: TOKEN_2022_PROGRAM_ID }
130+
);
131+
```
132+
133+
Merge the two responses, and you're good to go! If you can see your test account,
134+
then you've done it correctly.
135+
136+
If there are issues, your wallet may be deserializing the token account too strictly,
137+
so be sure to relax any restriction that the data size must be equal to 165 bytes.
138+
139+
## Part II: Use the Token Program Id for Instructions
140+
141+
If you try to transfer or burn a Token-2022 token, you will likely receive an
142+
error because the wallet is trying to send an instruction to Token instead of
143+
Token-2022.
144+
145+
Here are two possible ways to resolve the problem.
146+
147+
### Option 1: Store the token account's owner during fetch
148+
149+
In the first part, we fetched all of the token accounts and threw away the
150+
program id associated with the account. Instead of always targeting the Token
151+
program, we need to target the right program for that token.
152+
153+
If we store the program id for each token account, then we can re-use that
154+
information when we need to transfer or burn.
155+
156+
```typescript
157+
import { Connection, PublicKey } from '@solana/web3.js';
158+
import { createTransferInstruction } from '@solana/spl-token';
159+
160+
const TOKEN_PROGRAM_ID = new PublicKey(
161+
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
162+
);
163+
const TOKEN_2022_PROGRAM_ID = new PublicKey(
164+
'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'
165+
);
166+
const walletPublicKey = new PublicKey('11111111111111111111111111111111'); // insert your key
167+
const connection = new Connection('http://127.0.0.1:8899', 'confirmed');
168+
169+
const tokenAccounts = await connection.getTokenAccountsByOwner(
170+
walletPublicKey, { programId: TOKEN_PROGRAM_ID }
171+
);
172+
const token2022Accounts = await connection.getTokenAccountsByOwner(
173+
walletPublicKey, { programId: TOKEN_2022_PROGRAM_ID }
174+
);
175+
const accountsWithProgramId = [...tokenAccounts.value, ...token2022Accounts.value].map(
176+
({ account, pubkey }) =>
177+
{
178+
account,
179+
pubkey,
180+
programId: account.data.program === 'spl-token' ? TOKEN_PROGRAM_ID : TOKEN_2022_PROGRAM_ID,
181+
},
182+
);
183+
184+
// later on...
185+
const accountWithProgramId = accountsWithProgramId[0];
186+
const instruction = createTransferInstruction(
187+
accountWithProgramId.pubkey, // source
188+
accountWithProgramId.pubkey, // destination
189+
walletPublicKey, // owner
190+
1, // amount
191+
[], // multisigners
192+
accountWithProgramId.programId, // token program id
193+
);
194+
195+
```
196+
197+
### Option 2: Fetch the program owner before transfer / burn
198+
199+
This approach introduces one more network call, but may be simpler to integrate.
200+
Before creating an instruction, you can fetch the mint, source account, or
201+
destination account from the network, and pull out its `owner` field.
202+
203+
```typescript
204+
import { Connection, PublicKey } from '@solana/web3.js';
205+
206+
const connection = new Connection('http://127.0.0.1:8899', 'confirmed');
207+
const accountPublicKey = new PublicKey('11111111111111111111111111111111'); // insert your account key here
208+
const accountInfo = await connection.getParsedAccountInfo(accountPublicKey);
209+
if (accountInfo.value === null) {
210+
throw new Error('Account not found');
211+
}
212+
const programId = accountInfo.value.owner;
213+
```
214+
215+
## Part III: Use the Token Program Id for Associated Token Accounts
216+
217+
Whenever we derive an associated token account, we must use the correct token
218+
program id. Currently, most implementations hardcode the token program id.
219+
Instead, you must add the program id as a parameter:
220+
221+
```typescript
222+
import { PublicKey } from '@solana/web3.js';
223+
224+
const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey(
225+
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
226+
);
227+
228+
function associatedTokenAccountAddress(
229+
mint: PublicKey,
230+
wallet: PublicKey,
231+
programId: PublicKey,
232+
): PublicKey {
233+
return PublicKey.findProgramAddressSync(
234+
[wallet.toBuffer(), programId.toBuffer(), mint.toBuffer()],
235+
ASSOCIATED_TOKEN_PROGRAM_ID
236+
)[0];
237+
}
238+
```
239+
240+
With these three parts done, your wallet will provide basic support for Token-2022!

0 commit comments

Comments
 (0)