BCSkill (Block chain skill )
区块链中文技术社区

只讨论区块链底层技术
遵守一切相关法律政策!

使用 spl-token 命令行工具测试Solana代币发行

安装 CLI

安装Solana CLI

sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"

添加环境变量

export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"
sudo vi /etc/profile
export PATH=$PATH:/usr/local/go/bin
source /etc/profile

设置集群

solana config set --url https://api.devnet.solana.com

领取测试代币

solana airdrop 5

创建代币

spl-token create-token

返回

Creating token 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT under program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

Address:  34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT
Decimals:  9

Signature: adBFA3wdYahsRPZGWuHhFMSZ83D7Znr9M5D7SKtif31ArQWRS5HdzXa1ifEbtNZHNRmnPSce91R96fK9BdE6PwP

https://explorer.solana.com/tx/adBFA3wdYahsRPZGWuHhFMSZ83D7Znr9M5D7SKtif31ArQWRS5HdzXa1ifEbtNZHNRmnPSce91R96fK9BdE6PwP?cluster=devnet

  • 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT 是部署时临时创建的一个Keypair,这个作为当前创建Token的唯一标识符
  • 8TKG1ez28ZYzNgGTein6Pc97yvxWwnHecVL9ZhdZTxd5 是当前部署的地址,

js代码

import { createMint } from '@solana/spl-token';
import { clusterApiUrl, Connection, Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js';

const payer = Keypair.generate();
const mintAuthority = Keypair.generate();
const freezeAuthority = Keypair.generate();

const connection = new Connection(
  clusterApiUrl('devnet'),
  'confirmed'
);

const mint = await createMint(
  connection,
  payer,
  mintAuthority.publicKey,
  freezeAuthority.publicKey,
  9 // We are using 9 to match the CLI decimal default exactly
);

console.log(mint.toBase58());
// 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT

刚部署完,没有供应量,因为还没有mint

spl-token supply 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT

js代码

const mintInfo = await getMint(
  connection,
  mint
)

console.log(mintInfo.supply);
// 0

创建代币持有ATA账户

spl-token create-account 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT

返回

Creating account 3tykH6gHjjyqYHBigukDXZaj3K6XupjKxYLMA2Srfbc2

Signature: 3jK2bbob5JqpAiXxUFTqDfKJZMZ5kpgUASVUNaWi8dp3N9bsZPEXba2rB4E9KUyUUYCmAFuYQwB7uRXGQihQfvG7

https://solscan.io/tx/3jK2bbob5JqpAiXxUFTqDfKJZMZ5kpgUASVUNaWi8dp3N9bsZPEXba2rB4E9KUyUUYCmAFuYQwB7uRXGQihQfvG7?cluster=devnet

对应js代码

const tokenAccount = await getOrCreateAssociatedTokenAccount(
  connection,
  payer,
  mint,
  payer.publicKey
)

console.log(tokenAccount.address.toBase58());
// 3tykH6gHjjyqYHBigukDXZaj3K6XupjKxYLMA2Srfbc2

查询当前账户的余额,还没mint前,余额是0

spl-token balance 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT

查询的账户地址是 mint地址

铸造100个

spl-token mint 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT 100

返回

Minting 100 tokens
  Token: 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT
  Recipient: 3tykH6gHjjyqYHBigukDXZaj3K6XupjKxYLMA2Srfbc2

Signature: 37LD7EVARr5vtA5kHLMmjXk176gxXhTakNdRnFZ4u7GKWA62oAnhtix2xBAYoyNhWECPhdvyqf1XyZBupGqoqvzF

命令行自动计算的ATA地址,逻辑简单来说
交易fee提供地址(8TKG1ez28ZYzNgGTein6Pc97yvxWwnHecVL9ZhdZTxd5)-> 调用 SPL Token程序(TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA)-> 根据Token唯一标志(mint 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT)找到对应的Token->mint 100个代币-> 发送给交易Fee地址(8TKG1ez28ZYzNgGTein6Pc97yvxWwnHecVL9ZhdZTxd5)的ATA账户地址(3tykH6gHjjyqYHBigukDXZaj3K6XupjKxYLMA2Srfbc2)

对应的js代码

await mintTo(
  connection,
  payer,
  mint,
  tokenAccount.address,
  mintAuthority,
  100000000000 // because decimals for the mint are set to 9 
)

查询余额

spl-token balance 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT

返回 100
命令行会自动根据当前交易地址,计算对应的ATA地址,并查询余额

js代码

const tokenAccountInfo = await getAccount(
  connection,
  tokenAccount.address
)

console.log(tokenAccountInfo.amount);
// 0

查询已发行量

spl-token supply 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT

返回 100

查看拥有的所有代币

spl-token accounts
Token                                         Balance
-----------------------------------------------------
34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT  100

js 代码

import {AccountLayout, TOKEN_PROGRAM_ID} from "@solana/spl-token";
import {clusterApiUrl, Connection, PublicKey} from "@solana/web3.js";

(async () => {

  const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');

  const tokenAccounts = await connection.getTokenAccountsByOwner(
    new PublicKey('8YLKoCu7NwqHNS8GzuvA2ibsvLrsg22YMfMDafxh1B15'),
    {
      programId: TOKEN_PROGRAM_ID,
    }
  );

  console.log("Token                                         Balance");
  console.log("------------------------------------------------------------");
  tokenAccounts.value.forEach((tokenAccount) => {
    const accountData = AccountLayout.decode(tokenAccount.account.data);
    console.log(`${new PublicKey(accountData.mint)}   ${accountData.amount}`);
  })

})();

将 SOL 包装在代币中(SOL->WSOL)

spl-token wrap 1
Wrapping 1 SOL into F3XA5sEfzS5AJbZ8733YygzcGV3UKizrGS2FteHjwemB

Signature: 3BNYUpykfpcDjFJnJMkzXPKG5Rqego3ypLx9yRfRFxKXhR25WQtGxLhyT65VtPv72bpsShBDM2b7Swuy1LwwzvkL

js代码

import {NATIVE_MINT, createAssociatedTokenAccountInstruction, getAssociatedTokenAddress, createSyncNativeInstruction, getAccount} from "@solana/spl-token";
import {clusterApiUrl, Connection, Keypair, LAMPORTS_PER_SOL, SystemProgram, Transaction, sendAndConfirmTransaction} from "@solana/web3.js";

(async () => {

const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');

const wallet = Keypair.generate();

const airdropSignature = await connection.requestAirdrop(
  wallet.publicKey,
  2 * LAMPORTS_PER_SOL,
);

await connection.confirmTransaction(airdropSignature);

const associatedTokenAccount = await getAssociatedTokenAddress(
  NATIVE_MINT,
  wallet.publicKey
)

// Create token account to hold your wrapped SOL
const ataTransaction = new Transaction()
  .add(
    createAssociatedTokenAccountInstruction(
      wallet.publicKey,
      associatedTokenAccount,
      wallet.publicKey,
      NATIVE_MINT
    )
  );

await sendAndConfirmTransaction(connection, ataTransaction, [wallet]);

// Transfer SOL to associated token account and use SyncNative to update wrapped SOL balance
const solTransferTransaction = new Transaction()
  .add(
    SystemProgram.transfer({
        fromPubkey: wallet.publicKey,
        toPubkey: associatedTokenAccount,
        lamports: LAMPORTS_PER_SOL
      }),
      createSyncNativeInstruction(
        associatedTokenAccount
    )
  )

await sendAndConfirmTransaction(connection, solTransferTransaction, [wallet]);

const accountInfo = await getAccount(connection, associatedTokenAccount);

console.log(`Native: ${accountInfo.isNative}, Lamports: ${accountInfo.amount}`);

})();

逻辑上简单说就是给对应的NATIVE_MINT所属ATA地址转SOL

WSOL ->SOL 赎回

spl-token unwrap F3XA5sEfzS5AJbZ8733YygzcGV3UKizrGS2FteHjwemB
Unwrapping F3XA5sEfzS5AJbZ8733YygzcGV3UKizrGS2FteHjwemB
  Amount: 1 SOL
  Recipient: 8TKG1ez28ZYzNgGTein6Pc97yvxWwnHecVL9ZhdZTxd5


Signature: 3dsKnJHHFhBqUxeVf2JLX5WnGmBFqhy2hEC5jeGBhBHFFKQMWmVtVMCcoyjSdBTF8Y61eArYYizjZVT9yp3TXKhZ

代币转移

逻辑简单来说
发送From账户地址的ATA账户->给接收To地址对应的ATA地址进行转账

spl-token transfer 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT 50 C1XZNEpofMrDmV17SNzyqaygGo82yVPvGYNQFufMEZCd

如果接收地址没有该代币的ATA,需要添加 --allow-unfunded-recipient,已表明为该ATA提供创建租金

spl-token transfer 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT 50 C1XZNEpofMrDmV17SNzyqaygGo82yVPvGYNQFufMEZCd --allow-unfunded-recipient --fund-recipient

js代码

import { clusterApiUrl, Connection, Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { createMint, getOrCreateAssociatedTokenAccount, mintTo, transfer } from '@solana/spl-token';

(async () => {
    // Connect to cluster
    const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');

    // Generate a new wallet keypair and airdrop SOL
    const fromWallet = Keypair.generate();
    const fromAirdropSignature = await connection.requestAirdrop(fromWallet.publicKey, LAMPORTS_PER_SOL);

    // Wait for airdrop confirmation
    await connection.confirmTransaction(fromAirdropSignature);

    // Generate a new wallet to receive newly minted token
    const toWallet = Keypair.generate();

    // Create new token mint
    const mint = await createMint(connection, fromWallet, fromWallet.publicKey, null, 9);

    // Get the token account of the fromWallet address, and if it does not exist, create it
    const fromTokenAccount = await getOrCreateAssociatedTokenAccount(
        connection,
        fromWallet,
        mint,
        fromWallet.publicKey
    );

    // Get the token account of the toWallet address, and if it does not exist, create it
    const toTokenAccount = await getOrCreateAssociatedTokenAccount(connection, fromWallet, mint, toWallet.publicKey);

    // Mint 1 new token to the "fromTokenAccount" account we just created
    let signature = await mintTo(
        connection,
        fromWallet,
        mint,
        fromTokenAccount.address,
        fromWallet.publicKey,
        1000000000
    );
    console.log('mint tx:', signature);

    // Transfer the new token to the "toTokenAccount" we just created
    signature = await transfer(
        connection,
        fromWallet,
        fromTokenAccount.address,
        toTokenAccount.address,
        fromWallet.publicKey,
        50
    );
})();

查询余额

查询本地账户下

spl-token accounts 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT -v
Program                                       Account                                       Delegated  Close Authority  Balance
-------------------------------------------------------------------------------------------------------------------------------
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA   3tykH6gHjjyqYHBigukDXZaj3K6XupjKxYLMA2Srfbc2                              100

js 代码

import {getAccount, createMint, createAccount, mintTo, getOrCreateAssociatedTokenAccount, transfer} from "@solana/spl-token";
import {clusterApiUrl, Connection, Keypair, LAMPORTS_PER_SOL} from "@solana/web3.js";

(async () => {

  const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');

  const wallet = Keypair.generate();
  const auxiliaryKeypair = Keypair.generate();

  const airdropSignature = await connection.requestAirdrop(
    wallet.publicKey,
    LAMPORTS_PER_SOL,
  );

  await connection.confirmTransaction(airdropSignature);

  const mint = await createMint(
    connection,
    wallet,
    wallet.publicKey,
    wallet.publicKey,
    9
  );

  // Create custom token account
  const auxiliaryTokenAccount = await createAccount(
    connection,
    wallet,
    mint,
    wallet.publicKey,
    auxiliaryKeypair
  );

  const associatedTokenAccount = await getOrCreateAssociatedTokenAccount(
    connection,
    wallet,
    mint,
    wallet.publicKey
  );

  await mintTo(
    connection,
    wallet,
    mint,
    associatedTokenAccount.address,
    wallet,
    50
  );

  const accountInfo = await getAccount(connection, associatedTokenAccount.address);

  console.log(accountInfo.amount);
  // 50

  await transfer(
    connection,
    wallet,
    associatedTokenAccount.address,
    auxiliaryTokenAccount,
    wallet,
    50
  );

  const auxAccountInfo = await getAccount(connection, auxiliaryTokenAccount);

  console.log(auxAccountInfo.amount);
  // 50
})();

创建非同质化代币

创建小数点后为零的 token 类型,
流程与上面的一致

禁用mint

spl-token authorize 34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT mint --disable

js代码

let transaction = new Transaction()
  .add(createSetAuthorityInstruction(
    mint,
    wallet.publicKey,
    AuthorityType.MintTokens,
    null
  ));

await web3.sendAndConfirmTransaction(connection, transaction, [wallet]);

将mint权限更新为null

多重签名的使用

引用多重签名账户时命令行用法的主要区别spl-token在于指定--owner参数。通常,此参数指定的签名者直接提供授予其权限的签名,但在多重签名的情况下,它仅指向多重签名账户的地址。然后由参数指定的多重签名签名者集成员提供签名 --multisig-signer

任何拥有 SPL Token 铸币或代币账户的机构都可以使用多重签名账户。{

  • 铸币账户铸币权限: spl-token mint ...spl-token authorize ... mint ...
  • Mint账户 冻结权限: spl-token freeze ...,, spl-token thaw ...``spl-token authorize ... freeze ...
  • Token 账户 所有者 权限 : spl-token transfer ...,,,,,,, spl-token approve ...``spl-token revoke ...``spl-token burn ...``spl-token wrap ...``spl-token unwrap ...``spl-token authorize ... owner ...
  • Token 账户关闭权限: spl-token close ..., spl-token authorize ... close ...

}

使用多重签名的主要区别在于将所有者指定为多重签名密钥,并在构建交易时提供签名者列表。通常,您会提供有权运行交易的签名者作为所有者,但在多重签名的情况下,所有者将是多重签名密钥。

任何拥有 SPL Token 铸币或代币账户的机构都可以使用多重签名账户。

{

  • 铸币账户铸币权限: createMint(/* ... */, mintAuthority: multisigKey, /* ... */)
  • Mint账户冻结权限: createMint(/* ... */, freezeAuthority: multisigKey, /* ... */)
  • 代币账户所有者权限: getOrCreateAssociatedTokenAccount(/* ... */, mintAuthority: multisigKey, /* ... */)
  • Token账户关闭权限: closeAccount(/* ... */, authority: multisigKey, /* ... */)

}

示例:具有多重签名权限的 Mint

首先创建密钥对作为多重签名者集。实际上,这些可以是任何受支持的签名者,例如:Ledger 硬件钱包、密钥对文件或纸钱包。为方便起见,本示例将使用生成的密钥对。

$ for i in $(seq 3); do solana-keygen new --no-passphrase -so "signer-${i}.json"; done
Wrote new keypair to signer-1.json
Wrote new keypair to signer-2.json
Wrote new keypair to signer-3.json
const signer1 = Keypair.generate();
const signer2 = Keypair.generate();
const signer3 = Keypair.generate();

为了创建多重签名账户,必须收集签名者集的公钥。

$ for i in $(seq 3); do SIGNER="signer-${i}.json"; echo "$SIGNER: $(solana-keygen pubkey "$SIGNER")"; done
signer-1.json: BzWpkuRrwXHq4SSSFHa8FJf6DRQy4TaeoXnkA89vTgHZ
signer-2.json: DhkUfKgfZ8CF6PAGKwdABRL1VqkeNrTSRx8LZfpPFVNY
signer-3.json: D7ssXHrZJjfpZXsmDf8RwfPxe1BMMMmP1CtmX3WojPmG
console.log(signer1.publicKey.toBase58());
console.log(signer2.publicKey.toBase58());
console.log(signer3.publicKey.toBase58());
/*
  BzWpkuRrwXHq4SSSFHa8FJf6DRQy4TaeoXnkA89vTgHZ
  DhkUfKgfZ8CF6PAGKwdABRL1VqkeNrTSRx8LZfpPFVNY
  D7ssXHrZJjfpZXsmDf8RwfPxe1BMMMmP1CtmX3WojPmG
 */

现在可以使用子命令创建多重签名帐户。其第一个位置参数是必须签署影响此多重签名帐户控制的 token/mint 帐户的交易的spl-token create-multisig 最小签名者数量 ( )。其余位置参数是允许 ( ) 为多重签名帐户签名的所有密钥对的公钥。此示例将使用“2 of 3”多重签名帐户。也就是说,三个允许的密钥对中的两个必须签署所有交易。M``N

注意:SPL Token Multisig 帐户仅限于 11 个签名者(1 {'<='} N{'<='} 11),并且最低签名者数量不得超过N(1 {'<='} M{'<='} N

$ spl-token create-multisig 2 BzWpkuRrwXHq4SSSFHa8FJf6DRQy4TaeoXnkA89vTgHZ \
DhkUfKgfZ8CF6PAGKwdABRL1VqkeNrTSRx8LZfpPFVNY D7ssXHrZJjfpZXsmDf8RwfPxe1BMMMmP1CtmX3WojPmG
Creating 2/3 multisig 46ed77fd4WTN144q62BwjU2B3ogX3Xmmc8PT5Z3Xc2re
Signature: 2FN4KXnczAz33SAxwsuevqrD1BvikP6LUhLie5Lz4ETt594X8R7yvMZzZW2zjmFLPsLQNHsRuhQeumExHbnUGC9A
const multisigKey = await createMultisig(
  connection,
  payer,
  [
    signer1.publicKey,
    signer2.publicKey,
    signer3.publicKey
  ],
  2
);

console.log(`Created 2/3 multisig ${multisigKey.toBase58()}`);
// Created 2/3 multisig 46ed77fd4WTN144q62BwjU2B3ogX3Xmmc8PT5Z3Xc2re

接下来,按照前面描述的方式创建代币铸造账户和接收账户 ,并将铸造账户的铸造权限设置为多重签名账户

$ spl-token create-token
Creating token 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o
Signature: 3n6zmw3hS5Hyo5duuhnNvwjAbjzC42uzCA3TTsrgr9htUonzDUXdK1d8b8J77XoeSherqWQM8mD8E1TMYCpksS2r

$ spl-token create-account 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o
Creating account EX8zyi2ZQUuoYtXd4MKmyHYLTjqFdWeuoTHcsTdJcKHC
Signature: 5mVes7wjE7avuFqzrmSCWneKBQyPAjasCLYZPNSkmqmk2YFosYWAP9hYSiZ7b7NKpV866x5gwyKbbppX3d8PcE9s

$ spl-token authorize 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o mint 46ed77fd4WTN144q62BwjU2B3ogX3Xmmc8PT5Z3Xc2re
Updating 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o
  Current mint authority: 5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE
  New mint authority: 46ed77fd4WTN144q62BwjU2B3ogX3Xmmc8PT5Z3Xc2re
Signature: yy7dJiTx1t7jvLPCRX5RQWxNRNtFwvARSfbMJG94QKEiNS4uZcp3GhhjnMgZ1CaWMWe4jVEMy9zQBoUhzomMaxC
const mint = await createMint(
    connection,
    payer,
    multisigKey,
    multisigKey,
    9
  );

const associatedTokenAccount = await getOrCreateAssociatedTokenAccount(
  connection,
  payer,
  mint,
  signer1.publicKey
);

为了证明铸币账户现在处于多重签名账户的控制之下,尝试使用一个多重签名者进行铸币会失败

$ spl-token mint 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o 1 EX8zyi2ZQUuoYtXd4MKmyHYLTjqFdWeuoTHcsTdJcKHC \
--owner 46ed77fd4WTN144q62BwjU2B3ogX3Xmmc8PT5Z3Xc2re \
--multisig-signer signer-1.json
Minting 1 tokens
  Token: 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o
  Recipient: EX8zyi2ZQUuoYtXd4MKmyHYLTjqFdWeuoTHcsTdJcKHC
RPC response error -32002: Transaction simulation failed: Error processing Instruction 0: missing required signature for instruction
try {
  await mintTo(
    connection,
    payer,
    mint,
    associatedTokenAccount.address,
    multisigKey,
    1
  )
} catch (error) {
  console.log(error);
}
// Error: Signature verification failed

但使用第二个多重签名者重复操作,成功了

$ spl-token mint 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o 1 EX8zyi2ZQUuoYtXd4MKmyHYLTjqFdWeuoTHcsTdJcKHC \
--owner 46ed77fd4WTN144q62BwjU2B3ogX3Xmmc8PT5Z3Xc2re \
--multisig-signer signer-1.json \
--multisig-signer signer-2.json
Minting 1 tokens
  Token: 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o
  Recipient: EX8zyi2ZQUuoYtXd4MKmyHYLTjqFdWeuoTHcsTdJcKHC
Signature: 2ubqWqZb3ooDuc8FLaBkqZwzguhtMgQpgMAHhKsWcUzjy61qtJ7cZ1bfmYktKUfnbMYWTC1S8zdKgU6m4THsgspT
await mintTo(
  connection,
  payer,
  mint,
  associatedTokenAccount.address,
  multisigKey,
  1,
  [
    signer1,
    signer2
  ]
)

const mintInfo = await getMint(
  connection,
  mint
)

console.log(`Minted ${mintInfo.supply} token`);
// Minted 1 token

示例:使用多重签名进行离线签名

有时在线签名不可行或不受欢迎。例如,当签名者不在同一地理位置,或使用未连接到网络的隔离设备时。在这种情况下,我们使用离线签名,它将前面的多重签名示例与离线签名nonce 帐户结合起来。

此示例将使用与在线示例相同的 mint 帐户、token 帐户、多重签名帐户和多重签名者设置密钥对文件名,以及我们在此处创建的 nonce 帐户:

$ solana-keygen new -o nonce-keypair.json
...
======================================================================
pubkey: Fjyud2VXixk2vCs4DkBpfpsq48d81rbEzh6deKt7WvPj
======================================================================
$ solana create-nonce-account nonce-keypair.json 1
Signature: 3DALwrAAmCDxqeb4qXZ44WjpFcwVtgmJKhV4MW5qLJVtWeZ288j6Pzz1F4BmyPpnGLfx2P8MEJXmqPchX5y2Lf3r
$ solana nonce-account Fjyud2VXixk2vCs4DkBpfpsq48d81rbEzh6deKt7WvPj
Balance: 0.01 SOL
Minimum Balance Required: 0.00144768 SOL
Nonce blockhash: 6DPt2TfFBG7sR4Hqu16fbMXPj8ddHKkbU4Y3EEEWrC2E
Fee: 5000 lamports per signature
Authority: 5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE
const connection = new Connection(
  clusterApiUrl('devnet'),
  'confirmed',
);

const onlineAccount = Keypair.generate();
const nonceAccount = Keypair.generate();

const minimumAmount = await connection.getMinimumBalanceForRentExemption(
  NONCE_ACCOUNT_LENGTH,
);

// Form CreateNonceAccount transaction
const transaction = new Transaction()
  .add(
  SystemProgram.createNonceAccount({
    fromPubkey: onlineAccount.publicKey,
    noncePubkey: nonceAccount.publicKey,
    authorizedPubkey: onlineAccount.publicKey,
    lamports: minimumAmount,
  }),
);

await web3.sendAndConfirmTransaction(connection, transaction, [onlineAccount, nonceAccount])

const nonceAccountData = await connection.getNonce(
  nonceAccount.publicKey,
  'confirmed',
);

console.log(nonceAccountData);
/*
NonceAccount {
  authorizedPubkey: '5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE'
  nonce: '6DPt2TfFBG7sR4Hqu16fbMXPj8ddHKkbU4Y3EEEWrC2E',
  feeCalculator: { lamportsPerSignature: 5000 }
}
 */

对于费用支付者和随机数授权者角色, 5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE将使用本地热钱包。

首先,通过指定所有签名者的公钥来构建模板命令。运行此命令后,所有签名者将在输出中列为“缺席签名者”。每个离线签名者都将运行此命令以生成相应的签名。

注意:该参数的参数--blockhash是来自指定持久随机数账户的“Nonce blockhash:”字段。

$ spl-token mint 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o 1 EX8zyi2ZQUuoYtXd4MKmyHYLTjqFdWeuoTHcsTdJcKHC \
--owner 46ed77fd4WTN144q62BwjU2B3ogX3Xmmc8PT5Z3Xc2re \
--multisig-signer BzWpkuRrwXHq4SSSFHa8FJf6DRQy4TaeoXnkA89vTgHZ \
--multisig-signer DhkUfKgfZ8CF6PAGKwdABRL1VqkeNrTSRx8LZfpPFVNY \
--blockhash 6DPt2TfFBG7sR4Hqu16fbMXPj8ddHKkbU4Y3EEEWrC2E \
--fee-payer 5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE \
--nonce Fjyud2VXixk2vCs4DkBpfpsq48d81rbEzh6deKt7WvPj \
--nonce-authority 5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE \
--sign-only \
--mint-decimals 9
Minting 1 tokens
  Token: 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o
  Recipient: EX8zyi2ZQUuoYtXd4MKmyHYLTjqFdWeuoTHcsTdJcKHC

Blockhash: 6DPt2TfFBG7sR4Hqu16fbMXPj8ddHKkbU4Y3EEEWrC2E
Absent Signers (Pubkey):
 5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE
 BzWpkuRrwXHq4SSSFHa8FJf6DRQy4TaeoXnkA89vTgHZ
 DhkUfKgfZ8CF6PAGKwdABRL1VqkeNrTSRx8LZfpPFVNY

接下来,每个离线签名者执行模板命令,用相应的密钥对替换他们的公钥的每个实例。

$ spl-token mint 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o 1 EX8zyi2ZQUuoYtXd4MKmyHYLTjqFdWeuoTHcsTdJcKHC \
--owner 46ed77fd4WTN144q62BwjU2B3ogX3Xmmc8PT5Z3Xc2re \
--multisig-signer signer-1.json \
--multisig-signer DhkUfKgfZ8CF6PAGKwdABRL1VqkeNrTSRx8LZfpPFVNY \
--blockhash 6DPt2TfFBG7sR4Hqu16fbMXPj8ddHKkbU4Y3EEEWrC2E \
--fee-payer 5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE \
--nonce Fjyud2VXixk2vCs4DkBpfpsq48d81rbEzh6deKt7WvPj \
--nonce-authority 5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE \
--sign-only \
--mint-decimals 9
Minting 1 tokens
  Token: 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o
  Recipient: EX8zyi2ZQUuoYtXd4MKmyHYLTjqFdWeuoTHcsTdJcKHC

Blockhash: 6DPt2TfFBG7sR4Hqu16fbMXPj8ddHKkbU4Y3EEEWrC2E
Signers (Pubkey=Signature):
 BzWpkuRrwXHq4SSSFHa8FJf6DRQy4TaeoXnkA89vTgHZ=2QVah9XtvPAuhDB2QwE7gNaY962DhrGP6uy9zeN4sTWvY2xDUUzce6zkQeuT3xg44wsgtUw2H5Rf8pEArPSzJvHX
Absent Signers (Pubkey):
 5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE
 DhkUfKgfZ8CF6PAGKwdABRL1VqkeNrTSRx8LZfpPFVNY
$ spl-token mint 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o 1 EX8zyi2ZQUuoYtXd4MKmyHYLTjqFdWeuoTHcsTdJcKHC \
--owner 46ed77fd4WTN144q62BwjU2B3ogX3Xmmc8PT5Z3Xc2re \
--multisig-signer BzWpkuRrwXHq4SSSFHa8FJf6DRQy4TaeoXnkA89vTgHZ \
--multisig-signer signer-2.json \
--blockhash 6DPt2TfFBG7sR4Hqu16fbMXPj8ddHKkbU4Y3EEEWrC2E \
--fee-payer 5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE \
--nonce Fjyud2VXixk2vCs4DkBpfpsq48d81rbEzh6deKt7WvPj \
--nonce-authority 5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE \
--sign-only \
--mint-decimals 9
Minting 1 tokens
  Token: 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o
  Recipient: EX8zyi2ZQUuoYtXd4MKmyHYLTjqFdWeuoTHcsTdJcKHC

Blockhash: 6DPt2TfFBG7sR4Hqu16fbMXPj8ddHKkbU4Y3EEEWrC2E
Signers (Pubkey=Signature):
 DhkUfKgfZ8CF6PAGKwdABRL1VqkeNrTSRx8LZfpPFVNY=2brZbTiCfyVYSCp6vZE3p7qCDeFf3z1JFmJHPBrz8SnWSDZPjbpjsW2kxFHkktTNkhES3y6UULqS4eaWztLW7FrU
Absent Signers (Pubkey):
 5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE
 BzWpkuRrwXHq4SSSFHa8FJf6DRQy4TaeoXnkA89vTgHZ

最后,离线签名者将Pubkey=Signature其命令输出的配对传达给将交易广播到集群的一方。广播方随后在修改模板命令后运行该命令,如下所示:

  1. 用其密钥对替换任何相应的公钥(在此示例中--fee-payer ... 为)--nonce-authority ...
  2. 删除--sign-only参数,如果是mint子命令,则--mint-decimals ...删除将从集群中查询的参数
  3. --signer通过参数将离线签名添加到模板命令
$ spl-token mint 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o 1 EX8zyi2ZQUuoYtXd4MKmyHYLTjqFdWeuoTHcsTdJcKHC \
--owner 46ed77fd4WTN144q62BwjU2B3ogX3Xmmc8PT5Z3Xc2re \
--multisig-signer BzWpkuRrwXHq4SSSFHa8FJf6DRQy4TaeoXnkA89vTgHZ \
--multisig-signer DhkUfKgfZ8CF6PAGKwdABRL1VqkeNrTSRx8LZfpPFVNY \
--blockhash 6DPt2TfFBG7sR4Hqu16fbMXPj8ddHKkbU4Y3EEEWrC2E \
--fee-payer hot-wallet.json \
--nonce Fjyud2VXixk2vCs4DkBpfpsq48d81rbEzh6deKt7WvPj \
--nonce-authority hot-wallet.json \
--signer BzWpkuRrwXHq4SSSFHa8FJf6DRQy4TaeoXnkA89vTgHZ=2QVah9XtvPAuhDB2QwE7gNaY962DhrGP6uy9zeN4sTWvY2xDUUzce6zkQeuT3xg44wsgtUw2H5Rf8pEArPSzJvHX \
--signer DhkUfKgfZ8CF6PAGKwdABRL1VqkeNrTSRx8LZfpPFVNY=2brZbTiCfyVYSCp6vZE3p7qCDeFf3z1JFmJHPBrz8SnWSDZPjbpjsW2kxFHkktTNkhES3y6UULqS4eaWztLW7FrU
Minting 1 tokens
  Token: 4VNVRJetwapjwYU8jf4qPgaCeD76wyz8DuNj8yMCQ62o
  Recipient: EX8zyi2ZQUuoYtXd4MKmyHYLTjqFdWeuoTHcsTdJcKHC
Signature: 2AhZXVPDBVBxTQLJohyH1wAhkkSuxRiYKomSSXtwhPL9AdF3wmhrrJGD7WgvZjBPLZUFqWrockzPp9S3fvzbgicy

首先使用 nonceAccountInformation 和 tokenAccount 密钥构建原始交易。交易的所有签名者都被视为原始交易的一部分。此交易稍后将交给签名者进行签名。

const nonceAccountInfo = await connection.getAccountInfo(
  nonceAccount.publicKey,
  'confirmed'
);

const nonceAccountFromInfo = web3.NonceAccount.fromAccountData(nonceAccountInfo.data);

console.log(nonceAccountFromInfo);

const nonceInstruction = web3.SystemProgram.nonceAdvance({
  authorizedPubkey: onlineAccount.publicKey,
  noncePubkey: nonceAccount.publicKey
});

const nonce = nonceAccountFromInfo.nonce;

const mintToTransaction = new web3.Transaction({
  feePayer: onlineAccount.publicKey,
  nonceInfo: {nonce, nonceInstruction}
})
  .add(
    createMintToInstruction(
      mint,
      associatedTokenAccount.address,
      multisigkey,
      1,
      [
        signer1,
        onlineAccount
      ],
      TOKEN_PROGRAM_ID
    )
  );

接下来,每个离线签名者将获取交易缓冲区并使用其相应的密钥对其进行签名。

let mintToTransactionBuffer = mintToTransaction.serializeMessage();

let onlineSIgnature = nacl.sign.detached(mintToTransactionBuffer, onlineAccount.secretKey);
mintToTransaction.addSignature(onlineAccount.publicKey, onlineSIgnature);

// Handed to offline signer for signature
let offlineSignature = nacl.sign.detached(mintToTransactionBuffer, signer1.secretKey);
mintToTransaction.addSignature(signer1.publicKey, offlineSignature);

let rawMintToTransaction = mintToTransaction.serialize();

最后,热钱包将接收交易、对其进行序列化并将其广播到网络。

// Send to online signer for broadcast to network
await web3.sendAndConfirmRawTransaction(connection, rawMintToTransaction);

JSON RPC 方法

有一组丰富的 JSON RPC 方法可用于 SPL Token:

  • getTokenAccountBalance
  • getTokenAccountsByDelegate
  • getTokenAccountsByOwner
  • getTokenLargestAccounts
  • getTokenSupply

有关更多详细信息,请参阅https://docs.solana.com/apps/jsonrpc-api。

getProgramAccounts此外,可以以多种方式采用多功能JSON RPC 方法来获取感兴趣的 SPL 代币账户。

查找特定铸币厂的所有代币账户

查找TESTpKgj42ya3st2SQTKiANjTBmncQSCqLAZGcSPLGM铸币厂的所有代币账户:

curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getProgramAccounts",
    "params": [
      "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      {
        "encoding": "jsonParsed",
        "filters": [
          {
            "dataSize": 165
          },
          {
            "memcmp": {
              "offset": 0,
              "bytes": "TESTpKgj42ya3st2SQTKiANjTBmncQSCqLAZGcSPLGM"
            }
          }
        ]
      }
    ]
  }
'

过滤"dataSize": 165器选择所有代币账户,然后"memcmp": ...过滤器根据 每个代币账户内的铸币 地址进行选择。

查找钱包的所有代币账户

查找用户拥有的所有代币账户vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg

curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getProgramAccounts",
    "params": [
      "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      {
        "encoding": "jsonParsed",
        "filters": [
          {
            "dataSize": 165
          },
          {
            "memcmp": {
              "offset": 32,
              "bytes": "vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg"
            }
          }
        ]
      }
    ]
  }
'

过滤"dataSize": 165器选择所有代币账户,然后"memcmp": ...过滤器根据 每个代币账户内的所有者 地址进行选择。

运营概况

创建新的令牌类型

可以通过使用指令初始化新的 Mint 来创建新的代币类型 InitializeMint。Mint 用于创建或“铸造”新代币,这些代币存储在帐户中。Mint 与每个帐户相关联,这意味着特定代币类型的总供应量等于所有关联帐户的余额。

需要注意的是,该InitializeMint指令不需要 Solana 帐户初始化,也不需要签名者。该InitializeMint 指令应与创建 Solana 帐户的系统指令一起进行原子处理,方法是将两个指令都包含在同一笔交易中。

一旦铸币厂初始化完毕,mint_authority就可以使用 指令创建新代币 MintTo。只要铸币厂包含有效的mint_authority,铸币厂就被视为具有非固定供应,可以随时mint_authority使用 指令创建新代币。 指令可用于不可逆地将铸币厂的权限设置为,从而使铸币厂的供应固定。 永远无法铸造更多代币。MintTo``SetAuthority``None

可以随时通过发出Burn从账户中删除和丢弃代币的指令来减少代币供应。

创建帐户

账户持有代币余额,并使用指令创建InitializeAccount 。每个账户都有一个所有者,该所有者必须作为签名者出现在某些指令中。

账户所有者可以使用 SetAuthority指令将账户所有权转让给另一个账户。

需要注意的是,该InitializeAccount指令不需要 Solana 帐户初始化,也不需要签名者。该InitializeAccount 指令应与创建 Solana 帐户的系统指令一起进行原子处理,方法是将两个指令都包含在同一笔交易中。

转移代币

可以使用指令在账户之间转移余额。 当源账户和目标账户不同时,Transfer源账户的所有者必须作为指令中的签名者在场。Transfer

需要注意的是,当 的源和目标相同时Transfer,总是成功。因此, 成功并不一定意味着所涉及的帐户是有效的 SPL 代币帐户,任何代币都被移动,或者源帐户作为签名者存在。我们强烈建议开发人员在 从其程序中调用指令之前 仔细检查源和目标是否不同。Transfer``Transfer``Transfer

燃烧

Burn指令会减少一个账户的代币余额,而无需转移到另一个账户,从而有效地永久地将代币从流通中移除。

链上没有其他方式可以减少供应量。这类似于向未知私钥的账户转账或销毁私钥。但使用Burn指令进行销毁的行为更明确,并且可以被任何一方在链上确认。

权限委托

账户所有者可以使用指令委托部分或全部代币余额的权限Approve。被委托的权限可以转移或销毁其被委托的金额。账户所有者可以通过Revoke指令撤销权限委托。

多重签名

支持 M of N 多重签名,可用于代替 Mint 授权机构或帐户所有者或代表。多重签名授权机构必须使用InitializeMultisig指令进行初始化。初始化指定一组有效的 N 个公钥,以及必须作为指令签名者存在的 N 个公钥的数量 M,以使授权机构合法。

需要注意的是,该InitializeMultisig指令不需要 Solana 帐户初始化,也不需要签名者。该 InitializeMultisig指令应与创建 Solana 帐户的系统指令一起进行原子处理,方法是将两个指令都包含在同一笔交易中。

此外,多重签名允许在签名者集合中存在重复账户,从而实现非常简单的加权系统。例如,可以使用 3 个唯一公钥构建 2/4 多重签名,并将一个公钥指定两次以赋予该公钥双倍投票权。

冻结账户

铸币厂还可能包含一个freeze_authority,可用于发出 FreezeAccount指令,使帐户无法使用。包含冻结帐户的代币指令将失败,直到使用该ThawAccount指令解冻帐户。该SetAuthority指令可用于更改铸币厂的freeze_authority。如果铸币厂的freeze_authority设置为, None则帐户冻结和解冻将被永久禁用,并且所有当前冻结的帐户也将永久冻结。

包装 SOL

Token 程序可用于包装本机 SOL。这样做允许本机 SOL 像任何其他 Token 程序令牌类型一样被处理,并且在从与 Token 程序接口交互的其他程序调用时非常有用。

包含包装的 SOL 的账户使用公钥与称为“Native Mint”的特定 Mint 相关联 So11111111111111111111111111111111111111112

这些帐户有一些独特的行为

  • InitializeAccount将初始化 Account 的余额设置为正在初始化的 Solana 账户的 SOL 余额,从而得到与 SOL 余额相等的代币余额。
  • 转账不仅会修改代币余额,还会将等量的 SOL 从源账户转移到目标账户。
  • 不支持刻录
  • 关闭帐户时余额可能不为零。

无论当前包装了多少 SOL,Native Mint 的供应量始终会报告 0。

免租

为了确保供应量计算的可靠性、Mint 的一致性和 Multisig 账户的一致性,所有持有 Account、Mint 或 Multisig 的 Solana 账户必须包含足够的 SOL 才能被视为免租

关闭账户

可以使用指令关闭帐户CloseAccount。关闭帐户时,所有剩余的 SOL 将转移到另一个 Solana 帐户(不必与代币计划相关联)。非本地帐户的余额必须为零才能关闭。

非同质化代币

NFT 只是一种代币类型,其中只铸造了一个代币。

钱包集成指南

本节介绍如何将 SPL Token 支持集成到支持原生 SOL 的现有钱包中。它假设一个模型,即用户有一个系统帐户作为他们发送和接收 SOL 的主要钱包地址。

虽然所有 SPL Token 账户都有自己的链上地址,但没有必要向用户显示这些额外的地址。

钱包使用两个程序:

  • SPL 代币程序:所有 SPL 代币使用的通用程序
  • SPL 关联代币账户计划:定义约定并提供将用户的钱包地址映射到其持有的关联代币账户的机制。

如何获取并显示代币持有量

getTokenAccountsByOwner JSON RPC方法 可用于获取钱包地址的所有代币账户。

对于每个代币铸造,钱包可以有多个代币账户:相关代币账户和/或其他辅助代币账户

按照惯例,建议钱包将同一代币铸币厂的所有代币账户的余额汇总为用户的单一余额,以免受这种复杂性的影响。

请参阅垃圾收集辅助代币账户 部分,了解有关钱包如何代表用户清理辅助代币账户的建议。

关联代币账户

在用户可以接收代币之前,必须在链上创建其关联的代币账户,并需要少量 SOL 将该账户标记为免租金。

对于谁可以创建用户的关联代币账户没有任何限制。它可以由钱包代表用户创建,也可以由第三方通过空投活动提供资金。

创建过程描述于此

强烈建议钱包在向用户表明他们能够接收该类型的 SPL 代币(通常通过向用户显示其接收地址来完成)之前,先为给定的 SPL 代币创建关联的代币账户。选择不执行此步骤的钱包可能会限制其用户从其他钱包接收 SPL 代币的能力。

“添加令牌”工作流程示例

当用户想要接收某种类型的 SPL 代币时,应首先为其关联的代币账户注资,以便:

  1. 最大限度地提高与其他钱包实现的互操作性
  2. 避免将创建相关代币账户的成本转嫁给第一个发送者

钱包应提供允许用户“添加代币”的用户界面。用户选择代币类型,系统会显示添加代币需要花费多少 SOL 的信息。

确认后,钱包将创建如此处所述的相关代币 类型

“空投活动”工作流程示例

对于每个收件人的钱包地址,发送包含以下内容的交易:

  1. 代表接收者创建相关的令牌账户。
  2. 使用TokenInstruction::Transfer完成转移

关联代币账户所有权

⚠️钱包绝不应该将关联代币账户的权限TokenInstruction::SetAuthority设置 为另一个地址。AccountOwner

辅助代币账户

可以随时将现有 SPL 代币帐户的所有权分配给用户。实现此目的的一种方法是使用 spl-token authorize <TOKEN_ADDRESS> owner <USER_ADDRESS>命令。钱包应该准备好妥善管理他们自己没有为用户创建的代币帐户。

在钱包之间转移代币

在钱包之间转移代币的首选方法是转移到接收者的关联代币账户。

收件人必须向发件人提供其主钱包地址。然后,发件人:

  1. 为接收者获取关联的代币账户
  2. 通过 RPC 获取收件人的关联代币账户并检查其是否存在
  3. 如果收件人的关联代币账户尚不存在,则发送方钱包应按 此处所述创建收件人的关联代币账户。发送方的钱包可以选择通知用户,由于创建了账户,转账将需要比平时更多的 SOL。但是,此时选择不支持创建收件人关联代币账户的钱包应向用户显示一条消息,其中包含足够的信息,以找到解决方法来实现他们的目标
  4. 使用TokenInstruction::Transfer完成转移

发送者的钱包不得要求接收者的主钱包地址保留余额才允许转账。

代币详细信息注册表

目前,Token Mint 注册中心有几种解决方案:

分散式解决方案正在进行中。

垃圾收集辅助代币账户

钱包应尽快将辅助代币账户中的资金转入用户关联的代币账户,从而清空辅助代币账户。此举有两个目的:

  • 如果用户是辅助账户的关闭权限,钱包可以通过关闭账户为用户回收SOL。
  • 如果辅助账户由第三方提供资金,一旦账户清空,第三方可能会关闭该账户并收回 SOL。

垃圾回收辅助代币账户的一个自然时机是用户下次发送代币时。执行此操作的附加指令可以添加到现有交易中,并且不需要额外费用。

清理伪步骤:

  1. 对于所有非空的辅助代币账户,添加一条 TokenInstruction::Transfer指令,将全部代币金额转移到用户关联的代币账户中。
  2. 对于所有空的辅助令牌账户,其中用户是关闭权限,添加一条TokenInstruction::CloseAccount指令

如果添加一个或多个清理指令导致事务超出允许的最大事务大小,请删除这些额外的清理指令。它们可以在下一次发送操作期间被清理。

spl-token gc命令提供了此清理过程的示例实现。

代币归属

目前有两种解决方案可用于归属 SPL 代币:

1)Bonfida 代币归属

此程序允许您锁定任意 SPL 代币,并按照确定的解锁计划释放锁定的代币。一个由一个和一个代币unlock schedule组成,在初始化归属合约时,创建者可以传递一个任意大小的数组,从而使合约创建者可以完全控制代币随时间如何解锁。unix timestamp``amount``unlock schedule

解锁的工作原理是推动合约上的无权限曲柄,将代币移动到预先指定的地址。当前接收者密钥的所有者可以修改授权合约的接收者地址,这意味着授权合约锁定的代币可以进行交易。

2)Streamflow 时间锁

使用基于时间的锁定和托管账户,可以创建、提取、取消和转移代币归属合约。合约默认可由创建者取消,并可由接收者转移。

归属合约创建者在创建时可以选择多种选项,例如:

  • SPL 代币及归属金额
  • 接受者
  • 确切的开始和结束日期
  • (可选)悬崖日期和金额
  • (可选)发布频率

即将推出:

  • 合同是否可以由创作者/接收者转让
  • 合同是否可以由创作者/接收者取消
  • 主題/記錄

资源:

参考文档

https://github.com/solana-labs/solana-program-library/blob/master/docs/src/token.mdx

error: rustc 1.79.0-dev is not supported by the following package:

anchor build时报以下错误

info: uninstalling toolchain 'solana'
info: toolchain 'solana' uninstalled
error: rustc 1.79.0-dev is not supported by the following package:

                 Note that this is the rustc version that ships with Solana tools and not your system's rustc version. Use `solana-install update` or head over to https://docs.solanalabs.com/cli/install to install a newer version.
  bytemuck_derive@1.9.2 requires rustc 1.84
Either upgrade rustc or select compatible dependency versions with
`cargo update <name>@<current-ver> --precise <compatible-ver>`
where `<compatible-ver>` is the latest version supporting rustc 1.79.0-dev

执行以下命令

cargo update -p bytemuck_derive --precise 1.7.0

Solana基础 - 十分钟开始使用多签工具SQUADS

十分钟开始使用多签工具SQUADS

关于squads

Squads是Solana区块链上领先的多重签名(multisig)解决方案,已经为超过100亿美元的资产提供安全保障。它提供直观的用户界面和完善的安全机制,让团队能够安全地管理数字资产、程序升级权限和验证者节点。作为一个去中心化的自托管工具,Squads支持多种功能,包括资金库管理、代币发行、NFT操作以及与Solana生态系统中其他DeFi应用的集成。其核心特点是要求多方签名才能执行交易,大大提升了资产安全性,特别适合DAO组织和Web3项目团队使用。

什么是多重签名(multisig)

多重签名(Multisig)是一种数字资产安全管理机制,要求多个预设的密钥持有者共同授权才能执行某项操作。与传统的单一签名不同,多重签名通常设置为"M-of-N"模式,即在N个授权者中需要至少M个授权者同意才能完成交易。例如,在3-of-5的多重签名设置中,需要5个授权者中的至少3个同意才能执行交易。这种机制有效防止了单点故障,即使某个密钥丢失或被盗,资产仍然是安全的。

快速开始

接下来我们会有一个简单的教程展示如何使用Typescript在测试网上与SQUADS 协议交互。

首先,创建目录和文件结构:

mkdir squads_quickstart
cd squads_quickstart
  1. 创建 tsconfig.json:
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "esModuleInterop": true
  }
}
  1. 创建 package.json:
{
  "scripts": {
    "test": "npx mocha -r ts-node/register 'main.ts' --timeout 10000"
  },
  "dependencies": {
    "@solana/web3.js": "^1.73.0",
    "@sqds/multisig": "^2.1.3"
  },
  "devDependencies": {
    "@types/chai": "^4.3.3",
    "@types/mocha": "^10.0.6",
    "chai": "^4.3.6",
    "mocha": "^10.3.0",
    "ts-mocha": "^10.0.0",
    "typescript": "^4.8.3"
  }
}
  1. 创建main.ts 本文将会带领读者创建一个脚本逐步进行如下几步,创建多签,提出一个新的转账,进行投票并且执行。 首先, 设置多签成员并且创建地址
describe("Interacting with the Squads V4 SDK", () => {
  const creator = Keypair.generate();
  const secondMember = Keypair.generate();
  before(async () => {
    const airdropSignature = await connection.requestAirdrop(
      creator.publicKey,
      1 * LAMPORTS_PER_SOL
    );
    await connection.confirmTransaction(airdropSignature);
  });

  const createKey = Keypair.generate();

  // 从多签账户中派生PDA
  const [multisigPda] = multisig.getMultisigPda({
    createKey: createKey.publicKey,
  });

  it("Create a new multisig", async () => {
    const programConfigPda = multisig.getProgramConfigPda({})[0];

    console.log("Program Config PDA: ", programConfigPda.toBase58());

    const programConfig =
      await multisig.accounts.ProgramConfig.fromAccountAddress(
        connection,
        programConfigPda
      );

    const configTreasury = programConfig.treasury;

    // 生成多签
    const signature = await multisig.rpc.multisigCreateV2({
      connection,
      //一次性密钥
      createKey,
      // 创建者和费用支付这
      creator,
      multisigPda,
      configAuthority: null,
      timeLock: 0,
      members: [
        {
          key: creator.publicKey,
          permissions: Permissions.all(),
        },
        {
          key: secondMember.publicKey,
          // 这里的权限代表用户只可以投票
          permissions: Permissions.fromPermissions([Permission.Vote]),
        },
      ],
      // 这里代表至少需要两票才可以使提案通过
      threshold: 2,
      rentCollector: null,
      treasury: configTreasury,
      sendOptions: { skipPreflight: true },
    });
    await connection.confirmTransaction(signature);
    console.log("Multisig created: ", signature);
  });

生成转账提案

现在,我们可以进行转账提案的生成。 我们希望这个多签账户向生成者发送0.1 SOL。

  it("Create a transaction proposal", async () => {
    const [vaultPda] = multisig.getVaultPda({
      multisigPda,
      index: 0,
    });
    const instruction = SystemProgram.transfer({
      // 转账是从Squads金库签名的,这就是为什么我们使用VaultPda
      fromPubkey: vaultPda,
      toPubkey: creator.publicKey,
      lamports: 1 * LAMPORTS_PER_SOL,
    });
    // 这个消息包含了交易将要执行的指令
    const transferMessage = new TransactionMessage({
      payerKey: vaultPda,
      recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
      instructions: [instruction],
    });

    // 获取当前多签交易索引
    const multisigInfo = await multisig.accounts.Multisig.fromAccountAddress(
      connection,
      multisigPda
    );

    const currentTransactionIndex = Number(multisigInfo.transactionIndex);

    const newTransactionIndex = BigInt(currentTransactionIndex + 1);

    const signature1 = await multisig.rpc.vaultTransactionCreate({
      connection,
      feePayer: creator,
      multisigPda,
      transactionIndex: newTransactionIndex,
      creator: creator.publicKey,
      vaultIndex: 0,
      ephemeralSigners: 0,
      transactionMessage: transferMessage,
      memo: "Transfer 0.1 SOL to creator",
    });

    await connection.confirmTransaction(signature1);

    console.log("Transaction created: ", signature1);

    const signature2 = await multisig.rpc.proposalCreate({
      connection,
      feePayer: creator,
      multisigPda,
      transactionIndex: newTransactionIndex,
      creator,
    });

    await connection.confirmTransaction(signature2);

    console.log("Transaction proposal created: ", signature2);
  });

向提案投票,这里我们使用教程开始部分生成的两个密钥

  it("Vote on the created proposal", async () => {
    // 获取当前多签账户的交易索引
    const transactionIndex =
      await multisig.accounts.Multisig.fromAccountAddress(
        connection,
        multisigPda
      ).then((info) => Number(info.transactionIndex));

    // 第一个成员(创建者)对提案进行投票
    const signature1 = await multisig.rpc.proposalApprove({
      connection,
      feePayer: creator,      // 交易费用支付者
      multisigPda,            // 多签账户地址
      transactionIndex: BigInt(transactionIndex),  // 交易索引
      member: creator,        // 投票成员
    });

    // 等待第一个投票交易确认
    await connection.confirmTransaction(signature1);

    // 第二个成员对提案进行投票
    const signature2 = await multisig.rpc.proposalApprove({
      connection,
      feePayer: creator,      // 交易费用仍由创建者支付
      multisigPda,            // 多签账户地址
      transactionIndex: BigInt(transactionIndex),  // 交易索引
      member: secondMember,   // 第二个投票成员
    });

    // 等待第二个投票交易确认
    await connection.confirmTransaction(signature2);
  });

执行交易

  it("Execute the proposal", async () => {
    const transactionIndex =
      await multisig.accounts.Multisig.fromAccountAddress(
        connection,
        multisigPda
      ).then((info) => Number(info.transactionIndex));

    const [proposalPda] = multisig.getProposalPda({
      multisigPda,
      transactionIndex: BigInt(transactionIndex),
    });
    const signature = await multisig.rpc.vaultTransactionExecute({
      connection,
      feePayer: creator,
      multisigPda,
      transactionIndex: BigInt(transactionIndex),
      member: creator.publicKey,
      signers: [creator],
      sendOptions: { skipPreflight: true },
    });

    await connection.confirmTransaction(signature);
    console.log("Transaction executed: ", signature);
  });

});

最后,使用

yarn test

来执行脚本

好了,现在脚本已经执行完成,复制地址可以在solana explorer中查看。

完整代码可以参考 https://github.com/kimmy1886/squads_quickstart

Solana基础 - 给EVM开发者的 Solana开发指南

本文介绍了EVM开发者如何转向Solana平台,包括Solana的架构、技术优势、开发工具及账户模型的不同,强调程序的无状态特性以及数据存储的外部化。同时,文章比较了Ethereum和Solana的交易处理模型、费用机制及开发工具,帮助开发者顺利过渡。

概述

以太坊虚拟机(EVM)多年来一直是去中心化应用(dApp)开发的基础,但 Solana 的创新架构提供了诸如高吞吐量、低延迟和成本效率等独特优势。本指南旨在为希望在 Solana 上构建应用的 EVM 开发者设计。通过利用你对以太坊和 EVM 的现有知识,本指南将帮助你理解 Solana 的关键概念、工具和工作流程。

在本指南结束时,你将对 Solana 的架构有一个清晰的理解,并了解其与以太坊的不同之处。你将学习账户、执行模型和交易处理等关键概念如何与 EVM 进行比较,帮助你将开发思维从基于 Solidity 的智能合约转变为 Solana 的无状态 Rust 程序。

你将做什么

  • 理解 EVM 和 SVM 之间的关键差异,包括执行模型、账户结构和费用机制
  • 学习在 Solana 上如何存储和访问数据,与以太坊的存储模型进行比较
  • 探索两个网络的交易处理模型,以理解执行效率
  • 将你的思维方式从基于 Solidity 的智能合约开发转向基于 Rust 的 Solana 程序及其基于账户的架构

你需要什么

  • 以太坊智能合约开发(SolidityHardhatFoundry)经验
  • 基本 EVM 概念(交易、存储、gas 费用、合约执行)的知识

不需要有 Rust 或 Solana 的先前经验。我们将以适合以太坊开发者的方式解释关键差异。

为什么选择 Solana?

作为以太坊开发者,探索 Solana 提供了独特的机会,得益于其技术优势和相当明显的生态系统增长。

技术优势

  • 高吞吐量的并行执行:Solana 利用 Sealevel,一个并行执行引擎,允许多个交易同时处理,而不是以太坊的顺序执行模型。这使得在没有网络拥堵的情况下实现更高的吞吐量。
  • 无状态程序:与以太坊智能合约不同,后者存储自己的状态,Solana 程序是无状态的,并通过外部账户进行数据存储。这种代码和状态的分离提高了效率并减少了链上存储成本。
  • 单体设计:与以太坊的模块化路线图不同,Solana 旨在原生扩展,无需依赖 rollup 或分片。这意味着更简单的开发、更少的跨链复杂性、没有流动性分散,更好的用户体验。
  • 成本效率:Solana 的 Layer 1 架构被设计为低交易费用,为开发者和用户提供了经济可行的环境,而无需依赖附加的扩展解决方案。

生态系统增长与开发者机会

  • 链 GDP 增长:在 2024 年第四季度,Solana 的总应用收入(称为链 GDP)环比增长 213%,从 2.68 亿美元增长到 8.4 亿美元。值得注意的是,仅 11 月,当月的应用收入就达到了 3.67 亿美元。
  • DeFi 总锁定价值(TVL):在 2024 年第四季度末,Solana 成为第二大 DeFi TVL 区块链,达到 86 亿美元,标志着环比增长 213%。
  • 去中心化交易所(DEX)主导地位Raydium 作为 Solana 的一家领先 DEX,在 2024 年第四季度的平均日交易量达 32 亿美元,较去年大幅增长。Raydium 在全球 DEX 交易量的份额也环比上升超过 66%,使其成为按交易量计算的第二大 DEX,仅次于 Uniswap。
  • 流动性质押增长:Solana 的流动性质押率环比上升 33%,达到了 11.2%。作为一家领先的流动性质押提供商,Sanctum 现在支持超过 100 种流动性质押 Token(LST)。

网络的技术能力,加上不断扩展的生态系统,使其成为开发者的一个吸引人选择。通过学习如何在 Solana 上构建应用,你可以利用这些机会并为网络的增长做出贡献。

如需详细分析,请参考 Messari 的 State of Solana Q4 2024 报告

以太坊与 Solana 之间的关键架构差异

在开始之前,熟悉以太坊和 Solana 的术语差异至关重要。

以太坊与 Solana:术语对比表

以太坊 (EVM) Solana (SVM) 描述
智能合约 程序 在以太坊中,智能合约存储逻辑和状态。在 Solana 中,程序是包含可执行代码但无状态的账户,数据存储在单独的账户中。
Wei Lamports 在以太坊中,weiETH的最小单位,其中1 ETH = 10¹⁸ wei。在 Solana 中,lamportsSOL的最小单位,其中1 SOL = 1,000,000,000 lamports
Gas 计算单位 在以太坊中,Gas 测量交易所需的计算工作量,其中费用 = gas 使用量 ×(基础 + 优先级)。在 Solana 中,计算单位(CUs)充当类似角色,其中费用 = 固定基数(每签名)+(CU × CU 价格)
ABI IDL(接口描述语言) 在以太坊中,ABI(应用程序二进制接口)定义了合约与外部应用的交互方式。在 Solana 中,IDL(接口描述语言)具有相同的目的,定义程序如何为客户端交互暴露函数。
代理合约 程序 在以太坊中,代理合约用于启用智能合约更新。在 Solana 中,程序是默认可升级的。
Nonce 区块哈希 / 持久性 Nonce 以太坊交易使用逐增的每账户 nonce来防止重放攻击。Solana 主要使用最近的区块哈希进行交易验证,但也支持 Durable Nonces 用于离线签名和更长的交易有效期。
交易 指令 在以太坊中,交易通常调用单个合约函数,即使涉及复杂的合约逻辑。在 Solana 中,交易由一个或多个指令组成,每个指令调用一个程序以执行特定操作。
ERC20 Token SPL Token 在以太坊中,ERC-20 Token 是为每个 Token 部署的单独智能合约。在 Solana 中,SPL Token 通过一个共享的 Token 程序进行管理,消除了为每个 Token 部署单独合约的需求。

如果这些概念仍有些不清晰,别担心。我们将在本指南后续部分对其进行更详细的覆盖。

账户模型:有状态与无状态

在以太坊中,智能合约存储自己的状态,这意味着存储和执行是捆绑在一起的。在 Solana 中,程序根本不存储任何状态。所有状态都存储在与合约本身分开的账户中。

以太坊的账户模型

以太坊账户有两种形式:

  • EOAs(外部拥有账户):用户控制的带有私钥的账户。
  • CAs(合约账户):智能合约账户,存储代码和状态

当你在以太坊上部署智能合约时,它存储在合约账户中,并且所有状态修改都会发生在该账户内。

Solana 的账户模型

在 Solana 上,一切都是账户。但它们分为两类:

  • 程序账户(可执行):仅包含代码,类似于以太坊智能合约,但没有状态。
  • 数据账户(不可执行):存储所有链上数据,充当程序的持久存储。

Solana 中的账户如何运作

  • 每个账户都有一个所有者。唯一可以修改账户数据的实体是所有者程序。
  • 默认情况下,新账户由系统程序拥有,该程序处理某些操作,如转移 Token、在账户上分配数据和分配账户的所有权。
  • 要创建一个账户,你只需向该地址发送 SOL。
  • 当一个账户被分配给一个程序后,程序本身控制该账户可以发生的事情。
  • 账户需要保持一定数量的 SOL(lamports)以支付租金,这是一个确保有效使用区块链资源的机制。它要求账户保持最低余额以保持活跃。如果关闭账户,可以收回这笔租金。

程序账户可以通过分配来自另一个程序的账户或使用程序衍生地址(PDAs)模型在数据账户中存储其数据。开发人员可以通过将程序设置为账户所有者并使用种子生成地址来生成这些数据账户。

从下图可以看出,一个账户有五个字段:lamportsownerexecutablerent_epochdata

程序账户和数据账户图片来源

程序衍生地址(PDAs)

程序衍生地址(PDA)是一种特殊类型的账户地址,程序可以控制而无需私钥。它们允许程序签名。如果 PDAs 是基于其程序 ID 生成的,程序可以用于签署。此外,它们允许确定性数据存储,以确保相同的种子值将始终生成相同的 PDA。

PDA 可用作链上账户的地址(唯一标识符),提供方便存储、映射和提取程序状态的方法。 来源

开发人员可以使用种子和 bump 生成 PDA,确保它们不会与用户控制的地址发生冲突。bump 是一个额外值(介于 0 和 255 之间),加到种子上以生成唯一地址。

PDA 生成图片来源

例如,假设我们的程序有一个 increment 函数,用于递增计数器。我们可以为每个用户创建一个单独的 PDA,而不是将所有用户计数器存储在单个账户中,其中 PDA 的种子基于用户的公共密钥。

  • 如果用户 A 调用 increment,程序定位并更新用户 A 的计数器 PDA。
  • 如果用户 B 调用 increment,程序定位并更新用户 B 的计数器 PDA。
  • 因为程序拥有 PDA,所以只有程序可以修改它——用户不直接控制数据。但是,程序还可以通过在 PDA 的数据中存储一个 authority 字段来设计权限检查,以根据程序的逻辑给予用户间接控制。

这消除了每次写入时显式授权的需要,使 Solana 程序的高效性显著提高。

Solana 的账户模型带来了一些关键优势,改善了用户体验,使在 Solana 上构建变得更加容易。让我们探讨一些这些好处。

并行执行与本地化的费率市场

Solana 的账户模型通过要求每个交易预定义将要读取和写入的账户,启用了并行执行。这使得非冲突交易可以并行处理,提高了网络效率并减少了拥堵。

其工作原理

  • 如果两个交易仅从同一账户读取,则它们可以同时执行。
  • 如果一个交易写入而另一个交易读取同一账户,则执行必须是顺序的,以保持数据一致性。
  • 如果多个交易尝试写入同一账户,则它们将逐一处理。处理是在本地化费率市场中决定的,其中优先级更高(费用更高)的交易首先执行。

Solana 优先费 API

QuickNode 的 Solana Priority Fee API 允许开发人员获取最新费率的优先费用。

交易 1 交易 2 Solana 上的执行模型
Alice 向 Bob 发送 SOL Alice 向 Charlie 发送 SOL 顺序
Alice 向 Bob 发送 SOL Charlie 向 Carol 发送 SOL 并行

为什么这很重要

  • 未涉及高需求操作的用户(例如流行的 Token 铸造)不会受到无关交易的费用峰值影响。
  • 程序设计影响性能。例如,需求写入量高的自动化做市商(AMM)和 Token 铸造程序可能会发生执行瓶颈。设计程序以最小化写入争用可以改善性能。

程序可重用性

Solana 账户模型的其他关键优势是程序可重用性。Solana 允许多个应用程序与共享链上程序进行交互。这减少了代码重复、部署成本和执行效率低下,使 Solana 更加可扩展和可组合。

以太坊:智能合约是孤立的

在以太坊中,每个新用例(Token、NFT、DeFi 协议)通常需要:

  • 单独的智能合约部署,导致网络中逻辑冗余。
  • 由于重复合约,合约创建会产生不必要的交易费用。
  • 独立的存储和执行,使得合约之间很难实现无缝交互。

例如,如果你创建一个新的 ERC-20 Token,你必须部署一个全新的智能合约,尽管大多数 ERC-20 合约在功能上是相同的。

Solana:可重用的程序

Solana 程序设计为可共享和可重用的。开发者可以在现有程序下创建新账户,而不是为每个用例部署新程序。

例如,Solana Token Program 管理所有 SPL Token(Solana 的 Token 标准),消除了为单个 Token 创建单独合约的需要:

  • 你只需在 Token 程序下创建一个铸造账户,而不是部署合同。
  • 这个铸造账户定义 Token 的属性,例如供应量、小数位数和权限。
  • Token 程序处理所有操作(铸造、销毁、转移),而无需为每个 Token 要求单独的合约。

这些示例还可以扩展到 DAO、NFT 等其他用例。

本指南无法覆盖 Solana 账户模型的所有细节。如果你希望了解更多关于 Solana 账户模型的信息,请查看我们的 Solana 账户模型简介 指南。

Gas 费用

在以太坊和 Solana 之间转变时,理解交易费用的工作原理至关重要,因为它们的处理方式由于架构选择的不同而存在显著差异。虽然它们有一些相似之处,例如有优先费用和基础费用,但费率结构却不同。

以太坊费用

  • 全球费用市场:以太坊运行着一个通用费用市场,其中某一领域(如 NFT 铸造)的高需求可能会推动所有交易的 gas 价格上涨。Gas 测量计算工作量,而你设置的 gas 价格决定交易的优先级,导致费用高峰和网络拥堵期间成本增加。
  • 基础费用:以太坊的每个区块有一个基础费用,由当前区块之前的区块确定。
  • 优先费用:优先费用是为了激励验证者优先处理交易。用户可以支付更高的优先费用以确保其交易优先于其他交易包含在一个区块中,这可能导致在网络拥塞期间的费用峰值。

Gas 测量计算工作量,类似于 Solana 的计算单位,总费用计算为gas 使用量 * gas 价格,而 gas 价格由基础费用和优先费用组成。

Solana 费用

Solana 的费用结构确保只有与同一账户交互的交易相互竞争优先级,而其他交易不受高需求操作的影响。

基础费用

  • 依赖于交易所需的签名数量。
  • 每个签名费用为 5000 lamports(0.000005 SOL)。
  • 一半的费用被销毁,另一半归验证者。

优先费用

  • 用户可以出价额外费用以优先处理其交易,类似于以太坊的优先费用。
  • 验证者根据此费用确定在高需求情况下优先包含哪些交易。
  • 一半的费用被销毁,另一半归验证者。

总费用取决于用户愿意支付的优先费用以及交易所需的总计算(更复杂的交易需要更多的计算单位)。

租金(存储成本)

  • Solana 要求账户存入 SOL 以保持活跃——这防止了不活跃账户占用不必要的存储。
  • 这不是费用,而是可退还的押金。当账户关闭后,租金将根据程序所有者(例如,通常是账户权限)规定收回。
  • 关于租金的更多细节,请查看 Solana 上的租金 指南。

交易处理

Solana 和以太坊都支持原子交易,这意味着如果交易的任何部分失败,整个交易将被回滚。然而,Solana 和以太坊的交易处理方式略有不同。

以太坊交易

在以太坊中,交易通常是对智能合约的单一函数调用。因此,开发者可能需要创建自定义函数以处理特定用例。

以太坊使用增量 nonce(与你的账户相关联的计数器)确保交易唯一,并防止重放攻击。交易等待在内存池中,等待验证者选择,但这可能导致抢跑交易或在网络拥堵时期的高 gas 费用。

Solana 交易

Solana 采取滚动,而事务可以捆绑多个指令(可视为小函数调用),使你能够链式操作(例如,转移 tokens 和更新余额)一次完成。

然而,限制了可以包括在单个交易中的指令数量:

  • 交易大小限制:每个交易 1232 字节
  • 计算单位限制:交易必须保持在最大计算预算内,这意味着复杂交易可能需要拆分

Solana 的计算单位

限制 计算单位
每个区块的最大计算 4800 万
每个账户每个区块的最大计算 1200 万
每个交易的最大计算 140 万
默认交易计算 20 万

随着你对 Solana 的进一步了解,你可能会遇到这些交易限制。有一些工具,例如 Lil' JIT Jito Bundles,使多交易的原子批处理成为可能,从而允许复杂的执行超出标准的 CU 限制。

来源:优化 Solana 交易的策略

Solana 使用最近的 blockhash 而不是 nonce 来验证交易。此 blockhash 对 150 个区块有效,之后交易失效。

另外,Solana 支持 持久性 Nonces,允许事务离线签名后随时提交。持久性 Nonces 是唯一和顺序的,可以防止重放攻击,确保延迟交易的安全执行。

没有内存池,但优先费用仍然在订单排序中发挥作用。交易直接发送到验证者和领导者,减少了开销并缩短了延迟,但需要验证者向领导者转发交易,直到它们被处理。

领导者负责生成区块,每 4 个区块进行一次轮换。

Solana 交易

Solana 交易由以下组成:

  • 一组指令
  • 一组读取或写入的账户
  • 一组签名
  • 最近的 blockhash

有关 Solana 交易的更多详细信息,请查看我们的 指南

开发工具

以下是对以太坊(EVM)和 Solana(SVM)之间的一些常用开发工具的比较表,以帮助你顺利过渡。

以太坊工具 Solana 对应工具 描述
Solidity Rust、C、TypeScript、Assembly Rust(有或没有 Anchor)是 Solana 的标准。C、Assembly 和 TypeScript 也用于 Solana。
Hardhat/Foundry Solana 测试验证器LiteSVMLuzid 类似于 Hardhat 节点的本地区块链,用于测试程序和账户。
Hardhat/Foundry Program-testBankRun.js Rust 和 JS 测试框架,用于程序测试
Ethers.js / Viem @solana/kit(前身为 Solana Web3.js 2.x)、Solana Web3.js 1.x(不推荐使用) 用于客户端 dApp 交互的 JavaScript SDK,类似于 Ethers.js。
Remix Solana Playground 基于 Web 的 IDE,编写、测试和部署 Rust 程序,类似于 Remix 的便利性。
ABI CodamaShanksAnchor 标准化的 IDL 和客户端生成工具,替代以太坊的 ABI,进行程序交互。
Etherscan SolanaFMSolana ExplorerSolscan 用于检查账户和交易的区块链浏览器,类似于 Etherscan。
scaffold-eth create-solana-dapp 用于快速 dApp 设置的模板生成器,类似于 scaffold-eth。
RainbowKit Solana Wallet Adapter 处理钱包连接的框架,类似于以太坊中的 wagmi 或 RainbowKit。

不准备使用 Rust?

如果你还没有准备深入学习 Rust,可以使用 Neon,在 Solana 程序中部署的 EVM。它允许你编写 Solidity 合约并将其部署在 Solana 上。你可以在 这里 找到更多 Neona 信息,查看我们的 Neon 指南

智能合约(程序)开发

在智能合约(程序)开发中,Solana 采取不同的方法,程序是无状态的,数据存储在单独的账户中。

本节将帮助你理解以太坊和 Solana 智能合约开发之间的关键差异,而不深入研究。

智能合约结构:代码与状态分离

以太坊:智能合约同时持有代码和状态
  • 在以太坊中,智能合约是一个包含逻辑(代码)和持久存储(状态)的单元。
  • 它作为合约账户(CA)被部署,与用户控制的外部拥有账户(EOA)分开。
  • 示例:如果你提取 Token 余额,它会直接从合约的存储中获取。
  • 升级需要代理模式(例如,透明代理、UUPS)。
Solana:程序是无状态的;状态是外部的。
  • Solana 程序不存储任何状态。相反,所有数据存储在程序交互的账户中。
  • 程序被部署到可执行账户中,状态存储在不可执行账户中(例如,PDAs - 程序衍生地址)。
  • 示例: Token 余额不存储在智能合约中,而是存储在由 Token 程序拥有的账户中。
  • 程序是默认可升级的(但可以锁定以实现不可变性)。

系统程序 - Solana SPL Token 程序

数据存储:映射与账户

以太坊:使用映射存储数据
  • Solidity 允许映射(例如,mapping(address => uint256) balances;)在合约内部存储数据。
Solana:使用账户而不是映射
  • Solana 中没有直接等同于映射的概念。相反,PDAs(程序衍生地址)充当存储结构化数据的确定性账户。
  • 示例:而不是将 Token 余额映射到地址,你创建一个 PDA 账户来存储用户余额。

想问题时,不要以映射为思路,而是要以创建可以存储每个用户或实体所需数据的单独账户为思路。

函数执行:消息发送者 VS 显式账户

以太坊:msg.sendertx.origin 处理授权
  • 在 Solidity 中,合约可以自动知道通过 msg.sender 调用函数的用户是谁,以及通过 tx.origin 得到发起交易的用户。
Solana:需要显式账户传递
  • Solana 程序没有内置的 msg.sender 等效项。相反,你必须显式传递程序将要交互的账户。
  • 示例:要将 Token 从一个用户转移到另一个用户,交易必须包括:
    • Token 程序账户:执行 Token 转移逻辑的程序。
    • 发送者的 Token 账户:这个账户持有发送者的 Tokens,将被扣除。
    • 接收者的 Token 账户:这个账户将接收转移的 Tokens。
    • 发送者的主钱包账户:这个账户支付交易费用,通常是交易的签署者。
  • 这种显式性确保了 Solana 运行时确切知道涉及哪些账户及其角色(例如,可读写或签名)。

在设计 Solana 程序时,总是要以账户为思考对象。你必须传入程序交互的每个账户。

代币标准:ERC-20、ERC-721等 VS SPL Tokens

以太坊:每个 Token 都有自己的合约
  • 以太坊针对每种代币有不同的代币标准(例如 ERC-20、ERC-721 等)。每个代币遵循其对应的标准,但每个代币都是单独的智能合约,这需要单独为每个新代币进行合约部署。
Solana:一个 Token 程序,多个 Tokens
  • Solana 只有一个中心化的 Token 程序,管理所有 SPL(Solana 程序库)代币。
  • 相较于以太坊(例如 ERC-20),不需要为每种代币部署新合约,你只需在 Token 程序下创建一个铸造账户。
  • Token 程序处理所有核心代币功能,包括:
    • 创建新代币(铸造账户)
    • 将新代币铸造到账户
    • 销毁代币
    • 在账户之间转移代币
  • 每个用户的代币余额存储在专用代币账户中——一个与用户的钱包地址和 Token 的铸造地址Hook的 关联代币账户(ATA)
  • Solana 还引入了 SPL Token 2022,增强了原 Token 程序的高级功能,例如元数据、转移保护、转移费用等。

尽管 本指南没有涵盖所有差异,但这些是转变时应牢记的一些关键区别。

接下来,我们将探索使用 Solana 开发的基础知识,包括钱包和命令行工具。

开始使用 Solana:钱包和 CLI 基础

尽管可以通过库以编程方式创建钱包,但以太坊开发者通常会使用浏览器钱包(如 MetaMask 或 Rabby)来创建其钱包。这些钱包生成外部拥有账户(EOA),允许用户签署交易和管理资金。

在 Solana 上,你可以通过浏览器扩展(例如 PhantomSolflare)或通过命令行(Solana CLI)创建钱包。

在 Solana 上创建钱包

选项 1:使用浏览器钱包(Phantom、Solflare、Backpack)

  • 安装钱包。详细信息请查看 本指南
  • 创建一个新钱包并备份你的恢复密语短语。
  • 使用此钱包签署交易并与 Solana dApp 交互。

选项 2:使用 Solana CLI 创建钱包

如果你希望用于开发目的的钱包,可以使用 CLI 生成密钥对:

solana-keygen new --outfile ~/.config/solana/id.json

这将创建一个密钥对 JSON 文件(id.json),其中包含你的私钥(数组形式)。此外,你将有一个助记词,可用于恢复你的密钥对。

保护你的密钥对

保持 id.json 文件的安全和私密。绝不要分享其内容或你的助记词,因为它们给予完全访问钱包和资金的权限。

导入和导出钱包

如果你在浏览器扩展中有一个钱包(即 Phantom),但希望在 CLI 中使用它,则需要从扩展中导出其私钥。

我们将涵盖如何将 Phantom 钱包导出到 Solana CLI 以及反向操作。

将 Phantom 钱包导入 Solana CLI

  1. 打开 Phantom 并找到导出你的恢复密语短语的选项。
  2. 复制短语(一个单词序列)。
  3. 运行以下命令:
solana-keygen recover 'prompt://?key=0/0' -o my-wallet.json
  1. 在提示时输入你的恢复密语短语。
  2. 密钥对将导入到 Solana CLI。

将 Solana CLI 钱包导入 Phantom

  1. 在文本编辑器中打开 id.json 文件。
  2. 复制文件的内容(格式像是 [12, 34, 56, ...])。
  3. 在 Phantom 中找到导入私钥并粘贴复制的值。

这将把你在 CLI 生成的钱包恢复到 Phantom 中。

设置 Solana CLI 和 RPC 配置

开发者通过 RPC 端点与 Solana 网络交互。你可以配置与哪个网络交互(主网络、测试网或开发网)。

QuickNode RPC

为更快的发展,QuickNode 提供 Solana 的 RPC 端点。你可以使用这些 RPC 与 Solana 网络交互,而无需设置自己的节点或使用公共 RPC。
要获取 RPC 端点,请在 这里 注册一个免费帐户。

检查你的当前配置:

solana config get

设置你的 RPC 端点(例如,devnet):

solana config set --url devnet

验证你的钱包地址:

solana address

空投测试 SOL 以供开发:

solana airdrop 2
solana balance

这将为你提供 2 SOL 以便在开发网进行测试。此外,你还可以使用 QuickNode Faucet 获取测试 SOL。

有关更多信息,请查看 在 Solana 上空投测试 SOL 的完整指南

使用 QuickNode 进行多链开发

尽管以太坊和 Solana 在其技术设计上存在显著差异,但 QuickNode 提供了一个强大、统一的基础设施,方便开发者在这些生态系统之间桥接。以下是 QuickNode 如何支持以太坊(及其他 EVM 兼容链)和 Solana 之间无缝切换的技术概述,以及提升开发体验的关键特色。

多链开发的关键特征

  • 统一基础设施:QuickNode 作为以太坊和 Solana 生态系统信任的提供者,消除了在这两个区块链之间切换提供商的需要。
  • 链特定优化
    • 对于以太坊和 EVM 兼容链:高性能 RPC 端点、先进的分析和智能合约开发工具。
    • 对于 Solana:超低延迟访问,优化高吞吐量和快速区块时间。
  • 自定义产品和工具

QuickNode 提供针对每个区块链独特要求的专门产品和工具,包括 核心 RPC API、实时数据流、无服务器功能和附加服务

  • 以太坊:我们的 以太坊链页面 提供关于 QuickNode 所能做的一切的概述,从核心基础设施到像 Streams、Functions 和 Marketplace 附加工具等高级工具。在我们的 以太坊文档 中了解更多信息。
  • Solana:我们的 Solana 链页面 概述了 QuickNode 在 Solana 的能力,包括四种不同的数据流解决方案、核心 RPC 服务和通过 Marketplace 提供的额外开发者工具。有关更多信息,请参见我们的 Solana 文档
  • 全球低延迟架构:确保无论地理位置如何,性能都最优,关键于需要最小延迟的去中心化应用。

可靠性和合规性

QuickNode 的基础设施旨在满足任务关键应用的需求,确保安全性、正常运行时间和对开发者的支持。- SOC合规性:遵循SOC 1和SOC 2 Type 2标准,确保安全可靠的操作。

  • 保证正常运行时间:保持99.99%的正常运行时间,以确保应用程序持续性能。
  • 端到端支持:提供直接技术支持,以快速解决问题并保持开发进度。

凭借其强大的基础设施和以开发者为中心的工具,QuickNode简化了在Ethereum和Solana上构建的复杂性。

Solana基础指南

为了帮助你实际操作Solana,以下是一些涵盖基础知识的基础指南:

结论

从Ethereum(EVM)过渡到Solana(SVM)需要思维方式的转变,尤其是在程序架构、执行模型和账户管理方面。Solana将执行(程序)与存储(账户)分开,而不是智能合约存储自己的状态,这导致了更高的效率和更好的可扩展性。

虽然Solana开发与Ethereum不同,但许多概念可以映射,以便更轻松地进行过渡。通过理解关键差异并利用现有知识,你可以探索Solana的生态系统,构建高效的去中心化应用程序,并为网络的增长做出贡献。

想要看到更多类似内容吗?在下方留下你的反馈!

我们❤️反馈!

[告知我们](https://airtable.com/shrKKKP7O1Uw3ZcUB?prefill_Guide+Name=Solana Development for EVM Developers) 你的反馈或新主题请求。我们非常乐意听取你的意见。

订阅我们的时事通讯,获取更多关于Web3和区块链的文章和指南。如果你有任何问题或需要进一步的帮助,请随时加入我们的Discord服务器或使用下面的表格提供反馈。关注我们在Twitter(@QuickNode)和我们的Telegram公告频道以了解最新动态。

其他资源

对于那些希望深入了解Solana开发的人,以下是一些有用的资源:

QuickNode Solana内容

官方文档 & 指南

探索者 & 工具

社区 & 学习

转载:https://learnblockchain.cn/article/13189

Solana基础 - 在 Anchor 中的跨程序调用(CPI)

跨程序调用 (CPI) 是 Solana 的术语,用于描述一个程序调用另一个程序的公共函数。

我们之前已经进行过 CPI,当我们向系统程序发送一个 转账 SOL 交易。以下是相关代码片段,以作提醒:

    pub fn send_sol(ctx: Context<SendSol>, amount: u64) -> Result<()> {  
        let cpi_context = CpiContext::new(
            ctx.accounts.system_program.to_account_info(),
            system_program::Transfer {
                from: ctx.accounts.signer.to_account_info(),
                to: ctx.accounts.recipient.to_account_info(),
            }
        );

        let res = system_program::transfer(cpi_context, amount);

        if res.is_ok() {
            return Ok(());
        } else {
            return err!(Errors::TransferFailed);
        }
    }

CpiCpiContext 中字面上的意思是“跨程序调用”。

调用除系统程序外的其他程序的公共函数的工作流程并没有太大的不同——我们将在本教程中教授这一点。

本教程只关注如何调用另一个使用 Anchor 构建的 Solana 程序。如果其他程序是用纯 Rust 开发的,则以下指南将无法使用。

在我们的示例中,Alice 程序将调用 Bob 程序上的一个函数。

Bob 程序

我们从使用 Anchor 的 CLI 创建一个新项目开始:

    anchor init bob

然后在 bob/lib.rs 中复制并粘贴下面的代码。该账户有两个函数,一个用于初始化存储一个 u64 的账户,另一个函数 add_and_store 接受两个 u64 变量,将它们相加并存储在由结构体 BobData 定义的账户中。

    use anchor_lang::prelude::*;
    use std::mem::size_of;

    // 替换为你的 <PROGRAM_ID>
    declare_id!("8GYu5JYsvAYoinbFTvW4AACYB5GxGstz21FmZe3MNFn4");

    #[program]
    pub mod bob {
        use super::*;

        pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
            msg!("数据账户已初始化: {}", ctx.accounts.bob_data_account.key());

            Ok(())
        }

        pub fn add_and_store(ctx: Context<BobAddOp>, a: u64, b: u64) -> Result<()> {
            let result = a + b;

            // 修改/更新数据账户
            ctx.accounts.bob_data_account.result = result;
            Ok(())
        }
    }

    #[account]
    pub struct BobData {
        pub result: u64,
    }

    #[derive(Accounts)]
    pub struct BobAddOp<'info> {   
        #[account(mut)]
        pub bob_data_account: Account<'info, BobData>,
    }

    #[derive(Accounts)]
    pub struct Initialize<'info> {
        #[account(init, payer = signer, space = size_of::<BobData>() + 8)]
        pub bob_data_account: Account<'info, BobData>,

        #[account(mut)]
        pub signer: Signer<'info>,

        pub system_program: Program<'info, System>,
    }

本教程的目标是创建另一个程序 alice,调用 bob.add_and_store

在项目(bob)内,使用 anchor new 命令创建一个新程序:

    anchor new alice

命令行将打印出 创建了新程序

在开始编写 Alice 的程序之前,必须将下面的代码片段添加到 Alice 的 Cargo.toml 文件的 [dependencies] 部分,位置为 programs/alice/Cargo.toml

    [dependencies]
    bob = {path = "../bob", features = ["cpi"]}

Anchor 在这里做了大量的后台工作。Alice 现在可以访问 Bob 的公共函数和 Bob 的结构体的定义。你可以将这视为在 Solidity 中导入接口,以便知道如何与另一个合约进行交互

下面是 Alice 程序。 在代码顶部,Alice 程序正在导入承载 BobAddOp(用于 add_and_store)的结构体。请注意代码中的注释:

    use anchor_lang::prelude::*;
    // account struct for add_and_store
    use bob::cpi::accounts::BobAddOp;

    // Bob 的程序定义
    use bob::program::Bob;

    // Bob 存储和的账户
    use bob::BobData;

    declare_id!("6wZDNWprmb9TAZYMAPpT23kHDPABvBLT8jbWQKLHEmBy");

    #[program]
    pub mod alice {
        use super::*;

        pub fn ask_bob_to_add(ctx: Context<AliceOp>, a: u64, b: u64) -> Result<()> {
            let cpi_ctx = CpiContext::new(
                ctx.accounts.bob_program.to_account_info(),
                BobAddOp {
                    bob_data_account: ctx.accounts.bob_data_account.to_account_info(),
                }
            );

            let res = bob::cpi::add_and_store(cpi_ctx, a, b);

            // 如果 CPI 失败则返回错误
            if res.is_ok() {
                return Ok(());
            } else {
                return err!(Errors::CPIToBobFailed);
            }
        }
    }

    #[error_code]
    pub enum Errors {
        #[msg("cpi to bob 失败")]
        CPIToBobFailed,
    }

    #[derive(Accounts)]
    pub struct AliceOp<'info> {
        #[account(mut)]
        pub bob_data_account: Account<'info, BobData>,

        pub bob_program: Program<'info, Bob>,
    }

如果我们将 ask_bob_to_add 与本文顶部展示的转账 SOL 的代码片段进行比较,会发现很多相似之处。

跨程序调用

要实现 CPI,以下内容是必需的:

  • 目标程序的引用(作为 AccountInfo)(红框)
  • 目标程序运行所需的账户列表(包含所有账户的 ctx 结构体)(绿框)
  • 传递给函数的参数(橙框)

测试 CPI

以下 TypeScript 代码可用于测试 CPI:

    import * as anchor from "@coral-xyz/anchor";
    import { Program } from "@coral-xyz/anchor";
    import { Bob } from "../target/types/bob";
    import { Alice } from "../target/types/alice";
    import { expect } from "chai";

    describe("从 Alice 到 Bob 的 CPI", () => {
      const provider = anchor.AnchorProvider.env();

      // 配置客户端以使用本地集群。
      anchor.setProvider(provider);

      const bobProgram = anchor.workspace.Bob as Program<Bob>;
      const aliceProgram = anchor.workspace.Alice as Program<Alice>;
      const dataAccountKeypair = anchor.web3.Keypair.generate();

      it("已初始化!", async () => {
        // 在这里添加测试。
        const tx = await bobProgram.methods
          .initialize()
          .accounts({
            bobDataAccount: dataAccountKeypair.publicKey,
            signer: provider.wallet.publicKey,
            systemProgram: anchor.web3.SystemProgram.programId,
          })
          .signers([dataAccountKeypair])
          .rpc();
      });

      it("可以相加然后加倍!", async () => {
        // 在这里添加测试。
        const tx = await aliceProgram.methods
          .askBobToAddThenDouble(new anchor.BN(4), new anchor.BN(2))
          .accounts({
            bobDataAccount: dataAccountKeypair.publicKey,
            bobProgram: bobProgram.programId,
          })
          .rpc();
      });

it("可以断言 Bob 的数据账户中的值等于 4 + 2", async () => {

  const BobAccountValue = (
    await bobProgram.account.bobData.fetch(dataAccountKeypair.publicKey)    ).result.toNumber();
  expect(BobAccountValue).to.equal(6);
});
});

一行完成 CPI

因为传递给 Alice 的 ctx 账户包含进行交易所需的所有账户的引用,我们可以在该结构体的impl内创建一个函数来完成 CPI。请记住,所有impl将“附加”函数到可以使用结构体中的数据的结构体。由于ctx结构体AliceOp已经持有Bob进行交易所需的所有账户,我们可以将所有 CPI 代码移动:

let cpi_ctx = CpiContext::new(
    ctx.accounts.bob_program.to_account_info(),

    BobAddOp {
        bob_data_account: ctx.accounts.bob_data_account.to_account_info(),
    }
);

到一个impl中,如下所示:

let cpi_ctx = CpiContext::new(
    ctx.accounts.bob_program.to_account_info(),
    BobAddOp {
        bob_data_account: ctx.accounts.bob_data_account.to_account_info(),
    }
);

use anchor_lang::prelude::*;
use bob::cpi::accounts::BobAddOp;
use bob::program::Bob;
use bob::BobData;

// 用你的<PROGRAM_ID>替换 declare_id!("B2BNs2GecG8Ux5EchDDFZakRWX2NDfy1RDhPCTJuJtr5");

#[program]
pub mod alice {
    use super::*;

    pub fn ask_bob_to_add(ctx: Context<AliceOp>, a: u64, b: u64) -> Result<()> {
        // 调用 bob 程序中的`bob_add_operation`函数
        let res = bob::cpi::bob_add_operation(ctx.accounts.add_function_ctx(), a, b);

        if res.is_ok() {
            return Ok(());
        } else {
            return err!(Errors::CPIToBobFailed);
        }
    }
}

impl<'info> AliceOp<'info> {
    pub fn add_function_ctx(&self) -> CpiContext<'_, '_, '_, 'info, BobAddOp<'info>> {
        // 我们正在与之交互的 bob 程序
        let cpi_program = self.bob_program.to_account_info();

        // 将所需账户传递给 Bob 程序中的`BobAddOp`账户结构体
        let cpi_account = BobAddOp {
            bob_data_account: self.bob_data_account.to_account_info(),
        };

        // 使用新方法创建`CpiContext`对象
        CpiContext::new(cpi_program, cpi_account)
    }
}

#[error_code]
pub enum Errors {
    #[msg("cpi to bob 失败")]
    CPIToBobFailed,
}

#[derive(Accounts)]
pub struct AliceOp<'info> {
    #[account(mut)]

    pub bob_data_account: Account<'info, BobData>,
    pub bob_program: Program<'info, Bob>,
}

我们能够以“一行”的方式调用Bob的 CPI。这在 Alice 程序的其他部分调用 Bob 的 CPI 时可能很方便——将代码移动到impl中可以防止我们复制和粘贴代码来创建CpiContext

转载:https://learnblockchain.cn/article/10309