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.
- arc_builder_kit/__init__.py +4 -0
- arc_builder_kit/__main__.py +6 -0
- arc_builder_kit/_paths.py +47 -0
- arc_builder_kit/cli.py +277 -0
- arc_builder_kit/config/arc_testnet.facts.json +31 -0
- arc_builder_kit/doctor.py +936 -0
- arc_builder_kit/examples/agent-commerce-components/components.js +200 -0
- arc_builder_kit/examples/agent-commerce-components/index.html +120 -0
- arc_builder_kit/examples/agent-commerce-flows/flows.js +271 -0
- arc_builder_kit/examples/agent-commerce-flows/index.html +114 -0
- arc_builder_kit/examples/agent-commerce-live/commerce-live.js +190 -0
- arc_builder_kit/examples/agent-commerce-live/index.html +105 -0
- arc_builder_kit/examples/agent-commerce-review-packet/index.html +96 -0
- arc_builder_kit/examples/agent-commerce-review-packet/packet.js +125 -0
- arc_builder_kit/examples/agent-identity-profile-preview/identity.js +126 -0
- arc_builder_kit/examples/agent-identity-profile-preview/index.html +104 -0
- arc_builder_kit/examples/arc-agent-treasury-lab/index.html +152 -0
- arc_builder_kit/examples/arc-agent-treasury-lab/treasury.js +532 -0
- arc_builder_kit/examples/arc-testnet-operator-evidence/evidence.example.json +47 -0
- arc_builder_kit/examples/arc-testnet-wallet-send-gate/index.html +233 -0
- arc_builder_kit/examples/arc-testnet-wallet-send-gate/live-infrastructure-policy.example.json +59 -0
- arc_builder_kit/examples/arc-testnet-wallet-send-gate/wallet-send-gate.js +472 -0
- arc_builder_kit/examples/circle-wallet-integration/index.html +155 -0
- arc_builder_kit/examples/circle-wallet-integration/wallet-lab.js +91 -0
- arc_builder_kit/examples/job-escrow-simulator/index.html +121 -0
- arc_builder_kit/examples/job-escrow-simulator/simulator.js +162 -0
- arc_builder_kit/examples/payment-intent-demo/index.html +132 -0
- arc_builder_kit/examples/payment-intent-playground/index.html +301 -0
- arc_builder_kit/examples/payment-intent-playground/playground.js +835 -0
- arc_builder_kit/examples/payment-intent-receipt-matcher/index.html +157 -0
- arc_builder_kit/examples/payment-intent-receipt-matcher/matcher.js +877 -0
- arc_builder_kit/examples/receipt-verifier-playground/index.html +120 -0
- arc_builder_kit/examples/receipt-verifier-playground/verifier.js +226 -0
- arc_builder_kit/examples/receipt-viewer/index.html +138 -0
- arc_builder_kit/examples/receipt-viewer/receipt-viewer.js +472 -0
- arc_builder_kit/examples/transaction-status-playground/index.html +135 -0
- arc_builder_kit/examples/transaction-status-playground/status.js +518 -0
- arc_builder_kit/examples/x402-local-challenge-server/.env.example +25 -0
- arc_builder_kit/examples/x402-local-challenge-server/README.md +111 -0
- arc_builder_kit/examples/x402-local-challenge-server/server.py +711 -0
- arc_builder_kit/mcp_server.py +463 -0
- arc_builder_kit/release_packet.py +469 -0
- arc_builder_kit/templates/README.md +25 -0
- arc_builder_kit/templates/job-escrow-starter/README.md +25 -0
- arc_builder_kit/templates/job-escrow-starter/index.html +41 -0
- arc_builder_kit/templates/job-escrow-starter/index.js +14 -0
- arc_builder_kit/templates/payment-intent-starter/README.md +25 -0
- arc_builder_kit/templates/payment-intent-starter/index.html +42 -0
- arc_builder_kit/templates/payment-intent-starter/index.js +7 -0
- arc_builder_kit/templates/x402-agent-starter/README.md +29 -0
- arc_builder_kit/templates/x402-agent-starter/server.py +201 -0
- arc_builder_kit/validate_repo.py +2212 -0
- arc_builder_kit-0.2.0.dist-info/METADATA +543 -0
- arc_builder_kit-0.2.0.dist-info/RECORD +58 -0
- arc_builder_kit-0.2.0.dist-info/WHEEL +5 -0
- arc_builder_kit-0.2.0.dist-info/entry_points.txt +3 -0
- arc_builder_kit-0.2.0.dist-info/licenses/LICENSE +21 -0
- arc_builder_kit-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
const transactionHashInput = document.querySelector('#transaction-hash');
|
|
2
|
+
const expectedRecipientInput = document.querySelector('#expected-recipient');
|
|
3
|
+
const expectedAmountInput = document.querySelector('#expected-amount');
|
|
4
|
+
const checkButton = document.querySelector('#check-transaction');
|
|
5
|
+
const resetButton = document.querySelector('#reset-transaction');
|
|
6
|
+
const statusPill = document.querySelector('#status-pill');
|
|
7
|
+
const statusCheckList = document.querySelector('#status-check-list');
|
|
8
|
+
const transactionStatusJson = document.querySelector('#transaction-status-json');
|
|
9
|
+
|
|
10
|
+
const ARC_TRANSACTION_STATUS = Object.freeze({
|
|
11
|
+
network: 'Arc Testnet',
|
|
12
|
+
expectedChainId: 5042002,
|
|
13
|
+
expectedChainIdHex: '0x4cef52',
|
|
14
|
+
rpcUrl: 'https://rpc.testnet.arc.network',
|
|
15
|
+
explorerUrl: 'https://testnet.arcscan.app',
|
|
16
|
+
usdcAddress: '0x3600000000000000000000000000000000000000',
|
|
17
|
+
usdcDecimals: 6,
|
|
18
|
+
walletConnected: false,
|
|
19
|
+
backendCalls: false,
|
|
20
|
+
readOnlyRpcCheckOnly: true,
|
|
21
|
+
transactionBroadcast: false,
|
|
22
|
+
autonomousSpending: false,
|
|
23
|
+
humanApprovalRequired: true,
|
|
24
|
+
signingRequiresWalletChainGateAndHumanApproval: true,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const SAMPLE_TRANSACTION_HASH = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
|
28
|
+
const RPC_TIMEOUT_MS = 10_000;
|
|
29
|
+
const MAX_RPC_RESPONSE_BYTES = 1_000_000;
|
|
30
|
+
const RPC_REQUEST_ID = 'arc-transaction-status-read-only';
|
|
31
|
+
const TRANSFER_SELECTOR = 'a9059cbb';
|
|
32
|
+
const READ_ONLY_RPC_METHODS = Object.freeze([
|
|
33
|
+
{ method: 'eth_chainId', params: [] },
|
|
34
|
+
{ method: 'eth_getTransactionByHash', params: ['transactionHash'] },
|
|
35
|
+
{ method: 'eth_getTransactionReceipt', params: ['transactionHash'] },
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
function isValidHash(value) {
|
|
39
|
+
return /^0x[a-fA-F0-9]{64}$/.test(String(value || '').trim());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hashMatchesExpected(value, expectedHash) {
|
|
43
|
+
return isValidHash(value) && String(value).toLowerCase() === expectedHash.toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseHexQuantity(value) {
|
|
47
|
+
if (typeof value !== 'string' || !/^0x[0-9a-fA-F]+$/.test(value)) return null;
|
|
48
|
+
return Number.parseInt(value, 16);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isNonZeroAddress(value) {
|
|
52
|
+
return /^0x[a-fA-F0-9]{40}$/.test(String(value || '').trim())
|
|
53
|
+
&& !/^0x0{40}$/i.test(String(value || '').trim());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseUsdcAmount(rawValue) {
|
|
57
|
+
const value = String(rawValue || '').trim();
|
|
58
|
+
if (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/.test(value)) {
|
|
59
|
+
throw new Error('Expected USDC amount must be a plain positive decimal with at most 6 fractional digits.');
|
|
60
|
+
}
|
|
61
|
+
const [whole, fraction = ''] = value.split('.');
|
|
62
|
+
const baseUnits = (BigInt(whole) * 1000000n) + BigInt(fraction.padEnd(6, '0'));
|
|
63
|
+
if (baseUnits <= 0n) throw new Error('Expected USDC amount must be greater than zero.');
|
|
64
|
+
return { decimal: value, baseUnits: baseUnits.toString() };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function decodeTransferCalldata(data) {
|
|
68
|
+
const normalized = String(data || '').toLowerCase();
|
|
69
|
+
if (!new RegExp(`^0x${TRANSFER_SELECTOR}[0-9a-f]{128}$`).test(normalized)) {
|
|
70
|
+
throw new Error('Transaction input is not ERC-20 transfer(address,uint256) calldata.');
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
method: 'transfer(address,uint256)',
|
|
74
|
+
recipient: `0x${normalized.slice(34, 74)}`,
|
|
75
|
+
amountBaseUnits: BigInt(`0x${normalized.slice(74, 138)}`).toString(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildExpectedTransfer() {
|
|
80
|
+
const recipient = expectedRecipientInput.value.trim().toLowerCase();
|
|
81
|
+
if (!isNonZeroAddress(recipient)) {
|
|
82
|
+
throw new Error('Expected recipient must be a non-zero 0x-prefixed 20-byte address.');
|
|
83
|
+
}
|
|
84
|
+
if (recipient === ARC_TRANSACTION_STATUS.usdcAddress.toLowerCase()) {
|
|
85
|
+
throw new Error('Expected recipient cannot be the pinned USDC token contract.');
|
|
86
|
+
}
|
|
87
|
+
const amount = parseUsdcAmount(expectedAmountInput.value);
|
|
88
|
+
return {
|
|
89
|
+
token: ARC_TRANSACTION_STATUS.usdcAddress,
|
|
90
|
+
tokenDecimals: ARC_TRANSACTION_STATUS.usdcDecimals,
|
|
91
|
+
recipient,
|
|
92
|
+
amountDecimal: amount.decimal,
|
|
93
|
+
amountBaseUnits: amount.baseUnits,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function reviewExpectedTransfer(transaction, expectedTransfer) {
|
|
98
|
+
if (!transaction || typeof transaction !== 'object' || Array.isArray(transaction)) {
|
|
99
|
+
return {
|
|
100
|
+
state: 'unknown',
|
|
101
|
+
allMatched: false,
|
|
102
|
+
reason: 'Transaction was not found, so expected transfer fields cannot be compared.',
|
|
103
|
+
checks: [],
|
|
104
|
+
observed: null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
let decoded = null;
|
|
108
|
+
let decodeError = '';
|
|
109
|
+
try {
|
|
110
|
+
decoded = decodeTransferCalldata(transaction.input);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
decodeError = error.message;
|
|
113
|
+
}
|
|
114
|
+
const checks = [
|
|
115
|
+
{
|
|
116
|
+
id: 'token-target',
|
|
117
|
+
passed: String(transaction.to || '').toLowerCase() === expectedTransfer.token.toLowerCase(),
|
|
118
|
+
detail: 'Transaction target must be the pinned Arc Testnet USDC interface.',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'zero-native-value',
|
|
122
|
+
passed: parseHexQuantity(transaction.value) === 0,
|
|
123
|
+
detail: 'ERC-20 transfer must send zero native value.',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'transfer-calldata',
|
|
127
|
+
passed: Boolean(decoded),
|
|
128
|
+
detail: decoded ? 'Calldata decodes as transfer(address,uint256).' : decodeError,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'recipient',
|
|
132
|
+
passed: Boolean(decoded && decoded.recipient === expectedTransfer.recipient),
|
|
133
|
+
detail: 'Decoded recipient must match the expected recipient.',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'amount',
|
|
137
|
+
passed: Boolean(decoded && decoded.amountBaseUnits === expectedTransfer.amountBaseUnits),
|
|
138
|
+
detail: 'Decoded 6-decimal USDC base units must match the expected amount.',
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
const allMatched = checks.every((check) => check.passed);
|
|
142
|
+
return {
|
|
143
|
+
state: allMatched ? 'match' : 'mismatch',
|
|
144
|
+
allMatched,
|
|
145
|
+
reason: allMatched
|
|
146
|
+
? 'Observed transaction shape matches the expected Arc Testnet USDC transfer.'
|
|
147
|
+
: 'Observed transaction shape does not match every expected transfer field.',
|
|
148
|
+
checks,
|
|
149
|
+
observed: {
|
|
150
|
+
from: transaction.from || null,
|
|
151
|
+
tokenTarget: transaction.to || null,
|
|
152
|
+
nativeValue: transaction.value || null,
|
|
153
|
+
decoded,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function withTransferEvidence(result, transaction, expectedTransfer) {
|
|
159
|
+
const transferReview = reviewExpectedTransfer(transaction, expectedTransfer);
|
|
160
|
+
const evidenceVerdict = !result.rpcChainIdMatchesArcTestnet
|
|
161
|
+
? 'unknown_wrong_chain'
|
|
162
|
+
: result.rpcObjectHashesMatch === false
|
|
163
|
+
? 'unknown_hash_mismatch'
|
|
164
|
+
: transferReview.state === 'mismatch'
|
|
165
|
+
? 'mismatch_expected_transfer'
|
|
166
|
+
: transferReview.state === 'unknown'
|
|
167
|
+
? 'unknown_expected_transfer'
|
|
168
|
+
: `${result.state}_expected_transfer_shape`;
|
|
169
|
+
return {
|
|
170
|
+
...result,
|
|
171
|
+
expectedTransfer,
|
|
172
|
+
transferReview,
|
|
173
|
+
evidenceVerdict,
|
|
174
|
+
settlementProven: false,
|
|
175
|
+
businessAcceptanceProven: false,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function rpcCall(method, params = []) {
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
const timeout = window.setTimeout(() => controller.abort(), RPC_TIMEOUT_MS);
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetch(ARC_TRANSACTION_STATUS.rpcUrl, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: { 'Content-Type': 'application/json' },
|
|
186
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: RPC_REQUEST_ID, method, params }),
|
|
187
|
+
signal: controller.signal,
|
|
188
|
+
});
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new Error(`RPC HTTP ${response.status}`);
|
|
191
|
+
}
|
|
192
|
+
const responseText = await response.text();
|
|
193
|
+
if (new TextEncoder().encode(responseText).byteLength > MAX_RPC_RESPONSE_BYTES) {
|
|
194
|
+
throw new Error('RPC response exceeded the 1 MB safety limit');
|
|
195
|
+
}
|
|
196
|
+
const payload = JSON.parse(responseText);
|
|
197
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
198
|
+
throw new Error('RPC response must be a JSON object');
|
|
199
|
+
}
|
|
200
|
+
if (payload.jsonrpc !== '2.0' || payload.id !== RPC_REQUEST_ID) {
|
|
201
|
+
throw new Error('RPC response envelope did not match the request');
|
|
202
|
+
}
|
|
203
|
+
const hasResult = Object.prototype.hasOwnProperty.call(payload, 'result');
|
|
204
|
+
const hasError = Object.prototype.hasOwnProperty.call(payload, 'error');
|
|
205
|
+
if (hasResult === hasError) {
|
|
206
|
+
throw new Error('RPC response must contain exactly one result or error field');
|
|
207
|
+
}
|
|
208
|
+
if (hasError) {
|
|
209
|
+
const message = payload.error && typeof payload.error.message === 'string'
|
|
210
|
+
? payload.error.message.slice(0, 200)
|
|
211
|
+
: 'unknown RPC error';
|
|
212
|
+
throw new Error(`${method} failed: ${message}`);
|
|
213
|
+
}
|
|
214
|
+
return payload.result;
|
|
215
|
+
} finally {
|
|
216
|
+
window.clearTimeout(timeout);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function readOnlyLookup(transactionHash) {
|
|
221
|
+
const chainIdHex = await rpcCall('eth_chainId');
|
|
222
|
+
if (parseHexQuantity(chainIdHex) !== ARC_TRANSACTION_STATUS.expectedChainId) {
|
|
223
|
+
return { chainIdHex, transaction: null, receipt: null };
|
|
224
|
+
}
|
|
225
|
+
const transaction = await rpcCall('eth_getTransactionByHash', [transactionHash]);
|
|
226
|
+
const receipt = await rpcCall('eth_getTransactionReceipt', [transactionHash]);
|
|
227
|
+
return { chainIdHex, transaction, receipt };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function buildCheck(id, label, level, detail) {
|
|
231
|
+
return { id, label, level, detail };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function classifyTransactionStatus(chainIdHex, transaction, receipt, expectedTransfer, expectedTransactionHash) {
|
|
235
|
+
const chainIdDecimal = parseHexQuantity(chainIdHex);
|
|
236
|
+
const chainMatches = chainIdDecimal === ARC_TRANSACTION_STATUS.expectedChainId;
|
|
237
|
+
const transactionHashMatches = !transaction || hashMatchesExpected(transaction.hash, expectedTransactionHash);
|
|
238
|
+
const receiptHashMatches = !receipt || hashMatchesExpected(receipt.transactionHash, expectedTransactionHash);
|
|
239
|
+
const rpcObjectHashesMatch = transactionHashMatches && receiptHashMatches;
|
|
240
|
+
const base = {
|
|
241
|
+
kind: 'arc_testnet_transaction_status',
|
|
242
|
+
network: ARC_TRANSACTION_STATUS.network,
|
|
243
|
+
rpcUrl: ARC_TRANSACTION_STATUS.rpcUrl,
|
|
244
|
+
explorerUrl: ARC_TRANSACTION_STATUS.explorerUrl,
|
|
245
|
+
expectedChainId: ARC_TRANSACTION_STATUS.expectedChainId,
|
|
246
|
+
expectedChainIdHex: ARC_TRANSACTION_STATUS.expectedChainIdHex,
|
|
247
|
+
chainIdHex,
|
|
248
|
+
chainIdDecimal,
|
|
249
|
+
rpcChainIdMatchesArcTestnet: chainMatches,
|
|
250
|
+
expectedTransactionHash,
|
|
251
|
+
transactionHashMatches,
|
|
252
|
+
receiptHashMatches,
|
|
253
|
+
rpcObjectHashesMatch,
|
|
254
|
+
checkedAt: new Date().toISOString(),
|
|
255
|
+
safety: {
|
|
256
|
+
walletConnected: false,
|
|
257
|
+
backendCalls: false,
|
|
258
|
+
readOnlyRpcCheckOnly: true,
|
|
259
|
+
transactionBroadcast: false,
|
|
260
|
+
autonomousSpending: false,
|
|
261
|
+
humanApprovalRequired: true,
|
|
262
|
+
signingRequiresWalletChainGateAndHumanApproval: true,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
if (!chainMatches) {
|
|
267
|
+
return withTransferEvidence({
|
|
268
|
+
...base,
|
|
269
|
+
state: 'unknown',
|
|
270
|
+
reason: 'RPC chain ID did not match Arc Testnet.',
|
|
271
|
+
transactionFound: Boolean(transaction),
|
|
272
|
+
receiptFound: Boolean(receipt),
|
|
273
|
+
transaction,
|
|
274
|
+
receipt,
|
|
275
|
+
}, transaction, expectedTransfer);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!rpcObjectHashesMatch) {
|
|
279
|
+
return withTransferEvidence({
|
|
280
|
+
...base,
|
|
281
|
+
state: 'unknown',
|
|
282
|
+
reason: 'RPC returned a transaction or receipt for a different hash.',
|
|
283
|
+
transactionFound: Boolean(transaction),
|
|
284
|
+
receiptFound: Boolean(receipt),
|
|
285
|
+
observedTransactionHash: transaction && transaction.hash ? transaction.hash : null,
|
|
286
|
+
observedReceiptTransactionHash: receipt && receipt.transactionHash ? receipt.transactionHash : null,
|
|
287
|
+
}, null, expectedTransfer);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (receipt && receipt.status === '0x1') {
|
|
291
|
+
return withTransferEvidence({
|
|
292
|
+
...base,
|
|
293
|
+
state: 'confirmed',
|
|
294
|
+
reason: 'Transaction receipt exists and status is 0x1.',
|
|
295
|
+
transactionHash: receipt.transactionHash,
|
|
296
|
+
blockNumberHex: receipt.blockNumber,
|
|
297
|
+
blockNumberDecimal: parseHexQuantity(receipt.blockNumber),
|
|
298
|
+
transactionFound: Boolean(transaction),
|
|
299
|
+
receiptFound: true,
|
|
300
|
+
transaction,
|
|
301
|
+
receipt,
|
|
302
|
+
}, transaction, expectedTransfer);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (receipt && receipt.status === '0x0') {
|
|
306
|
+
return withTransferEvidence({
|
|
307
|
+
...base,
|
|
308
|
+
state: 'failed',
|
|
309
|
+
reason: 'Transaction receipt exists and status is 0x0.',
|
|
310
|
+
transactionHash: receipt.transactionHash,
|
|
311
|
+
blockNumberHex: receipt.blockNumber,
|
|
312
|
+
blockNumberDecimal: parseHexQuantity(receipt.blockNumber),
|
|
313
|
+
transactionFound: Boolean(transaction),
|
|
314
|
+
receiptFound: true,
|
|
315
|
+
transaction,
|
|
316
|
+
receipt,
|
|
317
|
+
}, transaction, expectedTransfer);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (transaction && !receipt) {
|
|
321
|
+
return withTransferEvidence({
|
|
322
|
+
...base,
|
|
323
|
+
state: 'pending',
|
|
324
|
+
reason: 'Transaction was found but no receipt is available yet.',
|
|
325
|
+
transactionHash: transaction.hash,
|
|
326
|
+
transactionFound: true,
|
|
327
|
+
receiptFound: false,
|
|
328
|
+
transaction,
|
|
329
|
+
receipt: null,
|
|
330
|
+
}, transaction, expectedTransfer);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return withTransferEvidence({
|
|
334
|
+
...base,
|
|
335
|
+
state: 'unknown',
|
|
336
|
+
reason: 'Transaction hash was not found or receipt status was ambiguous.',
|
|
337
|
+
transactionFound: Boolean(transaction),
|
|
338
|
+
receiptFound: Boolean(receipt),
|
|
339
|
+
transaction,
|
|
340
|
+
receipt,
|
|
341
|
+
}, transaction, expectedTransfer);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function checksForResult(result, hash) {
|
|
345
|
+
return [
|
|
346
|
+
buildCheck(
|
|
347
|
+
'hash',
|
|
348
|
+
'Transaction hash shape',
|
|
349
|
+
isValidHash(hash) ? 'pass' : 'fail',
|
|
350
|
+
'Expected a 32-byte 0x-prefixed transaction hash.'
|
|
351
|
+
),
|
|
352
|
+
buildCheck(
|
|
353
|
+
'chain',
|
|
354
|
+
'Arc Testnet chain ID',
|
|
355
|
+
result.rpcChainIdMatchesArcTestnet ? 'pass' : 'fail',
|
|
356
|
+
`Expected ${ARC_TRANSACTION_STATUS.expectedChainIdHex}; got ${result.chainIdHex || 'unavailable'}.`
|
|
357
|
+
),
|
|
358
|
+
buildCheck(
|
|
359
|
+
'method-scope',
|
|
360
|
+
'RPC method scope',
|
|
361
|
+
'pass',
|
|
362
|
+
'Used only eth_chainId, eth_getTransactionByHash, and eth_getTransactionReceipt.'
|
|
363
|
+
),
|
|
364
|
+
buildCheck(
|
|
365
|
+
'wallet',
|
|
366
|
+
'Wallet/signing boundary',
|
|
367
|
+
'pass',
|
|
368
|
+
'No wallet was connected and no transaction was submitted.'
|
|
369
|
+
),
|
|
370
|
+
buildCheck(
|
|
371
|
+
'state',
|
|
372
|
+
'Status state',
|
|
373
|
+
result.state === 'confirmed' ? 'pass' : result.state === 'failed' ? 'fail' : 'warn',
|
|
374
|
+
result.reason
|
|
375
|
+
),
|
|
376
|
+
buildCheck(
|
|
377
|
+
'expected-transfer',
|
|
378
|
+
'Expected USDC transfer shape',
|
|
379
|
+
result.transferReview && result.transferReview.state === 'match'
|
|
380
|
+
? 'pass'
|
|
381
|
+
: result.transferReview && result.transferReview.state === 'mismatch'
|
|
382
|
+
? 'fail'
|
|
383
|
+
: 'warn',
|
|
384
|
+
result.transferReview ? result.transferReview.reason : 'Expected transfer was not reviewed.'
|
|
385
|
+
),
|
|
386
|
+
buildCheck(
|
|
387
|
+
'settlement-boundary',
|
|
388
|
+
'Settlement boundary',
|
|
389
|
+
'warn',
|
|
390
|
+
'A receipt and matching calldata do not prove settlement, finality, or business acceptance.'
|
|
391
|
+
),
|
|
392
|
+
];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function renderChecks(checks) {
|
|
396
|
+
statusCheckList.replaceChildren(
|
|
397
|
+
...checks.map((item) => {
|
|
398
|
+
const listItem = document.createElement('li');
|
|
399
|
+
listItem.className = item.level;
|
|
400
|
+
const strong = document.createElement('strong');
|
|
401
|
+
strong.textContent = item.level === 'pass' ? 'PASS' : item.level === 'fail' ? 'REVIEW' : 'INFO';
|
|
402
|
+
listItem.append(strong, ` — ${item.label}: ${item.detail}`);
|
|
403
|
+
return listItem;
|
|
404
|
+
})
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function renderStatus(result, checks) {
|
|
409
|
+
statusPill.textContent = result.state;
|
|
410
|
+
statusPill.className = `status ${result.state}`;
|
|
411
|
+
renderChecks(checks);
|
|
412
|
+
transactionStatusJson.textContent = JSON.stringify(result, null, 2);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function renderInitialState() {
|
|
416
|
+
const result = {
|
|
417
|
+
kind: 'arc_testnet_transaction_status',
|
|
418
|
+
state: 'not_checked',
|
|
419
|
+
reason: 'No read-only lookup has run yet.',
|
|
420
|
+
network: ARC_TRANSACTION_STATUS.network,
|
|
421
|
+
rpcUrl: ARC_TRANSACTION_STATUS.rpcUrl,
|
|
422
|
+
expectedChainId: ARC_TRANSACTION_STATUS.expectedChainId,
|
|
423
|
+
expectedChainIdHex: ARC_TRANSACTION_STATUS.expectedChainIdHex,
|
|
424
|
+
evidenceVerdict: 'not_checked',
|
|
425
|
+
settlementProven: false,
|
|
426
|
+
businessAcceptanceProven: false,
|
|
427
|
+
safety: {
|
|
428
|
+
walletConnected: false,
|
|
429
|
+
backendCalls: false,
|
|
430
|
+
readOnlyRpcCheckOnly: true,
|
|
431
|
+
transactionBroadcast: false,
|
|
432
|
+
autonomousSpending: false,
|
|
433
|
+
humanApprovalRequired: true,
|
|
434
|
+
signingRequiresWalletChainGateAndHumanApproval: true,
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
renderStatus(result, [
|
|
438
|
+
buildCheck('state', 'Status state', 'warn', 'Paste a transaction hash and run the read-only lookup.'),
|
|
439
|
+
buildCheck('wallet', 'Wallet/signing boundary', 'pass', 'No wallet was connected and no transaction was submitted.'),
|
|
440
|
+
]);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function renderValidationError(hash, reason = 'Transaction hash is not a 32-byte 0x-prefixed value.') {
|
|
444
|
+
const result = {
|
|
445
|
+
kind: 'arc_testnet_transaction_status',
|
|
446
|
+
state: 'unknown',
|
|
447
|
+
reason,
|
|
448
|
+
checkedAt: new Date().toISOString(),
|
|
449
|
+
transactionHash: hash,
|
|
450
|
+
safety: {
|
|
451
|
+
walletConnected: false,
|
|
452
|
+
backendCalls: false,
|
|
453
|
+
readOnlyRpcCheckOnly: true,
|
|
454
|
+
transactionBroadcast: false,
|
|
455
|
+
autonomousSpending: false,
|
|
456
|
+
humanApprovalRequired: true,
|
|
457
|
+
signingRequiresWalletChainGateAndHumanApproval: true,
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
renderStatus(result, checksForResult({ ...result, rpcChainIdMatchesArcTestnet: false, chainIdHex: 'not_checked' }, hash));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function checkTransactionStatus() {
|
|
464
|
+
const hash = transactionHashInput.value.trim();
|
|
465
|
+
if (!isValidHash(hash)) {
|
|
466
|
+
renderValidationError(hash);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
let expectedTransfer;
|
|
470
|
+
try {
|
|
471
|
+
expectedTransfer = buildExpectedTransfer();
|
|
472
|
+
} catch (error) {
|
|
473
|
+
renderValidationError(hash, error.message);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
statusPill.textContent = 'checking';
|
|
477
|
+
statusPill.className = 'status pending';
|
|
478
|
+
transactionStatusJson.textContent = JSON.stringify({ state: 'checking_arc_rpc', transactionHash: hash }, null, 2);
|
|
479
|
+
try {
|
|
480
|
+
const lookup = await readOnlyLookup(hash);
|
|
481
|
+
const result = classifyTransactionStatus(lookup.chainIdHex, lookup.transaction, lookup.receipt, expectedTransfer, hash);
|
|
482
|
+
renderStatus({ ...result, transactionHash: hash }, checksForResult(result, hash));
|
|
483
|
+
} catch (error) {
|
|
484
|
+
const result = {
|
|
485
|
+
kind: 'arc_testnet_transaction_status',
|
|
486
|
+
state: 'unknown',
|
|
487
|
+
reason: `RPC lookup unavailable: ${error.message}`,
|
|
488
|
+
checkedAt: new Date().toISOString(),
|
|
489
|
+
transactionHash: hash,
|
|
490
|
+
rpcUrl: ARC_TRANSACTION_STATUS.rpcUrl,
|
|
491
|
+
expectedTransfer,
|
|
492
|
+
evidenceVerdict: 'unknown_rpc_unavailable',
|
|
493
|
+
settlementProven: false,
|
|
494
|
+
businessAcceptanceProven: false,
|
|
495
|
+
safety: {
|
|
496
|
+
walletConnected: false,
|
|
497
|
+
backendCalls: false,
|
|
498
|
+
readOnlyRpcCheckOnly: true,
|
|
499
|
+
transactionBroadcast: false,
|
|
500
|
+
autonomousSpending: false,
|
|
501
|
+
humanApprovalRequired: true,
|
|
502
|
+
signingRequiresWalletChainGateAndHumanApproval: true,
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
renderStatus(result, checksForResult({ ...result, rpcChainIdMatchesArcTestnet: false, chainIdHex: 'unavailable' }, hash));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function resetSample() {
|
|
510
|
+
transactionHashInput.value = SAMPLE_TRANSACTION_HASH;
|
|
511
|
+
expectedRecipientInput.value = '0x1111111111111111111111111111111111111111';
|
|
512
|
+
expectedAmountInput.value = '0.01';
|
|
513
|
+
renderInitialState();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
checkButton.addEventListener('click', checkTransactionStatus);
|
|
517
|
+
resetButton.addEventListener('click', resetSample);
|
|
518
|
+
renderInitialState();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Placeholder only. Do not put real secrets in this file.
|
|
2
|
+
# Copy to .env locally, then fill values in your deployment platform's secret store.
|
|
3
|
+
|
|
4
|
+
# Local demo defaults only. These keep the bundled x402 challenge server safe.
|
|
5
|
+
X402_DEMO_NETWORK=arc-testnet
|
|
6
|
+
X402_DEMO_ASSET=USDC
|
|
7
|
+
X402_DEMO_AMOUNT=0.01
|
|
8
|
+
X402_DEMO_PAY_TO=0xA11CE00000000000000000000000000000000000
|
|
9
|
+
X402_DEMO_MAINNET_ENABLED=false
|
|
10
|
+
|
|
11
|
+
# Production-shaped paid-agent endpoint smoke checks.
|
|
12
|
+
# ARC_PAID_AGENT_URL should point at the deployed protected endpoint, for example
|
|
13
|
+
# https://your-service.example/protected. The live smoke script first checks an
|
|
14
|
+
# unpaid 402 challenge and never creates payments by itself.
|
|
15
|
+
ARC_PAID_AGENT_URL=
|
|
16
|
+
EXPECT_402_ONLY=true
|
|
17
|
+
|
|
18
|
+
# Optional: paste an externally created x402/Circle Gateway proof only in your
|
|
19
|
+
# private shell/CI secret store when you want to test the paid retry path.
|
|
20
|
+
ARC_LIVE_X_PAYMENT=
|
|
21
|
+
|
|
22
|
+
# Future verifier handoff placeholders. Keep these empty in the public repo.
|
|
23
|
+
# Store real values only in a deployment secret manager.
|
|
24
|
+
CIRCLE_GATEWAY_API_KEY=
|
|
25
|
+
X402_GATEWAY_VERIFIER_URL=
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Local x402 challenge server demo
|
|
2
|
+
|
|
3
|
+
This example is a production-shaped boundary for a future Arc/Circle/x402 payment verifier. It is intentionally local-only:
|
|
4
|
+
|
|
5
|
+
- returns an HTTP `402 Payment Required` challenge for `/protected`;
|
|
6
|
+
- accepts only an explicit local demo proof shape;
|
|
7
|
+
- never opens a wallet, broadcasts transactions, or claims settlement;
|
|
8
|
+
- keeps human approval and mainnet disabled by default.
|
|
9
|
+
- refuses non-loopback HTTP bind hosts and invalid ports.
|
|
10
|
+
|
|
11
|
+
## Run locally
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python3 examples/x402-local-challenge-server/server.py --port 8087
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
HTTP mode accepts only `127.0.0.1` or `localhost`. Use a separately reviewed
|
|
18
|
+
deployment implementation for remote or staging access; do not expose the
|
|
19
|
+
deterministic local proof verifier on a network interface.
|
|
20
|
+
|
|
21
|
+
The server reads safe local overrides from environment variables documented in
|
|
22
|
+
[`../../.env.example`](../../.env.example). Defaults stay Arc Testnet only:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
X402_DEMO_AMOUNT=0.05 \
|
|
26
|
+
python3 examples/x402-local-challenge-server/server.py --print-challenge
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Unsafe overrides are rejected with clear errors: non-Arc network, any asset
|
|
30
|
+
other than the pinned `USDC` economics, `X402_DEMO_MAINNET_ENABLED=true`,
|
|
31
|
+
non-positive or over-precision amounts, and malformed or zero
|
|
32
|
+
`X402_DEMO_PAY_TO` addresses. The deterministic local proof is bound to the
|
|
33
|
+
reviewed pay-to address through the challenge id.
|
|
34
|
+
|
|
35
|
+
The same safety contract is enforced when Python helpers are called directly:
|
|
36
|
+
the reviewed resource, `local-simulation` verifier mode, mandatory human
|
|
37
|
+
approval, Arc Testnet, USDC, and disabled mainnet settings cannot be bypassed
|
|
38
|
+
with a manually constructed `PaymentConfig`.
|
|
39
|
+
|
|
40
|
+
In another terminal:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
curl -i http://127.0.0.1:8087/protected
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The response includes a challenge id plus an `mcpManifest` object with safe paid-agent tool metadata. To simulate approval, send:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
curl -i \
|
|
50
|
+
-H 'X-Payment: local-demo:<challenge-id>:0.01' \
|
|
51
|
+
http://127.0.0.1:8087/protected
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## MCP-style manifest
|
|
55
|
+
|
|
56
|
+
The unpaid `402` response includes `mcpManifest`, a machine-readable discovery contract for future agents. It lists the local tool surface, Arc Testnet constants, integer microUSD unit economics, safety flags, and the Circle Gateway/x402 production replacement boundary.
|
|
57
|
+
|
|
58
|
+
The same local tool surface is available through a dependency-free JSON-RPC/MCP-style stdio mode:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
printf '{"jsonrpc":"2.0","id":"tools","method":"tools/list"}\n' \
|
|
62
|
+
| python3 examples/x402-local-challenge-server/server.py --mcp-stdio
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The stdio parser rejects requests over 1 MB, invalid UTF-8, duplicate JSON
|
|
66
|
+
keys, non-`2.0` JSON-RPC envelopes, invalid request IDs, and non-string
|
|
67
|
+
methods before dispatching a tool. Runtime dispatch also enforces each
|
|
68
|
+
manifest `additionalProperties: false` contract for request, params, and tool
|
|
69
|
+
arguments.
|
|
70
|
+
|
|
71
|
+
HTTP and helper proof inputs accept exactly one `X-Payment` value, reject
|
|
72
|
+
control characters, and enforce a 4 KB maximum before verifier dispatch.
|
|
73
|
+
Verifier exceptions, malformed verifier results, and any successful verifier
|
|
74
|
+
receipt that does not keep both `settled` and `transactionBroadcast`
|
|
75
|
+
explicitly `false` are converted into a safe `402` response without reflecting
|
|
76
|
+
internal verifier failure details.
|
|
77
|
+
|
|
78
|
+
Quick JSON helpers:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
python3 examples/x402-local-challenge-server/server.py --print-manifest
|
|
82
|
+
python3 examples/x402-local-challenge-server/server.py --print-challenge
|
|
83
|
+
python3 examples/x402-local-challenge-server/server.py --verify-payment 'local-demo:<challenge-id>:0.01'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
These helpers never open wallets, accept private keys, broadcast transactions, or claim settlement.
|
|
87
|
+
|
|
88
|
+
See [x402 MCP manifest](../../docs/x402-mcp-manifest.md) for the field-by-field walkthrough.
|
|
89
|
+
|
|
90
|
+
## Boundary contract
|
|
91
|
+
|
|
92
|
+
The swappable production boundary is the `PaymentVerifier` protocol in `server.py`:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
class PaymentVerifier(Protocol):
|
|
96
|
+
def verify(self, proof: str, challenge: Mapping[str, object], config: PaymentConfig) -> VerificationResult:
|
|
97
|
+
...
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
A real verifier should replace `LocalDemoVerifier` only after these rules are explicit:
|
|
101
|
+
|
|
102
|
+
- exact supported network and asset, e.g. Arc Testnet USDC;
|
|
103
|
+
- recipient address ownership and rotation policy;
|
|
104
|
+
- proof format, expiry, replay protection, and nonce storage;
|
|
105
|
+
- settlement finality requirements;
|
|
106
|
+
- logging and privacy policy for payment proofs;
|
|
107
|
+
- failure behavior when the verifier is unavailable.
|
|
108
|
+
|
|
109
|
+
## Safety posture
|
|
110
|
+
|
|
111
|
+
This demo is not a mainnet payment processor. `mainnetEnabled` and `transactionBroadcast` remain `false` in both the challenge and receipt. The local proof is only a deterministic UI/API switch for demonstrating the 402 -> proof -> 200 flow.
|