arc-builder-kit 0.2.0__py3-none-any.whl

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.
Files changed (58) hide show
  1. arc_builder_kit/__init__.py +4 -0
  2. arc_builder_kit/__main__.py +6 -0
  3. arc_builder_kit/_paths.py +47 -0
  4. arc_builder_kit/cli.py +277 -0
  5. arc_builder_kit/config/arc_testnet.facts.json +31 -0
  6. arc_builder_kit/doctor.py +936 -0
  7. arc_builder_kit/examples/agent-commerce-components/components.js +200 -0
  8. arc_builder_kit/examples/agent-commerce-components/index.html +120 -0
  9. arc_builder_kit/examples/agent-commerce-flows/flows.js +271 -0
  10. arc_builder_kit/examples/agent-commerce-flows/index.html +114 -0
  11. arc_builder_kit/examples/agent-commerce-live/commerce-live.js +190 -0
  12. arc_builder_kit/examples/agent-commerce-live/index.html +105 -0
  13. arc_builder_kit/examples/agent-commerce-review-packet/index.html +96 -0
  14. arc_builder_kit/examples/agent-commerce-review-packet/packet.js +125 -0
  15. arc_builder_kit/examples/agent-identity-profile-preview/identity.js +126 -0
  16. arc_builder_kit/examples/agent-identity-profile-preview/index.html +104 -0
  17. arc_builder_kit/examples/arc-agent-treasury-lab/index.html +152 -0
  18. arc_builder_kit/examples/arc-agent-treasury-lab/treasury.js +532 -0
  19. arc_builder_kit/examples/arc-testnet-operator-evidence/evidence.example.json +47 -0
  20. arc_builder_kit/examples/arc-testnet-wallet-send-gate/index.html +233 -0
  21. arc_builder_kit/examples/arc-testnet-wallet-send-gate/live-infrastructure-policy.example.json +59 -0
  22. arc_builder_kit/examples/arc-testnet-wallet-send-gate/wallet-send-gate.js +472 -0
  23. arc_builder_kit/examples/circle-wallet-integration/index.html +155 -0
  24. arc_builder_kit/examples/circle-wallet-integration/wallet-lab.js +91 -0
  25. arc_builder_kit/examples/job-escrow-simulator/index.html +121 -0
  26. arc_builder_kit/examples/job-escrow-simulator/simulator.js +162 -0
  27. arc_builder_kit/examples/payment-intent-demo/index.html +132 -0
  28. arc_builder_kit/examples/payment-intent-playground/index.html +301 -0
  29. arc_builder_kit/examples/payment-intent-playground/playground.js +835 -0
  30. arc_builder_kit/examples/payment-intent-receipt-matcher/index.html +157 -0
  31. arc_builder_kit/examples/payment-intent-receipt-matcher/matcher.js +877 -0
  32. arc_builder_kit/examples/receipt-verifier-playground/index.html +120 -0
  33. arc_builder_kit/examples/receipt-verifier-playground/verifier.js +226 -0
  34. arc_builder_kit/examples/receipt-viewer/index.html +138 -0
  35. arc_builder_kit/examples/receipt-viewer/receipt-viewer.js +472 -0
  36. arc_builder_kit/examples/transaction-status-playground/index.html +135 -0
  37. arc_builder_kit/examples/transaction-status-playground/status.js +518 -0
  38. arc_builder_kit/examples/x402-local-challenge-server/.env.example +25 -0
  39. arc_builder_kit/examples/x402-local-challenge-server/README.md +111 -0
  40. arc_builder_kit/examples/x402-local-challenge-server/server.py +711 -0
  41. arc_builder_kit/mcp_server.py +463 -0
  42. arc_builder_kit/release_packet.py +469 -0
  43. arc_builder_kit/templates/README.md +25 -0
  44. arc_builder_kit/templates/job-escrow-starter/README.md +25 -0
  45. arc_builder_kit/templates/job-escrow-starter/index.html +41 -0
  46. arc_builder_kit/templates/job-escrow-starter/index.js +14 -0
  47. arc_builder_kit/templates/payment-intent-starter/README.md +25 -0
  48. arc_builder_kit/templates/payment-intent-starter/index.html +42 -0
  49. arc_builder_kit/templates/payment-intent-starter/index.js +7 -0
  50. arc_builder_kit/templates/x402-agent-starter/README.md +29 -0
  51. arc_builder_kit/templates/x402-agent-starter/server.py +201 -0
  52. arc_builder_kit/validate_repo.py +2212 -0
  53. arc_builder_kit-0.2.0.dist-info/METADATA +543 -0
  54. arc_builder_kit-0.2.0.dist-info/RECORD +58 -0
  55. arc_builder_kit-0.2.0.dist-info/WHEEL +5 -0
  56. arc_builder_kit-0.2.0.dist-info/entry_points.txt +3 -0
  57. arc_builder_kit-0.2.0.dist-info/licenses/LICENSE +21 -0
  58. arc_builder_kit-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,835 @@
1
+ const form = document.querySelector('#intent-form');
2
+ const jsonOutput = document.querySelector('#intent-json');
3
+ const statusPill = document.querySelector('#status-pill');
4
+ const statusLog = document.querySelector('#status-log');
5
+ const prepareButton = document.querySelector('#prepare');
6
+ const approveButton = document.querySelector('#approve');
7
+ const submitButton = document.querySelector('#submit');
8
+ const resetButton = document.querySelector('#reset');
9
+ const arcChainId = document.querySelector('#arc-chain-id');
10
+ const arcRpcUrl = document.querySelector('#arc-rpc-url');
11
+ const arcReadonlyState = document.querySelector('#arc-readonly-state');
12
+ const arcSafetyJson = document.querySelector('#arc-safety-json');
13
+ const walletGuardReasons = document.querySelector('#wallet-guard-reasons');
14
+ const walletProviderState = document.querySelector('#wallet-provider-state');
15
+ const walletAddressState = document.querySelector('#wallet-address-state');
16
+ const walletChainState = document.querySelector('#wallet-chain-state');
17
+ const validationSummaryList = document.querySelector('#validation-summary-list');
18
+ const statusStateList = document.querySelector('#status-state-list');
19
+ const signingPreflightReport = document.querySelector('#signing-preflight-report');
20
+ const copyPreflightButton = document.querySelector('#copy-preflight-report');
21
+ const expiryInput = document.querySelector('#expiry');
22
+ const erc20BaseUnits = document.querySelector('#erc20-base-units');
23
+ const erc20Decimals = document.querySelector('#erc20-decimals');
24
+ const nativeGasDecimals = document.querySelector('#native-gas-decimals');
25
+ const finalConfirmationCheckbox = document.querySelector('#final-confirmation-checkbox');
26
+ const finalConfirmationButton = document.querySelector('#final-confirmation-button');
27
+ const finalConfirmationReasons = document.querySelector('#final-confirmation-reasons');
28
+ const unsignedTransactionDraft = document.querySelector('#unsigned-transaction-draft');
29
+ const draftConsistencyList = document.querySelector('#draft-consistency-list');
30
+ const walletHandoffReadinessList = document.querySelector('#wallet-handoff-readiness-list');
31
+ const walletHandoffReadinessJson = document.querySelector('#wallet-handoff-readiness-json');
32
+
33
+ const ARC_TESTNET_STATUS = Object.freeze({
34
+ network: 'Arc Testnet',
35
+ expectedChainIdDecimal: 5042002,
36
+ expectedChainIdHex: '0x4cef52',
37
+ rpcUrl: 'https://rpc.testnet.arc.network',
38
+ explorerUrl: 'https://testnet.arcscan.app',
39
+ erc20UsdcAddress: '0x3600000000000000000000000000000000000000',
40
+ erc20UsdcDecimals: 6,
41
+ nativeGasAsset: 'USDC',
42
+ nativeGasDecimals: 18,
43
+ statusSource: 'static Arc docs constants + read-only helper baseline',
44
+ walletConnected: false,
45
+ backendCalls: false,
46
+ transactionBroadcast: false,
47
+ signingRequiresWalletChainGateAndHumanApproval: true,
48
+ });
49
+
50
+ const STATUS_STATES = Object.freeze([
51
+ {
52
+ id: 'draft',
53
+ label: 'Draft',
54
+ description: 'Intent exists but is not approved.',
55
+ },
56
+ {
57
+ id: 'ready_for_review',
58
+ label: 'Ready for review',
59
+ description: 'Fields are valid enough for human review.',
60
+ },
61
+ {
62
+ id: 'approved_local',
63
+ label: 'Approved locally',
64
+ description: 'Human approved the local exercise only.',
65
+ },
66
+ {
67
+ id: 'final_review_confirmed',
68
+ label: 'Final review confirmed',
69
+ description: 'Final local confirmation is recorded without a wallet request.',
70
+ },
71
+ {
72
+ id: 'blocked_wallet_unavailable',
73
+ label: 'Wallet blocked',
74
+ description: 'Signing remains disabled by guardrails.',
75
+ },
76
+ ]);
77
+
78
+ const initialEvents = [
79
+ ['draft', 'Playground loaded with local-only defaults.'],
80
+ ];
81
+
82
+ let currentStatus = 'draft';
83
+ let events = [...initialEvents];
84
+ let frozenIntentSnapshot = null;
85
+ let finalConfirmationRecorded = false;
86
+
87
+ function setDefaultExpiry() {
88
+ const future = new Date(Date.now() + (24 * 60 * 60 * 1000));
89
+ const local = new Date(future.getTime() - (future.getTimezoneOffset() * 60 * 1000));
90
+ expiryInput.value = local.toISOString().slice(0, 16);
91
+ }
92
+
93
+ function readIntent() {
94
+ const data = new FormData(form);
95
+ const intent = {
96
+ agent: String(data.get('agent') || '').trim(),
97
+ recipient: String(data.get('recipient') || '').trim(),
98
+ asset: String(data.get('asset') || 'USDC').trim(),
99
+ amount: String(data.get('amount') || '').trim(),
100
+ memo: String(data.get('memo') || '').trim(),
101
+ expiry: String(data.get('expiry') || '').trim(),
102
+ status: currentStatus,
103
+ networkReadiness: {
104
+ chainId: ARC_TESTNET_STATUS.expectedChainIdDecimal,
105
+ chainIdHex: ARC_TESTNET_STATUS.expectedChainIdHex,
106
+ rpcUrl: ARC_TESTNET_STATUS.rpcUrl,
107
+ explorerUrl: ARC_TESTNET_STATUS.explorerUrl,
108
+ assetAddress: ARC_TESTNET_STATUS.erc20UsdcAddress,
109
+ assetDecimals: ARC_TESTNET_STATUS.erc20UsdcDecimals,
110
+ nativeGasDecimals: ARC_TESTNET_STATUS.nativeGasDecimals,
111
+ statusSource: ARC_TESTNET_STATUS.statusSource,
112
+ },
113
+ safety: {
114
+ walletConnected: false,
115
+ backendCalls: false,
116
+ autonomousSpending: false,
117
+ humanApprovalRequired: true,
118
+ walletPreviewOnly: true,
119
+ walletAdapterFeatureFlag: false,
120
+ },
121
+ };
122
+
123
+ return {
124
+ ...intent,
125
+ unitPreview: buildUnitPreview(intent),
126
+ };
127
+ }
128
+
129
+ function appendEvent(status, message) {
130
+ currentStatus = status;
131
+ logEvent(status, message);
132
+ }
133
+
134
+ function logEvent(status, message) {
135
+ events = [[status, message], ...events].slice(0, 8);
136
+ render();
137
+ }
138
+
139
+ function renderArcStatusPanel() {
140
+ arcChainId.textContent = `${ARC_TESTNET_STATUS.expectedChainIdDecimal} (${ARC_TESTNET_STATUS.expectedChainIdHex})`;
141
+ arcRpcUrl.textContent = ARC_TESTNET_STATUS.rpcUrl;
142
+ arcReadonlyState.textContent = ARC_TESTNET_STATUS.transactionBroadcast
143
+ ? 'Broadcast enabled'
144
+ : 'Read-only / no broadcast';
145
+ arcSafetyJson.textContent = JSON.stringify(ARC_TESTNET_STATUS, null, 2);
146
+ }
147
+
148
+ function hasValidRecipient(recipient) {
149
+ const normalized = String(recipient || '').trim().toLowerCase();
150
+ return /^0x[a-f0-9]{40}$/.test(normalized)
151
+ && !/^0x0{40}$/.test(normalized)
152
+ && normalized !== ARC_TESTNET_STATUS.erc20UsdcAddress.toLowerCase();
153
+ }
154
+
155
+ function hasValidUsdcAmount(amount) {
156
+ return /^(?:0|[1-9]\d*)(?:\.\d{1,6})?$/.test(amount) && Number(amount) > 0;
157
+ }
158
+
159
+ function formatUsdcBaseUnits(amount) {
160
+ const normalizedAmount = String(amount || '').trim();
161
+ if (!hasValidUsdcAmount(normalizedAmount)) return 'invalid';
162
+
163
+ const [whole, fraction = ''] = normalizedAmount.split('.');
164
+ const paddedFraction = fraction.padEnd(ARC_TESTNET_STATUS.erc20UsdcDecimals, '0');
165
+ const scale = 10n ** BigInt(ARC_TESTNET_STATUS.erc20UsdcDecimals);
166
+ return String((BigInt(whole) * scale) + BigInt(paddedFraction));
167
+ }
168
+
169
+ function buildUnitPreview(intent) {
170
+ return {
171
+ baseUnits: formatUsdcBaseUnits(intent.amount),
172
+ erc20Decimals: ARC_TESTNET_STATUS.erc20UsdcDecimals,
173
+ nativeGasDecimals: ARC_TESTNET_STATUS.nativeGasDecimals,
174
+ warning: 'Do not use native gas decimals for ERC-20 USDC transfer amounts.',
175
+ localOnly: true,
176
+ };
177
+ }
178
+
179
+ function toPaddedHexFromDecimalString(decimalString) {
180
+ if (!/^(?:0|[1-9]\d*)$/.test(decimalString)) return null;
181
+ return BigInt(decimalString).toString(16).padStart(64, '0');
182
+ }
183
+
184
+ function buildErc20TransferCalldata(intent) {
185
+ if (!hasValidRecipient(intent.recipient) || !hasValidUsdcAmount(intent.amount)) return null;
186
+ const baseUnits = formatUsdcBaseUnits(intent.amount);
187
+ const paddedRecipient = intent.recipient.toLowerCase().replace(/^0x/, '').padStart(64, '0');
188
+ const paddedAmount = toPaddedHexFromDecimalString(baseUnits);
189
+ if (!paddedAmount) return null;
190
+ return `0xa9059cbb${paddedRecipient}${paddedAmount}`;
191
+ }
192
+
193
+ function buildUnsignedTransactionDraft(intent) {
194
+ const baseUnits = formatUsdcBaseUnits(intent.amount);
195
+ const isSupportedAsset = intent.asset === 'USDC';
196
+ const calldata = isSupportedAsset ? buildErc20TransferCalldata(intent) : null;
197
+ const readyForDraft = Boolean(calldata) && hasFutureExpiry(intent.expiry);
198
+
199
+ return {
200
+ type: 'unsigned_erc20_transfer_preview',
201
+ status: readyForDraft ? 'draft_ready_for_review' : 'blocked',
202
+ localOnly: true,
203
+ unsignedOnly: true,
204
+ walletRequestEnabled: false,
205
+ gasEstimateIncluded: false,
206
+ simulationIncluded: false,
207
+ chainId: ARC_TESTNET_STATUS.expectedChainIdDecimal,
208
+ chainIdHex: ARC_TESTNET_STATUS.expectedChainIdHex,
209
+ to: ARC_TESTNET_STATUS.erc20UsdcAddress,
210
+ value: '0x0',
211
+ data: calldata,
212
+ decoded: {
213
+ method: 'transfer(address,uint256)',
214
+ recipient: hasValidRecipient(intent.recipient) ? intent.recipient : null,
215
+ amountDecimal: hasValidUsdcAmount(intent.amount) ? intent.amount : null,
216
+ amountBaseUnits: baseUnits === 'invalid' ? null : baseUnits,
217
+ asset: intent.asset,
218
+ assetSupported: isSupportedAsset,
219
+ assetDecimals: ARC_TESTNET_STATUS.erc20UsdcDecimals,
220
+ },
221
+ blockers: [
222
+ ...(!isSupportedAsset ? ['Only USDC is supported for the first transaction draft.'] : []),
223
+ ...(!hasValidRecipient(intent.recipient) ? ['Recipient must be a non-zero 0x-prefixed address and cannot be the USDC token contract.'] : []),
224
+ ...(!hasValidUsdcAmount(intent.amount) ? ['Amount must be positive with at most 6 decimals.'] : []),
225
+ ...(!hasFutureExpiry(intent.expiry) ? ['Expiry must be in the future.'] : []),
226
+ 'Draft is not a wallet request and cannot move funds.',
227
+ ],
228
+ };
229
+ }
230
+
231
+ function decodeErc20TransferCalldata(data) {
232
+ if (typeof data !== 'string' || !/^0xa9059cbb[0-9a-fA-F]{128}$/.test(data)) return null;
233
+ const recipientWord = data.slice(10, 74);
234
+ const amountWord = data.slice(74, 138);
235
+ const recipient = `0x${recipientWord.slice(24)}`.toLowerCase();
236
+ const amountBaseUnits = BigInt(`0x${amountWord}`).toString(10);
237
+ return {
238
+ method: 'transfer(address,uint256)',
239
+ recipient,
240
+ amountBaseUnits,
241
+ };
242
+ }
243
+
244
+ function buildTransactionDraftConsistencyCheck(intent) {
245
+ const draft = buildUnsignedTransactionDraft(intent);
246
+ const decodedCalldata = decodeErc20TransferCalldata(draft.data);
247
+ const expectedBaseUnits = formatUsdcBaseUnits(intent.amount);
248
+ const expectedRecipient = hasValidRecipient(intent.recipient) ? intent.recipient.toLowerCase() : null;
249
+ const checks = [
250
+ {
251
+ id: 'unsigned-only',
252
+ label: 'Unsigned-only guard',
253
+ passed: draft.unsignedOnly === true && draft.walletRequestEnabled === false,
254
+ detail: 'Draft cannot open a wallet request by itself.',
255
+ },
256
+ {
257
+ id: 'token-target',
258
+ label: 'Token target',
259
+ passed: draft.to === ARC_TESTNET_STATUS.erc20UsdcAddress,
260
+ detail: 'Transaction target remains the reviewed Arc Testnet USDC token address.',
261
+ },
262
+ {
263
+ id: 'native-value',
264
+ label: 'Native value',
265
+ passed: draft.value === '0x0',
266
+ detail: 'ERC-20 transfer uses zero native value.',
267
+ },
268
+ {
269
+ id: 'chain-id',
270
+ label: 'Arc Testnet chain',
271
+ passed: draft.chainId === ARC_TESTNET_STATUS.expectedChainIdDecimal && draft.chainIdHex === ARC_TESTNET_STATUS.expectedChainIdHex,
272
+ detail: 'Draft chain ID stays pinned to Arc Testnet constants.',
273
+ },
274
+ {
275
+ id: 'calldata-decodes',
276
+ label: 'Calldata decodes',
277
+ passed: Boolean(decodedCalldata),
278
+ detail: 'Data must decode as ERC-20 transfer(address,uint256).',
279
+ },
280
+ {
281
+ id: 'recipient-match',
282
+ label: 'Recipient match',
283
+ passed: Boolean(decodedCalldata) && decodedCalldata.recipient === expectedRecipient,
284
+ detail: 'Decoded calldata recipient must match the current intent recipient.',
285
+ },
286
+ {
287
+ id: 'amount-match',
288
+ label: 'Amount match',
289
+ passed: Boolean(decodedCalldata) && expectedBaseUnits !== 'invalid' && decodedCalldata.amountBaseUnits === expectedBaseUnits,
290
+ detail: 'Decoded calldata amount must match the 6-decimal USDC base units.',
291
+ },
292
+ ];
293
+
294
+ return {
295
+ type: 'local_unsigned_transaction_consistency_check',
296
+ localOnly: true,
297
+ walletRequestEnabled: false,
298
+ decodedCalldata,
299
+ allPassed: checks.every((check) => check.passed),
300
+ checks,
301
+ };
302
+ }
303
+
304
+ function renderUnitPreview(intent) {
305
+ const preview = buildUnitPreview(intent);
306
+ erc20BaseUnits.textContent = preview.baseUnits;
307
+ erc20Decimals.textContent = String(preview.erc20Decimals);
308
+ nativeGasDecimals.textContent = String(preview.nativeGasDecimals);
309
+ }
310
+
311
+ function renderUnsignedTransactionDraft(intent) {
312
+ unsignedTransactionDraft.textContent = JSON.stringify(buildUnsignedTransactionDraft(intent), null, 2);
313
+ }
314
+
315
+ function renderTransactionDraftConsistencyCheck(intent) {
316
+ const consistencyCheck = buildTransactionDraftConsistencyCheck(intent);
317
+ draftConsistencyList.replaceChildren(
318
+ ...consistencyCheck.checks.map((check) => {
319
+ const item = document.createElement('li');
320
+ const strong = document.createElement('strong');
321
+ strong.textContent = `${check.passed ? 'PASS' : 'BLOCK'} · ${check.label}`;
322
+ item.append(strong, document.createTextNode(` — ${check.detail}`));
323
+ return item;
324
+ })
325
+ );
326
+ }
327
+
328
+ function buildWalletHandoffReadinessManifest(intent) {
329
+ const validationSummary = buildValidationSummary(intent);
330
+ const transactionDraftConsistency = buildTransactionDraftConsistencyCheck(intent);
331
+ const walletPreview = getWalletPreviewState(intent);
332
+ const frozenIntentPassed = Boolean(frozenIntentSnapshot) && !hasFrozenIntentChanged(intent);
333
+ const humanApprovalPassed = hasHumanApprovalMarker();
334
+ const checks = [
335
+ {
336
+ id: 'valid-intent-fields',
337
+ label: 'Intent fields are locally valid',
338
+ passed: validationSummary.every((check) => check.passed),
339
+ detail: 'Recipient, amount, expiry, and local approval prerequisites must be reviewable.',
340
+ },
341
+ {
342
+ id: 'frozen-intent-present',
343
+ label: 'Frozen intent snapshot is unchanged',
344
+ passed: frozenIntentPassed,
345
+ detail: 'Future wallet request must be generated from the same frozen fields reviewers saw.',
346
+ },
347
+ {
348
+ id: 'human-approval-recorded',
349
+ label: 'Human approval marker is present',
350
+ passed: humanApprovalPassed,
351
+ detail: 'Local approval is required but still is not wallet consent.',
352
+ },
353
+ {
354
+ id: 'final-confirmation-recorded',
355
+ label: 'Final local confirmation is recorded',
356
+ passed: finalConfirmationRecorded,
357
+ detail: 'A future send PR must require a fresh final review before opening a wallet prompt.',
358
+ },
359
+ {
360
+ id: 'unsigned-draft-consistent',
361
+ label: 'Unsigned transaction draft is consistent',
362
+ passed: transactionDraftConsistency.allPassed,
363
+ detail: 'Calldata, token target, chain, and native value must match the reviewed intent.',
364
+ },
365
+ {
366
+ id: 'wallet-chain-observed',
367
+ label: 'Wallet chain observed as Arc Testnet',
368
+ passed: walletPreview.chainMatches,
369
+ detail: 'This playground does not request accounts or switch chains; a future wallet PR must prove this live.',
370
+ },
371
+ {
372
+ id: 'wallet-request-still-disabled',
373
+ label: 'Wallet request remains disabled here',
374
+ passed: walletPreview.walletActionEnabled === false,
375
+ detail: 'This manifest cannot enable wallet send calls, signing, simulation, or broadcast.'
376
+ },
377
+ ];
378
+
379
+ return {
380
+ type: 'wallet_handoff_readiness_manifest',
381
+ localOnly: true,
382
+ walletRequestEnabled: false,
383
+ canRequestWallet: false,
384
+ sendPrRequired: true,
385
+ requiredBeforeSend: checks.map((check) => check.id),
386
+ allLocalPrerequisitesPassed: checks.every((check) => check.passed),
387
+ checks,
388
+ };
389
+ }
390
+
391
+ function renderWalletHandoffReadinessManifest(intent) {
392
+ const manifest = buildWalletHandoffReadinessManifest(intent);
393
+ walletHandoffReadinessList.replaceChildren(
394
+ ...manifest.checks.map((check) => {
395
+ const item = document.createElement('li');
396
+ const strong = document.createElement('strong');
397
+ strong.textContent = `${check.passed ? 'PASS' : 'BLOCK'} · ${check.label}`;
398
+ item.append(strong, document.createTextNode(` — ${check.detail}`));
399
+ return item;
400
+ })
401
+ );
402
+ walletHandoffReadinessJson.textContent = JSON.stringify({
403
+ walletRequestEnabled: manifest.walletRequestEnabled,
404
+ canRequestWallet: manifest.canRequestWallet,
405
+ sendPrRequired: manifest.sendPrRequired,
406
+ allLocalPrerequisitesPassed: manifest.allLocalPrerequisitesPassed,
407
+ requiredBeforeSend: manifest.requiredBeforeSend,
408
+ }, null, 2);
409
+ }
410
+
411
+ function hasFutureExpiry(expiry) {
412
+ if (!expiry) return false;
413
+ const expiryTime = new Date(expiry).getTime();
414
+ return Number.isFinite(expiryTime) && expiryTime > Date.now();
415
+ }
416
+
417
+ function normalizeIntentForFreeze(intent) {
418
+ return JSON.stringify({
419
+ recipient: intent.recipient,
420
+ asset: intent.asset,
421
+ amount: intent.amount,
422
+ memo: intent.memo,
423
+ expiry: intent.expiry,
424
+ chainId: ARC_TESTNET_STATUS.expectedChainIdDecimal,
425
+ assetAddress: ARC_TESTNET_STATUS.erc20UsdcAddress,
426
+ assetDecimals: ARC_TESTNET_STATUS.erc20UsdcDecimals,
427
+ baseUnits: formatUsdcBaseUnits(intent.amount),
428
+ });
429
+ }
430
+
431
+ function freezeIntentForReview(intent) {
432
+ frozenIntentSnapshot = {
433
+ frozenAt: new Date().toISOString(),
434
+ normalizedIntent: normalizeIntentForFreeze(intent),
435
+ fields: {
436
+ recipient: intent.recipient,
437
+ asset: intent.asset,
438
+ amount: intent.amount,
439
+ memo: intent.memo,
440
+ expiry: intent.expiry,
441
+ chainId: ARC_TESTNET_STATUS.expectedChainIdDecimal,
442
+ chainIdHex: ARC_TESTNET_STATUS.expectedChainIdHex,
443
+ assetAddress: ARC_TESTNET_STATUS.erc20UsdcAddress,
444
+ assetDecimals: ARC_TESTNET_STATUS.erc20UsdcDecimals,
445
+ baseUnits: formatUsdcBaseUnits(intent.amount),
446
+ },
447
+ };
448
+ }
449
+
450
+ function hasFrozenIntentChanged(intent) {
451
+ if (!frozenIntentSnapshot) return false;
452
+ return frozenIntentSnapshot.normalizedIntent !== normalizeIntentForFreeze(intent);
453
+ }
454
+
455
+ function getInjectedWalletProvider() {
456
+ return globalThis.ethereum || null;
457
+ }
458
+
459
+ function getWalletPreviewState(intent) {
460
+ const provider = getInjectedWalletProvider();
461
+ const chainId = provider && typeof provider.chainId === 'string' ? provider.chainId.toLowerCase() : null;
462
+ const selectedAddress = provider && typeof provider.selectedAddress === 'string' ? provider.selectedAddress : '';
463
+ const chainMatches = chainId === ARC_TESTNET_STATUS.expectedChainIdHex;
464
+ return {
465
+ mode: 'read-only wallet preview',
466
+ providerDetected: Boolean(provider),
467
+ requestMethodsCalled: false,
468
+ selectedAddress: selectedAddress || null,
469
+ connectedAddressKnown: hasValidRecipient(selectedAddress),
470
+ observedChainIdHex: chainId,
471
+ expectedChainIdHex: ARC_TESTNET_STATUS.expectedChainIdHex,
472
+ expectedChainIdDecimal: ARC_TESTNET_STATUS.expectedChainIdDecimal,
473
+ chainMatches,
474
+ frozenIntentPresent: Boolean(frozenIntentSnapshot),
475
+ frozenIntentChanged: hasFrozenIntentChanged(intent),
476
+ walletActionEnabled: false,
477
+ };
478
+ }
479
+
480
+ function getWalletGuardReasons(intent) {
481
+ const reasons = [
482
+ 'Wrong chain: expected Arc Testnet chain ID 5042002 (0x4cef52).',
483
+ 'RPC unavailable: no live browser RPC probe is enabled in this local-only demo.',
484
+ 'Unverified docs/constants: re-check Arc MCP/docs before any signing PR.',
485
+ 'User approval required: real signing must open an external wallet confirmation.',
486
+ 'Wallet adapter feature flag is off: this UI only previews provider/address/chain state.',
487
+ ];
488
+
489
+ if (!hasValidRecipient(intent.recipient)) {
490
+ reasons.push('Missing recipient: enter a 0x-prefixed Arc Testnet recipient before review.');
491
+ }
492
+ if (!hasValidUsdcAmount(intent.amount)) {
493
+ reasons.push('Invalid amount or decimals: use a positive USDC amount with at most 6 decimal places.');
494
+ }
495
+ if (!hasFutureExpiry(intent.expiry)) {
496
+ reasons.push('Expired intent: choose a future expiry before enabling wallet review.');
497
+ }
498
+ const walletPreview = getWalletPreviewState(intent);
499
+ if (!walletPreview.providerDetected) {
500
+ reasons.push('Wallet provider not detected: no injected browser wallet was observed.');
501
+ } else if (!walletPreview.connectedAddressKnown) {
502
+ reasons.push('Wallet account unknown: this guard does not request accounts or permissions.');
503
+ }
504
+ if (walletPreview.observedChainIdHex && !walletPreview.chainMatches) {
505
+ reasons.push(`Wrong wallet chain observed: expected ${ARC_TESTNET_STATUS.expectedChainIdHex}, saw ${walletPreview.observedChainIdHex}.`);
506
+ }
507
+ if (walletPreview.frozenIntentChanged) {
508
+ reasons.push('Frozen intent changed: restart review before any future wallet action.');
509
+ }
510
+
511
+ return reasons;
512
+ }
513
+
514
+ function renderWalletGuardPanel(intent) {
515
+ const walletPreview = getWalletPreviewState(intent);
516
+ walletProviderState.textContent = walletPreview.providerDetected ? 'Detected / read-only' : 'Not detected';
517
+ walletAddressState.textContent = walletPreview.selectedAddress || 'Not requested';
518
+ walletChainState.textContent = walletPreview.chainMatches ? 'Arc Testnet observed / still disabled' : 'Blocked';
519
+ const reasons = getWalletGuardReasons(intent);
520
+ walletGuardReasons.replaceChildren(
521
+ ...reasons.map((reason) => {
522
+ const item = document.createElement('li');
523
+ item.textContent = reason;
524
+ return item;
525
+ })
526
+ );
527
+ }
528
+
529
+ function isIntentReadyForReview(intent) {
530
+ return hasValidRecipient(intent.recipient)
531
+ && hasValidUsdcAmount(intent.amount)
532
+ && hasFutureExpiry(intent.expiry);
533
+ }
534
+
535
+ function nextStatusAfterPrepare(intent) {
536
+ return isIntentReadyForReview(intent) ? 'ready_for_review' : 'draft';
537
+ }
538
+
539
+ function markStatusStep(currentStatusId) {
540
+ const knownStatuses = new Set(STATUS_STATES.map((status) => status.id));
541
+ const safeStatusId = knownStatuses.has(currentStatusId) ? currentStatusId : 'draft';
542
+
543
+ statusStateList.querySelectorAll('[data-status-step]').forEach((item) => {
544
+ const isActive = item.dataset.statusStep === safeStatusId;
545
+ item.classList.toggle('active', isActive);
546
+ item.setAttribute('aria-current', isActive ? 'step' : 'false');
547
+ });
548
+ }
549
+
550
+ function hasHumanApprovalMarker() {
551
+ return currentStatus === 'approved_local' || currentStatus === 'final_review_confirmed';
552
+ }
553
+
554
+ function buildValidationSummary(intent) {
555
+ const humanApprovalMarked = hasHumanApprovalMarker();
556
+ return [
557
+ {
558
+ id: 'recipient',
559
+ label: 'Recipient format',
560
+ passed: hasValidRecipient(intent.recipient),
561
+ detail: hasValidRecipient(intent.recipient)
562
+ ? '0x recipient shape is valid.'
563
+ : 'Use a 0x-prefixed address with 40 hex characters.',
564
+ },
565
+ {
566
+ id: 'amount',
567
+ label: 'USDC amount',
568
+ passed: hasValidUsdcAmount(intent.amount),
569
+ detail: hasValidUsdcAmount(intent.amount)
570
+ ? 'Positive amount with at most 6 decimals.'
571
+ : 'Use a positive USDC amount with at most 6 decimals.',
572
+ },
573
+ {
574
+ id: 'expiry',
575
+ label: 'Future expiry',
576
+ passed: hasFutureExpiry(intent.expiry),
577
+ detail: hasFutureExpiry(intent.expiry)
578
+ ? 'Expiry is still in the future.'
579
+ : 'Choose a future expiry before wallet review.',
580
+ },
581
+ {
582
+ id: 'approval',
583
+ label: 'Human approval marker',
584
+ passed: humanApprovalMarked,
585
+ detail: humanApprovalMarked
586
+ ? 'Local approval marker is present.'
587
+ : 'Click Approve manually after reviewing the intent.',
588
+ },
589
+ ];
590
+ }
591
+
592
+ function renderValidationSummary(intent) {
593
+ validationSummaryList.replaceChildren(
594
+ ...buildValidationSummary(intent).map((check) => {
595
+ const item = document.createElement('li');
596
+ const strong = document.createElement('strong');
597
+ strong.textContent = `${check.passed ? 'PASS' : 'BLOCK'} · ${check.label}`;
598
+ item.append(strong, document.createTextNode(` — ${check.detail}`));
599
+ return item;
600
+ })
601
+ );
602
+ }
603
+
604
+ function getFinalConfirmationReasons(intent) {
605
+ const walletPreview = getWalletPreviewState(intent);
606
+ const reasons = [];
607
+
608
+ if (!isIntentReadyForReview(intent)) {
609
+ reasons.push('Intent is not review-ready: fix recipient, amount, and expiry first.');
610
+ }
611
+ if (!frozenIntentSnapshot) {
612
+ reasons.push('Frozen intent missing: prepare the intent before final confirmation.');
613
+ }
614
+ if (hasFrozenIntentChanged(intent)) {
615
+ reasons.push('Frozen intent changed: restart review before final confirmation.');
616
+ }
617
+ if (currentStatus !== 'approved_local' && currentStatus !== 'final_review_confirmed') {
618
+ reasons.push('Local approval missing: click Approve manually before final confirmation.');
619
+ }
620
+ if (!finalConfirmationCheckbox.checked) {
621
+ reasons.push('Final review checkbox is not checked.');
622
+ }
623
+ if (!walletPreview.chainMatches) {
624
+ reasons.push('Arc Testnet chain is not observed; this remains a no-transaction confirmation.');
625
+ }
626
+ reasons.push('Transaction request remains disabled until a separate reviewed testnet send PR.');
627
+
628
+ return reasons;
629
+ }
630
+
631
+ function canRecordFinalConfirmation(intent) {
632
+ return isIntentReadyForReview(intent)
633
+ && Boolean(frozenIntentSnapshot)
634
+ && !hasFrozenIntentChanged(intent)
635
+ && currentStatus === 'approved_local'
636
+ && finalConfirmationCheckbox.checked;
637
+ }
638
+
639
+ function renderFinalConfirmationPanel(intent) {
640
+ const canConfirm = canRecordFinalConfirmation(intent);
641
+ finalConfirmationButton.disabled = !canConfirm;
642
+ finalConfirmationButton.setAttribute('aria-disabled', canConfirm ? 'false' : 'true');
643
+ finalConfirmationReasons.replaceChildren(
644
+ ...getFinalConfirmationReasons(intent).map((reason) => {
645
+ const item = document.createElement('li');
646
+ item.textContent = reason;
647
+ return item;
648
+ })
649
+ );
650
+ }
651
+
652
+ function buildSigningPreflightReport(intent) {
653
+ return {
654
+ walletAction: 'blocked',
655
+ nextRequiredReview: 'separate testnet-only wallet PR',
656
+ generatedFrom: 'browser-local intent state only',
657
+ guardReasons: getWalletGuardReasons(intent),
658
+ unitPreview: buildUnitPreview(intent),
659
+ unsignedTransactionDraft: buildUnsignedTransactionDraft(intent),
660
+ transactionDraftConsistency: buildTransactionDraftConsistencyCheck(intent),
661
+ walletHandoffReadiness: buildWalletHandoffReadinessManifest(intent),
662
+ validationSummary: buildValidationSummary(intent),
663
+ walletPreview: getWalletPreviewState(intent),
664
+ frozenIntent: frozenIntentSnapshot ? frozenIntentSnapshot.fields : null,
665
+ finalConfirmation: {
666
+ recorded: finalConfirmationRecorded,
667
+ checkboxChecked: finalConfirmationCheckbox.checked,
668
+ canConfirmLocally: canRecordFinalConfirmation(intent),
669
+ reasons: getFinalConfirmationReasons(intent),
670
+ transactionRequestEnabled: false,
671
+ },
672
+ checks: {
673
+ chainGate: {
674
+ expectedChainId: ARC_TESTNET_STATUS.expectedChainIdDecimal,
675
+ expectedChainIdHex: ARC_TESTNET_STATUS.expectedChainIdHex,
676
+ passed: false,
677
+ note: 'No live wallet chain check is performed in this playground.',
678
+ },
679
+ recipientFormat: {
680
+ passed: hasValidRecipient(intent.recipient),
681
+ value: intent.recipient || null,
682
+ },
683
+ amountFormat: {
684
+ passed: hasValidUsdcAmount(intent.amount),
685
+ value: intent.amount || null,
686
+ decimals: ARC_TESTNET_STATUS.erc20UsdcDecimals,
687
+ },
688
+ expiryWindow: {
689
+ passed: hasFutureExpiry(intent.expiry),
690
+ value: intent.expiry || null,
691
+ },
692
+ frozenIntent: {
693
+ passed: Boolean(frozenIntentSnapshot) && !hasFrozenIntentChanged(intent),
694
+ present: Boolean(frozenIntentSnapshot),
695
+ changedAfterFreeze: hasFrozenIntentChanged(intent),
696
+ note: 'Future wallet PRs must sign exactly the frozen reviewed fields.',
697
+ },
698
+ humanApproval: {
699
+ passed: hasHumanApprovalMarker(),
700
+ required: true,
701
+ note: 'Local approval is only a review marker, not wallet consent.',
702
+ },
703
+ transactionDraftConsistency: {
704
+ passed: buildTransactionDraftConsistencyCheck(intent).allPassed,
705
+ required: true,
706
+ note: 'Unsigned draft must decode back to reviewed intent fields before wallet handoff.',
707
+ },
708
+ walletHandoffReadiness: {
709
+ passed: buildWalletHandoffReadinessManifest(intent).allLocalPrerequisitesPassed,
710
+ required: true,
711
+ note: 'Future send PR remains blocked until every handoff-readiness guard is satisfied.',
712
+ },
713
+ finalConfirmation: {
714
+ passed: finalConfirmationRecorded,
715
+ required: true,
716
+ note: 'Final confirmation is local UX only; it never enables a transaction request.',
717
+ },
718
+ },
719
+ };
720
+ }
721
+
722
+ function renderSigningPreflightReport(intent) {
723
+ signingPreflightReport.textContent = serializeSigningPreflightReport(intent);
724
+ }
725
+
726
+ function serializeSigningPreflightReport(intent) {
727
+ return JSON.stringify(buildSigningPreflightReport(intent), null, 2);
728
+ }
729
+
730
+ async function copySigningPreflightReport() {
731
+ const intent = readIntent();
732
+ const reportText = serializeSigningPreflightReport(intent);
733
+
734
+ if (!navigator.clipboard || !navigator.clipboard.writeText) {
735
+ signingPreflightReport.focus();
736
+ logEvent('copy_unavailable', 'Clipboard copy was unavailable; select and copy the report manually.');
737
+ return;
738
+ }
739
+
740
+ try {
741
+ await navigator.clipboard.writeText(reportText);
742
+ logEvent('copied_preflight_report', 'Signing preflight report was copied locally. No wallet or network call was made.');
743
+ } catch (error) {
744
+ signingPreflightReport.focus();
745
+ logEvent('copy_unavailable', 'Clipboard copy was unavailable; select and copy the report manually.');
746
+ }
747
+ }
748
+
749
+ function render() {
750
+ const intent = readIntent();
751
+ jsonOutput.textContent = JSON.stringify(intent, null, 2);
752
+ statusPill.textContent = currentStatus;
753
+ renderWalletGuardPanel(intent);
754
+ renderUnitPreview(intent);
755
+ renderUnsignedTransactionDraft(intent);
756
+ renderTransactionDraftConsistencyCheck(intent);
757
+ renderWalletHandoffReadinessManifest(intent);
758
+ renderValidationSummary(intent);
759
+ renderSigningPreflightReport(intent);
760
+ renderFinalConfirmationPanel(intent);
761
+ markStatusStep(currentStatus);
762
+ statusLog.replaceChildren(
763
+ ...events.map(([status, message]) => {
764
+ const row = document.createElement('div');
765
+ const strong = document.createElement('strong');
766
+ strong.textContent = status;
767
+ row.append(strong, document.createTextNode(` · ${message}`));
768
+ return row;
769
+ })
770
+ );
771
+ }
772
+
773
+ form.addEventListener('input', () => {
774
+ finalConfirmationRecorded = false;
775
+ render();
776
+ });
777
+
778
+ finalConfirmationCheckbox.addEventListener('change', () => {
779
+ finalConfirmationRecorded = false;
780
+ render();
781
+ });
782
+
783
+ prepareButton.addEventListener('click', () => {
784
+ const intent = readIntent();
785
+ const nextStatus = nextStatusAfterPrepare(intent);
786
+ if (nextStatus === 'ready_for_review') {
787
+ freezeIntentForReview(intent);
788
+ appendEvent('ready_for_review', 'Agent prepared and froze a reviewable intent object. No wallet prompt was opened.');
789
+ return;
790
+ }
791
+ appendEvent('draft', 'Intent needs valid recipient, amount, and future expiry before review.');
792
+ });
793
+
794
+ approveButton.addEventListener('click', () => {
795
+ const intent = readIntent();
796
+ if (!frozenIntentSnapshot || hasFrozenIntentChanged(intent)) {
797
+ appendEvent('draft', 'Approval blocked until the current intent is prepared and frozen again.');
798
+ return;
799
+ }
800
+ finalConfirmationRecorded = false;
801
+ appendEvent('approved_local', 'Human approval was recorded as local UI state only for the frozen intent.');
802
+ });
803
+
804
+ finalConfirmationButton.addEventListener('click', () => {
805
+ const intent = readIntent();
806
+ if (!canRecordFinalConfirmation(intent)) {
807
+ appendEvent('approved_local', 'Final confirmation stayed blocked until the frozen reviewed intent passes every local gate.');
808
+ return;
809
+ }
810
+ finalConfirmationRecorded = true;
811
+ appendEvent('final_review_confirmed', 'Final local confirmation recorded. Transaction requests remain disabled.');
812
+ });
813
+
814
+ submitButton.addEventListener('click', () => {
815
+ appendEvent('blocked_wallet_unavailable', 'Wallet submission stayed blocked. No transaction was broadcast.');
816
+ });
817
+
818
+ copyPreflightButton.addEventListener('click', () => {
819
+ copySigningPreflightReport();
820
+ });
821
+
822
+ resetButton.addEventListener('click', () => {
823
+ currentStatus = 'draft';
824
+ events = [...initialEvents];
825
+ frozenIntentSnapshot = null;
826
+ finalConfirmationRecorded = false;
827
+ form.reset();
828
+ setDefaultExpiry();
829
+ finalConfirmationCheckbox.checked = false;
830
+ render();
831
+ });
832
+
833
+ renderArcStatusPanel();
834
+ setDefaultExpiry();
835
+ render();