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,877 @@
1
+ const transactionHashInput = document.querySelector('#transaction-hash');
2
+ const intentInput = document.querySelector('#payment-intent');
3
+ const matchButton = document.querySelector('#match-receipt');
4
+ const resetButton = document.querySelector('#reset-matcher');
5
+ const statusPill = document.querySelector('#status-pill');
6
+ const matchSummaryList = document.querySelector('#match-summary-list');
7
+ const transferLogList = document.querySelector('#transfer-log-list');
8
+ const matchJson = document.querySelector('#match-json');
9
+ const downloadJsonButton = document.querySelector('#download-json-evidence');
10
+ const downloadMarkdownButton = document.querySelector('#download-markdown-evidence');
11
+
12
+ const EVIDENCE_DISCLAIMER = 'This evidence packet contains observed receipt and event-log data only. It is not a proof of settlement, custody, business acceptance, offchain fulfillment, or mainnet readiness. The Arc Testnet RPC and pinned USDC contract are public read-only references; always verify independently before taking any action.';
13
+
14
+ let lastMatchResult = null;
15
+
16
+ const ARC_MATCHER = Object.freeze({
17
+ network: 'Arc Testnet',
18
+ expectedChainId: 5042002,
19
+ expectedChainIdHex: '0x4cef52',
20
+ rpcUrl: 'https://rpc.testnet.arc.network',
21
+ explorerUrl: 'https://testnet.arcscan.app',
22
+ usdcAddress: '0x3600000000000000000000000000000000000000',
23
+ usdcDecimals: 6,
24
+ walletConnected: false,
25
+ backendCalls: false,
26
+ readOnlyRpcCheckOnly: true,
27
+ transactionBroadcast: false,
28
+ signingEnabled: false,
29
+ autonomousSpending: false,
30
+ humanApprovalRequired: true,
31
+ });
32
+
33
+ const SAMPLE_INTENT = JSON.stringify({
34
+ version: '2025-06-arc-payment-intent-v1',
35
+ network: 'Arc Testnet',
36
+ chainId: 5042002,
37
+ asset: 'USDC',
38
+ token: '0x3600000000000000000000000000000000000000',
39
+ recipient: '0x1111111111111111111111111111111111111111',
40
+ amount: '0.01',
41
+ amountBaseUnits: '10000',
42
+ decimals: 6,
43
+ memo: 'Demo payment-intent receipt matcher',
44
+ }, null, 2);
45
+
46
+ const SAMPLE_TRANSACTION_HASH = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
47
+ const RPC_TIMEOUT_MS = 15_000;
48
+ const MAX_RPC_RESPONSE_BYTES = 1_000_000;
49
+ const RPC_REQUEST_ID = 'arc-payment-intent-receipt-matcher-read-only';
50
+ const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
51
+ const MAX_INTENT_INPUT_LENGTH = 16_384;
52
+ const MAX_UINT256_DECIMAL_DIGITS = 78;
53
+ const MAX_DECIMAL_INPUT_LENGTH = 96;
54
+ const READ_ONLY_RPC_METHODS = Object.freeze([
55
+ { method: 'eth_chainId', params: [] },
56
+ { method: 'eth_getTransactionReceipt', params: ['transactionHash'] },
57
+ ]);
58
+
59
+ const ZERO_ADDRESS = '0x' + '0'.repeat(40);
60
+
61
+ function isValidHash(value) {
62
+ return /^0x[a-fA-F0-9]{64}$/.test(String(value || '').trim());
63
+ }
64
+
65
+ function normalizeHex(value) {
66
+ return String(value || '').trim().toLowerCase();
67
+ }
68
+
69
+ function isValidEvmAddress(value) {
70
+ return /^0x[0-9a-fA-F]{40}$/.test(String(value || '').trim());
71
+ }
72
+
73
+ function isNonZeroAddress(value) {
74
+ return isValidEvmAddress(value) && normalizeHex(value) !== ZERO_ADDRESS;
75
+ }
76
+
77
+ function hashMatchesExpected(value, expectedHash) {
78
+ return isValidHash(value) && normalizeHex(value) === normalizeHex(expectedHash);
79
+ }
80
+
81
+ function parseHexQuantity(value) {
82
+ if (typeof value !== 'string' || !/^0x[0-9a-fA-F]+$/.test(value)) return null;
83
+ return BigInt(value);
84
+ }
85
+
86
+ function decimalString(value) {
87
+ return typeof value === 'bigint' ? value.toString(10) : null;
88
+ }
89
+
90
+ function formatUsdcBaseUnits(baseUnits) {
91
+ const value = BigInt(baseUnits);
92
+ const scale = 10n ** BigInt(ARC_MATCHER.usdcDecimals);
93
+ const whole = value / scale;
94
+ const fraction = (value % scale).toString(10).padStart(ARC_MATCHER.usdcDecimals, '0');
95
+ const trimmed = fraction.replace(/0+$/, '');
96
+ return trimmed ? `${whole}.${trimmed}` : whole.toString(10);
97
+ }
98
+
99
+ function parseAmountBaseUnits(value) {
100
+ const trimmed = String(value || '').trim();
101
+ if (!trimmed) return null;
102
+ if (trimmed.length > MAX_UINT256_DECIMAL_DIGITS) return null;
103
+ // Reject hex, signs, scientific notation, commas, decimals, and leading zeros.
104
+ if (!/^[1-9]\d*$/.test(trimmed)) return null;
105
+ try {
106
+ return BigInt(trimmed);
107
+ } catch (_error) {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ function usdcBaseUnitsFromDecimal(decimalStringValue) {
113
+ const trimmed = String(decimalStringValue || '').trim();
114
+ if (!trimmed) return null;
115
+ if (trimmed.length > MAX_DECIMAL_INPUT_LENGTH) return null;
116
+
117
+ // Allow "1" or "0.01", but reject signs, scientific notation, commas, hex, empty parts,
118
+ // leading zeros, and anything with more than usdcDecimals fractional digits.
119
+ const isInteger = /^[1-9]\d*$/.test(trimmed);
120
+ const isDecimal = /^(0|[1-9]\d*)\.\d+$/.test(trimmed);
121
+ if (!isInteger && !isDecimal) return null;
122
+
123
+ const [wholePart, fractionPart = ''] = trimmed.split('.');
124
+ if (wholePart.length > MAX_UINT256_DECIMAL_DIGITS) return null;
125
+ if (fractionPart.length > ARC_MATCHER.usdcDecimals) return null;
126
+
127
+ const scale = 10n ** BigInt(ARC_MATCHER.usdcDecimals);
128
+ const whole = BigInt(wholePart || '0');
129
+ const fractionPadded = fractionPart.padEnd(ARC_MATCHER.usdcDecimals, '0');
130
+ const fraction = BigInt(fractionPadded);
131
+ const baseUnits = whole * scale + fraction;
132
+
133
+ // Reject zero and negative (the regex already rejects '-' signs; this is a second guard).
134
+ if (baseUnits <= 0n) return null;
135
+ return baseUnits;
136
+ }
137
+
138
+ function topicToAddress(topic) {
139
+ const normalized = normalizeHex(topic);
140
+ if (!/^0x[0-9a-f]{64}$/.test(normalized)) {
141
+ throw new Error('Transfer topic address must be a 32-byte indexed value.');
142
+ }
143
+ return `0x${normalized.slice(26)}`;
144
+ }
145
+
146
+ function parseTransferAmount(data) {
147
+ const normalized = normalizeHex(data);
148
+ if (!/^0x[0-9a-f]{64}$/.test(normalized)) {
149
+ throw new Error('Transfer data must be a 32-byte uint256 value.');
150
+ }
151
+ return BigInt(normalized);
152
+ }
153
+
154
+ function decodeUsdcTransferLog(log) {
155
+ if (!log || typeof log !== 'object' || Array.isArray(log)) return null;
156
+ if (normalizeHex(log.address) !== ARC_MATCHER.usdcAddress.toLowerCase()) return null;
157
+ const topics = Array.isArray(log.topics) ? log.topics.map(normalizeHex) : [];
158
+ if (topics.length < 3 || topics[0] !== TRANSFER_TOPIC) return null;
159
+ const amountBaseUnits = parseTransferAmount(log.data);
160
+ return {
161
+ kind: 'arc_testnet_usdc_transfer_log',
162
+ token: ARC_MATCHER.usdcAddress,
163
+ tokenDecimals: ARC_MATCHER.usdcDecimals,
164
+ from: topicToAddress(topics[1]),
165
+ to: topicToAddress(topics[2]),
166
+ amountBaseUnits: amountBaseUnits.toString(10),
167
+ amountUsdc: formatUsdcBaseUnits(amountBaseUnits),
168
+ logIndexHex: typeof log.logIndex === 'string' ? log.logIndex : null,
169
+ transactionIndexHex: typeof log.transactionIndex === 'string' ? log.transactionIndex : null,
170
+ };
171
+ }
172
+
173
+ function extractUsdcTransferLogs(receipt) {
174
+ const logs = receipt && Array.isArray(receipt.logs) ? receipt.logs : [];
175
+ const transfers = [];
176
+ for (const log of logs) {
177
+ try {
178
+ const decoded = decodeUsdcTransferLog(log);
179
+ if (decoded) transfers.push(decoded);
180
+ } catch (_error) {
181
+ transfers.push({
182
+ kind: 'arc_testnet_usdc_transfer_log_parse_error',
183
+ token: ARC_MATCHER.usdcAddress,
184
+ amountBaseUnits: null,
185
+ amountUsdc: null,
186
+ logIndexHex: log && typeof log.logIndex === 'string' ? log.logIndex : null,
187
+ });
188
+ }
189
+ }
190
+ return transfers;
191
+ }
192
+
193
+ async function rpcCall(method, params = []) {
194
+ if (!READ_ONLY_RPC_METHODS.some((entry) => entry.method === method)) {
195
+ throw new Error(`Blocked unreviewed RPC method: ${method}`);
196
+ }
197
+ const controller = new AbortController();
198
+ const timeout = window.setTimeout(() => controller.abort(), RPC_TIMEOUT_MS);
199
+ try {
200
+ const response = await fetch(ARC_MATCHER.rpcUrl, {
201
+ method: 'POST',
202
+ headers: { 'Content-Type': 'application/json' },
203
+ body: JSON.stringify({ jsonrpc: '2.0', id: RPC_REQUEST_ID, method, params }),
204
+ signal: controller.signal,
205
+ credentials: 'omit',
206
+ referrerPolicy: 'no-referrer',
207
+ cache: 'no-store',
208
+ });
209
+ if (!response.ok) {
210
+ throw new Error(`RPC HTTP ${response.status}`);
211
+ }
212
+ const responseText = await response.text();
213
+ if (new TextEncoder().encode(responseText).byteLength > MAX_RPC_RESPONSE_BYTES) {
214
+ throw new Error('RPC response exceeded the 1 MB safety limit');
215
+ }
216
+ const payload = JSON.parse(responseText);
217
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
218
+ throw new Error('RPC response must be a JSON object');
219
+ }
220
+ if (payload.jsonrpc !== '2.0' || payload.id !== RPC_REQUEST_ID) {
221
+ throw new Error('RPC response envelope did not match the request');
222
+ }
223
+ const hasResult = Object.prototype.hasOwnProperty.call(payload, 'result');
224
+ const hasError = Object.prototype.hasOwnProperty.call(payload, 'error');
225
+ if (hasResult === hasError) {
226
+ throw new Error('RPC response must contain exactly one result or error field');
227
+ }
228
+ if (hasError) {
229
+ const message = payload.error && typeof payload.error.message === 'string'
230
+ ? payload.error.message.slice(0, 200)
231
+ : 'unknown RPC error';
232
+ throw new Error(`${method} failed: ${message}`);
233
+ }
234
+ return payload.result;
235
+ } catch (error) {
236
+ if (error && error.name === 'AbortError') {
237
+ throw new Error('Request timed out after 15 seconds.');
238
+ }
239
+ throw error;
240
+ } finally {
241
+ window.clearTimeout(timeout);
242
+ }
243
+ }
244
+
245
+ async function readOnlyReceiptLookup(transactionHash) {
246
+ const chainIdHex = await rpcCall('eth_chainId');
247
+ if (normalizeHex(chainIdHex) !== ARC_MATCHER.expectedChainIdHex) {
248
+ return { chainIdHex, receipt: null };
249
+ }
250
+ const receipt = await rpcCall('eth_getTransactionReceipt', [transactionHash]);
251
+ return { chainIdHex, receipt };
252
+ }
253
+
254
+ function parseIntent(rawValue) {
255
+ const text = String(rawValue || '').trim();
256
+ if (!text) {
257
+ return { ok: false, error: 'Payment intent is empty.', intent: null };
258
+ }
259
+ if (text.length > MAX_INTENT_INPUT_LENGTH) {
260
+ return { ok: false, error: 'Payment intent input is too large.', intent: null };
261
+ }
262
+ let parsed;
263
+ try {
264
+ parsed = JSON.parse(text);
265
+ } catch (error) {
266
+ return { ok: false, error: `Payment intent is not valid JSON: ${error.message}`, intent: null };
267
+ }
268
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
269
+ return { ok: false, error: 'Payment intent must be a JSON object.', intent: null };
270
+ }
271
+
272
+ // Network pinning: only Arc Testnet is supported.
273
+ const network = parsed.network;
274
+ if (network !== 'Arc Testnet' && network !== 'arc-testnet') {
275
+ return { ok: false, error: "Payment intent network must be 'Arc Testnet' or 'arc-testnet'.", intent: null };
276
+ }
277
+ if (parsed.chainId !== ARC_MATCHER.expectedChainId) {
278
+ return { ok: false, error: `Payment intent chainId must be ${ARC_MATCHER.expectedChainId}.`, intent: null };
279
+ }
280
+ if (parsed.asset !== 'USDC') {
281
+ return { ok: false, error: "Payment intent asset must be 'USDC'.", intent: null };
282
+ }
283
+
284
+ // Token pinning: reject any token other than the canonical Arc Testnet USDC contract.
285
+ const token = normalizeHex(parsed.token);
286
+ if (token !== ARC_MATCHER.usdcAddress.toLowerCase()) {
287
+ return { ok: false, error: 'Payment intent token must be the pinned Arc Testnet USDC contract.', intent: null };
288
+ }
289
+
290
+ // Decimals pinning: only 6 decimals are supported for Arc Testnet USDC.
291
+ if (parsed.decimals !== ARC_MATCHER.usdcDecimals) {
292
+ return { ok: false, error: `Payment intent decimals must be ${ARC_MATCHER.usdcDecimals}.`, intent: null };
293
+ }
294
+
295
+ // Recipient must be a non-zero 20-byte EVM address and must not be the token contract.
296
+ const recipient = normalizeHex(parsed.recipient);
297
+ if (!isNonZeroAddress(recipient)) {
298
+ return { ok: false, error: 'Payment intent recipient must include a valid non-zero 20-byte recipient address.', intent: null };
299
+ }
300
+ if (recipient === ARC_MATCHER.usdcAddress.toLowerCase()) {
301
+ return { ok: false, error: 'Payment intent recipient must not be the USDC token contract.', intent: null };
302
+ }
303
+
304
+ // Amount: at least one of amount or amountBaseUnits must be present.
305
+ const hasAmount = typeof parsed.amount === 'string' && parsed.amount.trim() !== '';
306
+ const hasBaseUnits = typeof parsed.amountBaseUnits === 'string' && parsed.amountBaseUnits.trim() !== '';
307
+ if (!hasAmount && !hasBaseUnits) {
308
+ return { ok: false, error: 'Payment intent must include amount or amountBaseUnits.', intent: null };
309
+ }
310
+
311
+ let amountBaseUnits = null;
312
+ if (hasBaseUnits) {
313
+ amountBaseUnits = parseAmountBaseUnits(parsed.amountBaseUnits);
314
+ if (amountBaseUnits === null) {
315
+ return { ok: false, error: 'Payment intent amountBaseUnits must be a positive base-10 integer string.', intent: null };
316
+ }
317
+ }
318
+ if (hasAmount) {
319
+ const decimalBaseUnits = usdcBaseUnitsFromDecimal(parsed.amount);
320
+ if (decimalBaseUnits === null) {
321
+ return { ok: false, error: 'Payment intent amount must be a positive decimal with at most 6 fractional digits.', intent: null };
322
+ }
323
+ if (hasBaseUnits && decimalBaseUnits !== amountBaseUnits) {
324
+ return { ok: false, error: 'Payment intent amount and amountBaseUnits do not match.', intent: null };
325
+ }
326
+ amountBaseUnits = decimalBaseUnits;
327
+ }
328
+
329
+ const intent = {
330
+ version: typeof parsed.version === 'string' ? parsed.version : null,
331
+ network,
332
+ chainId: parsed.chainId,
333
+ asset: parsed.asset,
334
+ token,
335
+ recipient,
336
+ amountBaseUnits,
337
+ decimals: parsed.decimals,
338
+ memo: typeof parsed.memo === 'string' ? parsed.memo : null,
339
+ };
340
+ return { ok: true, error: null, intent };
341
+ }
342
+
343
+ function safetyBoundary() {
344
+ return {
345
+ walletConnected: false,
346
+ backendCalls: false,
347
+ readOnlyRpcCheckOnly: true,
348
+ transactionBroadcast: false,
349
+ signingEnabled: false,
350
+ autonomousSpending: false,
351
+ humanApprovalRequired: true,
352
+ };
353
+ }
354
+
355
+ function classifyMatch({ chainIdHex, receipt }, transactionHash, intent) {
356
+ const chainIdBigInt = parseHexQuantity(chainIdHex);
357
+ const chainMatches = normalizeHex(chainIdHex) === ARC_MATCHER.expectedChainIdHex;
358
+ const receiptHashMatches = !receipt || hashMatchesExpected(receipt.transactionHash, transactionHash);
359
+ const transferEvents = receiptHashMatches ? extractUsdcTransferLogs(receipt) : [];
360
+ const matchingTransfers = transferEvents.filter((transfer) => {
361
+ if (!transfer.amountBaseUnits) return false;
362
+ const transferAmount = BigInt(transfer.amountBaseUnits);
363
+ return transfer.to === intent.recipient
364
+ && transfer.token.toLowerCase() === intent.token
365
+ && transferAmount === intent.amountBaseUnits;
366
+ });
367
+ const base = {
368
+ kind: 'arc_testnet_payment_intent_receipt_match',
369
+ network: ARC_MATCHER.network,
370
+ rpcUrl: ARC_MATCHER.rpcUrl,
371
+ explorerUrl: ARC_MATCHER.explorerUrl,
372
+ expectedChainId: ARC_MATCHER.expectedChainId,
373
+ expectedChainIdHex: ARC_MATCHER.expectedChainIdHex,
374
+ chainIdHex,
375
+ chainIdDecimal: decimalString(chainIdBigInt),
376
+ rpcChainIdMatchesArcTestnet: chainMatches,
377
+ expectedTransactionHash: transactionHash,
378
+ receiptHashMatches,
379
+ intent: {
380
+ ...intent,
381
+ amountBaseUnits: intent.amountBaseUnits.toString(10),
382
+ amountUsdc: formatUsdcBaseUnits(intent.amountBaseUnits),
383
+ },
384
+ transferEvents,
385
+ transferEventCount: transferEvents.length,
386
+ matchingTransferCount: matchingTransfers.length,
387
+ matchingTransfers,
388
+ checkedAt: new Date().toISOString(),
389
+ safety: safetyBoundary(),
390
+ };
391
+
392
+ if (!chainMatches) {
393
+ return {
394
+ ...base,
395
+ state: 'unknown',
396
+ evidenceVerdict: 'unknown_wrong_chain',
397
+ reason: 'RPC chain ID did not match Arc Testnet.',
398
+ receiptFound: false,
399
+ receipt: null,
400
+ intentMatched: false,
401
+ settlementProven: false,
402
+ businessAcceptanceProven: false,
403
+ };
404
+ }
405
+
406
+ if (!receipt) {
407
+ return {
408
+ ...base,
409
+ state: 'not_found',
410
+ evidenceVerdict: 'receipt_not_found',
411
+ reason: 'Arc Testnet RPC returned no receipt for this hash.',
412
+ receiptFound: false,
413
+ receipt: null,
414
+ intentMatched: false,
415
+ settlementProven: false,
416
+ businessAcceptanceProven: false,
417
+ };
418
+ }
419
+
420
+ if (!receiptHashMatches) {
421
+ return {
422
+ ...base,
423
+ state: 'unknown',
424
+ evidenceVerdict: 'unknown_hash_mismatch',
425
+ reason: 'RPC returned a receipt for a different transaction hash.',
426
+ receiptFound: true,
427
+ observedReceiptTransactionHash: receipt.transactionHash || null,
428
+ receipt: null,
429
+ intentMatched: false,
430
+ settlementProven: false,
431
+ businessAcceptanceProven: false,
432
+ };
433
+ }
434
+
435
+ const common = {
436
+ ...base,
437
+ transactionHash: receipt.transactionHash,
438
+ blockNumberHex: receipt.blockNumber || null,
439
+ blockNumberDecimal: decimalString(parseHexQuantity(receipt.blockNumber)),
440
+ gasUsedHex: receipt.gasUsed || null,
441
+ gasUsedDecimal: decimalString(parseHexQuantity(receipt.gasUsed)),
442
+ logsCount: Array.isArray(receipt.logs) ? receipt.logs.length : 0,
443
+ receiptFound: true,
444
+ receipt,
445
+ };
446
+
447
+ if (receipt.status !== '0x1') {
448
+ return {
449
+ ...common,
450
+ state: 'revert',
451
+ evidenceVerdict: 'reverted_receipt_observed',
452
+ reason: 'Transaction receipt exists but status is not 0x1, so no successful transfer is expected.',
453
+ intentMatched: false,
454
+ settlementProven: false,
455
+ businessAcceptanceProven: false,
456
+ };
457
+ }
458
+
459
+ if (matchingTransfers.length === 0) {
460
+ return {
461
+ ...common,
462
+ state: 'mismatch',
463
+ evidenceVerdict: 'intent_receipt_mismatch',
464
+ reason: 'Receipt succeeded, but no USDC Transfer log matched the expected recipient, token, and amount.',
465
+ intentMatched: false,
466
+ settlementProven: false,
467
+ businessAcceptanceProven: false,
468
+ };
469
+ }
470
+
471
+ return {
472
+ ...common,
473
+ state: 'match',
474
+ evidenceVerdict: 'intent_receipt_match_observed',
475
+ reason: 'Receipt succeeded and at least one USDC Transfer log matched the expected recipient, token, and amount.',
476
+ intentMatched: true,
477
+ settlementProven: false,
478
+ businessAcceptanceProven: false,
479
+ };
480
+ }
481
+
482
+ function buildCheck(id, label, level, detail) {
483
+ return { id, label, level, detail };
484
+ }
485
+
486
+ function checksForResult(result, hash, intent) {
487
+ const checks = [
488
+ buildCheck(
489
+ 'hash',
490
+ 'Transaction hash shape',
491
+ isValidHash(hash) ? 'pass' : 'fail',
492
+ 'Expected a 32-byte 0x-prefixed transaction hash.'
493
+ ),
494
+ buildCheck(
495
+ 'intent',
496
+ 'Payment intent parse',
497
+ intent && !intent.error ? 'pass' : 'fail',
498
+ intent && !intent.error ? 'Intent parsed successfully.' : (intent && intent.error) || 'Intent is missing or invalid.'
499
+ ),
500
+ buildCheck(
501
+ 'chain',
502
+ 'Arc Testnet chain ID',
503
+ result.rpcChainIdMatchesArcTestnet ? 'pass' : 'fail',
504
+ `Expected ${ARC_MATCHER.expectedChainIdHex}; got ${result.chainIdHex || 'unavailable'}.`
505
+ ),
506
+ buildCheck(
507
+ 'method-scope',
508
+ 'RPC method scope',
509
+ 'pass',
510
+ 'Used only eth_chainId and eth_getTransactionReceipt.'
511
+ ),
512
+ buildCheck(
513
+ 'receipt-status',
514
+ 'Receipt status',
515
+ result.state === 'match'
516
+ ? 'pass'
517
+ : result.state === 'revert' || result.state === 'mismatch'
518
+ ? 'fail'
519
+ : 'warn',
520
+ result.reason
521
+ ),
522
+ buildCheck(
523
+ 'usdc-transfer-match',
524
+ 'USDC Transfer matches intent',
525
+ result.matchingTransferCount > 0 ? 'pass' : 'warn',
526
+ result.matchingTransferCount > 0
527
+ ? `${result.matchingTransferCount} Arc Testnet USDC Transfer log(s) matched the intent.`
528
+ : 'No pinned Arc Testnet USDC Transfer log matched the intent.'
529
+ ),
530
+ buildCheck(
531
+ 'settlement-boundary',
532
+ 'Settlement boundary',
533
+ 'warn',
534
+ 'A chain-side match does not prove business acceptance, offchain fulfillment, or product settlement.'
535
+ ),
536
+ ];
537
+ return checks;
538
+ }
539
+
540
+ function renderList(target, items) {
541
+ target.replaceChildren(
542
+ ...items.map((item) => {
543
+ const listItem = document.createElement('li');
544
+ listItem.className = item.level;
545
+ const strong = document.createElement('strong');
546
+ strong.textContent = item.level === 'pass' ? 'PASS' : item.level === 'fail' ? 'REVIEW' : 'INFO';
547
+ listItem.append(strong, ` - ${item.label}: ${item.detail}`);
548
+ return listItem;
549
+ })
550
+ );
551
+ }
552
+
553
+ function renderTransferLogs(transfers, intent) {
554
+ if (!transfers.length) {
555
+ renderList(transferLogList, [
556
+ buildCheck(
557
+ 'no-usdc-transfer-logs',
558
+ 'USDC logs',
559
+ 'warn',
560
+ 'No Arc Testnet USDC Transfer logs were found in this receipt.'
561
+ ),
562
+ ]);
563
+ return;
564
+ }
565
+ renderList(transferLogList, transfers.map((transfer, index) => {
566
+ const matchesIntent = transfer.amountBaseUnits
567
+ && transfer.to === intent.recipient
568
+ && transfer.token.toLowerCase() === intent.token
569
+ && BigInt(transfer.amountBaseUnits) === intent.amountBaseUnits;
570
+ return buildCheck(
571
+ `usdc-transfer-${index + 1}`,
572
+ `${transfer.from || 'unknown'} -> ${transfer.to || 'unknown'}`,
573
+ transfer.amountBaseUnits ? (matchesIntent ? 'pass' : 'warn') : 'warn',
574
+ transfer.amountBaseUnits
575
+ ? `${transfer.amountUsdc} USDC (${transfer.amountBaseUnits} base units)${matchesIntent ? ' ✓ matches intent' : ' ✗ does not match intent'}.`
576
+ : 'Transfer log matched the pinned token/topic but could not be fully decoded.'
577
+ );
578
+ }));
579
+ }
580
+
581
+ function renderStatus(result, hash, intent) {
582
+ lastMatchResult = result;
583
+ statusPill.textContent = result.state;
584
+ statusPill.className = `status ${result.state}`;
585
+ renderList(matchSummaryList, checksForResult(result, hash, intent));
586
+ renderTransferLogs(result.transferEvents || [], intent);
587
+ matchJson.textContent = JSON.stringify(result, null, 2);
588
+ updateExportButtons(result);
589
+ }
590
+
591
+ function buildEvidencePacket(result) {
592
+ const now = new Date().toISOString();
593
+ const intent = result && result.intent ? result.intent : null;
594
+ const receiptFound = result && result.receiptFound === true;
595
+ const receiptSummary = receiptFound ? {
596
+ transactionHash: result.transactionHash || null,
597
+ blockNumberHex: result.blockNumberHex || null,
598
+ blockNumberDecimal: result.blockNumberDecimal || null,
599
+ gasUsedHex: result.gasUsedHex || null,
600
+ gasUsedDecimal: result.gasUsedDecimal || null,
601
+ status: result.receipt && result.receipt.status ? result.receipt.status : null,
602
+ logsCount: typeof result.logsCount === 'number' ? result.logsCount : 0,
603
+ receiptFound: true,
604
+ } : {
605
+ receiptFound: false,
606
+ };
607
+
608
+ const transferLogSummary = (result && Array.isArray(result.matchingTransfers) ? result.matchingTransfers : []).map((transfer) => ({
609
+ kind: transfer.kind || 'arc_testnet_usdc_transfer_log',
610
+ token: transfer.token || null,
611
+ tokenDecimals: transfer.tokenDecimals || null,
612
+ from: transfer.from || null,
613
+ to: transfer.to || null,
614
+ amountBaseUnits: transfer.amountBaseUnits || null,
615
+ amountUsdc: transfer.amountUsdc || null,
616
+ logIndexHex: transfer.logIndexHex || null,
617
+ matchesIntent: transfer.amountBaseUnits
618
+ && transfer.to
619
+ && intent
620
+ && transfer.to === intent.recipient
621
+ && (transfer.token || '').toLowerCase() === (intent.token || '').toLowerCase()
622
+ && BigInt(transfer.amountBaseUnits) === BigInt(intent.amountBaseUnits || 0),
623
+ }));
624
+
625
+ const mismatchReasons = [];
626
+ if (!result || result.state === 'not_checked') {
627
+ mismatchReasons.push('No payment-intent receipt match has run yet.');
628
+ } else if (result.state !== 'match') {
629
+ if (result.reason) mismatchReasons.push(result.reason);
630
+ if (result.state === 'mismatch') mismatchReasons.push('No pinned Arc Testnet USDC Transfer log matched the expected recipient, token, and amount.');
631
+ if (result.state === 'revert') mismatchReasons.push('Transaction receipt status is not 0x1.');
632
+ if (result.state === 'not_found') mismatchReasons.push('No receipt was returned for the supplied transaction hash.');
633
+ if (result.state === 'unknown') mismatchReasons.push('The matcher could not reach a deterministic conclusion.');
634
+ }
635
+
636
+ return {
637
+ matcherVersion: '2025-06-arc-payment-intent-receipt-matcher-v1',
638
+ exportedAt: now,
639
+ exportOrigin: 'local_browser_blob_download',
640
+ networkConstants: {
641
+ network: ARC_MATCHER.network,
642
+ chainId: ARC_MATCHER.expectedChainId,
643
+ chainIdHex: ARC_MATCHER.expectedChainIdHex,
644
+ rpcLabel: 'Public Arc Testnet RPC',
645
+ explorerLabel: 'Arc Testnet explorer',
646
+ },
647
+ paymentIntent: intent,
648
+ receiptSummary,
649
+ transferLogSummary,
650
+ verdict: result && result.state ? result.state : 'unknown',
651
+ evidenceVerdict: result && result.evidenceVerdict ? result.evidenceVerdict : 'not_checked',
652
+ mismatchReasons,
653
+ safetyBoundary: result && result.safety ? result.safety : safetyBoundary(),
654
+ disclaimer: EVIDENCE_DISCLAIMER,
655
+ };
656
+ }
657
+
658
+ function generateMarkdownEvidence(packet) {
659
+ const sections = [
660
+ '# Payment Intent Receipt Matcher Evidence Packet',
661
+ '',
662
+ `- **Matcher version:** ${packet.matcherVersion}`,
663
+ `- **Exported at:** ${packet.exportedAt}`,
664
+ `- **Export origin:** ${packet.exportOrigin}`,
665
+ '',
666
+ '## Network constants',
667
+ '',
668
+ `- Network: ${packet.networkConstants.network}`,
669
+ `- Chain ID: ${packet.networkConstants.chainId} (${packet.networkConstants.chainIdHex})`,
670
+ `- RPC label: ${packet.networkConstants.rpcLabel}`,
671
+ `- Explorer label: ${packet.networkConstants.explorerLabel}`,
672
+ '',
673
+ '## Payment intent',
674
+ '',
675
+ '```json',
676
+ JSON.stringify(packet.paymentIntent, null, 2),
677
+ '```',
678
+ '',
679
+ '## Receipt summary',
680
+ '',
681
+ packet.receiptSummary.receiptFound
682
+ ? [
683
+ `- Transaction hash: ${packet.receiptSummary.transactionHash}`,
684
+ `- Block number: ${packet.receiptSummary.blockNumberDecimal || packet.receiptSummary.blockNumberHex || 'unavailable'}`,
685
+ `- Gas used: ${packet.receiptSummary.gasUsedDecimal || packet.receiptSummary.gasUsedHex || 'unavailable'}`,
686
+ `- Status: ${packet.receiptSummary.status || 'unavailable'}`,
687
+ `- Logs count: ${packet.receiptSummary.logsCount}`,
688
+ ].join('\n')
689
+ : '- No receipt was returned for the supplied transaction hash.',
690
+ '',
691
+ '## Matched USDC Transfer log summary',
692
+ '',
693
+ ];
694
+
695
+ if (packet.transferLogSummary.length === 0) {
696
+ sections.push('No matching Transfer logs were found.');
697
+ } else {
698
+ sections.push('| # | Token | From | To | Amount (USDC) | Amount (base units) |');
699
+ sections.push('|---|-------|------|----|---------------|---------------------|');
700
+ packet.transferLogSummary.forEach((log, index) => {
701
+ sections.push(`| ${index + 1} | ${log.token || '—'} | ${log.from || '—'} | ${log.to || '—'} | ${log.amountUsdc || '—'} | ${log.amountBaseUnits || '—'} |`);
702
+ });
703
+ }
704
+
705
+ sections.push('');
706
+ sections.push('## Verdict');
707
+ sections.push('');
708
+ sections.push(`- State: **${packet.verdict}**`);
709
+ sections.push(`- Evidence verdict: ${packet.evidenceVerdict}`);
710
+ sections.push('');
711
+
712
+ if (packet.mismatchReasons.length > 0) {
713
+ sections.push('## Mismatch reasons');
714
+ sections.push('');
715
+ packet.mismatchReasons.forEach((reason) => sections.push(`- ${reason}`));
716
+ sections.push('');
717
+ }
718
+
719
+ sections.push('## Disclaimer');
720
+ sections.push('');
721
+ sections.push(packet.disclaimer);
722
+ sections.push('');
723
+
724
+ return sections.join('\n');
725
+ }
726
+
727
+ function downloadEvidenceFile(content, filename, mimeType) {
728
+ const blob = new Blob([content], { type: mimeType });
729
+ const url = URL.createObjectURL(blob);
730
+ const anchor = document.createElement('a');
731
+ anchor.href = url;
732
+ anchor.download = filename;
733
+ anchor.style.display = 'none';
734
+ document.body.appendChild(anchor);
735
+ anchor.click();
736
+ document.body.removeChild(anchor);
737
+ URL.revokeObjectURL(url);
738
+ }
739
+
740
+ function updateExportButtons(result) {
741
+ const exportable = result
742
+ && result.state
743
+ && result.state !== 'not_checked'
744
+ && result.state !== 'checking_arc_receipt_match';
745
+ downloadJsonButton.disabled = !exportable;
746
+ downloadMarkdownButton.disabled = !exportable;
747
+ }
748
+
749
+ function renderInitialState() {
750
+ const result = {
751
+ kind: 'arc_testnet_payment_intent_receipt_match',
752
+ state: 'not_checked',
753
+ evidenceVerdict: 'not_checked',
754
+ reason: 'No payment-intent receipt match has run yet.',
755
+ network: ARC_MATCHER.network,
756
+ rpcUrl: ARC_MATCHER.rpcUrl,
757
+ expectedChainId: ARC_MATCHER.expectedChainId,
758
+ expectedChainIdHex: ARC_MATCHER.expectedChainIdHex,
759
+ transferEvents: [],
760
+ transferEventCount: 0,
761
+ matchingTransferCount: 0,
762
+ intentMatched: false,
763
+ settlementProven: false,
764
+ businessAcceptanceProven: false,
765
+ safety: safetyBoundary(),
766
+ };
767
+ renderStatus(result, '', { error: 'Intent not parsed.' });
768
+ }
769
+
770
+ async function runMatch() {
771
+ const hash = transactionHashInput.value.trim();
772
+ const intentParse = parseIntent(intentInput.value);
773
+ if (!isValidHash(hash)) {
774
+ const result = {
775
+ kind: 'arc_testnet_payment_intent_receipt_match',
776
+ state: 'unknown',
777
+ evidenceVerdict: 'invalid_local_input',
778
+ reason: 'Transaction hash is not a 32-byte 0x-prefixed value.',
779
+ checkedAt: new Date().toISOString(),
780
+ expectedTransactionHash: hash,
781
+ intent: intentParse.ok ? { ...intentParse.intent, amountBaseUnits: intentParse.intent.amountBaseUnits.toString(10) } : null,
782
+ chainIdHex: 'not_checked',
783
+ rpcChainIdMatchesArcTestnet: false,
784
+ transferEvents: [],
785
+ transferEventCount: 0,
786
+ matchingTransferCount: 0,
787
+ intentMatched: false,
788
+ settlementProven: false,
789
+ businessAcceptanceProven: false,
790
+ safety: safetyBoundary(),
791
+ };
792
+ renderStatus(result, hash, intentParse);
793
+ return;
794
+ }
795
+ if (!intentParse.ok) {
796
+ const result = {
797
+ kind: 'arc_testnet_payment_intent_receipt_match',
798
+ state: 'unknown',
799
+ evidenceVerdict: 'invalid_local_input',
800
+ reason: intentParse.error,
801
+ checkedAt: new Date().toISOString(),
802
+ expectedTransactionHash: hash,
803
+ intent: null,
804
+ chainIdHex: 'not_checked',
805
+ rpcChainIdMatchesArcTestnet: false,
806
+ transferEvents: [],
807
+ transferEventCount: 0,
808
+ matchingTransferCount: 0,
809
+ intentMatched: false,
810
+ settlementProven: false,
811
+ businessAcceptanceProven: false,
812
+ safety: safetyBoundary(),
813
+ };
814
+ renderStatus(result, hash, intentParse);
815
+ return;
816
+ }
817
+ statusPill.textContent = 'checking';
818
+ statusPill.className = 'status checking';
819
+ matchButton.disabled = true;
820
+ matchJson.textContent = JSON.stringify({ state: 'checking_arc_receipt_match', transactionHash: hash }, null, 2);
821
+ try {
822
+ const lookup = await readOnlyReceiptLookup(hash);
823
+ const result = classifyMatch(lookup, hash, intentParse.intent);
824
+ renderStatus(result, hash, intentParse.intent);
825
+ } catch (error) {
826
+ const result = {
827
+ kind: 'arc_testnet_payment_intent_receipt_match',
828
+ state: 'unknown',
829
+ evidenceVerdict: 'unknown_rpc_unavailable',
830
+ reason: `RPC receipt lookup unavailable: ${error.message}`,
831
+ checkedAt: new Date().toISOString(),
832
+ expectedTransactionHash: hash,
833
+ intent: { ...intentParse.intent, amountBaseUnits: intentParse.intent.amountBaseUnits.toString(10) },
834
+ chainIdHex: 'unavailable',
835
+ rpcUrl: ARC_MATCHER.rpcUrl,
836
+ rpcChainIdMatchesArcTestnet: false,
837
+ transferEvents: [],
838
+ transferEventCount: 0,
839
+ matchingTransferCount: 0,
840
+ intentMatched: false,
841
+ settlementProven: false,
842
+ businessAcceptanceProven: false,
843
+ safety: safetyBoundary(),
844
+ };
845
+ renderStatus(result, hash, intentParse.intent);
846
+ } finally {
847
+ matchButton.disabled = false;
848
+ }
849
+ }
850
+
851
+ function resetSample() {
852
+ transactionHashInput.value = SAMPLE_TRANSACTION_HASH;
853
+ intentInput.value = SAMPLE_INTENT;
854
+ renderInitialState();
855
+ }
856
+
857
+ function downloadJsonEvidence() {
858
+ if (!lastMatchResult) return;
859
+ const packet = buildEvidencePacket(lastMatchResult);
860
+ const content = JSON.stringify(packet, null, 2);
861
+ const filename = `arc-receipt-matcher-evidence-${packet.verdict}-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
862
+ downloadEvidenceFile(content, filename, 'application/json');
863
+ }
864
+
865
+ function downloadMarkdownEvidence() {
866
+ if (!lastMatchResult) return;
867
+ const packet = buildEvidencePacket(lastMatchResult);
868
+ const content = generateMarkdownEvidence(packet);
869
+ const filename = `arc-receipt-matcher-evidence-${packet.verdict}-${new Date().toISOString().replace(/[:.]/g, '-')}.md`;
870
+ downloadEvidenceFile(content, filename, 'text/markdown');
871
+ }
872
+
873
+ matchButton.addEventListener('click', runMatch);
874
+ resetButton.addEventListener('click', resetSample);
875
+ downloadJsonButton.addEventListener('click', downloadJsonEvidence);
876
+ downloadMarkdownButton.addEventListener('click', downloadMarkdownEvidence);
877
+ renderInitialState();