PTB 구성하기
이 가이드는 programmable transaction block (PTB)를 Sui 위에서 TypeScript SDK를 사용해 생성하는 방법을 다룬다.
이 예시는 SUI token을 전송하는 PTB를 구성한다.
먼저 Transaction 클래스를 import하고 이를 생성한다:
import { Transaction } from '@mysten/sui/transactions';
const tx = new Transaction();
이 객체를 사용해 PTB에 트랜잭션을 추가할 수 있다:
// Create a new coin with balance 100, based on the coins used as gas payment.
// You can define any balance here.
const [coin] = tx.splitCoins(tx.gas, [tx.pure('u64', 100)]);
// Transfer the split coin to a specific address.
tx.transferObjects([coin], tx.pure('address', '0xSomeSuiAddress'));
같은 타 입의 여러 트랜잭션 command를 하나의 PTB에 붙일 수 있다. 예를 들어 transfer 목록을 가져와 이를 순회하면서 각 대상에게 코인을 전송하려면 다음과 같이 한다:
interface Transfer {
to: string;
amount: number;
}
// Procure a list of some Sui transfers to make:
const transfers: Transfer[] = getTransfers();
const tx = new Transaction();
// First, split the gas coin into multiple coins:
const coins = tx.splitCoins(
tx.gas,
transfers.map((transfer) => tx.pure('u64', transfer.amount)),
);
// Next, create a transfer transaction for each coin:
transfers.forEach((transfer, index) => {
tx.transferObjects([coins[index]], tx.pure('address', transfer.to));
});
트랜잭션 정의가 끝나면 Sui client와 KeyPair를 사용해 client.signAndExecuteTransaction으로 직접 실행할 수 있다.
client.signAndExecuteTransaction({ signer: keypair, transaction: tx });
입력 구성
입력은 외부 값을 PTB에 제공하는 방법이며, 예를 들어 전송할 SUI amount, Move call에 전달할 객체, 공유 객체 등을 넣는다.
입력을 정의하는 방법은 2가지가 있다:
-
객체용:
tx.object(objectId)함수는 객체 reference를 담은 입력을 구성하는 데 사용된다. -
pure value용:
tx.pure(type, value)함수는 non-객체 입력을 구성하는 데 사용된다.-
값이
Uint8Array이면 raw bytes로 간주되어 직접 사용된다:tx.pure(SomeUint8Array). -
그렇지 않으면 값에 대한 BCS serialization layout을 생성하는 데 사용된다.
-
새 version에서는 더 직관적인 표기법이 제공되며, 예를 들어
tx.pure.u64(100),tx.pure.string('SomeString'),tx.pure.address('0xSomeSuiAddress'),tx.pure.vector('bool', [true, false])처럼 작성할 수 있다.
-
inputs에 대해 더 알아본다.
트랜잭션 결과를 인자로 전달
트랜잭션 command의 결과를 이후 트랜잭션 command의 인자로 사용할 수 있다. 트랜잭션 builder의 각 트랜잭션 command method는 트랜잭션 결과를 가리키는 reference를 반환한다.
// Split a coin object off of the gas object:
const [coin] = tx.splitCoins(tx.gas, [tx.pure.u64(100)]);
// Transfer the resulting coin object:
tx.transferObjects([coin], tx.pure.address(address));
트랜잭션 command가 여러 결과를 반환할 때는 destructuring 또는 배열 인덱스를 사용해 특정 인덱스의 결과에 접근할 수 있다.
// Destructuring (preferred, as it gives you logical local names):
const [nft1, nft2] = tx.moveCall({ target: '0x2::nft::mint_many' });
tx.transferObjects([nft1, nft2], tx.pure.address(address));
// Array indexes:
const mintMany = tx.moveCall({ target: '0x2::nft::mint_many' });
tx.transferObjects([mintMany[0], mintMany[1]], tx.pure.address(address));
가스 코인 사용
PTB에서는 splitCoin을 사용해 설정된 balance를 가진 코인을 gas payment 코인으로부터 구성할 수 있다.
이는 SUI 결제에 유용하며 사전 코인 선택이 필요하지 않게 해준다.
tx.gas를 사용해 PTB 안에서 가스 코인에 접근할 수 있으며, 어떤 인자에도 유효한 입력으로 사용할 수 있다.
transferObjects를 제외하면 tx.gas는 reference로 사용되어야 한다.
즉 mergeCoins로 가스 코인에 코인을 더하거나 moveCall을 통해 Move 함수에 빌려줄 수도 있다.
모든 코인 balance를 다른 address로 보내고 싶다면 transferObjects를 사용해 가스 코인을 전송할 수 있다.
wallet 안의 다른 코인은 해당 Object ID를 사용해 전송할 수도 있다.
예를 들면 다음과 같다:
const otherCoin = tx.object('0xCoinObjectId');
const coin = tx.splitCoins(otherCoin, [tx.pure.u64(100)]);
tx.transferObjects([coin], tx.pure.address(address));
PTB byte 가져오기
PTB에 서명하거나 실행하는 대신 PTB byte가 필요하다면 트랜잭션 builder 자체의 build method를 사용할 수 있다.
PTB의 setSender() 필드가 채워지도록 하려면 sender를 명시적으로 호출해야 할 수 있다.
보통 이 작업은 signer가 transaction 서명 전에 수행하지만, 직접 PTB byte를 생성하는 경우에는 자동으로 수행되지 않는다.
const tx = new Transaction();
// ... add some transactions...
await tx.build({ provider });
대부분의 경우 build를 수행하려면 JSON RPC provider가 입력 값을 완전히 resolve해야 한다.
PTB byte가 있다면 이를 다시 Transaction 클래스로 변환할 수도 있다:
const bytes = getTransactionBytesFromSomewhere();
const tx = Transaction.from(bytes);
offline build
provider가 필요 없도록 PTB를 오프라인에서 build하려면 모든 입력 값과 gas 구성을 완전히 정의해야 한다.
pure value에는 트랜잭션에서 직접 사용하는 Uint8Array를 제공할 수 있다.
객체에는 Inputs helper를 사용해 객체 reference를 구성할 수 있다.
import { Inputs } from '@mysten/sui/transactions';
// For pure values:
tx.pure(pureValueAsBytes);
// For owned or immutable objects:
tx.object(Inputs.ObjectRef({ digest, objectId, version }));
// For shared objects:
tx.object(Inputs.SharedObjectRef({ objectId, initialSharedVersion, mutable }));
그런 다음 트랜잭션에 대해 provider를 호출할 때 build 객체를 생략할 수 있다.
필수 데이터가 누락되어 있으면 error가 발생한다.
gas 구성
트랜잭션 builder는 가스 가격, budget, gas로 사용할 코인 선택을 포함해 모든 gas 로직에 대한 기본 동작을 제공한다. 이 동작은 사용자화할 수 있다.
가스 가격
기본적으로 SDK는 가스 가격를 network의 reference 가스 가격로 설정한다.
setGasPrice를 트랜잭션 builder에 호출해 PTB의 가스 가격를 명시적으로 설정할 수도 있다:
tx.setGasPrice(gasPrice);
budget
기본적으로 SDK는 먼저 PTB의 dry-run을 실행해 가스 예산을 자동으로 산정한다.
그 다음 SDK는 dry run gas consumption을 사용해 트랜잭션에 필요한 balance를 결정한다.
이 동작은 트랜잭션 builder의 setGasBudget을 사용해 트랜잭션의 가스 예산을 명시적으로 설정함으로써 재정의할 수 있다.
Sui는 가스 예산을 SUI로 표현하므로 PTB의 가스 가격를 고려해야 한다.
tx.setGasBudget(gasBudgetAmount);
gas payment
기본적으로 SDK는 gas payment를 자동으로 결정한다. PTB의 입력으로 사용되지 않는, 제공된 address의 모든 코인을 선택한다.
SDK는 PTB를 실행하기 전에 gas payment에 사용하는 코인 목록을 하나의 가스 코인으로 병합하고, gas 객체 중 1개를 제외한 나머지를 모두 삭제한다. 0 인덱스의 가스 코인이 나머지 코인이 병합되는 대상이다.
// NOTE: You need to ensure that the coins do not overlap with any
// of the input objects for the PTB.
tx.setGasPayment([coin1, coin2]);
Gas 코인은 코인의 { objectId: string, version: string | number, digest: string }을 담은 객체여야 한다.
App / Wallet integration
이제 Wallet Standard 인터페이스는 Transaction kind를 직접 지원한다.
앱이 wallet에 호출하는 모든 signTransaction 및 signAndExecuteTransaction은 Transaction 클래스를 제공해야 한다.
그런 다음 이 PTB 클래스를 serialize하여 wallet으로 보내 실행할 수 있다.
wallet으로 보낼 PTB를 serialize하려면 Sui는 불투명한 문자열 표현을 반환하는 tx.serialize() 함수 사용을 권장하며, 이 문자열은 Wallet Standard 앱 컨텍스트에서 wallet로 전달될 수 있다.
그다음 Transaction을 사용해 이를 다시 Transaction.from()으로 변환할 수 있다.
앱 코드에서 byte로부터 PTB를 build해서는 안 된다.
serialize 대신 build를 사용하면 wallet 자체 안에서 PTB byte를 build할 수 있다.
이렇게 하면 wallet이 필요에 따라 gas 로직과 코인 선택을 수행할 수 있다.
// Within an app
const tx = new Transaction();
wallet.signTransaction({ transaction: tx });
// Your Wallet Standard code:
function handleSignTransaction(input) {
sendToWalletContext({ transaction: input.transaction.serialize() });
}
// Within your wallet context:
function handleSignRequest(input) {
const userTx = Transaction.from(input.transaction);
}
sponsored PTB
PTB builder는 PTB를 build할 때 onlyTransactionKind flag를 사용함으로써 sponsored PTB를 지원할 수 있다.
const tx = new Transaction();
// ... add some transactions...
const kindBytes = await tx.build({ provider, onlyTransactionKind: true });
// Construct a sponsored transaction from the kind bytes:
const sponsoredTx = Transaction.fromKind(kindBytes);
// You can now set the sponsored transaction data that is required:
sponsoredTx.setSender(sender);
sponsoredTx.setGasOwner(sponsor);
sponsoredTx.setGasPayment(sponsorCoins);
sponsored transactions에 대해 더 알아본다.