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/codegenQuick 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 codegenThis generates TypeScript code in your configured output directory (for example, ./src/contracts).
Configuration options
The SuiCodegenConfig type supports the following options:
| Option | Type | Default | Description |
|---|---|---|---|
output | string | - | The directory where codegen writes generated code |
packages | PackageConfig[] | - | Array of Move packages to generate code for |
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 (types, functions) for all packages |
importExtension | '.js' | '.ts' | '' | '.js' | File extension used in generated import statements |
includePhantomTypeParameters | boolean | false | Include phantom type parameters as function arguments in generated BCS types |
fullnodeUrls | network URL map | Sui public fullnodes | Override 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
| 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 | Map this package's function parameters to a runtime config object |
bcsOverrides | BcsOverrides | no | Replace 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:
| Option | Type | Required | Description |
|---|---|---|---|
package | string | yes | Package ID or name emitted into generated code |
sourcePackageId | string | no | Package ID to fetch when generating an unpublished package name |
packageName | string | yes | Name for the generated code directory |
network | 'mainnet' | 'testnet' | yes | Network to resolve and fetch the package from |
generate | PackageGenerateOptions | no | Control what gets generated from this package |
configArguments | ConfigArguments | no | Map this package's function parameters to a runtime config object |
bcsOverrides | BcsOverrides | no | Replace 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 typesfalse: skip type generationstring[]: 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 functionsfalse: skip function generationstring[]: 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 modulesRecord<string, true | { types?, functions? }>: only include the listed modules, with per-module overrides fortypesandfunctions. Usetrueas 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::Typerefers 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
parameterNameorparameterIndex. 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:
- An explicit
options.packageargument - The config key declared by the package entry (for example,
config.corePackageId) - 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:
typeuses the same package scoping asconfigArgumentsmatchers (baremodule::Typefor 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
fieldsreplaces 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 withfields. - Replacement applies at any depth, because the type renderer consults overrides as it recurses.
An override on
u64also replaces theu64insidevector<u64>andOption<u64>. A whole-type match wins over its elements, so an entry forvector<u64>replaces the vector rather than its items. fieldsrestricts an entry to the field sites matching one glob, where*matches any run of characters. Sites are writtenmodule::Type.field, ormodule::Type.variant.fieldfor enum variant fields (positional fields are namedpos0,pos1, …). A glob that names no module —Order.*_price— is matched against theType.fieldsuffix, so it applies in every module. An entry withfieldsnever 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.
sourceis 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 configuredimportExtension(so a./src/bcs/units.tssource is imported asunits.jsby default); bare package specifiers are emitted as-is. Without a fragment, datatype entries import the Move type's name; entries whosetypeis a primitive orvectormust 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 plainBcsType, so a type replaced that way loses theMoveStructhelpers (typeTag(),resolveTypeTag(),get(),getMany()). ExtendMoveStructinstead 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,
},
},
},
});
},
});Related resources
- create-dapp - Bootstrap a working app with codegen already configured
- Sui Move documentation
- BCS documentation
- Transaction building
- dApp Kit