Skip to main content

构建可编程交易块

本指南探讨了如何使用 TypeScript SDK 在 Sui 上创建可编程交易块(PTB)。有关 PTB 是什么的概述,请参阅可编程交易块的概念部分。如果你尚未安装 Sui TypeScript SDK,请按照Sui TypeScript SDK 网站上的安装说明进行操作。

此示例从构建一个 PTB 开始,以发送 Sui。如果你熟悉传统 Sui 交易类型,这类似于 paySui 交易。要构建交易,请导入 TransactionBlock 类并进行构建:

import { TransactionBlock } from "@mysten/sui.js";
const txb = new TransactionBlock();

使用它,你可以将交易添加到该 PTB。

// Create a new coin with balance 100, based on the coins used as gas payment.
// You can define any balance here.
const [coin] = txb.splitCoins(txb.gas, [txb.pure(100)]);

// Transfer the split coin to a specific address.
txb.transferObjects([coin], txb.pure("0xSomeSuiAddress"));

你还可以将相同类型的多个交易命令附加到 PTB。例如,要获取转账列表,并对它们进行迭代以向每个转账对象转账:

interface Transfer {
to: string;
amount: number;
}

// Procure a list of some Sui transfers to make:
const transfers: Transfer[] = getTransfers();

const txb = new TransactionBlock();

// First, split the gas coin into multiple coins:
const coins = txb.splitCoins(
txb.gas,
transfers.map((transfer) => txb.pure(transfer.amount))
);

// Next, create a transfer transaction for each coin:
transfers.forEach((transfer, index) => {
txb.transferObjects([coins[index]], txb.pure(transfer.to));
});

定义了 PTB 后,你可以使用 signAndExecuteTransactionBlock 直接使用 signer 执行它。

signer.signAndExecuteTransactionBlock({ transactionBlock: txb });

构造输入

输入是你向 PTB 提供外部值的方式。例如,定义要转移的 Sui 金额,或传递到 Move 调用或共享对象中的对象。

目前有两种定义输入的方法:

  • 对于对象:使用 txb.object(objectId) 函数构建包含对象引用的输入。
  • 对于纯值:使用 txb.pure(value, type?) 函数构建非对象输入的输入。
    • 如果值是 Uint8Array,则假定该值是原始字节并直接使用。
    • 如果提供了类型,则使用它来生成值的 BCS 序列化布局。如果未提供,则根据值自动确定类型。

可用的事务

Sui 支持以下事务命令:

  • txb.splitCoins(coin, amounts): 从提供的代币中拆分具有定义金额的新代币。返回代币,以便在后续事务中使用。
    • 示例:txb.splitCoins(txb.gas, [txb.pure(100), txb.pure(200)])
  • txb.mergeCoins(destinationCoin, sourceCoins): 将源代币合并到目标代币中。
    • 示例:txb.mergeCoins(txb.object(coin1), [txb.object(coin2), txb.object(coin3)])
  • txb.transferObjects(objects, address): 将对象列表转移给指定地址。
    • 示例:txb.transferObjects([txb.object(thing1), txb.object(thing2)], txb.pure(myAddress))
  • txb.moveCall({ target, arguments, typeArguments }): 执行 Move 调用。返回 Sui Move 调用返回的任何内容。
    • 示例:txb.moveCall({ target: '0x2::devnet_nft::mint', arguments: [txb.pure(name), txb.pure(description), txb.pure(image)] })
  • txb.makeMoveVec({ type, objects }): 构造一个对象向量,可以传递到 moveCall。这是必需的,因为没有其他方法来定义一个输入为向量。
    • 示例:txb.makeMoveVec({ objects: [txb.object(id1), txb.object(id2)] })
  • txb.publish(modules, dependencies): 发布一个 Move 包。返回升级能力对象。

将事务结果作为参数传递

你可以将事务命令的结果用作后续事务命令的参数。事务构建器上的每个事务命令方法都返回对事务结果的引用。

// Split a coin object off of the gas object:
const [coin] = txb.splitCoins(txb.gas, [txb.pure(100)]);
// Transfer the resulting coin object:
txb.transferObjects([coin], txb.pure(address));
When a transaction command returns multiple results, you can access the result at a specific index either using destructuring, or array indexes.
// Destructuring (preferred, as it gives you logical local names):
const [nft1, nft2] = txb.moveCall({ target: "0x2::nft::mint_many" });
txb.transferObjects([nft1, nft2], txb.pure(address));

// Array indexes:
const mintMany = txb.moveCall({ target: "0x2::nft::mint_many" });
txb.transferObjects([mintMany[0], mintMany[1]], txb.pure(address));

使用 gas 代币

通过 PTB,你可以使用 gas 付款代币使用 splitCoin 构建具有固定余额的代币。这对于 Sui 付款非常有用,并且避免了需要预先选择代币。你可以使用 txb.gas 在 PTB 中访问 gas 代币,并且它是任何参数的有效输入;除了 transferObjects 外,txb.gas 必须通过引用使用。实际上,这意味着你还可以使用 mergeCoins 添加到 gas 代币,或者使用 moveCall 借用它进行 Move 函数。

你还可以使用 transferObjects 转移 gas 代币,以便将你的全部代币余额转移到另一个地址。

获取 PTB 字节

如果你需要 PTB 字节,而不是签署或执行 PTB,可以在事务构建器本身上使用 build 方法。

tip

你可能需要在 PTB 上显式调用 setSender() 以确保填充 sender 字段。这通常由签署者在签署事务之前完成,但如果你自己构建 PTB 字节,则不会自动执行。

const txb = new TransactionBlock();

// ... add some transactions...

await txb.build({ provider });

在大多数情况下,构建需要你的 JSON RPC 提供程序完全解析输入值。

如果你有 PTB 字节,还可以将它们转换回 TransactionBlock 类:

const bytes = getTransactionBlockBytesFromSomewhere();
const txb = TransactionBlock.from(bytes);

离线构建

如果你希望离线构建 PTB(即无需 provider),则需要完全定义所有输入值和 gas 配置(请参阅以下示例)。对于纯值,你可以提供一个 Uint8Array,它将直接用于事务。对于对象,你可以使用 Inputs 辅助程序构建对象引用。

import { Inputs } from "@mysten/sui.js";

// For pure values:
txb.pure(pureValueAsBytes);

// For owned or immutable objects:
txb.object(Inputs.ObjectRef({ digest, objectId, version }));

// For shared objects:
txb.object(Inputs.SharedObjectRef({ objectId, initialSharedVersion, mutable }));

然后,在调用事务的 build 时,可以省略 provider 对象。如果缺少任何必需的数据,这将引发错误。

gas 配置

新的事务构建器具有所有 gas 逻辑的默认行为,包括自动设置 gas 价格、预算和选择用作 gas 的代币。此行为可以定制。

gas 价格

默认情况下,gas 价格设置为网络的参考 gas 价格。你还可以通过在事务构建器上调用 setGasPrice 明确设置 PTB 的 gas 价格。

txb.setGasPrice(gasPrice);

预算

默认情况下,gas 预算通过预先执行 PTB 的干扰运行自动推导。然后,干扰运行的 gas 消耗用于确定事务的余额。你可以通过在事务构建器上调用 setGasBudget 明确为事务设置 gas 预算来覆盖此行为。

info

gas 预算以 Sui 表示,并应考虑 PTB 的 gas 价格。

txb.setGasBudget(gasBudgetAmount);

gas 付款

默认情况下,gas 付款由 SDK 自动确定。SDK 选择在 PTB 中未用作输入的提供的地址的所有代币。

在执行 PTB 之前,将将用作 gas 付款的代币列表合并为单个 gas 代币,并删除除一个以外的所有 gas 对象。索引为 0 的 gas 代币将是所有其他代币合并到其中的代币。

// NOTE: You need to ensure that the coins do not overlap with any
// of the input objects for the PTB.
txb.setGasPayment([coin1, coin2]);

dApp / 钱包集成

钱包标准接口已更新,以直接支持 TransactionBlock 类型。从 dApp 到钱包的所有 signTransactionsignAndExecuteTransaction 调用都预期提供一个 TransactionBlock 类。然后,可以将此 PTB 类序列化并发送到你的钱包以进行执行。

为了将 PTB 序列化以发送到钱包,Sui 建议使用 txb.serialize() 函数,该函数返回 PTB 的不透明字符串表示形式,可以从钱包标准 dApp 上下文传递到你的钱包。然后,可以使用 TransactionBlock.from() 将其转换回 TransactionBlock

tip

不应在 dApp 代码中从字节构建 PTB。使用 serialize 而不是 build 允许你在钱包内部构建 PTB 字节。这允许钱包根据需要执行 gas 逻辑和选择代币。

// Within a dApp
const tx = new TransactionBlock();
wallet.signTransactionBlock({ transactionBlock: tx });

// Your wallet standard code:
function handleSignTransactionBlock(input) {
sendToWalletContext({ transactionBlock: input.transactionBlock.serialize() });
}

// Within your wallet context:
function handleSignRequest(input) {
const userTx = TransactionBlock.from(input.transaction);
}

赞助 PTB

PTB 构建器可以通过在构建 PTB 时使用 onlyTransactionKind 标志来支持赞助 PTB。

const txb = new TransactionBlock();
// ... add some transactions...

const kindBytes = await txb.build({ provider, onlyTransactionKind: true });

// Construct a sponsored transaction from the kind bytes:
const sponsoredTxb = TransactionBlock.fromKind(kindBytes);

// You can now set the sponsored transaction data that is required:
sponsoredTxb.setSender(sender);
sponsoredTxb.setGasOwner(sponsor);
sponsoredTxb.setGasPayment(sponsorCoins);