import { CoinStruct, PaginatedCoins, SuiClient, SuiObjectResponse } from '@mysten/sui.js/client'; import { TransactionBlock } from '@mysten/sui.js/transactions'; import { SUI_COIN } from './constants'; import { CoinTransferIntention, ObjectTransferIntention, SuiAddress, TxIntention } from './types'; import { isCoinObjectType, isSameAddress, isSuiStructEqual } from './utils'; export function serializeIntention(intention: any) { return JSON.stringify(intention, (_, value) => (typeof value === 'bigint' ? `${value.toString()}n` : value)); } /** * Get all owner coins * @param client sui client * @param owner owner address * @param coinType coin type * @returns coins */ export async function getAllCoins(client: SuiClient, owner: SuiAddress, coinType: string | undefined) { let hasNext = true; let cursor: string | undefined | null; const res: CoinStruct[] = []; while (hasNext) { const currentPage: PaginatedCoins = await client.getCoins({ owner, coinType, cursor, }); res.push(...currentPage.data); hasNext = currentPage.hasNextPage; cursor = currentPage.nextCursor; } return res; } export function deserializeIntention(value: string): TxIntention { const intention = JSON.parse(value); if (typeof intention !== 'object') { throw new Error('Invalid intention'); } return intention; } export function buildRejectTxb(sender: SuiAddress) { const block = new TransactionBlock(); block.setSender(sender); return block; } export async function buildCoinTransferTxb(client: SuiClient, intention: CoinTransferIntention, sender: SuiAddress) { if (isSuiStructEqual(intention.coinType, SUI_COIN)) { return buildSuiCoinTransferTxb(intention, sender); } return buildOtherCoinTransferTxb(client, intention, sender); } export function buildSuiCoinTransferTxb(intention: CoinTransferIntention, sender: SuiAddress) { const block = new TransactionBlock(); const [coin] = block.splitCoins(block.gas, [block.pure(intention.amount)]); block.transferObjects([coin], block.pure(intention.recipient)); block.setSender(sender); return block; } export async function buildOtherCoinTransferTxb( client: SuiClient, intention: CoinTransferIntention, sender: SuiAddress, ) { const objs = await getAllCoins(client, sender, intention.coinType); if (objs.length === 0) { throw new Error('No valid coin found to send'); } const totalBal = objs.reduce((sum, coin) => sum + BigInt(coin.balance), 0n); if (totalBal < BigInt(intention.amount)) { throw new Error('Not enough balance'); } const txb = new TransactionBlock(); const primary = txb.object(objs[0].coinObjectId); if (objs.length > 1) { txb.mergeCoins( primary, objs.slice(1).map((obj) => txb.object(obj.coinObjectId)), ); } const [coin] = txb.splitCoins(primary, [txb.pure(intention.amount)]); txb.transferObjects([coin], txb.pure(intention.recipient)); txb.setSender(sender); return txb; } export async function buildObjectTransferTxb( client: SuiClient, intention: ObjectTransferIntention, sender: SuiAddress, ) { await validateObjectTransfer(client, intention, sender); const txb = new TransactionBlock(); txb.transferObjects([txb.object(intention.objectId)], txb.pure(intention.receiver)); txb.setSender(sender); return txb; } async function validateObjectTransfer(client: SuiClient, intention: ObjectTransferIntention, sender: SuiAddress) { const obj = await client.getObject({ id: intention.objectId, options: { showType: true, showOwner: true, }, }); if (obj.data === undefined) { throw new Error('Object not found'); } if (!obj.data?.type) { throw new Error('Object type is null'); } if (!isSuiStructEqual(obj.data.type, intention.objectType)) { throw new Error('Object type not expected'); } if (isCoinObjectType(obj.data.type)) { throw new Error('Can not transfer coin object in Object Transfer transactions'); } const addressOwner = getAddressOwner(obj); if (!isSameAddress(addressOwner, sender)) { throw new Error('Object owner not match'); } } function getAddressOwner(object: SuiObjectResponse) { const owner = object.data?.owner; if (!owner) { throw new Error('Object Owner not found'); } if (typeof owner !== 'object' || !('AddressOwner' in owner)) { throw new Error('Invalid object owner'); } return owner.AddressOwner; }