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

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 +56 -0
  2. package/CLI.md +378 -0
  3. package/LICENSE +201 -0
  4. package/NOTICE +7 -0
  5. package/README.md +581 -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 +116 -0
  21. package/dist/browser/login.d.ts +1 -0
  22. package/dist/browser/login.js +177 -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 +99 -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 +18 -0
  79. package/dist/node/browser-login.js +212 -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 +147 -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 +393 -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 +135 -3
@@ -0,0 +1,703 @@
1
+ /** Durable keeper policy. All writes are reserved before signing; ambiguous work is monitored. */
2
+ import { base58, base64 } from '@scure/base';
3
+ import { decodeSignedTransaction } from 'near-api-js';
4
+ import { getFile, getPurchase, readOutcome, rpcReply, view } from '../near/rpc.js';
5
+ import { BudgetError, reconcileSpend, reserveSpend, } from '../node/budget.js';
6
+ import { network } from '../settle-near/codec.js';
7
+ import { getDelivery, listOpenDeliveries, materializeRefund, openDeliveryCount, } from './keeper.js';
8
+ import { keeperBalance } from './keeper-balance.js';
9
+ export function retryDelay(tries, jitter = Math.random()) {
10
+ if (!Number.isFinite(jitter) || jitter < 0 || jitter > 1)
11
+ throw new TypeError('invalid keeper jitter');
12
+ return Math.round(([1, 2, 4, 8, 16, 30][Math.min(Math.max(tries - 1, 0), 5)] ?? 30) *
13
+ 60000 *
14
+ (0.9 + 0.2 * jitter));
15
+ }
16
+ const progress = (old, d) => old &&
17
+ (BigInt(d.funded) > BigInt(old.funded) ||
18
+ d.slots.some((s, i) => s.step.state === 2 && !old.paid.includes(i)));
19
+ const observation = (d) => ({
20
+ funded: d.funded,
21
+ paid: d.slots.flatMap((s, i) => (s.step.state === 2 ? [i] : [])),
22
+ });
23
+ export async function runKeeper(o, transport) {
24
+ network(o.network);
25
+ const now = o.now ?? Date.now;
26
+ const scope = `${o.network}|${o.contractId}`;
27
+ const out = { sent: [], errors: [], incidents: [] };
28
+ const belowMinimum = async (d) => {
29
+ const id = `${scope}|delivery:${d.id}|dust`;
30
+ const cached = await o.store.load(id);
31
+ if (cached?.belowMinimum !== undefined)
32
+ return cached.belowMinimum;
33
+ let verdict = false;
34
+ if (d.purchase !== null && BigInt(d.amount) < (o.minPayout ?? 100000n)) {
35
+ const purchase = await getPurchase(o.nearRpc, o.contractId, d.purchase);
36
+ if (!purchase)
37
+ throw new Error('purchase unavailable for dust decision');
38
+ const file = await getFile(o.nearRpc, o.contractId, purchase.file_id);
39
+ if (!file)
40
+ throw new Error('file unavailable for dust decision');
41
+ verdict = BigInt(d.amount) <= BigInt(file.terms.near.price);
42
+ }
43
+ const next = { id, revision: (cached?.revision ?? -1) + 1, belowMinimum: verdict };
44
+ if (await o.store.compareAndSet(id, cached, next))
45
+ return verdict;
46
+ return (await o.store.load(id))?.belowMinimum ?? verdict;
47
+ };
48
+ const maxPages = o.maxPages ?? 20, maxTx = o.maxTransactions ?? 20;
49
+ if (!o.store ||
50
+ !o.budgetStore ||
51
+ !Number.isSafeInteger(maxPages) ||
52
+ maxPages < 1 ||
53
+ !Number.isSafeInteger(maxTx) ||
54
+ maxTx < 1 ||
55
+ o.gasPrice <= 0n ||
56
+ (o.minPayout !== undefined && o.minPayout < 0n) ||
57
+ (o.balanceLowWater !== undefined && o.balanceLowWater < 0n) ||
58
+ (o.storagePerRefund !== undefined && o.storagePerRefund < 0n) ||
59
+ !Number.isSafeInteger(o.maxUnresolved ?? 50) ||
60
+ (o.maxUnresolved ?? 50) < 1)
61
+ throw new TypeError('durable keeper stores, limits and gas price required');
62
+ const update = async (id, patch) => {
63
+ for (;;) {
64
+ const old = await o.store.load(id);
65
+ const next = { ...old, ...patch, id, revision: (old?.revision ?? -1) + 1 };
66
+ if (await o.store.compareAndSet(id, old, next))
67
+ return next;
68
+ }
69
+ };
70
+ // A pause/lock is shared by every delivery under this contract and dependency sender.
71
+ const breakerId = `${scope}|breaker`;
72
+ let breaker = await o.store.load(breakerId);
73
+ const failed = async (id, old, dependency = false) => {
74
+ const tries = (old?.tries ?? 0) + 1;
75
+ await update(id, { tries, nextAt: now() + retryDelay(tries, o.jitter?.() ?? Math.random()) });
76
+ if (dependency) {
77
+ breaker = await update(breakerId, {
78
+ tries: (breaker?.tries ?? 0) + 1,
79
+ nextAt: now() + retryDelay((breaker?.tries ?? 0) + 1, o.jitter?.() ?? Math.random()),
80
+ });
81
+ o.alert?.({ type: 'breaker' });
82
+ }
83
+ };
84
+ const reserveId = `${scope}|reserve-monitor`;
85
+ if (now() - ((await o.store.load(reserveId))?.reserveCheckedAt ?? Number.NEGATIVE_INFINITY) >=
86
+ 60000) {
87
+ try {
88
+ const reserve = await view(o.nearRpc, o.contractId, 'receiver_reserve', {});
89
+ if (typeof reserve !== 'string' || !/^\d+$/.test(reserve))
90
+ throw new Error('invalid receiver reserve');
91
+ await update(reserveId, { reserveCheckedAt: now() });
92
+ if (o.reserveLowWater !== undefined && BigInt(reserve) < o.reserveLowWater)
93
+ o.alert?.({ type: 'reserve_low', reserve });
94
+ }
95
+ catch (error) {
96
+ out.errors.push({ error });
97
+ }
98
+ }
99
+ const activeId = `${scope}|active:${o.sender.accountId}`;
100
+ const untrack = async (id) => {
101
+ for (;;) {
102
+ const old = await o.store.load(activeId);
103
+ if (!old)
104
+ return;
105
+ if (await o.store.compareAndSet(activeId, old, {
106
+ ...old,
107
+ revision: old.revision + 1,
108
+ active: (old.active ?? []).filter((x) => x.id !== id),
109
+ }))
110
+ return;
111
+ }
112
+ };
113
+ const track = async (id, deliveryId) => {
114
+ for (;;) {
115
+ const old = await o.store.load(activeId);
116
+ const active = old?.active ?? [];
117
+ if (active.some((x) => x.id === id))
118
+ return false;
119
+ if (active.length >= (o.maxUnresolved ?? 50))
120
+ return false;
121
+ if (await o.store.compareAndSet(activeId, old, {
122
+ ...old,
123
+ id: activeId,
124
+ revision: (old?.revision ?? -1) + 1,
125
+ active: [...active, { id, deliveryId }],
126
+ }))
127
+ return true;
128
+ }
129
+ };
130
+ const sweepId = `${scope}|sweep`;
131
+ let cursor = (await o.store.load(sweepId))?.sweep;
132
+ if (!cursor || cursor.from >= cursor.count) {
133
+ const block = await rpcReply(o.nearRpc, 'block', { finality: 'final' });
134
+ const hash = block.result?.header?.hash;
135
+ if (block.error || typeof hash !== 'string' || !hash)
136
+ throw new Error('finalized block unavailable');
137
+ cursor = {
138
+ block: hash,
139
+ from: 0,
140
+ count: await openDeliveryCount(o.nearRpc, o.contractId, hash),
141
+ };
142
+ await update(sweepId, { sweep: cursor });
143
+ }
144
+ let attempts = 0;
145
+ let pendingCost = 0n;
146
+ let passBalance;
147
+ const balanceId = `${o.network}|${o.sender.accountId}|balance`;
148
+ const balanceState = async (low, balance, needed) => {
149
+ for (;;) {
150
+ const old = await o.store.load(balanceId);
151
+ if ((old?.balanceLow ?? false) === low)
152
+ return;
153
+ if (await o.store.compareAndSet(balanceId, old, {
154
+ ...old,
155
+ id: balanceId,
156
+ revision: (old?.revision ?? -1) + 1,
157
+ balanceLow: low,
158
+ })) {
159
+ if (low)
160
+ o.alert?.({
161
+ type: 'low_balance',
162
+ balance: balance.toString(),
163
+ needed: needed.toString(),
164
+ });
165
+ return;
166
+ }
167
+ }
168
+ };
169
+ const rejected = async (id, job, deliveryId, result) => {
170
+ if (!(await o.store.compareAndSet(id, job, {
171
+ ...job,
172
+ revision: job.revision + 1,
173
+ stage: 'ready',
174
+ tx: null,
175
+ tries: (job.tries ?? 0) + 1,
176
+ nextAt: now() + retryDelay((job.tries ?? 0) + 1, o.jitter?.() ?? Math.random()),
177
+ })))
178
+ return;
179
+ await untrack(id);
180
+ out.incidents.push({
181
+ classification: 'rejected',
182
+ deliveryId,
183
+ ...(result.reason ? { reason: result.reason } : {}),
184
+ });
185
+ };
186
+ const execute = async (id, method, args, gas, old, d, phases = [], deliveryId = d?.id ?? 0) => {
187
+ if (attempts >= (method === 'settle' && o.refunds ? Math.ceil(maxTx / 2) : maxTx) ||
188
+ (breaker?.nextAt ?? 0) > now())
189
+ return;
190
+ // Cross-process claim. Only the winner of the durable signed-hash CAS may send.
191
+ const finalBlock = await rpcReply(o.nearRpc, 'block', { finality: 'final' });
192
+ const finalHash = finalBlock.result?.header?.hash;
193
+ if (finalBlock.error || !finalHash)
194
+ throw new Error('final gas price block unavailable');
195
+ const price = await rpcReply(o.nearRpc, 'gas_price', { block_id: finalHash });
196
+ const gasPrice = price.result?.gas_price;
197
+ if (price.error ||
198
+ typeof gasPrice !== 'string' ||
199
+ !/^[0-9]+$/.test(gasPrice) ||
200
+ BigInt(gasPrice) > o.gasPrice)
201
+ throw new Error('keeper gas price exceeds configured cap or is unavailable');
202
+ const funds = await keeperBalance({
203
+ nearRpc: o.nearRpc,
204
+ accountId: o.sender.accountId,
205
+ contractId: o.contractId,
206
+ block: finalHash,
207
+ gasPrice: BigInt(gasPrice),
208
+ method,
209
+ args,
210
+ gas,
211
+ });
212
+ const needed = funds.cost > (o.balanceLowWater ?? 0n) ? funds.cost : (o.balanceLowWater ?? 0n);
213
+ const available = funds.balance > pendingCost ? funds.balance - pendingCost : 0n;
214
+ // Judge the alert once after the pass, so cheaper calls cannot clear a costly call's warning.
215
+ passBalance = {
216
+ available: passBalance && passBalance.available < available ? passBalance.available : available,
217
+ needed: passBalance && passBalance.needed > needed ? passBalance.needed : needed,
218
+ };
219
+ if (available < needed)
220
+ return;
221
+ // Only affordable work consumes the half-open dependency's one probe.
222
+ if ((breaker?.tries ?? 0) > 0) {
223
+ const probe = {
224
+ ...breaker,
225
+ id: breakerId,
226
+ revision: (breaker?.revision ?? -1) + 1,
227
+ nextAt: now() + retryDelay(breaker?.tries ?? 1, o.jitter?.() ?? Math.random()),
228
+ };
229
+ if (!(await o.store.compareAndSet(breakerId, breaker, probe)))
230
+ return;
231
+ breaker = probe;
232
+ }
233
+ if (!(await track(id, deliveryId)))
234
+ return;
235
+ const claimed = {
236
+ ...old,
237
+ id,
238
+ revision: (old?.revision ?? -1) + 1,
239
+ stage: 'preparing',
240
+ uncertainAlerted: false,
241
+ ...(d ? { observed: observation(d) } : {}),
242
+ };
243
+ if (!(await o.store.compareAndSet(id, old, claimed))) {
244
+ await untrack(id);
245
+ return;
246
+ }
247
+ attempts++;
248
+ try {
249
+ const storageReserved = method === 'materialize_refund' ? (o.storagePerRefund ?? 40960000000000000000000n) : 0n;
250
+ const reservation = {
251
+ id: `${id}|${claimed.revision}`,
252
+ at: now(),
253
+ amount: String(gas * o.gasPrice * 2n + storageReserved),
254
+ };
255
+ await reserveSpend(o.budgetStore, `${o.network}|${o.sender.accountId}|keeper-spend`, BigInt(reservation.amount), { daily: o.dailyBudget, hourly: o.hourlyBudget }, reservation.at, reservation.id);
256
+ const tx = await transport.sign(method, args, gas);
257
+ const referenceHash = base58.encode(Uint8Array.from(decodeSignedTransaction(base64.decode(tx.signed)).transaction.blockHash));
258
+ const referenceBlock = await rpcReply(o.nearRpc, 'block', { block_id: referenceHash });
259
+ const reference = referenceBlock.result
260
+ ?.header;
261
+ if (referenceBlock.error ||
262
+ reference?.hash !== referenceHash ||
263
+ typeof reference.height !== 'number' ||
264
+ !Number.isSafeInteger(reference.height) ||
265
+ reference.height < 0)
266
+ throw new Error('signed transaction reference block unavailable');
267
+ if (!(await o.store.compareAndSet(id, claimed, {
268
+ ...claimed,
269
+ revision: claimed.revision + 1,
270
+ stage: 'submitted',
271
+ tx: {
272
+ hash: tx.txHash,
273
+ signer: o.sender.accountId,
274
+ signed: tx.signed,
275
+ method,
276
+ args,
277
+ phases,
278
+ reference: { hash: referenceHash, height: reference.height },
279
+ reservation,
280
+ storageReserved: String(storageReserved),
281
+ },
282
+ })))
283
+ return;
284
+ const result = await transport.send(tx.txHash, tx.signed);
285
+ if (result.uncertain) {
286
+ const current = await o.store.load(id);
287
+ if (current &&
288
+ !current.uncertainAlerted &&
289
+ (await o.store.compareAndSet(id, current, {
290
+ ...current,
291
+ revision: current.revision + 1,
292
+ uncertainAlerted: true,
293
+ })))
294
+ o.alert?.({ type: 'uncertain', deliveryId });
295
+ }
296
+ out.sent.push({ method, args, result });
297
+ if (result.rejected) {
298
+ const job = await o.store.load(id);
299
+ if (job?.tx?.hash === tx.txHash)
300
+ await rejected(id, job, deliveryId, result);
301
+ }
302
+ else
303
+ pendingCost += funds.cost;
304
+ }
305
+ catch (error) {
306
+ // Errors before a hash is persisted did not reach send. Preserve the reservation.
307
+ const current = await o.store.load(id);
308
+ if (!current?.tx) {
309
+ await update(id, { stage: 'ready' });
310
+ await failed(id, current);
311
+ await untrack(id);
312
+ }
313
+ out.errors.push({ ...(d ? { deliveryId: d.id } : {}), error });
314
+ out.incidents.push({
315
+ classification: error instanceof BudgetError ? 'budget' : current?.tx ? 'uncertain' : 'transport',
316
+ ...(d ? { deliveryId: d.id } : {}),
317
+ });
318
+ }
319
+ };
320
+ const monitor = async (id, job, deliveryId) => {
321
+ const tx = job.tx;
322
+ if (!tx) {
323
+ if (job.stage === 'preparing') {
324
+ // Send is reachable only after a signed hash is durable. Reclaiming this CAS invalidates
325
+ // an older signer's claim; it must win the hash write before it can submit anything.
326
+ if (!(await o.store.compareAndSet(id, job, {
327
+ ...job,
328
+ revision: job.revision + 1,
329
+ stage: 'ready',
330
+ })))
331
+ return false;
332
+ }
333
+ await untrack(id);
334
+ return true;
335
+ }
336
+ try {
337
+ const reply = await rpcReply(o.nearRpc, 'tx', {
338
+ tx_hash: tx.hash,
339
+ sender_account_id: tx.signer,
340
+ wait_until: 'NONE',
341
+ });
342
+ if (reply.error?.cause?.name === 'UNKNOWN_TRANSACTION') {
343
+ const signed = decodeSignedTransaction(base64.decode(tx.signed)).transaction;
344
+ const final = await rpcReply(o.nearRpc, 'block', { finality: 'final' });
345
+ const header = final.result?.header;
346
+ if (final.error || !header?.hash || !Number.isSafeInteger(header.height))
347
+ throw new Error('final block unavailable');
348
+ const key = await rpcReply(o.nearRpc, 'query', {
349
+ request_type: 'view_access_key',
350
+ block_id: header.hash,
351
+ account_id: tx.signer,
352
+ public_key: signed.publicKey.toString(),
353
+ });
354
+ const nonce = key.result?.nonce;
355
+ const validNonce = nonce !== undefined &&
356
+ /^(0|[1-9][0-9]*)$/.test(String(nonce)) &&
357
+ (typeof nonce !== 'number' || Number.isSafeInteger(nonce));
358
+ const consumed = !key.error && validNonce && BigInt(String(nonce)) >= signed.nonce;
359
+ let expired = false;
360
+ if (!consumed) {
361
+ // near-api-js decodes the hash as a plain number[]; base58 needs a Uint8Array.
362
+ const referenceHash = base58.encode(Uint8Array.from(signed.blockHash));
363
+ // Older records can recover the height through RPC; new records already retain it.
364
+ const reference = tx.reference?.hash === referenceHash
365
+ ? { result: { header: tx.reference } }
366
+ : await rpcReply(o.nearRpc, 'block', { block_id: referenceHash });
367
+ const protocol = await rpcReply(o.nearRpc, 'EXPERIMENTAL_protocol_config', {
368
+ block_id: header.hash,
369
+ });
370
+ const height = reference.result?.header?.height;
371
+ const period = protocol.result
372
+ ?.transaction_validity_period;
373
+ if (reference.error ||
374
+ protocol.error ||
375
+ typeof height !== 'number' ||
376
+ typeof period !== 'number' ||
377
+ !Number.isSafeInteger(height) ||
378
+ height < 0 ||
379
+ !Number.isSafeInteger(period) ||
380
+ period <= 0)
381
+ throw new Error('transaction validity unavailable');
382
+ expired = header.height - height > period + 2;
383
+ if (!expired && (key.error || !validNonce))
384
+ throw new Error('final keeper key unavailable');
385
+ }
386
+ if (consumed || expired) {
387
+ // Final state can reveal execution even when the RPC has discarded the tx result.
388
+ const d = await getDelivery(o.nearRpc, o.contractId, deliveryId);
389
+ const tries = !d || progress(job.observed, d) ? 0 : (job.tries ?? 0) + 1;
390
+ if (!(await o.store.compareAndSet(id, job, {
391
+ ...job,
392
+ revision: job.revision + 1,
393
+ tx: null,
394
+ stage: d || tx.method === 'materialize_refund' ? 'ready' : 'retired',
395
+ tries,
396
+ nextAt: now() + retryDelay(Math.max(1, tries), o.jitter?.() ?? Math.random()),
397
+ ...(d ? { observed: observation(d) } : {}),
398
+ })))
399
+ return false;
400
+ await untrack(id);
401
+ out.incidents.push({
402
+ classification: 'finalized_failure',
403
+ deliveryId,
404
+ reason: consumed ? 'nonce consumed at finality' : 'block hash expired at finality',
405
+ });
406
+ return true;
407
+ }
408
+ if (attempts >= maxTx)
409
+ return false;
410
+ const d = await getDelivery(o.nearRpc, o.contractId, deliveryId);
411
+ const uncertain = d?.fund.state === 1 || d?.slots.some((s) => s.step.state === 1);
412
+ if (!uncertain && (d || tx.method === 'materialize_refund')) {
413
+ attempts++;
414
+ const result = await transport.send(tx.hash, tx.signed);
415
+ out.sent.push({ method: tx.method, args: tx.args, result });
416
+ if (result.rejected) {
417
+ await rejected(id, job, deliveryId, result);
418
+ return true;
419
+ }
420
+ }
421
+ }
422
+ if (reply.error ||
423
+ reply.result?.final_execution_status !== 'FINAL') {
424
+ out.incidents.push({ classification: 'uncertain', deliveryId });
425
+ const current = await o.store.load(id);
426
+ if (current &&
427
+ !current.uncertainAlerted &&
428
+ (await o.store.compareAndSet(id, current, {
429
+ ...current,
430
+ revision: current.revision + 1,
431
+ uncertainAlerted: true,
432
+ })))
433
+ o.alert?.({ type: 'uncertain', deliveryId });
434
+ return false;
435
+ }
436
+ const result = readOutcome(reply.result, o.contractId, {
437
+ txHash: tx.hash,
438
+ signer: tx.signer,
439
+ method: tx.method,
440
+ args: tx.args,
441
+ });
442
+ if (result.state === 'pending')
443
+ return false;
444
+ const events = result.events.filter((e) => ['funding_failed', 'payment_failed'].includes(e.event) &&
445
+ e.data.delivery_id === deliveryId);
446
+ const d = await getDelivery(o.nearRpc, o.contractId, deliveryId);
447
+ const advanced = !d || progress(job.observed, d);
448
+ const receipts = reply.result.receipts_outcome ?? [];
449
+ const trusted = new Set([o.contractId, ...(d ? [d.sender] : [])]);
450
+ const dependency = receipts.some((r) => trusted.has(r.outcome?.executor_id ?? '') &&
451
+ r.outcome?.status?.Failure !== undefined &&
452
+ /\b(paus(?:e|ed|ing)|lock(?:ed|ing)?)\b/i.test(JSON.stringify(r.outcome.status.Failure))) ||
453
+ events.some((e) => /\b(paus(?:e|ed|ing)|lock(?:ed|ing)?)\b/i.test(String(e.data.reason)));
454
+ const claimed = { ...job, revision: job.revision + 1, stage: 'finalizing' };
455
+ if (!(await o.store.compareAndSet(id, job, claimed)))
456
+ return false;
457
+ // Refund only complete FINAL burn accounting. Missing data keeps the conservative hold.
458
+ const final = reply.result;
459
+ const burns = [final.transaction_outcome, ...(final.receipts_outcome ?? [])].map((r) => r?.outcome?.tokens_burnt);
460
+ if (tx.reservation &&
461
+ final.transaction_outcome &&
462
+ Array.isArray(final.receipts_outcome) &&
463
+ burns.every((v) => typeof v === 'string' && /^[0-9]+$/.test(v))) {
464
+ const actual = burns.reduce((sum, v) => sum + BigInt(v), 0n);
465
+ await reconcileSpend(o.budgetStore, `${o.network}|${o.sender.accountId}|keeper-spend`, tx.reservation, actual + BigInt(tx.storageReserved ?? '0'));
466
+ }
467
+ // A successful entry may still have failed child receipts. State is authoritative.
468
+ if (tx.method === 'settle') {
469
+ const phases = new Set([
470
+ ...tx.phases,
471
+ ...events.flatMap((e) => e.event === 'payment_failed' && Number.isSafeInteger(e.data.slot)
472
+ ? [`slot:${e.data.slot}`]
473
+ : []),
474
+ ]);
475
+ for (const phase of phases) {
476
+ const phaseId = `${scope}|delivery:${deliveryId}|${phase}`;
477
+ const old = await o.store.load(phaseId);
478
+ if (old?.handled === tx.hash)
479
+ continue;
480
+ const slot = Number(phase.slice(5));
481
+ const moved = !d ||
482
+ (phase === 'fund'
483
+ ? BigInt(d.funded) > BigInt(job.observed?.funded ?? '0')
484
+ : d.slots[slot]?.step.state === 2);
485
+ if (moved)
486
+ await update(phaseId, {
487
+ tries: 0,
488
+ nextAt: 0,
489
+ handled: tx.hash,
490
+ ...(d ? { observed: observation(d) } : {}),
491
+ });
492
+ else if (d.fund.state !== 1 && (phase === 'fund' || d.slots[slot]?.step.state !== 1)) {
493
+ const tries = (old?.tries ?? 0) + 1;
494
+ await update(phaseId, {
495
+ tries,
496
+ nextAt: now() + retryDelay(tries, o.jitter?.() ?? Math.random()),
497
+ handled: tx.hash,
498
+ observed: observation(d),
499
+ });
500
+ }
501
+ }
502
+ }
503
+ if (result.state === 'refused' || events.length || !advanced)
504
+ out.incidents.push({
505
+ classification: 'finalized_failure',
506
+ deliveryId,
507
+ reason: result.state === 'refused'
508
+ ? result.reason
509
+ : events.length
510
+ ? events.map((e) => String(e.data.reason)).join(',')
511
+ : 'no verified progress',
512
+ });
513
+ const terminalRefund = tx.method === 'materialize_refund' && result.state === 'done' && !events.length;
514
+ const cleared = {
515
+ ...claimed,
516
+ revision: claimed.revision + 1,
517
+ tx: null,
518
+ stage: terminalRefund ? 'retired' : 'ready',
519
+ ...(advanced ? { tries: 0, nextAt: 0 } : {}),
520
+ };
521
+ if (!(await o.store.compareAndSet(id, claimed, cleared)))
522
+ return false;
523
+ await untrack(id);
524
+ if (dependency) {
525
+ const tries = (breaker?.tries ?? 0) + 1;
526
+ breaker = await update(breakerId, {
527
+ tries,
528
+ nextAt: now() + retryDelay(tries, o.jitter?.() ?? Math.random()),
529
+ });
530
+ o.alert?.({ type: 'breaker' });
531
+ }
532
+ else if (advanced && breaker)
533
+ breaker = await update(breakerId, { tries: 0, nextAt: 0 });
534
+ if (tx.method === 'materialize_refund' && !terminalRefund)
535
+ await failed(id, cleared);
536
+ return true;
537
+ }
538
+ catch (error) {
539
+ out.errors.push({ deliveryId, error });
540
+ out.incidents.push({ classification: 'transport', deliveryId });
541
+ return false;
542
+ }
543
+ };
544
+ // Monitor even if retirement removed a delivery from the open index, or a source stops returning a leaf.
545
+ const monitored = new Set();
546
+ for (const work of (await o.store.load(activeId))?.active ?? []) {
547
+ const job = await o.store.load(work.id);
548
+ if (job) {
549
+ await monitor(work.id, job, work.deliveryId);
550
+ monitored.add(work.id);
551
+ }
552
+ else
553
+ await untrack(work.id);
554
+ }
555
+ for (let page = 0; page < maxPages && cursor.from < cursor.count; page++) {
556
+ const from = cursor.from;
557
+ try {
558
+ const deliveries = await listOpenDeliveries(o.nearRpc, o.contractId, from, 50, cursor.block);
559
+ for (const snapshot of deliveries) {
560
+ const ageId = `${scope}|delivery:${snapshot.id}|age`;
561
+ let age = await o.store.load(ageId);
562
+ if (age?.firstSeenAt === undefined)
563
+ age = await update(ageId, { firstSeenAt: now() });
564
+ const seconds = Math.floor((now() - (age.firstSeenAt ?? now())) / 1000);
565
+ if (seconds >= (o.oldestDeliverySeconds ?? 1800) &&
566
+ !age.oldDeliveryAlerted &&
567
+ (await o.store.compareAndSet(ageId, age, {
568
+ ...age,
569
+ revision: age.revision + 1,
570
+ oldDeliveryAlerted: true,
571
+ })))
572
+ o.alert?.({ type: 'old_delivery', deliveryId: snapshot.id, seconds });
573
+ try {
574
+ if (await belowMinimum(snapshot))
575
+ continue;
576
+ const id = `${scope}|delivery:${snapshot.id}|submission`;
577
+ const job = await o.store.load(id);
578
+ if (job?.tx || job?.stage === 'preparing') {
579
+ if (monitored.has(id) || !(await monitor(id, job, snapshot.id)))
580
+ continue;
581
+ }
582
+ let current = await o.store.load(id);
583
+ // The pinned page already has enough state to honor backoff without another RPC.
584
+ // Observed progress in that snapshot may reset backoff; final state is read before sending.
585
+ let snapshotDue = progress(current?.observed, snapshot) ? 0 : (current?.nextAt ?? 0);
586
+ const snapshotPhases = snapshot.fund.state === 0
587
+ ? ['fund']
588
+ : snapshot.slots.flatMap((slot, i) => (slot.step.state === 0 ? [`slot:${i}`] : []));
589
+ for (const phase of snapshotPhases) {
590
+ const record = await o.store.load(`${scope}|delivery:${snapshot.id}|${phase}`);
591
+ if (!progress(record?.observed, snapshot))
592
+ snapshotDue = Math.max(snapshotDue, record?.nextAt ?? 0);
593
+ }
594
+ if (snapshotDue > now()) {
595
+ if (snapshot.fund.state === 1 ||
596
+ (snapshot.fund.state === 2 && snapshot.slots.some((slot) => slot.step.state === 1))) {
597
+ out.incidents.push({ classification: 'in_flight', deliveryId: snapshot.id });
598
+ if (!current?.inFlightAlerted &&
599
+ (await o.store.compareAndSet(id, current, {
600
+ ...current,
601
+ id,
602
+ revision: (current?.revision ?? -1) + 1,
603
+ inFlightAlerted: true,
604
+ })))
605
+ o.alert?.({ type: 'in_flight', deliveryId: snapshot.id });
606
+ }
607
+ continue;
608
+ }
609
+ const d = await getDelivery(o.nearRpc, o.contractId, snapshot.id);
610
+ if (d && (await belowMinimum(d)))
611
+ continue;
612
+ if (!d) {
613
+ await update(id, { stage: 'retired', tries: 0, nextAt: 0, inFlightAlerted: false });
614
+ continue;
615
+ }
616
+ const advanced = progress(current?.observed, d);
617
+ if (advanced) {
618
+ current = await update(id, { tries: 0, nextAt: 0, observed: observation(d) });
619
+ }
620
+ if (d.fund.state === 1 ||
621
+ (d.fund.state === 2 && d.slots.some((s) => s.step.state === 1))) {
622
+ if (!current?.inFlightAlerted) {
623
+ const alerted = {
624
+ ...current,
625
+ id,
626
+ revision: (current?.revision ?? -1) + 1,
627
+ inFlightAlerted: true,
628
+ };
629
+ if (await o.store.compareAndSet(id, current, alerted)) {
630
+ current = alerted;
631
+ o.alert?.({ type: 'in_flight', deliveryId: d.id });
632
+ }
633
+ else
634
+ current = await o.store.load(id);
635
+ }
636
+ out.incidents.push({ classification: 'in_flight', deliveryId: d.id });
637
+ if (d.fund.state === 1)
638
+ continue;
639
+ }
640
+ else if (current?.inFlightAlerted) {
641
+ current = await update(id, { inFlightAlerted: false });
642
+ }
643
+ const phases = d.fund.state === 0
644
+ ? ['fund']
645
+ : d.slots.flatMap((slot, index) => (slot.step.state === 0 ? [`slot:${index}`] : []));
646
+ if (!phases.length)
647
+ continue;
648
+ let due = current?.nextAt ?? 0;
649
+ // Persist a separate deadline and failure count for each funded slot / funding phase.
650
+ for (const phase of phases) {
651
+ const phaseId = `${scope}|delivery:${d.id}|${phase}`;
652
+ let record = await o.store.load(phaseId);
653
+ if (progress(record?.observed, d))
654
+ record = await update(phaseId, { tries: 0, nextAt: 0, observed: observation(d) });
655
+ due = Math.max(due, record?.nextAt ?? 0);
656
+ }
657
+ if (due > now())
658
+ continue;
659
+ await execute(id, 'settle', { delivery_id: d.id }, d.fund.state === 2 ? 450000000000000n : 600000000000000n, current, d, phases);
660
+ }
661
+ catch (error) {
662
+ out.errors.push({ deliveryId: snapshot.id, error });
663
+ }
664
+ }
665
+ }
666
+ catch (error) {
667
+ out.errors.push({ from, error });
668
+ }
669
+ cursor = { ...cursor, from: from + 50 };
670
+ await update(sweepId, { sweep: cursor });
671
+ }
672
+ if (o.refunds && attempts < maxTx) {
673
+ try {
674
+ for (const leaf of (await o.refunds.leaves({
675
+ nearRpc: o.nearRpc,
676
+ contractId: o.contractId,
677
+ limit: maxTx - attempts,
678
+ })).slice(0, maxTx - attempts)) {
679
+ const id = `${scope}|refund:${leaf.index}`;
680
+ const job = await o.store.load(id);
681
+ if (job?.tx || job?.stage === 'preparing') {
682
+ if (monitored.has(id) || !(await monitor(id, job, leaf.leaf.delivery_id)))
683
+ continue;
684
+ }
685
+ const current = await o.store.load(id);
686
+ if (current?.stage === 'retired' ||
687
+ (current?.nextAt ?? 0) > now() ||
688
+ (await getDelivery(o.nearRpc, o.contractId, leaf.leaf.delivery_id)))
689
+ continue;
690
+ // Validate through the same SDK binding and use prepaid storage (deposit zero).
691
+ await materializeRefund({
692
+ call: async (call) => execute(id, 'materialize_refund', call.args, call.gas, current, undefined, [], leaf.leaf.delivery_id),
693
+ }, o.contractId, leaf, 0n);
694
+ }
695
+ }
696
+ catch (error) {
697
+ out.errors.push({ error });
698
+ }
699
+ }
700
+ if (passBalance)
701
+ await balanceState(passBalance.available < passBalance.needed, passBalance.available, passBalance.needed);
702
+ return out;
703
+ }