@memfork/cli 0.1.24 → 0.1.25

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.
@@ -19,6 +19,9 @@
19
19
  * to SuiJsonRpcClient, and the MemWal SDK's internal auto-init would fail on v2.
20
20
  */
21
21
  import chalk from "chalk";
22
+ import fs from "node:fs";
23
+ import os from "node:os";
24
+ import path from "node:path";
22
25
  import { confirm } from "@inquirer/prompts";
23
26
  import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
24
27
  import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
@@ -31,20 +34,48 @@ function step(n, msg) {
31
34
  }
32
35
  function done() { console.log(chalk.green("done")); }
33
36
  function skip(reason) { console.log(chalk.dim("skip " + reason)); }
37
+ function checkpointPath() {
38
+ return path.join(os.homedir(), ".memfork", ".pending-init.json");
39
+ }
40
+ function readCheckpoint() {
41
+ const p = checkpointPath();
42
+ try {
43
+ return JSON.parse(fs.readFileSync(p, "utf8"));
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ function saveCheckpoint(data) {
50
+ const p = checkpointPath();
51
+ fs.mkdirSync(path.dirname(p), { recursive: true });
52
+ fs.writeFileSync(p, JSON.stringify(data, null, 2), { mode: 0o600 });
53
+ }
54
+ function clearCheckpoint() {
55
+ try {
56
+ fs.unlinkSync(checkpointPath());
57
+ }
58
+ catch { /* already gone */ }
59
+ }
34
60
  export async function autoProvision(opts) {
35
61
  const network = opts.network;
36
62
  const consts = MEMWAL_CONSTANTS[network];
37
63
  const rpcUrl = getJsonRpcFullnodeUrl(network);
38
64
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
65
  const suiClient = new SuiJsonRpcClient({ transport: new JsonRpcHTTPTransport({ url: rpcUrl }), network });
66
+ // Load any checkpoint from a previous failed attempt.
67
+ const ckpt = readCheckpoint();
68
+ // If the checkpoint is for a different network, ignore it.
69
+ const cp = (ckpt?.network === network) ? ckpt : { network };
40
70
  // ── 1. Keypair ──────────────────────────────────────────────────────────────
41
71
  step(1, "Generating Sui keypair");
42
72
  let keypair;
43
- if (opts.existingKey) {
44
- keypair = opts.existingKey.startsWith("suiprivkey")
45
- ? Ed25519Keypair.fromSecretKey(decodeSuiPrivateKey(opts.existingKey).secretKey)
46
- : Ed25519Keypair.fromSecretKey(Uint8Array.from(Buffer.from(opts.existingKey, "hex")));
47
- skip("reusing existing key");
73
+ const resumeKey = opts.existingKey ?? cp.privateKey;
74
+ if (resumeKey) {
75
+ keypair = resumeKey.startsWith("suiprivkey")
76
+ ? Ed25519Keypair.fromSecretKey(decodeSuiPrivateKey(resumeKey).secretKey)
77
+ : Ed25519Keypair.fromSecretKey(Uint8Array.from(Buffer.from(resumeKey, "hex")));
78
+ skip(cp.privateKey && !opts.existingKey ? "resuming from checkpoint" : "reusing existing key");
48
79
  }
49
80
  else {
50
81
  keypair = new Ed25519Keypair();
@@ -53,6 +84,11 @@ export async function autoProvision(opts) {
53
84
  const address = keypair.toSuiAddress();
54
85
  const privateKey = keypair.getSecretKey(); // bech32 suiprivkey1…
55
86
  console.log(chalk.dim(` address: ${address}`));
87
+ // Save keypair to checkpoint so a retry can reuse the same key.
88
+ if (!cp.privateKey) {
89
+ cp.privateKey = privateKey;
90
+ saveCheckpoint(cp);
91
+ }
56
92
  // ── 2. Fund wallet ───────────────────────────────────────────────────────────
57
93
  // sponsorBase = root URL, used for /drip and /sponsor paths.
58
94
  // sponsorEndpoint = full URL the MemForksClient POSTs to (no path appended internally).
@@ -73,7 +109,6 @@ export async function autoProvision(opts) {
73
109
  // Mainnet: drip covers the two MemWal calls (createAccount + addDelegateKey).
74
110
  // initTree (step 6) goes through /sponsor — the MemForksClient handles that path.
75
111
  step(2, "Requesting mainnet gas from MemForks sponsor");
76
- // Normalise: strip a trailing /sponsor so the base URL is always path-free.
77
112
  sponsorBase = (process.env.MEMFORK_SPONSOR_URL ?? "https://memforks-sponsor-production.up.railway.app")
78
113
  .replace(/\/sponsor\/?$/, "");
79
114
  sponsorEndpoint = `${sponsorBase}/sponsor`;
@@ -104,57 +139,79 @@ export async function autoProvision(opts) {
104
139
  }
105
140
  }
106
141
  // ── 3. MemWal account ────────────────────────────────────────────────────────
107
- step(3, "Creating MemWal account on-chain");
108
142
  let accountId;
109
- try {
110
- const result = await createAccount({
111
- packageId: consts.packageId,
112
- registryId: consts.registryId,
113
- suiPrivateKey: privateKey,
114
- suiClient,
115
- });
116
- accountId = result.accountId;
117
- done();
143
+ if (cp.accountId) {
144
+ step(3, "Creating MemWal account on-chain");
145
+ skip("resuming from checkpoint");
146
+ accountId = cp.accountId;
118
147
  console.log(chalk.dim(` accountId: ${accountId}`));
119
148
  }
120
- catch (e) {
121
- const msg = String(e);
122
- if (msg.includes("EAccountAlreadyExists") || msg.includes("MoveAbort") && msg.includes(", 3)")) {
123
- // Error code 3 = EAccountAlreadyExists — fine, just fetch the existing one.
124
- console.log(chalk.dim("already exists"));
125
- accountId = await resolveExistingMemwalAccount(suiClient, consts.packageId, address);
149
+ else {
150
+ step(3, "Creating MemWal account on-chain");
151
+ try {
152
+ const result = await createAccount({
153
+ packageId: consts.packageId,
154
+ registryId: consts.registryId,
155
+ suiPrivateKey: privateKey,
156
+ suiClient,
157
+ });
158
+ accountId = result.accountId;
159
+ done();
126
160
  console.log(chalk.dim(` accountId: ${accountId}`));
127
161
  }
128
- else {
129
- console.log(chalk.red("failed"));
130
- throw new Error(`MemWal account creation failed: ${msg}`);
162
+ catch (e) {
163
+ const msg = String(e);
164
+ if (msg.includes("EAccountAlreadyExists") || msg.includes("MoveAbort") && msg.includes(", 3)")) {
165
+ console.log(chalk.dim("already exists"));
166
+ accountId = await resolveExistingMemwalAccount(suiClient, consts.packageId, address);
167
+ console.log(chalk.dim(` accountId: ${accountId}`));
168
+ }
169
+ else {
170
+ console.log(chalk.red("failed"));
171
+ throw new Error(`MemWal account creation failed: ${msg}`);
172
+ }
131
173
  }
174
+ cp.accountId = accountId;
175
+ saveCheckpoint(cp);
132
176
  }
133
177
  // ── 4 + 5. Delegate key ───────────────────────────────────────────────────────
134
- step(4, "Generating MemWal delegate key");
135
- const delegate = await generateDelegateKey();
136
- done();
137
- step(5, "Registering delegate key with MemWal");
138
- try {
139
- await addDelegateKey({
140
- packageId: consts.packageId,
141
- accountId,
142
- publicKey: delegate.publicKey,
143
- label: `memfork-cli-${new Date().toISOString().slice(0, 10)}`,
144
- suiPrivateKey: privateKey,
145
- suiClient,
146
- });
147
- done();
178
+ let delegatePrivateKey;
179
+ if (cp.delegateKey) {
180
+ step(4, "Generating MemWal delegate key");
181
+ skip("resuming from checkpoint");
182
+ step(5, "Registering delegate key with MemWal");
183
+ skip("resuming from checkpoint");
184
+ delegatePrivateKey = cp.delegateKey;
148
185
  }
149
- catch (e) {
150
- const msg = String(e);
151
- if (msg.includes("EDelegateKeyAlreadyExists") || msg.includes("MoveAbort") && msg.includes(", 0)")) {
152
- skip("key already registered");
186
+ else {
187
+ step(4, "Generating MemWal delegate key");
188
+ const delegate = await generateDelegateKey();
189
+ done();
190
+ delegatePrivateKey = delegate.privateKey;
191
+ step(5, "Registering delegate key with MemWal");
192
+ try {
193
+ await addDelegateKey({
194
+ packageId: consts.packageId,
195
+ accountId,
196
+ publicKey: delegate.publicKey,
197
+ label: `memfork-cli-${new Date().toISOString().slice(0, 10)}`,
198
+ suiPrivateKey: privateKey,
199
+ suiClient,
200
+ });
201
+ done();
153
202
  }
154
- else {
155
- console.log(chalk.red("failed"));
156
- throw new Error(`Failed to register delegate key: ${msg}`);
203
+ catch (e) {
204
+ const msg = String(e);
205
+ if (msg.includes("EDelegateKeyAlreadyExists") || msg.includes("MoveAbort") && msg.includes(", 0)")) {
206
+ skip("key already registered");
207
+ }
208
+ else {
209
+ console.log(chalk.red("failed"));
210
+ throw new Error(`Failed to register delegate key: ${msg}`);
211
+ }
157
212
  }
213
+ cp.delegateKey = delegatePrivateKey;
214
+ saveCheckpoint(cp);
158
215
  }
159
216
  // ── 6. MemoryTree ────────────────────────────────────────────────────────────
160
217
  step(6, "Creating MemoryTree on Sui");
@@ -163,19 +220,15 @@ export async function autoProvision(opts) {
163
220
  signer: privateKey,
164
221
  network,
165
222
  packageId: consts.memforksPackageId,
166
- // sponsorEndpoint is the full POST URL — MemForksClient appends no path.
167
223
  ...(sponsorEndpoint ? { sponsorUrl: sponsorEndpoint } : {}),
168
224
  memwal: {
169
225
  accountId,
170
- delegateKey: delegate.privateKey,
226
+ delegateKey: delegatePrivateKey,
171
227
  serverUrl: consts.relayer,
172
228
  },
173
229
  });
174
230
  let treeId;
175
231
  let digest;
176
- // Retry once on a transient gas-coin version race: the sponsor refreshes its
177
- // pool after the drip, but if initTree lands before that completes the
178
- // fullnode may report a stale-object error. A short wait + retry clears it.
179
232
  const maxAttempts = 2;
180
233
  for (let attempt = 1;; attempt++) {
181
234
  try {
@@ -194,8 +247,8 @@ export async function autoProvision(opts) {
194
247
  console.log(chalk.red("failed"));
195
248
  if (msg.includes("429") || msg.toLowerCase().includes("rate limit")) {
196
249
  throw new Error("Sponsor rate limit hit for init_tree (1/IP/day).\n" +
197
- "Wait 24 h and run `memfork init --quick` again, or fund the wallet manually:\n" +
198
- ` ${address}`);
250
+ "Wait 24 h and run `memfork init --quick` again to resume your keypair and\n" +
251
+ "MemWal account are already saved and will be reused automatically.");
199
252
  }
200
253
  if (msg.includes("Sponsor error: 404")) {
201
254
  throw new Error(`Sponsor endpoint not reachable (${sponsorEndpoint}).\n` +
@@ -210,11 +263,13 @@ export async function autoProvision(opts) {
210
263
  done();
211
264
  console.log(chalk.dim(` treeId: ${treeId}`));
212
265
  console.log(chalk.dim(` tx: ${digest}`));
266
+ // All steps succeeded — clear the checkpoint.
267
+ clearCheckpoint();
213
268
  return {
214
269
  treeId,
215
270
  privateKey,
216
271
  memwalAccountId: accountId,
217
- memwalKey: delegate.privateKey,
272
+ memwalKey: delegatePrivateKey,
218
273
  network,
219
274
  };
220
275
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memfork/cli",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "MemForks CLI — init, commit, recall, merge, install plugins",
5
5
  "repository": {
6
6
  "type": "git",