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 generated code will be written
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
configArgumentsConfigArguments-Map function parameters and package addresses to a runtime config object, shared by all packages
importExtension'.js' | '.ts' | '''.js'File extension used in generated import statements
includePhantomTypeParametersbooleanfalseInclude phantom type parameters as function arguments in generated BCS types

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
configArgumentsConfigArgumentsnoPackage-scoped config argument matchers
{
  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 MVR name
packageNamestringyesName for the generated code directory
network'mainnet' | 'testnet'yesNetwork to fetch the package from
generatePackageGenerateOptionsnoControl what gets generated from this package
configArgumentsConfigArgumentsnoPackage-scoped config argument matchers
{
  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

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

const config: SuiCodegenConfig = {
	output: './src/contracts',
	packages: [
		{
			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: 'registry::Registry' },
				// Generic type without type arguments: matches every instantiation, and the
				// config value must be a resolver function
				pool: { type: 'pool::Pool' },
				// Fully instantiated generic: only matches parameters concretely typed with
				// 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. 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
				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' },
			},
		},
	],
};

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<T> or a nested uninstantiated generic) are not supported. Use an uninstantiated matcher with a resolver function instead.

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

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:

export interface BorrowOptions {
	package?: string;
	arguments:
		| BorrowArguments
		| [
				pool: RawTransactionArgument<string> | undefined,
				amount: RawTransactionArgument<number | bigint>,
		  ];
	config?: {
		pool: (ctx: ConfigResolverContext) => string | TransactionObjectArgument;
		corePackageId?: string;
	};
	typeArguments: [string];
}

In the tuple form of arguments, a matched position followed by a required one accepts an explicit 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, 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:

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
	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]],
	corePackageId: '0xabc...',
} satisfies CoreConfig;

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

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

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. 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. For nameless (onchain bytecode) signatures, 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 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 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

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 will cause parsing to fail. See the Core API docs 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.

Local packages

For local packages using @local-pkg/* identifiers, configure your client with package overrides:

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',
			},
		},
	},
});

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',
	},
};

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,
					},
				},
			},
		});
	},
});

On this page