|
2 | 2 | title: Wallet Guide
|
3 | 3 | ---
|
4 | 4 |
|
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