llms.txt
@mysten/sui v2.0 and a new dApp Kit are here! Check out the migration guide
Mysten Labs SDKs

Sui TypeScript Codegen

Generate type-safe TypeScript bindings from onchain Sui Move packages.

The @mysten/codegen package automatically generates type-safe TypeScript code from your Move packages, enabling seamless interaction with your smart contracts from TypeScript applications.

This package is currently in development and might have breaking changes.

Features

  • Type-safe Move calls: Generate TypeScript functions with full type safety for calling your Move functions
  • BCS type definitions: Automatic BCS struct definitions for parsing onchain data
  • Auto-completion: IDE support with intelligent code completion for Move function arguments
  • Package resolution: Support for both MVR-registered packages and local packages

Installation

Install the codegen package as a dev dependency:

npm install -D @mysten/codegen

Quick start

Step 1: Create a configuration file

Create a sui-codegen.config.ts file in your project root:

import type { SuiCodegenConfig } from '@mysten/codegen';

const config: SuiCodegenConfig = {
	output: './src/contracts',
	packages: [
		{
			package: '@local-pkg/counter',
			path: './move/counter',
		},
	],
};

export default config;

Step 2: Generate TypeScript code

Add a script to your package.json:

{
	"scripts": {
		"codegen": "sui-ts-codegen generate"
	}
}

Then run:

pnpm codegen

This generates TypeScript code in your configured output directory (for example, ./src/contracts).

Configuration options

The SuiCodegenConfig type supports the following options:

OptionTypeDefaultDescription
outputstring-The directory where codegen writes generated code
packagesPackageConfig[]-Array of Move packages to generate code for
prunebooleantrueWhen enabled, only generates code for the main package and omits dependency modules (dependency types referenced by included types are still generated under deps/)
generateSummariesbooleantrueAutomatically run sui move summary before generating code. Creates a package_summaries directory in your Move package which can be added to .gitignore
generateGenerateOptions-Default generate options (types, functions) for all packages
importExtension'.js' | '.ts' | '''.js'File extension used in generated import statements
includePhantomTypeParametersbooleanfalseInclude phantom type parameters as function arguments in generated BCS types
fullnodeUrlsnetwork URL mapSui public fullnodesOverride the Mainnet or Testnet fullnode used to fetch onchain package summaries

Package configuration

Each entry in the packages array configures a Move package to generate code from. Packages can be local (from source) or onchain (fetched from a network).

Local packages

OptionTypeRequiredDescription
packagestringyesPackage identifier (for example, @local-pkg/my-package)
pathstringyesPath to the Move package directory
packageNamestringnoCustom name for generated code directory
generatePackageGenerateOptionsnoControl what gets generated from this package
configArgumentsConfigArgumentsnoMap this package's function parameters to a runtime config object
bcsOverridesBcsOverridesnoReplace generated BCS types with custom types
{
  package: '@local-pkg/my-package',
  path: './move/my-package',
}

Onchain packages

For packages already deployed onchain, generate code directly from a package ID or MVR name without needing local source code:

OptionTypeRequiredDescription
packagestringyesPackage ID or name emitted into generated code
sourcePackageIdstringnoPackage ID to fetch when generating an unpublished package name
packageNamestringyesName for the generated code directory
network'mainnet' | 'testnet'yesNetwork to resolve and fetch the package from
generatePackageGenerateOptionsnoControl what gets generated from this package
configArgumentsConfigArgumentsnoMap this package's function parameters to a runtime config object
bcsOverridesBcsOverridesnoReplace generated BCS types with custom types
{
  package: '0xabf837e98c26087cba0883c0a7a28326b1fa3c5e1e2c5abdb486f9e8f594c837',
  packageName: 'pyth',
  network: 'testnet',
}

The generate option

The generate option controls what code is produced. It can be set at the global level (as a default for all packages), at the per-package level, and at the per-module level. More specific settings override less specific ones.

When no generate option is set, everything is generated (all types and functions). Package-level types and functions also default to true. In the record form of modules, per-module types and functions default to false, so you opt in to exactly what you need from each module. Use true as a shorthand to include everything from a module with package-level defaults.

At the global and package levels, types and functions only accept boolean values (or an object for functions). Name-based filtering with string[] is only available at the module level inside the record form of modules, where the filter applies unambiguously to a single module.

// Global or package level
generate: {
  types: true | false,
  functions: true | false | { private: boolean | 'entry' },
  modules: string[] | Record<string, true | { types?, functions? }>,  // package-level only
}

// Module level (inside the record form of modules)
modules: {
  my_module: true,  // shorthand for "include everything"
  other_module: {
    types: true | false | string[],
    functions: true | false | string[] | { private: boolean | 'entry' },
  }
}

Types

Controls which BCS type definitions (structs and enums) are generated:

  • true: generate all types
  • false: skip type generation
  • string[]: generate only the listed types by name (module level only)

Functions

Controls which Move function wrappers are generated:

  • true: generate all public functions and private entry functions
  • false: skip function generation
  • string[]: generate only the listed functions by name; includes private functions (module level only)
  • { private: 'entry' }: generate public functions plus private entry functions
  • { private: true }: generate all functions including private
  • { private: false }: only generate public functions

Modules

Controls which modules from the package are included. Only available at the package level, not at the global level.

  • Not set (default): include all modules
  • string[]: only include the listed modules
  • Record<string, true | { types?, functions? }>: only include the listed modules, with per-module overrides for types and functions. Use true as a shorthand to include everything from a module with package-level defaults

Examples

Only generate code from specific modules of the Sui framework:

{
  package: '0x0000000000000000000000000000000000000000000000000000000000000002',
  packageName: '0x2',
  network: 'testnet',
  generate: {
    modules: ['kiosk', 'kiosk_extension', 'transfer_policy'],
  },
}

Only generate a single type from a dependency (functions are omitted automatically because generate is configured and functions is not specified):

{
  package: '0xabf837e98c26087cba0883c0a7a28326b1fa3c5e1e2c5abdb486f9e8f594c837',
  packageName: 'pyth',
  network: 'testnet',
  generate: {
    modules: {
      state: { types: ['State'] },
    },
  },
}

Generate specific types and functions from individual modules:

{
  package: '@local-pkg/my-package',
  path: './move/my-package',
  generate: {
    modules: {
      token: {
        types: ['Token', 'TokenMetadata'],
        functions: ['mint', 'burn', 'transfer'],
      },
      admin: {
        types: true,
        functions: ['initialize'],
      },
    },
  },
}

Generate all types but include all private functions for a local package:

{
  package: '@local-pkg/my-package',
  path: './move/my-package',
  generate: {
    functions: { private: true },
  },
}

Dependency pruning

The global prune option (default: true) controls whether dependency packages are included in the output. Even when pruning is enabled, dependency types referenced by your included types are still generated under deps/:

src/contracts/
├── mypackage/
│   ├── module_a.ts
│   ├── module_b.ts
│   └── deps/
│       └── 0x2/
│           └── balance.ts     # Auto-included dependency type
└── utils/
    └── index.ts               # Shared utilities (always generated)

Set prune: false to generate all dependency modules with their full types and functions.

The configArguments option

SDKs built on generated bindings often spend a lot of wrapper code passing values from a per-network config object (package IDs, registry or treasury object IDs, pool addresses) into generated function calls. The configArguments option moves that mapping into codegen: declare which Move types (or package addresses) come from a config object, and the generated functions accept that config object directly.

configArguments is set on a package entry and maps config keys to matchers. Matchers reference types as module::Type, optionally qualified with a package identifier from the packages config or an address:

const config: SuiCodegenConfig = {
	output: './src/contracts',
	packages: [
		{
			package: '@myapp/core',
			path: './move/core',
			configArguments: {
				// Every parameter of this type resolves from `config.registry`
				registry: { type: 'registry::Registry' },
				// Generic type: matches every instantiation, config value must be a resolver function
				pool: { type: 'pool::Pool' },
				// Instantiated generic: only matches this exact instantiation
				suiPool: { type: 'pool::Pool<0x2::sui::SUI>' },
				// Function matcher: configures a single function's parameter
				adminCap: { function: 'admin::set_fees', parameterName: 'cap' },
				// Package entry: adds a 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 are referenced by their package identifier
				vaultPool: { type: '@myapp/core::pool::Pool' },
			},
		},
	],
};

Matcher rules:

  • A bare module::Type refers to a type from the package the config is declared on.
  • Types from other packages in the run use their package identifier (for example, @myapp/core::pool::Pool) and must be dependencies of the declaring package.
  • Types can also be referenced by an explicit address (for example, 0x2::sui::SUI). Addresses aren't validated, so only use addresses that are valid on every network your generated code targets.
  • Partially instantiated generics like Pool<T> are not supported. Use an uninstantiated matcher with a resolver function instead.
  • Function matchers can target a parameter by parameterName or parameterIndex. Both can be omitted when the function has a single argument.
  • A key can declare an array of matchers. If they span multiple types, the config value must be a resolver function.
  • Invalid matchers fail at generation time, and keys that never match any generated parameter produce a warning.

Generated output

For each function with matched parameters, the generated options gain an optional config property typed with the keys that function uses. Matched parameters become optional in arguments, and an explicitly passed argument always takes precedence over the config value:

export interface BorrowOptions {
	package?: string;
	arguments: BorrowArguments; // `pool` is optional here
	config?: {
		pool: (ctx: ConfigResolverContext) => string | TransactionObjectArgument;
		corePackageId?: string;
	};
	typeArguments: [string];
}

export function borrow(options: BorrowOptions) {
	const packageAddress = options.package ?? options.config?.corePackageId ?? '@myapp/core';
	// ...
	pool: options.arguments?.pool ?? options.config?.pool?.({ ... }),
}

Keys bound to a single concrete type accept a plain value (an object ID or a transaction argument). Keys that match a generic type, or multiple types, must be resolver functions, because a single ID can't be correct for every instantiation. A resolver can also return a transaction callback ((ctx) => (tx) => ...) to build the argument dynamically. Resolvers receive the matched parameter's own type arguments plus call-site metadata:

export interface ConfigResolverContext {
	typeArguments: string[]; // canonical for concrete instantiations; as-provided for generic positions
	packageAddress: string;
	moduleName: string;
	functionName: string;
	parameterName?: string; // Move parameter name, when the summary includes names
	parameterIndex: number; // position in the generated function's arguments
}

This makes resolvers reusable across functions that use the type in different positions:

const myConfig = {
	registry: '0x123...',
	pool: (ctx: ConfigResolverContext) => poolsByCoinType[ctx.typeArguments[0]],
	suiPool: '0x456...',
	adminCap: '0x789...',
	corePackageId: '0xabc...',
} satisfies CoreConfig;

tx.add(
	borrow({
		arguments: { amount: 100n },
		config: myConfig,
		typeArguments: ['0x2::sui::SUI'],
	}),
);

Each package's output also includes a config-arguments.ts file with an interface covering the package's config keys, named after the package's packageName (for example, packageName: 'core' produces CoreConfig). Use it with satisfies when defining your config object, as shown above. An SDK spanning multiple packages can check one shared config object against the intersection of the per-package interfaces:

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<T>), 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:

configArguments: {
	basePool: { type: 'pool::Pool', parameterName: 'base_pool' },
	quotePool: { type: 'pool::Pool', parameterName: 'quote_pool' },
},

Parameter names are only available in summaries generated from local packages. For onchain packages without parameter names, use function matchers with parameterIndex to target individual parameters.

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 networks where the package's MVR name doesn't resolve, supply the deployed package ID through the config object. Package entries only apply to the main package's generated modules, and for packages generated without an MVR name or address package remains required.

The bcsOverrides option

Generated BCS types mirror the raw Move layout, but the raw layout is often not the shape an SDK wants to expose: a u64 field might be a 1e9 fixed-point price, or a struct like i64::I64 { magnitude: u64, is_negative: bool } is really a signed integer. The bcsOverrides option replaces generated BCS types with custom ones — typically built with transform — so parsed values come out in the shape you want and inputs accept it.

bcsOverrides is set on a package entry as an array. Every entry names the Move type it replaces and the source module the replacement is imported from, and can optionally narrow the entry to specific fields:

const config: SuiCodegenConfig = {
	output: './src/contracts',
	packages: [
		{
			package: '@local-pkg/deepbook_predict',
			path: './move/deepbook_predict',
			bcsOverrides: [
				// Replace a datatype's generated declaration: every generated layout that
				// references it uses the custom type. Imports `I64` from the source module.
				{ type: 'fixed_math::i64::I64', source: './src/bcs/i64.ts' },

				// `fields` narrows an entry to the field sites matching a glob. Entries are tried
				// in declaration order and the first match wins, so narrow entries go first.
				{ type: 'u64', fields: 'order::*.*_price', source: './src/bcs/units.ts#Price9' },

				// A type with no generated declaration is replaced wherever it is rendered —
				// here, every remaining `u64` parses to a bigint instead of a decimal string.
				{ type: 'u64', source: './src/bcs/integers.ts#U64' },

				// A whole-type match wins over its elements, so this replaces the vector itself
				// rather than letting the `u64` rule apply to its items.
				{ type: 'vector<u64>', source: './src/bcs/units.ts#Payouts' },
			],
		},
	],
};

The replacement module exports plain BCS types:

// ./src/bcs/units.ts
import { bcs } from '@mysten/sui/bcs';

/** 1e9 fixed-point u64 exposed as a decimal number. */
const Price9 = bcs.u64().transform({
	input: (value: number) => BigInt(Math.round(value * 1e9)).toString(),
	output: (raw) => Number(raw) / 1e9,
});

export const Payouts = bcs.vector(Price9);

The custom type's inferred input and output types flow through the generated code, so Node.parse(bytes).levels is a number[] and Node.serialize accepts one.

Entry rules:

  • type uses the same package scoping as configArguments matchers (bare module::Type for the declaring package, package identifiers, explicit addresses), and additionally accepts named-address labels from the package's summaries (for example, fixed_math::i64::I64) so entries can target dependency packages that are not codegen-run entries.
  • A datatype entry written without type arguments or fields replaces the type's generated declaration: the module exports the custom type under the Move type's name, and every use picks it up because uses already reference the declaration. For a generic type the source must export a function (...typeParameters: BcsType<any>[]) => BcsType<any> mirroring the generated call convention.
  • Every other entry replaces the type wherever it is rendered. That covers types with no declaration to replace (primitives, vector, and the stdlib types generated layouts serialize inline — String, Option, ID/UID), a single instantiation of a generic (pool::Pool<0x2::sui::SUI>), and any entry narrowed with fields.
  • Replacement applies at any depth, because the type renderer consults overrides as it recurses. An override on u64 also replaces the u64 inside vector<u64> and Option<u64>. A whole-type match wins over its elements, so an entry for vector<u64> replaces the vector rather than its items.
  • fields restricts an entry to the field sites matching one glob, where * matches any run of characters. Sites are written module::Type.field, or module::Type.variant.field for enum variant fields (positional fields are named pos0, pos1, …). A glob that names no module — Order.*_price — is matched against the Type.field suffix, so it applies in every module. An entry with fields never replaces a declaration, since a declaration exists once for every use.
  • At each use site, matching entries are tried in declaration order and the first match wins. An unrestricted datatype declaration remains the fallback for sites not matched by an earlier field-restricted entry. There is no specificity scoring, so put narrow entries before broad ones.
  • source is an import specifier, optionally suffixed with #ExportName. Relative specifiers resolve against the config file's directory and are rewritten into relative imports from each generated file, with their extension replaced by the configured importExtension (so a ./src/bcs/units.ts source is imported as units.js by default); bare package specifiers are emitted as-is. Without a fragment, datatype entries import the Move type's name; entries whose type is a primitive or vector must include one.
  • Replacement modules must be self-contained: a module whose export replaces a declaration can't import generated modules that reference the replaced type, because that would create a circular import. Declare the raw layout inline when building a transform.
  • A replacement built with .transform() is a plain BcsType, so a type replaced that way loses the MoveStruct helpers (typeTag(), resolveTypeTag(), get(), getMany()). Extend MoveStruct instead if the replaced type needs them.
  • An entry that matches nothing is not an error. A shared set of overrides can be applied to several packages even when only some of them have fields of a given type.

Overrides only affect generated BCS layouts (parsing and serializing structs, enums, and events, including MoveStruct.get()). Generated transaction-building functions serialize their arguments from Move type tags at runtime, so pure function arguments are unaffected.

Phantom types

In Move, phantom type parameters are type parameters that only appear at the type level and don't affect the runtime data layout of a struct. For example, Balance<T> has a phantom type parameter T that indicates the coin type, but the actual serialized data only contains a u64 value:

public struct Balance<phantom T> has store {
    value: u64,
}

Default behavior

By default, codegen excludes phantom type parameters from the generated BCS type functions because they don't affect serialization. The generated type is a constant rather than a function:

export const Balance = new MoveStruct({
	name: `${$moduleName}::Balance<phantom T>`,
	fields: {
		value: bcs.u64(),
	},
});

This works correctly for parsing onchain data because phantom types don't change the binary layout.

With the default behavior, phantom parameters appear as literals in the type name (for example, Balance<phantom T>). These names are useful for debugging but are not valid onchain type tags. Use the typeTag method to build valid type tags with the phantom parameters filled in.

Including phantom type parameters

If you need the phantom type parameters as function arguments (for example, to preserve type information for other tooling), enable includePhantomTypeParameters:

const config: SuiCodegenConfig = {
	output: './src/contracts',
	includePhantomTypeParameters: true,
	packages: [
		// ...
	],
};

With this option enabled, phantom type parameters become function arguments:

export function Balance<T extends BcsType<any>>(T: T) {
	return new MoveStruct({
		name: `${$moduleName}::Balance<${T.name}>` as const,
		fields: {
			value: bcs.u64(),
		},
	});
}

Using generated code

Calling Move functions

The generated code provides type-safe functions for calling Move functions:

import { Transaction } from '@mysten/sui/transactions';
import * as counter from './contracts/counter/counter';

// Increment a counter
const tx = new Transaction();
tx.add(
	counter.increment({
		arguments: {
			counter: '0x123...', // Counter object ID
		},
	}),
);

Parsing BCS data

Use generated BCS types to parse onchain object data. Fetch the object with include: { content: true } and pass object.content to the generated type's .parse() method:

import { Counter as CounterStruct } from './contracts/counter/counter';

async function readCounter(client: ClientWithCoreApi, id: string) {
	const { object } = await client.core.getObject({
		objectId: id,
		include: { content: true },
	});

	// Parse the Move struct fields from BCS content
	const parsed = CounterStruct.parse(object.content);
	console.log('Counter value:', parsed.value);
	console.log('Counter owner:', parsed.owner);

	return parsed;
}

Always use content, not objectBcs, when parsing with generated types. The objectBcs field contains a full object envelope with additional metadata that causes parsing to fail. See Querying data for details.

Getting type tags

Generated types build their own type tag strings with the typeTag method, so you don't hand-write strings like `${packageId}::module::Name<${coinType}>`. By default the tag uses the package the type was generated from — a real address for framework types, or the configured name for a local or MVR package:

import { Counter } from './contracts/counter/counter';
import { Balance } from './contracts/counter/deps/sui/balance';

Balance.typeTag({ typeArguments: ['0x2::sui::SUI'] });
// '0x2::balance::Balance<0x2::sui::SUI>'

Counter.typeTag();
// '@local-pkg/counter::counter::Counter'

Types with phantom type parameters require typeArguments; types without them take none. This is enforced at compile time:

Counter.typeTag(); // ok — no type parameters
Balance.typeTag({ typeArguments: ['0x2::sui::SUI'] }); // ok

// @ts-expect-error — Balance has a phantom parameter, typeArguments is required
Balance.typeTag();

typeArguments is the full positional list, in Move declaration order. Each entry is a type tag string, another typeTag() result, or a BCS type (its name is used):

import { bcs } from '@mysten/sui/bcs';

Balance.typeTag({ typeArguments: [bcs.u64()] });
// '0x2::balance::Balance<u64>'

To override the package identifier — for example, to pin a specific published address — pass package:

Counter.typeTag({ package: '0xPACKAGE_ID' });
// '0xPACKAGE_ID::counter::Counter'

Resolving type tags

For a local or MVR package, typeTag returns the configured name (@local-pkg/counter::…). That is valid in transaction typeArguments — it resolves automatically when the transaction is built — but query filters and comparisons against onchain data need a resolved, address-only tag. resolveTypeTag takes the same options as typeTag plus a client, resolves any names through it, and normalizes the result:

const counterType = await Counter.resolveTypeTag({ client });
// '0x0000…0123::counter::Counter'

const balanceType = await Balance.resolveTypeTag({
	client,
	typeArguments: ['0x2::sui::SUI'],
});
// '0x0000…0002::balance::Balance<0x0000…0002::sui::SUI>'

Client configuration

Using with MVR (Move Version Registry)

If your package is registered on MVR, the generated code works without additional configuration. The configured name is preserved in generated function targets and type tags. At runtime MVR resolves function packages and type origins independently for the client's network.

Local packages

For local packages using @local-pkg/* identifiers, configure package overrides for Move calls and first-level type overrides for each local type used as a transaction type argument or resolved for a query:

import { SuiGrpcClient } from '@mysten/sui/grpc';

const client = new SuiGrpcClient({
	network: 'testnet',
	baseUrl: 'https://fullnode.testnet.sui.io:443',
	mvr: {
		overrides: {
			packages: {
				'@local-pkg/counter': '0xYOUR_PACKAGE_ID',
			},
			types: {
				'@local-pkg/counter::counter::Counter': '0xTYPE_ORIGIN::counter::Counter',
			},
		},
	},
});

With dApp Kit

Configure package overrides when creating your dApp Kit instance:

import { createDAppKit } from '@mysten/dapp-kit-core';
import { SuiGrpcClient } from '@mysten/sui/grpc';

const GRPC_URLS = {
	testnet: 'https://fullnode.testnet.sui.io:443',
};

const PACKAGE_IDS = {
	testnet: {
		counter: '0xYOUR_PACKAGE_ID',
		counterType: '0xTYPE_ORIGIN::counter::Counter',
	},
};

const dAppKit = createDAppKit({
	networks: ['testnet'],
	createClient: (network) => {
		return new SuiGrpcClient({
			network,
			baseUrl: GRPC_URLS[network],
			mvr: {
				overrides: {
					packages: {
						'@local-pkg/counter': PACKAGE_IDS[network].counter,
					},
					types: {
						'@local-pkg/counter::counter::Counter': PACKAGE_IDS[network].counterType,
					},
				},
			},
		});
	},
});

On this page