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,472 @@
1
+ const ARC_TESTNET = Object.freeze({
2
+ name: 'Arc Testnet',
3
+ chainId: 5042002,
4
+ chainIdHex: '0x4cef52',
5
+ rpcUrl: 'https://rpc.testnet.arc.network',
6
+ explorerUrl: 'https://testnet.arcscan.app',
7
+ usdcAddress: '0x3600000000000000000000000000000000000000',
8
+ usdcDecimals: 6,
9
+ nativeGasDecimals: 18,
10
+ maxAmountBaseUnits: 1000000n,
11
+ });
12
+
13
+ const REQUIRED_CONFIRMATION = 'SEND ARC TESTNET USDC';
14
+ const FEATURE_GATE_NAME = 'enableArcTestnetSend';
15
+ const FEATURE_GATE_VALUE = 'reviewed-testnet-only';
16
+ const TRANSFER_SELECTOR = 'a9059cbb';
17
+ const ALLOWED_WALLET_METHODS = new Set([
18
+ 'eth_requestAccounts',
19
+ 'eth_accounts',
20
+ 'eth_chainId',
21
+ 'wallet_switchEthereumChain',
22
+ 'wallet_addEthereumChain',
23
+ 'eth_sendTransaction',
24
+ ]);
25
+
26
+ const elements = {
27
+ featureGateStatus: document.querySelector('#feature-gate-status'),
28
+ riskAcknowledgement: document.querySelector('#risk-acknowledgement'),
29
+ walletStatus: document.querySelector('#wallet-status'),
30
+ providerState: document.querySelector('#provider-state'),
31
+ accountState: document.querySelector('#account-state'),
32
+ chainState: document.querySelector('#chain-state'),
33
+ connectWallet: document.querySelector('#connect-wallet'),
34
+ switchNetwork: document.querySelector('#switch-network'),
35
+ recipient: document.querySelector('#recipient'),
36
+ amount: document.querySelector('#amount'),
37
+ expiry: document.querySelector('#expiry'),
38
+ memo: document.querySelector('#memo'),
39
+ freezeIntent: document.querySelector('#freeze-intent'),
40
+ intentStatus: document.querySelector('#intent-status'),
41
+ frozenPayload: document.querySelector('#frozen-payload'),
42
+ confirmationPhrase: document.querySelector('#confirmation-phrase'),
43
+ finalSendConfirmation: document.querySelector('#final-send-confirmation'),
44
+ sendTransaction: document.querySelector('#send-transaction'),
45
+ sendStatus: document.querySelector('#send-status'),
46
+ sendResult: document.querySelector('#send-result'),
47
+ transactionLink: document.querySelector('#transaction-link'),
48
+ guardList: document.querySelector('#guard-list'),
49
+ methodLog: document.querySelector('#method-log'),
50
+ };
51
+
52
+ const state = {
53
+ featureGatePresent: new URLSearchParams(window.location.search).get(FEATURE_GATE_NAME) === FEATURE_GATE_VALUE,
54
+ topLevelContext: window.top === window.self,
55
+ provider: window.ethereum || null,
56
+ account: '',
57
+ chainIdHex: '',
58
+ frozenIntent: null,
59
+ frozenDraft: null,
60
+ sendAttempted: false,
61
+ transactionHash: '',
62
+ methodNames: [],
63
+ };
64
+
65
+ function normalizeHex(value) {
66
+ return String(value || '').toLowerCase();
67
+ }
68
+
69
+ function isAddress(value) {
70
+ return /^0x[a-fA-F0-9]{40}$/.test(String(value || '').trim());
71
+ }
72
+
73
+ function isNonZeroAddress(value) {
74
+ return isAddress(value) && !/^0x0{40}$/i.test(String(value || '').trim());
75
+ }
76
+
77
+ function parseUsdcAmount(rawValue) {
78
+ const value = String(rawValue || '').trim();
79
+ if (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/.test(value)) {
80
+ throw new Error('Amount must be a plain positive decimal with at most 6 fractional digits.');
81
+ }
82
+ const [whole, fraction = ''] = value.split('.');
83
+ const baseUnits = (BigInt(whole) * 1000000n) + BigInt(fraction.padEnd(6, '0'));
84
+ if (baseUnits <= 0n) throw new Error('Amount must be greater than zero.');
85
+ if (baseUnits > ARC_TESTNET.maxAmountBaseUnits) throw new Error('Amount exceeds the 1.00 USDC safety cap.');
86
+ return { decimal: value, baseUnits };
87
+ }
88
+
89
+ function encodeTransferCalldata(recipient, amountBaseUnits) {
90
+ const addressWord = recipient.toLowerCase().replace(/^0x/, '').padStart(64, '0');
91
+ const amountWord = amountBaseUnits.toString(16).padStart(64, '0');
92
+ return `0x${TRANSFER_SELECTOR}${addressWord}${amountWord}`;
93
+ }
94
+
95
+ function decodeTransferCalldata(data) {
96
+ const normalized = String(data || '').toLowerCase();
97
+ if (!new RegExp(`^0x${TRANSFER_SELECTOR}[0-9a-f]{128}$`).test(normalized)) {
98
+ throw new Error('Calldata is not the pinned ERC-20 transfer(address,uint256) shape.');
99
+ }
100
+ return {
101
+ method: 'transfer(address,uint256)',
102
+ recipient: `0x${normalized.slice(34, 74)}`,
103
+ amountBaseUnits: BigInt(`0x${normalized.slice(74, 138)}`).toString(),
104
+ };
105
+ }
106
+
107
+ function currentIntent() {
108
+ const recipient = elements.recipient.value.trim().toLowerCase();
109
+ if (!isNonZeroAddress(recipient)) {
110
+ throw new Error('Recipient must be a non-zero 0x-prefixed 20-byte address.');
111
+ }
112
+ if (recipient === ARC_TESTNET.usdcAddress.toLowerCase()) {
113
+ throw new Error('Recipient cannot be the pinned USDC token contract address.');
114
+ }
115
+ const amount = parseUsdcAmount(elements.amount.value);
116
+ const expiryText = elements.expiry.value;
117
+ const expiryMs = Date.parse(expiryText);
118
+ const now = Date.now();
119
+ if (!expiryText || !Number.isFinite(expiryMs) || expiryMs <= now) {
120
+ throw new Error('Expiry must be a valid future time.');
121
+ }
122
+ if (expiryMs > now + (24 * 60 * 60 * 1000)) {
123
+ throw new Error('Expiry must be within 24 hours.');
124
+ }
125
+ const memo = elements.memo.value.trim();
126
+ if (!memo) throw new Error('Memo is required for visible intent binding.');
127
+ if (memo.length > 180) throw new Error('Memo must be 180 characters or fewer.');
128
+ return {
129
+ network: ARC_TESTNET.name,
130
+ chainId: ARC_TESTNET.chainId,
131
+ chainIdHex: ARC_TESTNET.chainIdHex,
132
+ account: state.account.toLowerCase(),
133
+ token: ARC_TESTNET.usdcAddress,
134
+ tokenDecimals: ARC_TESTNET.usdcDecimals,
135
+ recipient,
136
+ amountDecimal: amount.decimal,
137
+ amountBaseUnits: amount.baseUnits.toString(),
138
+ memo,
139
+ expiry: new Date(expiryMs).toISOString(),
140
+ };
141
+ }
142
+
143
+ function buildDraft(intent) {
144
+ const data = encodeTransferCalldata(intent.recipient, BigInt(intent.amountBaseUnits));
145
+ const decoded = decodeTransferCalldata(data);
146
+ return {
147
+ type: 'guarded_arc_testnet_erc20_transfer',
148
+ chainId: ARC_TESTNET.chainIdHex,
149
+ from: intent.account,
150
+ to: ARC_TESTNET.usdcAddress,
151
+ value: '0x0',
152
+ data,
153
+ decoded,
154
+ };
155
+ }
156
+
157
+ function intentMatchesFrozen() {
158
+ if (!state.frozenIntent) return false;
159
+ try {
160
+ return JSON.stringify(currentIntent()) === JSON.stringify(state.frozenIntent);
161
+ } catch (_error) {
162
+ return false;
163
+ }
164
+ }
165
+
166
+ function draftMatchesFrozenIntent() {
167
+ if (!state.frozenIntent || !state.frozenDraft) return false;
168
+ try {
169
+ const rebuilt = buildDraft(state.frozenIntent);
170
+ return (
171
+ JSON.stringify(rebuilt) === JSON.stringify(state.frozenDraft)
172
+ && rebuilt.chainId === ARC_TESTNET.chainIdHex
173
+ && rebuilt.to === ARC_TESTNET.usdcAddress
174
+ && rebuilt.value === '0x0'
175
+ && rebuilt.decoded.recipient === state.frozenIntent.recipient
176
+ && rebuilt.decoded.amountBaseUnits === state.frozenIntent.amountBaseUnits
177
+ );
178
+ } catch (_error) {
179
+ return false;
180
+ }
181
+ }
182
+
183
+ function buildGuardReport() {
184
+ const riskAcknowledged = elements.riskAcknowledgement.checked;
185
+ const providerAvailable = Boolean(state.provider && typeof state.provider.request === 'function');
186
+ const accountValid = isNonZeroAddress(state.account);
187
+ const chainMatches = normalizeHex(state.chainIdHex) === ARC_TESTNET.chainIdHex;
188
+ const phraseMatches = elements.confirmationPhrase.value === REQUIRED_CONFIRMATION;
189
+ const finalChecked = elements.finalSendConfirmation.checked;
190
+ let intentValid = false;
191
+ let intentError = '';
192
+ try {
193
+ currentIntent();
194
+ intentValid = true;
195
+ } catch (error) {
196
+ intentError = error.message;
197
+ }
198
+ const frozen = Boolean(state.frozenIntent && state.frozenDraft);
199
+ const frozenParity = frozen && intentMatchesFrozen() && draftMatchesFrozenIntent();
200
+ const checks = [
201
+ ['feature-gate', state.featureGatePresent, 'Exact reviewed-testnet query gate is present.'],
202
+ ['top-level-context', state.topLevelContext, 'Page is running in a top-level browsing context, not an embedded frame.'],
203
+ ['risk-ack', riskAcknowledged, 'Risk acknowledgement is checked.'],
204
+ ['provider', providerAvailable, 'Injected EVM wallet provider is available.'],
205
+ ['account', accountValid, 'Connected account is a valid EVM address.'],
206
+ ['chain', chainMatches, `Wallet reports Arc Testnet ${ARC_TESTNET.chainIdHex}.`],
207
+ ['intent', intentValid, intentValid ? 'Intent fields are valid.' : intentError],
208
+ ['frozen-parity', frozenParity, 'Current fields and deterministic calldata match the frozen review.'],
209
+ ['typed-confirmation', phraseMatches, 'Exact confirmation phrase matches.'],
210
+ ['final-checkbox', finalChecked, 'Final human confirmation is checked.'],
211
+ ['one-shot-lock', !state.sendAttempted, 'No wallet transaction attempt has occurred this page load.'],
212
+ ];
213
+ return {
214
+ passed: checks.every((check) => check[1]),
215
+ checks: checks.map(([id, passed, detail]) => ({ id, passed, detail })),
216
+ };
217
+ }
218
+
219
+ function canAttemptSend() {
220
+ return buildGuardReport().passed;
221
+ }
222
+
223
+ function recordMethod(method) {
224
+ state.methodNames.push({ method, requestedAt: new Date().toISOString() });
225
+ renderMethodLog();
226
+ }
227
+
228
+ async function requestWallet(request) {
229
+ if (!state.provider || typeof state.provider.request !== 'function') {
230
+ throw new Error('Injected wallet provider is unavailable.');
231
+ }
232
+ if (!ALLOWED_WALLET_METHODS.has(request.method)) {
233
+ throw new Error('Wallet method is outside the reviewed Arc Testnet allowlist.');
234
+ }
235
+ recordMethod(request.method);
236
+ return state.provider.request(request);
237
+ }
238
+
239
+ function safeWalletError(error) {
240
+ const code = Number(error && error.code);
241
+ if (code === 4001) return 'Wallet request was rejected by the user.';
242
+ if (code === 4902) return 'Arc Testnet is not configured in this wallet.';
243
+ if (code === -32002) return 'A wallet request is already pending. Open the wallet extension.';
244
+ return 'Wallet request failed. Review the wallet UI and reload before another send attempt.';
245
+ }
246
+
247
+ function clearFrozenReview(reason) {
248
+ state.frozenIntent = null;
249
+ state.frozenDraft = null;
250
+ elements.finalSendConfirmation.checked = false;
251
+ elements.confirmationPhrase.value = '';
252
+ elements.frozenPayload.textContent = reason;
253
+ }
254
+
255
+ async function connectWallet() {
256
+ if (!state.featureGatePresent || !state.topLevelContext || !elements.riskAcknowledgement.checked) return;
257
+ try {
258
+ const accounts = await requestWallet({ method: 'eth_requestAccounts', params: [] });
259
+ const chainIdHex = await requestWallet({ method: 'eth_chainId', params: [] });
260
+ state.account = Array.isArray(accounts) && isNonZeroAddress(accounts[0]) ? accounts[0] : '';
261
+ state.chainIdHex = normalizeHex(chainIdHex);
262
+ clearFrozenReview('Wallet state changed. Freeze the intent again after reviewing the connected account and chain.');
263
+ } catch (error) {
264
+ elements.sendResult.textContent = safeWalletError(error);
265
+ }
266
+ render();
267
+ }
268
+
269
+ async function switchToArcTestnet() {
270
+ if (!state.featureGatePresent || !state.topLevelContext || !elements.riskAcknowledgement.checked) return;
271
+ try {
272
+ try {
273
+ await requestWallet({
274
+ method: 'wallet_switchEthereumChain',
275
+ params: [{ chainId: ARC_TESTNET.chainIdHex }],
276
+ });
277
+ } catch (error) {
278
+ if (Number(error && error.code) !== 4902) throw error;
279
+ await requestWallet({
280
+ method: 'wallet_addEthereumChain',
281
+ params: [{
282
+ chainId: ARC_TESTNET.chainIdHex,
283
+ chainName: ARC_TESTNET.name,
284
+ nativeCurrency: { name: 'USDC', symbol: 'USDC', decimals: ARC_TESTNET.nativeGasDecimals },
285
+ rpcUrls: [ARC_TESTNET.rpcUrl],
286
+ blockExplorerUrls: [ARC_TESTNET.explorerUrl],
287
+ }],
288
+ });
289
+ }
290
+ state.chainIdHex = normalizeHex(await requestWallet({ method: 'eth_chainId', params: [] }));
291
+ clearFrozenReview('Network state changed. Freeze the intent again after proving Arc Testnet.');
292
+ } catch (error) {
293
+ elements.sendResult.textContent = safeWalletError(error);
294
+ }
295
+ render();
296
+ }
297
+
298
+ function freezeIntent() {
299
+ try {
300
+ const report = buildGuardReport();
301
+ const prerequisiteIds = ['feature-gate', 'top-level-context', 'risk-ack', 'provider', 'account', 'chain', 'intent'];
302
+ const failedPrerequisite = report.checks.find((check) => prerequisiteIds.includes(check.id) && !check.passed);
303
+ if (failedPrerequisite) {
304
+ throw new Error(failedPrerequisite.detail || 'Resolve the visible intent guards before freezing.');
305
+ }
306
+ state.frozenIntent = currentIntent();
307
+ state.frozenDraft = buildDraft(state.frozenIntent);
308
+ elements.finalSendConfirmation.checked = false;
309
+ elements.confirmationPhrase.value = '';
310
+ elements.frozenPayload.textContent = JSON.stringify({
311
+ intent: state.frozenIntent,
312
+ transactionRequest: state.frozenDraft,
313
+ safety: {
314
+ humanApprovalRequired: true,
315
+ oneAttemptPerPageLoad: true,
316
+ automaticRetry: false,
317
+ retryRule: 'No automatic retry',
318
+ privateKeyHandling: false,
319
+ custody: false,
320
+ },
321
+ }, null, 2);
322
+ elements.sendResult.textContent = 'Intent frozen. Compare every field before final confirmation.';
323
+ } catch (error) {
324
+ clearFrozenReview(error.message);
325
+ elements.sendResult.textContent = error.message;
326
+ }
327
+ render();
328
+ }
329
+
330
+ async function requestOneTransaction() {
331
+ if (!canAttemptSend()) return;
332
+ state.sendAttempted = true;
333
+ render();
334
+ try {
335
+ const liveChain = normalizeHex(await requestWallet({ method: 'eth_chainId', params: [] }));
336
+ const liveAccounts = await requestWallet({ method: 'eth_accounts', params: [] });
337
+ const liveAccount = Array.isArray(liveAccounts) && isNonZeroAddress(liveAccounts[0]) ? liveAccounts[0].toLowerCase() : '';
338
+ if (liveChain !== ARC_TESTNET.chainIdHex || liveAccount !== state.frozenIntent.account) {
339
+ throw new Error('Wallet chain or account changed after review.');
340
+ }
341
+ if (!intentMatchesFrozen() || !draftMatchesFrozenIntent()) {
342
+ throw new Error('Frozen payload parity failed immediately before wallet handoff.');
343
+ }
344
+
345
+ const transactionHash = await requestWallet({
346
+ method: 'eth_sendTransaction',
347
+ params: [{
348
+ chainId: state.frozenDraft.chainId,
349
+ from: state.frozenDraft.from,
350
+ to: state.frozenDraft.to,
351
+ value: state.frozenDraft.value,
352
+ data: state.frozenDraft.data,
353
+ }],
354
+ });
355
+ if (!/^0x[a-fA-F0-9]{64}$/.test(String(transactionHash || ''))) {
356
+ throw new Error('Wallet did not return a valid transaction hash.');
357
+ }
358
+ state.transactionHash = transactionHash;
359
+ elements.sendResult.textContent = 'Wallet returned a transaction hash. Status is submitted/pending, not confirmed.';
360
+ elements.transactionLink.href = `${ARC_TESTNET.explorerUrl}/tx/${transactionHash}`;
361
+ elements.transactionLink.hidden = false;
362
+ } catch (error) {
363
+ const internalMessage = error instanceof Error ? error.message : '';
364
+ elements.sendResult.textContent = internalMessage.startsWith('Wallet chain') || internalMessage.startsWith('Frozen payload')
365
+ ? internalMessage
366
+ : safeWalletError(error);
367
+ }
368
+ render();
369
+ }
370
+
371
+ function renderMethodLog() {
372
+ if (!state.methodNames.length) {
373
+ elements.methodLog.replaceChildren(Object.assign(document.createElement('li'), { textContent: 'No wallet methods requested.' }));
374
+ return;
375
+ }
376
+ elements.methodLog.replaceChildren(...state.methodNames.map((entry) => {
377
+ const item = document.createElement('li');
378
+ const code = document.createElement('code');
379
+ code.textContent = entry.method;
380
+ item.append(code, ` at ${entry.requestedAt}`);
381
+ return item;
382
+ }));
383
+ }
384
+
385
+ function renderGuards() {
386
+ const report = buildGuardReport();
387
+ elements.guardList.replaceChildren(...report.checks.map((check) => {
388
+ const item = document.createElement('li');
389
+ const verdict = document.createElement('strong');
390
+ verdict.className = check.passed ? 'pass' : 'fail';
391
+ verdict.textContent = check.passed ? 'PASS' : 'BLOCK';
392
+ item.append(verdict, check.detail);
393
+ return item;
394
+ }));
395
+ return report;
396
+ }
397
+
398
+ function render() {
399
+ const acknowledged = elements.riskAcknowledgement.checked;
400
+ const providerAvailable = Boolean(state.provider && typeof state.provider.request === 'function');
401
+ const chainMatches = normalizeHex(state.chainIdHex) === ARC_TESTNET.chainIdHex;
402
+ const report = renderGuards();
403
+
404
+ const reviewEnabled = state.featureGatePresent && state.topLevelContext && acknowledged;
405
+ elements.featureGateStatus.textContent = !state.topLevelContext ? 'blocked in embedded frame' : reviewEnabled ? 'enabled for review' : 'disabled';
406
+ elements.featureGateStatus.className = `status ${reviewEnabled ? 'pass' : 'fail'}`;
407
+ elements.providerState.textContent = providerAvailable ? 'Injected provider detected' : 'No injected provider';
408
+ elements.accountState.textContent = state.account || 'Not requested';
409
+ elements.chainState.textContent = state.chainIdHex || 'Not requested';
410
+ elements.walletStatus.textContent = state.account && chainMatches ? 'Arc Testnet proven' : 'not ready';
411
+ elements.walletStatus.className = `status ${state.account && chainMatches ? 'pass' : 'warn'}`;
412
+ elements.intentStatus.textContent = state.frozenIntent && intentMatchesFrozen() ? 'frozen / parity pass' : 'editable';
413
+ elements.intentStatus.className = `status ${state.frozenIntent && intentMatchesFrozen() ? 'pass' : 'warn'}`;
414
+ elements.sendStatus.textContent = state.transactionHash ? 'submitted / pending' : state.sendAttempted ? 'attempt locked' : report.passed ? 'ready for wallet' : 'blocked';
415
+ elements.sendStatus.className = `status ${state.transactionHash ? 'warn' : report.passed ? 'pass' : 'fail'}`;
416
+
417
+ elements.riskAcknowledgement.disabled = !state.featureGatePresent || !state.topLevelContext || state.sendAttempted;
418
+ elements.connectWallet.disabled = !state.featureGatePresent || !state.topLevelContext || !acknowledged || !providerAvailable || state.sendAttempted;
419
+ elements.switchNetwork.disabled = !state.featureGatePresent || !state.topLevelContext || !acknowledged || !providerAvailable || state.sendAttempted;
420
+ elements.freezeIntent.disabled = !state.featureGatePresent || !state.topLevelContext || !acknowledged || !providerAvailable || !state.account || !chainMatches || state.sendAttempted;
421
+ elements.sendTransaction.disabled = !report.passed;
422
+ for (const input of [elements.recipient, elements.amount, elements.expiry, elements.memo, elements.confirmationPhrase, elements.finalSendConfirmation]) {
423
+ input.disabled = state.sendAttempted;
424
+ }
425
+ renderMethodLog();
426
+ }
427
+
428
+ function setDefaultExpiry() {
429
+ const future = new Date(Date.now() + (30 * 60 * 1000));
430
+ const local = new Date(future.getTime() - (future.getTimezoneOffset() * 60000));
431
+ elements.expiry.value = local.toISOString().slice(0, 16);
432
+ }
433
+
434
+ elements.riskAcknowledgement.addEventListener('change', () => {
435
+ if (!elements.riskAcknowledgement.checked) {
436
+ clearFrozenReview('Risk acknowledgement cleared. Freeze and review the intent again.');
437
+ }
438
+ render();
439
+ });
440
+ elements.connectWallet.addEventListener('click', connectWallet);
441
+ elements.switchNetwork.addEventListener('click', switchToArcTestnet);
442
+ elements.freezeIntent.addEventListener('click', freezeIntent);
443
+ elements.sendTransaction.addEventListener('click', requestOneTransaction);
444
+ for (const input of [elements.recipient, elements.amount, elements.expiry, elements.memo, elements.confirmationPhrase, elements.finalSendConfirmation]) {
445
+ input.addEventListener('input', render);
446
+ input.addEventListener('change', render);
447
+ }
448
+
449
+ if (state.provider && typeof state.provider.on === 'function') {
450
+ state.provider.on('accountsChanged', (accounts) => {
451
+ state.account = Array.isArray(accounts) && isNonZeroAddress(accounts[0]) ? accounts[0] : '';
452
+ clearFrozenReview('Wallet account changed. Freeze and review the intent again.');
453
+ render();
454
+ });
455
+ state.provider.on('chainChanged', (chainIdHex) => {
456
+ state.chainIdHex = normalizeHex(chainIdHex);
457
+ clearFrozenReview('Wallet network changed. Freeze and review the intent again.');
458
+ render();
459
+ });
460
+ }
461
+
462
+ if (typeof window.addEventListener === 'function') {
463
+ window.addEventListener('beforeunload', (event) => {
464
+ if (state.transactionHash) {
465
+ event.preventDefault();
466
+ event.returnValue = 'A wallet transaction has been submitted and may still be pending. Leave anyway?';
467
+ }
468
+ });
469
+ }
470
+
471
+ setDefaultExpiry();
472
+ render();
@@ -0,0 +1,155 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="description" content="Circle agent wallet integration lab for Arc Testnet — bootstrap, faucet, transfer, bridge, and Gateway walkthrough." />
7
+ <title>Circle Wallet Integration Lab · Arc MCP Builder Assistant</title>
8
+ <style>
9
+ :root { color-scheme: dark; --bg: #070916; --panel: rgba(255,255,255,.075); --line: rgba(255,255,255,.16); --text: #fbf9ff; --muted: #c6bedc; --accent: #7cf7d4; --violet: #b597ff; --warn: #ffd166; --good: #89ffb8; --circle: #00d4ff; }
10
+ * { box-sizing: border-box; }
11
+ html { scroll-behavior: smooth; }
12
+ body { margin: 0; min-height: 100vh; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--text); line-height: 1.64; background: radial-gradient(circle at 12% 2%, rgba(0,212,255,.18), transparent 34rem), radial-gradient(circle at 90% 6%, rgba(124,247,212,.24), transparent 38rem), linear-gradient(180deg, #070916 0%, #121832 70%, #070916 100%); }
13
+ a { color: var(--accent); text-decoration: none; }
14
+ a:hover { text-decoration: underline; }
15
+ a:focus-visible, button:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
16
+ .wrap { width: min(1160px, calc(100% - 32px)); margin: 0 auto; }
17
+ header { padding: 26px 0 12px; }
18
+ .topbar, nav, .actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
19
+ .topbar { justify-content: space-between; }
20
+ .brand { color: var(--text); font-weight: 950; letter-spacing: -.02em; }
21
+ nav a, button, .badge { border: 1px solid var(--line); border-radius: 999px; padding: 10px 14px; background: rgba(255,255,255,.055); color: var(--text); font-weight: 850; }
22
+ button { cursor: pointer; }
23
+ button.primary { background: var(--circle); color: #06110f; border-color: transparent; }
24
+ .hero { display: grid; grid-template-columns: .95fr 1.05fr; gap: 18px; align-items: start; padding: 34px 0 22px; }
25
+ .layout { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; padding-bottom: 72px; align-items: start; }
26
+ .panel { border: 1px solid var(--line); border-radius: 28px; padding: clamp(20px, 4vw, 32px); background: linear-gradient(180deg, rgba(255,255,255,.1), var(--panel)); box-shadow: 0 22px 80px rgba(0,0,0,.34); }
27
+ .warning { border-color: rgba(255,209,102,.36); background: rgba(255,209,102,.08); }
28
+ .eyebrow { color: var(--circle); font-size: .78rem; letter-spacing: .16em; text-transform: uppercase; font-weight: 950; }
29
+ h1, h2, h3 { letter-spacing: -.035em; line-height: 1.08; }
30
+ h1 { margin: 12px 0 14px; font-size: clamp(2.15rem, 7vw, 4.6rem); }
31
+ h2 { margin-top: 0; font-size: clamp(1.35rem, 3vw, 2rem); }
32
+ p, li, label { color: var(--muted); }
33
+ .field { display: grid; gap: 8px; margin-bottom: 14px; }
34
+ input, select { width: 100%; border: 1px solid var(--line); border-radius: 16px; background: rgba(0,0,0,.24); color: var(--text); padding: 12px 14px; font: inherit; }
35
+ pre { white-space: pre-wrap; word-break: break-word; overflow-x: auto; border: 1px solid var(--line); border-radius: 18px; padding: 18px; background: rgba(0,0,0,.42); color: #eafff8; font-size: .82rem; }
36
+ .badge { display: inline-flex; width: fit-content; }
37
+ .badge.circle { color: var(--circle); border-color: rgba(0,212,255,.36); background: rgba(0,212,255,.08); }
38
+ .badge.good { color: var(--good); border-color: rgba(137,255,184,.36); background: rgba(137,255,184,.08); }
39
+ .badge.warn { color: var(--warn); border-color: rgba(255,209,102,.36); background: rgba(255,209,102,.08); }
40
+ .info-grid { display: grid; gap: 10px; }
41
+ .info-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; border: 1px solid var(--line); border-radius: 14px; background: rgba(0,0,0,.2); }
42
+ .info-label { color: var(--muted); font-size: .85rem; }
43
+ .info-value { color: var(--text); font-weight: 800; font-family: ui-monospace, "SF Mono", monospace; font-size: .85rem; }
44
+ .step-list { counter-reset: step; list-style: none; padding: 0; margin: 0; }
45
+ .step-list li { counter-increment: step; padding: 14px 0 14px 42px; position: relative; border-bottom: 1px solid var(--line); }
46
+ .step-list li:last-child { border-bottom: none; }
47
+ .step-list li::before { content: counter(step); position: absolute; left: 0; top: 14px; width: 28px; height: 28px; border-radius: 50%; background: var(--circle); color: #06110f; display: flex; align-items: center; justify-content: center; font-weight: 950; font-size: .82rem; }
48
+ code { font-family: ui-monospace, "SF Mono", monospace; font-size: .85rem; background: rgba(0,0,0,.3); padding: 2px 6px; border-radius: 6px; }
49
+ @media (max-width: 920px) { .hero, .layout { grid-template-columns: 1fr; } nav { display: none; } }
50
+ @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <a class="skip-link" href="#content" style="position:absolute;top:12px;left:-9999px;z-index:10;padding:10px 14px;border-radius:999px;background:var(--accent);color:#06110f;font-weight:950;">Skip to content</a>
55
+ <header class="wrap">
56
+ <div class="topbar">
57
+ <a class="brand" href="../../">Arc MCP Builder Assistant</a>
58
+ <nav aria-label="Circle wallet lab navigation">
59
+ <a href="../../">Home</a>
60
+ <a href="../../docs/view.html#circle-wallet-integration.md">Docs</a>
61
+ <a href="../arc-testnet-wallet-send-gate/">Wallet send gate</a>
62
+ <a href="../arc-agent-treasury-lab/">Treasury lab</a>
63
+ <a href="../x402-local-challenge-server/">x402 challenge</a>
64
+ </nav>
65
+ </div>
66
+ </header>
67
+ <main id="circle-wallet-lab" class="wrap">
68
+ <section class="hero">
69
+ <div>
70
+ <p class="eyebrow">Circle Agent Wallet · Arc Testnet</p>
71
+ <h1>Circle wallet integration lab</h1>
72
+ <p>Walk through the Circle agent wallet bootstrap flow on Arc Testnet: login, wallet creation, faucet funding, USDC transfer, and CCTP bridge — all through the <code>circle</code> CLI.</p>
73
+ <div class="actions">
74
+ <span class="badge circle" id="wallet-status">Session: not checked</span>
75
+ <span class="badge good" id="wallet-network">Arc Testnet · 5042002</span>
76
+ </div>
77
+ </div>
78
+ <div class="panel warning">
79
+ <h2>Safety boundary</h2>
80
+ <ul>
81
+ <li>No private keys in this page or repo — Circle CLI manages keys internally.</li>
82
+ <li>No custody, no mainnet, no autonomous spending.</li>
83
+ <li>Testnet USDC only — faucet tokens have no real value.</li>
84
+ <li>Human approval required for every wallet action.</li>
85
+ <li>Testnet only — faucet tokens have no real value.</li>
86
+ <li>x402 marketplace payments require mainnet USDC on an accepted chain.</li>
87
+ </ul>
88
+ </div>
89
+ </section>
90
+ <section class="layout">
91
+ <div class="panel">
92
+ <h2>Wallet state (read-only preview)</h2>
93
+ <p>This panel shows a static preview of the Circle wallet integration. It does not connect to Circle, RPC, or any external service. Use the <code>circle</code> CLI for live operations.</p>
94
+ <div class="info-grid">
95
+ <div class="info-row"><span class="info-label">Wallet address</span><span class="info-value" id="wallet-address">0x0cd9…a401</span></div>
96
+ <div class="info-row"><span class="info-label">Chain</span><span class="info-value">Arc Testnet</span></div>
97
+ <div class="info-row"><span class="info-label">Chain ID</span><span class="info-value">5042002 / 0x4cef52</span></div>
98
+ <div class="info-row"><span class="info-label">USDC ERC-20</span><span class="info-value" id="wallet-balance-erc20">— USDC</span></div>
99
+ <div class="info-row"><span class="info-label">USDC native (gas)</span><span class="info-value" id="wallet-balance-native">— USDC</span></div>
100
+ <div class="info-row"><span class="info-label">USDC contract</span><span class="info-value">0x3600…0000</span></div>
101
+ <div class="info-row"><span class="info-label">Gateway domain</span><span class="info-value">26 (Arc)</span></div>
102
+ <div class="info-row"><span class="info-label">Session</span><span class="info-value" id="wallet-session">testnet</span></div>
103
+ </div>
104
+ <div class="actions" style="margin-top:18px">
105
+ <button class="primary" id="refresh-preview">Refresh preview</button>
106
+ <button id="show-cli">Show CLI commands</button>
107
+ </div>
108
+ </div>
109
+ <div class="panel">
110
+ <h2>Bootstrap steps</h2>
111
+ <ol class="step-list">
112
+ <li><strong>Login (mainnet + testnet)</strong><br><code>circle wallet login &lt;email&gt; --type agent --init</code><br><code>circle wallet login &lt;email&gt; --type agent --testnet --init</code></li>
113
+ <li><strong>Create agent wallet</strong><br><code>circle wallet create --output json</code></li>
114
+ <li><strong>Fund via faucet</strong><br><code>circle wallet fund --address &lt;addr&gt; --chain "Arc Testnet" --token usdc</code></li>
115
+ <li><strong>Check balance</strong><br><code>circle wallet balance --address &lt;addr&gt; --chain "Arc Testnet"</code></li>
116
+ <li><strong>Transfer USDC</strong><br><code>circle wallet transfer &lt;to&gt; --amount 1 --address &lt;addr&gt; --chain "Arc Testnet"</code></li>
117
+ <li><strong>Bridge via CCTP</strong><br><code>circle bridge transfer "Base Sepolia" --amount 1 --address &lt;addr&gt; --chain "Arc Testnet"</code></li>
118
+ </ol>
119
+ <pre id="cli-output" style="display:none"># Login (mainnet)
120
+ circle wallet login user@example.com --type agent --init
121
+ # → OTP sent, then:
122
+ circle wallet login --type agent --request &lt;id&gt; --otp &lt;code&gt;
123
+
124
+ # Login (testnet — separate session)
125
+ circle wallet login user@example.com --type agent --testnet --init
126
+ circle wallet login --type agent --testnet --request &lt;id&gt; --otp &lt;code&gt;
127
+
128
+ # Create wallet
129
+ circle wallet create --output json
130
+
131
+ # Fund from faucet
132
+ circle wallet fund --address 0x0cd9… --chain "Arc Testnet" --token usdc
133
+
134
+ # Balance
135
+ circle wallet balance --address 0x0cd9… --chain "Arc Testnet" --output json
136
+
137
+ # Transfer 1 USDC
138
+ circle wallet transfer 0xrecipient… --amount 1 \
139
+ --address 0x0cd9… --chain "Arc Testnet" --output json
140
+
141
+ # Bridge to Base Sepolia
142
+ circle bridge transfer "Base Sepolia" --amount 1 \
143
+ --address 0x0cd9… --chain "Arc Testnet" --output json
144
+
145
+ # Transaction history
146
+ circle transaction list --address 0x0cd9… --chain "Arc Testnet"
147
+
148
+ # Gateway balance
149
+ circle gateway balance --address 0x0cd9… --chain "Arc Testnet"</pre>
150
+ </div>
151
+ </section>
152
+ </main>
153
+ <script src="./wallet-lab.js" defer></script>
154
+ </body>
155
+ </html>