@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,297 @@
1
+ import { usdc } from '../settle-near/codec.js';
2
+ /**
3
+ * Paying for an Annon file on Base: the checks before the buyer's wallet is asked anything, then
4
+ * the EIP-3009 signature, checked and dry-run against USDC before it leaves the client.
5
+ *
6
+ * Every value the buyer signs comes from the Annon contract, never from the payment request: the
7
+ * request is only compared against it. The host can make the purchase fail, not misdirect it.
8
+ */
9
+ import { sha256 } from '@noble/hashes/sha2.js';
10
+ import { sameResource } from '../download/proof.js';
11
+ import { inspect, inspectHeader, Pay2Error } from '../envelope/pay2.js';
12
+ import { keyFromSecret } from '../keys/near-ckd.js';
13
+ import { getConfig, getFile } from '../near/rpc.js';
14
+ import { baseNonceV2, buildPurchaseMessage, purchaseId, } from '../settle-near/purchase.js';
15
+ import { balanceOf, block, dryRun, fail, hasCode, head, PayError, paymentStatus, } from './base.js';
16
+ import { digest, signer as recover, toHex, typedData, } from './eip3009.js';
17
+ import { bounds, purchaseKey, randomHex, } from './purchase.js';
18
+ import { ANNON_EXACT, BASE_NETWORK, PAYMENT_IDENTIFIER, paymentSignature, } from './x402.js';
19
+ /** Bound the lifetime of an outstanding payment authorization. */
20
+ export const MAX_VALIDITY_SECONDS = 180;
21
+ /** How far the Base RPC's clock may be from ours. */
22
+ export const MAX_CLOCK_SKEW_SECONDS = 60;
23
+ const lower = (v) => (typeof v === 'string' ? v.toLowerCase() : undefined);
24
+ const isAddress = (v) => typeof v === 'string' && /^0x[0-9a-fA-F]{40}$/.test(v);
25
+ /**
26
+ * Profile section 7: the header (or, for a public body, the whole file) is the registered file,
27
+ * signed by its seller, with a key stanza for this contract. Before any signature.
28
+ */
29
+ async function checkPay2(pay2, wholeFile, contractId, fileId, file) {
30
+ const listing = {
31
+ fileId,
32
+ cipherHash: file.cipherHash,
33
+ seller: file.seller,
34
+ ...(file.headerHash ? { publishedHeaderHash: file.headerHash } : {}),
35
+ };
36
+ let r;
37
+ try {
38
+ r = await (wholeFile ? inspect : inspectHeader)(pay2, listing, contractId);
39
+ }
40
+ catch (e) {
41
+ if (e instanceof Pay2Error)
42
+ return fail('file_invalid', `the PAY2 file failed its check: ${e.code}`);
43
+ throw e;
44
+ }
45
+ if (!r.usable)
46
+ fail('file_invalid', 'the PAY2 header has no key for this contract');
47
+ return r;
48
+ }
49
+ /** The registration, if the file can still be bought. */
50
+ async function forSale(nearRpc, contractId, fileId) {
51
+ const file = await getFile(nearRpc, contractId, fileId);
52
+ if (!file)
53
+ return fail('unknown_file', 'the file is not registered on the Annon contract');
54
+ if (file.takenDown)
55
+ fail('taken_down', `the file was taken down (${file.takenDown})`);
56
+ if (file.salesStoppedAt !== null)
57
+ fail('sales_stopped', 'the seller stopped selling this file');
58
+ return file;
59
+ }
60
+ /** Every check that can refuse a purchase before the wallet is asked to sign. */
61
+ export async function preflight(o) {
62
+ if (!/^0x[0-9a-f]{64}$/.test(o.fileId))
63
+ throw new TypeError('fileId must be 0x and 32 bytes of lowercase hex');
64
+ if (!isAddress(o.buyer))
65
+ throw new TypeError('buyer must be a 0x EVM address');
66
+ const buyer = o.buyer.toLowerCase();
67
+ // The payment request: only its own shape here; its values are compared with the contract below.
68
+ const pr = o.paymentRequired;
69
+ const named = pr?.resource?.url;
70
+ if (typeof named !== 'string' || !sameResource(named, o.url)) {
71
+ fail('bad_requirements', 'resource.url is not the URL that answered with this 402');
72
+ }
73
+ const offers = (Array.isArray(pr?.accepts) ? pr.accepts : []).filter((a) => a?.scheme === ANNON_EXACT && a?.network === BASE_NETWORK);
74
+ const [req] = offers;
75
+ if (offers.length !== 1 || !req)
76
+ return fail('network_mismatch', 'no single Annon offer on Base');
77
+ const extra = (req.extra ?? {});
78
+ // The extension is optional; when present it must name the file being bought.
79
+ const info = pr.extensions?.annon?.info;
80
+ if (info && (info.contract !== o.contractId || lower(info.fileId) !== o.fileId)) {
81
+ fail('file_mismatch', 'the payment request is for another Annon contract or file');
82
+ }
83
+ const timeout = req.maxTimeoutSeconds;
84
+ if (!Number.isSafeInteger(timeout) || timeout < 1) {
85
+ fail('bad_requirements', 'maxTimeoutSeconds must be a positive integer');
86
+ }
87
+ if (typeof req.amount !== 'string' || !/^\d{1,78}$/.test(req.amount)) {
88
+ fail('bad_requirements', 'amount must be a decimal string');
89
+ }
90
+ // The registration.
91
+ const file = await forSale(o.nearRpc, o.contractId, o.fileId);
92
+ if (file.moderator !== null && !(o.moderators ?? []).includes(file.moderator)) {
93
+ fail('unknown_moderator', `the file's moderator ${file.moderator} is not one you trust`);
94
+ }
95
+ const config = await getConfig(o.nearRpc, o.contractId);
96
+ if (file.terms.network !== config.network ||
97
+ file.terms.near.token !== config.nearUsdc ||
98
+ (file.terms.base && file.terms.base.usdc !== config.usdc))
99
+ fail('network_mismatch');
100
+ if (!(o.pay2 instanceof Uint8Array))
101
+ throw new TypeError('pay2 must be the PAY2 header bytes');
102
+ const checked = await checkPay2(o.pay2, info?.public === true, o.contractId, o.fileId, file);
103
+ if (!file.terms.base)
104
+ fail('method_mismatch', 'the file has no Base route');
105
+ const to = file.payTo;
106
+ // Facilitators take the EIP-712 domain from the offer, so it must be there and be USDC's.
107
+ if (lower(req.asset) !== config.usdc || extra.name !== 'USD Coin' || extra.version !== '2') {
108
+ fail('token_mismatch', 'the payment request is not for USDC on Base');
109
+ }
110
+ const method = extra.assetTransferMethod;
111
+ if (method !== undefined && method !== 'eip3009') {
112
+ fail('method_mismatch', `the offer is not a plain Annon transfer`);
113
+ }
114
+ // Extension v3 settles before serving and accepts only the commitment nonce.
115
+ const flow = extra.paymentFlow;
116
+ if (info?.version !== 3 || req.extra?.nonceVersion !== 2 || flow !== 'upfront') {
117
+ fail('method_mismatch', `unsupported payment flow ${String(extra.paymentFlow)}`);
118
+ }
119
+ // A split in the payment itself would take part of it away from the registered pay_to.
120
+ if (extra.splits !== undefined && !(Array.isArray(extra.splits) && extra.splits.length === 0)) {
121
+ fail('method_mismatch', 'the offer splits the payment; Annon pays the registered pay_to only');
122
+ }
123
+ if (lower(req.payTo) !== to) {
124
+ fail('recipient_mismatch', 'the payment request pays an address the registration does not');
125
+ }
126
+ if (BigInt(req.amount) !== file.price) {
127
+ fail('price_mismatch', 'the payment request asks a different price than the registration');
128
+ }
129
+ if (file.price > o.maxPrice)
130
+ fail('price_above_max', 'the price is above your limit');
131
+ // The buyer's wallet on Base.
132
+ const h = await head(o.baseRpc);
133
+ if (await hasCode(o.baseRpc, buyer, h.number)) {
134
+ fail('smart_wallet_unsupported', 'the wallet is a contract or delegated (EIP-7702); pay from a plain account');
135
+ }
136
+ if ((await balanceOf(o.baseRpc, config.usdc, buyer, h.number)) < file.price) {
137
+ fail('insufficient_balance', 'not enough USDC on Base');
138
+ }
139
+ return {
140
+ nearRpc: o.nearRpc,
141
+ baseRpc: o.baseRpc,
142
+ contractId: o.contractId,
143
+ fileId: o.fileId,
144
+ buyer,
145
+ seller: file.seller,
146
+ to,
147
+ usdc: config.usdc,
148
+ price: file.price,
149
+ validity: Math.min(timeout, MAX_VALIDITY_SECONDS),
150
+ file,
151
+ paymentRequired: pr,
152
+ offer: req,
153
+ header: checked.header,
154
+ };
155
+ }
156
+ /** The authorization an attempt signs. */
157
+ const authorizationOf = (r, attempt = r.attempts[0]) => ({
158
+ from: r.terms.buyer,
159
+ to: r.terms.to,
160
+ value: BigInt(r.terms.price),
161
+ validAfter: 0n,
162
+ validBefore: BigInt(attempt.validBefore),
163
+ nonce: r.terms.nonce,
164
+ });
165
+ /**
166
+ * Asks the wallet to sign and returns the `PAYMENT-SIGNATURE` header. The purchase is recorded
167
+ * first (refused if this purchase_id already has a record); the signature is checked
168
+ * to be the buyer's, in the form USDC takes; the payload is recorded, and only then run against
169
+ * USDC and returned.
170
+ */
171
+ export async function authorize(q, o) {
172
+ if (lower(o.wallet.address) !== q.buyer)
173
+ throw new TypeError('the wallet is not the buyer the quote was made for');
174
+ const p = o.purchase;
175
+ const id = purchaseId(p, { baseUsdc: q.usdc });
176
+ const message = buildPurchaseMessage(p, { baseUsdc: q.usdc });
177
+ if (p.route !== 'base-usdc' ||
178
+ p.contract !== q.contractId ||
179
+ p.file_id !== q.fileId ||
180
+ p.terms_hash !== q.file.termsHash ||
181
+ p.price !== q.price.toString() ||
182
+ p.asset !== q.usdc ||
183
+ toHex(keyFromSecret(o.buyerSecret).publicKeyBytes) !== p.buyer_key ||
184
+ BigInt(p.expires_at) <= BigInt(Math.floor(Date.now() / 1000)))
185
+ throw new TypeError('purchase does not match the checked quote/key');
186
+ // Both clocks are read, and checked against each other, before the registration: however long the
187
+ // reads below stall, and even if a clock is corrected meanwhile, the deadline stays within 180 s
188
+ // of a moment the file was still on sale (a stop must be checked again before settlement).
189
+ const h = await head(q.baseRpc);
190
+ const local = Math.floor(Date.now() / 1000);
191
+ if (Math.abs(h.timestamp - local) > MAX_CLOCK_SKEW_SECONDS) {
192
+ fail('transport', 'the Base RPC is not at the current time; its deadline would be wrong');
193
+ }
194
+ // The quote may be old: the file must still be for sale on the same terms.
195
+ const now = await forSale(q.nearRpc, q.contractId, q.fileId);
196
+ if (now.termsHash !== q.file.termsHash ||
197
+ now.price !== q.price ||
198
+ now.payTo !== q.file.payTo ||
199
+ now.moderator !== q.file.moderator ||
200
+ now.cipherHash !== q.file.cipherHash) {
201
+ fail('price_mismatch', 'the registration changed since the quote; check it again');
202
+ }
203
+ // Never sign without a checked header, even for a quote not made by `preflight`.
204
+ await checkPay2(q.header, false, q.contractId, q.fileId, now);
205
+ // The search for the payment starts at the finalized block: a reorg or a lying RPC can't put the
206
+ // payment below it.
207
+ const final = await block(q.baseRpc, 'finalized');
208
+ const start = Math.min(h.timestamp, local);
209
+ const created = {
210
+ schemaVersion: 2,
211
+ recoveryProved: false,
212
+ payment: {
213
+ route: 'base',
214
+ amountUsdc: usdc(q.price),
215
+ amountBaseUnits: q.price.toString(),
216
+ asset: { symbol: 'USDC', id: q.usdc },
217
+ network: BASE_NETWORK,
218
+ },
219
+ id,
220
+ purchase: p,
221
+ message,
222
+ buyerSecret: toHex(o.buyerSecret),
223
+ quote: null,
224
+ recovery: p.recovery,
225
+ checkedHeaderHash: toHex(sha256(q.header)),
226
+ revision: 0,
227
+ terms: {
228
+ network: BASE_NETWORK,
229
+ contractId: q.contractId,
230
+ fileId: q.fileId,
231
+ buyer: q.buyer,
232
+ seller: q.seller,
233
+ to: q.to,
234
+ usdc: q.usdc,
235
+ price: q.price.toString(),
236
+ buyerKey: p.buyer_key,
237
+ nonce: baseNonceV2(id),
238
+ headerHash: toHex(sha256(q.header)),
239
+ cipherHash: q.file.cipherHash,
240
+ moderator: q.file.moderator,
241
+ },
242
+ attempts: [
243
+ {
244
+ attemptId: randomHex(16),
245
+ ...(q.paymentRequired.extensions?.[PAYMENT_IDENTIFIER]
246
+ ? { paymentId: `pay_${randomHex(16)}` }
247
+ : {}),
248
+ preparedAt: start,
249
+ searchFromBlock: Math.min(final.number, h.number),
250
+ // The earliest reading: no clock and no stall can stretch the window.
251
+ validBefore: Math.min(start + q.validity, Number(p.expires_at)).toString(),
252
+ paymentRequired: q.paymentRequired,
253
+ },
254
+ ],
255
+ };
256
+ const key = purchaseKey(id);
257
+ if (!(await o.store.compareAndSet(key, null, created))) {
258
+ fail('purchase_open', 'this wallet has an open purchase of this file: follow its nextStep');
259
+ }
260
+ const attempt = created.attempts[0];
261
+ const a = authorizationOf(created);
262
+ const signature = await o.wallet.signTypedData(typedData(q.usdc, a));
263
+ if (typeof signature !== 'string' || recover(digest(q.usdc, a), signature) !== q.buyer) {
264
+ fail('bad_signature', 'the wallet did not return a plain signature from the buyer');
265
+ }
266
+ const header = paymentSignature(q.paymentRequired, q.offer, a, signature, p, attempt.paymentId);
267
+ // Recorded before any RPC sees it: a crash from here on can only resend this payload.
268
+ const purchase = {
269
+ ...created,
270
+ revision: 1,
271
+ attempts: [{ ...attempt, payload: header }],
272
+ };
273
+ if (!(await o.store.compareAndSet(key, created, purchase))) {
274
+ const e = new PayError('store_conflict', 'the purchase changed while the wallet signed');
275
+ e.purchase = created;
276
+ throw e;
277
+ }
278
+ try {
279
+ await dryRun(q.baseRpc, q.usdc, a, signature);
280
+ }
281
+ catch (e) {
282
+ // The RPC has seen a valid authorization: it can still land, so the purchase stays open.
283
+ if (e instanceof PayError)
284
+ e.purchase = purchase;
285
+ throw e;
286
+ }
287
+ return { header, purchase };
288
+ }
289
+ /** Where a recorded purchase stands on Base, over all its attempts. */
290
+ export const status = (baseRpc, r) => paymentStatus(baseRpc, {
291
+ usdc: r.terms.usdc,
292
+ to: r.terms.to,
293
+ from: r.terms.buyer,
294
+ nonce: r.terms.nonce,
295
+ price: BigInt(r.terms.price),
296
+ ...bounds(r),
297
+ });
@@ -0,0 +1,103 @@
1
+ import type { OneClickQuote } from '../oneclick/oneclick.js';
2
+ import type { BuyRoute } from '../routes.js';
3
+ import type { Identity } from '../settle-near/identity.js';
4
+ import type { Purchase } from '../settle-near/purchase.js';
5
+ /**
6
+ * A purchase on the buyer's side (x402-annon.md section 5): what is recorded before the wallet signs,
7
+ * and the signed payload recorded before anything else sees it. Records are addressed by purchase commitment; the original grant secret remains available after acceptance.
8
+ */
9
+ import type { Versioned } from './store.js';
10
+ import type { PaymentRequired } from './x402.js';
11
+ /** What the purchase pays for; fixed when it is created. */
12
+ interface PurchaseTerms {
13
+ network: string;
14
+ contractId: string;
15
+ fileId: string;
16
+ buyer: string;
17
+ seller: Identity;
18
+ /** The registered `pay_to`. */
19
+ to: string;
20
+ usdc: string;
21
+ price: string;
22
+ /** The one-time key the nonce commits to, 0x and 48 bytes. */
23
+ buyerKey: string;
24
+ /** base_nonce for the purchase commitment. */
25
+ nonce: string;
26
+ /** `SHA256` of the PAY2 header checked before signing (x402-annon.md section 7), 0x and lowercase. */
27
+ headerHash: string;
28
+ cipherHash: string;
29
+ moderator: string | null;
30
+ }
31
+ /** A saved Base payment authorization attempt. */
32
+ export interface PaymentAttempt {
33
+ attemptId: string;
34
+ /** The `payment-identifier` id of its payload, when the server advertised the extension. */
35
+ paymentId?: string;
36
+ /** Unix seconds, before the wallet was asked. */
37
+ preparedAt: number;
38
+ /** The Base block its payment is searched from: finalized when it was prepared. */
39
+ searchFromBlock: number;
40
+ validBefore: string;
41
+ /** The 402 it answers. */
42
+ paymentRequired: PaymentRequired;
43
+ /**
44
+ * The `PAYMENT-SIGNATURE` header: a signed authorization anyone can submit until `validBefore`.
45
+ * Missing when the client stopped between the wallet and this record: then it may be signed.
46
+ */
47
+ payload?: string;
48
+ }
49
+ export interface SourceAsset {
50
+ chain: string;
51
+ asset: string;
52
+ symbol?: string;
53
+ decimals?: number;
54
+ contractAddress?: string;
55
+ }
56
+ export interface SourcePayment extends SourceAsset {
57
+ amount: string;
58
+ amountFormatted?: string;
59
+ depositBefore: string;
60
+ }
61
+ /** Public payment facts, without authorization payloads or deposit destinations. */
62
+ export interface PaymentSummary {
63
+ amountUsdc: string;
64
+ amountBaseUnits: string;
65
+ asset: {
66
+ symbol: 'USDC';
67
+ id: string;
68
+ };
69
+ network: string;
70
+ route: BuyRoute;
71
+ source?: SourcePayment;
72
+ }
73
+ export interface PurchaseRecord extends Versioned {
74
+ schemaVersion: 2;
75
+ /** Durable ownership proof checkpoint. */
76
+ recoveryProved: boolean;
77
+ /** Source details are the reviewed 1Click quote. */
78
+ payment: PaymentSummary;
79
+ nearSubmission?: {
80
+ state: 'uncertain';
81
+ txHash: string;
82
+ };
83
+ paymentState?: 'expired' | 'canceled';
84
+ purchase: Purchase;
85
+ message: string;
86
+ buyerSecret: string;
87
+ quote: OneClickQuote | null;
88
+ recovery: Identity;
89
+ checkedHeaderHash: string;
90
+ terms: PurchaseTerms;
91
+ attempts: PaymentAttempt[];
92
+ }
93
+ /** Records are addressed by the commitment, independent of the paying wallet. */
94
+ export declare const purchaseKey: (purchaseId: string) => string;
95
+ /** What a search over every attempt covers: the earliest start, the latest deadline. */
96
+ export declare function bounds(r: PurchaseRecord): {
97
+ block: number;
98
+ validBefore: bigint;
99
+ };
100
+ /** The recorded payload that can still settle by `now`, latest first; null if none can. */
101
+ export declare function resendable(r: PurchaseRecord, now: number): string | null;
102
+ export declare const randomHex: (bytes: number) => string;
103
+ export {};
@@ -0,0 +1,20 @@
1
+ /** Records are addressed by the commitment, independent of the paying wallet. */
2
+ export const purchaseKey = (purchaseId) => `annon/purchase/${purchaseId}`;
3
+ /** What a search over every attempt covers: the earliest start, the latest deadline. */
4
+ export function bounds(r) {
5
+ if (!r.attempts.length)
6
+ throw new TypeError('a purchase record has at least one attempt');
7
+ return {
8
+ block: Math.min(...r.attempts.map((a) => a.searchFromBlock)),
9
+ validBefore: r.attempts.reduce((m, a) => (BigInt(a.validBefore) > m ? BigInt(a.validBefore) : m), 0n),
10
+ };
11
+ }
12
+ /** The recorded payload that can still settle by `now`, latest first; null if none can. */
13
+ export function resendable(r, now) {
14
+ for (const a of [...r.attempts].reverse()) {
15
+ if (a.payload && BigInt(a.validBefore) > BigInt(now))
16
+ return a.payload;
17
+ }
18
+ return null;
19
+ }
20
+ export const randomHex = (bytes) => Array.from(crypto.getRandomValues(new Uint8Array(bytes)), (b) => b.toString(16).padStart(2, '0')).join('');
@@ -0,0 +1,31 @@
1
+ import { type EvmLog } from './base.js';
2
+ import { type PurchaseRecord } from './purchase.js';
3
+ import type { Store } from './store.js';
4
+ type NextStep = {
5
+ step: 'claim_key';
6
+ args: {
7
+ purchase_id: string;
8
+ grant: number;
9
+ };
10
+ } | {
11
+ step: 'unlock';
12
+ args: {
13
+ purchase: string;
14
+ logs: EvmLog[];
15
+ header: string | null;
16
+ };
17
+ } | {
18
+ step: 'resend';
19
+ header: string;
20
+ } | {
21
+ step: 'wait';
22
+ } | {
23
+ step: 'buy';
24
+ };
25
+ interface NextStepOptions {
26
+ nearRpc: string;
27
+ baseRpc: string;
28
+ store: Store<PurchaseRecord>;
29
+ }
30
+ export declare function nextStep(r: PurchaseRecord, o: NextStepOptions): Promise<NextStep>;
31
+ export {};
@@ -0,0 +1,34 @@
1
+ /** Resume by purchase commitment; wallet addresses never establish entitlement. */
2
+ import { getPurchase } from '../near/rpc.js';
3
+ import { blockHeader, PayError } from './base.js';
4
+ import { status } from './pay.js';
5
+ import { purchaseKey, resendable } from './purchase.js';
6
+ export async function nextStep(r, o) {
7
+ if (await getPurchase(o.nearRpc, r.purchase.contract, r.id))
8
+ return { step: 'claim_key', args: { purchase_id: r.id, grant: 0 } };
9
+ if (r.purchase.route === 'near-usdc')
10
+ return { step: 'wait' };
11
+ const s = await status(o.baseRpc, r);
12
+ if (s.state === 'pending') {
13
+ const header = resendable(r, Math.floor(Date.now() / 1000));
14
+ return header ? { step: 'resend', header } : { step: 'wait' };
15
+ }
16
+ if (s.state !== 'paid') {
17
+ if (r.paymentState !== s.state &&
18
+ !(await o.store.compareAndSet(purchaseKey(r.id), r, {
19
+ ...r,
20
+ revision: r.revision + 1,
21
+ paymentState: s.state,
22
+ })))
23
+ throw new PayError('store_conflict');
24
+ return { step: 'buy' };
25
+ }
26
+ return {
27
+ step: 'unlock',
28
+ args: {
29
+ purchase: r.message,
30
+ logs: s.logs,
31
+ header: await blockHeader(o.baseRpc, `0x${s.logs[0].block_hash}`),
32
+ },
33
+ };
34
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The seller's side of `specs/x402-annon.md`: the 402 for an Annon file, built from its
3
+ * registration, and the check a server runs on `PAYMENT-SIGNATURE` before it settles. Settlement
4
+ * itself stays with the server's facilitator: any standard x402 one, through `forFacilitator`.
5
+ */
6
+ import { type FileInfo } from '../near/rpc.js';
7
+ import { type Purchase } from '../settle-near/purchase.js';
8
+ import { type Offer, type PaymentRequired } from './x402.js';
9
+ export interface OfferOptions {
10
+ nearRpc: string;
11
+ contractId: string;
12
+ fileId: string;
13
+ /**
14
+ * The PAY2 file (x402 `resource.url`): the paid request returns it, and buyers prove a purchase
15
+ * grant there to get it again (specs/download.md). https only.
16
+ */
17
+ url: string;
18
+ /** URL of the complete PAY2 header, public (x402-annon.md section 7): the client checks it first. */
19
+ header: string;
20
+ /** The seller publishes the body too: the client then checks the whole file before signing. */
21
+ public?: boolean;
22
+ /** At most this long for the buyer's authorization; the Annon client signs for at most 180. */
23
+ maxTimeoutSeconds?: number;
24
+ paymentMinValiditySeconds?: number;
25
+ /** The seller app's x402 builder code (`^[a-z0-9_]{1,32}$`), declared as `builder-code`. */
26
+ appCode?: string;
27
+ /** Describes the file for x402 discovery (`bazaar`, x402-annon.md section 11). */
28
+ discovery?: {
29
+ description?: string;
30
+ mimeType?: string;
31
+ serviceName?: string;
32
+ tags?: string[];
33
+ iconUrl?: string;
34
+ };
35
+ }
36
+ /**
37
+ * The `PaymentRequired` for a registration already read: the offer while sales are open, none once
38
+ * they are stopped: buyers still prove their purchase grant to download.
39
+ */
40
+ export declare function paymentRequired(o: OfferOptions, file: FileInfo, usdc: string): PaymentRequired;
41
+ /** A payment that may be settled, and what settlement needs. */
42
+ export interface CheckedPayment {
43
+ purchase: Purchase;
44
+ purchaseId: string;
45
+ payer: string;
46
+ /** The buyer's one-time key: `unlock` needs it. */
47
+ buyerKey: string;
48
+ /** The nonce the buyer signed: `base_nonce(purchase_id)`. */
49
+ nonce: string;
50
+ validBefore: bigint;
51
+ /** The payload's `payment-identifier` id, if it has one. */
52
+ paymentId?: string;
53
+ offer: Offer;
54
+ }
55
+ type Payload = {
56
+ x402Version?: unknown;
57
+ accepted?: Offer;
58
+ payload?: {
59
+ signature?: unknown;
60
+ authorization?: Record<string, unknown>;
61
+ };
62
+ extensions?: Record<string, {
63
+ info?: Record<string, unknown>;
64
+ } & Record<string, unknown>>;
65
+ };
66
+ /**
67
+ * What a server runs before it settles: the registration read again now (a file stopped or taken
68
+ * down since the 402 is refused), then `checkPayment` against the offer it gives, with "now" taken
69
+ * from the earlier of this server's clock and Base's (USDC compares `validBefore` with Base time).
70
+ * New settlements require sales to be open and a short outstanding authorization window.
71
+ */
72
+ export declare function verifyPayment(header: string, o: OfferOptions & {
73
+ baseRpc: string;
74
+ }): Promise<CheckedPayment>;
75
+ /** The base64 JSON of `PAYMENT-SIGNATURE`, as an x402 v2 payload. */
76
+ export declare function readPayload(header: string): Payload;
77
+ /**
78
+ * The purchase terms of a payload against the registration and the contract's configuration,
79
+ * without the seller's settings, the sale status or the deadline (x402-annon.md section 10, order of
80
+ * checks): what a retry of a payment already settling must still pass.
81
+ */
82
+ export declare function checkTerms(header: string, t: {
83
+ contractId: string;
84
+ fileId: string;
85
+ usdc: string;
86
+ price: bigint;
87
+ payTo: string;
88
+ termsHash: string;
89
+ }): CheckedPayment;
90
+ /**
91
+ * @internal Use `verifyPayment`: this checks against a `PaymentRequired` that may be stale.
92
+ * Checks `PAYMENT-SIGNATURE` against the `PaymentRequired` this server sent (x402-annon.md section 3):
93
+ * the offer echoed unchanged, the `annon` extension echoed with a valid buyer key, an advertised
94
+ * `payment-identifier` echoed with a valid id, and the nonce the one that key gives. Run it before
95
+ * settling: a payment with any other nonce would move the buyer's money and never release the key.
96
+ * Throws `PayError` `bad_signature` or `bad_requirements`.
97
+ */
98
+ export declare function checkPayment(header: string, pr: PaymentRequired, now?: number): CheckedPayment;
99
+ /**
100
+ * A checked `annon-exact` payment as a standard x402 facilitator's `/verify` and `/settle` take it:
101
+ * the same signed authorization, every extension kept, with the scheme named `exact` in both the
102
+ * payload and the requirements (the official facilitator checks both). Run `verifyPayment` first.
103
+ */
104
+ export declare function forFacilitator(header: string, offer: Offer): {
105
+ x402Version: 2;
106
+ paymentPayload: {
107
+ extensions?: {
108
+ [x: string]: unknown;
109
+ };
110
+ accepted: {
111
+ scheme: string;
112
+ network: string;
113
+ amount: string;
114
+ asset: string;
115
+ payTo: string;
116
+ maxTimeoutSeconds: number;
117
+ extra?: Record<string, unknown>;
118
+ };
119
+ };
120
+ paymentRequirements: {
121
+ scheme: string;
122
+ network: string;
123
+ amount: string;
124
+ asset: string;
125
+ payTo: string;
126
+ maxTimeoutSeconds: number;
127
+ extra?: Record<string, unknown>;
128
+ };
129
+ };
130
+ export {};