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 generated code will be written |
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 |
configArguments | ConfigArguments | - | 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 |
includePhantomTypeParameters | boolean | false | Include 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
| 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 |
{
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 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 |
{
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
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:
- 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 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,
},
},
},
});
},
});Related resources
- create-dapp - Bootstrap a working app with codegen already configured
- Sui Move documentation
- BCS documentation
- Transaction building
- dApp Kit