本教程介绍了 Aptos SDK 以及如何生成、提交和验证提交给 Aptos 区块链的交易。我们将运行 transfer-coin
的示例。
第 1 步:选择一个 SDK
第 2 步:运行示例代码
克隆 aptos-core
仓库:
Copy git clone git@github.com:aptos-labs/aptos-core.git ~/aptos-core
Typescript
进入到 Typescript SDK 示例的路径下
Copy cd ~/aptos-core/ecosystem/typescript/sdk/examples/typescript
安装必要的依赖:
运行 transfer_coin
示例代码
Copy yarn run transfer_coin
Python
进入到 Python SDK 示例的路径下:
Copy cd ~/aptos-core/ecosystem/python/sdk
安装必要的依赖:
Copy curl -sSL <https://install.python-poetry.org> | python3
poetry update
运行 transfer-coin
示例代码
Copy poetry run python -m examples.transfer-coin
Rust
进入到 Rust SDK 示例的路径下:
运行 transfer-coin
示例代码
Copy cargo run --example transfer-coin:
第 3 步:程序输出解析
在你运行上述示例中的命令后,将会出现类似以下的输出:
Copy === Addresses ===
Alice: 0x0baec07bfc42f8018ea304ddc307a359c1c6ab20fbce598065b6cb19acff7043
Bob: 0xc98ceafadaa32e50d06d181842406dbbf518b6586ab67cfa2b736aaddeb7c74f
=== Initial Balances ===
Alice: 20000
Bob: 0
=== Intermediate Balances ===
Alice: 18996
Bob: 1000
=== Final Balances ===
Alice: 17992
Bob: 2000
上面的输出显示,transfer-coin
例子执行了以下步骤。
创建两个账户:Alice 和 Bob。
通过 Faucet 接口创建 Alice 的账户,并向其注资。
将 1000 个代币从 Alice 的地址转到 Bob 的地址。
爱丽丝支付了 4 个单位 gas 费用,以实现这一转账操作。
再一次从 Alice 的地址转移 1000 个代币到 Bob 的地址。
爱丽丝又支付了 4 个单位的 gas 费用来完成这次转账。
接下来,请看下面用于完成上述步骤的 SDK 接口的详细解释。
第 4 步:深入理解 SDK 原理
transfer-coin
示例代码使用辅助函数与 REST API 互动。本节将对每个调用进行回顾,并对功能进行深入分析。
第 4.1 步:初始化客户端
第一步, transfer-coin
实例中初始化了 REST 和 faucet 的客户端。
faucet客户端与 devnet Faucet 服务交互,用于创建账户和向账户中注入代币。
Typescript
Copy const client = new AptosClient ( NODE_URL );
const faucetClient = new FaucetClient ( NODE_URL , FAUCET_URL );
使用API客户端,我们可以创建一个 CoinClient
,我们用它来进行常见的代币操作,如转移代币和检查余额。
Copy const coinClient = new CoinClient(client);
common.ts
初始化了以下的 URL 值。
Copy export const NODE_URL = process . env . APTOS_NODE_URL || "<https://fullnode.devnet.aptoslabs.com>" ;
export const FAUCET_URL = process . env . APTOS_FAUCET_URL || "<https://faucet.devnet.aptoslabs.com>" ;
Python
Copy rest_client = RestClient (NODE_URL)
faucet_client = FaucetClient (FAUCET_URL, rest_client)
[common.py]
初始化了以下配置值
Copy NODE_URL = os . getenv ( "APTOS_NODE_URL" , "<https://fullnode.devnet.aptoslabs.com/v1>" )
FAUCET_URL = os . getenv ( "APTOS_FAUCET_URL" , "<https://faucet.devnet.aptoslabs.com>" )
Rust
Copy let rest_client = Client :: new (NODE_URL . clone ());
let faucet_client = FaucetClient :: new (FAUCET_URL . clone (), NODE_URL . clone ());
使用 client API,我们可以创建一个 CoinClient 对象,我们用它来进行常见的硬币操作,如转移硬币和检查余额。
Copy let coin_client = CoinClient :: new ( & rest_client);
在本例中,我们按照如下方法配置测试网 URL
Copy static NODE_URL : Lazy < Url > = Lazy :: new ( || {
Url :: from_str (
std :: env :: var ( "APTOS_NODE_URL" )
. as_ref ()
. map ( | s | s . as_str ())
. unwrap_or ( "<https://fullnode.devnet.aptoslabs.com>" ),
)
. unwrap ()
});
static FAUCET_URL : Lazy < Url > = Lazy :: new ( || {
Url :: from_str (
std :: env :: var ( "APTOS_FAUCET_URL" )
. as_ref ()
. map ( | s | s . as_str ())
. unwrap_or ( "<https://faucet.devnet.aptoslabs.com>" ),
)
. unwrap ()
});
请注意: 默认情况下,两个服务的 URL 都指向 Aptos devnet 服务。它们也可以通过以下环境变量进行配置。
APTOS_FAUCET_URL
</aside>
第 4.2 步:在本地创建地址
下一步,是在本地创建两个账户。
账户 同时代表链上和链下状态。链下状态包括一个地址和用于验证所有权的公钥、私钥对。这一步演示了如何生成链下状态。
Typescript
Copy const alice = new AptosAccount ();
const bob = new AptosAccount ();
Python
Copy alice = Account . generate ()
bob = Account . generate ()
Rust
Copy let mut alice = LocalAccount :: generate ( &mut rand :: rngs :: OsRng );
let bob = LocalAccount :: generate ( &mut rand :: rngs :: OsRng );
第 4.3 步:创建链上账户rust
在 Aptos 网络中,每个账户都必须有一个链上表示,以支持接收代币和硬币,以及在其他 DApps 中进行互动。一个账户代表了一个存储资产的媒介,因此它必须明确地被创建。这个例子利用 Faucet 提供的接口来创建和资助 Alice 的账户;对于 Bob 的账户,我们只做创建的操作:
Typescript
Copy await faucetClient .fundAccount ( alice .address () , 20_000 );
await faucetClient .fundAccount ( bob .address () , 0 );type
Python
Copy faucet_client . fund_account (alice. address (), 20_000 )
faucet_client . fund_account (bob. address (), 0 )
Rust
Copy faucet_client
. fund (alice . address (), 20_000 )
.await
. context ( "Failed to fund Alice's account" ) ? ;
faucet_client
. create_account (bob . address ())
.await
. context ( "Failed to fund Bob's account" ) ? ;
第 4.4 步:读取账户余额
在这一步中,我们将使用 Aptos SDK 提供的能力请求一个资源,并且读取该资源中的成员变量
Typescript
Copy console .log ( `Alice: ${ await coinClient .checkBalance (alice) } ` );
console .log ( `Bob: ${ await coinClient .checkBalance (bob) } ` );
源码逻辑:TypeScript SDK 提供的 CoinClient
下的 checkBalance
函数查询了当前账户地址下所有资源,然后过滤出 Aptos 测试代币(APTOS_COIN)所在的资源,并且读取了当前的值,即用户持有的 Aptos 测试代币的余额。
Copy async checkBalance (
account: AptosAccount ,
extraArgs ?: {
// The coin type to use, defaults to 0x1::aptos_coin::AptosCoin
coinType? : string;
} ,
): Promise < bigint > {
const coinType = extraArgs ?.coinType ?? APTOS_COIN ;
const typeTag = `0x1::coin::CoinStore< ${ coinType } >` ;
const resources = await this . aptosClient .getAccountResources ( account .address ());
const accountResource = resources .find ((r) => r .type === typeTag);
return BigInt ((accountResource!.data as any).coin.value);
}
Python
Copy print ( f "Alice: { rest_client. account_balance (alice. address ()) } " )
print ( f "Bob: { rest_client. account_balance (bob. address ()) } " )
源码逻辑:Python SDK 的 account_balance 函数直接查询了 Aptos 测试代币所在的资源,并且读取了当前的值,即当前地址下 Aptos 测试代币的余额
Copy def account_balance ( self , account_address : str ) -> int :
"""Returns the test coin balance associated with the account"""
return self . account_resource (
account_address, "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>"
) [ "data" ][ "coin" ][ "value" ]
Rust
Copy println! (
"Alice: {:?}" ,
coin_client
. get_account_balance ( & alice . address ())
.await
. context ( "Failed to get Alice's account balance the second time" ) ?
);
println! (
"Bob: {:?}" ,
coin_client
. get_account_balance ( & bob . address ())
.await
. context ( "Failed to get Bob's account balance the second time" ) ?
);
源码逻辑:rust SDK 的 get_account_resource 函数直接查询了 Aptos 测试代币所在的资源,并且读取了当前的值,即当前地址下 Aptos 测试代币的余额
Copy let balance = self
. get_account_resource (address, "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>" )
.await? ;
第 4.5 步:转账
和 Step 4.4 一样,这是将 Aptos 代币从 Alice 的地址转移到 Bob 的地址另一个辅助步骤。对于正确生成的交易,API 将返回一个交易哈希值,可以在后续步骤中使用,以检查交易状态。Aptos 区块链在提交时执行了一些验证检查,如果其中任何一项失败,用户将得到一个错误的响应。这些验证包括交易签名,未使用的序列号,以及将交易提交给适当的链。
Typescript
Copy let txnHash = await coinClient .transfer (alice , bob , 1_000 );
源码逻辑:transfer
函数生成了一个交易的 payload 信息,并且让客户端签署,发送,最后等待交易发送的响应结果
Copy async transfer (
from: AptosAccount ,
to: AptosAccount ,
amount: number | bigint ,
extraArgs ?: {
// The coin type to use, defaults to 0x1::aptos_coin::AptosCoin
coinType? : string;
maxGasAmount ?: BCS .Uint64;
gasUnitPrice ?: BCS .Uint64;
expireTimestamp ?: BCS .Uint64;
} ,
): Promise < string > {
const coinTypeToTransfer = extraArgs ?.coinType ?? APTOS_COIN ;
const payload = this . transactionBuilder .buildTransactionPayload (
"0x1::coin::transfer" ,
[coinTypeToTransfer] ,
[ to .address () , amount] ,
);
return this.aptosClient.generateSignSubmitTransaction(from , payload , extraArgs);
}
在 aptosClient
中,generateSignSubmitTransaction
函数做了以下的事情:
Copy const rawTransaction = await this .generateRawTransaction ( sender .address () , payload , extraArgs);
const bcsTxn = AptosClient .generateBCSTransaction (sender , rawTransaction);
const pendingTransaction = await this .submitSignedBCSTransaction (bcsTxn);
return pendingTransaction .hash;
我们一步步来看代码的逻辑:
transfer
在内部是 Coin Move 模块 中的一个EntryFunction
,即 Move 中的一个入口函数,可以直接调用。
Move函数被存储在 coin 模块上: 0x1::coin
。
因为 coin 模块可以被其他 coin 使用,所以转移时必须明确指定要转移的 coin 类型。如果没有指定coinType
,则默认为0x1::aptos_coin::AptosCoin
。
Python
Copy txn_hash = rest_client . transfer (alice, bob. address (), 1_000 )
源码逻辑:Python SDK 生成交易,签署并发送该交易,最后等待交易发送的响应结果。
Copy def bcs_transfer (
self , sender : Account , recipient : AccountAddress , amount : int
) -> str :
transaction_arguments = [
TransactionArgument (recipient, Serializer.struct),
TransactionArgument (amount, Serializer.u64),
]
payload = EntryFunction . natural (
"0x1::coin" ,
"transfer" ,
[ TypeTag (StructTag. from_str ( "0x1::aptos_coin::AptosCoin" ))],
transaction_arguments,
)
signed_transaction = self . create_single_signer_bcs_transaction (
sender, TransactionPayload (payload)
)
return self . submit_bcs_transaction (signed_transaction)
我们一步步来看代码的逻辑:
transfer
在内部是 Coin Move 模块 中的一个EntryFunction
,即 Move 中的一个入口函数,可以直接调用。
Move函数被存储在 coin 模块上。0x1::coin。
因为 coin 模块可以被其他 coin 使用,所以转账必须明确地使用 TypeTag
来定义要对哪种 coin 进行转账。
交易入参必须被放入带有类型指定器(Serializer.{type}
)的TransactionArguments
中,这将在交易生成时将入参序列化为适当的类型。
Rust
Copy let txn_hash = coin_client
. transfer ( &mut alice, bob . address (), 1_000 , None )
.await
. context ( "Failed to submit transaction to transfer coins" ) ? ;
源码逻辑:Rust SDK 生成交易,签署并发送该交易,最后等待交易发送的响应结果。
Copy let chain_id = self
. api_client
. get_index ()
.await
. context ( "Failed to get chain ID" ) ?
. inner ()
. chain_id;
let transaction_builder = TransactionBuilder :: new (
TransactionPayload :: EntryFunction (EntryFunction :: new (
ModuleId :: new (AccountAddress :: ONE, Identifier :: new ( "coin" ) . unwrap ()),
Identifier :: new ( "transfer" ) . unwrap (),
vec! [TypeTag :: from_str (options . coin_type) . unwrap ()],
vec! [
bcs :: to_bytes ( & to_account) . unwrap (),
bcs :: to_bytes ( & amount) . unwrap (),
],
)),
SystemTime :: now ()
. duration_since (UNIX_EPOCH)
. unwrap ()
. as_secs ()
+ options . timeout_secs,
ChainId :: new (chain_id),
)
. sender (from_account . address ())
. sequence_number (from_account . sequence_number ())
. max_gas_amount (options . max_gas_amount)
. gas_unit_price (options . gas_unit_price);
let signed_txn = from_account . sign_with_transaction_builder (transaction_builder);
Ok (self
. api_client
. submit ( & signed_txn)
.await
. context ( "Failed to submit transfer transaction" ) ?
. into_inner ())
我们一步步来看代码的逻辑:
首先,我们获取链的 ID,这是建立交易的有效 payload 所必需的。
transfer
在内部是 Coin Move 模块 中的一个EntryFunction
,即 Move 中的一个入口函数,可以直接调用。
Move函数被存储在 coin 模块上。0x1::coin。
因为 coin 模块可以被其他 coin 使用,所以转账必须明确地使用 TypeTag
来定义要对哪种 coin 进行转账。
交易参数,如 to_account
(目标账户地址)和 amount
(金额),必须被编码为 BCS,以便与 TransactionBuilder
一起使用。
第 4.6 步:等待交易结果
Typescript
在Typescript中,只要调用 coinClient.transfer
就可以等待交易完成。一旦处理完毕(无论成功与否),该函数将返回 Transaction
对象,如果处理超时,则抛出一个错误。
如果你想让它在交易没有成功提交时抛出错误,你可以在调用 transfer
时 checkSuccess 设置为 true。
Copy await client .waitForTransaction (txnHash);
Python
交易的哈希值可以用来查询该交易当前的执行状态,Python SDK 提供了等待当前交易哈希值对应交易输出最终执行结果的能力。
Copy rest_client . wait_for_transaction (txn_hash)
Rust
交易的哈希值可以用来查询该交易当前的执行状态,Rust SDK 提供了等待当前交易哈希值对应交易输出最终执行结果的能力。
Copy rest_client
. wait_for_transaction ( & txn_hash)
.await
. context ( "Failed when waiting for the transfer transaction" ) ? ;