@mysten/sui
Version:
Sui TypeScript API
270 lines (268 loc) • 9.88 kB
JavaScript
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