nebulon-escrow-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/bin/nebulon.js +3 -0
- package/idl/nebulon.json +4320 -0
- package/package.json +33 -0
- package/src/capsules.js +154 -0
- package/src/cli.js +292 -0
- package/src/commands/capsule.js +46 -0
- package/src/commands/config.js +445 -0
- package/src/commands/contract.js +4895 -0
- package/src/commands/hosted.js +90 -0
- package/src/commands/init.js +458 -0
- package/src/commands/invites.js +345 -0
- package/src/commands/login.js +320 -0
- package/src/commands/logout.js +22 -0
- package/src/commands/reset.js +102 -0
- package/src/commands/status.js +72 -0
- package/src/commands/tee.js +169 -0
- package/src/commands/wallet.js +166 -0
- package/src/config.js +80 -0
- package/src/constants.js +70 -0
- package/src/hosted.js +288 -0
- package/src/nebulon.js +121 -0
- package/src/paths.js +24 -0
- package/src/privacy.js +117 -0
- package/src/session.js +131 -0
- package/src/solana.js +196 -0
- package/src/ui.js +36 -0
- package/src/wallets.js +85 -0
|
@@ -0,0 +1,4895 @@
|
|
|
1
|
+
|
|
2
|
+
const { prompt } = require("enquirer");
|
|
3
|
+
const chalk = require("chalk");
|
|
4
|
+
const anchor = require("@coral-xyz/anchor");
|
|
5
|
+
const crypto = require("crypto");
|
|
6
|
+
const nacl = require("tweetnacl");
|
|
7
|
+
const {
|
|
8
|
+
PublicKey,
|
|
9
|
+
Keypair,
|
|
10
|
+
SystemProgram,
|
|
11
|
+
Transaction,
|
|
12
|
+
Connection,
|
|
13
|
+
} = require("@solana/web3.js");
|
|
14
|
+
const {
|
|
15
|
+
createAssociatedTokenAccountIdempotent,
|
|
16
|
+
getAssociatedTokenAddress,
|
|
17
|
+
getAccount,
|
|
18
|
+
TOKEN_PROGRAM_ID,
|
|
19
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
20
|
+
} = require("@solana/spl-token");
|
|
21
|
+
const { SessionTokenManager } = require("@magicblock-labs/gum-sdk");
|
|
22
|
+
const {
|
|
23
|
+
createDelegatePermissionInstruction,
|
|
24
|
+
permissionPdaFromAccount,
|
|
25
|
+
PERMISSION_PROGRAM_ID,
|
|
26
|
+
AUTHORITY_FLAG,
|
|
27
|
+
TX_LOGS_FLAG,
|
|
28
|
+
MAGIC_PROGRAM_ID,
|
|
29
|
+
MAGIC_CONTEXT_ID,
|
|
30
|
+
DELEGATION_PROGRAM_ID,
|
|
31
|
+
ConnectionMagicRouter,
|
|
32
|
+
getAuthToken,
|
|
33
|
+
getPermissionStatus,
|
|
34
|
+
waitUntilPermissionActive,
|
|
35
|
+
} = require("@magicblock-labs/ephemeral-rollups-sdk");
|
|
36
|
+
const { loadConfig, saveConfig } = require("../config");
|
|
37
|
+
const { loadWalletKeypair } = require("../wallets");
|
|
38
|
+
const { ensureHostedSession } = require("../session");
|
|
39
|
+
const { successMessage, errorMessage } = require("../ui");
|
|
40
|
+
const { checkTeeAvailability } = require("./tee");
|
|
41
|
+
const {
|
|
42
|
+
getContracts,
|
|
43
|
+
getContract,
|
|
44
|
+
getContractKeys,
|
|
45
|
+
createInvite,
|
|
46
|
+
setContractKey,
|
|
47
|
+
updateContract,
|
|
48
|
+
lockContract,
|
|
49
|
+
signContract,
|
|
50
|
+
markFunded,
|
|
51
|
+
refreshContract,
|
|
52
|
+
linkEscrow,
|
|
53
|
+
rateContract,
|
|
54
|
+
} = require("../hosted");
|
|
55
|
+
const {
|
|
56
|
+
formatAmount,
|
|
57
|
+
getConnection,
|
|
58
|
+
getSolBalance,
|
|
59
|
+
getTokenBalance,
|
|
60
|
+
toPublicKey,
|
|
61
|
+
getEscrowState,
|
|
62
|
+
getMilestoneState,
|
|
63
|
+
} = require("../solana");
|
|
64
|
+
const {
|
|
65
|
+
getProgram,
|
|
66
|
+
deriveMilestonePda,
|
|
67
|
+
derivePrivateMilestonePda,
|
|
68
|
+
deriveEscrowPda,
|
|
69
|
+
deriveTermsPda,
|
|
70
|
+
derivePerVaultPda,
|
|
71
|
+
deriveDisputePda,
|
|
72
|
+
textToHash,
|
|
73
|
+
} = require("../nebulon");
|
|
74
|
+
const {
|
|
75
|
+
ensureContractKeypair,
|
|
76
|
+
deriveContractKey,
|
|
77
|
+
encryptPayload,
|
|
78
|
+
decryptPayload,
|
|
79
|
+
isEncryptedPayload,
|
|
80
|
+
} = require("../privacy");
|
|
81
|
+
|
|
82
|
+
const FEE_RECEIVER = new PublicKey(
|
|
83
|
+
"w8sdYr2sM1dfyD7vsTt6EXcQWQ1mfNWfQJMzQNNnUXq"
|
|
84
|
+
);
|
|
85
|
+
const LOCAL_VALIDATOR_IDENTITY = new PublicKey(
|
|
86
|
+
"mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev"
|
|
87
|
+
);
|
|
88
|
+
const FEE_BPS = 200n;
|
|
89
|
+
const BPS_DENOMINATOR = 10_000n;
|
|
90
|
+
|
|
91
|
+
const ACTIVE_STATUSES = new Set([
|
|
92
|
+
"waiting_for_milestones_report",
|
|
93
|
+
"in_progress",
|
|
94
|
+
"ready_to_claim",
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
const EXCLUDE_INVITE_STATUSES = new Set([
|
|
98
|
+
"pending_invite",
|
|
99
|
+
"invite_expired",
|
|
100
|
+
"invite_canceled",
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
const ROLE_CHOICES = [
|
|
104
|
+
{
|
|
105
|
+
name: "client",
|
|
106
|
+
message: `Im the Client ${chalk.gray("(Ill fund this contract)")}`,
|
|
107
|
+
value: "client",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "contractor",
|
|
111
|
+
message: `Im the Service Provider ${chalk.gray(
|
|
112
|
+
"(The other user will fund this contract)"
|
|
113
|
+
)}`,
|
|
114
|
+
value: "contractor",
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const SHARE_CHOICES = [
|
|
119
|
+
{ name: "direct", message: "invite directly by Nebulon ID", value: "direct" },
|
|
120
|
+
{ name: "link", message: "generate an invitation link", value: "link" },
|
|
121
|
+
{ name: "code", message: "generate a contract code", value: "code" },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const isPromptCancel = (error) => {
|
|
125
|
+
if (!error) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
if (typeof error === "string") {
|
|
129
|
+
return error.trim() === "";
|
|
130
|
+
}
|
|
131
|
+
const message = (error.message || "").toLowerCase();
|
|
132
|
+
return (
|
|
133
|
+
message.includes("cancel") ||
|
|
134
|
+
message.includes("aborted") ||
|
|
135
|
+
message.includes("terminated")
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const normalizeHandle = (value) =>
|
|
140
|
+
value.trim().toLowerCase().replace(/^@/, "");
|
|
141
|
+
|
|
142
|
+
let cachedValidator = null;
|
|
143
|
+
let cachedValidatorEndpoint = null;
|
|
144
|
+
|
|
145
|
+
const normalizeEndpoint = (endpoint) => endpoint.replace(/\/$/, "");
|
|
146
|
+
|
|
147
|
+
const TEE_TOKEN_TTL_SECONDS = 240;
|
|
148
|
+
const teeTokenCache = new Map();
|
|
149
|
+
const teeWsHealthCache = new Map();
|
|
150
|
+
const teeProgramCache = new Map();
|
|
151
|
+
|
|
152
|
+
const isLocalnetConfig = (config) => {
|
|
153
|
+
const network = (config?.network || config?.solanaNetwork || "").toLowerCase();
|
|
154
|
+
if (network === "localnet") {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
const rpc = String(config?.rpcUrl || "");
|
|
158
|
+
const er = String(config?.ephemeralProviderUrl || "");
|
|
159
|
+
return (
|
|
160
|
+
rpc.includes("localhost:8899") ||
|
|
161
|
+
rpc.includes("127.0.0.1:8899") ||
|
|
162
|
+
er.includes("localhost:7799") ||
|
|
163
|
+
er.includes("127.0.0.1:7799")
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
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
|
+
const getEphemeralProgram = async (config, signerKeypair, options = {}) => {
|
|
192
|
+
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;
|
|
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;
|
|
360
|
+
}
|
|
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;
|
|
368
|
+
}
|
|
369
|
+
if (config.ephemeralValidatorIdentity) {
|
|
370
|
+
return new PublicKey(config.ephemeralValidatorIdentity);
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const formatCreatedAt = (value) => {
|
|
376
|
+
if (!value) {
|
|
377
|
+
return "n/a";
|
|
378
|
+
}
|
|
379
|
+
const date = new Date(value * 1000);
|
|
380
|
+
if (Number.isNaN(date.getTime())) {
|
|
381
|
+
return "n/a";
|
|
382
|
+
}
|
|
383
|
+
return date.toISOString().replace("T", " ").slice(0, 19);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const trimPubkey = (value) =>
|
|
387
|
+
value ? `${value.slice(0, 4)}...${value.slice(-4)}` : "n/a";
|
|
388
|
+
|
|
389
|
+
const formatParticipant = (label, handle, wallet, selfWallet) => {
|
|
390
|
+
const tag = handle ? `@${handle}` : "unknown";
|
|
391
|
+
const suffix = wallet === selfWallet ? " (YOU)" : "";
|
|
392
|
+
return `${label}: ${tag} (${trimPubkey(wallet)})${suffix}`;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const formatSignerLabel = (label, handle, wallet) => {
|
|
396
|
+
const tag = handle ? `@${handle}` : "unknown";
|
|
397
|
+
return `${label}: ${tag} (${trimPubkey(wallet)})`;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const ensureEditableContract = (contract) => {
|
|
401
|
+
if (contract.status !== "negotiating") {
|
|
402
|
+
console.error("Contract terms/milestones are locked.");
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
if (contract.client_signed_at || contract.contractor_signed_at) {
|
|
406
|
+
console.error("Contract is already signed. Terms and milestones are immutable.");
|
|
407
|
+
if (contract.client_signed_at) {
|
|
408
|
+
console.error(
|
|
409
|
+
`Signed by customer: ${formatSignerLabel(
|
|
410
|
+
"Customer",
|
|
411
|
+
contract.client_handle,
|
|
412
|
+
contract.client_wallet
|
|
413
|
+
)}`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
if (contract.contractor_signed_at) {
|
|
417
|
+
console.error(
|
|
418
|
+
`Signed by service provider: ${formatSignerLabel(
|
|
419
|
+
"Service Provider",
|
|
420
|
+
contract.contractor_handle,
|
|
421
|
+
contract.contractor_wallet
|
|
422
|
+
)}`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const ensureContractPhase = (contract, allowed, nextHint) => {
|
|
430
|
+
if (allowed.includes(contract.status)) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
console.error(`Invalid command for current phase. [Current_phase : ${contract.status}]`);
|
|
434
|
+
process.exit(1);
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const ensureEscrowUndelegated = async (config, keypair, escrowPda) => {
|
|
438
|
+
const { connection } = getProgram(config, keypair);
|
|
439
|
+
const info = await connection.getAccountInfo(escrowPda, "confirmed");
|
|
440
|
+
if (!info || !info.owner || !info.owner.equals(DELEGATION_PROGRAM_ID)) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const { programId } = getProgram(config, keypair);
|
|
444
|
+
const { program: erProgram } = await getEphemeralProgram(config, keypair);
|
|
445
|
+
await erProgram.methods
|
|
446
|
+
.undelegateEscrow()
|
|
447
|
+
.accounts({
|
|
448
|
+
payer: keypair.publicKey,
|
|
449
|
+
escrow: escrowPda,
|
|
450
|
+
magicProgram: MAGIC_PROGRAM_ID,
|
|
451
|
+
magicContext: MAGIC_CONTEXT_ID,
|
|
452
|
+
})
|
|
453
|
+
.signers([keypair])
|
|
454
|
+
.rpc({ skipPreflight: true });
|
|
455
|
+
};
|
|
456
|
+
|
|
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
|
+
const buildPrivacyContext = (scope, contractId, index) => {
|
|
482
|
+
const tail = index === undefined ? "" : `:${index}`;
|
|
483
|
+
return `nebulon:${scope}:v1:${contractId}${tail}`;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const getContractPrivacyKey = async (config, contractId, wallet) => {
|
|
487
|
+
const keypair = ensureContractKeypair(config, contractId);
|
|
488
|
+
const keys = await getContractKeys(
|
|
489
|
+
config.backendUrl,
|
|
490
|
+
config.auth.token,
|
|
491
|
+
contractId
|
|
492
|
+
);
|
|
493
|
+
const entries = Array.isArray(keys.keys) ? keys.keys : [];
|
|
494
|
+
const peer = entries.find((entry) => entry.wallet !== wallet);
|
|
495
|
+
if (!peer || !peer.public_key) {
|
|
496
|
+
throw new Error("privacy_key_pending");
|
|
497
|
+
}
|
|
498
|
+
return deriveContractKey(keypair.secretKey, peer.public_key, contractId);
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const encryptTermsPayload = (key, contractId, deadline, totalPayment) => {
|
|
502
|
+
const payload = JSON.stringify({
|
|
503
|
+
deadline: deadline || null,
|
|
504
|
+
totalPayment: totalPayment || null,
|
|
505
|
+
});
|
|
506
|
+
return encryptPayload(key, payload, buildPrivacyContext("terms", contractId));
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const decryptTermsPayload = (key, contractId, encrypted) => {
|
|
510
|
+
const plaintext = decryptPayload(
|
|
511
|
+
key,
|
|
512
|
+
encrypted,
|
|
513
|
+
buildPrivacyContext("terms", contractId)
|
|
514
|
+
);
|
|
515
|
+
const parsed = JSON.parse(plaintext);
|
|
516
|
+
return {
|
|
517
|
+
deadline: parsed.deadline || null,
|
|
518
|
+
totalPayment: parsed.totalPayment || null,
|
|
519
|
+
};
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const encryptMilestonePayload = (key, contractId, index, title) => {
|
|
523
|
+
const payload = JSON.stringify({ title });
|
|
524
|
+
return encryptPayload(
|
|
525
|
+
key,
|
|
526
|
+
payload,
|
|
527
|
+
buildPrivacyContext("milestone", contractId, index)
|
|
528
|
+
);
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const decryptMilestonePayload = (key, contractId, index, encrypted) => {
|
|
532
|
+
const plaintext = decryptPayload(
|
|
533
|
+
key,
|
|
534
|
+
encrypted,
|
|
535
|
+
buildPrivacyContext("milestone", contractId, index)
|
|
536
|
+
);
|
|
537
|
+
const parsed = JSON.parse(plaintext);
|
|
538
|
+
return parsed.title || "";
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const getMilestoneLabel = (milestone) => {
|
|
542
|
+
if (!milestone) {
|
|
543
|
+
return "(untitled)";
|
|
544
|
+
}
|
|
545
|
+
if (milestone.title) {
|
|
546
|
+
return milestone.title;
|
|
547
|
+
}
|
|
548
|
+
if (milestone.details && !isEncryptedPayload(milestone.details)) {
|
|
549
|
+
return milestone.details;
|
|
550
|
+
}
|
|
551
|
+
if (milestone.details && isEncryptedPayload(milestone.details)) {
|
|
552
|
+
return "(encrypted)";
|
|
553
|
+
}
|
|
554
|
+
return "(untitled)";
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const updateContractIfNegotiating = async (config, contract, payload) => {
|
|
558
|
+
if (contract.status !== "negotiating") {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
await updateContract(config.backendUrl, config.auth.token, contract.id, payload);
|
|
563
|
+
return true;
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (error?.message === "invalid_status") {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const updateContractMilestones = async (config, contract, milestones) => {
|
|
573
|
+
try {
|
|
574
|
+
await updateContract(config.backendUrl, config.auth.token, contract.id, {
|
|
575
|
+
milestones,
|
|
576
|
+
});
|
|
577
|
+
return true;
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (error?.message === "invalid_status") {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const formatUsdc = (amountBase) => {
|
|
587
|
+
const raw = BigInt(amountBase || 0);
|
|
588
|
+
const whole = raw / 1_000_000n;
|
|
589
|
+
const fraction = (raw % 1_000_000n).toString().padStart(6, "0");
|
|
590
|
+
return `${whole.toString()}.${fraction}`;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const printTxLogs = async (error) => {
|
|
594
|
+
if (!error) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (Array.isArray(error.logs) && error.logs.length) {
|
|
598
|
+
console.error("Transaction logs:");
|
|
599
|
+
error.logs.forEach((line) => console.error(line));
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (typeof error.getLogs === "function") {
|
|
603
|
+
try {
|
|
604
|
+
const logs = await error.getLogs();
|
|
605
|
+
if (Array.isArray(logs) && logs.length) {
|
|
606
|
+
console.error("Transaction logs:");
|
|
607
|
+
logs.forEach((line) => console.error(line));
|
|
608
|
+
}
|
|
609
|
+
} catch {
|
|
610
|
+
// ignore log fetch failures
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const parseAmountInput = (value) => {
|
|
616
|
+
const trimmed = String(value || "").trim();
|
|
617
|
+
const cleaned = trimmed
|
|
618
|
+
.toLowerCase()
|
|
619
|
+
.replace(/usdc/g, "")
|
|
620
|
+
.replace(/\\s+/g, "")
|
|
621
|
+
.replace(/[$,_]/g, "")
|
|
622
|
+
.replace(/,/g, "");
|
|
623
|
+
if (!cleaned || !/^\d+(\.\d+)?$/.test(cleaned)) {
|
|
624
|
+
throw new Error("Invalid amount.");
|
|
625
|
+
}
|
|
626
|
+
const [wholePart, fracPart = ""] = cleaned.split(".");
|
|
627
|
+
if (fracPart.length > 6) {
|
|
628
|
+
throw new Error("Amount supports up to 6 decimals.");
|
|
629
|
+
}
|
|
630
|
+
const normalized = fracPart.padEnd(6, "0");
|
|
631
|
+
const base = BigInt(wholePart) * 1_000_000n + BigInt(normalized || "0");
|
|
632
|
+
return {
|
|
633
|
+
base,
|
|
634
|
+
display: `${formatUsdc(base)} USDC`,
|
|
635
|
+
};
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const formatRelativeSeconds = (seconds) => {
|
|
639
|
+
const abs = Math.abs(seconds);
|
|
640
|
+
if (abs % 86400 === 0) {
|
|
641
|
+
return `${abs / 86400}d`;
|
|
642
|
+
}
|
|
643
|
+
if (abs % 3600 === 0) {
|
|
644
|
+
return `${abs / 3600}h`;
|
|
645
|
+
}
|
|
646
|
+
if (abs % 60 === 0) {
|
|
647
|
+
return `${abs / 60}m`;
|
|
648
|
+
}
|
|
649
|
+
return `${abs}s`;
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const parseDeadlineInput = (value) => {
|
|
653
|
+
const trimmed = (value || "").trim();
|
|
654
|
+
if (!trimmed) {
|
|
655
|
+
throw new Error("Deadline required.");
|
|
656
|
+
}
|
|
657
|
+
const relativeMatch = trimmed.match(/^(\d+)([smhd])$/i);
|
|
658
|
+
if (relativeMatch) {
|
|
659
|
+
const qty = Number(relativeMatch[1]);
|
|
660
|
+
const unit = relativeMatch[2].toLowerCase();
|
|
661
|
+
const seconds =
|
|
662
|
+
unit === "d"
|
|
663
|
+
? qty * 86400
|
|
664
|
+
: unit === "h"
|
|
665
|
+
? qty * 3600
|
|
666
|
+
: unit === "m"
|
|
667
|
+
? qty * 60
|
|
668
|
+
: qty;
|
|
669
|
+
return {
|
|
670
|
+
deadline: new anchor.BN(seconds).neg(),
|
|
671
|
+
label: `${formatRelativeSeconds(seconds)} from funding`,
|
|
672
|
+
kind: "relative",
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (/^\d+$/.test(trimmed)) {
|
|
677
|
+
const seconds = Number(trimmed);
|
|
678
|
+
if (!Number.isFinite(seconds)) {
|
|
679
|
+
throw new Error("Invalid deadline.");
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
deadline: new anchor.BN(seconds),
|
|
683
|
+
label: new Date(seconds * 1000)
|
|
684
|
+
.toISOString()
|
|
685
|
+
.replace("T", " ")
|
|
686
|
+
.slice(0, 19),
|
|
687
|
+
kind: "absolute",
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const parsed = new Date(trimmed);
|
|
692
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
693
|
+
const seconds = Math.floor(parsed.getTime() / 1000);
|
|
694
|
+
return {
|
|
695
|
+
deadline: new anchor.BN(seconds),
|
|
696
|
+
label: parsed.toISOString().replace("T", " ").slice(0, 19),
|
|
697
|
+
kind: "absolute",
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
throw new Error("Invalid deadline format.");
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const formatDeadlineValue = (deadline) => {
|
|
705
|
+
if (deadline === null || deadline === undefined) {
|
|
706
|
+
return "n/a";
|
|
707
|
+
}
|
|
708
|
+
const value = Number(deadline);
|
|
709
|
+
if (!Number.isFinite(value) || value === 0) {
|
|
710
|
+
return "n/a";
|
|
711
|
+
}
|
|
712
|
+
if (value < 0) {
|
|
713
|
+
return `${formatRelativeSeconds(value)} from funding`;
|
|
714
|
+
}
|
|
715
|
+
return new Date(value * 1000)
|
|
716
|
+
.toISOString()
|
|
717
|
+
.replace("T", " ")
|
|
718
|
+
.slice(0, 19);
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const buildTermsHash = (deadline, payment) => {
|
|
722
|
+
const payload = `deadline:${deadline ?? ""}|payment:${payment ?? ""}`;
|
|
723
|
+
return textToHash(payload);
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
const buildTermsHashHex = (deadline, payment) =>
|
|
727
|
+
Buffer.from(buildTermsHash(deadline, payment)).toString("hex");
|
|
728
|
+
|
|
729
|
+
const parsePublicKey = (value, label) => {
|
|
730
|
+
if (!value) {
|
|
731
|
+
console.error(`Missing ${label} address.`);
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
try {
|
|
735
|
+
if (value instanceof PublicKey) {
|
|
736
|
+
return value;
|
|
737
|
+
}
|
|
738
|
+
if (value?.toBase58) {
|
|
739
|
+
return new PublicKey(value.toBase58());
|
|
740
|
+
}
|
|
741
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
742
|
+
return new PublicKey(value);
|
|
743
|
+
}
|
|
744
|
+
return new PublicKey(String(value));
|
|
745
|
+
} catch (error) {
|
|
746
|
+
console.error(`Invalid ${label} address: ${String(value)}`);
|
|
747
|
+
process.exit(1);
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
|
|
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";
|
|
789
|
+
}
|
|
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";
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const feeFromGross = (amount) => (amount * FEE_BPS) / BPS_DENOMINATOR;
|
|
800
|
+
|
|
801
|
+
const netFromGross = (amount) => amount - feeFromGross(amount);
|
|
802
|
+
|
|
803
|
+
const grossFromNet = (amount) => {
|
|
804
|
+
const denom = BPS_DENOMINATOR - FEE_BPS;
|
|
805
|
+
return (amount * BPS_DENOMINATOR + denom - 1n) / denom;
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
const normalizeHashBytes = (value) => {
|
|
809
|
+
if (!value) {
|
|
810
|
+
return Buffer.alloc(32);
|
|
811
|
+
}
|
|
812
|
+
if (Buffer.isBuffer(value)) {
|
|
813
|
+
return value;
|
|
814
|
+
}
|
|
815
|
+
if (value instanceof Uint8Array) {
|
|
816
|
+
return Buffer.from(value);
|
|
817
|
+
}
|
|
818
|
+
if (Array.isArray(value)) {
|
|
819
|
+
return Buffer.from(value);
|
|
820
|
+
}
|
|
821
|
+
return Buffer.from(String(value), "utf8").subarray(0, 32);
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
const buildMilestonesHash = (milestones) => {
|
|
825
|
+
const hash = crypto.createHash("sha256");
|
|
826
|
+
const ordered = [...milestones].sort((a, b) => a.index - b.index);
|
|
827
|
+
ordered.forEach((milestone) => {
|
|
828
|
+
hash.update(Buffer.from([milestone.index]));
|
|
829
|
+
hash.update(normalizeHashBytes(milestone.descriptionHash));
|
|
830
|
+
});
|
|
831
|
+
return hash.digest();
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
const resolveTermsDeadline = (deadline, fundedAt) => {
|
|
835
|
+
const value = Number(deadline);
|
|
836
|
+
if (!Number.isFinite(value)) {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
if (value > 0) {
|
|
840
|
+
return value;
|
|
841
|
+
}
|
|
842
|
+
const funded = Number(fundedAt || 0);
|
|
843
|
+
if (!funded) {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
return funded + Math.abs(value);
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
const formatPrivateMilestoneStatus = (status) => {
|
|
850
|
+
switch (status) {
|
|
851
|
+
case 0:
|
|
852
|
+
return "created";
|
|
853
|
+
case 1:
|
|
854
|
+
return "submitted";
|
|
855
|
+
case 2:
|
|
856
|
+
return "rejected";
|
|
857
|
+
case 3:
|
|
858
|
+
return "paid";
|
|
859
|
+
case 4:
|
|
860
|
+
return "disabled";
|
|
861
|
+
default:
|
|
862
|
+
return "unknown";
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
const isRemoteSubscriptionError = (error) => {
|
|
867
|
+
const message = (error?.message || "").toLowerCase();
|
|
868
|
+
return (
|
|
869
|
+
message.includes("remote account provider") ||
|
|
870
|
+
message.includes("failed to manage subscriptions") ||
|
|
871
|
+
message.includes("accountsubscriptionstaskfailed") ||
|
|
872
|
+
message.includes("magicblock-0 disconnected")
|
|
873
|
+
);
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
const getPrivateMilestoneStatus = async (
|
|
877
|
+
config,
|
|
878
|
+
contract,
|
|
879
|
+
keypair,
|
|
880
|
+
walletKey,
|
|
881
|
+
index
|
|
882
|
+
) => {
|
|
883
|
+
const { program, programId } = getProgram(config, keypair);
|
|
884
|
+
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
|
+
const { program: erProgram, sessionSigner, sessionPda } =
|
|
895
|
+
await getPerProgramBundle(config, keypair, programId, program.provider);
|
|
896
|
+
const milestones = await fetchPrivateMilestones(
|
|
897
|
+
erProgram,
|
|
898
|
+
escrowPda,
|
|
899
|
+
programId,
|
|
900
|
+
[index]
|
|
901
|
+
);
|
|
902
|
+
return milestones[0];
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
const getRole = (contract, wallet) => {
|
|
906
|
+
if (!contract) {
|
|
907
|
+
return "unknown";
|
|
908
|
+
}
|
|
909
|
+
if (contract.client_wallet === wallet) {
|
|
910
|
+
return "client";
|
|
911
|
+
}
|
|
912
|
+
if (contract.contractor_wallet === wallet) {
|
|
913
|
+
return "contractor";
|
|
914
|
+
}
|
|
915
|
+
if (contract.issuer_wallet === wallet) {
|
|
916
|
+
return "issuer";
|
|
917
|
+
}
|
|
918
|
+
return "other";
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
const truncateField = (value, width) => {
|
|
922
|
+
const text = String(value);
|
|
923
|
+
if (text.length <= width) {
|
|
924
|
+
return text;
|
|
925
|
+
}
|
|
926
|
+
if (width <= 1) {
|
|
927
|
+
return text.slice(0, width);
|
|
928
|
+
}
|
|
929
|
+
return `${text.slice(0, width - 1)}…`;
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const printContracts = (items, options = {}) => {
|
|
933
|
+
if (!items.length) {
|
|
934
|
+
console.log("No contracts found.");
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (options.fullFields) {
|
|
938
|
+
console.log(
|
|
939
|
+
"# Contract ID Escrow PDA Role Status Created"
|
|
940
|
+
);
|
|
941
|
+
} else {
|
|
942
|
+
const header = [
|
|
943
|
+
"#".padEnd(3, " "),
|
|
944
|
+
"Contract ID".padEnd(12, " "),
|
|
945
|
+
"Escrow PDA".padEnd(10, " "),
|
|
946
|
+
"Role".padEnd(10, " "),
|
|
947
|
+
"Status".padEnd(30, " "),
|
|
948
|
+
"Created",
|
|
949
|
+
];
|
|
950
|
+
console.log(header.join(" "));
|
|
951
|
+
}
|
|
952
|
+
items.forEach((item, index) => {
|
|
953
|
+
const full = Boolean(options.fullFields);
|
|
954
|
+
const idField = full ? item.id : truncateField(item.id, 12);
|
|
955
|
+
const escrowField = full
|
|
956
|
+
? item.escrowPda || "n/a"
|
|
957
|
+
: truncateField(item.escrowPda || "n/a", 10);
|
|
958
|
+
const roleField = item.role;
|
|
959
|
+
const statusField = item.status;
|
|
960
|
+
const row = [
|
|
961
|
+
String(index + 1).padEnd(3, " "),
|
|
962
|
+
String(idField).padEnd(12, " "),
|
|
963
|
+
String(escrowField).padEnd(10, " "),
|
|
964
|
+
String(roleField).padEnd(10, " "),
|
|
965
|
+
String(statusField).padEnd(30, " "),
|
|
966
|
+
item.createdAt,
|
|
967
|
+
];
|
|
968
|
+
console.log(row.join(" "));
|
|
969
|
+
});
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
const ensureHosted = async (config) => {
|
|
973
|
+
if (config.mode !== "hosted") {
|
|
974
|
+
console.error("Direct mode contract commands are not implemented yet.");
|
|
975
|
+
process.exit(1);
|
|
976
|
+
}
|
|
977
|
+
try {
|
|
978
|
+
await ensureHostedSession(config, { quiet: true, requireHandle: true });
|
|
979
|
+
} catch (error) {
|
|
980
|
+
console.error(error.message);
|
|
981
|
+
process.exit(1);
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
const getWalletContext = (config) => {
|
|
986
|
+
if (!config.activeWallet) {
|
|
987
|
+
throw new Error("No active wallet. Run: nebulon init");
|
|
988
|
+
}
|
|
989
|
+
const keypair = loadWalletKeypair(config, config.activeWallet);
|
|
990
|
+
return {
|
|
991
|
+
keypair,
|
|
992
|
+
wallet: keypair.publicKey.toBase58(),
|
|
993
|
+
walletKey: keypair.publicKey,
|
|
994
|
+
};
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
const getContractsForListing = (contracts) =>
|
|
998
|
+
contracts.filter((contract) => !EXCLUDE_INVITE_STATUSES.has(contract.status));
|
|
999
|
+
|
|
1000
|
+
const resolveContractFromList = (contracts, target) => {
|
|
1001
|
+
const list = getContractsForListing(contracts);
|
|
1002
|
+
const idx = Number.parseInt(target, 10);
|
|
1003
|
+
if (!Number.isNaN(idx)) {
|
|
1004
|
+
return list[idx - 1] || null;
|
|
1005
|
+
}
|
|
1006
|
+
if (target.length === 44) {
|
|
1007
|
+
return list.find((contract) => contract.escrow_pda === target) || null;
|
|
1008
|
+
}
|
|
1009
|
+
return list.find((contract) => contract.id === target) || null;
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
const confirmAction = async (skip, message) => {
|
|
1013
|
+
if (skip) {
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
const normalize = (value) => {
|
|
1017
|
+
const lower = String(value || "").trim().toLowerCase();
|
|
1018
|
+
if (["yes", "y"].includes(lower)) {
|
|
1019
|
+
return true;
|
|
1020
|
+
}
|
|
1021
|
+
if (["no", "n"].includes(lower)) {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
return null;
|
|
1025
|
+
};
|
|
1026
|
+
const answer = await prompt({
|
|
1027
|
+
type: "input",
|
|
1028
|
+
name: "decision",
|
|
1029
|
+
message,
|
|
1030
|
+
initial: "yes",
|
|
1031
|
+
validate: (value) =>
|
|
1032
|
+
normalize(value) === null ? "Type yes or no." : true,
|
|
1033
|
+
});
|
|
1034
|
+
return normalize(answer.decision);
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
const resolveContract = async (config, target) => {
|
|
1038
|
+
const result = await getContracts(config.backendUrl, config.auth.token);
|
|
1039
|
+
const contracts = Array.isArray(result.contracts) ? result.contracts : [];
|
|
1040
|
+
const match = resolveContractFromList(contracts, target);
|
|
1041
|
+
if (!match) {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
const detail = await getContract(config.backendUrl, config.auth.token, match.id);
|
|
1045
|
+
const contract = detail.contract || match;
|
|
1046
|
+
const hydrated = { ...contract };
|
|
1047
|
+
if (!contract.terms_encrypted && !Array.isArray(contract.milestones)) {
|
|
1048
|
+
return hydrated;
|
|
1049
|
+
}
|
|
1050
|
+
const { wallet } = getWalletContext(config);
|
|
1051
|
+
try {
|
|
1052
|
+
const key = await getContractPrivacyKey(config, contract.id, wallet);
|
|
1053
|
+
hydrated.privacyReady = true;
|
|
1054
|
+
if (contract.terms_encrypted) {
|
|
1055
|
+
const terms = decryptTermsPayload(
|
|
1056
|
+
key,
|
|
1057
|
+
contract.id,
|
|
1058
|
+
contract.terms_encrypted
|
|
1059
|
+
);
|
|
1060
|
+
hydrated.deadline = terms.deadline;
|
|
1061
|
+
hydrated.total_payment = terms.totalPayment;
|
|
1062
|
+
}
|
|
1063
|
+
if (Array.isArray(contract.milestones)) {
|
|
1064
|
+
hydrated.milestones = contract.milestones.map((milestone) => {
|
|
1065
|
+
const next = { ...milestone };
|
|
1066
|
+
if (next.details && isEncryptedPayload(next.details)) {
|
|
1067
|
+
try {
|
|
1068
|
+
next.title = decryptMilestonePayload(
|
|
1069
|
+
key,
|
|
1070
|
+
contract.id,
|
|
1071
|
+
next.index,
|
|
1072
|
+
next.details
|
|
1073
|
+
);
|
|
1074
|
+
} catch {
|
|
1075
|
+
next.title = null;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return next;
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
hydrated.privacyReady = false;
|
|
1083
|
+
}
|
|
1084
|
+
return hydrated;
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
const renderContractSummary = (contract, selfWallet) => {
|
|
1088
|
+
const customer = formatParticipant(
|
|
1089
|
+
"Customer",
|
|
1090
|
+
contract.client_handle,
|
|
1091
|
+
contract.client_wallet,
|
|
1092
|
+
selfWallet
|
|
1093
|
+
);
|
|
1094
|
+
const provider = formatParticipant(
|
|
1095
|
+
"Service Provider",
|
|
1096
|
+
contract.contractor_handle,
|
|
1097
|
+
contract.contractor_wallet,
|
|
1098
|
+
selfWallet
|
|
1099
|
+
);
|
|
1100
|
+
console.log(`Contract ID: ${contract.id}`);
|
|
1101
|
+
console.log(customer);
|
|
1102
|
+
console.log(provider);
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
const renderBalances = async (config, walletKey, mintAddress) => {
|
|
1106
|
+
console.log("-Balances-");
|
|
1107
|
+
try {
|
|
1108
|
+
const connection = getConnection(config.rpcUrl);
|
|
1109
|
+
const mintKey = toPublicKey(mintAddress);
|
|
1110
|
+
const [sol, usdc] = await Promise.all([
|
|
1111
|
+
getSolBalance(connection, walletKey),
|
|
1112
|
+
getTokenBalance(connection, walletKey, mintKey),
|
|
1113
|
+
]);
|
|
1114
|
+
console.log(`SOL : ${formatAmount(sol, 6)} SOL`);
|
|
1115
|
+
console.log(`USDC : ${formatAmount(usdc, 6)} USDC`);
|
|
1116
|
+
console.log("");
|
|
1117
|
+
return { sol, usdc };
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
console.log("SOL : unavailable");
|
|
1120
|
+
console.log("USDC : unavailable");
|
|
1121
|
+
console.log("");
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
const ensureSolBalance = (balances) => {
|
|
1127
|
+
if (!balances) {
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1130
|
+
if (balances.sol <= 0) {
|
|
1131
|
+
console.error("Insufficient SOL balance to perform this action.");
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
1134
|
+
return true;
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const renderBalancesOrAbort = async (config, walletKey, mintAddress) => {
|
|
1138
|
+
const balances = await renderBalances(config, walletKey, mintAddress);
|
|
1139
|
+
return ensureSolBalance(balances);
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
const renderMilestones = (milestones) => {
|
|
1143
|
+
if (!milestones || !milestones.length) {
|
|
1144
|
+
console.log("(No milestones)");
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
milestones.forEach((milestone, idx) => {
|
|
1148
|
+
const label = getMilestoneLabel(milestone);
|
|
1149
|
+
console.log(`${idx + 1}) ${label}`);
|
|
1150
|
+
});
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
const ensureSessionSigner = (config, wallet) => {
|
|
1154
|
+
config.sessions = config.sessions || {};
|
|
1155
|
+
if (config.sessions[wallet] && config.sessions[wallet].secretKey) {
|
|
1156
|
+
return Keypair.fromSecretKey(
|
|
1157
|
+
Uint8Array.from(config.sessions[wallet].secretKey)
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
const signer = Keypair.generate();
|
|
1161
|
+
config.sessions[wallet] = {
|
|
1162
|
+
secretKey: Array.from(signer.secretKey),
|
|
1163
|
+
};
|
|
1164
|
+
saveConfig(config);
|
|
1165
|
+
return signer;
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
const deriveSessionTokenPda = (
|
|
1169
|
+
programId,
|
|
1170
|
+
sessionProgramId,
|
|
1171
|
+
signer,
|
|
1172
|
+
authority
|
|
1173
|
+
) =>
|
|
1174
|
+
PublicKey.findProgramAddressSync(
|
|
1175
|
+
[
|
|
1176
|
+
Buffer.from("session_token"),
|
|
1177
|
+
programId.toBytes(),
|
|
1178
|
+
signer.toBytes(),
|
|
1179
|
+
authority.toBytes(),
|
|
1180
|
+
],
|
|
1181
|
+
sessionProgramId
|
|
1182
|
+
)[0];
|
|
1183
|
+
|
|
1184
|
+
const ensureSessionToken = async (config, provider, programId, authorityKey) => {
|
|
1185
|
+
const sessionManager = new SessionTokenManager(
|
|
1186
|
+
provider.wallet,
|
|
1187
|
+
provider.connection
|
|
1188
|
+
);
|
|
1189
|
+
let sessionSigner = ensureSessionSigner(config, authorityKey.toBase58());
|
|
1190
|
+
const sessionProgramId = sessionManager.program.programId;
|
|
1191
|
+
let sessionPda = deriveSessionTokenPda(
|
|
1192
|
+
programId,
|
|
1193
|
+
sessionProgramId,
|
|
1194
|
+
sessionSigner.publicKey,
|
|
1195
|
+
authorityKey
|
|
1196
|
+
);
|
|
1197
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1198
|
+
let existing = null;
|
|
1199
|
+
try {
|
|
1200
|
+
existing = await sessionManager.get(sessionPda);
|
|
1201
|
+
} catch {
|
|
1202
|
+
existing = null;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const existingUntil = existing?.validUntil ?? existing?.valid_until ?? null;
|
|
1206
|
+
if (existingUntil && Number(existingUntil) > now + 30) {
|
|
1207
|
+
return { sessionSigner, sessionPda };
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (existing) {
|
|
1211
|
+
try {
|
|
1212
|
+
const revokeTx = await sessionManager.program.methods
|
|
1213
|
+
.revokeSession()
|
|
1214
|
+
.accounts({
|
|
1215
|
+
sessionToken: sessionPda,
|
|
1216
|
+
authority: authorityKey,
|
|
1217
|
+
systemProgram: SystemProgram.programId,
|
|
1218
|
+
})
|
|
1219
|
+
.transaction();
|
|
1220
|
+
await provider.sendAndConfirm(revokeTx, []);
|
|
1221
|
+
} catch {
|
|
1222
|
+
sessionSigner = Keypair.generate();
|
|
1223
|
+
config.sessions[authorityKey.toBase58()] = {
|
|
1224
|
+
secretKey: Array.from(sessionSigner.secretKey),
|
|
1225
|
+
};
|
|
1226
|
+
saveConfig(config);
|
|
1227
|
+
sessionPda = deriveSessionTokenPda(
|
|
1228
|
+
programId,
|
|
1229
|
+
sessionProgramId,
|
|
1230
|
+
sessionSigner.publicKey,
|
|
1231
|
+
authorityKey
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const validUntil = new anchor.BN(now + 3600);
|
|
1237
|
+
const tx = await sessionManager.program.methods
|
|
1238
|
+
.createSession(true, validUntil, new anchor.BN(0))
|
|
1239
|
+
.accounts({
|
|
1240
|
+
targetProgram: programId,
|
|
1241
|
+
sessionSigner: sessionSigner.publicKey,
|
|
1242
|
+
authority: authorityKey,
|
|
1243
|
+
})
|
|
1244
|
+
.transaction();
|
|
1245
|
+
await provider.sendAndConfirm(tx, [sessionSigner]);
|
|
1246
|
+
|
|
1247
|
+
return { sessionSigner, sessionPda };
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
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 };
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
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 };
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
const isAlreadyExistsError = (error) => {
|
|
1279
|
+
const message = (error?.message || "").toLowerCase();
|
|
1280
|
+
return (
|
|
1281
|
+
message.includes("already in use") ||
|
|
1282
|
+
message.includes("exists") ||
|
|
1283
|
+
message.includes("already initialized")
|
|
1284
|
+
);
|
|
1285
|
+
};
|
|
1286
|
+
|
|
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
|
+
};
|
|
1359
|
+
|
|
1360
|
+
const ensureDelegatedAccount = async (
|
|
1361
|
+
config,
|
|
1362
|
+
program,
|
|
1363
|
+
keypair,
|
|
1364
|
+
accountType,
|
|
1365
|
+
pda
|
|
1366
|
+
) => {
|
|
1367
|
+
if (config && config.__l1_mode) {
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const info = await program.provider.connection.getAccountInfo(pda, "confirmed");
|
|
1371
|
+
if (info && info.owner.equals(DELEGATION_PROGRAM_ID)) {
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
const isPerVault = accountType && Object.prototype.hasOwnProperty.call(accountType, "perVault");
|
|
1375
|
+
if (
|
|
1376
|
+
info &&
|
|
1377
|
+
!info.owner.equals(program.programId) &&
|
|
1378
|
+
!(isPerVault && info.owner.equals(SystemProgram.programId))
|
|
1379
|
+
) {
|
|
1380
|
+
throw new Error(
|
|
1381
|
+
`Account ${pda.toBase58()} is owned by ${info.owner.toBase58()}, expected ${program.programId.toBase58()}.`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
try {
|
|
1385
|
+
const validator = await getDelegationValidator(config);
|
|
1386
|
+
const remainingAccounts = validator
|
|
1387
|
+
? [{ pubkey: validator, isWritable: false, isSigner: false }]
|
|
1388
|
+
: [];
|
|
1389
|
+
await program.methods
|
|
1390
|
+
.delegateAccount(accountType)
|
|
1391
|
+
.accounts({
|
|
1392
|
+
payer: keypair.publicKey,
|
|
1393
|
+
pda,
|
|
1394
|
+
})
|
|
1395
|
+
.remainingAccounts(remainingAccounts)
|
|
1396
|
+
.signers([keypair])
|
|
1397
|
+
.rpc();
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
if (!isAlreadyExistsError(error)) {
|
|
1400
|
+
throw error;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
|
|
1405
|
+
const ensureDelegatedEscrow = async (
|
|
1406
|
+
config,
|
|
1407
|
+
program,
|
|
1408
|
+
keypair,
|
|
1409
|
+
escrowId,
|
|
1410
|
+
escrowPda,
|
|
1411
|
+
client
|
|
1412
|
+
) => {
|
|
1413
|
+
if (config && config.__l1_mode) {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const info = await program.provider.connection.getAccountInfo(
|
|
1417
|
+
escrowPda,
|
|
1418
|
+
"confirmed"
|
|
1419
|
+
);
|
|
1420
|
+
if (info && info.owner.equals(DELEGATION_PROGRAM_ID)) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (info && !info.owner.equals(program.programId)) {
|
|
1424
|
+
throw new Error(
|
|
1425
|
+
`Escrow ${escrowPda.toBase58()} is owned by ${info.owner.toBase58()}, expected ${program.programId.toBase58()}.`
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
try {
|
|
1429
|
+
const validator = await getDelegationValidator(config);
|
|
1430
|
+
const remainingAccounts = validator
|
|
1431
|
+
? [{ pubkey: validator, isWritable: false, isSigner: false }]
|
|
1432
|
+
: [];
|
|
1433
|
+
await program.methods
|
|
1434
|
+
.delegateEscrow(new anchor.BN(escrowId.toString()))
|
|
1435
|
+
.accounts({
|
|
1436
|
+
payer: keypair.publicKey,
|
|
1437
|
+
pda: escrowPda,
|
|
1438
|
+
client,
|
|
1439
|
+
})
|
|
1440
|
+
.remainingAccounts(remainingAccounts)
|
|
1441
|
+
.signers([keypair])
|
|
1442
|
+
.rpc();
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
if (!isAlreadyExistsError(error)) {
|
|
1445
|
+
throw error;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
|
|
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
|
+
const ensureAta = async (connection, payer, owner, mint) => {
|
|
1461
|
+
const ata = await getAssociatedTokenAddress(mint, owner, true);
|
|
1462
|
+
try {
|
|
1463
|
+
await getAccount(connection, ata);
|
|
1464
|
+
return ata;
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
await createAssociatedTokenAccountIdempotent(
|
|
1467
|
+
connection,
|
|
1468
|
+
payer,
|
|
1469
|
+
mint,
|
|
1470
|
+
owner,
|
|
1471
|
+
{ commitment: "confirmed" },
|
|
1472
|
+
TOKEN_PROGRAM_ID,
|
|
1473
|
+
undefined,
|
|
1474
|
+
true
|
|
1475
|
+
);
|
|
1476
|
+
return ata;
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
const getMilestonesForContract = async (connection, programId, escrowPda, count) => {
|
|
1481
|
+
const milestones = [];
|
|
1482
|
+
for (let index = 0; index < count; index += 1) {
|
|
1483
|
+
const pda = deriveMilestonePda(escrowPda, index, programId);
|
|
1484
|
+
const state = await getMilestoneState(connection, programId, pda);
|
|
1485
|
+
if (state) {
|
|
1486
|
+
milestones.push({ index, pda, state });
|
|
1487
|
+
} else {
|
|
1488
|
+
milestones.push({ index, pda, state: null });
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return milestones;
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
const runContractsShow = async (scope, options = {}) => {
|
|
1495
|
+
const config = loadConfig();
|
|
1496
|
+
await ensureHosted(config);
|
|
1497
|
+
|
|
1498
|
+
const { keypair } = getWalletContext(config);
|
|
1499
|
+
const wallet = keypair.publicKey.toBase58();
|
|
1500
|
+
|
|
1501
|
+
const target = (scope || "").trim().toLowerCase();
|
|
1502
|
+
const result = await getContracts(config.backendUrl, config.auth.token);
|
|
1503
|
+
const contracts = Array.isArray(result.contracts) ? result.contracts : [];
|
|
1504
|
+
|
|
1505
|
+
if (target === "disputed") {
|
|
1506
|
+
if (config.auth.role !== "judge") {
|
|
1507
|
+
console.error("Only the judge can list disputed contracts.");
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
const items = contracts
|
|
1511
|
+
.filter((contract) => contract.status === "dispute_open")
|
|
1512
|
+
.map((contract) => ({
|
|
1513
|
+
id: contract.id,
|
|
1514
|
+
escrowPda: contract.escrow_pda || "n/a",
|
|
1515
|
+
role: getRole(contract, wallet),
|
|
1516
|
+
status: contract.status,
|
|
1517
|
+
createdAt: formatCreatedAt(contract.created_at),
|
|
1518
|
+
}));
|
|
1519
|
+
printContracts(items, options);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
if (target === "active") {
|
|
1524
|
+
const items = contracts
|
|
1525
|
+
.filter((contract) => ACTIVE_STATUSES.has(contract.status))
|
|
1526
|
+
.map((contract) => ({
|
|
1527
|
+
id: contract.id,
|
|
1528
|
+
escrowPda: contract.escrow_pda || "n/a",
|
|
1529
|
+
role: getRole(contract, wallet),
|
|
1530
|
+
status: contract.status,
|
|
1531
|
+
createdAt: formatCreatedAt(contract.created_at),
|
|
1532
|
+
}));
|
|
1533
|
+
printContracts(items, options);
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
if (target === "all" || !target) {
|
|
1538
|
+
const items = getContractsForListing(contracts).map((contract) => ({
|
|
1539
|
+
id: contract.id,
|
|
1540
|
+
escrowPda: contract.escrow_pda || "n/a",
|
|
1541
|
+
role: getRole(contract, wallet),
|
|
1542
|
+
status: contract.status,
|
|
1543
|
+
createdAt: formatCreatedAt(contract.created_at),
|
|
1544
|
+
}));
|
|
1545
|
+
printContracts(items, options);
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const match = resolveContractFromList(contracts, target);
|
|
1550
|
+
if (!match) {
|
|
1551
|
+
console.log("Contract not found.");
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
printContracts(
|
|
1555
|
+
[
|
|
1556
|
+
{
|
|
1557
|
+
id: match.id,
|
|
1558
|
+
escrowPda: match.escrow_pda || "n/a",
|
|
1559
|
+
role: getRole(match, wallet),
|
|
1560
|
+
status: match.status,
|
|
1561
|
+
createdAt: formatCreatedAt(match.created_at),
|
|
1562
|
+
},
|
|
1563
|
+
],
|
|
1564
|
+
options
|
|
1565
|
+
);
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
const runContractsCreate = async () => {
|
|
1569
|
+
const config = loadConfig();
|
|
1570
|
+
await ensureHosted(config);
|
|
1571
|
+
|
|
1572
|
+
let inviteeRole = null;
|
|
1573
|
+
let shareMode = null;
|
|
1574
|
+
let inviteeHandle = null;
|
|
1575
|
+
|
|
1576
|
+
try {
|
|
1577
|
+
const roleAnswer = await prompt({
|
|
1578
|
+
type: "select",
|
|
1579
|
+
name: "selfRole",
|
|
1580
|
+
message: "Whats your role on this contract?",
|
|
1581
|
+
choices: ROLE_CHOICES,
|
|
1582
|
+
});
|
|
1583
|
+
inviteeRole = roleAnswer.selfRole === "client" ? "contractor" : "client";
|
|
1584
|
+
|
|
1585
|
+
const shareAnswer = await prompt({
|
|
1586
|
+
type: "select",
|
|
1587
|
+
name: "share",
|
|
1588
|
+
message: "How would you like to share this contract?",
|
|
1589
|
+
choices: SHARE_CHOICES,
|
|
1590
|
+
});
|
|
1591
|
+
shareMode = shareAnswer.share;
|
|
1592
|
+
|
|
1593
|
+
if (shareMode === "direct") {
|
|
1594
|
+
const handleAnswer = await prompt({
|
|
1595
|
+
type: "input",
|
|
1596
|
+
name: "handle",
|
|
1597
|
+
message: "Nebulon ID",
|
|
1598
|
+
validate: (value) => {
|
|
1599
|
+
if (!value || !value.trim()) {
|
|
1600
|
+
return "Nebulon ID required.";
|
|
1601
|
+
}
|
|
1602
|
+
return true;
|
|
1603
|
+
},
|
|
1604
|
+
});
|
|
1605
|
+
inviteeHandle = normalizeHandle(handleAnswer.handle);
|
|
1606
|
+
}
|
|
1607
|
+
} catch (error) {
|
|
1608
|
+
if (isPromptCancel(error)) {
|
|
1609
|
+
console.log("Contract creation canceled.");
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
throw error;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
const payload = { inviteeRole };
|
|
1616
|
+
if (inviteeHandle) {
|
|
1617
|
+
payload.inviteeHandle = inviteeHandle;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const result = await createInvite(config.backendUrl, config.auth.token, payload);
|
|
1621
|
+
|
|
1622
|
+
const keypair = ensureContractKeypair(config, result.contractId);
|
|
1623
|
+
console.log("Setting up privacy keys...");
|
|
1624
|
+
try {
|
|
1625
|
+
await setContractKey(
|
|
1626
|
+
config.backendUrl,
|
|
1627
|
+
config.auth.token,
|
|
1628
|
+
result.contractId,
|
|
1629
|
+
keypair.publicKey
|
|
1630
|
+
);
|
|
1631
|
+
const keys = await getContractKeys(
|
|
1632
|
+
config.backendUrl,
|
|
1633
|
+
config.auth.token,
|
|
1634
|
+
result.contractId
|
|
1635
|
+
);
|
|
1636
|
+
const count = Array.isArray(keys.keys) ? keys.keys.length : 0;
|
|
1637
|
+
if (count > 1) {
|
|
1638
|
+
console.log("Privacy key exchange complete.");
|
|
1639
|
+
} else {
|
|
1640
|
+
console.log("Privacy key registered. Awaiting counterparty.");
|
|
1641
|
+
}
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
console.log("Privacy key setup pending. Try again after invite acceptance.");
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
console.log("Contract invite created.");
|
|
1647
|
+
console.log(`Contract ID: ${result.contractId}`);
|
|
1648
|
+
|
|
1649
|
+
if (shareMode === "direct") {
|
|
1650
|
+
console.log(`Invite sent to @${inviteeHandle}.`);
|
|
1651
|
+
}
|
|
1652
|
+
if (shareMode === "link") {
|
|
1653
|
+
console.log(`Invite link: ${result.inviteUrl}`);
|
|
1654
|
+
console.log(`Contract code: ${result.token}`);
|
|
1655
|
+
}
|
|
1656
|
+
if (shareMode === "code") {
|
|
1657
|
+
console.log(`Contract code: ${result.token}`);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
console.log("Invite status: pending");
|
|
1661
|
+
successMessage("Invite created.");
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
const runInitContract = async (config, contract, options) => {
|
|
1665
|
+
const { keypair, wallet, walletKey } = getWalletContext(config);
|
|
1666
|
+
if (!contract.client_wallet || !contract.contractor_wallet) {
|
|
1667
|
+
console.error("Invite not accepted yet.");
|
|
1668
|
+
process.exit(1);
|
|
1669
|
+
}
|
|
1670
|
+
if (contract.escrow_pda) {
|
|
1671
|
+
console.log("Escrow already initialized.");
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
if (wallet !== contract.client_wallet && wallet !== contract.contractor_wallet) {
|
|
1675
|
+
console.error("Only contract participants can initialize the escrow.");
|
|
1676
|
+
process.exit(1);
|
|
1677
|
+
}
|
|
1678
|
+
await ensureExecutionMode(config, contract, keypair, options);
|
|
1679
|
+
|
|
1680
|
+
console.log("Verify data and confirm actions please.");
|
|
1681
|
+
renderContractSummary(contract, wallet);
|
|
1682
|
+
console.log("-Action-");
|
|
1683
|
+
console.log("Initialize contract escrow");
|
|
1684
|
+
console.log("");
|
|
1685
|
+
|
|
1686
|
+
const balances = await renderBalances(
|
|
1687
|
+
config,
|
|
1688
|
+
walletKey,
|
|
1689
|
+
contract.mint || config.usdcMint
|
|
1690
|
+
);
|
|
1691
|
+
if (!ensureSolBalance(balances)) {
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
|
|
1696
|
+
if (!proceed) {
|
|
1697
|
+
console.log("Canceled.");
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
try {
|
|
1702
|
+
let step = "init";
|
|
1703
|
+
const { program, connection, programId } = getProgram(config, keypair);
|
|
1704
|
+
step = "program_info";
|
|
1705
|
+
const programInfo = await connection.getAccountInfo(programId);
|
|
1706
|
+
if (!programInfo || !programInfo.executable) {
|
|
1707
|
+
console.error("Nebulon program is not deployed on this network.");
|
|
1708
|
+
console.error(`Program ID: ${programId.toBase58()}`);
|
|
1709
|
+
console.error("Deploy it with `anchor deploy` and retry.");
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
step = "ata_program_info";
|
|
1713
|
+
const ataProgramInfo = await connection.getAccountInfo(
|
|
1714
|
+
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
1715
|
+
);
|
|
1716
|
+
if (!ataProgramInfo || !ataProgramInfo.executable) {
|
|
1717
|
+
console.error("Associated Token Program is not available on this network.");
|
|
1718
|
+
console.error(
|
|
1719
|
+
`Program ID: ${ASSOCIATED_TOKEN_PROGRAM_ID.toBase58()}`
|
|
1720
|
+
);
|
|
1721
|
+
console.error(
|
|
1722
|
+
"Start localnet with the program or deploy it before initializing escrows."
|
|
1723
|
+
);
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
step = "parse_mint";
|
|
1727
|
+
const mint = parsePublicKey(
|
|
1728
|
+
contract.mint || config.usdcMint,
|
|
1729
|
+
"USDC mint"
|
|
1730
|
+
);
|
|
1731
|
+
step = "mint_info";
|
|
1732
|
+
const mintInfo = await connection.getAccountInfo(mint);
|
|
1733
|
+
if (!mintInfo) {
|
|
1734
|
+
console.error(
|
|
1735
|
+
"Test-USDC mint is not initialized on this network. Run `nebulon request-usdc` or create the mint first."
|
|
1736
|
+
);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
step = "derive_keys";
|
|
1740
|
+
const escrowId = new anchor.BN(Date.now());
|
|
1741
|
+
const clientKey = parsePublicKey(contract.client_wallet, "client wallet");
|
|
1742
|
+
const contractorKey = parsePublicKey(contract.contractor_wallet, "contractor wallet");
|
|
1743
|
+
const escrowPda = deriveEscrowPda(clientKey, escrowId, programId);
|
|
1744
|
+
const perVaultPda = derivePerVaultPda(escrowPda, programId);
|
|
1745
|
+
step = "vault_token";
|
|
1746
|
+
const vaultToken = await getAssociatedTokenAddress(mint, escrowPda, true);
|
|
1747
|
+
|
|
1748
|
+
console.log("Processing.");
|
|
1749
|
+
step = "create_escrow";
|
|
1750
|
+
const sig = await program.methods
|
|
1751
|
+
.createEscrow(escrowId, FEE_RECEIVER)
|
|
1752
|
+
.accounts({
|
|
1753
|
+
payer: walletKey,
|
|
1754
|
+
client: clientKey,
|
|
1755
|
+
contractor: contractorKey,
|
|
1756
|
+
mint,
|
|
1757
|
+
escrow: escrowPda,
|
|
1758
|
+
perVault: perVaultPda,
|
|
1759
|
+
vaultToken,
|
|
1760
|
+
tokenProgram: TOKEN_PROGRAM_ID,
|
|
1761
|
+
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
1762
|
+
systemProgram: SystemProgram.programId,
|
|
1763
|
+
})
|
|
1764
|
+
.signers([keypair])
|
|
1765
|
+
.rpc();
|
|
1766
|
+
console.log(`Escrow initialized. (tx: ${sig})`);
|
|
1767
|
+
|
|
1768
|
+
await linkEscrow(config.backendUrl, config.auth.token, contract.id, {
|
|
1769
|
+
escrowPda: escrowPda.toBase58(),
|
|
1770
|
+
mint: mint.toBase58(),
|
|
1771
|
+
vaultToken: vaultToken.toBase58(),
|
|
1772
|
+
});
|
|
1773
|
+
console.log("Escrow linked to contract.");
|
|
1774
|
+
successMessage("Escrow initialized.");
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
const message = error?.message || "";
|
|
1777
|
+
if (message.includes("Expected Buffer")) {
|
|
1778
|
+
console.error("Expected Buffer!");
|
|
1779
|
+
console.error(`Debug step: ${typeof step !== "undefined" ? step : "unknown"}`);
|
|
1780
|
+
if (error?.stack) {
|
|
1781
|
+
console.error(error.stack);
|
|
1782
|
+
}
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
throw error;
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
const runAddMilestone = async (config, contract, title, options) => {
|
|
1790
|
+
const { keypair, wallet, walletKey } = getWalletContext(config);
|
|
1791
|
+
ensureEditableContract(contract);
|
|
1792
|
+
if (!contract.escrow_pda) {
|
|
1793
|
+
console.error("Contract has no escrow. Unable to add milestones yet.");
|
|
1794
|
+
process.exit(1);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
const { program, connection, programId } = getProgram(config, keypair);
|
|
1798
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
1799
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
1800
|
+
if (!escrowState) {
|
|
1801
|
+
console.error("Escrow not found on-chain.");
|
|
1802
|
+
process.exit(1);
|
|
1803
|
+
}
|
|
1804
|
+
if (escrowState.paidOut) {
|
|
1805
|
+
console.log("Funds already claimed. Contract completed successfully.");
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
const escrowId = escrowState.escrowId;
|
|
1810
|
+
const index = Array.isArray(contract.milestones)
|
|
1811
|
+
? contract.milestones.length
|
|
1812
|
+
: 0;
|
|
1813
|
+
|
|
1814
|
+
console.log("Verify data and confirm actions please.");
|
|
1815
|
+
renderContractSummary(contract, wallet);
|
|
1816
|
+
console.log("-Action-");
|
|
1817
|
+
console.log("Add Milestone");
|
|
1818
|
+
console.log(`Number : ${index + 1}`);
|
|
1819
|
+
console.log(`Details : \"${title}\"`);
|
|
1820
|
+
console.log("");
|
|
1821
|
+
const canProceed = await renderBalancesOrAbort(
|
|
1822
|
+
config,
|
|
1823
|
+
walletKey,
|
|
1824
|
+
contract.mint || config.usdcMint
|
|
1825
|
+
);
|
|
1826
|
+
if (!canProceed) {
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
|
|
1830
|
+
if (!proceed) {
|
|
1831
|
+
console.log("Canceled.");
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const executionMode = await ensureExecutionMode(config, contract, keypair, options);
|
|
1836
|
+
const l1Mode = executionMode === "l1";
|
|
1837
|
+
|
|
1838
|
+
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
|
+
}
|
|
1866
|
+
let privacyKey;
|
|
1867
|
+
try {
|
|
1868
|
+
privacyKey = await getContractPrivacyKey(config, contract.id, wallet);
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
console.error(
|
|
1871
|
+
"Privacy key exchange pending. Wait for the counterparty to accept."
|
|
1872
|
+
);
|
|
1873
|
+
process.exit(1);
|
|
1874
|
+
}
|
|
1875
|
+
const encryptedDetails = encryptMilestonePayload(
|
|
1876
|
+
privacyKey,
|
|
1877
|
+
contract.id,
|
|
1878
|
+
index,
|
|
1879
|
+
title
|
|
1880
|
+
);
|
|
1881
|
+
|
|
1882
|
+
const encryptedBytes = Buffer.from(encryptedDetails, "utf8");
|
|
1883
|
+
|
|
1884
|
+
// Ensure encrypted bytes fit exactly into the expected buffer size
|
|
1885
|
+
// Look for the error message "requires (length X) Buffer as src"
|
|
1886
|
+
// and set the buffer size to X bytes
|
|
1887
|
+
const expectedSize = 73; // From error: "requires (length 73) Buffer as src"
|
|
1888
|
+
const paddedBytes = Buffer.alloc(expectedSize, 0);
|
|
1889
|
+
encryptedBytes.copy(paddedBytes, 0, 0, Math.min(encryptedBytes.length, expectedSize));
|
|
1890
|
+
|
|
1891
|
+
const descriptionHash = crypto
|
|
1892
|
+
.createHash("sha256")
|
|
1893
|
+
.update(encryptedDetails)
|
|
1894
|
+
.digest();
|
|
1895
|
+
|
|
1896
|
+
const privateMilestonePda = derivePrivateMilestonePda(
|
|
1897
|
+
escrowPda,
|
|
1898
|
+
index,
|
|
1899
|
+
programId
|
|
1900
|
+
);
|
|
1901
|
+
await program.methods
|
|
1902
|
+
.initPrivateMilestoneStub(new anchor.BN(escrowId.toString()), index)
|
|
1903
|
+
.accounts({
|
|
1904
|
+
payer: walletKey,
|
|
1905
|
+
escrow: escrowPda,
|
|
1906
|
+
privateMilestone: privateMilestonePda,
|
|
1907
|
+
systemProgram: SystemProgram.programId,
|
|
1908
|
+
})
|
|
1909
|
+
.signers([keypair])
|
|
1910
|
+
.rpc();
|
|
1911
|
+
|
|
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
|
+
await ensureDelegatedAccount(
|
|
1927
|
+
config,
|
|
1928
|
+
program,
|
|
1929
|
+
keypair,
|
|
1930
|
+
members.accountType,
|
|
1931
|
+
privateMilestonePda
|
|
1932
|
+
);
|
|
1933
|
+
|
|
1934
|
+
const perVaultPda = derivePerVaultPda(escrowPda, programId);
|
|
1935
|
+
await ensureDelegatedAccount(
|
|
1936
|
+
config,
|
|
1937
|
+
program,
|
|
1938
|
+
keypair,
|
|
1939
|
+
{ perVault: { escrow: escrowPda } },
|
|
1940
|
+
perVaultPda
|
|
1941
|
+
);
|
|
1942
|
+
await ensureDelegatedEscrow(
|
|
1943
|
+
config,
|
|
1944
|
+
program,
|
|
1945
|
+
keypair,
|
|
1946
|
+
escrowId,
|
|
1947
|
+
escrowPda,
|
|
1948
|
+
new PublicKey(contract.client_wallet)
|
|
1949
|
+
);
|
|
1950
|
+
|
|
1951
|
+
const { program: erProgram, sessionSigner, sessionPda } =
|
|
1952
|
+
await getPerProgramBundle(config, keypair, programId, program.provider);
|
|
1953
|
+
const metaSig = await erProgram.methods
|
|
1954
|
+
.createPrivateMilestone(
|
|
1955
|
+
new anchor.BN(escrowId.toString()),
|
|
1956
|
+
index,
|
|
1957
|
+
Buffer.from(descriptionHash),
|
|
1958
|
+
paddedBytes
|
|
1959
|
+
)
|
|
1960
|
+
.accounts({
|
|
1961
|
+
user: walletKey,
|
|
1962
|
+
payer: sessionSigner.publicKey,
|
|
1963
|
+
sessionToken: sessionPda,
|
|
1964
|
+
escrow: escrowPda,
|
|
1965
|
+
privateMilestone: privateMilestonePda,
|
|
1966
|
+
perVault: perVaultPda,
|
|
1967
|
+
systemProgram: SystemProgram.programId,
|
|
1968
|
+
})
|
|
1969
|
+
.signers([sessionSigner])
|
|
1970
|
+
.rpc({ skipPreflight: true });
|
|
1971
|
+
console.log("Submitting Metadata...");
|
|
1972
|
+
console.log(`Done. (tx: ${metaSig})`);
|
|
1973
|
+
|
|
1974
|
+
const milestones = Array.isArray(contract.milestones)
|
|
1975
|
+
? [...contract.milestones]
|
|
1976
|
+
: [];
|
|
1977
|
+
milestones.push({ index, details: encryptedDetails, status: "created" });
|
|
1978
|
+
const persisted = await updateContractMilestones(config, contract, milestones);
|
|
1979
|
+
if (!persisted) {
|
|
1980
|
+
console.log(
|
|
1981
|
+
"Warning: backend did not persist milestone status (check backend version)."
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
successMessage("Milestone added.");
|
|
1985
|
+
};
|
|
1986
|
+
|
|
1987
|
+
const runDisableMilestone = async (config, contract, number, options) => {
|
|
1988
|
+
const { keypair, wallet, walletKey } = getWalletContext(config);
|
|
1989
|
+
if (!contract.escrow_pda) {
|
|
1990
|
+
console.error("Contract has no escrow. Unable to disable milestones.");
|
|
1991
|
+
process.exit(1);
|
|
1992
|
+
}
|
|
1993
|
+
const role = getRole(contract, wallet);
|
|
1994
|
+
if (role !== "client" && role !== "contractor") {
|
|
1995
|
+
console.error("Only contract participants can disable milestones.");
|
|
1996
|
+
process.exit(1);
|
|
1997
|
+
}
|
|
1998
|
+
const index = Number(number) - 1;
|
|
1999
|
+
if (!Number.isFinite(index) || index < 0) {
|
|
2000
|
+
console.error("Invalid milestone number.");
|
|
2001
|
+
process.exit(1);
|
|
2002
|
+
}
|
|
2003
|
+
const milestoneList = Array.isArray(contract.milestones)
|
|
2004
|
+
? contract.milestones
|
|
2005
|
+
: [];
|
|
2006
|
+
const milestoneMeta = milestoneList.find((milestone) => milestone.index === index);
|
|
2007
|
+
if (!milestoneMeta) {
|
|
2008
|
+
console.error("Milestone not found.");
|
|
2009
|
+
console.error("Check the list with: nebulon contract <id> check milestone list");
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
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
|
+
}
|
|
2067
|
+
if (!config.ephemeralProviderUrl) {
|
|
2068
|
+
console.error(
|
|
2069
|
+
"Warning: MagicBlock RPC is not configured; milestone status checks may be unreliable."
|
|
2070
|
+
);
|
|
2071
|
+
console.error("Run `nebulon init` to fetch the MagicBlock endpoints.");
|
|
2072
|
+
}
|
|
2073
|
+
if (milestoneMeta.status === "ready") {
|
|
2074
|
+
console.error("Milestone already submitted and awaiting customer confirmation.");
|
|
2075
|
+
console.error("Current status: submitted");
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
if (milestoneMeta.status === "approved") {
|
|
2079
|
+
console.error("Milestone already confirmed.");
|
|
2080
|
+
console.error("Current status: paid");
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
if (milestoneMeta.status === "disabled") {
|
|
2084
|
+
console.error("Milestone is disabled.");
|
|
2085
|
+
console.error("Current status: disabled");
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
let currentStatus = null;
|
|
2089
|
+
try {
|
|
2090
|
+
const milestone = await getPrivateMilestoneStatus(
|
|
2091
|
+
config,
|
|
2092
|
+
contract,
|
|
2093
|
+
keypair,
|
|
2094
|
+
walletKey,
|
|
2095
|
+
index
|
|
2096
|
+
);
|
|
2097
|
+
currentStatus = milestone ? milestone.status : null;
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
console.error("Unable to verify milestone status.");
|
|
2100
|
+
process.exit(1);
|
|
2101
|
+
}
|
|
2102
|
+
const statusValue = Number(currentStatus);
|
|
2103
|
+
if (!Number.isFinite(statusValue)) {
|
|
2104
|
+
console.error("Unable to determine milestone status.");
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
const statusLabel = formatPrivateMilestoneStatus(statusValue);
|
|
2108
|
+
if (statusValue === 1) {
|
|
2109
|
+
console.error("Milestone already submitted and awaiting customer confirmation.");
|
|
2110
|
+
console.error("Current status: submitted");
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
if (statusValue === 3) {
|
|
2114
|
+
console.error("Milestone already confirmed.");
|
|
2115
|
+
console.error("Current status: paid");
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
if (statusValue === 4) {
|
|
2119
|
+
console.error("Milestone is disabled.");
|
|
2120
|
+
console.error("Current status: disabled");
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
if (statusValue !== 0 && statusValue !== 2) {
|
|
2124
|
+
console.error(`Cannot mark milestone as ready from status: ${statusLabel}`);
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
console.log("Verify data and confirm actions please.");
|
|
2129
|
+
renderContractSummary(contract, wallet);
|
|
2130
|
+
console.log("-Action-");
|
|
2131
|
+
console.log("Disable milestone");
|
|
2132
|
+
console.log(`Number : ${index + 1}`);
|
|
2133
|
+
const milestones = Array.isArray(contract.milestones) ? contract.milestones : [];
|
|
2134
|
+
const current = milestones.find((m) => m.index === index);
|
|
2135
|
+
console.log(`Details : \"${current ? getMilestoneLabel(current) : "unknown"}\"`);
|
|
2136
|
+
console.log("");
|
|
2137
|
+
|
|
2138
|
+
const canProceed = await renderBalancesOrAbort(
|
|
2139
|
+
config,
|
|
2140
|
+
walletKey,
|
|
2141
|
+
contract.mint || config.usdcMint
|
|
2142
|
+
);
|
|
2143
|
+
if (!canProceed) {
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
|
|
2148
|
+
if (!proceed) {
|
|
2149
|
+
console.log("Canceled.");
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
console.log("Processing.");
|
|
2154
|
+
const sig = await updatePrivateMilestoneStatus(
|
|
2155
|
+
config,
|
|
2156
|
+
contract,
|
|
2157
|
+
keypair,
|
|
2158
|
+
walletKey,
|
|
2159
|
+
index,
|
|
2160
|
+
4
|
|
2161
|
+
);
|
|
2162
|
+
console.log(`Milestone disabled. (tx: ${sig})`);
|
|
2163
|
+
|
|
2164
|
+
const nextMilestones = milestones.map((milestone) =>
|
|
2165
|
+
milestone.index === index
|
|
2166
|
+
? { ...milestone, status: "disabled" }
|
|
2167
|
+
: milestone
|
|
2168
|
+
);
|
|
2169
|
+
await updateContractIfNegotiating(config, contract, {
|
|
2170
|
+
milestones: nextMilestones,
|
|
2171
|
+
});
|
|
2172
|
+
await runSyncFlags(config, contract, { ...options, confirm: true });
|
|
2173
|
+
successMessage("Milestone disabled.");
|
|
2174
|
+
};
|
|
2175
|
+
|
|
2176
|
+
const ensureTermsPrepared = async (
|
|
2177
|
+
config,
|
|
2178
|
+
contract,
|
|
2179
|
+
keypair,
|
|
2180
|
+
escrowPda,
|
|
2181
|
+
escrowId
|
|
2182
|
+
) => {
|
|
2183
|
+
const { program, programId } = getProgram(config, keypair);
|
|
2184
|
+
const termsPda = deriveTermsPda(escrowPda, programId);
|
|
2185
|
+
await program.methods
|
|
2186
|
+
.initPrivateTermsStub(new anchor.BN(escrowId.toString()))
|
|
2187
|
+
.accounts({
|
|
2188
|
+
payer: keypair.publicKey,
|
|
2189
|
+
escrow: escrowPda,
|
|
2190
|
+
terms: termsPda,
|
|
2191
|
+
systemProgram: SystemProgram.programId,
|
|
2192
|
+
})
|
|
2193
|
+
.signers([keypair])
|
|
2194
|
+
.rpc();
|
|
2195
|
+
|
|
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
|
+
await ensureDelegatedAccount(
|
|
2202
|
+
config,
|
|
2203
|
+
program,
|
|
2204
|
+
keypair,
|
|
2205
|
+
members.accountType,
|
|
2206
|
+
termsPda
|
|
2207
|
+
);
|
|
2208
|
+
|
|
2209
|
+
const perVaultPda = derivePerVaultPda(escrowPda, programId);
|
|
2210
|
+
await ensureDelegatedAccount(
|
|
2211
|
+
config,
|
|
2212
|
+
program,
|
|
2213
|
+
keypair,
|
|
2214
|
+
{ perVault: { escrow: escrowPda } },
|
|
2215
|
+
perVaultPda
|
|
2216
|
+
);
|
|
2217
|
+
await ensureDelegatedEscrow(
|
|
2218
|
+
config,
|
|
2219
|
+
program,
|
|
2220
|
+
keypair,
|
|
2221
|
+
escrowId,
|
|
2222
|
+
escrowPda,
|
|
2223
|
+
new PublicKey(contract.client_wallet)
|
|
2224
|
+
);
|
|
2225
|
+
|
|
2226
|
+
return { termsPda, perVaultPda, program, programId };
|
|
2227
|
+
};
|
|
2228
|
+
|
|
2229
|
+
const submitTerms = async (
|
|
2230
|
+
config,
|
|
2231
|
+
contract,
|
|
2232
|
+
keypair,
|
|
2233
|
+
escrowPda,
|
|
2234
|
+
escrowId,
|
|
2235
|
+
totalPayment,
|
|
2236
|
+
deadline,
|
|
2237
|
+
encryptedTerms
|
|
2238
|
+
) => {
|
|
2239
|
+
const { termsPda, perVaultPda, program, programId } = await ensureTermsPrepared(
|
|
2240
|
+
config,
|
|
2241
|
+
contract,
|
|
2242
|
+
keypair,
|
|
2243
|
+
escrowPda,
|
|
2244
|
+
escrowId
|
|
2245
|
+
);
|
|
2246
|
+
|
|
2247
|
+
const { program: erProgram, sessionSigner, sessionPda } =
|
|
2248
|
+
await getPerProgramBundle(config, keypair, programId, program.provider);
|
|
2249
|
+
const encryptedBytes = Buffer.from(encryptedTerms || "", "utf8");
|
|
2250
|
+
const attemptCreate = async (forceRefresh = false) => {
|
|
2251
|
+
const { program } = forceRefresh
|
|
2252
|
+
? await getEphemeralProgram(config, keypair, { forceRefresh: true })
|
|
2253
|
+
: { program: erProgram };
|
|
2254
|
+
return program.methods
|
|
2255
|
+
.createPrivateTerms(
|
|
2256
|
+
new anchor.BN(escrowId.toString()),
|
|
2257
|
+
buildTermsHash(deadline, totalPayment),
|
|
2258
|
+
new anchor.BN(totalPayment.toString()),
|
|
2259
|
+
new anchor.BN(deadline.toString()),
|
|
2260
|
+
encryptedBytes
|
|
2261
|
+
)
|
|
2262
|
+
.accounts({
|
|
2263
|
+
user: keypair.publicKey,
|
|
2264
|
+
payer: sessionSigner.publicKey,
|
|
2265
|
+
sessionToken: sessionPda,
|
|
2266
|
+
escrow: escrowPda,
|
|
2267
|
+
terms: termsPda,
|
|
2268
|
+
perVault: perVaultPda,
|
|
2269
|
+
systemProgram: SystemProgram.programId,
|
|
2270
|
+
})
|
|
2271
|
+
.signers([sessionSigner])
|
|
2272
|
+
.rpc({ skipPreflight: true });
|
|
2273
|
+
};
|
|
2274
|
+
|
|
2275
|
+
const attemptCreateDirect = async () => {
|
|
2276
|
+
const { program: userErProgram } = await getEphemeralProgram(
|
|
2277
|
+
config,
|
|
2278
|
+
keypair,
|
|
2279
|
+
{ forceRefresh: true }
|
|
2280
|
+
);
|
|
2281
|
+
return userErProgram.methods
|
|
2282
|
+
.createPrivateTerms(
|
|
2283
|
+
new anchor.BN(escrowId.toString()),
|
|
2284
|
+
buildTermsHash(deadline, totalPayment),
|
|
2285
|
+
new anchor.BN(totalPayment.toString()),
|
|
2286
|
+
new anchor.BN(deadline.toString()),
|
|
2287
|
+
encryptedBytes
|
|
2288
|
+
)
|
|
2289
|
+
.accounts({
|
|
2290
|
+
user: keypair.publicKey,
|
|
2291
|
+
payer: sessionSigner.publicKey,
|
|
2292
|
+
sessionToken: sessionPda,
|
|
2293
|
+
escrow: escrowPda,
|
|
2294
|
+
terms: termsPda,
|
|
2295
|
+
perVault: perVaultPda,
|
|
2296
|
+
systemProgram: SystemProgram.programId,
|
|
2297
|
+
})
|
|
2298
|
+
.signers([sessionSigner])
|
|
2299
|
+
.rpc({ skipPreflight: true });
|
|
2300
|
+
};
|
|
2301
|
+
|
|
2302
|
+
const retries = [0, 3000, 6000];
|
|
2303
|
+
let lastError = null;
|
|
2304
|
+
for (const delay of retries) {
|
|
2305
|
+
if (delay) {
|
|
2306
|
+
await sleep(delay);
|
|
2307
|
+
}
|
|
2308
|
+
try {
|
|
2309
|
+
return await attemptCreate(delay > 0);
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
lastError = error;
|
|
2312
|
+
const message = (error?.message || "").toLowerCase();
|
|
2313
|
+
if (
|
|
2314
|
+
message.includes("writable account") ||
|
|
2315
|
+
message.includes("cannot be written")
|
|
2316
|
+
) {
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
if (isRemoteSubscriptionError(error)) {
|
|
2320
|
+
try {
|
|
2321
|
+
return await attemptCreateDirect();
|
|
2322
|
+
} catch (directError) {
|
|
2323
|
+
lastError = directError;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
throw error;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
throw lastError;
|
|
2330
|
+
};
|
|
2331
|
+
|
|
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
|
+
const runAddTerm = async (config, contract, field, value, options) => {
|
|
2475
|
+
const { keypair, wallet, walletKey } = getWalletContext(config);
|
|
2476
|
+
ensureEditableContract(contract);
|
|
2477
|
+
if (!contract.escrow_pda) {
|
|
2478
|
+
console.error("Contract has no escrow. Unable to edit terms.");
|
|
2479
|
+
process.exit(1);
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
const { program, connection, programId } = getProgram(config, keypair);
|
|
2483
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
2484
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
2485
|
+
if (!escrowState) {
|
|
2486
|
+
console.error("Escrow not found on-chain.");
|
|
2487
|
+
console.error(
|
|
2488
|
+
"If you restarted localnet, re-run `nebulon contract <id> init`."
|
|
2489
|
+
);
|
|
2490
|
+
process.exit(1);
|
|
2491
|
+
}
|
|
2492
|
+
const escrowId = escrowState.escrowId;
|
|
2493
|
+
|
|
2494
|
+
let deadline = contract.deadline;
|
|
2495
|
+
let totalPayment = contract.total_payment;
|
|
2496
|
+
let promptLabel = "";
|
|
2497
|
+
|
|
2498
|
+
if (field === "deadline") {
|
|
2499
|
+
if (wallet !== contract.client_wallet) {
|
|
2500
|
+
console.error("Only the customer can set the deadline.");
|
|
2501
|
+
process.exit(1);
|
|
2502
|
+
}
|
|
2503
|
+
const parsed = parseDeadlineInput(value);
|
|
2504
|
+
deadline = parsed.deadline.toString();
|
|
2505
|
+
promptLabel = parsed.label;
|
|
2506
|
+
} else if (field === "payment") {
|
|
2507
|
+
const parsed = parseAmountInput(value);
|
|
2508
|
+
totalPayment = parsed.base.toString();
|
|
2509
|
+
promptLabel = parsed.display;
|
|
2510
|
+
} else {
|
|
2511
|
+
console.error("Unknown term field.");
|
|
2512
|
+
process.exit(1);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
console.log("Verify data and confirm actions please.");
|
|
2516
|
+
renderContractSummary(contract, wallet);
|
|
2517
|
+
console.log("-Action-");
|
|
2518
|
+
console.log(`Add term ${field}`);
|
|
2519
|
+
console.log(`Value : ${promptLabel}`);
|
|
2520
|
+
console.log("");
|
|
2521
|
+
|
|
2522
|
+
const canProceed = await renderBalancesOrAbort(
|
|
2523
|
+
config,
|
|
2524
|
+
walletKey,
|
|
2525
|
+
contract.mint || config.usdcMint
|
|
2526
|
+
);
|
|
2527
|
+
if (!canProceed) {
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
|
|
2532
|
+
if (!proceed) {
|
|
2533
|
+
console.log("Canceled.");
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
const executionMode = await ensureExecutionMode(config, contract, keypair, options);
|
|
2538
|
+
const l1Mode = executionMode === "l1";
|
|
2539
|
+
|
|
2540
|
+
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
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
await updateContractIfNegotiating(config, contract, {
|
|
2559
|
+
...(l1Mode ? {} : { termsEncrypted: encryptedTerms }),
|
|
2560
|
+
termsHash: buildTermsHashHex(deadline, totalPayment),
|
|
2561
|
+
...(l1Mode ? { deadline, totalPayment } : {}),
|
|
2562
|
+
});
|
|
2563
|
+
|
|
2564
|
+
if (!deadline || !totalPayment) {
|
|
2565
|
+
console.log("Saved term. Set the remaining term to submit on-chain.");
|
|
2566
|
+
successMessage("Term saved.");
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
if (wallet !== contract.client_wallet) {
|
|
2570
|
+
console.log(
|
|
2571
|
+
"Saved term. Ask the customer to submit the terms on-chain to continue."
|
|
2572
|
+
);
|
|
2573
|
+
successMessage("Term saved.");
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
console.log("Processing.");
|
|
2578
|
+
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
|
+
);
|
|
2599
|
+
if (!sig) {
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
console.log(`Terms submitted. (tx: ${sig})`);
|
|
2603
|
+
successMessage("Terms submitted.");
|
|
2604
|
+
} catch (error) {
|
|
2605
|
+
await printTxLogs(error);
|
|
2606
|
+
throw error;
|
|
2607
|
+
}
|
|
2608
|
+
};
|
|
2609
|
+
|
|
2610
|
+
const runSignContract = async (config, contract, options) => {
|
|
2611
|
+
const { keypair, wallet, walletKey } = getWalletContext(config);
|
|
2612
|
+
if (!contract.escrow_pda) {
|
|
2613
|
+
console.error("Contract has no escrow. Unable to sign.");
|
|
2614
|
+
process.exit(1);
|
|
2615
|
+
}
|
|
2616
|
+
const executionMode = await ensureExecutionMode(config, contract, keypair, options);
|
|
2617
|
+
const l1Mode = executionMode === "l1";
|
|
2618
|
+
ensureContractPhase(
|
|
2619
|
+
contract,
|
|
2620
|
+
["negotiating", "awaiting_signatures"],
|
|
2621
|
+
"Finish terms/milestones, then sign."
|
|
2622
|
+
);
|
|
2623
|
+
if (!l1Mode && contract.terms_encrypted && !contract.privacyReady) {
|
|
2624
|
+
console.error("Privacy key exchange pending. Unable to read terms.");
|
|
2625
|
+
process.exit(1);
|
|
2626
|
+
}
|
|
2627
|
+
const missing = [];
|
|
2628
|
+
const milestones = Array.isArray(contract.milestones)
|
|
2629
|
+
? contract.milestones
|
|
2630
|
+
: [];
|
|
2631
|
+
if (!milestones.length) {
|
|
2632
|
+
missing.push("It needs at least 1 Milestone");
|
|
2633
|
+
}
|
|
2634
|
+
if (!contract.deadline) {
|
|
2635
|
+
missing.push("Missing Term : Deadline");
|
|
2636
|
+
}
|
|
2637
|
+
if (!contract.total_payment) {
|
|
2638
|
+
missing.push("Missing Term : Payment");
|
|
2639
|
+
}
|
|
2640
|
+
if (missing.length) {
|
|
2641
|
+
console.error("Cant sign contract until the following is resolved.");
|
|
2642
|
+
missing.forEach((item) => console.error(`-${item}`));
|
|
2643
|
+
process.exit(1);
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
const { program, connection, programId } = getProgram(config, keypair);
|
|
2647
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
2648
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
2649
|
+
if (!escrowState) {
|
|
2650
|
+
console.error("Escrow not found on-chain.");
|
|
2651
|
+
process.exit(1);
|
|
2652
|
+
}
|
|
2653
|
+
const escrowId = escrowState.escrowId;
|
|
2654
|
+
|
|
2655
|
+
console.log("Verify data and confirm actions please.");
|
|
2656
|
+
renderContractSummary(contract, wallet);
|
|
2657
|
+
console.log("-Action-");
|
|
2658
|
+
console.log("Sign Contract");
|
|
2659
|
+
console.log("");
|
|
2660
|
+
console.log("-Milestone List-");
|
|
2661
|
+
renderMilestones(contract.milestones || []);
|
|
2662
|
+
console.log("");
|
|
2663
|
+
console.log("-Terms-");
|
|
2664
|
+
console.log(`Deadline : ${formatDeadlineValue(contract.deadline)}`);
|
|
2665
|
+
console.log(`Payment : ${formatUsdc(contract.total_payment)} USDC`);
|
|
2666
|
+
console.log("");
|
|
2667
|
+
console.log(
|
|
2668
|
+
"By signing this contract you agree to the milestones and terms of it, and commit to be transparent and active with the progress of it."
|
|
2669
|
+
);
|
|
2670
|
+
console.log("");
|
|
2671
|
+
|
|
2672
|
+
const canProceed = await renderBalancesOrAbort(
|
|
2673
|
+
config,
|
|
2674
|
+
walletKey,
|
|
2675
|
+
contract.mint || config.usdcMint
|
|
2676
|
+
);
|
|
2677
|
+
if (!canProceed) {
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
const proceed = await confirmAction(options.confirm, "Sign contract? yes/no");
|
|
2682
|
+
if (!proceed) {
|
|
2683
|
+
console.log("Canceled.");
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
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})`);
|
|
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(
|
|
2710
|
+
config,
|
|
2711
|
+
contract,
|
|
2712
|
+
keypair,
|
|
2713
|
+
escrowPda,
|
|
2714
|
+
escrowId
|
|
2715
|
+
);
|
|
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()))
|
|
2725
|
+
.accounts({
|
|
2726
|
+
user: keypair.publicKey,
|
|
2727
|
+
payer: sessionSigner.publicKey,
|
|
2728
|
+
sessionToken: sessionPda,
|
|
2729
|
+
escrow: escrowPda,
|
|
2730
|
+
terms: termsPda,
|
|
2731
|
+
})
|
|
2732
|
+
.signers([sessionSigner])
|
|
2733
|
+
.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
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
if (commitSig) {
|
|
2752
|
+
console.log(`Terms committed. (tx: ${commitSig})`);
|
|
2753
|
+
console.log("Syncing contract flags...");
|
|
2754
|
+
let synced = false;
|
|
2755
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
2756
|
+
try {
|
|
2757
|
+
await runSyncFlags(config, contract, { confirm: true });
|
|
2758
|
+
synced = true;
|
|
2759
|
+
break;
|
|
2760
|
+
} catch (error) {
|
|
2761
|
+
console.log("DEBUG: Sync error:", error);
|
|
2762
|
+
const errorString = JSON.stringify(error);
|
|
2763
|
+
console.log("DEBUG: Error string:", errorString);
|
|
2764
|
+
const message = (error && error.message ? error.message : errorString).toLowerCase();
|
|
2765
|
+
if (message.includes("cannot be written") || message.includes("writable account") || message.includes("invalidwritableaccount")) {
|
|
2766
|
+
if (attempt === 0) {
|
|
2767
|
+
await sleep(3000);
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2770
|
+
console.log("Sync failed. Run `nebulon contract <id> sync` and retry after a few seconds.");
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
throw error;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
} else {
|
|
2777
|
+
console.log("Waiting for the other party to sign.");
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
if (contract.status === "negotiating") {
|
|
2781
|
+
await lockContract(config.backendUrl, config.auth.token, contract.id);
|
|
2782
|
+
}
|
|
2783
|
+
try {
|
|
2784
|
+
await signContract(config.backendUrl, config.auth.token, contract.id);
|
|
2785
|
+
} catch (error) {
|
|
2786
|
+
if (error && error.message === "already_signed") {
|
|
2787
|
+
console.log("You have already signed this contract.");
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
if (error && error.message === "invalid_status") {
|
|
2791
|
+
try {
|
|
2792
|
+
const refreshed = await refreshContract(
|
|
2793
|
+
config.backendUrl,
|
|
2794
|
+
config.auth.token,
|
|
2795
|
+
contract.id
|
|
2796
|
+
);
|
|
2797
|
+
if (refreshed && refreshed.contract && refreshed.contract.status) {
|
|
2798
|
+
console.log(
|
|
2799
|
+
`Contract phase updated to ${refreshed.contract.status}.`
|
|
2800
|
+
);
|
|
2801
|
+
} else {
|
|
2802
|
+
console.log("Contract phase does not allow signing right now.");
|
|
2803
|
+
}
|
|
2804
|
+
} catch (refreshError) {
|
|
2805
|
+
console.log("Contract phase does not allow signing right now.");
|
|
2806
|
+
}
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
throw error;
|
|
2810
|
+
}
|
|
2811
|
+
successMessage("Contract signed.");
|
|
2812
|
+
};
|
|
2813
|
+
|
|
2814
|
+
const runFundContract = async (config, contract, options) => {
|
|
2815
|
+
const { keypair, wallet, walletKey } = getWalletContext(config);
|
|
2816
|
+
if (!contract.escrow_pda) {
|
|
2817
|
+
console.error("Contract has no escrow. Unable to fund.");
|
|
2818
|
+
process.exit(1);
|
|
2819
|
+
}
|
|
2820
|
+
ensureContractPhase(contract, ["waiting_for_funding"], "Run sign first.");
|
|
2821
|
+
if (!isL1Mode(contract) && contract.terms_encrypted && !contract.privacyReady) {
|
|
2822
|
+
console.error("Privacy key exchange pending. Unable to read terms.");
|
|
2823
|
+
process.exit(1);
|
|
2824
|
+
}
|
|
2825
|
+
const role = getRole(contract, wallet);
|
|
2826
|
+
if (role !== "client") {
|
|
2827
|
+
console.error("Only the customer can fund this contract.");
|
|
2828
|
+
process.exit(1);
|
|
2829
|
+
}
|
|
2830
|
+
await ensureExecutionMode(config, contract, keypair, options);
|
|
2831
|
+
if (!contract.total_payment) {
|
|
2832
|
+
console.error("Contract payment is missing.");
|
|
2833
|
+
process.exit(1);
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
const { program, connection, programId } = getProgram(config, keypair);
|
|
2837
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
2838
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
2839
|
+
if (!escrowState) {
|
|
2840
|
+
console.error("Escrow not found on-chain.");
|
|
2841
|
+
process.exit(1);
|
|
2842
|
+
}
|
|
2843
|
+
if (escrowState._ownerIsDelegated) {
|
|
2844
|
+
console.error("Escrow is delegated. Run `nebulon contract <id> sync` to undelegate before funding.");
|
|
2845
|
+
process.exit(1);
|
|
2846
|
+
}
|
|
2847
|
+
const hasTerms = escrowState.termsHash
|
|
2848
|
+
? escrowState.termsHash.some((byte) => byte !== 0)
|
|
2849
|
+
: false;
|
|
2850
|
+
if (!hasTerms) {
|
|
2851
|
+
console.error("Terms are not committed on-chain yet.");
|
|
2852
|
+
process.exit(1);
|
|
2853
|
+
}
|
|
2854
|
+
const hasMilestones = Array.isArray(contract.milestones)
|
|
2855
|
+
? contract.milestones.length > 0
|
|
2856
|
+
: false;
|
|
2857
|
+
const hasMilestonesHash = escrowState.milestonesHash
|
|
2858
|
+
? escrowState.milestonesHash.some((byte) => byte !== 0)
|
|
2859
|
+
: false;
|
|
2860
|
+
if (hasMilestones && !hasMilestonesHash) {
|
|
2861
|
+
console.error("Milestones are not committed on-chain yet.");
|
|
2862
|
+
console.error("Run `nebulon contract <id> sync` before funding.");
|
|
2863
|
+
process.exit(1);
|
|
2864
|
+
}
|
|
2865
|
+
const escrowId = escrowState.escrowId;
|
|
2866
|
+
const amount = BigInt(contract.total_payment);
|
|
2867
|
+
|
|
2868
|
+
console.log("Verify data and confirm actions please.");
|
|
2869
|
+
renderContractSummary(contract, wallet);
|
|
2870
|
+
console.log("-Action-");
|
|
2871
|
+
console.log("Fund Contract");
|
|
2872
|
+
console.log(`Amount : ${formatUsdc(amount)} USDC`);
|
|
2873
|
+
const feeEstimate = feeFromGross(amount);
|
|
2874
|
+
const netEstimate = netFromGross(amount);
|
|
2875
|
+
const coverAmount = grossFromNet(amount);
|
|
2876
|
+
console.log(
|
|
2877
|
+
` Protocol fee (2%): ${formatUsdc(feeEstimate)} USDC (contractor receives ${formatUsdc(netEstimate)}).`
|
|
2878
|
+
);
|
|
2879
|
+
console.log(
|
|
2880
|
+
` To cover the fee so the contractor receives your desired amount, set payment to ~${formatUsdc(coverAmount)} USDC.`
|
|
2881
|
+
);
|
|
2882
|
+
console.log("");
|
|
2883
|
+
|
|
2884
|
+
const balances = await renderBalances(
|
|
2885
|
+
config,
|
|
2886
|
+
walletKey,
|
|
2887
|
+
contract.mint || config.usdcMint
|
|
2888
|
+
);
|
|
2889
|
+
if (!ensureSolBalance(balances)) {
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
if (balances) {
|
|
2893
|
+
const requiredUsdc = Number(formatUsdc(contract.total_payment));
|
|
2894
|
+
if (Number.isFinite(requiredUsdc) && balances.usdc < requiredUsdc) {
|
|
2895
|
+
console.error(
|
|
2896
|
+
`Insufficient USDC balance. Need ${formatUsdc(contract.total_payment)} USDC, have ${balances.usdc.toFixed(6)} USDC.`
|
|
2897
|
+
);
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
const proceed = await confirmAction(
|
|
2903
|
+
options.confirm,
|
|
2904
|
+
`Do you want to fund this contract with ${formatUsdc(amount)} USDC? yes/no`
|
|
2905
|
+
);
|
|
2906
|
+
if (!proceed) {
|
|
2907
|
+
console.log("Canceled.");
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
console.log("Processing.");
|
|
2912
|
+
const mint = new PublicKey(contract.mint || config.usdcMint);
|
|
2913
|
+
const clientToken = await ensureAta(connection, keypair, walletKey, mint);
|
|
2914
|
+
const feeReceiverToken = await ensureAta(
|
|
2915
|
+
connection,
|
|
2916
|
+
keypair,
|
|
2917
|
+
FEE_RECEIVER,
|
|
2918
|
+
mint
|
|
2919
|
+
);
|
|
2920
|
+
const vaultToken = await getAssociatedTokenAddress(mint, escrowPda, true);
|
|
2921
|
+
|
|
2922
|
+
const sig = await program.methods
|
|
2923
|
+
.fundEscrow(
|
|
2924
|
+
new anchor.BN(escrowId.toString()),
|
|
2925
|
+
new anchor.BN(amount.toString())
|
|
2926
|
+
)
|
|
2927
|
+
.accounts({
|
|
2928
|
+
client: walletKey,
|
|
2929
|
+
escrow: escrowPda,
|
|
2930
|
+
clientToken,
|
|
2931
|
+
feeReceiverToken,
|
|
2932
|
+
vaultToken,
|
|
2933
|
+
tokenProgram: TOKEN_PROGRAM_ID,
|
|
2934
|
+
})
|
|
2935
|
+
.signers([keypair])
|
|
2936
|
+
.rpc();
|
|
2937
|
+
console.log(`Funding submitted. (tx: ${sig})`);
|
|
2938
|
+
|
|
2939
|
+
await markFunded(config.backendUrl, config.auth.token, contract.id);
|
|
2940
|
+
console.log("Syncing private state to escrow flags.");
|
|
2941
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2942
|
+
try {
|
|
2943
|
+
await runSyncFlags(config, contract, { ...options, confirm: true });
|
|
2944
|
+
break;
|
|
2945
|
+
} catch (error) {
|
|
2946
|
+
const message = (error && error.message ? error.message : JSON.stringify(error)).toLowerCase();
|
|
2947
|
+
if (message.includes("cannot be written") || message.includes("writable account") || message.includes("invalidwritableaccount")) {
|
|
2948
|
+
if (attempt < 2) {
|
|
2949
|
+
console.log(`Sync attempt ${attempt + 1} failed. Retrying in 5 seconds...`);
|
|
2950
|
+
await sleep(5000);
|
|
2951
|
+
continue;
|
|
2952
|
+
}
|
|
2953
|
+
console.log("Sync failed. Run `nebulon contract <id> sync` and retry after a few seconds.");
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
throw error;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
successMessage("Contract funded.");
|
|
2960
|
+
};
|
|
2961
|
+
|
|
2962
|
+
const runUpdateMilestone = async (config, contract, number, options) => {
|
|
2963
|
+
const { keypair, wallet, walletKey } = getWalletContext(config);
|
|
2964
|
+
if (!contract.escrow_pda) {
|
|
2965
|
+
console.error("Contract has no escrow.");
|
|
2966
|
+
process.exit(1);
|
|
2967
|
+
}
|
|
2968
|
+
ensureContractPhase(
|
|
2969
|
+
contract,
|
|
2970
|
+
["waiting_for_milestones_report", "in_progress"],
|
|
2971
|
+
"Fund the contract first."
|
|
2972
|
+
);
|
|
2973
|
+
const role = getRole(contract, wallet);
|
|
2974
|
+
if (role !== "contractor") {
|
|
2975
|
+
console.error("Only the service provider can submit milestones.");
|
|
2976
|
+
process.exit(1);
|
|
2977
|
+
}
|
|
2978
|
+
const index = Number(number) - 1;
|
|
2979
|
+
if (!Number.isFinite(index) || index < 0) {
|
|
2980
|
+
console.error("Invalid milestone number.");
|
|
2981
|
+
process.exit(1);
|
|
2982
|
+
}
|
|
2983
|
+
const milestoneList = Array.isArray(contract.milestones)
|
|
2984
|
+
? contract.milestones
|
|
2985
|
+
: [];
|
|
2986
|
+
const milestoneMeta = milestoneList.find((milestone) => milestone.index === index);
|
|
2987
|
+
if (!milestoneMeta) {
|
|
2988
|
+
console.error("Milestone not found.");
|
|
2989
|
+
console.error("Check the list with: nebulon contract <id> check milestone list");
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
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
|
+
}
|
|
3061
|
+
if (!config.ephemeralProviderUrl) {
|
|
3062
|
+
console.error(
|
|
3063
|
+
"Warning: MagicBlock RPC is not configured; milestone status checks may be unreliable."
|
|
3064
|
+
);
|
|
3065
|
+
console.error("Run `nebulon init` to fetch the MagicBlock endpoints.");
|
|
3066
|
+
}
|
|
3067
|
+
if (milestoneMeta.status === "approved") {
|
|
3068
|
+
console.error("Milestone already confirmed.");
|
|
3069
|
+
console.error("Current status: paid");
|
|
3070
|
+
return;
|
|
3071
|
+
}
|
|
3072
|
+
if (milestoneMeta.status === "disabled") {
|
|
3073
|
+
console.error("Milestone is disabled.");
|
|
3074
|
+
console.error("Current status: disabled");
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
let currentStatus = null;
|
|
3078
|
+
try {
|
|
3079
|
+
const milestone = await getPrivateMilestoneStatus(
|
|
3080
|
+
config,
|
|
3081
|
+
contract,
|
|
3082
|
+
keypair,
|
|
3083
|
+
walletKey,
|
|
3084
|
+
index
|
|
3085
|
+
);
|
|
3086
|
+
currentStatus = milestone ? milestone.status : null;
|
|
3087
|
+
} catch (error) {
|
|
3088
|
+
console.error("Unable to verify milestone status.");
|
|
3089
|
+
process.exit(1);
|
|
3090
|
+
}
|
|
3091
|
+
const statusValue = Number(currentStatus);
|
|
3092
|
+
if (!Number.isFinite(statusValue)) {
|
|
3093
|
+
console.error("Unable to determine milestone status.");
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
const statusLabel = formatPrivateMilestoneStatus(statusValue);
|
|
3097
|
+
if (statusValue === 1) {
|
|
3098
|
+
console.error("Milestone already submitted and awaiting customer confirmation.");
|
|
3099
|
+
console.error("Current status: submitted");
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
if (statusValue === 3) {
|
|
3103
|
+
console.error("Milestone already confirmed.");
|
|
3104
|
+
console.error("Current status: paid");
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
if (statusValue === 4) {
|
|
3108
|
+
console.error("Milestone is disabled.");
|
|
3109
|
+
console.error("Current status: disabled");
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
if (statusValue !== 0 && statusValue !== 2) {
|
|
3113
|
+
console.error(`Cannot mark milestone as ready from status: ${statusLabel}`);
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
console.log("Verify data and confirm actions please.");
|
|
3118
|
+
renderContractSummary(contract, wallet);
|
|
3119
|
+
console.log("-Action-");
|
|
3120
|
+
console.log("Update milestone");
|
|
3121
|
+
console.log(`Number : ${index + 1}`);
|
|
3122
|
+
console.log("Status : ready");
|
|
3123
|
+
console.log("");
|
|
3124
|
+
|
|
3125
|
+
const canProceed = await renderBalancesOrAbort(
|
|
3126
|
+
config,
|
|
3127
|
+
walletKey,
|
|
3128
|
+
contract.mint || config.usdcMint
|
|
3129
|
+
);
|
|
3130
|
+
if (!canProceed) {
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
|
|
3135
|
+
if (!proceed) {
|
|
3136
|
+
console.log("Canceled.");
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
console.log("Processing.");
|
|
3141
|
+
const sig = await updatePrivateMilestoneStatus(
|
|
3142
|
+
config,
|
|
3143
|
+
contract,
|
|
3144
|
+
keypair,
|
|
3145
|
+
walletKey,
|
|
3146
|
+
index,
|
|
3147
|
+
1
|
|
3148
|
+
);
|
|
3149
|
+
console.log(`Milestone updated. (tx: ${sig})`);
|
|
3150
|
+
|
|
3151
|
+
const milestones = Array.isArray(contract.milestones)
|
|
3152
|
+
? contract.milestones.map((milestone) =>
|
|
3153
|
+
milestone.index === index
|
|
3154
|
+
? { ...milestone, status: "ready" }
|
|
3155
|
+
: milestone
|
|
3156
|
+
)
|
|
3157
|
+
: [];
|
|
3158
|
+
const persisted = await updateContractMilestones(config, contract, milestones);
|
|
3159
|
+
if (!persisted) {
|
|
3160
|
+
console.log(
|
|
3161
|
+
"Warning: backend did not persist milestone status (check backend version)."
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
await runSyncFlags(config, contract, { ...options, confirm: true });
|
|
3165
|
+
successMessage("Milestone updated.");
|
|
3166
|
+
};
|
|
3167
|
+
|
|
3168
|
+
const runConfirmMilestone = async (config, contract, number, options) => {
|
|
3169
|
+
const { keypair, wallet, walletKey } = getWalletContext(config);
|
|
3170
|
+
if (!contract.escrow_pda) {
|
|
3171
|
+
console.error("Contract has no escrow.");
|
|
3172
|
+
process.exit(1);
|
|
3173
|
+
}
|
|
3174
|
+
ensureContractPhase(
|
|
3175
|
+
contract,
|
|
3176
|
+
["in_progress"],
|
|
3177
|
+
"Waiting on contractor submissions."
|
|
3178
|
+
);
|
|
3179
|
+
const role = getRole(contract, wallet);
|
|
3180
|
+
if (role !== "client") {
|
|
3181
|
+
console.error("Only the customer can confirm milestones.");
|
|
3182
|
+
process.exit(1);
|
|
3183
|
+
}
|
|
3184
|
+
const index = Number(number) - 1;
|
|
3185
|
+
if (!Number.isFinite(index) || index < 0) {
|
|
3186
|
+
console.error("Invalid milestone number.");
|
|
3187
|
+
process.exit(1);
|
|
3188
|
+
}
|
|
3189
|
+
|
|
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
|
+
}
|
|
3264
|
+
|
|
3265
|
+
console.log("Verify data and confirm actions please.");
|
|
3266
|
+
renderContractSummary(contract, wallet);
|
|
3267
|
+
console.log("-Action-");
|
|
3268
|
+
console.log("Confirm milestone");
|
|
3269
|
+
console.log(`Number : ${index + 1}`);
|
|
3270
|
+
console.log("");
|
|
3271
|
+
|
|
3272
|
+
let currentStatus = null;
|
|
3273
|
+
try {
|
|
3274
|
+
const milestone = await getPrivateMilestoneStatus(
|
|
3275
|
+
config,
|
|
3276
|
+
contract,
|
|
3277
|
+
keypair,
|
|
3278
|
+
walletKey,
|
|
3279
|
+
index
|
|
3280
|
+
);
|
|
3281
|
+
currentStatus = milestone ? milestone.status : null;
|
|
3282
|
+
} catch (error) {
|
|
3283
|
+
console.error("Unable to verify milestone status.");
|
|
3284
|
+
process.exit(1);
|
|
3285
|
+
}
|
|
3286
|
+
const statusValue = Number(currentStatus);
|
|
3287
|
+
if (!Number.isFinite(statusValue)) {
|
|
3288
|
+
console.error("Unable to determine milestone status.");
|
|
3289
|
+
return;
|
|
3290
|
+
}
|
|
3291
|
+
const statusLabel = formatPrivateMilestoneStatus(statusValue);
|
|
3292
|
+
if (statusValue !== 1) {
|
|
3293
|
+
console.error(`Milestone is not ready to confirm. Current status: ${statusLabel}`);
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
const canProceed = await renderBalancesOrAbort(
|
|
3298
|
+
config,
|
|
3299
|
+
walletKey,
|
|
3300
|
+
contract.mint || config.usdcMint
|
|
3301
|
+
);
|
|
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;
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
console.log("Processing.");
|
|
3313
|
+
const sig = await updatePrivateMilestoneStatus(
|
|
3314
|
+
config,
|
|
3315
|
+
contract,
|
|
3316
|
+
keypair,
|
|
3317
|
+
walletKey,
|
|
3318
|
+
index,
|
|
3319
|
+
3
|
|
3320
|
+
);
|
|
3321
|
+
console.log(`Milestone confirmed. (tx: ${sig})`);
|
|
3322
|
+
|
|
3323
|
+
const milestones = Array.isArray(contract.milestones)
|
|
3324
|
+
? contract.milestones.map((milestone) =>
|
|
3325
|
+
milestone.index === index
|
|
3326
|
+
? { ...milestone, status: "approved" }
|
|
3327
|
+
: milestone
|
|
3328
|
+
)
|
|
3329
|
+
: [];
|
|
3330
|
+
const persisted = await updateContractMilestones(config, contract, milestones);
|
|
3331
|
+
if (!persisted) {
|
|
3332
|
+
console.log(
|
|
3333
|
+
"Warning: backend did not persist milestone status (check backend version)."
|
|
3334
|
+
);
|
|
3335
|
+
}
|
|
3336
|
+
await runSyncFlags(config, contract, { ...options, confirm: true });
|
|
3337
|
+
try {
|
|
3338
|
+
const refreshed = await getEscrowState(connection, programId, escrowPda);
|
|
3339
|
+
if (refreshed && refreshed.readyToClaim) {
|
|
3340
|
+
successMessage("Contract funds are now ready to claim by the service provider.");
|
|
3341
|
+
}
|
|
3342
|
+
} catch {}
|
|
3343
|
+
successMessage("Milestone confirmed.");
|
|
3344
|
+
};
|
|
3345
|
+
|
|
3346
|
+
const runClaimFunds = async (config, contract, options) => {
|
|
3347
|
+
const { keypair, wallet, walletKey } = getWalletContext(config);
|
|
3348
|
+
if (!contract.escrow_pda) {
|
|
3349
|
+
console.error("Contract has no escrow.");
|
|
3350
|
+
process.exit(1);
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
const { program, connection, programId } = getProgram(config, keypair);
|
|
3354
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
3355
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
3356
|
+
if (!escrowState) {
|
|
3357
|
+
console.error("Escrow not found on-chain.");
|
|
3358
|
+
process.exit(1);
|
|
3359
|
+
}
|
|
3360
|
+
if (escrowState.paidOut) {
|
|
3361
|
+
console.log("Funds already claimed. Contract completed successfully.");
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
const escrowId = escrowState.escrowId;
|
|
3365
|
+
const readyToClaim = Boolean(escrowState.readyToClaim);
|
|
3366
|
+
const timeoutFundsReady = Boolean(escrowState.timeoutFundsReady);
|
|
3367
|
+
const timeoutRefundReady = Boolean(escrowState.timeoutRefundReady);
|
|
3368
|
+
|
|
3369
|
+
const mint = new PublicKey(contract.mint || config.usdcMint);
|
|
3370
|
+
const vaultToken = await getAssociatedTokenAddress(mint, escrowPda, true);
|
|
3371
|
+
|
|
3372
|
+
if (wallet === contract.contractor_wallet) {
|
|
3373
|
+
if (!readyToClaim && !timeoutFundsReady) {
|
|
3374
|
+
console.error("Contract is not ready to claim yet.");
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
const rawFunded =
|
|
3378
|
+
typeof escrowState.fundedAmount === "bigint"
|
|
3379
|
+
? escrowState.fundedAmount
|
|
3380
|
+
: BigInt(escrowState.fundedAmount || 0);
|
|
3381
|
+
console.log("Verify data and confirm actions please.");
|
|
3382
|
+
renderContractSummary(contract, wallet);
|
|
3383
|
+
console.log("-Action-");
|
|
3384
|
+
console.log("Claim funds");
|
|
3385
|
+
console.log(`Amount : ${formatUsdc(rawFunded)} USDC`);
|
|
3386
|
+
console.log("");
|
|
3387
|
+
|
|
3388
|
+
const canProceed = await renderBalancesOrAbort(
|
|
3389
|
+
config,
|
|
3390
|
+
walletKey,
|
|
3391
|
+
contract.mint || config.usdcMint
|
|
3392
|
+
);
|
|
3393
|
+
if (!canProceed) {
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
|
|
3398
|
+
if (!proceed) {
|
|
3399
|
+
console.log("Canceled.");
|
|
3400
|
+
return;
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
const contractorToken = await ensureAta(connection, keypair, walletKey, mint);
|
|
3404
|
+
if (readyToClaim) {
|
|
3405
|
+
try {
|
|
3406
|
+
const sig = await program.methods
|
|
3407
|
+
.claimFunds(new anchor.BN(escrowId.toString()))
|
|
3408
|
+
.accounts({
|
|
3409
|
+
contractor: walletKey,
|
|
3410
|
+
creator: escrowState.creator,
|
|
3411
|
+
escrow: escrowPda,
|
|
3412
|
+
contractorToken,
|
|
3413
|
+
vaultToken,
|
|
3414
|
+
tokenProgram: TOKEN_PROGRAM_ID,
|
|
3415
|
+
})
|
|
3416
|
+
.signers([keypair])
|
|
3417
|
+
.rpc();
|
|
3418
|
+
console.log(`Funds claimed. (tx: ${sig})`);
|
|
3419
|
+
successMessage(
|
|
3420
|
+
`Funds claimed successfully. (+${formatUsdc(rawFunded)} USDC)`
|
|
3421
|
+
);
|
|
3422
|
+
successMessage("Escrow rent returned to the creator.");
|
|
3423
|
+
try {
|
|
3424
|
+
await refreshContract(config.backendUrl, config.auth.token, contract.id);
|
|
3425
|
+
} catch {
|
|
3426
|
+
console.log("Warning: backend status refresh failed.");
|
|
3427
|
+
}
|
|
3428
|
+
return;
|
|
3429
|
+
} catch (error) {
|
|
3430
|
+
errorMessage("Escrow rent was not returned.");
|
|
3431
|
+
throw error;
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
if (timeoutFundsReady) {
|
|
3435
|
+
try {
|
|
3436
|
+
const sig = await program.methods
|
|
3437
|
+
.claimTimeoutFunds(new anchor.BN(escrowId.toString()))
|
|
3438
|
+
.accounts({
|
|
3439
|
+
contractor: walletKey,
|
|
3440
|
+
creator: escrowState.creator,
|
|
3441
|
+
escrow: escrowPda,
|
|
3442
|
+
contractorToken,
|
|
3443
|
+
vaultToken,
|
|
3444
|
+
tokenProgram: TOKEN_PROGRAM_ID,
|
|
3445
|
+
})
|
|
3446
|
+
.signers([keypair])
|
|
3447
|
+
.rpc();
|
|
3448
|
+
console.log(`Timeout funds claimed. (tx: ${sig})`);
|
|
3449
|
+
successMessage(
|
|
3450
|
+
`Funds claimed successfully. (+${formatUsdc(rawFunded)} USDC)`
|
|
3451
|
+
);
|
|
3452
|
+
successMessage("Escrow rent returned to the creator.");
|
|
3453
|
+
try {
|
|
3454
|
+
await refreshContract(config.backendUrl, config.auth.token, contract.id);
|
|
3455
|
+
} catch {
|
|
3456
|
+
console.log("Warning: backend status refresh failed.");
|
|
3457
|
+
}
|
|
3458
|
+
return;
|
|
3459
|
+
} catch (error) {
|
|
3460
|
+
errorMessage("Escrow rent was not returned.");
|
|
3461
|
+
throw error;
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
if (wallet === contract.client_wallet) {
|
|
3468
|
+
if (!timeoutRefundReady) {
|
|
3469
|
+
console.error("Refund is not available for this contract.");
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
console.log("Verify data and confirm actions please.");
|
|
3473
|
+
renderContractSummary(contract, wallet);
|
|
3474
|
+
console.log("-Action-");
|
|
3475
|
+
console.log("Claim funds");
|
|
3476
|
+
console.log("");
|
|
3477
|
+
|
|
3478
|
+
const canProceed = await renderBalancesOrAbort(
|
|
3479
|
+
config,
|
|
3480
|
+
walletKey,
|
|
3481
|
+
contract.mint || config.usdcMint
|
|
3482
|
+
);
|
|
3483
|
+
if (!canProceed) {
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
|
|
3488
|
+
if (!proceed) {
|
|
3489
|
+
console.log("Canceled.");
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
const clientToken = await ensureAta(connection, keypair, walletKey, mint);
|
|
3494
|
+
try {
|
|
3495
|
+
const sig = await program.methods
|
|
3496
|
+
.claimTimeoutRefund(new anchor.BN(escrowId.toString()))
|
|
3497
|
+
.accounts({
|
|
3498
|
+
client: walletKey,
|
|
3499
|
+
creator: escrowState.creator,
|
|
3500
|
+
escrow: escrowPda,
|
|
3501
|
+
clientToken,
|
|
3502
|
+
vaultToken,
|
|
3503
|
+
tokenProgram: TOKEN_PROGRAM_ID,
|
|
3504
|
+
})
|
|
3505
|
+
.signers([keypair])
|
|
3506
|
+
.rpc();
|
|
3507
|
+
console.log(`Refund claimed. (tx: ${sig})`);
|
|
3508
|
+
successMessage("Refund claimed.");
|
|
3509
|
+
successMessage("Escrow rent returned to the creator.");
|
|
3510
|
+
try {
|
|
3511
|
+
await refreshContract(config.backendUrl, config.auth.token, contract.id);
|
|
3512
|
+
} catch {
|
|
3513
|
+
console.log("Warning: backend status refresh failed.");
|
|
3514
|
+
}
|
|
3515
|
+
return;
|
|
3516
|
+
} catch (error) {
|
|
3517
|
+
errorMessage("Escrow rent was not returned.");
|
|
3518
|
+
throw error;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
console.error("Only contract participants can claim funds.");
|
|
3523
|
+
};
|
|
3524
|
+
|
|
3525
|
+
|
|
3526
|
+
const fetchPrivateMilestones = async (
|
|
3527
|
+
erProgram,
|
|
3528
|
+
escrowPda,
|
|
3529
|
+
programId,
|
|
3530
|
+
indices
|
|
3531
|
+
) => {
|
|
3532
|
+
const milestones = [];
|
|
3533
|
+
for (const index of indices) {
|
|
3534
|
+
const pda = derivePrivateMilestonePda(escrowPda, index, programId);
|
|
3535
|
+
const state = await erProgram.account.privateMilestone.fetch(pda);
|
|
3536
|
+
milestones.push({
|
|
3537
|
+
index,
|
|
3538
|
+
status: Number(state.status),
|
|
3539
|
+
descriptionHash: state.descriptionHash || state.description_hash,
|
|
3540
|
+
pda,
|
|
3541
|
+
});
|
|
3542
|
+
}
|
|
3543
|
+
return milestones;
|
|
3544
|
+
};
|
|
3545
|
+
|
|
3546
|
+
const updatePrivateMilestoneStatus = async (
|
|
3547
|
+
config,
|
|
3548
|
+
contract,
|
|
3549
|
+
keypair,
|
|
3550
|
+
walletKey,
|
|
3551
|
+
index,
|
|
3552
|
+
status
|
|
3553
|
+
) => {
|
|
3554
|
+
const { program, connection, programId } = getProgram(config, keypair);
|
|
3555
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
3556
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
3557
|
+
if (!escrowState) {
|
|
3558
|
+
console.error("Escrow not found on-chain.");
|
|
3559
|
+
process.exit(1);
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
const escrowId = escrowState.escrowId;
|
|
3563
|
+
const privateMilestonePda = derivePrivateMilestonePda(
|
|
3564
|
+
escrowPda,
|
|
3565
|
+
index,
|
|
3566
|
+
programId
|
|
3567
|
+
);
|
|
3568
|
+
|
|
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
|
+
await ensureDelegatedAccount(
|
|
3584
|
+
config,
|
|
3585
|
+
program,
|
|
3586
|
+
keypair,
|
|
3587
|
+
members.accountType,
|
|
3588
|
+
privateMilestonePda
|
|
3589
|
+
);
|
|
3590
|
+
await ensureDelegatedEscrow(
|
|
3591
|
+
config,
|
|
3592
|
+
program,
|
|
3593
|
+
keypair,
|
|
3594
|
+
escrowId,
|
|
3595
|
+
escrowPda,
|
|
3596
|
+
new PublicKey(contract.client_wallet)
|
|
3597
|
+
);
|
|
3598
|
+
|
|
3599
|
+
const { program: erProgram, sessionSigner, sessionPda } =
|
|
3600
|
+
await getPerProgramBundle(config, keypair, programId, program.provider);
|
|
3601
|
+
|
|
3602
|
+
const sig = await erProgram.methods
|
|
3603
|
+
.updatePrivateMilestoneStatus(
|
|
3604
|
+
new anchor.BN(escrowId.toString()),
|
|
3605
|
+
index,
|
|
3606
|
+
status
|
|
3607
|
+
)
|
|
3608
|
+
.accounts({
|
|
3609
|
+
user: walletKey,
|
|
3610
|
+
payer: sessionSigner.publicKey,
|
|
3611
|
+
sessionToken: sessionPda,
|
|
3612
|
+
escrow: escrowPda,
|
|
3613
|
+
privateMilestone: privateMilestonePda,
|
|
3614
|
+
})
|
|
3615
|
+
.signers([sessionSigner])
|
|
3616
|
+
.rpc({ skipPreflight: true });
|
|
3617
|
+
|
|
3618
|
+
return sig;
|
|
3619
|
+
};
|
|
3620
|
+
|
|
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
|
+
const runSyncFlags = async (config, contract, options) => {
|
|
3810
|
+
const { keypair, walletKey } = getWalletContext(config);
|
|
3811
|
+
if (!contract.escrow_pda) {
|
|
3812
|
+
console.error("Contract has no escrow.");
|
|
3813
|
+
process.exit(1);
|
|
3814
|
+
}
|
|
3815
|
+
if (isL1Mode(contract)) {
|
|
3816
|
+
await runSyncFlagsL1(config, contract, options);
|
|
3817
|
+
return;
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
const { program, connection, programId } = getProgram(config, keypair);
|
|
3821
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
3822
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
3823
|
+
if (!escrowState) {
|
|
3824
|
+
console.error("Escrow not found on-chain.");
|
|
3825
|
+
process.exit(1);
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
const escrowId = escrowState.escrowId;
|
|
3829
|
+
const { termsPda, program: l1Program } = await ensureTermsPrepared(
|
|
3830
|
+
config,
|
|
3831
|
+
contract,
|
|
3832
|
+
keypair,
|
|
3833
|
+
escrowPda,
|
|
3834
|
+
escrowId
|
|
3835
|
+
);
|
|
3836
|
+
|
|
3837
|
+
const milestoneIndices = Array.isArray(contract.milestones)
|
|
3838
|
+
? contract.milestones.map((milestone) => milestone.index)
|
|
3839
|
+
: [];
|
|
3840
|
+
milestoneIndices.sort((a, b) => a - b);
|
|
3841
|
+
for (const index of milestoneIndices) {
|
|
3842
|
+
const privateMilestonePda = derivePrivateMilestonePda(
|
|
3843
|
+
escrowPda,
|
|
3844
|
+
index,
|
|
3845
|
+
programId
|
|
3846
|
+
);
|
|
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
|
+
await ensureDelegatedAccount(
|
|
3862
|
+
config,
|
|
3863
|
+
l1Program,
|
|
3864
|
+
keypair,
|
|
3865
|
+
members.accountType,
|
|
3866
|
+
privateMilestonePda
|
|
3867
|
+
);
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
await ensureDelegatedEscrow(
|
|
3871
|
+
config,
|
|
3872
|
+
l1Program,
|
|
3873
|
+
keypair,
|
|
3874
|
+
escrowId,
|
|
3875
|
+
escrowPda,
|
|
3876
|
+
new PublicKey(contract.client_wallet)
|
|
3877
|
+
);
|
|
3878
|
+
|
|
3879
|
+
const { program: erProgram, sessionSigner, sessionPda } =
|
|
3880
|
+
await getPerProgramBundle(config, keypair, programId, l1Program.provider);
|
|
3881
|
+
|
|
3882
|
+
let terms;
|
|
3883
|
+
try {
|
|
3884
|
+
terms = await erProgram.account.terms.fetch(termsPda);
|
|
3885
|
+
} catch (error) {
|
|
3886
|
+
console.error("Terms not available in PER yet.");
|
|
3887
|
+
process.exit(1);
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
const milestones = await fetchPrivateMilestones(
|
|
3891
|
+
erProgram,
|
|
3892
|
+
escrowPda,
|
|
3893
|
+
programId,
|
|
3894
|
+
milestoneIndices
|
|
3895
|
+
);
|
|
3896
|
+
const milestonesHash = buildMilestonesHash(milestones);
|
|
3897
|
+
|
|
3898
|
+
const fundedAmount =
|
|
3899
|
+
typeof escrowState.fundedAmount === "bigint"
|
|
3900
|
+
? escrowState.fundedAmount
|
|
3901
|
+
: BigInt(escrowState.fundedAmount || 0);
|
|
3902
|
+
const canCommitMilestones = fundedAmount === 0n && !escrowState.fundingOk;
|
|
3903
|
+
const totalPayment = BigInt(terms.totalPayment.toString());
|
|
3904
|
+
const requiredNet = netFromGross(totalPayment);
|
|
3905
|
+
const fundingOk = fundedAmount === requiredNet;
|
|
3906
|
+
const hasTermsHash = escrowState.termsHash
|
|
3907
|
+
? escrowState.termsHash.some((byte) => byte !== 0)
|
|
3908
|
+
: false;
|
|
3909
|
+
if (!hasTermsHash) {
|
|
3910
|
+
console.log("Terms hash not committed on-chain yet.");
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
const deadline = resolveTermsDeadline(
|
|
3914
|
+
Number(terms.deadline.toString()),
|
|
3915
|
+
escrowState.fundedAt
|
|
3916
|
+
);
|
|
3917
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3918
|
+
const deadlinePassed = deadline ? now > deadline : false;
|
|
3919
|
+
|
|
3920
|
+
const statuses = milestones.map((milestone) => milestone.status);
|
|
3921
|
+
const allApproved =
|
|
3922
|
+
milestones.length === 0 ||
|
|
3923
|
+
statuses.every((status) => status === 3 || status === 4);
|
|
3924
|
+
const allSubmitted =
|
|
3925
|
+
milestones.length === 0 ||
|
|
3926
|
+
statuses.every((status) => status === 1 || status === 3 || status === 4);
|
|
3927
|
+
const hasUnsubmitted =
|
|
3928
|
+
milestones.length === 0 ||
|
|
3929
|
+
statuses.some((status) => status === 0 || status === 2);
|
|
3930
|
+
|
|
3931
|
+
const milestoneCount = milestones.length;
|
|
3932
|
+
if (milestoneCount > 255) {
|
|
3933
|
+
console.error("Too many milestones to sync.");
|
|
3934
|
+
process.exit(1);
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
const proceed = await confirmAction(options.confirm, "Update escrow flags? yes/no");
|
|
3938
|
+
if (!proceed) {
|
|
3939
|
+
console.log("Canceled.");
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
let didUpdate = false;
|
|
3944
|
+
const milestoneAccounts = milestones.map((milestone) => ({
|
|
3945
|
+
pubkey: milestone.pda,
|
|
3946
|
+
isWritable: false,
|
|
3947
|
+
isSigner: false,
|
|
3948
|
+
}));
|
|
3949
|
+
|
|
3950
|
+
if (canCommitMilestones && milestonesHash && escrowState.milestonesHash) {
|
|
3951
|
+
const currentHash = Buffer.from(escrowState.milestonesHash);
|
|
3952
|
+
if (!currentHash.equals(milestonesHash)) {
|
|
3953
|
+
const sig = await erProgram.methods
|
|
3954
|
+
.commitMilestones(
|
|
3955
|
+
new anchor.BN(escrowId.toString()),
|
|
3956
|
+
Array.from(milestonesHash)
|
|
3957
|
+
)
|
|
3958
|
+
.accounts({
|
|
3959
|
+
user: keypair.publicKey,
|
|
3960
|
+
payer: sessionSigner.publicKey,
|
|
3961
|
+
sessionToken: sessionPda,
|
|
3962
|
+
escrow: escrowPda,
|
|
3963
|
+
})
|
|
3964
|
+
.signers([sessionSigner])
|
|
3965
|
+
.rpc({ skipPreflight: true });
|
|
3966
|
+
console.log(`Milestones committed. (tx: ${sig})`);
|
|
3967
|
+
didUpdate = true;
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
if (hasTermsHash && fundingOk && !escrowState.fundingOk) {
|
|
3972
|
+
const sig = await erProgram.methods
|
|
3973
|
+
.setFundingOk(new anchor.BN(escrowId.toString()))
|
|
3974
|
+
.accounts({
|
|
3975
|
+
user: keypair.publicKey,
|
|
3976
|
+
payer: sessionSigner.publicKey,
|
|
3977
|
+
sessionToken: sessionPda,
|
|
3978
|
+
escrow: escrowPda,
|
|
3979
|
+
terms: termsPda,
|
|
3980
|
+
})
|
|
3981
|
+
.signers([sessionSigner])
|
|
3982
|
+
.rpc({ skipPreflight: true });
|
|
3983
|
+
console.log(`Funding verified. (tx: ${sig})`);
|
|
3984
|
+
didUpdate = true;
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
let readyToClaimSet = false;
|
|
3988
|
+
if (hasTermsHash && fundingOk && allApproved && !escrowState.readyToClaim) {
|
|
3989
|
+
const sig = await erProgram.methods
|
|
3990
|
+
.setReadyToClaim(
|
|
3991
|
+
new anchor.BN(escrowId.toString()),
|
|
3992
|
+
milestoneCount
|
|
3993
|
+
)
|
|
3994
|
+
.accounts({
|
|
3995
|
+
user: keypair.publicKey,
|
|
3996
|
+
payer: sessionSigner.publicKey,
|
|
3997
|
+
sessionToken: sessionPda,
|
|
3998
|
+
escrow: escrowPda,
|
|
3999
|
+
})
|
|
4000
|
+
.remainingAccounts(milestoneAccounts)
|
|
4001
|
+
.signers([sessionSigner])
|
|
4002
|
+
.rpc({ skipPreflight: true });
|
|
4003
|
+
console.log(`Ready to claim set. (tx: ${sig})`);
|
|
4004
|
+
didUpdate = true;
|
|
4005
|
+
readyToClaimSet = true;
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
if (
|
|
4009
|
+
hasTermsHash &&
|
|
4010
|
+
fundingOk &&
|
|
4011
|
+
deadlinePassed &&
|
|
4012
|
+
hasUnsubmitted &&
|
|
4013
|
+
!escrowState.timeoutRefundReady
|
|
4014
|
+
) {
|
|
4015
|
+
const sig = await erProgram.methods
|
|
4016
|
+
.setTimeoutRefundReady(
|
|
4017
|
+
new anchor.BN(escrowId.toString()),
|
|
4018
|
+
milestoneCount
|
|
4019
|
+
)
|
|
4020
|
+
.accounts({
|
|
4021
|
+
user: keypair.publicKey,
|
|
4022
|
+
payer: sessionSigner.publicKey,
|
|
4023
|
+
sessionToken: sessionPda,
|
|
4024
|
+
escrow: escrowPda,
|
|
4025
|
+
terms: termsPda,
|
|
4026
|
+
})
|
|
4027
|
+
.remainingAccounts(milestoneAccounts)
|
|
4028
|
+
.signers([sessionSigner])
|
|
4029
|
+
.rpc({ skipPreflight: true });
|
|
4030
|
+
console.log(`Timeout refund ready set. (tx: ${sig})`);
|
|
4031
|
+
didUpdate = true;
|
|
4032
|
+
}
|
|
4033
|
+
|
|
4034
|
+
if (
|
|
4035
|
+
hasTermsHash &&
|
|
4036
|
+
fundingOk &&
|
|
4037
|
+
deadlinePassed &&
|
|
4038
|
+
allSubmitted &&
|
|
4039
|
+
!escrowState.timeoutFundsReady
|
|
4040
|
+
) {
|
|
4041
|
+
const sig = await erProgram.methods
|
|
4042
|
+
.setTimeoutFundsReady(
|
|
4043
|
+
new anchor.BN(escrowId.toString()),
|
|
4044
|
+
milestoneCount
|
|
4045
|
+
)
|
|
4046
|
+
.accounts({
|
|
4047
|
+
user: keypair.publicKey,
|
|
4048
|
+
payer: sessionSigner.publicKey,
|
|
4049
|
+
sessionToken: sessionPda,
|
|
4050
|
+
escrow: escrowPda,
|
|
4051
|
+
terms: termsPda,
|
|
4052
|
+
})
|
|
4053
|
+
.remainingAccounts(milestoneAccounts)
|
|
4054
|
+
.signers([sessionSigner])
|
|
4055
|
+
.rpc({ skipPreflight: true });
|
|
4056
|
+
console.log(`Timeout funds ready set. (tx: ${sig})`);
|
|
4057
|
+
didUpdate = true;
|
|
4058
|
+
}
|
|
4059
|
+
|
|
4060
|
+
const shouldUndelegate = didUpdate || escrowState._ownerIsDelegated;
|
|
4061
|
+
if (shouldUndelegate) {
|
|
4062
|
+
const sig = await erProgram.methods
|
|
4063
|
+
.undelegateEscrow()
|
|
4064
|
+
.accounts({
|
|
4065
|
+
payer: sessionSigner.publicKey,
|
|
4066
|
+
escrow: escrowPda,
|
|
4067
|
+
magicProgram: MAGIC_PROGRAM_ID,
|
|
4068
|
+
magicContext: MAGIC_CONTEXT_ID,
|
|
4069
|
+
})
|
|
4070
|
+
.signers([sessionSigner])
|
|
4071
|
+
.rpc({ skipPreflight: true });
|
|
4072
|
+
console.log(`Escrow committed. (tx: ${sig})`);
|
|
4073
|
+
} else {
|
|
4074
|
+
console.log("No escrow flag changes needed.");
|
|
4075
|
+
}
|
|
4076
|
+
successMessage("Sync completed.");
|
|
4077
|
+
if (readyToClaimSet) {
|
|
4078
|
+
successMessage("Contract funds are now ready to claim by the service provider.");
|
|
4079
|
+
}
|
|
4080
|
+
};
|
|
4081
|
+
|
|
4082
|
+
const runMilestoneList = async (config, contract) => {
|
|
4083
|
+
const { keypair, wallet, walletKey } = getWalletContext(config);
|
|
4084
|
+
if (!contract.escrow_pda) {
|
|
4085
|
+
console.error("Contract has no escrow.");
|
|
4086
|
+
process.exit(1);
|
|
4087
|
+
}
|
|
4088
|
+
const role = getRole(contract, wallet);
|
|
4089
|
+
if (role !== "client" && role !== "contractor") {
|
|
4090
|
+
console.error("Only contract participants can view milestone details.");
|
|
4091
|
+
process.exit(1);
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
renderContractSummary(contract, wallet);
|
|
4095
|
+
console.log("");
|
|
4096
|
+
const phaseLabel = ACTIVE_STATUSES.has(contract.status)
|
|
4097
|
+
? "Milestones"
|
|
4098
|
+
: `Milestones [${contract.status} phase]`;
|
|
4099
|
+
console.log(phaseLabel);
|
|
4100
|
+
|
|
4101
|
+
const { program, connection, programId } = getProgram(config, keypair);
|
|
4102
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
4103
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
4104
|
+
if (!escrowState) {
|
|
4105
|
+
console.error("Escrow not found on-chain.");
|
|
4106
|
+
process.exit(1);
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
const milestoneIndices = Array.isArray(contract.milestones)
|
|
4110
|
+
? contract.milestones.map((milestone) => milestone.index)
|
|
4111
|
+
: [];
|
|
4112
|
+
if (!milestoneIndices.length) {
|
|
4113
|
+
console.log("(No milestones)");
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
milestoneIndices.sort((a, b) => a - b);
|
|
4117
|
+
|
|
4118
|
+
let milestones = [];
|
|
4119
|
+
if (isL1Mode(contract)) {
|
|
4120
|
+
milestones = await fetchPublicMilestones(
|
|
4121
|
+
program,
|
|
4122
|
+
escrowPda,
|
|
4123
|
+
programId,
|
|
4124
|
+
milestoneIndices
|
|
4125
|
+
);
|
|
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
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4144
|
+
const metadataByIndex = new Map(
|
|
4145
|
+
(contract.milestones || []).map((milestone) => [milestone.index, milestone])
|
|
4146
|
+
);
|
|
4147
|
+
const ordered = [...milestones].sort((a, b) => a.index - b.index);
|
|
4148
|
+
ordered.forEach((milestone) => {
|
|
4149
|
+
const meta = metadataByIndex.get(milestone.index) || {};
|
|
4150
|
+
const label = getMilestoneLabel(meta);
|
|
4151
|
+
const statusLabel = Number.isFinite(milestone.status)
|
|
4152
|
+
? formatPrivateMilestoneStatus(milestone.status)
|
|
4153
|
+
: "unknown";
|
|
4154
|
+
const suffix = statusLabel === "disabled" ? ` ${chalk.gray("[disabled]")}` : "";
|
|
4155
|
+
console.log(`${milestone.index + 1}) ${label}${suffix}`);
|
|
4156
|
+
});
|
|
4157
|
+
};
|
|
4158
|
+
|
|
4159
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4160
|
+
|
|
4161
|
+
const runMilestoneStatus = async (config, contract, number) => {
|
|
4162
|
+
const { keypair, wallet, walletKey } = getWalletContext(config);
|
|
4163
|
+
if (!contract.escrow_pda) {
|
|
4164
|
+
console.error("Contract has no escrow.");
|
|
4165
|
+
process.exit(1);
|
|
4166
|
+
}
|
|
4167
|
+
if (!ACTIVE_STATUSES.has(contract.status)) {
|
|
4168
|
+
console.error(
|
|
4169
|
+
"Contract is not in progress. Use: nebulon contract <id> check milestone list"
|
|
4170
|
+
);
|
|
4171
|
+
process.exit(1);
|
|
4172
|
+
}
|
|
4173
|
+
const role = getRole(contract, wallet);
|
|
4174
|
+
if (role !== "client" && role !== "contractor") {
|
|
4175
|
+
console.error("Only contract participants can view milestone details.");
|
|
4176
|
+
process.exit(1);
|
|
4177
|
+
}
|
|
4178
|
+
|
|
4179
|
+
const index = Number(number) - 1;
|
|
4180
|
+
if (!Number.isFinite(index) || index < 0) {
|
|
4181
|
+
console.error("Invalid milestone number.");
|
|
4182
|
+
process.exit(1);
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
const { program, connection, programId } = getProgram(config, keypair);
|
|
4186
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
4187
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
4188
|
+
if (!escrowState) {
|
|
4189
|
+
console.error("Escrow not found on-chain.");
|
|
4190
|
+
process.exit(1);
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
let milestone;
|
|
4194
|
+
if (isL1Mode(contract)) {
|
|
4195
|
+
const items = await fetchPublicMilestones(
|
|
4196
|
+
program,
|
|
4197
|
+
escrowPda,
|
|
4198
|
+
programId,
|
|
4199
|
+
[index]
|
|
4200
|
+
);
|
|
4201
|
+
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
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
const meta = (contract.milestones || []).find((m) => m.index === index) || {};
|
|
4222
|
+
const label = getMilestoneLabel(meta);
|
|
4223
|
+
const statusLabel = Number.isFinite(milestone.status)
|
|
4224
|
+
? formatPrivateMilestoneStatus(milestone.status)
|
|
4225
|
+
: "unknown";
|
|
4226
|
+
console.log(`Milestone ${index + 1}: ${label}`);
|
|
4227
|
+
console.log(`Status: ${statusLabel}`);
|
|
4228
|
+
};
|
|
4229
|
+
|
|
4230
|
+
const runCheckMilestones = async (config, contract) => {
|
|
4231
|
+
const { wallet } = getWalletContext(config);
|
|
4232
|
+
const role = getRole(contract, wallet);
|
|
4233
|
+
if (role !== "client" && role !== "contractor") {
|
|
4234
|
+
console.error("Only contract participants can view milestone details.");
|
|
4235
|
+
process.exit(1);
|
|
4236
|
+
}
|
|
4237
|
+
const milestones = Array.isArray(contract.milestones)
|
|
4238
|
+
? contract.milestones
|
|
4239
|
+
: [];
|
|
4240
|
+
if (!milestones.length) {
|
|
4241
|
+
console.log("Milestones: none");
|
|
4242
|
+
return;
|
|
4243
|
+
}
|
|
4244
|
+
console.log(`Milestones: ${milestones.length}`);
|
|
4245
|
+
milestones
|
|
4246
|
+
.slice()
|
|
4247
|
+
.sort((a, b) => a.index - b.index)
|
|
4248
|
+
.forEach((milestone) => {
|
|
4249
|
+
const label = getMilestoneLabel(milestone);
|
|
4250
|
+
const status = milestone.status ? ` (${milestone.status})` : "";
|
|
4251
|
+
console.log(`${milestone.index + 1}) ${label}${status}`);
|
|
4252
|
+
});
|
|
4253
|
+
};
|
|
4254
|
+
|
|
4255
|
+
const runContractDetails = async (config, contract) => {
|
|
4256
|
+
const { keypair, wallet } = getWalletContext(config);
|
|
4257
|
+
const role = getRole(contract, wallet);
|
|
4258
|
+
console.log("Contract details");
|
|
4259
|
+
console.log(`ID: ${contract.id}`);
|
|
4260
|
+
console.log(`Status: ${contract.status}`);
|
|
4261
|
+
console.log(`Execution mode: ${getExecutionMode(contract)}`);
|
|
4262
|
+
console.log(`Role: ${role}`);
|
|
4263
|
+
console.log(
|
|
4264
|
+
formatParticipant(
|
|
4265
|
+
"Customer",
|
|
4266
|
+
contract.client_handle,
|
|
4267
|
+
contract.client_wallet,
|
|
4268
|
+
wallet
|
|
4269
|
+
)
|
|
4270
|
+
);
|
|
4271
|
+
console.log(
|
|
4272
|
+
formatParticipant(
|
|
4273
|
+
"Service Provider",
|
|
4274
|
+
contract.contractor_handle,
|
|
4275
|
+
contract.contractor_wallet,
|
|
4276
|
+
wallet
|
|
4277
|
+
)
|
|
4278
|
+
);
|
|
4279
|
+
console.log(`Escrow: ${contract.escrow_pda || "n/a"}`);
|
|
4280
|
+
console.log(`Mint: ${contract.mint || "n/a"}`);
|
|
4281
|
+
console.log(`Vault: ${contract.vault_token || "n/a"}`);
|
|
4282
|
+
const privacyLabel = isL1Mode(contract)
|
|
4283
|
+
? "l1"
|
|
4284
|
+
: contract.privacyReady
|
|
4285
|
+
? "ready"
|
|
4286
|
+
: "pending";
|
|
4287
|
+
console.log(`Privacy: ${privacyLabel}`);
|
|
4288
|
+
|
|
4289
|
+
if (!isL1Mode(contract) && contract.terms_encrypted && !contract.privacyReady) {
|
|
4290
|
+
console.log("Terms: encrypted (waiting on privacy keys)");
|
|
4291
|
+
} else {
|
|
4292
|
+
console.log(`Deadline: ${contract.deadline ? formatDeadlineValue(contract.deadline) : "missing"}`);
|
|
4293
|
+
console.log(
|
|
4294
|
+
`Payment: ${contract.total_payment ? `${formatUsdc(contract.total_payment)} USDC` : "missing"}`
|
|
4295
|
+
);
|
|
4296
|
+
}
|
|
4297
|
+
|
|
4298
|
+
const milestones = Array.isArray(contract.milestones)
|
|
4299
|
+
? contract.milestones
|
|
4300
|
+
: [];
|
|
4301
|
+
console.log(`Milestones: ${milestones.length}`);
|
|
4302
|
+
|
|
4303
|
+
if (!contract.escrow_pda) {
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
const { connection, programId } = getProgram(config, keypair);
|
|
4308
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
4309
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
4310
|
+
if (!escrowState) {
|
|
4311
|
+
console.log("Escrow state: unavailable");
|
|
4312
|
+
return;
|
|
4313
|
+
}
|
|
4314
|
+
console.log("Escrow state:");
|
|
4315
|
+
console.log(` fundedAmount: ${formatUsdc(escrowState.fundedAmount)} USDC`);
|
|
4316
|
+
console.log(` releasedAmount: ${formatUsdc(escrowState.releasedAmount)} USDC`);
|
|
4317
|
+
console.log(` fundingOk: ${escrowState.fundingOk}`);
|
|
4318
|
+
console.log(` readyToClaim: ${escrowState.readyToClaim}`);
|
|
4319
|
+
console.log(` timeoutRefundReady: ${escrowState.timeoutRefundReady}`);
|
|
4320
|
+
console.log(` timeoutFundsReady: ${escrowState.timeoutFundsReady}`);
|
|
4321
|
+
console.log(` disputeOpen: ${escrowState.disputeOpen}`);
|
|
4322
|
+
console.log(` paidOut: ${escrowState.paidOut}`);
|
|
4323
|
+
};
|
|
4324
|
+
|
|
4325
|
+
const runContractStatus = async (config, contract) => {
|
|
4326
|
+
const { keypair, wallet } = getWalletContext(config);
|
|
4327
|
+
const role = getRole(contract, wallet);
|
|
4328
|
+
console.log(`Contract ID: ${contract.id}`);
|
|
4329
|
+
console.log(
|
|
4330
|
+
formatParticipant(
|
|
4331
|
+
"Customer",
|
|
4332
|
+
contract.client_handle,
|
|
4333
|
+
contract.client_wallet,
|
|
4334
|
+
wallet
|
|
4335
|
+
)
|
|
4336
|
+
);
|
|
4337
|
+
console.log(
|
|
4338
|
+
formatParticipant(
|
|
4339
|
+
"Service Provider",
|
|
4340
|
+
contract.contractor_handle,
|
|
4341
|
+
contract.contractor_wallet,
|
|
4342
|
+
wallet
|
|
4343
|
+
)
|
|
4344
|
+
);
|
|
4345
|
+
if (role && role !== "unknown") {
|
|
4346
|
+
console.log(`Role: ${role}`);
|
|
4347
|
+
}
|
|
4348
|
+
console.log(`Current status: ${contract.status}`);
|
|
4349
|
+
console.log(`Execution mode: ${getExecutionMode(contract)}`);
|
|
4350
|
+
if (contract.created_at) {
|
|
4351
|
+
console.log(`Created: ${formatCreatedAt(contract.created_at)}`);
|
|
4352
|
+
}
|
|
4353
|
+
|
|
4354
|
+
let fundedLabel = "unknown";
|
|
4355
|
+
if (contract.escrow_pda) {
|
|
4356
|
+
const { connection, programId } = getProgram(config, keypair);
|
|
4357
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
4358
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
4359
|
+
if (escrowState) {
|
|
4360
|
+
fundedLabel = escrowState.fundedAmount > 0 ? "yes" : "no";
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
console.log(`Is funded: ${fundedLabel}`);
|
|
4364
|
+
};
|
|
4365
|
+
|
|
4366
|
+
const runContractWhoami = async (config, contract) => {
|
|
4367
|
+
const { wallet } = getWalletContext(config);
|
|
4368
|
+
const role = getRole(contract, wallet);
|
|
4369
|
+
renderContractSummary(contract, wallet);
|
|
4370
|
+
console.log(`Role: ${role}`);
|
|
4371
|
+
};
|
|
4372
|
+
|
|
4373
|
+
const runContractNowWhat = async (config, contract) => {
|
|
4374
|
+
const { wallet } = getWalletContext(config);
|
|
4375
|
+
const role = getRole(contract, wallet);
|
|
4376
|
+
if (role !== "client" && role !== "contractor") {
|
|
4377
|
+
console.error("Only contract participants can use this command.");
|
|
4378
|
+
process.exit(1);
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
const status = contract.status;
|
|
4382
|
+
let message = "No next step available.";
|
|
4383
|
+
|
|
4384
|
+
switch (status) {
|
|
4385
|
+
case "waiting_for_init":
|
|
4386
|
+
message = "Initialize the escrow: `nebulon contract <id> init`";
|
|
4387
|
+
break;
|
|
4388
|
+
case "negotiating":
|
|
4389
|
+
if (role === "client") {
|
|
4390
|
+
message =
|
|
4391
|
+
"Set terms and milestones, then sign: `nebulon contract <id> add term ...`, `nebulon contract <id> add milestone ...`, `nebulon contract <id> sign`";
|
|
4392
|
+
} else {
|
|
4393
|
+
message =
|
|
4394
|
+
"Review terms/milestones, then sign: `nebulon contract <id> check terms`, `nebulon contract <id> check milestone list`, `nebulon contract <id> sign`";
|
|
4395
|
+
}
|
|
4396
|
+
break;
|
|
4397
|
+
case "awaiting_signatures":
|
|
4398
|
+
message =
|
|
4399
|
+
"Waiting on the other party to sign. You can check status with `nebulon contract <id> status`.";
|
|
4400
|
+
break;
|
|
4401
|
+
case "waiting_for_funding":
|
|
4402
|
+
if (role === "client") {
|
|
4403
|
+
message = "Fund the contract: `nebulon contract <id> fund`";
|
|
4404
|
+
} else {
|
|
4405
|
+
message =
|
|
4406
|
+
"Waiting for client to fund. You can check status with `nebulon contract <id> status`.";
|
|
4407
|
+
}
|
|
4408
|
+
break;
|
|
4409
|
+
case "waiting_for_milestones_report":
|
|
4410
|
+
if (role === "contractor") {
|
|
4411
|
+
message =
|
|
4412
|
+
"Submit the next milestone: `nebulon contract <id> update milestone <n> ready`";
|
|
4413
|
+
} else {
|
|
4414
|
+
message =
|
|
4415
|
+
"Waiting for contractor to submit a milestone. Use `nebulon contract <id> check milestone list`.";
|
|
4416
|
+
}
|
|
4417
|
+
break;
|
|
4418
|
+
case "in_progress":
|
|
4419
|
+
if (role === "contractor") {
|
|
4420
|
+
message =
|
|
4421
|
+
"Submit the next milestone: `nebulon contract <id> update milestone <n> ready`";
|
|
4422
|
+
} else {
|
|
4423
|
+
message =
|
|
4424
|
+
"Confirm the submitted milestone: `nebulon contract <id> confirm milestone <n>`";
|
|
4425
|
+
}
|
|
4426
|
+
break;
|
|
4427
|
+
case "ready_to_claim":
|
|
4428
|
+
if (role === "contractor") {
|
|
4429
|
+
message = "Claim funds: `nebulon contract <id> claim_funds`";
|
|
4430
|
+
} else {
|
|
4431
|
+
message = "Waiting for contractor to claim funds.";
|
|
4432
|
+
}
|
|
4433
|
+
break;
|
|
4434
|
+
case "completed":
|
|
4435
|
+
message = "Contract completed. No further actions.";
|
|
4436
|
+
break;
|
|
4437
|
+
case "dispute_open":
|
|
4438
|
+
message = "Dispute is open. Await resolution.";
|
|
4439
|
+
break;
|
|
4440
|
+
default:
|
|
4441
|
+
message = `No guidance for status: ${status}`;
|
|
4442
|
+
break;
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
console.log(message);
|
|
4446
|
+
};
|
|
4447
|
+
|
|
4448
|
+
const runRateContract = async (config, contract, scoreValue) => {
|
|
4449
|
+
const { wallet } = getWalletContext(config);
|
|
4450
|
+
const role = getRole(contract, wallet);
|
|
4451
|
+
if (role !== "client" && role !== "contractor") {
|
|
4452
|
+
console.error("Only contract participants can rate a contract.");
|
|
4453
|
+
return;
|
|
4454
|
+
}
|
|
4455
|
+
if (contract.status !== "completed") {
|
|
4456
|
+
console.error("Ratings are only allowed once the contract is completed.");
|
|
4457
|
+
return;
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
const score = Number.parseInt(scoreValue, 10);
|
|
4461
|
+
if (!Number.isInteger(score) || score < 1 || score > 5) {
|
|
4462
|
+
console.error("Usage: nebulon contract <id> rate <1-5>");
|
|
4463
|
+
return;
|
|
4464
|
+
}
|
|
4465
|
+
|
|
4466
|
+
await rateContract(config.backendUrl, config.auth.token, contract.id, score);
|
|
4467
|
+
console.log(`Rating submitted: ${score} star${score === 1 ? "" : "s"}.`);
|
|
4468
|
+
};
|
|
4469
|
+
|
|
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
|
+
const runCheckTerms = async (config, contract) => {
|
|
4511
|
+
const { wallet } = getWalletContext(config);
|
|
4512
|
+
const role = getRole(contract, wallet);
|
|
4513
|
+
if (role !== "client" && role !== "contractor") {
|
|
4514
|
+
console.error("Only contract participants can view terms.");
|
|
4515
|
+
process.exit(1);
|
|
4516
|
+
}
|
|
4517
|
+
if (!isL1Mode(contract) && contract.terms_encrypted && !contract.privacyReady) {
|
|
4518
|
+
console.error("Privacy key exchange pending. Unable to read terms.");
|
|
4519
|
+
process.exit(1);
|
|
4520
|
+
}
|
|
4521
|
+
const deadline = contract.deadline;
|
|
4522
|
+
const payment = contract.total_payment;
|
|
4523
|
+
console.log(`Deadline : ${deadline ? formatDeadlineValue(deadline) : "missing"}`);
|
|
4524
|
+
console.log(
|
|
4525
|
+
`Payment : ${payment ? `${formatUsdc(payment)} USDC` : "missing"}`
|
|
4526
|
+
);
|
|
4527
|
+
};
|
|
4528
|
+
|
|
4529
|
+
const runOpenDispute = async (config, contract, options) => {
|
|
4530
|
+
const { keypair, wallet, walletKey } = getWalletContext(config);
|
|
4531
|
+
if (!contract.escrow_pda) {
|
|
4532
|
+
console.error("Contract has no escrow.");
|
|
4533
|
+
process.exit(1);
|
|
4534
|
+
}
|
|
4535
|
+
if (contract.status !== "in_progress") {
|
|
4536
|
+
console.error("Disputes can only be opened when the contract is in progress.");
|
|
4537
|
+
return;
|
|
4538
|
+
}
|
|
4539
|
+
|
|
4540
|
+
console.log("Verify data and confirm actions please.");
|
|
4541
|
+
renderContractSummary(contract, wallet);
|
|
4542
|
+
console.log("-Action-");
|
|
4543
|
+
console.log("Dispute contract");
|
|
4544
|
+
console.log("");
|
|
4545
|
+
|
|
4546
|
+
const canProceed = await renderBalancesOrAbort(
|
|
4547
|
+
config,
|
|
4548
|
+
walletKey,
|
|
4549
|
+
contract.mint || config.usdcMint
|
|
4550
|
+
);
|
|
4551
|
+
if (!canProceed) {
|
|
4552
|
+
return;
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
|
|
4556
|
+
if (!proceed) {
|
|
4557
|
+
console.log("Canceled.");
|
|
4558
|
+
return;
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
const { program, connection, programId } = getProgram(config, keypair);
|
|
4562
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
4563
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
4564
|
+
if (!escrowState) {
|
|
4565
|
+
console.error("Escrow not found on-chain.");
|
|
4566
|
+
process.exit(1);
|
|
4567
|
+
}
|
|
4568
|
+
const escrowId = escrowState.escrowId;
|
|
4569
|
+
const disputePda = deriveDisputePda(escrowPda, programId);
|
|
4570
|
+
|
|
4571
|
+
console.log("Processing.");
|
|
4572
|
+
try {
|
|
4573
|
+
const sig = await program.methods
|
|
4574
|
+
.openDispute(new anchor.BN(escrowId.toString()))
|
|
4575
|
+
.accounts({
|
|
4576
|
+
actor: walletKey,
|
|
4577
|
+
escrow: escrowPda,
|
|
4578
|
+
dispute: disputePda,
|
|
4579
|
+
systemProgram: SystemProgram.programId,
|
|
4580
|
+
})
|
|
4581
|
+
.signers([keypair])
|
|
4582
|
+
.rpc();
|
|
4583
|
+
console.log(`Dispute opened. (tx: ${sig})`);
|
|
4584
|
+
console.log("Contact @nortbyt3 on Discord to discuss your case.");
|
|
4585
|
+
successMessage("Dispute opened.");
|
|
4586
|
+
} catch (error) {
|
|
4587
|
+
const message = (error && error.message ? error.message : "").toLowerCase();
|
|
4588
|
+
if (message.includes("invalidstate")) {
|
|
4589
|
+
console.error("Unable to open a dispute in the current contract state.");
|
|
4590
|
+
return;
|
|
4591
|
+
}
|
|
4592
|
+
throw error;
|
|
4593
|
+
}
|
|
4594
|
+
};
|
|
4595
|
+
|
|
4596
|
+
const runResolveDispute = async (
|
|
4597
|
+
config,
|
|
4598
|
+
contract,
|
|
4599
|
+
clientPercent,
|
|
4600
|
+
contractorPercent,
|
|
4601
|
+
options
|
|
4602
|
+
) => {
|
|
4603
|
+
const { keypair, wallet, walletKey } = getWalletContext(config);
|
|
4604
|
+
if (!contract.escrow_pda) {
|
|
4605
|
+
console.error("Contract has no escrow.");
|
|
4606
|
+
process.exit(1);
|
|
4607
|
+
}
|
|
4608
|
+
|
|
4609
|
+
console.log("Verify data and confirm actions please.");
|
|
4610
|
+
renderContractSummary(contract, wallet);
|
|
4611
|
+
console.log("-Action-");
|
|
4612
|
+
console.log("Resolve dispute");
|
|
4613
|
+
console.log(`Customer : ${clientPercent}%`);
|
|
4614
|
+
console.log(`Service Provider : ${contractorPercent}%`);
|
|
4615
|
+
console.log("");
|
|
4616
|
+
|
|
4617
|
+
const canProceed = await renderBalancesOrAbort(
|
|
4618
|
+
config,
|
|
4619
|
+
walletKey,
|
|
4620
|
+
contract.mint || config.usdcMint
|
|
4621
|
+
);
|
|
4622
|
+
if (!canProceed) {
|
|
4623
|
+
return;
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4626
|
+
const proceed = await confirmAction(options.confirm, "Proceed? yes/no");
|
|
4627
|
+
if (!proceed) {
|
|
4628
|
+
console.log("Canceled.");
|
|
4629
|
+
return;
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4632
|
+
const { program, connection, programId } = getProgram(config, keypair);
|
|
4633
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
4634
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
4635
|
+
if (!escrowState) {
|
|
4636
|
+
console.error("Escrow not found on-chain.");
|
|
4637
|
+
process.exit(1);
|
|
4638
|
+
}
|
|
4639
|
+
const escrowId = escrowState.escrowId;
|
|
4640
|
+
const disputePda = deriveDisputePda(escrowPda, programId);
|
|
4641
|
+
const mint = new PublicKey(contract.mint || config.usdcMint);
|
|
4642
|
+
|
|
4643
|
+
const clientToken = await ensureAta(
|
|
4644
|
+
connection,
|
|
4645
|
+
keypair,
|
|
4646
|
+
new PublicKey(contract.client_wallet),
|
|
4647
|
+
mint
|
|
4648
|
+
);
|
|
4649
|
+
const contractorToken = await ensureAta(
|
|
4650
|
+
connection,
|
|
4651
|
+
keypair,
|
|
4652
|
+
new PublicKey(contract.contractor_wallet),
|
|
4653
|
+
mint
|
|
4654
|
+
);
|
|
4655
|
+
const vaultToken = await getAssociatedTokenAddress(mint, escrowPda, true);
|
|
4656
|
+
|
|
4657
|
+
console.log("Processing.");
|
|
4658
|
+
try {
|
|
4659
|
+
const sig = await program.methods
|
|
4660
|
+
.resolveDisputeSplit(
|
|
4661
|
+
new anchor.BN(escrowId.toString()),
|
|
4662
|
+
Number(clientPercent),
|
|
4663
|
+
Number(contractorPercent)
|
|
4664
|
+
)
|
|
4665
|
+
.accounts({
|
|
4666
|
+
judge: walletKey,
|
|
4667
|
+
escrow: escrowPda,
|
|
4668
|
+
dispute: disputePda,
|
|
4669
|
+
clientToken,
|
|
4670
|
+
contractorToken,
|
|
4671
|
+
vaultToken,
|
|
4672
|
+
tokenProgram: TOKEN_PROGRAM_ID,
|
|
4673
|
+
})
|
|
4674
|
+
.signers([keypair])
|
|
4675
|
+
.rpc();
|
|
4676
|
+
console.log(`Dispute resolved. (tx: ${sig})`);
|
|
4677
|
+
successMessage("Dispute resolved.");
|
|
4678
|
+
} catch (error) {
|
|
4679
|
+
const message = (error && error.message ? error.message : "").toLowerCase();
|
|
4680
|
+
if (message.includes("invalidstate")) {
|
|
4681
|
+
console.error("Unable to resolve a dispute in the current contract state.");
|
|
4682
|
+
return;
|
|
4683
|
+
}
|
|
4684
|
+
if (message.includes("nodispute")) {
|
|
4685
|
+
console.error("No open dispute to resolve.");
|
|
4686
|
+
return;
|
|
4687
|
+
}
|
|
4688
|
+
throw error;
|
|
4689
|
+
}
|
|
4690
|
+
};
|
|
4691
|
+
|
|
4692
|
+
const runContractCommand = async (args, options = {}) => {
|
|
4693
|
+
const config = loadConfig();
|
|
4694
|
+
await ensureHosted(config);
|
|
4695
|
+
|
|
4696
|
+
if (!args.length) {
|
|
4697
|
+
console.log("Usage: nebulon contract <action>");
|
|
4698
|
+
return;
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
const [head, ...rest] = args;
|
|
4702
|
+
const normalized = head.toLowerCase();
|
|
4703
|
+
|
|
4704
|
+
if (normalized === "create") {
|
|
4705
|
+
await runContractsCreate();
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
if (normalized === "list") {
|
|
4709
|
+
await runContractsShow("all", options);
|
|
4710
|
+
return;
|
|
4711
|
+
}
|
|
4712
|
+
if (normalized === "show") {
|
|
4713
|
+
await runContractsShow(rest[0] || "all", options);
|
|
4714
|
+
return;
|
|
4715
|
+
}
|
|
4716
|
+
if (normalized === "active") {
|
|
4717
|
+
await runContractsShow("active", options);
|
|
4718
|
+
return;
|
|
4719
|
+
}
|
|
4720
|
+
if (normalized === "disputed") {
|
|
4721
|
+
await runContractsShow("disputed", options);
|
|
4722
|
+
return;
|
|
4723
|
+
}
|
|
4724
|
+
if (normalized === "isfunded") {
|
|
4725
|
+
const contract = await resolveContract(config, rest[0] || target);
|
|
4726
|
+
if (!contract) {
|
|
4727
|
+
console.error("Contract not found.");
|
|
4728
|
+
process.exit(1);
|
|
4729
|
+
}
|
|
4730
|
+
const { keypair } = getWalletContext(config);
|
|
4731
|
+
if (!contract.escrow_pda) {
|
|
4732
|
+
console.log("Is funded: no (escrow not initialized)");
|
|
4733
|
+
return;
|
|
4734
|
+
}
|
|
4735
|
+
const { connection, programId } = getProgram(config, keypair);
|
|
4736
|
+
const escrowPda = new PublicKey(contract.escrow_pda);
|
|
4737
|
+
const escrowState = await getEscrowState(connection, programId, escrowPda);
|
|
4738
|
+
if (!escrowState) {
|
|
4739
|
+
console.log("Is funded: unknown");
|
|
4740
|
+
return;
|
|
4741
|
+
}
|
|
4742
|
+
console.log(`Is funded: ${escrowState.fundedAmount > 0 ? "yes" : "no"}`);
|
|
4743
|
+
return;
|
|
4744
|
+
}
|
|
4745
|
+
if (normalized === "status") {
|
|
4746
|
+
const contract = await resolveContract(config, rest[0] || target);
|
|
4747
|
+
if (!contract) {
|
|
4748
|
+
console.error("Contract not found.");
|
|
4749
|
+
process.exit(1);
|
|
4750
|
+
}
|
|
4751
|
+
await runContractStatus(config, contract);
|
|
4752
|
+
return;
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4755
|
+
const target = head;
|
|
4756
|
+
const contract = await resolveContract(config, target);
|
|
4757
|
+
if (!contract) {
|
|
4758
|
+
console.error("Contract not found.");
|
|
4759
|
+
process.exit(1);
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4762
|
+
if (!rest.length) {
|
|
4763
|
+
console.log("Usage: nebulon contract <id|index> <action>");
|
|
4764
|
+
return;
|
|
4765
|
+
}
|
|
4766
|
+
|
|
4767
|
+
const action = rest[0].toLowerCase();
|
|
4768
|
+
if (action === "init") {
|
|
4769
|
+
await runInitContract(config, contract, options);
|
|
4770
|
+
return;
|
|
4771
|
+
}
|
|
4772
|
+
if (action === "details") {
|
|
4773
|
+
await runContractDetails(config, contract);
|
|
4774
|
+
return;
|
|
4775
|
+
}
|
|
4776
|
+
if (action === "status") {
|
|
4777
|
+
await runContractStatus(config, contract);
|
|
4778
|
+
return;
|
|
4779
|
+
}
|
|
4780
|
+
if (action === "add" && rest[1] === "milestone") {
|
|
4781
|
+
const title = rest.slice(2).join(" ").trim();
|
|
4782
|
+
if (!title) {
|
|
4783
|
+
console.error("Milestone description required.");
|
|
4784
|
+
return;
|
|
4785
|
+
}
|
|
4786
|
+
await runAddMilestone(config, contract, title, options);
|
|
4787
|
+
return;
|
|
4788
|
+
}
|
|
4789
|
+
if (action === "milestone" && rest[1] === "list") {
|
|
4790
|
+
await runMilestoneList(config, contract);
|
|
4791
|
+
return;
|
|
4792
|
+
}
|
|
4793
|
+
if (action === "term" && rest[1] === "list") {
|
|
4794
|
+
await runCheckTerms(config, contract);
|
|
4795
|
+
return;
|
|
4796
|
+
}
|
|
4797
|
+
if (action === "milestone" && rest[1] && rest[2] === "status") {
|
|
4798
|
+
await runMilestoneStatus(config, contract, rest[1]);
|
|
4799
|
+
return;
|
|
4800
|
+
}
|
|
4801
|
+
if (action === "whoami") {
|
|
4802
|
+
await runContractWhoami(config, contract);
|
|
4803
|
+
return;
|
|
4804
|
+
}
|
|
4805
|
+
if (action === "nowwhat") {
|
|
4806
|
+
await runContractNowWhat(config, contract);
|
|
4807
|
+
return;
|
|
4808
|
+
}
|
|
4809
|
+
if (action === "rate") {
|
|
4810
|
+
await runRateContract(config, contract, rest[1]);
|
|
4811
|
+
return;
|
|
4812
|
+
}
|
|
4813
|
+
if (action === "mode") {
|
|
4814
|
+
await runContractMode(config, contract, rest[1], options);
|
|
4815
|
+
return;
|
|
4816
|
+
}
|
|
4817
|
+
|
|
4818
|
+
if (action === "disable" && rest[1] === "milestone") {
|
|
4819
|
+
if (!rest[2]) {
|
|
4820
|
+
console.error("Usage: nebulon contract <id> disable milestone <n>");
|
|
4821
|
+
return;
|
|
4822
|
+
}
|
|
4823
|
+
await runDisableMilestone(config, contract, rest[2], options);
|
|
4824
|
+
return;
|
|
4825
|
+
}
|
|
4826
|
+
|
|
4827
|
+
if (action === "add" && rest[1] === "term") {
|
|
4828
|
+
const field = (rest[2] || "").toLowerCase();
|
|
4829
|
+
const value = rest.slice(3).join(" ").trim();
|
|
4830
|
+
if (!field || !value) {
|
|
4831
|
+
console.error(
|
|
4832
|
+
"Usage: nebulon contract <id> add term <deadline|payment> <value>"
|
|
4833
|
+
);
|
|
4834
|
+
return;
|
|
4835
|
+
}
|
|
4836
|
+
await runAddTerm(config, contract, field, value, options);
|
|
4837
|
+
return;
|
|
4838
|
+
}
|
|
4839
|
+
|
|
4840
|
+
if (action === "sign") {
|
|
4841
|
+
await runSignContract(config, contract, options);
|
|
4842
|
+
return;
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4845
|
+
if (action === "fund") {
|
|
4846
|
+
await runFundContract(config, contract, options);
|
|
4847
|
+
return;
|
|
4848
|
+
}
|
|
4849
|
+
|
|
4850
|
+
if (
|
|
4851
|
+
action === "milestone" &&
|
|
4852
|
+
rest[1] &&
|
|
4853
|
+
(rest[2] === "ready" || rest[2] === "setready")
|
|
4854
|
+
) {
|
|
4855
|
+
await runUpdateMilestone(config, contract, rest[1], options);
|
|
4856
|
+
return;
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4859
|
+
if (action === "milestone" && rest[1] && rest[2] === "confirm") {
|
|
4860
|
+
await runConfirmMilestone(config, contract, rest[1], options);
|
|
4861
|
+
return;
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4864
|
+
if (action === "claim_funds") {
|
|
4865
|
+
await runClaimFunds(config, contract, options);
|
|
4866
|
+
return;
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4869
|
+
if (action === "sync") {
|
|
4870
|
+
await runSyncFlags(config, contract, options);
|
|
4871
|
+
return;
|
|
4872
|
+
}
|
|
4873
|
+
|
|
4874
|
+
if (action === "dispute") {
|
|
4875
|
+
await runOpenDispute(config, contract, options);
|
|
4876
|
+
return;
|
|
4877
|
+
}
|
|
4878
|
+
|
|
4879
|
+
if (action === "resolve_dispute") {
|
|
4880
|
+
if (rest.length < 3) {
|
|
4881
|
+
console.error(
|
|
4882
|
+
"Usage: nebulon contract <id> resolve_dispute <customer%> <provider%>"
|
|
4883
|
+
);
|
|
4884
|
+
return;
|
|
4885
|
+
}
|
|
4886
|
+
await runResolveDispute(config, contract, rest[1], rest[2], options);
|
|
4887
|
+
return;
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4890
|
+
console.error("Unknown contract command.");
|
|
4891
|
+
};
|
|
4892
|
+
|
|
4893
|
+
module.exports = {
|
|
4894
|
+
runContractCommand,
|
|
4895
|
+
};
|