Add SPL Token wallet integration guide
This commit is contained in:
parent
8ae0f927db
commit
414544042e
|
@ -117,8 +117,7 @@ $ spl-token balance 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||||
|
|
||||||
Mint 100 tokens into the account:
|
Mint 100 tokens into the account:
|
||||||
```sh
|
```sh
|
||||||
$ spl-token mint AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 100 \
|
$ spl-token mint AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 100
|
||||||
7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
|
||||||
Minting 100 tokens
|
Minting 100 tokens
|
||||||
Token: AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
Token: AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
||||||
Recipient: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
Recipient: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||||
|
@ -162,7 +161,44 @@ Unwrapping GJTxcnA5Sydy8YRhqvHxbQ5QNsPyRKvzguodQEaShJje
|
||||||
Signature: f7opZ86ZHKGvkJBQsJ8Pk81v8F3v1VUfyd4kFs4CABmfTnSZK5BffETznUU3tEWvzibgKJASCf7TUpDmwGi8Rmh
|
Signature: f7opZ86ZHKGvkJBQsJ8Pk81v8F3v1VUfyd4kFs4CABmfTnSZK5BffETznUU3tEWvzibgKJASCf7TUpDmwGi8Rmh
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example: Transferring tokens
|
### Example: Transferring tokens to another user, with sender-funding
|
||||||
|
First the receiver uses `spl-token create-account` to create their associated
|
||||||
|
token account for the Token type. Then the receiver obtains their wallet
|
||||||
|
address by running `solana address` and provides it to the sender.
|
||||||
|
|
||||||
|
The sender then runs:
|
||||||
|
```
|
||||||
|
$ spl-token transfer 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||||
|
Transfer 50 tokens
|
||||||
|
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||||
|
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||||
|
Recipient associated token account: F59618aQB8r6asXeMcB9jWuY6NEx1VduT9yFo1GTi1ks
|
||||||
|
|
||||||
|
Signature: 5a3qbvoJQnTAxGPHCugibZTbSu7xuTgkxvF4EJupRjRXGgZZrnWFmKzfEzcqKF2ogCaF4QKVbAtuFx7xGwrDUcGd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Transferring tokens to another user, with sender-funding
|
||||||
|
If the receiver does not yet have an associated token account, the sender may
|
||||||
|
choose to fund the receiver's account.
|
||||||
|
|
||||||
|
The receiver obtains their wallet address by running `solana address` and provides it to the sender.
|
||||||
|
|
||||||
|
The sender then runs to fund the receiver's associated token account, at the
|
||||||
|
sender's expense, and then transfers 50 tokens into it:
|
||||||
|
```
|
||||||
|
$ spl-token transfer --fund-recipient 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||||
|
Transfer 50 tokens
|
||||||
|
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||||
|
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||||
|
Recipient associated token account: F59618aQB8r6asXeMcB9jWuY6NEx1VduT9yFo1GTi1ks
|
||||||
|
Funding recipient: F59618aQB8r6asXeMcB9jWuY6NEx1VduT9yFo1GTi1ks (0.00203928 SOL)
|
||||||
|
|
||||||
|
Signature: 5a3qbvoJQnTAxGPHCugibZTbSu7xuTgkxvF4EJupRjRXGgZZrnWFmKzfEzcqKF2ogCaF4QKVbAtuFx7xGwrDUcGd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Transferring tokens to an explicit recipient token account
|
||||||
|
Tokens may be transferred to a specific recipient token account. The recipient
|
||||||
|
token account must already exist and be of the same Token type.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ spl-token create-account AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
$ spl-token create-account AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
||||||
|
@ -176,12 +212,12 @@ Account Token
|
||||||
7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 100
|
7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 100
|
||||||
CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 0
|
CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 0
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
$ spl-token transfer 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ
|
$ spl-token transfer 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ
|
||||||
Transfer 50 tokens
|
Transfer 50 tokens
|
||||||
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||||
Recipient: CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ
|
Recipient: CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ
|
||||||
|
|
||||||
Signature: 5a3qbvoJQnTAxGPHCugibZTbSu7xuTgkxvF4EJupRjRXGgZZrnWFmKzfEzcqKF2ogCaF4QKVbAtuFx7xGwrDUcGd
|
Signature: 5a3qbvoJQnTAxGPHCugibZTbSu7xuTgkxvF4EJupRjRXGgZZrnWFmKzfEzcqKF2ogCaF4QKVbAtuFx7xGwrDUcGd
|
||||||
```
|
```
|
||||||
```
|
```
|
||||||
|
@ -192,6 +228,9 @@ Account Token
|
||||||
CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 50
|
CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 50
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Example: Transferring tokens with sender funding
|
||||||
|
If the recipient a
|
||||||
|
|
||||||
### Example: Create a non-fungible token
|
### Example: Create a non-fungible token
|
||||||
|
|
||||||
Create the token type,
|
Create the token type,
|
||||||
|
@ -476,3 +515,151 @@ have a balance of zero to be closed.
|
||||||
|
|
||||||
### Non-Fungible tokens
|
### Non-Fungible tokens
|
||||||
An NTF is simply a token type where only a single token has been minted.
|
An NTF is simply a token type where only a single token has been minted.
|
||||||
|
|
||||||
|
## Wallet Integration Guide
|
||||||
|
This section describes how to integrate SPL Token support into an existing
|
||||||
|
wallet supporting native SOL. It assumes a model whereby the user has a single
|
||||||
|
system account as their **main wallet address** that they send and receive SOL
|
||||||
|
from.
|
||||||
|
|
||||||
|
Although all SPL Token accounts do have their own address on-chain, there's no
|
||||||
|
need to surface these additional addresses to the user.
|
||||||
|
|
||||||
|
There are two programs that are used by the wallet:
|
||||||
|
* SPL Token program - generic program that is used by all SPL Tokens
|
||||||
|
* SPL Associated Token Account program - this program defines the convention and the
|
||||||
|
provides the mechanism for mapping the user's wallet address to the associated
|
||||||
|
token accounts they hold.
|
||||||
|
|
||||||
|
### Associated Token Account
|
||||||
|
The associated token account convention allows all tokens to have the same
|
||||||
|
destination address, allowing the user share their main wallet address to
|
||||||
|
receive any SPL token.
|
||||||
|
|
||||||
|
The following Rust function can be used to derive the address of a user's
|
||||||
|
associated token account for a given SPL Token mint:
|
||||||
|
```rust
|
||||||
|
/// Finds the associated token address for a given wallet
|
||||||
|
/// address and SPL Token mint
|
||||||
|
pub fn get_associated_token_address(
|
||||||
|
wallet_address: &Pubkey,
|
||||||
|
spl_token_mint_address: &Pubkey,
|
||||||
|
) -> Pubkey {
|
||||||
|
Pubkey::find_program_address(
|
||||||
|
&[
|
||||||
|
&primary_account_address.to_bytes(),
|
||||||
|
&spl_token::id().to_bytes(),
|
||||||
|
&spl_token_mint_address.to_bytes(),
|
||||||
|
],
|
||||||
|
&spl_associated_token_account::id()
|
||||||
|
).0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Javascript equivalent:
|
||||||
|
```
|
||||||
|
import {PublicKey, PublicKeyNonce} from '@solana/web3.js';
|
||||||
|
|
||||||
|
async function findAssociatedTokenAddress(
|
||||||
|
walletAddress: Pubkey,
|
||||||
|
tokenMintAddress: Pubkey
|
||||||
|
): Promise<PublicKey> {
|
||||||
|
return PublicKey.findProgramAddress(
|
||||||
|
[
|
||||||
|
walletAddress.toBuffer(),
|
||||||
|
TOKEN_PROGRAM_ID.toBuffer(),
|
||||||
|
tokenMintAddress.toBuffer(),
|
||||||
|
],
|
||||||
|
SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID
|
||||||
|
)[0];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to fetch and display token holdings
|
||||||
|
The [getTokenAccountsByOwner](https://docs.solana.com/apps/jsonrpc-api#gettokenaccountsbyowner)
|
||||||
|
JSON RPC method can be used to fetch all token accounts for a wallet address.
|
||||||
|
|
||||||
|
For each token mint, the wallet could have be multiple token accounts: the
|
||||||
|
associated token account and/or other ancillary token accounts
|
||||||
|
|
||||||
|
By convention it is suggested that wallets roll up the balances from all token
|
||||||
|
accounts of the same token mint into a single balance for the user to shield the
|
||||||
|
user from this complexity. See the
|
||||||
|
[Garbage Collecting Ancillary Token Accounts](#garbage-collecting-ancillary-token-accounts)
|
||||||
|
section for suggestions on how the wallet should clean up ancillary token accounts on the user's
|
||||||
|
|
||||||
|
### Associated Token Account
|
||||||
|
Before the user can receive tokens their associated token account must be created
|
||||||
|
on-chain, requiring a small amount of SOL to mark the account as rent-exempt.
|
||||||
|
|
||||||
|
There's no restriction on who can create a user's associated token account. It
|
||||||
|
could either be created by the wallet on behalf of the user or funded by a 3rd
|
||||||
|
party through an airdrop campaign.
|
||||||
|
|
||||||
|
Ultimately the
|
||||||
|
`spl_associated_token_account::create_associated_token_account()` instruction
|
||||||
|
just needs to be executed by some party.
|
||||||
|
|
||||||
|
#### Sample "Add Token" workflow
|
||||||
|
When the user wants to receive tokens, they should fund their associated token
|
||||||
|
account.
|
||||||
|
|
||||||
|
To do so, the wallet should provide a UI that allow the users to "add a token".
|
||||||
|
The user selects the kind of token, and is presented with information about how
|
||||||
|
much SOL it will cost to add the token. Upon confirmation, the wallet sends a
|
||||||
|
transaction with the
|
||||||
|
`spl_associated_token_account::create_associated_token_account()` instruction.
|
||||||
|
|
||||||
|
#### Sample "Airdrop campaign" workflow
|
||||||
|
For each recipient wallet addresses, send a transaction containing:
|
||||||
|
1. `spl_associated_token_account::create_associated_token_account()` to create
|
||||||
|
the recipient's associated token account if necessary
|
||||||
|
2. `TokenInstruction::Transfer` to complete the airdrop
|
||||||
|
|
||||||
|
#### Associated Token Account Ownership
|
||||||
|
The wallet should never use `TokenInstruction::SetAuthority` to set the
|
||||||
|
`AccountOwner` authority of the associated token account to another address.
|
||||||
|
|
||||||
|
### Ancillary Token Accounts
|
||||||
|
At any time ownership of an existing SPL Token account may be assigned to the
|
||||||
|
user. One way to accomplish this is with the
|
||||||
|
`spl-token authorize <TOKEN_ADDRESS> owner <USER_ADDRESS>` command.
|
||||||
|
|
||||||
|
### Transferring Tokens Between Wallets
|
||||||
|
The preferred method of transferring tokens between wallets is to transfer into
|
||||||
|
associated token account of the recipient.
|
||||||
|
|
||||||
|
The recipient must provide their main wallet address to the sender. The sender
|
||||||
|
then:
|
||||||
|
1. Derives the associated token account for the recipient using `spl_associated_token_account::get_associated_token_address`
|
||||||
|
1. Fetches the recipient's associated token account over RPC and checks that it exists.
|
||||||
|
1. If the recipient's associated token accountdoes not exist, the sender wallet may choose to first fund the recipient's wallet at their expense
|
||||||
|
1. Use `TokenInstruction::Transfer` to complete the transfer.
|
||||||
|
|
||||||
|
### Registry for token details
|
||||||
|
At the moment Token Mint addresses need to be hard coded by each wallet. **Improving this situation is a work in progress.**
|
||||||
|
|
||||||
|
### Garbage Collecting Ancillary Token Accounts
|
||||||
|
Wallets should empty ancillary token accounts as quickly as practical by
|
||||||
|
transferring into the user's associated token account. This effort serves two
|
||||||
|
purposes:
|
||||||
|
* If the user is the close authority for the ancillary account, the wallet can
|
||||||
|
reclaim SOL for the user by closing the account.
|
||||||
|
* If the ancillary account was funded by a 3rd party, once the account is
|
||||||
|
emptied that 3rd party may close the account and reclaim the SOL.
|
||||||
|
|
||||||
|
One natural time to garbage collect ancillary token accounts is when the user
|
||||||
|
next sends tokens. The additional instructions to do so can be added to the
|
||||||
|
existing transaction, and will not require an additional fee.
|
||||||
|
|
||||||
|
Cleanup Pseudo Steps:
|
||||||
|
1. For all non-empty ancillary token accounts, add a
|
||||||
|
`TokenInstruction::Transfer` instruction to the transfer the full token
|
||||||
|
amount to the user's associated token account.
|
||||||
|
2. For all empty ancillary token accounts where the user is the close authority,
|
||||||
|
add a `TokenInstruction::CloseAccount` instruction
|
||||||
|
|
||||||
|
If adding one or more of clean up instructions cause the transaction to exceed
|
||||||
|
the maximum allowed transaction size, remove those extra clean up instructions.
|
||||||
|
They can be cleaned up during the next send operation.
|
||||||
|
|
Loading…
Reference in New Issue