@mysten/sui
Version:
Sui TypeScript API(Work in Progress)
336 lines (335 loc) • 12.1 kB
JavaScript
import { parse } from "valibot";
import { bcs } from "../bcs/index.js";
import { normalizeSuiAddress, normalizeSuiObjectId, SUI_TYPE_ARG } from "../utils/index.js";
import { ObjectRef } from "./data/internal.js";
import { Inputs } from "./Inputs.js";
import { getPureBcsSchema, isTxContext, normalizedTypeToMoveTypeSignature } from "./serializer.js";
const MAX_OBJECTS_PER_FETCH = 50;
const GAS_SAFE_OVERHEAD = 1000n;
const MAX_GAS = 5e10;
async function resolveTransactionData(transactionData, options, next) {
await normalizeInputs(transactionData, options);
await resolveObjectReferences(transactionData, options);
if (!options.onlyTransactionKind) {
await setGasPrice(transactionData, options);
await setGasBudget(transactionData, options);
await setGasPayment(transactionData, options);
}
await validate(transactionData);
return await next();
}
async function setGasPrice(transactionData, options) {
if (!transactionData.gasConfig.price) {
transactionData.gasConfig.price = String(await getClient(options).getReferenceGasPrice());
}
}
async function setGasBudget(transactionData, options) {
if (transactionData.gasConfig.budget) {
return;
}
const dryRunResult = await getClient(options).dryRunTransactionBlock({
transactionBlock: transactionData.build({
overrides: {
gasData: {
budget: String(MAX_GAS),
payment: []
}
}
})
});
if (dryRunResult.effects.status.status !== "success") {
throw new Error(
`Dry run failed, could not automatically determine a budget: ${dryRunResult.effects.status.error}`,
{ cause: dryRunResult }
);
}
const safeOverhead = GAS_SAFE_OVERHEAD * BigInt(transactionData.gasConfig.price || 1n);
const baseComputationCostWithOverhead = BigInt(dryRunResult.effects.gasUsed.computationCost) + safeOverhead;
const gasBudget = baseComputationCostWithOverhead + BigInt(dryRunResult.effects.gasUsed.storageCost) - BigInt(dryRunResult.effects.gasUsed.storageRebate);
transactionData.gasConfig.budget = String(
gasBudget > baseComputationCostWithOverhead ? gasBudget : baseComputationCostWithOverhead
);
}
async function setGasPayment(transactionData, options) {
if (!transactionData.gasConfig.payment) {
const coins = await getClient(options).getCoins({
owner: transactionData.gasConfig.owner || transactionData.sender,
coinType: SUI_TYPE_ARG
});
const paymentCoins = coins.data.filter((coin) => {
const matchingInput = transactionData.inputs.find((input) => {
if (input.Object?.ImmOrOwnedObject) {
return coin.coinObjectId === input.Object.ImmOrOwnedObject.objectId;
}
return false;
});
return !matchingInput;
}).map((coin) => ({
objectId: coin.coinObjectId,
digest: coin.digest,
version: coin.version
}));
if (!paymentCoins.length) {
throw new Error("No valid gas coins found for the transaction.");
}
transactionData.gasConfig.payment = paymentCoins.map((payment) => parse(ObjectRef, payment));
}
}
async function resolveObjectReferences(transactionData, options) {
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(
(chunk2) => getClient(options).multiGetObjects({
ids: chunk2,
options: { showOwner: true }
})
)
)).flat();
const responsesById = new Map(
dedupedIds.map((id, index) => {
return [id, resolved[index]];
})
);
const invalidObjects = Array.from(responsesById).filter(([_, obj]) => obj.error).map(([_, obj]) => JSON.stringify(obj.error));
if (invalidObjects.length) {
throw new Error(`The following input objects are invalid: ${invalidObjects.join(", ")}`);
}
const objects = resolved.map((object) => {
if (object.error || !object.data) {
throw new Error(`Failed to fetch object: ${object.error}`);
}
const owner = object.data.owner;
const initialSharedVersion = owner && typeof owner === "object" && "Shared" in owner ? owner.Shared.initial_shared_version : null;
return {
objectId: object.data.objectId,
digest: object.data.digest,
version: object.data.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: 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, options) {
const { inputs, commands } = transactionData;
const moveCallsToResolve = [];
const moveFunctionsToResolve = /* @__PURE__ */ new Set();
commands.forEach((command) => {
if (command.MoveCall) {
if (command.MoveCall._argumentTypes) {
return;
}
const inputs2 = command.MoveCall.arguments.map((arg) => {
if (arg.$kind === "Input") {
return transactionData.inputs[arg.Input];
}
return null;
});
const needsResolution = inputs2.some(
(input) => input?.UnresolvedPure || input?.UnresolvedObject
);
if (needsResolution) {
const functionName = `${command.MoveCall.package}::${command.MoveCall.module}::${command.MoveCall.function}`;
moveFunctionsToResolve.add(functionName);
moveCallsToResolve.push(command.MoveCall);
}
}
switch (command.$kind) {
case "SplitCoins":
command.SplitCoins.amounts.forEach((amount) => {
normalizeRawArgument(amount, bcs.U64, transactionData);
});
break;
case "TransferObjects":
normalizeRawArgument(command.TransferObjects.address, bcs.Address, transactionData);
break;
}
});
const moveFunctionParameters = /* @__PURE__ */ new Map();
if (moveFunctionsToResolve.size > 0) {
const client = getClient(options);
await Promise.all(
[...moveFunctionsToResolve].map(async (functionName) => {
const [packageId, moduleId, functionId] = functionName.split("::");
const def = await client.getNormalizedMoveFunction({
package: packageId,
module: moduleId,
function: functionId
});
moveFunctionParameters.set(
functionName,
def.parameters.map((param) => normalizedTypeToMoveTypeSignature(param))
);
})
);
}
if (moveCallsToResolve.length) {
await Promise.all(
moveCallsToResolve.map(async (moveCall) => {
const parameters = moveFunctionParameters.get(
`${moveCall.package}::${moveCall.module}::${moveCall.function}`
);
if (!parameters) {
return;
}
const hasTxContext = parameters.length > 0 && isTxContext(parameters.at(-1));
const params = hasTxContext ? parameters.slice(0, parameters.length - 1) : parameters;
moveCall._argumentTypes = params;
})
);
}
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 validate(transactionData) {
transactionData.inputs.forEach((input, index) => {
if (input.$kind !== "Object" && input.$kind !== "Pure") {
throw new Error(
`Input at index ${index} has not been resolved. Expected a Pure or Object input, but found ${JSON.stringify(
input
)}`
);
}
});
}
function normalizeRawArgument(arg, schema, transactionData) {
if (arg.$kind !== "Input") {
return;
}
const input = transactionData.inputs[arg.Input];
if (input.$kind !== "UnresolvedPure") {
return;
}
transactionData.inputs[arg.Input] = Inputs.Pure(schema.serialize(input.UnresolvedPure.value));
}
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].ref !== "&" || usedAsMutable;
}
if (tx.$kind === "MakeMoveVec" || tx.$kind === "MergeCoins" || tx.$kind === "SplitCoins") {
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;
}
function isReceivingType(type) {
if (typeof type.body !== "object" || !("datatype" in type.body)) {
return false;
}
return type.body.datatype.package === "0x2" && type.body.datatype.module === "transfer" && type.body.datatype.type === "Receiving";
}
function getClient(options) {
if (!options.client) {
throw new Error(
`No provider passed to Transaction#build, but transaction data was not sufficient to build offline.`
);
}
return options.client;
}
function chunk(arr, size) {
return Array.from(
{ length: Math.ceil(arr.length / size) },
(_, i) => arr.slice(i * size, i * size + size)
);
}
export {
getClient,
resolveTransactionData
};
//# sourceMappingURL=json-rpc-resolver.js.map