@suisei-mcp/agent-signer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # @suisei-mcp/agent-signer
2
+
3
+ The non-custodial signer for a **Tier-1 Sui agent wallet**.
4
+
5
+ [`@suisei-mcp/mcp`](../mcp) builds *unsigned* transaction
6
+ bytes and never holds a key. This package is the one piece that does: it
7
+ generates the agent's keypair, stores it **encrypted on your machine**, and
8
+ signs builder bytes. The plaintext key is created here, used here, and
9
+ **never crosses a process boundary** — it never travels through an MCP
10
+ response or into an LLM's context.
11
+
12
+ ## What "Tier-1 agent wallet" means
13
+
14
+ A real Sui wallet the agent fully controls, funded with a small allowance.
15
+ The agent can stake, transfer, swap — anything — but only up to its
16
+ balance. Blast radius = what you fund. Refill to extend, sweep to revoke.
17
+ Your main wallet is never touched: the agent has its own freshly-generated
18
+ key, and your owner key stays in your real wallet.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install -g @suisei-mcp/agent-signer
24
+ ```
25
+
26
+ ## Use
27
+
28
+ Set a passphrase (encrypts the key at rest) and create the wallet:
29
+
30
+ ```bash
31
+ export AGENT_WALLET_PASSPHRASE="something-long-and-private"
32
+ agent-signer create
33
+ # -> { "address": "0x…", "path": "~/.suisei/agent-wallet.json" }
34
+ ```
35
+
36
+ Then the loop with Claude + the MCP:
37
+
38
+ 1. Ask the agent to fund it: it calls `agent_wallet_fund` (owner-signed) so
39
+ your real wallet sends SUI to the agent address.
40
+ 2. Ask the agent to act, e.g. *"stake 1 SUI to the top validator from my
41
+ agent wallet"* — it builds the tx and gives you `tx_bytes_base64`.
42
+ 3. Sign with the agent key:
43
+ ```bash
44
+ agent-signer sign <tx_bytes_base64>
45
+ # -> { "signature": "…" }
46
+ ```
47
+ 4. Tell the agent to `sui_execute_signed_tx` with that signature.
48
+
49
+ To revoke, ask the agent for `agent_wallet_sweep` (drains the wallet back to
50
+ you), sign it, submit, and stop funding.
51
+
52
+ ## You own the key — back it up or move it
53
+
54
+ The wallet isn't locked inside this tool. The private key is yours,
55
+ encrypted under your passphrase, and you can take it out anytime:
56
+
57
+ ```bash
58
+ agent-signer export
59
+ # -> { "address": "0x…", "secret_key": "suiprivkey1…", "warning": "…" }
60
+ ```
61
+
62
+ That `suiprivkey1…` is a standard Sui secret. Import it into **Sui Wallet**,
63
+ **Suiet**, or the CLI (`sui keytool import "<suiprivkey1…>" ed25519`) and
64
+ you have the same wallet there. Back it up offline — anyone holding it
65
+ controls the wallet.
66
+
67
+ Bring your own key instead of generating one:
68
+
69
+ ```bash
70
+ agent-signer import "suiprivkey1…"
71
+ ```
72
+
73
+ Because the key is exportable, losing the keystore file isn't fatal **if
74
+ you backed up the export**. If you didn't and you lose the file or
75
+ passphrase, the agent wallet is gone — but it only ever holds an allowance,
76
+ so create a fresh one and refund. Your owner wallet is untouched.
77
+
78
+ ## Configuration
79
+
80
+ | Env / flag | Default | Meaning |
81
+ | --- | --- | --- |
82
+ | `AGENT_WALLET_PASSPHRASE` / `--passphrase` | — (required) | Encrypts/decrypts the key |
83
+ | `AGENT_WALLET_PATH` / `--path` | `~/.suisei/agent-wallet.json` | Keystore location |
84
+
85
+ ## Security model
86
+
87
+ - **Ed25519** key, sealed with **AES-256-GCM** under a **scrypt**-derived
88
+ key (N=32768). Keystore written `0600`.
89
+ - Wrong passphrase → decryption fails closed (GCM auth tag).
90
+ - `create`/`address` emit only the public address; `sign` emits only a
91
+ signature. The raw secret is revealed only by an explicit `export` (with
92
+ a warning) — never incidentally, and never to the MCP or an agent.
93
+ - Lose the keystore or passphrase and the agent wallet is gone — by design
94
+ it holds only an allowance, so create a fresh one and refund. Your owner
95
+ wallet (in your real wallet app) is unaffected.
96
+
97
+ This is **Tier 1** of the agent-wallet design. On-chain policy limits
98
+ (Tier 2) and a multisig co-signer (Tier 3) layer on top — see
99
+ [`docs/AGENT_WALLET_DESIGN.md`](../../docs/AGENT_WALLET_DESIGN.md).
100
+
101
+ ## License
102
+
103
+ MIT.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ import { createWallet, walletAddress, signTxBytes, exportSecret, importWallet, defaultPath, } from './index.js';
3
+ /**
4
+ * agent-signer CLI — the manual signing path for a Tier-1 agent wallet.
5
+ *
6
+ * agent-signer create [--overwrite] generate + encrypt a new agent key
7
+ * agent-signer import <suiprivkey> seal an existing key as the wallet
8
+ * agent-signer export reveal the raw key (backup / import elsewhere)
9
+ * agent-signer address print the agent wallet address
10
+ * agent-signer sign <txBytesBase64> sign builder bytes -> base64 signature
11
+ *
12
+ * Passphrase comes from AGENT_WALLET_PASSPHRASE (preferred) or --passphrase.
13
+ * Keystore path from AGENT_WALLET_PATH or --path (default ~/.suisei/agent-wallet.json).
14
+ *
15
+ * Typical loop with Claude + the MCP:
16
+ * 1. agent builds a tx -> tx_bytes_base64
17
+ * 2. agent-signer sign <tx_bytes_base64> -> copy the "signature"
18
+ * 3. tell the agent to sui_execute_signed_tx with that signature
19
+ */
20
+ function flag(name) {
21
+ const i = process.argv.indexOf(`--${name}`);
22
+ return i !== -1 ? process.argv[i + 1] : undefined;
23
+ }
24
+ function has(name) {
25
+ return process.argv.includes(`--${name}`);
26
+ }
27
+ function getPassphrase() {
28
+ const p = process.env.AGENT_WALLET_PASSPHRASE ?? flag('passphrase');
29
+ if (!p) {
30
+ fail('No passphrase. Set AGENT_WALLET_PASSPHRASE or pass --passphrase <value>. ' +
31
+ 'This encrypts the agent key at rest.');
32
+ }
33
+ return p;
34
+ }
35
+ function getPath() {
36
+ return flag('path') ?? defaultPath();
37
+ }
38
+ function out(obj) {
39
+ process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
40
+ }
41
+ function fail(msg) {
42
+ process.stderr.write(`error: ${msg}\n`);
43
+ process.exit(1);
44
+ }
45
+ async function main() {
46
+ const cmd = process.argv[2];
47
+ switch (cmd) {
48
+ case 'create': {
49
+ const res = createWallet({
50
+ passphrase: getPassphrase(),
51
+ path: getPath(),
52
+ overwrite: has('overwrite'),
53
+ });
54
+ out({
55
+ created: true,
56
+ address: res.address,
57
+ path: res.path,
58
+ next_step: 'Fund it with the agent_wallet_fund MCP tool (owner-signed), then the agent can spend up to its balance.',
59
+ });
60
+ break;
61
+ }
62
+ case 'import': {
63
+ const secret = process.argv[3];
64
+ if (!secret || secret.startsWith('--')) {
65
+ fail('Usage: agent-signer import <suiprivkey1...>');
66
+ }
67
+ const res = importWallet({
68
+ passphrase: getPassphrase(),
69
+ secret,
70
+ path: getPath(),
71
+ overwrite: has('overwrite'),
72
+ });
73
+ out({ imported: true, address: res.address, path: res.path });
74
+ break;
75
+ }
76
+ case 'export': {
77
+ const { address, secretBech32 } = exportSecret({
78
+ passphrase: getPassphrase(),
79
+ path: getPath(),
80
+ });
81
+ out({
82
+ address,
83
+ secret_key: secretBech32,
84
+ warning: 'This is the full private key. Anyone with it controls the wallet. Back it up offline; import into Sui Wallet / Suiet / `sui keytool import` to use it elsewhere.',
85
+ });
86
+ break;
87
+ }
88
+ case 'address': {
89
+ out({ address: walletAddress({ passphrase: getPassphrase(), path: getPath() }) });
90
+ break;
91
+ }
92
+ case 'sign': {
93
+ const txBytesBase64 = process.argv[3];
94
+ if (!txBytesBase64 || txBytesBase64.startsWith('--')) {
95
+ fail('Usage: agent-signer sign <txBytesBase64>');
96
+ }
97
+ const res = await signTxBytes({
98
+ passphrase: getPassphrase(),
99
+ txBytesBase64,
100
+ path: getPath(),
101
+ });
102
+ out({
103
+ address: res.address,
104
+ signature: res.signature,
105
+ next_step: 'Submit with the sui_execute_signed_tx MCP tool: pass the same tx bytes and this signature.',
106
+ });
107
+ break;
108
+ }
109
+ default:
110
+ process.stdout.write([
111
+ 'agent-signer — non-custodial signer for a Tier-1 Sui agent wallet',
112
+ '',
113
+ 'Commands:',
114
+ ' create [--overwrite] generate + encrypt a new agent key',
115
+ ' import <suiprivkey> seal an existing key as the wallet',
116
+ ' export reveal the raw key (backup / import elsewhere)',
117
+ ' address print the agent wallet address',
118
+ ' sign <txBytesBase64> sign builder bytes -> base64 signature',
119
+ '',
120
+ 'Auth: AGENT_WALLET_PASSPHRASE (or --passphrase), AGENT_WALLET_PATH (or --path)',
121
+ '',
122
+ ].join('\n'));
123
+ if (cmd && cmd !== 'help' && cmd !== '--help')
124
+ process.exit(1);
125
+ }
126
+ }
127
+ main().catch((e) => fail(e instanceof Error ? e.message : String(e)));
128
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
2
+ /**
3
+ * @suisei-mcp/agent-signer — the non-custodial signer for a Tier-1 Sui agent
4
+ * wallet.
5
+ *
6
+ * The MCP toolkit builds unsigned tx bytes and never holds a key. This is
7
+ * the only piece that does: it generates the agent keypair, stores it
8
+ * encrypted on the user's machine, and signs tx bytes. The plaintext key
9
+ * is created here, used here, and never returned across a process boundary
10
+ * — in particular it never travels through an MCP response or an LLM
11
+ * context.
12
+ *
13
+ * Pair the signature this produces with sui_execute_signed_tx to submit.
14
+ */
15
+ export interface CreateResult {
16
+ address: string;
17
+ path: string;
18
+ }
19
+ /** Generate a fresh agent keypair and persist it encrypted. Returns the address only. */
20
+ export declare function createWallet(opts: {
21
+ passphrase: string;
22
+ path?: string;
23
+ overwrite?: boolean;
24
+ }): CreateResult;
25
+ /** Load and decrypt the agent keypair. Plaintext stays inside this process. */
26
+ export declare function loadKeypair(opts: {
27
+ passphrase: string;
28
+ path?: string;
29
+ }): Ed25519Keypair;
30
+ /** The agent wallet address, without decrypting the key (reads the stored address). */
31
+ export declare function walletAddress(opts: {
32
+ passphrase: string;
33
+ path?: string;
34
+ }): string;
35
+ /**
36
+ * Reveal the raw bech32 `suiprivkey...` secret so the user can back it up
37
+ * or import the agent wallet into a standard wallet (Sui Wallet, Suiet,
38
+ * `sui keytool import`). This is the user's escape hatch — it proves they,
39
+ * not us, own the key. Handle the output carefully: anyone with this string
40
+ * controls the wallet.
41
+ */
42
+ export declare function exportSecret(opts: {
43
+ passphrase: string;
44
+ path?: string;
45
+ }): {
46
+ address: string;
47
+ secretBech32: string;
48
+ };
49
+ /**
50
+ * Import an existing key as the agent wallet: seal a bech32 `suiprivkey...`
51
+ * (or raw 32-byte) secret into the keystore. Lets a user bring their own
52
+ * key instead of generating one.
53
+ */
54
+ export declare function importWallet(opts: {
55
+ passphrase: string;
56
+ secret: string;
57
+ path?: string;
58
+ overwrite?: boolean;
59
+ }): CreateResult;
60
+ /**
61
+ * Sign base64 tx bytes (from any @suisei-mcp/mcp builder tool) with the agent
62
+ * key. Returns the base64 Sui signature to pass to sui_execute_signed_tx.
63
+ */
64
+ export declare function signTxBytes(opts: {
65
+ passphrase: string;
66
+ txBytesBase64: string;
67
+ path?: string;
68
+ }): Promise<{
69
+ address: string;
70
+ signature: string;
71
+ }>;
72
+ export { defaultPath } from './keystore.js';
package/dist/index.js ADDED
@@ -0,0 +1,69 @@
1
+ import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
2
+ import { defaultPath, saveSecret, loadSecret, keystoreExists } from './keystore.js';
3
+ /** Generate a fresh agent keypair and persist it encrypted. Returns the address only. */
4
+ export function createWallet(opts) {
5
+ const path = opts.path ?? defaultPath();
6
+ if (keystoreExists(path) && !opts.overwrite) {
7
+ throw new Error(`An agent wallet already exists at ${path}. Pass overwrite to replace it (you will lose the old key).`);
8
+ }
9
+ const kp = new Ed25519Keypair();
10
+ const address = kp.getPublicKey().toSuiAddress();
11
+ saveSecret({ path, passphrase: opts.passphrase, address, secretBech32: kp.getSecretKey() });
12
+ return { address, path };
13
+ }
14
+ /** Load and decrypt the agent keypair. Plaintext stays inside this process. */
15
+ export function loadKeypair(opts) {
16
+ const path = opts.path ?? defaultPath();
17
+ const { secretBech32 } = loadSecret({ path, passphrase: opts.passphrase });
18
+ return Ed25519Keypair.fromSecretKey(secretBech32);
19
+ }
20
+ /** The agent wallet address, without decrypting the key (reads the stored address). */
21
+ export function walletAddress(opts) {
22
+ const path = opts.path ?? defaultPath();
23
+ return loadSecret({ path, passphrase: opts.passphrase }).address;
24
+ }
25
+ /**
26
+ * Reveal the raw bech32 `suiprivkey...` secret so the user can back it up
27
+ * or import the agent wallet into a standard wallet (Sui Wallet, Suiet,
28
+ * `sui keytool import`). This is the user's escape hatch — it proves they,
29
+ * not us, own the key. Handle the output carefully: anyone with this string
30
+ * controls the wallet.
31
+ */
32
+ export function exportSecret(opts) {
33
+ const path = opts.path ?? defaultPath();
34
+ return loadSecret({ path, passphrase: opts.passphrase });
35
+ }
36
+ /**
37
+ * Import an existing key as the agent wallet: seal a bech32 `suiprivkey...`
38
+ * (or raw 32-byte) secret into the keystore. Lets a user bring their own
39
+ * key instead of generating one.
40
+ */
41
+ export function importWallet(opts) {
42
+ const path = opts.path ?? defaultPath();
43
+ if (keystoreExists(path) && !opts.overwrite) {
44
+ throw new Error(`An agent wallet already exists at ${path}. Pass overwrite to replace it (you will lose the old key).`);
45
+ }
46
+ let kp;
47
+ try {
48
+ kp = Ed25519Keypair.fromSecretKey(opts.secret.trim());
49
+ }
50
+ catch {
51
+ throw new Error('Invalid secret key. Expected a bech32 "suiprivkey1..." string.');
52
+ }
53
+ const address = kp.getPublicKey().toSuiAddress();
54
+ // Re-serialize through getSecretKey() so storage is always canonical bech32.
55
+ saveSecret({ path, passphrase: opts.passphrase, address, secretBech32: kp.getSecretKey() });
56
+ return { address, path };
57
+ }
58
+ /**
59
+ * Sign base64 tx bytes (from any @suisei-mcp/mcp builder tool) with the agent
60
+ * key. Returns the base64 Sui signature to pass to sui_execute_signed_tx.
61
+ */
62
+ export async function signTxBytes(opts) {
63
+ const kp = loadKeypair({ passphrase: opts.passphrase, path: opts.path });
64
+ const bytes = new Uint8Array(Buffer.from(opts.txBytesBase64, 'base64'));
65
+ const { signature } = await kp.signTransaction(bytes);
66
+ return { address: kp.getPublicKey().toSuiAddress(), signature };
67
+ }
68
+ export { defaultPath } from './keystore.js';
69
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Encrypted-at-rest keystore for the agent wallet's Ed25519 secret key.
3
+ *
4
+ * The secret is sealed with AES-256-GCM under a key derived from a
5
+ * passphrase via scrypt. We store only ciphertext + the public address.
6
+ * Nothing here ever returns a plaintext key to a caller other than the
7
+ * in-process signer, and the file is written 0600. The key never leaves
8
+ * the user's machine and never enters an agent/LLM context.
9
+ */
10
+ export interface KeystoreFile {
11
+ version: 1;
12
+ address: string;
13
+ scheme: 'ed25519';
14
+ kdf: 'scrypt';
15
+ salt: string;
16
+ iv: string;
17
+ auth_tag: string;
18
+ ciphertext: string;
19
+ created_at: string;
20
+ }
21
+ /** Default keystore path; override with AGENT_WALLET_PATH. */
22
+ export declare function defaultPath(): string;
23
+ /** Encrypt a bech32 `suiprivkey...` string into a keystore file on disk. */
24
+ export declare function saveSecret(opts: {
25
+ path: string;
26
+ passphrase: string;
27
+ address: string;
28
+ secretBech32: string;
29
+ }): void;
30
+ /** Decrypt and return the bech32 `suiprivkey...` string. Throws on bad passphrase. */
31
+ export declare function loadSecret(opts: {
32
+ path: string;
33
+ passphrase: string;
34
+ }): {
35
+ address: string;
36
+ secretBech32: string;
37
+ };
38
+ export declare function keystoreExists(path: string): boolean;
@@ -0,0 +1,71 @@
1
+ import { randomBytes, scryptSync, createCipheriv, createDecipheriv, } from 'node:crypto';
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ const SCRYPT_N = 1 << 15; // 32768
6
+ const SCRYPT_PARAMS = { N: SCRYPT_N, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
7
+ /** Default keystore path; override with AGENT_WALLET_PATH. */
8
+ export function defaultPath() {
9
+ return process.env.AGENT_WALLET_PATH ?? join(homedir(), '.suisei', 'agent-wallet.json');
10
+ }
11
+ function deriveKey(passphrase, salt) {
12
+ return scryptSync(passphrase, salt, 32, SCRYPT_PARAMS);
13
+ }
14
+ /** Encrypt a bech32 `suiprivkey...` string into a keystore file on disk. */
15
+ export function saveSecret(opts) {
16
+ const salt = randomBytes(16);
17
+ const iv = randomBytes(12);
18
+ const key = deriveKey(opts.passphrase, salt);
19
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
20
+ const ciphertext = Buffer.concat([
21
+ cipher.update(Buffer.from(opts.secretBech32, 'utf8')),
22
+ cipher.final(),
23
+ ]);
24
+ const authTag = cipher.getAuthTag();
25
+ const file = {
26
+ version: 1,
27
+ address: opts.address,
28
+ scheme: 'ed25519',
29
+ kdf: 'scrypt',
30
+ salt: salt.toString('hex'),
31
+ iv: iv.toString('hex'),
32
+ auth_tag: authTag.toString('hex'),
33
+ ciphertext: ciphertext.toString('hex'),
34
+ created_at: new Date().toISOString(),
35
+ };
36
+ mkdirSync(dirname(opts.path), { recursive: true });
37
+ writeFileSync(opts.path, JSON.stringify(file, null, 2), { mode: 0o600 });
38
+ try {
39
+ chmodSync(opts.path, 0o600);
40
+ }
41
+ catch {
42
+ // best-effort on platforms without POSIX perms
43
+ }
44
+ }
45
+ /** Decrypt and return the bech32 `suiprivkey...` string. Throws on bad passphrase. */
46
+ export function loadSecret(opts) {
47
+ if (!existsSync(opts.path)) {
48
+ throw new Error(`No agent wallet at ${opts.path}. Run "agent-signer create" first.`);
49
+ }
50
+ const file = JSON.parse(readFileSync(opts.path, 'utf8'));
51
+ if (file.version !== 1)
52
+ throw new Error(`Unsupported keystore version ${file.version}.`);
53
+ const key = deriveKey(opts.passphrase, Buffer.from(file.salt, 'hex'));
54
+ const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(file.iv, 'hex'));
55
+ decipher.setAuthTag(Buffer.from(file.auth_tag, 'hex'));
56
+ let secretBech32;
57
+ try {
58
+ secretBech32 = Buffer.concat([
59
+ decipher.update(Buffer.from(file.ciphertext, 'hex')),
60
+ decipher.final(),
61
+ ]).toString('utf8');
62
+ }
63
+ catch {
64
+ throw new Error('Decryption failed — wrong passphrase or corrupted keystore.');
65
+ }
66
+ return { address: file.address, secretBech32 };
67
+ }
68
+ export function keystoreExists(path) {
69
+ return existsSync(path);
70
+ }
71
+ //# sourceMappingURL=keystore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keystore.js","sourceRoot":"","sources":["../src/keystore.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@suisei-mcp/agent-signer",
3
+ "version": "0.1.0",
4
+ "description": "Local, non-custodial signer for a Tier-1 Sui agent wallet. Generates and encrypts an agent keypair on the user's machine and signs tx bytes from @suisei-mcp/mcp. The key never leaves the host and never enters an agent's context.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "exports": {
9
+ ".": "./dist/index.js"
10
+ },
11
+ "bin": {
12
+ "agent-signer": "dist/cli.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "dev": "tsc --watch",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "keywords": [
27
+ "sui",
28
+ "agent",
29
+ "wallet",
30
+ "signer",
31
+ "non-custodial",
32
+ "mcp"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "@mysten/sui": "^1.45.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.9.0",
42
+ "typescript": "^5.6.3"
43
+ }
44
+ }