nebulon-escrow-cli 0.1.0 → 0.8.5

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,78 @@ 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
- });
157
+ return getProgram(config, signerKeypair, { endpoint, wsEndpoint });
197
158
  }
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;
159
+ if (options.forceRefresh) {
160
+ return getProgram(config, signerKeypair, { endpoint, wsEndpoint });
203
161
  }
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;
162
+ return getProgram(config, signerKeypair, { endpoint, wsEndpoint });
227
163
  };
228
164
 
229
- const ensureTeeWsHealthy = async (connection, wsEndpoint) => {
230
- if (!connection || !wsEndpoint) {
231
- return;
165
+ const getDelegationValidator = async (config) => {
166
+ const endpoint = (config.ephemeralProviderUrl || "").toLowerCase();
167
+ const wsEndpoint = (config.ephemeralWsUrl || "").toLowerCase();
168
+ if (config && config.__verbose) {
169
+ console.log("DEBUG: delegation resolver endpoints", { endpoint, wsEndpoint });
232
170
  }
233
- const cacheKey = wsEndpoint;
234
- const now = Date.now();
235
- const cached = teeWsHealthCache.get(cacheKey);
236
- if (cached && cached.expiresAt > now) {
237
- return;
171
+ if (endpoint.includes("localhost") || endpoint.includes("127.0.0.1")) {
172
+ return LOCAL_VALIDATOR_IDENTITY;
238
173
  }
239
174
 
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...");
175
+ const safePublicKey = (value) => {
176
+ try {
177
+ return value ? new PublicKey(value) : null;
178
+ } catch {
179
+ return null;
246
180
  }
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
- }
181
+ };
182
+
183
+ const shouldUseRouter =
184
+ endpoint.includes("router") ||
185
+ wsEndpoint.includes("router") ||
186
+ endpoint.includes("magicblock.app") ||
187
+ wsEndpoint.includes("magicblock.app");
188
+ if (shouldUseRouter && config.ephemeralProviderUrl) {
189
+ const cacheKey = `${endpoint}|${wsEndpoint}`;
190
+ if (cachedValidator && cachedValidatorEndpoint === cacheKey) {
191
+ if (config && config.__verbose) {
192
+ console.log("DEBUG: using cached validator", cachedValidator.toBase58());
253
193
  }
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");
194
+ return cachedValidator;
279
195
  }
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
196
+ try {
197
+ const router = new ConnectionMagicRouter(config.ephemeralProviderUrl, {
198
+ wsEndpoint: config.ephemeralWsUrl || undefined,
199
+ });
200
+ const closest = await router.getClosestValidator();
201
+ if (config && config.__verbose) {
202
+ console.log("DEBUG: router closest validator:", closest);
287
203
  }
204
+ const identity =
205
+ closest?.validatorIdentity || closest?.identity || closest?.validator;
206
+ const candidate = safePublicKey(identity || closest?.pubkey || closest);
207
+ if (candidate) {
208
+ cachedValidator = candidate;
209
+ cachedValidatorEndpoint = cacheKey;
210
+ return cachedValidator;
211
+ }
212
+ } catch {
213
+ // fall back to configured identity below
288
214
  }
289
215
  }
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;
334
- }
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
- );
349
- }
350
- };
351
-
352
- const getDelegationValidator = async (config) => {
353
- const endpoint = (config.ephemeralProviderUrl || "").toLowerCase();
354
- if (endpoint.includes("localhost") || endpoint.includes("127.0.0.1")) {
355
- return LOCAL_VALIDATOR_IDENTITY;
356
- }
357
- if (endpoint.includes("router")) {
358
- if (cachedValidator && cachedValidatorEndpoint === endpoint) {
359
- return cachedValidator;
216
+ const fallback = safePublicKey(config.ephemeralValidatorIdentity);
217
+ if (fallback) {
218
+ if (config && config.__verbose) {
219
+ console.log("DEBUG: using configured validator identity", fallback.toBase58());
360
220
  }
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;
221
+ return fallback;
368
222
  }
369
- if (config.ephemeralValidatorIdentity) {
370
- return new PublicKey(config.ephemeralValidatorIdentity);
223
+ if (config && config.__verbose) {
224
+ console.warn("DEBUG: no validator identity resolved.");
371
225
  }
372
226
  return null;
373
227
  };
@@ -454,30 +308,6 @@ const ensureEscrowUndelegated = async (config, keypair, escrowPda) => {
454
308
  .rpc({ skipPreflight: true });
455
309
  };
456
310
 
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
311
  const buildPrivacyContext = (scope, contractId, index) => {
482
312
  const tail = index === undefined ? "" : `:${index}`;
483
313
  return `nebulon:${scope}:v1:${contractId}${tail}`;
@@ -748,52 +578,13 @@ const parsePublicKey = (value, label) => {
748
578
  }
749
579
  };
750
580
 
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";
581
+ const ensureExecutionMode = async (config) => {
582
+ if (!isLocalnetConfig(config) && !config.ephemeralProviderUrl) {
583
+ console.warn(
584
+ "Warning: MagicBlock ER endpoint is not configured. Run `nebulon init` to fetch endpoints."
585
+ );
789
586
  }
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";
587
+ return "per";
797
588
  };
798
589
 
799
590
  const feeFromGross = (amount) => (amount * FEE_BPS) / BPS_DENOMINATOR;
@@ -821,6 +612,15 @@ const normalizeHashBytes = (value) => {
821
612
  return Buffer.from(String(value), "utf8").subarray(0, 32);
822
613
  };
823
614
 
615
+ const logProgress = (message) => {
616
+ console.log(chalk.gray(message));
617
+ };
618
+
619
+ const logTx = (label, sig, isEr = false) => {
620
+ const prefix = isEr ? "ER-tx" : "tx";
621
+ console.log(`${label} (${prefix}: ${sig})`);
622
+ };
623
+
824
624
  const buildMilestonesHash = (milestones) => {
825
625
  const hash = crypto.createHash("sha256");
826
626
  const ordered = [...milestones].sort((a, b) => a.index - b.index);
@@ -882,15 +682,6 @@ const getPrivateMilestoneStatus = async (
882
682
  ) => {
883
683
  const { program, programId } = getProgram(config, keypair);
884
684
  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
685
  const { program: erProgram, sessionSigner, sessionPda } =
895
686
  await getPerProgramBundle(config, keypair, programId, program.provider);
896
687
  const milestones = await fetchPrivateMilestones(
@@ -1248,31 +1039,24 @@ const ensureSessionToken = async (config, provider, programId, authorityKey) =>
1248
1039
  };
1249
1040
 
1250
1041
  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 };
1042
+ const { sessionSigner, sessionPda } = await ensureSessionToken(
1043
+ config,
1044
+ program.provider,
1045
+ program.programId,
1046
+ keypair.publicKey
1047
+ );
1048
+ return { signer: sessionSigner, sessionPda, payer: sessionSigner.publicKey };
1261
1049
  };
1262
1050
 
1263
1051
  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 };
1052
+ const { sessionSigner, sessionPda } = await ensureSessionToken(
1053
+ config,
1054
+ provider,
1055
+ programId,
1056
+ keypair.publicKey
1057
+ );
1058
+ const { program } = await getEphemeralProgram(config, sessionSigner);
1059
+ return { program, sessionSigner, sessionPda };
1276
1060
  };
1277
1061
 
1278
1062
  const isAlreadyExistsError = (error) => {
@@ -1284,91 +1068,28 @@ const isAlreadyExistsError = (error) => {
1284
1068
  );
1285
1069
  };
1286
1070
 
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
- };
1071
+ const isVerbose = (options) => Boolean(options && options.verbose);
1359
1072
 
1360
1073
  const ensureDelegatedAccount = async (
1361
1074
  config,
1362
1075
  program,
1363
1076
  keypair,
1364
1077
  accountType,
1365
- pda
1078
+ pda,
1079
+ options
1366
1080
  ) => {
1367
- if (config && config.__l1_mode) {
1368
- return;
1369
- }
1370
1081
  const info = await program.provider.connection.getAccountInfo(pda, "confirmed");
1082
+ if (isVerbose(options)) {
1083
+ console.log("DEBUG: delegation check", {
1084
+ pda: pda.toBase58(),
1085
+ owner: info?.owner?.toBase58?.(),
1086
+ delegated: Boolean(info && info.owner.equals(DELEGATION_PROGRAM_ID)),
1087
+ });
1088
+ }
1371
1089
  if (info && info.owner.equals(DELEGATION_PROGRAM_ID)) {
1090
+ if (isVerbose(options)) {
1091
+ console.log(`Delegation already active for ${pda.toBase58()}; skipping.`);
1092
+ }
1372
1093
  return;
1373
1094
  }
1374
1095
  const isPerVault = accountType && Object.prototype.hasOwnProperty.call(accountType, "perVault");
@@ -1383,6 +1104,13 @@ const ensureDelegatedAccount = async (
1383
1104
  }
1384
1105
  try {
1385
1106
  const validator = await getDelegationValidator(config);
1107
+ if (validator) {
1108
+ if (isVerbose(options)) {
1109
+ console.log(`Delegating account to validator: ${validator.toBase58()}`);
1110
+ }
1111
+ } else if (isVerbose(options)) {
1112
+ console.warn("Delegation skipped: no validator identity resolved.");
1113
+ }
1386
1114
  const remainingAccounts = validator
1387
1115
  ? [{ pubkey: validator, isWritable: false, isSigner: false }]
1388
1116
  : [];
@@ -1394,7 +1122,7 @@ const ensureDelegatedAccount = async (
1394
1122
  })
1395
1123
  .remainingAccounts(remainingAccounts)
1396
1124
  .signers([keypair])
1397
- .rpc();
1125
+ .rpc({ skipPreflight: true });
1398
1126
  } catch (error) {
1399
1127
  if (!isAlreadyExistsError(error)) {
1400
1128
  throw error;
@@ -1408,16 +1136,24 @@ const ensureDelegatedEscrow = async (
1408
1136
  keypair,
1409
1137
  escrowId,
1410
1138
  escrowPda,
1411
- client
1139
+ client,
1140
+ options
1412
1141
  ) => {
1413
- if (config && config.__l1_mode) {
1414
- return;
1415
- }
1416
1142
  const info = await program.provider.connection.getAccountInfo(
1417
1143
  escrowPda,
1418
1144
  "confirmed"
1419
1145
  );
1146
+ if (isVerbose(options)) {
1147
+ console.log("DEBUG: escrow delegation check", {
1148
+ escrow: escrowPda.toBase58(),
1149
+ owner: info?.owner?.toBase58?.(),
1150
+ delegated: Boolean(info && info.owner.equals(DELEGATION_PROGRAM_ID)),
1151
+ });
1152
+ }
1420
1153
  if (info && info.owner.equals(DELEGATION_PROGRAM_ID)) {
1154
+ if (isVerbose(options)) {
1155
+ console.log(`Delegation already active for ${escrowPda.toBase58()}; skipping.`);
1156
+ }
1421
1157
  return;
1422
1158
  }
1423
1159
  if (info && !info.owner.equals(program.programId)) {
@@ -1427,6 +1163,13 @@ const ensureDelegatedEscrow = async (
1427
1163
  }
1428
1164
  try {
1429
1165
  const validator = await getDelegationValidator(config);
1166
+ if (validator) {
1167
+ if (isVerbose(options)) {
1168
+ console.log(`Delegating escrow to validator: ${validator.toBase58()}`);
1169
+ }
1170
+ } else if (isVerbose(options)) {
1171
+ console.warn("Delegation skipped: no validator identity resolved.");
1172
+ }
1430
1173
  const remainingAccounts = validator
1431
1174
  ? [{ pubkey: validator, isWritable: false, isSigner: false }]
1432
1175
  : [];
@@ -1439,7 +1182,7 @@ const ensureDelegatedEscrow = async (
1439
1182
  })
1440
1183
  .remainingAccounts(remainingAccounts)
1441
1184
  .signers([keypair])
1442
- .rpc();
1185
+ .rpc({ skipPreflight: true });
1443
1186
  } catch (error) {
1444
1187
  if (!isAlreadyExistsError(error)) {
1445
1188
  throw error;
@@ -1447,16 +1190,6 @@ const ensureDelegatedEscrow = async (
1447
1190
  }
1448
1191
  };
1449
1192
 
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
1193
  const ensureAta = async (connection, payer, owner, mint) => {
1461
1194
  const ata = await getAssociatedTokenAddress(mint, owner, true);
1462
1195
  try {
@@ -1763,7 +1496,7 @@ const runInitContract = async (config, contract, options) => {
1763
1496
  })
1764
1497
  .signers([keypair])
1765
1498
  .rpc();
1766
- console.log(`Escrow initialized. (tx: ${sig})`);
1499
+ logTx("Escrow initialized.", sig, false);
1767
1500
 
1768
1501
  await linkEscrow(config.backendUrl, config.auth.token, contract.id, {
1769
1502
  escrowPda: escrowPda.toBase58(),
@@ -1832,37 +1565,12 @@ const runAddMilestone = async (config, contract, title, options) => {
1832
1565
  return;
1833
1566
  }
1834
1567
 
1835
- const executionMode = await ensureExecutionMode(config, contract, keypair, options);
1836
- const l1Mode = executionMode === "l1";
1568
+ await ensureExecutionMode(config, contract, keypair, options);
1837
1569
 
1838
1570
  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
- }
1571
+ logProgress("Preparing terms for ER submission");
1572
+ await sleep(200);
1573
+ logProgress("Encrypting milestone details");
1866
1574
  let privacyKey;
1867
1575
  try {
1868
1576
  privacyKey = await getContractPrivacyKey(config, contract.id, wallet);
@@ -1878,6 +1586,10 @@ const runAddMilestone = async (config, contract, title, options) => {
1878
1586
  index,
1879
1587
  title
1880
1588
  );
1589
+ if (!isEncryptedPayload(encryptedDetails)) {
1590
+ console.error("Failed to encrypt milestone details.");
1591
+ process.exit(1);
1592
+ }
1881
1593
 
1882
1594
  const encryptedBytes = Buffer.from(encryptedDetails, "utf8");
1883
1595
 
@@ -1893,6 +1605,7 @@ const runAddMilestone = async (config, contract, title, options) => {
1893
1605
  .update(encryptedDetails)
1894
1606
  .digest();
1895
1607
 
1608
+ logProgress("Preparing milestone accounts");
1896
1609
  const privateMilestonePda = derivePrivateMilestonePda(
1897
1610
  escrowPda,
1898
1611
  index,
@@ -1909,26 +1622,13 @@ const runAddMilestone = async (config, contract, title, options) => {
1909
1622
  .signers([keypair])
1910
1623
  .rpc();
1911
1624
 
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
1625
  await ensureDelegatedAccount(
1927
1626
  config,
1928
1627
  program,
1929
1628
  keypair,
1930
- members.accountType,
1931
- privateMilestonePda
1629
+ { privateMilestone: { escrow: escrowPda, index } },
1630
+ privateMilestonePda,
1631
+ options
1932
1632
  );
1933
1633
 
1934
1634
  const perVaultPda = derivePerVaultPda(escrowPda, programId);
@@ -1937,7 +1637,8 @@ const runAddMilestone = async (config, contract, title, options) => {
1937
1637
  program,
1938
1638
  keypair,
1939
1639
  { perVault: { escrow: escrowPda } },
1940
- perVaultPda
1640
+ perVaultPda,
1641
+ options
1941
1642
  );
1942
1643
  await ensureDelegatedEscrow(
1943
1644
  config,
@@ -1945,9 +1646,11 @@ const runAddMilestone = async (config, contract, title, options) => {
1945
1646
  keypair,
1946
1647
  escrowId,
1947
1648
  escrowPda,
1948
- new PublicKey(contract.client_wallet)
1649
+ new PublicKey(contract.client_wallet),
1650
+ options
1949
1651
  );
1950
1652
 
1653
+ logProgress("Submitting metadata on ER");
1951
1654
  const { program: erProgram, sessionSigner, sessionPda } =
1952
1655
  await getPerProgramBundle(config, keypair, programId, program.provider);
1953
1656
  const metaSig = await erProgram.methods
@@ -1968,8 +1671,7 @@ const runAddMilestone = async (config, contract, title, options) => {
1968
1671
  })
1969
1672
  .signers([sessionSigner])
1970
1673
  .rpc({ skipPreflight: true });
1971
- console.log("Submitting Metadata...");
1972
- console.log(`Done. (tx: ${metaSig})`);
1674
+ logTx("Metadata submitted.", metaSig, true);
1973
1675
 
1974
1676
  const milestones = Array.isArray(contract.milestones)
1975
1677
  ? [...contract.milestones]
@@ -2009,61 +1711,7 @@ const runDisableMilestone = async (config, contract, number, options) => {
2009
1711
  console.error("Check the list with: nebulon contract <id> check milestone list");
2010
1712
  return;
2011
1713
  }
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
- }
1714
+ await ensureExecutionMode(config, contract, keypair, options);
2067
1715
  if (!config.ephemeralProviderUrl) {
2068
1716
  console.error(
2069
1717
  "Warning: MagicBlock RPC is not configured; milestone status checks may be unreliable."
@@ -2085,6 +1733,47 @@ const runDisableMilestone = async (config, contract, number, options) => {
2085
1733
  console.error("Current status: disabled");
2086
1734
  return;
2087
1735
  }
1736
+ const programBundle = getProgram(config, keypair);
1737
+ const escrowPda = new PublicKey(contract.escrow_pda);
1738
+ const programId = programBundle.programId;
1739
+ const privateMilestonePda = derivePrivateMilestonePda(
1740
+ escrowPda,
1741
+ index,
1742
+ programId
1743
+ );
1744
+ await ensureDelegatedAccount(
1745
+ config,
1746
+ programBundle.program,
1747
+ keypair,
1748
+ { privateMilestone: { escrow: escrowPda, index } },
1749
+ privateMilestonePda,
1750
+ options
1751
+ );
1752
+ const perVaultPda = derivePerVaultPda(escrowPda, programId);
1753
+ await ensureDelegatedAccount(
1754
+ config,
1755
+ programBundle.program,
1756
+ keypair,
1757
+ { perVault: { escrow: escrowPda } },
1758
+ perVaultPda,
1759
+ options
1760
+ );
1761
+ const escrowState = await getEscrowState(
1762
+ programBundle.connection,
1763
+ programId,
1764
+ escrowPda
1765
+ );
1766
+ if (escrowState) {
1767
+ await ensureDelegatedEscrow(
1768
+ config,
1769
+ programBundle.program,
1770
+ keypair,
1771
+ escrowState.escrowId,
1772
+ escrowPda,
1773
+ new PublicKey(contract.client_wallet),
1774
+ options
1775
+ );
1776
+ }
2088
1777
  let currentStatus = null;
2089
1778
  try {
2090
1779
  const milestone = await getPrivateMilestoneStatus(
@@ -2151,15 +1840,18 @@ const runDisableMilestone = async (config, contract, number, options) => {
2151
1840
  }
2152
1841
 
2153
1842
  console.log("Processing.");
1843
+ logProgress("Submitting milestone update on ER");
1844
+ await sleep(200);
2154
1845
  const sig = await updatePrivateMilestoneStatus(
2155
1846
  config,
2156
1847
  contract,
2157
1848
  keypair,
2158
1849
  walletKey,
2159
1850
  index,
2160
- 4
1851
+ 4,
1852
+ options
2161
1853
  );
2162
- console.log(`Milestone disabled. (tx: ${sig})`);
1854
+ logTx("Milestone disabled.", sig, true);
2163
1855
 
2164
1856
  const nextMilestones = milestones.map((milestone) =>
2165
1857
  milestone.index === index
@@ -2169,6 +1861,8 @@ const runDisableMilestone = async (config, contract, number, options) => {
2169
1861
  await updateContractIfNegotiating(config, contract, {
2170
1862
  milestones: nextMilestones,
2171
1863
  });
1864
+ logProgress("Syncing private state to escrow flags");
1865
+ logProgress("Syncing private state to escrow flags");
2172
1866
  await runSyncFlags(config, contract, { ...options, confirm: true });
2173
1867
  successMessage("Milestone disabled.");
2174
1868
  };
@@ -2178,7 +1872,8 @@ const ensureTermsPrepared = async (
2178
1872
  contract,
2179
1873
  keypair,
2180
1874
  escrowPda,
2181
- escrowId
1875
+ escrowId,
1876
+ options
2182
1877
  ) => {
2183
1878
  const { program, programId } = getProgram(config, keypair);
2184
1879
  const termsPda = deriveTermsPda(escrowPda, programId);
@@ -2193,17 +1888,13 @@ const ensureTermsPrepared = async (
2193
1888
  .signers([keypair])
2194
1889
  .rpc();
2195
1890
 
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
1891
  await ensureDelegatedAccount(
2202
1892
  config,
2203
1893
  program,
2204
1894
  keypair,
2205
- members.accountType,
2206
- termsPda
1895
+ { terms: { escrow: escrowPda } },
1896
+ termsPda,
1897
+ options
2207
1898
  );
2208
1899
 
2209
1900
  const perVaultPda = derivePerVaultPda(escrowPda, programId);
@@ -2212,7 +1903,8 @@ const ensureTermsPrepared = async (
2212
1903
  program,
2213
1904
  keypair,
2214
1905
  { perVault: { escrow: escrowPda } },
2215
- perVaultPda
1906
+ perVaultPda,
1907
+ options
2216
1908
  );
2217
1909
  await ensureDelegatedEscrow(
2218
1910
  config,
@@ -2220,7 +1912,8 @@ const ensureTermsPrepared = async (
2220
1912
  keypair,
2221
1913
  escrowId,
2222
1914
  escrowPda,
2223
- new PublicKey(contract.client_wallet)
1915
+ new PublicKey(contract.client_wallet),
1916
+ options
2224
1917
  );
2225
1918
 
2226
1919
  return { termsPda, perVaultPda, program, programId };
@@ -2234,19 +1927,26 @@ const submitTerms = async (
2234
1927
  escrowId,
2235
1928
  totalPayment,
2236
1929
  deadline,
2237
- encryptedTerms
1930
+ encryptedTerms,
1931
+ options
2238
1932
  ) => {
2239
1933
  const { termsPda, perVaultPda, program, programId } = await ensureTermsPrepared(
2240
1934
  config,
2241
1935
  contract,
2242
1936
  keypair,
2243
1937
  escrowPda,
2244
- escrowId
1938
+ escrowId,
1939
+ options
2245
1940
  );
2246
1941
 
2247
1942
  const { program: erProgram, sessionSigner, sessionPda } =
2248
1943
  await getPerProgramBundle(config, keypair, programId, program.provider);
2249
1944
  const encryptedBytes = Buffer.from(encryptedTerms || "", "utf8");
1945
+ if (encryptedBytes.length > 256) {
1946
+ throw new Error(
1947
+ `Encrypted terms too large (${encryptedBytes.length} bytes). Max is 256 bytes.`
1948
+ );
1949
+ }
2250
1950
  const attemptCreate = async (forceRefresh = false) => {
2251
1951
  const { program } = forceRefresh
2252
1952
  ? await getEphemeralProgram(config, keypair, { forceRefresh: true })
@@ -2275,7 +1975,7 @@ const submitTerms = async (
2275
1975
  const attemptCreateDirect = async () => {
2276
1976
  const { program: userErProgram } = await getEphemeralProgram(
2277
1977
  config,
2278
- keypair,
1978
+ sessionSigner,
2279
1979
  { forceRefresh: true }
2280
1980
  );
2281
1981
  return userErProgram.methods
@@ -2329,148 +2029,6 @@ const submitTerms = async (
2329
2029
  throw lastError;
2330
2030
  };
2331
2031
 
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
2032
  const runAddTerm = async (config, contract, field, value, options) => {
2475
2033
  const { keypair, wallet, walletKey } = getWalletContext(config);
2476
2034
  ensureEditableContract(contract);
@@ -2534,35 +2092,34 @@ const runAddTerm = async (config, contract, field, value, options) => {
2534
2092
  return;
2535
2093
  }
2536
2094
 
2537
- const executionMode = await ensureExecutionMode(config, contract, keypair, options);
2538
- const l1Mode = executionMode === "l1";
2095
+ await ensureExecutionMode(config, contract, keypair, options);
2539
2096
 
2540
2097
  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
- }
2098
+ try {
2099
+ const privacyKey = await getContractPrivacyKey(config, contract.id, wallet);
2100
+ encryptedTerms = encryptTermsPayload(
2101
+ privacyKey,
2102
+ contract.id,
2103
+ deadline,
2104
+ totalPayment
2105
+ );
2106
+ } catch (error) {
2107
+ console.error(
2108
+ "Privacy key exchange pending. Wait for the counterparty to accept."
2109
+ );
2110
+ process.exit(1);
2556
2111
  }
2557
2112
 
2558
2113
  await updateContractIfNegotiating(config, contract, {
2559
- ...(l1Mode ? {} : { termsEncrypted: encryptedTerms }),
2114
+ termsEncrypted: encryptedTerms,
2560
2115
  termsHash: buildTermsHashHex(deadline, totalPayment),
2561
- ...(l1Mode ? { deadline, totalPayment } : {}),
2562
2116
  });
2563
2117
 
2564
2118
  if (!deadline || !totalPayment) {
2565
- console.log("Saved term. Set the remaining term to submit on-chain.");
2119
+ const missingLabel = !deadline ? "deadline" : "payment";
2120
+ console.log(
2121
+ `Saved term. Set ${missingLabel} with \`nebulon contract <id> add term ${missingLabel} <value>\` to submit on-chain.`
2122
+ );
2566
2123
  successMessage("Term saved.");
2567
2124
  return;
2568
2125
  }
@@ -2576,32 +2133,27 @@ const runAddTerm = async (config, contract, field, value, options) => {
2576
2133
 
2577
2134
  console.log("Processing.");
2578
2135
  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
- );
2136
+ const sig = await submitTerms(
2137
+ config,
2138
+ contract,
2139
+ keypair,
2140
+ escrowPda,
2141
+ escrowId,
2142
+ totalPayment,
2143
+ deadline,
2144
+ encryptedTerms,
2145
+ options
2146
+ );
2599
2147
  if (!sig) {
2600
2148
  return;
2601
2149
  }
2602
- console.log(`Terms submitted. (tx: ${sig})`);
2150
+ logTx("Terms submitted.", sig, true);
2603
2151
  successMessage("Terms submitted.");
2604
2152
  } catch (error) {
2153
+ console.error(error?.message || error);
2154
+ if (error?.stack) {
2155
+ console.error(error.stack);
2156
+ }
2605
2157
  await printTxLogs(error);
2606
2158
  throw error;
2607
2159
  }
@@ -2613,14 +2165,13 @@ const runSignContract = async (config, contract, options) => {
2613
2165
  console.error("Contract has no escrow. Unable to sign.");
2614
2166
  process.exit(1);
2615
2167
  }
2616
- const executionMode = await ensureExecutionMode(config, contract, keypair, options);
2617
- const l1Mode = executionMode === "l1";
2168
+ await ensureExecutionMode(config, contract, keypair, options);
2618
2169
  ensureContractPhase(
2619
2170
  contract,
2620
2171
  ["negotiating", "awaiting_signatures"],
2621
2172
  "Finish terms/milestones, then sign."
2622
2173
  );
2623
- if (!l1Mode && contract.terms_encrypted && !contract.privacyReady) {
2174
+ if (contract.terms_encrypted && !contract.privacyReady) {
2624
2175
  console.error("Privacy key exchange pending. Unable to read terms.");
2625
2176
  process.exit(1);
2626
2177
  }
@@ -2685,41 +2236,46 @@ const runSignContract = async (config, contract, options) => {
2685
2236
  }
2686
2237
 
2687
2238
  console.log("Processing.");
2688
- let signSig = null;
2689
- 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})`);
2239
+ if (isVerbose(options)) {
2240
+ console.log("DEBUG: PER config", {
2241
+ ephemeralProviderUrl: config.ephemeralProviderUrl,
2242
+ ephemeralWsUrl: config.ephemeralWsUrl,
2243
+ ephemeralValidatorIdentity: config.ephemeralValidatorIdentity,
2244
+ rpcUrl: config.rpcUrl,
2245
+ wsUrl: config.wsUrl,
2246
+ network: config.network,
2247
+ });
2698
2248
  try {
2699
- commitSig = await commitPublicTermsOnChain(
2700
- config,
2701
- keypair,
2702
- escrowPda,
2703
- escrowId
2704
- );
2249
+ if (config.ephemeralProviderUrl) {
2250
+ const router = new ConnectionMagicRouter(config.ephemeralProviderUrl, {
2251
+ wsEndpoint: config.ephemeralWsUrl || undefined,
2252
+ });
2253
+ const closest = await router.getClosestValidator();
2254
+ console.log("DEBUG: router closest validator (sign flow):", closest);
2255
+ }
2705
2256
  } catch (error) {
2706
- commitSig = null;
2257
+ console.log("DEBUG: router closest validator lookup failed:", error?.message || error);
2707
2258
  }
2708
- } else {
2709
- const { termsPda } = await ensureTermsPrepared(
2259
+ }
2260
+ logProgress("Submitting signature on ER");
2261
+ let signSig = null;
2262
+ let commitSig = null;
2263
+ const { termsPda } = await ensureTermsPrepared(
2264
+ config,
2265
+ contract,
2266
+ keypair,
2267
+ escrowPda,
2268
+ escrowId,
2269
+ options
2270
+ );
2271
+ const { program: erProgram, sessionSigner, sessionPda } =
2272
+ await getPerProgramBundle(
2710
2273
  config,
2711
- contract,
2712
2274
  keypair,
2713
- escrowPda,
2714
- escrowId
2275
+ programId,
2276
+ program.provider
2715
2277
  );
2716
- const { program: erProgram, sessionSigner, sessionPda } =
2717
- await getPerProgramBundle(
2718
- config,
2719
- keypair,
2720
- programId,
2721
- program.provider
2722
- );
2278
+ try {
2723
2279
  signSig = await erProgram.methods
2724
2280
  .signPrivateTerms(new anchor.BN(escrowId.toString()))
2725
2281
  .accounts({
@@ -2731,25 +2287,33 @@ const runSignContract = async (config, contract, options) => {
2731
2287
  })
2732
2288
  .signers([sessionSigner])
2733
2289
  .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;
2290
+ } catch (error) {
2291
+ console.error("ERROR: signPrivateTerms failed:", error?.message || error);
2292
+ const msg = (error?.message || "").toLowerCase();
2293
+ if (msg.includes("invalidwritableaccount")) {
2294
+ console.error("HINT: InvalidWritableAccount usually means validator mismatch. Run with --verbose and compare router validator vs delegation.");
2749
2295
  }
2296
+ throw error;
2297
+ }
2298
+ logTx("Signature submitted.", signSig, true);
2299
+ try {
2300
+ logProgress("Committing terms on ER");
2301
+ commitSig = await erProgram.methods
2302
+ .commitTerms(new anchor.BN(escrowId.toString()))
2303
+ .accounts({
2304
+ user: keypair.publicKey,
2305
+ payer: sessionSigner.publicKey,
2306
+ sessionToken: sessionPda,
2307
+ escrow: escrowPda,
2308
+ terms: termsPda,
2309
+ })
2310
+ .signers([sessionSigner])
2311
+ .rpc({ skipPreflight: true });
2312
+ } catch (error) {
2313
+ commitSig = null;
2750
2314
  }
2751
2315
  if (commitSig) {
2752
- console.log(`Terms committed. (tx: ${commitSig})`);
2316
+ logTx("Terms committed.", commitSig, true);
2753
2317
  console.log("Syncing contract flags...");
2754
2318
  let synced = false;
2755
2319
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -2818,7 +2382,7 @@ const runFundContract = async (config, contract, options) => {
2818
2382
  process.exit(1);
2819
2383
  }
2820
2384
  ensureContractPhase(contract, ["waiting_for_funding"], "Run sign first.");
2821
- if (!isL1Mode(contract) && contract.terms_encrypted && !contract.privacyReady) {
2385
+ if (contract.terms_encrypted && !contract.privacyReady) {
2822
2386
  console.error("Privacy key exchange pending. Unable to read terms.");
2823
2387
  process.exit(1);
2824
2388
  }
@@ -2934,10 +2498,10 @@ const runFundContract = async (config, contract, options) => {
2934
2498
  })
2935
2499
  .signers([keypair])
2936
2500
  .rpc();
2937
- console.log(`Funding submitted. (tx: ${sig})`);
2501
+ logTx("Funding submitted.", sig, false);
2938
2502
 
2939
2503
  await markFunded(config.backendUrl, config.auth.token, contract.id);
2940
- console.log("Syncing private state to escrow flags.");
2504
+ logProgress("Syncing private state to escrow flags");
2941
2505
  for (let attempt = 0; attempt < 3; attempt += 1) {
2942
2506
  try {
2943
2507
  await runSyncFlags(config, contract, { ...options, confirm: true });
@@ -2989,75 +2553,7 @@ const runUpdateMilestone = async (config, contract, number, options) => {
2989
2553
  console.error("Check the list with: nebulon contract <id> check milestone list");
2990
2554
  return;
2991
2555
  }
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
- }
2556
+ await ensureExecutionMode(config, contract, keypair, options);
3061
2557
  if (!config.ephemeralProviderUrl) {
3062
2558
  console.error(
3063
2559
  "Warning: MagicBlock RPC is not configured; milestone status checks may be unreliable."
@@ -3144,9 +2640,10 @@ const runUpdateMilestone = async (config, contract, number, options) => {
3144
2640
  keypair,
3145
2641
  walletKey,
3146
2642
  index,
3147
- 1
2643
+ 1,
2644
+ options
3148
2645
  );
3149
- console.log(`Milestone updated. (tx: ${sig})`);
2646
+ logTx("Milestone updated.", sig, true);
3150
2647
 
3151
2648
  const milestones = Array.isArray(contract.milestones)
3152
2649
  ? contract.milestones.map((milestone) =>
@@ -3161,6 +2658,7 @@ const runUpdateMilestone = async (config, contract, number, options) => {
3161
2658
  "Warning: backend did not persist milestone status (check backend version)."
3162
2659
  );
3163
2660
  }
2661
+ logProgress("Syncing private state to escrow flags");
3164
2662
  await runSyncFlags(config, contract, { ...options, confirm: true });
3165
2663
  successMessage("Milestone updated.");
3166
2664
  };
@@ -3187,80 +2685,7 @@ const runConfirmMilestone = async (config, contract, number, options) => {
3187
2685
  process.exit(1);
3188
2686
  }
3189
2687
 
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
- }
2688
+ await ensureExecutionMode(config, contract, keypair, options);
3264
2689
 
3265
2690
  console.log("Verify data and confirm actions please.");
3266
2691
  renderContractSummary(contract, wallet);
@@ -3299,26 +2724,68 @@ const runConfirmMilestone = async (config, contract, number, options) => {
3299
2724
  walletKey,
3300
2725
  contract.mint || config.usdcMint
3301
2726
  );
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;
2727
+ if (!canProceed) {
2728
+ return;
2729
+ }
2730
+
2731
+ const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
2732
+ if (!proceed) {
2733
+ console.log("Canceled.");
2734
+ return;
2735
+ }
2736
+
2737
+ console.log("Processing.");
2738
+ const programBundle = getProgram(config, keypair);
2739
+ const escrowPda = new PublicKey(contract.escrow_pda);
2740
+ const programId = programBundle.programId;
2741
+ const privateMilestonePda = derivePrivateMilestonePda(
2742
+ escrowPda,
2743
+ index,
2744
+ programId
2745
+ );
2746
+ await ensureDelegatedAccount(
2747
+ config,
2748
+ programBundle.program,
2749
+ keypair,
2750
+ { privateMilestone: { escrow: escrowPda, index } },
2751
+ privateMilestonePda,
2752
+ options
2753
+ );
2754
+ const perVaultPda = derivePerVaultPda(escrowPda, programId);
2755
+ await ensureDelegatedAccount(
2756
+ config,
2757
+ programBundle.program,
2758
+ keypair,
2759
+ { perVault: { escrow: escrowPda } },
2760
+ perVaultPda,
2761
+ options
2762
+ );
2763
+ const escrowState = await getEscrowState(
2764
+ programBundle.connection,
2765
+ programId,
2766
+ escrowPda
2767
+ );
2768
+ if (escrowState) {
2769
+ await ensureDelegatedEscrow(
2770
+ config,
2771
+ programBundle.program,
2772
+ keypair,
2773
+ escrowState.escrowId,
2774
+ escrowPda,
2775
+ new PublicKey(contract.client_wallet),
2776
+ options
2777
+ );
3310
2778
  }
3311
-
3312
- console.log("Processing.");
3313
2779
  const sig = await updatePrivateMilestoneStatus(
3314
2780
  config,
3315
2781
  contract,
3316
2782
  keypair,
3317
2783
  walletKey,
3318
2784
  index,
3319
- 3
2785
+ 3,
2786
+ options
3320
2787
  );
3321
- console.log(`Milestone confirmed. (tx: ${sig})`);
2788
+ logTx("Milestone confirmed.", sig, true);
3322
2789
 
3323
2790
  const milestones = Array.isArray(contract.milestones)
3324
2791
  ? contract.milestones.map((milestone) =>
@@ -3415,7 +2882,7 @@ const runClaimFunds = async (config, contract, options) => {
3415
2882
  })
3416
2883
  .signers([keypair])
3417
2884
  .rpc();
3418
- console.log(`Funds claimed. (tx: ${sig})`);
2885
+ logTx("Funds claimed.", sig, false);
3419
2886
  successMessage(
3420
2887
  `Funds claimed successfully. (+${formatUsdc(rawFunded)} USDC)`
3421
2888
  );
@@ -3445,7 +2912,7 @@ const runClaimFunds = async (config, contract, options) => {
3445
2912
  })
3446
2913
  .signers([keypair])
3447
2914
  .rpc();
3448
- console.log(`Timeout funds claimed. (tx: ${sig})`);
2915
+ logTx("Timeout funds claimed.", sig, false);
3449
2916
  successMessage(
3450
2917
  `Funds claimed successfully. (+${formatUsdc(rawFunded)} USDC)`
3451
2918
  );
@@ -3504,7 +2971,7 @@ const runClaimFunds = async (config, contract, options) => {
3504
2971
  })
3505
2972
  .signers([keypair])
3506
2973
  .rpc();
3507
- console.log(`Refund claimed. (tx: ${sig})`);
2974
+ logTx("Refund claimed.", sig, false);
3508
2975
  successMessage("Refund claimed.");
3509
2976
  successMessage("Escrow rent returned to the creator.");
3510
2977
  try {
@@ -3549,7 +3016,8 @@ const updatePrivateMilestoneStatus = async (
3549
3016
  keypair,
3550
3017
  walletKey,
3551
3018
  index,
3552
- status
3019
+ status,
3020
+ options
3553
3021
  ) => {
3554
3022
  const { program, connection, programId } = getProgram(config, keypair);
3555
3023
  const escrowPda = new PublicKey(contract.escrow_pda);
@@ -3566,26 +3034,13 @@ const updatePrivateMilestoneStatus = async (
3566
3034
  programId
3567
3035
  );
3568
3036
 
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
3037
  await ensureDelegatedAccount(
3584
3038
  config,
3585
3039
  program,
3586
3040
  keypair,
3587
- members.accountType,
3588
- privateMilestonePda
3041
+ { privateMilestone: { escrow: escrowPda, index } },
3042
+ privateMilestonePda,
3043
+ options
3589
3044
  );
3590
3045
  await ensureDelegatedEscrow(
3591
3046
  config,
@@ -3593,12 +3048,15 @@ const updatePrivateMilestoneStatus = async (
3593
3048
  keypair,
3594
3049
  escrowId,
3595
3050
  escrowPda,
3596
- new PublicKey(contract.client_wallet)
3051
+ new PublicKey(contract.client_wallet),
3052
+ options
3597
3053
  );
3598
3054
 
3599
3055
  const { program: erProgram, sessionSigner, sessionPda } =
3600
3056
  await getPerProgramBundle(config, keypair, programId, program.provider);
3601
3057
 
3058
+ logProgress("Submitting milestone update on ER");
3059
+ await sleep(200);
3602
3060
  const sig = await erProgram.methods
3603
3061
  .updatePrivateMilestoneStatus(
3604
3062
  new anchor.BN(escrowId.toString()),
@@ -3618,204 +3076,12 @@ const updatePrivateMilestoneStatus = async (
3618
3076
  return sig;
3619
3077
  };
3620
3078
 
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
3079
  const runSyncFlags = async (config, contract, options) => {
3810
3080
  const { keypair, walletKey } = getWalletContext(config);
3811
3081
  if (!contract.escrow_pda) {
3812
3082
  console.error("Contract has no escrow.");
3813
3083
  process.exit(1);
3814
3084
  }
3815
- if (isL1Mode(contract)) {
3816
- await runSyncFlagsL1(config, contract, options);
3817
- return;
3818
- }
3819
3085
 
3820
3086
  const { program, connection, programId } = getProgram(config, keypair);
3821
3087
  const escrowPda = new PublicKey(contract.escrow_pda);
@@ -3826,12 +3092,13 @@ const runSyncFlags = async (config, contract, options) => {
3826
3092
  }
3827
3093
 
3828
3094
  const escrowId = escrowState.escrowId;
3829
- const { termsPda, program: l1Program } = await ensureTermsPrepared(
3095
+ const { termsPda, program: baseProgram } = await ensureTermsPrepared(
3830
3096
  config,
3831
3097
  contract,
3832
3098
  keypair,
3833
3099
  escrowPda,
3834
- escrowId
3100
+ escrowId,
3101
+ options
3835
3102
  );
3836
3103
 
3837
3104
  const milestoneIndices = Array.isArray(contract.milestones)
@@ -3844,40 +3111,28 @@ const runSyncFlags = async (config, contract, options) => {
3844
3111
  index,
3845
3112
  programId
3846
3113
  );
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
3114
  await ensureDelegatedAccount(
3862
3115
  config,
3863
- l1Program,
3116
+ baseProgram,
3864
3117
  keypair,
3865
- members.accountType,
3866
- privateMilestonePda
3118
+ { privateMilestone: { escrow: escrowPda, index } },
3119
+ privateMilestonePda,
3120
+ options
3867
3121
  );
3868
3122
  }
3869
3123
 
3870
3124
  await ensureDelegatedEscrow(
3871
3125
  config,
3872
- l1Program,
3126
+ baseProgram,
3873
3127
  keypair,
3874
3128
  escrowId,
3875
3129
  escrowPda,
3876
- new PublicKey(contract.client_wallet)
3130
+ new PublicKey(contract.client_wallet),
3131
+ options
3877
3132
  );
3878
3133
 
3879
3134
  const { program: erProgram, sessionSigner, sessionPda } =
3880
- await getPerProgramBundle(config, keypair, programId, l1Program.provider);
3135
+ await getPerProgramBundle(config, keypair, programId, baseProgram.provider);
3881
3136
 
3882
3137
  let terms;
3883
3138
  try {
@@ -3963,7 +3218,7 @@ const runSyncFlags = async (config, contract, options) => {
3963
3218
  })
3964
3219
  .signers([sessionSigner])
3965
3220
  .rpc({ skipPreflight: true });
3966
- console.log(`Milestones committed. (tx: ${sig})`);
3221
+ logTx("Milestones committed.", sig, false);
3967
3222
  didUpdate = true;
3968
3223
  }
3969
3224
  }
@@ -3980,7 +3235,7 @@ const runSyncFlags = async (config, contract, options) => {
3980
3235
  })
3981
3236
  .signers([sessionSigner])
3982
3237
  .rpc({ skipPreflight: true });
3983
- console.log(`Funding verified. (tx: ${sig})`);
3238
+ logTx("Funding verified.", sig, false);
3984
3239
  didUpdate = true;
3985
3240
  }
3986
3241
 
@@ -4000,7 +3255,7 @@ const runSyncFlags = async (config, contract, options) => {
4000
3255
  .remainingAccounts(milestoneAccounts)
4001
3256
  .signers([sessionSigner])
4002
3257
  .rpc({ skipPreflight: true });
4003
- console.log(`Ready to claim set. (tx: ${sig})`);
3258
+ logTx("Ready to claim set.", sig, false);
4004
3259
  didUpdate = true;
4005
3260
  readyToClaimSet = true;
4006
3261
  }
@@ -4027,7 +3282,7 @@ const runSyncFlags = async (config, contract, options) => {
4027
3282
  .remainingAccounts(milestoneAccounts)
4028
3283
  .signers([sessionSigner])
4029
3284
  .rpc({ skipPreflight: true });
4030
- console.log(`Timeout refund ready set. (tx: ${sig})`);
3285
+ logTx("Timeout refund ready set.", sig, false);
4031
3286
  didUpdate = true;
4032
3287
  }
4033
3288
 
@@ -4053,7 +3308,7 @@ const runSyncFlags = async (config, contract, options) => {
4053
3308
  .remainingAccounts(milestoneAccounts)
4054
3309
  .signers([sessionSigner])
4055
3310
  .rpc({ skipPreflight: true });
4056
- console.log(`Timeout funds ready set. (tx: ${sig})`);
3311
+ logTx("Timeout funds ready set.", sig, false);
4057
3312
  didUpdate = true;
4058
3313
  }
4059
3314
 
@@ -4069,7 +3324,7 @@ const runSyncFlags = async (config, contract, options) => {
4069
3324
  })
4070
3325
  .signers([sessionSigner])
4071
3326
  .rpc({ skipPreflight: true });
4072
- console.log(`Escrow committed. (tx: ${sig})`);
3327
+ logTx("Escrow committed.", sig, false);
4073
3328
  } else {
4074
3329
  console.log("No escrow flag changes needed.");
4075
3330
  }
@@ -4116,29 +3371,17 @@ const runMilestoneList = async (config, contract) => {
4116
3371
  milestoneIndices.sort((a, b) => a - b);
4117
3372
 
4118
3373
  let milestones = [];
4119
- if (isL1Mode(contract)) {
4120
- milestones = await fetchPublicMilestones(
4121
- program,
3374
+ const { program: erProgram } = await getEphemeralProgram(config, keypair);
3375
+ try {
3376
+ milestones = await fetchPrivateMilestones(
3377
+ erProgram,
4122
3378
  escrowPda,
4123
3379
  programId,
4124
3380
  milestoneIndices
4125
3381
  );
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
- }
3382
+ } catch (error) {
3383
+ console.error("Private milestones are not available yet.");
3384
+ process.exit(1);
4142
3385
  }
4143
3386
 
4144
3387
  const metadataByIndex = new Map(
@@ -4191,31 +3434,18 @@ const runMilestoneStatus = async (config, contract, number) => {
4191
3434
  }
4192
3435
 
4193
3436
  let milestone;
4194
- if (isL1Mode(contract)) {
4195
- const items = await fetchPublicMilestones(
4196
- program,
3437
+ const { program: erProgram } = await getEphemeralProgram(config, keypair);
3438
+ try {
3439
+ const items = await fetchPrivateMilestones(
3440
+ erProgram,
4197
3441
  escrowPda,
4198
3442
  programId,
4199
3443
  [index]
4200
3444
  );
4201
3445
  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
- }
3446
+ } catch (error) {
3447
+ console.error("Private milestone is not available yet.");
3448
+ process.exit(1);
4219
3449
  }
4220
3450
 
4221
3451
  const meta = (contract.milestones || []).find((m) => m.index === index) || {};
@@ -4258,7 +3488,7 @@ const runContractDetails = async (config, contract) => {
4258
3488
  console.log("Contract details");
4259
3489
  console.log(`ID: ${contract.id}`);
4260
3490
  console.log(`Status: ${contract.status}`);
4261
- console.log(`Execution mode: ${getExecutionMode(contract)}`);
3491
+ console.log("Mode: PER");
4262
3492
  console.log(`Role: ${role}`);
4263
3493
  console.log(
4264
3494
  formatParticipant(
@@ -4279,14 +3509,10 @@ const runContractDetails = async (config, contract) => {
4279
3509
  console.log(`Escrow: ${contract.escrow_pda || "n/a"}`);
4280
3510
  console.log(`Mint: ${contract.mint || "n/a"}`);
4281
3511
  console.log(`Vault: ${contract.vault_token || "n/a"}`);
4282
- const privacyLabel = isL1Mode(contract)
4283
- ? "l1"
4284
- : contract.privacyReady
4285
- ? "ready"
4286
- : "pending";
3512
+ const privacyLabel = contract.privacyReady ? "ready" : "pending";
4287
3513
  console.log(`Privacy: ${privacyLabel}`);
4288
3514
 
4289
- if (!isL1Mode(contract) && contract.terms_encrypted && !contract.privacyReady) {
3515
+ if (contract.terms_encrypted && !contract.privacyReady) {
4290
3516
  console.log("Terms: encrypted (waiting on privacy keys)");
4291
3517
  } else {
4292
3518
  console.log(`Deadline: ${contract.deadline ? formatDeadlineValue(contract.deadline) : "missing"}`);
@@ -4346,7 +3572,7 @@ const runContractStatus = async (config, contract) => {
4346
3572
  console.log(`Role: ${role}`);
4347
3573
  }
4348
3574
  console.log(`Current status: ${contract.status}`);
4349
- console.log(`Execution mode: ${getExecutionMode(contract)}`);
3575
+ console.log("Mode: PER");
4350
3576
  if (contract.created_at) {
4351
3577
  console.log(`Created: ${formatCreatedAt(contract.created_at)}`);
4352
3578
  }
@@ -4467,46 +3693,6 @@ const runRateContract = async (config, contract, scoreValue) => {
4467
3693
  console.log(`Rating submitted: ${score} star${score === 1 ? "" : "s"}.`);
4468
3694
  };
4469
3695
 
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
3696
  const runCheckTerms = async (config, contract) => {
4511
3697
  const { wallet } = getWalletContext(config);
4512
3698
  const role = getRole(contract, wallet);
@@ -4514,7 +3700,7 @@ const runCheckTerms = async (config, contract) => {
4514
3700
  console.error("Only contract participants can view terms.");
4515
3701
  process.exit(1);
4516
3702
  }
4517
- if (!isL1Mode(contract) && contract.terms_encrypted && !contract.privacyReady) {
3703
+ if (contract.terms_encrypted && !contract.privacyReady) {
4518
3704
  console.error("Privacy key exchange pending. Unable to read terms.");
4519
3705
  process.exit(1);
4520
3706
  }
@@ -4580,7 +3766,7 @@ const runOpenDispute = async (config, contract, options) => {
4580
3766
  })
4581
3767
  .signers([keypair])
4582
3768
  .rpc();
4583
- console.log(`Dispute opened. (tx: ${sig})`);
3769
+ logTx("Dispute opened.", sig, false);
4584
3770
  console.log("Contact @nortbyt3 on Discord to discuss your case.");
4585
3771
  successMessage("Dispute opened.");
4586
3772
  } catch (error) {
@@ -4673,7 +3859,7 @@ const runResolveDispute = async (
4673
3859
  })
4674
3860
  .signers([keypair])
4675
3861
  .rpc();
4676
- console.log(`Dispute resolved. (tx: ${sig})`);
3862
+ logTx("Dispute resolved.", sig, false);
4677
3863
  successMessage("Dispute resolved.");
4678
3864
  } catch (error) {
4679
3865
  const message = (error && error.message ? error.message : "").toLowerCase();
@@ -4691,6 +3877,7 @@ const runResolveDispute = async (
4691
3877
 
4692
3878
  const runContractCommand = async (args, options = {}) => {
4693
3879
  const config = loadConfig();
3880
+ config.__verbose = Boolean(options && options.verbose);
4694
3881
  await ensureHosted(config);
4695
3882
 
4696
3883
  if (!args.length) {
@@ -4810,10 +3997,6 @@ const runContractCommand = async (args, options = {}) => {
4810
3997
  await runRateContract(config, contract, rest[1]);
4811
3998
  return;
4812
3999
  }
4813
- if (action === "mode") {
4814
- await runContractMode(config, contract, rest[1], options);
4815
- return;
4816
- }
4817
4000
 
4818
4001
  if (action === "disable" && rest[1] === "milestone") {
4819
4002
  if (!rest[2]) {