nebulon-escrow-cli 0.1.0 → 0.8.4

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.
@@ -20,24 +20,15 @@ const {
20
20
  } = require("@solana/spl-token");
21
21
  const { SessionTokenManager } = require("@magicblock-labs/gum-sdk");
22
22
  const {
23
- createDelegatePermissionInstruction,
24
- permissionPdaFromAccount,
25
- PERMISSION_PROGRAM_ID,
26
- AUTHORITY_FLAG,
27
- TX_LOGS_FLAG,
28
23
  MAGIC_PROGRAM_ID,
29
24
  MAGIC_CONTEXT_ID,
30
25
  DELEGATION_PROGRAM_ID,
31
26
  ConnectionMagicRouter,
32
- getAuthToken,
33
- getPermissionStatus,
34
- waitUntilPermissionActive,
35
27
  } = require("@magicblock-labs/ephemeral-rollups-sdk");
36
28
  const { loadConfig, saveConfig } = require("../config");
37
29
  const { loadWalletKeypair } = require("../wallets");
38
30
  const { ensureHostedSession } = require("../session");
39
31
  const { successMessage, errorMessage } = require("../ui");
40
- const { checkTeeAvailability } = require("./tee");
41
32
  const {
42
33
  getContracts,
43
34
  getContract,
@@ -144,11 +135,6 @@ let cachedValidatorEndpoint = null;
144
135
 
145
136
  const normalizeEndpoint = (endpoint) => endpoint.replace(/\/$/, "");
146
137
 
147
- const TEE_TOKEN_TTL_SECONDS = 240;
148
- const teeTokenCache = new Map();
149
- const teeWsHealthCache = new Map();
150
- const teeProgramCache = new Map();
151
-
152
138
  const isLocalnetConfig = (config) => {
153
139
  const network = (config?.network || config?.solanaNetwork || "").toLowerCase();
154
140
  if (network === "localnet") {
@@ -164,210 +150,66 @@ const isLocalnetConfig = (config) => {
164
150
  );
165
151
  };
166
152
 
167
- const getTeeEndpoint = async (config, keypair, options = {}) => {
168
- const base = normalizeEndpoint(
169
- config.ephemeralTeeEndpoint ||
170
- config.ephemeralPermissionEndpoint ||
171
- "https://tee.magicblock.app"
172
- );
173
- const cacheKey = `${base}:${keypair.publicKey.toBase58()}`;
174
- const cached = teeTokenCache.get(cacheKey);
175
- const now = Math.floor(Date.now() / 1000);
176
- if (!options.forceRefresh && cached && cached.expiresAt > now) {
177
- return cached;
178
- }
179
- const auth = await getAuthToken(
180
- base,
181
- keypair.publicKey,
182
- (message) => nacl.sign.detached(message, keypair.secretKey)
183
- );
184
- const endpoint = `${base}?token=${auth.token}`;
185
- const expiresAt = now + TEE_TOKEN_TTL_SECONDS;
186
- const record = { endpoint, token: auth.token, expiresAt };
187
- teeTokenCache.set(cacheKey, record);
188
- return record;
189
- };
190
-
191
153
  const getEphemeralProgram = async (config, signerKeypair, options = {}) => {
154
+ const endpoint = config.ephemeralProviderUrl || config.rpcUrl;
155
+ const wsEndpoint = config.ephemeralWsUrl || undefined;
192
156
  if (isLocalnetConfig(config)) {
193
- return getProgram(config, signerKeypair, {
194
- endpoint: config.ephemeralProviderUrl || config.rpcUrl,
195
- wsEndpoint: config.ephemeralWsUrl || undefined,
196
- });
197
- }
198
- const cacheKey = signerKeypair.publicKey.toBase58();
199
- const cached = teeProgramCache.get(cacheKey);
200
- const now = Math.floor(Date.now() / 1000);
201
- if (!options.forceRefresh && cached && cached.expiresAt > now + 30) {
202
- return cached.bundle;
203
- }
204
- const { endpoint, token, expiresAt } = await getTeeEndpoint(
205
- config,
206
- signerKeypair,
207
- options
208
- );
209
- let wsEndpoint =
210
- config.ephemeralTeeWsEndpoint ||
211
- endpoint.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
212
- if (token && !wsEndpoint.includes("token=")) {
213
- wsEndpoint += wsEndpoint.includes("?") ? `&token=${token}` : `?token=${token}`;
214
- }
215
- if (process.env.NEBULON_TEE_DEBUG === "1") {
216
- console.log("TEE signer:", signerKeypair.publicKey.toBase58());
217
- console.log("TEE RPC endpoint:", endpoint);
218
- console.log("TEE WS endpoint:", wsEndpoint);
219
- }
220
- const programBundle = getProgram(config, signerKeypair, {
221
- endpoint,
222
- wsEndpoint,
223
- });
224
- await ensureTeeWsHealthy(programBundle.provider.connection, wsEndpoint);
225
- teeProgramCache.set(cacheKey, { bundle: programBundle, expiresAt });
226
- return programBundle;
227
- };
228
-
229
- const ensureTeeWsHealthy = async (connection, wsEndpoint) => {
230
- if (!connection || !wsEndpoint) {
231
- return;
232
- }
233
- const cacheKey = wsEndpoint;
234
- const now = Date.now();
235
- const cached = teeWsHealthCache.get(cacheKey);
236
- if (cached && cached.expiresAt > now) {
237
- return;
238
- }
239
-
240
- const timeoutMs = 4000;
241
- let subscriptionId = null;
242
- let resolved = false;
243
- try {
244
- if (process.env.NEBULON_TEE_DEBUG === "1") {
245
- console.log("TEE WS health check: subscribing for slot change...");
246
- }
247
- subscriptionId = connection.onSlotChange((info) => {
248
- if (!resolved) {
249
- resolved = true;
250
- if (process.env.NEBULON_TEE_DEBUG === "1") {
251
- console.log(`TEE WS health check: slot ${info.slot}`);
252
- }
253
- }
254
- });
255
- await new Promise((resolve, reject) => {
256
- const timeout = setTimeout(() => {
257
- if (!resolved) {
258
- reject(
259
- new Error(
260
- "TEE WS health check failed (no slot notifications). Check MagicBlock WS connectivity."
261
- )
262
- );
263
- } else {
264
- resolve();
265
- }
266
- }, timeoutMs);
267
- const poll = () => {
268
- if (resolved) {
269
- clearTimeout(timeout);
270
- resolve();
271
- return;
272
- }
273
- setTimeout(poll, 50);
274
- };
275
- poll();
276
- });
277
- if (process.env.NEBULON_TEE_DEBUG === "1") {
278
- console.log("TEE WS health check: OK");
279
- }
280
- teeWsHealthCache.set(cacheKey, { expiresAt: now + 15000 });
281
- } finally {
282
- if (subscriptionId !== null) {
283
- try {
284
- await connection.removeSlotChangeListener(subscriptionId);
285
- } catch {
286
- // ignore cleanup errors
287
- }
288
- }
289
- }
290
- };
291
-
292
- const buildPermissionEndpoint = async (config, keypair) => {
293
- const network = (config?.network || config?.solanaNetwork || "").toLowerCase();
294
- const rpc = String(config?.rpcUrl || "");
295
- const isLocal =
296
- network === "localnet" ||
297
- rpc.includes("localhost:8899") ||
298
- rpc.includes("127.0.0.1:8899");
299
- if (isLocal) {
300
- return null;
301
- }
302
- const base = normalizeEndpoint(
303
- config.ephemeralPermissionEndpoint || "https://tee.magicblock.app"
304
- );
305
- if (base.includes("localhost") || base.includes("127.0.0.1")) {
306
- return null;
307
- }
308
- try {
309
- const auth = await getAuthToken(
310
- base,
311
- keypair.publicKey,
312
- (message) => nacl.sign.detached(message, keypair.secretKey)
313
- );
314
- return `${base}?token=${auth.token}`;
315
- } catch (error) {
316
- console.warn("Permission token fetch failed. Skipping permission checks.");
317
- return base;
318
- }
319
- };
320
-
321
- const waitForPermissionActive = async (config, keypair, pda, label) => {
322
- const network = (config?.network || config?.solanaNetwork || "").toLowerCase();
323
- const rpc = String(config?.rpcUrl || "");
324
- if (
325
- network === "localnet" ||
326
- rpc.includes("localhost:8899") ||
327
- rpc.includes("127.0.0.1:8899")
328
- ) {
329
- return;
330
- }
331
- const endpoint = await buildPermissionEndpoint(config, keypair);
332
- if (!endpoint) {
333
- return;
157
+ return getProgram(config, signerKeypair, { endpoint, wsEndpoint });
334
158
  }
335
- try {
336
- const ready = await waitUntilPermissionActive(endpoint, pda, 20_000);
337
- if (ready) {
338
- return;
339
- }
340
- const status = await getPermissionStatus(endpoint, pda).catch(() => null);
341
- const state = status?.status || "unknown";
342
- throw new Error(
343
- `${label} permission is not active yet (status: ${state}). Try again in a few seconds.`
344
- );
345
- } catch (error) {
346
- console.warn(
347
- `Warning: permission check failed for ${label}. Proceeding without confirmation.`
348
- );
159
+ if (options.forceRefresh) {
160
+ return getProgram(config, signerKeypair, { endpoint, wsEndpoint });
349
161
  }
162
+ return getProgram(config, signerKeypair, { endpoint, wsEndpoint });
350
163
  };
351
164
 
352
165
  const getDelegationValidator = async (config) => {
353
166
  const endpoint = (config.ephemeralProviderUrl || "").toLowerCase();
167
+ const wsEndpoint = (config.ephemeralWsUrl || "").toLowerCase();
354
168
  if (endpoint.includes("localhost") || endpoint.includes("127.0.0.1")) {
355
169
  return LOCAL_VALIDATOR_IDENTITY;
356
170
  }
357
- if (endpoint.includes("router")) {
358
- if (cachedValidator && cachedValidatorEndpoint === endpoint) {
171
+
172
+ const safePublicKey = (value) => {
173
+ try {
174
+ return value ? new PublicKey(value) : null;
175
+ } catch {
176
+ return null;
177
+ }
178
+ };
179
+
180
+ const shouldUseRouter =
181
+ endpoint.includes("router") ||
182
+ wsEndpoint.includes("router") ||
183
+ endpoint.includes("magicblock.app") ||
184
+ wsEndpoint.includes("magicblock.app");
185
+ if (shouldUseRouter && config.ephemeralProviderUrl) {
186
+ const cacheKey = `${endpoint}|${wsEndpoint}`;
187
+ if (cachedValidator && cachedValidatorEndpoint === cacheKey) {
359
188
  return cachedValidator;
360
189
  }
361
- const router = new ConnectionMagicRouter(config.ephemeralProviderUrl, {
362
- wsEndpoint: config.ephemeralWsUrl || undefined,
363
- });
364
- const closest = await router.getClosestValidator();
365
- cachedValidator = new PublicKey(closest.pubkey || closest.validator || closest);
366
- cachedValidatorEndpoint = endpoint;
367
- return cachedValidator;
190
+ try {
191
+ const router = new ConnectionMagicRouter(config.ephemeralProviderUrl, {
192
+ wsEndpoint: config.ephemeralWsUrl || undefined,
193
+ });
194
+ const closest = await router.getClosestValidator();
195
+ if (config && config.__verbose) {
196
+ console.log("Router closest:", JSON.stringify(closest));
197
+ }
198
+ const identity =
199
+ closest?.validatorIdentity || closest?.identity || closest?.validator;
200
+ const candidate = safePublicKey(identity || closest?.pubkey || closest);
201
+ if (candidate) {
202
+ cachedValidator = candidate;
203
+ cachedValidatorEndpoint = cacheKey;
204
+ return cachedValidator;
205
+ }
206
+ } catch {
207
+ // fall back to configured identity below
208
+ }
368
209
  }
369
- if (config.ephemeralValidatorIdentity) {
370
- return new PublicKey(config.ephemeralValidatorIdentity);
210
+ const fallback = safePublicKey(config.ephemeralValidatorIdentity);
211
+ if (fallback) {
212
+ return fallback;
371
213
  }
372
214
  return null;
373
215
  };
@@ -454,30 +296,6 @@ const ensureEscrowUndelegated = async (config, keypair, escrowPda) => {
454
296
  .rpc({ skipPreflight: true });
455
297
  };
456
298
 
457
- const ensureEscrowOwnedByProgramL1 = async (config, keypair, escrowPda) => {
458
- const { connection, programId } = getProgram(config, keypair);
459
- const info = await connection.getAccountInfo(escrowPda, "confirmed");
460
- if (!info || !info.owner) {
461
- console.error("Escrow not found on-chain.");
462
- return false;
463
- }
464
- if (info.owner.equals(DELEGATION_PROGRAM_ID)) {
465
- console.error(
466
- "Escrow is delegated to MagicBlock (PER). L1 MODE cannot modify it while delegated."
467
- );
468
- console.error(
469
- "Switch back to PER mode or wait for TEE availability and run `nebulon contract <id> sync` to undelegate."
470
- );
471
- return false;
472
- }
473
- if (!info.owner.equals(programId)) {
474
- console.error("Escrow is owned by an unexpected program.");
475
- console.error(`Owner: ${info.owner.toBase58()}`);
476
- return false;
477
- }
478
- return true;
479
- };
480
-
481
299
  const buildPrivacyContext = (scope, contractId, index) => {
482
300
  const tail = index === undefined ? "" : `:${index}`;
483
301
  return `nebulon:${scope}:v1:${contractId}${tail}`;
@@ -748,52 +566,13 @@ const parsePublicKey = (value, label) => {
748
566
  }
749
567
  };
750
568
 
751
- const getExecutionMode = (contract) => {
752
- const mode = (contract?.execution_mode || contract?.executionMode || "per")
753
- .toString()
754
- .toLowerCase();
755
- return mode === "l1" ? "l1" : "per";
756
- };
757
-
758
- const isL1Mode = (contract) => getExecutionMode(contract) === "l1";
759
-
760
- const ensureExecutionMode = async (config, contract, keypair, options = {}) => {
761
- if (isL1Mode(contract)) {
762
- config.__l1_mode = true;
763
- return "l1";
764
- }
765
- config.__l1_mode = false;
766
- if (isLocalnetConfig(config)) {
767
- config.__l1_mode = false;
768
- return "per";
769
- }
770
- const check = await checkTeeAvailability(config, keypair, {
771
- timeoutMs: 4000,
772
- });
773
- if (check.ok) {
774
- config.__l1_mode = false;
775
- return "per";
776
- }
777
- console.warn("Warning: MagicBlock TEE is not available from this location.");
778
- console.warn(
779
- "You can proceed in L1 MODE (no privacy, but functional on-chain flow)."
780
- );
781
- const proceed = await confirmAction(
782
- options.confirm,
783
- "Enable L1 MODE for this contract? yes/no"
784
- );
785
- if (!proceed) {
786
- console.log("Continuing without L1 mode (TEE may still fail).");
787
- config.__l1_mode = false;
788
- return "per";
569
+ const ensureExecutionMode = async (config) => {
570
+ if (!isLocalnetConfig(config) && !config.ephemeralProviderUrl) {
571
+ console.warn(
572
+ "Warning: MagicBlock ER endpoint is not configured. Run `nebulon init` to fetch endpoints."
573
+ );
789
574
  }
790
- await updateContract(config.backendUrl, config.auth.token, contract.id, {
791
- executionMode: "l1",
792
- });
793
- contract.execution_mode = "l1";
794
- config.__l1_mode = true;
795
- console.log("L1 MODE enabled for this contract.");
796
- return "l1";
575
+ return "per";
797
576
  };
798
577
 
799
578
  const feeFromGross = (amount) => (amount * FEE_BPS) / BPS_DENOMINATOR;
@@ -821,6 +600,15 @@ const normalizeHashBytes = (value) => {
821
600
  return Buffer.from(String(value), "utf8").subarray(0, 32);
822
601
  };
823
602
 
603
+ const logProgress = (message) => {
604
+ console.log(chalk.gray(message));
605
+ };
606
+
607
+ const logTx = (label, sig, isEr = false) => {
608
+ const prefix = isEr ? "ER-tx" : "tx";
609
+ console.log(`${label} (${prefix}: ${sig})`);
610
+ };
611
+
824
612
  const buildMilestonesHash = (milestones) => {
825
613
  const hash = crypto.createHash("sha256");
826
614
  const ordered = [...milestones].sort((a, b) => a.index - b.index);
@@ -882,15 +670,6 @@ const getPrivateMilestoneStatus = async (
882
670
  ) => {
883
671
  const { program, programId } = getProgram(config, keypair);
884
672
  const escrowPda = new PublicKey(contract.escrow_pda);
885
- if (isL1Mode(contract)) {
886
- const milestones = await fetchPublicMilestones(
887
- program,
888
- escrowPda,
889
- programId,
890
- [index]
891
- );
892
- return milestones[0];
893
- }
894
673
  const { program: erProgram, sessionSigner, sessionPda } =
895
674
  await getPerProgramBundle(config, keypair, programId, program.provider);
896
675
  const milestones = await fetchPrivateMilestones(
@@ -1248,31 +1027,24 @@ const ensureSessionToken = async (config, provider, programId, authorityKey) =>
1248
1027
  };
1249
1028
 
1250
1029
  const getSessionContext = async (config, keypair, program) => {
1251
- if (isLocalnetConfig(config)) {
1252
- const { sessionSigner, sessionPda } = await ensureSessionToken(
1253
- config,
1254
- program.provider,
1255
- program.programId,
1256
- keypair.publicKey
1257
- );
1258
- return { signer: sessionSigner, sessionPda, payer: sessionSigner.publicKey };
1259
- }
1260
- return { signer: keypair, sessionPda: null, payer: keypair.publicKey };
1030
+ const { sessionSigner, sessionPda } = await ensureSessionToken(
1031
+ config,
1032
+ program.provider,
1033
+ program.programId,
1034
+ keypair.publicKey
1035
+ );
1036
+ return { signer: sessionSigner, sessionPda, payer: sessionSigner.publicKey };
1261
1037
  };
1262
1038
 
1263
1039
  const getPerProgramBundle = async (config, keypair, programId, provider) => {
1264
- if (isLocalnetConfig(config)) {
1265
- const { sessionSigner, sessionPda } = await ensureSessionToken(
1266
- config,
1267
- provider,
1268
- programId,
1269
- keypair.publicKey
1270
- );
1271
- const { program } = getProgram(config, sessionSigner, { useEphemeral: true });
1272
- return { program, sessionSigner, sessionPda };
1273
- }
1274
- const { program } = await getEphemeralProgram(config, keypair);
1275
- return { program, sessionSigner: keypair, sessionPda: null };
1040
+ const { sessionSigner, sessionPda } = await ensureSessionToken(
1041
+ config,
1042
+ provider,
1043
+ programId,
1044
+ keypair.publicKey
1045
+ );
1046
+ const { program } = await getEphemeralProgram(config, sessionSigner);
1047
+ return { program, sessionSigner, sessionPda };
1276
1048
  };
1277
1049
 
1278
1050
  const isAlreadyExistsError = (error) => {
@@ -1284,91 +1056,21 @@ const isAlreadyExistsError = (error) => {
1284
1056
  );
1285
1057
  };
1286
1058
 
1287
- const ensurePermission = async (
1288
- _config,
1289
- program,
1290
- keypair,
1291
- permissionedAccount,
1292
- members
1293
- ) => {
1294
- try {
1295
- await program.methods
1296
- .createPermission(members.accountType, members.entries)
1297
- .accountsPartial({
1298
- payer: keypair.publicKey,
1299
- permissionedAccount,
1300
- permission: permissionPdaFromAccount(permissionedAccount),
1301
- permissionProgram: PERMISSION_PROGRAM_ID,
1302
- systemProgram: SystemProgram.programId,
1303
- })
1304
- .signers([keypair])
1305
- .rpc();
1306
- return true;
1307
- } catch (error) {
1308
- if (isAlreadyExistsError(error)) {
1309
- return false;
1310
- }
1311
- throw error;
1312
- }
1313
- };
1314
-
1315
- const ensureDelegatedPermission = async (
1316
- config,
1317
- provider,
1318
- keypair,
1319
- permissionedAccount
1320
- ) => {
1321
- if (config && config.__l1_mode) {
1322
- return;
1323
- }
1324
- const permissionPda = permissionPdaFromAccount(permissionedAccount);
1325
- const info = await provider.connection.getAccountInfo(
1326
- permissionPda,
1327
- "confirmed"
1328
- );
1329
- if (info && info.owner.equals(DELEGATION_PROGRAM_ID)) {
1330
- return;
1331
- }
1332
- if (info && !info.owner.equals(PERMISSION_PROGRAM_ID)) {
1333
- throw new Error(
1334
- `Permission ${permissionPda.toBase58()} is owned by ${info.owner.toBase58()}, expected ${PERMISSION_PROGRAM_ID.toBase58()}.`
1335
- );
1336
- }
1337
- const validator = await getDelegationValidator(config);
1338
- const ix = createDelegatePermissionInstruction({
1339
- payer: keypair.publicKey,
1340
- validator,
1341
- permissionedAccount: [permissionedAccount, false],
1342
- authority: [keypair.publicKey, true],
1343
- });
1344
- const permissionMeta = ix.keys.find((key) =>
1345
- key.pubkey.equals(permissionedAccount)
1346
- );
1347
- if (permissionMeta) {
1348
- permissionMeta.isWritable = true;
1349
- }
1350
- const tx = new Transaction().add(ix);
1351
- try {
1352
- await provider.sendAndConfirm(tx, [keypair]);
1353
- } catch (error) {
1354
- if (!isAlreadyExistsError(error)) {
1355
- throw error;
1356
- }
1357
- }
1358
- };
1059
+ const isVerbose = (options) => Boolean(options && options.verbose);
1359
1060
 
1360
1061
  const ensureDelegatedAccount = async (
1361
1062
  config,
1362
1063
  program,
1363
1064
  keypair,
1364
1065
  accountType,
1365
- pda
1066
+ pda,
1067
+ options
1366
1068
  ) => {
1367
- if (config && config.__l1_mode) {
1368
- return;
1369
- }
1370
1069
  const info = await program.provider.connection.getAccountInfo(pda, "confirmed");
1371
1070
  if (info && info.owner.equals(DELEGATION_PROGRAM_ID)) {
1071
+ if (isVerbose(options)) {
1072
+ console.log(`Delegation already active for ${pda.toBase58()}; skipping.`);
1073
+ }
1372
1074
  return;
1373
1075
  }
1374
1076
  const isPerVault = accountType && Object.prototype.hasOwnProperty.call(accountType, "perVault");
@@ -1383,6 +1085,13 @@ const ensureDelegatedAccount = async (
1383
1085
  }
1384
1086
  try {
1385
1087
  const validator = await getDelegationValidator(config);
1088
+ if (validator) {
1089
+ if (isVerbose(options)) {
1090
+ console.log(`Delegating account to validator: ${validator.toBase58()}`);
1091
+ }
1092
+ } else if (isVerbose(options)) {
1093
+ console.warn("Delegation skipped: no validator identity resolved.");
1094
+ }
1386
1095
  const remainingAccounts = validator
1387
1096
  ? [{ pubkey: validator, isWritable: false, isSigner: false }]
1388
1097
  : [];
@@ -1394,7 +1103,7 @@ const ensureDelegatedAccount = async (
1394
1103
  })
1395
1104
  .remainingAccounts(remainingAccounts)
1396
1105
  .signers([keypair])
1397
- .rpc();
1106
+ .rpc({ skipPreflight: true });
1398
1107
  } catch (error) {
1399
1108
  if (!isAlreadyExistsError(error)) {
1400
1109
  throw error;
@@ -1408,16 +1117,17 @@ const ensureDelegatedEscrow = async (
1408
1117
  keypair,
1409
1118
  escrowId,
1410
1119
  escrowPda,
1411
- client
1120
+ client,
1121
+ options
1412
1122
  ) => {
1413
- if (config && config.__l1_mode) {
1414
- return;
1415
- }
1416
1123
  const info = await program.provider.connection.getAccountInfo(
1417
1124
  escrowPda,
1418
1125
  "confirmed"
1419
1126
  );
1420
1127
  if (info && info.owner.equals(DELEGATION_PROGRAM_ID)) {
1128
+ if (isVerbose(options)) {
1129
+ console.log(`Delegation already active for ${escrowPda.toBase58()}; skipping.`);
1130
+ }
1421
1131
  return;
1422
1132
  }
1423
1133
  if (info && !info.owner.equals(program.programId)) {
@@ -1427,6 +1137,13 @@ const ensureDelegatedEscrow = async (
1427
1137
  }
1428
1138
  try {
1429
1139
  const validator = await getDelegationValidator(config);
1140
+ if (validator) {
1141
+ if (isVerbose(options)) {
1142
+ console.log(`Delegating escrow to validator: ${validator.toBase58()}`);
1143
+ }
1144
+ } else if (isVerbose(options)) {
1145
+ console.warn("Delegation skipped: no validator identity resolved.");
1146
+ }
1430
1147
  const remainingAccounts = validator
1431
1148
  ? [{ pubkey: validator, isWritable: false, isSigner: false }]
1432
1149
  : [];
@@ -1439,7 +1156,7 @@ const ensureDelegatedEscrow = async (
1439
1156
  })
1440
1157
  .remainingAccounts(remainingAccounts)
1441
1158
  .signers([keypair])
1442
- .rpc();
1159
+ .rpc({ skipPreflight: true });
1443
1160
  } catch (error) {
1444
1161
  if (!isAlreadyExistsError(error)) {
1445
1162
  throw error;
@@ -1447,16 +1164,6 @@ const ensureDelegatedEscrow = async (
1447
1164
  }
1448
1165
  };
1449
1166
 
1450
- const buildMembers = (contract) => {
1451
- const flags = AUTHORITY_FLAG | TX_LOGS_FLAG;
1452
- return {
1453
- entries: [
1454
- { flags, pubkey: new PublicKey(contract.client_wallet) },
1455
- { flags, pubkey: new PublicKey(contract.contractor_wallet) },
1456
- ],
1457
- };
1458
- };
1459
-
1460
1167
  const ensureAta = async (connection, payer, owner, mint) => {
1461
1168
  const ata = await getAssociatedTokenAddress(mint, owner, true);
1462
1169
  try {
@@ -1763,7 +1470,7 @@ const runInitContract = async (config, contract, options) => {
1763
1470
  })
1764
1471
  .signers([keypair])
1765
1472
  .rpc();
1766
- console.log(`Escrow initialized. (tx: ${sig})`);
1473
+ logTx("Escrow initialized.", sig, false);
1767
1474
 
1768
1475
  await linkEscrow(config.backendUrl, config.auth.token, contract.id, {
1769
1476
  escrowPda: escrowPda.toBase58(),
@@ -1832,37 +1539,12 @@ const runAddMilestone = async (config, contract, title, options) => {
1832
1539
  return;
1833
1540
  }
1834
1541
 
1835
- const executionMode = await ensureExecutionMode(config, contract, keypair, options);
1836
- const l1Mode = executionMode === "l1";
1542
+ await ensureExecutionMode(config, contract, keypair, options);
1837
1543
 
1838
1544
  console.log("Processing.");
1839
- if (l1Mode) {
1840
- const milestonePda = deriveMilestonePda(escrowPda, index, programId);
1841
- const sig = await program.methods
1842
- .addMilestone(new anchor.BN(escrowId.toString()), index)
1843
- .accounts({
1844
- actor: walletKey,
1845
- escrow: escrowPda,
1846
- milestone: milestonePda,
1847
- systemProgram: SystemProgram.programId,
1848
- })
1849
- .signers([keypair])
1850
- .rpc();
1851
- console.log(`Milestone created. (tx: ${sig})`);
1852
-
1853
- const milestones = Array.isArray(contract.milestones)
1854
- ? [...contract.milestones]
1855
- : [];
1856
- milestones.push({ index, details: title, status: "created" });
1857
- const persisted = await updateContractMilestones(config, contract, milestones);
1858
- if (!persisted) {
1859
- console.log(
1860
- "Warning: backend did not persist milestone status (check backend version)."
1861
- );
1862
- }
1863
- successMessage("Milestone added.");
1864
- return;
1865
- }
1545
+ logProgress("Preparing terms for ER submission");
1546
+ await sleep(200);
1547
+ logProgress("Encrypting milestone details");
1866
1548
  let privacyKey;
1867
1549
  try {
1868
1550
  privacyKey = await getContractPrivacyKey(config, contract.id, wallet);
@@ -1878,6 +1560,10 @@ const runAddMilestone = async (config, contract, title, options) => {
1878
1560
  index,
1879
1561
  title
1880
1562
  );
1563
+ if (!isEncryptedPayload(encryptedDetails)) {
1564
+ console.error("Failed to encrypt milestone details.");
1565
+ process.exit(1);
1566
+ }
1881
1567
 
1882
1568
  const encryptedBytes = Buffer.from(encryptedDetails, "utf8");
1883
1569
 
@@ -1893,6 +1579,7 @@ const runAddMilestone = async (config, contract, title, options) => {
1893
1579
  .update(encryptedDetails)
1894
1580
  .digest();
1895
1581
 
1582
+ logProgress("Preparing milestone accounts");
1896
1583
  const privateMilestonePda = derivePrivateMilestonePda(
1897
1584
  escrowPda,
1898
1585
  index,
@@ -1909,26 +1596,13 @@ const runAddMilestone = async (config, contract, title, options) => {
1909
1596
  .signers([keypair])
1910
1597
  .rpc();
1911
1598
 
1912
- const members = buildMembers(contract);
1913
- members.accountType = {
1914
- privateMilestone: {
1915
- escrow: escrowPda,
1916
- index,
1917
- },
1918
- };
1919
- await ensurePermission(config, program, keypair, privateMilestonePda, members);
1920
- await ensureDelegatedPermission(
1921
- config,
1922
- program.provider,
1923
- keypair,
1924
- privateMilestonePda
1925
- );
1926
1599
  await ensureDelegatedAccount(
1927
1600
  config,
1928
1601
  program,
1929
1602
  keypair,
1930
- members.accountType,
1931
- privateMilestonePda
1603
+ { privateMilestone: { escrow: escrowPda, index } },
1604
+ privateMilestonePda,
1605
+ options
1932
1606
  );
1933
1607
 
1934
1608
  const perVaultPda = derivePerVaultPda(escrowPda, programId);
@@ -1937,7 +1611,8 @@ const runAddMilestone = async (config, contract, title, options) => {
1937
1611
  program,
1938
1612
  keypair,
1939
1613
  { perVault: { escrow: escrowPda } },
1940
- perVaultPda
1614
+ perVaultPda,
1615
+ options
1941
1616
  );
1942
1617
  await ensureDelegatedEscrow(
1943
1618
  config,
@@ -1945,9 +1620,11 @@ const runAddMilestone = async (config, contract, title, options) => {
1945
1620
  keypair,
1946
1621
  escrowId,
1947
1622
  escrowPda,
1948
- new PublicKey(contract.client_wallet)
1623
+ new PublicKey(contract.client_wallet),
1624
+ options
1949
1625
  );
1950
1626
 
1627
+ logProgress("Submitting metadata on ER");
1951
1628
  const { program: erProgram, sessionSigner, sessionPda } =
1952
1629
  await getPerProgramBundle(config, keypair, programId, program.provider);
1953
1630
  const metaSig = await erProgram.methods
@@ -1968,8 +1645,7 @@ const runAddMilestone = async (config, contract, title, options) => {
1968
1645
  })
1969
1646
  .signers([sessionSigner])
1970
1647
  .rpc({ skipPreflight: true });
1971
- console.log("Submitting Metadata...");
1972
- console.log(`Done. (tx: ${metaSig})`);
1648
+ logTx("Metadata submitted.", metaSig, true);
1973
1649
 
1974
1650
  const milestones = Array.isArray(contract.milestones)
1975
1651
  ? [...contract.milestones]
@@ -2009,61 +1685,7 @@ const runDisableMilestone = async (config, contract, number, options) => {
2009
1685
  console.error("Check the list with: nebulon contract <id> check milestone list");
2010
1686
  return;
2011
1687
  }
2012
- const executionMode = await ensureExecutionMode(config, contract, keypair, options);
2013
- const l1Mode = executionMode === "l1";
2014
- if (l1Mode) {
2015
- const { program, connection, programId } = getProgram(config, keypair);
2016
- const escrowPda = new PublicKey(contract.escrow_pda);
2017
- const escrowState = await getEscrowState(connection, programId, escrowPda);
2018
- if (!escrowState) {
2019
- console.error("Escrow not found on-chain.");
2020
- process.exit(1);
2021
- }
2022
- const ok = await ensureEscrowOwnedByProgramL1(config, keypair, escrowPda);
2023
- if (!ok) {
2024
- return;
2025
- }
2026
- const escrowId = escrowState.escrowId;
2027
- const milestonePda = deriveMilestonePda(escrowPda, index, programId);
2028
- console.log("Verify data and confirm actions please.");
2029
- renderContractSummary(contract, wallet);
2030
- console.log("-Action-");
2031
- console.log("Disable milestone");
2032
- console.log(`Number : ${index + 1}`);
2033
- const milestones = Array.isArray(contract.milestones) ? contract.milestones : [];
2034
- const current = milestones.find((m) => m.index === index);
2035
- console.log(`Details : \"${current ? getMilestoneLabel(current) : "unknown"}\"`);
2036
- console.log("");
2037
- const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
2038
- if (!proceed) {
2039
- console.log("Canceled.");
2040
- return;
2041
- }
2042
- console.log("Processing.");
2043
- const sig = await program.methods
2044
- .disableMilestone(new anchor.BN(escrowId.toString()), index)
2045
- .accounts({
2046
- actor: walletKey,
2047
- escrow: escrowPda,
2048
- milestone: milestonePda,
2049
- })
2050
- .signers([keypair])
2051
- .rpc();
2052
- console.log(`Milestone disabled. (tx: ${sig})`);
2053
- const nextMilestones = milestones.map((milestone) =>
2054
- milestone.index === index
2055
- ? { ...milestone, status: "disabled" }
2056
- : milestone
2057
- );
2058
- const persisted = await updateContractMilestones(config, contract, nextMilestones);
2059
- if (!persisted) {
2060
- console.log(
2061
- "Warning: backend did not persist milestone status (check backend version)."
2062
- );
2063
- }
2064
- successMessage("Milestone disabled.");
2065
- return;
2066
- }
1688
+ await ensureExecutionMode(config, contract, keypair, options);
2067
1689
  if (!config.ephemeralProviderUrl) {
2068
1690
  console.error(
2069
1691
  "Warning: MagicBlock RPC is not configured; milestone status checks may be unreliable."
@@ -2085,6 +1707,47 @@ const runDisableMilestone = async (config, contract, number, options) => {
2085
1707
  console.error("Current status: disabled");
2086
1708
  return;
2087
1709
  }
1710
+ const programBundle = getProgram(config, keypair);
1711
+ const escrowPda = new PublicKey(contract.escrow_pda);
1712
+ const programId = programBundle.programId;
1713
+ const privateMilestonePda = derivePrivateMilestonePda(
1714
+ escrowPda,
1715
+ index,
1716
+ programId
1717
+ );
1718
+ await ensureDelegatedAccount(
1719
+ config,
1720
+ programBundle.program,
1721
+ keypair,
1722
+ { privateMilestone: { escrow: escrowPda, index } },
1723
+ privateMilestonePda,
1724
+ options
1725
+ );
1726
+ const perVaultPda = derivePerVaultPda(escrowPda, programId);
1727
+ await ensureDelegatedAccount(
1728
+ config,
1729
+ programBundle.program,
1730
+ keypair,
1731
+ { perVault: { escrow: escrowPda } },
1732
+ perVaultPda,
1733
+ options
1734
+ );
1735
+ const escrowState = await getEscrowState(
1736
+ programBundle.connection,
1737
+ programId,
1738
+ escrowPda
1739
+ );
1740
+ if (escrowState) {
1741
+ await ensureDelegatedEscrow(
1742
+ config,
1743
+ programBundle.program,
1744
+ keypair,
1745
+ escrowState.escrowId,
1746
+ escrowPda,
1747
+ new PublicKey(contract.client_wallet),
1748
+ options
1749
+ );
1750
+ }
2088
1751
  let currentStatus = null;
2089
1752
  try {
2090
1753
  const milestone = await getPrivateMilestoneStatus(
@@ -2151,15 +1814,18 @@ const runDisableMilestone = async (config, contract, number, options) => {
2151
1814
  }
2152
1815
 
2153
1816
  console.log("Processing.");
1817
+ logProgress("Submitting milestone update on ER");
1818
+ await sleep(200);
2154
1819
  const sig = await updatePrivateMilestoneStatus(
2155
1820
  config,
2156
1821
  contract,
2157
1822
  keypair,
2158
1823
  walletKey,
2159
1824
  index,
2160
- 4
1825
+ 4,
1826
+ options
2161
1827
  );
2162
- console.log(`Milestone disabled. (tx: ${sig})`);
1828
+ logTx("Milestone disabled.", sig, true);
2163
1829
 
2164
1830
  const nextMilestones = milestones.map((milestone) =>
2165
1831
  milestone.index === index
@@ -2169,6 +1835,8 @@ const runDisableMilestone = async (config, contract, number, options) => {
2169
1835
  await updateContractIfNegotiating(config, contract, {
2170
1836
  milestones: nextMilestones,
2171
1837
  });
1838
+ logProgress("Syncing private state to escrow flags");
1839
+ logProgress("Syncing private state to escrow flags");
2172
1840
  await runSyncFlags(config, contract, { ...options, confirm: true });
2173
1841
  successMessage("Milestone disabled.");
2174
1842
  };
@@ -2178,7 +1846,8 @@ const ensureTermsPrepared = async (
2178
1846
  contract,
2179
1847
  keypair,
2180
1848
  escrowPda,
2181
- escrowId
1849
+ escrowId,
1850
+ options
2182
1851
  ) => {
2183
1852
  const { program, programId } = getProgram(config, keypair);
2184
1853
  const termsPda = deriveTermsPda(escrowPda, programId);
@@ -2193,17 +1862,13 @@ const ensureTermsPrepared = async (
2193
1862
  .signers([keypair])
2194
1863
  .rpc();
2195
1864
 
2196
- const members = buildMembers(contract);
2197
- members.accountType = { terms: { escrow: escrowPda } };
2198
- await ensurePermission(config, program, keypair, termsPda, members);
2199
- await ensureDelegatedPermission(config, program.provider, keypair, termsPda);
2200
- await waitForPermissionActive(config, keypair, termsPda, "Terms");
2201
1865
  await ensureDelegatedAccount(
2202
1866
  config,
2203
1867
  program,
2204
1868
  keypair,
2205
- members.accountType,
2206
- termsPda
1869
+ { terms: { escrow: escrowPda } },
1870
+ termsPda,
1871
+ options
2207
1872
  );
2208
1873
 
2209
1874
  const perVaultPda = derivePerVaultPda(escrowPda, programId);
@@ -2212,7 +1877,8 @@ const ensureTermsPrepared = async (
2212
1877
  program,
2213
1878
  keypair,
2214
1879
  { perVault: { escrow: escrowPda } },
2215
- perVaultPda
1880
+ perVaultPda,
1881
+ options
2216
1882
  );
2217
1883
  await ensureDelegatedEscrow(
2218
1884
  config,
@@ -2220,7 +1886,8 @@ const ensureTermsPrepared = async (
2220
1886
  keypair,
2221
1887
  escrowId,
2222
1888
  escrowPda,
2223
- new PublicKey(contract.client_wallet)
1889
+ new PublicKey(contract.client_wallet),
1890
+ options
2224
1891
  );
2225
1892
 
2226
1893
  return { termsPda, perVaultPda, program, programId };
@@ -2234,19 +1901,26 @@ const submitTerms = async (
2234
1901
  escrowId,
2235
1902
  totalPayment,
2236
1903
  deadline,
2237
- encryptedTerms
1904
+ encryptedTerms,
1905
+ options
2238
1906
  ) => {
2239
1907
  const { termsPda, perVaultPda, program, programId } = await ensureTermsPrepared(
2240
1908
  config,
2241
1909
  contract,
2242
1910
  keypair,
2243
1911
  escrowPda,
2244
- escrowId
1912
+ escrowId,
1913
+ options
2245
1914
  );
2246
1915
 
2247
1916
  const { program: erProgram, sessionSigner, sessionPda } =
2248
1917
  await getPerProgramBundle(config, keypair, programId, program.provider);
2249
1918
  const encryptedBytes = Buffer.from(encryptedTerms || "", "utf8");
1919
+ if (encryptedBytes.length > 256) {
1920
+ throw new Error(
1921
+ `Encrypted terms too large (${encryptedBytes.length} bytes). Max is 256 bytes.`
1922
+ );
1923
+ }
2250
1924
  const attemptCreate = async (forceRefresh = false) => {
2251
1925
  const { program } = forceRefresh
2252
1926
  ? await getEphemeralProgram(config, keypair, { forceRefresh: true })
@@ -2275,7 +1949,7 @@ const submitTerms = async (
2275
1949
  const attemptCreateDirect = async () => {
2276
1950
  const { program: userErProgram } = await getEphemeralProgram(
2277
1951
  config,
2278
- keypair,
1952
+ sessionSigner,
2279
1953
  { forceRefresh: true }
2280
1954
  );
2281
1955
  return userErProgram.methods
@@ -2329,148 +2003,6 @@ const submitTerms = async (
2329
2003
  throw lastError;
2330
2004
  };
2331
2005
 
2332
- const submitPublicTerms = async (
2333
- config,
2334
- contract,
2335
- keypair,
2336
- escrowPda,
2337
- escrowId,
2338
- totalPayment,
2339
- deadline
2340
- ) => {
2341
- const { program, programId } = getProgram(config, keypair);
2342
- const termsPda = deriveTermsPda(escrowPda, programId);
2343
- const ok = await ensureEscrowOwnedByProgramL1(config, keypair, escrowPda);
2344
- if (!ok) {
2345
- return null;
2346
- }
2347
- const ix = await program.methods
2348
- .setPublicTerms(
2349
- new anchor.BN(escrowId.toString()),
2350
- buildTermsHash(deadline, totalPayment),
2351
- new anchor.BN(totalPayment.toString()),
2352
- new anchor.BN(deadline.toString())
2353
- )
2354
- .accounts({
2355
- user: keypair.publicKey,
2356
- escrow: escrowPda,
2357
- terms: termsPda,
2358
- systemProgram: SystemProgram.programId,
2359
- })
2360
- .instruction();
2361
- ix.keys = ix.keys.map((key) => {
2362
- if (key.pubkey.equals(escrowPda) || key.pubkey.equals(termsPda)) {
2363
- return { ...key, isWritable: true };
2364
- }
2365
- return key;
2366
- });
2367
- const tx = new Transaction().add(ix);
2368
- const sig = await program.provider.sendAndConfirm(tx, [keypair]);
2369
- return sig;
2370
- };
2371
-
2372
- const signPublicTermsOnChain = async (
2373
- config,
2374
- keypair,
2375
- escrowPda,
2376
- escrowId
2377
- ) => {
2378
- const { program, programId } = getProgram(config, keypair);
2379
- const termsPda = deriveTermsPda(escrowPda, programId);
2380
- try {
2381
- const escrowState = await program.account.escrow.fetch(escrowPda);
2382
- const termsState = await program.account.terms.fetch(termsPda);
2383
- const escrowIdOnChain = escrowState.escrowId?.toString?.() || String(escrowState.escrowId);
2384
- const expectedEscrowId = escrowId.toString();
2385
- if (escrowIdOnChain !== expectedEscrowId) {
2386
- console.error(
2387
- `L1 sign precheck: escrow_id mismatch (chain=${escrowIdOnChain}, local=${expectedEscrowId})`
2388
- );
2389
- }
2390
- if (termsState.escrow && termsState.escrow.toBase58) {
2391
- const termsEscrow = termsState.escrow.toBase58();
2392
- if (termsEscrow !== escrowPda.toBase58()) {
2393
- console.error(
2394
- `L1 sign precheck: terms.escrow mismatch (terms=${termsEscrow}, escrow=${escrowPda.toBase58()})`
2395
- );
2396
- }
2397
- }
2398
- if (escrowState.fundedAmount && escrowState.fundedAmount.toString) {
2399
- const funded = escrowState.fundedAmount.toString();
2400
- if (funded !== "0") {
2401
- console.error(`L1 sign precheck: funded_amount is ${funded}, expected 0`);
2402
- }
2403
- }
2404
- if (escrowState.fundingOk) {
2405
- console.error("L1 sign precheck: funding_ok already true");
2406
- }
2407
- } catch (error) {
2408
- console.error("L1 sign precheck failed to fetch escrow/terms.");
2409
- }
2410
- const ix = await program.methods
2411
- .signPublicTerms(new anchor.BN(escrowId.toString()))
2412
- .accounts({
2413
- user: keypair.publicKey,
2414
- escrow: escrowPda,
2415
- terms: termsPda,
2416
- })
2417
- .instruction();
2418
- ix.keys = ix.keys.map((key) => {
2419
- if (key.pubkey.equals(escrowPda) || key.pubkey.equals(termsPda)) {
2420
- return { ...key, isWritable: true };
2421
- }
2422
- return key;
2423
- });
2424
- const tx = new Transaction().add(ix);
2425
- const sig = await program.provider.sendAndConfirm(tx, [keypair]);
2426
- return { sig, termsPda };
2427
- };
2428
-
2429
- const commitPublicTermsOnChain = async (
2430
- config,
2431
- keypair,
2432
- escrowPda,
2433
- escrowId
2434
- ) => {
2435
- const { program, programId } = getProgram(config, keypair);
2436
- const termsPda = deriveTermsPda(escrowPda, programId);
2437
- const ix = await program.methods
2438
- .commitPublicTerms(new anchor.BN(escrowId.toString()))
2439
- .accounts({
2440
- user: keypair.publicKey,
2441
- escrow: escrowPda,
2442
- terms: termsPda,
2443
- })
2444
- .instruction();
2445
- ix.keys = ix.keys.map((key) => {
2446
- if (key.pubkey.equals(escrowPda) || key.pubkey.equals(termsPda)) {
2447
- return { ...key, isWritable: true };
2448
- }
2449
- return key;
2450
- });
2451
- const tx = new Transaction().add(ix);
2452
- return program.provider.sendAndConfirm(tx, [keypair]);
2453
- };
2454
-
2455
- const fetchPublicMilestones = async (
2456
- program,
2457
- escrowPda,
2458
- programId,
2459
- indices
2460
- ) => {
2461
- const milestones = [];
2462
- for (const index of indices) {
2463
- const pda = deriveMilestonePda(escrowPda, index, programId);
2464
- try {
2465
- const state = await program.account.milestone.fetch(pda);
2466
- milestones.push({ index, status: Number(state.status), pda });
2467
- } catch {
2468
- milestones.push({ index, status: null, pda });
2469
- }
2470
- }
2471
- return milestones;
2472
- };
2473
-
2474
2006
  const runAddTerm = async (config, contract, field, value, options) => {
2475
2007
  const { keypair, wallet, walletKey } = getWalletContext(config);
2476
2008
  ensureEditableContract(contract);
@@ -2534,35 +2066,34 @@ const runAddTerm = async (config, contract, field, value, options) => {
2534
2066
  return;
2535
2067
  }
2536
2068
 
2537
- const executionMode = await ensureExecutionMode(config, contract, keypair, options);
2538
- const l1Mode = executionMode === "l1";
2069
+ await ensureExecutionMode(config, contract, keypair, options);
2539
2070
 
2540
2071
  let encryptedTerms = null;
2541
- if (!l1Mode) {
2542
- try {
2543
- const privacyKey = await getContractPrivacyKey(config, contract.id, wallet);
2544
- encryptedTerms = encryptTermsPayload(
2545
- privacyKey,
2546
- contract.id,
2547
- deadline,
2548
- totalPayment
2549
- );
2550
- } catch (error) {
2551
- console.error(
2552
- "Privacy key exchange pending. Wait for the counterparty to accept."
2553
- );
2554
- process.exit(1);
2555
- }
2072
+ try {
2073
+ const privacyKey = await getContractPrivacyKey(config, contract.id, wallet);
2074
+ encryptedTerms = encryptTermsPayload(
2075
+ privacyKey,
2076
+ contract.id,
2077
+ deadline,
2078
+ totalPayment
2079
+ );
2080
+ } catch (error) {
2081
+ console.error(
2082
+ "Privacy key exchange pending. Wait for the counterparty to accept."
2083
+ );
2084
+ process.exit(1);
2556
2085
  }
2557
2086
 
2558
2087
  await updateContractIfNegotiating(config, contract, {
2559
- ...(l1Mode ? {} : { termsEncrypted: encryptedTerms }),
2088
+ termsEncrypted: encryptedTerms,
2560
2089
  termsHash: buildTermsHashHex(deadline, totalPayment),
2561
- ...(l1Mode ? { deadline, totalPayment } : {}),
2562
2090
  });
2563
2091
 
2564
2092
  if (!deadline || !totalPayment) {
2565
- console.log("Saved term. Set the remaining term to submit on-chain.");
2093
+ const missingLabel = !deadline ? "deadline" : "payment";
2094
+ console.log(
2095
+ `Saved term. Set ${missingLabel} with \`nebulon contract <id> add term ${missingLabel} <value>\` to submit on-chain.`
2096
+ );
2566
2097
  successMessage("Term saved.");
2567
2098
  return;
2568
2099
  }
@@ -2576,32 +2107,27 @@ const runAddTerm = async (config, contract, field, value, options) => {
2576
2107
 
2577
2108
  console.log("Processing.");
2578
2109
  try {
2579
- const sig = l1Mode
2580
- ? await submitPublicTerms(
2581
- config,
2582
- contract,
2583
- keypair,
2584
- escrowPda,
2585
- escrowId,
2586
- totalPayment,
2587
- deadline
2588
- )
2589
- : await submitTerms(
2590
- config,
2591
- contract,
2592
- keypair,
2593
- escrowPda,
2594
- escrowId,
2595
- totalPayment,
2596
- deadline,
2597
- encryptedTerms
2598
- );
2110
+ const sig = await submitTerms(
2111
+ config,
2112
+ contract,
2113
+ keypair,
2114
+ escrowPda,
2115
+ escrowId,
2116
+ totalPayment,
2117
+ deadline,
2118
+ encryptedTerms,
2119
+ options
2120
+ );
2599
2121
  if (!sig) {
2600
2122
  return;
2601
2123
  }
2602
- console.log(`Terms submitted. (tx: ${sig})`);
2124
+ logTx("Terms submitted.", sig, true);
2603
2125
  successMessage("Terms submitted.");
2604
2126
  } catch (error) {
2127
+ console.error(error?.message || error);
2128
+ if (error?.stack) {
2129
+ console.error(error.stack);
2130
+ }
2605
2131
  await printTxLogs(error);
2606
2132
  throw error;
2607
2133
  }
@@ -2613,14 +2139,13 @@ const runSignContract = async (config, contract, options) => {
2613
2139
  console.error("Contract has no escrow. Unable to sign.");
2614
2140
  process.exit(1);
2615
2141
  }
2616
- const executionMode = await ensureExecutionMode(config, contract, keypair, options);
2617
- const l1Mode = executionMode === "l1";
2142
+ await ensureExecutionMode(config, contract, keypair, options);
2618
2143
  ensureContractPhase(
2619
2144
  contract,
2620
2145
  ["negotiating", "awaiting_signatures"],
2621
2146
  "Finish terms/milestones, then sign."
2622
2147
  );
2623
- if (!l1Mode && contract.terms_encrypted && !contract.privacyReady) {
2148
+ if (contract.terms_encrypted && !contract.privacyReady) {
2624
2149
  console.error("Privacy key exchange pending. Unable to read terms.");
2625
2150
  process.exit(1);
2626
2151
  }
@@ -2685,43 +2210,40 @@ const runSignContract = async (config, contract, options) => {
2685
2210
  }
2686
2211
 
2687
2212
  console.log("Processing.");
2213
+ logProgress("Submitting signature on ER");
2688
2214
  let signSig = null;
2689
2215
  let commitSig = null;
2690
- if (l1Mode) {
2691
- signSig = (await signPublicTermsOnChain(
2692
- config,
2693
- keypair,
2694
- escrowPda,
2695
- escrowId
2696
- )).sig;
2697
- console.log(`Signature submitted. (tx: ${signSig})`);
2698
- try {
2699
- commitSig = await commitPublicTermsOnChain(
2700
- config,
2701
- keypair,
2702
- escrowPda,
2703
- escrowId
2704
- );
2705
- } catch (error) {
2706
- commitSig = null;
2707
- }
2708
- } else {
2709
- const { termsPda } = await ensureTermsPrepared(
2216
+ const { termsPda } = await ensureTermsPrepared(
2217
+ config,
2218
+ contract,
2219
+ keypair,
2220
+ escrowPda,
2221
+ escrowId,
2222
+ options
2223
+ );
2224
+ const { program: erProgram, sessionSigner, sessionPda } =
2225
+ await getPerProgramBundle(
2710
2226
  config,
2711
- contract,
2712
2227
  keypair,
2713
- escrowPda,
2714
- escrowId
2228
+ programId,
2229
+ program.provider
2715
2230
  );
2716
- const { program: erProgram, sessionSigner, sessionPda } =
2717
- await getPerProgramBundle(
2718
- config,
2719
- keypair,
2720
- programId,
2721
- program.provider
2722
- );
2723
- signSig = await erProgram.methods
2724
- .signPrivateTerms(new anchor.BN(escrowId.toString()))
2231
+ signSig = await erProgram.methods
2232
+ .signPrivateTerms(new anchor.BN(escrowId.toString()))
2233
+ .accounts({
2234
+ user: keypair.publicKey,
2235
+ payer: sessionSigner.publicKey,
2236
+ sessionToken: sessionPda,
2237
+ escrow: escrowPda,
2238
+ terms: termsPda,
2239
+ })
2240
+ .signers([sessionSigner])
2241
+ .rpc({ skipPreflight: true });
2242
+ logTx("Signature submitted.", signSig, true);
2243
+ try {
2244
+ logProgress("Committing terms on ER");
2245
+ commitSig = await erProgram.methods
2246
+ .commitTerms(new anchor.BN(escrowId.toString()))
2725
2247
  .accounts({
2726
2248
  user: keypair.publicKey,
2727
2249
  payer: sessionSigner.publicKey,
@@ -2731,25 +2253,11 @@ const runSignContract = async (config, contract, options) => {
2731
2253
  })
2732
2254
  .signers([sessionSigner])
2733
2255
  .rpc({ skipPreflight: true });
2734
- console.log(`Signature submitted. (tx: ${signSig})`);
2735
- try {
2736
- commitSig = await erProgram.methods
2737
- .commitTerms(new anchor.BN(escrowId.toString()))
2738
- .accounts({
2739
- user: keypair.publicKey,
2740
- payer: sessionSigner.publicKey,
2741
- sessionToken: sessionPda,
2742
- escrow: escrowPda,
2743
- terms: termsPda,
2744
- })
2745
- .signers([sessionSigner])
2746
- .rpc({ skipPreflight: true });
2747
- } catch (error) {
2748
- commitSig = null;
2749
- }
2256
+ } catch (error) {
2257
+ commitSig = null;
2750
2258
  }
2751
2259
  if (commitSig) {
2752
- console.log(`Terms committed. (tx: ${commitSig})`);
2260
+ logTx("Terms committed.", commitSig, true);
2753
2261
  console.log("Syncing contract flags...");
2754
2262
  let synced = false;
2755
2263
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -2818,7 +2326,7 @@ const runFundContract = async (config, contract, options) => {
2818
2326
  process.exit(1);
2819
2327
  }
2820
2328
  ensureContractPhase(contract, ["waiting_for_funding"], "Run sign first.");
2821
- if (!isL1Mode(contract) && contract.terms_encrypted && !contract.privacyReady) {
2329
+ if (contract.terms_encrypted && !contract.privacyReady) {
2822
2330
  console.error("Privacy key exchange pending. Unable to read terms.");
2823
2331
  process.exit(1);
2824
2332
  }
@@ -2934,10 +2442,10 @@ const runFundContract = async (config, contract, options) => {
2934
2442
  })
2935
2443
  .signers([keypair])
2936
2444
  .rpc();
2937
- console.log(`Funding submitted. (tx: ${sig})`);
2445
+ logTx("Funding submitted.", sig, false);
2938
2446
 
2939
2447
  await markFunded(config.backendUrl, config.auth.token, contract.id);
2940
- console.log("Syncing private state to escrow flags.");
2448
+ logProgress("Syncing private state to escrow flags");
2941
2449
  for (let attempt = 0; attempt < 3; attempt += 1) {
2942
2450
  try {
2943
2451
  await runSyncFlags(config, contract, { ...options, confirm: true });
@@ -2989,75 +2497,7 @@ const runUpdateMilestone = async (config, contract, number, options) => {
2989
2497
  console.error("Check the list with: nebulon contract <id> check milestone list");
2990
2498
  return;
2991
2499
  }
2992
- const executionMode = await ensureExecutionMode(config, contract, keypair, options);
2993
- const l1Mode = executionMode === "l1";
2994
- if (l1Mode) {
2995
- const { program, connection, programId } = getProgram(config, keypair);
2996
- const escrowPda = new PublicKey(contract.escrow_pda);
2997
- const escrowState = await getEscrowState(connection, programId, escrowPda);
2998
- if (!escrowState) {
2999
- console.error("Escrow not found on-chain.");
3000
- process.exit(1);
3001
- }
3002
- const ok = await ensureEscrowOwnedByProgramL1(config, keypair, escrowPda);
3003
- if (!ok) {
3004
- return;
3005
- }
3006
- const escrowId = escrowState.escrowId;
3007
- const milestonePda = deriveMilestonePda(escrowPda, index, programId);
3008
-
3009
- console.log("Verify data and confirm actions please.");
3010
- renderContractSummary(contract, wallet);
3011
- console.log("-Action-");
3012
- console.log("Update milestone");
3013
- console.log(`Number : ${index + 1}`);
3014
- console.log("Status : ready");
3015
- console.log("");
3016
-
3017
- const canProceed = await renderBalancesOrAbort(
3018
- config,
3019
- walletKey,
3020
- contract.mint || config.usdcMint
3021
- );
3022
- if (!canProceed) {
3023
- return;
3024
- }
3025
-
3026
- const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
3027
- if (!proceed) {
3028
- console.log("Canceled.");
3029
- return;
3030
- }
3031
-
3032
- console.log("Processing.");
3033
- const sig = await program.methods
3034
- .submitMilestone(new anchor.BN(escrowId.toString()), index)
3035
- .accounts({
3036
- contractor: walletKey,
3037
- escrow: escrowPda,
3038
- milestone: milestonePda,
3039
- })
3040
- .signers([keypair])
3041
- .rpc();
3042
- console.log(`Milestone updated. (tx: ${sig})`);
3043
-
3044
- const milestones = Array.isArray(contract.milestones)
3045
- ? contract.milestones.map((milestone) =>
3046
- milestone.index === index
3047
- ? { ...milestone, status: "ready" }
3048
- : milestone
3049
- )
3050
- : [];
3051
- const persisted = await updateContractMilestones(config, contract, milestones);
3052
- if (!persisted) {
3053
- console.log(
3054
- "Warning: backend did not persist milestone status (check backend version)."
3055
- );
3056
- }
3057
- await runSyncFlags(config, contract, { ...options, confirm: true });
3058
- successMessage("Milestone updated.");
3059
- return;
3060
- }
2500
+ await ensureExecutionMode(config, contract, keypair, options);
3061
2501
  if (!config.ephemeralProviderUrl) {
3062
2502
  console.error(
3063
2503
  "Warning: MagicBlock RPC is not configured; milestone status checks may be unreliable."
@@ -3144,9 +2584,10 @@ const runUpdateMilestone = async (config, contract, number, options) => {
3144
2584
  keypair,
3145
2585
  walletKey,
3146
2586
  index,
3147
- 1
2587
+ 1,
2588
+ options
3148
2589
  );
3149
- console.log(`Milestone updated. (tx: ${sig})`);
2590
+ logTx("Milestone updated.", sig, true);
3150
2591
 
3151
2592
  const milestones = Array.isArray(contract.milestones)
3152
2593
  ? contract.milestones.map((milestone) =>
@@ -3161,6 +2602,7 @@ const runUpdateMilestone = async (config, contract, number, options) => {
3161
2602
  "Warning: backend did not persist milestone status (check backend version)."
3162
2603
  );
3163
2604
  }
2605
+ logProgress("Syncing private state to escrow flags");
3164
2606
  await runSyncFlags(config, contract, { ...options, confirm: true });
3165
2607
  successMessage("Milestone updated.");
3166
2608
  };
@@ -3187,80 +2629,7 @@ const runConfirmMilestone = async (config, contract, number, options) => {
3187
2629
  process.exit(1);
3188
2630
  }
3189
2631
 
3190
- const executionMode = await ensureExecutionMode(config, contract, keypair, options);
3191
- const l1Mode = executionMode === "l1";
3192
- if (l1Mode) {
3193
- const { program, connection, programId } = getProgram(config, keypair);
3194
- const escrowPda = new PublicKey(contract.escrow_pda);
3195
- const escrowState = await getEscrowState(connection, programId, escrowPda);
3196
- if (!escrowState) {
3197
- console.error("Escrow not found on-chain.");
3198
- process.exit(1);
3199
- }
3200
- const ok = await ensureEscrowOwnedByProgramL1(config, keypair, escrowPda);
3201
- if (!ok) {
3202
- return;
3203
- }
3204
- const escrowId = escrowState.escrowId;
3205
- const milestonePda = deriveMilestonePda(escrowPda, index, programId);
3206
-
3207
- console.log("Verify data and confirm actions please.");
3208
- renderContractSummary(contract, wallet);
3209
- console.log("-Action-");
3210
- console.log("Confirm milestone");
3211
- console.log(`Number : ${index + 1}`);
3212
- console.log("");
3213
-
3214
- const canProceed = await renderBalancesOrAbort(
3215
- config,
3216
- walletKey,
3217
- contract.mint || config.usdcMint
3218
- );
3219
- if (!canProceed) {
3220
- return;
3221
- }
3222
-
3223
- const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
3224
- if (!proceed) {
3225
- console.log("Canceled.");
3226
- return;
3227
- }
3228
-
3229
- console.log("Processing.");
3230
- const sig = await program.methods
3231
- .approveMilestone(new anchor.BN(escrowId.toString()), index)
3232
- .accounts({
3233
- client: walletKey,
3234
- escrow: escrowPda,
3235
- milestone: milestonePda,
3236
- })
3237
- .signers([keypair])
3238
- .rpc();
3239
- console.log(`Milestone confirmed. (tx: ${sig})`);
3240
-
3241
- const milestones = Array.isArray(contract.milestones)
3242
- ? contract.milestones.map((milestone) =>
3243
- milestone.index === index
3244
- ? { ...milestone, status: "approved" }
3245
- : milestone
3246
- )
3247
- : [];
3248
- const persisted = await updateContractMilestones(config, contract, milestones);
3249
- if (!persisted) {
3250
- console.log(
3251
- "Warning: backend did not persist milestone status (check backend version)."
3252
- );
3253
- }
3254
- await runSyncFlags(config, contract, { ...options, confirm: true });
3255
- try {
3256
- const refreshed = await getEscrowState(connection, programId, escrowPda);
3257
- if (refreshed && refreshed.readyToClaim) {
3258
- successMessage("Contract funds are now ready to claim by the service provider.");
3259
- }
3260
- } catch {}
3261
- successMessage("Milestone confirmed.");
3262
- return;
3263
- }
2632
+ await ensureExecutionMode(config, contract, keypair, options);
3264
2633
 
3265
2634
  console.log("Verify data and confirm actions please.");
3266
2635
  renderContractSummary(contract, wallet);
@@ -3299,26 +2668,68 @@ const runConfirmMilestone = async (config, contract, number, options) => {
3299
2668
  walletKey,
3300
2669
  contract.mint || config.usdcMint
3301
2670
  );
3302
- if (!canProceed) {
3303
- return;
3304
- }
3305
-
3306
- const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
3307
- if (!proceed) {
3308
- console.log("Canceled.");
3309
- return;
2671
+ if (!canProceed) {
2672
+ return;
2673
+ }
2674
+
2675
+ const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
2676
+ if (!proceed) {
2677
+ console.log("Canceled.");
2678
+ return;
2679
+ }
2680
+
2681
+ console.log("Processing.");
2682
+ const programBundle = getProgram(config, keypair);
2683
+ const escrowPda = new PublicKey(contract.escrow_pda);
2684
+ const programId = programBundle.programId;
2685
+ const privateMilestonePda = derivePrivateMilestonePda(
2686
+ escrowPda,
2687
+ index,
2688
+ programId
2689
+ );
2690
+ await ensureDelegatedAccount(
2691
+ config,
2692
+ programBundle.program,
2693
+ keypair,
2694
+ { privateMilestone: { escrow: escrowPda, index } },
2695
+ privateMilestonePda,
2696
+ options
2697
+ );
2698
+ const perVaultPda = derivePerVaultPda(escrowPda, programId);
2699
+ await ensureDelegatedAccount(
2700
+ config,
2701
+ programBundle.program,
2702
+ keypair,
2703
+ { perVault: { escrow: escrowPda } },
2704
+ perVaultPda,
2705
+ options
2706
+ );
2707
+ const escrowState = await getEscrowState(
2708
+ programBundle.connection,
2709
+ programId,
2710
+ escrowPda
2711
+ );
2712
+ if (escrowState) {
2713
+ await ensureDelegatedEscrow(
2714
+ config,
2715
+ programBundle.program,
2716
+ keypair,
2717
+ escrowState.escrowId,
2718
+ escrowPda,
2719
+ new PublicKey(contract.client_wallet),
2720
+ options
2721
+ );
3310
2722
  }
3311
-
3312
- console.log("Processing.");
3313
2723
  const sig = await updatePrivateMilestoneStatus(
3314
2724
  config,
3315
2725
  contract,
3316
2726
  keypair,
3317
2727
  walletKey,
3318
2728
  index,
3319
- 3
2729
+ 3,
2730
+ options
3320
2731
  );
3321
- console.log(`Milestone confirmed. (tx: ${sig})`);
2732
+ logTx("Milestone confirmed.", sig, true);
3322
2733
 
3323
2734
  const milestones = Array.isArray(contract.milestones)
3324
2735
  ? contract.milestones.map((milestone) =>
@@ -3415,7 +2826,7 @@ const runClaimFunds = async (config, contract, options) => {
3415
2826
  })
3416
2827
  .signers([keypair])
3417
2828
  .rpc();
3418
- console.log(`Funds claimed. (tx: ${sig})`);
2829
+ logTx("Funds claimed.", sig, false);
3419
2830
  successMessage(
3420
2831
  `Funds claimed successfully. (+${formatUsdc(rawFunded)} USDC)`
3421
2832
  );
@@ -3445,7 +2856,7 @@ const runClaimFunds = async (config, contract, options) => {
3445
2856
  })
3446
2857
  .signers([keypair])
3447
2858
  .rpc();
3448
- console.log(`Timeout funds claimed. (tx: ${sig})`);
2859
+ logTx("Timeout funds claimed.", sig, false);
3449
2860
  successMessage(
3450
2861
  `Funds claimed successfully. (+${formatUsdc(rawFunded)} USDC)`
3451
2862
  );
@@ -3504,7 +2915,7 @@ const runClaimFunds = async (config, contract, options) => {
3504
2915
  })
3505
2916
  .signers([keypair])
3506
2917
  .rpc();
3507
- console.log(`Refund claimed. (tx: ${sig})`);
2918
+ logTx("Refund claimed.", sig, false);
3508
2919
  successMessage("Refund claimed.");
3509
2920
  successMessage("Escrow rent returned to the creator.");
3510
2921
  try {
@@ -3549,7 +2960,8 @@ const updatePrivateMilestoneStatus = async (
3549
2960
  keypair,
3550
2961
  walletKey,
3551
2962
  index,
3552
- status
2963
+ status,
2964
+ options
3553
2965
  ) => {
3554
2966
  const { program, connection, programId } = getProgram(config, keypair);
3555
2967
  const escrowPda = new PublicKey(contract.escrow_pda);
@@ -3566,26 +2978,13 @@ const updatePrivateMilestoneStatus = async (
3566
2978
  programId
3567
2979
  );
3568
2980
 
3569
- const members = buildMembers(contract);
3570
- members.accountType = {
3571
- privateMilestone: {
3572
- escrow: escrowPda,
3573
- index,
3574
- },
3575
- };
3576
- await ensurePermission(config, program, keypair, privateMilestonePda, members);
3577
- await ensureDelegatedPermission(
3578
- config,
3579
- program.provider,
3580
- keypair,
3581
- privateMilestonePda
3582
- );
3583
2981
  await ensureDelegatedAccount(
3584
2982
  config,
3585
2983
  program,
3586
2984
  keypair,
3587
- members.accountType,
3588
- privateMilestonePda
2985
+ { privateMilestone: { escrow: escrowPda, index } },
2986
+ privateMilestonePda,
2987
+ options
3589
2988
  );
3590
2989
  await ensureDelegatedEscrow(
3591
2990
  config,
@@ -3593,12 +2992,15 @@ const updatePrivateMilestoneStatus = async (
3593
2992
  keypair,
3594
2993
  escrowId,
3595
2994
  escrowPda,
3596
- new PublicKey(contract.client_wallet)
2995
+ new PublicKey(contract.client_wallet),
2996
+ options
3597
2997
  );
3598
2998
 
3599
2999
  const { program: erProgram, sessionSigner, sessionPda } =
3600
3000
  await getPerProgramBundle(config, keypair, programId, program.provider);
3601
3001
 
3002
+ logProgress("Submitting milestone update on ER");
3003
+ await sleep(200);
3602
3004
  const sig = await erProgram.methods
3603
3005
  .updatePrivateMilestoneStatus(
3604
3006
  new anchor.BN(escrowId.toString()),
@@ -3618,204 +3020,12 @@ const updatePrivateMilestoneStatus = async (
3618
3020
  return sig;
3619
3021
  };
3620
3022
 
3621
- const runSyncFlagsL1 = async (config, contract, options) => {
3622
- const { keypair, walletKey } = getWalletContext(config);
3623
- if (!contract.escrow_pda) {
3624
- console.error("Contract has no escrow.");
3625
- process.exit(1);
3626
- }
3627
-
3628
- const { program, connection, programId } = getProgram(config, keypair);
3629
- const escrowPda = new PublicKey(contract.escrow_pda);
3630
- const escrowState = await getEscrowState(connection, programId, escrowPda);
3631
- if (!escrowState) {
3632
- console.error("Escrow not found on-chain.");
3633
- process.exit(1);
3634
- }
3635
- const ok = await ensureEscrowOwnedByProgramL1(config, keypair, escrowPda);
3636
- if (!ok) {
3637
- return;
3638
- }
3639
-
3640
- const escrowId = escrowState.escrowId;
3641
- const termsPda = deriveTermsPda(escrowPda, programId);
3642
-
3643
- let terms;
3644
- try {
3645
- terms = await program.account.terms.fetch(termsPda);
3646
- } catch (error) {
3647
- console.error("Terms not available on-chain yet.");
3648
- process.exit(1);
3649
- }
3650
-
3651
- const milestoneIndices = Array.isArray(contract.milestones)
3652
- ? contract.milestones.map((milestone) => milestone.index)
3653
- : [];
3654
- milestoneIndices.sort((a, b) => a - b);
3655
- const milestones = await fetchPublicMilestones(
3656
- program,
3657
- escrowPda,
3658
- programId,
3659
- milestoneIndices
3660
- );
3661
- const milestonesHash = buildMilestonesHash(milestones);
3662
-
3663
- const fundedAmount =
3664
- typeof escrowState.fundedAmount === "bigint"
3665
- ? escrowState.fundedAmount
3666
- : BigInt(escrowState.fundedAmount || 0);
3667
- const canCommitMilestones = fundedAmount === 0n && !escrowState.fundingOk;
3668
- const totalPayment = BigInt(terms.totalPayment.toString());
3669
- const requiredNet = netFromGross(totalPayment);
3670
- const fundingOk = fundedAmount >= requiredNet;
3671
- const hasTermsHash = escrowState.termsHash
3672
- ? escrowState.termsHash.some((byte) => byte !== 0)
3673
- : false;
3674
- if (!hasTermsHash) {
3675
- console.log("Terms hash not committed on-chain yet.");
3676
- }
3677
-
3678
- const deadline = resolveTermsDeadline(
3679
- Number(terms.deadline.toString()),
3680
- escrowState.fundedAt
3681
- );
3682
- const now = Math.floor(Date.now() / 1000);
3683
- const deadlinePassed = deadline ? now > deadline : false;
3684
-
3685
- const statuses = milestones.map((milestone) =>
3686
- Number.isFinite(milestone.status) ? milestone.status : 0
3687
- );
3688
- const allApproved =
3689
- milestones.length === 0 ||
3690
- statuses.every((status) => status === 3 || status === 4);
3691
- const allSubmitted =
3692
- milestones.length === 0 ||
3693
- statuses.every((status) => status === 1 || status === 3 || status === 4);
3694
- const hasUnsubmitted =
3695
- milestones.length === 0 ||
3696
- statuses.some((status) => status === 0 || status === 2);
3697
-
3698
- const milestoneCount = milestones.length;
3699
- if (milestoneCount > 255) {
3700
- console.error("Too many milestones to sync.");
3701
- process.exit(1);
3702
- }
3703
-
3704
- const proceed = await confirmAction(options.confirm, "Update escrow flags? yes/no");
3705
- if (!proceed) {
3706
- console.log("Canceled.");
3707
- return;
3708
- }
3709
-
3710
- let didUpdate = false;
3711
- const milestoneAccounts = milestones.map((milestone) => ({
3712
- pubkey: milestone.pda,
3713
- isWritable: false,
3714
- isSigner: false,
3715
- }));
3716
-
3717
- if (canCommitMilestones && milestonesHash && escrowState.milestonesHash) {
3718
- const currentHash = Buffer.from(escrowState.milestonesHash);
3719
- if (!currentHash.equals(milestonesHash)) {
3720
- const sig = await program.methods
3721
- .commitPublicMilestones(
3722
- new anchor.BN(escrowId.toString()),
3723
- Array.from(milestonesHash)
3724
- )
3725
- .accounts({
3726
- user: walletKey,
3727
- escrow: escrowPda,
3728
- })
3729
- .signers([keypair])
3730
- .rpc();
3731
- console.log(`Milestones committed. (tx: ${sig})`);
3732
- didUpdate = true;
3733
- }
3734
- }
3735
-
3736
- if (hasTermsHash && fundingOk && !escrowState.fundingOk) {
3737
- const sig = await program.methods
3738
- .setFundingOkPublic(new anchor.BN(escrowId.toString()))
3739
- .accounts({
3740
- user: walletKey,
3741
- escrow: escrowPda,
3742
- terms: termsPda,
3743
- })
3744
- .signers([keypair])
3745
- .rpc();
3746
- console.log(`Funding verified. (tx: ${sig})`);
3747
- didUpdate = true;
3748
- }
3749
-
3750
- let readyToClaimSet = false;
3751
- if (hasTermsHash && fundingOk && allApproved && !escrowState.readyToClaim) {
3752
- const sig = await program.methods
3753
- .setReadyToClaimPublic(new anchor.BN(escrowId.toString()), milestoneCount)
3754
- .accounts({
3755
- user: walletKey,
3756
- escrow: escrowPda,
3757
- })
3758
- .remainingAccounts(milestoneAccounts)
3759
- .signers([keypair])
3760
- .rpc();
3761
- console.log(`Ready to claim set. (tx: ${sig})`);
3762
- didUpdate = true;
3763
- readyToClaimSet = true;
3764
- }
3765
-
3766
- if (!readyToClaimSet && deadlinePassed && fundingOk && hasUnsubmitted && !escrowState.timeoutRefundReady) {
3767
- const sig = await program.methods
3768
- .setTimeoutRefundReadyPublic(
3769
- new anchor.BN(escrowId.toString()),
3770
- milestoneCount
3771
- )
3772
- .accounts({
3773
- user: walletKey,
3774
- escrow: escrowPda,
3775
- terms: termsPda,
3776
- })
3777
- .remainingAccounts(milestoneAccounts)
3778
- .signers([keypair])
3779
- .rpc();
3780
- console.log(`Timeout refund ready set. (tx: ${sig})`);
3781
- didUpdate = true;
3782
- }
3783
-
3784
- if (!readyToClaimSet && deadlinePassed && fundingOk && allSubmitted && !escrowState.timeoutFundsReady) {
3785
- const sig = await program.methods
3786
- .setTimeoutFundsReadyPublic(
3787
- new anchor.BN(escrowId.toString()),
3788
- milestoneCount
3789
- )
3790
- .accounts({
3791
- user: walletKey,
3792
- escrow: escrowPda,
3793
- terms: termsPda,
3794
- })
3795
- .remainingAccounts(milestoneAccounts)
3796
- .signers([keypair])
3797
- .rpc();
3798
- console.log(`Timeout funds ready set. (tx: ${sig})`);
3799
- didUpdate = true;
3800
- }
3801
-
3802
- if (didUpdate) {
3803
- successMessage("Sync completed.");
3804
- return;
3805
- }
3806
- console.log("No updates were required.");
3807
- };
3808
-
3809
3023
  const runSyncFlags = async (config, contract, options) => {
3810
3024
  const { keypair, walletKey } = getWalletContext(config);
3811
3025
  if (!contract.escrow_pda) {
3812
3026
  console.error("Contract has no escrow.");
3813
3027
  process.exit(1);
3814
3028
  }
3815
- if (isL1Mode(contract)) {
3816
- await runSyncFlagsL1(config, contract, options);
3817
- return;
3818
- }
3819
3029
 
3820
3030
  const { program, connection, programId } = getProgram(config, keypair);
3821
3031
  const escrowPda = new PublicKey(contract.escrow_pda);
@@ -3826,12 +3036,13 @@ const runSyncFlags = async (config, contract, options) => {
3826
3036
  }
3827
3037
 
3828
3038
  const escrowId = escrowState.escrowId;
3829
- const { termsPda, program: l1Program } = await ensureTermsPrepared(
3039
+ const { termsPda, program: baseProgram } = await ensureTermsPrepared(
3830
3040
  config,
3831
3041
  contract,
3832
3042
  keypair,
3833
3043
  escrowPda,
3834
- escrowId
3044
+ escrowId,
3045
+ options
3835
3046
  );
3836
3047
 
3837
3048
  const milestoneIndices = Array.isArray(contract.milestones)
@@ -3844,40 +3055,28 @@ const runSyncFlags = async (config, contract, options) => {
3844
3055
  index,
3845
3056
  programId
3846
3057
  );
3847
- const members = buildMembers(contract);
3848
- members.accountType = {
3849
- privateMilestone: {
3850
- escrow: escrowPda,
3851
- index,
3852
- },
3853
- };
3854
- await ensurePermission(config, l1Program, keypair, privateMilestonePda, members);
3855
- await ensureDelegatedPermission(
3856
- config,
3857
- l1Program.provider,
3858
- keypair,
3859
- privateMilestonePda
3860
- );
3861
3058
  await ensureDelegatedAccount(
3862
3059
  config,
3863
- l1Program,
3060
+ baseProgram,
3864
3061
  keypair,
3865
- members.accountType,
3866
- privateMilestonePda
3062
+ { privateMilestone: { escrow: escrowPda, index } },
3063
+ privateMilestonePda,
3064
+ options
3867
3065
  );
3868
3066
  }
3869
3067
 
3870
3068
  await ensureDelegatedEscrow(
3871
3069
  config,
3872
- l1Program,
3070
+ baseProgram,
3873
3071
  keypair,
3874
3072
  escrowId,
3875
3073
  escrowPda,
3876
- new PublicKey(contract.client_wallet)
3074
+ new PublicKey(contract.client_wallet),
3075
+ options
3877
3076
  );
3878
3077
 
3879
3078
  const { program: erProgram, sessionSigner, sessionPda } =
3880
- await getPerProgramBundle(config, keypair, programId, l1Program.provider);
3079
+ await getPerProgramBundle(config, keypair, programId, baseProgram.provider);
3881
3080
 
3882
3081
  let terms;
3883
3082
  try {
@@ -3963,7 +3162,7 @@ const runSyncFlags = async (config, contract, options) => {
3963
3162
  })
3964
3163
  .signers([sessionSigner])
3965
3164
  .rpc({ skipPreflight: true });
3966
- console.log(`Milestones committed. (tx: ${sig})`);
3165
+ logTx("Milestones committed.", sig, false);
3967
3166
  didUpdate = true;
3968
3167
  }
3969
3168
  }
@@ -3980,7 +3179,7 @@ const runSyncFlags = async (config, contract, options) => {
3980
3179
  })
3981
3180
  .signers([sessionSigner])
3982
3181
  .rpc({ skipPreflight: true });
3983
- console.log(`Funding verified. (tx: ${sig})`);
3182
+ logTx("Funding verified.", sig, false);
3984
3183
  didUpdate = true;
3985
3184
  }
3986
3185
 
@@ -4000,7 +3199,7 @@ const runSyncFlags = async (config, contract, options) => {
4000
3199
  .remainingAccounts(milestoneAccounts)
4001
3200
  .signers([sessionSigner])
4002
3201
  .rpc({ skipPreflight: true });
4003
- console.log(`Ready to claim set. (tx: ${sig})`);
3202
+ logTx("Ready to claim set.", sig, false);
4004
3203
  didUpdate = true;
4005
3204
  readyToClaimSet = true;
4006
3205
  }
@@ -4027,7 +3226,7 @@ const runSyncFlags = async (config, contract, options) => {
4027
3226
  .remainingAccounts(milestoneAccounts)
4028
3227
  .signers([sessionSigner])
4029
3228
  .rpc({ skipPreflight: true });
4030
- console.log(`Timeout refund ready set. (tx: ${sig})`);
3229
+ logTx("Timeout refund ready set.", sig, false);
4031
3230
  didUpdate = true;
4032
3231
  }
4033
3232
 
@@ -4053,7 +3252,7 @@ const runSyncFlags = async (config, contract, options) => {
4053
3252
  .remainingAccounts(milestoneAccounts)
4054
3253
  .signers([sessionSigner])
4055
3254
  .rpc({ skipPreflight: true });
4056
- console.log(`Timeout funds ready set. (tx: ${sig})`);
3255
+ logTx("Timeout funds ready set.", sig, false);
4057
3256
  didUpdate = true;
4058
3257
  }
4059
3258
 
@@ -4069,7 +3268,7 @@ const runSyncFlags = async (config, contract, options) => {
4069
3268
  })
4070
3269
  .signers([sessionSigner])
4071
3270
  .rpc({ skipPreflight: true });
4072
- console.log(`Escrow committed. (tx: ${sig})`);
3271
+ logTx("Escrow committed.", sig, false);
4073
3272
  } else {
4074
3273
  console.log("No escrow flag changes needed.");
4075
3274
  }
@@ -4116,29 +3315,17 @@ const runMilestoneList = async (config, contract) => {
4116
3315
  milestoneIndices.sort((a, b) => a - b);
4117
3316
 
4118
3317
  let milestones = [];
4119
- if (isL1Mode(contract)) {
4120
- milestones = await fetchPublicMilestones(
4121
- program,
3318
+ const { program: erProgram } = await getEphemeralProgram(config, keypair);
3319
+ try {
3320
+ milestones = await fetchPrivateMilestones(
3321
+ erProgram,
4122
3322
  escrowPda,
4123
3323
  programId,
4124
3324
  milestoneIndices
4125
3325
  );
4126
- } else {
4127
- const { program: erProgram } = await getEphemeralProgram(
4128
- config,
4129
- keypair
4130
- );
4131
- try {
4132
- milestones = await fetchPrivateMilestones(
4133
- erProgram,
4134
- escrowPda,
4135
- programId,
4136
- milestoneIndices
4137
- );
4138
- } catch (error) {
4139
- console.error("Private milestones are not available yet.");
4140
- process.exit(1);
4141
- }
3326
+ } catch (error) {
3327
+ console.error("Private milestones are not available yet.");
3328
+ process.exit(1);
4142
3329
  }
4143
3330
 
4144
3331
  const metadataByIndex = new Map(
@@ -4191,31 +3378,18 @@ const runMilestoneStatus = async (config, contract, number) => {
4191
3378
  }
4192
3379
 
4193
3380
  let milestone;
4194
- if (isL1Mode(contract)) {
4195
- const items = await fetchPublicMilestones(
4196
- program,
3381
+ const { program: erProgram } = await getEphemeralProgram(config, keypair);
3382
+ try {
3383
+ const items = await fetchPrivateMilestones(
3384
+ erProgram,
4197
3385
  escrowPda,
4198
3386
  programId,
4199
3387
  [index]
4200
3388
  );
4201
3389
  milestone = items[0];
4202
- } else {
4203
- const { program: erProgram } = await getEphemeralProgram(
4204
- config,
4205
- keypair
4206
- );
4207
- try {
4208
- const items = await fetchPrivateMilestones(
4209
- erProgram,
4210
- escrowPda,
4211
- programId,
4212
- [index]
4213
- );
4214
- milestone = items[0];
4215
- } catch (error) {
4216
- console.error("Private milestone is not available yet.");
4217
- process.exit(1);
4218
- }
3390
+ } catch (error) {
3391
+ console.error("Private milestone is not available yet.");
3392
+ process.exit(1);
4219
3393
  }
4220
3394
 
4221
3395
  const meta = (contract.milestones || []).find((m) => m.index === index) || {};
@@ -4258,7 +3432,7 @@ const runContractDetails = async (config, contract) => {
4258
3432
  console.log("Contract details");
4259
3433
  console.log(`ID: ${contract.id}`);
4260
3434
  console.log(`Status: ${contract.status}`);
4261
- console.log(`Execution mode: ${getExecutionMode(contract)}`);
3435
+ console.log("Mode: PER");
4262
3436
  console.log(`Role: ${role}`);
4263
3437
  console.log(
4264
3438
  formatParticipant(
@@ -4279,14 +3453,10 @@ const runContractDetails = async (config, contract) => {
4279
3453
  console.log(`Escrow: ${contract.escrow_pda || "n/a"}`);
4280
3454
  console.log(`Mint: ${contract.mint || "n/a"}`);
4281
3455
  console.log(`Vault: ${contract.vault_token || "n/a"}`);
4282
- const privacyLabel = isL1Mode(contract)
4283
- ? "l1"
4284
- : contract.privacyReady
4285
- ? "ready"
4286
- : "pending";
3456
+ const privacyLabel = contract.privacyReady ? "ready" : "pending";
4287
3457
  console.log(`Privacy: ${privacyLabel}`);
4288
3458
 
4289
- if (!isL1Mode(contract) && contract.terms_encrypted && !contract.privacyReady) {
3459
+ if (contract.terms_encrypted && !contract.privacyReady) {
4290
3460
  console.log("Terms: encrypted (waiting on privacy keys)");
4291
3461
  } else {
4292
3462
  console.log(`Deadline: ${contract.deadline ? formatDeadlineValue(contract.deadline) : "missing"}`);
@@ -4346,7 +3516,7 @@ const runContractStatus = async (config, contract) => {
4346
3516
  console.log(`Role: ${role}`);
4347
3517
  }
4348
3518
  console.log(`Current status: ${contract.status}`);
4349
- console.log(`Execution mode: ${getExecutionMode(contract)}`);
3519
+ console.log("Mode: PER");
4350
3520
  if (contract.created_at) {
4351
3521
  console.log(`Created: ${formatCreatedAt(contract.created_at)}`);
4352
3522
  }
@@ -4467,46 +3637,6 @@ const runRateContract = async (config, contract, scoreValue) => {
4467
3637
  console.log(`Rating submitted: ${score} star${score === 1 ? "" : "s"}.`);
4468
3638
  };
4469
3639
 
4470
- const runContractMode = async (config, contract, mode, options) => {
4471
- const { wallet } = getWalletContext(config);
4472
- const role = getRole(contract, wallet);
4473
- if (role !== "client" && role !== "contractor") {
4474
- console.error("Only contract participants can change execution mode.");
4475
- process.exit(1);
4476
- }
4477
- const desired = (mode || "").toString().toLowerCase();
4478
- if (desired !== "per" && desired !== "l1") {
4479
- console.error("Usage: nebulon contract <id> mode per|l1");
4480
- return;
4481
- }
4482
- const allowedStatuses = new Set(["waiting_for_init", "negotiating", "awaiting_signatures"]);
4483
- if (!allowedStatuses.has(contract.status)) {
4484
- console.error("Execution mode can only be changed before funding.");
4485
- return;
4486
- }
4487
- if (desired === getExecutionMode(contract)) {
4488
- console.log(`Execution mode is already ${desired.toUpperCase()}.`);
4489
- return;
4490
- }
4491
- const proceed = await confirmAction(
4492
- options.confirm,
4493
- `Set execution mode to ${desired.toUpperCase()}? yes/no`
4494
- );
4495
- if (!proceed) {
4496
- console.log("Canceled.");
4497
- return;
4498
- }
4499
- await updateContract(config.backendUrl, config.auth.token, contract.id, {
4500
- executionMode: desired,
4501
- });
4502
- contract.execution_mode = desired;
4503
- if (desired === "l1") {
4504
- console.log("L1 MODE enabled for this contract (no TEE privacy).");
4505
- } else {
4506
- console.log("PER mode enabled for this contract.");
4507
- }
4508
- };
4509
-
4510
3640
  const runCheckTerms = async (config, contract) => {
4511
3641
  const { wallet } = getWalletContext(config);
4512
3642
  const role = getRole(contract, wallet);
@@ -4514,7 +3644,7 @@ const runCheckTerms = async (config, contract) => {
4514
3644
  console.error("Only contract participants can view terms.");
4515
3645
  process.exit(1);
4516
3646
  }
4517
- if (!isL1Mode(contract) && contract.terms_encrypted && !contract.privacyReady) {
3647
+ if (contract.terms_encrypted && !contract.privacyReady) {
4518
3648
  console.error("Privacy key exchange pending. Unable to read terms.");
4519
3649
  process.exit(1);
4520
3650
  }
@@ -4580,7 +3710,7 @@ const runOpenDispute = async (config, contract, options) => {
4580
3710
  })
4581
3711
  .signers([keypair])
4582
3712
  .rpc();
4583
- console.log(`Dispute opened. (tx: ${sig})`);
3713
+ logTx("Dispute opened.", sig, false);
4584
3714
  console.log("Contact @nortbyt3 on Discord to discuss your case.");
4585
3715
  successMessage("Dispute opened.");
4586
3716
  } catch (error) {
@@ -4673,7 +3803,7 @@ const runResolveDispute = async (
4673
3803
  })
4674
3804
  .signers([keypair])
4675
3805
  .rpc();
4676
- console.log(`Dispute resolved. (tx: ${sig})`);
3806
+ logTx("Dispute resolved.", sig, false);
4677
3807
  successMessage("Dispute resolved.");
4678
3808
  } catch (error) {
4679
3809
  const message = (error && error.message ? error.message : "").toLowerCase();
@@ -4691,6 +3821,7 @@ const runResolveDispute = async (
4691
3821
 
4692
3822
  const runContractCommand = async (args, options = {}) => {
4693
3823
  const config = loadConfig();
3824
+ config.__verbose = Boolean(options && options.verbose);
4694
3825
  await ensureHosted(config);
4695
3826
 
4696
3827
  if (!args.length) {
@@ -4810,10 +3941,6 @@ const runContractCommand = async (args, options = {}) => {
4810
3941
  await runRateContract(config, contract, rest[1]);
4811
3942
  return;
4812
3943
  }
4813
- if (action === "mode") {
4814
- await runContractMode(config, contract, rest[1], options);
4815
- return;
4816
- }
4817
3944
 
4818
3945
  if (action === "disable" && rest[1] === "milestone") {
4819
3946
  if (!rest[2]) {