UNPKG

@mysten/sui

Version:

Sui TypeScript API(Work in Progress)

336 lines (335 loc) 12.1 kB
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