@defdone/annon 0.0.0-stage → 0.1.0-alpha.1

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 (214) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/CLI.md +378 -0
  3. package/LICENSE +201 -0
  4. package/NOTICE +7 -0
  5. package/README.md +591 -2
  6. package/dist/api/annon-types.d.ts +108 -0
  7. package/dist/api/annon-types.js +2 -0
  8. package/dist/api/annon.d.ts +117 -0
  9. package/dist/api/annon.js +1152 -0
  10. package/dist/api/hosted-record.d.ts +13 -0
  11. package/dist/api/hosted-record.js +149 -0
  12. package/dist/api/sharing-types.d.ts +109 -0
  13. package/dist/api/sharing-types.js +1 -0
  14. package/dist/api/sharing.d.ts +36 -0
  15. package/dist/api/sharing.js +879 -0
  16. package/dist/browser/download.d.ts +19 -0
  17. package/dist/browser/download.js +52 -0
  18. package/dist/browser/index.d.ts +23 -0
  19. package/dist/browser/index.js +22 -0
  20. package/dist/browser/login-bundle.js +151 -0
  21. package/dist/browser/login.d.ts +1 -0
  22. package/dist/browser/login.js +216 -0
  23. package/dist/browser/purchase-bundle.js +84 -0
  24. package/dist/browser/purchases.d.ts +98 -0
  25. package/dist/browser/purchases.js +735 -0
  26. package/dist/browser/record.d.ts +55 -0
  27. package/dist/browser/record.js +16 -0
  28. package/dist/browser/selling.d.ts +146 -0
  29. package/dist/browser/selling.js +418 -0
  30. package/dist/browser/wallets.d.ts +53 -0
  31. package/dist/browser/wallets.js +286 -0
  32. package/dist/bytes.d.ts +4 -0
  33. package/dist/bytes.js +16 -0
  34. package/dist/download/download.d.ts +59 -0
  35. package/dist/download/download.js +384 -0
  36. package/dist/download/host.d.ts +56 -0
  37. package/dist/download/host.js +161 -0
  38. package/dist/download/http.d.ts +23 -0
  39. package/dist/download/http.js +87 -0
  40. package/dist/download/proof.d.ts +20 -0
  41. package/dist/download/proof.js +79 -0
  42. package/dist/download/upload.d.ts +130 -0
  43. package/dist/download/upload.js +408 -0
  44. package/dist/envelope/evm.d.ts +4 -0
  45. package/dist/envelope/evm.js +32 -0
  46. package/dist/envelope/pay2.d.ts +176 -0
  47. package/dist/envelope/pay2.js +541 -0
  48. package/dist/envelope/replay.d.ts +45 -0
  49. package/dist/envelope/replay.js +186 -0
  50. package/dist/envelope/stream.d.ts +78 -0
  51. package/dist/envelope/stream.js +372 -0
  52. package/dist/errors.d.ts +21 -0
  53. package/dist/errors.js +300 -0
  54. package/dist/index.d.ts +25 -0
  55. package/dist/index.js +23 -0
  56. package/dist/keys/near-ckd.d.ts +47 -0
  57. package/dist/keys/near-ckd.js +96 -0
  58. package/dist/limits.d.ts +4 -0
  59. package/dist/limits.js +4 -0
  60. package/dist/near/nep413.d.ts +22 -0
  61. package/dist/near/nep413.js +39 -0
  62. package/dist/near/rpc.d.ts +170 -0
  63. package/dist/near/rpc.js +423 -0
  64. package/dist/near/texts.d.ts +39 -0
  65. package/dist/near/texts.js +60 -0
  66. package/dist/net/ip.d.ts +7 -0
  67. package/dist/net/ip.js +62 -0
  68. package/dist/net/retry.d.ts +3 -0
  69. package/dist/net/retry.js +31 -0
  70. package/dist/node/adapter-checks.d.ts +6 -0
  71. package/dist/node/adapter-checks.js +13 -0
  72. package/dist/node/adapter.d.ts +8 -0
  73. package/dist/node/adapter.js +98 -0
  74. package/dist/node/agent.d.ts +49 -0
  75. package/dist/node/agent.js +266 -0
  76. package/dist/node/amounts.d.ts +2 -0
  77. package/dist/node/amounts.js +20 -0
  78. package/dist/node/browser-login.d.ts +25 -0
  79. package/dist/node/browser-login.js +219 -0
  80. package/dist/node/budget.d.ts +24 -0
  81. package/dist/node/budget.js +59 -0
  82. package/dist/node/buy-commands.d.ts +24 -0
  83. package/dist/node/buy-commands.js +301 -0
  84. package/dist/node/cli.d.ts +60 -0
  85. package/dist/node/cli.js +201 -0
  86. package/dist/node/command-context.d.ts +82 -0
  87. package/dist/node/command-context.js +77 -0
  88. package/dist/node/config.d.ts +37 -0
  89. package/dist/node/config.js +189 -0
  90. package/dist/node/core-adapter.d.ts +43 -0
  91. package/dist/node/core-adapter.js +122 -0
  92. package/dist/node/errors.d.ts +8 -0
  93. package/dist/node/errors.js +12 -0
  94. package/dist/node/index.d.ts +7 -0
  95. package/dist/node/index.js +5 -0
  96. package/dist/node/io.d.ts +13 -0
  97. package/dist/node/io.js +112 -0
  98. package/dist/node/keychain.d.ts +10 -0
  99. package/dist/node/keychain.js +67 -0
  100. package/dist/node/main.d.ts +2 -0
  101. package/dist/node/main.js +21 -0
  102. package/dist/node/offers.d.ts +16 -0
  103. package/dist/node/offers.js +91 -0
  104. package/dist/node/origin.d.ts +11 -0
  105. package/dist/node/origin.js +80 -0
  106. package/dist/node/paths.d.ts +3 -0
  107. package/dist/node/paths.js +71 -0
  108. package/dist/node/payment-terms.d.ts +26 -0
  109. package/dist/node/payment-terms.js +127 -0
  110. package/dist/node/purchase-commands.d.ts +16 -0
  111. package/dist/node/purchase-commands.js +237 -0
  112. package/dist/node/purchase-guard.d.ts +19 -0
  113. package/dist/node/purchase-guard.js +53 -0
  114. package/dist/node/relay-transport.d.ts +6 -0
  115. package/dist/node/relay-transport.js +173 -0
  116. package/dist/node/resources.d.ts +2 -0
  117. package/dist/node/resources.js +26 -0
  118. package/dist/node/retry-gate.d.ts +15 -0
  119. package/dist/node/retry-gate.js +53 -0
  120. package/dist/node/seller-commands.d.ts +12 -0
  121. package/dist/node/seller-commands.js +207 -0
  122. package/dist/node/sharing.d.ts +3 -0
  123. package/dist/node/sharing.js +28 -0
  124. package/dist/node/shops.d.ts +17 -0
  125. package/dist/node/shops.js +74 -0
  126. package/dist/node/signer-mock.d.ts +11 -0
  127. package/dist/node/signer-mock.js +79 -0
  128. package/dist/node/signer-wallet.d.ts +8 -0
  129. package/dist/node/signer-wallet.js +149 -0
  130. package/dist/node/signing.d.ts +29 -0
  131. package/dist/node/signing.js +200 -0
  132. package/dist/node/storage-commands.d.ts +11 -0
  133. package/dist/node/storage-commands.js +50 -0
  134. package/dist/node/sync-dir.d.ts +1 -0
  135. package/dist/node/sync-dir.js +11 -0
  136. package/dist/node/wallets.d.ts +13 -0
  137. package/dist/node/wallets.js +146 -0
  138. package/dist/oneclick/oneclick.d.ts +132 -0
  139. package/dist/oneclick/oneclick.js +342 -0
  140. package/dist/pay/backup.d.ts +70 -0
  141. package/dist/pay/backup.js +402 -0
  142. package/dist/pay/base.d.ts +135 -0
  143. package/dist/pay/base.js +386 -0
  144. package/dist/pay/eip3009.d.ts +59 -0
  145. package/dist/pay/eip3009.js +72 -0
  146. package/dist/pay/pay.d.ts +86 -0
  147. package/dist/pay/pay.js +297 -0
  148. package/dist/pay/purchase.d.ts +103 -0
  149. package/dist/pay/purchase.js +20 -0
  150. package/dist/pay/recover.d.ts +31 -0
  151. package/dist/pay/recover.js +34 -0
  152. package/dist/pay/seller.d.ts +130 -0
  153. package/dist/pay/seller.js +444 -0
  154. package/dist/pay/settle.d.ts +96 -0
  155. package/dist/pay/settle.js +308 -0
  156. package/dist/pay/split.d.ts +115 -0
  157. package/dist/pay/split.js +256 -0
  158. package/dist/pay/store-node.d.ts +8 -0
  159. package/dist/pay/store-node.js +224 -0
  160. package/dist/pay/store.d.ts +54 -0
  161. package/dist/pay/store.js +135 -0
  162. package/dist/pay/x402.d.ts +95 -0
  163. package/dist/pay/x402.js +136 -0
  164. package/dist/relay/admission.d.ts +8 -0
  165. package/dist/relay/admission.js +58 -0
  166. package/dist/relay/capabilities.d.ts +36 -0
  167. package/dist/relay/capabilities.js +232 -0
  168. package/dist/relay/fees.d.ts +17 -0
  169. package/dist/relay/fees.js +105 -0
  170. package/dist/relay/index.d.ts +10 -0
  171. package/dist/relay/index.js +8 -0
  172. package/dist/relay/keeper-balance.d.ts +17 -0
  173. package/dist/relay/keeper-balance.js +46 -0
  174. package/dist/relay/keeper-runner.d.ts +114 -0
  175. package/dist/relay/keeper-runner.js +703 -0
  176. package/dist/relay/keeper.d.ts +38 -0
  177. package/dist/relay/keeper.js +73 -0
  178. package/dist/relay/ledger.d.ts +155 -0
  179. package/dist/relay/ledger.js +392 -0
  180. package/dist/relay/relay-checks.d.ts +3 -0
  181. package/dist/relay/relay-checks.js +244 -0
  182. package/dist/relay/relay-reconciliation.d.ts +15 -0
  183. package/dist/relay/relay-reconciliation.js +293 -0
  184. package/dist/relay/relay-request.d.ts +6 -0
  185. package/dist/relay/relay-request.js +139 -0
  186. package/dist/relay/relay-transactions.d.ts +23 -0
  187. package/dist/relay/relay-transactions.js +182 -0
  188. package/dist/relay/relay-types.d.ts +128 -0
  189. package/dist/relay/relay-types.js +17 -0
  190. package/dist/relay/relay.d.ts +102 -0
  191. package/dist/relay/relay.js +426 -0
  192. package/dist/relay/reservation.d.ts +29 -0
  193. package/dist/relay/reservation.js +132 -0
  194. package/dist/relay/sponsorship.d.ts +42 -0
  195. package/dist/relay/sponsorship.js +184 -0
  196. package/dist/routes.d.ts +2 -0
  197. package/dist/routes.js +1 -0
  198. package/dist/settle-near/codec.d.ts +17 -0
  199. package/dist/settle-near/codec.js +86 -0
  200. package/dist/settle-near/identity.d.ts +27 -0
  201. package/dist/settle-near/identity.js +80 -0
  202. package/dist/settle-near/purchase.d.ts +40 -0
  203. package/dist/settle-near/purchase.js +153 -0
  204. package/dist/settle-near/terms.d.ts +37 -0
  205. package/dist/settle-near/terms.js +89 -0
  206. package/dist/shops/discovery.d.ts +95 -0
  207. package/dist/shops/discovery.js +384 -0
  208. package/dist/wallet/account-only.d.ts +11 -0
  209. package/dist/wallet/account-only.js +6 -0
  210. package/dist/wallet/uncertain.d.ts +8 -0
  211. package/dist/wallet/uncertain.js +13 -0
  212. package/dist/wallet/wallet.d.ts +100 -0
  213. package/dist/wallet/wallet.js +204 -0
  214. package/package.json +136 -3
@@ -0,0 +1,386 @@
1
+ import { AnnonError } from '../errors.js';
2
+ /**
3
+ * Reading Base over JSON-RPC: what the buyer's checks need before signing (balance, code, the
4
+ * head block), a dry run of the signed authorization against USDC itself, and, after the payment,
5
+ * the logs `unlock` takes. The RPC is trusted to report the chain, as on the NEAR side; a payment
6
+ * it misreports is caught by NEAR MPC, which reads Base itself.
7
+ */
8
+ import { keccak_256 } from '@noble/hashes/sha3.js';
9
+ import { RPC_TIMEOUT_MS } from '../near/rpc.js';
10
+ import { addressWord, fromHex, toHex, word } from './eip3009.js';
11
+ export class PayError extends AnnonError {
12
+ code;
13
+ /**
14
+ * Set when the wallet had already signed: the authorization may have reached an RPC and could
15
+ * still be settled. The purchase stays open: follow `nextStep`.
16
+ */
17
+ purchase;
18
+ retryAfterSeconds;
19
+ constructor(code, message = code) {
20
+ super(code, message);
21
+ this.code = code;
22
+ this.name = 'PayError';
23
+ }
24
+ }
25
+ /** A typed RPC rate limit; callers retry through isTransient. */
26
+ export class BaseRpcRateLimited extends PayError {
27
+ constructor() {
28
+ super('base_rpc_rate_limited', 'Base RPC rate limited (429)');
29
+ this.name = 'BaseRpcRateLimited';
30
+ }
31
+ }
32
+ export const fail = (code, message) => {
33
+ throw new PayError(code, message);
34
+ };
35
+ const malformed = (what) => fail('transport', `malformed Base RPC answer: ${what}`);
36
+ let nextId = 0;
37
+ async function rpc(url, method, params) {
38
+ const id = `annon-base-${++nextId}`;
39
+ let body;
40
+ try {
41
+ const res = await fetch(url, {
42
+ credentials: 'omit',
43
+ method: 'POST',
44
+ headers: { 'content-type': 'application/json' },
45
+ body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
46
+ signal: AbortSignal.timeout(RPC_TIMEOUT_MS),
47
+ });
48
+ const httpFailure = () => {
49
+ if (res.status === 429)
50
+ throw new BaseRpcRateLimited();
51
+ return fail('transport', `Base RPC HTTP ${res.status}`);
52
+ };
53
+ try {
54
+ body = await res.json();
55
+ }
56
+ catch (error) {
57
+ if (!res.ok)
58
+ return httpFailure();
59
+ throw error;
60
+ }
61
+ if (res.status === 429)
62
+ return httpFailure();
63
+ // Preserve structured RPC replies on non-2xx responses, as on NEAR.
64
+ if (!res.ok &&
65
+ (!body ||
66
+ typeof body !== 'object' ||
67
+ Array.isArray(body) ||
68
+ (body.jsonrpc !== '2.0' && body.id !== id) ||
69
+ (!('result' in body) && !('error' in body))))
70
+ return httpFailure();
71
+ }
72
+ catch (e) {
73
+ if (e instanceof PayError)
74
+ throw e;
75
+ return fail('transport', `Base RPC unreachable: ${e.message}`);
76
+ }
77
+ if (body?.id !== id)
78
+ return malformed('a reply to another request');
79
+ return body;
80
+ }
81
+ async function ask(url, method, params) {
82
+ const body = await rpc(url, method, params);
83
+ if (body.error)
84
+ fail(method === 'eth_call' && body.error.code === 3 ? 'refused' : 'transport', `Base RPC error: ${JSON.stringify(body.error)}`);
85
+ if (body.result === undefined)
86
+ return malformed(`no result for ${method}`);
87
+ return body.result;
88
+ }
89
+ const isHexData = (v) => typeof v === 'string' && /^0x([0-9a-fA-F]{2})*$/.test(v);
90
+ const quantity = (v, what) => {
91
+ if (typeof v !== 'string' || !/^0x(0|[1-9a-f][0-9a-f]*)$/i.test(v))
92
+ return malformed(what);
93
+ const n = Number.parseInt(v, 16);
94
+ return Number.isSafeInteger(n) ? n : malformed(what);
95
+ };
96
+ const qty = (n) => `0x${n.toString(16)}`;
97
+ const selector = (signature) => keccak_256(new TextEncoder().encode(signature)).subarray(0, 4);
98
+ const call = (...parts) => toHex(Uint8Array.from(parts.flatMap((p) => Array.from(p))));
99
+ /** The latest block, after checking the RPC is on Base mainnet. */
100
+ export async function head(url) {
101
+ if ((await ask(url, 'eth_chainId', [])) !== '0x2105') {
102
+ fail('network_mismatch', 'the Base RPC is not on Base mainnet (chain 8453)');
103
+ }
104
+ return block(url, 'latest');
105
+ }
106
+ /** A block by number or tag (`latest`, `safe`, `finalized`). */
107
+ export async function block(url, at) {
108
+ const b = (await ask(url, 'eth_getBlockByNumber', [
109
+ typeof at === 'number' ? qty(at) : at,
110
+ false,
111
+ ]));
112
+ return {
113
+ number: quantity(b?.number, 'block number'),
114
+ timestamp: quantity(b?.timestamp, 'timestamp'),
115
+ ...(typeof b?.hash === 'string' ? { hash: b.hash.toLowerCase() } : {}),
116
+ };
117
+ }
118
+ /** A transaction's receipt as the RPC returns it; null while the transaction is unknown. */
119
+ export const getReceipt = (url, txHash) => ask(url, 'eth_getTransactionReceipt', [txHash]);
120
+ /** A view call at `block` that returns one 32-byte word. */
121
+ export async function viewWord(url, to, data, block) {
122
+ const r = await ask(url, 'eth_call', [{ to, data }, qty(block)]);
123
+ if (!isHexData(r) || r.length !== 66)
124
+ return malformed('eth_call result');
125
+ return r.toLowerCase();
126
+ }
127
+ const uint = async (url, to, data, block) => BigInt(await viewWord(url, to, data, block));
128
+ export const balanceOf = (url, usdc, owner, block) => uint(url, usdc, call(selector('balanceOf(address)'), addressWord(owner)), block);
129
+ /** USDC's `authorizationState`: true once the nonce was used *or* canceled. A hint, not proof. */
130
+ export const authorizationState = async (url, usdc, authorizer, nonce, block) => (await uint(url, usdc, call(selector('authorizationState(address,bytes32)'), addressWord(authorizer), fromHex(nonce, 32, 'nonce')), block)) !== 0n;
131
+ /** Whether an address has code: a contract, or an EOA delegated with EIP-7702. */
132
+ /** The runtime code at `address`, lowercase hex; `0x` for none. */
133
+ export async function getCode(url, address, block) {
134
+ const r = await ask(url, 'eth_getCode', [address, qty(block)]);
135
+ if (!isHexData(r))
136
+ return malformed('eth_getCode result');
137
+ return r.toLowerCase();
138
+ }
139
+ export const hasCode = async (url, address, block) => (await getCode(url, address, block)) !== '0x';
140
+ /** `transferWithAuthorization(from, to, value, after, before, nonce, v, r, s)`. */
141
+ export function authorizationCall(a, signature) {
142
+ const sig = fromHex(signature, 65, 'signature');
143
+ return call(selector('transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32)'), addressWord(a.from), addressWord(a.to), word(a.value), word(a.validAfter), word(a.validBefore), fromHex(a.nonce, 32, 'nonce'), word(BigInt(sig[64] ?? 0)), sig.subarray(0, 32), sig.subarray(32, 64));
144
+ }
145
+ /**
146
+ * Runs the signed authorization against USDC without sending it: signature, nonce, validity
147
+ * window, balance and blacklist, all by USDC's own code.
148
+ */
149
+ export async function dryRun(url, usdc, a, signature) {
150
+ const body = await rpc(url, 'eth_call', [
151
+ { from: a.from, to: usdc, data: authorizationCall(a, signature) },
152
+ 'latest',
153
+ ]);
154
+ if (body.error) {
155
+ const message = String(body.error.message ?? '');
156
+ if (body.error.code === 3) {
157
+ fail('payment_rejected', `USDC refused the authorization: ${message}`);
158
+ }
159
+ fail('transport', `Base RPC error: ${JSON.stringify(body.error)}`);
160
+ }
161
+ if (!isHexData(body.result))
162
+ malformed('eth_call result');
163
+ }
164
+ // ---------- the payment's block header ----------
165
+ /** Header fields in RLP order (London to Prague / Isthmus); integers are minimal big-endian. */
166
+ const HEADER_FIELDS = [
167
+ ['parentHash', 'bytes'],
168
+ ['sha3Uncles', 'bytes'],
169
+ ['miner', 'bytes'],
170
+ ['stateRoot', 'bytes'],
171
+ ['transactionsRoot', 'bytes'],
172
+ ['receiptsRoot', 'bytes'],
173
+ ['logsBloom', 'bytes'],
174
+ ['difficulty', 'int'],
175
+ ['number', 'int'],
176
+ ['gasLimit', 'int'],
177
+ ['gasUsed', 'int'],
178
+ ['timestamp', 'int'],
179
+ ['extraData', 'bytes'],
180
+ ['mixHash', 'bytes'],
181
+ ['nonce', 'bytes'],
182
+ ['baseFeePerGas', 'int'],
183
+ ['withdrawalsRoot', 'bytes'],
184
+ ['blobGasUsed', 'int'],
185
+ ['excessBlobGas', 'int'],
186
+ ['parentBeaconBlockRoot', 'bytes'],
187
+ ['requestsHash', 'bytes'],
188
+ ];
189
+ const rlpLength = (n, offset) => {
190
+ if (n < 56)
191
+ return [offset + n];
192
+ const b = [];
193
+ for (let x = n; x > 0; x = Math.floor(x / 256))
194
+ b.unshift(x % 256);
195
+ return [offset + 55 + b.length, ...b];
196
+ };
197
+ const rlpString = (b) => b.length === 1 && (b[0] ?? 0) < 0x80 ? [...b] : [...rlpLength(b.length, 0x80), ...b];
198
+ const minimal = (h) => {
199
+ const n = BigInt(h);
200
+ if (n === 0n)
201
+ return new Uint8Array();
202
+ const s = n.toString(16);
203
+ return Uint8Array.from((s.length % 2 ? `0${s}` : s).match(/../g) ?? [], (x) => Number.parseInt(x, 16));
204
+ };
205
+ const bytesOf = (h, what) => {
206
+ if (!isHexData(h))
207
+ return malformed(`header ${what}`);
208
+ return Uint8Array.from(h.slice(2).match(/../g) ?? [], (x) => Number.parseInt(x, 16));
209
+ };
210
+ /**
211
+ * The RLP header of the block `hash`, rebuilt from the RPC's fields and checked to hash to it.
212
+ * `unlock` takes it for a file whose sales were stopped: the contract reads the payment's time
213
+ * from it. A header with a field this SDK doesn't know fails here, never on NEAR.
214
+ */
215
+ export async function blockHeader(url, hash) {
216
+ const b = (await ask(url, 'eth_getBlockByHash', [hash, false]));
217
+ const present = HEADER_FIELDS.filter(([k]) => b?.[k] !== undefined);
218
+ const body = present.flatMap(([k, kind]) => rlpString(kind === 'int' ? minimal(String(b[k])) : bytesOf(b[k], k)));
219
+ const rlp = Uint8Array.from([...rlpLength(body.length, 0xc0), ...body]);
220
+ if (toHex(keccak_256(rlp)) !== hash.toLowerCase()) {
221
+ return fail('transport', `block ${hash}: the rebuilt header does not hash to it`);
222
+ }
223
+ return toHex(rlp);
224
+ }
225
+ // ---------- after the payment ----------
226
+ const topic = (signature) => toHex(keccak_256(new TextEncoder().encode(signature)));
227
+ const TOPICS = {
228
+ authorizationUsed: topic('AuthorizationUsed(address,bytes32)'),
229
+ authorizationCanceled: topic('AuthorizationCanceled(address,bytes32)'),
230
+ transfer: topic('Transfer(address,address,uint256)'),
231
+ };
232
+ const hash = (v, what) => typeof v === 'string' && /^0x[0-9a-fA-F]{64}$/.test(v) ? v.toLowerCase() : malformed(what);
233
+ function evmLog(l) {
234
+ if (typeof l?.address !== 'string' || !/^0x[0-9a-fA-F]{40}$/.test(l.address)) {
235
+ return malformed('log address');
236
+ }
237
+ if (!Array.isArray(l.topics) || !isHexData(l.data) || typeof l.removed !== 'boolean') {
238
+ return malformed('log');
239
+ }
240
+ return {
241
+ address: l.address.slice(2).toLowerCase(),
242
+ block_hash: hash(l.blockHash, 'log blockHash').slice(2),
243
+ block_number: quantity(l.blockNumber, 'log blockNumber'),
244
+ data: l.data.toLowerCase(),
245
+ log_index: quantity(l.logIndex, 'log logIndex'),
246
+ removed: l.removed,
247
+ topics: l.topics.map((t) => hash(t, 'log topic').slice(2)),
248
+ transaction_hash: hash(l.transactionHash, 'log transactionHash').slice(2),
249
+ transaction_index: quantity(l.transactionIndex, 'log transactionIndex'),
250
+ };
251
+ }
252
+ const strip = (h) => h.slice(2).toLowerCase();
253
+ const topicOf = (address) => strip(toHex(addressWord(address)));
254
+ /**
255
+ * The logs of this purchase in a transaction receipt, exactly as `unlock` checks them: USDC's
256
+ * `AuthorizationUsed` for the nonce and the `Transfer` to `pay_to` right after it. Null when the
257
+ * receipt holds no such payment.
258
+ */
259
+ export function paymentLogs(receipt, e) {
260
+ const r = receipt;
261
+ if (r?.status !== '0x1' || !Array.isArray(r.logs))
262
+ return null;
263
+ const tx = hash(r.transactionHash, 'receipt transactionHash').slice(2);
264
+ const logs = r.logs.map(evmLog);
265
+ const at = (address, t0) => (l) => l.transaction_hash === tx &&
266
+ !l.removed &&
267
+ l.address === strip(address) &&
268
+ l.topics.length === 3 &&
269
+ l.topics[0] === strip(t0);
270
+ const amount = (data, offset) => BigInt(`0x${data.slice(2 + offset * 64, 2 + (offset + 1) * 64) || '0'}`);
271
+ // EIP-3009 nonces are per authorizer: another payer's log with the same nonce in the same
272
+ // transaction must not hide this one.
273
+ const i = logs.findIndex((l) => at(e.usdc, TOPICS.authorizationUsed)(l) &&
274
+ l.topics[1] === topicOf(e.from) &&
275
+ l.topics[2] === strip(e.nonce));
276
+ const [auth, transfer] = [logs[i], logs[i + 1]];
277
+ if (i < 0 || !auth || !transfer || !at(e.usdc, TOPICS.transfer)(transfer))
278
+ return null;
279
+ if (transfer.log_index !== auth.log_index + 1 ||
280
+ transfer.topics[1] !== topicOf(e.from) ||
281
+ transfer.topics[2] !== topicOf(e.to) ||
282
+ transfer.data.length !== 2 + 64) {
283
+ return null;
284
+ }
285
+ return amount(transfer.data, 0) === e.price ? [auth, transfer] : null;
286
+ }
287
+ /**
288
+ * The last block the payment could be in: the first at or past `validBefore` (USDC refuses it
289
+ * there and after), or the head. Searched forward, so it holds whatever Base's block time is.
290
+ */
291
+ async function lastBlock(url, from, validBefore, now) {
292
+ // Note: a first guess of 5 blocks a second (Base since Denim, 5 Nov 2026), doubled on a miss.
293
+ let step = Math.max(1, (Number(validBefore) - (await block(url, from)).timestamp) * 5);
294
+ for (let at = from + step;; at += step, step *= 2) {
295
+ if (at >= now.number)
296
+ return now.number;
297
+ if (BigInt((await block(url, at)).timestamp) >= validBefore)
298
+ return at;
299
+ }
300
+ }
301
+ /** Note: blocks per eth_getLogs, the common RPC limit (QuickNode refuses a wider range). */
302
+ const LOG_RANGE = 10_000;
303
+ /**
304
+ * USDC's logs for `topics` (a list in a position matches any of it) in `[from, to]`, read in
305
+ * chunks of `LOG_RANGE` blocks, earliest first. Each chunk's logs must answer that chunk's query.
306
+ * Stops after the first chunk with a log `enough` accepts; an empty range reads nothing.
307
+ */
308
+ async function usdcLogs(url, usdc, topics, from, to, enough) {
309
+ const all = [];
310
+ for (let at = from; at <= to; at += LOG_RANGE) {
311
+ const end = Math.min(to, at + LOG_RANGE - 1);
312
+ const found = await ask(url, 'eth_getLogs', [
313
+ { address: usdc, fromBlock: qty(at), toBlock: qty(end), topics },
314
+ ]);
315
+ if (!Array.isArray(found))
316
+ return malformed('eth_getLogs result');
317
+ const logs = found.map(evmLog).filter((l) => !l.removed);
318
+ // Every log must answer the query; anything else is an RPC that can't be believed.
319
+ for (const l of logs) {
320
+ if (l.address !== strip(usdc) ||
321
+ l.topics.length !== topics.length ||
322
+ !topics.every((t, i) => [t].flat().some((x) => strip(x) === l.topics[i])) ||
323
+ l.block_number < at ||
324
+ l.block_number > end) {
325
+ return malformed('eth_getLogs returned a log the query did not ask for');
326
+ }
327
+ }
328
+ all.push(...logs);
329
+ if (logs.some(enough))
330
+ break;
331
+ }
332
+ return all;
333
+ }
334
+ /** The block of an `AuthorizationCanceled` for this nonce in `[from, to]`. */
335
+ async function canceledAfter(url, e, from, to) {
336
+ const topics = [TOPICS.authorizationCanceled, toHex(addressWord(e.from)), e.nonce.toLowerCase()];
337
+ const [first] = await usdcLogs(url, e.usdc, topics, from, to, () => true);
338
+ return first ? first.block_number : null;
339
+ }
340
+ /**
341
+ * Where a signed purchase stands, found by its nonce alone, so a lost transaction hash loses
342
+ * nothing. `block` is the head when it was signed. A payment counts as soon as it is in a block;
343
+ * `expired` and `canceled` only from the finalized chain, because a new purchase follows them and
344
+ * a reorg must not turn that into a second payment. `authorizationState` is only read to catch an
345
+ * RPC that misses logs.
346
+ */
347
+ export async function paymentStatus(url, e) {
348
+ // Finalized first: the head read after it is at least as far, so a payment in any block the
349
+ // expiry below relies on is inside the search.
350
+ const final = await block(url, 'finalized');
351
+ const now = await head(url);
352
+ const last = await lastBlock(url, e.block, e.validBefore, now);
353
+ // A used authorization in any chunk wins over a cancellation, so only that ends the search.
354
+ const usedTopic = strip(TOPICS.authorizationUsed);
355
+ const logs = await usdcLogs(url, e.usdc, [
356
+ [TOPICS.authorizationUsed, TOPICS.authorizationCanceled],
357
+ toHex(addressWord(e.from)),
358
+ e.nonce.toLowerCase(),
359
+ ], e.block, last, (l) => l.topics[0] === usedTopic);
360
+ const used = logs.find((l) => l.topics[0] === usedTopic);
361
+ if (used) {
362
+ const receipt = await ask(url, 'eth_getTransactionReceipt', [`0x${used.transaction_hash}`]);
363
+ const paid = paymentLogs(receipt, e);
364
+ if (!paid?.[0] || paid[0].transaction_hash !== used.transaction_hash) {
365
+ return fail('transport', 'the authorization was used, but not for this purchase as expected');
366
+ }
367
+ return {
368
+ state: 'paid',
369
+ txHash: `0x${paid[0].transaction_hash}`,
370
+ logIndex: paid[0].log_index,
371
+ logs: paid,
372
+ };
373
+ }
374
+ const canceled = logs.find((l) => l.topics[0] === strip(TOPICS.authorizationCanceled));
375
+ if (canceled)
376
+ return canceled.block_number <= final.number ? { state: 'canceled' } : { state: 'pending' };
377
+ if (await authorizationState(url, e.usdc, e.from, e.nonce, now.number)) {
378
+ // USDC takes a cancellation after the deadline too: look for one up to the head.
379
+ const late = await canceledAfter(url, e, last + 1, now.number);
380
+ if (late !== null)
381
+ return late <= final.number ? { state: 'canceled' } : { state: 'pending' };
382
+ return fail('transport', 'USDC marks the authorization used, but the RPC returned no log for it');
383
+ }
384
+ // Blocks after the finalized one are later still, so none of them can take the payment.
385
+ return BigInt(final.timestamp) >= e.validBefore ? { state: 'expired' } : { state: 'pending' };
386
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * EIP-3009 on Base USDC: the authorization a buyer signs, its EIP-712 digest, the checks on the
3
+ * signature a wallet sends back. Nonces as in `specs/nonce.md`.
4
+ */
5
+ export declare const BASE_CHAIN_ID = 8453;
6
+ export interface Authorization {
7
+ from: string;
8
+ to: string;
9
+ value: bigint;
10
+ validAfter: bigint;
11
+ validBefore: bigint;
12
+ nonce: string;
13
+ }
14
+ /** The typed data in the shape `signTypedData` takes (viem, ethers' `_signTypedData` minus domain types). */
15
+ export declare const typedData: (usdc: string, a: Authorization) => {
16
+ domain: {
17
+ name: string;
18
+ version: string;
19
+ chainId: number;
20
+ verifyingContract: string;
21
+ };
22
+ types: {
23
+ TransferWithAuthorization: readonly [{
24
+ readonly name: "from";
25
+ readonly type: "address";
26
+ }, {
27
+ readonly name: "to";
28
+ readonly type: "address";
29
+ }, {
30
+ readonly name: "value";
31
+ readonly type: "uint256";
32
+ }, {
33
+ readonly name: "validAfter";
34
+ readonly type: "uint256";
35
+ }, {
36
+ readonly name: "validBefore";
37
+ readonly type: "uint256";
38
+ }, {
39
+ readonly name: "nonce";
40
+ readonly type: "bytes32";
41
+ }];
42
+ };
43
+ primaryType: string;
44
+ message: Authorization;
45
+ };
46
+ export type TypedData = ReturnType<typeof typedData>;
47
+ export declare const toHex: (b: Uint8Array) => string;
48
+ export declare function fromHex(s: string, bytes: number, name: string): Uint8Array;
49
+ /** A uint256 word. */
50
+ export declare function word(n: bigint): Uint8Array;
51
+ /** An address as a 32-byte word. */
52
+ export declare const addressWord: (a: string) => Uint8Array<ArrayBuffer>;
53
+ /** The EIP-712 digest a wallet signs for `typedData(usdc, a)`. */
54
+ export declare function digest(usdc: string, a: Authorization): Uint8Array;
55
+ /**
56
+ * The address that signed `digest`, or null. Only what USDC itself accepts: 65 bytes r ‖ s ‖ v,
57
+ * v 27 or 28, low s (FiatToken's ECRecover refuses the rest).
58
+ */
59
+ export declare function signer(digestBytes: Uint8Array, signature: string): string | null;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * EIP-3009 on Base USDC: the authorization a buyer signs, its EIP-712 digest, the checks on the
3
+ * signature a wallet sends back. Nonces as in `specs/nonce.md`.
4
+ */
5
+ import { secp256k1 } from '@noble/curves/secp256k1.js';
6
+ import { keccak_256 } from '@noble/hashes/sha3.js';
7
+ import { concatParts, prefixedHex } from '../bytes.js';
8
+ export const BASE_CHAIN_ID = 8453;
9
+ /** What the buyer signs: USDC `TransferWithAuthorization` straight to the file's `pay_to`. */
10
+ const PRIMARY_TYPE = 'TransferWithAuthorization';
11
+ const FIELDS = [
12
+ { name: 'from', type: 'address' },
13
+ { name: 'to', type: 'address' },
14
+ { name: 'value', type: 'uint256' },
15
+ { name: 'validAfter', type: 'uint256' },
16
+ { name: 'validBefore', type: 'uint256' },
17
+ { name: 'nonce', type: 'bytes32' },
18
+ ];
19
+ /** The typed data in the shape `signTypedData` takes (viem, ethers' `_signTypedData` minus domain types). */
20
+ export const typedData = (usdc, a) => ({
21
+ domain: { name: 'USD Coin', version: '2', chainId: BASE_CHAIN_ID, verifyingContract: usdc },
22
+ types: { [PRIMARY_TYPE]: FIELDS },
23
+ primaryType: PRIMARY_TYPE,
24
+ message: a,
25
+ });
26
+ const utf8 = new TextEncoder();
27
+ const concat = (...parts) => concatParts(parts);
28
+ export const toHex = (b) => prefixedHex(b);
29
+ export function fromHex(s, bytes, name) {
30
+ if (!new RegExp(`^0x[0-9a-fA-F]{${2 * bytes}}$`).test(s)) {
31
+ throw new TypeError(`${name} must be 0x and ${bytes} bytes of hex`);
32
+ }
33
+ return Uint8Array.from(s.slice(2).match(/../g) ?? [], (x) => Number.parseInt(x, 16));
34
+ }
35
+ /** A uint256 word. */
36
+ export function word(n) {
37
+ if (n < 0n || n >= 2n ** 256n)
38
+ throw new RangeError('not a uint256');
39
+ return fromHex(`0x${n.toString(16).padStart(64, '0')}`, 32, 'word');
40
+ }
41
+ /** An address as a 32-byte word. */
42
+ export const addressWord = (a) => concat(new Uint8Array(12), fromHex(a, 20, 'address'));
43
+ /** The EIP-712 digest a wallet signs for `typedData(usdc, a)`. */
44
+ export function digest(usdc, a) {
45
+ const domainType = keccak_256(utf8.encode('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'));
46
+ const domain = keccak_256(concat(domainType, keccak_256(utf8.encode('USD Coin')), keccak_256(utf8.encode('2')), word(BigInt(BASE_CHAIN_ID)), addressWord(usdc)));
47
+ const type = keccak_256(utf8.encode(`${PRIMARY_TYPE}(${FIELDS.map((f) => `${f.type} ${f.name}`)})`));
48
+ const struct = keccak_256(concat(type, addressWord(a.from), addressWord(a.to), word(a.value), word(a.validAfter), word(a.validBefore), fromHex(a.nonce, 32, 'nonce')));
49
+ return keccak_256(concat(new Uint8Array([0x19, 0x01]), domain, struct));
50
+ }
51
+ /**
52
+ * The address that signed `digest`, or null. Only what USDC itself accepts: 65 bytes r ‖ s ‖ v,
53
+ * v 27 or 28, low s (FiatToken's ECRecover refuses the rest).
54
+ */
55
+ export function signer(digestBytes, signature) {
56
+ if (!/^0x[0-9a-fA-F]{130}$/.test(signature))
57
+ return null;
58
+ const sig = fromHex(signature, 65, 'signature');
59
+ const v = sig[64] ?? 0;
60
+ const s = BigInt(toHex(sig.subarray(32, 64)));
61
+ if ((v !== 27 && v !== 28) || s > secp256k1.Point.Fn.ORDER / 2n)
62
+ return null;
63
+ try {
64
+ const pub = secp256k1.Point.fromBytes(secp256k1.recoverPublicKey(concat(new Uint8Array([v - 27]), sig.subarray(0, 64)), digestBytes, {
65
+ prehash: false,
66
+ }));
67
+ return toHex(keccak_256(pub.toBytes(false).subarray(1)).subarray(12));
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
@@ -0,0 +1,86 @@
1
+ import { type FileInfo } from '../near/rpc.js';
2
+ import type { Identity } from '../settle-near/identity.js';
3
+ import { type Purchase } from '../settle-near/purchase.js';
4
+ import { type PaymentStatus } from './base.js';
5
+ import { type TypedData } from './eip3009.js';
6
+ import { type PurchaseRecord } from './purchase.js';
7
+ import type { Store } from './store.js';
8
+ import { type Offer, type PaymentRequired } from './x402.js';
9
+ /** Bound the lifetime of an outstanding payment authorization. */
10
+ export declare const MAX_VALIDITY_SECONDS = 180;
11
+ /** How far the Base RPC's clock may be from ours. */
12
+ export declare const MAX_CLOCK_SKEW_SECONDS = 60;
13
+ interface PreflightOptions {
14
+ nearRpc: string;
15
+ baseRpc: string;
16
+ /** The Annon contract the client trusts. */
17
+ contractId: string;
18
+ /** The file being bought, 0x and 32 bytes, from the link, not from the payment request. */
19
+ fileId: string;
20
+ /** The wallet that pays and later opens the file. */
21
+ buyer: string;
22
+ /** The 402's `PAYMENT-REQUIRED`, read with `readPaymentRequired`. */
23
+ paymentRequired: PaymentRequired;
24
+ /**
25
+ * The URL of the request that got that 402. Its `resource.url` must be this URL, so the buyer
26
+ * proof is never made for a URL another endpoint advertised (download.md section 3).
27
+ */
28
+ url: string;
29
+ /**
30
+ * The PAY2 header served at `extensions.annon.info.header`; the whole file when the offer says
31
+ * `public`. Checked against the registration before anything is signed (x402-annon.md section 7).
32
+ */
33
+ pay2: Uint8Array;
34
+ /** USDC base units: refused above it. */
35
+ maxPrice: bigint;
36
+ /** Moderators the buyer trusts to take a file down. A file with any other is refused. */
37
+ moderators?: readonly string[];
38
+ }
39
+ /** A purchase that passed every check, ready to sign. */
40
+ interface Quote {
41
+ nearRpc: string;
42
+ baseRpc: string;
43
+ contractId: string;
44
+ fileId: string;
45
+ buyer: string;
46
+ seller: Identity;
47
+ /** The registered `pay_to`. */
48
+ to: string;
49
+ usdc: string;
50
+ price: bigint;
51
+ /** What the request allows; capped at `MAX_VALIDITY_SECONDS`. */
52
+ validity: number;
53
+ file: FileInfo;
54
+ paymentRequired: PaymentRequired;
55
+ offer: Offer;
56
+ /** The checked PAY2 header; `authorize` checks it again and the download names its hash. */
57
+ header: Uint8Array;
58
+ }
59
+ /** Every check that can refuse a purchase before the wallet is asked to sign. */
60
+ export declare function preflight(o: PreflightOptions): Promise<Quote>;
61
+ interface Wallet {
62
+ address: string;
63
+ /** EIP-712, as viem's `signTypedData`: returns the 65-byte signature as 0x hex. */
64
+ signTypedData(t: TypedData): Promise<string>;
65
+ }
66
+ interface AuthorizeOptions {
67
+ /** The canonical purchase binding its terms, buyer key, refund and recovery identities. */
68
+ purchase: Purchase;
69
+ buyerSecret: Uint8Array;
70
+ wallet: Wallet;
71
+ /** Where the purchase is recorded by purchase_id (x402-annon.md section 5). */
72
+ store: Store<PurchaseRecord>;
73
+ }
74
+ /**
75
+ * Asks the wallet to sign and returns the `PAYMENT-SIGNATURE` header. The purchase is recorded
76
+ * first (refused if this purchase_id already has a record); the signature is checked
77
+ * to be the buyer's, in the form USDC takes; the payload is recorded, and only then run against
78
+ * USDC and returned.
79
+ */
80
+ export declare function authorize(q: Quote, o: AuthorizeOptions): Promise<{
81
+ header: string;
82
+ purchase: PurchaseRecord;
83
+ }>;
84
+ /** Where a recorded purchase stands on Base, over all its attempts. */
85
+ export declare const status: (baseRpc: string, r: PurchaseRecord) => Promise<PaymentStatus>;
86
+ export {};