getAccountInfo 返回与提供的公钥的账户相关的所有信息,和以太坊 eth_getAccount类似 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getAccountInfo", "params": [ "8TKG1ez28ZYzNgGTein6Pc97yvxWwnHecVL9ZhdZTxd5", { "encoding": "base58" } ] } ' 返回 { "jsonrpc": "2.0", "result": { "context": { "apiVersion": "2.2.3", "slot": 370321974 }, "value": { "data": [ "", "base58" ], "executable": false, "lamports": 4994414840, "owner": "11111111111111111111111111111111", "rentEpoch": 18446744073709551615, "space": 0 } }, "id": 1 } getBalance 返回所提供公钥的账户的 Lamport 余额, 和以太坊eth_getBalance类似 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getBalance", "params": [ "8TKG1ez28ZYzNgGTein6Pc97yvxWwnHecVL9ZhdZTxd5" ] } ' 返回 { "jsonrpc": "2.0", "result": { "context": { "apiVersion": "2.2.3", "slot": 370322177 }, "value": 4994414840 }, "id": 1 } getBlock 返回账本中已确认区块的身份和交易信息，和以太坊 eth_getBlockByNumber类似 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getBlock", "params": [ 358300848, { "encoding": "json", "maxSupportedTransactionVersion": 0, "transactionDetails": "full", "rewards": false } ] } ' 如果类似如下提示，这通常意味着你尝试访问的特定槽（Slot）存在问题，具体可能是该槽被跳过或者在长期存储中缺失 { "jsonrpc": "2.0", "error": { "code": -32009, "message": "Slot 100000430 was skipped, or missing in long-term storage" }, "id": 1 } { "jsonrpc": "2.0", "result": { "blockHeight": 346328196, "blockTime": 1738488135, "blockhash": "HGrdL4Z5vEvQVWchG2pnJEQ5dMxQhqiXncpL6GJvZimx", "parentSlot": 358300847, "previousBlockhash": "3QuyTMBRAK5PhLXehDCTMH4zgnfn7xgZnHG3fBnZFsiQ", "transactions": [ ... { "meta": { "computeUnitsConsumed": 2100, "err": null, "fee": 5000, "innerInstructions": [], "loadedAddresses": { "readonly": [], "writable": [] }, "logMessages": [ "Program Vote111111111111111111111111111111111111111 invoke [1]", "Program Vote111111111111111111111111111111111111111 success" ], "postBalances": [ 912514572484583, 8402660357128602, 1 ], "postTokenBalances": [], "preBalances": [ 912514572489583, 8402660357128602, 1 ], "preTokenBalances": [], "rewards": null, "status": { "Ok": null } }, "transaction": { "message": { "accountKeys": [ "dv4ACNkpYPcE3aKmYDqZm9G5EB3J4MRoeE7WNDRBVJB", "23AoPQc3EPkfLWb14cKiWNahh1H9rtb3UBk8gWseohjF", "Vote111111111111111111111111111111111111111" ], "header": { "numReadonlySignedAccounts": 0, "numReadonlyUnsignedAccounts": 1, "numRequiredSignatures": 1 }, "instructions": [ { "accounts": [ 1, 0 ], "data": "[binary]", "programIdIndex": 2, "stackHeight": null } ], "recentBlockhash": "3QuyTMBRAK5PhLXehDCTMH4zgnfn7xgZnHG3fBnZFsiQ" }, "signatures": [ "56uXBR4rgLNbDCtCr8gyTktGYCBBsKdJ4mUEGS9rEkcS9tG9ZghksbZCUdjXdoLJqnScG2e4zQCxvUKE6ctwyjpw" ] }, "version": "legacy" } ... ] }, "id": 1 } getBlockCommitment 返回特定区块的承诺 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getBlockCommitment", "params": [ 346328196 ] } ' 返回 { "jsonrpc": "2.0", "result": { "commitment": null, "totalStake": 157541826450322300 }, "id": 1 } getBlockHeight 返回节点当前的区块高度，和以太坊eth_blockNumber 类似 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getBlockHeight", "params": [] } ' 返回 { "jsonrpc": "2.0", "result": 358302920, "id": 1 } getBlockProduction 获取当前epoch期间内，所有产生区块的地址和相应的产块结果[应产出：实际产出] range是当前周期的起始和当前slot区间 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getBlockProduction" } ' 注：对于产块结果，可以作为对应验证节点的稳定性判定 getBlocks 返回两个 slot 之间已确认的区块列表 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getBlocks", "params": [ 5, // 起始Slot 10 // 结束Slot (间距<= 500000) ] } ' getBlocksWithLimit 返回从给定槽开始的已确认区块列表 和getBlocks类似，区别就是由结束Slot改为了Limit curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getBlocksWithLimit", "params": [ 5, // 起始Slot 3 // Limit (间距<= 500000) ] } ' getBlockTime 返回某个区块的预计生产时间。 每个验证者都会定期向账本报告其 UTC 时间，方法是间歇性地为特定区块的投票添加时间戳。请求的区块时间是根据账本上记录的一组最近区块中投票时间戳的权益加权平均值计算得出的。 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getBlockTime", "params": [ 358319114 ] } ' getClusterNodes 返回有关参与集群的所有节点的信息 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getClusterNodes" } ' getEpochInfo 返回有关当前纪元的信息 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getEpochInfo" } ' 返回 { "jsonrpc": "2.0", "result": { "absoluteSlot": 370353885, // 当前槽位 "blockHeight": 358332171, // 当前区块高度 "epoch": 857, // 当前epoch "slotIndex": 129885, // 当前 slot 相对于当前 epoch 的开始 "slotsInEpoch": 432000, // 此纪元中的时隙数 "transactionCount": 15406146576 // 自创世以来无错误处理的交易总数 }, "id": 1 } getEpochSchedule 从此集群的创世配置中返回纪元调度信息 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getEpochSchedule" } ' 返回 { "jsonrpc": "2.0", "result": { "firstNormalEpoch": 0, "firstNormalSlot": 0, "leaderScheduleSlotOffset": 432000, "slotsPerEpoch": 432000, "warmup": false // 是否周期一开始很短，然后逐渐增长 }, "id": 1 } getFeeForMessage 获取网络针对特定消息收取的费用,和以太坊eth_estimateGas类似 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getFeeForMessage", "params": [ "AQABAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQAA", { "commitment": "processed" } ] } ' 返回 { "jsonrpc": "2.0", "result": { "context": { "slot": 5068 }, "value": 5000 }, "id": 1 } getFirstAvailableBlock 返回当前节点上可访问的最早的区块编号（block number） curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getFirstAvailableBlock" } ' 注：简单说就是查询当前RPC节点存储的历史区块最早的高度，由于存储成本问题，对于官方devnet testnet都只保留近期一部分数据，对于主网保留所有数据，对于历史数据保存需要使用bigtable外部存储，对于设置启动参数--limit-ledger-size,大概保存近期 600w左右区块数据 getGenesisHash 查询genesis hash curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getGenesisHash" } ' 返回 { "jsonrpc": "2.0", "result": "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG", "id": 1 } getHealth 返回节点的当前健康状况。健康节点是指位于 HEALTH_CHECK_SLOT_DISTANCE最新集群确认槽内的节点。 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getHealth" } ' 注：如果节点健康将返回ok，可用于节点的监控指标 getHighestSnapshotSlot 返回节点具有快照的最高插槽信息。 这将找到最高的完整快照槽，以及基于完整快照槽的最高增量快照槽（如果有） curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getHighestSnapshotSlot" } ' getIdentity 返回当前节点的身份公钥 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getIdentity" } ' 注：也就是返回对应RPC 配置的--identity的pubkey getInflationGovernor 返回当前通货膨胀调控器 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getInflationGovernor" } ' getInflationRate 返回当前时期的具体通胀值 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getInflationRate" } ' getInflationReward 返回某个时期地址列表的通胀/权益奖励 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getInflationReward", "params": [ [ "6dmNQ5jwLeLk5REvio1JcMshcbvkYMwy26sJ8pbkvStu", "BGsqMegLpV6n6Ve146sSX2dTjUMj3M92HnU8BbNRMhF2" ], { "epoch": 2 } ] } ' getLargestAccounts 按 Lamport 余额返回前 20 个最大的账户（结果可能缓存长达两小时） curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getLargestAccounts", "params": [] } ' getLatestBlockhash 返回最新的区块哈希 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getLatestBlockhash", "params": [ { "commitment": "processed" } ] } ' 注：当前值用于常规交易发送时的recentBlockhash getLeaderSchedule 返回一个时期的领导者时间表 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getLeaderSchedule", "params": [ null, { "identity": "4Qkev8aNZcqFNSRhQzwyLMFSsi94jHqE8WNVTJzTP99F" } ] } ' getMaxRetransmitSlot 获取重传阶段看到的最大时隙。 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getMaxRetransmitSlot" } ' getMaxShredInsertSlot 获取撕碎插入后看到的最大插槽。 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getMaxShredInsertSlot" } ' getMinimumBalanceForRentExemption 返回免除账户租金所需的最低余额。 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getMinimumBalanceForRentExemption", "params": [ 50 ] } ' getMultipleAccounts 返回公钥列表的帐户信息。 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getMultipleAccounts", "params": [ [ "vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg", "4fYNw3dojWmQ4dXtSGE9epjRGy9pFSx62YypT7avPYvA" ], { "encoding": "base58" } ] } ' getProgramAccounts 返回所提供程序 Pubkey 拥有的所有账户 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getProgramAccounts", "params": [ "4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T", { "filters": [ { "dataSize": 17 }, { "memcmp": { "offset": 4, "bytes": "3Mc6vR" } } ] } ] } ' getRecentPerformanceSamples 返回最近性能样本的列表（按槽位倒序排列）。性能样本每 60 秒采集一次，包括给定时间窗口内发生的事务和槽位数量。 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getRecentPerformanceSamples", "params": [ 2 ] } ' getRecentPrioritizationFees 返回最近区块的优先费用列表。 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getRecentPrioritizationFees", "params": [ ["CxELquR1gPP8wHe33gZ4QxqGB3sZ9RSwsJ2KshVewkFY"] ] } ' getSignaturesForAddress 返回已确认交易的签名，这些交易的列表中包含指定地址accountKeys。按时间倒序返回提供的签名或最近确认的区块的签名 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getSignaturesForAddress", "params": [ "Vote111111111111111111111111111111111111111", { "limit": 1 // 1-1000, 默认1000 } ] } ' getSignatureStatuses 返回签名列表的状态。每个签名必须是 txid，即交易的第一个签名。 除非searchTransactionHistory包含配置参数，否则此方法仅搜索签名的近期状态缓存，其中保留所有活动插槽和MAX_RECENT_BLOCKHASHES根插槽的状态。 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getSignatureStatuses", "params": [ [ "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW" ], { "searchTransactionHistory": true } ] } ' 返回 { "jsonrpc": "2.0", "result": { "context": { "apiVersion": "2.0.18", "slot": 24219064 }, "value": [ { "confirmationStatus": "finalized", // processed->confirmed->finalized "confirmations": null, "err": null, "slot": 24218468, "status": { "Ok": null } } ] }, "id": 1 } getSlot 返回已达到给定或默认承诺级别的槽 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getSlot" } ' 注：默认是返回最近finalized的Slot getSlotLeader 返回当前 slot 领导者 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getSlotLeader" } ' 注：如果定制链做Rollup，那返回的为sequencer的账户地址 getSlotLeaders 返回给定槽范围的槽领导者 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getSlotLeaders", "params": [ 100, // 起始Slot 10 //Limit (1-5000) ] } ' getStakeMinimumDelegation 以 lampors 为单位，返回股权最低授权。 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getStakeMinimumDelegation" } ' 注：如果限制外部节点的加入，可以修改链代码，将最小委托量修改为10000000000000000000 getSupply 返回有关当前供应的信息。 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getSupply" } ' getTokenAccountBalance 返回 SPL 代币账户的代币余额 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getTokenAccountBalance", "params": [ "3tykH6gHjjyqYHBigukDXZaj3K6XupjKxYLMA2Srfbc2" // 对应账户地址->ATA账户 ] } ' 返回 { "jsonrpc": "2.0", "result": { "context": { "apiVersion": "2.1.15", "slot": 370370345 }, "value": { "amount": "100000000000", "decimals": 9, "uiAmount": 100.0, // amount /（10的decimals次方） "uiAmountString": "100" // uiAmount 转字符串 } }, "id": 1 } 注：查询地址为对应账户->对应Token(mint)->ATA账户地址 getTokenAccountsByDelegate 返回已获批准的代表的所有 SPL 代币账户 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getTokenAccountsByDelegate", "params": [ "4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T", { "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, { "encoding": "jsonParsed" } ] } ' getTokenAccountsByOwner 按代币所有者返回所有 SPL 代币账户 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getTokenAccountsByOwner", "params": [ "8TKG1ez28ZYzNgGTein6Pc97yvxWwnHecVL9ZhdZTxd5", // 这里是Keypair原地址 { "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, { "encoding": "jsonParsed" } ] } ' 返回 { "jsonrpc": "2.0", "result": { "context": { "apiVersion": "2.2.3", "slot": 370371798 }, "value": [ { "account": { "data": { "parsed": { "info": { "isNative": false, "mint": "34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT", // 代币标志位 "owner": "8TKG1ez28ZYzNgGTein6Pc97yvxWwnHecVL9ZhdZTxd5", "state": "initialized", "tokenAmount": { "amount": "100000000000", "decimals": 9, "uiAmount": 100.0, "uiAmountString": "100" } }, "type": "account" }, "program": "spl-token", "space": 165 }, "executable": false, "lamports": 2039280, "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "rentEpoch": 18446744073709551615, "space": 165 }, "pubkey": "3tykH6gHjjyqYHBigukDXZaj3K6XupjKxYLMA2Srfbc2" // ATA账户地址 } ] }, "id": 1 } getTokenLargestAccounts 返回特定 SPL 代币类型的 20 个最大余额账户。 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getTokenLargestAccounts", "params": [ "34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT" // 代币mint地址 ] } ' 返回 { "jsonrpc": "2.0", "result": { "context": { "apiVersion": "2.1.15", "slot": 370372669 }, "value": [ { "address": "ChpLTT95FAZNVSeM79C7iun141Lp54dxjoURegsQAtZN", "amount": "100000000000", "decimals": 9, "uiAmount": 100.0, "uiAmountString": "100" }, { "address": "3tykH6gHjjyqYHBigukDXZaj3K6XupjKxYLMA2Srfbc2", "amount": "100000000000", "decimals": 9, "uiAmount": 100.0, "uiAmountString": "100" } ] }, "id": 1 } getTokenSupply 返回 SPL 代币类型的总供应量 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getTokenSupply", "params": [ "34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT" ] } ' 返回 { "jsonrpc": "2.0", "result": { "context": { "apiVersion": "2.1.15", "slot": 370372872 }, "value": { "amount": "200000000000", "decimals": 9, "uiAmount": 200.0, "uiAmountString": "200" } }, "id": 1 } getTransaction 返回已确认交易的交易详情 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getTransaction", "params": [ "5cvy15goDmKA81kaMKk8nuuWuSN3AgCssjTK4qTvm49FyeZPoHXp5DjatqvSoUTDYArjio5PHE2LCuzBss9P7BDK", "json" ] } ' 返回 { "jsonrpc": "2.0", "result": { "blockTime": 1743069997, "meta": { "computeUnitsConsumed": 4648, "err": null, "fee": 5000, "innerInstructions": [], "loadedAddresses": { "readonly": [], "writable": [] }, "logMessages": [ "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]", "Program log: Instruction: MintToChecked", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4498 of 4648 compute units", "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", "Program ComputeBudget111111111111111111111111111111 invoke [1]", "Program ComputeBudget111111111111111111111111111111 success" ], "postBalances": [ 4994414840, 1461600, 2039280, 1, 934087680 ], "postTokenBalances": [ { "accountIndex": 2, "mint": "34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT", "owner": "8TKG1ez28ZYzNgGTein6Pc97yvxWwnHecVL9ZhdZTxd5", "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "uiTokenAmount": { "amount": "100000000000", "decimals": 9, "uiAmount": 100.0, "uiAmountString": "100" } } ], "preBalances": [ 4994419840, 1461600, 2039280, 1, 934087680 ], "preTokenBalances": [ { "accountIndex": 2, "mint": "34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT", "owner": "8TKG1ez28ZYzNgGTein6Pc97yvxWwnHecVL9ZhdZTxd5", "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "uiTokenAmount": { "amount": "0", "decimals": 9, "uiAmount": null, "uiAmountString": "0" } } ], "rewards": [], "status": { "Ok": null } }, "slot": 370161711, "transaction": { "message": { "accountKeys": [ "8TKG1ez28ZYzNgGTein6Pc97yvxWwnHecVL9ZhdZTxd5", "34GbKbRzLMfvpHEFywhe2KyheKMMxab8tQAFj5t2rBYT", "3tykH6gHjjyqYHBigukDXZaj3K6XupjKxYLMA2Srfbc2", "ComputeBudget111111111111111111111111111111", "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" ], "header": { "numReadonlySignedAccounts": 0, "numReadonlyUnsignedAccounts": 2, "numRequiredSignatures": 1 }, "instructions": [ { "accounts": [ 1, 2, 0 ], "data": "ndcLHYJ5qd8F2", "programIdIndex": 4, "stackHeight": null }, { "accounts": [], "data": "F7UDE7", "programIdIndex": 3, "stackHeight": null } ], "recentBlockhash": "BUuLwgC1zPLC1VcQ5xRrKSnDz2vheZaxW82GUJX59CLM" }, "signatures": [ "5cvy15goDmKA81kaMKk8nuuWuSN3AgCssjTK4qTvm49FyeZPoHXp5DjatqvSoUTDYArjio5PHE2LCuzBss9P7BDK" ] } }, "id": 1 } getTransactionCount 返回当前网络从创世以来所有的交易总数 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getTransactionCount" } ' getVersion 返回当前查询RPC节点所运行的Solana 版本 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getVersion" } ' getVoteAccounts 返回当前银行所有投票账户的账户信息和相关股份。 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getVoteAccounts", "params": [ { "votePubkey": "3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw" } ] } ' isBlockhashValid 查询指定块hash是否仍然有效 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 45, "method": "isBlockhashValid", "params": [ "J7rBdM6AecPDEZp8aPq5iPSNKVkU5Q76F3oAV4eW5wsW", { "commitment": "processed" } ] } ' 参考：https://solana.com/zh/docs/rpc/http
在 Solana 中，交易的 confirmationStatus 用于表示交易的确认状态，主要有以下几种状态： 1. finalized 含义 ：这是最高级别的确认状态。当交易达到 finalized 状态时，意味着该交易已经被集群中的大多数验证节点确认，并且不会被回滚。在区块链的共识机制下，这是一个非常安全的状态，表明交易已经被永久记录在区块链上。 应用场景 ：在需要确保交易不可逆转的场景中，如涉及资金转移、重要数据记录等，通常会等待交易达到 finalized 状态。 2. confirmed 含义 ：表示交易已经被集群中的一个验证节点确认。虽然交易处于 confirmed 状态，但仍然存在一定的风险，因为在某些情况下，该交易可能会被回滚。不过，这种情况相对较少发生。 应用场景 ：在一些对交易确认速度要求较高，而对交易最终确定性要求不是特别严格的场景中，可以使用 confirmed 状态。例如，在一些实时性要求较高的游戏或交互场景中，当用户完成操作后，可以快速告知用户交易已 confirmed ，让用户继续后续操作。 3. processed 含义 ：表示交易已经被集群接收并处理，但尚未被任何验证节点确认。这是交易确认过程中的初始状态，意味着交易已经进入了集群的处理流程，但还没有得到足够的确认。 应用场景 ：在开发过程中，当需要快速反馈交易是否已经被集群接收时，可以使用 processed 状态。例如，在用户提交交易后，立即告知用户交易已被接收并正在处理中。 代码示例 在使用 @solana/web3.js 进行交易时，可以指定 confirmationStatus 参数。以下是一个示例代码： const { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction, sendAndConfirmTransaction } = require('@solana/web3.js'); // 连接到 Solana 网络 const connection = new Connection('https://api-devnet.solana.com', 'confirmed'); // 生成发送方和接收方的密钥对 const fromKeypair = Keypair.generate(); const toPublicKey = new PublicKey('...'); // 替换为实际的接收方公钥 // 创建交易 const transaction = new Transaction().add( SystemProgram.transfer({ fromPubkey: fromKeypair.publicKey, toPubkey: toPublicKey, lamports: 1 * LAMPORTS_PER_SOL, }) ); // 发送并确认交易，指定 confirmationStatus 为 'finalized' sendAndConfirmTransaction(connection, transaction, [fromKeypair], { commitment: 'finalized' }) .then((signature) => { console.log(`交易已成功发送，签名: ${signature}`); }) .catch((error) => { console.error('转账过程中出现错误:', error); }); 在上述代码中， commitment 参数指定了交易的确认状态，这里设置为 finalized ，表示等待交易达到最终确认状态。你可以根据实际需求将其设置为 confirmed 或 processed 。
各种 Solana RPC 方法将返回更复杂的响应作为结构化 JSON 对象，并填充特定的键值。 这些 JSON 数据结构中最常见的包括： 交易 内在指示 代币余额 交易 交易与其他区块链上的交易截然不同。请务必查看交易剖析以了解 Solana 上的交易。 JSON 交易的JSON结构定义如下： "transaction": { "message": { "accountKeys": [ "EF3cbGuhJus5mCdGZkVz7GQce7QHbswBhZu6fmK9zkCR", "4LAyP5B5jNyNm7Ar2dG8sNipEiwTMEyCHd1iCHhhXYkY", "11111111111111111111111111111111" ], "header": { "numReadonlySignedAccounts": 0, "numReadonlyUnsignedAccounts": 1, "numRequiredSignatures": 1 }, "instructions": [ { "accounts": [ 0, 1 ], "data": "3Bxs411Dtc7pkFQj", "programIdIndex": 2, "stackHeight": null } ], "recentBlockhash": "6pw7JBwq9tb5GHiBQgVY6RAp5otbouwYvEc1kbbxKFec" }, "signatures": [ "2M8mvwhtxyz3vAokXESVeR9FQ4t9QQxF5ek6ENNBBHVkW5XyZvJVK5MQej5ccwTZH6iWBJJoZ2CcizBs89pvpPBh" ] } message: - 定义交易的内容。 accountKeys: <array[string]>- 交易使用的 base-58 编码公钥列表，包括指令和签名。第一个 message.header.numRequiredSignatures公钥必须签署交易。 header: - 详细说明交易所需的账户类型和签名。 numRequiredSignatures: <number>- 使交易有效所需的签名总数。签名必须与第一个numRequiredSignatures签名相匹配message.accountKeys。 numReadonlySignedAccounts: <number>- 最后 numReadonlySignedAccounts一个签名密钥是只读帐户。程序可以处理在单个 PoH 条目中加载只读帐户的多个交易，但不允许贷记或借记 lamport 或修改帐户数据。针对同一读写帐户的交易将按顺序进行评估。 numReadonlyUnsignedAccounts: <number>- 最后一个 numReadonlyUnsignedAccounts未签名的密钥是只读账户。 recentBlockhash: <string>- 账本中最近一个区块的 Base58 编码哈希值，用于防止交易重复并提供交易生命周期。 instructions: <array[object]> - 将按顺序执行并在一个原子事务中提交的程序指令列表（如果全部成功）。 programIdIndex: <number>- 数组索引，message.accountKeys指示执行该指令的程序账户。 accounts: <array[number]>- 数组中有序索引的列表， message.accountKeys指示要传递给程序的帐户。 data: <string>- 程序输入以 base-58 字符串编码的数据。 addressTableLookups: <array[object]|undefined> - 交易使用的地址表查找列表，用于从链上地址查找表中动态加载地址。如果maxSupportedTransactionVersion 未设置，则为未定义。 accountKey: <string>- 用于地址查找表帐户的 base-58 编码公钥。 writableIndexes: <array[number]>- 用于从查找表加载可写帐户地址的索引列表。 readonlyIndexes: <array[number]>- 用于从查找表加载只读帐户地址的索引列表。 signatures: <array[string]>- 应用于交易的 base-58 编码签名列表。列表的长度始终为 message.header.numRequiredSignatures且不为空。索引处的签名 对应于中i索引处的公钥。第一个用作 交易 ID。i``message.accountKeys JSON 解析 交易的 JSON 解析结构遵循与常规 JSON 格式类似的结构，并附加了帐户和指令信息的解析： "transaction": { "message": { "accountKeys": [ { "pubkey": "EF3cbGuhJus5mCdGZkVz7GQce7QHbswBhZu6fmK9zkCR", "signer": true, "source": "transaction", "writable": true }, { "pubkey": "4LAyP5B5jNyNm7Ar2dG8sNipEiwTMEyCHd1iCHhhXYkY", "signer": false, "source": "transaction", "writable": true }, { "pubkey": "11111111111111111111111111111111", "signer": false, "source": "transaction", "writable": false } ], "instructions": [ { "parsed": { "info": { "destination": "4LAyP5B5jNyNm7Ar2dG8sNipEiwTMEyCHd1iCHhhXYkY", "lamports": 100000000, "source": "EF3cbGuhJus5mCdGZkVz7GQce7QHbswBhZu6fmK9zkCR" }, "type": "transfer" }, "program": "system", "programId": "11111111111111111111111111111111", "stackHeight": null } ], "recentBlockhash": "6pw7JBwq9tb5GHiBQgVY6RAp5otbouwYvEc1kbbxKFec" }, "signatures": [ "2M8mvwhtxyz3vAokXESVeR9FQ4t9QQxF5ek6ENNBBHVkW5XyZvJVK5MQej5ccwTZH6iWBJJoZ2CcizBs89pvpPBh" ] } message: - 定义交易的内容。 accountKeys: <array[object]> - 交易使用的账户信息列表。 pubkey: <string>- 账户的 Base58 编码公钥。 signer: <boolean>- 表明该账户是否是必需的交易签名者。 writable: <boolean>- 指示该帐户是否可写。 source: <string>- 账户来源（交易或查找表）。 recentBlockhash: <string>- 账本中最近一个区块的 Base58 编码哈希值，用于防止交易重复并提供交易生命周期。 instructions: <array[object]> - 已解析的程序指令列表。 program: <string>- 被调用的程序的名称。 programId: <string>- 该程序的 Base58 编码公钥。 stackHeight: <number|null>- 指令的堆栈高度。 parsed: - 特定程序的解析数据。 type: <string>- 指令的类型（例如“转移”）。 info: - 解析特定于程序和指令类型的指令信息。 signatures: <array[string]>- 应用于交易的 Base-58 编码签名列表。 交易状态元数据 { "meta": { "err": null, "fee": 5000, "innerInstructions": [], "logMessages": [], "postBalances": [499998932500, 26858640, 1, 1, 1], "postTokenBalances": [], "preBalances": [499998937500, 26858640, 1, 1, 1], "preTokenBalances": [], "rewards": null, "status": { "Ok": null } } } err: <object|null>- 如果交易失败则返回错误，如果交易成功则返回 null。TransactionError 定义 fee: <u64>- 本次交易收取的费用，以 u64 整数形式 preBalances: <array>- 交易处理前的 u64 账户余额数组 postBalances: <array>- 交易处理后的 u64 账户余额数组 innerInstructions: <array|null>-内部指令列表 或null 此交易期间是否未启用内部指令记录 preTokenBalances: <array|undefined>- 交易处理之前的代币余额列表 ，如果在此交易期间尚未启用代币余额记录，则省略 postTokenBalances: <array|undefined>- 交易处理后的代币余额列表 ，如果在此交易期间尚未启用代币余额记录，则省略 logMessages: <array|null>- 字符串日志消息数组或null如果在此事务期间未启用日志消息记录 rewards: <array|null> - 交易级奖励；包含以下内容的 JSON 对象数组： pubkey: <string>- 收到奖励的账户的公钥，以 base58 编码的字符串形式 lamports: <i64>- 账户记入或扣除的奖励灯数量，以 i64 表示 postBalance: <u64>- 奖励使用后 lampors 账户余额 rewardType: <string|undefined>- 奖励类型：“费用”、“租金”、“投票”、“质押” commission: <u8|undefined>- 奖励到账时投票账户佣金，仅适用于投票和质押奖励 已弃用： status: - 交易状态 "Ok": <null>- 交易成功 "Err": <ERR>- 交易失败，出现 TransactionError loadedAddresses: <object|undefined> - 从地址查找表加载交易地址。如果 axSupportedTransactionVersion 请求参数中未设置，或者 jsonParsed 请求参数中设置了编码，则未定义。 writable: <array[string]>- 可写加载账户的 Base58 编码地址的有序列表 readonly: <array[string]>- 只读加载帐户的 Base58 编码地址的有序列表 returnData: <object|undefined> - 交易中指令生成的最新返回数据，包含以下字段： programId: <string>- 生成返回数据的程序，以 base-58 编码的公钥形式 data: <[string, encoding]>- 返回数据本身，以 base64 编码的二进制数据 computeUnitsConsumed: <u64|undefined>-交易消耗的计算单元数量 version: <"legacy"|number|undefined>- 交易版本。如果 maxSupportedTransactionVersion请求参数中未设置，则为未定义。 signatures: <array>- 如果请求“签名”以获取交易详细信息，则存在；签名字符串数组，对应于块中的交易顺序 内部指示 Solana 运行时记录在交易处理期间调用的跨程序指令，并使这些指令可用，从而提高每个交易指令在链上执行内容的透明度。调用的指令按原始交易指令分组，并按处理顺序列出。 内部指令的 JSON 结构定义为以下结构的对象列表 "innerInstructions": [ { "index": 0, "instructions": [ { "accounts": [ 0, 1, 2 ], "data": "WPNHsFPyEMr", "programIdIndex": 3, "stackHeight": 2 }, { "accounts": [ 0, 1 ], "data": "11111dBUPbGETd4QtNMQVg8HqgcZtKy6DcJm6R4TZufJkuhkDS47VsauCCGhLf2xrm5BQ", "programIdIndex": 2, "stackHeight": 3 } ] } ] 参考交易： "transaction": { "message": { "accountKeys": [ "4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1", "Bpo7aaM9kqfCjM6JgZCSdev7HsaUFj51mBPPEhQcDpUR", "11111111111111111111111111111111", "8HupNBr7SBhBLcBsLhbtes3tCarBm6Bvpqp5AfVjHuj8", "GENmb1D59wqCKRwujq4PJ8461EccQ5srLHrXyXp4HMTH" ], "header": { "numReadonlySignedAccounts": 0, "numReadonlyUnsignedAccounts": 3, "numRequiredSignatures": 2 }, "instructions": [ { "accounts": [ 0, 1, 2, 3 ], "data": "H2ot5wbZsmL", "programIdIndex": 4, "stackHeight": null } ], "recentBlockhash": "28CroH2jyCaCFF6ssyUK989zBZY6dBxnUNU9A4oPUbER" }, "signatures": [ "4i4EuRQ1sNzKWEBYwg28VAMkQbaAeHyRRwu1tQRksowtQhGRJtgaHXpDAhBfpYZnVodGoQYwiUiB5yBRDoWbZ7VH", "2dipFcFF4CvwtbCFbRdctQmyzAYcq8RWrLryZErbKPhnriCJ6wDmKfJoSJfDjFNzUEcJDKkfasS2pcjvGEUjdYN6" ] } index: number- 内部指令来源的交易指令索引 instructions: <array[object]>- 在单个事务指令期间调用的内部程序指令的有序列表。 programIdIndex: <number>- 数组索引，message.accountKeys指示执行该指令的程序账户。 accounts: <array[number]>- 数组中有序索引的列表， message.accountKeys指示要传递给程序的帐户。 data: <string>- 程序输入以 base-58 字符串编码的数据。 代币余额 "postTokenBalances": [ { "accountIndex": 1, "mint": "6HvU8PbqP3nZLkFF59rr2zkTHqetPLgb6NnxKZLHQxNp", "owner": "DKypunNAGLPGBj3SocY8fF4ZrnDNVTf6QcUyW4trvkB", "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", "uiTokenAmount": { "amount": "0", "decimals": 2, "uiAmount": null, "uiAmountString": "0" } }, { "accountIndex": 2, "mint": "6HvU8PbqP3nZLkFF59rr2zkTHqetPLgb6NnxKZLHQxNp", "owner": "8xm9beCpBH7SgqRz1mKua7KJF52whAVCiDEV1qREGHNV", "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", "uiTokenAmount": { "amount": "100", "decimals": 2, "uiAmount": 1.0, "uiAmountString": "1" } } ], "preTokenBalances": [ { "accountIndex": 1, "mint": "6HvU8PbqP3nZLkFF59rr2zkTHqetPLgb6NnxKZLHQxNp", "owner": "DKypunNAGLPGBj3SocY8fF4ZrnDNVTf6QcUyW4trvkB", "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", "uiTokenAmount": { "amount": "100", "decimals": 2, "uiAmount": 1.0, "uiAmountString": "1" } }, { "accountIndex": 2, "mint": "6HvU8PbqP3nZLkFF59rr2zkTHqetPLgb6NnxKZLHQxNp", "owner": "8xm9beCpBH7SgqRz1mKua7KJF52whAVCiDEV1qREGHNV", "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", "uiTokenAmount": { "amount": "0", "decimals": 2, "uiAmount": null, "uiAmountString": "0" } } ] 参考交易： "transaction": { "message": { "accountKeys": [ { "pubkey": "DKypunNAGLPGBj3SocY8fF4ZrnDNVTf6QcUyW4trvkB", "signer": true, "source": "transaction", "writable": true }, { "pubkey": "39nzuQ2WYHf231DJRPt1TLfaXSWXEKYGcqP3NQf6zK7G", "signer": false, "source": "transaction", "writable": true }, { "pubkey": "DtCPWGmvCTov7CNmNTx8AFe3SEFSxgy265kZawv8SVL3", "signer": false, "source": "transaction", "writable": true }, { "pubkey": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", "signer": false, "source": "transaction", "writable": false } ], "addressTableLookups": [], "instructions": [ { "parsed": { "info": { "amount": "100", "authority": "DKypunNAGLPGBj3SocY8fF4ZrnDNVTf6QcUyW4trvkB", "destination": "DtCPWGmvCTov7CNmNTx8AFe3SEFSxgy265kZawv8SVL3", "source": "39nzuQ2WYHf231DJRPt1TLfaXSWXEKYGcqP3NQf6zK7G" }, "type": "transfer" }, "program": "spl-token", "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", "stackHeight": null } ], "recentBlockhash": "42mjf871LtzDK8NVZUAht1xBoCvNMagQGSM7BnFWZD6M" }, "signatures": [ "5ZQqsF4tTFJDR5vuNJxejtw2GMc8KEtnPXnQjwhGzAtdbPTKtrLfPkFAbBTyPjZSVB3CbR5BiP5S8zAfZNtuwh88" ] } 代币余额的 JSON 结构定义为以下结构的对象列表： accountIndex: <number>- 提供代币余额的账户索引。 mint: <string>- 代币铸币厂的公钥。 owner: <string|undefined>- 代币余额所有者的公钥。 programId: <string|undefined>- 拥有该账户的Token程序的公钥。 uiTokenAmount: amount: <string>- 代币的原始数量为字符串，忽略小数。 decimals: <number>- 为代币铸造配置的小数位数。 uiAmount: <number|null>- 代币数量为浮点数，包含小数。已弃用 uiAmountString: <string>- Token数量为字符串，保留小数位。 原文：https://solana.com/zh/docs/rpc/json-structures
安装 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其命令输出的配对传达给将交易广播到集群的一方。广播方随后在修改模板命令后运行该命令，如下所示： 用其密钥对替换任何相应的公钥（在此示例中--fee-payer ... 为）--nonce-authority ... 删除--sign-only参数，如果是mint子命令，则--mint-decimals ...删除将从集群中查询的参数 --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 代币时，应首先为其关联的代币账户注资，以便： 最大限度地提高与其他钱包实现的互操作性 避免将创建相关代币账户的成本转嫁给第一个发送者 钱包应提供允许用户“添加代币”的用户界面。用户选择代币类型，系统会显示添加代币需要花费多少 SOL 的信息。 确认后，钱包将创建如此处所述的相关代币 类型。 “空投活动”工作流程示例 对于每个收件人的钱包地址，发送包含以下内容的交易： 代表接收者创建相关的令牌账户。 使用TokenInstruction::Transfer完成转移 关联代币账户所有权 ⚠️钱包绝不应该将关联代币账户的权限TokenInstruction::SetAuthority设置 为另一个地址。AccountOwner 辅助代币账户 可以随时将现有 SPL 代币帐户的所有权分配给用户。实现此目的的一种方法是使用 spl-token authorize <TOKEN_ADDRESS> owner <USER_ADDRESS>命令。钱包应该准备好妥善管理他们自己没有为用户创建的代币帐户。 在钱包之间转移代币 在钱包之间转移代币的首选方法是转移到接收者的关联代币账户。 收件人必须向发件人提供其主钱包地址。然后，发件人： 为接收者获取关联的代币账户 通过 RPC 获取收件人的关联代币账户并检查其是否存在 如果收件人的关联代币账户尚不存在，则发送方钱包应按 此处所述创建收件人的关联代币账户。发送方的钱包可以选择通知用户，由于创建了账户，转账将需要比平时更多的 SOL。但是，此时选择不支持创建收件人关联代币账户的钱包应向用户显示一条消息，其中包含足够的信息，以找到解决方法来实现他们的目标 使用TokenInstruction::Transfer完成转移 发送者的钱包不得要求接收者的主钱包地址保留余额才允许转账。 代币详细信息注册表 目前，Token Mint 注册中心有几种解决方案： 钱包或 dapp 中的硬编码地址 Metaplex 令牌元数据。更多信息请参阅 令牌元数据文档 已弃用的 token-list repo 中有 关于创建自己的元数据的说明 分散式解决方案正在进行中。 垃圾收集辅助代币账户 钱包应尽快将辅助代币账户中的资金转入用户关联的代币账户，从而清空辅助代币账户。此举有两个目的： 如果用户是辅助账户的关闭权限，钱包可以通过关闭账户为用户回收SOL。 如果辅助账户由第三方提供资金，一旦账户清空，第三方可能会关闭该账户并收回 SOL。 垃圾回收辅助代币账户的一个自然时机是用户下次发送代币时。执行此操作的附加指令可以添加到现有交易中，并且不需要额外费用。 清理伪步骤： 对于所有非空的辅助代币账户，添加一条 TokenInstruction::Transfer指令，将全部代币金额转移到用户关联的代币账户中。 对于所有空的辅助令牌账户，其中用户是关闭权限，添加一条TokenInstruction::CloseAccount指令 如果添加一个或多个清理指令导致事务超出允许的最大事务大小，请删除这些额外的清理指令。它们可以在下一次发送操作期间被清理。 该spl-token gc命令提供了此清理过程的示例实现。 代币归属 目前有两种解决方案可用于归属 SPL 代币： 1）Bonfida 代币归属 此程序允许您锁定任意 SPL 代币，并按照确定的解锁计划释放锁定的代币。一个由一个和一个代币unlock schedule组成，在初始化归属合约时，创建者可以传递一个任意大小的数组，从而使合约创建者可以完全控制代币随时间如何解锁。unix timestamp``amount``unlock schedule 解锁的工作原理是推动合约上的无权限曲柄，将代币移动到预先指定的地址。当前接收者密钥的所有者可以修改授权合约的接收者地址，这意味着授权合约锁定的代币可以进行交易。 代码：https://github.com/Bonfida/token-vesting 界面：https://vesting.bonfida.com/#/ 审计：审计由 Kudelski 进行，报告可在此处找到 2）Streamflow 时间锁 使用基于时间的锁定和托管账户，可以创建、提取、取消和转移代币归属合约。合约默认可由创建者取消，并可由接收者转移。 归属合约创建者在创建时可以选择多种选项，例如： SPL 代币及归属金额 接受者 确切的开始和结束日期 （可选）悬崖日期和金额 （可选）发布频率 即将推出： 合同是否可以由创作者/接收者转让 合同是否可以由创作者/接收者取消 主題/記錄 资源： 审计：报告可在这里和这里找到。 带有 UI 的应用程序：https://app.streamflow.finance/vesting JS SDK：https://npmjs.com/@streamflow/timelock（[来源](https://github.com/StreamFlow-Finance/timelock/tree/master/packages/timelock)） Rust SDK：https://crates.io/crates/streamflow-timelock（[来源](https://github.com/streamflow-finance/timelock-crate)） 程序代码：https://github.com/streamflow-finance/timelock 参考文档 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
十分钟开始使用多签工具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 创建 tsconfig.json: { "compilerOptions": { "module": "commonjs", "target": "es6", "esModuleInterop": true } } 创建 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" } } 创建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
本文介绍了EVM开发者如何转向Solana平台，包括Solana的架构、技术优势、开发工具及账户模型的不同，强调程序的无状态特性以及数据存储的外部化。同时，文章比较了Ethereum和Solana的交易处理模型、费用机制及开发工具，帮助开发者顺利过渡。 概述 以太坊虚拟机（EVM）多年来一直是去中心化应用（dApp）开发的基础，但 Solana 的创新架构提供了诸如高吞吐量、低延迟和成本效率等独特优势。本指南旨在为希望在 Solana 上构建应用的 EVM 开发者设计。通过利用你对以太坊和 EVM 的现有知识，本指南将帮助你理解 Solana 的关键概念、工具和工作流程。 在本指南结束时，你将对 Solana 的架构有一个清晰的理解，并了解其与以太坊的不同之处。你将学习账户、执行模型和交易处理等关键概念如何与 EVM 进行比较，帮助你将开发思维从基于 Solidity 的智能合约转变为 Solana 的无状态 Rust 程序。 你将做什么 理解 EVM 和 SVM 之间的关键差异，包括执行模型、账户结构和费用机制 学习在 Solana 上如何存储和访问数据，与以太坊的存储模型进行比较 探索两个网络的交易处理模型，以理解执行效率 将你的思维方式从基于 Solidity 的智能合约开发转向基于 Rust 的 Solana 程序及其基于账户的架构 你需要什么 以太坊智能合约开发（Solidity、Hardhat、Foundry）经验 基本 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 在以太坊中，wei是ETH的最小单位，其中1 ETH = 10¹⁸ wei。在 Solana 中，lamports是SOL的最小单位，其中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）模型在数据账户中存储其数据。开发人员可以通过将程序设置为账户所有者并使用种子生成地址来生成这些数据账户。 从下图可以看出，一个账户有五个字段：lamports、owner、executable、rent_epoch 和 data。 图片来源 程序衍生地址（PDAs） 程序衍生地址（PDA）是一种特殊类型的账户地址，程序可以控制而无需私钥。它们允许程序签名。如果 PDAs 是基于其程序 ID 生成的，程序可以用于签署。此外，它们允许确定性数据存储，以确保相同的种子值将始终生成相同的 PDA。 PDA 可用作链上账户的地址（唯一标识符），提供方便存储、映射和提取程序状态的方法。 来源 开发人员可以使用种子和 bump 生成 PDA，确保它们不会与用户控制的地址发生冲突。bump 是一个额外值（介于 0 和 255 之间），加到种子上以生成唯一地址。 图片来源 例如，假设我们的程序有一个 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 测试验证器、LiteSVM、Luzid 类似于 Hardhat 节点的本地区块链，用于测试程序和账户。 Hardhat/Foundry Program-test、BankRun.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 Codama、Shanks、Anchor 标准化的 IDL 和客户端生成工具，替代以太坊的 ABI，进行程序交互。 Etherscan SolanaFM、Solana Explorer、Solscan 用于检查账户和交易的区块链浏览器，类似于 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 程序拥有的账户中。 程序是默认可升级的（但可以锁定以实现不可变性）。 数据存储：映射与账户 以太坊：使用映射存储数据 Solidity 允许映射（例如，mapping(address => uint256) balances;）在合约内部存储数据。 Solana：使用账户而不是映射 Solana 中没有直接等同于映射的概念。相反，PDAs（程序衍生地址）充当存储结构化数据的确定性账户。 示例：而不是将 Token 余额映射到地址，你创建一个 PDA 账户来存储用户余额。 想问题时，不要以映射为思路，而是要以创建可以存储每个用户或实体所需数据的单独账户为思路。 函数执行：消息发送者 VS 显式账户 以太坊：msg.sender 和 tx.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 上，你可以通过浏览器扩展（例如 Phantom、Solflare）或通过命令行（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 打开 Phantom 并找到导出你的恢复密语短语的选项。 复制短语（一个单词序列）。 运行以下命令： solana-keygen recover 'prompt://?key=0/0' -o my-wallet.json 在提示时输入你的恢复密语短语。 密钥对将导入到 Solana CLI。 将 Solana CLI 钱包导入 Phantom 在文本编辑器中打开 id.json 文件。 复制文件的内容（格式像是 [12, 34, 56, ...]）。 在 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，以下是一些涵盖基础知识的基础指南： 发送你的首个交易 创建代币 转移SPL代币 编写你的第一个Anchor程序 如何使用Solana Memo程序 如何使用程序派生地址 结论 从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内容 Solana文档 - Solana的技术文档。 Solana指南 - 为Solana开发者提供的所有指南。 Solana开发入门 - Solana开发基础的免费课程。 Solana基础概念参考指南 – Solana开发者的关键概念和术语。 Solana钱包入门 – 找一个Solana钱包开始使用。 官方文档 & 指南 Solana官方文档 – 官方技术文档。 Solana Cookbook – 适合开发者的Solana指南。 Anchor框架 – 使用简化的Solana智能合约框架。 探索者 & 工具 Solana探索者 – 查看交易和账户。 SolanaFM – 高级区块浏览器。 Solana Playground – 用于编写和测试Solana程序的网页IDE。 社区 & 学习 Solana Stack Exchange – 提问技术问题。 Solana GitHub – 探索核心Solana代码库。 Anza GitHub - Agave客户端和其他Solana工具（以前是Solana Labs）。 转载：https://learnblockchain.cn/article/13189
跨程序调用 (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); } } Cpi 在 CpiContext 中字面上的意思是“跨程序调用”。 调用除系统程序外的其他程序的公共函数的工作流程并没有太大的不同——我们将在本教程中教授这一点。 本教程只关注如何调用另一个使用 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
Solana基础 - 在链上读取另一个锚点程序账户数据
本文详细介绍了在Solana链上程序中如何读取不属于自己的账户数据，通过创建data_holder和data_reader两个程序，展示了如何初始化并读取PDA中的数据，并探讨了Anchor框架下的数据反序列化机制及其限制。 在 Solidity 中，读取另一个合约的存储需要调用 view 函数或者存储变量是公共的。在 Solana 中，离线客户端可以直接读取存储账户。这个教程展示了一个链上 Solana 程序如何读取它不拥有的账户中的数据。 我们将设置两个程序： data_holder 和 data_reader。 data_holder 将初始化并拥有一个 PDA，其数据将被 data_reader 读取。 设置存储数据的 data_holder 程序: Shell 1 以下代码是一个基本的 Solana 程序，它初始化账户 Storage，其中包含 u64 字段 x，并在初始化时将值 9 存储在其中： Typescript 代码： import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { DataHolder } from "../target/types/data_holder"; describe("data-holder", () => { anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.DataHolder as Program<DataHolder>; it("Is initialized!", async () => { const seeds = []; const [storage, _bump] = anchor.web3.PublicKey.findProgramAddressSync( seeds, program.programId ); await program.methods .initialize() .accounts({ storage: storage }) .rpc(); let storageStruct = await program.account.storage.fetch( storage ); console.log("The value of x is: ",storageStruct.x.toString()); console.log("Storage account address: ", storage.toBase58()); }); }); 测试将打印出 PDA 的地址，我们稍后会提到这个地址： 读取器 为了让 data_reader 读取另一个账户，必须通过 Context 结构将该账户的公共密钥作为交易的一部分传递。这与传递任何其他类型的账户没有区别。 账户中的数据以序列化字节的形式存储。 为了反序列化账户，data_reader 程序需要读取它的 Rust 结构定义。我们需要以下账户定义，并且它与 data_holder 中的 Storage 结构完全相同： #[account] pub struct Storage { x: u64, } 这个结构与 data_reader 中的结构完全相同——连名称也必须相同（稍后我们会介绍为什么）。读取账户的代码在以下两行中： let mut data_slice: &[u8] = &data_account.data.borrow(); let data_struct: Storage = AccountDeserialize::try_deserialize( &mut data_slice, )?; data_slice 是账户中数据的原始字节。如果你运行 solana account <pda address>（使用我们在部署 data_holder 时生成的 PDA 地址），你可以在那里看到数据，包括我们储存在红框中的数字 9： 黄框中前 8 个字节是账户鉴别符，稍后我们将对此进行描述。 反序列化发生在此步骤： let data_struct: Storage = AccountDeserialize::try_deserialize( &mut data_slice, )?; 在这里传递类型 Storage（我们上面定义的结构），告诉 Solana 如何（尝试）反序列化数据。 现在让我们在新文件夹中创建一个单独的 anchor 项目 anchor new data_reader。 完整的 Rust 代码如下： use anchor_lang::prelude::*; declare_id!("HjJ1Rqsth5uxA6HKNGy8VVRvwK4W7aFgmQsss7UxePBw"); #[program] pub mod data_reader { use super::*; pub fn read_other_data( ctx: Context<ReadOtherData>, ) -> Result<()> { let data_account = &ctx.accounts.other_data; if data_account.data_is_empty() { return err!(MyError::NoData); } let mut data_slice: &[u8] = &data_account.data.borrow(); let data_struct: Storage = AccountDeserialize::try_deserialize( &mut data_slice, )?; msg!("The value of x is: {}", data_struct.x); Ok(()) } } #[error_code] pub enum MyError { #[msg("No data")] NoData, } #[derive(Accounts)] pub struct ReadOtherData<'info> { /// CHECK: We do not own this account so // we must be very cautious with how we // use the data other_data: UncheckedAccount<'info>, } #[account] pub struct Storage { x: u64, } 以下是要运行的测试代码。确保在下面的代码中更改 PDA 的地址： import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { DataReader } from "../target/types/data_reader"; describe("data-reader", () => { anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace .DataReader as Program<DataReader>; it("Is initialized!", async () => { // CHANGE THIS TO THE ADDRESS OF THE PDA OF // DATA ACCOUNT HOLDER const otherStorageAddress ="HRGqGCLXxLryZav2SeKJKqBWYs8Ne7ppJxf3MLM3Y71E"; const pub_key_other_storage = new anchor.web3.PublicKey( otherStorageAddress ); const tx = await program.methods .readOtherData() .accounts({ otherData: pub_key_other_storage }) .rpc(); }); }); 要测试读取另一个账户的数据： 在后台运行 solana-test-validator 启动 data_holder 测试。 复制并粘贴 Storage 账户打印的公共密钥。 将该公共密钥放入 data_reader 测试的 otherStorageAddress 中。 在另一个 shell 中运行 Solana 日志。 运行 data_reader 的测试以读取数据。 以下内容应在 Solana 日志中可见： 如果我们不给结构体相同的名称，会发生什么？ 如果你将 data_reader 中的 Storage 结构改为其他名称，如 Storage2 并尝试读取该账户，则会发生以下错误： Anchor 计算的账户鉴别符是结构名称的 sha256 的前八个字节。账户鉴别符不依赖于结构中的变量。 当 Anchor 读取账户时，它检查前八个字节（账户鉴别符）以查看它是否与它在本地用于反序列化数据的结构定义的账户鉴别符匹配。如果它们不匹配，Anchor 将不会反序列化数据。 检查账户鉴别符是防止客户端错误地传入错误账户或数据格式与 Anchor 预期不符的账户数据的一种保障。 反序列化不会因解析更大结构而回退 Anchor 仅 检查账户鉴别符是否匹配——它不会验证被读取的账户内部的字段。 案例 1: Anchor 不检查结构字段名称是否匹配 让我们将 data_reader 中的 Storage 结构中的 x 字段改为 y，保持 data_holder 中的 Storage 结构不变： // data_reader #[account] pub struct Storage { y: u64, } 我们还需要将日志行更改如下： msg!("The value of y is: {}", data_struct.y); 当我们重新运行测试时，它成功读取了数据： Program log: Instruction: ReadOtherData Program log: The value of y is: 9 案例 2: Anchor 不检查数据类型 现在让我们将 y 在 data_reader 中的 Storage 结构的数据类型更改为 u32，尽管原始结构是 u64。 // data_reader #[account] pub struct Storage { y: u32, } 当我们运行测试时，Anchor 仍然成功解析账户数据。 Program log: Instruction: ReadOtherData Program log: The value of y using u32 is: 9 之所以“成功”，是因为数据的布局如下： 7 里的 9 位于前几个字节中——u32 将在前 4 个字节中查找数据，因此它能够“看到”9。 当然，如果我们要存储 u32 无法容纳的值，例如 $2^{32}$，那么我们的读取程序将打印错误的数字。 练习 : 重置验证器并重新部署 data_holder，值设置为 $2^{32}$。在 Rust 中幂运算的方式是 let result = u64::pow(base, exponent)。例如，let result = u64::pow(2, 32); 查看 data_reader 记录了什么值。 案例 3: 解析的字段数据超出存储 存储账户的大小为 16 字节。它存储 8 字节给账户鉴别符，以及 8 字节给 u64 变量。如果我们尝试读取比实际大更多的数据，例如通过定义一个需要超过 16 字节来存储的结构，反序列化将失败： #[account] pub struct Storage { y: u64, z: u64, } 上述结构需要 16 字节来存储 y 和 z，但还需要额外的 8 字节来保存账户鉴别符，使得账户大小达到 24 字节。 解析 Anchor 账户数据总结 在从外部账户读取数据时，Anchor 将检查账户鉴别符是否匹配，以及账户中是否有足够的数据可反序列化为用作 try_deserialize 类型的结构： let data_struct: Storage = AccountDeserialize::try_deserialize( &mut data_slice, )?; Anchor 不检查变量的名称或长度。 在底层，Anchor 不存储任何如何解释账户中数据的元数据。它只是存储变量的字节，按顺序存储。 并非所有数据账户都遵循 Anchor 的约定 Solana 不要求使用账户鉴别符。用原始 Rust 编写的 Solana 程序——没有 Anchor 框架——可能以与 Anchor 的序列化方法（即 AccountDeserialize::try_deserialize）不直接兼容的方式存储数据。要反序列化非 Anchor 数据，开发者必须提前知道使用的序列化方法——在 Solana 生态系统中并没有强制的通用约定。 读取任意账户数据时要小心 Solana 程序默认是可升级的。它们如何在帐户中存储数据的方式可能随时改变，这可能会破坏正在读取它们的程序。 接受来自任意账户的数据是危险的——在读取其数据之前，通常应检查该账户是否由受信任的程序拥有。 转载：https://learnblockchain.cn/article/11409
本文详细介绍了 Solana Anchor 框架中的 [derive(Accounts)] 宏，解释了 Solana 并行交易处理机制及其账户访问控制的重要性，并深入探讨了 Account、UncheckedAccount、Signer 和 Program 四种账户类型的使用场景和实现细节。 Anchor中的#[derive(Accounts)]：不同类型的账户 #[derive(Accounts)]在Solana Anchor中是一个类似属性的宏，用于包含所有在执行期间将被函数访问的账户的引用。 在Solana中，交易将要访问的每个账户必须提前指定 Solana之所以如此快速的一个原因是它以并行的方式执行交易。也就是说，如果Alice和Bob都想进行一笔交易，Solana将尝试同时处理他们的交易。然而，如果他们的交易因访问同一存储而发生冲突，就会出现问题。例如，假设Alice和Bob都在尝试写入同一账户。显然，他们的交易不能并行执行。 为了让Solana知道Alice和Bob的交易不能并行处理，Alice和Bob都必须提前指定所有他们的交易将更新的账户。 由于Alice和Bob都指定了一个（存储）账户，Solana运行时可以推断出两个交易之间存在冲突。必须选择一个（推测是支付了更高优先级费用的那个），另一个最终将失败。 这就是为什么每个函数都有自己单独的#[derive(Accounts)]结构体。结构体中的每个字段都是程序在执行期间打算（但并不要求）访问的账户。 一些以太坊开发者可能会注意到这一要求与EIP 2930访问列表交易的相似性。 账户的类型向Anchor信号你打算如何与该账户交互。 最常用的账户类型：Account、Unchecked Account、System Program和Signer 在我们初始化存储的代码中，我们看到了三种不同的“类型”账户： Account Signer Program 这里是代码片段： 当我们读取账户余额时，我们看到了第四种类型： UncheckedAccount 这里是我们使用的代码： 我们用绿色框标出的每个项目都是通过文件顶部的anchor_lang::prelude::*;引入的。 Account、UncheckedAccount、Signer和Program的目的是在继续之前对传入的账户进行某种检查，并且还提供与这些账户交互的函数。 我们将在接下来的部分进一步解释这四种类型。 Account Account类型会检查被加载的账户的所有者是否确实被程序拥有。如果所有者不匹配，则不会加载。这作为一种重要的安全措施，以防止意外读取程序未创建的数据。 在以下示例中，我们创建了一个密钥对账户，并尝试将其传递给foo。因为该账户不属于程序，所以交易失败。 Rust： use anchor_lang::prelude::*; declare_id!("ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs"); ##[program] pub mod account_types { use super::*; pub fn foo(ctx: Context<Foo>) -> Result<()> { // 我们不对账户SomeAccount做任何操作 Ok(()) } } ##[derive(Accounts)] pub struct Foo<'info> { some_account: Account<'info, SomeAccount>, } ##[account] pub struct SomeAccount {} Typescript: import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { AccountTypes } from "../target/types/account_types"; describe("account_types", () => { async function airdropSol(publicKey, amount) { let airdropTx = await anchor .getProvider() .connection.requestAirdrop( publicKey, amount * anchor.web3.LAMPORTS_PER_SOL ); await confirmTransaction(airdropTx); } async function confirmTransaction(tx) { const latestBlockHash = await anchor .getProvider() .connection.getLatestBlockhash(); await anchor .getProvider() .connection.confirmTransaction({ blockhash: latestBlockHash.blockhash, lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, signature: tx, }); } // 配置客户端以使用本地区块链 anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.AccountTypes as Program<AccountTypes>; it("账户拥有者错误", async () => { const newKeypair = anchor.web3.Keypair.generate(); await airdropSol(newKeypair.publicKey, 10); await program.methods .foo() .accounts({someAccount: newKeypair .publicKey}).rpc(); }); }); 这是执行测试后的输出： 如果我们向Account添加一个init宏，那么它将尝试将所有权从系统程序转移到该程序。然而，上面的代码没有init宏。 有关Account类型的更多信息可以在文档中找到：https://docs.rs/anchor-lang/latest/anchor_lang/accounts/account/struct.Account.html UncheckedAccount或AccountInfo UncheckedAccount是AccountInfo的别名。它不检查所有权，因此必须小心，因为它将接受任意账户。 这是使用UncheckedAccount读取一个不属于它的账户数据的示例。 use anchor_lang::prelude::*; declare_id!("ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs"); ##[program] pub mod account_types { use super::*; pub fn foo(ctx: Context<Foo>) -> Result<()> { let data = &ctx.accounts.some_account.try_borrow_data()?; msg!("{:?}", data); Ok(()) } } ##[derive(Accounts)] pub struct Foo<'info> { /// 检查：我们只是打印数据 some_account: AccountInfo<'info>, } 这是我们的Typescript代码。请注意，我们直接调用系统程序来创建密钥对账户，以便我们可以分配16字节的数据。 import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { AccountTypes } from "../target/types/account_types"; describe("account_types", () => { const wallet = anchor.workspace.AccountTypes.provider.wallet; // 配置客户端以使用本地区块链 anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.AccountTypes as Program<AccountTypes>; it("使用账户信息加载账户", async () => { // 创建一个不属于程序的账户 const newKeypair = anchor.web3.Keypair.generate(); const tx = new anchor.web3.Transaction().add( anchor.web3.SystemProgram.createAccount({ fromPubkey: wallet.publicKey, newAccountPubkey: newKeypair.publicKey, space: 16, lamports: await anchor .getProvider() .connection .getMinimumBalanceForRentExemption(32), programId: program.programId, }) ); await anchor.web3.sendAndConfirmTransaction( anchor.getProvider().connection, tx, [wallet.payer, newKeypair] ); // 读取账户中的数据 await program.methods .foo() .accounts({ someAccount: newKeypair.publicKey }) .rpc(); }); }); 程序运行后，我们可以看到它打印了账户中的数据，该数据包含16个零字节： 在槽14298中执行的交易： 签名：64fv6NqYB4tji9UfLpH8PgFDY1QV4vbMovrnnpw3271vStg7J5g1z1bm9YbE8Lobzozkc6y2YzLdgMjGdftCGKqv 状态：成功 日志消息： 程序ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs调用[1] 程序日志：指令：Foo 程序日志：[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 程序ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs消耗5334的200000计算单位 程序ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs成功 当我们传入任意地址时，我们需要使用这种账户类型，但在使用数据时要非常小心，因为黑客可能能够在账户中构造恶意数据，然后将其传递给Solana程序。 Signer 该类型将检查Signer账户是否签署了交易；它检查签名是否与账户的公钥匹配。 由于签名者也是一个账户，你可以读取签名者的余额或存储在账户中的数据（如果有的话），尽管它的主要目的是验证签名。 根据文档<https://docs.rs/anchor-lang/latest/anchor_lang/accounts/signer/struct.Signer.html%3E，`Signer`是一种验证账户签署了交易的类型。不会进行其他所有权或类型检查。如果使用了这个，便不应尝试访问底层账户数据。 Rust示例： use anchor_lang::prelude::*; declare_id!("ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs"); ##[program] pub mod account_types { use super::*; pub fn hello(ctx: Context<Hello>) -> Result<()> { let lamports = ctx.accounts.signer.lamports(); let address = &ctx.accounts .signer .signer_key().unwrap(); msg!( "你好 {:?} 你有 {} lamports", address, lamports ); Ok(()) } } ##[derive(Accounts)] pub struct Hello<'info> { pub signer: Signer<'info>, } Typescript： import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { AccountTypes } from "../target/types/account_types"; describe("account_types", () => { anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.AccountTypes as Program<AccountTypes>; it("账户拥有者错误", async () => { await program.methods.hello().rpc(); }); }); 这里是程序的输出： 在槽11184中执行的交易： 签名：4xipobKHHp7a3N4durXN4YPGUesDAJNg7wsatBemdJAm7U1dXYG3gveLwnuY39iCTEZvaj6nnAViVJwDS8124uJJ 状态：成功 日志消息： 程序ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs调用[1] 程序日志：指令：Hello 程序日志：你好5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj你有499999994602666000 lamports 程序ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs消耗13096的200000计算单位 程序ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs成功 Program 这个应该是不言自明的。它向Anchor信号该账户是一个可执行账户，即一个程序，你可以向其发出跨程序调用。我们一直在使用的就是系统程序，稍后我们将使用我们自己的程序。 转载：https://learnblockchain.cn/article/11419