Solana 上的代币
代币是代表对各种资产所有权的数字资产。 代币化使得财产权的数字化成为可能,是管理 可替代和不可替代资产的基本组成部分。
- 可替代代币代表同类型和同价值的可互换和可分割资产(例如 USDC)。
- 不可替代代币(NFT)代表不可分割资产的所有权(例如艺术品)。
本节将介绍代币在 Solana 上的基本表示方式。这些代币被称为 SPL (Solana Program Library) 代币。
-
代币程序(Token Program)包含与网络上的代币(包括可替代和不 可替代)交互的所有指令逻辑。
-
铸造账户(Mint Account)代表一种特定类型的代币,并存储关于代 币的全局元数据,如总供应量和铸造权限(有权创建新代币单位的地址)。
-
代币账户(Token Account)跟踪特定地址拥有的特定类型代币(铸 造账户)的单位数量。
目前有两个版本的代币程序。 原始 的代币程序和代币扩展程序 (Token2022)。 代币扩展程序与原始代币程序功能相同,但具有额外的功能和改进。 代 币扩展程序是创建新代币(铸造账户)的推荐版本。
关键点 #
-
代币代表对可替代(可互换)或不可替代(唯一)资产的所有权。
-
代币程序包含与网络上可替代和不可替代代币交互的所有指令。
-
代币扩展程序是代币程序的新版本,包含额外功能,同时保持相同的核心功能。
-
铸造账户代表网络上的唯一代币,并存储全局元数据,如总供应量。
-
代币账户跟踪特定铸造账户的代币的个人所有权。
-
关联代币账户( Associated Token Account)是使用所有者和铸造账户地址派生的地址 创建的代币账户。
代币程序 #
代币程序 包含与网络上的代币(包括可替代和不可替代)交互的所有指令逻辑。Solana 上的所有代 币实际上都是由代币程序拥有的数据账户。 Solana 上的所有代币实际上都是由代币程序拥有 的数据账户。
你可以 在这里找 到代币程序指令的完整列表。
代币程序
一些常用的指令包括:
InitializeMint
: 创建一个新的铸造账户以代表一种新的代币类型。InitializeAccount
: 创建一个新的代币账户以持有特定类型代币(铸造)。MintTo
: 创建特定类型代币的新单位并将其添加到代币账户中。这会增加代币的供应量,并且只能 由铸造账户的铸造权限执行。Transfer
: 将特定类型代币的单位从一个代币账户转移到另一个代币账户。
铸造账户 #
Solana 上的代币通过由代币程序拥有 的铸造账户的 地址唯一标识。 该账户实际上是特定代币的全局计数器,并存储以下数据:
- 供应量:代币的总供应量
- 小数位:代币的小数精度
- 铸造权限:有权创建新代币单位的账户,从而增加供应量
- 冻结权限:有权冻结代币账户中代币转移的账户
铸造账户
每个铸造账户存储的完整详细信息包括以下内容:
pub struct Mint {
/// Optional authority used to mint new tokens. The mint authority may only
/// be provided during mint creation. If no mint authority is present
/// then the mint has a fixed supply and no further tokens may be
/// minted.
pub mint_authority: COption<Pubkey>,
/// Total supply of tokens.
pub supply: u64,
/// Number of base 10 digits to the right of the decimal place.
pub decimals: u8,
/// Is `true` if this structure has been initialized
pub is_initialized: bool,
/// Optional authority to freeze token accounts.
pub freeze_authority: COption<Pubkey>,
}
作为参考,这里是 Solana Explorer 上 USDC 铸造账户的 链接。
代币账户 #
为了跟踪每个特定代币单位的个人所有权,必须创建另一种由代币程序拥有的数据账户。该 账户被称 为代币账户。
代币账户中最常引用的数据包括以下内容:
- 铸造:代币账户持有的代币类型
- 所有者:有权从代币账户转移代币的账户
- 数量:代币账户当前持有的代币单位
代币账户
每个代币账户存储的完整详细信息包括以下内容:
pub struct Account {
/// The mint associated with this account
pub mint: Pubkey,
/// The owner of this account.
pub owner: Pubkey,
/// The amount of tokens this account holds.
pub amount: u64,
/// If `delegate` is `Some` then `delegated_amount` represents
/// the amount authorized by the delegate
pub delegate: COption<Pubkey>,
/// The account's state
pub state: AccountState,
/// If is_native.is_some, this is a native token, and the value logs the
/// rent-exempt reserve. An Account is required to be rent-exempt, so
/// the value is used by the Processor to ensure that wrapped SOL
/// accounts do not drop below this threshold.
pub is_native: COption<u64>,
/// The amount delegated
pub delegated_amount: u64,
/// Optional authority to close the account.
pub close_authority: COption<Pubkey>,
}
请注意,每个代币账户的数据包括一个owner
字段,用于标识谁对该特定代币账户拥有权
限。这与 AccountInfo 中指定的程序所有者是
分开的,所有代币账户的程序所有者都是代币程序。 一个钱包可以为同一种代币创建多个
代币账户,但每个代币账户只能由一个钱包拥有,并且只能持有一种类型的代币。
账户关系
请注意,每个代币账户的数据包括一个owner
字段,用于标识谁对该特定代币账户拥有
权限。 这与 AccountInfo 中指定的程序所有
者是分开的,所有代币账户的程序所有者都是代币程序。
关联代币账户 #
为了简化查找特定铸造和所有者的代币账户地址的过程,我们通常使用关联代币账户。
关联代币账户是使用所有者地址和铸造账户地址确定性派生的代币账户。 你可以将关联代 币账户视为特定铸造和所有者的“默认”代币账户。
重要的是要理解,关联代币账户并不是一种不同类型的代币账户。 它只是一个具有特定地 址的代币账户。
关联代币账户 这介绍了 Solana 开发中的一个关键概念: 程序派生地址 (PDA)。从概念上 讲,PDA 提供了一种使用一些预定义输入生成地址的确定性方法。这使我们能够在以后轻松 找到账户的地址。
这介绍了 Solana 开发中的一个关键概念: 程序派生地址 (PDA)。 从概念上讲,PDA 提供了一种使用一些预定义输入生成地址的确定性方法。 这使我们能够 在以后轻松找到账户的地址。
这里有一个 Solana Playground 示例,它派生了 USDC 关联代币账户地址和所有者。 对于相同的铸造和所有者,它将始终 生成相同 的地址 。
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
const associatedTokenAccountAddress = getAssociatedTokenAddressSync(
USDC_MINT_ADDRESS,
OWNER_ADDRESS,
);
具体来说,关联代币账户的地址是使用以下输入派生的。 这里有一个 Solana Playground 示例,它生成 了与前一个示例相同的地址。
import { PublicKey } from "@solana/web3.js";
const [PDA, bump] = PublicKey.findProgramAddressSync(
[
OWNER_ADDRESS.toBuffer(),
TOKEN_PROGRAM_ID.toBuffer(),
USDC_MINT_ADDRESS.toBuffer(),
],
ASSOCIATED_TOKEN_PROGRAM_ID,
);
为了让两个钱包持有相同类型的代币,每个钱包需要为特定的铸造账户拥有自己的代币账 户。 下图展示了这种账户关系的样子。
账户关系扩展
代币示例 #
The spl-token
CLI can be used to experiment with
SPL tokens. 在下面的示例中,我们将使用
Solana Playground 终端直接在浏览器中运行 CLI 命令,而
无需在本地安装 CLI。
创建代币和账户需要 SOL 用于账户租金存款和交易费用。 如果这是你第一次使用 Solana
Playground,创建一个 Playground 钱包并在 Playground 终端中运行solana airdrop
命
令。 你也可以使用公共网络水龙头获取 devnet 上的
SOL。
solana airdrop 2
运行spl-token --help
以获取可用命令的完整描述。
spl-token --help
或者,你可以使用以下命令在本地安装 spl-token CLI。 这需要首 先安装 Rust。
在以下部分中,当你运行 CLI 命令时显示的账户地址将与下面示例输出不同。 请在跟随
操作时使用 Playground 终端中显示的地址。 例如,create-token
的地址输出是你的
Playground 钱包被设置为铸造权限的铸造账户。
创建新代币 #
要创建一个新代币( 铸造账户 ),在 Solana Playground 终端中运行 以下命令。
spl-token create-token
你应该会看到类似于以下的输出。 你可以使用Address
和Signature
在
Solana Explorer 上检查代币和交易
详情。
在下面的示例输出中,新代币的唯一标识符(地址)
是99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg
。
Creating token 99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg
Address: 99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg
Decimals: 9
Signature: 44fvKfT1ezBUwdzrCys3fvCdFxbLMnNvBstds76QZyE6cXag5NupBprSXwxPTzzjrC3cA6nvUZaLFTvmcKyzxrm1
新代币最初没有供应量。 你可以使用以下命令检查代币的当前供应量:
spl-token supply <TOKEN_ADDRESS>
运行supply
命令以获取新创建代币的供应量将返回值0
:
spl-token supply 99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg
在底层,创建一个新的铸造账户需要发送一个包含两个指令的交易。 这里有一个在 Solana Playground 上的 Javascript 示例。
-
调用系统程序创建一个具有足够空间的新账户以存储铸造账户数据,然后将所有权转移 给代币程序。
-
调用代币程序初始化新账户的数据作为铸造账户。
创建代币账户 #
要持有特定代币的单位,你必须首先创建一个代币账户 。 要创建一个 新的代币账户,请使用以下命令:
spl-token create-account [OPTIONS] <TOKEN_ADDRESS>
例如,在 Solana Playground 终端中运行以下命令:
spl-token create-account 99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg
返回以下输出:
AfB7uwBEsGtrrBqPTVqEgzWed5XdYfM1psPNLmf7EeX9
是创建的代币账户的地址,用于持 有create-account
命令中指定的代币单位。
Creating account AfB7uwBEsGtrrBqPTVqEgzWed5XdYfM1psPNLmf7EeX9
Signature: 2BtrynuCLX9CNofFiaw6Yzbx6hit66pup9Sk7aFjwU2NEbFz7NCHD9w9sWhrCfEd73XveAGK1DxFpJoQZPXU9tS1
默认情况下,create-account
命令使用你的钱包地址作为代币账户所有者创建一
个关联代币账户 。
你可以使用以下命令创建一个具有不同所有者的代币账户:
spl-token create-account --owner <OWNER_ADDRESS> <TOKEN_ADDRESS>
例如,运行以下命令:
spl-token create-account --owner 2i3KvjDCZWxBsqcxBHpdEaZYQwQSYE6LXUMx5VjY5XrR 99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg
返回以下输出:
Hmyk3FSw4cfsuAes7sanp2oxSkE9ivaH6pMzDzbacqmt
是创建的代币账户的地址,用于持 有create-account
命令中指定的代币单位 (99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg
),并由--owner
标志后指定的 地址(2i3KvjDCZWxBsqcxBHpdEaZYQwQSYE6LXUMx5VjY5XrR
)拥有。 当你需要为其他用 户创建代币账户时,这很有用。
Creating account Hmyk3FSw4cfsuAes7sanp2oxSkE9ivaH6pMzDzbacqmt
Signature: 44vqKdfzspT592REDPY4goaRJH3uJ3Ce13G4BCuUHg35dVUbHuGTHvqn4ZjYF9BGe9QrjMfe9GmuLkQhSZCBQuEt
在底层,创建一个关联代币账户需要一个调 用关联代币程序的 指令。这里有一个在 Solana Playground 上的 Javascript 示例。
关联代币程序使用跨程序调用来处理:
或者,使用随机生成的密钥对(不是关联代币账户)创建一个新的代币账户需要发送一个包 含两个指令的交易。 这里有一个在 Solana Playground 上的 Javascript 示例。
-
调用系统程序创建一个具有足够空间的新账户以存储代币账户数据,然后将所有权转移 给代币程序。
-
调用代币程序初始化新账户的数据作为代币账户。
铸造代币 #
要创建新的代币单位,请使用以下命令:
spl-token mint [OPTIONS] <TOKEN_ADDRESS> <TOKEN_AMOUNT> [--] [RECIPIENT_TOKEN_ACCOUNT_ADDRESS]
例如,运行以下命令:
spl-token mint 99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg 100
返回以下输出:
-
99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg
是代币铸造账户的地址(增加总供 应量) -
AfB7uwBEsGtrrBqPTVqEgzWed5XdYfM1psPNLmf7EeX9
是你的钱包的代币账户的地址,代币 单位正在铸造到该账户(增加数量)。
Minting 100 tokens
Token: 99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg
Recipient: AfB7uwBEsGtrrBqPTVqEgzWed5XdYfM1psPNLmf7EeX9
Signature: 2NJ1m7qCraPSBAVxbr2ssmWZmBU9Jc8pDtJAnyZsZJRcaYCYMqq1oRY1gqA4ddQno3g3xcnny5fzr1dvsnFKMEqG
要将代币铸造到不同的代币账户,请指定预期接收代币账户的地址。 例如,运行以下命 令:
spl-token mint 99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg 100 -- Hmyk3FSw4cfsuAes7sanp2oxSkE9ivaH6pMzDzbacqmt
返回以下输出:
-
99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg
是铸造账户的地址,代币正在为其铸 造(增加总供应量)。 -
Hmyk3FSw4cfsuAes7sanp2oxSkE9ivaH6pMzDzbacqmt
是代币账户的地址,代币被铸造到 该账户(增加数量)
Minting 100 tokens
Token: 99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg
Recipient: Hmyk3FSw4cfsuAes7sanp2oxSkE9ivaH6pMzDzbacqmt
Signature: 3SQvNM3o9DsTiLwcEkSPT1Edr14RgE2wC54TEjonEP2swyVCp2jPWYWdD6RwXUGpvDNUkKWzVBZVFShn5yntxVd7
在底层,创建新的代币单位需要调用代币程序上的 MintTo
指令。 该指令必须由铸造权
限签名。 该指令将新的代币单位铸造到代币账户,并增加铸造账户上的总供应量。 这里有
一个在 Solana Playground 上的
Javascript 示例。
转移代币 #
要在两个代币账户之间转移代币单位,请使用以下命令:
spl-token transfer [OPTIONS] <TOKEN_ADDRESS> <TOKEN_AMOUNT> <RECIPIENT_ADDRESS
or RECIPIENT_TOKEN_ACCOUNT_ADDRESS>
要将代币铸造到不同的代币账户,请指定预期接收代币账户的地址。例如,运行以下命令:
spl-token transfer 99zqUzQGohamfYxyo8ykTEbi91iom3CLmwCA75FK5zTg 100 Hmyk3FSw4cfsuAes7sanp2oxSkE9ivaH6pMzDzbacqmt
返回以下输出:
-
AfB7uwBEsGtrrBqPTVqEgzWed5XdYfM1psPNLmf7EeX9
是代币账户的地址,代币从该账户 转移。 这将是你指定代币的代币账户地址 -
Hmyk3FSw4cfsuAes7sanp2oxSkE9ivaH6pMzDzbacqmt
是代币账户的地址,代币被转移到 该账户
Transfer 100 tokens
Sender: AfB7uwBEsGtrrBqPTVqEgzWed5XdYfM1psPNLmf7EeX9
Recipient: Hmyk3FSw4cfsuAes7sanp2oxSkE9ivaH6pMzDzbacqmt
Signature: 5y6HVwV8V2hHGLTVmTmdySRiEUCZnWmkasAvJ7J6m7JR46obbGKCBqUFgLpZu5zQGwM4Xy6GZ4M5LKd1h6Padx3o
在底层,转移代币需要调用代币程序上的 Transfer
指令。 该指令必须由发送者代币账
户的所有者签名。 该指令将代币单位从一个代币账户转移到另一个代币账户。 这里有一个
在 Solana Playground 上的
Javascript 示例。
重要的是要理解,发送者和接收者必须有现有的特定类型代币的代币账户。 发送者可以在 交易中包含额外的指令来创建接收者的代币账户,通常是关联代币账户。
创建代币元数据 #
代币扩展程序允许将额外的可自定义元数据(如名称、符号、图像链接)直接存储在铸造账 户上。
要使用代币扩展 CLI 标志,请确保你本地安装了 CLI,版本为 3.4.0 或更高:
cargo install --version 3.4.0 spl-token-cli
要创建启用了元数据扩展的新代币,请使用以下命令:
spl-token create-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
--enable-metadata
该命令返回以下输出:
BdhzpzhTD1MFqBiwNdrRy4jFo2FHFufw3n9e8sVjJczP
是启用了元数据扩展的新代币的地 址
Creating token BdhzpzhTD1MFqBiwNdrRy4jFo2FHFufw3n9e8sVjJczP under program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
To initialize metadata inside the mint, please run `spl-token initialize-metadata BdhzpzhTD1MFqBiwNdrRy4jFo2FHFufw3n9e8sVjJczP <YOUR_TOKEN_NAME> <YOUR_TOKEN_SYMBOL> <YOUR_TOKEN_URI>`, and sign with the mint authority.
Address: BdhzpzhTD1MFqBiwNdrRy4jFo2FHFufw3n9e8sVjJczP
Decimals: 9
Signature: 5iQofFeXdYhMi9uTzZghcq8stAaa6CY6saUwcdnELST13eNSifiuLbvR5DnRt311frkCTUh5oecj8YEvZSB3wfai
一旦创建了启用了元数据扩展的新代币,请使用以下命令初始化元数据。
spl-token initialize-metadata <TOKEN_MINT_ADDRESS> <YOUR_TOKEN_NAME>
<YOUR_TOKEN_SYMBOL> <YOUR_TOKEN_URI>
The token URI is normally a link to offchain metadata you want to associate with the token. 你可以 在这里找 到 JSON 格式的示例。
例如,运行以下命令将直接在指定的铸造账户上存储额外的元数据:
spl-token initialize-metadata BdhzpzhTD1MFqBiwNdrRy4jFo2FHFufw3n9e8sVjJczP "TokenName" "TokenSymbol" "https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/DeveloperPortal/metadata.json"
然后,你可以在浏览器上查找铸造账户的地址以检查元数据。 例如,这里是一个启用了元 数据扩展的代币在 SolanaFm 浏览器上的示例。