UNPKG

@mysten/sui

Version:
270 lines (268 loc) 9.88 kB
import { normalizeStructTag } from "../../utils/sui-types.mjs"; import { bcs as suiBcs } from "../../bcs/index.mjs"; import { TransactionCommands } from "../Commands.mjs"; import { Inputs } from "../Inputs.mjs"; import { bigint, object, optional, parse, picklist, string } from "valibot"; //#region src/transactions/intents/CoinWithBalance.ts const COIN_WITH_BALANCE = "CoinWithBalance"; const SUI_TYPE = normalizeStructTag("0x2::sui::SUI"); function coinWithBalance({ type = SUI_TYPE, balance, useGasCoin = true }) { let coinResult = null; return (tx) => { if (coinResult) return coinResult; tx.addIntentResolver(COIN_WITH_BALANCE, resolveCoinBalance); const coinType = type === "gas" ? type : normalizeStructTag(type); coinResult = tx.add(TransactionCommands.Intent({ name: COIN_WITH_BALANCE, inputs: {}, data: { type: coinType === SUI_TYPE && useGasCoin ? "gas" : coinType, balance: BigInt(balance), outputKind: "coin" } })); return coinResult; }; } function createBalance({ type = SUI_TYPE, balance, useGasCoin = true }) { let balanceResult = null; return (tx) => { if (balanceResult) return balanceResult; tx.addIntentResolver(COIN_WITH_BALANCE, resolveCoinBalance); const coinType = type === "gas" ? type : normalizeStructTag(type); balanceResult = tx.add(TransactionCommands.Intent({ name: COIN_WITH_BALANCE, inputs: {}, data: { type: coinType === SUI_TYPE && useGasCoin ? "gas" : coinType, balance: BigInt(balance), outputKind: "balance" } })); return balanceResult; }; } const CoinWithBalanceData = object({ type: string(), balance: bigint(), outputKind: optional(picklist(["coin", "balance"])) }); async function resolveCoinBalance(transactionData, buildOptions, next) { const coinTypes = /* @__PURE__ */ new Set(); const totalByType = /* @__PURE__ */ new Map(); const intentsByType = /* @__PURE__ */ new Map(); if (!transactionData.sender) throw new Error("Sender must be set to resolve CoinWithBalance"); for (const [i, command] of transactionData.commands.entries()) { if (command.$kind !== "$Intent" || command.$Intent.name !== COIN_WITH_BALANCE) continue; const { type, balance, outputKind } = parse(CoinWithBalanceData, command.$Intent.data); if (balance === 0n) { const coinType = type === "gas" ? SUI_TYPE : type; transactionData.replaceCommand(i, TransactionCommands.MoveCall({ target: (outputKind ?? "coin") === "balance" ? "0x2::balance::zero" : "0x2::coin::zero", typeArguments: [coinType] })); continue; } if (type !== "gas") coinTypes.add(type); totalByType.set(type, (totalByType.get(type) ?? 0n) + balance); if (!intentsByType.has(type)) intentsByType.set(type, []); intentsByType.get(type).push({ balance, outputKind: outputKind ?? "coin" }); } const usedIds = /* @__PURE__ */ new Set(); for (const input of transactionData.inputs) { if (input.Object?.ImmOrOwnedObject) usedIds.add(input.Object.ImmOrOwnedObject.objectId); if (input.UnresolvedObject?.objectId) usedIds.add(input.UnresolvedObject.objectId); } const coinsByType = /* @__PURE__ */ new Map(); const addressBalanceByType = /* @__PURE__ */ new Map(); const client = buildOptions.client; if (!client) throw new Error("Client must be provided to build or serialize transactions with CoinWithBalance intents"); await Promise.all([...[...coinTypes].map(async (coinType) => { const { coins, addressBalance } = await getCoinsAndBalanceOfType({ coinType, balance: totalByType.get(coinType), client, owner: transactionData.sender, usedIds }); coinsByType.set(coinType, coins); addressBalanceByType.set(coinType, addressBalance); }), totalByType.has("gas") ? await client.core.getBalance({ owner: transactionData.sender, coinType: SUI_TYPE }).then(({ balance }) => { addressBalanceByType.set("gas", BigInt(balance.addressBalance)); }) : null]); const mergedCoins = /* @__PURE__ */ new Map(); const exactBalanceByType = /* @__PURE__ */ new Map(); const typeState = /* @__PURE__ */ new Map(); let index = 0; while (index < transactionData.commands.length) { const transaction = transactionData.commands[index]; if (transaction.$kind !== "$Intent" || transaction.$Intent.name !== COIN_WITH_BALANCE) { index++; continue; } const { type, balance } = transaction.$Intent.data; const coinType = type === "gas" ? SUI_TYPE : type; const totalRequired = totalByType.get(type); const addressBalance = addressBalanceByType.get(type) ?? 0n; const commands = []; let intentResult; const intentsForType = intentsByType.get(type) ?? []; if (intentsForType.every((i) => i.outputKind === "balance") && addressBalance >= totalRequired) { commands.push(TransactionCommands.MoveCall({ target: "0x2::balance::redeem_funds", typeArguments: [coinType], arguments: [transactionData.addInput("withdrawal", Inputs.FundsWithdrawal({ reservation: { $kind: "MaxAmountU64", MaxAmountU64: String(balance) }, typeArg: { $kind: "Balance", Balance: coinType }, withdrawFrom: { $kind: "Sender", Sender: true } }))] })); intentResult = { $kind: "NestedResult", NestedResult: [index + commands.length - 1, 0] }; } else { if (!typeState.has(type)) { const intents = intentsForType; const sources = []; if (type === "gas") sources.push({ $kind: "GasCoin", GasCoin: true }); else { const coins = coinsByType.get(type); const loadedCoinBalance = coins.reduce((sum, c) => sum + BigInt(c.balance), 0n); const abNeeded = totalRequired > loadedCoinBalance ? totalRequired - loadedCoinBalance : 0n; exactBalanceByType.set(type, loadedCoinBalance + abNeeded === totalRequired); for (const coin of coins) sources.push(transactionData.addInput("object", Inputs.ObjectRef({ objectId: coin.objectId, digest: coin.digest, version: coin.version }))); if (abNeeded > 0n) { commands.push(TransactionCommands.MoveCall({ target: "0x2::coin::redeem_funds", typeArguments: [coinType], arguments: [transactionData.addInput("withdrawal", Inputs.FundsWithdrawal({ reservation: { $kind: "MaxAmountU64", MaxAmountU64: String(abNeeded) }, typeArg: { $kind: "Balance", Balance: coinType }, withdrawFrom: { $kind: "Sender", Sender: true } }))] })); sources.push({ $kind: "Result", Result: index + commands.length - 1 }); } } const baseCoin = sources[0]; const rest = sources.slice(1); for (let i = 0; i < rest.length; i += 500) commands.push(TransactionCommands.MergeCoins(baseCoin, rest.slice(i, i + 500))); mergedCoins.set(type, baseCoin); const splitCmdIndex = index + commands.length; commands.push(TransactionCommands.SplitCoins(baseCoin, intents.map((i) => transactionData.addInput("pure", Inputs.Pure(suiBcs.u64().serialize(i.balance)))))); const results = []; for (let i = 0; i < intents.length; i++) { const splitResult = { $kind: "NestedResult", NestedResult: [splitCmdIndex, i] }; if (intents[i].outputKind === "balance") { commands.push(TransactionCommands.MoveCall({ target: "0x2::coin::into_balance", typeArguments: [coinType], arguments: [splitResult] })); results.push({ $kind: "NestedResult", NestedResult: [index + commands.length - 1, 0] }); } else results.push(splitResult); } typeState.set(type, { results, nextIntent: 0 }); } const state = typeState.get(type); intentResult = state.results[state.nextIntent++]; } transactionData.replaceCommand(index, commands, intentResult); index += commands.length; } for (const [type, mergedCoin] of mergedCoins) { if (type === "gas") continue; if (intentsByType.get(type)?.some((i) => i.outputKind === "balance")) transactionData.commands.push(TransactionCommands.MoveCall({ target: "0x2::coin::send_funds", typeArguments: [type], arguments: [mergedCoin, transactionData.addInput("pure", Inputs.Pure(suiBcs.Address.serialize(transactionData.sender)))] })); else if (exactBalanceByType.get(type)) transactionData.commands.push(TransactionCommands.MoveCall({ target: "0x2::coin::destroy_zero", typeArguments: [type], arguments: [mergedCoin] })); } return next(); } async function getCoinsAndBalanceOfType({ coinType, balance, client, owner, usedIds }) { let remainingBalance = balance; const coins = []; const balanceRequest = client.core.getBalance({ owner, coinType }).then(({ balance: balance$1 }) => { remainingBalance -= BigInt(balance$1.addressBalance); return balance$1; }); const [allCoins, balanceResponse] = await Promise.all([loadMoreCoins(), balanceRequest]); if (BigInt(balanceResponse.balance) < balance) throw new Error(`Insufficient balance of ${coinType} for owner ${owner}. Required: ${balance}, Available: ${balance - remainingBalance}`); return { coins: allCoins, balance: BigInt(balanceResponse.coinBalance), addressBalance: BigInt(balanceResponse.addressBalance), coinBalance: BigInt(balanceResponse.coinBalance) }; async function loadMoreCoins(cursor = null) { const { objects, hasNextPage, cursor: nextCursor } = await client.core.listCoins({ owner, coinType, cursor }); await balanceRequest; for (const coin of objects) { if (usedIds.has(coin.objectId)) continue; coins.push(coin); remainingBalance -= BigInt(coin.balance); } if (remainingBalance > 0n && hasNextPage) return loadMoreCoins(nextCursor); return coins; } } //#endregion export { COIN_WITH_BALANCE, coinWithBalance, createBalance, resolveCoinBalance }; //# sourceMappingURL=CoinWithBalance.mjs.map