본문으로 건너뛰기

PTB 구성하기

이 가이드는 programmable transaction block (PTB)를 Sui 위에서 TypeScript SDK를 사용해 생성하는 방법을 다룬다.

이 예시는 SUI token을 전송하는 PTB를 구성한다. 먼저 Transaction 클래스를 import하고 이를 생성한다:

import { Transaction } from '@mysten/sui/transactions';

const tx = new Transaction();

이 객체를 사용해 PTB에 transaction을 추가할 수 있다:

// gas 결제에 사용되는 coin을 기반으로 잔액 100인 새 coin을 만든다.
// 여기서는 어떤 잔액이든 정의할 수 있다.
const [coin] = tx.splitCoins(tx.gas, [tx.pure('u64', 100)]);

// 분할된 coin을 특정 address로 전송한다.
tx.transferObjects([coin], tx.pure('address', '0xSomeSuiAddress'));

같은 타입의 여러 transaction command를 하나의 PTB에 붙일 수 있다. 예를 들어 transfer 목록을 가져와 이를 순회하면서 각 대상에게 coin을 전송하려면 다음과 같이 한다:

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

// 수행할 Sui transfer 목록을 준비한다:
const transfers: Transfer[] = getTransfers();

const tx = new Transaction();

// 먼저 gas coin을 여러 coin으로 분할한다:
const coins = tx.splitCoins(
tx.gas,
transfers.map((transfer) => tx.pure('u64', transfer.amount)),
);

// 다음으로 각 coin에 대한 transfer transaction을 만든다:
transfers.forEach((transfer, index) => {
tx.transferObjects([coins[index]], tx.pure('address', transfer.to));
});

transaction 정의가 끝나면 Sui client와 KeyPair를 사용해 client.signAndExecuteTransaction으로 직접 실행할 수 있다.

client.signAndExecuteTransaction({ signer: keypair, transaction: tx });

Constructing inputs

입력은 외부 값을 PTB에 제공하는 방법이며, 예를 들어 전송할 SUI amount, Move call에 전달할 object, shared object 등을 넣는다.

입력을 정의하는 방법은 2가지가 있다:

  • object용: tx.object(objectId) 함수는 object reference를 담은 입력을 구성하는 데 사용된다.

  • pure value용: tx.pure(type, value) 함수는 non-object 입력을 구성하는 데 사용된다.

    • 값이 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에 대해 더 알아본다.

Passing transaction results as arguments

transaction command의 결과를 이후 transaction command의 인자로 사용할 수 있다. transaction builder의 각 transaction command method는 transaction 결과를 가리키는 reference를 반환한다.

// gas object에서 coin object를 하나 분할한다:
const [coin] = tx.splitCoins(tx.gas, [tx.pure.u64(100)]);
// 결과로 생긴 coin object를 전송한다:
tx.transferObjects([coin], tx.pure.address(address));

transaction command가 여러 결과를 반환할 때는 destructuring 또는 배열 인덱스를 사용해 특정 인덱스의 결과에 접근할 수 있다.

// Destructuring(권장, 논리적인 로컬 이름을 붙일 수 있기 때문):
const [nft1, nft2] = tx.moveCall({ target: '0x2::nft::mint_many' });
tx.transferObjects([nft1, nft2], tx.pure.address(address));

// 배열 인덱스:
const mintMany = tx.moveCall({ target: '0x2::nft::mint_many' });
tx.transferObjects([mintMany[0], mintMany[1]], tx.pure.address(address));

Use the gas coin

PTB에서는 splitCoin을 사용해 설정된 balance를 가진 coin을 gas payment coin으로부터 구성할 수 있다. 이는 SUI 결제에 유용하며 사전 coin 선택이 필요하지 않게 해준다. tx.gas를 사용해 PTB 안에서 gas coin에 접근할 수 있으며, 어떤 인자에도 유효한 입력으로 사용할 수 있다. transferObjects를 제외하면 tx.gas는 reference로 사용되어야 한다. 즉 mergeCoins gas coin에 coin을 더하거나 moveCall을 통해 Move 함수에 빌려줄 수도 있다.

모든 coin balance를 다른 address로 보내고 싶다면 transferObjects를 사용해 gas coin을 전송할 수 있다. wallet 안의 다른 coin은 해당 Object ID를 사용해 전송할 수도 있다. 예를 들면 다음과 같다:

const otherCoin = tx.object('0xCoinObjectId');
const coin = tx.splitCoins(otherCoin, [tx.pure.u64(100)]);
tx.transferObjects([coin], tx.pure.address(address));

Get PTB bytes

PTB에 서명하거나 실행하는 대신 PTB byte가 필요하다면 transaction builder 자체의 build method를 사용할 수 있다.

PTB의 sender field가 채워지도록 하려면 setSender()를 명시적으로 호출해야 할 수 있다. 보통 이 작업은 signer가 signing the transaction 전에 수행하지만, 직접 PTB byte를 생성하는 경우에는 자동으로 수행되지 않는다.

const tx = new Transaction();

// ... transaction을 몇 개 추가한다...

await tx.build({ provider });

대부분의 경우 build를 수행하려면 JSON RPC provider가 입력 값을 완전히 resolve해야 한다. PTB byte가 있다면 이를 다시 Transaction 클래스로 변환할 수도 있다:

const bytes = getTransactionBytesFromSomewhere();
const tx = Transaction.from(bytes);

Building offline

provider가 필요 없도록 PTB를 오프라인에서 build하려면 모든 입력 값과 gas 구성을 완전히 정의해야 한다. pure value에는 transaction에서 직접 사용하는 Uint8Array를 제공할 수 있다. object에는 Inputs helper를 사용해 object reference를 구성할 수 있다.

import { Inputs } from '@mysten/sui/transactions';

// pure value의 경우:
tx.pure(pureValueAsBytes);

// owned 또는 immutable object의 경우:
tx.object(Inputs.ObjectRef({ digest, objectId, version }));

// shared object의 경우:
tx.object(Inputs.SharedObjectRef({ objectId, initialSharedVersion, mutable }));

그런 다음 transaction에 대해 build를 호출할 때 provider object를 생략할 수 있다. 필수 데이터가 누락되어 있으면 error가 발생한다.

Gas configuration

transaction builder는 gas price, budget, gas로 사용할 coin 선택을 포함해 모든 gas 로직에 대한 기본 동작을 제공한다. 이 동작은 사용자화할 수 있다.

Gas price

기본적으로 SDK는 gas price를 network의 reference gas price로 설정한다. setGasPrice transaction builder에 호출해 PTB의 gas price를 명시적으로 설정할 수도 있다:

tx.setGasPrice(gasPrice);

Budget

기본적으로 SDK는 먼저 PTB의 dry-run을 실행해 gas budget을 자동으로 산정한다. 그 다음 SDK는 dry run gas consumption을 사용해 transaction에 필요한 balance를 결정한다. 이 동작은 transaction builder의 setGasBudget을 사용해 transaction의 gas budget을 명시적으로 설정함으로써 재정의할 수 있다.

정보

Sui는 gas budget을 SUI로 표현하므로 PTB의 gas price를 고려해야 한다.

tx.setGasBudget(gasBudgetAmount);

Gas payment

기본적으로 SDK는 gas payment를 자동으로 결정한다. PTB의 입력으로 사용되지 않는, 제공된 address의 모든 coin을 선택한다.

SDK는 PTB를 실행하기 전에 gas payment에 사용하는 coin 목록을 하나의 gas coin으로 병합하고, gas object 중 1개를 제외한 나머지를 모두 삭제한다. 0 인덱스의 gas coin이 나머지 coin이 병합되는 대상이다.

// 참고: coin이 PTB의 어떤 입력 object와도 겹치지 않도록 해야 한다.
tx.setGasPayment([coin1, coin2]);

Gas coin은 coin의 { objectId: string, version: string | number, digest: string }을 담은 object여야 한다.

App / Wallet integration

이제 Wallet Standard 인터페이스는 Transaction kind를 직접 지원한다. 앱이 wallet에 호출하는 모든 signTransactionsignAndExecuteTransactionTransaction 클래스를 제공해야 한다. 그런 다음 이 PTB 클래스를 serialize하여 wallet으로 보내 실행할 수 있다.

wallet으로 보낼 PTB를 serialize하려면 Sui는 불투명한 문자열 표현을 반환하는 tx.serialize() 함수 사용을 권장하며, 이 문자열은 Wallet Standard 앱 컨텍스트에서 wallet로 전달될 수 있다. 그다음 Transaction.from()을 사용해 이를 다시 Transaction으로 변환할 수 있다.

앱 코드에서 byte로부터 PTB를 build해서는 안 된다. build 대신 serialize를 사용하면 wallet 자체 안에서 PTB byte를 build할 수 있다. 이렇게 하면 wallet이 필요에 따라 gas 로직과 coin 선택을 수행할 수 있다.

// 앱 내부
const tx = new Transaction();
wallet.signTransaction({ transaction: tx });

// Wallet Standard 코드 내부:
function handleSignTransaction(input) {
sendToWalletContext({ transaction: input.transaction.serialize() });
}

// wallet 컨텍스트 내부:
function handleSignRequest(input) {
const userTx = Transaction.from(input.transaction);
}

PTB builder는 PTB를 build할 때 onlyTransactionKind flag를 사용함으로써 sponsored PTB를 지원할 수 있다.

const tx = new Transaction();
// ... transaction을 몇 개 추가한다...

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

// kind bytes에서 sponsored transaction을 구성한다:
const sponsoredTx = Transaction.fromKind(kindBytes);

// 이제 필요한 sponsored transaction 데이터를 설정할 수 있다:
sponsoredTx.setSender(sender);
sponsoredTx.setGasOwner(sponsor);
sponsoredTx.setGasPayment(sponsorCoins);

sponsored transactions에 대해 더 알아본다.