@mysten/sui
Version:
Sui TypeScript API
268 lines (266 loc) • 13.3 kB
JavaScript
import { normalizeStructTag, normalizeSuiAddress, normalizeSuiObjectId } from "../utils/sui-types.mjs";
import { SimulationError } from "./errors.mjs";
import { SUI_TYPE_ARG } from "../utils/constants.mjs";
import { createCoinReservationRef } from "../utils/coin-reservation.mjs";
import { Inputs } from "../transactions/Inputs.mjs";
import { getPureBcsSchema, isTxContext } from "../transactions/serializer.mjs";
import { chunk } from "@mysten/utils";
//#region src/client/core-resolver.ts
const MAX_OBJECTS_PER_FETCH = 50;
const GAS_SAFE_OVERHEAD = 1000n;
const MAX_GAS = 5e10;
function getClient(options) {
if (!options.client) throw new Error(`No sui client passed to Transaction#build, but transaction data was not sufficient to build offline.`);
return options.client;
}
async function coreClientResolveTransactionPlugin(transactionData, options, next) {
const client = getClient(options);
const needsGasPrice = !options.onlyTransactionKind && !transactionData.gasData.price;
const needsPayment = !options.onlyTransactionKind && !transactionData.gasData.payment;
const gasPayer = transactionData.gasData.owner ?? transactionData.sender;
let usesGasCoin = false;
let withdrawals = 0n;
transactionData.mapArguments((arg) => {
if (arg.$kind === "GasCoin") usesGasCoin = true;
return arg;
});
const normalizedGasPayer = gasPayer ? normalizeSuiAddress(gasPayer) : null;
for (const input of transactionData.inputs) {
if (input.$kind !== "FundsWithdrawal" || !normalizedGasPayer) continue;
if (normalizeStructTag(input.FundsWithdrawal.typeArg.Balance) !== SUI_TYPE_ARG) continue;
const withdrawalOwner = input.FundsWithdrawal.withdrawFrom.Sender ? transactionData.sender : gasPayer;
if (withdrawalOwner && normalizeSuiAddress(withdrawalOwner) === normalizedGasPayer && input.FundsWithdrawal.reservation.$kind === "MaxAmountU64") withdrawals += BigInt(input.FundsWithdrawal.reservation.MaxAmountU64);
}
const needsSystemState = needsGasPrice || needsPayment && usesGasCoin;
const [, systemStateResult, balanceResult, coinsResult, protocolConfigResult, chainIdResult] = await Promise.all([
normalizeInputs(transactionData, client),
needsSystemState ? client.core.getCurrentSystemState() : null,
needsPayment && gasPayer ? client.core.getBalance({ owner: gasPayer }) : null,
needsPayment && gasPayer ? client.core.listCoins({
owner: gasPayer,
coinType: SUI_TYPE_ARG
}) : null,
needsPayment && usesGasCoin ? client.core.getProtocolConfig() : null,
needsPayment && usesGasCoin ? client.core.getChainIdentifier() : null
]);
await resolveObjectReferences(transactionData, client);
if (!options.onlyTransactionKind) {
const systemState = systemStateResult?.systemState ?? null;
if (systemState && !transactionData.gasData.price) transactionData.gasData.price = systemState.referenceGasPrice;
await setGasBudget(transactionData, client);
if (needsPayment) {
if (!balanceResult || !coinsResult) throw new Error("Could not resolve gas payment: a gas owner or sender must be set to fetch balance and coins.");
setGasPayment({
transactionData,
balance: balanceResult,
coins: coinsResult,
usesGasCoin,
withdrawals,
protocolConfig: protocolConfigResult?.protocolConfig,
gasPayer,
chainIdentifier: chainIdResult?.chainIdentifier ?? null,
epoch: systemState?.epoch ?? null
});
}
if (!transactionData.expiration && transactionData.gasData.payment?.length === 0) await setExpiration(transactionData, client, systemState, chainIdResult?.chainIdentifier ?? null);
}
return await next();
}
async function setGasBudget(transactionData, client) {
if (transactionData.gasData.budget) return;
const simulateResult = await client.core.simulateTransaction({
transaction: transactionData.build({ overrides: { gasData: {
budget: String(MAX_GAS),
payment: []
} } }),
include: { effects: true }
});
if (simulateResult.$kind === "FailedTransaction") {
const executionError = simulateResult.FailedTransaction.status.error ?? void 0;
throw new SimulationError(`Transaction resolution failed: ${executionError?.message ?? "Unknown error"}`, {
cause: simulateResult,
executionError
});
}
const gasUsed = simulateResult.Transaction.effects.gasUsed;
const safeOverhead = GAS_SAFE_OVERHEAD * BigInt(transactionData.gasData.price || 1n);
const baseComputationCostWithOverhead = BigInt(gasUsed.computationCost) + safeOverhead;
const gasBudget = baseComputationCostWithOverhead + BigInt(gasUsed.storageCost) - BigInt(gasUsed.storageRebate);
transactionData.gasData.budget = String(gasBudget > baseComputationCostWithOverhead ? gasBudget : baseComputationCostWithOverhead);
}
function setGasPayment({ transactionData, balance, coins, usesGasCoin, withdrawals, protocolConfig, gasPayer, chainIdentifier, epoch }) {
const budget = BigInt(transactionData.gasData.budget);
const addressBalance = BigInt(balance.balance.addressBalance);
if (budget === 0n || !usesGasCoin && addressBalance >= budget + withdrawals) {
transactionData.gasData.payment = [];
return;
}
const filteredCoins = coins.objects.filter((coin) => {
return !transactionData.inputs.find((input) => {
if (input.Object?.ImmOrOwnedObject) return coin.objectId === input.Object.ImmOrOwnedObject.objectId;
return false;
});
});
const paymentCoins = filteredCoins.map((coin) => ({
objectId: coin.objectId,
digest: coin.digest,
version: coin.version
}));
const reservationAmount = addressBalance - withdrawals;
if (usesGasCoin && reservationAmount > 0n && protocolConfig?.featureFlags?.["enable_coin_reservation_obj_refs"] && chainIdentifier && epoch) transactionData.gasData.payment = [createCoinReservationRef(reservationAmount, gasPayer, chainIdentifier, epoch), ...paymentCoins];
else if (!filteredCoins.length) throw new Error("No valid gas coins found for the transaction.");
else transactionData.gasData.payment = paymentCoins;
}
async function setExpiration(transactionData, client, systemState, existingChainIdentifier = null) {
const [chainIdentifier, resolvedSystemState] = await Promise.all([existingChainIdentifier ?? client.core.getChainIdentifier().then((r) => r.chainIdentifier), systemState ?? client.core.getCurrentSystemState().then((r) => r.systemState)]);
const currentEpoch = BigInt(resolvedSystemState.epoch);
transactionData.expiration = {
$kind: "ValidDuring",
ValidDuring: {
minEpoch: String(currentEpoch),
maxEpoch: String(currentEpoch + 1n),
minTimestamp: null,
maxTimestamp: null,
chain: chainIdentifier,
nonce: Math.random() * 4294967296 >>> 0
}
};
}
async function resolveObjectReferences(transactionData, client) {
const objectsToResolve = transactionData.inputs.filter((input) => {
return input.UnresolvedObject && !(input.UnresolvedObject.version || input.UnresolvedObject?.initialSharedVersion);
});
const dedupedIds = [...new Set(objectsToResolve.map((input) => normalizeSuiObjectId(input.UnresolvedObject.objectId)))];
const objectChunks = dedupedIds.length ? chunk(dedupedIds, MAX_OBJECTS_PER_FETCH) : [];
const resolved = (await Promise.all(objectChunks.map((chunkIds) => client.core.getObjects({ objectIds: chunkIds })))).flatMap((result) => result.objects);
const responsesById = new Map(dedupedIds.map((id, index) => {
return [id, resolved[index]];
}));
const invalidObjects = Array.from(responsesById).filter(([_, obj]) => obj instanceof Error).map(([_, obj]) => obj.message);
if (invalidObjects.length) throw new Error(`The following input objects are invalid: ${invalidObjects.join(", ")}`);
const objects = resolved.map((object) => {
if (object instanceof Error) throw new Error(`Failed to fetch object: ${object.message}`);
const owner = object.owner;
const initialSharedVersion = owner && typeof owner === "object" ? owner.$kind === "Shared" ? owner.Shared.initialSharedVersion : owner.$kind === "ConsensusAddressOwner" ? owner.ConsensusAddressOwner.startVersion : null : null;
return {
objectId: object.objectId,
digest: object.digest,
version: object.version,
initialSharedVersion
};
});
const objectsById = new Map(dedupedIds.map((id, index) => {
return [id, objects[index]];
}));
for (const [index, input] of transactionData.inputs.entries()) {
if (!input.UnresolvedObject) continue;
let updated;
const id = normalizeSuiAddress(input.UnresolvedObject.objectId);
const object = objectsById.get(id);
if (input.UnresolvedObject.initialSharedVersion ?? object?.initialSharedVersion) updated = Inputs.SharedObjectRef({
objectId: id,
initialSharedVersion: input.UnresolvedObject.initialSharedVersion || object?.initialSharedVersion,
mutable: input.UnresolvedObject.mutable || isUsedAsMutable(transactionData, index)
});
else if (isUsedAsReceiving(transactionData, index)) updated = Inputs.ReceivingRef({
objectId: id,
digest: input.UnresolvedObject.digest ?? object?.digest,
version: input.UnresolvedObject.version ?? object?.version
});
transactionData.inputs[transactionData.inputs.indexOf(input)] = updated ?? Inputs.ObjectRef({
objectId: id,
digest: input.UnresolvedObject.digest ?? object?.digest,
version: input.UnresolvedObject.version ?? object?.version
});
}
}
async function normalizeInputs(transactionData, client) {
const { inputs, commands } = transactionData;
const moveCallsToResolve = [];
const moveFunctionsToResolve = /* @__PURE__ */ new Set();
commands.forEach((command) => {
if (command.MoveCall) {
if (command.MoveCall._argumentTypes) return;
if (command.MoveCall.arguments.map((arg) => {
if (arg.$kind === "Input") return transactionData.inputs[arg.Input];
return null;
}).some((input) => input?.UnresolvedPure || input?.UnresolvedObject && typeof input?.UnresolvedObject.mutable !== "boolean")) {
const functionName = `${command.MoveCall.package}::${command.MoveCall.module}::${command.MoveCall.function}`;
moveFunctionsToResolve.add(functionName);
moveCallsToResolve.push(command.MoveCall);
}
}
});
const moveFunctionParameters = /* @__PURE__ */ new Map();
if (moveFunctionsToResolve.size > 0) await Promise.all([...moveFunctionsToResolve].map(async (functionName) => {
const [packageId, moduleName, name] = functionName.split("::");
const { function: def } = await client.core.getMoveFunction({
packageId,
moduleName,
name
});
moveFunctionParameters.set(functionName, def.parameters);
}));
if (moveCallsToResolve.length) await Promise.all(moveCallsToResolve.map(async (moveCall) => {
const parameters = moveFunctionParameters.get(`${moveCall.package}::${moveCall.module}::${moveCall.function}`);
if (!parameters) return;
moveCall._argumentTypes = parameters.length > 0 && isTxContext(parameters.at(-1)) ? parameters.slice(0, parameters.length - 1) : parameters;
}));
commands.forEach((command) => {
if (!command.MoveCall) return;
const moveCall = command.MoveCall;
const fnName = `${moveCall.package}::${moveCall.module}::${moveCall.function}`;
const params = moveCall._argumentTypes;
if (!params) return;
if (params.length !== command.MoveCall.arguments.length) throw new Error(`Incorrect number of arguments for ${fnName}`);
params.forEach((param, i) => {
const arg = moveCall.arguments[i];
if (arg.$kind !== "Input") return;
const input = inputs[arg.Input];
if (!input.UnresolvedPure && !input.UnresolvedObject) return;
const inputValue = input.UnresolvedPure?.value ?? input.UnresolvedObject?.objectId;
const schema = getPureBcsSchema(param.body);
if (schema) {
arg.type = "pure";
inputs[inputs.indexOf(input)] = Inputs.Pure(schema.serialize(inputValue));
return;
}
if (typeof inputValue !== "string") throw new Error(`Expect the argument to be an object id string, got ${JSON.stringify(inputValue, null, 2)}`);
arg.type = "object";
const unresolvedObject = input.UnresolvedPure ? {
$kind: "UnresolvedObject",
UnresolvedObject: { objectId: inputValue }
} : input;
inputs[arg.Input] = unresolvedObject;
});
});
}
function isUsedAsMutable(transactionData, index) {
let usedAsMutable = false;
transactionData.getInputUses(index, (arg, tx) => {
if (tx.MoveCall && tx.MoveCall._argumentTypes) {
const argIndex = tx.MoveCall.arguments.indexOf(arg);
usedAsMutable = tx.MoveCall._argumentTypes[argIndex].reference !== "immutable" || usedAsMutable;
}
if (tx.$kind === "MakeMoveVec" || tx.$kind === "MergeCoins" || tx.$kind === "SplitCoins" || tx.$kind === "TransferObjects") usedAsMutable = true;
});
return usedAsMutable;
}
function isUsedAsReceiving(transactionData, index) {
let usedAsReceiving = false;
transactionData.getInputUses(index, (arg, tx) => {
if (tx.MoveCall && tx.MoveCall._argumentTypes) {
const argIndex = tx.MoveCall.arguments.indexOf(arg);
usedAsReceiving = isReceivingType(tx.MoveCall._argumentTypes[argIndex]) || usedAsReceiving;
}
});
return usedAsReceiving;
}
const RECEIVING_TYPE = "0x0000000000000000000000000000000000000000000000000000000000000002::transfer::Receiving";
function isReceivingType(type) {
if (type.body.$kind !== "datatype") return false;
return type.body.datatype.typeName === RECEIVING_TYPE;
}
//#endregion
export { coreClientResolveTransactionPlugin };
//# sourceMappingURL=core-resolver.mjs.map