UNPKG

@mysten/sui

Version:
957 lines (824 loc) 28 kB
// Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 import type { SerializedBcs } from '@mysten/bcs'; import { fromBase64, isSerializedBcs } from '@mysten/bcs'; import type { InferInput } from 'valibot'; import { is, parse } from 'valibot'; import type { SignatureWithBytes, Signer } from '../cryptography/index.js'; import { normalizeSuiAddress } from '../utils/sui-types.js'; import type { TransactionArgument } from './Commands.js'; import { TransactionCommands } from './Commands.js'; import type { CallArg, Command, Argument, ObjectRef } from './data/internal.js'; import { ArgumentSchema, NormalizedCallArg, ObjectRefSchema, TransactionExpiration, } from './data/internal.js'; import { serializeV1TransactionData } from './data/v1.js'; import { SerializedTransactionDataV2Schema } from './data/v2.js'; import { Inputs } from './Inputs.js'; import { needsTransactionResolution, resolveTransactionPlugin } from './resolve.js'; import type { BuildTransactionOptions, SerializeTransactionOptions, TransactionPlugin, } from './resolve.js'; import { createObjectMethods } from './object.js'; import { createPure } from './pure.js'; import { TransactionDataBuilder } from './TransactionData.js'; import { getIdFromCallArg } from './utils.js'; import { namedPackagesPlugin } from './plugins/NamedPackagesPlugin.js'; import { COIN_WITH_BALANCE, resolveCoinBalance, coinWithBalance, createBalance, } from './intents/CoinWithBalance.js'; import type { ClientWithCoreApi } from '../client/core.js'; export type TransactionObjectArgument = | Exclude<InferInput<typeof ArgumentSchema>, { Input: unknown; type?: 'pure' }> | (( tx: Transaction, ) => Exclude<InferInput<typeof ArgumentSchema>, { Input: unknown; type?: 'pure' }>) | AsyncTransactionThunk<TransactionResultArgument>; export type TransactionResult = Extract<Argument, { Result: unknown }> & Extract<Argument, { NestedResult: unknown }>[]; export type TransactionResultArgument = | Extract<Argument, { Result: unknown }> | readonly Extract<Argument, { NestedResult: unknown }>[]; export type AsyncTransactionThunk< T extends TransactionResultArgument | void = TransactionResultArgument | void, > = (tx: Transaction) => Promise<T | void>; function createTransactionResult( index: number | (() => number), length = Infinity, ): TransactionResult { const baseResult = { $kind: 'Result' as const, get Result() { return typeof index === 'function' ? index() : index; }, }; const nestedResults: { $kind: 'NestedResult'; NestedResult: [number, number]; }[] = []; const nestedResultFor = ( resultIndex: number, ): { $kind: 'NestedResult'; NestedResult: [number, number]; } => (nestedResults[resultIndex] ??= { $kind: 'NestedResult' as const, get NestedResult() { return [typeof index === 'function' ? index() : index, resultIndex] as [number, number]; }, }); return new Proxy(baseResult, { set() { throw new Error( 'The transaction result is a proxy, and does not support setting properties directly', ); }, // TODO: Instead of making this return a concrete argument, we should ideally // make it reference-based (so that this gets resolved at build-time), which // allows re-ordering transactions. get(target, property) { // This allows this transaction argument to be used in the singular form: if (property in target) { return Reflect.get(target, property); } // Support destructuring: if (property === Symbol.iterator) { return function* () { let i = 0; while (i < length) { yield nestedResultFor(i); i++; } }; } if (typeof property === 'symbol') return; const resultIndex = parseInt(property, 10); if (Number.isNaN(resultIndex) || resultIndex < 0) return; return nestedResultFor(resultIndex); }, }) as TransactionResult; } const TRANSACTION_BRAND = Symbol.for('@mysten/transaction') as never; interface SignOptions extends BuildTransactionOptions { signer: Signer; } export function isTransaction(obj: unknown): obj is TransactionLike { return !!obj && typeof obj === 'object' && (obj as any)[TRANSACTION_BRAND] === true; } export type TransactionObjectInput = string | CallArg | TransactionObjectArgument; type InputSection = (CallArg | InputSection)[]; type CommandSection = (Command | CommandSection)[]; type TransactionLike = { getData(): unknown; }; /** * Transaction Builder */ export class Transaction { #serializationPlugins: TransactionPlugin[]; #buildPlugins: TransactionPlugin[]; #intentResolvers = new Map<string, TransactionPlugin>(); #inputSection: InputSection = []; #commandSection: CommandSection = []; #availableResults: Set<number> = new Set(); #pendingPromises = new Set<Promise<unknown>>(); #added = new Map<(...args: any[]) => unknown, unknown>(); /** * Converts from a serialize transaction kind (built with `build({ onlyTransactionKind: true })`) to a `Transaction` class. * Supports either a byte array, or base64-encoded bytes. */ static fromKind(serialized: string | Uint8Array) { const tx = new Transaction(); tx.#data = TransactionDataBuilder.fromKindBytes( typeof serialized === 'string' ? fromBase64(serialized) : serialized, ); tx.#inputSection = tx.#data.inputs.slice(); tx.#commandSection = tx.#data.commands.slice(); tx.#availableResults = new Set(tx.#commandSection.map((_, i) => i)); return tx; } /** * Converts from a serialized transaction format to a `Transaction` class. * There are two supported serialized formats: * - A string returned from `Transaction#serialize`. The serialized format must be compatible, or it will throw an error. * - A byte array (or base64-encoded bytes) containing BCS transaction data. */ static from(transaction: string | Uint8Array | TransactionLike) { const newTransaction = new Transaction(); if (isTransaction(transaction)) { newTransaction.#data = TransactionDataBuilder.restore( transaction.getData() as InferInput<typeof SerializedTransactionDataV2Schema>, ); } else if (typeof transaction !== 'string' || !transaction.startsWith('{')) { newTransaction.#data = TransactionDataBuilder.fromBytes( typeof transaction === 'string' ? fromBase64(transaction) : transaction, ); } else { newTransaction.#data = TransactionDataBuilder.restore(JSON.parse(transaction)); } newTransaction.#inputSection = newTransaction.#data.inputs.slice(); newTransaction.#commandSection = newTransaction.#data.commands.slice(); newTransaction.#availableResults = new Set(newTransaction.#commandSection.map((_, i) => i)); if (!newTransaction.isPreparedForSerialization({ supportedIntents: [COIN_WITH_BALANCE] })) { throw new Error( 'Transaction has unresolved intents or async thunks. Call `prepareForSerialization` before copying.', ); } if (newTransaction.#data.commands.some((cmd) => cmd.$Intent?.name === COIN_WITH_BALANCE)) { newTransaction.addIntentResolver(COIN_WITH_BALANCE, resolveCoinBalance); } return newTransaction; } addSerializationPlugin(step: TransactionPlugin) { this.#serializationPlugins.push(step); } addBuildPlugin(step: TransactionPlugin) { this.#buildPlugins.push(step); } addIntentResolver(intent: string, resolver: TransactionPlugin) { if (this.#intentResolvers.has(intent) && this.#intentResolvers.get(intent) !== resolver) { throw new Error(`Intent resolver for ${intent} already exists`); } this.#intentResolvers.set(intent, resolver); } setSender(sender: string) { this.#data.sender = sender; } /** * Sets the sender only if it has not already been set. * This is useful for sponsored transaction flows where the sender may not be the same as the signer address. */ setSenderIfNotSet(sender: string) { if (!this.#data.sender) { this.#data.sender = sender; } } setExpiration(expiration?: InferInput<typeof TransactionExpiration> | null) { this.#data.expiration = expiration ? parse(TransactionExpiration, expiration) : null; } setGasPrice(price: number | bigint | string) { this.#data.gasData.price = String(price); } setGasBudget(budget: number | bigint | string) { this.#data.gasData.budget = String(budget); } setGasBudgetIfNotSet(budget: number | bigint | string) { if (this.#data.gasData.budget == null) { this.#data.gasData.budget = String(budget); } } setGasOwner(owner: string) { this.#data.gasData.owner = owner; } setGasPayment(payments: ObjectRef[]) { this.#data.gasData.payment = payments.map((payment) => parse(ObjectRefSchema, payment)); } #data: TransactionDataBuilder; /** Get a snapshot of the transaction data, in JSON form: */ getData() { return this.#data.snapshot(); } // Used to brand transaction classes so that they can be identified, even between multiple copies // of the builder. get [TRANSACTION_BRAND]() { return true; } // Temporary workaround for the wallet interface accidentally serializing transactions via postMessage get pure(): ReturnType<typeof createPure<Argument>> { Object.defineProperty(this, 'pure', { enumerable: false, value: createPure<Argument>((value): Argument => { if (isSerializedBcs(value)) { return this.#addInput('pure', { $kind: 'Pure', Pure: { bytes: value.toBase64(), }, }); } // TODO: we can also do some deduplication here return this.#addInput( 'pure', is(NormalizedCallArg, value) ? parse(NormalizedCallArg, value) : value instanceof Uint8Array ? Inputs.Pure(value) : { $kind: 'UnresolvedPure', UnresolvedPure: { value } }, ); }), }); return this.pure; } constructor() { this.#data = new TransactionDataBuilder(); this.#buildPlugins = []; this.#serializationPlugins = []; } /** Returns an argument for the gas coin, to be used in a transaction. */ get gas() { return { $kind: 'GasCoin' as const, GasCoin: true as const }; } /** * Creates a coin of the specified type and balance. * Sourced from address balance when available, falling back to owned coins. */ coin({ type, balance, useGasCoin, }: { balance: bigint | number; type?: string; useGasCoin?: boolean; }): TransactionResult { return this.add(coinWithBalance({ type, balance, useGasCoin })); } /** * Creates a Balance object of the specified type and balance. * Sourced from address balance when available, falling back to owned coins. */ balance({ type, balance, useGasCoin, }: { balance: bigint | number; type?: string; useGasCoin?: boolean; }): TransactionResult { return this.add(createBalance({ type, balance, useGasCoin })); } /** * Add a new object input to the transaction. */ object: ReturnType< typeof createObjectMethods<{ $kind: 'Input'; Input: number; type?: 'object' }> > = createObjectMethods( (value: TransactionObjectInput): { $kind: 'Input'; Input: number; type?: 'object' } => { if (typeof value === 'function') { return this.object(this.add(value as (tx: Transaction) => TransactionObjectArgument)); } if (typeof value === 'object' && is(ArgumentSchema, value)) { return value as { $kind: 'Input'; Input: number; type?: 'object' }; } const id = getIdFromCallArg(value); const inserted = this.#data.inputs.find((i) => id === getIdFromCallArg(i)); // Upgrade shared object inputs to mutable if needed: if ( inserted?.Object?.SharedObject && typeof value === 'object' && value.Object?.SharedObject ) { inserted.Object.SharedObject.mutable = inserted.Object.SharedObject.mutable || value.Object.SharedObject.mutable; } return inserted ? { $kind: 'Input', Input: this.#data.inputs.indexOf(inserted), type: 'object' } : this.#addInput( 'object', typeof value === 'string' ? { $kind: 'UnresolvedObject', UnresolvedObject: { objectId: normalizeSuiAddress(value) }, } : value, ); }, ); /** * Add a new object input to the transaction using the fully-resolved object reference. * If you only have an object ID, use `builder.object(id)` instead. */ objectRef(...args: Parameters<(typeof Inputs)['ObjectRef']>) { return this.object(Inputs.ObjectRef(...args)); } /** * Add a new receiving input to the transaction using the fully-resolved object reference. * If you only have an object ID, use `builder.object(id)` instead. */ receivingRef(...args: Parameters<(typeof Inputs)['ReceivingRef']>) { return this.object(Inputs.ReceivingRef(...args)); } /** * Add a new shared object input to the transaction using the fully-resolved shared object reference. * If you only have an object ID, use `builder.object(id)` instead. */ sharedObjectRef(...args: Parameters<(typeof Inputs)['SharedObjectRef']>) { return this.object(Inputs.SharedObjectRef(...args)); } #fork() { const fork = new Transaction(); fork.#data = this.#data; fork.#serializationPlugins = this.#serializationPlugins; fork.#buildPlugins = this.#buildPlugins; fork.#intentResolvers = this.#intentResolvers; fork.#pendingPromises = this.#pendingPromises; fork.#availableResults = new Set(this.#availableResults); fork.#added = this.#added; this.#inputSection.push(fork.#inputSection); this.#commandSection.push(fork.#commandSection); return fork; } /** Add a transaction to the transaction */ add<T extends Command>(command: T): TransactionResult; add<T extends void | TransactionResultArgument | TransactionArgument | Command>( thunk: (tx: Transaction) => T, ): T; add<T extends TransactionResultArgument | void>( asyncTransactionThunk: AsyncTransactionThunk<T>, ): T; add(command: Command | AsyncTransactionThunk | ((tx: Transaction) => unknown)): unknown { if (typeof command === 'function') { if (this.#added.has(command)) { return this.#added.get(command); } const fork = this.#fork(); const result = command(fork); if (!(result && typeof result === 'object' && 'then' in result)) { this.#availableResults = fork.#availableResults; this.#added.set(command, result); return result; } const placeholder = this.#addCommand({ $kind: '$Intent', $Intent: { name: 'AsyncTransactionThunk', inputs: {}, data: { resultIndex: this.#data.commands.length, result: null as TransactionResult | null, }, }, }); this.#pendingPromises.add( Promise.resolve(result as Promise<TransactionResult>).then((result) => { placeholder.$Intent.data.result = result; }), ); const txResult = createTransactionResult(() => placeholder.$Intent.data.resultIndex); this.#added.set(command, txResult); return txResult; } else { this.#addCommand(command); } return createTransactionResult(this.#data.commands.length - 1); } #addCommand<T extends Command>(command: T) { const resultIndex = this.#data.commands.length; this.#commandSection.push(command); this.#availableResults.add(resultIndex); this.#data.commands.push(command); this.#data.mapCommandArguments(resultIndex, (arg) => { if (arg.$kind === 'Result' && !this.#availableResults.has(arg.Result)) { throw new Error( `Result { Result: ${arg.Result} } is not available to use in the current transaction`, ); } if (arg.$kind === 'NestedResult' && !this.#availableResults.has(arg.NestedResult[0])) { throw new Error( `Result { NestedResult: [${arg.NestedResult[0]}, ${arg.NestedResult[1]}] } is not available to use in the current transaction`, ); } if (arg.$kind === 'Input' && arg.Input >= this.#data.inputs.length) { throw new Error( `Input { Input: ${arg.Input} } references an input that does not exist in the current transaction`, ); } return arg; }); return command; } #addInput<T extends 'pure' | 'object'>(type: T, input: CallArg) { this.#inputSection.push(input); return this.#data.addInput(type, input); } #normalizeTransactionArgument(arg: TransactionArgument | SerializedBcs<any>) { if (isSerializedBcs(arg)) { return this.pure(arg); } return this.#resolveArgument(arg as TransactionArgument); } #resolveArgument(arg: TransactionArgument): Argument { if (typeof arg === 'function') { const resolved = this.add(arg as never); if (typeof resolved === 'function') { return this.#resolveArgument(resolved); } return parse(ArgumentSchema, resolved); } return parse(ArgumentSchema, arg); } // Method shorthands: splitCoins< const Amounts extends (TransactionArgument | SerializedBcs<any> | number | string | bigint)[], >(coin: TransactionObjectArgument | string, amounts: Amounts) { const command = TransactionCommands.SplitCoins( typeof coin === 'string' ? this.object(coin) : this.#resolveArgument(coin), amounts.map((amount) => typeof amount === 'number' || typeof amount === 'bigint' || typeof amount === 'string' ? this.pure.u64(amount) : this.#normalizeTransactionArgument(amount), ), ); this.#addCommand(command); return createTransactionResult(this.#data.commands.length - 1, amounts.length) as Extract< Argument, { Result: unknown } > & { [K in keyof Amounts]: Extract<Argument, { NestedResult: unknown }>; }; } mergeCoins( destination: TransactionObjectArgument | string, sources: (TransactionObjectArgument | string)[], ) { return this.add( TransactionCommands.MergeCoins( this.object(destination), sources.map((src) => this.object(src)), ), ); } publish({ modules, dependencies }: { modules: number[][] | string[]; dependencies: string[] }) { return this.add( TransactionCommands.Publish({ modules, dependencies, }), ); } upgrade({ modules, dependencies, package: packageId, ticket, }: { modules: number[][] | string[]; dependencies: string[]; package: string; ticket: TransactionObjectArgument | string; }) { return this.add( TransactionCommands.Upgrade({ modules, dependencies, package: packageId, ticket: this.object(ticket), }), ); } moveCall({ arguments: args, ...input }: | { package: string; module: string; function: string; arguments?: (TransactionArgument | SerializedBcs<any>)[]; typeArguments?: string[]; } | { target: string; arguments?: (TransactionArgument | SerializedBcs<any>)[]; typeArguments?: string[]; }) { return this.add( TransactionCommands.MoveCall({ ...input, arguments: args?.map((arg) => this.#normalizeTransactionArgument(arg)), } as Parameters<typeof TransactionCommands.MoveCall>[0]), ); } transferObjects( objects: (TransactionObjectArgument | string)[], address: TransactionArgument | SerializedBcs<any> | string, ) { return this.add( TransactionCommands.TransferObjects( objects.map((obj) => this.object(obj)), typeof address === 'string' ? this.pure.address(address) : this.#normalizeTransactionArgument(address), ), ); } makeMoveVec({ type, elements, }: { elements: (TransactionObjectArgument | string)[]; type?: string; }) { return this.add( TransactionCommands.MakeMoveVec({ type, elements: elements.map((obj) => this.object(obj)), }), ); } /** * Create a FundsWithdrawal input for withdrawing Balance<T> from an address balance accumulator. * This is used for gas payments from address balances. * * @param options.amount - The Amount to withdraw (u64). * @param options.type - The balance type (e.g., "0x2::sui::SUI"). Defaults to SUI. */ withdrawal({ amount, type }: { amount: number | bigint | string; type?: string | null }): { $kind: 'Input'; Input: number; type?: 'object'; } { const input: CallArg = { $kind: 'FundsWithdrawal', FundsWithdrawal: { // TODO: support entire balance withdrawals once supported reservation: { $kind: 'MaxAmountU64', MaxAmountU64: String(amount) }, typeArg: { $kind: 'Balance', Balance: type ?? '0x2::sui::SUI' }, withdrawFrom: // fromSponsor === true // ? { $kind: 'Sponsor', Sponsor: true } : // TODO: currently only supporting withdrawals from sender { $kind: 'Sender', Sender: true }, }, }; return this.#addInput('object', input); } /** * @deprecated Use toJSON instead. * For synchronous serialization, you can use `getData()` * */ serialize() { return JSON.stringify(serializeV1TransactionData(this.#data.snapshot())); } async toJSON(options: SerializeTransactionOptions = {}): Promise<string> { await this.prepareForSerialization(options); const fullyResolved = this.isFullyResolved(); return JSON.stringify( parse( SerializedTransactionDataV2Schema, fullyResolved ? { ...this.#data.snapshot(), digest: this.#data.getDigest(), } : this.#data.snapshot(), ), (_key, value) => (typeof value === 'bigint' ? value.toString() : value), 2, ); } /** Build the transaction to BCS bytes, and sign it with the provided keypair. */ async sign(options: SignOptions): Promise<SignatureWithBytes> { const { signer, ...buildOptions } = options; const bytes = await this.build(buildOptions); return signer.signTransaction(bytes); } /** * Checks if the transaction is prepared for serialization to JSON. * This means: * - All async thunks have been fully resolved * - All transaction intents have been resolved (unless in supportedIntents) * * Unlike `isFullyResolved()`, this does not require the sender, gas payment, * budget, or object versions to be set. */ isPreparedForSerialization(options: { supportedIntents?: string[] } = {}) { if (this.#pendingPromises.size > 0) { return false; } if ( this.#data.commands.some( (cmd) => cmd.$Intent && !options.supportedIntents?.includes(cmd.$Intent.name), ) ) { return false; } return true; } /** * Ensures that: * - All objects have been fully resolved to a specific version * - All pure inputs have been serialized to bytes * - All async thunks have been fully resolved * - All transaction intents have been resolved * - The gas payment, budget, and price have been set * - The transaction sender has been set * * When true, the transaction will always be built to the same bytes and digest (unless the transaction is mutated) */ isFullyResolved() { if (!this.isPreparedForSerialization()) { return false; } if (!this.#data.sender) { return false; } if (needsTransactionResolution(this.#data, {})) { return false; } return true; } /** Build the transaction to BCS bytes. */ async build(options: BuildTransactionOptions = {}): Promise<Uint8Array<ArrayBuffer>> { await this.prepareForSerialization(options); await this.#prepareBuild(options); return this.#data.build({ onlyTransactionKind: options.onlyTransactionKind, }); } /** Derive transaction digest */ async getDigest( options: { client?: ClientWithCoreApi; } = {}, ): Promise<string> { await this.prepareForSerialization(options); await this.#prepareBuild(options); return this.#data.getDigest(); } /** * Prepare the transaction by validating the transaction data and resolving all inputs * so that it can be built into bytes. */ async #prepareBuild(options: BuildTransactionOptions) { if (!options.onlyTransactionKind && !this.#data.sender) { throw new Error('Missing transaction sender'); } await this.#runPlugins([...this.#buildPlugins, resolveTransactionPlugin], options); } async #runPlugins(plugins: TransactionPlugin[], options: SerializeTransactionOptions) { try { const createNext = (i: number) => { if (i >= plugins.length) { return () => {}; } const plugin = plugins[i]; return async () => { const next = createNext(i + 1); let calledNext = false; let nextResolved = false; await plugin(this.#data, options, async () => { if (calledNext) { throw new Error(`next() was call multiple times in TransactionPlugin ${i}`); } calledNext = true; await next(); nextResolved = true; }); if (!calledNext) { throw new Error(`next() was not called in TransactionPlugin ${i}`); } if (!nextResolved) { throw new Error(`next() was not awaited in TransactionPlugin ${i}`); } }; }; await createNext(0)(); } finally { this.#inputSection = this.#data.inputs.slice(); this.#commandSection = this.#data.commands.slice(); this.#availableResults = new Set(this.#commandSection.map((_, i) => i)); } } async #waitForPendingTasks() { while (this.#pendingPromises.size > 0) { const newPromise = Promise.all(this.#pendingPromises); this.#pendingPromises.clear(); this.#pendingPromises.add(newPromise); await newPromise; this.#pendingPromises.delete(newPromise); } } #sortCommandsAndInputs() { const unorderedCommands = this.#data.commands; const unorderedInputs = this.#data.inputs; const orderedCommands = (this.#commandSection as Command[]).flat(Infinity); const orderedInputs = (this.#inputSection as CallArg[]).flat(Infinity); if (orderedCommands.length !== unorderedCommands.length) { throw new Error('Unexpected number of commands found in transaction data'); } if (orderedInputs.length !== unorderedInputs.length) { throw new Error('Unexpected number of inputs found in transaction data'); } const filteredCommands = orderedCommands.filter( (cmd) => cmd.$Intent?.name !== 'AsyncTransactionThunk', ); this.#data.commands = filteredCommands; this.#data.inputs = orderedInputs; this.#commandSection = filteredCommands; this.#inputSection = orderedInputs; this.#availableResults = new Set(filteredCommands.map((_, i) => i)); function getOriginalIndex(index: number): number { const command = unorderedCommands[index]; if (command.$Intent?.name === 'AsyncTransactionThunk') { const result = command.$Intent.data.result as TransactionResult | null; if (result == null) { throw new Error('AsyncTransactionThunk has not been resolved'); } return getOriginalIndex(result.Result); } const updated = filteredCommands.indexOf(command); if (updated === -1) { throw new Error('Unable to find original index for command'); } return updated; } this.#data.mapArguments((arg) => { if (arg.$kind === 'Input') { const updated = orderedInputs.indexOf(unorderedInputs[arg.Input]); if (updated === -1) { throw new Error('Input has not been resolved'); } return { ...arg, Input: updated }; } else if (arg.$kind === 'Result') { const updated = getOriginalIndex(arg.Result); return { ...arg, Result: updated }; } else if (arg.$kind === 'NestedResult') { const updated = getOriginalIndex(arg.NestedResult[0]); return { ...arg, NestedResult: [updated, arg.NestedResult[1]] }; } return arg; }); for (const [i, cmd] of unorderedCommands.entries()) { if (cmd.$Intent?.name === 'AsyncTransactionThunk') { try { cmd.$Intent.data.resultIndex = getOriginalIndex(i); } catch { // If async thunk did not return a result, this will error, but is safe to ignore } } } } async prepareForSerialization(options: SerializeTransactionOptions) { await this.#waitForPendingTasks(); this.#sortCommandsAndInputs(); const intents = new Set<string>(); for (const command of this.#data.commands) { if (command.$Intent) { intents.add(command.$Intent.name); } } const steps = [...this.#serializationPlugins]; for (const intent of intents) { if (options.supportedIntents?.includes(intent)) { continue; } if (!this.#intentResolvers.has(intent)) { throw new Error(`Missing intent resolver for ${intent}`); } steps.push(this.#intentResolvers.get(intent)!); } steps.push(namedPackagesPlugin()); await this.#runPlugins(steps, options); } }