From a16ab70cdeda055a19cc0abac6351981ab392684 Mon Sep 17 00:00:00 2001 From: Michael Hayes Date: Wed, 22 Jul 2026 13:06:40 -0700 Subject: [PATCH 1/7] feat(codegen): config-mapped arguments and AccumulatorRoot well-known object Adds a configArguments option to @mysten/codegen that maps Move types and package addresses to keys of a runtime config object. Matched function parameters become optional in generated arguments and resolve from a typed per-function config slice instead; generic types require resolver functions that receive the matched parameter's instantiation. Also treats 0x2::accumulator::AccumulatorRoot as a well-known auto-injected object. Co-Authored-By: Claude Fable 5 --- .changeset/config-mapped-arguments.md | 9 + .../codegen/src/cli/commands/generate/impl.ts | 1 + packages/codegen/src/config-arguments.ts | 295 +++++++ packages/codegen/src/config.ts | 49 +- packages/codegen/src/generate-utils.ts | 76 +- packages/codegen/src/index.ts | 113 ++- packages/codegen/src/module-registry.ts | 28 + packages/codegen/src/move-module-builder.ts | 155 +++- packages/codegen/src/render-types.ts | 55 ++ packages/codegen/src/utils.ts | 4 + packages/codegen/tests/codegen-output.test.ts | 62 ++ .../codegen/tests/config-arguments.test.ts | 791 ++++++++++++++++++ packages/codegen/tests/utils.test.ts | 105 +++ packages/docs/content/codegen/index.mdx | 147 +++- 14 files changed, 1865 insertions(+), 25 deletions(-) create mode 100644 .changeset/config-mapped-arguments.md create mode 100644 packages/codegen/src/config-arguments.ts create mode 100644 packages/codegen/tests/config-arguments.test.ts diff --git a/.changeset/config-mapped-arguments.md b/.changeset/config-mapped-arguments.md new file mode 100644 index 000000000..a7285a745 --- /dev/null +++ b/.changeset/config-mapped-arguments.md @@ -0,0 +1,9 @@ +--- +'@mysten/codegen': minor +--- + +Add `configArguments` codegen option for mapping function parameters and package addresses to a +runtime config object. Matched parameters become optional in generated `arguments` and are resolved +from a typed `config` object instead (with per-function minimal config slices, resolver functions +for generic types, and a generated per-package config interface in `config-args.ts`). Also treat +`0x2::accumulator::AccumulatorRoot` as a well-known object that is auto-injected like `Clock`. diff --git a/packages/codegen/src/cli/commands/generate/impl.ts b/packages/codegen/src/cli/commands/generate/impl.ts index 9427f19a5..a1db29738 100644 --- a/packages/codegen/src/cli/commands/generate/impl.ts +++ b/packages/codegen/src/cli/commands/generate/impl.ts @@ -149,6 +149,7 @@ export default async function generate( importExtension, includePhantomTypeParameters: config.includePhantomTypeParameters, errorClass: config.errorClass, + configArguments: config.configArguments, }); } } diff --git a/packages/codegen/src/config-arguments.ts b/packages/codegen/src/config-arguments.ts new file mode 100644 index 000000000..a0258732b --- /dev/null +++ b/packages/codegen/src/config-arguments.ts @@ -0,0 +1,295 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { normalizeSuiAddress } from '@mysten/sui/utils'; +import type { ConfigArguments } from './config.js'; +import type { ModuleRegistry } from './module-registry.js'; +import type { Parameter, Type } from './types/summary.js'; + +/** A parsed type tag from a `configArguments` matcher. Always fully concrete. */ +export type ParsedTypeTag = + | { prim: string } + | { vector: ParsedTypeTag } + | { datatype: { address: string; module: string; name: string; typeArguments: ParsedTypeTag[] } }; + +export interface TypeConfigArgument { + kind: 'type'; + key: string; + address: string; + module: string; + name: string; + /** `null` when the matcher is written without type arguments (matches every instantiation). */ + typeArguments: ParsedTypeTag[] | null; + paramName?: string; + /** + * Whether the matched Move type is generic. Uninstantiated matchers on generic types require a + * resolver function as the config value (a static id cannot be correct across instantiations). + */ + isGeneric: boolean; +} + +export interface PackageConfigArgument { + kind: 'package'; + key: string; + package: string; +} + +export type ParsedConfigArgument = TypeConfigArgument | PackageConfigArgument; + +const PRIMITIVES = new Set(['bool', 'u8', 'u16', 'u32', 'u64', 'u128', 'u256', 'address']); + +function splitTopLevelTypeArgs(inner: string): string[] { + const parts: string[] = []; + let depth = 0; + let current = ''; + for (const char of inner) { + if (char === ',' && depth === 0) { + parts.push(current.trim()); + current = ''; + continue; + } + if (char === '<') depth++; + if (char === '>') depth--; + current += char; + } + if (current.trim()) parts.push(current.trim()); + return parts; +} + +function parseTypeTag(tag: string, resolveAddress: (address: string) => string): ParsedTypeTag { + const trimmed = tag.trim(); + + if (PRIMITIVES.has(trimmed)) { + return { prim: trimmed }; + } + + if (trimmed.startsWith('vector<') && trimmed.endsWith('>')) { + return { vector: parseTypeTag(trimmed.slice('vector<'.length, -1), resolveAddress) }; + } + + const lt = trimmed.indexOf('<'); + const base = lt === -1 ? trimmed : trimmed.slice(0, lt); + const parts = base.split('::'); + + if (parts.length !== 3 || parts.some((part) => part.length === 0)) { + throw new Error( + `Invalid type in configArguments matcher: "${tag}". Expected a fully-qualified Move type like "0x2::sui::SUI".`, + ); + } + + if (lt !== -1 && !trimmed.endsWith('>')) { + throw new Error(`Invalid type in configArguments matcher: "${tag}"`); + } + + const typeArguments = + lt === -1 + ? [] + : splitTopLevelTypeArgs(trimmed.slice(lt + 1, -1)).map((arg) => + parseTypeTag(arg, resolveAddress), + ); + + return { + datatype: { + address: resolveMatcherAddress(parts[0], resolveAddress), + module: parts[1], + name: parts[2], + typeArguments, + }, + }; +} + +const HEX_ADDRESS = /^0x[0-9a-fA-F]{1,64}$/; + +function resolveMatcherAddress(address: string, resolveAddress: (address: string) => string) { + const resolved = resolveAddress(address); + return HEX_ADDRESS.test(resolved) ? normalizeSuiAddress(resolved) : resolved; +} + +/** + * Parse and validate a `configArguments` record against the modules loaded in `registry`. + * All addresses (in matchers and during matching) are resolved through the registry's address + * mapping and normalized, so matchers can use named addresses from `address_mapping.json`. + * + * Type matchers referencing types that don't exist in this package's summaries are returned in + * `unresolvedKeys` instead of the entry list — a shared global `configArguments` block may span + * multiple packages in one codegen run, and entries for other packages can't match anything here. + */ +export function parseConfigArguments( + configArguments: ConfigArguments, + registry: ModuleRegistry, +): { entries: ParsedConfigArgument[]; unresolvedKeys: string[] } { + const resolveAddress = (address: string) => registry.resolveAddress(address); + const entries: ParsedConfigArgument[] = []; + const unresolvedKeys: string[] = []; + + for (const [key, matcher] of Object.entries(configArguments)) { + if ('package' in matcher) { + entries.push({ kind: 'package', key, package: matcher.package }); + continue; + } + + const parsed = parseTypeTag(matcher.type, resolveAddress); + + if (!('datatype' in parsed)) { + throw new Error( + `configArguments.${key}: matcher type "${matcher.type}" must be a Move datatype`, + ); + } + + const { address, module, name, typeArguments } = parsed.datatype; + const summary = registry.getSummaryByResolvedAddress(address, module); + const datatype = summary?.structs[name] ?? summary?.enums[name]; + + if (!datatype) { + unresolvedKeys.push(key); + continue; + } + + const arity = datatype.type_parameters.length; + const isGeneric = arity > 0; + // A generic type written without `<...>` matches every instantiation. + const uninstantiated = isGeneric && !matcher.type.includes('<'); + + if (!uninstantiated && typeArguments.length !== arity) { + throw new Error( + `configArguments.${key}: type "${matcher.type}" expects ${arity} type argument(s), got ${typeArguments.length}`, + ); + } + + entries.push({ + kind: 'type', + key, + address, + module, + name, + typeArguments: uninstantiated ? null : typeArguments, + paramName: matcher.name, + isGeneric, + }); + } + + return { entries, unresolvedKeys }; +} + +function typeEqualsTag( + type: Type, + tag: ParsedTypeTag, + resolveAddress: (address: string) => string, +): boolean { + if (typeof type === 'string') { + return 'prim' in tag && tag.prim === type; + } + + if ('Reference' in type) { + return typeEqualsTag(type.Reference[1], tag, resolveAddress); + } + + if ('vector' in type) { + return 'vector' in tag && typeEqualsTag(type.vector, tag.vector, resolveAddress); + } + + if ('Datatype' in type) { + if (!('datatype' in tag)) return false; + const { Datatype } = type; + return ( + resolveMatcherAddress(Datatype.module.address, resolveAddress) === tag.datatype.address && + Datatype.module.name === tag.datatype.module && + Datatype.name === tag.datatype.name && + Datatype.type_arguments.length === tag.datatype.typeArguments.length && + Datatype.type_arguments.every((arg, i) => + typeEqualsTag(arg.argument, tag.datatype.typeArguments[i], resolveAddress), + ) + ); + } + + // TypeParameter / NamedTypeParameter / tuple / fun never match a concrete tag. + return false; +} + +/** + * Find the config entry matching a function parameter, or `null`. + * + * Most-specific matcher wins, decided statically: a fully instantiated matcher beats an + * uninstantiated one, and a `name`-refined matcher beats a bare one at the same level. Ties are a + * hard generation-time error. + */ +export function findConfigArgumentMatch( + param: Parameter, + entries: ParsedConfigArgument[], + { + resolveAddress, + functionLabel, + }: { + resolveAddress: (address: string) => string; + functionLabel: string; + }, +): TypeConfigArgument | null { + let type = param.type_; + while (typeof type !== 'string' && 'Reference' in type) { + type = type.Reference[1]; + } + + if (typeof type === 'string' || !('Datatype' in type)) { + return null; + } + + const { Datatype } = type; + const paramAddress = resolveMatcherAddress(Datatype.module.address, resolveAddress); + + const candidates: { entry: TypeConfigArgument; specificity: number }[] = []; + + for (const entry of entries) { + if (entry.kind !== 'type') continue; + if ( + entry.address !== paramAddress || + entry.module !== Datatype.module.name || + entry.name !== Datatype.name + ) { + continue; + } + + if (entry.paramName && param.name === undefined) { + throw new Error( + `configArguments.${entry.key} uses a parameter-name matcher, but parameters of ${functionLabel} have no names. ` + + `Name matchers are only supported for summaries generated from local packages.`, + ); + } + + if (entry.paramName && entry.paramName !== param.name) { + continue; + } + + if (entry.typeArguments !== null) { + // Fully instantiated matcher: only matches parameters concretely typed with that + // exact instantiation in the Move signature. + if ( + Datatype.type_arguments.length !== entry.typeArguments.length || + !Datatype.type_arguments.every((arg, i) => + typeEqualsTag(arg.argument, entry.typeArguments![i], resolveAddress), + ) + ) { + continue; + } + candidates.push({ entry, specificity: 2 + (entry.paramName ? 1 : 0) }); + } else { + candidates.push({ entry, specificity: entry.paramName ? 1 : 0 }); + } + } + + if (candidates.length === 0) { + return null; + } + + const best = Math.max(...candidates.map((c) => c.specificity)); + const winners = candidates.filter((c) => c.specificity === best); + + if (winners.length > 1) { + throw new Error( + `Parameter ${param.name ?? ''} of ${functionLabel} is matched by multiple configArguments entries with equal specificity: ${winners + .map((c) => c.entry.key) + .join(', ')}. Refine the matchers with type arguments or a parameter name.`, + ); + } + + return winners[0].entry; +} diff --git a/packages/codegen/src/config.ts b/packages/codegen/src/config.ts index 95aa8a56d..807b2973d 100644 --- a/packages/codegen/src/config.ts +++ b/packages/codegen/src/config.ts @@ -32,6 +32,45 @@ export const moduleGenerateSchema = z.object({ types: typesOptionSchema.optional(), }); +const IDENTIFIER = /^[A-Za-z_$][\w$]*$/; + +export const configArgumentMatcherSchema = z.union([ + z.object({ + /** + * Fully-qualified Move type to match function parameters against, e.g. + * `0x...::margin_registry::MarginRegistry`. A generic type written without type arguments + * (e.g. `0x...::pool::Pool`) matches every instantiation and requires a resolver function + * as the config value. A fully instantiated generic (e.g. `0x...::margin_pool::MarginPool<0x2::sui::SUI>`) + * only matches parameters concretely typed with that exact instantiation. + */ + type: z.string(), + /** + * Optional Move parameter-name refinement, for signatures with two parameters of the same + * matched type. Only supported for summaries generated from local packages (bytecode + * summaries do not include parameter names). + */ + name: z.string().optional(), + }), + z.object({ + /** + * Package entry, keyed by the package's name/MVR name from the `packages` config. Adds an + * optional config key that overrides the package address used for generated calls. + */ + package: z.string(), + }), +]); + +export const configArgumentsSchema = z.record( + z.string().regex(IDENTIFIER, { + message: + 'configArguments keys become properties of the generated config interface and must be valid identifiers', + }), + configArgumentMatcherSchema, +); + +export type ConfigArgumentMatcher = z.infer; +export type ConfigArguments = z.infer; + export const packageGenerateSchema = globalGenerateSchema.extend({ modules: z .union([ @@ -49,6 +88,7 @@ export const onChainPackageSchema = z.object({ path: z.never().optional(), network: z.enum(['mainnet', 'testnet']), generate: packageGenerateSchema.optional(), + configArguments: configArgumentsSchema.optional(), }); export const localPackageSchema = z.object({ @@ -56,6 +96,7 @@ export const localPackageSchema = z.object({ package: z.string(), packageName: z.string().optional(), generate: packageGenerateSchema.optional(), + configArguments: configArgumentsSchema.optional(), }); export const packageConfigSchema = z.union([onChainPackageSchema, localPackageSchema]); @@ -68,8 +109,6 @@ export type PackageGenerate = z.infer; export type FunctionsOption = z.infer; export type TypesOption = z.infer; -const IDENTIFIER = /^[A-Za-z_$][\w$]*$/; - export const errorClassSchema = z.object({ name: z.string().regex(IDENTIFIER, { message: 'errorClass.name must start with a letter, $ or _ and contain only [A-Za-z0-9_$]', @@ -84,6 +123,12 @@ export const configSchema = z.object({ generateSummaries: z.boolean().optional().default(true), packages: z.array(packageConfigSchema), generate: globalGenerateSchema.optional(), + /** + * Maps config-object keys to Move type (or package) matchers. Matched function parameters are + * resolved from a runtime config object instead of being required arguments. Per-package + * `configArguments` entries are merged over these global entries. + */ + configArguments: configArgumentsSchema.optional(), /** @deprecated Use `generate: { functions: { private: 'entry' } }` instead */ privateMethods: z.union([z.literal('none'), z.literal('entry'), z.literal('all')]).optional(), importExtension: importExtensionSchema.optional().default('.js'), diff --git a/packages/codegen/src/generate-utils.ts b/packages/codegen/src/generate-utils.ts index 6a828a52c..6e07a5519 100644 --- a/packages/codegen/src/generate-utils.ts +++ b/packages/codegen/src/generate-utils.ts @@ -28,7 +28,11 @@ import { BcsTuple, } from '@mysten/sui/bcs'; import { normalizeStructTag, normalizeSuiAddress } from '@mysten/sui/utils'; -import { type TransactionArgument, isArgument } from '@mysten/sui/transactions'; +import { + type TransactionArgument, + type TransactionObjectArgument, + isArgument, +} from '@mysten/sui/transactions'; import { type ClientWithCoreApi, type SuiClientTypes } from '@mysten/sui/client'; const MOVE_STDLIB_ADDRESS = normalizeSuiAddress('0x1'); @@ -126,6 +130,12 @@ export function normalizeMoveArguments( continue; } + if (argType === '0x2::accumulator::AccumulatorRoot') { + // Chain-wide shared singleton at a fixed address (SUI_ACCUMULATOR_ROOT_OBJECT_ID). + normalizedArgs.push((tx) => tx.object('0xacc')); + continue; + } + if (argType === '0x3::sui_system::SuiSystemState') { normalizedArgs.push((tx) => tx.object.system()); continue; @@ -177,6 +187,70 @@ export function normalizeMoveArguments( return normalizedArgs; } +/* -------------------------- Config-mapped arguments -------------------------- */ + +/** Context passed to config resolver functions. */ +export interface ConfigResolverContext { + /** + * The matched parameter's own instantiated type arguments (not the whole function's type + * argument tuple), as fully-qualified type tags. + */ + typeArguments: string[]; +} + +/** + * A value in a generated config object: a plain object id/argument, or a resolver function. + * + * Any function is treated as a resolver and called with a \`ConfigResolverContext\`. To provide a + * transaction-callback object argument dynamically, return it from a resolver: + * \`(ctx) => (tx) => ...\`. + */ +export type ConfigValue = + | string + | Exclude unknown> + | ((ctx: ConfigResolverContext) => string | TransactionObjectArgument); + +export function resolveConfigArg( + value: ConfigValue | undefined, + ctx: ConfigResolverContext, + name: string, +): string | TransactionObjectArgument { + if (value == null) { + throw new __ERROR_CLASS__( + \`Missing config value for "\${name}": pass it explicitly in arguments, or include it in the config object\`, + ); + } + + return typeof value === 'function' ? value(ctx) : value; +} + +/** + * Fill unset config-mapped positions in an arguments array/object with their resolved config + * values. Resolvers are only invoked for positions the caller did not pass explicitly. + */ +export function applyConfigArguments( + args: T, + defaults: readonly { index: number; name?: string; resolve: () => unknown }[], +): T { + if (Array.isArray(args)) { + const result = [...args]; + for (const entry of defaults) { + if (result[entry.index] === undefined) { + result[entry.index] = entry.resolve(); + } + } + return result as T; + } + + const result: Record = { ...args }; + for (const entry of defaults) { + if (entry.name !== undefined && result[entry.name] === undefined) { + result[entry.name] = entry.resolve(); + } + } + return result as T; +} + /* -------------------------- Move type tags -------------------------- */ /** A type argument: a type tag string, or a BCS type whose name is a Move type. */ diff --git a/packages/codegen/src/index.ts b/packages/codegen/src/index.ts index 1e3dd1015..5d26435c8 100644 --- a/packages/codegen/src/index.ts +++ b/packages/codegen/src/index.ts @@ -8,8 +8,13 @@ import { MoveModuleBuilder } from './move-module-builder.js'; import { existsSync, statSync } from 'node:fs'; import { getUtilsContent } from './generate-utils.js'; import { parse } from 'toml'; +import { FileBuilder } from './file-builder.js'; +import { parseConfigArguments } from './config-arguments.js'; +import type { ParsedConfigArgument } from './config-arguments.js'; +import { camelCase, capitalize, parseTS } from './utils.js'; import type { RootPackageMetadata } from './types/summary.js'; import type { + ConfigArguments, ErrorClassConfig, FunctionsOption, GenerateBase, @@ -18,7 +23,11 @@ import type { PackageGenerate, TypesOption, } from './config.js'; -export { type SuiCodegenConfig } from './config.js'; +export { + type SuiCodegenConfig, + type ConfigArguments, + type ConfigArgumentMatcher, +} from './config.js'; export async function generateFromPackageSummary({ package: pkg, @@ -28,6 +37,7 @@ export async function generateFromPackageSummary({ importExtension = '.js', includePhantomTypeParameters = false, errorClass, + configArguments: globalConfigArguments, }: { package: PackageConfig; prune: boolean; @@ -36,6 +46,7 @@ export async function generateFromPackageSummary({ importExtension?: ImportExtension; includePhantomTypeParameters?: boolean; errorClass?: ErrorClassConfig; + configArguments?: ConfigArguments; }) { if (!pkg.path) { throw new Error(`Package path is required (got ${pkg.package})`); @@ -154,6 +165,41 @@ export async function generateFromPackageSummary({ ) ).flat(); + const effectiveConfigArguments: ConfigArguments = { + ...globalConfigArguments, + ...pkg.configArguments, + }; + + const { entries: configArgumentEntries, unresolvedKeys } = Object.keys(effectiveConfigArguments) + .length + ? parseConfigArguments(effectiveConfigArguments, registry) + : { entries: [], unresolvedKeys: [] }; + + if (unresolvedKeys.length > 0) { + console.warn( + `configArguments keys not resolvable in ${pkg.package} (skipped): ${unresolvedKeys.join(', ')}`, + ); + } + + const packageEntries = configArgumentEntries.filter( + (entry) => entry.kind === 'package' && entry.package === pkg.package, + ); + if (packageEntries.length > 1) { + throw new Error( + `Multiple configArguments package entries match ${pkg.package}: ${packageEntries + .map((entry) => entry.key) + .join(', ')}`, + ); + } + const packageConfigKey = packageEntries[0]?.key; + + for (const mod of modules) { + mod.builder.setConfigArguments( + configArgumentEntries, + mod.isMainPackage ? packageConfigKey : undefined, + ); + } + const packageGenerate: PackageGenerate | undefined = 'generate' in pkg ? pkg.generate : undefined; const pkgModules = packageGenerate?.modules; const pkgTypes: TypesOption = packageGenerate?.types ?? globalGenerate?.types ?? true; @@ -219,6 +265,71 @@ export async function generateFromPackageSummary({ ); }), ); + + if (configArgumentEntries.length > 0) { + await generateConfigInterface({ + packageOutputDir, + outputDir, + packageName, + entries: configArgumentEntries, + importExtension, + }); + } +} + +/** + * Emit `//config-args.ts` with a convenience interface covering every + * declared config key, for `satisfies` on the user side. The hyphenated filename can never + * collide with a generated Move module file. + */ +async function generateConfigInterface({ + packageOutputDir, + outputDir, + packageName, + entries, + importExtension, +}: { + packageOutputDir: string; + outputDir: string; + packageName: string; + entries: ParsedConfigArgument[]; + importExtension: ImportExtension; +}) { + const builder = new FileBuilder(); + const utilsModule = `~outputRoot/utils/index${importExtension}`; + + const interfaceName = `${capitalize( + camelCase(packageName.replaceAll(/[^A-Za-z0-9_$]+/g, '_').replace(/^(\d)/, '_$1')), + )}Config`; + + const fields = entries.map((entry) => { + if (entry.kind === 'package') { + return `${entry.key}?: string`; + } + + if (entry.isGeneric && entry.typeArguments === null) { + const ctxName = builder.addImport(utilsModule, 'type ConfigResolverContext'); + const objArgName = builder.addImport( + '@mysten/sui/transactions', + 'type TransactionObjectArgument', + ); + return `${entry.key}: (ctx: ${ctxName}) => string | ${objArgName}`; + } + + return `${entry.key}: ${builder.addImport(utilsModule, 'type ConfigValue')}`; + }); + + builder.statements.push( + ...parseTS /* ts */ `export interface ${interfaceName} { + ${fields.join(';\n')} + }`, + ); + + await mkdir(packageOutputDir, { recursive: true }); + await writeFile( + join(packageOutputDir, 'config-args.ts'), + await builder.toString(packageOutputDir, 'config-args.ts', outputDir), + ); } async function generateUtils({ diff --git a/packages/codegen/src/module-registry.ts b/packages/codegen/src/module-registry.ts index a6945c6b1..61988db88 100644 --- a/packages/codegen/src/module-registry.ts +++ b/packages/codegen/src/module-registry.ts @@ -1,9 +1,16 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +import { normalizeSuiAddress } from '@mysten/sui/utils'; import type { MoveModuleBuilder } from './move-module-builder.js'; import type { Ability, ModuleSummary } from './types/summary.js'; +const HEX_ADDRESS = /^0x[0-9a-fA-F]{1,64}$/; + +function normalizeAddress(address: string) { + return HEX_ADDRESS.test(address) ? normalizeSuiAddress(address) : address; +} + export class ModuleRegistry { readonly addressMappings: Record; readonly #builders = new Map(); @@ -31,6 +38,27 @@ export class ModuleRegistry { return this.getBuilder(address, module)?.summary; } + /** + * Look up a module by its resolved (canonical) address. Builders are keyed by the address + * used in their summaries, which may be a named address — this scans by resolving each + * builder's address instead. + */ + getSummaryByResolvedAddress(resolvedAddress: string, module: string): ModuleSummary | undefined { + const direct = this.getSummary(resolvedAddress, module); + if (direct) return direct; + + for (const builder of this.#builders.values()) { + if ( + builder.summary.id.name === module && + normalizeAddress(this.resolveAddress(builder.summary.id.address)) === + normalizeAddress(resolvedAddress) + ) { + return builder.summary; + } + } + return undefined; + } + getAbilities(address: string, module: string, name: string): Ability[] | undefined { const summary = this.getSummary(address, module); return summary?.structs[name]?.abilities ?? summary?.enums[name]?.abilities; diff --git a/packages/codegen/src/move-module-builder.ts b/packages/codegen/src/move-module-builder.ts index cce27b1ef..093503af1 100644 --- a/packages/codegen/src/move-module-builder.ts +++ b/packages/codegen/src/move-module-builder.ts @@ -7,10 +7,13 @@ import { ModuleRegistry } from './module-registry.js'; import { getSafeName, isSupportedRawTransactionInput, + renderResolverTypeTag, renderTypeSignature, SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_ADDRESS, } from './render-types.js'; +import { findConfigArgumentMatch } from './config-arguments.js'; +import type { ParsedConfigArgument, TypeConfigArgument } from './config-arguments.js'; import { camelCase, capitalize, @@ -35,6 +38,11 @@ const IMPORT_MAP = { MoveEnum: { module: '~outputRoot/utils/index', isType: false }, normalizeMoveArguments: { module: '~outputRoot/utils/index', isType: false }, RawTransactionArgument: { module: '~outputRoot/utils/index', isType: true }, + resolveConfigArg: { module: '~outputRoot/utils/index', isType: false }, + applyConfigArguments: { module: '~outputRoot/utils/index', isType: false }, + ConfigValue: { module: '~outputRoot/utils/index', isType: true }, + ConfigResolverContext: { module: '~outputRoot/utils/index', isType: true }, + TransactionObjectArgument: { module: '@mysten/sui/transactions', isType: true }, } as const; type ImportName = keyof typeof IMPORT_MAP; @@ -52,6 +60,8 @@ export class MoveModuleBuilder extends FileBuilder { #importNames: Partial> = {}; #importExtension: ImportExtension; #includePhantomTypeParameters: boolean; + #configArguments: ParsedConfigArgument[] = []; + #packageConfigKey?: string; constructor({ mvrNameOrAddress, @@ -144,6 +154,16 @@ export class MoveModuleBuilder extends FileBuilder { return this.#importNames[name]!; } + /** + * Configure parsed `configArguments` entries for this module's generated functions. Must be + * called before `renderFunctions`. `packageConfigKey` is the config key (if any) that supplies + * this package's call address. + */ + setConfigArguments(entries: ParsedConfigArgument[], packageConfigKey?: string) { + this.#configArguments = entries; + this.#packageConfigKey = packageConfigKey; + } + override async getHeader() { if (!this.summary.doc) { return super.getHeader(); @@ -554,6 +574,37 @@ export class MoveModuleBuilder extends FileBuilder { !isWellKnownObjectParameter(param.type_, (address) => this.#resolveAddress(address)), ); + const functionLabel = `${this.summary.id.address}::${this.summary.id.name}::${name}`; + // Parameters (by index into `requiredParameters`) resolved from the runtime config + // object instead of being required arguments. + const configMatches = new Map(); + if (this.#configArguments.length > 0) { + const bareMatches = new Map(); + requiredParameters.forEach((param, i) => { + const match = findConfigArgumentMatch(param, this.#configArguments, { + resolveAddress: (address) => this.#resolveAddress(address), + functionLabel, + }); + if (!match) return; + configMatches.set(i, match); + if (!match.paramName) { + bareMatches.set(match.key, [ + ...(bareMatches.get(match.key) ?? []), + param.name ?? `#${i}`, + ]); + } + }); + for (const [key, matched] of bareMatches) { + if (matched.length > 1) { + throw new Error( + `configArguments.${key} matches multiple parameters of ${functionLabel} (${matched.join(', ')}). ` + + `Add a \`name\` refinement to disambiguate.`, + ); + } + } + } + const hasConfigMatches = configMatches.size > 0; + const normalizeName = parameters.length > 0 ? this.#getImportName('normalizeMoveArguments') : null; @@ -588,14 +639,26 @@ export class MoveModuleBuilder extends FileBuilder { const wrap = (type: string | null): string => type === null ? transactionArgName! : `${rawTxArgName}<${type}>`; - const argumentsTypes = renderedArgTypes + // Interface fields: config-matched parameters become optional properties. + const argumentFields = renderedArgTypes .map((type, i) => requiredParameters[i].name - ? `${camelCase(requiredParameters[i].name)}: ${wrap(type)}` + ? `${camelCase(requiredParameters[i].name)}${configMatches.has(i) ? '?' : ''}: ${wrap(type)}` : wrap(type), ) .join(',\n'); + // Tuple items: optional tuple elements can't precede required ones, so config-matched + // positions accept an explicit `undefined` instead. + const argumentTupleItems = renderedArgTypes + .map((type, i) => { + const itemType = configMatches.has(i) ? `${wrap(type)} | undefined` : wrap(type); + return requiredParameters[i].name + ? `${camelCase(requiredParameters[i].name)}: ${itemType}` + : itemType; + }) + .join(',\n'); + const bcsTypeName = usedTypeParameters.size > 0 ? this.#getImportName('BcsType') : null; const filteredTypeParameters = func.type_parameters @@ -621,24 +684,56 @@ export class MoveModuleBuilder extends FileBuilder { if (hasAllParameterNames) { this.statements.push( ...parseTS /* ts */ `export interface ${argumentsInterface}${genericTypes} { - ${argumentsTypes} + ${argumentFields} }`, ); } const optionsInterface = this.getUnusedName(`${capitalize(fnName.replace(/^_/, ''))}Options`); const packageIsRequired = !this.#mvrNameOrAddress; + // The package-address config key only applies when a generated default exists — + // otherwise `package` stays required and always takes precedence. + const packageConfigKey = packageIsRequired ? undefined : this.#packageConfigKey; const requiresOptions = - packageIsRequired || argumentsTypes.length > 0 || func.type_parameters.length > 0; + packageIsRequired || requiredParameters.length > 0 || func.type_parameters.length > 0; + + // The minimal structural config slice for this function: only the keys whose matchers + // hit its parameters, plus this package's own package key if declared. + const configSliceFields: string[] = []; + const seenConfigKeys = new Set(); + for (const match of configMatches.values()) { + if (seenConfigKeys.has(match.key)) continue; + seenConfigKeys.add(match.key); + // An uninstantiated matcher on a generic type requires a resolver function — a + // static id cannot be correct across instantiations. + configSliceFields.push( + match.isGeneric && match.typeArguments === null + ? `${match.key}: (ctx: ${this.#getImportName('ConfigResolverContext')}) => string | ${this.#getImportName('TransactionObjectArgument')}` + : `${match.key}: ${this.#getImportName('ConfigValue')}`, + ); + } + if (packageConfigKey) { + configSliceFields.push(`${packageConfigKey}?: string`); + } + + const argumentsOptional = + requiredParameters.length === 0 || requiredParameters.every((_, i) => configMatches.has(i)); this.statements.push( ...parseTS /* ts */ `export interface ${optionsInterface}${genericTypes} { package${packageIsRequired ? ': string' : '?: string'} - ${argumentsTypes.length > 0 ? 'arguments: ' : 'arguments?: '}${ + arguments${argumentsOptional ? '?' : ''}: ${ hasAllParameterNames - ? `${argumentsInterface}${genericTypeArgs} | [${argumentsTypes}]` - : `[${argumentsTypes}]` + ? `${argumentsInterface}${genericTypeArgs} | [${argumentTupleItems}]` + : `[${argumentTupleItems}]` }, + ${ + configSliceFields.length > 0 + ? `config${hasConfigMatches ? '' : '?'}: { + ${configSliceFields.join(',\n')} + },` + : '' + } ${ func.type_parameters.length ? `typeArguments: [${func.type_parameters.map(() => 'string').join(', ')}]` @@ -647,11 +742,53 @@ export class MoveModuleBuilder extends FileBuilder { }`, ); + // Config-matched positions are resolved lazily — only when the caller did not pass the + // argument explicitly. + const configDefaults = [...configMatches.entries()].map(([i, match]) => { + const param = requiredParameters[i]; + let paramType = param.type_; + while (typeof paramType !== 'string' && 'Reference' in paramType) { + paramType = paramType.Reference[1]; + } + const paramTypeArguments = + typeof paramType !== 'string' && 'Datatype' in paramType + ? paramType.Datatype.type_arguments + : []; + const ctxTags = paramTypeArguments.map((arg) => { + const tag = renderResolverTypeTag(arg.argument, { + summary: this.summary, + typeParameters: func.type_parameters, + registry: this.registry, + }); + return tag.includes('${') ? `\`${tag}\`` : `'${tag}'`; + }); + return `{ index: ${i}, ${param.name ? `name: ${JSON.stringify(camelCase(param.name))}, ` : ''}resolve: () => ${this.#getImportName('resolveConfigArg')}(options.config.${match.key}, { typeArguments: [${ctxTags.join(', ')}] }, ${JSON.stringify(match.key)}) }`; + }); + + const baseArgumentsExpr = `options.arguments${ + requiredParameters.length === 0 + ? ' ?? []' + : argumentsOptional + ? hasAllParameterNames + ? ' ?? {}' + : ' ?? []' + : '' + }`; + const argumentsExpr = hasConfigMatches + ? `${this.#getImportName('applyConfigArguments')}(${baseArgumentsExpr}, [${configDefaults.join(', ')}])` + : baseArgumentsExpr; + + const packageAddressExpr = `options.package${ + packageConfigKey + ? ` ?? options.config${hasConfigMatches ? '' : '?'}.${packageConfigKey}` + : '' + }${packageIsRequired ? '' : ` ?? '${this.#mvrNameOrAddress}'`}`; + this.statements.push( ...(await withComment( func, parseTS /* ts */ `export function ${fnName}${genericTypes}(options: ${optionsInterface}${genericTypeArgs}${requiresOptions ? '' : ' = {}'}) { - const packageAddress = options.package${packageIsRequired ? '' : ` ?? '${this.#mvrNameOrAddress}'`}; + const packageAddress = ${packageAddressExpr}; ${ parameters.length > 0 ? `const argumentsTypes = [ @@ -676,7 +813,7 @@ export class MoveModuleBuilder extends FileBuilder { package: packageAddress, module: '${this.summary.id.name}', function: '${name}', - ${parameters.length > 0 ? `arguments: ${normalizeName}(options.arguments${argumentsTypes.length > 0 ? '' : ' ?? []'} , argumentsTypes${hasAllParameterNames ? `, parameterNames` : ''}),` : ''} + ${parameters.length > 0 ? `arguments: ${normalizeName}(${argumentsExpr}, argumentsTypes${hasAllParameterNames ? `, parameterNames` : ''}),` : ''} ${func.type_parameters.length ? 'typeArguments: options.typeArguments' : ''} }) }`, diff --git a/packages/codegen/src/render-types.ts b/packages/codegen/src/render-types.ts index 484cd48e7..36af2dc10 100644 --- a/packages/codegen/src/render-types.ts +++ b/packages/codegen/src/render-types.ts @@ -185,6 +185,59 @@ export function renderTypeSignature(type: Type, options: RenderTypeSignatureOpti throw new Error(`Unknown type signature: ${JSON.stringify(type, null, 2)}`); } +/** + * Render a type as a full type-tag string for config resolver contexts. Unlike the `typeTag` + * format (which emits `null` for non-pure datatypes because the runtime doesn't need those tags), + * this always produces a real tag. Function type parameters are interpolated from the generated + * function's `options.typeArguments`, so the result may be a template-literal fragment. + */ +export function renderResolverTypeTag( + type: Type, + options: Pick, +): string { + if (typeof type === 'string') { + if (type === 'signer' || type === '_') { + throw new Error(`${type} is not supported in type arguments`); + } + return type; + } + + if ('Reference' in type) { + return renderResolverTypeTag(type.Reference[1], options); + } + + if ('vector' in type) { + return `vector<${renderResolverTypeTag(type.vector, options)}>`; + } + + if ('TypeParameter' in type) { + return `\${options.typeArguments[${type.TypeParameter}]}`; + } + + if ('NamedTypeParameter' in type) { + const originalIndex = + options.typeParameters?.findIndex((p) => p.name === type.NamedTypeParameter) ?? -1; + if (originalIndex === -1) { + throw new Error(`Named type parameter ${type.NamedTypeParameter} not found`); + } + return `\${options.typeArguments[${originalIndex}]}`; + } + + if ('Datatype' in type) { + const { Datatype } = type; + const address = resolveAddress(options, Datatype.module.address); + const base = `${address}::${Datatype.module.name}::${Datatype.name}`; + if (Datatype.type_arguments.length === 0) { + return base; + } + return `${base}<${Datatype.type_arguments + .map((arg) => renderResolverTypeTag(arg.argument, options)) + .join(', ')}>`; + } + + throw new Error(`Unknown type signature: ${JSON.stringify(type, null, 2)}`); +} + function getDatatypeAbilities( type: Datatype, options: Pick, @@ -260,6 +313,8 @@ function renderDataType(type: Datatype, options: RenderTypeSignatureOptions): st if (type.module.name === 'random' && type.name === 'Random') return '0x2::random::Random'; if (type.module.name === 'deny_list' && type.name === 'DenyList') return '0x2::deny_list::DenyList'; + if (type.module.name === 'accumulator' && type.name === 'AccumulatorRoot') + return '0x2::accumulator::AccumulatorRoot'; if (type.module.name === 'object' && (type.name === 'ID' || type.name === 'UID')) return '0x2::object::ID'; } diff --git a/packages/codegen/src/utils.ts b/packages/codegen/src/utils.ts index d37b58913..679e94e28 100644 --- a/packages/codegen/src/utils.ts +++ b/packages/codegen/src/utils.ts @@ -168,6 +168,10 @@ export function isWellKnownObjectParameter( if (Datatype.module.name === 'clock') { return Datatype.name === 'Clock'; } + + if (Datatype.module.name === 'accumulator') { + return Datatype.name === 'AccumulatorRoot'; + } } if (address === SUI_SYSTEM_ADDRESS) { diff --git a/packages/codegen/tests/codegen-output.test.ts b/packages/codegen/tests/codegen-output.test.ts index 691759ad6..12a964ae5 100644 --- a/packages/codegen/tests/codegen-output.test.ts +++ b/packages/codegen/tests/codegen-output.test.ts @@ -586,6 +586,68 @@ describe('function codegen output', () => { expect(fnBody?.[0]).toContain("'0x2::clock::Clock'"); }); + it('function with well-known AccumulatorRoot parameter', async () => { + const summary = { + id: { address: 'testpkg', name: 'settlement' }, + doc: '', + immediate_dependencies: [], + attributes: [], + functions: { + settle: { + source_index: 0, + index: 0, + doc: '', + attributes: [], + visibility: 'Public', + entry: false, + macro_: false, + type_parameters: [], + parameters: [ + { + name: 'root', + type_: { + Reference: [ + true, + { + Datatype: { + module: { address: 'sui', name: 'accumulator' }, + name: 'AccumulatorRoot', + type_arguments: [], + }, + }, + ], + }, + }, + { name: 'amount', type_: 'u64' }, + ], + return_: [], + }, + }, + structs: {}, + enums: {}, + }; + + const builder = new MoveModuleBuilder({ + summary: summary as any, + registry: new ModuleRegistry(ADDRESS_MAPPINGS), + mvrNameOrAddress: '@test/testpkg', + importExtension: '.js', + }); + builder.includeFunctions(); + const output = await render(builder, { functions: true }); + + // AccumulatorRoot should be auto-injected, not in the arguments interface + const argInterface = output.match(/export interface SettleArguments[\s\S]*?^}/m); + expect(argInterface?.[0]).toMatchInlineSnapshot(` + "export interface SettleArguments { + amount: RawTransactionArgument; + }" + `); + + const fnBody = output.match(/export function settle[\s\S]*?^}/m); + expect(fnBody?.[0]).toContain("'0x2::accumulator::AccumulatorRoot'"); + }); + it('function with enum parameter (is_active)', async () => { const { registry } = await createBuilders(); registry.includeTypes(); diff --git a/packages/codegen/tests/config-arguments.test.ts b/packages/codegen/tests/config-arguments.test.ts new file mode 100644 index 000000000..d95adb409 --- /dev/null +++ b/packages/codegen/tests/config-arguments.test.ts @@ -0,0 +1,791 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'; +import { readFile, readdir, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import ts from 'typescript'; +import { Transaction } from '@mysten/sui/transactions'; +import { ModuleRegistry } from '../src/module-registry.js'; +import { MoveModuleBuilder } from '../src/move-module-builder.js'; +import { parseConfigArguments } from '../src/config-arguments.js'; +import { generateFromPackageSummary } from '../src/index.js'; +import type { ConfigArguments } from '../src/config.js'; + +const FIXTURE_PATH = join(__dirname, 'move/testpkg'); +const SUMMARIES_DIR = join(FIXTURE_PATH, 'package_summaries'); +const GENERATED_DIR = join(__dirname, 'generated-config'); + +const ADDRESS_MAPPINGS = { + std: '0x0000000000000000000000000000000000000000000000000000000000000001', + sui: '0x0000000000000000000000000000000000000000000000000000000000000002', + testpkg: '0x0000000000000000000000000000000000000000000000000000000000000000', +}; + +async function createBuilders(configArguments: ConfigArguments, packageConfigKey?: string) { + const registry = new ModuleRegistry(ADDRESS_MAPPINGS); + const counter = await MoveModuleBuilder.fromSummaryFile( + join(SUMMARIES_DIR, 'testpkg', 'counter.json'), + registry, + '@test/testpkg', + ); + const registryBuilder = await MoveModuleBuilder.fromSummaryFile( + join(SUMMARIES_DIR, 'testpkg', 'registry.json'), + registry, + '@test/testpkg', + ); + + const { entries } = parseConfigArguments(configArguments, registry); + counter.setConfigArguments(entries, packageConfigKey); + registryBuilder.setConfigArguments(entries, packageConfigKey); + + return { counter, registry: registryBuilder, moduleRegistry: registry }; +} + +async function render(builder: MoveModuleBuilder) { + await builder.renderFunctions(); + return builder.toString('./', './testpkg/test.ts'); +} + +/** + * A synthetic module with a generic `Pool` type used by functions in generic, + * concretely-instantiated, and same-type-twice positions. + */ +function poolsSummary({ parameterNames = true }: { parameterNames?: boolean } = {}) { + const poolType = (typeArgument: unknown) => ({ + Reference: [ + false, + { + Datatype: { + module: { address: 'testpkg', name: 'pools' }, + name: 'Pool', + type_arguments: [{ phantom: true, argument: typeArgument }], + }, + }, + ], + }); + const suiType = { + Datatype: { module: { address: 'sui', name: 'sui' }, name: 'SUI', type_arguments: [] }, + }; + const param = (name: string, type_: unknown) => (parameterNames ? { name, type_ } : { type_ }); + const fn = (parameters: unknown[], type_parameters: unknown[] = []) => ({ + source_index: 0, + index: 0, + doc: '', + attributes: [], + visibility: 'Public', + entry: false, + macro_: false, + type_parameters, + parameters, + return_: [], + }); + + return { + id: { address: 'testpkg', name: 'pools' }, + doc: '', + immediate_dependencies: [], + attributes: [], + functions: { + use_generic: fn( + [param('pool', poolType({ TypeParameter: 0 })), param('amount', 'u64')], + [{ name: 'T', phantom: false, constraints: [] }], + ), + use_concrete: fn([param('pool', poolType(suiType)), param('amount', 'u64')]), + swap: fn( + [ + param('base_pool', poolType({ TypeParameter: 0 })), + param('quote_pool', poolType({ TypeParameter: 1 })), + ], + [ + { name: 'Base', phantom: false, constraints: [] }, + { name: 'Quote', phantom: false, constraints: [] }, + ], + ), + }, + structs: { + Pool: { + index: 0, + doc: '', + attributes: [], + abilities: ['Key'], + type_parameters: [{ name: 'T', phantom: true, constraints: [] }], + fields: { + positional_fields: false, + fields: { id: { index: 0, doc: null, type_: 'address' } }, + }, + }, + }, + enums: {}, + }; +} + +function createPoolsBuilder( + configArguments: ConfigArguments, + options: { parameterNames?: boolean } = {}, +) { + const registry = new ModuleRegistry(ADDRESS_MAPPINGS); + const builder = new MoveModuleBuilder({ + summary: poolsSummary(options) as any, + registry, + mvrNameOrAddress: '@test/testpkg', + importExtension: '.js', + }); + const { entries } = parseConfigArguments(configArguments, registry); + builder.setConfigArguments(entries); + return builder; +} + +describe('parseConfigArguments', () => { + it('parses type, instantiated type, and package matchers', async () => { + const registry = new ModuleRegistry(ADDRESS_MAPPINGS); + new MoveModuleBuilder({ + summary: poolsSummary() as any, + registry, + mvrNameOrAddress: '@test/testpkg', + importExtension: '.js', + }); + + const { entries, unresolvedKeys } = parseConfigArguments( + { + pool: { type: 'testpkg::pools::Pool' }, + suiPool: { type: 'testpkg::pools::Pool<0x2::sui::SUI>' }, + pkg: { package: '@test/testpkg' }, + }, + registry, + ); + + expect(unresolvedKeys).toEqual([]); + expect(entries).toMatchObject([ + { + kind: 'type', + key: 'pool', + module: 'pools', + name: 'Pool', + typeArguments: null, + isGeneric: true, + }, + { + kind: 'type', + key: 'suiPool', + typeArguments: [ + { + datatype: { + address: '0x0000000000000000000000000000000000000000000000000000000000000002', + module: 'sui', + name: 'SUI', + typeArguments: [], + }, + }, + ], + isGeneric: true, + }, + { kind: 'package', key: 'pkg', package: '@test/testpkg' }, + ]); + }); + + it('reports matchers for types that are not in the summaries as unresolved', async () => { + const registry = new ModuleRegistry(ADDRESS_MAPPINGS); + new MoveModuleBuilder({ + summary: poolsSummary() as any, + registry, + mvrNameOrAddress: '@test/testpkg', + importExtension: '.js', + }); + + const { entries, unresolvedKeys } = parseConfigArguments( + { + missingType: { type: 'testpkg::pools::DoesNotExist' }, + missingModule: { type: '0x999::other::Thing' }, + pool: { type: 'testpkg::pools::Pool' }, + }, + registry, + ); + + expect(unresolvedKeys).toEqual(['missingType', 'missingModule']); + expect(entries.map((entry) => entry.key)).toEqual(['pool']); + }); + + it('rejects malformed matcher types', async () => { + const registry = new ModuleRegistry(ADDRESS_MAPPINGS); + + expect(() => parseConfigArguments({ bad: { type: 'Pool' } }, registry)).toThrowError( + /Expected a fully-qualified Move type/, + ); + expect(() => parseConfigArguments({ bad: { type: 'u64' } }, registry)).toThrowError( + /must be a Move datatype/, + ); + }); + + it('rejects instantiated matchers with the wrong arity', async () => { + const registry = new ModuleRegistry(ADDRESS_MAPPINGS); + new MoveModuleBuilder({ + summary: poolsSummary() as any, + registry, + mvrNameOrAddress: '@test/testpkg', + importExtension: '.js', + }); + + expect(() => + parseConfigArguments( + { pool: { type: 'testpkg::pools::Pool<0x2::sui::SUI, u64>' } }, + registry, + ), + ).toThrowError(/expects 1 type argument\(s\), got 2/); + }); +}); + +describe('config-driven function codegen', () => { + it('non-generic matcher: matched parameter becomes optional with a required config slice', async () => { + const { registry } = await createBuilders({ + registryObj: { type: 'testpkg::registry::Registry' }, + }); + registry.includeFunctions(['register']); + const output = await render(registry); + + const argInterface = output.match(/export interface RegisterArguments[\s\S]*?^}/m); + expect(argInterface?.[0]).toMatchInlineSnapshot(` + "export interface RegisterArguments { + registry?: RawTransactionArgument; + name: RawTransactionArgument; + tags: RawTransactionArgument>; + }" + `); + + const optionsInterface = output.match(/export interface RegisterOptions[\s\S]*?^}/m); + expect(optionsInterface?.[0]).toMatchInlineSnapshot(` + "export interface RegisterOptions { + package?: string; + arguments: RegisterArguments | [ + registry: RawTransactionArgument | undefined, + name: RawTransactionArgument, + tags: RawTransactionArgument> + ]; + config: { + registryObj: ConfigValue; + }; + }" + `); + + const fnBody = output.match(/export function register[\s\S]*?^}/m); + expect(fnBody?.[0]).toMatchInlineSnapshot(` + "export function register(options: RegisterOptions) { + const packageAddress = options.package ?? '@test/testpkg'; + const argumentsTypes = [ + null, + '0x1::string::String', + 'vector<0x1::string::String>' + ] satisfies (string | null)[]; + const parameterNames = ["registry", "name", "tags"]; + return (tx: Transaction) => tx.moveCall({ + package: packageAddress, + module: 'registry', + function: 'register', + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "registry", resolve: () => resolveConfigArg(options.config.registryObj, { typeArguments: [] }, "registryObj") }]), argumentsTypes, parameterNames), + }); + }" + `); + }); + + it('makes arguments optional when every parameter is config-matched', async () => { + const { registry } = await createBuilders({ + registryObj: { type: 'testpkg::registry::Registry' }, + }); + registry.includeFunctions(['lookup']); + const output = await render(registry); + + const optionsInterface = output.match(/export interface LookupOptions[\s\S]*?^}/m); + expect(optionsInterface?.[0]).toMatchInlineSnapshot(` + "export interface LookupOptions { + package?: string; + arguments?: LookupArguments | [ + registry: RawTransactionArgument | undefined + ]; + config: { + registryObj: ConfigValue; + }; + }" + `); + + const fnBody = output.match(/export function lookup[\s\S]*?^}/m); + expect(fnBody?.[0]).toMatchInlineSnapshot(` + "export function lookup(options: LookupOptions) { + const packageAddress = options.package ?? '@test/testpkg'; + const argumentsTypes = [ + null + ] satisfies (string | null)[]; + const parameterNames = ["registry"]; + return (tx: Transaction) => tx.moveCall({ + package: packageAddress, + module: 'registry', + function: 'lookup', + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "registry", resolve: () => resolveConfigArg(options.config.registryObj, { typeArguments: [] }, "registryObj") }]), argumentsTypes, parameterNames), + }); + }" + `); + }); + + it('uninstantiated generic matcher: config value requires a resolver and receives the parameter instantiation', async () => { + const { registry } = await createBuilders({ + container: { type: 'testpkg::registry::Container' }, + }); + registry.includeFunctions(['container_size']); + const output = await render(registry); + + const optionsInterface = output.match(/export interface ContainerSizeOptions[\s\S]*?^}/m); + expect(optionsInterface?.[0]).toMatchInlineSnapshot(` + "export interface ContainerSizeOptions { + package?: string; + arguments?: ContainerSizeArguments | [ + container: RawTransactionArgument | undefined + ]; + config: { + container: (ctx: ConfigResolverContext) => string | TransactionObjectArgument; + }; + typeArguments: [ + string + ]; + }" + `); + + const fnBody = output.match(/export function containerSize[\s\S]*?^}/m); + expect(fnBody?.[0]).toMatchInlineSnapshot(` + "export function containerSize(options: ContainerSizeOptions) { + const packageAddress = options.package ?? '@test/testpkg'; + const argumentsTypes = [ + null + ] satisfies (string | null)[]; + const parameterNames = ["container"]; + return (tx: Transaction) => tx.moveCall({ + package: packageAddress, + module: 'registry', + function: 'container_size', + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "container", resolve: () => resolveConfigArg(options.config.container, { typeArguments: [\`\${options.typeArguments[0]}\`] }, "container") }]), argumentsTypes, parameterNames), + typeArguments: options.typeArguments + }); + }" + `); + }); + + it('instantiated matcher only matches concrete instantiations and wins over the uninstantiated matcher', async () => { + const builder = createPoolsBuilder({ + pool: { type: 'testpkg::pools::Pool' }, + suiPool: { type: 'testpkg::pools::Pool<0x2::sui::SUI>' }, + }); + builder.includeFunctions(['use_generic', 'use_concrete']); + const output = await render(builder); + + // use_concrete is concretely typed Pool in the Move signature: the instantiated + // matcher wins and a plain value is allowed. + const concreteOptions = output.match(/export interface UseConcreteOptions[\s\S]*?^}/m); + expect(concreteOptions?.[0]).toMatchInlineSnapshot(` + "export interface UseConcreteOptions { + package?: string; + arguments: UseConcreteArguments | [ + pool: RawTransactionArgument | undefined, + amount: RawTransactionArgument + ]; + config: { + suiPool: ConfigValue; + }; + }" + `); + + const concreteBody = output.match(/export function useConcrete[\s\S]*?^}/m); + expect(concreteBody?.[0]).toMatchInlineSnapshot(` + "export function useConcrete(options: UseConcreteOptions) { + const packageAddress = options.package ?? '@test/testpkg'; + const argumentsTypes = [ + null, + 'u64' + ] satisfies (string | null)[]; + const parameterNames = ["pool", "amount"]; + return (tx: Transaction) => tx.moveCall({ + package: packageAddress, + module: 'pools', + function: 'use_concrete', + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "pool", resolve: () => resolveConfigArg(options.config.suiPool, { typeArguments: ['0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI'] }, "suiPool") }]), argumentsTypes, parameterNames), + }); + }" + `); + + // use_generic is typed Pool: it always binds to the uninstantiated matcher, which + // requires a resolver function. + const genericOptions = output.match(/export interface UseGenericOptions[\s\S]*?^}/m); + expect(genericOptions?.[0]).toMatchInlineSnapshot(` + "export interface UseGenericOptions { + package?: string; + arguments: UseGenericArguments | [ + pool: RawTransactionArgument | undefined, + amount: RawTransactionArgument + ]; + config: { + pool: (ctx: ConfigResolverContext) => string | TransactionObjectArgument; + }; + typeArguments: [ + string + ]; + }" + `); + + const genericBody = output.match(/export function useGeneric[\s\S]*?^}/m); + expect(genericBody?.[0]).toMatchInlineSnapshot(` + "export function useGeneric(options: UseGenericOptions) { + const packageAddress = options.package ?? '@test/testpkg'; + const argumentsTypes = [ + null, + 'u64' + ] satisfies (string | null)[]; + const parameterNames = ["pool", "amount"]; + return (tx: Transaction) => tx.moveCall({ + package: packageAddress, + module: 'pools', + function: 'use_generic', + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "pool", resolve: () => resolveConfigArg(options.config.pool, { typeArguments: [\`\${options.typeArguments[0]}\`] }, "pool") }]), argumentsTypes, parameterNames), + typeArguments: options.typeArguments + }); + }" + `); + }); + + it('errors when a bare matcher hits two parameters in one signature', async () => { + const builder = createPoolsBuilder({ + pool: { type: 'testpkg::pools::Pool' }, + }); + builder.includeFunctions(['swap']); + + await expect(render(builder)).rejects.toThrowError( + /configArguments\.pool matches multiple parameters of testpkg::pools::swap \(base_pool, quote_pool\)/, + ); + }); + + it('name refinement disambiguates two parameters of the same type', async () => { + const builder = createPoolsBuilder({ + basePool: { type: 'testpkg::pools::Pool', name: 'base_pool' }, + quotePool: { type: 'testpkg::pools::Pool', name: 'quote_pool' }, + }); + builder.includeFunctions(['swap']); + const output = await render(builder); + + const optionsInterface = output.match(/export interface SwapOptions[\s\S]*?^}/m); + expect(optionsInterface?.[0]).toMatchInlineSnapshot(` + "export interface SwapOptions { + package?: string; + arguments?: SwapArguments | [ + basePool: RawTransactionArgument | undefined, + quotePool: RawTransactionArgument | undefined + ]; + config: { + basePool: (ctx: ConfigResolverContext) => string | TransactionObjectArgument; + quotePool: (ctx: ConfigResolverContext) => string | TransactionObjectArgument; + }; + typeArguments: [ + string, + string + ]; + }" + `); + + const fnBody = output.match(/export function swap[\s\S]*?^}/m); + expect(fnBody?.[0]).toMatchInlineSnapshot(` + "export function swap(options: SwapOptions) { + const packageAddress = options.package ?? '@test/testpkg'; + const argumentsTypes = [ + null, + null + ] satisfies (string | null)[]; + const parameterNames = ["basePool", "quotePool"]; + return (tx: Transaction) => tx.moveCall({ + package: packageAddress, + module: 'pools', + function: 'swap', + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "basePool", resolve: () => resolveConfigArg(options.config.basePool, { typeArguments: [\`\${options.typeArguments[0]}\`] }, "basePool") }, { index: 1, name: "quotePool", resolve: () => resolveConfigArg(options.config.quotePool, { typeArguments: [\`\${options.typeArguments[1]}\`] }, "quotePool") }]), argumentsTypes, parameterNames), + typeArguments: options.typeArguments + }); + }" + `); + }); + + it('name-refined matchers win over a bare matcher for the same type', async () => { + const builder = createPoolsBuilder({ + pool: { type: 'testpkg::pools::Pool' }, + basePool: { type: 'testpkg::pools::Pool', name: 'base_pool' }, + quotePool: { type: 'testpkg::pools::Pool', name: 'quote_pool' }, + }); + builder.includeFunctions(['swap', 'use_generic']); + const output = await render(builder); + + // swap binds base/quote to the name-refined keys; use_generic still binds `pool`. + const swapOptions = output.match(/export interface SwapOptions[\s\S]*?^}/m); + expect(swapOptions?.[0]).toContain('basePool'); + expect(swapOptions?.[0]).toContain('quotePool'); + expect(swapOptions?.[0]).not.toContain('pool:'); + + const genericOptions = output.match(/export interface UseGenericOptions[\s\S]*?^}/m); + expect(genericOptions?.[0]).toContain('pool:'); + }); + + it('errors when two matchers of equal specificity hit the same parameter', async () => { + const builder = createPoolsBuilder({ + poolA: { type: 'testpkg::pools::Pool' }, + poolB: { type: 'testpkg::pools::Pool' }, + }); + builder.includeFunctions(['use_generic']); + + await expect(render(builder)).rejects.toThrowError( + /matched by multiple configArguments entries with equal specificity: poolA, poolB/, + ); + }); + + it('errors when a name matcher targets a summary without parameter names', async () => { + const builder = createPoolsBuilder( + { basePool: { type: 'testpkg::pools::Pool', name: 'base_pool' } }, + { parameterNames: false }, + ); + builder.includeFunctions(['swap']); + + await expect(render(builder)).rejects.toThrowError( + /parameters of testpkg::pools::swap have no names/, + ); + }); + + it('package entries are added to the package-address precedence chain', async () => { + const { registry } = await createBuilders( + { + registryObj: { type: 'testpkg::registry::Registry' }, + testpkgAddress: { package: '@test/testpkg' }, + }, + 'testpkgAddress', + ); + registry.includeFunctions(['lookup']); + const output = await render(registry); + + const fnBody = output.match(/export function lookup[\s\S]*?^}/m); + expect(fnBody?.[0]).toContain( + "const packageAddress = options.package ?? options.config.testpkgAddress ?? '@test/testpkg';", + ); + }); + + it('package entries alone produce an optional config slice', async () => { + const { registry } = await createBuilders( + { testpkgAddress: { package: '@test/testpkg' } }, + 'testpkgAddress', + ); + registry.includeFunctions(['lookup']); + const output = await render(registry); + + const optionsInterface = output.match(/export interface LookupOptions[\s\S]*?^}/m); + expect(optionsInterface?.[0]).toMatchInlineSnapshot(` + "export interface LookupOptions { + package?: string; + arguments: LookupArguments | [ + registry: RawTransactionArgument + ]; + config?: { + testpkgAddress?: string; + }; + }" + `); + + const fnBody = output.match(/export function lookup[\s\S]*?^}/m); + expect(fnBody?.[0]).toContain( + "const packageAddress = options.package ?? options.config?.testpkgAddress ?? '@test/testpkg';", + ); + }); + + it('keeps origin-addressed type tags while calls use the config-supplied package address', async () => { + const ORIGIN_V1 = '0x000000000000000000000000000000000000000000000000000000000000aaaa'; + const registry = new ModuleRegistry(ADDRESS_MAPPINGS); + const builder = await MoveModuleBuilder.fromSummaryFile( + join(SUMMARIES_DIR, 'testpkg', 'registry.json'), + registry, + '@test/testpkg', + '.js', + false, + { Registry: ORIGIN_V1, Container: ORIGIN_V1 }, + ); + const { entries } = parseConfigArguments( + { + registryObj: { type: 'testpkg::registry::Registry' }, + testpkgAddress: { package: '@test/testpkg' }, + }, + registry, + ); + builder.setConfigArguments(entries, 'testpkgAddress'); + builder.includeTypes(['Registry']); + builder.includeFunctions(['lookup']); + await builder.renderBCSTypes(); + const output = await render(builder); + + // BCS type names keep the origin address; the call package comes from the config chain. + expect(output).toContain(`name: \`${ORIGIN_V1}::registry::Registry\``); + expect(output).toContain( + "const packageAddress = options.package ?? options.config.testpkgAddress ?? '@test/testpkg';", + ); + }); +}); + +describe('generateFromPackageSummary with configArguments', () => { + afterAll(async () => { + await rm(GENERATED_DIR, { recursive: true, force: true }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + async function generate() { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + await generateFromPackageSummary({ + package: { + package: '@test/testpkg', + path: FIXTURE_PATH, + configArguments: { + registryObj: { type: 'testpkg::registry::Registry' }, + container: { type: 'testpkg::registry::Container' }, + testpkgAddress: { package: '@test/testpkg' }, + missing: { type: 'testpkg::registry::DoesNotExist' }, + }, + }, + prune: true, + outputDir: GENERATED_DIR, + }); + return warn; + } + + it('emits config-args.ts and config-driven bindings, warning on unresolved keys', async () => { + const warn = await generate(); + + expect(warn).toHaveBeenCalledWith( + 'configArguments keys not resolvable in @test/testpkg (skipped): missing', + ); + + const configArgs = await readFile(join(GENERATED_DIR, 'testpkg', 'config-args.ts'), 'utf-8'); + expect(configArgs).toMatchInlineSnapshot(` + "/************************************************************** + * THIS FILE IS GENERATED AND SHOULD NOT BE MANUALLY MODIFIED * + **************************************************************/ + import { type ConfigValue, type ConfigResolverContext } from '../utils/index.js'; + import { type TransactionObjectArgument } from '@mysten/sui/transactions'; + export interface TestpkgConfig { + registryObj: ConfigValue; + container: (ctx: ConfigResolverContext) => string | TransactionObjectArgument; + testpkgAddress?: string; + }" + `); + + const registryModule = await readFile(join(GENERATED_DIR, 'testpkg', 'registry.ts'), 'utf-8'); + expect(registryModule).toContain('applyConfigArguments'); + expect(registryModule).toContain('resolveConfigArg'); + }); + + it('generated output typechecks under strict settings', { timeout: 60_000 }, async () => { + await generate(); + + const files: string[] = []; + const walk = async (dir: string) => { + for (const entry of await readdir(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + await walk(path); + } else if (entry.name.endsWith('.ts')) { + files.push(path); + } + } + }; + await walk(GENERATED_DIR); + + const program = ts.createProgram({ + rootNames: files, + options: { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.NodeNext, + moduleResolution: ts.ModuleResolutionKind.NodeNext, + strict: true, + noUncheckedIndexedAccess: true, + noEmit: true, + skipLibCheck: true, + esModuleInterop: true, + lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], + }, + }); + + const diagnostics = ts + .getPreEmitDiagnostics(program) + .filter((diagnostic) => diagnostic.file && files.includes(diagnostic.file.fileName)); + + const messages = diagnostics.map((diagnostic) => { + const text = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + if (diagnostic.file && diagnostic.start !== undefined) { + const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + return `[${diagnostic.file.fileName}:${line + 1}:${character + 1}] ${text}`; + } + return text; + }); + + expect(messages, `Generated output has type errors:\n${messages.join('\n')}`).toEqual([]); + }); + + it('config values are applied at runtime, with explicit arguments overriding', async () => { + await generate(); + + const mod = await import(join(GENERATED_DIR, 'testpkg', 'registry.js')); + const PACKAGE_ID = '0x00000000000000000000000000000000000000000000000000000000000000ee'; + const REGISTRY_ID = '0x0000000000000000000000000000000000000000000000000000000000000123'; + + // Config-provided object id and package address. + const tx = new Transaction(); + tx.add( + mod.lookup({ + config: { registryObj: REGISTRY_ID, testpkgAddress: PACKAGE_ID }, + }), + ); + const json = JSON.parse(await tx.toJSON()); + expect(json.inputs).toEqual([{ UnresolvedObject: { objectId: REGISTRY_ID } }]); + expect(json.commands[0].MoveCall.package).toBe(PACKAGE_ID); + + // An explicitly passed argument overrides config resolution. + const OVERRIDE_ID = '0x0000000000000000000000000000000000000000000000000000000000000456'; + const tx2 = new Transaction(); + tx2.add( + mod.lookup({ + arguments: { registry: OVERRIDE_ID }, + config: { + registryObj: () => { + throw new Error('should not be called'); + }, + testpkgAddress: PACKAGE_ID, + }, + }), + ); + const json2 = JSON.parse(await tx2.toJSON()); + expect(json2.inputs).toEqual([{ UnresolvedObject: { objectId: OVERRIDE_ID } }]); + }); + + it('resolvers receive the matched parameter instantiation at runtime', async () => { + await generate(); + + const mod = await import(join(GENERATED_DIR, 'testpkg', 'registry.js')); + const PACKAGE_ID = '0x00000000000000000000000000000000000000000000000000000000000000ee'; + const CONTAINER_ID = '0x0000000000000000000000000000000000000000000000000000000000000789'; + const contexts: unknown[] = []; + + const tx = new Transaction(); + tx.add( + mod.containerSize({ + typeArguments: ['0x2::sui::SUI'], + config: { + container: (ctx: unknown) => { + contexts.push(ctx); + return CONTAINER_ID; + }, + testpkgAddress: PACKAGE_ID, + }, + }), + ); + const json = JSON.parse(await tx.toJSON()); + + expect(contexts).toEqual([{ typeArguments: ['0x2::sui::SUI'] }]); + expect(json.inputs).toEqual([{ UnresolvedObject: { objectId: CONTAINER_ID } }]); + }); +}); diff --git a/packages/codegen/tests/utils.test.ts b/packages/codegen/tests/utils.test.ts index 94ec69ada..f3dacdcb1 100644 --- a/packages/codegen/tests/utils.test.ts +++ b/packages/codegen/tests/utils.test.ts @@ -13,6 +13,11 @@ let normalizeMoveArguments: ( argTypes: readonly (string | null)[], parameterNames?: string[], ) => any; +let resolveConfigArg: (value: unknown, ctx: { typeArguments: string[] }, name: string) => unknown; +let applyConfigArguments: ( + args: unknown[] | object, + defaults: readonly { index: number; name?: string; resolve: () => unknown }[], +) => unknown[] | object; beforeAll(async () => { await mkdir(join(GENERATED_DIR, 'utils'), { recursive: true }); @@ -20,6 +25,8 @@ beforeAll(async () => { const modPath = join(GENERATED_DIR, 'utils', 'index.js'); const mod = await import(modPath); normalizeMoveArguments = mod.normalizeMoveArguments; + resolveConfigArg = mod.resolveConfigArg; + applyConfigArguments = mod.applyConfigArguments; }); afterAll(async () => { @@ -399,3 +406,101 @@ describe('normalizeMoveArguments', () => { ]); }); }); + +describe('well-known AccumulatorRoot injection', () => { + it('injects the accumulator root object without consuming arguments', async () => { + const tx = new Transaction(); + tx.moveCall({ + target: '0x0::test::test', + arguments: normalizeMoveArguments( + { arbitraryValue: 42 }, + ['u32', '0x2::accumulator::AccumulatorRoot'], + ['arbitraryValue'], + ), + }); + + const json = JSON.parse(await tx.toJSON()); + expect(json.inputs).toEqual([ + { Pure: { bytes: 'KgAAAA==' } }, + { + UnresolvedObject: { + objectId: '0x0000000000000000000000000000000000000000000000000000000000000acc', + }, + }, + ]); + }); +}); + +describe('resolveConfigArg', () => { + it('returns plain values as-is', () => { + expect(resolveConfigArg('0x123', { typeArguments: [] }, 'pool')).toBe('0x123'); + }); + + it('invokes resolver functions with the context', () => { + const contexts: unknown[] = []; + const value = (ctx: unknown) => { + contexts.push(ctx); + return '0x456'; + }; + + expect(resolveConfigArg(value, { typeArguments: ['0x2::sui::SUI'] }, 'pool')).toBe('0x456'); + expect(contexts).toEqual([{ typeArguments: ['0x2::sui::SUI'] }]); + }); + + it('throws a descriptive error for missing values', () => { + expect(() => resolveConfigArg(undefined, { typeArguments: [] }, 'pool')).toThrowError( + 'Missing config value for "pool": pass it explicitly in arguments, or include it in the config object', + ); + }); +}); + +describe('applyConfigArguments', () => { + it('fills missing named arguments and leaves explicit ones untouched', () => { + const resolved: string[] = []; + const result = applyConfigArguments({ amount: 42n }, [ + { + index: 0, + name: 'pool', + resolve: () => { + resolved.push('pool'); + return '0x123'; + }, + }, + ]); + + expect(result).toEqual({ amount: 42n, pool: '0x123' }); + expect(resolved).toEqual(['pool']); + }); + + it('does not invoke resolvers for explicitly passed arguments', () => { + const result = applyConfigArguments({ pool: '0xabc', amount: 42n }, [ + { + index: 0, + name: 'pool', + resolve: () => { + throw new Error('should not be called'); + }, + }, + ]); + + expect(result).toEqual({ pool: '0xabc', amount: 42n }); + }); + + it('fills undefined positions in array arguments', () => { + const result = applyConfigArguments( + [undefined, 42n], + [{ index: 0, name: 'pool', resolve: () => '0x123' }], + ); + + expect(result).toEqual(['0x123', 42n]); + }); + + it('fills trailing omitted positions in array arguments', () => { + const result = applyConfigArguments( + [42n], + [{ index: 1, name: 'pool', resolve: () => '0x123' }], + ); + + expect(result).toEqual([42n, '0x123']); + }); +}); diff --git a/packages/docs/content/codegen/index.mdx b/packages/docs/content/codegen/index.mdx index 7aae1b3f4..5389d9fe7 100644 --- a/packages/docs/content/codegen/index.mdx +++ b/packages/docs/content/codegen/index.mdx @@ -92,6 +92,7 @@ The `SuiCodegenConfig` type supports the following options: | `prune` | `boolean` | `true` | When enabled, only generates code for the main package and omits dependency modules (dependency types referenced by included types are still generated under `deps/`) | | `generateSummaries` | `boolean` | `true` | Automatically run `sui move summary` before generating code. Creates a `package_summaries` directory in your Move package which can be added to `.gitignore` | | `generate` | `GenerateOptions` | - | Default [generate options](#the-generate-option) (types, functions) for all packages | +| `configArguments` | `ConfigArguments` | - | Map function parameters and package addresses to a [runtime config object](#the-configarguments-option), shared by all packages | | `importExtension` | `'.js' \| '.ts' \| ''` | `'.js'` | File extension used in generated import statements | | `includePhantomTypeParameters` | `boolean` | `false` | Include [phantom type parameters](#phantom-types) as function arguments in generated BCS types | @@ -102,12 +103,13 @@ local (from source) or onchain (fetched from a network). #### Local packages -| Option | Type | Required | Description | -| ------------- | ------------------------ | -------- | --------------------------------------------------------- | -| `package` | `string` | yes | Package identifier (for example, `@local-pkg/my-package`) | -| `path` | `string` | yes | Path to the Move package directory | -| `packageName` | `string` | no | Custom name for generated code directory | -| `generate` | `PackageGenerateOptions` | no | Control what gets generated from this package | +| Option | Type | Required | Description | +| ----------------- | ------------------------ | -------- | ---------------------------------------------------------------------- | +| `package` | `string` | yes | Package identifier (for example, `@local-pkg/my-package`) | +| `path` | `string` | yes | Path to the Move package directory | +| `packageName` | `string` | no | Custom name for generated code directory | +| `generate` | `PackageGenerateOptions` | no | Control what gets generated from this package | +| `configArguments` | `ConfigArguments` | no | Package-scoped [config argument matchers](#the-configarguments-option) | ```typescript { @@ -121,12 +123,13 @@ local (from source) or onchain (fetched from a network). For packages already deployed onchain, generate code directly from a package ID or MVR name without needing local source code: -| Option | Type | Required | Description | -| ------------- | ------------------------ | -------- | --------------------------------------------- | -| `package` | `string` | yes | Package ID or MVR name | -| `packageName` | `string` | yes | Name for the generated code directory | -| `network` | `'mainnet' \| 'testnet'` | yes | Network to fetch the package from | -| `generate` | `PackageGenerateOptions` | no | Control what gets generated from this package | +| Option | Type | Required | Description | +| ----------------- | ------------------------ | -------- | ---------------------------------------------------------------------- | +| `package` | `string` | yes | Package ID or MVR name | +| `packageName` | `string` | yes | Name for the generated code directory | +| `network` | `'mainnet' \| 'testnet'` | yes | Network to fetch the package from | +| `generate` | `PackageGenerateOptions` | no | Control what gets generated from this package | +| `configArguments` | `ConfigArguments` | no | Package-scoped [config argument matchers](#the-configarguments-option) | ```typescript { @@ -285,6 +288,126 @@ src/contracts/ Set `prune: false` to generate all dependency modules with their full types and functions. +## The `configArguments` option + +Many SDKs built on generated bindings spend most of their wrapper code mapping values from a +per-network config object (package IDs, registry or treasury object IDs, pool addresses) into +arguments of generated functions. The `configArguments` option moves that plumbing into codegen: you +declare which Move types (or package addresses) come from a config object, and the generated +functions accept that config object directly instead of requiring those arguments on every call. + +`configArguments` maps author-chosen keys to matchers. It can be declared globally (shared by all +packages) or per package entry (merged over the global block): + +```typescript +const config: SuiCodegenConfig = { + output: './src/contracts', + packages: [ + { + package: '@myapp/core', + path: './move/core', + configArguments: { + // Non-generic type: parameters of this type resolve from `config.registry` + registry: { type: '0x...::registry::Registry' }, + // Generic type without type arguments: matches every instantiation, and the + // config value must be a resolver function + pool: { type: '0x...::pool::Pool' }, + // Fully instantiated generic: only matches parameters concretely typed with + // this exact instantiation in the Move signature + suiPool: { type: '0x...::pool::Pool<0x2::sui::SUI>' }, + // Package entry (keyed by the `package` value of a `packages` entry): adds an + // optional config key that overrides the package address used for calls + corePackageId: { package: '@myapp/core' }, + }, + }, + ], +}; +``` + +Matcher addresses are resolved through the package's address mapping, so named addresses (for +example, `myapp::registry::Registry`) also work. + +### Generated output + +For each function with matched parameters, the generated options gain a `config` property typed as +the minimal slice of keys that function actually uses. Matched parameters become optional in +`arguments` — passing one explicitly overrides config resolution: + +```typescript +export interface BorrowOptions { + package?: string; + arguments: + | BorrowArguments + | [ + pool: RawTransactionArgument | undefined, + amount: RawTransactionArgument, + ]; + config: { + pool: (ctx: ConfigResolverContext) => string | TransactionObjectArgument; + corePackageId?: string; + }; + typeArguments: [string]; +} +``` + +Config values can be a plain object ID, a transaction argument, or a resolver function. Resolver +functions receive the matched parameter's own instantiated type arguments (not the whole function's +type argument tuple), which makes them reusable across functions that use the type in different +positions: + +```typescript +const myConfig = { + registry: '0x123...', + pool: (ctx: ConfigResolverContext) => poolsByCoinType[ctx.typeArguments[0]], + corePackageId: '0xabc...', +} satisfies MyappCoreConfig; + +tx.add( + borrow({ + arguments: { amount: 100n }, + config: myConfig, + typeArguments: ['0x2::sui::SUI'], + }), +); +``` + +For generic types matched without type arguments, a resolver function is required — a static ID +can't be correct across instantiations. A parameter typed with the function's own type parameter +(for example, `Pool`) always binds to the uninstantiated matcher, even when a fully instantiated +matcher also exists; only parameters concretely instantiated in the Move signature bind to +instantiated matchers. + +Each package output also includes a `config-args.ts` file with an interface covering every declared +key (named after the package, for example `MyappCoreConfig`), for use with `satisfies` when defining +your config object. + +### Name refinement + +When a signature has two parameters of the same matched type (for example, `base_pool` and +`quote_pool`, both `Pool`), a bare type matcher is ambiguous and codegen fails with an error. +Refine the matchers with the Move parameter names: + +```typescript +configArguments: { + basePool: { type: '0x...::pool::Pool', name: 'base_pool' }, + quotePool: { type: '0x...::pool::Pool', name: 'quote_pool' }, +}, +``` + +Parameter names are only available in summaries generated from local packages — using a `name` +matcher against an onchain package's bytecode summary is a generation-time error. + +### Package address precedence + +For package entries, the address used for a generated call is resolved in this order: + +1. An explicit `options.package` argument +2. The config key declared by the package entry (for example, `config.corePackageId`) +3. The generated default (the package's MVR name or address) + +On mainnet with MVR names the config entry can be omitted entirely; on networks where the MVR name +doesn't resolve, supply the deployed package ID through the config object. + ## Phantom types In Move, phantom type parameters are type parameters that only appear at the type level and don't From 61335945ea20ea017183eab74df2047ce18132a5 Mon Sep 17 00:00:00 2001 From: Michael Hayes Date: Wed, 22 Jul 2026 14:16:21 -0700 Subject: [PATCH 2/7] fix(codegen): address multi-agent review and PR feedback for configArguments - Make the generated config property optional; missing values fail with the descriptive resolveConfigArgument error instead of a TypeError - Defer the name-matcher-on-nameless-params error until the matcher would actually win, so shared global blocks survive bytecode summaries; degrade bare-matcher double matches on nameless signatures to warn-and-skip - Route resolver-context type tags through type-origin/MVR addresses and normalize hex tags at runtime, so resolvers see consistent canonical tags - Add call-site metadata (packageAddress/module/function/parameter) to ConfigResolverContext and validate resolver return values - Harden the matcher parser (bracket balance, identifier/address validation, dedicated partial-instantiation error) and reject prototype-polluting keys - Error on unresolved package-scoped matchers, validate package entries against the run's packages, and warn on keys that match nothing - Guard applyConfigArguments against argument holes and inherited properties - Rename matcher field name -> parameterName, resolveConfigArg -> resolveConfigArgument, generated file config-args.ts -> config-arguments.ts; restrict generated interfaces to the package's own package key - Tuple-form matched suffixes become genuinely optional elements - Expand tests (172 passing) and rewrite the configArguments docs section Co-Authored-By: Claude Fable 5 --- .changeset/config-mapped-arguments.md | 6 +- .../codegen/src/cli/commands/generate/impl.ts | 52 +- packages/codegen/src/config-arguments.ts | 214 ++++++-- packages/codegen/src/config.ts | 21 +- packages/codegen/src/generate-utils.ts | 69 ++- packages/codegen/src/index.ts | 67 ++- packages/codegen/src/move-module-builder.ts | 97 +++- packages/codegen/src/render-types.ts | 12 +- .../codegen/tests/config-arguments.test.ts | 462 +++++++++++++++--- packages/codegen/tests/utils.test.ts | 111 ++++- packages/docs/content/codegen/index.mdx | 79 ++- 11 files changed, 1008 insertions(+), 182 deletions(-) diff --git a/.changeset/config-mapped-arguments.md b/.changeset/config-mapped-arguments.md index a7285a745..dceefdb1f 100644 --- a/.changeset/config-mapped-arguments.md +++ b/.changeset/config-mapped-arguments.md @@ -4,6 +4,8 @@ Add `configArguments` codegen option for mapping function parameters and package addresses to a runtime config object. Matched parameters become optional in generated `arguments` and are resolved -from a typed `config` object instead (with per-function minimal config slices, resolver functions -for generic types, and a generated per-package config interface in `config-args.ts`). Also treat +from a typed optional `config` object instead, with per-function minimal config slices, resolver +functions for generic types (receiving the matched parameter's normalized instantiation and +call-site metadata), and a generated per-package config interface in `config-arguments.ts`. +Misconfigured matchers are surfaced at generation time. Also treat `0x2::accumulator::AccumulatorRoot` as a well-known object that is auto-injected like `Clock`. diff --git a/packages/codegen/src/cli/commands/generate/impl.ts b/packages/codegen/src/cli/commands/generate/impl.ts index a1db29738..b48fcfbd8 100644 --- a/packages/codegen/src/cli/commands/generate/impl.ts +++ b/packages/codegen/src/cli/commands/generate/impl.ts @@ -49,6 +49,30 @@ export default async function generate( }) : config.packages; + // Package entries in any configArguments block must reference a package in this run — a typo'd + // name would otherwise silently never apply. + const knownPackageNames = new Set(normalizedPackages.map((p) => p.package)); + const configArgumentBlocks = [ + config.configArguments ?? {}, + ...normalizedPackages.map((p) => ('configArguments' in p ? (p.configArguments ?? {}) : {})), + ]; + for (const block of configArgumentBlocks) { + for (const [key, matcher] of Object.entries(block)) { + if ('package' in matcher && !knownPackageNames.has(matcher.package)) { + throw new Error( + `configArguments.${key} references package "${matcher.package}", which is not part of ` + + `this codegen run (packages: ${[...knownPackageNames].join(', ')})`, + ); + } + } + } + + const globalTypeConfigKeys = Object.entries(config.configArguments ?? {}) + .filter(([, matcher]) => 'type' in matcher) + .map(([key]) => key); + const unresolvedConfigKeysByPackage: Set[] = []; + const unusedConfigKeysByPackage: Set[] = []; + const generateSummaries = flags.noSummaries === undefined ? config.generateSummaries : !flags.noSummaries; @@ -141,7 +165,7 @@ export default async function generate( } : config.generate; - await generateFromPackageSummary({ + const result = await generateFromPackageSummary({ package: pkgWithOverrides, prune: flags.noPrune === undefined ? config.prune : !flags.noPrune, outputDir: flags.outputDir ?? config.output, @@ -151,5 +175,31 @@ export default async function generate( errorClass: config.errorClass, configArguments: config.configArguments, }); + + unresolvedConfigKeysByPackage.push(new Set(result.unresolvedConfigKeys)); + unusedConfigKeysByPackage.push(new Set(result.unusedConfigKeys)); + } + + if (unresolvedConfigKeysByPackage.length === 0) { + return; + } + + // Per-package unresolved global keys are only warnings (they may belong to another package in + // the run), but a global key that resolved nowhere — or matched nothing anywhere — is a + // misconfiguration for the run as a whole. + for (const key of globalTypeConfigKeys) { + if (unresolvedConfigKeysByPackage.every((keys) => keys.has(key))) { + throw new Error( + `configArguments.${key} did not resolve in any package in this codegen run — check the matcher type for typos`, + ); + } + const usedSomewhere = unresolvedConfigKeysByPackage.some( + (unresolved, i) => !unresolved.has(key) && !unusedConfigKeysByPackage[i].has(key), + ); + if (!usedSomewhere) { + console.warn( + `configArguments.${key} matched no generated function parameters in any package in this codegen run`, + ); + } } } diff --git a/packages/codegen/src/config-arguments.ts b/packages/codegen/src/config-arguments.ts index a0258732b..a9f37b82f 100644 --- a/packages/codegen/src/config-arguments.ts +++ b/packages/codegen/src/config-arguments.ts @@ -1,7 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { normalizeSuiAddress } from '@mysten/sui/utils'; +import { isValidNamedPackage, normalizeSuiAddress } from '@mysten/sui/utils'; import type { ConfigArguments } from './config.js'; import type { ModuleRegistry } from './module-registry.js'; import type { Parameter, Type } from './types/summary.js'; @@ -12,15 +12,19 @@ export type ParsedTypeTag = | { vector: ParsedTypeTag } | { datatype: { address: string; module: string; name: string; typeArguments: ParsedTypeTag[] } }; +/** Whether an entry came from the shared global block or a package-scoped block. */ +export type ConfigArgumentSource = 'global' | 'package'; + export interface TypeConfigArgument { kind: 'type'; key: string; + source: ConfigArgumentSource; address: string; module: string; name: string; /** `null` when the matcher is written without type arguments (matches every instantiation). */ typeArguments: ParsedTypeTag[] | null; - paramName?: string; + parameterName?: string; /** * Whether the matched Move type is generic. Uninstantiated matchers on generic types require a * resolver function as the config value (a static id cannot be correct across instantiations). @@ -31,12 +35,29 @@ export interface TypeConfigArgument { export interface PackageConfigArgument { kind: 'package'; key: string; + source: ConfigArgumentSource; package: string; } export type ParsedConfigArgument = TypeConfigArgument | PackageConfigArgument; const PRIMITIVES = new Set(['bool', 'u8', 'u16', 'u32', 'u64', 'u128', 'u256', 'address']); +const MOVE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/; +const HEX_ADDRESS = /^0x[0-9a-fA-F]{1,64}$/; + +function assertBalancedBrackets(tag: string) { + let depth = 0; + for (const char of tag) { + if (char === '<') depth++; + if (char === '>') depth--; + if (depth < 0) { + throw new Error(`Invalid type in configArguments matcher: "${tag}" (unbalanced '>')`); + } + } + if (depth !== 0) { + throw new Error(`Invalid type in configArguments matcher: "${tag}" (unbalanced '<')`); + } +} function splitTopLevelTypeArgs(inner: string): string[] { const parts: string[] = []; @@ -52,19 +73,41 @@ function splitTopLevelTypeArgs(inner: string): string[] { if (char === '>') depth--; current += char; } - if (current.trim()) parts.push(current.trim()); + parts.push(current.trim()); return parts; } -function parseTypeTag(tag: string, resolveAddress: (address: string) => string): ParsedTypeTag { +function parseTypeTag( + tag: string, + resolveAddress: (address: string) => string, + { isTypeArgument = false, root = tag }: { isTypeArgument?: boolean; root?: string } = {}, +): ParsedTypeTag { const trimmed = tag.trim(); + if (!isTypeArgument) { + assertBalancedBrackets(trimmed); + } + if (PRIMITIVES.has(trimmed)) { return { prim: trimmed }; } if (trimmed.startsWith('vector<') && trimmed.endsWith('>')) { - return { vector: parseTypeTag(trimmed.slice('vector<'.length, -1), resolveAddress) }; + return { + vector: parseTypeTag(trimmed.slice('vector<'.length, -1), resolveAddress, { + isTypeArgument: true, + root, + }), + }; + } + + // A bare identifier in type-argument position is a type-parameter placeholder like `Pool`. + if (isTypeArgument && MOVE_IDENTIFIER.test(trimmed)) { + throw new Error( + `configArguments matcher "${root}" contains the type parameter "${trimmed}" — partially ` + + `instantiated matchers are not supported. Use an uninstantiated matcher (no type ` + + `arguments) with a resolver function instead.`, + ); } const lt = trimmed.indexOf('<'); @@ -81,50 +124,118 @@ function parseTypeTag(tag: string, resolveAddress: (address: string) => string): throw new Error(`Invalid type in configArguments matcher: "${tag}"`); } + const [addressPart, modulePart, namePart] = parts; + + if (!MOVE_IDENTIFIER.test(modulePart) || !MOVE_IDENTIFIER.test(namePart)) { + throw new Error( + `Invalid type in configArguments matcher: "${tag}" ("${modulePart}::${namePart}" is not a valid module::type pair)`, + ); + } + + if (isValidNamedPackage(addressPart)) { + throw new Error( + `Invalid address "${addressPart}" in configArguments matcher "${tag}": MVR names cannot be ` + + `matched against package summaries. Use the package's named address from ` + + `address_mapping.json or its hex address instead.`, + ); + } + + if (!HEX_ADDRESS.test(addressPart) && !MOVE_IDENTIFIER.test(addressPart)) { + throw new Error(`Invalid address "${addressPart}" in configArguments matcher "${tag}"`); + } + const typeArguments = lt === -1 ? [] - : splitTopLevelTypeArgs(trimmed.slice(lt + 1, -1)).map((arg) => - parseTypeTag(arg, resolveAddress), - ); + : splitTopLevelTypeArgs(trimmed.slice(lt + 1, -1)).map((arg) => { + if (arg.length === 0) { + throw new Error( + `Invalid type in configArguments matcher: "${tag}" (empty type argument)`, + ); + } + return parseTypeTag(arg, resolveAddress, { isTypeArgument: true, root }); + }); return { datatype: { - address: resolveMatcherAddress(parts[0], resolveAddress), - module: parts[1], - name: parts[2], + address: resolveMatcherAddress(addressPart, resolveAddress), + module: modulePart, + name: namePart, typeArguments, }, }; } -const HEX_ADDRESS = /^0x[0-9a-fA-F]{1,64}$/; - function resolveMatcherAddress(address: string, resolveAddress: (address: string) => string) { const resolved = resolveAddress(address); return HEX_ADDRESS.test(resolved) ? normalizeSuiAddress(resolved) : resolved; } /** - * Parse and validate a `configArguments` record against the modules loaded in `registry`. - * All addresses (in matchers and during matching) are resolved through the registry's address - * mapping and normalized, so matchers can use named addresses from `address_mapping.json`. + * Check that every datatype nested in an instantiated matcher is itself fully instantiated, + * as far as the summaries can tell (unknown types are skipped). + */ +function assertFullyInstantiated( + tag: ParsedTypeTag, + registry: ModuleRegistry, + matcherType: string, +) { + if ('prim' in tag) return; + if ('vector' in tag) { + assertFullyInstantiated(tag.vector, registry, matcherType); + return; + } + + const { address, module, name, typeArguments } = tag.datatype; + const summary = registry.getSummaryByResolvedAddress(address, module); + const arity = (summary?.structs[name] ?? summary?.enums[name])?.type_parameters.length; + + if (arity !== undefined && arity !== typeArguments.length) { + throw new Error( + `configArguments matcher "${matcherType}": ${module}::${name} expects ${arity} type ` + + `argument(s), got ${typeArguments.length}. Partially instantiated matchers are not ` + + `supported — use an uninstantiated matcher (no type arguments) with a resolver function instead.`, + ); + } + + for (const argument of typeArguments) { + assertFullyInstantiated(argument, registry, matcherType); + } +} + +/** + * Parse and validate `configArguments` blocks against the modules loaded in `registry`. + * Per-package entries are merged over global entries (per key). All addresses (in matchers and + * during matching) are resolved through the registry's address mapping and normalized, so + * matchers can use named addresses from `address_mapping.json`. * - * Type matchers referencing types that don't exist in this package's summaries are returned in - * `unresolvedKeys` instead of the entry list — a shared global `configArguments` block may span + * Type matchers referencing types that don't exist in this package's summaries are a hard error + * when declared in a package-scoped block (they can only refer to this package's summaries), and + * are returned in `unresolvedKeys` when declared globally — a shared global block may span * multiple packages in one codegen run, and entries for other packages can't match anything here. */ export function parseConfigArguments( - configArguments: ConfigArguments, + blocks: { global?: ConfigArguments; package?: ConfigArguments }, registry: ModuleRegistry, ): { entries: ParsedConfigArgument[]; unresolvedKeys: string[] } { const resolveAddress = (address: string) => registry.resolveAddress(address); const entries: ParsedConfigArgument[] = []; const unresolvedKeys: string[] = []; - for (const [key, matcher] of Object.entries(configArguments)) { + const merged = new Map< + string, + { matcher: NonNullable; source: ConfigArgumentSource } + >(); + for (const [key, matcher] of Object.entries(blocks.global ?? {})) { + merged.set(key, { matcher, source: 'global' }); + } + for (const [key, matcher] of Object.entries(blocks.package ?? {})) { + merged.set(key, { matcher, source: 'package' }); + } + + for (const [key, { matcher, source }] of merged) { if ('package' in matcher) { - entries.push({ kind: 'package', key, package: matcher.package }); + entries.push({ kind: 'package', key, source, package: matcher.package }); continue; } @@ -141,6 +252,14 @@ export function parseConfigArguments( const datatype = summary?.structs[name] ?? summary?.enums[name]; if (!datatype) { + if (source === 'package') { + throw new Error( + `configArguments.${key}: type "${matcher.type}" was not found in this package's ` + + `summaries. Package-scoped configArguments can only reference types reachable from ` + + `this package — fix the type, or move the entry to the global configArguments block ` + + `if it targets another package.`, + ); + } unresolvedKeys.push(key); continue; } @@ -156,14 +275,19 @@ export function parseConfigArguments( ); } + for (const argument of typeArguments) { + assertFullyInstantiated(argument, registry, matcher.type); + } + entries.push({ kind: 'type', key, + source, address, module, name, typeArguments: uninstantiated ? null : typeArguments, - paramName: matcher.name, + parameterName: matcher.parameterName, isGeneric, }); } @@ -210,8 +334,13 @@ function typeEqualsTag( * Find the config entry matching a function parameter, or `null`. * * Most-specific matcher wins, decided statically: a fully instantiated matcher beats an - * uninstantiated one, and a `name`-refined matcher beats a bare one at the same level. Ties are a - * hard generation-time error. + * uninstantiated one, and a `parameterName`-refined matcher beats a bare one at the same level. + * Ties are a hard generation-time error. + * + * A `parameterName`-refined matcher never matches a parameter without a name — but if such a + * matcher would otherwise apply (type and instantiation match) and nothing else matches the + * parameter, that is a hard error: the matcher clearly targets this parameter's type and only the + * missing parameter names (bytecode summaries don't include them) prevent matching it. */ export function findConfigArgumentMatch( param: Parameter, @@ -237,6 +366,7 @@ export function findConfigArgumentMatch( const paramAddress = resolveMatcherAddress(Datatype.module.address, resolveAddress); const candidates: { entry: TypeConfigArgument; specificity: number }[] = []; + const blockedNameMatchers: TypeConfigArgument[] = []; for (const entry of entries) { if (entry.kind !== 'type') continue; @@ -248,16 +378,7 @@ export function findConfigArgumentMatch( continue; } - if (entry.paramName && param.name === undefined) { - throw new Error( - `configArguments.${entry.key} uses a parameter-name matcher, but parameters of ${functionLabel} have no names. ` + - `Name matchers are only supported for summaries generated from local packages.`, - ); - } - - if (entry.paramName && entry.paramName !== param.name) { - continue; - } + let specificity = 0; if (entry.typeArguments !== null) { // Fully instantiated matcher: only matches parameters concretely typed with that @@ -270,13 +391,32 @@ export function findConfigArgumentMatch( ) { continue; } - candidates.push({ entry, specificity: 2 + (entry.paramName ? 1 : 0) }); - } else { - candidates.push({ entry, specificity: entry.paramName ? 1 : 0 }); + specificity += 2; } + + if (entry.parameterName) { + if (param.name === undefined) { + blockedNameMatchers.push(entry); + continue; + } + if (entry.parameterName !== param.name) { + continue; + } + specificity += 1; + } + + candidates.push({ entry, specificity }); } if (candidates.length === 0) { + if (blockedNameMatchers.length > 0) { + throw new Error( + `configArguments ${blockedNameMatchers.map((entry) => entry.key).join(', ')} use ` + + `parameterName matchers that would apply to a parameter of ${functionLabel}, but its ` + + `parameters have no names (bytecode summaries do not include parameter names). Remove ` + + `the parameterName refinement, or exclude this package from the matcher's scope.`, + ); + } return null; } diff --git a/packages/codegen/src/config.ts b/packages/codegen/src/config.ts index 807b2973d..6b8e3c653 100644 --- a/packages/codegen/src/config.ts +++ b/packages/codegen/src/config.ts @@ -33,9 +33,11 @@ export const moduleGenerateSchema = z.object({ }); const IDENTIFIER = /^[A-Za-z_$][\w$]*$/; +/** Keys that would collide with `Object.prototype` or mutate prototypes on plain objects. */ +const FORBIDDEN_CONFIG_KEYS = new Set(['__proto__', 'constructor', 'prototype']); export const configArgumentMatcherSchema = z.union([ - z.object({ + z.strictObject({ /** * Fully-qualified Move type to match function parameters against, e.g. * `0x...::margin_registry::MarginRegistry`. A generic type written without type arguments @@ -49,9 +51,9 @@ export const configArgumentMatcherSchema = z.union([ * matched type. Only supported for summaries generated from local packages (bytecode * summaries do not include parameter names). */ - name: z.string().optional(), + parameterName: z.string().optional(), }), - z.object({ + z.strictObject({ /** * Package entry, keyed by the package's name/MVR name from the `packages` config. Adds an * optional config key that overrides the package address used for generated calls. @@ -61,10 +63,15 @@ export const configArgumentMatcherSchema = z.union([ ]); export const configArgumentsSchema = z.record( - z.string().regex(IDENTIFIER, { - message: - 'configArguments keys become properties of the generated config interface and must be valid identifiers', - }), + z + .string() + .regex(IDENTIFIER, { + message: + 'configArguments keys become properties of the generated config interface and must be valid identifiers', + }) + .refine((key) => !FORBIDDEN_CONFIG_KEYS.has(key), { + message: 'configArguments keys must not be prototype property names', + }), configArgumentMatcherSchema, ); diff --git a/packages/codegen/src/generate-utils.ts b/packages/codegen/src/generate-utils.ts index 6e07a5519..373b5cb91 100644 --- a/packages/codegen/src/generate-utils.ts +++ b/packages/codegen/src/generate-utils.ts @@ -154,7 +154,10 @@ export function normalizeMoveArguments( throw new __ERROR_CLASS__(\`Expected arguments to be passed as an array\`); } const name = parameterNames[index]; - arg = args[name as keyof typeof args]; + arg = + name !== undefined && Object.prototype.hasOwnProperty.call(args, name) + ? args[name as keyof typeof args] + : undefined; if (arg === undefined) { throw new __ERROR_CLASS__(\`Parameter \${name} is required\`); @@ -193,9 +196,18 @@ export function normalizeMoveArguments( export interface ConfigResolverContext { /** * The matched parameter's own instantiated type arguments (not the whole function's type - * argument tuple), as fully-qualified type tags. + * argument tuple), as fully-qualified type tags. Hex-addressed struct tags are normalized to + * their long form before the resolver is invoked; MVR-named tags are passed through unchanged. */ typeArguments: string[]; + /** The package address the generated call will be sent to. */ + packageAddress: string; + /** The Move module of the generated call. */ + moduleName: string; + /** The Move function of the generated call. */ + functionName: string; + /** The Move name of the matched parameter, when the summary includes parameter names. */ + parameterName?: string; } /** @@ -210,7 +222,19 @@ export type ConfigValue = | Exclude unknown> | ((ctx: ConfigResolverContext) => string | TransactionObjectArgument); -export function resolveConfigArg( +/** Normalize a hex-addressed struct tag to its long form; pass anything else through. */ +function normalizeConfigTypeTag(tag: string): string { + if (/[@/]/.test(tag) || !tag.includes('::')) { + return tag; + } + try { + return normalizeStructTag(tag); + } catch { + return tag; + } +} + +export function resolveConfigArgument( value: ConfigValue | undefined, ctx: ConfigResolverContext, name: string, @@ -221,7 +245,22 @@ export function resolveConfigArg( ); } - return typeof value === 'function' ? value(ctx) : value; + if (typeof value !== 'function') { + return value; + } + + const resolved = value({ + ...ctx, + typeArguments: ctx.typeArguments.map(normalizeConfigTypeTag), + }); + + if (resolved == null) { + throw new __ERROR_CLASS__( + \`Config resolver for "\${name}" returned \${resolved} (\${ctx.moduleName}::\${ctx.functionName}, typeArguments: [\${ctx.typeArguments.join(', ')}])\`, + ); + } + + return resolved; } /** @@ -234,18 +273,36 @@ export function applyConfigArguments( ): T { if (Array.isArray(args)) { const result = [...args]; + const matchedIndexes = new Set(defaults.map((entry) => entry.index)); for (const entry of defaults) { if (result[entry.index] === undefined) { result[entry.index] = entry.resolve(); } } + // Filling a trailing config-mapped position can extend the array past positions the + // caller omitted; catch those holes here rather than failing deep inside serialization. + for (let i = 0; i < result.length; i++) { + if (result[i] === undefined && !matchedIndexes.has(i)) { + throw new __ERROR_CLASS__(\`Missing argument at position \${i}\`); + } + } return result as T; } const result: Record = { ...args }; for (const entry of defaults) { - if (entry.name !== undefined && result[entry.name] === undefined) { - result[entry.name] = entry.resolve(); + if (entry.name === undefined) { + continue; + } + // Own-property check so inherited properties (e.g. a key named "constructor") are never + // mistaken for explicitly passed arguments. + if (!Object.prototype.hasOwnProperty.call(result, entry.name) || result[entry.name] === undefined) { + Object.defineProperty(result, entry.name, { + value: entry.resolve(), + enumerable: true, + writable: true, + configurable: true, + }); } } return result as T; diff --git a/packages/codegen/src/index.ts b/packages/codegen/src/index.ts index 5d26435c8..3c827317e 100644 --- a/packages/codegen/src/index.ts +++ b/packages/codegen/src/index.ts @@ -165,16 +165,17 @@ export async function generateFromPackageSummary({ ) ).flat(); - const effectiveConfigArguments: ConfigArguments = { - ...globalConfigArguments, - ...pkg.configArguments, - }; - - const { entries: configArgumentEntries, unresolvedKeys } = Object.keys(effectiveConfigArguments) - .length - ? parseConfigArguments(effectiveConfigArguments, registry) - : { entries: [], unresolvedKeys: [] }; - + const { entries: configArgumentEntries, unresolvedKeys } = + Object.keys(globalConfigArguments ?? {}).length || Object.keys(pkg.configArguments ?? {}).length + ? parseConfigArguments( + { global: globalConfigArguments, package: pkg.configArguments }, + registry, + ) + : { entries: [], unresolvedKeys: [] }; + + // Unresolved keys here are always from the global block (package-scoped unresolved matchers + // throw during parsing). They may belong to another package generated in the same run — the + // CLI aggregates these across packages and errors for keys that resolve nowhere. if (unresolvedKeys.length > 0) { console.warn( `configArguments keys not resolvable in ${pkg.package} (skipped): ${unresolvedKeys.join(', ')}`, @@ -266,21 +267,55 @@ export async function generateFromPackageSummary({ }), ); + const usedConfigKeys = new Set(); + for (const mod of modules) { + for (const key of mod.builder.usedConfigKeys) { + usedConfigKeys.add(key); + } + } + + const unusedTypeEntries = configArgumentEntries.filter( + (entry) => entry.kind === 'type' && !usedConfigKeys.has(entry.key), + ); + // Package-scoped entries can only target this package, so an unused one is a misconfiguration + // (wrong parameterName, wrong instantiation, or a function filtered out of generation). + const unusedPackageScoped = unusedTypeEntries.filter((entry) => entry.source === 'package'); + if (unusedPackageScoped.length > 0) { + console.warn( + `configArguments keys that matched no generated function parameters in ${pkg.package}: ${unusedPackageScoped + .map((entry) => entry.key) + .join(', ')}`, + ); + } + if (configArgumentEntries.length > 0) { await generateConfigInterface({ packageOutputDir, outputDir, packageName, - entries: configArgumentEntries, + entries: configArgumentEntries.filter( + (entry) => entry.kind === 'type' || entry.package === pkg.package, + ), importExtension, }); } + + return { + /** Global-block keys whose matcher type was not found in this package's summaries. */ + unresolvedConfigKeys: unresolvedKeys, + /** Global-block type keys that resolved here but matched no generated parameter. */ + unusedConfigKeys: unusedTypeEntries + .filter((entry) => entry.source === 'global') + .map((entry) => entry.key), + }; } /** - * Emit `//config-args.ts` with a convenience interface covering every - * declared config key, for `satisfies` on the user side. The hyphenated filename can never - * collide with a generated Move module file. + * Emit `//config-arguments.ts` with a convenience interface covering this + * package's resolvable config keys (and its own package-address key), for `satisfies` on the user + * side. With a global block spanning multiple packages, intersect the per-package interfaces + * (`CoreConfig & MarginConfig`). The hyphenated filename can never collide with a generated Move + * module file. */ async function generateConfigInterface({ packageOutputDir, @@ -327,8 +362,8 @@ async function generateConfigInterface({ await mkdir(packageOutputDir, { recursive: true }); await writeFile( - join(packageOutputDir, 'config-args.ts'), - await builder.toString(packageOutputDir, 'config-args.ts', outputDir), + join(packageOutputDir, 'config-arguments.ts'), + await builder.toString(packageOutputDir, 'config-arguments.ts', outputDir), ); } diff --git a/packages/codegen/src/move-module-builder.ts b/packages/codegen/src/move-module-builder.ts index 093503af1..b35843bb6 100644 --- a/packages/codegen/src/move-module-builder.ts +++ b/packages/codegen/src/move-module-builder.ts @@ -23,7 +23,7 @@ import { parseTS, withComment, } from './utils.js'; -import type { Fields, ModuleSummary, Type, TypeParameter } from './types/summary.js'; +import type { Datatype, Fields, ModuleSummary, Type, TypeParameter } from './types/summary.js'; import type { FunctionsOption, ImportExtension, TypesOption } from './config.js'; import { join } from 'node:path'; import { isValidSuiObjectId } from '@mysten/sui/utils'; @@ -38,7 +38,7 @@ const IMPORT_MAP = { MoveEnum: { module: '~outputRoot/utils/index', isType: false }, normalizeMoveArguments: { module: '~outputRoot/utils/index', isType: false }, RawTransactionArgument: { module: '~outputRoot/utils/index', isType: true }, - resolveConfigArg: { module: '~outputRoot/utils/index', isType: false }, + resolveConfigArgument: { module: '~outputRoot/utils/index', isType: false }, applyConfigArguments: { module: '~outputRoot/utils/index', isType: false }, ConfigValue: { module: '~outputRoot/utils/index', isType: true }, ConfigResolverContext: { module: '~outputRoot/utils/index', isType: true }, @@ -62,6 +62,8 @@ export class MoveModuleBuilder extends FileBuilder { #includePhantomTypeParameters: boolean; #configArguments: ParsedConfigArgument[] = []; #packageConfigKey?: string; + /** Config keys that matched at least one parameter of a rendered function. */ + readonly usedConfigKeys = new Set(); constructor({ mvrNameOrAddress, @@ -164,6 +166,34 @@ export class MoveModuleBuilder extends FileBuilder { this.#packageConfigKey = packageConfigKey; } + /** + * The address this module's generated type tags use for `name`: the type-origin address when + * known, otherwise the same address BCS type names use (MVR name / root package id / resolved + * summary address). + */ + getTypeTagAddress(name: string): string { + const origin = this.#typeOrigins?.[name]; + if (origin) { + return origin; + } + const moduleTypeName = this.#getModuleTypeName(); + return moduleTypeName.startsWith('0x') || /[@/]/.test(moduleTypeName) + ? moduleTypeName + : this.#resolveAddress(moduleTypeName); + } + + /** + * Address for a datatype appearing in a resolver-context type tag: delegate to the defining + * module's builder so origins and MVR names match generated BCS type names. + */ + #getResolverTagAddress(datatype: Datatype): string { + const builder = this.registry.getBuilder(datatype.module.address, datatype.module.name); + if (builder) { + return builder.getTypeTagAddress(datatype.name); + } + return this.#resolveAddress(datatype.module.address); + } + override async getHeader() { if (!this.summary.doc) { return super.getHeader(); @@ -579,7 +609,7 @@ export class MoveModuleBuilder extends FileBuilder { // object instead of being required arguments. const configMatches = new Map(); if (this.#configArguments.length > 0) { - const bareMatches = new Map(); + const bareMatches = new Map(); requiredParameters.forEach((param, i) => { const match = findConfigArgumentMatch(param, this.#configArguments, { resolveAddress: (address) => this.#resolveAddress(address), @@ -587,21 +617,34 @@ export class MoveModuleBuilder extends FileBuilder { }); if (!match) return; configMatches.set(i, match); - if (!match.paramName) { - bareMatches.set(match.key, [ - ...(bareMatches.get(match.key) ?? []), - param.name ?? `#${i}`, - ]); + if (!match.parameterName) { + bareMatches.set(match.key, [...(bareMatches.get(match.key) ?? []), i]); } }); for (const [key, matched] of bareMatches) { if (matched.length > 1) { - throw new Error( - `configArguments.${key} matches multiple parameters of ${functionLabel} (${matched.join(', ')}). ` + - `Add a \`name\` refinement to disambiguate.`, + if (matched.every((i) => requiredParameters[i].name)) { + throw new Error( + `configArguments.${key} matches multiple parameters of ${functionLabel} (${matched + .map((i) => requiredParameters[i].name) + .join(', ')}). Add a \`parameterName\` refinement to disambiguate.`, + ); + } + // Bytecode summaries have no parameter names, so refinement is impossible — + // skip config mapping for this function instead of failing the run. + console.warn( + `configArguments.${key} matches multiple parameters of ${functionLabel}, which cannot ` + + `be disambiguated because its parameters have no names. Config mapping is skipped ` + + `for this function.`, ); + for (const i of matched) { + configMatches.delete(i); + } } } + for (const match of configMatches.values()) { + this.usedConfigKeys.add(match.key); + } } const hasConfigMatches = configMatches.size > 0; @@ -648,14 +691,21 @@ export class MoveModuleBuilder extends FileBuilder { ) .join(',\n'); - // Tuple items: optional tuple elements can't precede required ones, so config-matched - // positions accept an explicit `undefined` instead. + // Tuple items: a config-matched suffix becomes genuinely optional tuple elements. + // Optional elements can't precede required ones, so matched positions followed by a + // required one accept an explicit `undefined` instead. + const lastUnmatchedIndex = renderedArgTypes.reduce( + (last, _, i) => (configMatches.has(i) ? last : i), + -1, + ); const argumentTupleItems = renderedArgTypes .map((type, i) => { + const paramName = requiredParameters[i].name; + if (configMatches.has(i) && i > lastUnmatchedIndex) { + return paramName ? `${camelCase(paramName)}?: ${wrap(type)}` : `${wrap(type)}?`; + } const itemType = configMatches.has(i) ? `${wrap(type)} | undefined` : wrap(type); - return requiredParameters[i].name - ? `${camelCase(requiredParameters[i].name)}: ${itemType}` - : itemType; + return paramName ? `${camelCase(paramName)}: ${itemType}` : itemType; }) .join(',\n'); @@ -716,8 +766,7 @@ export class MoveModuleBuilder extends FileBuilder { configSliceFields.push(`${packageConfigKey}?: string`); } - const argumentsOptional = - requiredParameters.length === 0 || requiredParameters.every((_, i) => configMatches.has(i)); + const argumentsOptional = requiredParameters.every((_, i) => configMatches.has(i)); this.statements.push( ...parseTS /* ts */ `export interface ${optionsInterface}${genericTypes} { @@ -729,7 +778,7 @@ export class MoveModuleBuilder extends FileBuilder { }, ${ configSliceFields.length > 0 - ? `config${hasConfigMatches ? '' : '?'}: { + ? `config?: { ${configSliceFields.join(',\n')} },` : '' @@ -759,10 +808,14 @@ export class MoveModuleBuilder extends FileBuilder { summary: this.summary, typeParameters: func.type_parameters, registry: this.registry, + getDatatypeTagAddress: (datatype) => this.#getResolverTagAddress(datatype), }); return tag.includes('${') ? `\`${tag}\`` : `'${tag}'`; }); - return `{ index: ${i}, ${param.name ? `name: ${JSON.stringify(camelCase(param.name))}, ` : ''}resolve: () => ${this.#getImportName('resolveConfigArg')}(options.config.${match.key}, { typeArguments: [${ctxTags.join(', ')}] }, ${JSON.stringify(match.key)}) }`; + const ctx = `{ typeArguments: [${ctxTags.join(', ')}], packageAddress, moduleName: '${this.summary.id.name}', functionName: '${name}'${ + param.name ? `, parameterName: ${JSON.stringify(param.name)}` : '' + } }`; + return `{ index: ${i}, ${param.name ? `name: ${JSON.stringify(camelCase(param.name))}, ` : ''}resolve: () => ${this.#getImportName('resolveConfigArgument')}(options.config?.${match.key}, ${ctx}, ${JSON.stringify(match.key)}) }`; }); const baseArgumentsExpr = `options.arguments${ @@ -779,9 +832,7 @@ export class MoveModuleBuilder extends FileBuilder { : baseArgumentsExpr; const packageAddressExpr = `options.package${ - packageConfigKey - ? ` ?? options.config${hasConfigMatches ? '' : '?'}.${packageConfigKey}` - : '' + packageConfigKey ? ` ?? options.config?.${packageConfigKey}` : '' }${packageIsRequired ? '' : ` ?? '${this.#mvrNameOrAddress}'`}`; this.statements.push( diff --git a/packages/codegen/src/render-types.ts b/packages/codegen/src/render-types.ts index 36af2dc10..89ad00f00 100644 --- a/packages/codegen/src/render-types.ts +++ b/packages/codegen/src/render-types.ts @@ -193,7 +193,14 @@ export function renderTypeSignature(type: Type, options: RenderTypeSignatureOpti */ export function renderResolverTypeTag( type: Type, - options: Pick, + options: Pick & { + /** + * Overrides the address used for a concrete datatype (e.g. to use type-origin addresses or + * MVR names consistent with generated BCS type names). Falls back to the registry's + * address mapping. + */ + getDatatypeTagAddress?: (datatype: Datatype) => string; + }, ): string { if (typeof type === 'string') { if (type === 'signer' || type === '_') { @@ -225,7 +232,8 @@ export function renderResolverTypeTag( if ('Datatype' in type) { const { Datatype } = type; - const address = resolveAddress(options, Datatype.module.address); + const address = + options.getDatatypeTagAddress?.(Datatype) ?? resolveAddress(options, Datatype.module.address); const base = `${address}::${Datatype.module.name}::${Datatype.name}`; if (Datatype.type_arguments.length === 0) { return base; diff --git a/packages/codegen/tests/config-arguments.test.ts b/packages/codegen/tests/config-arguments.test.ts index d95adb409..edcb18bc1 100644 --- a/packages/codegen/tests/config-arguments.test.ts +++ b/packages/codegen/tests/config-arguments.test.ts @@ -10,6 +10,7 @@ import { ModuleRegistry } from '../src/module-registry.js'; import { MoveModuleBuilder } from '../src/move-module-builder.js'; import { parseConfigArguments } from '../src/config-arguments.js'; import { generateFromPackageSummary } from '../src/index.js'; +import { configArgumentsSchema } from '../src/config.js'; import type { ConfigArguments } from '../src/config.js'; const FIXTURE_PATH = join(__dirname, 'move/testpkg'); @@ -35,7 +36,7 @@ async function createBuilders(configArguments: ConfigArguments, packageConfigKey '@test/testpkg', ); - const { entries } = parseConfigArguments(configArguments, registry); + const { entries } = parseConfigArguments({ global: configArguments }, registry); counter.setConfigArguments(entries, packageConfigKey); registryBuilder.setConfigArguments(entries, packageConfigKey); @@ -49,7 +50,8 @@ async function render(builder: MoveModuleBuilder) { /** * A synthetic module with a generic `Pool` type used by functions in generic, - * concretely-instantiated, and same-type-twice positions. + * concretely-instantiated, and same-type-twice positions, plus a `Coin` type used as a concrete + * own-package type argument. */ function poolsSummary({ parameterNames = true }: { parameterNames?: boolean } = {}) { const poolType = (typeArgument: unknown) => ({ @@ -67,6 +69,9 @@ function poolsSummary({ parameterNames = true }: { parameterNames?: boolean } = const suiType = { Datatype: { module: { address: 'sui', name: 'sui' }, name: 'SUI', type_arguments: [] }, }; + const ownCoinType = { + Datatype: { module: { address: 'testpkg', name: 'pools' }, name: 'Coin', type_arguments: [] }, + }; const param = (name: string, type_: unknown) => (parameterNames ? { name, type_ } : { type_ }); const fn = (parameters: unknown[], type_parameters: unknown[] = []) => ({ source_index: 0, @@ -92,6 +97,7 @@ function poolsSummary({ parameterNames = true }: { parameterNames?: boolean } = [{ name: 'T', phantom: false, constraints: [] }], ), use_concrete: fn([param('pool', poolType(suiType)), param('amount', 'u64')]), + use_own_coin: fn([param('pool', poolType(ownCoinType)), param('amount', 'u64')]), swap: fn( [ param('base_pool', poolType({ TypeParameter: 0 })), @@ -115,6 +121,17 @@ function poolsSummary({ parameterNames = true }: { parameterNames?: boolean } = fields: { id: { index: 0, doc: null, type_: 'address' } }, }, }, + Coin: { + index: 1, + doc: '', + attributes: [], + abilities: ['Store'], + type_parameters: [], + fields: { + positional_fields: false, + fields: { value: { index: 0, doc: null, type_: 'u64' } }, + }, + }, }, enums: {}, }; @@ -122,7 +139,7 @@ function poolsSummary({ parameterNames = true }: { parameterNames?: boolean } = function createPoolsBuilder( configArguments: ConfigArguments, - options: { parameterNames?: boolean } = {}, + options: { parameterNames?: boolean; typeOrigins?: Record } = {}, ) { const registry = new ModuleRegistry(ADDRESS_MAPPINGS); const builder = new MoveModuleBuilder({ @@ -130,12 +147,34 @@ function createPoolsBuilder( registry, mvrNameOrAddress: '@test/testpkg', importExtension: '.js', + typeOrigins: options.typeOrigins, }); - const { entries } = parseConfigArguments(configArguments, registry); + const { entries } = parseConfigArguments({ global: configArguments }, registry); builder.setConfigArguments(entries); return builder; } +describe('configArguments schema', () => { + it('rejects prototype-polluting keys', () => { + // zod itself drops `__proto__` record keys; the others are rejected by the key schema. + expect( + Object.keys(configArgumentsSchema.parse({ ['__proto__']: { type: '0x2::sui::SUI' } })), + ).toEqual([]); + expect(() => + configArgumentsSchema.parse({ constructor: { type: '0x2::sui::SUI' } }), + ).toThrowError(/prototype property names/); + expect(() => + configArgumentsSchema.parse({ prototype: { type: '0x2::sui::SUI' } }), + ).toThrowError(/prototype property names/); + }); + + it('rejects matchers mixing type and package', () => { + expect(() => + configArgumentsSchema.parse({ both: { type: '0x2::sui::SUI', package: '@x/y' } }), + ).toThrowError(); + }); +}); + describe('parseConfigArguments', () => { it('parses type, instantiated type, and package matchers', async () => { const registry = new ModuleRegistry(ADDRESS_MAPPINGS); @@ -148,9 +187,11 @@ describe('parseConfigArguments', () => { const { entries, unresolvedKeys } = parseConfigArguments( { - pool: { type: 'testpkg::pools::Pool' }, - suiPool: { type: 'testpkg::pools::Pool<0x2::sui::SUI>' }, - pkg: { package: '@test/testpkg' }, + global: { + pool: { type: 'testpkg::pools::Pool' }, + suiPool: { type: 'testpkg::pools::Pool<0x2::sui::SUI>' }, + pkg: { package: '@test/testpkg' }, + }, }, registry, ); @@ -160,6 +201,7 @@ describe('parseConfigArguments', () => { { kind: 'type', key: 'pool', + source: 'global', module: 'pools', name: 'Pool', typeArguments: null, @@ -184,7 +226,36 @@ describe('parseConfigArguments', () => { ]); }); - it('reports matchers for types that are not in the summaries as unresolved', async () => { + it('merges package-scoped entries over global entries per key', async () => { + const registry = new ModuleRegistry(ADDRESS_MAPPINGS); + new MoveModuleBuilder({ + summary: poolsSummary() as any, + registry, + mvrNameOrAddress: '@test/testpkg', + importExtension: '.js', + }); + + const { entries } = parseConfigArguments( + { + global: { + pool: { type: 'testpkg::pools::Pool' }, + coin: { type: 'testpkg::pools::Coin' }, + }, + package: { + pool: { type: 'testpkg::pools::Pool<0x2::sui::SUI>' }, + }, + }, + registry, + ); + + // Map insertion order keeps the global position for overridden keys. + expect(entries).toMatchObject([ + { key: 'pool', source: 'package', typeArguments: [{ datatype: { name: 'SUI' } }] }, + { key: 'coin', source: 'global', typeArguments: [] }, + ]); + }); + + it('reports global matchers for types that are not in the summaries as unresolved', async () => { const registry = new ModuleRegistry(ADDRESS_MAPPINGS); new MoveModuleBuilder({ summary: poolsSummary() as any, @@ -195,9 +266,11 @@ describe('parseConfigArguments', () => { const { entries, unresolvedKeys } = parseConfigArguments( { - missingType: { type: 'testpkg::pools::DoesNotExist' }, - missingModule: { type: '0x999::other::Thing' }, - pool: { type: 'testpkg::pools::Pool' }, + global: { + missingType: { type: 'testpkg::pools::DoesNotExist' }, + missingModule: { type: '0x999::other::Thing' }, + pool: { type: 'testpkg::pools::Pool' }, + }, }, registry, ); @@ -206,17 +279,70 @@ describe('parseConfigArguments', () => { expect(entries.map((entry) => entry.key)).toEqual(['pool']); }); + it('errors for package-scoped matchers whose type is not in the summaries', async () => { + const registry = new ModuleRegistry(ADDRESS_MAPPINGS); + new MoveModuleBuilder({ + summary: poolsSummary() as any, + registry, + mvrNameOrAddress: '@test/testpkg', + importExtension: '.js', + }); + + expect(() => + parseConfigArguments( + { package: { missing: { type: 'testpkg::pools::DoesNotExist' } } }, + registry, + ), + ).toThrowError(/was not found in this package's summaries/); + }); + it('rejects malformed matcher types', async () => { const registry = new ModuleRegistry(ADDRESS_MAPPINGS); + new MoveModuleBuilder({ + summary: poolsSummary() as any, + registry, + mvrNameOrAddress: '@test/testpkg', + importExtension: '.js', + }); + + const parse = (type: string) => parseConfigArguments({ global: { bad: { type } } }, registry); - expect(() => parseConfigArguments({ bad: { type: 'Pool' } }, registry)).toThrowError( - /Expected a fully-qualified Move type/, + expect(() => parse('Pool')).toThrowError(/Expected a fully-qualified Move type/); + expect(() => parse('u64')).toThrowError(/must be a Move datatype/); + expect(() => parse('testpkg::pools::Pool<0x2::sui::SUI>>')).toThrowError(/unbalanced '>'/); + expect(() => parse('testpkg::pools::Pool<0x2::sui::SUI')).toThrowError(/unbalanced ' parse('testpkg::pools::Coin<>')).toThrowError(/empty type argument/); + expect(() => parse('testpkg::pools::Pool <0x2::sui::SUI>')).toThrowError( + /is not a valid module::type pair/, ); - expect(() => parseConfigArguments({ bad: { type: 'u64' } }, registry)).toThrowError( - /must be a Move datatype/, + expect(() => parse('testpkg::pools::Pool<2::sui::SUI>')).toThrowError(/Invalid address "2"/); + expect(() => parse('@test/testpkg::pools::Pool')).toThrowError( + /MVR names cannot be matched against package summaries/, ); }); + it('rejects partially instantiated matchers with a dedicated error', async () => { + const registry = new ModuleRegistry(ADDRESS_MAPPINGS); + new MoveModuleBuilder({ + summary: poolsSummary() as any, + registry, + mvrNameOrAddress: '@test/testpkg', + importExtension: '.js', + }); + + expect(() => + parseConfigArguments({ global: { pool: { type: 'testpkg::pools::Pool' } } }, registry), + ).toThrowError(/partially instantiated matchers are not supported/); + + // A nested uninstantiated generic is also a partial instantiation. + expect(() => + parseConfigArguments( + { global: { pool: { type: 'testpkg::pools::Pool' } } }, + registry, + ), + ).toThrowError(/Partially instantiated matchers are not supported/); + }); + it('rejects instantiated matchers with the wrong arity', async () => { const registry = new ModuleRegistry(ADDRESS_MAPPINGS); new MoveModuleBuilder({ @@ -228,7 +354,7 @@ describe('parseConfigArguments', () => { expect(() => parseConfigArguments( - { pool: { type: 'testpkg::pools::Pool<0x2::sui::SUI, u64>' } }, + { global: { pool: { type: 'testpkg::pools::Pool<0x2::sui::SUI, u64>' } } }, registry, ), ).toThrowError(/expects 1 type argument\(s\), got 2/); @@ -236,7 +362,7 @@ describe('parseConfigArguments', () => { }); describe('config-driven function codegen', () => { - it('non-generic matcher: matched parameter becomes optional with a required config slice', async () => { + it('non-generic matcher: matched parameter becomes optional with an optional config slice', async () => { const { registry } = await createBuilders({ registryObj: { type: 'testpkg::registry::Registry' }, }); @@ -261,7 +387,7 @@ describe('config-driven function codegen', () => { name: RawTransactionArgument, tags: RawTransactionArgument> ]; - config: { + config?: { registryObj: ConfigValue; }; }" @@ -281,13 +407,13 @@ describe('config-driven function codegen', () => { package: packageAddress, module: 'registry', function: 'register', - arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "registry", resolve: () => resolveConfigArg(options.config.registryObj, { typeArguments: [] }, "registryObj") }]), argumentsTypes, parameterNames), + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "registry", resolve: () => resolveConfigArgument(options.config?.registryObj, { typeArguments: [], packageAddress, moduleName: 'registry', functionName: 'register', parameterName: "registry" }, "registryObj") }]), argumentsTypes, parameterNames), }); }" `); }); - it('makes arguments optional when every parameter is config-matched', async () => { + it('makes arguments optional and the tuple suffix optional when every parameter is config-matched', async () => { const { registry } = await createBuilders({ registryObj: { type: 'testpkg::registry::Registry' }, }); @@ -299,9 +425,9 @@ describe('config-driven function codegen', () => { "export interface LookupOptions { package?: string; arguments?: LookupArguments | [ - registry: RawTransactionArgument | undefined + registry?: RawTransactionArgument ]; - config: { + config?: { registryObj: ConfigValue; }; }" @@ -319,7 +445,7 @@ describe('config-driven function codegen', () => { package: packageAddress, module: 'registry', function: 'lookup', - arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "registry", resolve: () => resolveConfigArg(options.config.registryObj, { typeArguments: [] }, "registryObj") }]), argumentsTypes, parameterNames), + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "registry", resolve: () => resolveConfigArgument(options.config?.registryObj, { typeArguments: [], packageAddress, moduleName: 'registry', functionName: 'lookup', parameterName: "registry" }, "registryObj") }]), argumentsTypes, parameterNames), }); }" `); @@ -337,9 +463,9 @@ describe('config-driven function codegen', () => { "export interface ContainerSizeOptions { package?: string; arguments?: ContainerSizeArguments | [ - container: RawTransactionArgument | undefined + container?: RawTransactionArgument ]; - config: { + config?: { container: (ctx: ConfigResolverContext) => string | TransactionObjectArgument; }; typeArguments: [ @@ -360,7 +486,7 @@ describe('config-driven function codegen', () => { package: packageAddress, module: 'registry', function: 'container_size', - arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "container", resolve: () => resolveConfigArg(options.config.container, { typeArguments: [\`\${options.typeArguments[0]}\`] }, "container") }]), argumentsTypes, parameterNames), + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "container", resolve: () => resolveConfigArgument(options.config?.container, { typeArguments: [\`\${options.typeArguments[0]}\`], packageAddress, moduleName: 'registry', functionName: 'container_size', parameterName: "container" }, "container") }]), argumentsTypes, parameterNames), typeArguments: options.typeArguments }); }" @@ -385,7 +511,7 @@ describe('config-driven function codegen', () => { pool: RawTransactionArgument | undefined, amount: RawTransactionArgument ]; - config: { + config?: { suiPool: ConfigValue; }; }" @@ -404,7 +530,7 @@ describe('config-driven function codegen', () => { package: packageAddress, module: 'pools', function: 'use_concrete', - arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "pool", resolve: () => resolveConfigArg(options.config.suiPool, { typeArguments: ['0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI'] }, "suiPool") }]), argumentsTypes, parameterNames), + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "pool", resolve: () => resolveConfigArgument(options.config?.suiPool, { typeArguments: ['0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI'], packageAddress, moduleName: 'pools', functionName: 'use_concrete', parameterName: "pool" }, "suiPool") }]), argumentsTypes, parameterNames), }); }" `); @@ -419,7 +545,7 @@ describe('config-driven function codegen', () => { pool: RawTransactionArgument | undefined, amount: RawTransactionArgument ]; - config: { + config?: { pool: (ctx: ConfigResolverContext) => string | TransactionObjectArgument; }; typeArguments: [ @@ -441,14 +567,38 @@ describe('config-driven function codegen', () => { package: packageAddress, module: 'pools', function: 'use_generic', - arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "pool", resolve: () => resolveConfigArg(options.config.pool, { typeArguments: [\`\${options.typeArguments[0]}\`] }, "pool") }]), argumentsTypes, parameterNames), + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "pool", resolve: () => resolveConfigArgument(options.config?.pool, { typeArguments: [\`\${options.typeArguments[0]}\`], packageAddress, moduleName: 'pools', functionName: 'use_generic', parameterName: "pool" }, "pool") }]), argumentsTypes, parameterNames), typeArguments: options.typeArguments }); }" `); }); - it('errors when a bare matcher hits two parameters in one signature', async () => { + it('resolver context tags for own-package types use the package name, not the placeholder address', async () => { + const builder = createPoolsBuilder({ + pool: { type: 'testpkg::pools::Pool' }, + }); + builder.includeFunctions(['use_own_coin']); + const output = await render(builder); + + const fnBody = output.match(/export function useOwnCoin[\s\S]*?^}/m); + expect(fnBody?.[0]).toContain("typeArguments: ['@test/testpkg::pools::Coin']"); + }); + + it('resolver context tags use origin addresses for upgraded packages', async () => { + const ORIGIN_V1 = '0x000000000000000000000000000000000000000000000000000000000000aaaa'; + const builder = createPoolsBuilder( + { pool: { type: 'testpkg::pools::Pool' } }, + { typeOrigins: { Coin: ORIGIN_V1 } }, + ); + builder.includeFunctions(['use_own_coin']); + const output = await render(builder); + + const fnBody = output.match(/export function useOwnCoin[\s\S]*?^}/m); + expect(fnBody?.[0]).toContain(`typeArguments: ['${ORIGIN_V1}::pools::Coin']`); + }); + + it('errors when a bare matcher hits two named parameters in one signature', async () => { const builder = createPoolsBuilder({ pool: { type: 'testpkg::pools::Pool' }, }); @@ -459,10 +609,75 @@ describe('config-driven function codegen', () => { ); }); + it('warns and skips config mapping when a bare matcher hits two nameless parameters', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + try { + const builder = createPoolsBuilder( + { pool: { type: 'testpkg::pools::Pool' } }, + { parameterNames: false }, + ); + builder.includeFunctions(['swap']); + const output = await render(builder); + + expect(warn).toHaveBeenCalledWith( + expect.stringContaining('configArguments.pool matches multiple parameters'), + ); + // swap is still generated, without config mapping. + const optionsInterface = output.match(/export interface SwapOptions[\s\S]*?^}/m); + expect(optionsInterface?.[0]).not.toContain('config'); + } finally { + warn.mockRestore(); + } + }); + + it('generates tuple-only bindings for nameless summaries with a matched parameter', async () => { + const builder = createPoolsBuilder( + { pool: { type: 'testpkg::pools::Pool' } }, + { parameterNames: false }, + ); + builder.includeFunctions(['use_generic']); + const output = await render(builder); + + const optionsInterface = output.match(/export interface UseGenericOptions[\s\S]*?^}/m); + expect(optionsInterface?.[0]).toMatchInlineSnapshot(` + "export interface UseGenericOptions { + package?: string; + arguments: [ + RawTransactionArgument | undefined, + RawTransactionArgument + ]; + config?: { + pool: (ctx: ConfigResolverContext) => string | TransactionObjectArgument; + }; + typeArguments: [ + string + ]; + }" + `); + + const fnBody = output.match(/export function useGeneric[\s\S]*?^}/m); + expect(fnBody?.[0]).toMatchInlineSnapshot(` + "export function useGeneric(options: UseGenericOptions) { + const packageAddress = options.package ?? '@test/testpkg'; + const argumentsTypes = [ + null, + 'u64' + ] satisfies (string | null)[]; + return (tx: Transaction) => tx.moveCall({ + package: packageAddress, + module: 'pools', + function: 'use_generic', + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, resolve: () => resolveConfigArgument(options.config?.pool, { typeArguments: [\`\${options.typeArguments[0]}\`], packageAddress, moduleName: 'pools', functionName: 'use_generic' }, "pool") }]), argumentsTypes), + typeArguments: options.typeArguments + }); + }" + `); + }); + it('name refinement disambiguates two parameters of the same type', async () => { const builder = createPoolsBuilder({ - basePool: { type: 'testpkg::pools::Pool', name: 'base_pool' }, - quotePool: { type: 'testpkg::pools::Pool', name: 'quote_pool' }, + basePool: { type: 'testpkg::pools::Pool', parameterName: 'base_pool' }, + quotePool: { type: 'testpkg::pools::Pool', parameterName: 'quote_pool' }, }); builder.includeFunctions(['swap']); const output = await render(builder); @@ -472,10 +687,10 @@ describe('config-driven function codegen', () => { "export interface SwapOptions { package?: string; arguments?: SwapArguments | [ - basePool: RawTransactionArgument | undefined, - quotePool: RawTransactionArgument | undefined + basePool?: RawTransactionArgument, + quotePool?: RawTransactionArgument ]; - config: { + config?: { basePool: (ctx: ConfigResolverContext) => string | TransactionObjectArgument; quotePool: (ctx: ConfigResolverContext) => string | TransactionObjectArgument; }; @@ -499,7 +714,7 @@ describe('config-driven function codegen', () => { package: packageAddress, module: 'pools', function: 'swap', - arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "basePool", resolve: () => resolveConfigArg(options.config.basePool, { typeArguments: [\`\${options.typeArguments[0]}\`] }, "basePool") }, { index: 1, name: "quotePool", resolve: () => resolveConfigArg(options.config.quotePool, { typeArguments: [\`\${options.typeArguments[1]}\`] }, "quotePool") }]), argumentsTypes, parameterNames), + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "basePool", resolve: () => resolveConfigArgument(options.config?.basePool, { typeArguments: [\`\${options.typeArguments[0]}\`], packageAddress, moduleName: 'pools', functionName: 'swap', parameterName: "base_pool" }, "basePool") }, { index: 1, name: "quotePool", resolve: () => resolveConfigArgument(options.config?.quotePool, { typeArguments: [\`\${options.typeArguments[1]}\`], packageAddress, moduleName: 'pools', functionName: 'swap', parameterName: "quote_pool" }, "quotePool") }]), argumentsTypes, parameterNames), typeArguments: options.typeArguments }); }" @@ -509,8 +724,8 @@ describe('config-driven function codegen', () => { it('name-refined matchers win over a bare matcher for the same type', async () => { const builder = createPoolsBuilder({ pool: { type: 'testpkg::pools::Pool' }, - basePool: { type: 'testpkg::pools::Pool', name: 'base_pool' }, - quotePool: { type: 'testpkg::pools::Pool', name: 'quote_pool' }, + basePool: { type: 'testpkg::pools::Pool', parameterName: 'base_pool' }, + quotePool: { type: 'testpkg::pools::Pool', parameterName: 'quote_pool' }, }); builder.includeFunctions(['swap', 'use_generic']); const output = await render(builder); @@ -537,18 +752,49 @@ describe('config-driven function codegen', () => { ); }); - it('errors when a name matcher targets a summary without parameter names', async () => { + it('errors when a name matcher would apply to a nameless parameter and nothing else matches', async () => { const builder = createPoolsBuilder( - { basePool: { type: 'testpkg::pools::Pool', name: 'base_pool' } }, + { basePool: { type: 'testpkg::pools::Pool', parameterName: 'base_pool' } }, { parameterNames: false }, ); builder.includeFunctions(['swap']); await expect(render(builder)).rejects.toThrowError( - /parameters of testpkg::pools::swap have no names/, + /parameters have no names \(bytecode summaries do not include parameter names\)/, ); }); + it('does not error for a name matcher whose instantiation cannot match the nameless parameter', async () => { + // The instantiated+named matcher targets Pool; use_own_coin's parameter is + // Pool, so the matcher is filtered by instantiation before the nameless check. + const builder = createPoolsBuilder( + { + suiPool: { type: 'testpkg::pools::Pool<0x2::sui::SUI>', parameterName: 'sui_pool' }, + }, + { parameterNames: false }, + ); + builder.includeFunctions(['use_own_coin']); + const output = await render(builder); + + const optionsInterface = output.match(/export interface UseOwnCoinOptions[\s\S]*?^}/m); + expect(optionsInterface?.[0]).not.toContain('config'); + }); + + it('falls back to a bare matcher instead of erroring when a name matcher hits a nameless parameter', async () => { + const builder = createPoolsBuilder( + { + pool: { type: 'testpkg::pools::Pool' }, + basePool: { type: 'testpkg::pools::Pool', parameterName: 'base_pool' }, + }, + { parameterNames: false }, + ); + builder.includeFunctions(['use_generic']); + const output = await render(builder); + + const optionsInterface = output.match(/export interface UseGenericOptions[\s\S]*?^}/m); + expect(optionsInterface?.[0]).toContain('pool:'); + }); + it('package entries are added to the package-address precedence chain', async () => { const { registry } = await createBuilders( { @@ -562,7 +808,7 @@ describe('config-driven function codegen', () => { const fnBody = output.match(/export function lookup[\s\S]*?^}/m); expect(fnBody?.[0]).toContain( - "const packageAddress = options.package ?? options.config.testpkgAddress ?? '@test/testpkg';", + "const packageAddress = options.package ?? options.config?.testpkgAddress ?? '@test/testpkg';", ); }); @@ -606,8 +852,10 @@ describe('config-driven function codegen', () => { ); const { entries } = parseConfigArguments( { - registryObj: { type: 'testpkg::registry::Registry' }, - testpkgAddress: { package: '@test/testpkg' }, + global: { + registryObj: { type: 'testpkg::registry::Registry' }, + testpkgAddress: { package: '@test/testpkg' }, + }, }, registry, ); @@ -620,7 +868,7 @@ describe('config-driven function codegen', () => { // BCS type names keep the origin address; the call package comes from the config chain. expect(output).toContain(`name: \`${ORIGIN_V1}::registry::Registry\``); expect(output).toContain( - "const packageAddress = options.package ?? options.config.testpkgAddress ?? '@test/testpkg';", + "const packageAddress = options.package ?? options.config?.testpkgAddress ?? '@test/testpkg';", ); }); }); @@ -636,31 +884,38 @@ describe('generateFromPackageSummary with configArguments', () => { async function generate() { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - await generateFromPackageSummary({ + const result = await generateFromPackageSummary({ package: { package: '@test/testpkg', path: FIXTURE_PATH, - configArguments: { - registryObj: { type: 'testpkg::registry::Registry' }, - container: { type: 'testpkg::registry::Container' }, - testpkgAddress: { package: '@test/testpkg' }, - missing: { type: 'testpkg::registry::DoesNotExist' }, - }, }, prune: true, outputDir: GENERATED_DIR, + configArguments: { + registryObj: { type: 'testpkg::registry::Registry' }, + container: { type: 'testpkg::registry::Container' }, + testpkgAddress: { package: '@test/testpkg' }, + missing: { type: 'testpkg::registry::DoesNotExist' }, + unusedEntry: { type: 'testpkg::registry::Entry' }, + }, }); - return warn; + return { warn, result }; } - it('emits config-args.ts and config-driven bindings, warning on unresolved keys', async () => { - const warn = await generate(); + it('emits config-arguments.ts and config-driven bindings, reporting unresolved and unused keys', async () => { + const { warn, result } = await generate(); expect(warn).toHaveBeenCalledWith( 'configArguments keys not resolvable in @test/testpkg (skipped): missing', ); + expect(result.unresolvedConfigKeys).toEqual(['missing']); + // Entry is a plain store struct that no generated function takes as a parameter. + expect(result.unusedConfigKeys).toEqual(['unusedEntry']); - const configArgs = await readFile(join(GENERATED_DIR, 'testpkg', 'config-args.ts'), 'utf-8'); + const configArgs = await readFile( + join(GENERATED_DIR, 'testpkg', 'config-arguments.ts'), + 'utf-8', + ); expect(configArgs).toMatchInlineSnapshot(` "/************************************************************** * THIS FILE IS GENERATED AND SHOULD NOT BE MANUALLY MODIFIED * @@ -671,12 +926,50 @@ describe('generateFromPackageSummary with configArguments', () => { registryObj: ConfigValue; container: (ctx: ConfigResolverContext) => string | TransactionObjectArgument; testpkgAddress?: string; + unusedEntry: ConfigValue; }" `); const registryModule = await readFile(join(GENERATED_DIR, 'testpkg', 'registry.ts'), 'utf-8'); expect(registryModule).toContain('applyConfigArguments'); - expect(registryModule).toContain('resolveConfigArg'); + expect(registryModule).toContain('resolveConfigArgument'); + }); + + it('errors for unresolved keys in a package-scoped block', async () => { + await expect( + generateFromPackageSummary({ + package: { + package: '@test/testpkg', + path: FIXTURE_PATH, + configArguments: { + missing: { type: 'testpkg::registry::DoesNotExist' }, + }, + }, + prune: true, + outputDir: GENERATED_DIR, + }), + ).rejects.toThrowError(/was not found in this package's summaries/); + }); + + it('warns for unused keys in a package-scoped block', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + await generateFromPackageSummary({ + package: { + package: '@test/testpkg', + path: FIXTURE_PATH, + configArguments: { + unusedEntry: { type: 'testpkg::registry::Entry' }, + }, + }, + prune: true, + outputDir: GENERATED_DIR, + }); + + expect(warn).toHaveBeenCalledWith( + expect.stringContaining( + 'configArguments keys that matched no generated function parameters in @test/testpkg: unusedEntry', + ), + ); }); it('generated output typechecks under strict settings', { timeout: 60_000 }, async () => { @@ -744,7 +1037,7 @@ describe('generateFromPackageSummary with configArguments', () => { expect(json.inputs).toEqual([{ UnresolvedObject: { objectId: REGISTRY_ID } }]); expect(json.commands[0].MoveCall.package).toBe(PACKAGE_ID); - // An explicitly passed argument overrides config resolution. + // An explicitly passed argument overrides config resolution (object form). const OVERRIDE_ID = '0x0000000000000000000000000000000000000000000000000000000000000456'; const tx2 = new Transaction(); tx2.add( @@ -760,9 +1053,46 @@ describe('generateFromPackageSummary with configArguments', () => { ); const json2 = JSON.parse(await tx2.toJSON()); expect(json2.inputs).toEqual([{ UnresolvedObject: { objectId: OVERRIDE_ID } }]); + + // Tuple form: an empty tuple resolves from config, an explicit tuple element overrides. + const tx3 = new Transaction(); + tx3.add( + mod.lookup({ + arguments: [], + config: { registryObj: REGISTRY_ID, testpkgAddress: PACKAGE_ID }, + }), + ); + const json3 = JSON.parse(await tx3.toJSON()); + expect(json3.inputs).toEqual([{ UnresolvedObject: { objectId: REGISTRY_ID } }]); + + const tx4 = new Transaction(); + tx4.add( + mod.lookup({ + arguments: [OVERRIDE_ID], + config: { + registryObj: () => { + throw new Error('should not be called'); + }, + testpkgAddress: PACKAGE_ID, + }, + }), + ); + const json4 = JSON.parse(await tx4.toJSON()); + expect(json4.inputs).toEqual([{ UnresolvedObject: { objectId: OVERRIDE_ID } }]); }); - it('resolvers receive the matched parameter instantiation at runtime', async () => { + it('omitting both the argument and the config value fails with a descriptive error', async () => { + await generate(); + + const mod = await import(join(GENERATED_DIR, 'testpkg', 'registry.js')); + const tx = new Transaction(); + + expect(() => tx.add(mod.lookup({}))).toThrowError( + 'Missing config value for "registryObj": pass it explicitly in arguments, or include it in the config object', + ); + }); + + it('resolvers receive the normalized matched parameter instantiation and call-site metadata', async () => { await generate(); const mod = await import(join(GENERATED_DIR, 'testpkg', 'registry.js')); @@ -785,7 +1115,17 @@ describe('generateFromPackageSummary with configArguments', () => { ); const json = JSON.parse(await tx.toJSON()); - expect(contexts).toEqual([{ typeArguments: ['0x2::sui::SUI'] }]); + expect(contexts).toEqual([ + { + typeArguments: [ + '0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI', + ], + packageAddress: PACKAGE_ID, + moduleName: 'registry', + functionName: 'container_size', + parameterName: 'container', + }, + ]); expect(json.inputs).toEqual([{ UnresolvedObject: { objectId: CONTAINER_ID } }]); }); }); diff --git a/packages/codegen/tests/utils.test.ts b/packages/codegen/tests/utils.test.ts index f3dacdcb1..9ad46aafa 100644 --- a/packages/codegen/tests/utils.test.ts +++ b/packages/codegen/tests/utils.test.ts @@ -13,7 +13,20 @@ let normalizeMoveArguments: ( argTypes: readonly (string | null)[], parameterNames?: string[], ) => any; -let resolveConfigArg: (value: unknown, ctx: { typeArguments: string[] }, name: string) => unknown; +interface TestResolverContext { + typeArguments: string[]; + packageAddress: string; + moduleName: string; + functionName: string; + parameterName?: string; +} +const TEST_CTX: TestResolverContext = { + typeArguments: [], + packageAddress: '0x0', + moduleName: 'test', + functionName: 'test', +}; +let resolveConfigArgument: (value: unknown, ctx: TestResolverContext, name: string) => unknown; let applyConfigArguments: ( args: unknown[] | object, defaults: readonly { index: number; name?: string; resolve: () => unknown }[], @@ -25,7 +38,7 @@ beforeAll(async () => { const modPath = join(GENERATED_DIR, 'utils', 'index.js'); const mod = await import(modPath); normalizeMoveArguments = mod.normalizeMoveArguments; - resolveConfigArg = mod.resolveConfigArg; + resolveConfigArgument = mod.resolveConfigArgument; applyConfigArguments = mod.applyConfigArguments; }); @@ -431,27 +444,54 @@ describe('well-known AccumulatorRoot injection', () => { }); }); -describe('resolveConfigArg', () => { +describe('resolveConfigArgument', () => { it('returns plain values as-is', () => { - expect(resolveConfigArg('0x123', { typeArguments: [] }, 'pool')).toBe('0x123'); + expect(resolveConfigArgument('0x123', TEST_CTX, 'pool')).toBe('0x123'); }); - it('invokes resolver functions with the context', () => { + it('invokes resolver functions with the context, normalizing hex struct tags', () => { const contexts: unknown[] = []; const value = (ctx: unknown) => { contexts.push(ctx); return '0x456'; }; - expect(resolveConfigArg(value, { typeArguments: ['0x2::sui::SUI'] }, 'pool')).toBe('0x456'); - expect(contexts).toEqual([{ typeArguments: ['0x2::sui::SUI'] }]); + expect( + resolveConfigArgument( + value, + { ...TEST_CTX, typeArguments: ['0x2::sui::SUI', '@mvr/name::a::B', 'u64'] }, + 'pool', + ), + ).toBe('0x456'); + expect(contexts).toEqual([ + { + ...TEST_CTX, + typeArguments: [ + '0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI', + '@mvr/name::a::B', + 'u64', + ], + }, + ]); }); it('throws a descriptive error for missing values', () => { - expect(() => resolveConfigArg(undefined, { typeArguments: [] }, 'pool')).toThrowError( + expect(() => resolveConfigArgument(undefined, TEST_CTX, 'pool')).toThrowError( 'Missing config value for "pool": pass it explicitly in arguments, or include it in the config object', ); }); + + it('throws a descriptive error when a resolver returns undefined', () => { + expect(() => + resolveConfigArgument( + () => undefined, + { ...TEST_CTX, typeArguments: ['0x2::sui::SUI'] }, + 'pool', + ), + ).toThrowError( + 'Config resolver for "pool" returned undefined (test::test, typeArguments: [0x2::sui::SUI])', + ); + }); }); describe('applyConfigArguments', () => { @@ -503,4 +543,59 @@ describe('applyConfigArguments', () => { expect(result).toEqual([42n, '0x123']); }); + + it('rejects array arguments with holes at non-matched positions', () => { + // Filling a trailing matched position must not mask an omitted required middle argument. + expect(() => + applyConfigArguments(['0xaa'], [{ index: 2, name: 'pool', resolve: () => '0x123' }]), + ).toThrowError('Missing argument at position 1'); + }); + + it('does not treat inherited object properties as explicitly passed arguments', () => { + // `constructor` is a valid Move identifier; the inherited Object.prototype.constructor + // must not be mistaken for an explicit argument. + const result = applyConfigArguments({ amount: 1n }, [ + { index: 0, name: 'constructor', resolve: () => '0x123' }, + ]); + + expect((result as Record)['constructor']).toBe('0x123'); + }); +}); + +describe('well-known and config-matched parameters combined', () => { + it('aligns config-filled positions with well-known injection at runtime', async () => { + // Mimics a generated body for `fn(registry: &Registry, clock: &Clock, amount: u64)`: + // clock is elided from arguments, so the config-matched registry is index 0 and amount is + // index 1 while argumentsTypes still includes the clock tag between them. + const tx = new Transaction(); + tx.moveCall({ + target: '0x0::test::test', + arguments: normalizeMoveArguments( + applyConfigArguments({ amount: 42 }, [ + { index: 0, name: 'registry', resolve: () => '0x123' }, + ]), + [null, '0x2::clock::Clock', 'u32'], + ['registry', 'amount'], + ), + }); + + const json = JSON.parse(await tx.toJSON()); + expect(json.inputs).toEqual([ + { + UnresolvedObject: { + objectId: '0x0000000000000000000000000000000000000000000000000000000000000123', + }, + }, + { + Object: { + SharedObject: { + objectId: '0x0000000000000000000000000000000000000000000000000000000000000006', + initialSharedVersion: 1, + mutable: false, + }, + }, + }, + { Pure: { bytes: 'KgAAAA==' } }, + ]); + }); }); diff --git a/packages/docs/content/codegen/index.mdx b/packages/docs/content/codegen/index.mdx index 5389d9fe7..25373a8da 100644 --- a/packages/docs/content/codegen/index.mdx +++ b/packages/docs/content/codegen/index.mdx @@ -297,7 +297,7 @@ declare which Move types (or package addresses) come from a config object, and t functions accept that config object directly instead of requiring those arguments on every call. `configArguments` maps author-chosen keys to matchers. It can be declared globally (shared by all -packages) or per package entry (merged over the global block): +packages) or per package entry (merged over the global block, per key): ```typescript const config: SuiCodegenConfig = { @@ -325,13 +325,23 @@ const config: SuiCodegenConfig = { ``` Matcher addresses are resolved through the package's address mapping, so named addresses (for -example, `myapp::registry::Registry`) also work. +example, `myapp::registry::Registry`) also work. Partially instantiated matchers (for example, +`Pool` or a nested uninstantiated generic) are not supported — use an uninstantiated matcher with +a resolver function instead. + +Misconfigured matchers are surfaced at generation time: malformed types, wrong arity, and +package-scoped matchers whose type isn't in the package's summaries are hard errors. Global-block +matchers that don't resolve in a package are skipped with a warning (a shared global block may span +multiple packages in one run), and the CLI errors if a global key resolves in no package at all. +Keys that resolve but never match any generated parameter produce a warning. ### Generated output -For each function with matched parameters, the generated options gain a `config` property typed as -the minimal slice of keys that function actually uses. Matched parameters become optional in -`arguments` — passing one explicitly overrides config resolution: +For each function with matched parameters, the generated options gain an optional `config` property +typed as the minimal slice of keys that function actually uses. Matched parameters become optional +in `arguments` — passing one explicitly overrides config resolution, and resolvers are only invoked +for arguments the caller did not pass. If a matched argument is omitted and no config value is +available, the call fails with a descriptive runtime error: ```typescript export interface BorrowOptions { @@ -342,7 +352,7 @@ export interface BorrowOptions { pool: RawTransactionArgument | undefined, amount: RawTransactionArgument, ]; - config: { + config?: { pool: (ctx: ConfigResolverContext) => string | TransactionObjectArgument; corePackageId?: string; }; @@ -350,17 +360,34 @@ export interface BorrowOptions { } ``` -Config values can be a plain object ID, a transaction argument, or a resolver function. Resolver -functions receive the matched parameter's own instantiated type arguments (not the whole function's -type argument tuple), which makes them reusable across functions that use the type in different -positions: +In the tuple form of `arguments`, a matched position followed by a required one accepts an explicit +`undefined`; a matched suffix (the common case — registry-style objects last) becomes genuinely +optional trailing elements. + +Config values can be a plain object ID, a transaction argument, or a resolver function. Any function +value is treated as a resolver — to provide a transaction-callback object argument dynamically, +return it from a resolver: `(ctx) => (tx) => ...`. Resolvers receive the matched parameter's own +instantiated type arguments (not the whole function's type argument tuple), plus static call-site +metadata: + +```typescript +export interface ConfigResolverContext { + typeArguments: string[]; // hex-addressed struct tags are normalized to their long form + packageAddress: string; + moduleName: string; + functionName: string; + parameterName?: string; // Move parameter name, when the summary includes names +} +``` + +This makes resolvers reusable across functions that use the type in different positions: ```typescript const myConfig = { registry: '0x123...', pool: (ctx: ConfigResolverContext) => poolsByCoinType[ctx.typeArguments[0]], corePackageId: '0xabc...', -} satisfies MyappCoreConfig; +} satisfies CoreConfig; tx.add( borrow({ @@ -377,9 +404,15 @@ can't be correct across instantiations. A parameter typed with the function's ow matcher also exists; only parameters concretely instantiated in the Move signature bind to instantiated matchers. -Each package output also includes a `config-args.ts` file with an interface covering every declared -key (named after the package, for example `MyappCoreConfig`), for use with `satisfies` when defining -your config object. +Each package output also includes a `config-arguments.ts` file with an interface covering the +package's resolvable keys and its own package-address key, for use with `satisfies` when defining +your config object. The interface is named after the package's `packageName` (for example, +`packageName: 'core'` produces `CoreConfig`). When a global block spans multiple packages, define +one shared config object and check it against the intersection of the per-package interfaces: + +```typescript +const myConfig = { ... } satisfies CoreConfig & MarginConfig; +``` ### Name refinement @@ -389,13 +422,17 @@ Refine the matchers with the Move parameter names: ```typescript configArguments: { - basePool: { type: '0x...::pool::Pool', name: 'base_pool' }, - quotePool: { type: '0x...::pool::Pool', name: 'quote_pool' }, + basePool: { type: '0x...::pool::Pool', parameterName: 'base_pool' }, + quotePool: { type: '0x...::pool::Pool', parameterName: 'quote_pool' }, }, ``` -Parameter names are only available in summaries generated from local packages — using a `name` -matcher against an onchain package's bytecode summary is a generation-time error. +Parameter names are only available in summaries generated from local packages. A `parameterName` +matcher never matches a parameter without a name; if such a matcher would otherwise apply to a +nameless parameter (same type and instantiation) and nothing else matches it, codegen fails with a +clear error. When a bare matcher hits two parameters of a nameless (onchain bytecode) signature — +where refinement is impossible — config mapping is skipped for that function with a warning instead +of failing the run. ### Package address precedence @@ -406,7 +443,11 @@ For package entries, the address used for a generated call is resolved in this o 3. The generated default (the package's MVR name or address) On mainnet with MVR names the config entry can be omitted entirely; on networks where the MVR name -doesn't resolve, supply the deployed package ID through the config object. +doesn't resolve, supply the deployed package ID through the config object. Two caveats: package +entries only apply to the main package's generated modules (dependency modules under `deps/` never +consult them), and they only apply where a generated default address exists — for packages generated +without an MVR name or address, `package` stays required and always takes precedence. The CLI +validates that every package entry references a package that is part of the codegen run. ## Phantom types From 0cbd154751bdbd080f8bbe86450a782a5ea53569 Mon Sep 17 00:00:00 2001 From: Michael Hayes Date: Wed, 22 Jul 2026 14:48:53 -0700 Subject: [PATCH 3/7] docs(codegen): fix style-guide violations (em dashes, Mainnet capitalization) Co-Authored-By: Claude Fable 5 --- packages/docs/content/codegen/index.mdx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/docs/content/codegen/index.mdx b/packages/docs/content/codegen/index.mdx index 25373a8da..3775dcc9b 100644 --- a/packages/docs/content/codegen/index.mdx +++ b/packages/docs/content/codegen/index.mdx @@ -326,7 +326,7 @@ const config: SuiCodegenConfig = { Matcher addresses are resolved through the package's address mapping, so named addresses (for example, `myapp::registry::Registry`) also work. Partially instantiated matchers (for example, -`Pool` or a nested uninstantiated generic) are not supported — use an uninstantiated matcher with +`Pool` or a nested uninstantiated generic) are not supported. Use an uninstantiated matcher with a resolver function instead. Misconfigured matchers are surfaced at generation time: malformed types, wrong arity, and @@ -339,7 +339,7 @@ Keys that resolve but never match any generated parameter produce a warning. For each function with matched parameters, the generated options gain an optional `config` property typed as the minimal slice of keys that function actually uses. Matched parameters become optional -in `arguments` — passing one explicitly overrides config resolution, and resolvers are only invoked +in `arguments`. Passing one explicitly overrides config resolution, and resolvers are only invoked for arguments the caller did not pass. If a matched argument is omitted and no config value is available, the call fails with a descriptive runtime error: @@ -361,11 +361,11 @@ export interface BorrowOptions { ``` In the tuple form of `arguments`, a matched position followed by a required one accepts an explicit -`undefined`; a matched suffix (the common case — registry-style objects last) becomes genuinely +`undefined`; a matched suffix (the common case, with registry-style objects last) becomes genuinely optional trailing elements. Config values can be a plain object ID, a transaction argument, or a resolver function. Any function -value is treated as a resolver — to provide a transaction-callback object argument dynamically, +value is treated as a resolver. To provide a transaction-callback object argument dynamically, return it from a resolver: `(ctx) => (tx) => ...`. Resolvers receive the matched parameter's own instantiated type arguments (not the whole function's type argument tuple), plus static call-site metadata: @@ -398,8 +398,8 @@ tx.add( ); ``` -For generic types matched without type arguments, a resolver function is required — a static ID -can't be correct across instantiations. A parameter typed with the function's own type parameter +For generic types matched without type arguments, a resolver function is required, because a static +ID can't be correct across instantiations. A parameter typed with the function's own type parameter (for example, `Pool`) always binds to the uninstantiated matcher, even when a fully instantiated matcher also exists; only parameters concretely instantiated in the Move signature bind to instantiated matchers. @@ -430,8 +430,8 @@ configArguments: { Parameter names are only available in summaries generated from local packages. A `parameterName` matcher never matches a parameter without a name; if such a matcher would otherwise apply to a nameless parameter (same type and instantiation) and nothing else matches it, codegen fails with a -clear error. When a bare matcher hits two parameters of a nameless (onchain bytecode) signature — -where refinement is impossible — config mapping is skipped for that function with a warning instead +clear error. When a bare matcher hits two parameters of a nameless (onchain bytecode) signature, +where refinement is impossible, config mapping is skipped for that function with a warning instead of failing the run. ### Package address precedence @@ -442,10 +442,10 @@ For package entries, the address used for a generated call is resolved in this o 2. The config key declared by the package entry (for example, `config.corePackageId`) 3. The generated default (the package's MVR name or address) -On mainnet with MVR names the config entry can be omitted entirely; on networks where the MVR name +On Mainnet with MVR names the config entry can be omitted entirely; on networks where the MVR name doesn't resolve, supply the deployed package ID through the config object. Two caveats: package entries only apply to the main package's generated modules (dependency modules under `deps/` never -consult them), and they only apply where a generated default address exists — for packages generated +consult them), and they only apply where a generated default address exists. For packages generated without an MVR name or address, `package` stays required and always takes precedence. The CLI validates that every package entry references a package that is part of the codegen run. From 1f25d1c64d24ef5230a994f457d6c0c9b62a0f95 Mon Sep 17 00:00:00 2001 From: Michael Hayes Date: Wed, 22 Jul 2026 15:13:18 -0700 Subject: [PATCH 4/7] refactor(codegen): network-agnostic configArguments matchers Matchers no longer contain package addresses (codegen output must work across networks). Types are identified as module::Type, scoped to the declaring package's own block, qualified with a package identifier from the packages config (@myapp/core::pool::Pool) for cross-package references, or with the chain-stable framework addresses 0x1-0x3. The CLI resolves each run package's root address up front so matchers can reference any package in the run; matcher typos error in the run of the package they target, replacing the unresolved-key warn/aggregate machinery. Global-block matchers must be package-qualified. Also reformat the create-dapp template utils file that was failing prettier:check. Co-Authored-By: Claude Fable 5 --- .../codegen/src/cli/commands/generate/impl.ts | 51 +-- packages/codegen/src/config-arguments.ts | 161 ++++++--- packages/codegen/src/config.ts | 13 +- packages/codegen/src/index.ts | 105 ++++-- packages/codegen/src/module-registry.ts | 11 + .../codegen/tests/config-arguments.test.ts | 320 +++++++++++------- .../src/contracts/utils/index.ts | 5 +- packages/docs/content/codegen/index.mdx | 41 ++- 8 files changed, 451 insertions(+), 256 deletions(-) diff --git a/packages/codegen/src/cli/commands/generate/impl.ts b/packages/codegen/src/cli/commands/generate/impl.ts index b48fcfbd8..8be6be22c 100644 --- a/packages/codegen/src/cli/commands/generate/impl.ts +++ b/packages/codegen/src/cli/commands/generate/impl.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { LocalContext } from '../../context.js'; -import { generateFromPackageSummary } from '../../../index.js'; +import { generateFromPackageSummary, resolvePackageRootAddress } from '../../../index.js'; import { loadConfig, type GenerateBase, type PackageGenerate } from '../../../config.js'; import { isValidNamedPackage, isValidSuiObjectId } from '@mysten/sui/utils'; import { execSync } from 'node:child_process'; @@ -67,12 +67,6 @@ export default async function generate( } } - const globalTypeConfigKeys = Object.entries(config.configArguments ?? {}) - .filter(([, matcher]) => 'type' in matcher) - .map(([key]) => key); - const unresolvedConfigKeysByPackage: Set[] = []; - const unusedConfigKeysByPackage: Set[] = []; - const generateSummaries = flags.noSummaries === undefined ? config.generateSummaries : !flags.noSummaries; @@ -105,6 +99,8 @@ export default async function generate( } : undefined; + // First ensure summaries exist for every package, then resolve each package's root address so + // configArguments matchers can reference any package in the run by its identifier. for (const pkg of normalizedPackages) { // Detect on-chain packages: they have 'network' field and no 'path' const isOnChainPackage = @@ -132,6 +128,18 @@ export default async function generate( stdio: 'inherit', }); } + } + + const packageAddresses: Record = {}; + for (const pkg of normalizedPackages) { + if (!pkg.path) continue; + const address = await resolvePackageRootAddress(pkg.path); + if (address !== undefined) { + packageAddresses[pkg.package] = address; + } + } + + for (const pkg of normalizedPackages) { const importExtension = flags.importExtension === undefined ? config.importExtension @@ -165,7 +173,7 @@ export default async function generate( } : config.generate; - const result = await generateFromPackageSummary({ + await generateFromPackageSummary({ package: pkgWithOverrides, prune: flags.noPrune === undefined ? config.prune : !flags.noPrune, outputDir: flags.outputDir ?? config.output, @@ -174,32 +182,7 @@ export default async function generate( includePhantomTypeParameters: config.includePhantomTypeParameters, errorClass: config.errorClass, configArguments: config.configArguments, + packageAddresses, }); - - unresolvedConfigKeysByPackage.push(new Set(result.unresolvedConfigKeys)); - unusedConfigKeysByPackage.push(new Set(result.unusedConfigKeys)); - } - - if (unresolvedConfigKeysByPackage.length === 0) { - return; - } - - // Per-package unresolved global keys are only warnings (they may belong to another package in - // the run), but a global key that resolved nowhere — or matched nothing anywhere — is a - // misconfiguration for the run as a whole. - for (const key of globalTypeConfigKeys) { - if (unresolvedConfigKeysByPackage.every((keys) => keys.has(key))) { - throw new Error( - `configArguments.${key} did not resolve in any package in this codegen run — check the matcher type for typos`, - ); - } - const usedSomewhere = unresolvedConfigKeysByPackage.some( - (unresolved, i) => !unresolved.has(key) && !unusedConfigKeysByPackage[i].has(key), - ); - if (!usedSomewhere) { - console.warn( - `configArguments.${key} matched no generated function parameters in any package in this codegen run`, - ); - } } } diff --git a/packages/codegen/src/config-arguments.ts b/packages/codegen/src/config-arguments.ts index a9f37b82f..c9f1a151a 100644 --- a/packages/codegen/src/config-arguments.ts +++ b/packages/codegen/src/config-arguments.ts @@ -1,7 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { isValidNamedPackage, normalizeSuiAddress } from '@mysten/sui/utils'; +import { normalizeSuiAddress } from '@mysten/sui/utils'; import type { ConfigArguments } from './config.js'; import type { ModuleRegistry } from './module-registry.js'; import type { Parameter, Type } from './types/summary.js'; @@ -41,9 +41,37 @@ export interface PackageConfigArgument { export type ParsedConfigArgument = TypeConfigArgument | PackageConfigArgument; +export interface ConfigArgumentsContext { + /** Identifier (from the `packages` config) and resolved address of the package being generated. */ + package: { id: string; address: string }; + /** + * Resolved root addresses of the other packages in the codegen run, keyed by their `packages` + * identifier. Matchers can reference any of these packages' types. + */ + packageAddresses?: Record; +} + const PRIMITIVES = new Set(['bool', 'u8', 'u16', 'u32', 'u64', 'u128', 'u256', 'address']); const MOVE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/; const HEX_ADDRESS = /^0x[0-9a-fA-F]{1,64}$/; +/** The only packages with chain-stable addresses. Everything else must use a package identifier. */ +const FRAMEWORK_ADDRESS = /^0x0*[123]$/; + +function normalizeAddress(address: string) { + return HEX_ADDRESS.test(address) ? normalizeSuiAddress(address) : address; +} + +interface ParseContext { + /** + * Address a bare `module::Type` resolves against, or `null` when there is no ambient package + * (global block), which makes the qualifier required. + */ + scopeAddress: string | null; + /** Resolved addresses by package identifier from the `packages` config. */ + packageAddresses: Record; + root: string; + isTypeArgument?: boolean; +} function assertBalancedBrackets(tag: string) { let depth = 0; @@ -77,14 +105,10 @@ function splitTopLevelTypeArgs(inner: string): string[] { return parts; } -function parseTypeTag( - tag: string, - resolveAddress: (address: string) => string, - { isTypeArgument = false, root = tag }: { isTypeArgument?: boolean; root?: string } = {}, -): ParsedTypeTag { +function parseTypeTag(tag: string, ctx: ParseContext): ParsedTypeTag { const trimmed = tag.trim(); - if (!isTypeArgument) { + if (!ctx.isTypeArgument) { assertBalancedBrackets(trimmed); } @@ -94,17 +118,17 @@ function parseTypeTag( if (trimmed.startsWith('vector<') && trimmed.endsWith('>')) { return { - vector: parseTypeTag(trimmed.slice('vector<'.length, -1), resolveAddress, { + vector: parseTypeTag(trimmed.slice('vector<'.length, -1), { + ...ctx, isTypeArgument: true, - root, }), }; } // A bare identifier in type-argument position is a type-parameter placeholder like `Pool`. - if (isTypeArgument && MOVE_IDENTIFIER.test(trimmed)) { + if (ctx.isTypeArgument && MOVE_IDENTIFIER.test(trimmed)) { throw new Error( - `configArguments matcher "${root}" contains the type parameter "${trimmed}" — partially ` + + `configArguments matcher "${ctx.root}" contains the type parameter "${trimmed}" — partially ` + `instantiated matchers are not supported. Use an uninstantiated matcher (no type ` + `arguments) with a resolver function instead.`, ); @@ -114,9 +138,11 @@ function parseTypeTag( const base = lt === -1 ? trimmed : trimmed.slice(0, lt); const parts = base.split('::'); - if (parts.length !== 3 || parts.some((part) => part.length === 0)) { + if ((parts.length !== 2 && parts.length !== 3) || parts.some((part) => part.length === 0)) { throw new Error( - `Invalid type in configArguments matcher: "${tag}". Expected a fully-qualified Move type like "0x2::sui::SUI".`, + `Invalid type in configArguments matcher: "${tag}". Expected "module::Type", optionally ` + + `qualified with a package from the codegen config ("@pkg/name::module::Type") or a Sui ` + + `framework address ("0x2::module::Type").`, ); } @@ -124,7 +150,9 @@ function parseTypeTag( throw new Error(`Invalid type in configArguments matcher: "${tag}"`); } - const [addressPart, modulePart, namePart] = parts; + const packagePart = parts.length === 3 ? parts[0] : undefined; + const modulePart = parts[parts.length - 2]; + const namePart = parts[parts.length - 1]; if (!MOVE_IDENTIFIER.test(modulePart) || !MOVE_IDENTIFIER.test(namePart)) { throw new Error( @@ -132,16 +160,38 @@ function parseTypeTag( ); } - if (isValidNamedPackage(addressPart)) { + let address: string; + + if (packagePart === undefined) { + // Codegen output is network-agnostic: a bare `module::Type` refers to the package whose + // configArguments block declares it. + if (ctx.scopeAddress === null) { + throw new Error( + `configArguments matcher "${ctx.root}": "${modulePart}::${namePart}" must be qualified ` + + `with a package in the global configArguments block (e.g. ` + + `"@pkg/name::${modulePart}::${namePart}"). Bare module::Type matchers are only ` + + `supported in a package's own configArguments block.`, + ); + } + address = ctx.scopeAddress; + } else if (FRAMEWORK_ADDRESS.test(packagePart)) { + address = normalizeSuiAddress(packagePart); + } else if (HEX_ADDRESS.test(packagePart)) { throw new Error( - `Invalid address "${addressPart}" in configArguments matcher "${tag}": MVR names cannot be ` + - `matched against package summaries. Use the package's named address from ` + - `address_mapping.json or its hex address instead.`, + `Invalid package "${packagePart}" in configArguments matcher "${tag}": package addresses ` + + `are network-specific and cannot be used in matchers (only the framework addresses ` + + `0x1-0x3 are chain-stable). Reference the package by its identifier from the codegen ` + + `config instead (e.g. "@pkg/name::${modulePart}::${namePart}").`, ); - } - - if (!HEX_ADDRESS.test(addressPart) && !MOVE_IDENTIFIER.test(addressPart)) { - throw new Error(`Invalid address "${addressPart}" in configArguments matcher "${tag}"`); + } else { + const resolved = ctx.packageAddresses[packagePart]; + if (resolved === undefined) { + throw new Error( + `Unknown package "${packagePart}" in configArguments matcher "${tag}". Known packages in ` + + `this codegen run: ${Object.keys(ctx.packageAddresses).join(', ')}`, + ); + } + address = resolved; } const typeArguments = @@ -153,12 +203,12 @@ function parseTypeTag( `Invalid type in configArguments matcher: "${tag}" (empty type argument)`, ); } - return parseTypeTag(arg, resolveAddress, { isTypeArgument: true, root }); + return parseTypeTag(arg, { ...ctx, isTypeArgument: true }); }); return { datatype: { - address: resolveMatcherAddress(addressPart, resolveAddress), + address: normalizeAddress(address), module: modulePart, name: namePart, typeArguments, @@ -166,11 +216,6 @@ function parseTypeTag( }; } -function resolveMatcherAddress(address: string, resolveAddress: (address: string) => string) { - const resolved = resolveAddress(address); - return HEX_ADDRESS.test(resolved) ? normalizeSuiAddress(resolved) : resolved; -} - /** * Check that every datatype nested in an instantiated matcher is itself fully instantiated, * as far as the summaries can tell (unknown types are skipped). @@ -205,22 +250,28 @@ function assertFullyInstantiated( /** * Parse and validate `configArguments` blocks against the modules loaded in `registry`. - * Per-package entries are merged over global entries (per key). All addresses (in matchers and - * during matching) are resolved through the registry's address mapping and normalized, so - * matchers can use named addresses from `address_mapping.json`. + * Per-package entries are merged over global entries (per key). * - * Type matchers referencing types that don't exist in this package's summaries are a hard error - * when declared in a package-scoped block (they can only refer to this package's summaries), and - * are returned in `unresolvedKeys` when declared globally — a shared global block may span - * multiple packages in one codegen run, and entries for other packages can't match anything here. + * Matchers identify packages network-agnostically: by the package identifiers from the codegen + * config (resolved through `context.packageAddresses`), by the ambient package for bare + * `module::Type` matchers in a package-scoped block, or by the chain-stable framework addresses + * 0x1-0x3. If a matcher's package is part of this package's summaries, the matched type must + * exist there (typos fail generation); matchers referencing run packages that aren't part of this + * dependency closure can't match anything here and are skipped. */ export function parseConfigArguments( blocks: { global?: ConfigArguments; package?: ConfigArguments }, registry: ModuleRegistry, -): { entries: ParsedConfigArgument[]; unresolvedKeys: string[] } { - const resolveAddress = (address: string) => registry.resolveAddress(address); + context: ConfigArgumentsContext, +): { entries: ParsedConfigArgument[] } { const entries: ParsedConfigArgument[] = []; - const unresolvedKeys: string[] = []; + const currentAddress = normalizeAddress(context.package.address); + const packageAddresses: Record = Object.fromEntries( + Object.entries({ + ...context.packageAddresses, + [context.package.id]: context.package.address, + }).map(([id, address]) => [id, normalizeAddress(address)]), + ); const merged = new Map< string, @@ -239,7 +290,11 @@ export function parseConfigArguments( continue; } - const parsed = parseTypeTag(matcher.type, resolveAddress); + const parsed = parseTypeTag(matcher.type, { + scopeAddress: source === 'package' ? currentAddress : null, + packageAddresses, + root: matcher.type, + }); if (!('datatype' in parsed)) { throw new Error( @@ -252,15 +307,23 @@ export function parseConfigArguments( const datatype = summary?.structs[name] ?? summary?.enums[name]; if (!datatype) { - if (source === 'package') { + if (registry.hasResolvedAddress(address)) { + // The matcher's package is part of this dependency closure, so the type has to + // exist — this is a typo. throw new Error( - `configArguments.${key}: type "${matcher.type}" was not found in this package's ` + - `summaries. Package-scoped configArguments can only reference types reachable from ` + - `this package — fix the type, or move the entry to the global configArguments block ` + - `if it targets another package.`, + `configArguments.${key}: type "${matcher.type}" was not found in its package's summaries`, + ); + } + // The matcher references a run package that isn't part of this package's dependency + // closure — it can't match anything here. It is validated when its own package is + // generated. + if (source === 'package') { + console.warn( + `configArguments.${key}: type "${matcher.type}" is not part of ${context.package.id}'s ` + + `dependencies and will never match — consider moving it to the global block or the ` + + `package it belongs to.`, ); } - unresolvedKeys.push(key); continue; } @@ -292,7 +355,7 @@ export function parseConfigArguments( }); } - return { entries, unresolvedKeys }; + return { entries }; } function typeEqualsTag( @@ -316,7 +379,7 @@ function typeEqualsTag( if (!('datatype' in tag)) return false; const { Datatype } = type; return ( - resolveMatcherAddress(Datatype.module.address, resolveAddress) === tag.datatype.address && + normalizeAddress(resolveAddress(Datatype.module.address)) === tag.datatype.address && Datatype.module.name === tag.datatype.module && Datatype.name === tag.datatype.name && Datatype.type_arguments.length === tag.datatype.typeArguments.length && @@ -363,7 +426,7 @@ export function findConfigArgumentMatch( } const { Datatype } = type; - const paramAddress = resolveMatcherAddress(Datatype.module.address, resolveAddress); + const paramAddress = normalizeAddress(resolveAddress(Datatype.module.address)); const candidates: { entry: TypeConfigArgument; specificity: number }[] = []; const blockedNameMatchers: TypeConfigArgument[] = []; diff --git a/packages/codegen/src/config.ts b/packages/codegen/src/config.ts index 6b8e3c653..e8e07efd7 100644 --- a/packages/codegen/src/config.ts +++ b/packages/codegen/src/config.ts @@ -39,11 +39,14 @@ const FORBIDDEN_CONFIG_KEYS = new Set(['__proto__', 'constructor', 'prototype']) export const configArgumentMatcherSchema = z.union([ z.strictObject({ /** - * Fully-qualified Move type to match function parameters against, e.g. - * `0x...::margin_registry::MarginRegistry`. A generic type written without type arguments - * (e.g. `0x...::pool::Pool`) matches every instantiation and requires a resolver function - * as the config value. A fully instantiated generic (e.g. `0x...::margin_pool::MarginPool<0x2::sui::SUI>`) - * only matches parameters concretely typed with that exact instantiation. + * Move type to match function parameters against, written network-agnostically as + * `module::TypeName`. In a package's own `configArguments` block a bare `module::TypeName` + * refers to that package's type; other packages in the run are referenced by their + * `packages` identifier (`@myapp/core::pool::Pool`), and the chain-stable framework + * packages by address (`0x2::sui::SUI`). A generic type written without type arguments + * matches every instantiation and requires a resolver function as the config value; a + * fully instantiated generic (e.g. `pool::Pool<0x2::sui::SUI>`) only matches parameters + * concretely typed with that exact instantiation. */ type: z.string(), /** diff --git a/packages/codegen/src/index.ts b/packages/codegen/src/index.ts index 3c827317e..07256a5f8 100644 --- a/packages/codegen/src/index.ts +++ b/packages/codegen/src/index.ts @@ -6,6 +6,7 @@ import { basename, join } from 'node:path'; import { ModuleRegistry } from './module-registry.js'; import { MoveModuleBuilder } from './move-module-builder.js'; import { existsSync, statSync } from 'node:fs'; +import { normalizeSuiAddress } from '@mysten/sui/utils'; import { getUtilsContent } from './generate-utils.js'; import { parse } from 'toml'; import { FileBuilder } from './file-builder.js'; @@ -38,6 +39,7 @@ export async function generateFromPackageSummary({ includePhantomTypeParameters = false, errorClass, configArguments: globalConfigArguments, + packageAddresses, }: { package: PackageConfig; prune: boolean; @@ -47,6 +49,12 @@ export async function generateFromPackageSummary({ includePhantomTypeParameters?: boolean; errorClass?: ErrorClassConfig; configArguments?: ConfigArguments; + /** + * Resolved root addresses of the other packages in the codegen run, keyed by their `packages` + * identifier, so `configArguments` matchers can reference them (see + * `resolvePackageRootAddress`). The CLI builds this automatically. + */ + packageAddresses?: Record; }) { if (!pkg.path) { throw new Error(`Package path is required (got ${pkg.package})`); @@ -165,22 +173,21 @@ export async function generateFromPackageSummary({ ) ).flat(); - const { entries: configArgumentEntries, unresolvedKeys } = + const currentPackageAddress = isOnChainPackage + ? rootPackageId! + : (addressMappings[mainPackageDir] ?? mainPackageDir); + + const { entries: configArgumentEntries } = Object.keys(globalConfigArguments ?? {}).length || Object.keys(pkg.configArguments ?? {}).length ? parseConfigArguments( { global: globalConfigArguments, package: pkg.configArguments }, registry, + { + package: { id: pkg.package, address: currentPackageAddress }, + packageAddresses, + }, ) - : { entries: [], unresolvedKeys: [] }; - - // Unresolved keys here are always from the global block (package-scoped unresolved matchers - // throw during parsing). They may belong to another package generated in the same run — the - // CLI aggregates these across packages and errors for keys that resolve nowhere. - if (unresolvedKeys.length > 0) { - console.warn( - `configArguments keys not resolvable in ${pkg.package} (skipped): ${unresolvedKeys.join(', ')}`, - ); - } + : { entries: [] }; const packageEntries = configArgumentEntries.filter( (entry) => entry.kind === 'package' && entry.package === pkg.package, @@ -274,15 +281,19 @@ export async function generateFromPackageSummary({ } } - const unusedTypeEntries = configArgumentEntries.filter( - (entry) => entry.kind === 'type' && !usedConfigKeys.has(entry.key), + // Every matcher is checked in the run of the package it targets: an unused entry targeting + // this package is a misconfiguration (wrong parameterName, wrong instantiation, or a function + // filtered out of generation). Entries targeting other packages are checked in their own runs. + const normalizedCurrentAddress = normalizePackageAddress(currentPackageAddress); + const unusedOwnEntries = configArgumentEntries.filter( + (entry) => + entry.kind === 'type' && + entry.address === normalizedCurrentAddress && + !usedConfigKeys.has(entry.key), ); - // Package-scoped entries can only target this package, so an unused one is a misconfiguration - // (wrong parameterName, wrong instantiation, or a function filtered out of generation). - const unusedPackageScoped = unusedTypeEntries.filter((entry) => entry.source === 'package'); - if (unusedPackageScoped.length > 0) { + if (unusedOwnEntries.length > 0) { console.warn( - `configArguments keys that matched no generated function parameters in ${pkg.package}: ${unusedPackageScoped + `configArguments keys that matched no generated function parameters in ${pkg.package}: ${unusedOwnEntries .map((entry) => entry.key) .join(', ')}`, ); @@ -299,15 +310,57 @@ export async function generateFromPackageSummary({ importExtension, }); } +} + +const HEX_ADDRESS = /^0x[0-9a-fA-F]{1,64}$/; + +function normalizePackageAddress(address: string) { + return HEX_ADDRESS.test(address) ? normalizeSuiAddress(address) : address; +} + +/** + * Resolve a package's root address from its summaries directory, for the `packageAddresses` map + * passed to `generateFromPackageSummary`. Requires summaries to already exist at `pkgPath`. + */ +export async function resolvePackageRootAddress(pkgPath: string): Promise { + const metadataPath = join(pkgPath, 'root_package_metadata.json'); + if (existsSync(metadataPath)) { + const metadata: RootPackageMetadata = JSON.parse(await readFile(metadataPath, 'utf-8')); + return metadata.root_package_original_id ?? metadata.root_package_id; + } + + const summaryDir = join(pkgPath, 'package_summaries'); + if (!existsSync(join(summaryDir, 'address_mapping.json'))) { + return undefined; + } + const addressMappings: Record = JSON.parse( + await readFile(join(summaryDir, 'address_mapping.json'), 'utf-8'), + ); - return { - /** Global-block keys whose matcher type was not found in this package's summaries. */ - unresolvedConfigKeys: unresolvedKeys, - /** Global-block type keys that resolved here but matched no generated parameter. */ - unusedConfigKeys: unusedTypeEntries - .filter((entry) => entry.source === 'global') - .map((entry) => entry.key), - }; + let localAddressLabels: string[] = []; + let packageName = ''; + try { + const parsedToml: { package?: { name?: unknown }; addresses?: Record } = parse( + await readFile(join(pkgPath, 'Move.toml'), 'utf-8'), + ); + localAddressLabels = Object.keys(parsedToml.addresses ?? {}); + if (typeof parsedToml.package?.name === 'string') { + packageName = parsedToml.package.name.toLowerCase(); + } + } catch { + // fall through to summary-directory-based resolution + } + + const packages = (await readdir(summaryDir)).filter((file) => + statSync(join(summaryDir, file)).isDirectory(), + ); + + try { + const mainDir = resolveLocalMainPackageDir(localAddressLabels, packages, packageName, pkgPath); + return addressMappings[mainDir] ?? mainDir; + } catch { + return undefined; + } } /** diff --git a/packages/codegen/src/module-registry.ts b/packages/codegen/src/module-registry.ts index 61988db88..f4593bfa8 100644 --- a/packages/codegen/src/module-registry.ts +++ b/packages/codegen/src/module-registry.ts @@ -64,6 +64,17 @@ export class ModuleRegistry { return summary?.structs[name]?.abilities ?? summary?.enums[name]?.abilities; } + /** Whether any loaded module belongs to a package with this resolved address. */ + hasResolvedAddress(resolvedAddress: string): boolean { + const normalized = normalizeAddress(resolvedAddress); + for (const builder of this.#builders.values()) { + if (normalizeAddress(this.resolveAddress(builder.summary.id.address)) === normalized) { + return true; + } + } + return false; + } + #keyOf(address: string, module: string): string { return `${address}::${module}`; } diff --git a/packages/codegen/tests/config-arguments.test.ts b/packages/codegen/tests/config-arguments.test.ts index edcb18bc1..3a8d56d31 100644 --- a/packages/codegen/tests/config-arguments.test.ts +++ b/packages/codegen/tests/config-arguments.test.ts @@ -23,6 +23,10 @@ const ADDRESS_MAPPINGS = { testpkg: '0x0000000000000000000000000000000000000000000000000000000000000000', }; +const TESTPKG_CONTEXT = { + package: { id: '@test/testpkg', address: ADDRESS_MAPPINGS.testpkg }, +}; + async function createBuilders(configArguments: ConfigArguments, packageConfigKey?: string) { const registry = new ModuleRegistry(ADDRESS_MAPPINGS); const counter = await MoveModuleBuilder.fromSummaryFile( @@ -36,7 +40,7 @@ async function createBuilders(configArguments: ConfigArguments, packageConfigKey '@test/testpkg', ); - const { entries } = parseConfigArguments({ global: configArguments }, registry); + const { entries } = parseConfigArguments({ global: configArguments }, registry, TESTPKG_CONTEXT); counter.setConfigArguments(entries, packageConfigKey); registryBuilder.setConfigArguments(entries, packageConfigKey); @@ -149,7 +153,7 @@ function createPoolsBuilder( importExtension: '.js', typeOrigins: options.typeOrigins, }); - const { entries } = parseConfigArguments({ global: configArguments }, registry); + const { entries } = parseConfigArguments({ global: configArguments }, registry, TESTPKG_CONTEXT); builder.setConfigArguments(entries); return builder; } @@ -176,7 +180,7 @@ describe('configArguments schema', () => { }); describe('parseConfigArguments', () => { - it('parses type, instantiated type, and package matchers', async () => { + function createRegistry() { const registry = new ModuleRegistry(ADDRESS_MAPPINGS); new MoveModuleBuilder({ summary: poolsSummary() as any, @@ -184,24 +188,28 @@ describe('parseConfigArguments', () => { mvrNameOrAddress: '@test/testpkg', importExtension: '.js', }); + return registry; + } - const { entries, unresolvedKeys } = parseConfigArguments( + it('parses package-qualified, framework-qualified, and package matchers', async () => { + const { entries } = parseConfigArguments( { global: { - pool: { type: 'testpkg::pools::Pool' }, - suiPool: { type: 'testpkg::pools::Pool<0x2::sui::SUI>' }, + pool: { type: '@test/testpkg::pools::Pool' }, + suiPool: { type: '@test/testpkg::pools::Pool<0x2::sui::SUI>' }, pkg: { package: '@test/testpkg' }, }, }, - registry, + createRegistry(), + TESTPKG_CONTEXT, ); - expect(unresolvedKeys).toEqual([]); expect(entries).toMatchObject([ { kind: 'type', key: 'pool', source: 'global', + address: '0x0000000000000000000000000000000000000000000000000000000000000000', module: 'pools', name: 'Pool', typeArguments: null, @@ -226,136 +234,194 @@ describe('parseConfigArguments', () => { ]); }); - it('merges package-scoped entries over global entries per key', async () => { - const registry = new ModuleRegistry(ADDRESS_MAPPINGS); - new MoveModuleBuilder({ - summary: poolsSummary() as any, - registry, - mvrNameOrAddress: '@test/testpkg', - importExtension: '.js', - }); - + it('resolves bare module::Type matchers against the declaring package', async () => { const { entries } = parseConfigArguments( { - global: { - pool: { type: 'testpkg::pools::Pool' }, - coin: { type: 'testpkg::pools::Coin' }, - }, package: { - pool: { type: 'testpkg::pools::Pool<0x2::sui::SUI>' }, + pool: { type: 'pools::Pool' }, + coinPool: { type: 'pools::Pool' }, }, }, - registry, + createRegistry(), + TESTPKG_CONTEXT, ); - // Map insertion order keeps the global position for overridden keys. expect(entries).toMatchObject([ - { key: 'pool', source: 'package', typeArguments: [{ datatype: { name: 'SUI' } }] }, - { key: 'coin', source: 'global', typeArguments: [] }, + { + key: 'pool', + address: '0x0000000000000000000000000000000000000000000000000000000000000000', + module: 'pools', + name: 'Pool', + typeArguments: null, + }, + { + key: 'coinPool', + typeArguments: [ + { + datatype: { + address: '0x0000000000000000000000000000000000000000000000000000000000000000', + module: 'pools', + name: 'Coin', + }, + }, + ], + }, ]); }); - it('reports global matchers for types that are not in the summaries as unresolved', async () => { - const registry = new ModuleRegistry(ADDRESS_MAPPINGS); - new MoveModuleBuilder({ - summary: poolsSummary() as any, - registry, - mvrNameOrAddress: '@test/testpkg', - importExtension: '.js', - }); + it('requires package qualification for global matchers', async () => { + expect(() => + parseConfigArguments( + { global: { pool: { type: 'pools::Pool' } } }, + createRegistry(), + TESTPKG_CONTEXT, + ), + ).toThrowError(/must be qualified with a package in the global configArguments block/); + }); - const { entries, unresolvedKeys } = parseConfigArguments( + it('resolves other run packages through the packageAddresses map', async () => { + const { entries } = parseConfigArguments( + { global: { pool: { type: '@other/pkg::pools::Pool' } } }, + createRegistry(), + { + ...TESTPKG_CONTEXT, + // @other/pkg resolves to the same summary package in this test closure. + packageAddresses: { '@other/pkg': ADDRESS_MAPPINGS.testpkg }, + }, + ); + + expect(entries).toMatchObject([{ key: 'pool', module: 'pools', name: 'Pool' }]); + }); + + it('merges package-scoped entries over global entries per key', async () => { + const { entries } = parseConfigArguments( { global: { - missingType: { type: 'testpkg::pools::DoesNotExist' }, - missingModule: { type: '0x999::other::Thing' }, - pool: { type: 'testpkg::pools::Pool' }, + pool: { type: '@test/testpkg::pools::Pool' }, + coin: { type: '@test/testpkg::pools::Coin' }, + }, + package: { + pool: { type: 'pools::Pool<0x2::sui::SUI>' }, }, }, - registry, + createRegistry(), + TESTPKG_CONTEXT, ); - expect(unresolvedKeys).toEqual(['missingType', 'missingModule']); - expect(entries.map((entry) => entry.key)).toEqual(['pool']); + // Map insertion order keeps the global position for overridden keys. + expect(entries).toMatchObject([ + { key: 'pool', source: 'package', typeArguments: [{ datatype: { name: 'SUI' } }] }, + { key: 'coin', source: 'global', typeArguments: [] }, + ]); }); - it('errors for package-scoped matchers whose type is not in the summaries', async () => { - const registry = new ModuleRegistry(ADDRESS_MAPPINGS); - new MoveModuleBuilder({ - summary: poolsSummary() as any, - registry, - mvrNameOrAddress: '@test/testpkg', - importExtension: '.js', - }); + it('skips matchers for run packages outside this dependency closure', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + try { + // Global entries for other packages are silently inapplicable here (they are + // validated when their own package is generated). + const { entries } = parseConfigArguments( + { global: { other: { type: '@other/pkg::vault::Vault' } } }, + createRegistry(), + { + ...TESTPKG_CONTEXT, + packageAddresses: { + '@other/pkg': '0x0000000000000000000000000000000000000000000000000000000000000042', + }, + }, + ); + expect(entries).toEqual([]); + expect(warn).not.toHaveBeenCalled(); + // A package-scoped entry referencing a package that is not a dependency can never + // match, which deserves a warning. + parseConfigArguments( + { package: { other: { type: '@other/pkg::vault::Vault' } } }, + createRegistry(), + { + ...TESTPKG_CONTEXT, + packageAddresses: { + '@other/pkg': '0x0000000000000000000000000000000000000000000000000000000000000042', + }, + }, + ); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('will never match')); + } finally { + warn.mockRestore(); + } + }); + + it('errors when the matched type does not exist in a package that is part of the closure', async () => { expect(() => parseConfigArguments( - { package: { missing: { type: 'testpkg::pools::DoesNotExist' } } }, - registry, + { global: { missing: { type: '@test/testpkg::pools::DoesNotExist' } } }, + createRegistry(), + TESTPKG_CONTEXT, ), - ).toThrowError(/was not found in this package's summaries/); + ).toThrowError(/was not found in its package's summaries/); }); it('rejects malformed matcher types', async () => { - const registry = new ModuleRegistry(ADDRESS_MAPPINGS); - new MoveModuleBuilder({ - summary: poolsSummary() as any, - registry, - mvrNameOrAddress: '@test/testpkg', - importExtension: '.js', - }); - - const parse = (type: string) => parseConfigArguments({ global: { bad: { type } } }, registry); + const registry = createRegistry(); + const parse = (type: string) => + parseConfigArguments({ global: { bad: { type } } }, registry, TESTPKG_CONTEXT); - expect(() => parse('Pool')).toThrowError(/Expected a fully-qualified Move type/); + expect(() => parse('Pool')).toThrowError(/Expected "module::Type"/); expect(() => parse('u64')).toThrowError(/must be a Move datatype/); - expect(() => parse('testpkg::pools::Pool<0x2::sui::SUI>>')).toThrowError(/unbalanced '>'/); - expect(() => parse('testpkg::pools::Pool<0x2::sui::SUI')).toThrowError(/unbalanced ' parse('testpkg::pools::Coin<>')).toThrowError(/empty type argument/); - expect(() => parse('testpkg::pools::Pool <0x2::sui::SUI>')).toThrowError( + expect(() => parse('@test/testpkg::pools::Pool<0x2::sui::SUI>>')).toThrowError( + /unbalanced '>'/, + ); + expect(() => parse('@test/testpkg::pools::Pool<0x2::sui::SUI')).toThrowError(/unbalanced ' parse('@test/testpkg::pools::Coin<>')).toThrowError(/empty type argument/); + expect(() => parse('@test/testpkg::pools::Pool <0x2::sui::SUI>')).toThrowError( /is not a valid module::type pair/, ); - expect(() => parse('testpkg::pools::Pool<2::sui::SUI>')).toThrowError(/Invalid address "2"/); - expect(() => parse('@test/testpkg::pools::Pool')).toThrowError( - /MVR names cannot be matched against package summaries/, + expect(() => parse('2::sui::SUI')).toThrowError(/Unknown package "2"/); + expect(() => parse('@unknown/pkg::pools::Pool')).toThrowError( + /Unknown package "@unknown\/pkg"/, + ); + }); + + it('rejects non-framework package addresses', async () => { + const registry = createRegistry(); + const parse = (type: string) => + parseConfigArguments({ global: { bad: { type } } }, registry, TESTPKG_CONTEXT); + + expect(() => parse('0x999::vault::Vault')).toThrowError( + /package addresses are network-specific/, + ); + expect(() => parse('@test/testpkg::pools::Pool<0xabc123::coin::COIN>')).toThrowError( + /package addresses are network-specific/, ); }); it('rejects partially instantiated matchers with a dedicated error', async () => { - const registry = new ModuleRegistry(ADDRESS_MAPPINGS); - new MoveModuleBuilder({ - summary: poolsSummary() as any, - registry, - mvrNameOrAddress: '@test/testpkg', - importExtension: '.js', - }); + const registry = createRegistry(); expect(() => - parseConfigArguments({ global: { pool: { type: 'testpkg::pools::Pool' } } }, registry), + parseConfigArguments( + { global: { pool: { type: '@test/testpkg::pools::Pool' } } }, + registry, + TESTPKG_CONTEXT, + ), ).toThrowError(/partially instantiated matchers are not supported/); // A nested uninstantiated generic is also a partial instantiation. expect(() => parseConfigArguments( - { global: { pool: { type: 'testpkg::pools::Pool' } } }, + { global: { pool: { type: '@test/testpkg::pools::Pool<@test/testpkg::pools::Pool>' } } }, registry, + TESTPKG_CONTEXT, ), ).toThrowError(/Partially instantiated matchers are not supported/); }); it('rejects instantiated matchers with the wrong arity', async () => { - const registry = new ModuleRegistry(ADDRESS_MAPPINGS); - new MoveModuleBuilder({ - summary: poolsSummary() as any, - registry, - mvrNameOrAddress: '@test/testpkg', - importExtension: '.js', - }); - expect(() => parseConfigArguments( - { global: { pool: { type: 'testpkg::pools::Pool<0x2::sui::SUI, u64>' } } }, - registry, + { global: { pool: { type: '@test/testpkg::pools::Pool<0x2::sui::SUI, u64>' } } }, + createRegistry(), + TESTPKG_CONTEXT, ), ).toThrowError(/expects 1 type argument\(s\), got 2/); }); @@ -364,7 +430,7 @@ describe('parseConfigArguments', () => { describe('config-driven function codegen', () => { it('non-generic matcher: matched parameter becomes optional with an optional config slice', async () => { const { registry } = await createBuilders({ - registryObj: { type: 'testpkg::registry::Registry' }, + registryObj: { type: '@test/testpkg::registry::Registry' }, }); registry.includeFunctions(['register']); const output = await render(registry); @@ -415,7 +481,7 @@ describe('config-driven function codegen', () => { it('makes arguments optional and the tuple suffix optional when every parameter is config-matched', async () => { const { registry } = await createBuilders({ - registryObj: { type: 'testpkg::registry::Registry' }, + registryObj: { type: '@test/testpkg::registry::Registry' }, }); registry.includeFunctions(['lookup']); const output = await render(registry); @@ -453,7 +519,7 @@ describe('config-driven function codegen', () => { it('uninstantiated generic matcher: config value requires a resolver and receives the parameter instantiation', async () => { const { registry } = await createBuilders({ - container: { type: 'testpkg::registry::Container' }, + container: { type: '@test/testpkg::registry::Container' }, }); registry.includeFunctions(['container_size']); const output = await render(registry); @@ -495,8 +561,8 @@ describe('config-driven function codegen', () => { it('instantiated matcher only matches concrete instantiations and wins over the uninstantiated matcher', async () => { const builder = createPoolsBuilder({ - pool: { type: 'testpkg::pools::Pool' }, - suiPool: { type: 'testpkg::pools::Pool<0x2::sui::SUI>' }, + pool: { type: '@test/testpkg::pools::Pool' }, + suiPool: { type: '@test/testpkg::pools::Pool<0x2::sui::SUI>' }, }); builder.includeFunctions(['use_generic', 'use_concrete']); const output = await render(builder); @@ -576,7 +642,7 @@ describe('config-driven function codegen', () => { it('resolver context tags for own-package types use the package name, not the placeholder address', async () => { const builder = createPoolsBuilder({ - pool: { type: 'testpkg::pools::Pool' }, + pool: { type: '@test/testpkg::pools::Pool' }, }); builder.includeFunctions(['use_own_coin']); const output = await render(builder); @@ -588,7 +654,7 @@ describe('config-driven function codegen', () => { it('resolver context tags use origin addresses for upgraded packages', async () => { const ORIGIN_V1 = '0x000000000000000000000000000000000000000000000000000000000000aaaa'; const builder = createPoolsBuilder( - { pool: { type: 'testpkg::pools::Pool' } }, + { pool: { type: '@test/testpkg::pools::Pool' } }, { typeOrigins: { Coin: ORIGIN_V1 } }, ); builder.includeFunctions(['use_own_coin']); @@ -600,7 +666,7 @@ describe('config-driven function codegen', () => { it('errors when a bare matcher hits two named parameters in one signature', async () => { const builder = createPoolsBuilder({ - pool: { type: 'testpkg::pools::Pool' }, + pool: { type: '@test/testpkg::pools::Pool' }, }); builder.includeFunctions(['swap']); @@ -613,7 +679,7 @@ describe('config-driven function codegen', () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); try { const builder = createPoolsBuilder( - { pool: { type: 'testpkg::pools::Pool' } }, + { pool: { type: '@test/testpkg::pools::Pool' } }, { parameterNames: false }, ); builder.includeFunctions(['swap']); @@ -632,7 +698,7 @@ describe('config-driven function codegen', () => { it('generates tuple-only bindings for nameless summaries with a matched parameter', async () => { const builder = createPoolsBuilder( - { pool: { type: 'testpkg::pools::Pool' } }, + { pool: { type: '@test/testpkg::pools::Pool' } }, { parameterNames: false }, ); builder.includeFunctions(['use_generic']); @@ -676,8 +742,8 @@ describe('config-driven function codegen', () => { it('name refinement disambiguates two parameters of the same type', async () => { const builder = createPoolsBuilder({ - basePool: { type: 'testpkg::pools::Pool', parameterName: 'base_pool' }, - quotePool: { type: 'testpkg::pools::Pool', parameterName: 'quote_pool' }, + basePool: { type: '@test/testpkg::pools::Pool', parameterName: 'base_pool' }, + quotePool: { type: '@test/testpkg::pools::Pool', parameterName: 'quote_pool' }, }); builder.includeFunctions(['swap']); const output = await render(builder); @@ -723,9 +789,9 @@ describe('config-driven function codegen', () => { it('name-refined matchers win over a bare matcher for the same type', async () => { const builder = createPoolsBuilder({ - pool: { type: 'testpkg::pools::Pool' }, - basePool: { type: 'testpkg::pools::Pool', parameterName: 'base_pool' }, - quotePool: { type: 'testpkg::pools::Pool', parameterName: 'quote_pool' }, + pool: { type: '@test/testpkg::pools::Pool' }, + basePool: { type: '@test/testpkg::pools::Pool', parameterName: 'base_pool' }, + quotePool: { type: '@test/testpkg::pools::Pool', parameterName: 'quote_pool' }, }); builder.includeFunctions(['swap', 'use_generic']); const output = await render(builder); @@ -742,8 +808,8 @@ describe('config-driven function codegen', () => { it('errors when two matchers of equal specificity hit the same parameter', async () => { const builder = createPoolsBuilder({ - poolA: { type: 'testpkg::pools::Pool' }, - poolB: { type: 'testpkg::pools::Pool' }, + poolA: { type: '@test/testpkg::pools::Pool' }, + poolB: { type: '@test/testpkg::pools::Pool' }, }); builder.includeFunctions(['use_generic']); @@ -754,7 +820,7 @@ describe('config-driven function codegen', () => { it('errors when a name matcher would apply to a nameless parameter and nothing else matches', async () => { const builder = createPoolsBuilder( - { basePool: { type: 'testpkg::pools::Pool', parameterName: 'base_pool' } }, + { basePool: { type: '@test/testpkg::pools::Pool', parameterName: 'base_pool' } }, { parameterNames: false }, ); builder.includeFunctions(['swap']); @@ -769,7 +835,7 @@ describe('config-driven function codegen', () => { // Pool, so the matcher is filtered by instantiation before the nameless check. const builder = createPoolsBuilder( { - suiPool: { type: 'testpkg::pools::Pool<0x2::sui::SUI>', parameterName: 'sui_pool' }, + suiPool: { type: '@test/testpkg::pools::Pool<0x2::sui::SUI>', parameterName: 'sui_pool' }, }, { parameterNames: false }, ); @@ -783,8 +849,8 @@ describe('config-driven function codegen', () => { it('falls back to a bare matcher instead of erroring when a name matcher hits a nameless parameter', async () => { const builder = createPoolsBuilder( { - pool: { type: 'testpkg::pools::Pool' }, - basePool: { type: 'testpkg::pools::Pool', parameterName: 'base_pool' }, + pool: { type: '@test/testpkg::pools::Pool' }, + basePool: { type: '@test/testpkg::pools::Pool', parameterName: 'base_pool' }, }, { parameterNames: false }, ); @@ -798,7 +864,7 @@ describe('config-driven function codegen', () => { it('package entries are added to the package-address precedence chain', async () => { const { registry } = await createBuilders( { - registryObj: { type: 'testpkg::registry::Registry' }, + registryObj: { type: '@test/testpkg::registry::Registry' }, testpkgAddress: { package: '@test/testpkg' }, }, 'testpkgAddress', @@ -853,11 +919,12 @@ describe('config-driven function codegen', () => { const { entries } = parseConfigArguments( { global: { - registryObj: { type: 'testpkg::registry::Registry' }, + registryObj: { type: '@test/testpkg::registry::Registry' }, testpkgAddress: { package: '@test/testpkg' }, }, }, registry, + TESTPKG_CONTEXT, ); builder.setConfigArguments(entries, 'testpkgAddress'); builder.includeTypes(['Registry']); @@ -884,7 +951,7 @@ describe('generateFromPackageSummary with configArguments', () => { async function generate() { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const result = await generateFromPackageSummary({ + await generateFromPackageSummary({ package: { package: '@test/testpkg', path: FIXTURE_PATH, @@ -892,25 +959,24 @@ describe('generateFromPackageSummary with configArguments', () => { prune: true, outputDir: GENERATED_DIR, configArguments: { - registryObj: { type: 'testpkg::registry::Registry' }, - container: { type: 'testpkg::registry::Container' }, + registryObj: { type: '@test/testpkg::registry::Registry' }, + container: { type: '@test/testpkg::registry::Container' }, testpkgAddress: { package: '@test/testpkg' }, - missing: { type: 'testpkg::registry::DoesNotExist' }, - unusedEntry: { type: 'testpkg::registry::Entry' }, + unusedEntry: { type: '@test/testpkg::registry::Entry' }, }, }); - return { warn, result }; + return warn; } - it('emits config-arguments.ts and config-driven bindings, reporting unresolved and unused keys', async () => { - const { warn, result } = await generate(); + it('emits config-arguments.ts and config-driven bindings, warning on unused keys', async () => { + const warn = await generate(); + // Entry is a plain store struct that no generated function takes as a parameter. expect(warn).toHaveBeenCalledWith( - 'configArguments keys not resolvable in @test/testpkg (skipped): missing', + expect.stringContaining( + 'configArguments keys that matched no generated function parameters in @test/testpkg: unusedEntry', + ), ); - expect(result.unresolvedConfigKeys).toEqual(['missing']); - // Entry is a plain store struct that no generated function takes as a parameter. - expect(result.unusedConfigKeys).toEqual(['unusedEntry']); const configArgs = await readFile( join(GENERATED_DIR, 'testpkg', 'config-arguments.ts'), @@ -935,20 +1001,20 @@ describe('generateFromPackageSummary with configArguments', () => { expect(registryModule).toContain('resolveConfigArgument'); }); - it('errors for unresolved keys in a package-scoped block', async () => { + it('errors when a matcher references a type missing from its package', async () => { await expect( generateFromPackageSummary({ package: { package: '@test/testpkg', path: FIXTURE_PATH, configArguments: { - missing: { type: 'testpkg::registry::DoesNotExist' }, + missing: { type: '@test/testpkg::registry::DoesNotExist' }, }, }, prune: true, outputDir: GENERATED_DIR, }), - ).rejects.toThrowError(/was not found in this package's summaries/); + ).rejects.toThrowError(/was not found in its package's summaries/); }); it('warns for unused keys in a package-scoped block', async () => { @@ -958,7 +1024,7 @@ describe('generateFromPackageSummary with configArguments', () => { package: '@test/testpkg', path: FIXTURE_PATH, configArguments: { - unusedEntry: { type: 'testpkg::registry::Entry' }, + unusedEntry: { type: '@test/testpkg::registry::Entry' }, }, }, prune: true, diff --git a/packages/create-dapp/templates/react-e2e-counter/src/contracts/utils/index.ts b/packages/create-dapp/templates/react-e2e-counter/src/contracts/utils/index.ts index 9a6624c19..20e68d294 100644 --- a/packages/create-dapp/templates/react-e2e-counter/src/contracts/utils/index.ts +++ b/packages/create-dapp/templates/react-e2e-counter/src/contracts/utils/index.ts @@ -9,7 +9,10 @@ import { } from "@mysten/sui/bcs"; import { normalizeSuiAddress } from "@mysten/sui/utils"; import { type TransactionArgument, isArgument } from "@mysten/sui/transactions"; -import { type ClientWithCoreApi, type SuiClientTypes } from "@mysten/sui/client"; +import { + type ClientWithCoreApi, + type SuiClientTypes, +} from "@mysten/sui/client"; const MOVE_STDLIB_ADDRESS = normalizeSuiAddress("0x1"); const SUI_FRAMEWORK_ADDRESS = normalizeSuiAddress("0x2"); diff --git a/packages/docs/content/codegen/index.mdx b/packages/docs/content/codegen/index.mdx index 3775dcc9b..72b4f9ad6 100644 --- a/packages/docs/content/codegen/index.mdx +++ b/packages/docs/content/codegen/index.mdx @@ -297,7 +297,9 @@ declare which Move types (or package addresses) come from a config object, and t functions accept that config object directly instead of requiring those arguments on every call. `configArguments` maps author-chosen keys to matchers. It can be declared globally (shared by all -packages) or per package entry (merged over the global block, per key): +packages) or per package entry (merged over the global block, per key). Because codegen output is +network-agnostic, matchers never contain package addresses: types are identified by +`module::TypeName`, qualified with the package identifier you already use in the `packages` config: ```typescript const config: SuiCodegenConfig = { @@ -307,33 +309,44 @@ const config: SuiCodegenConfig = { package: '@myapp/core', path: './move/core', configArguments: { + // In a package's own block, a bare module::Type refers to that package's type. // Non-generic type: parameters of this type resolve from `config.registry` - registry: { type: '0x...::registry::Registry' }, + registry: { type: 'registry::Registry' }, // Generic type without type arguments: matches every instantiation, and the // config value must be a resolver function - pool: { type: '0x...::pool::Pool' }, + pool: { type: 'pool::Pool' }, // Fully instantiated generic: only matches parameters concretely typed with - // this exact instantiation in the Move signature - suiPool: { type: '0x...::pool::Pool<0x2::sui::SUI>' }, + // this exact instantiation in the Move signature. Framework packages (0x1-0x3) + // have chain-stable addresses and can be referenced directly. + suiPool: { type: 'pool::Pool<0x2::sui::SUI>' }, // Package entry (keyed by the `package` value of a `packages` entry): adds an // optional config key that overrides the package address used for calls corePackageId: { package: '@myapp/core' }, }, }, + { + package: '@myapp/vaults', + path: './move/vaults', + configArguments: { + // Types from other packages in the run are referenced by their identifier. + vaultPool: { type: '@myapp/core::pool::Pool' }, + }, + }, ], }; ``` -Matcher addresses are resolved through the package's address mapping, so named addresses (for -example, `myapp::registry::Registry`) also work. Partially instantiated matchers (for example, +In the global `configArguments` block there is no ambient package, so type matchers there must be +package-qualified (`@myapp/core::pool::Pool`). Partially instantiated matchers (for example, `Pool` or a nested uninstantiated generic) are not supported. Use an uninstantiated matcher with a resolver function instead. -Misconfigured matchers are surfaced at generation time: malformed types, wrong arity, and -package-scoped matchers whose type isn't in the package's summaries are hard errors. Global-block -matchers that don't resolve in a package are skipped with a warning (a shared global block may span -multiple packages in one run), and the CLI errors if a global key resolves in no package at all. -Keys that resolve but never match any generated parameter produce a warning. +Misconfiguration is surfaced at generation time: malformed types, wrong arity, unknown package +identifiers, and non-framework package addresses are hard errors, and every matcher's type is +checked for existence when the package it references is part of the generated package's dependency +closure. Matchers referencing run packages outside that closure can't match anything there and are +skipped (they are validated when their own package is generated). Keys that never match any +generated parameter of their own package produce a warning. ### Generated output @@ -422,8 +435,8 @@ Refine the matchers with the Move parameter names: ```typescript configArguments: { - basePool: { type: '0x...::pool::Pool', parameterName: 'base_pool' }, - quotePool: { type: '0x...::pool::Pool', parameterName: 'quote_pool' }, + basePool: { type: 'pool::Pool', parameterName: 'base_pool' }, + quotePool: { type: 'pool::Pool', parameterName: 'quote_pool' }, }, ``` From 576229fd05a99b5847720fc8fbad5cf0c6bdc30e Mon Sep 17 00:00:00 2001 From: Michael Hayes Date: Wed, 22 Jul 2026 15:28:34 -0700 Subject: [PATCH 5/7] feat(codegen): function matchers, multi-matcher keys, and richer resolver context - Function matchers ({ function: 'module::fn', parameterName | parameterIndex }) configure a single function parameter directly, with the type derived from the signature; the parameter can be inferred for single-argument functions. Highest matcher specificity, scoped/qualified like type matchers. - One config key may declare an array of matchers (multiple types); any key that can resolve more than one binding (multiple matchers, a generic, or multiple parameters in one signature) is typed resolver-only in generated config slices and interfaces. This replaces the bare-matcher-two-params hard error: multi-matches are legal and disambiguated in the resolver. - ConfigResolverContext gains parameterIndex, so nameless (bytecode) positions are distinguishable in resolvers. Co-Authored-By: Claude Fable 5 --- packages/codegen/src/config-arguments.ts | 428 +++++++++++++----- packages/codegen/src/config.ts | 82 ++-- packages/codegen/src/generate-utils.ts | 2 + packages/codegen/src/index.ts | 28 +- packages/codegen/src/move-module-builder.ts | 61 +-- .../codegen/tests/config-arguments.test.ts | 130 ++++-- packages/codegen/tests/utils.test.ts | 2 + packages/docs/content/codegen/index.mdx | 19 +- 8 files changed, 552 insertions(+), 200 deletions(-) diff --git a/packages/codegen/src/config-arguments.ts b/packages/codegen/src/config-arguments.ts index c9f1a151a..f7edacc27 100644 --- a/packages/codegen/src/config-arguments.ts +++ b/packages/codegen/src/config-arguments.ts @@ -5,6 +5,7 @@ import { normalizeSuiAddress } from '@mysten/sui/utils'; import type { ConfigArguments } from './config.js'; import type { ModuleRegistry } from './module-registry.js'; import type { Parameter, Type } from './types/summary.js'; +import { isWellKnownObjectParameter } from './utils.js'; /** A parsed type tag from a `configArguments` matcher. Always fully concrete. */ export type ParsedTypeTag = @@ -30,6 +31,22 @@ export interface TypeConfigArgument { * resolver function as the config value (a static id cannot be correct across instantiations). */ isGeneric: boolean; + /** Whether this entry alone forces the config value to be a resolver function. */ + requiresResolver: boolean; +} + +export interface FunctionConfigArgument { + kind: 'function'; + key: string; + source: ConfigArgumentSource; + address: string; + module: string; + functionName: string; + parameterName?: string; + /** Position in the generated function's arguments (TxContext/well-known excluded). */ + parameterIndex?: number; + /** Whether this entry alone forces the config value to be a resolver function. */ + requiresResolver: boolean; } export interface PackageConfigArgument { @@ -39,7 +56,10 @@ export interface PackageConfigArgument { package: string; } -export type ParsedConfigArgument = TypeConfigArgument | PackageConfigArgument; +export type ParsedConfigArgument = + | TypeConfigArgument + | FunctionConfigArgument + | PackageConfigArgument; export interface ConfigArgumentsContext { /** Identifier (from the `packages` config) and resolved address of the package being generated. */ @@ -61,6 +81,44 @@ function normalizeAddress(address: string) { return HEX_ADDRESS.test(address) ? normalizeSuiAddress(address) : address; } +function resolveQualifier( + packagePart: string | undefined, + unqualified: string, + tag: string, + ctx: { scopeAddress: string | null; packageAddresses: Record; root: string }, +): string { + if (packagePart === undefined) { + if (ctx.scopeAddress === null) { + throw new Error( + `configArguments matcher "${ctx.root}": "${unqualified}" must be qualified ` + + `with a package in the global configArguments block (e.g. ` + + `"@pkg/name::${unqualified}"). Bare matchers are only supported in a package's own ` + + `configArguments block.`, + ); + } + return ctx.scopeAddress; + } + if (FRAMEWORK_ADDRESS.test(packagePart)) { + return normalizeSuiAddress(packagePart); + } + if (HEX_ADDRESS.test(packagePart)) { + throw new Error( + `Invalid package "${packagePart}" in configArguments matcher "${tag}": package addresses ` + + `are network-specific and cannot be used in matchers (only the framework addresses ` + + `0x1-0x3 are chain-stable). Reference the package by its identifier from the codegen ` + + `config instead (e.g. "@pkg/name::${unqualified}").`, + ); + } + const resolved = ctx.packageAddresses[packagePart]; + if (resolved === undefined) { + throw new Error( + `Unknown package "${packagePart}" in configArguments matcher "${tag}". Known packages in ` + + `this codegen run: ${Object.keys(ctx.packageAddresses).join(', ')}`, + ); + } + return resolved; +} + interface ParseContext { /** * Address a bare `module::Type` resolves against, or `null` when there is no ambient package @@ -160,39 +218,7 @@ function parseTypeTag(tag: string, ctx: ParseContext): ParsedTypeTag { ); } - let address: string; - - if (packagePart === undefined) { - // Codegen output is network-agnostic: a bare `module::Type` refers to the package whose - // configArguments block declares it. - if (ctx.scopeAddress === null) { - throw new Error( - `configArguments matcher "${ctx.root}": "${modulePart}::${namePart}" must be qualified ` + - `with a package in the global configArguments block (e.g. ` + - `"@pkg/name::${modulePart}::${namePart}"). Bare module::Type matchers are only ` + - `supported in a package's own configArguments block.`, - ); - } - address = ctx.scopeAddress; - } else if (FRAMEWORK_ADDRESS.test(packagePart)) { - address = normalizeSuiAddress(packagePart); - } else if (HEX_ADDRESS.test(packagePart)) { - throw new Error( - `Invalid package "${packagePart}" in configArguments matcher "${tag}": package addresses ` + - `are network-specific and cannot be used in matchers (only the framework addresses ` + - `0x1-0x3 are chain-stable). Reference the package by its identifier from the codegen ` + - `config instead (e.g. "@pkg/name::${modulePart}::${namePart}").`, - ); - } else { - const resolved = ctx.packageAddresses[packagePart]; - if (resolved === undefined) { - throw new Error( - `Unknown package "${packagePart}" in configArguments matcher "${tag}". Known packages in ` + - `this codegen run: ${Object.keys(ctx.packageAddresses).join(', ')}`, - ); - } - address = resolved; - } + const address = resolveQualifier(packagePart, `${modulePart}::${namePart}`, tag, ctx); const typeArguments = lt === -1 @@ -248,6 +274,151 @@ function assertFullyInstantiated( } } +function isContextParameter(type: Type, resolveAddress: (address: string) => string): boolean { + if (typeof type === 'string') return false; + if ('Reference' in type) return isContextParameter(type.Reference[1], resolveAddress); + if ('Datatype' in type) { + return ( + normalizeAddress(resolveAddress(type.Datatype.module.address)) === + normalizeSuiAddress('0x2') && + type.Datatype.module.name === 'tx_context' && + type.Datatype.name === 'TxContext' + ); + } + return false; +} + +function typeHasTypeParameter(type: Type): boolean { + if (typeof type === 'string') return false; + if ('Reference' in type) return typeHasTypeParameter(type.Reference[1]); + if ('vector' in type) return typeHasTypeParameter(type.vector); + if ('Datatype' in type) { + return type.Datatype.type_arguments.some((argument) => typeHasTypeParameter(argument.argument)); + } + return 'TypeParameter' in type || 'NamedTypeParameter' in type; +} + +function parseFunctionMatcher( + key: string, + source: ConfigArgumentSource, + matcher: { function: string; parameterName?: string; parameterIndex?: number }, + { + registry, + scopeAddress, + packageAddresses, + packageId, + }: { + registry: ModuleRegistry; + scopeAddress: string | null; + packageAddresses: Record; + packageId: string; + }, +): FunctionConfigArgument | null { + const parts = matcher.function.split('::'); + if ((parts.length !== 2 && parts.length !== 3) || parts.some((part) => part.length === 0)) { + throw new Error( + `configArguments.${key}: invalid function "${matcher.function}". Expected ` + + `"module::function_name", optionally qualified with a package from the codegen config.`, + ); + } + + const packagePart = parts.length === 3 ? parts[0] : undefined; + const modulePart = parts[parts.length - 2]; + const functionPart = parts[parts.length - 1]; + + if (!MOVE_IDENTIFIER.test(modulePart) || !MOVE_IDENTIFIER.test(functionPart)) { + throw new Error(`configArguments.${key}: invalid function "${matcher.function}"`); + } + + if (matcher.parameterName !== undefined && matcher.parameterIndex !== undefined) { + throw new Error( + `configArguments.${key}: specify either parameterName or parameterIndex, not both`, + ); + } + + const address = resolveQualifier( + packagePart, + `${modulePart}::${functionPart}`, + matcher.function, + { + scopeAddress, + packageAddresses, + root: matcher.function, + }, + ); + + const summary = registry.getSummaryByResolvedAddress(address, modulePart); + const func = summary?.functions[functionPart]; + + if (!func) { + if (registry.hasResolvedAddress(address)) { + throw new Error( + `configArguments.${key}: function "${matcher.function}" was not found in its package's summaries`, + ); + } + if (source === 'package') { + console.warn( + `configArguments.${key}: function "${matcher.function}" is not part of ${packageId}'s ` + + `dependencies and will never match.`, + ); + } + return null; + } + + const resolveAddress = (target: string) => registry.resolveAddress(target); + // The same positions the generated arguments use: TxContext and auto-injected well-known + // objects are excluded. + const parameters = func.parameters.filter( + (param) => + !isContextParameter(param.type_, resolveAddress) && + !isWellKnownObjectParameter(param.type_, resolveAddress), + ); + + let bound: Parameter | undefined; + let parameterIndex = matcher.parameterIndex; + + if (matcher.parameterName !== undefined) { + bound = parameters.find((param) => param.name === matcher.parameterName); + if (!bound) { + throw new Error( + `configArguments.${key}: function "${matcher.function}" has no parameter named ` + + `"${matcher.parameterName}"${parameters.some((param) => param.name === undefined) ? " (this package's summaries do not include parameter names — use parameterIndex instead)" : ''}`, + ); + } + parameterIndex = undefined; + } else if (parameterIndex !== undefined) { + bound = parameters[parameterIndex]; + if (!bound) { + throw new Error( + `configArguments.${key}: function "${matcher.function}" has ${parameters.length} ` + + `argument(s); parameterIndex ${parameterIndex} is out of range`, + ); + } + } else { + if (parameters.length !== 1) { + throw new Error( + `configArguments.${key}: function "${matcher.function}" has ${parameters.length} ` + + `argument(s) — specify parameterName or parameterIndex`, + ); + } + bound = parameters[0]; + parameterIndex = 0; + } + + return { + kind: 'function', + key, + source, + address, + module: modulePart, + functionName: functionPart, + parameterName: matcher.parameterName, + parameterIndex, + // A parameter typed with the function's own type parameters cannot be a single static id. + requiresResolver: typeHasTypeParameter(bound.type_), + }; +} + /** * Parse and validate `configArguments` blocks against the modules loaded in `registry`. * Per-package entries are merged over global entries (per key). @@ -285,74 +456,97 @@ export function parseConfigArguments( } for (const [key, { matcher, source }] of merged) { - if ('package' in matcher) { + if (!Array.isArray(matcher) && 'package' in matcher) { entries.push({ kind: 'package', key, source, package: matcher.package }); continue; } - const parsed = parseTypeTag(matcher.type, { - scopeAddress: source === 'package' ? currentAddress : null, - packageAddresses, - root: matcher.type, - }); + // One key may declare several matchers; it then resolves multiple bindings, so its config + // value must be a resolver function. + const matchers = Array.isArray(matcher) ? matcher : [matcher]; + const multi = matchers.length > 1; - if (!('datatype' in parsed)) { - throw new Error( - `configArguments.${key}: matcher type "${matcher.type}" must be a Move datatype`, - ); - } + for (const single of matchers) { + const scopeAddress = source === 'package' ? currentAddress : null; - const { address, module, name, typeArguments } = parsed.datatype; - const summary = registry.getSummaryByResolvedAddress(address, module); - const datatype = summary?.structs[name] ?? summary?.enums[name]; + if ('function' in single) { + const entry = parseFunctionMatcher(key, source, single, { + registry, + scopeAddress, + packageAddresses, + packageId: context.package.id, + }); + if (entry) { + entries.push({ ...entry, requiresResolver: entry.requiresResolver || multi }); + } + continue; + } - if (!datatype) { - if (registry.hasResolvedAddress(address)) { - // The matcher's package is part of this dependency closure, so the type has to - // exist — this is a typo. + const parsed = parseTypeTag(single.type, { + scopeAddress, + packageAddresses, + root: single.type, + }); + + if (!('datatype' in parsed)) { throw new Error( - `configArguments.${key}: type "${matcher.type}" was not found in its package's summaries`, + `configArguments.${key}: matcher type "${single.type}" must be a Move datatype`, ); } - // The matcher references a run package that isn't part of this package's dependency - // closure — it can't match anything here. It is validated when its own package is - // generated. - if (source === 'package') { - console.warn( - `configArguments.${key}: type "${matcher.type}" is not part of ${context.package.id}'s ` + - `dependencies and will never match — consider moving it to the global block or the ` + - `package it belongs to.`, - ); + + const { address, module, name, typeArguments } = parsed.datatype; + const summary = registry.getSummaryByResolvedAddress(address, module); + const datatype = summary?.structs[name] ?? summary?.enums[name]; + + if (!datatype) { + if (registry.hasResolvedAddress(address)) { + // The matcher's package is part of this dependency closure, so the type has to + // exist — this is a typo. + throw new Error( + `configArguments.${key}: type "${single.type}" was not found in its package's summaries`, + ); + } + // The matcher references a run package that isn't part of this package's dependency + // closure — it can't match anything here. It is validated when its own package is + // generated. + if (source === 'package') { + console.warn( + `configArguments.${key}: type "${single.type}" is not part of ${context.package.id}'s ` + + `dependencies and will never match — consider moving it to the global block or the ` + + `package it belongs to.`, + ); + } + continue; } - continue; - } - const arity = datatype.type_parameters.length; - const isGeneric = arity > 0; - // A generic type written without `<...>` matches every instantiation. - const uninstantiated = isGeneric && !matcher.type.includes('<'); + const arity = datatype.type_parameters.length; + const isGeneric = arity > 0; + // A generic type written without `<...>` matches every instantiation. + const uninstantiated = isGeneric && !single.type.includes('<'); - if (!uninstantiated && typeArguments.length !== arity) { - throw new Error( - `configArguments.${key}: type "${matcher.type}" expects ${arity} type argument(s), got ${typeArguments.length}`, - ); - } + if (!uninstantiated && typeArguments.length !== arity) { + throw new Error( + `configArguments.${key}: type "${single.type}" expects ${arity} type argument(s), got ${typeArguments.length}`, + ); + } - for (const argument of typeArguments) { - assertFullyInstantiated(argument, registry, matcher.type); - } + for (const argument of typeArguments) { + assertFullyInstantiated(argument, registry, single.type); + } - entries.push({ - kind: 'type', - key, - source, - address, - module, - name, - typeArguments: uninstantiated ? null : typeArguments, - parameterName: matcher.parameterName, - isGeneric, - }); + entries.push({ + kind: 'type', + key, + source, + address, + module, + name, + typeArguments: uninstantiated ? null : typeArguments, + parameterName: single.parameterName, + isGeneric, + requiresResolver: (isGeneric && uninstantiated) || multi, + }); + } } return { entries }; @@ -411,32 +605,62 @@ export function findConfigArgumentMatch( { resolveAddress, functionLabel, + functionRef, + parameterIndex, }: { resolveAddress: (address: string) => string; functionLabel: string; + /** The module and function the parameter belongs to, for function matchers. */ + functionRef: { moduleAddress: string; moduleName: string; functionName: string }; + /** The parameter's position in the generated arguments. */ + parameterIndex: number; }, -): TypeConfigArgument | null { +): TypeConfigArgument | FunctionConfigArgument | null { let type = param.type_; while (typeof type !== 'string' && 'Reference' in type) { type = type.Reference[1]; } - if (typeof type === 'string' || !('Datatype' in type)) { - return null; - } + const datatype = typeof type !== 'string' && 'Datatype' in type ? type.Datatype : null; + const paramAddress = datatype ? normalizeAddress(resolveAddress(datatype.module.address)) : null; + const moduleAddress = normalizeAddress(resolveAddress(functionRef.moduleAddress)); - const { Datatype } = type; - const paramAddress = normalizeAddress(resolveAddress(Datatype.module.address)); - - const candidates: { entry: TypeConfigArgument; specificity: number }[] = []; - const blockedNameMatchers: TypeConfigArgument[] = []; + const candidates: { + entry: TypeConfigArgument | FunctionConfigArgument; + specificity: number; + }[] = []; + const blockedNameMatchers: (TypeConfigArgument | FunctionConfigArgument)[] = []; for (const entry of entries) { - if (entry.kind !== 'type') continue; + if (entry.kind === 'function') { + if ( + entry.address !== moduleAddress || + entry.module !== functionRef.moduleName || + entry.functionName !== functionRef.functionName + ) { + continue; + } + if (entry.parameterName !== undefined) { + if (param.name === undefined) { + blockedNameMatchers.push(entry); + continue; + } + if (entry.parameterName !== param.name) { + continue; + } + } else if (entry.parameterIndex !== parameterIndex) { + continue; + } + // Function matchers are the most specific form. + candidates.push({ entry, specificity: 4 }); + continue; + } + + if (entry.kind !== 'type' || !datatype) continue; if ( entry.address !== paramAddress || - entry.module !== Datatype.module.name || - entry.name !== Datatype.name + entry.module !== datatype.module.name || + entry.name !== datatype.name ) { continue; } @@ -447,8 +671,8 @@ export function findConfigArgumentMatch( // Fully instantiated matcher: only matches parameters concretely typed with that // exact instantiation in the Move signature. if ( - Datatype.type_arguments.length !== entry.typeArguments.length || - !Datatype.type_arguments.every((arg, i) => + datatype.type_arguments.length !== entry.typeArguments.length || + !datatype.type_arguments.every((arg, i) => typeEqualsTag(arg.argument, entry.typeArguments![i], resolveAddress), ) ) { @@ -486,7 +710,7 @@ export function findConfigArgumentMatch( const best = Math.max(...candidates.map((c) => c.specificity)); const winners = candidates.filter((c) => c.specificity === best); - if (winners.length > 1) { + if (winners.length > 1 && !winners.every((c) => c.entry.key === winners[0].entry.key)) { throw new Error( `Parameter ${param.name ?? ''} of ${functionLabel} is matched by multiple configArguments entries with equal specificity: ${winners .map((c) => c.entry.key) diff --git a/packages/codegen/src/config.ts b/packages/codegen/src/config.ts index e8e07efd7..b8f156ddc 100644 --- a/packages/codegen/src/config.ts +++ b/packages/codegen/src/config.ts @@ -36,33 +36,56 @@ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/; /** Keys that would collide with `Object.prototype` or mutate prototypes on plain objects. */ const FORBIDDEN_CONFIG_KEYS = new Set(['__proto__', 'constructor', 'prototype']); +const typeMatcherSchema = z.strictObject({ + /** + * Move type to match function parameters against, written network-agnostically as + * `module::TypeName`. In a package's own `configArguments` block a bare `module::TypeName` + * refers to that package's type; other packages in the run are referenced by their + * `packages` identifier (`@myapp/core::pool::Pool`), and the chain-stable framework + * packages by address (`0x2::sui::SUI`). A generic type written without type arguments + * matches every instantiation and requires a resolver function as the config value; a + * fully instantiated generic (e.g. `pool::Pool<0x2::sui::SUI>`) only matches parameters + * concretely typed with that exact instantiation. + */ + type: z.string(), + /** + * Optional Move parameter-name refinement, for signatures with two parameters of the same + * matched type. Only supported for summaries generated from local packages (bytecode + * summaries do not include parameter names). + */ + parameterName: z.string().optional(), +}); + +const functionMatcherSchema = z.strictObject({ + /** + * A Move function whose parameter is configured directly, written `module::function_name` + * (scoped like type matchers: bare form in a package's own block, `@pkg::module::fn` + * otherwise). The parameter's type is derived from the signature. + */ + function: z.string(), + /** The Move name of the parameter to configure. */ + parameterName: z.string().optional(), + /** + * The position of the parameter to configure, in the generated function's arguments (the + * same positions as the tuple form of `arguments` — `TxContext` and auto-injected well-known + * objects are excluded). Use for summaries without parameter names. When both this and + * `parameterName` are omitted, the function must have exactly one argument. + */ + parameterIndex: z.number().int().nonnegative().optional(), +}); + +const packageMatcherSchema = z.strictObject({ + /** + * Package entry, keyed by the package's name/MVR name from the `packages` config. Adds an + * optional config key that overrides the package address used for generated calls. + */ + package: z.string(), +}); + export const configArgumentMatcherSchema = z.union([ - z.strictObject({ - /** - * Move type to match function parameters against, written network-agnostically as - * `module::TypeName`. In a package's own `configArguments` block a bare `module::TypeName` - * refers to that package's type; other packages in the run are referenced by their - * `packages` identifier (`@myapp/core::pool::Pool`), and the chain-stable framework - * packages by address (`0x2::sui::SUI`). A generic type written without type arguments - * matches every instantiation and requires a resolver function as the config value; a - * fully instantiated generic (e.g. `pool::Pool<0x2::sui::SUI>`) only matches parameters - * concretely typed with that exact instantiation. - */ - type: z.string(), - /** - * Optional Move parameter-name refinement, for signatures with two parameters of the same - * matched type. Only supported for summaries generated from local packages (bytecode - * summaries do not include parameter names). - */ - parameterName: z.string().optional(), - }), - z.strictObject({ - /** - * Package entry, keyed by the package's name/MVR name from the `packages` config. Adds an - * optional config key that overrides the package address used for generated calls. - */ - package: z.string(), - }), + typeMatcherSchema, + functionMatcherSchema, + packageMatcherSchema, ]); export const configArgumentsSchema = z.record( @@ -75,7 +98,12 @@ export const configArgumentsSchema = z.record( .refine((key) => !FORBIDDEN_CONFIG_KEYS.has(key), { message: 'configArguments keys must not be prototype property names', }), - configArgumentMatcherSchema, + z.union([ + configArgumentMatcherSchema, + // One key may resolve multiple types/parameters; its config value must then be a + // resolver function. + z.array(z.union([typeMatcherSchema, functionMatcherSchema])), + ]), ); export type ConfigArgumentMatcher = z.infer; diff --git a/packages/codegen/src/generate-utils.ts b/packages/codegen/src/generate-utils.ts index 373b5cb91..ba598047d 100644 --- a/packages/codegen/src/generate-utils.ts +++ b/packages/codegen/src/generate-utils.ts @@ -208,6 +208,8 @@ export interface ConfigResolverContext { functionName: string; /** The Move name of the matched parameter, when the summary includes parameter names. */ parameterName?: string; + /** The matched parameter's position in the generated function's arguments. */ + parameterIndex: number; } /** diff --git a/packages/codegen/src/index.ts b/packages/codegen/src/index.ts index 07256a5f8..0e6f5e3a1 100644 --- a/packages/codegen/src/index.ts +++ b/packages/codegen/src/index.ts @@ -190,7 +190,8 @@ export async function generateFromPackageSummary({ : { entries: [] }; const packageEntries = configArgumentEntries.filter( - (entry) => entry.kind === 'package' && entry.package === pkg.package, + (entry): entry is ParsedConfigArgument & { kind: 'package' } => + entry.kind === 'package' && entry.package === pkg.package, ); if (packageEntries.length > 1) { throw new Error( @@ -287,7 +288,7 @@ export async function generateFromPackageSummary({ const normalizedCurrentAddress = normalizePackageAddress(currentPackageAddress); const unusedOwnEntries = configArgumentEntries.filter( (entry) => - entry.kind === 'type' && + entry.kind !== 'package' && entry.address === normalizedCurrentAddress && !usedConfigKeys.has(entry.key), ); @@ -305,7 +306,7 @@ export async function generateFromPackageSummary({ outputDir, packageName, entries: configArgumentEntries.filter( - (entry) => entry.kind === 'type' || entry.package === pkg.package, + (entry) => entry.kind !== 'package' || entry.package === pkg.package, ), importExtension, }); @@ -390,21 +391,30 @@ async function generateConfigInterface({ camelCase(packageName.replaceAll(/[^A-Za-z0-9_$]+/g, '_').replace(/^(\d)/, '_$1')), )}Config`; - const fields = entries.map((entry) => { - if (entry.kind === 'package') { - return `${entry.key}?: string`; + const fieldEntries = new Map(); + for (const entry of entries) { + fieldEntries.set(entry.key, [...(fieldEntries.get(entry.key) ?? []), entry]); + } + + const fields = [...fieldEntries.entries()].map(([key, group]) => { + if (group[0].kind === 'package') { + return `${key}?: string`; } - if (entry.isGeneric && entry.typeArguments === null) { + // A key resolving multiple bindings (or a generic) must be a resolver function. + const requiresResolver = + group.length > 1 || group.some((entry) => entry.kind !== 'package' && entry.requiresResolver); + + if (requiresResolver) { const ctxName = builder.addImport(utilsModule, 'type ConfigResolverContext'); const objArgName = builder.addImport( '@mysten/sui/transactions', 'type TransactionObjectArgument', ); - return `${entry.key}: (ctx: ${ctxName}) => string | ${objArgName}`; + return `${key}: (ctx: ${ctxName}) => string | ${objArgName}`; } - return `${entry.key}: ${builder.addImport(utilsModule, 'type ConfigValue')}`; + return `${key}: ${builder.addImport(utilsModule, 'type ConfigValue')}`; }); builder.statements.push( diff --git a/packages/codegen/src/move-module-builder.ts b/packages/codegen/src/move-module-builder.ts index b35843bb6..914a632b2 100644 --- a/packages/codegen/src/move-module-builder.ts +++ b/packages/codegen/src/move-module-builder.ts @@ -13,7 +13,11 @@ import { SUI_SYSTEM_ADDRESS, } from './render-types.js'; import { findConfigArgumentMatch } from './config-arguments.js'; -import type { ParsedConfigArgument, TypeConfigArgument } from './config-arguments.js'; +import type { + FunctionConfigArgument, + ParsedConfigArgument, + TypeConfigArgument, +} from './config-arguments.js'; import { camelCase, capitalize, @@ -607,39 +611,30 @@ export class MoveModuleBuilder extends FileBuilder { const functionLabel = `${this.summary.id.address}::${this.summary.id.name}::${name}`; // Parameters (by index into `requiredParameters`) resolved from the runtime config // object instead of being required arguments. - const configMatches = new Map(); + const configMatches = new Map(); + // Keys that match more than one parameter of this signature: legal, but the config + // value must be a resolver (it receives per-parameter context). + const multiMatchedKeys = new Set(); if (this.#configArguments.length > 0) { - const bareMatches = new Map(); + const matchesByKey = new Map(); requiredParameters.forEach((param, i) => { const match = findConfigArgumentMatch(param, this.#configArguments, { resolveAddress: (address) => this.#resolveAddress(address), functionLabel, + functionRef: { + moduleAddress: this.summary.id.address, + moduleName: this.summary.id.name, + functionName: name, + }, + parameterIndex: i, }); if (!match) return; configMatches.set(i, match); - if (!match.parameterName) { - bareMatches.set(match.key, [...(bareMatches.get(match.key) ?? []), i]); - } + matchesByKey.set(match.key, (matchesByKey.get(match.key) ?? 0) + 1); }); - for (const [key, matched] of bareMatches) { - if (matched.length > 1) { - if (matched.every((i) => requiredParameters[i].name)) { - throw new Error( - `configArguments.${key} matches multiple parameters of ${functionLabel} (${matched - .map((i) => requiredParameters[i].name) - .join(', ')}). Add a \`parameterName\` refinement to disambiguate.`, - ); - } - // Bytecode summaries have no parameter names, so refinement is impossible — - // skip config mapping for this function instead of failing the run. - console.warn( - `configArguments.${key} matches multiple parameters of ${functionLabel}, which cannot ` + - `be disambiguated because its parameters have no names. Config mapping is skipped ` + - `for this function.`, - ); - for (const i of matched) { - configMatches.delete(i); - } + for (const [key, count] of matchesByKey) { + if (count > 1) { + multiMatchedKeys.add(key); } } for (const match of configMatches.values()) { @@ -754,10 +749,18 @@ export class MoveModuleBuilder extends FileBuilder { for (const match of configMatches.values()) { if (seenConfigKeys.has(match.key)) continue; seenConfigKeys.add(match.key); - // An uninstantiated matcher on a generic type requires a resolver function — a - // static id cannot be correct across instantiations. + // A key must be a resolver function when it can resolve more than one binding: + // an uninstantiated generic, multiple declared matchers, or multiple parameters + // matched in this signature. + const keyEntries = this.#configArguments.filter( + (entry) => entry.kind !== 'package' && entry.key === match.key, + ); + const requiresResolver = + keyEntries.length > 1 || + keyEntries.some((entry) => entry.kind !== 'package' && entry.requiresResolver) || + multiMatchedKeys.has(match.key); configSliceFields.push( - match.isGeneric && match.typeArguments === null + requiresResolver ? `${match.key}: (ctx: ${this.#getImportName('ConfigResolverContext')}) => string | ${this.#getImportName('TransactionObjectArgument')}` : `${match.key}: ${this.#getImportName('ConfigValue')}`, ); @@ -812,7 +815,7 @@ export class MoveModuleBuilder extends FileBuilder { }); return tag.includes('${') ? `\`${tag}\`` : `'${tag}'`; }); - const ctx = `{ typeArguments: [${ctxTags.join(', ')}], packageAddress, moduleName: '${this.summary.id.name}', functionName: '${name}'${ + const ctx = `{ typeArguments: [${ctxTags.join(', ')}], packageAddress, moduleName: '${this.summary.id.name}', functionName: '${name}', parameterIndex: ${i}${ param.name ? `, parameterName: ${JSON.stringify(param.name)}` : '' } }`; return `{ index: ${i}, ${param.name ? `name: ${JSON.stringify(camelCase(param.name))}, ` : ''}resolve: () => ${this.#getImportName('resolveConfigArgument')}(options.config?.${match.key}, ${ctx}, ${JSON.stringify(match.key)}) }`; diff --git a/packages/codegen/tests/config-arguments.test.ts b/packages/codegen/tests/config-arguments.test.ts index 3a8d56d31..b9f2ef455 100644 --- a/packages/codegen/tests/config-arguments.test.ts +++ b/packages/codegen/tests/config-arguments.test.ts @@ -473,7 +473,7 @@ describe('config-driven function codegen', () => { package: packageAddress, module: 'registry', function: 'register', - arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "registry", resolve: () => resolveConfigArgument(options.config?.registryObj, { typeArguments: [], packageAddress, moduleName: 'registry', functionName: 'register', parameterName: "registry" }, "registryObj") }]), argumentsTypes, parameterNames), + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "registry", resolve: () => resolveConfigArgument(options.config?.registryObj, { typeArguments: [], packageAddress, moduleName: 'registry', functionName: 'register', parameterIndex: 0, parameterName: "registry" }, "registryObj") }]), argumentsTypes, parameterNames), }); }" `); @@ -511,7 +511,7 @@ describe('config-driven function codegen', () => { package: packageAddress, module: 'registry', function: 'lookup', - arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "registry", resolve: () => resolveConfigArgument(options.config?.registryObj, { typeArguments: [], packageAddress, moduleName: 'registry', functionName: 'lookup', parameterName: "registry" }, "registryObj") }]), argumentsTypes, parameterNames), + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "registry", resolve: () => resolveConfigArgument(options.config?.registryObj, { typeArguments: [], packageAddress, moduleName: 'registry', functionName: 'lookup', parameterIndex: 0, parameterName: "registry" }, "registryObj") }]), argumentsTypes, parameterNames), }); }" `); @@ -552,7 +552,7 @@ describe('config-driven function codegen', () => { package: packageAddress, module: 'registry', function: 'container_size', - arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "container", resolve: () => resolveConfigArgument(options.config?.container, { typeArguments: [\`\${options.typeArguments[0]}\`], packageAddress, moduleName: 'registry', functionName: 'container_size', parameterName: "container" }, "container") }]), argumentsTypes, parameterNames), + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "container", resolve: () => resolveConfigArgument(options.config?.container, { typeArguments: [\`\${options.typeArguments[0]}\`], packageAddress, moduleName: 'registry', functionName: 'container_size', parameterIndex: 0, parameterName: "container" }, "container") }]), argumentsTypes, parameterNames), typeArguments: options.typeArguments }); }" @@ -596,7 +596,7 @@ describe('config-driven function codegen', () => { package: packageAddress, module: 'pools', function: 'use_concrete', - arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "pool", resolve: () => resolveConfigArgument(options.config?.suiPool, { typeArguments: ['0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI'], packageAddress, moduleName: 'pools', functionName: 'use_concrete', parameterName: "pool" }, "suiPool") }]), argumentsTypes, parameterNames), + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "pool", resolve: () => resolveConfigArgument(options.config?.suiPool, { typeArguments: ['0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI'], packageAddress, moduleName: 'pools', functionName: 'use_concrete', parameterIndex: 0, parameterName: "pool" }, "suiPool") }]), argumentsTypes, parameterNames), }); }" `); @@ -633,7 +633,7 @@ describe('config-driven function codegen', () => { package: packageAddress, module: 'pools', function: 'use_generic', - arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "pool", resolve: () => resolveConfigArgument(options.config?.pool, { typeArguments: [\`\${options.typeArguments[0]}\`], packageAddress, moduleName: 'pools', functionName: 'use_generic', parameterName: "pool" }, "pool") }]), argumentsTypes, parameterNames), + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, name: "pool", resolve: () => resolveConfigArgument(options.config?.pool, { typeArguments: [\`\${options.typeArguments[0]}\`], packageAddress, moduleName: 'pools', functionName: 'use_generic', parameterIndex: 0, parameterName: "pool" }, "pool") }]), argumentsTypes, parameterNames), typeArguments: options.typeArguments }); }" @@ -664,36 +664,109 @@ describe('config-driven function codegen', () => { expect(fnBody?.[0]).toContain(`typeArguments: ['${ORIGIN_V1}::pools::Coin']`); }); - it('errors when a bare matcher hits two named parameters in one signature', async () => { + it('allows one key to match multiple parameters, forcing a resolver-typed config value', async () => { const builder = createPoolsBuilder({ pool: { type: '@test/testpkg::pools::Pool' }, }); builder.includeFunctions(['swap']); + const output = await render(builder); - await expect(render(builder)).rejects.toThrowError( - /configArguments\.pool matches multiple parameters of testpkg::pools::swap \(base_pool, quote_pool\)/, + // Both base_pool and quote_pool resolve through config.pool; the resolver + // disambiguates via ctx (parameterName/parameterIndex/typeArguments). + const optionsInterface = output.match(/export interface SwapOptions[\s\S]*?^}/m); + expect(optionsInterface?.[0]).toContain( + 'pool: (ctx: ConfigResolverContext) => string | TransactionObjectArgument', ); + + const fnBody = output.match(/export function swap[\s\S]*?^}/m); + expect(fnBody?.[0]).toContain('parameterIndex: 0'); + expect(fnBody?.[0]).toContain('parameterIndex: 1'); }); - it('warns and skips config mapping when a bare matcher hits two nameless parameters', async () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - try { - const builder = createPoolsBuilder( - { pool: { type: '@test/testpkg::pools::Pool' } }, - { parameterNames: false }, - ); - builder.includeFunctions(['swap']); - const output = await render(builder); + it('allows one key to declare matchers for multiple types, forcing a resolver-typed config value', async () => { + const builder = createPoolsBuilder({ + objects: [ + { type: '@test/testpkg::pools::Coin' }, + { type: '@test/testpkg::pools::Pool<0x2::sui::SUI>' }, + ], + }); + builder.includeFunctions(['use_concrete']); + const output = await render(builder); - expect(warn).toHaveBeenCalledWith( - expect.stringContaining('configArguments.pool matches multiple parameters'), - ); - // swap is still generated, without config mapping. - const optionsInterface = output.match(/export interface SwapOptions[\s\S]*?^}/m); - expect(optionsInterface?.[0]).not.toContain('config'); - } finally { - warn.mockRestore(); - } + const optionsInterface = output.match(/export interface UseConcreteOptions[\s\S]*?^}/m); + expect(optionsInterface?.[0]).toContain( + 'objects: (ctx: ConfigResolverContext) => string | TransactionObjectArgument', + ); + }); + + it('function matchers configure a single function parameter directly', async () => { + const { registry } = await createBuilders({ + // lookup has exactly one argument, so the parameter can be inferred. + lookupRegistry: { function: '@test/testpkg::registry::lookup' }, + registerRegistry: { + function: '@test/testpkg::registry::register', + parameterName: 'registry', + }, + }); + registry.includeFunctions(['lookup', 'register']); + const output = await render(registry); + + const lookupOptions = output.match(/export interface LookupOptions[\s\S]*?^}/m); + expect(lookupOptions?.[0]).toContain('lookupRegistry: ConfigValue'); + + const registerOptions = output.match(/export interface RegisterOptions[\s\S]*?^}/m); + expect(registerOptions?.[0]).toContain('registerRegistry: ConfigValue'); + // The function matcher only applies to its own function. + expect(registerOptions?.[0]).not.toContain('lookupRegistry'); + }); + + it('function matchers win over type matchers and support parameterIndex', async () => { + const builder = createPoolsBuilder({ + pool: { type: '@test/testpkg::pools::Pool' }, + concretePool: { function: '@test/testpkg::pools::use_concrete', parameterIndex: 0 }, + }); + builder.includeFunctions(['use_concrete', 'use_generic']); + const output = await render(builder); + + const concreteOptions = output.match(/export interface UseConcreteOptions[\s\S]*?^}/m); + expect(concreteOptions?.[0]).toContain('concretePool: ConfigValue'); + const concreteSlice = concreteOptions?.[0].match(/config\?: \{[\s\S]*?\}/); + expect(concreteSlice?.[0]).not.toContain('pool: (ctx'); + + const genericOptions = output.match(/export interface UseGenericOptions[\s\S]*?^}/m); + expect(genericOptions?.[0]).toContain('pool:'); + }); + + it('validates function matchers at parse time', async () => { + const registry = new ModuleRegistry(ADDRESS_MAPPINGS); + new MoveModuleBuilder({ + summary: poolsSummary() as any, + registry, + mvrNameOrAddress: '@test/testpkg', + importExtension: '.js', + }); + const parse = (matcher: object) => + parseConfigArguments({ global: { key: matcher as never } }, registry, TESTPKG_CONTEXT); + + expect(() => parse({ function: '@test/testpkg::pools::nope' })).toThrowError( + /was not found in its package's summaries/, + ); + expect(() => parse({ function: '@test/testpkg::pools::swap' })).toThrowError( + /has 2 argument\(s\) — specify parameterName or parameterIndex/, + ); + expect(() => + parse({ function: '@test/testpkg::pools::swap', parameterName: 'nope' }), + ).toThrowError(/has no parameter named "nope"/); + expect(() => parse({ function: '@test/testpkg::pools::swap', parameterIndex: 5 })).toThrowError( + /parameterIndex 5 is out of range/, + ); + expect(() => + parse({ + function: '@test/testpkg::pools::swap', + parameterName: 'base_pool', + parameterIndex: 0, + }), + ).toThrowError(/not both/); }); it('generates tuple-only bindings for nameless summaries with a matched parameter', async () => { @@ -733,7 +806,7 @@ describe('config-driven function codegen', () => { package: packageAddress, module: 'pools', function: 'use_generic', - arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, resolve: () => resolveConfigArgument(options.config?.pool, { typeArguments: [\`\${options.typeArguments[0]}\`], packageAddress, moduleName: 'pools', functionName: 'use_generic' }, "pool") }]), argumentsTypes), + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments, [{ index: 0, resolve: () => resolveConfigArgument(options.config?.pool, { typeArguments: [\`\${options.typeArguments[0]}\`], packageAddress, moduleName: 'pools', functionName: 'use_generic', parameterIndex: 0 }, "pool") }]), argumentsTypes), typeArguments: options.typeArguments }); }" @@ -780,7 +853,7 @@ describe('config-driven function codegen', () => { package: packageAddress, module: 'pools', function: 'swap', - arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "basePool", resolve: () => resolveConfigArgument(options.config?.basePool, { typeArguments: [\`\${options.typeArguments[0]}\`], packageAddress, moduleName: 'pools', functionName: 'swap', parameterName: "base_pool" }, "basePool") }, { index: 1, name: "quotePool", resolve: () => resolveConfigArgument(options.config?.quotePool, { typeArguments: [\`\${options.typeArguments[1]}\`], packageAddress, moduleName: 'pools', functionName: 'swap', parameterName: "quote_pool" }, "quotePool") }]), argumentsTypes, parameterNames), + arguments: normalizeMoveArguments(applyConfigArguments(options.arguments ?? {}, [{ index: 0, name: "basePool", resolve: () => resolveConfigArgument(options.config?.basePool, { typeArguments: [\`\${options.typeArguments[0]}\`], packageAddress, moduleName: 'pools', functionName: 'swap', parameterIndex: 0, parameterName: "base_pool" }, "basePool") }, { index: 1, name: "quotePool", resolve: () => resolveConfigArgument(options.config?.quotePool, { typeArguments: [\`\${options.typeArguments[1]}\`], packageAddress, moduleName: 'pools', functionName: 'swap', parameterIndex: 1, parameterName: "quote_pool" }, "quotePool") }]), argumentsTypes, parameterNames), typeArguments: options.typeArguments }); }" @@ -1190,6 +1263,7 @@ describe('generateFromPackageSummary with configArguments', () => { moduleName: 'registry', functionName: 'container_size', parameterName: 'container', + parameterIndex: 0, }, ]); expect(json.inputs).toEqual([{ UnresolvedObject: { objectId: CONTAINER_ID } }]); diff --git a/packages/codegen/tests/utils.test.ts b/packages/codegen/tests/utils.test.ts index 9ad46aafa..8b37038e4 100644 --- a/packages/codegen/tests/utils.test.ts +++ b/packages/codegen/tests/utils.test.ts @@ -19,12 +19,14 @@ interface TestResolverContext { moduleName: string; functionName: string; parameterName?: string; + parameterIndex: number; } const TEST_CTX: TestResolverContext = { typeArguments: [], packageAddress: '0x0', moduleName: 'test', functionName: 'test', + parameterIndex: 0, }; let resolveConfigArgument: (value: unknown, ctx: TestResolverContext, name: string) => unknown; let applyConfigArguments: ( diff --git a/packages/docs/content/codegen/index.mdx b/packages/docs/content/codegen/index.mdx index 72b4f9ad6..1aa8c8519 100644 --- a/packages/docs/content/codegen/index.mdx +++ b/packages/docs/content/codegen/index.mdx @@ -319,6 +319,14 @@ const config: SuiCodegenConfig = { // this exact instantiation in the Move signature. Framework packages (0x1-0x3) // have chain-stable addresses and can be referenced directly. suiPool: { type: 'pool::Pool<0x2::sui::SUI>' }, + // Function matcher: configure one function's parameter directly (opt-in). + // The type is derived from the signature. Use parameterIndex for onchain + // packages without parameter names; omit both when the function has exactly + // one argument. + adminCap: { function: 'admin::set_fees', parameterName: 'cap' }, + // One key may declare several matchers. It then resolves multiple types, so + // its config value must be a resolver function. + registries: [{ type: 'registry::Registry' }, { type: 'config::GlobalConfig' }], // Package entry (keyed by the `package` value of a `packages` entry): adds an // optional config key that overrides the package address used for calls corePackageId: { package: '@myapp/core' }, @@ -390,6 +398,7 @@ export interface ConfigResolverContext { moduleName: string; functionName: string; parameterName?: string; // Move parameter name, when the summary includes names + parameterIndex: number; // position in the generated function's arguments } ``` @@ -430,8 +439,9 @@ const myConfig = { ... } satisfies CoreConfig & MarginConfig; ### Name refinement When a signature has two parameters of the same matched type (for example, `base_pool` and -`quote_pool`, both `Pool`), a bare type matcher is ambiguous and codegen fails with an error. -Refine the matchers with the Move parameter names: +`quote_pool`, both `Pool`), a bare type matcher matches both: the key's config value must then be +a resolver function, which receives each parameter's own context. To give each parameter its own +config key instead, refine the matchers with the Move parameter names: ```typescript configArguments: { @@ -443,9 +453,8 @@ configArguments: { Parameter names are only available in summaries generated from local packages. A `parameterName` matcher never matches a parameter without a name; if such a matcher would otherwise apply to a nameless parameter (same type and instantiation) and nothing else matches it, codegen fails with a -clear error. When a bare matcher hits two parameters of a nameless (onchain bytecode) signature, -where refinement is impossible, config mapping is skipped for that function with a warning instead -of failing the run. +clear error. For nameless (onchain bytecode) signatures, use function matchers with `parameterIndex` +to target individual parameters. ### Package address precedence From 5c38b1981b11def506837b45da50e033a37ecc42 Mon Sep 17 00:00:00 2001 From: Michael Hayes Date: Wed, 22 Jul 2026 15:35:06 -0700 Subject: [PATCH 6/7] refactor(codegen): resolver requirement keyed on distinct bound types A config key with multiple matchers (e.g. several functions) that all bind the same concrete type keeps a plain config value; only keys spanning multiple distinct types, or binding a generic, are typed resolver-only. Co-Authored-By: Claude Fable 5 --- packages/codegen/src/config-arguments.ts | 55 +++++++++++++++---- packages/codegen/src/index.ts | 8 ++- packages/codegen/src/move-module-builder.ts | 26 +++------ .../codegen/tests/config-arguments.test.ts | 17 ++++++ packages/docs/content/codegen/index.mdx | 5 +- 5 files changed, 77 insertions(+), 34 deletions(-) diff --git a/packages/codegen/src/config-arguments.ts b/packages/codegen/src/config-arguments.ts index f7edacc27..bc4f0e603 100644 --- a/packages/codegen/src/config-arguments.ts +++ b/packages/codegen/src/config-arguments.ts @@ -31,8 +31,12 @@ export interface TypeConfigArgument { * resolver function as the config value (a static id cannot be correct across instantiations). */ isGeneric: boolean; - /** Whether this entry alone forces the config value to be a resolver function. */ - requiresResolver: boolean; + /** + * Canonical identity of the concrete type this entry binds, or `null` when it can bind many + * (an uninstantiated generic). A key whose entries span multiple identities (or any `null`) + * must be a resolver function. + */ + boundType: string | null; } export interface FunctionConfigArgument { @@ -45,8 +49,8 @@ export interface FunctionConfigArgument { parameterName?: string; /** Position in the generated function's arguments (TxContext/well-known excluded). */ parameterIndex?: number; - /** Whether this entry alone forces the config value to be a resolver function. */ - requiresResolver: boolean; + /** See `TypeConfigArgument.boundType`. */ + boundType: string | null; } export interface PackageConfigArgument { @@ -298,6 +302,35 @@ function typeHasTypeParameter(type: Type): boolean { return 'TypeParameter' in type || 'NamedTypeParameter' in type; } +function tagIdentity(tag: ParsedTypeTag): string { + if ('prim' in tag) return tag.prim; + if ('vector' in tag) return `vector<${tagIdentity(tag.vector)}>`; + const { address, module, name, typeArguments } = tag.datatype; + const base = `${address}::${module}::${name}`; + return typeArguments.length ? `${base}<${typeArguments.map(tagIdentity).join(',')}>` : base; +} + +function canonicalTypeIdentity( + type: Type, + resolveAddress: (address: string) => string, +): string | null { + if (typeof type === 'string') return type === 'signer' || type === '_' ? null : type; + if ('Reference' in type) return canonicalTypeIdentity(type.Reference[1], resolveAddress); + if ('vector' in type) { + const inner = canonicalTypeIdentity(type.vector, resolveAddress); + return inner === null ? null : `vector<${inner}>`; + } + if ('Datatype' in type) { + const args = type.Datatype.type_arguments.map((argument) => + canonicalTypeIdentity(argument.argument, resolveAddress), + ); + if (args.some((argument) => argument === null)) return null; + const base = `${normalizeAddress(resolveAddress(type.Datatype.module.address))}::${type.Datatype.module.name}::${type.Datatype.name}`; + return args.length ? `${base}<${args.join(',')}>` : base; + } + return null; +} + function parseFunctionMatcher( key: string, source: ConfigArgumentSource, @@ -415,7 +448,9 @@ function parseFunctionMatcher( parameterName: matcher.parameterName, parameterIndex, // A parameter typed with the function's own type parameters cannot be a single static id. - requiresResolver: typeHasTypeParameter(bound.type_), + boundType: typeHasTypeParameter(bound.type_) + ? null + : canonicalTypeIdentity(bound.type_, (target) => registry.resolveAddress(target)), }; } @@ -461,10 +496,8 @@ export function parseConfigArguments( continue; } - // One key may declare several matchers; it then resolves multiple bindings, so its config - // value must be a resolver function. + // One key may declare several matchers (e.g. several functions sharing one config value). const matchers = Array.isArray(matcher) ? matcher : [matcher]; - const multi = matchers.length > 1; for (const single of matchers) { const scopeAddress = source === 'package' ? currentAddress : null; @@ -477,7 +510,7 @@ export function parseConfigArguments( packageId: context.package.id, }); if (entry) { - entries.push({ ...entry, requiresResolver: entry.requiresResolver || multi }); + entries.push(entry); } continue; } @@ -544,7 +577,9 @@ export function parseConfigArguments( typeArguments: uninstantiated ? null : typeArguments, parameterName: single.parameterName, isGeneric, - requiresResolver: (isGeneric && uninstantiated) || multi, + boundType: uninstantiated + ? null + : tagIdentity({ datatype: { address, module, name, typeArguments } }), }); } } diff --git a/packages/codegen/src/index.ts b/packages/codegen/src/index.ts index 0e6f5e3a1..1f0614dbe 100644 --- a/packages/codegen/src/index.ts +++ b/packages/codegen/src/index.ts @@ -401,9 +401,11 @@ async function generateConfigInterface({ return `${key}?: string`; } - // A key resolving multiple bindings (or a generic) must be a resolver function. - const requiresResolver = - group.length > 1 || group.some((entry) => entry.kind !== 'package' && entry.requiresResolver); + // A key binding multiple distinct types (or a generic) must be a resolver function. + const boundTypes = new Set( + group.flatMap((entry) => (entry.kind !== 'package' ? [entry.boundType] : [])), + ); + const requiresResolver = boundTypes.size > 1 || boundTypes.has(null); if (requiresResolver) { const ctxName = builder.addImport(utilsModule, 'type ConfigResolverContext'); diff --git a/packages/codegen/src/move-module-builder.ts b/packages/codegen/src/move-module-builder.ts index 914a632b2..5bb312733 100644 --- a/packages/codegen/src/move-module-builder.ts +++ b/packages/codegen/src/move-module-builder.ts @@ -612,11 +612,7 @@ export class MoveModuleBuilder extends FileBuilder { // Parameters (by index into `requiredParameters`) resolved from the runtime config // object instead of being required arguments. const configMatches = new Map(); - // Keys that match more than one parameter of this signature: legal, but the config - // value must be a resolver (it receives per-parameter context). - const multiMatchedKeys = new Set(); if (this.#configArguments.length > 0) { - const matchesByKey = new Map(); requiredParameters.forEach((param, i) => { const match = findConfigArgumentMatch(param, this.#configArguments, { resolveAddress: (address) => this.#resolveAddress(address), @@ -630,13 +626,7 @@ export class MoveModuleBuilder extends FileBuilder { }); if (!match) return; configMatches.set(i, match); - matchesByKey.set(match.key, (matchesByKey.get(match.key) ?? 0) + 1); }); - for (const [key, count] of matchesByKey) { - if (count > 1) { - multiMatchedKeys.add(key); - } - } for (const match of configMatches.values()) { this.usedConfigKeys.add(match.key); } @@ -749,16 +739,14 @@ export class MoveModuleBuilder extends FileBuilder { for (const match of configMatches.values()) { if (seenConfigKeys.has(match.key)) continue; seenConfigKeys.add(match.key); - // A key must be a resolver function when it can resolve more than one binding: - // an uninstantiated generic, multiple declared matchers, or multiple parameters - // matched in this signature. - const keyEntries = this.#configArguments.filter( - (entry) => entry.kind !== 'package' && entry.key === match.key, + // A key must be a resolver function when it can bind more than one distinct type + // (or a generic). Multiple matchers sharing one concrete type share a plain value. + const boundTypes = new Set( + this.#configArguments.flatMap((entry) => + entry.kind !== 'package' && entry.key === match.key ? [entry.boundType] : [], + ), ); - const requiresResolver = - keyEntries.length > 1 || - keyEntries.some((entry) => entry.kind !== 'package' && entry.requiresResolver) || - multiMatchedKeys.has(match.key); + const requiresResolver = boundTypes.size > 1 || boundTypes.has(null); configSliceFields.push( requiresResolver ? `${match.key}: (ctx: ${this.#getImportName('ConfigResolverContext')}) => string | ${this.#getImportName('TransactionObjectArgument')}` diff --git a/packages/codegen/tests/config-arguments.test.ts b/packages/codegen/tests/config-arguments.test.ts index b9f2ef455..2bc8ce897 100644 --- a/packages/codegen/tests/config-arguments.test.ts +++ b/packages/codegen/tests/config-arguments.test.ts @@ -720,6 +720,23 @@ describe('config-driven function codegen', () => { expect(registerOptions?.[0]).not.toContain('lookupRegistry'); }); + it('multiple matchers binding the same concrete type keep a plain config value', async () => { + const { registry } = await createBuilders({ + reg: [ + { function: '@test/testpkg::registry::lookup' }, + { function: '@test/testpkg::registry::register', parameterName: 'registry' }, + ], + }); + registry.includeFunctions(['lookup', 'register']); + const output = await render(registry); + + // Both bindings are `registry::Registry`, so one static id serves both functions. + const lookupOptions = output.match(/export interface LookupOptions[\s\S]*?^}/m); + expect(lookupOptions?.[0]).toContain('reg: ConfigValue'); + const registerOptions = output.match(/export interface RegisterOptions[\s\S]*?^}/m); + expect(registerOptions?.[0]).toContain('reg: ConfigValue'); + }); + it('function matchers win over type matchers and support parameterIndex', async () => { const builder = createPoolsBuilder({ pool: { type: '@test/testpkg::pools::Pool' }, diff --git a/packages/docs/content/codegen/index.mdx b/packages/docs/content/codegen/index.mdx index 1aa8c8519..1eacf84b9 100644 --- a/packages/docs/content/codegen/index.mdx +++ b/packages/docs/content/codegen/index.mdx @@ -324,8 +324,9 @@ const config: SuiCodegenConfig = { // packages without parameter names; omit both when the function has exactly // one argument. adminCap: { function: 'admin::set_fees', parameterName: 'cap' }, - // One key may declare several matchers. It then resolves multiple types, so - // its config value must be a resolver function. + // One key may declare several matchers. If they all bind the same concrete + // type, a plain value still works; if they span multiple types, the config + // value must be a resolver function. registries: [{ type: 'registry::Registry' }, { type: 'config::GlobalConfig' }], // Package entry (keyed by the `package` value of a `packages` entry): adds an // optional config key that overrides the package address used for calls From c7ec62852ebb394712e29cd1bf7f6fee2c197cc1 Mon Sep 17 00:00:00 2001 From: Michael Hayes Date: Wed, 22 Jul 2026 16:11:29 -0700 Subject: [PATCH 7/7] feat(walrus): use codegen config arguments --- packages/walrus/src/client.ts | 47 +- packages/walrus/src/contracts/utils/index.ts | 140 +- .../contracts/walrus/apportionment_queue.ts | 15 +- packages/walrus/src/contracts/walrus/auth.ts | 20 +- packages/walrus/src/contracts/walrus/blob.ts | 90 +- .../walrus/src/contracts/walrus/committee.ts | 20 +- .../src/contracts/walrus/config-arguments.ts | 9 + .../walrus/src/contracts/walrus/encoding.ts | 5 +- .../src/contracts/walrus/extended_field.ts | 25 +- packages/walrus/src/contracts/walrus/init.ts | 126 +- .../walrus/src/contracts/walrus/metadata.ts | 20 +- .../src/contracts/walrus/node_metadata.ts | 45 +- .../src/contracts/walrus/shared_blob.ts | 68 +- .../walrus/src/contracts/walrus/slashing.ts | 111 +- .../walrus/src/contracts/walrus/staked_wal.ts | 40 +- .../walrus/src/contracts/walrus/staking.ts | 1138 +++++++++++++++-- .../contracts/walrus/storage_accounting.ts | 25 +- .../src/contracts/walrus/storage_node.ts | 30 +- .../src/contracts/walrus/storage_pool.ts | 85 +- .../src/contracts/walrus/storage_resource.ts | 45 +- .../walrus/src/contracts/walrus/system.ts | 1097 ++++++++++++++-- .../walrus/src/contracts/walrus/upgrade.ts | 171 ++- packages/walrus/sui-codegen.config.ts | 5 + 23 files changed, 2943 insertions(+), 434 deletions(-) create mode 100644 packages/walrus/src/contracts/walrus/config-arguments.ts diff --git a/packages/walrus/src/client.ts b/packages/walrus/src/client.ts index e3b7301fa..a5b127f31 100644 --- a/packages/walrus/src/client.ts +++ b/packages/walrus/src/client.ts @@ -22,6 +22,7 @@ import { removeMetadataPair, } from './contracts/walrus/blob.js'; import type { Committee } from './contracts/walrus/committee.js'; +import type { WalrusConfig } from './contracts/walrus/config-arguments.js'; import * as metadata from './contracts/walrus/metadata.js'; import { StakingInnerV1 } from './contracts/walrus/staking_inner.js'; import { StakingPool } from './contracts/walrus/staking_pool.js'; @@ -254,6 +255,14 @@ export class WalrusClient { }); } + async #getWalrusConfig(): Promise { + return { + walrusPackageId: await this.#getWalrusPackageId(), + systemObjectId: this.#packageConfig.systemObjectId, + stakingPoolId: this.#packageConfig.stakingPoolId, + }; + } + #wasmBindings() { return this.#cache.read(['wasmBindings'], async () => { return getWasmBindings(this.#wasmUrl); @@ -739,21 +748,19 @@ export class WalrusClient { */ createStorage({ size, epochs, walCoin }: StorageWithSizeOptions) { return async (tx: Transaction) => { - const systemObject = await this.systemObject(); const systemState = await this.systemState(); const encodedSize = encodedBlobLength(size, systemState.committee.n_shards); - const [{ storageCost }, walrusPackageId] = await Promise.all([ + const [{ storageCost }, walrusConfig] = await Promise.all([ this.storageCost(size, epochs), - this.#getWalrusPackageId(), + this.#getWalrusConfig(), ]); return tx.add( this.#withWal(storageCost, walCoin ?? null, (coin, tx) => { return tx.add( reserveSpace({ - package: walrusPackageId, + config: walrusConfig, arguments: { - self: systemObject.id, storageAmount: encodedSize, epochsAhead: epochs, payment: coin, @@ -884,15 +891,14 @@ export class WalrusClient { }: RegisterBlobOptions) { return async (tx: Transaction) => { const { writeCost } = await this.storageCost(size, epochs); - const walrusPackageId = await this.#getWalrusPackageId(); + const walrusConfig = await this.#getWalrusConfig(); return tx.add( this.#withWal(writeCost, walCoin ?? null, async (writeCoin, tx) => { const blob = tx.add( registerBlob({ - package: walrusPackageId, + config: walrusConfig, arguments: { - self: tx.object(this.#packageConfig.systemObjectId), storage: this.createStorage({ size, epochs, walCoin }), blobId: blobIdToInt(blobId), rootHash: BigInt(bcs.u256().parse(rootHash)), @@ -1218,13 +1224,12 @@ export class WalrusClient { blobObjectId, })); - const walrusPackageId = await this.#getWalrusPackageId(); + const walrusConfig = await this.#getWalrusConfig(); tx.add( certifyBlob({ - package: walrusPackageId, + config: walrusConfig, arguments: { - self: this.#packageConfig.systemObjectId, blob: blobObjectId, signature: tx.pure.vector('u8', combinedSignature.signature), signersBitmap: tx.pure.vector( @@ -1290,12 +1295,11 @@ export class WalrusClient { */ deleteBlob({ blobObjectId }: DeleteBlobOptions) { return async (tx: Transaction) => { - const walrusPackageId = await this.#getWalrusPackageId(); + const walrusConfig = await this.#getWalrusConfig(); const storage = tx.add( deleteBlob({ - package: walrusPackageId, + config: walrusConfig, arguments: { - self: this.#packageConfig.systemObjectId, blob: blobObjectId, }, }), @@ -1375,15 +1379,14 @@ export class WalrusClient { Number(blob.storage.storage_size), numEpochs, ); - const walrusPackageId = await this.#getWalrusPackageId(); + const walrusConfig = await this.#getWalrusConfig(); return tx.add( this.#withWal(storageCost, walCoin ?? null, async (coin, tx) => { tx.add( extendBlob({ - package: walrusPackageId, + config: walrusConfig, arguments: { - self: this.#packageConfig.systemObjectId, blob: blobObjectId, extendedEpochs: numEpochs, payment: coin, @@ -1463,16 +1466,16 @@ export class WalrusClient { blob: TransactionObjectArgument; }) { return async (tx: Transaction) => { - const walrusPackageId = await this.#getWalrusPackageId(); + const walrusConfig = await this.#getWalrusConfig(); if (!existingAttributes) { tx.add( addMetadata({ - package: walrusPackageId, + config: walrusConfig, arguments: { self: blob, metadata: metadata._new({ - package: walrusPackageId, + config: walrusConfig, }), }, }), @@ -1486,7 +1489,7 @@ export class WalrusClient { if (existingAttributes && key in existingAttributes) { tx.add( removeMetadataPair({ - package: walrusPackageId, + config: walrusConfig, arguments: { self: blob, key, @@ -1497,7 +1500,7 @@ export class WalrusClient { } else { tx.add( insertOrUpdateMetadataPair({ - package: walrusPackageId, + config: walrusConfig, arguments: { self: blob, key, diff --git a/packages/walrus/src/contracts/utils/index.ts b/packages/walrus/src/contracts/utils/index.ts index dcaecb04c..23a2fb7c1 100644 --- a/packages/walrus/src/contracts/utils/index.ts +++ b/packages/walrus/src/contracts/utils/index.ts @@ -8,7 +8,11 @@ import { BcsTuple, } from '@mysten/sui/bcs'; import { normalizeStructTag, normalizeSuiAddress } from '@mysten/sui/utils'; -import { type TransactionArgument, isArgument } from '@mysten/sui/transactions'; +import { + type TransactionArgument, + type TransactionObjectArgument, + isArgument, +} from '@mysten/sui/transactions'; import { type ClientWithCoreApi, type SuiClientTypes } from '@mysten/sui/client'; const MOVE_STDLIB_ADDRESS = normalizeSuiAddress('0x1'); @@ -106,6 +110,12 @@ export function normalizeMoveArguments( continue; } + if (argType === '0x2::accumulator::AccumulatorRoot') { + // Chain-wide shared singleton at a fixed address (SUI_ACCUMULATOR_ROOT_OBJECT_ID). + normalizedArgs.push((tx) => tx.object('0xacc')); + continue; + } + if (argType === '0x3::sui_system::SuiSystemState') { normalizedArgs.push((tx) => tx.object.system()); continue; @@ -124,7 +134,10 @@ export function normalizeMoveArguments( throw new Error(`Expected arguments to be passed as an array`); } const name = parameterNames[index]; - arg = args[name as keyof typeof args]; + arg = + name !== undefined && Object.prototype.hasOwnProperty.call(args, name) + ? args[name as keyof typeof args] + : undefined; if (arg === undefined) { throw new Error(`Parameter ${name} is required`); @@ -157,6 +170,129 @@ export function normalizeMoveArguments( return normalizedArgs; } +/* -------------------------- Config-mapped arguments -------------------------- */ + +/** Context passed to config resolver functions. */ +export interface ConfigResolverContext { + /** + * The matched parameter's own instantiated type arguments (not the whole function's type + * argument tuple), as fully-qualified type tags. Hex-addressed struct tags are normalized to + * their long form before the resolver is invoked; MVR-named tags are passed through unchanged. + */ + typeArguments: string[]; + /** The package address the generated call will be sent to. */ + packageAddress: string; + /** The Move module of the generated call. */ + moduleName: string; + /** The Move function of the generated call. */ + functionName: string; + /** The Move name of the matched parameter, when the summary includes parameter names. */ + parameterName?: string; + /** The matched parameter's position in the generated function's arguments. */ + parameterIndex: number; +} + +/** + * A value in a generated config object: a plain object id/argument, or a resolver function. + * + * Any function is treated as a resolver and called with a `ConfigResolverContext`. To provide a + * transaction-callback object argument dynamically, return it from a resolver: + * `(ctx) => (tx) => ...`. + */ +export type ConfigValue = + | string + | Exclude unknown> + | ((ctx: ConfigResolverContext) => string | TransactionObjectArgument); + +/** Normalize a hex-addressed struct tag to its long form; pass anything else through. */ +function normalizeConfigTypeTag(tag: string): string { + if (/[@/]/.test(tag) || !tag.includes('::')) { + return tag; + } + try { + return normalizeStructTag(tag); + } catch { + return tag; + } +} + +export function resolveConfigArgument( + value: ConfigValue | undefined, + ctx: ConfigResolverContext, + name: string, +): string | TransactionObjectArgument { + if (value == null) { + throw new Error( + `Missing config value for "${name}": pass it explicitly in arguments, or include it in the config object`, + ); + } + + if (typeof value !== 'function') { + return value; + } + + const resolved = value({ + ...ctx, + typeArguments: ctx.typeArguments.map(normalizeConfigTypeTag), + }); + + if (resolved == null) { + throw new Error( + `Config resolver for "${name}" returned ${resolved} (${ctx.moduleName}::${ctx.functionName}, typeArguments: [${ctx.typeArguments.join(', ')}])`, + ); + } + + return resolved; +} + +/** + * Fill unset config-mapped positions in an arguments array/object with their resolved config + * values. Resolvers are only invoked for positions the caller did not pass explicitly. + */ +export function applyConfigArguments( + args: T, + defaults: readonly { index: number; name?: string; resolve: () => unknown }[], +): T { + if (Array.isArray(args)) { + const result = [...args]; + const matchedIndexes = new Set(defaults.map((entry) => entry.index)); + for (const entry of defaults) { + if (result[entry.index] === undefined) { + result[entry.index] = entry.resolve(); + } + } + // Filling a trailing config-mapped position can extend the array past positions the + // caller omitted; catch those holes here rather than failing deep inside serialization. + for (let i = 0; i < result.length; i++) { + if (result[i] === undefined && !matchedIndexes.has(i)) { + throw new Error(`Missing argument at position ${i}`); + } + } + return result as T; + } + + const result: Record = { ...args }; + for (const entry of defaults) { + if (entry.name === undefined) { + continue; + } + // Own-property check so inherited properties (e.g. a key named "constructor") are never + // mistaken for explicitly passed arguments. + if ( + !Object.prototype.hasOwnProperty.call(result, entry.name) || + result[entry.name] === undefined + ) { + Object.defineProperty(result, entry.name, { + value: entry.resolve(), + enumerable: true, + writable: true, + configurable: true, + }); + } + } + return result as T; +} + /* -------------------------- Move type tags -------------------------- */ /** A type argument: a type tag string, or a BCS type whose name is a Move type. */ diff --git a/packages/walrus/src/contracts/walrus/apportionment_queue.ts b/packages/walrus/src/contracts/walrus/apportionment_queue.ts index 10afe2a48..28e712572 100644 --- a/packages/walrus/src/contracts/walrus/apportionment_queue.ts +++ b/packages/walrus/src/contracts/walrus/apportionment_queue.ts @@ -40,11 +40,14 @@ export function ApportionmentQueue>(...typeParameters: [T export interface NewOptions { package?: string; arguments?: []; + config?: { + walrusPackageId?: string; + }; typeArguments: [string]; } /** Create a new priority queue. */ export function _new(options: NewOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; return (tx: Transaction) => tx.moveCall({ package: packageAddress, @@ -59,11 +62,14 @@ export interface PopMaxArguments { export interface PopMaxOptions { package?: string; arguments: PopMaxArguments | [pq: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; typeArguments: [string]; } /** Pop the entry with the highest priority value. */ export function popMax(options: PopMaxOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['pq']; return (tx: Transaction) => @@ -91,11 +97,14 @@ export interface InsertOptions> { tieBreaker: RawTransactionArgument, value: RawTransactionArgument, ]; + config?: { + walrusPackageId?: string; + }; typeArguments: [string]; } /** Insert a new entry into the queue. */ export function insert>(options: InsertOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u64', `${options.typeArguments[0]}`] satisfies ( | string | null diff --git a/packages/walrus/src/contracts/walrus/auth.ts b/packages/walrus/src/contracts/walrus/auth.ts index 6743468ab..5ee132af3 100644 --- a/packages/walrus/src/contracts/walrus/auth.ts +++ b/packages/walrus/src/contracts/walrus/auth.ts @@ -30,10 +30,13 @@ export const Authorized = new MoveEnum({ export interface AuthenticateSenderOptions { package?: string; arguments?: []; + config?: { + walrusPackageId?: string; + }; } /** Authenticates the sender as the authorizer. */ export function authenticateSender(options: AuthenticateSenderOptions = {}) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; return (tx: Transaction) => tx.moveCall({ package: packageAddress, @@ -47,13 +50,16 @@ export interface AuthenticateWithObjectArguments> { export interface AuthenticateWithObjectOptions> { package?: string; arguments: AuthenticateWithObjectArguments | [obj: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; typeArguments: [string]; } /** Authenticates an object as the authorizer. */ export function authenticateWithObject>( options: AuthenticateWithObjectOptions, ) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [`${options.typeArguments[0]}`] satisfies (string | null)[]; const parameterNames = ['obj']; return (tx: Transaction) => @@ -71,10 +77,13 @@ export interface AuthorizedAddressArguments { export interface AuthorizedAddressOptions { package?: string; arguments: AuthorizedAddressArguments | [addr: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Returns the `Authorized` as an address. */ export function authorizedAddress(options: AuthorizedAddressOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = ['address'] satisfies (string | null)[]; const parameterNames = ['addr']; return (tx: Transaction) => @@ -91,10 +100,13 @@ export interface AuthorizedObjectArguments { export interface AuthorizedObjectOptions { package?: string; arguments: AuthorizedObjectArguments | [id: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Returns the `Authorized` as an object. */ export function authorizedObject(options: AuthorizedObjectOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = ['0x2::object::ID'] satisfies (string | null)[]; const parameterNames = ['id']; return (tx: Transaction) => diff --git a/packages/walrus/src/contracts/walrus/blob.ts b/packages/walrus/src/contracts/walrus/blob.ts index 7107959c1..7595692d6 100644 --- a/packages/walrus/src/contracts/walrus/blob.ts +++ b/packages/walrus/src/contracts/walrus/blob.ts @@ -33,9 +33,12 @@ export interface ObjectIdArguments { export interface ObjectIdOptions { package?: string; arguments: ObjectIdArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function objectId(options: ObjectIdOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -52,9 +55,12 @@ export interface RegisteredEpochArguments { export interface RegisteredEpochOptions { package?: string; arguments: RegisteredEpochArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function registeredEpoch(options: RegisteredEpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -71,9 +77,12 @@ export interface BlobIdArguments { export interface BlobIdOptions { package?: string; arguments: BlobIdArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function blobId(options: BlobIdOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -90,9 +99,12 @@ export interface SizeArguments { export interface SizeOptions { package?: string; arguments: SizeArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function size(options: SizeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -109,9 +121,12 @@ export interface EncodingTypeArguments { export interface EncodingTypeOptions { package?: string; arguments: EncodingTypeArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function encodingType(options: EncodingTypeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -128,9 +143,12 @@ export interface CertifiedEpochArguments { export interface CertifiedEpochOptions { package?: string; arguments: CertifiedEpochArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function certifiedEpoch(options: CertifiedEpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -147,9 +165,12 @@ export interface StorageArguments { export interface StorageOptions { package?: string; arguments: StorageArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function storage(options: StorageOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -166,9 +187,12 @@ export interface IsDeletableArguments { export interface IsDeletableOptions { package?: string; arguments: IsDeletableArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function isDeletable(options: IsDeletableOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -188,9 +212,12 @@ export interface EncodedSizeOptions { arguments: | EncodedSizeArguments | [self: RawTransactionArgument, nShards: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function encodedSize(options: EncodedSizeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u16'] satisfies (string | null)[]; const parameterNames = ['self', 'nShards']; return (tx: Transaction) => @@ -207,9 +234,12 @@ export interface EndEpochArguments { export interface EndEpochOptions { package?: string; arguments: EndEpochArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function endEpoch(options: EndEpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -234,10 +264,13 @@ export interface DeriveBlobIdOptions { encodingType: RawTransactionArgument, size: RawTransactionArgument, ]; + config?: { + walrusPackageId?: string; + }; } /** Derives the blob_id for a blob given the root_hash, encoding_type and size. */ export function deriveBlobId(options: DeriveBlobIdOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = ['u256', 'u8', 'u64'] satisfies (string | null)[]; const parameterNames = ['rootHash', 'encodingType', 'size']; return (tx: Transaction) => @@ -254,6 +287,9 @@ export interface BurnArguments { export interface BurnOptions { package?: string; arguments: BurnArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Allow the owner of a blob object to destroy it. @@ -261,7 +297,7 @@ export interface BurnOptions { * This function also burns any [`Metadata`] associated with the blob, if present. */ export function burn(options: BurnOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -281,6 +317,9 @@ export interface AddMetadataOptions { arguments: | AddMetadataArguments | [self: RawTransactionArgument, metadata: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Adds the metadata dynamic field to the Blob. @@ -288,7 +327,7 @@ export interface AddMetadataOptions { * Aborts if the metadata is already present. */ export function addMetadata(options: AddMetadataOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['self', 'metadata']; return (tx: Transaction) => @@ -308,6 +347,9 @@ export interface AddOrReplaceMetadataOptions { arguments: | AddOrReplaceMetadataArguments | [self: RawTransactionArgument, metadata: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Adds the metadata dynamic field to the Blob, replacing the existing metadata if @@ -316,7 +358,7 @@ export interface AddOrReplaceMetadataOptions { * Returns the replaced metadata if present. */ export function addOrReplaceMetadata(options: AddOrReplaceMetadataOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['self', 'metadata']; return (tx: Transaction) => @@ -333,6 +375,9 @@ export interface TakeMetadataArguments { export interface TakeMetadataOptions { package?: string; arguments: TakeMetadataArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Removes the metadata dynamic field from the Blob, returning the contained @@ -341,7 +386,7 @@ export interface TakeMetadataOptions { * Aborts if the metadata does not exist. */ export function takeMetadata(options: TakeMetadataOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -366,6 +411,9 @@ export interface InsertOrUpdateMetadataPairOptions { key: RawTransactionArgument, value: RawTransactionArgument, ]; + config?: { + walrusPackageId?: string; + }; } /** * Inserts a key-value pair into the metadata. @@ -374,7 +422,7 @@ export interface InsertOrUpdateMetadataPairOptions { * Blob object if it does not exist already. */ export function insertOrUpdateMetadataPair(options: InsertOrUpdateMetadataPairOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x1::string::String', '0x1::string::String'] satisfies ( | string | null @@ -397,6 +445,9 @@ export interface RemoveMetadataPairOptions { arguments: | RemoveMetadataPairArguments | [self: RawTransactionArgument, key: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Removes the metadata associated with the given key. @@ -404,7 +455,7 @@ export interface RemoveMetadataPairOptions { * Aborts if the metadata does not exist. */ export function removeMetadataPair(options: RemoveMetadataPairOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x1::string::String'] satisfies (string | null)[]; const parameterNames = ['self', 'key']; return (tx: Transaction) => @@ -424,10 +475,13 @@ export interface RemoveMetadataPairIfExistsOptions { arguments: | RemoveMetadataPairIfExistsArguments | [self: RawTransactionArgument, key: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Removes and returns the metadata associated with the given key, if it exists. */ export function removeMetadataPairIfExists(options: RemoveMetadataPairIfExistsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x1::string::String'] satisfies (string | null)[]; const parameterNames = ['self', 'key']; return (tx: Transaction) => diff --git a/packages/walrus/src/contracts/walrus/committee.ts b/packages/walrus/src/contracts/walrus/committee.ts index 4718bd442..0af900f28 100644 --- a/packages/walrus/src/contracts/walrus/committee.ts +++ b/packages/walrus/src/contracts/walrus/committee.ts @@ -24,10 +24,13 @@ export interface ShardsArguments { export interface ShardsOptions { package?: string; arguments: ShardsArguments | [cmt: TransactionArgument, nodeId: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Get the shards assigned to the given `node_id`. */ export function shards(options: ShardsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x2::object::ID'] satisfies (string | null)[]; const parameterNames = ['cmt', 'nodeId']; return (tx: Transaction) => @@ -44,10 +47,13 @@ export interface SizeArguments { export interface SizeOptions { package?: string; arguments: SizeArguments | [cmt: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Get the number of nodes in the committee. */ export function size(options: SizeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['cmt']; return (tx: Transaction) => @@ -64,10 +70,13 @@ export interface InnerArguments { export interface InnerOptions { package?: string; arguments: InnerArguments | [cmt: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Get the inner representation of the committee. */ export function inner(options: InnerOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['cmt']; return (tx: Transaction) => @@ -84,10 +93,13 @@ export interface ToInnerArguments { export interface ToInnerOptions { package?: string; arguments: ToInnerArguments | [cmt: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Copy the inner representation of the committee. */ export function toInner(options: ToInnerOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['cmt']; return (tx: Transaction) => diff --git a/packages/walrus/src/contracts/walrus/config-arguments.ts b/packages/walrus/src/contracts/walrus/config-arguments.ts new file mode 100644 index 000000000..6a8bb984f --- /dev/null +++ b/packages/walrus/src/contracts/walrus/config-arguments.ts @@ -0,0 +1,9 @@ +/************************************************************** + * THIS FILE IS GENERATED AND SHOULD NOT BE MANUALLY MODIFIED * + **************************************************************/ +import { type ConfigValue } from '../utils/index.js'; +export interface WalrusConfig { + walrusPackageId?: string; + systemObjectId: ConfigValue; + stakingPoolId: ConfigValue; +} diff --git a/packages/walrus/src/contracts/walrus/encoding.ts b/packages/walrus/src/contracts/walrus/encoding.ts index a037f6fbe..c60b48bcf 100644 --- a/packages/walrus/src/contracts/walrus/encoding.ts +++ b/packages/walrus/src/contracts/walrus/encoding.ts @@ -17,13 +17,16 @@ export interface EncodedBlobLengthOptions { encodingType: RawTransactionArgument, nShards: RawTransactionArgument, ]; + config?: { + walrusPackageId?: string; + }; } /** * Computes the encoded length of a blob given its unencoded length, encoding type * and number of shards `n_shards`. */ export function encodedBlobLength(options: EncodedBlobLengthOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = ['u64', 'u8', 'u16'] satisfies (string | null)[]; const parameterNames = ['unencodedLength', 'encodingType', 'nShards']; return (tx: Transaction) => diff --git a/packages/walrus/src/contracts/walrus/extended_field.ts b/packages/walrus/src/contracts/walrus/extended_field.ts index e5d9a2668..fc6c52a17 100644 --- a/packages/walrus/src/contracts/walrus/extended_field.ts +++ b/packages/walrus/src/contracts/walrus/extended_field.ts @@ -26,11 +26,14 @@ export interface NewArguments> { export interface NewOptions> { package?: string; arguments: NewArguments | [value: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; typeArguments: [string]; } /** Creates a new extended field with the given value. */ export function _new>(options: NewOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [`${options.typeArguments[0]}`] satisfies (string | null)[]; const parameterNames = ['value']; return (tx: Transaction) => @@ -48,11 +51,14 @@ export interface BorrowArguments { export interface BorrowOptions { package?: string; arguments: BorrowArguments | [field: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; typeArguments: [string]; } /** Borrows the value stored in the extended field. */ export function borrow(options: BorrowOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['field']; return (tx: Transaction) => @@ -70,11 +76,14 @@ export interface BorrowMutArguments { export interface BorrowMutOptions { package?: string; arguments: BorrowMutArguments | [field: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; typeArguments: [string]; } /** Borrows the value stored in the extended field mutably. */ export function borrowMut(options: BorrowMutOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['field']; return (tx: Transaction) => @@ -95,11 +104,14 @@ export interface SwapOptions> { arguments: | SwapArguments | [field: RawTransactionArgument, value: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; typeArguments: [string]; } /** Swaps the value stored in the extended field with the given value. */ export function swap>(options: SwapOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, `${options.typeArguments[0]}`] satisfies (string | null)[]; const parameterNames = ['field', 'value']; return (tx: Transaction) => @@ -117,11 +129,14 @@ export interface DestroyArguments { export interface DestroyOptions { package?: string; arguments: DestroyArguments | [field: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; typeArguments: [string]; } /** Destroys the extended field and returns the value stored in it. */ export function destroy(options: DestroyOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['field']; return (tx: Transaction) => diff --git a/packages/walrus/src/contracts/walrus/init.ts b/packages/walrus/src/contracts/walrus/init.ts index 947a3aa3e..aed89e4c9 100644 --- a/packages/walrus/src/contracts/walrus/init.ts +++ b/packages/walrus/src/contracts/walrus/init.ts @@ -1,7 +1,14 @@ /************************************************************** * THIS FILE IS GENERATED AND SHOULD NOT BE MANUALLY MODIFIED * **************************************************************/ -import { MoveStruct, normalizeMoveArguments, type RawTransactionArgument } from '../utils/index.js'; +import { + MoveStruct, + normalizeMoveArguments, + type RawTransactionArgument, + type ConfigValue, + resolveConfigArgument, + applyConfigArguments, +} from '../utils/index.js'; import { bcs } from '@mysten/sui/bcs'; import { type Transaction } from '@mysten/sui/transactions'; import * as _package from './deps/sui/package.js'; @@ -39,6 +46,9 @@ export interface InitializeWalrusOptions { nShards: RawTransactionArgument, maxEpochsAhead: RawTransactionArgument, ]; + config?: { + walrusPackageId?: string; + }; } /** * Initializes Walrus and shares the system and staking objects. @@ -46,7 +56,7 @@ export interface InitializeWalrusOptions { * This can only be called once, after which the `InitCap` is destroyed. */ export function initializeWalrus(options: InitializeWalrusOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u64', 'u64', 'u16', 'u32', '0x2::clock::Clock'] satisfies ( | string | null @@ -68,18 +78,23 @@ export function initializeWalrus(options: InitializeWalrusOptions) { }); } export interface MigrateArguments { - Staking: RawTransactionArgument; - System: RawTransactionArgument; + Staking?: RawTransactionArgument; + System?: RawTransactionArgument; } export interface MigrateOptions { package?: string; - arguments: + arguments?: | MigrateArguments - | [Staking: RawTransactionArgument, System: RawTransactionArgument]; + | [Staking?: RawTransactionArgument, System?: RawTransactionArgument]; + config?: { + stakingPoolId: ConfigValue; + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Deprecated old migration function. */ export function migrate(options: MigrateOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['Staking', 'System']; return (tx: Transaction) => @@ -87,18 +102,62 @@ export function migrate(options: MigrateOptions) { package: packageAddress, module: 'init', function: 'migrate', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'Staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'init', + functionName: 'migrate', + parameterIndex: 0, + parameterName: '_staking', + }, + 'stakingPoolId', + ), + }, + { + index: 1, + name: 'System', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'init', + functionName: 'migrate', + parameterIndex: 1, + parameterName: '_system', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface MigrateV2Arguments { - staking: RawTransactionArgument; - system: RawTransactionArgument; + staking?: RawTransactionArgument; + system?: RawTransactionArgument; } export interface MigrateV2Options { package?: string; - arguments: + arguments?: | MigrateV2Arguments - | [staking: RawTransactionArgument, system: RawTransactionArgument]; + | [staking?: RawTransactionArgument, system?: RawTransactionArgument]; + config?: { + stakingPoolId: ConfigValue; + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Migrates the staking and system objects to the new package ID. @@ -116,7 +175,7 @@ export interface MigrateV2Options { * - No additional steps beyond version bump. */ export function migrateV2(options: MigrateV2Options) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['staking', 'system']; return (tx: Transaction) => @@ -124,6 +183,45 @@ export function migrateV2(options: MigrateV2Options) { package: packageAddress, module: 'init', function: 'migrate_v2', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'init', + functionName: 'migrate_v2', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + { + index: 1, + name: 'system', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'init', + functionName: 'migrate_v2', + parameterIndex: 1, + parameterName: 'system', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } diff --git a/packages/walrus/src/contracts/walrus/metadata.ts b/packages/walrus/src/contracts/walrus/metadata.ts index 9d79734f8..306543783 100644 --- a/packages/walrus/src/contracts/walrus/metadata.ts +++ b/packages/walrus/src/contracts/walrus/metadata.ts @@ -18,10 +18,13 @@ export const Metadata = new MoveStruct({ export interface NewOptions { package?: string; arguments?: []; + config?: { + walrusPackageId?: string; + }; } /** Creates a new instance of Metadata. */ export function _new(options: NewOptions = {}) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; return (tx: Transaction) => tx.moveCall({ package: packageAddress, @@ -43,6 +46,9 @@ export interface InsertOrUpdateOptions { key: RawTransactionArgument, value: RawTransactionArgument, ]; + config?: { + walrusPackageId?: string; + }; } /** * Inserts a key-value pair into the metadata. @@ -50,7 +56,7 @@ export interface InsertOrUpdateOptions { * If the key is already present, the value is updated. */ export function insertOrUpdate(options: InsertOrUpdateOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x1::string::String', '0x1::string::String'] satisfies ( | string | null @@ -71,10 +77,13 @@ export interface RemoveArguments { export interface RemoveOptions { package?: string; arguments: RemoveArguments | [self: TransactionArgument, key: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Removes the metadata associated with the given key. */ export function remove(options: RemoveOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x1::string::String'] satisfies (string | null)[]; const parameterNames = ['self', 'key']; return (tx: Transaction) => @@ -94,6 +103,9 @@ export interface RemoveIfExistsOptions { arguments: | RemoveIfExistsArguments | [self: TransactionArgument, key: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Removes the metadata associated with the given key, if it exists. @@ -101,7 +113,7 @@ export interface RemoveIfExistsOptions { * Optionally returns the previous value associated with the key. */ export function removeIfExists(options: RemoveIfExistsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x1::string::String'] satisfies (string | null)[]; const parameterNames = ['self', 'key']; return (tx: Transaction) => diff --git a/packages/walrus/src/contracts/walrus/node_metadata.ts b/packages/walrus/src/contracts/walrus/node_metadata.ts index af8333fca..5b112f107 100644 --- a/packages/walrus/src/contracts/walrus/node_metadata.ts +++ b/packages/walrus/src/contracts/walrus/node_metadata.ts @@ -32,10 +32,13 @@ export interface NewOptions { projectUrl: RawTransactionArgument, description: RawTransactionArgument, ]; + config?: { + walrusPackageId?: string; + }; } /** Create a new `NodeMetadata` instance */ export function _new(options: NewOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [ '0x1::string::String', '0x1::string::String', @@ -59,10 +62,13 @@ export interface SetImageUrlOptions { arguments: | SetImageUrlArguments | [metadata: TransactionArgument, imageUrl: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Set the image URL of the Validator. */ export function setImageUrl(options: SetImageUrlOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x1::string::String'] satisfies (string | null)[]; const parameterNames = ['metadata', 'imageUrl']; return (tx: Transaction) => @@ -82,10 +88,13 @@ export interface SetProjectUrlOptions { arguments: | SetProjectUrlArguments | [metadata: TransactionArgument, projectUrl: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Set the project URL of the Validator. */ export function setProjectUrl(options: SetProjectUrlOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x1::string::String'] satisfies (string | null)[]; const parameterNames = ['metadata', 'projectUrl']; return (tx: Transaction) => @@ -105,10 +114,13 @@ export interface SetDescriptionOptions { arguments: | SetDescriptionArguments | [metadata: TransactionArgument, description: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Set the description of the Validator. */ export function setDescription(options: SetDescriptionOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x1::string::String'] satisfies (string | null)[]; const parameterNames = ['metadata', 'description']; return (tx: Transaction) => @@ -128,10 +140,13 @@ export interface SetExtraFieldsOptions { arguments: | SetExtraFieldsArguments | [metadata: TransactionArgument, extraFields: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Set an extra field of the Validator. */ export function setExtraFields(options: SetExtraFieldsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['metadata', 'extraFields']; return (tx: Transaction) => @@ -148,10 +163,13 @@ export interface ImageUrlArguments { export interface ImageUrlOptions { package?: string; arguments: ImageUrlArguments | [metadata: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Returns the image URL of the Validator. */ export function imageUrl(options: ImageUrlOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['metadata']; return (tx: Transaction) => @@ -168,10 +186,13 @@ export interface ProjectUrlArguments { export interface ProjectUrlOptions { package?: string; arguments: ProjectUrlArguments | [metadata: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Returns the project URL of the Validator. */ export function projectUrl(options: ProjectUrlOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['metadata']; return (tx: Transaction) => @@ -188,10 +209,13 @@ export interface DescriptionArguments { export interface DescriptionOptions { package?: string; arguments: DescriptionArguments | [metadata: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Returns the description of the Validator. */ export function description(options: DescriptionOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['metadata']; return (tx: Transaction) => @@ -208,10 +232,13 @@ export interface ExtraFieldsArguments { export interface ExtraFieldsOptions { package?: string; arguments: ExtraFieldsArguments | [metadata: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Returns the extra fields of the Validator. */ export function extraFields(options: ExtraFieldsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['metadata']; return (tx: Transaction) => diff --git a/packages/walrus/src/contracts/walrus/shared_blob.ts b/packages/walrus/src/contracts/walrus/shared_blob.ts index 85efc8d90..0a4543acd 100644 --- a/packages/walrus/src/contracts/walrus/shared_blob.ts +++ b/packages/walrus/src/contracts/walrus/shared_blob.ts @@ -1,7 +1,14 @@ /************************************************************** * THIS FILE IS GENERATED AND SHOULD NOT BE MANUALLY MODIFIED * **************************************************************/ -import { MoveStruct, normalizeMoveArguments, type RawTransactionArgument } from '../utils/index.js'; +import { + MoveStruct, + normalizeMoveArguments, + type RawTransactionArgument, + type ConfigValue, + resolveConfigArgument, + applyConfigArguments, +} from '../utils/index.js'; import { bcs } from '@mysten/sui/bcs'; import { type Transaction } from '@mysten/sui/transactions'; import * as blob_1 from './blob.js'; @@ -21,10 +28,13 @@ export interface NewArguments { export interface NewOptions { package?: string; arguments: NewArguments | [blob: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Shares the provided `blob` as a `SharedBlob` with zero funds. */ export function _new(options: NewOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['blob']; return (tx: Transaction) => @@ -44,10 +54,13 @@ export interface NewFundedOptions { arguments: | NewFundedArguments | [blob: RawTransactionArgument, funds: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Shares the provided `blob` as a `SharedBlob` with funds. */ export function newFunded(options: NewFundedOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['blob', 'funds']; return (tx: Transaction) => @@ -67,10 +80,13 @@ export interface FundOptions { arguments: | FundArguments | [self: RawTransactionArgument, addedFunds: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Adds the provided `Coin` to the stored funds. */ export function fund(options: FundOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['self', 'addedFunds']; return (tx: Transaction) => @@ -83,7 +99,7 @@ export function fund(options: FundOptions) { } export interface ExtendArguments { self: RawTransactionArgument; - system: RawTransactionArgument; + system?: RawTransactionArgument; extendedEpochs: RawTransactionArgument; } export interface ExtendOptions { @@ -92,9 +108,13 @@ export interface ExtendOptions { | ExtendArguments | [ self: RawTransactionArgument, - system: RawTransactionArgument, + system: RawTransactionArgument | undefined, extendedEpochs: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Extends the lifetime of the wrapped `Blob` by `extended_epochs` epochs if the @@ -102,7 +122,7 @@ export interface ExtendOptions { * lifetime. */ export function extend(options: ExtendOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u32'] satisfies (string | null)[]; const parameterNames = ['self', 'system', 'extendedEpochs']; return (tx: Transaction) => @@ -110,7 +130,29 @@ export function extend(options: ExtendOptions) { package: packageAddress, module: 'shared_blob', function: 'extend', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 1, + name: 'system', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'shared_blob', + functionName: 'extend', + parameterIndex: 1, + parameterName: 'system', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface BlobArguments { @@ -119,10 +161,13 @@ export interface BlobArguments { export interface BlobOptions { package?: string; arguments: BlobArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Returns a reference to the wrapped `Blob`. */ export function blob(options: BlobOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -139,10 +184,13 @@ export interface FundsArguments { export interface FundsOptions { package?: string; arguments: FundsArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Returns the balance of funds stored in the `SharedBlob`. */ export function funds(options: FundsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => diff --git a/packages/walrus/src/contracts/walrus/slashing.ts b/packages/walrus/src/contracts/walrus/slashing.ts index b6c3547cd..2267ebe9d 100644 --- a/packages/walrus/src/contracts/walrus/slashing.ts +++ b/packages/walrus/src/contracts/walrus/slashing.ts @@ -13,7 +13,14 @@ * the new epoch and prior votes are cleared. */ -import { MoveStruct, normalizeMoveArguments, type RawTransactionArgument } from '../utils/index.js'; +import { + MoveStruct, + normalizeMoveArguments, + type RawTransactionArgument, + type ConfigValue, + resolveConfigArgument, + applyConfigArguments, +} from '../utils/index.js'; import { bcs } from '@mysten/sui/bcs'; import { type Transaction, type TransactionArgument } from '@mysten/sui/transactions'; import * as vec_set from './deps/sui/vec_set.js'; @@ -47,7 +54,7 @@ export const SlashingManager = new MoveStruct({ }); export interface VoteForSlashingArguments { self: RawTransactionArgument; - staking: RawTransactionArgument; + staking?: RawTransactionArgument; auth: TransactionArgument; voterNodeId: RawTransactionArgument; candidateNodeId: RawTransactionArgument; @@ -58,11 +65,15 @@ export interface VoteForSlashingOptions { | VoteForSlashingArguments | [ self: RawTransactionArgument, - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, auth: TransactionArgument, voterNodeId: RawTransactionArgument, candidateNodeId: RawTransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Vote for slashing a node given its node ID. @@ -72,7 +83,7 @@ export interface VoteForSlashingOptions { * cleared and the epoch is updated). */ export function voteForSlashing(options: VoteForSlashingOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, null, '0x2::object::ID', '0x2::object::ID'] satisfies ( | string | null @@ -83,12 +94,34 @@ export function voteForSlashing(options: VoteForSlashingOptions) { package: packageAddress, module: 'slashing', function: 'vote_for_slashing', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 1, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'slashing', + functionName: 'vote_for_slashing', + parameterIndex: 1, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface ExecuteSlashingArguments { self: RawTransactionArgument; - staking: RawTransactionArgument; + staking?: RawTransactionArgument; treasury: RawTransactionArgument; candidateNodeId: RawTransactionArgument; } @@ -98,10 +131,14 @@ export interface ExecuteSlashingOptions { | ExecuteSlashingArguments | [ self: RawTransactionArgument, - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, treasury: RawTransactionArgument, candidateNodeId: RawTransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Execute slashing for a node whose proposal has reached quorum. @@ -110,7 +147,7 @@ export interface ExecuteSlashingOptions { * must be from the current epoch and have reached quorum. */ export function executeSlashing(options: ExecuteSlashingOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, null, '0x2::object::ID'] satisfies (string | null)[]; const parameterNames = ['self', 'staking', 'treasury', 'candidateNodeId']; return (tx: Transaction) => @@ -118,12 +155,34 @@ export function executeSlashing(options: ExecuteSlashingOptions) { package: packageAddress, module: 'slashing', function: 'execute_slashing', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 1, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'slashing', + functionName: 'execute_slashing', + parameterIndex: 1, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface CleanupSlashingProposalsArguments { self: RawTransactionArgument; - staking: RawTransactionArgument; + staking?: RawTransactionArgument; nodeIds: RawTransactionArgument>; } export interface CleanupSlashingProposalsOptions { @@ -132,9 +191,13 @@ export interface CleanupSlashingProposalsOptions { | CleanupSlashingProposalsArguments | [ self: RawTransactionArgument, - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, nodeIds: RawTransactionArgument>, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Remove any slashing proposals whose epoch is in the past. @@ -142,7 +205,7 @@ export interface CleanupSlashingProposalsOptions { * This is a permissionless cleanup function that anyone can call. */ export function cleanupSlashingProposals(options: CleanupSlashingProposalsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'vector<0x2::object::ID>'] satisfies (string | null)[]; const parameterNames = ['self', 'staking', 'nodeIds']; return (tx: Transaction) => @@ -150,6 +213,28 @@ export function cleanupSlashingProposals(options: CleanupSlashingProposalsOption package: packageAddress, module: 'slashing', function: 'cleanup_slashing_proposals', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 1, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'slashing', + functionName: 'cleanup_slashing_proposals', + parameterIndex: 1, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } diff --git a/packages/walrus/src/contracts/walrus/staked_wal.ts b/packages/walrus/src/contracts/walrus/staked_wal.ts index 07131629f..470a64359 100644 --- a/packages/walrus/src/contracts/walrus/staked_wal.ts +++ b/packages/walrus/src/contracts/walrus/staked_wal.ts @@ -57,10 +57,13 @@ export interface NodeIdArguments { export interface NodeIdOptions { package?: string; arguments: NodeIdArguments | [sw: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Returns the `node_id` of the staked WAL. */ export function nodeId(options: NodeIdOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['sw']; return (tx: Transaction) => @@ -77,13 +80,16 @@ export interface ValueArguments { export interface ValueOptions { package?: string; arguments: ValueArguments | [sw: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Returns the `principal` of the staked WAL. Called `value` to be consistent with * `Coin`. */ export function value(options: ValueOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['sw']; return (tx: Transaction) => @@ -100,10 +106,13 @@ export interface ActivationEpochArguments { export interface ActivationEpochOptions { package?: string; arguments: ActivationEpochArguments | [sw: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Returns the `activation_epoch` of the staked WAL. */ export function activationEpoch(options: ActivationEpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['sw']; return (tx: Transaction) => @@ -120,10 +129,13 @@ export interface IsStakedArguments { export interface IsStakedOptions { package?: string; arguments: IsStakedArguments | [sw: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Returns true if the staked WAL is in the `Staked` state. */ export function isStaked(options: IsStakedOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['sw']; return (tx: Transaction) => @@ -140,10 +152,13 @@ export interface IsWithdrawingArguments { export interface IsWithdrawingOptions { package?: string; arguments: IsWithdrawingArguments | [sw: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Checks whether the staked WAL is in the `Withdrawing` state. */ export function isWithdrawing(options: IsWithdrawingOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['sw']; return (tx: Transaction) => @@ -160,13 +175,16 @@ export interface WithdrawEpochArguments { export interface WithdrawEpochOptions { package?: string; arguments: WithdrawEpochArguments | [sw: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Returns the `withdraw_epoch` of the staked WAL if it is in the `Withdrawing`. * Aborts otherwise. */ export function withdrawEpoch(options: WithdrawEpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['sw']; return (tx: Transaction) => @@ -186,6 +204,9 @@ export interface JoinOptions { arguments: | JoinArguments | [sw: RawTransactionArgument, other: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Joins the staked WAL with another staked WAL, adding the `principal` of the @@ -194,7 +215,7 @@ export interface JoinOptions { * Aborts if the `node_id` or `activation_epoch` of the staked WALs do not match. */ export function join(options: JoinOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['sw', 'other']; return (tx: Transaction) => @@ -214,6 +235,9 @@ export interface SplitOptions { arguments: | SplitArguments | [sw: RawTransactionArgument, amount: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Splits the staked WAL into two parts, one with the `amount` and the other with @@ -224,7 +248,7 @@ export interface SplitOptions { * if the `amount` is zero. */ export function split(options: SplitOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u64'] satisfies (string | null)[]; const parameterNames = ['sw', 'amount']; return (tx: Transaction) => diff --git a/packages/walrus/src/contracts/walrus/staking.ts b/packages/walrus/src/contracts/walrus/staking.ts index 846fce59f..0e67e6398 100644 --- a/packages/walrus/src/contracts/walrus/staking.ts +++ b/packages/walrus/src/contracts/walrus/staking.ts @@ -4,7 +4,14 @@ /** Module: staking */ -import { MoveStruct, normalizeMoveArguments, type RawTransactionArgument } from '../utils/index.js'; +import { + MoveStruct, + normalizeMoveArguments, + type RawTransactionArgument, + type ConfigValue, + resolveConfigArgument, + applyConfigArguments, +} from '../utils/index.js'; import { bcs } from '@mysten/sui/bcs'; import { type Transaction, type TransactionArgument } from '@mysten/sui/transactions'; const $moduleName = '@local-pkg/walrus::staking'; @@ -18,7 +25,7 @@ export const Staking = new MoveStruct({ }, }); export interface RegisterCandidateArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; name: RawTransactionArgument; networkAddress: RawTransactionArgument; metadata: TransactionArgument; @@ -35,7 +42,7 @@ export interface RegisterCandidateOptions { arguments: | RegisterCandidateArguments | [ - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, name: RawTransactionArgument, networkAddress: RawTransactionArgument, metadata: TransactionArgument, @@ -47,13 +54,17 @@ export interface RegisterCandidateOptions { writePrice: RawTransactionArgument, nodeCapacity: RawTransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Creates a staking pool for the candidate, registers the candidate as a storage * node. */ export function registerCandidate(options: RegisterCandidateOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [ null, '0x1::string::String', @@ -85,11 +96,33 @@ export function registerCandidate(options: RegisterCandidateOptions) { package: packageAddress, module: 'staking', function: 'register_candidate', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'register_candidate', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface SetNextCommissionArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; cap: RawTransactionArgument; commissionRate: RawTransactionArgument; } @@ -98,10 +131,14 @@ export interface SetNextCommissionOptions { arguments: | SetNextCommissionArguments | [ - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, cap: RawTransactionArgument, commissionRate: RawTransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Sets next_commission in the staking pool, which will then take effect as @@ -109,7 +146,7 @@ export interface SetNextCommissionOptions { * setting this). */ export function setNextCommission(options: SetNextCommissionOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u16'] satisfies (string | null)[]; const parameterNames = ['staking', 'cap', 'commissionRate']; return (tx: Transaction) => @@ -117,11 +154,33 @@ export function setNextCommission(options: SetNextCommissionOptions) { package: packageAddress, module: 'staking', function: 'set_next_commission', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'set_next_commission', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface CollectCommissionArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; nodeId: RawTransactionArgument; auth: TransactionArgument; } @@ -130,17 +189,21 @@ export interface CollectCommissionOptions { arguments: | CollectCommissionArguments | [ - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, nodeId: RawTransactionArgument, auth: TransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Collects the commission for the node. Transaction sender must be the * `CommissionReceiver` for the `StakingPool`. */ export function collectCommission(options: CollectCommissionOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x2::object::ID', null] satisfies (string | null)[]; const parameterNames = ['staking', 'nodeId', 'auth']; return (tx: Transaction) => @@ -148,11 +211,33 @@ export function collectCommission(options: CollectCommissionOptions) { package: packageAddress, module: 'staking', function: 'collect_commission', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'collect_commission', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface SetCommissionReceiverArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; nodeId: RawTransactionArgument; auth: TransactionArgument; receiver: TransactionArgument; @@ -162,15 +247,19 @@ export interface SetCommissionReceiverOptions { arguments: | SetCommissionReceiverArguments | [ - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, nodeId: RawTransactionArgument, auth: TransactionArgument, receiver: TransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Sets the commission receiver for the node. */ export function setCommissionReceiver(options: SetCommissionReceiverOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x2::object::ID', null, null] satisfies (string | null)[]; const parameterNames = ['staking', 'nodeId', 'auth', 'receiver']; return (tx: Transaction) => @@ -178,11 +267,33 @@ export function setCommissionReceiver(options: SetCommissionReceiverOptions) { package: packageAddress, module: 'staking', function: 'set_commission_receiver', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'set_commission_receiver', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface SetGovernanceAuthorizedArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; nodeId: RawTransactionArgument; auth: TransactionArgument; authorized: TransactionArgument; @@ -192,15 +303,19 @@ export interface SetGovernanceAuthorizedOptions { arguments: | SetGovernanceAuthorizedArguments | [ - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, nodeId: RawTransactionArgument, auth: TransactionArgument, authorized: TransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Sets the governance authorized object for the pool. */ export function setGovernanceAuthorized(options: SetGovernanceAuthorizedOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x2::object::ID', null, null] satisfies (string | null)[]; const parameterNames = ['staking', 'nodeId', 'auth', 'authorized']; return (tx: Transaction) => @@ -208,19 +323,45 @@ export function setGovernanceAuthorized(options: SetGovernanceAuthorizedOptions) package: packageAddress, module: 'staking', function: 'set_governance_authorized', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'set_governance_authorized', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface CommitteeArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; } export interface CommitteeOptions { package?: string; - arguments: CommitteeArguments | [staking: RawTransactionArgument]; + arguments?: CommitteeArguments | [staking?: RawTransactionArgument]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Get the current committee. */ export function committee(options: CommitteeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['staking']; return (tx: Transaction) => @@ -228,19 +369,45 @@ export function committee(options: CommitteeOptions) { package: packageAddress, module: 'staking', function: 'committee', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'committee', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface PreviousCommitteeArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; } export interface PreviousCommitteeOptions { package?: string; - arguments: PreviousCommitteeArguments | [staking: RawTransactionArgument]; + arguments?: PreviousCommitteeArguments | [staking?: RawTransactionArgument]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Get the previous committee. */ export function previousCommittee(options: PreviousCommitteeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['staking']; return (tx: Transaction) => @@ -248,19 +415,45 @@ export function previousCommittee(options: PreviousCommitteeOptions) { package: packageAddress, module: 'staking', function: 'previous_committee', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'previous_committee', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface ComputeNextCommitteeArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; } export interface ComputeNextCommitteeOptions { package?: string; - arguments: ComputeNextCommitteeArguments | [staking: RawTransactionArgument]; + arguments?: ComputeNextCommitteeArguments | [staking?: RawTransactionArgument]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Computes the committee for the next epoch. */ export function computeNextCommittee(options: ComputeNextCommitteeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['staking']; return (tx: Transaction) => @@ -268,11 +461,33 @@ export function computeNextCommittee(options: ComputeNextCommitteeOptions) { package: packageAddress, module: 'staking', function: 'compute_next_committee', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'compute_next_committee', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface SetStoragePriceVoteArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; cap: RawTransactionArgument; storagePrice: RawTransactionArgument; } @@ -281,14 +496,18 @@ export interface SetStoragePriceVoteOptions { arguments: | SetStoragePriceVoteArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, cap: RawTransactionArgument, storagePrice: RawTransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Sets the storage price vote for the pool. */ export function setStoragePriceVote(options: SetStoragePriceVoteOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u64'] satisfies (string | null)[]; const parameterNames = ['self', 'cap', 'storagePrice']; return (tx: Transaction) => @@ -296,11 +515,33 @@ export function setStoragePriceVote(options: SetStoragePriceVoteOptions) { package: packageAddress, module: 'staking', function: 'set_storage_price_vote', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'set_storage_price_vote', + parameterIndex: 0, + parameterName: 'self', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface SetWritePriceVoteArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; cap: RawTransactionArgument; writePrice: RawTransactionArgument; } @@ -309,14 +550,18 @@ export interface SetWritePriceVoteOptions { arguments: | SetWritePriceVoteArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, cap: RawTransactionArgument, writePrice: RawTransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Sets the write price vote for the pool. */ export function setWritePriceVote(options: SetWritePriceVoteOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u64'] satisfies (string | null)[]; const parameterNames = ['self', 'cap', 'writePrice']; return (tx: Transaction) => @@ -324,11 +569,33 @@ export function setWritePriceVote(options: SetWritePriceVoteOptions) { package: packageAddress, module: 'staking', function: 'set_write_price_vote', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'set_write_price_vote', + parameterIndex: 0, + parameterName: 'self', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface SetNodeCapacityVoteArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; cap: RawTransactionArgument; nodeCapacity: RawTransactionArgument; } @@ -337,14 +604,18 @@ export interface SetNodeCapacityVoteOptions { arguments: | SetNodeCapacityVoteArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, cap: RawTransactionArgument, nodeCapacity: RawTransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Sets the node capacity vote for the pool. */ export function setNodeCapacityVote(options: SetNodeCapacityVoteOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u64'] satisfies (string | null)[]; const parameterNames = ['self', 'cap', 'nodeCapacity']; return (tx: Transaction) => @@ -352,18 +623,45 @@ export function setNodeCapacityVote(options: SetNodeCapacityVoteOptions) { package: packageAddress, module: 'staking', function: 'set_node_capacity_vote', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'set_node_capacity_vote', + parameterIndex: 0, + parameterName: 'self', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface UpdatePricesArguments { - staking: RawTransactionArgument; - system: RawTransactionArgument; + staking?: RawTransactionArgument; + system?: RawTransactionArgument; } export interface UpdatePricesOptions { package?: string; - arguments: + arguments?: | UpdatePricesArguments - | [staking: RawTransactionArgument, system: RawTransactionArgument]; + | [staking?: RawTransactionArgument, system?: RawTransactionArgument]; + config?: { + stakingPoolId: ConfigValue; + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Recalculates the quorum storage and write prices from the current committee and @@ -371,7 +669,7 @@ export interface UpdatePricesOptions { * same PTB) and is also called during epoch change. */ export function updatePrices(options: UpdatePricesOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['staking', 'system']; return (tx: Transaction) => @@ -379,22 +677,65 @@ export function updatePrices(options: UpdatePricesOptions) { package: packageAddress, module: 'staking', function: 'update_prices', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'update_prices', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + { + index: 1, + name: 'system', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'update_prices', + parameterIndex: 1, + parameterName: 'system', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface NodeMetadataArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; nodeId: RawTransactionArgument; } export interface NodeMetadataOptions { package?: string; arguments: | NodeMetadataArguments - | [self: RawTransactionArgument, nodeId: RawTransactionArgument]; + | [self: RawTransactionArgument | undefined, nodeId: RawTransactionArgument]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Get `NodeMetadata` for the given node. */ export function nodeMetadata(options: NodeMetadataOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x2::object::ID'] satisfies (string | null)[]; const parameterNames = ['self', 'nodeId']; return (tx: Transaction) => @@ -402,11 +743,33 @@ export function nodeMetadata(options: NodeMetadataOptions) { package: packageAddress, module: 'staking', function: 'node_metadata', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'node_metadata', + parameterIndex: 0, + parameterName: 'self', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface SetNextPublicKeyArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; cap: RawTransactionArgument; publicKey: RawTransactionArgument>; proofOfPossession: RawTransactionArgument>; @@ -416,18 +779,22 @@ export interface SetNextPublicKeyOptions { arguments: | SetNextPublicKeyArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, cap: RawTransactionArgument, publicKey: RawTransactionArgument>, proofOfPossession: RawTransactionArgument>, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Sets the public key of a node to be used starting from the next epoch for which * the node is selected. */ export function setNextPublicKey(options: SetNextPublicKeyOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'vector', 'vector'] satisfies (string | null)[]; const parameterNames = ['self', 'cap', 'publicKey', 'proofOfPossession']; return (tx: Transaction) => @@ -435,11 +802,33 @@ export function setNextPublicKey(options: SetNextPublicKeyOptions) { package: packageAddress, module: 'staking', function: 'set_next_public_key', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'set_next_public_key', + parameterIndex: 0, + parameterName: 'self', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface SetNameArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; cap: RawTransactionArgument; name: RawTransactionArgument; } @@ -448,14 +837,18 @@ export interface SetNameOptions { arguments: | SetNameArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, cap: RawTransactionArgument, name: RawTransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Sets the name of a storage node. */ export function setName(options: SetNameOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, '0x1::string::String'] satisfies (string | null)[]; const parameterNames = ['self', 'cap', 'name']; return (tx: Transaction) => @@ -463,11 +856,33 @@ export function setName(options: SetNameOptions) { package: packageAddress, module: 'staking', function: 'set_name', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'set_name', + parameterIndex: 0, + parameterName: 'self', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface SetNetworkAddressArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; cap: RawTransactionArgument; networkAddress: RawTransactionArgument; } @@ -476,14 +891,18 @@ export interface SetNetworkAddressOptions { arguments: | SetNetworkAddressArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, cap: RawTransactionArgument, networkAddress: RawTransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Sets the network address or host of a storage node. */ export function setNetworkAddress(options: SetNetworkAddressOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, '0x1::string::String'] satisfies (string | null)[]; const parameterNames = ['self', 'cap', 'networkAddress']; return (tx: Transaction) => @@ -491,11 +910,33 @@ export function setNetworkAddress(options: SetNetworkAddressOptions) { package: packageAddress, module: 'staking', function: 'set_network_address', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'set_network_address', + parameterIndex: 0, + parameterName: 'self', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface SetNetworkPublicKeyArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; cap: RawTransactionArgument; networkPublicKey: RawTransactionArgument>; } @@ -504,14 +945,18 @@ export interface SetNetworkPublicKeyOptions { arguments: | SetNetworkPublicKeyArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, cap: RawTransactionArgument, networkPublicKey: RawTransactionArgument>, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Sets the public key used for TLS communication for a node. */ export function setNetworkPublicKey(options: SetNetworkPublicKeyOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'vector'] satisfies (string | null)[]; const parameterNames = ['self', 'cap', 'networkPublicKey']; return (tx: Transaction) => @@ -519,11 +964,33 @@ export function setNetworkPublicKey(options: SetNetworkPublicKeyOptions) { package: packageAddress, module: 'staking', function: 'set_network_public_key', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'set_network_public_key', + parameterIndex: 0, + parameterName: 'self', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface SetNodeMetadataArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; cap: RawTransactionArgument; metadata: TransactionArgument; } @@ -532,14 +999,18 @@ export interface SetNodeMetadataOptions { arguments: | SetNodeMetadataArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, cap: RawTransactionArgument, metadata: TransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Sets the metadata of a storage node. */ export function setNodeMetadata(options: SetNodeMetadataOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, null] satisfies (string | null)[]; const parameterNames = ['self', 'cap', 'metadata']; return (tx: Transaction) => @@ -547,15 +1018,41 @@ export function setNodeMetadata(options: SetNodeMetadataOptions) { package: packageAddress, module: 'staking', function: 'set_node_metadata', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'set_node_metadata', + parameterIndex: 0, + parameterName: 'self', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface VotingEndArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; } export interface VotingEndOptions { package?: string; - arguments: VotingEndArguments | [staking: RawTransactionArgument]; + arguments?: VotingEndArguments | [staking?: RawTransactionArgument]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Ends the voting period and runs the apportionment if the current time allows. @@ -564,7 +1061,7 @@ export interface VotingEndOptions { * `EpochParametersSelected` event. */ export function votingEnd(options: VotingEndOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x2::clock::Clock'] satisfies (string | null)[]; const parameterNames = ['staking']; return (tx: Transaction) => @@ -572,21 +1069,48 @@ export function votingEnd(options: VotingEndOptions) { package: packageAddress, module: 'staking', function: 'voting_end', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'voting_end', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface InitiateEpochChangeArguments { - staking: RawTransactionArgument; - system: RawTransactionArgument; + staking?: RawTransactionArgument; + system?: RawTransactionArgument; } export interface InitiateEpochChangeOptions { package?: string; - arguments: + arguments?: | InitiateEpochChangeArguments - | [staking: RawTransactionArgument, system: RawTransactionArgument]; + | [staking?: RawTransactionArgument, system?: RawTransactionArgument]; + config?: { + stakingPoolId: ConfigValue; + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } export function initiateEpochChange(options: InitiateEpochChangeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, '0x2::clock::Clock'] satisfies (string | null)[]; const parameterNames = ['staking', 'system']; return (tx: Transaction) => @@ -594,12 +1118,51 @@ export function initiateEpochChange(options: InitiateEpochChangeOptions) { package: packageAddress, module: 'staking', function: 'initiate_epoch_change', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'initiate_epoch_change', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + { + index: 1, + name: 'system', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'initiate_epoch_change', + parameterIndex: 1, + parameterName: 'system', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface InitiateEpochChangeV2Arguments { - staking: RawTransactionArgument; - system: RawTransactionArgument; + staking?: RawTransactionArgument; + system?: RawTransactionArgument; treasury: RawTransactionArgument; } export interface InitiateEpochChangeV2Options { @@ -607,10 +1170,15 @@ export interface InitiateEpochChangeV2Options { arguments: | InitiateEpochChangeV2Arguments | [ - staking: RawTransactionArgument, - system: RawTransactionArgument, + staking: RawTransactionArgument | undefined, + system: RawTransactionArgument | undefined, treasury: RawTransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Initiates the epoch change if the current time allows. @@ -618,7 +1186,7 @@ export interface InitiateEpochChangeV2Options { * Emits the `EpochChangeStart` event. */ export function initiateEpochChangeV2(options: InitiateEpochChangeV2Options) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, null, '0x2::clock::Clock'] satisfies (string | null)[]; const parameterNames = ['staking', 'system', 'treasury']; return (tx: Transaction) => @@ -626,11 +1194,50 @@ export function initiateEpochChangeV2(options: InitiateEpochChangeV2Options) { package: packageAddress, module: 'staking', function: 'initiate_epoch_change_v2', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'initiate_epoch_change_v2', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + { + index: 1, + name: 'system', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'initiate_epoch_change_v2', + parameterIndex: 1, + parameterName: 'system', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface EpochSyncDoneArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; cap: RawTransactionArgument; epoch: RawTransactionArgument; } @@ -639,17 +1246,21 @@ export interface EpochSyncDoneOptions { arguments: | EpochSyncDoneArguments | [ - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, cap: RawTransactionArgument, epoch: RawTransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Signals to the contract that the node has received all its shards for the new * epoch. */ export function epochSyncDone(options: EpochSyncDoneOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u32', '0x2::clock::Clock'] satisfies (string | null)[]; const parameterNames = ['staking', 'cap', 'epoch']; return (tx: Transaction) => @@ -657,11 +1268,33 @@ export function epochSyncDone(options: EpochSyncDoneOptions) { package: packageAddress, module: 'staking', function: 'epoch_sync_done', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'epoch_sync_done', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface StakeWithPoolArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; toStake: RawTransactionArgument; nodeId: RawTransactionArgument; } @@ -670,14 +1303,18 @@ export interface StakeWithPoolOptions { arguments: | StakeWithPoolArguments | [ - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, toStake: RawTransactionArgument, nodeId: RawTransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Stake `Coin` with the staking pool. */ export function stakeWithPool(options: StakeWithPoolOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, '0x2::object::ID'] satisfies (string | null)[]; const parameterNames = ['staking', 'toStake', 'nodeId']; return (tx: Transaction) => @@ -685,18 +1322,47 @@ export function stakeWithPool(options: StakeWithPoolOptions) { package: packageAddress, module: 'staking', function: 'stake_with_pool', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'stake_with_pool', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface RequestWithdrawStakeArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; stakedWal: RawTransactionArgument; } export interface RequestWithdrawStakeOptions { package?: string; arguments: | RequestWithdrawStakeArguments - | [staking: RawTransactionArgument, stakedWal: RawTransactionArgument]; + | [ + staking: RawTransactionArgument | undefined, + stakedWal: RawTransactionArgument, + ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Marks the amount as a withdrawal to be processed and removes it from the stake @@ -706,7 +1372,7 @@ export interface RequestWithdrawStakeOptions { * epoch and shard transfer is done. */ export function requestWithdrawStake(options: RequestWithdrawStakeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['staking', 'stakedWal']; return (tx: Transaction) => @@ -714,22 +1380,51 @@ export function requestWithdrawStake(options: RequestWithdrawStakeOptions) { package: packageAddress, module: 'staking', function: 'request_withdraw_stake', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'request_withdraw_stake', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface WithdrawStakeArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; stakedWal: RawTransactionArgument; } export interface WithdrawStakeOptions { package?: string; arguments: | WithdrawStakeArguments - | [staking: RawTransactionArgument, stakedWal: RawTransactionArgument]; + | [ + staking: RawTransactionArgument | undefined, + stakedWal: RawTransactionArgument, + ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Withdraws the staked amount from the staking pool. */ export function withdrawStake(options: WithdrawStakeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['staking', 'stakedWal']; return (tx: Transaction) => @@ -737,18 +1432,44 @@ export function withdrawStake(options: WithdrawStakeOptions) { package: packageAddress, module: 'staking', function: 'withdraw_stake', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'withdraw_stake', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface TryJoinActiveSetArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; cap: RawTransactionArgument; } export interface TryJoinActiveSetOptions { package?: string; arguments: | TryJoinActiveSetArguments - | [staking: RawTransactionArgument, cap: RawTransactionArgument]; + | [staking: RawTransactionArgument | undefined, cap: RawTransactionArgument]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Allows a node to join the active set if it has sufficient stake. @@ -758,7 +1479,7 @@ export interface TryJoinActiveSetOptions { * active set either the next time stake is added or by calling this function. */ export function tryJoinActiveSet(options: TryJoinActiveSetOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['staking', 'cap']; return (tx: Transaction) => @@ -766,11 +1487,33 @@ export function tryJoinActiveSet(options: TryJoinActiveSetOptions) { package: packageAddress, module: 'staking', function: 'try_join_active_set', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'try_join_active_set', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface AddCommissionToPoolsArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; nodeIds: RawTransactionArgument>; commissions: TransactionArgument; } @@ -779,14 +1522,18 @@ export interface AddCommissionToPoolsOptions { arguments: | AddCommissionToPoolsArguments | [ - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, nodeIds: RawTransactionArgument>, commissions: TransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Adds `commissions[i]` to the commission of pool `node_ids[i]`. */ export function addCommissionToPools(options: AddCommissionToPoolsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'vector<0x2::object::ID>', 'vector'] satisfies ( | string | null @@ -797,19 +1544,45 @@ export function addCommissionToPools(options: AddCommissionToPoolsOptions) { package: packageAddress, module: 'staking', function: 'add_commission_to_pools', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'add_commission_to_pools', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface EpochArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; } export interface EpochOptions { package?: string; - arguments: EpochArguments | [staking: RawTransactionArgument]; + arguments?: EpochArguments | [staking?: RawTransactionArgument]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Returns the current epoch of the staking object. */ export function epoch(options: EpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['staking']; return (tx: Transaction) => @@ -817,11 +1590,33 @@ export function epoch(options: EpochOptions) { package: packageAddress, module: 'staking', function: 'epoch', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'epoch', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface CalculateRewardsArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; nodeId: RawTransactionArgument; stakedPrincipal: RawTransactionArgument; activationEpoch: RawTransactionArgument; @@ -832,12 +1627,16 @@ export interface CalculateRewardsOptions { arguments: | CalculateRewardsArguments | [ - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, nodeId: RawTransactionArgument, stakedPrincipal: RawTransactionArgument, activationEpoch: RawTransactionArgument, withdrawEpoch: RawTransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Calculates the rewards for an amount with value `staked_principal`, staked in @@ -849,7 +1648,7 @@ export interface CalculateRewardsOptions { * node over a given period. */ export function calculateRewards(options: CalculateRewardsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, '0x2::object::ID', 'u64', 'u32', 'u32'] satisfies (string | null)[]; const parameterNames = [ 'staking', @@ -863,25 +1662,54 @@ export function calculateRewards(options: CalculateRewardsOptions) { package: packageAddress, module: 'staking', function: 'calculate_rewards', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'calculate_rewards', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface CanWithdrawStakedWalEarlyArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; stakedWal: RawTransactionArgument; } export interface CanWithdrawStakedWalEarlyOptions { package?: string; arguments: | CanWithdrawStakedWalEarlyArguments - | [staking: RawTransactionArgument, stakedWal: RawTransactionArgument]; + | [ + staking: RawTransactionArgument | undefined, + stakedWal: RawTransactionArgument, + ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Call `staked_wal::can_withdraw_early` to allow calling this method in * applications. */ export function canWithdrawStakedWalEarly(options: CanWithdrawStakedWalEarlyOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['staking', 'stakedWal']; return (tx: Transaction) => @@ -889,22 +1717,48 @@ export function canWithdrawStakedWalEarly(options: CanWithdrawStakedWalEarlyOpti package: packageAddress, module: 'staking', function: 'can_withdraw_staked_wal_early', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'can_withdraw_staked_wal_early', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface SetMigrationEpochArguments { - staking: RawTransactionArgument; + staking?: RawTransactionArgument; } export interface SetMigrationEpochOptions { package?: string; - arguments: SetMigrationEpochArguments | [staking: RawTransactionArgument]; + arguments?: SetMigrationEpochArguments | [staking?: RawTransactionArgument]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Sets the epoch in which the staking and system objects can be migrated after an * upgrade. */ export function setMigrationEpoch(options: SetMigrationEpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['staking']; return (tx: Transaction) => @@ -912,6 +1766,28 @@ export function setMigrationEpoch(options: SetMigrationEpochOptions) { package: packageAddress, module: 'staking', function: 'set_migration_epoch', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'staking', + functionName: 'set_migration_epoch', + parameterIndex: 0, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } diff --git a/packages/walrus/src/contracts/walrus/storage_accounting.ts b/packages/walrus/src/contracts/walrus/storage_accounting.ts index 1aa4141f2..a559f2d0f 100644 --- a/packages/walrus/src/contracts/walrus/storage_accounting.ts +++ b/packages/walrus/src/contracts/walrus/storage_accounting.ts @@ -32,10 +32,13 @@ export interface MaxEpochsAheadArguments { export interface MaxEpochsAheadOptions { package?: string; arguments: MaxEpochsAheadArguments | [self: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** The maximum number of epochs for which we can use `self`. */ export function maxEpochsAhead(options: MaxEpochsAheadOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -55,10 +58,13 @@ export interface RingLookupOptions { arguments: | RingLookupArguments | [self: TransactionArgument, epochsInFuture: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Read-only lookup for an element in the `FutureAccountingRingBuffer` */ export function ringLookup(options: RingLookupOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u32'] satisfies (string | null)[]; const parameterNames = ['self', 'epochsInFuture']; return (tx: Transaction) => @@ -75,10 +81,13 @@ export interface EpochArguments { export interface EpochOptions { package?: string; arguments: EpochArguments | [accounting: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Accessor for epoch, read-only. */ export function epoch(options: EpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['accounting']; return (tx: Transaction) => @@ -95,10 +104,13 @@ export interface UsedCapacityArguments { export interface UsedCapacityOptions { package?: string; arguments: UsedCapacityArguments | [accounting: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Accessor for used_capacity, read-only. */ export function usedCapacity(options: UsedCapacityOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['accounting']; return (tx: Transaction) => @@ -115,10 +127,13 @@ export interface RewardsArguments { export interface RewardsOptions { package?: string; arguments: RewardsArguments | [accounting: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Accessor for rewards, read-only. */ export function rewards(options: RewardsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['accounting']; return (tx: Transaction) => diff --git a/packages/walrus/src/contracts/walrus/storage_node.ts b/packages/walrus/src/contracts/walrus/storage_node.ts index fd3eb6e3c..3a1901d2c 100644 --- a/packages/walrus/src/contracts/walrus/storage_node.ts +++ b/packages/walrus/src/contracts/walrus/storage_node.ts @@ -41,10 +41,13 @@ export interface IdArguments { export interface IdOptions { package?: string; arguments: IdArguments | [cap: TransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Return the node ID of the storage node. */ export function id(options: IdOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['cap']; return (tx: Transaction) => @@ -61,10 +64,13 @@ export interface NodeIdArguments { export interface NodeIdOptions { package?: string; arguments: NodeIdArguments | [cap: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Return the pool ID of the storage node. */ export function nodeId(options: NodeIdOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['cap']; return (tx: Transaction) => @@ -81,13 +87,16 @@ export interface LastEpochSyncDoneArguments { export interface LastEpochSyncDoneOptions { package?: string; arguments: LastEpochSyncDoneArguments | [cap: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Return the last epoch in which the storage node attested that it has finished * syncing. */ export function lastEpochSyncDone(options: LastEpochSyncDoneOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['cap']; return (tx: Transaction) => @@ -104,10 +113,13 @@ export interface LastEventBlobAttestationArguments { export interface LastEventBlobAttestationOptions { package?: string; arguments: LastEventBlobAttestationArguments | [cap: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Return the latest event blob attestation. */ export function lastEventBlobAttestation(options: LastEventBlobAttestationOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['cap']; return (tx: Transaction) => @@ -124,10 +136,13 @@ export interface DenyListRootArguments { export interface DenyListRootOptions { package?: string; arguments: DenyListRootArguments | [cap: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Return the deny list root of the storage node. */ export function denyListRoot(options: DenyListRootOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['cap']; return (tx: Transaction) => @@ -144,10 +159,13 @@ export interface DenyListSequenceArguments { export interface DenyListSequenceOptions { package?: string; arguments: DenyListSequenceArguments | [cap: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Return the deny list sequence number of the storage node. */ export function denyListSequence(options: DenyListSequenceOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['cap']; return (tx: Transaction) => diff --git a/packages/walrus/src/contracts/walrus/storage_pool.ts b/packages/walrus/src/contracts/walrus/storage_pool.ts index 8eebc110b..2697345c4 100644 --- a/packages/walrus/src/contracts/walrus/storage_pool.ts +++ b/packages/walrus/src/contracts/walrus/storage_pool.ts @@ -53,9 +53,12 @@ export interface StartEpochArguments { export interface StartEpochOptions { package?: string; arguments: StartEpochArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function startEpoch(options: StartEpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -72,9 +75,12 @@ export interface EndEpochArguments { export interface EndEpochOptions { package?: string; arguments: EndEpochArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function endEpoch(options: EndEpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -91,9 +97,12 @@ export interface ReservedEncodedCapacityBytesArguments { export interface ReservedEncodedCapacityBytesOptions { package?: string; arguments: ReservedEncodedCapacityBytesArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function reservedEncodedCapacityBytes(options: ReservedEncodedCapacityBytesOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -110,9 +119,12 @@ export interface UsedEncodedBytesArguments { export interface UsedEncodedBytesOptions { package?: string; arguments: UsedEncodedBytesArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function usedEncodedBytes(options: UsedEncodedBytesOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -129,9 +141,12 @@ export interface AvailableEncodedBytesArguments { export interface AvailableEncodedBytesOptions { package?: string; arguments: AvailableEncodedBytesArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function availableEncodedBytes(options: AvailableEncodedBytesOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -148,10 +163,13 @@ export interface StorageArguments { export interface StorageOptions { package?: string; arguments: StorageArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Returns a reference to the embedded storage reservation. */ export function storage(options: StorageOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -168,9 +186,12 @@ export interface BlobCountArguments { export interface BlobCountOptions { package?: string; arguments: BlobCountArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function blobCount(options: BlobCountOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -190,9 +211,12 @@ export interface ContainsBlobOptions { arguments: | ContainsBlobArguments | [self: RawTransactionArgument, blobId: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function containsBlob(options: ContainsBlobOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u256'] satisfies (string | null)[]; const parameterNames = ['self', 'blobId']; return (tx: Transaction) => @@ -212,10 +236,13 @@ export interface BlobObjectIdOptions { arguments: | BlobObjectIdArguments | [self: RawTransactionArgument, blobId: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** External wrappers use this to build certification messages for deletable blobs. */ export function blobObjectId(options: BlobObjectIdOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u256'] satisfies (string | null)[]; const parameterNames = ['self', 'blobId']; return (tx: Transaction) => @@ -232,10 +259,13 @@ export interface ObjectIdArguments { export interface ObjectIdOptions { package?: string; arguments: ObjectIdArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Returns the object ID of this storage pool. */ export function objectId(options: ObjectIdOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -252,13 +282,16 @@ export interface DestroyArguments { export interface DestroyOptions { package?: string; arguments: DestroyArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Destroys the pool and returns the embedded `Storage` reservation. Asserts the * blobs table is empty and `blob_count == 0`. */ export function destroy(options: DestroyOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -283,6 +316,9 @@ export interface AddBlobMetadataOptions { blobId: RawTransactionArgument, metadata: TransactionArgument, ]; + config?: { + walrusPackageId?: string; + }; } /** * Adds metadata to a pooled blob by blob ID. @@ -290,7 +326,7 @@ export interface AddBlobMetadataOptions { * Aborts if the metadata is already present. */ export function addBlobMetadata(options: AddBlobMetadataOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u256', null] satisfies (string | null)[]; const parameterNames = ['self', 'blobId', 'metadata']; return (tx: Transaction) => @@ -315,6 +351,9 @@ export interface AddOrReplaceBlobMetadataOptions { blobId: RawTransactionArgument, metadata: TransactionArgument, ]; + config?: { + walrusPackageId?: string; + }; } /** * Adds metadata to a pooled blob by blob ID, replacing existing metadata if @@ -323,7 +362,7 @@ export interface AddOrReplaceBlobMetadataOptions { * Returns the replaced metadata if present. */ export function addOrReplaceBlobMetadata(options: AddOrReplaceBlobMetadataOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u256', null] satisfies (string | null)[]; const parameterNames = ['self', 'blobId', 'metadata']; return (tx: Transaction) => @@ -343,6 +382,9 @@ export interface TakeBlobMetadataOptions { arguments: | TakeBlobMetadataArguments | [self: RawTransactionArgument, blobId: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Removes and returns the metadata from a pooled blob by blob ID. @@ -350,7 +392,7 @@ export interface TakeBlobMetadataOptions { * Aborts if the metadata does not exist. */ export function takeBlobMetadata(options: TakeBlobMetadataOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u256'] satisfies (string | null)[]; const parameterNames = ['self', 'blobId']; return (tx: Transaction) => @@ -377,6 +419,9 @@ export interface InsertOrUpdateBlobMetadataPairOptions { key: RawTransactionArgument, value: RawTransactionArgument, ]; + config?: { + walrusPackageId?: string; + }; } /** * Inserts or updates a key-value pair in a pooled blob's metadata by blob ID. @@ -384,7 +429,7 @@ export interface InsertOrUpdateBlobMetadataPairOptions { * Creates new metadata on the blob if it does not exist already. */ export function insertOrUpdateBlobMetadataPair(options: InsertOrUpdateBlobMetadataPairOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u256', '0x1::string::String', '0x1::string::String'] satisfies ( | string | null @@ -412,6 +457,9 @@ export interface RemoveBlobMetadataPairOptions { blobId: RawTransactionArgument, key: RawTransactionArgument, ]; + config?: { + walrusPackageId?: string; + }; } /** * Removes the metadata pair with the given key from a pooled blob by blob ID. @@ -419,7 +467,7 @@ export interface RemoveBlobMetadataPairOptions { * Aborts if the metadata does not exist. */ export function removeBlobMetadataPair(options: RemoveBlobMetadataPairOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u256', '0x1::string::String'] satisfies (string | null)[]; const parameterNames = ['self', 'blobId', 'key']; return (tx: Transaction) => @@ -444,13 +492,16 @@ export interface RemoveBlobMetadataPairIfExistsOptions { blobId: RawTransactionArgument, key: RawTransactionArgument, ]; + config?: { + walrusPackageId?: string; + }; } /** * Removes and returns the value for the given key from a pooled blob's metadata, * if it exists. */ export function removeBlobMetadataPairIfExists(options: RemoveBlobMetadataPairIfExistsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u256', '0x1::string::String'] satisfies (string | null)[]; const parameterNames = ['self', 'blobId', 'key']; return (tx: Transaction) => diff --git a/packages/walrus/src/contracts/walrus/storage_resource.ts b/packages/walrus/src/contracts/walrus/storage_resource.ts index 61a3ae921..220e650b6 100644 --- a/packages/walrus/src/contracts/walrus/storage_resource.ts +++ b/packages/walrus/src/contracts/walrus/storage_resource.ts @@ -20,9 +20,12 @@ export interface StartEpochArguments { export interface StartEpochOptions { package?: string; arguments: StartEpochArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function startEpoch(options: StartEpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -39,9 +42,12 @@ export interface EndEpochArguments { export interface EndEpochOptions { package?: string; arguments: EndEpochArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function endEpoch(options: EndEpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -58,9 +64,12 @@ export interface SizeArguments { export interface SizeOptions { package?: string; arguments: SizeArguments | [self: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } export function size(options: SizeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -80,6 +89,9 @@ export interface SplitByEpochOptions { arguments: | SplitByEpochArguments | [storage: RawTransactionArgument, splitEpoch: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Splits the storage object into two based on `split_epoch`. @@ -88,7 +100,7 @@ export interface SplitByEpochOptions { * and a new storage object covering `split_epoch` to `end_epoch` is returned. */ export function splitByEpoch(options: SplitByEpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u32'] satisfies (string | null)[]; const parameterNames = ['storage', 'splitEpoch']; return (tx: Transaction) => @@ -108,6 +120,9 @@ export interface SplitBySizeOptions { arguments: | SplitBySizeArguments | [storage: RawTransactionArgument, splitSize: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Splits the storage object into two based on `split_size`. @@ -116,7 +131,7 @@ export interface SplitBySizeOptions { * `storage.storage_size - split_size` is created. */ export function splitBySize(options: SplitBySizeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u64'] satisfies (string | null)[]; const parameterNames = ['storage', 'splitSize']; return (tx: Transaction) => @@ -136,10 +151,13 @@ export interface FusePeriodsOptions { arguments: | FusePeriodsArguments | [first: RawTransactionArgument, second: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Fuse two storage objects that cover adjacent periods with the same storage size. */ export function fusePeriods(options: FusePeriodsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['first', 'second']; return (tx: Transaction) => @@ -159,10 +177,13 @@ export interface FuseAmountOptions { arguments: | FuseAmountArguments | [first: RawTransactionArgument, second: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Fuse two storage objects that cover the same period. */ export function fuseAmount(options: FuseAmountOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['first', 'second']; return (tx: Transaction) => @@ -182,13 +203,16 @@ export interface FuseOptions { arguments: | FuseArguments | [first: RawTransactionArgument, second: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Fuse two storage objects that either cover the same period or adjacent periods * with the same storage size. */ export function fuse(options: FuseOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['first', 'second']; return (tx: Transaction) => @@ -205,10 +229,13 @@ export interface DestroyArguments { export interface DestroyOptions { package?: string; arguments: DestroyArguments | [storage: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** Destructor for [Storage] objects. */ export function destroy(options: DestroyOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['storage']; return (tx: Transaction) => diff --git a/packages/walrus/src/contracts/walrus/system.ts b/packages/walrus/src/contracts/walrus/system.ts index aa515fe0e..6a93fe8ca 100644 --- a/packages/walrus/src/contracts/walrus/system.ts +++ b/packages/walrus/src/contracts/walrus/system.ts @@ -4,7 +4,14 @@ /** Module: system */ -import { MoveStruct, normalizeMoveArguments, type RawTransactionArgument } from '../utils/index.js'; +import { + MoveStruct, + normalizeMoveArguments, + type RawTransactionArgument, + type ConfigValue, + resolveConfigArgument, + applyConfigArguments, +} from '../utils/index.js'; import { bcs } from '@mysten/sui/bcs'; import { type Transaction, type TransactionArgument } from '@mysten/sui/transactions'; const $moduleName = '@local-pkg/walrus::system'; @@ -18,7 +25,7 @@ export const System = new MoveStruct({ }, }); export interface InvalidateBlobIdArguments { - system: RawTransactionArgument; + system?: RawTransactionArgument; signature: RawTransactionArgument>; membersBitmap: RawTransactionArgument>; message: RawTransactionArgument>; @@ -28,18 +35,22 @@ export interface InvalidateBlobIdOptions { arguments: | InvalidateBlobIdArguments | [ - system: RawTransactionArgument, + system: RawTransactionArgument | undefined, signature: RawTransactionArgument>, membersBitmap: RawTransactionArgument>, message: RawTransactionArgument>, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * === Public Functions === Marks blob as invalid given an invalid blob * certificate. */ export function invalidateBlobId(options: InvalidateBlobIdOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'vector', 'vector', 'vector'] satisfies ( | string | null @@ -50,11 +61,33 @@ export function invalidateBlobId(options: InvalidateBlobIdOptions) { package: packageAddress, module: 'system', function: 'invalidate_blob_id', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'system', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'invalidate_blob_id', + parameterIndex: 0, + parameterName: 'system', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface CertifyEventBlobArguments { - system: RawTransactionArgument; + system?: RawTransactionArgument; cap: RawTransactionArgument; blobId: RawTransactionArgument; rootHash: RawTransactionArgument; @@ -68,7 +101,7 @@ export interface CertifyEventBlobOptions { arguments: | CertifyEventBlobArguments | [ - system: RawTransactionArgument, + system: RawTransactionArgument | undefined, cap: RawTransactionArgument, blobId: RawTransactionArgument, rootHash: RawTransactionArgument, @@ -77,10 +110,14 @@ export interface CertifyEventBlobOptions { endingCheckpointSequenceNum: RawTransactionArgument, epoch: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Certifies a blob containing Walrus events. */ export function certifyEventBlob(options: CertifyEventBlobOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u256', 'u256', 'u64', 'u8', 'u64', 'u32'] satisfies ( | string | null @@ -100,11 +137,33 @@ export function certifyEventBlob(options: CertifyEventBlobOptions) { package: packageAddress, module: 'system', function: 'certify_event_blob', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'system', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'certify_event_blob', + parameterIndex: 0, + parameterName: 'system', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface ReserveSpaceArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; storageAmount: RawTransactionArgument; epochsAhead: RawTransactionArgument; payment: RawTransactionArgument; @@ -114,15 +173,19 @@ export interface ReserveSpaceOptions { arguments: | ReserveSpaceArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, storageAmount: RawTransactionArgument, epochsAhead: RawTransactionArgument, payment: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Allows buying a storage reservation for a given period of epochs. */ export function reserveSpace(options: ReserveSpaceOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u64', 'u32', null] satisfies (string | null)[]; const parameterNames = ['self', 'storageAmount', 'epochsAhead', 'payment']; return (tx: Transaction) => @@ -130,11 +193,33 @@ export function reserveSpace(options: ReserveSpaceOptions) { package: packageAddress, module: 'system', function: 'reserve_space', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'reserve_space', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface ReserveSpaceForEpochsArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; storageAmount: RawTransactionArgument; startEpoch: RawTransactionArgument; endEpoch: RawTransactionArgument; @@ -145,12 +230,16 @@ export interface ReserveSpaceForEpochsOptions { arguments: | ReserveSpaceForEpochsArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, storageAmount: RawTransactionArgument, startEpoch: RawTransactionArgument, endEpoch: RawTransactionArgument, payment: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Allows buying a storage reservation for a given period of epochs. @@ -160,7 +249,7 @@ export interface ReserveSpaceForEpochsOptions { * starting from the current epoch. */ export function reserveSpaceForEpochs(options: ReserveSpaceForEpochsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u64', 'u32', 'u32', null] satisfies (string | null)[]; const parameterNames = ['self', 'storageAmount', 'startEpoch', 'endEpoch', 'payment']; return (tx: Transaction) => @@ -168,11 +257,33 @@ export function reserveSpaceForEpochs(options: ReserveSpaceForEpochsOptions) { package: packageAddress, module: 'system', function: 'reserve_space_for_epochs', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'reserve_space_for_epochs', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface RegisterBlobArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; storage: RawTransactionArgument; blobId: RawTransactionArgument; rootHash: RawTransactionArgument; @@ -186,7 +297,7 @@ export interface RegisterBlobOptions { arguments: | RegisterBlobArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, storage: RawTransactionArgument, blobId: RawTransactionArgument, rootHash: RawTransactionArgument, @@ -195,13 +306,17 @@ export interface RegisterBlobOptions { deletable: RawTransactionArgument, writePayment: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Registers a new blob in the system. `size` is the size of the unencoded blob. * The reserved space in `storage` must be at least the size of the encoded blob. */ export function registerBlob(options: RegisterBlobOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u256', 'u256', 'u64', 'u8', 'bool', null] satisfies ( | string | null @@ -221,11 +336,33 @@ export function registerBlob(options: RegisterBlobOptions) { package: packageAddress, module: 'system', function: 'register_blob', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'register_blob', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface CertifyBlobArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; blob: RawTransactionArgument; signature: RawTransactionArgument>; signersBitmap: RawTransactionArgument>; @@ -236,19 +373,23 @@ export interface CertifyBlobOptions { arguments: | CertifyBlobArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, blob: RawTransactionArgument, signature: RawTransactionArgument>, signersBitmap: RawTransactionArgument>, message: RawTransactionArgument>, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Certify that a blob will be available in the storage system until the end epoch * of the storage associated with it. */ export function certifyBlob(options: CertifyBlobOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'vector', 'vector', 'vector'] satisfies ( | string | null @@ -259,22 +400,48 @@ export function certifyBlob(options: CertifyBlobOptions) { package: packageAddress, module: 'system', function: 'certify_blob', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'certify_blob', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface DeleteBlobArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; blob: RawTransactionArgument; } export interface DeleteBlobOptions { package?: string; arguments: | DeleteBlobArguments - | [self: RawTransactionArgument, blob: RawTransactionArgument]; + | [self: RawTransactionArgument | undefined, blob: RawTransactionArgument]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Deletes a deletable blob and returns the contained storage resource. */ export function deleteBlob(options: DeleteBlobOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['self', 'blob']; return (tx: Transaction) => @@ -282,11 +449,33 @@ export function deleteBlob(options: DeleteBlobOptions) { package: packageAddress, module: 'system', function: 'delete_blob', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'delete_blob', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface ExtendBlobWithResourceArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; blob: RawTransactionArgument; extension: RawTransactionArgument; } @@ -295,10 +484,14 @@ export interface ExtendBlobWithResourceOptions { arguments: | ExtendBlobWithResourceArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, blob: RawTransactionArgument, extension: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Extend the period of validity of a blob with a new storage resource. The new @@ -306,7 +499,7 @@ export interface ExtendBlobWithResourceOptions { * and have a longer period of validity. */ export function extendBlobWithResource(options: ExtendBlobWithResourceOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, null] satisfies (string | null)[]; const parameterNames = ['self', 'blob', 'extension']; return (tx: Transaction) => @@ -314,11 +507,33 @@ export function extendBlobWithResource(options: ExtendBlobWithResourceOptions) { package: packageAddress, module: 'system', function: 'extend_blob_with_resource', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'extend_blob_with_resource', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface ExtendBlobArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; blob: RawTransactionArgument; extendedEpochs: RawTransactionArgument; payment: RawTransactionArgument; @@ -328,18 +543,22 @@ export interface ExtendBlobOptions { arguments: | ExtendBlobArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, blob: RawTransactionArgument, extendedEpochs: RawTransactionArgument, payment: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Extend the period of validity of a blob by extending its contained storage * resource by `extended_epochs` epochs. */ export function extendBlob(options: ExtendBlobOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u32', null] satisfies (string | null)[]; const parameterNames = ['self', 'blob', 'extendedEpochs', 'payment']; return (tx: Transaction) => @@ -347,11 +566,33 @@ export function extendBlob(options: ExtendBlobOptions) { package: packageAddress, module: 'system', function: 'extend_blob', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'extend_blob', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface CreateStoragePoolArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; reservedEncodedCapacityBytes: RawTransactionArgument; epochsAhead: RawTransactionArgument; payment: RawTransactionArgument; @@ -361,15 +602,19 @@ export interface CreateStoragePoolOptions { arguments: | CreateStoragePoolArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, reservedEncodedCapacityBytes: RawTransactionArgument, epochsAhead: RawTransactionArgument, payment: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Creates a new storage pool with the given capacity and epoch range. */ export function createStoragePool(options: CreateStoragePoolOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'u64', 'u32', null] satisfies (string | null)[]; const parameterNames = ['self', 'reservedEncodedCapacityBytes', 'epochsAhead', 'payment']; return (tx: Transaction) => @@ -377,22 +622,48 @@ export function createStoragePool(options: CreateStoragePoolOptions) { package: packageAddress, module: 'system', function: 'create_storage_pool', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'create_storage_pool', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface CreateStoragePoolWithStorageArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; storage: RawTransactionArgument; } export interface CreateStoragePoolWithStorageOptions { package?: string; arguments: | CreateStoragePoolWithStorageArguments - | [self: RawTransactionArgument, storage: RawTransactionArgument]; + | [self: RawTransactionArgument | undefined, storage: RawTransactionArgument]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Creates a new storage pool backed by an existing `Storage` reservation. */ export function createStoragePoolWithStorage(options: CreateStoragePoolWithStorageOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null] satisfies (string | null)[]; const parameterNames = ['self', 'storage']; return (tx: Transaction) => @@ -400,11 +671,33 @@ export function createStoragePoolWithStorage(options: CreateStoragePoolWithStora package: packageAddress, module: 'system', function: 'create_storage_pool_with_storage', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'create_storage_pool_with_storage', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface RegisterPooledBlobArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; storagePool: RawTransactionArgument; blobId: RawTransactionArgument; rootHash: RawTransactionArgument; @@ -418,7 +711,7 @@ export interface RegisterPooledBlobOptions { arguments: | RegisterPooledBlobArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, storagePool: RawTransactionArgument, blobId: RawTransactionArgument, rootHash: RawTransactionArgument, @@ -427,10 +720,14 @@ export interface RegisterPooledBlobOptions { deletable: RawTransactionArgument, writePayment: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Registers a new blob against a storage pool. */ export function registerPooledBlob(options: RegisterPooledBlobOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u256', 'u256', 'u64', 'u8', 'bool', null] satisfies ( | string | null @@ -450,11 +747,33 @@ export function registerPooledBlob(options: RegisterPooledBlobOptions) { package: packageAddress, module: 'system', function: 'register_pooled_blob', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'register_pooled_blob', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface DeletePooledBlobArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; storagePool: RawTransactionArgument; blobId: RawTransactionArgument; } @@ -463,14 +782,18 @@ export interface DeletePooledBlobOptions { arguments: | DeletePooledBlobArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, storagePool: RawTransactionArgument, blobId: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Deletes a blob from a storage pool and frees its capacity. */ export function deletePooledBlob(options: DeletePooledBlobOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u256'] satisfies (string | null)[]; const parameterNames = ['self', 'storagePool', 'blobId']; return (tx: Transaction) => @@ -478,11 +801,33 @@ export function deletePooledBlob(options: DeletePooledBlobOptions) { package: packageAddress, module: 'system', function: 'delete_pooled_blob', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'delete_pooled_blob', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface BurnExpiredPooledBlobArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; storagePool: RawTransactionArgument; blobId: RawTransactionArgument; } @@ -491,17 +836,21 @@ export interface BurnExpiredPooledBlobOptions { arguments: | BurnExpiredPooledBlobArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, storagePool: RawTransactionArgument, blobId: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Burns a blob from an expired storage pool, regardless of the `deletable` flag. * The pool must have expired (`end_epoch <= current_epoch`). */ export function burnExpiredPooledBlob(options: BurnExpiredPooledBlobOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u256'] satisfies (string | null)[]; const parameterNames = ['self', 'storagePool', 'blobId']; return (tx: Transaction) => @@ -509,11 +858,33 @@ export function burnExpiredPooledBlob(options: BurnExpiredPooledBlobOptions) { package: packageAddress, module: 'system', function: 'burn_expired_pooled_blob', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'burn_expired_pooled_blob', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface ExtendStoragePoolArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; storagePool: RawTransactionArgument; extendedEpochs: RawTransactionArgument; payment: RawTransactionArgument; @@ -523,15 +894,19 @@ export interface ExtendStoragePoolOptions { arguments: | ExtendStoragePoolArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, storagePool: RawTransactionArgument, extendedEpochs: RawTransactionArgument, payment: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Extends the lifetime of a storage pool by `extended_epochs`. */ export function extendStoragePool(options: ExtendStoragePoolOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u32', null] satisfies (string | null)[]; const parameterNames = ['self', 'storagePool', 'extendedEpochs', 'payment']; return (tx: Transaction) => @@ -539,11 +914,33 @@ export function extendStoragePool(options: ExtendStoragePoolOptions) { package: packageAddress, module: 'system', function: 'extend_storage_pool', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'extend_storage_pool', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface IncreaseStoragePoolCapacityArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; storagePool: RawTransactionArgument; additionalEncodedCapacityBytes: RawTransactionArgument; payment: RawTransactionArgument; @@ -553,18 +950,22 @@ export interface IncreaseStoragePoolCapacityOptions { arguments: | IncreaseStoragePoolCapacityArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, storagePool: RawTransactionArgument, additionalEncodedCapacityBytes: RawTransactionArgument, payment: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Increases the reserved capacity of a storage pool for the remainder of its * lifetime. */ export function increaseStoragePoolCapacity(options: IncreaseStoragePoolCapacityOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u64', null] satisfies (string | null)[]; const parameterNames = ['self', 'storagePool', 'additionalEncodedCapacityBytes', 'payment']; return (tx: Transaction) => @@ -572,11 +973,33 @@ export function increaseStoragePoolCapacity(options: IncreaseStoragePoolCapacity package: packageAddress, module: 'system', function: 'increase_storage_pool_capacity', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'increase_storage_pool_capacity', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface IncreaseStoragePoolCapacityWithStorageArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; storagePool: RawTransactionArgument; storage: RawTransactionArgument; } @@ -585,16 +1008,20 @@ export interface IncreaseStoragePoolCapacityWithStorageOptions { arguments: | IncreaseStoragePoolCapacityWithStorageArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, storagePool: RawTransactionArgument, storage: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Increases the pool's capacity by absorbing an existing `Storage` object. */ export function increaseStoragePoolCapacityWithStorage( options: IncreaseStoragePoolCapacityWithStorageOptions, ) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, null] satisfies (string | null)[]; const parameterNames = ['self', 'storagePool', 'storage']; return (tx: Transaction) => @@ -602,11 +1029,33 @@ export function increaseStoragePoolCapacityWithStorage( package: packageAddress, module: 'system', function: 'increase_storage_pool_capacity_with_storage', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'increase_storage_pool_capacity_with_storage', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface DecreaseStoragePoolCapacityBySizeArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; storagePool: RawTransactionArgument; size: RawTransactionArgument; } @@ -615,10 +1064,14 @@ export interface DecreaseStoragePoolCapacityBySizeOptions { arguments: | DecreaseStoragePoolCapacityBySizeArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, storagePool: RawTransactionArgument, size: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Reduces the pool's capacity by extracting a `Storage` object of the given size. @@ -627,7 +1080,7 @@ export interface DecreaseStoragePoolCapacityBySizeOptions { export function decreaseStoragePoolCapacityBySize( options: DecreaseStoragePoolCapacityBySizeOptions, ) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u64'] satisfies (string | null)[]; const parameterNames = ['self', 'storagePool', 'size']; return (tx: Transaction) => @@ -635,11 +1088,33 @@ export function decreaseStoragePoolCapacityBySize( package: packageAddress, module: 'system', function: 'decrease_storage_pool_capacity_by_size', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'decrease_storage_pool_capacity_by_size', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface DecreaseStoragePoolUnusedCapacityByPercentArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; storagePool: RawTransactionArgument; percent: RawTransactionArgument; } @@ -648,10 +1123,14 @@ export interface DecreaseStoragePoolUnusedCapacityByPercentOptions { arguments: | DecreaseStoragePoolUnusedCapacityByPercentArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, storagePool: RawTransactionArgument, percent: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Reduces the pool's capacity by extracting `percent` of the unused capacity as a @@ -661,7 +1140,7 @@ export interface DecreaseStoragePoolUnusedCapacityByPercentOptions { export function decreaseStoragePoolUnusedCapacityByPercent( options: DecreaseStoragePoolUnusedCapacityByPercentOptions, ) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u8'] satisfies (string | null)[]; const parameterNames = ['self', 'storagePool', 'percent']; return (tx: Transaction) => @@ -669,11 +1148,33 @@ export function decreaseStoragePoolUnusedCapacityByPercent( package: packageAddress, module: 'system', function: 'decrease_storage_pool_unused_capacity_by_percent', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'decrease_storage_pool_unused_capacity_by_percent', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface CertifyPooledBlobArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; storagePool: RawTransactionArgument; blobId: RawTransactionArgument; signature: RawTransactionArgument>; @@ -685,17 +1186,21 @@ export interface CertifyPooledBlobOptions { arguments: | CertifyPooledBlobArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, storagePool: RawTransactionArgument, blobId: RawTransactionArgument, signature: RawTransactionArgument>, signersBitmap: RawTransactionArgument>, message: RawTransactionArgument>, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Certifies a blob within a storage pool. */ export function certifyPooledBlob(options: CertifyPooledBlobOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u256', 'vector', 'vector', 'vector'] satisfies ( | string | null @@ -706,11 +1211,33 @@ export function certifyPooledBlob(options: CertifyPooledBlobOptions) { package: packageAddress, module: 'system', function: 'certify_pooled_blob', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'certify_pooled_blob', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface AddSubsidyArguments { - system: RawTransactionArgument; + system?: RawTransactionArgument; subsidy: RawTransactionArgument; epochsAhead: RawTransactionArgument; } @@ -719,10 +1246,14 @@ export interface AddSubsidyOptions { arguments: | AddSubsidyArguments | [ - system: RawTransactionArgument, + system: RawTransactionArgument | undefined, subsidy: RawTransactionArgument, epochsAhead: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Adds rewards to the system for the specified number of epochs ahead. The rewards @@ -730,7 +1261,7 @@ export interface AddSubsidyOptions { * epoch. */ export function addSubsidy(options: AddSubsidyOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u32'] satisfies (string | null)[]; const parameterNames = ['system', 'subsidy', 'epochsAhead']; return (tx: Transaction) => @@ -738,25 +1269,51 @@ export function addSubsidy(options: AddSubsidyOptions) { package: packageAddress, module: 'system', function: 'add_subsidy', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'system', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'add_subsidy', + parameterIndex: 0, + parameterName: 'system', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface AddPerEpochSubsidiesArguments { - system: RawTransactionArgument; + system?: RawTransactionArgument; subsidies: TransactionArgument; } export interface AddPerEpochSubsidiesOptions { package?: string; arguments: | AddPerEpochSubsidiesArguments - | [system: RawTransactionArgument, subsidies: TransactionArgument]; + | [system: RawTransactionArgument | undefined, subsidies: TransactionArgument]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Adds rewards to the system for future epochs, where `subsidies[i]` is added to * the rewards of epoch `system.epoch() + i`. */ export function addPerEpochSubsidies(options: AddPerEpochSubsidiesOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'vector'] satisfies (string | null)[]; const parameterNames = ['system', 'subsidies']; return (tx: Transaction) => @@ -764,11 +1321,33 @@ export function addPerEpochSubsidies(options: AddPerEpochSubsidiesOptions) { package: packageAddress, module: 'system', function: 'add_per_epoch_subsidies', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'system', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'add_per_epoch_subsidies', + parameterIndex: 0, + parameterName: 'system', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface UpdateProtocolVersionArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; cap: RawTransactionArgument; signature: RawTransactionArgument>; membersBitmap: RawTransactionArgument>; @@ -779,16 +1358,20 @@ export interface UpdateProtocolVersionOptions { arguments: | UpdateProtocolVersionArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, cap: RawTransactionArgument, signature: RawTransactionArgument>, membersBitmap: RawTransactionArgument>, message: RawTransactionArgument>, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Node collects signatures on the protocol version event and emits it. */ export function updateProtocolVersion(options: UpdateProtocolVersionOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'vector', 'vector', 'vector'] satisfies ( | string | null @@ -799,11 +1382,33 @@ export function updateProtocolVersion(options: UpdateProtocolVersionOptions) { package: packageAddress, module: 'system', function: 'update_protocol_version', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'update_protocol_version', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface RegisterDenyListUpdateArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; cap: RawTransactionArgument; denyListRoot: RawTransactionArgument; denyListSequence: RawTransactionArgument; @@ -813,15 +1418,19 @@ export interface RegisterDenyListUpdateOptions { arguments: | RegisterDenyListUpdateArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, cap: RawTransactionArgument, denyListRoot: RawTransactionArgument, denyListSequence: RawTransactionArgument, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Register a deny list update. */ export function registerDenyListUpdate(options: RegisterDenyListUpdateOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'u256', 'u64'] satisfies (string | null)[]; const parameterNames = ['self', 'cap', 'denyListRoot', 'denyListSequence']; return (tx: Transaction) => @@ -829,11 +1438,33 @@ export function registerDenyListUpdate(options: RegisterDenyListUpdateOptions) { package: packageAddress, module: 'system', function: 'register_deny_list_update', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'register_deny_list_update', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface UpdateDenyListArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; cap: RawTransactionArgument; signature: RawTransactionArgument>; membersBitmap: RawTransactionArgument>; @@ -844,16 +1475,20 @@ export interface UpdateDenyListOptions { arguments: | UpdateDenyListArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, cap: RawTransactionArgument, signature: RawTransactionArgument>, membersBitmap: RawTransactionArgument>, message: RawTransactionArgument>, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Perform the update of the deny list. */ export function updateDenyList(options: UpdateDenyListOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'vector', 'vector', 'vector'] satisfies ( | string | null @@ -864,11 +1499,33 @@ export function updateDenyList(options: UpdateDenyListOptions) { package: packageAddress, module: 'system', function: 'update_deny_list', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'update_deny_list', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface DeleteDenyListedBlobArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; signature: RawTransactionArgument>; membersBitmap: RawTransactionArgument>; message: RawTransactionArgument>; @@ -878,15 +1535,19 @@ export interface DeleteDenyListedBlobOptions { arguments: | DeleteDenyListedBlobArguments | [ - self: RawTransactionArgument, + self: RawTransactionArgument | undefined, signature: RawTransactionArgument>, membersBitmap: RawTransactionArgument>, message: RawTransactionArgument>, ]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Delete a blob that is deny listed by f+1 members. */ export function deleteDenyListedBlob(options: DeleteDenyListedBlobOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, 'vector', 'vector', 'vector'] satisfies ( | string | null @@ -897,19 +1558,45 @@ export function deleteDenyListedBlob(options: DeleteDenyListedBlobOptions) { package: packageAddress, module: 'system', function: 'delete_deny_listed_blob', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'delete_deny_listed_blob', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface EpochArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; } export interface EpochOptions { package?: string; - arguments: EpochArguments | [self: RawTransactionArgument]; + arguments?: EpochArguments | [self?: RawTransactionArgument]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Get epoch. Uses the committee to get the epoch. */ export function epoch(options: EpochOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -917,19 +1604,45 @@ export function epoch(options: EpochOptions) { package: packageAddress, module: 'system', function: 'epoch', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'epoch', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface TotalCapacitySizeArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; } export interface TotalCapacitySizeOptions { package?: string; - arguments: TotalCapacitySizeArguments | [self: RawTransactionArgument]; + arguments?: TotalCapacitySizeArguments | [self?: RawTransactionArgument]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Accessor for total capacity size. */ export function totalCapacitySize(options: TotalCapacitySizeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -937,19 +1650,45 @@ export function totalCapacitySize(options: TotalCapacitySizeOptions) { package: packageAddress, module: 'system', function: 'total_capacity_size', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'total_capacity_size', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface UsedCapacitySizeArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; } export interface UsedCapacitySizeOptions { package?: string; - arguments: UsedCapacitySizeArguments | [self: RawTransactionArgument]; + arguments?: UsedCapacitySizeArguments | [self?: RawTransactionArgument]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Accessor for used capacity size. */ export function usedCapacitySize(options: UsedCapacitySizeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -957,19 +1696,45 @@ export function usedCapacitySize(options: UsedCapacitySizeOptions) { package: packageAddress, module: 'system', function: 'used_capacity_size', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'used_capacity_size', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface NShardsArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; } export interface NShardsOptions { package?: string; - arguments: NShardsArguments | [self: RawTransactionArgument]; + arguments?: NShardsArguments | [self?: RawTransactionArgument]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Accessor for the number of shards. */ export function nShards(options: NShardsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -977,19 +1742,45 @@ export function nShards(options: NShardsOptions) { package: packageAddress, module: 'system', function: 'n_shards', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'n_shards', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface FutureAccountingArguments { - self: RawTransactionArgument; + self?: RawTransactionArgument; } export interface FutureAccountingOptions { package?: string; - arguments: FutureAccountingArguments | [self: RawTransactionArgument]; + arguments?: FutureAccountingArguments | [self?: RawTransactionArgument]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** Read-only access to the accounting ring buffer. */ export function futureAccounting(options: FutureAccountingOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['self']; return (tx: Transaction) => @@ -997,18 +1788,44 @@ export function futureAccounting(options: FutureAccountingOptions) { package: packageAddress, module: 'system', function: 'future_accounting', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'self', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'future_accounting', + parameterIndex: 0, + parameterName: 'self', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface VersionArguments { - system: RawTransactionArgument; + system?: RawTransactionArgument; } export interface VersionOptions { package?: string; - arguments: VersionArguments | [system: RawTransactionArgument]; + arguments?: VersionArguments | [system?: RawTransactionArgument]; + config?: { + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } export function version(options: VersionOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['system']; return (tx: Transaction) => @@ -1016,6 +1833,28 @@ export function version(options: VersionOptions) { package: packageAddress, module: 'system', function: 'version', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments ?? {}, [ + { + index: 0, + name: 'system', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'system', + functionName: 'version', + parameterIndex: 0, + parameterName: 'system', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } diff --git a/packages/walrus/src/contracts/walrus/upgrade.ts b/packages/walrus/src/contracts/walrus/upgrade.ts index 0714973a8..c506c07f2 100644 --- a/packages/walrus/src/contracts/walrus/upgrade.ts +++ b/packages/walrus/src/contracts/walrus/upgrade.ts @@ -18,6 +18,9 @@ import { MoveStruct, normalizeMoveArguments, type RawTransactionArgument, + type ConfigValue, + resolveConfigArgument, + applyConfigArguments, } from '../utils/index.js'; import { bcs } from '@mysten/sui/bcs'; import { type Transaction, type TransactionArgument } from '@mysten/sui/transactions'; @@ -70,7 +73,7 @@ export const EmergencyUpgradeCap = new MoveStruct({ }); export interface VoteForUpgradeArguments { self: RawTransactionArgument; - staking: RawTransactionArgument; + staking?: RawTransactionArgument; auth: TransactionArgument; nodeId: RawTransactionArgument; digest: RawTransactionArgument>; @@ -81,11 +84,15 @@ export interface VoteForUpgradeOptions { | VoteForUpgradeArguments | [ self: RawTransactionArgument, - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, auth: TransactionArgument, nodeId: RawTransactionArgument, digest: RawTransactionArgument>, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Vote for an upgrade given the digest of the package to upgrade to. @@ -93,7 +100,7 @@ export interface VoteForUpgradeOptions { * This will create a new upgrade proposal if none exists for the given digest. */ export function voteForUpgrade(options: VoteForUpgradeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, null, '0x2::object::ID', 'vector'] satisfies ( | string | null @@ -104,12 +111,34 @@ export function voteForUpgrade(options: VoteForUpgradeOptions) { package: packageAddress, module: 'upgrade', function: 'vote_for_upgrade', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 1, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'upgrade', + functionName: 'vote_for_upgrade', + parameterIndex: 1, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface AuthorizeUpgradeArguments { self: RawTransactionArgument; - staking: RawTransactionArgument; + staking?: RawTransactionArgument; digest: RawTransactionArgument>; } export interface AuthorizeUpgradeOptions { @@ -118,13 +147,17 @@ export interface AuthorizeUpgradeOptions { | AuthorizeUpgradeArguments | [ self: RawTransactionArgument, - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, digest: RawTransactionArgument>, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** Authorizes an upgrade that has reached quorum. */ export function authorizeUpgrade(options: AuthorizeUpgradeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'vector'] satisfies (string | null)[]; const parameterNames = ['self', 'staking', 'digest']; return (tx: Transaction) => @@ -132,7 +165,29 @@ export function authorizeUpgrade(options: AuthorizeUpgradeOptions) { package: packageAddress, module: 'upgrade', function: 'authorize_upgrade', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 1, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'upgrade', + functionName: 'authorize_upgrade', + parameterIndex: 1, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface AuthorizeEmergencyUpgradeArguments { @@ -149,6 +204,9 @@ export interface AuthorizeEmergencyUpgradeOptions { emergencyUpgradeCap: RawTransactionArgument, digest: RawTransactionArgument>, ]; + config?: { + walrusPackageId?: string; + }; } /** * Authorizes an upgrade using the emergency upgrade cap. @@ -157,7 +215,7 @@ export interface AuthorizeEmergencyUpgradeOptions { * governance, the EmergencyUpgradeCap should be burned. */ export function authorizeEmergencyUpgrade(options: AuthorizeEmergencyUpgradeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'vector'] satisfies (string | null)[]; const parameterNames = ['upgradeManager', 'emergencyUpgradeCap', 'digest']; return (tx: Transaction) => @@ -170,8 +228,8 @@ export function authorizeEmergencyUpgrade(options: AuthorizeEmergencyUpgradeOpti } export interface CommitUpgradeArguments { upgradeManager: RawTransactionArgument; - staking: RawTransactionArgument; - system: RawTransactionArgument; + staking?: RawTransactionArgument; + system?: RawTransactionArgument; receipt: TransactionArgument; } export interface CommitUpgradeOptions { @@ -180,10 +238,15 @@ export interface CommitUpgradeOptions { | CommitUpgradeArguments | [ upgradeManager: RawTransactionArgument, - staking: RawTransactionArgument, - system: RawTransactionArgument, + staking: RawTransactionArgument | undefined, + system: RawTransactionArgument | undefined, receipt: TransactionArgument, ]; + config?: { + stakingPoolId: ConfigValue; + systemObjectId: ConfigValue; + walrusPackageId?: string; + }; } /** * Commits an upgrade and sets the new package id in the staking and system @@ -194,7 +257,7 @@ export interface CommitUpgradeOptions { * storage nodes and prevent previous package versions from being used. */ export function commitUpgrade(options: CommitUpgradeOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, null, null] satisfies (string | null)[]; const parameterNames = ['upgradeManager', 'staking', 'system', 'receipt']; return (tx: Transaction) => @@ -202,12 +265,51 @@ export function commitUpgrade(options: CommitUpgradeOptions) { package: packageAddress, module: 'upgrade', function: 'commit_upgrade', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 1, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'upgrade', + functionName: 'commit_upgrade', + parameterIndex: 1, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + { + index: 2, + name: 'system', + resolve: () => + resolveConfigArgument( + options.config?.systemObjectId, + { + typeArguments: [], + packageAddress, + moduleName: 'upgrade', + functionName: 'commit_upgrade', + parameterIndex: 2, + parameterName: 'system', + }, + 'systemObjectId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface CleanupUpgradeProposalsArguments { self: RawTransactionArgument; - staking: RawTransactionArgument; + staking?: RawTransactionArgument; proposals: RawTransactionArgument>>; } export interface CleanupUpgradeProposalsOptions { @@ -216,9 +318,13 @@ export interface CleanupUpgradeProposalsOptions { | CleanupUpgradeProposalsArguments | [ self: RawTransactionArgument, - staking: RawTransactionArgument, + staking: RawTransactionArgument | undefined, proposals: RawTransactionArgument>>, ]; + config?: { + stakingPoolId: ConfigValue; + walrusPackageId?: string; + }; } /** * Cleans up the upgrade proposals table. @@ -227,7 +333,7 @@ export interface CleanupUpgradeProposalsOptions { * current version. */ export function cleanupUpgradeProposals(options: CleanupUpgradeProposalsOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null, null, 'vector>'] satisfies (string | null)[]; const parameterNames = ['self', 'staking', 'proposals']; return (tx: Transaction) => @@ -235,7 +341,29 @@ export function cleanupUpgradeProposals(options: CleanupUpgradeProposalsOptions) package: packageAddress, module: 'upgrade', function: 'cleanup_upgrade_proposals', - arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + arguments: normalizeMoveArguments( + applyConfigArguments(options.arguments, [ + { + index: 1, + name: 'staking', + resolve: () => + resolveConfigArgument( + options.config?.stakingPoolId, + { + typeArguments: [], + packageAddress, + moduleName: 'upgrade', + functionName: 'cleanup_upgrade_proposals', + parameterIndex: 1, + parameterName: 'staking', + }, + 'stakingPoolId', + ), + }, + ]), + argumentsTypes, + parameterNames, + ), }); } export interface BurnEmergencyUpgradeCapArguments { @@ -246,6 +374,9 @@ export interface BurnEmergencyUpgradeCapOptions { arguments: | BurnEmergencyUpgradeCapArguments | [emergencyUpgradeCap: RawTransactionArgument]; + config?: { + walrusPackageId?: string; + }; } /** * Burns the emergency upgrade cap. @@ -254,7 +385,7 @@ export interface BurnEmergencyUpgradeCapOptions { * make upgrades fully reliant on quorum-based governance. */ export function burnEmergencyUpgradeCap(options: BurnEmergencyUpgradeCapOptions) { - const packageAddress = options.package ?? '@local-pkg/walrus'; + const packageAddress = options.package ?? options.config?.walrusPackageId ?? '@local-pkg/walrus'; const argumentsTypes = [null] satisfies (string | null)[]; const parameterNames = ['emergencyUpgradeCap']; return (tx: Transaction) => diff --git a/packages/walrus/sui-codegen.config.ts b/packages/walrus/sui-codegen.config.ts index c021c225a..37f317b3a 100644 --- a/packages/walrus/sui-codegen.config.ts +++ b/packages/walrus/sui-codegen.config.ts @@ -13,6 +13,11 @@ const config: SuiCodegenConfig = { { package: '@local-pkg/walrus', path: '../../../walrus/contracts/walrus', + configArguments: { + walrusPackageId: { package: '@local-pkg/walrus' }, + systemObjectId: { type: 'system::System' }, + stakingPoolId: { type: 'staking::Staking' }, + }, }, ], };