@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,244 @@
1
+ /** Relay authorization, file eligibility and payment evidence checks. */
2
+ import { isTransient } from '../errors.js';
3
+ import { getFile, getGrant, getPurchase } from '../near/rpc.js';
4
+ import { fileFundingText, reopenSellerText, reopenText, stopSalesText } from '../near/texts.js';
5
+ import { block, blockHeader, getReceipt, paymentLogs } from '../pay/base.js';
6
+ import { createSplit, readSplitState } from '../pay/split.js';
7
+ import { identityJson, verifyAuthorization } from '../settle-near/identity.js';
8
+ import { baseNonceV2, parsePurchaseMessage, purchaseId } from '../settle-near/purchase.js';
9
+ import { termsHash } from '../settle-near/terms.js';
10
+ import { checkProtocolFee } from './fees.js';
11
+ import { asRelayError, refusedBaseTerms, signedByItsWallet, verifiedBaseTerms } from './relay.js';
12
+ import { fail } from './relay-types.js';
13
+ import { verifyReservation } from './reservation.js';
14
+ import { admitTerms, checkedDecision, temporary } from './sponsorship.js';
15
+ export async function checkRelay(r, o) {
16
+ try {
17
+ if ('deadline' in r.args) {
18
+ const now = o.now ?? Math.floor(Date.now() / 1000);
19
+ if ((r.method === 'fund_file_storage' ? r.args.deadline <= now : r.args.deadline < now) ||
20
+ r.args.deadline >
21
+ now +
22
+ (r.method === 'fund_file_storage'
23
+ ? (o.policy.topupMaxDeadlineSeconds ?? 600)
24
+ : o.policy.maxDeadlineSeconds))
25
+ fail('bad_deadline');
26
+ }
27
+ const fileId = r.method === 'register'
28
+ ? r.args.terms.file_id
29
+ : r.method === 'unlock'
30
+ ? parsePurchaseMessage(r.args.purchase, { baseUsdc: o.usdc }).file_id
31
+ : 'file_id' in r.args
32
+ ? r.args.file_id
33
+ : undefined;
34
+ const p = r.method === 'reopen' || r.method === 'claim_key'
35
+ ? await getPurchase(o.nearRpc, o.contractId, r.args.purchase_id)
36
+ : null;
37
+ if ((r.method === 'reopen' || r.method === 'claim_key') && !p)
38
+ fail('not_buyer');
39
+ const target = fileId ?? p?.file_id;
40
+ if (!target)
41
+ return fail('not_buyer');
42
+ const file = await getFile(o.nearRpc, o.contractId, target);
43
+ if (r.method === 'register') {
44
+ if (r.args.terms.contract !== o.contractId ||
45
+ BigInt(r.args.terms.near.price) < o.policy.minPrice ||
46
+ (r.args.terms.base !== null && BigInt(r.args.terms.base.price) < o.policy.minPrice))
47
+ fail('not_sponsored');
48
+ if (file)
49
+ fail('already_registered');
50
+ signedByItsWallet(r, o.contractId, {
51
+ baseUsdc: o.usdc,
52
+ ...(o.nearUsdc ? { nearUsdc: o.nearUsdc } : {}),
53
+ });
54
+ if (o.policy.fee) {
55
+ try {
56
+ checkProtocolFee(r.args.terms, o.policy.fee);
57
+ }
58
+ catch {
59
+ fail('not_sponsored', 'required treasury share missing or invalid');
60
+ }
61
+ }
62
+ if (r.args.terms.base) {
63
+ const provenance = r.baseSplit;
64
+ if (o.policy.baseSponsorship === false || !provenance)
65
+ return fail('not_sponsored');
66
+ try {
67
+ if (r.args.terms.near.recipients.some((r) => r.identity.kind !== 'evm'))
68
+ throw new Error();
69
+ const split = createSplit(provenance.kind, {
70
+ recipients: r.args.terms.near.recipients.map((r) => r.identity.id),
71
+ allocations: r.args.terms.near.recipients.map((r) => BigInt(r.bps)),
72
+ totalAllocation: 10000n,
73
+ distributionIncentive: 0,
74
+ }, provenance.salt);
75
+ if (split.address !== r.args.terms.base.pay_to)
76
+ throw new Error();
77
+ }
78
+ catch {
79
+ fail('not_sponsored', 'Base split does not match recipients');
80
+ }
81
+ }
82
+ if (o.ledger) {
83
+ const listed = await o.policy.sellerAllowed?.(identityJson(r.args.terms.seller));
84
+ if (!listed && o.policy.openPoolEnabled === false)
85
+ temporary();
86
+ await admitTerms(r.args.terms, o, listed ? 'listed' : 'pool');
87
+ if (r.hostingReservation) {
88
+ if (!o.reservation)
89
+ return fail('not_sponsored');
90
+ await verifyReservation(r.hostingReservation, r.args.terms, {
91
+ ...o.reservation,
92
+ now: o.now ?? Math.floor(Date.now() / 1000),
93
+ }).catch(() => fail('not_sponsored'));
94
+ }
95
+ else if (!listed)
96
+ fail('not_sponsored', 'A hosting reservation is required.');
97
+ }
98
+ return;
99
+ }
100
+ if (!file)
101
+ return fail('unknown_file');
102
+ if (file.takenDown)
103
+ fail('taken_down');
104
+ if (r.method === 'fund_file_storage') {
105
+ if (!verifyAuthorization(file.seller, fileFundingText({
106
+ ...r.args,
107
+ network: file.terms.network,
108
+ contract: o.contractId,
109
+ seller: file.seller,
110
+ }), r.auth))
111
+ fail('bad_signature');
112
+ if (file.salesStoppedAt !== null)
113
+ fail('sales_stopped');
114
+ if (!o.ledger || !(await o.ledger.decision(r.args.file_id, file.termsHash)))
115
+ fail('not_sponsored');
116
+ return;
117
+ }
118
+ if (r.method === 'claim_key') {
119
+ const price = p?.route === 'base-usdc' ? file.terms.base?.price : file.terms.near.price;
120
+ if (price === undefined || BigInt(price) < o.policy.minPrice)
121
+ fail('not_sponsored');
122
+ if (!(await getGrant(o.nearRpc, o.contractId, r.args.purchase_id, r.args.grant)))
123
+ fail('not_buyer');
124
+ return;
125
+ }
126
+ if (r.method === 'reopen') {
127
+ if (!p)
128
+ return fail('not_buyer');
129
+ if (!verifyAuthorization(p.recovery, reopenText({
130
+ ...r.args,
131
+ contract: o.contractId,
132
+ file_id: p.file_id,
133
+ recovery: p.recovery,
134
+ }), r.auth))
135
+ fail('bad_signature');
136
+ return;
137
+ }
138
+ if (r.method === 'reopen_seller' || r.method === 'stop_sales') {
139
+ const text = r.method === 'stop_sales'
140
+ ? stopSalesText(o.contractId, r.args, file.seller)
141
+ : reopenSellerText(o.contractId, r.args, file.seller);
142
+ if (!verifyAuthorization(file.seller, text, r.auth))
143
+ fail('bad_signature');
144
+ if (r.method === 'stop_sales' && file.salesStoppedAt !== null)
145
+ fail('sales_stopped');
146
+ return;
147
+ }
148
+ if (r.method !== 'unlock')
149
+ return;
150
+ if (!o.ledger && o.policy.baseSponsorship === false)
151
+ fail('not_sponsored');
152
+ const a = r.args;
153
+ const purchase = parsePurchaseMessage(a.purchase, { baseUsdc: o.usdc });
154
+ if (!o.ledger && BigInt(purchase.price) < o.policy.minPrice)
155
+ fail('not_sponsored');
156
+ const id = purchaseId(purchase, { baseUsdc: o.usdc });
157
+ if (purchase.route !== 'base-usdc' ||
158
+ purchase.contract !== o.contractId ||
159
+ !file.terms.base ||
160
+ purchase.terms_hash !== file.termsHash ||
161
+ purchase.price !== file.terms.base.price ||
162
+ purchase.asset !== o.usdc)
163
+ return fail('bad_request');
164
+ if (o.ledger)
165
+ await checkedDecision(file, o, true);
166
+ else {
167
+ try {
168
+ if (file.terms.near.recipients.some((r) => r.identity.kind !== 'evm'))
169
+ fail('not_sponsored', 'Base split recipients must be EVM identities');
170
+ if (o.policy.fee)
171
+ checkProtocolFee(file.terms, o.policy.fee);
172
+ }
173
+ catch {
174
+ fail('not_sponsored', 'required treasury share missing or invalid');
175
+ }
176
+ const verifiedKey = JSON.stringify([
177
+ o.baseRpc,
178
+ o.nearRpc,
179
+ o.contractId,
180
+ termsHash(file.terms),
181
+ ]);
182
+ const now = Date.now();
183
+ for (const [key, until] of refusedBaseTerms) {
184
+ if (until <= now)
185
+ refusedBaseTerms.delete(key);
186
+ }
187
+ if (refusedBaseTerms.has(verifiedKey))
188
+ fail('not_sponsored', 'Base split code or state does not match registered recipients');
189
+ if (!verifiedBaseTerms.has(verifiedKey)) {
190
+ try {
191
+ await readSplitState(o.baseRpc, file.terms.base.pay_to, {
192
+ recipients: file.terms.near.recipients.map((r) => r.identity.id),
193
+ allocations: file.terms.near.recipients.map((r) => BigInt(r.bps)),
194
+ totalAllocation: 10000n,
195
+ distributionIncentive: 0,
196
+ });
197
+ }
198
+ catch (error) {
199
+ if (isTransient(error))
200
+ fail('unavailable', 'Base split verification unavailable; retry later');
201
+ // Bound attacker-controlled refusals; transient RPC failures are never cached.
202
+ if (refusedBaseTerms.size >= 1024) {
203
+ const oldest = refusedBaseTerms.keys().next().value;
204
+ if (oldest !== undefined)
205
+ refusedBaseTerms.delete(oldest);
206
+ }
207
+ refusedBaseTerms.set(verifiedKey, Date.now() + 60000);
208
+ fail('not_sponsored', 'Base split code or state does not match registered recipients');
209
+ }
210
+ verifiedBaseTerms.add(verifiedKey);
211
+ }
212
+ }
213
+ if (await getPurchase(o.nearRpc, o.contractId, id))
214
+ fail('payment_used');
215
+ const auth = a.logs[0];
216
+ if (!auth)
217
+ return fail('bad_request');
218
+ const found = paymentLogs(await getReceipt(o.baseRpc, `0x${auth.transaction_hash}`), {
219
+ usdc: o.usdc,
220
+ to: file.payTo,
221
+ from: `0x${auth.topics[1]?.slice(-40)}`,
222
+ nonce: baseNonceV2(id),
223
+ price: BigInt(purchase.price),
224
+ });
225
+ if (!found || JSON.stringify(found) !== JSON.stringify(a.logs))
226
+ fail('payment_not_found');
227
+ const safe = await block(o.baseRpc, 'safe');
228
+ if (safe.number < auth.block_number ||
229
+ (await block(o.baseRpc, auth.block_number)).hash !== `0x${auth.block_hash}`)
230
+ fail('settlement_pending');
231
+ if (a.header !== null && (await blockHeader(o.baseRpc, `0x${auth.block_hash}`)) !== a.header)
232
+ fail('bad_header');
233
+ if (file.salesStoppedAt !== null) {
234
+ if (a.header === null)
235
+ fail('bad_header');
236
+ const paidAt = (await block(o.baseRpc, auth.block_number)).timestamp;
237
+ if (BigInt(paidAt) > BigInt(file.salesStoppedAt) + 300n)
238
+ fail('sales_stopped');
239
+ }
240
+ }
241
+ catch (e) {
242
+ asRelayError(e);
243
+ }
244
+ }
@@ -0,0 +1,15 @@
1
+ /** Durable relay status refresh and storage reconciliation. */
2
+ import type { RelayLedger } from './ledger.js';
3
+ import { type CheckOptions, type RelayRequest, type RelayResult, type Sender } from './relay-types.js';
4
+ /** Reconcile final evidence and resume only previously signed transaction bytes. */
5
+ export declare function reconcileRelayStorage(o: {
6
+ ledger?: RelayLedger;
7
+ nearRpc: string;
8
+ contractId: string;
9
+ sender?: Sender;
10
+ disabled?: boolean;
11
+ alert?: (type: string) => void;
12
+ }): Promise<void>;
13
+ /** Durable status never re-signs an unresolved operation. */
14
+ declare function durableStatus(r: RelayRequest, o: CheckOptions): Promise<RelayResult | 'pending' | null>;
15
+ export { durableStatus };
@@ -0,0 +1,293 @@
1
+ /** Durable relay status refresh and storage reconciliation. */
2
+ import { sha256 } from '@noble/hashes/sha2.js';
3
+ import { AnnonError } from '../errors.js';
4
+ import { getFile, getGrant, getPurchase, readOutcome, rpcReply } from '../near/rpc.js';
5
+ import { bytes } from '../settle-near/codec.js';
6
+ import { parsePurchaseMessage, purchaseId } from '../settle-near/purchase.js';
7
+ import { requestHash } from './ledger.js';
8
+ import { enqueue, HOLD, identity, reconciliationCursor, requestBinding, signedByItsWallet, statusReads, } from './relay.js';
9
+ import { lookUp, send, senderKey } from './relay-transactions.js';
10
+ import { fail, } from './relay-types.js';
11
+ /** Reconcile final evidence and resume only previously signed transaction bytes. */
12
+ export async function reconcileRelayStorage(o) {
13
+ if (!o.ledger)
14
+ return;
15
+ const all = Object.values((await o.ledger.read()).operations).filter((op) => op.stage === 'submitted' && op.tx && !op.storageFinalized);
16
+ const offset = (reconciliationCursor.get(o.ledger) ?? 0) % (all.length || 1);
17
+ const pending = [...all.slice(offset), ...all.slice(0, offset)].slice(0, 32);
18
+ reconciliationCursor.set(o.ledger, (offset + pending.length) % (all.length || 1));
19
+ for (const op of pending) {
20
+ const tx = op.tx;
21
+ if (!tx)
22
+ continue;
23
+ const reply = await lookUp(o.nearRpc, tx.txHash, tx.signer);
24
+ if (!o.disabled &&
25
+ reply?.error?.cause?.name === 'UNKNOWN_TRANSACTION' &&
26
+ o.sender?.accountId === tx.signer) {
27
+ const resumed = { ...o, sender: o.sender };
28
+ let expired = false;
29
+ if (tx.referenceHash) {
30
+ try {
31
+ const protocol = await rpcReply(o.nearRpc, 'EXPERIMENTAL_protocol_config', {
32
+ finality: 'final',
33
+ });
34
+ const head = await rpcReply(o.nearRpc, 'block', { finality: 'final' });
35
+ const reference = await rpcReply(o.nearRpc, 'block', { block_id: tx.referenceHash });
36
+ const period = protocol.result
37
+ ?.transaction_validity_period;
38
+ const height = head.result?.header?.height;
39
+ const from = reference.result?.header?.height;
40
+ expired =
41
+ !protocol.error &&
42
+ !head.error &&
43
+ !reference.error &&
44
+ Number.isSafeInteger(period) &&
45
+ Number.isSafeInteger(height) &&
46
+ Number.isSafeInteger(from) &&
47
+ (period ?? 0) > 0 &&
48
+ (from ?? -1) >= 0 &&
49
+ (height ?? 0) - (from ?? 0) > (period ?? 0);
50
+ }
51
+ catch {
52
+ /* Missing expiry evidence keeps the original hold. */
53
+ }
54
+ }
55
+ const result = expired
56
+ ? { txHash: tx.txHash, rejected: true, reason: 'Expired' }
57
+ : await enqueue(() => send(resumed, senderKey(resumed), tx.txHash, tx.signed, false, true), senderKey(resumed));
58
+ if (result.rejected && ['Expired', 'InvalidNonce'].includes(result.reason ?? '')) {
59
+ // Check again after the refusal: a landed transaction always wins over fresh signing.
60
+ const final = await lookUp(o.nearRpc, tx.txHash, tx.signer);
61
+ if (final?.error?.cause?.name === 'UNKNOWN_TRANSACTION') {
62
+ try {
63
+ let landed = false;
64
+ if (op.method === 'register' || op.method === 'stop_sales') {
65
+ const file = await getFile(o.nearRpc, o.contractId, op.fileId);
66
+ if (op.method === 'register' && file?.termsHash === op.termsHash) {
67
+ await o.ledger.registered(op.fileId, op.termsHash);
68
+ landed = true;
69
+ }
70
+ else if (op.method === 'stop_sales')
71
+ landed = file?.salesStoppedAt != null;
72
+ }
73
+ else if (op.method === 'unlock') {
74
+ const args = tx.args.args;
75
+ const id = purchaseId(parsePurchaseMessage(args.purchase));
76
+ landed = !!(await getPurchase(o.nearRpc, o.contractId, id));
77
+ }
78
+ else if (op.method === 'reopen') {
79
+ const args = tx.args.args;
80
+ const purchase = await getPurchase(o.nearRpc, o.contractId, args.purchase_id);
81
+ landed = purchase?.buyer_key === args.fresh_key;
82
+ const grants = purchase?.grants ?? 0;
83
+ for (let grant = grants - 1; !landed && grant >= Math.max(1, grants - 32); grant--)
84
+ landed =
85
+ (await getGrant(o.nearRpc, o.contractId, args.purchase_id, grant))?.buyer_key ===
86
+ args.fresh_key;
87
+ // A bounded pass cannot disprove an older grant outside this window.
88
+ if (!landed && (purchase?.grants ?? 0) > 33) {
89
+ o.alert?.('reconcile_held');
90
+ continue;
91
+ }
92
+ }
93
+ if (op.method === 'claim_key') {
94
+ const args = tx.args;
95
+ // Claim has no persistent effect. An existing grant conservatively retains its quota.
96
+ landed = !!(await getGrant(o.nearRpc, o.contractId, args.purchase_id, args.grant ?? 0));
97
+ }
98
+ else if (op.method === 'reopen_seller') {
99
+ const args = tx.args.args;
100
+ // The unchanged contract stores r || SHA256(domain || file_id || key) as a unit value.
101
+ const marker = sha256(new Uint8Array([
102
+ ...new TextEncoder().encode('annon/reopen-seller/v2\0'),
103
+ ...bytes(args.file_id, 32),
104
+ ...bytes(args.key ?? '', 48),
105
+ ]));
106
+ const prefix = btoa(String.fromCharCode(114, ...marker));
107
+ const state = await rpcReply(o.nearRpc, 'query', {
108
+ request_type: 'view_state',
109
+ account_id: o.contractId,
110
+ prefix_base64: prefix,
111
+ finality: 'final',
112
+ });
113
+ const values = state.result
114
+ ?.values;
115
+ if (state.error ||
116
+ !Array.isArray(values) ||
117
+ values.length > 1 ||
118
+ values.some((v) => v.key !== prefix || v.value !== '')) {
119
+ o.alert?.('reconcile_held');
120
+ continue;
121
+ }
122
+ landed = values.length === 1;
123
+ }
124
+ else if (op.method === 'fund_file_storage') {
125
+ const file = await getFile(o.nearRpc, o.contractId, op.fileId);
126
+ // Balance can be consumed by subsequent purchases. Absence cannot release this hold.
127
+ if (op.fundingBalanceBefore === undefined || !file) {
128
+ o.alert?.('reconcile_held');
129
+ continue;
130
+ }
131
+ landed =
132
+ file.purchaseStorageBalance >=
133
+ BigInt(op.fundingBalanceBefore) + BigInt(op.allocation);
134
+ if (!landed) {
135
+ o.alert?.('reconcile_held');
136
+ continue;
137
+ }
138
+ }
139
+ if (landed) {
140
+ await o.ledger.finalizeStorage(op.id, tx.txHash, 'done');
141
+ continue;
142
+ }
143
+ if (op.method === 'unlock')
144
+ await o.ledger.defer(op.id, { method: 'unlock', args: tx.args.args }, op.requestHash, Date.now() + 30000);
145
+ await o.ledger.reject(op.id);
146
+ if (op.method === 'unlock')
147
+ o.alert?.('paid_unlock_delayed');
148
+ }
149
+ catch (error) {
150
+ o.alert?.(error instanceof AnnonError && error.code === 'settlement_pending'
151
+ ? 'paid_work_full'
152
+ : 'reconcile_held');
153
+ }
154
+ continue;
155
+ }
156
+ }
157
+ await o.ledger.result(op.id, result.rejected ? { txHash: tx.txHash, uncertain: true } : result);
158
+ continue;
159
+ }
160
+ if (reply?.result?.final_execution_status !== 'FINAL')
161
+ continue;
162
+ try {
163
+ const result = readOutcome(reply?.result, o.contractId, {
164
+ txHash: tx.txHash,
165
+ signer: tx.signer,
166
+ method: op.method,
167
+ args: tx.args,
168
+ });
169
+ if (result.state !== 'pending') {
170
+ if (op.method === 'register') {
171
+ const file = await getFile(o.nearRpc, o.contractId, op.fileId);
172
+ if (file?.termsHash === op.termsHash)
173
+ await o.ledger.registered(op.fileId, op.termsHash);
174
+ else {
175
+ await o.ledger.reject(op.id, false);
176
+ continue;
177
+ }
178
+ }
179
+ await o.ledger.finalizeStorage(op.id, tx.txHash, result.state);
180
+ }
181
+ }
182
+ catch {
183
+ /* Unbound evidence cannot release a prepaid hold. */
184
+ }
185
+ }
186
+ }
187
+ /** Durable status never re-signs an unresolved operation. */
188
+ async function durableStatus(r, o) {
189
+ const ledger = o.ledger;
190
+ if (!ledger)
191
+ return null;
192
+ const op = await ledger.operation(identity(r));
193
+ if (!op || op.stage === 'rejected')
194
+ return null;
195
+ if (op.requestHash !== requestHash(requestBinding(r)))
196
+ return fail('bad_request');
197
+ signedByItsWallet(r, o.contractId, {
198
+ baseUsdc: o.usdc,
199
+ ...(o.nearUsdc ? { nearUsdc: o.nearUsdc } : {}),
200
+ });
201
+ if (!op.tx)
202
+ return 'pending';
203
+ if (op.compact && op.stage === 'submitted')
204
+ return op.result ?? { txHash: op.tx.txHash };
205
+ if (r.method === 'register') {
206
+ const file = await getFile(o.nearRpc, o.contractId, op.fileId).catch(() => null);
207
+ if (file?.termsHash === op.termsHash)
208
+ await ledger.registered(op.fileId, op.termsHash);
209
+ else if (op.tx) {
210
+ const reply = await lookUp(o.nearRpc, op.tx.txHash, op.tx.signer);
211
+ if (reply?.result?.final_execution_status === 'FINAL') {
212
+ try {
213
+ readOutcome(reply?.result, o.contractId, {
214
+ txHash: op.tx.txHash,
215
+ signer: op.tx.signer,
216
+ method: r.method,
217
+ args: op.tx.args,
218
+ });
219
+ // An unavailable file read does not prove rejection. Read final state again before releasing.
220
+ const final = await getFile(o.nearRpc, o.contractId, op.fileId);
221
+ if (final?.termsHash === op.termsHash)
222
+ await ledger.registered(op.fileId, op.termsHash);
223
+ else if (!final) {
224
+ await ledger.reject(op.id, false);
225
+ return null;
226
+ }
227
+ }
228
+ catch {
229
+ /* Preserve unresolved registration evidence. */
230
+ }
231
+ }
232
+ }
233
+ }
234
+ if (!op.tx)
235
+ return 'pending';
236
+ if (r.method === 'unlock' &&
237
+ op.storageFinalized &&
238
+ op.at + (o.policy.holdMs ?? HOLD) <= Date.now() &&
239
+ !(await getPurchase(o.nearRpc, o.contractId, identity(r).slice('unlock:'.length)))) {
240
+ await ledger.retire(op.id, op.tx.txHash);
241
+ return null;
242
+ }
243
+ if (r.method === 'claim_key' && op.at + (o.policy.holdMs ?? HOLD) <= Date.now()) {
244
+ const tx = op.tx;
245
+ if (op.finalState === 'released')
246
+ return op.result ?? { txHash: tx.txHash };
247
+ let reads = statusReads.get(ledger);
248
+ if (!reads) {
249
+ reads = new Map();
250
+ statusReads.set(ledger, reads);
251
+ }
252
+ const key = `${op.id}|${tx.txHash}`;
253
+ const previous = reads.get(key);
254
+ if (previous)
255
+ return previous;
256
+ const refresh = (async () => {
257
+ const reply = await lookUp(o.nearRpc, tx.txHash, tx.signer);
258
+ if (reply?.result?.final_execution_status === 'FINAL') {
259
+ try {
260
+ const result = readOutcome(reply?.result, o.contractId, {
261
+ txHash: tx.txHash,
262
+ signer: tx.signer,
263
+ method: r.method,
264
+ args: tx.args,
265
+ });
266
+ if (result.state !== 'pending')
267
+ await ledger.finalizeStorage(op.id, tx.txHash, result.state);
268
+ if (result.state !== 'released' && result.state !== 'pending')
269
+ await ledger.retire(op.id, tx.txHash);
270
+ }
271
+ catch {
272
+ /* An unbound outcome cannot authorize a fresh send. */
273
+ }
274
+ }
275
+ const current = await ledger.operation(op.id);
276
+ if (!current || current.stage === 'rejected')
277
+ return null;
278
+ return (current.result ??
279
+ (current.tx
280
+ ? { txHash: current.tx.txHash, uncertain: true }
281
+ : 'pending'));
282
+ })();
283
+ reads.set(key, refresh);
284
+ try {
285
+ return await refresh;
286
+ }
287
+ finally {
288
+ reads.delete(key);
289
+ }
290
+ }
291
+ return op.result ?? { txHash: op.tx.txHash, uncertain: true };
292
+ }
293
+ export { durableStatus };
@@ -0,0 +1,6 @@
1
+ /** Relay request canonicalization, contract arguments and operation identity. */
2
+ import { type SettlementAssets } from '../settle-near/terms.js';
3
+ import { type RelayRequest } from './relay-types.js';
4
+ export declare function canonicalRequest(input: unknown, assets?: SettlementAssets): RelayRequest;
5
+ /** Contract wire shape: unlock and identity calls wrap args; claim_key is flat. */
6
+ export declare const contractArgs: (r: RelayRequest) => object;