@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.
- package/CHANGELOG.md +56 -0
- package/CLI.md +378 -0
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +581 -2
- package/dist/api/annon-types.d.ts +108 -0
- package/dist/api/annon-types.js +2 -0
- package/dist/api/annon.d.ts +117 -0
- package/dist/api/annon.js +1152 -0
- package/dist/api/hosted-record.d.ts +13 -0
- package/dist/api/hosted-record.js +149 -0
- package/dist/api/sharing-types.d.ts +109 -0
- package/dist/api/sharing-types.js +1 -0
- package/dist/api/sharing.d.ts +36 -0
- package/dist/api/sharing.js +879 -0
- package/dist/browser/download.d.ts +19 -0
- package/dist/browser/download.js +52 -0
- package/dist/browser/index.d.ts +23 -0
- package/dist/browser/index.js +22 -0
- package/dist/browser/login-bundle.js +116 -0
- package/dist/browser/login.d.ts +1 -0
- package/dist/browser/login.js +177 -0
- package/dist/browser/purchase-bundle.js +84 -0
- package/dist/browser/purchases.d.ts +98 -0
- package/dist/browser/purchases.js +735 -0
- package/dist/browser/record.d.ts +55 -0
- package/dist/browser/record.js +16 -0
- package/dist/browser/selling.d.ts +146 -0
- package/dist/browser/selling.js +418 -0
- package/dist/browser/wallets.d.ts +53 -0
- package/dist/browser/wallets.js +286 -0
- package/dist/bytes.d.ts +4 -0
- package/dist/bytes.js +16 -0
- package/dist/download/download.d.ts +59 -0
- package/dist/download/download.js +384 -0
- package/dist/download/host.d.ts +56 -0
- package/dist/download/host.js +161 -0
- package/dist/download/http.d.ts +23 -0
- package/dist/download/http.js +87 -0
- package/dist/download/proof.d.ts +20 -0
- package/dist/download/proof.js +79 -0
- package/dist/download/upload.d.ts +130 -0
- package/dist/download/upload.js +408 -0
- package/dist/envelope/evm.d.ts +4 -0
- package/dist/envelope/evm.js +32 -0
- package/dist/envelope/pay2.d.ts +176 -0
- package/dist/envelope/pay2.js +541 -0
- package/dist/envelope/replay.d.ts +45 -0
- package/dist/envelope/replay.js +186 -0
- package/dist/envelope/stream.d.ts +78 -0
- package/dist/envelope/stream.js +372 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +300 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +23 -0
- package/dist/keys/near-ckd.d.ts +47 -0
- package/dist/keys/near-ckd.js +96 -0
- package/dist/limits.d.ts +4 -0
- package/dist/limits.js +4 -0
- package/dist/near/nep413.d.ts +22 -0
- package/dist/near/nep413.js +39 -0
- package/dist/near/rpc.d.ts +170 -0
- package/dist/near/rpc.js +423 -0
- package/dist/near/texts.d.ts +39 -0
- package/dist/near/texts.js +60 -0
- package/dist/net/ip.d.ts +7 -0
- package/dist/net/ip.js +62 -0
- package/dist/net/retry.d.ts +3 -0
- package/dist/net/retry.js +31 -0
- package/dist/node/adapter-checks.d.ts +6 -0
- package/dist/node/adapter-checks.js +13 -0
- package/dist/node/adapter.d.ts +8 -0
- package/dist/node/adapter.js +99 -0
- package/dist/node/agent.d.ts +49 -0
- package/dist/node/agent.js +266 -0
- package/dist/node/amounts.d.ts +2 -0
- package/dist/node/amounts.js +20 -0
- package/dist/node/browser-login.d.ts +18 -0
- package/dist/node/browser-login.js +212 -0
- package/dist/node/budget.d.ts +24 -0
- package/dist/node/budget.js +59 -0
- package/dist/node/buy-commands.d.ts +24 -0
- package/dist/node/buy-commands.js +301 -0
- package/dist/node/cli.d.ts +60 -0
- package/dist/node/cli.js +201 -0
- package/dist/node/command-context.d.ts +82 -0
- package/dist/node/command-context.js +77 -0
- package/dist/node/config.d.ts +37 -0
- package/dist/node/config.js +189 -0
- package/dist/node/core-adapter.d.ts +43 -0
- package/dist/node/core-adapter.js +122 -0
- package/dist/node/errors.d.ts +8 -0
- package/dist/node/errors.js +12 -0
- package/dist/node/index.d.ts +7 -0
- package/dist/node/index.js +5 -0
- package/dist/node/io.d.ts +13 -0
- package/dist/node/io.js +112 -0
- package/dist/node/keychain.d.ts +10 -0
- package/dist/node/keychain.js +67 -0
- package/dist/node/main.d.ts +2 -0
- package/dist/node/main.js +21 -0
- package/dist/node/offers.d.ts +16 -0
- package/dist/node/offers.js +91 -0
- package/dist/node/origin.d.ts +11 -0
- package/dist/node/origin.js +80 -0
- package/dist/node/paths.d.ts +3 -0
- package/dist/node/paths.js +71 -0
- package/dist/node/payment-terms.d.ts +26 -0
- package/dist/node/payment-terms.js +127 -0
- package/dist/node/purchase-commands.d.ts +16 -0
- package/dist/node/purchase-commands.js +237 -0
- package/dist/node/purchase-guard.d.ts +19 -0
- package/dist/node/purchase-guard.js +53 -0
- package/dist/node/relay-transport.d.ts +6 -0
- package/dist/node/relay-transport.js +173 -0
- package/dist/node/resources.d.ts +2 -0
- package/dist/node/resources.js +26 -0
- package/dist/node/retry-gate.d.ts +15 -0
- package/dist/node/retry-gate.js +53 -0
- package/dist/node/seller-commands.d.ts +12 -0
- package/dist/node/seller-commands.js +207 -0
- package/dist/node/sharing.d.ts +3 -0
- package/dist/node/sharing.js +28 -0
- package/dist/node/shops.d.ts +17 -0
- package/dist/node/shops.js +74 -0
- package/dist/node/signer-mock.d.ts +11 -0
- package/dist/node/signer-mock.js +79 -0
- package/dist/node/signer-wallet.d.ts +8 -0
- package/dist/node/signer-wallet.js +147 -0
- package/dist/node/signing.d.ts +29 -0
- package/dist/node/signing.js +200 -0
- package/dist/node/storage-commands.d.ts +11 -0
- package/dist/node/storage-commands.js +50 -0
- package/dist/node/sync-dir.d.ts +1 -0
- package/dist/node/sync-dir.js +11 -0
- package/dist/node/wallets.d.ts +13 -0
- package/dist/node/wallets.js +146 -0
- package/dist/oneclick/oneclick.d.ts +132 -0
- package/dist/oneclick/oneclick.js +342 -0
- package/dist/pay/backup.d.ts +70 -0
- package/dist/pay/backup.js +402 -0
- package/dist/pay/base.d.ts +135 -0
- package/dist/pay/base.js +393 -0
- package/dist/pay/eip3009.d.ts +59 -0
- package/dist/pay/eip3009.js +72 -0
- package/dist/pay/pay.d.ts +86 -0
- package/dist/pay/pay.js +297 -0
- package/dist/pay/purchase.d.ts +103 -0
- package/dist/pay/purchase.js +20 -0
- package/dist/pay/recover.d.ts +31 -0
- package/dist/pay/recover.js +34 -0
- package/dist/pay/seller.d.ts +130 -0
- package/dist/pay/seller.js +444 -0
- package/dist/pay/settle.d.ts +96 -0
- package/dist/pay/settle.js +308 -0
- package/dist/pay/split.d.ts +115 -0
- package/dist/pay/split.js +256 -0
- package/dist/pay/store-node.d.ts +8 -0
- package/dist/pay/store-node.js +224 -0
- package/dist/pay/store.d.ts +54 -0
- package/dist/pay/store.js +135 -0
- package/dist/pay/x402.d.ts +95 -0
- package/dist/pay/x402.js +136 -0
- package/dist/relay/admission.d.ts +8 -0
- package/dist/relay/admission.js +58 -0
- package/dist/relay/capabilities.d.ts +36 -0
- package/dist/relay/capabilities.js +232 -0
- package/dist/relay/fees.d.ts +17 -0
- package/dist/relay/fees.js +105 -0
- package/dist/relay/index.d.ts +10 -0
- package/dist/relay/index.js +8 -0
- package/dist/relay/keeper-balance.d.ts +17 -0
- package/dist/relay/keeper-balance.js +46 -0
- package/dist/relay/keeper-runner.d.ts +114 -0
- package/dist/relay/keeper-runner.js +703 -0
- package/dist/relay/keeper.d.ts +38 -0
- package/dist/relay/keeper.js +73 -0
- package/dist/relay/ledger.d.ts +155 -0
- package/dist/relay/ledger.js +392 -0
- package/dist/relay/relay-checks.d.ts +3 -0
- package/dist/relay/relay-checks.js +244 -0
- package/dist/relay/relay-reconciliation.d.ts +15 -0
- package/dist/relay/relay-reconciliation.js +293 -0
- package/dist/relay/relay-request.d.ts +6 -0
- package/dist/relay/relay-request.js +139 -0
- package/dist/relay/relay-transactions.d.ts +23 -0
- package/dist/relay/relay-transactions.js +182 -0
- package/dist/relay/relay-types.d.ts +128 -0
- package/dist/relay/relay-types.js +17 -0
- package/dist/relay/relay.d.ts +102 -0
- package/dist/relay/relay.js +426 -0
- package/dist/relay/reservation.d.ts +29 -0
- package/dist/relay/reservation.js +132 -0
- package/dist/relay/sponsorship.d.ts +42 -0
- package/dist/relay/sponsorship.js +184 -0
- package/dist/routes.d.ts +2 -0
- package/dist/routes.js +1 -0
- package/dist/settle-near/codec.d.ts +17 -0
- package/dist/settle-near/codec.js +86 -0
- package/dist/settle-near/identity.d.ts +27 -0
- package/dist/settle-near/identity.js +80 -0
- package/dist/settle-near/purchase.d.ts +40 -0
- package/dist/settle-near/purchase.js +153 -0
- package/dist/settle-near/terms.d.ts +37 -0
- package/dist/settle-near/terms.js +89 -0
- package/dist/shops/discovery.d.ts +95 -0
- package/dist/shops/discovery.js +384 -0
- package/dist/wallet/account-only.d.ts +11 -0
- package/dist/wallet/account-only.js +6 -0
- package/dist/wallet/uncertain.d.ts +8 -0
- package/dist/wallet/uncertain.js +13 -0
- package/dist/wallet/wallet.d.ts +100 -0
- package/dist/wallet/wallet.js +204 -0
- package/package.json +135 -3
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { isTransient } from '../errors.js';
|
|
2
|
+
import { getFile, getPurchase, rpcReply, view } from '../near/rpc.js';
|
|
3
|
+
import { registerText } from '../near/texts.js';
|
|
4
|
+
// Preserve dependency evaluation order before the split modules enter the relay cycle.
|
|
5
|
+
import '../net/retry.js';
|
|
6
|
+
import '../pay/base.js';
|
|
7
|
+
import '../pay/split.js';
|
|
8
|
+
import { identityJson, verifyAuthorization } from '../settle-near/identity.js';
|
|
9
|
+
import { parsePurchaseMessage } from '../settle-near/purchase.js';
|
|
10
|
+
import { termsHash } from '../settle-near/terms.js';
|
|
11
|
+
import { admissionBalance, checkAdmissionFloor } from './admission.js';
|
|
12
|
+
import './fees.js';
|
|
13
|
+
import { runKeeper } from './keeper-runner.js';
|
|
14
|
+
import { requestHash } from './ledger.js';
|
|
15
|
+
import './reservation.js';
|
|
16
|
+
import './sponsorship.js';
|
|
17
|
+
import { checkRelay } from './relay-checks.js';
|
|
18
|
+
import { durableStatus } from './relay-reconciliation.js';
|
|
19
|
+
import { canonicalRequest, contractArgs } from './relay-request.js';
|
|
20
|
+
import { send, sign } from './relay-transactions.js';
|
|
21
|
+
import { fail, RelayError, } from './relay-types.js';
|
|
22
|
+
import { admitTerms, relayLimits, temporary } from './sponsorship.js';
|
|
23
|
+
export { checkRelay } from './relay-checks.js';
|
|
24
|
+
export { reconcileRelayStorage } from './relay-reconciliation.js';
|
|
25
|
+
export { canonicalRequest, contractArgs } from './relay-request.js';
|
|
26
|
+
export { RelayError } from './relay-types.js';
|
|
27
|
+
const asRelayError = (e) => {
|
|
28
|
+
if (e instanceof RelayError)
|
|
29
|
+
throw e;
|
|
30
|
+
return fail(isTransient(e) ? 'transport' : 'bad_request', e.message);
|
|
31
|
+
};
|
|
32
|
+
/** Gas for every call: `unlock` needs 150 Tgas, MPC's callbacks use the rest; unused gas returns. */
|
|
33
|
+
const GAS = 300000000000000n;
|
|
34
|
+
/**
|
|
35
|
+
* Prepaid storage counted per write (`register`, `unlock`, `reopen`): more than any of them takes
|
|
36
|
+
* (an unlock reserves 0.005 NEAR, a reopen marker under 0.001).
|
|
37
|
+
*/
|
|
38
|
+
const STORAGE_PER_WRITE = 10n ** 22n;
|
|
39
|
+
// 0.01 NEAR
|
|
40
|
+
/** An operation is held this long after it is sent, so a repeat doesn't burn gas on a refusal. */
|
|
41
|
+
const HOLD = 15 * 60_000;
|
|
42
|
+
/** How many more tries after the first send: the same bytes again, or after PENDING a lookup. */
|
|
43
|
+
const RESENDS = 2;
|
|
44
|
+
/** The first wait before trying again when NEAR says "later" or "pending"; it grows each try. */
|
|
45
|
+
const RETRY_WAIT = 1_000;
|
|
46
|
+
/** Requests waiting for their turn; past this the relayer answers busy. */
|
|
47
|
+
const MAX_WAITING = 32;
|
|
48
|
+
const accounts = new Map();
|
|
49
|
+
const verifiedBaseTerms = new Set();
|
|
50
|
+
const refusedBaseTerms = new Map();
|
|
51
|
+
const queues = new Map();
|
|
52
|
+
const statusReads = new WeakMap();
|
|
53
|
+
const reconciliationCursor = new WeakMap();
|
|
54
|
+
let waiting = 0;
|
|
55
|
+
let checking = 0;
|
|
56
|
+
const isObject = (v) => typeof v === 'object' && v !== null;
|
|
57
|
+
/** The statuses of a transaction in a block (`NONE`: known, not yet in one). */
|
|
58
|
+
const IN_BLOCK = new Set([
|
|
59
|
+
'INCLUDED',
|
|
60
|
+
'EXECUTED_OPTIMISTIC',
|
|
61
|
+
'INCLUDED_FINAL',
|
|
62
|
+
'EXECUTED',
|
|
63
|
+
'FINAL',
|
|
64
|
+
]);
|
|
65
|
+
const inBlock = (result) => {
|
|
66
|
+
const s = result
|
|
67
|
+
?.final_execution_status;
|
|
68
|
+
return typeof s === 'string' && IN_BLOCK.has(s);
|
|
69
|
+
};
|
|
70
|
+
/** Ask for FINAL evidence by hash; a lost reply never proves rejection. */
|
|
71
|
+
const lookUp = (url, tx, accountId) => rpcReply(url, 'tx', { tx_hash: tx, sender_account_id: accountId, wait_until: 'FINAL' }).catch(() => null);
|
|
72
|
+
const senderKey = (o) => `${o.nearRpc}|${o.sender.accountId}|${o.sender.key}`;
|
|
73
|
+
/**
|
|
74
|
+
* What makes two requests the same operation. Only a party that could make the request succeed
|
|
75
|
+
* can hold it: wallet signatures are checked against their authoritative identities before
|
|
76
|
+
* anything is held, and an unlock
|
|
77
|
+
* includes the buyer's key, which only the buyer knows before it is sent.
|
|
78
|
+
*/
|
|
79
|
+
const identity = (r) => {
|
|
80
|
+
if (r.method === 'unlock')
|
|
81
|
+
return `unlock:${JSON.parse(r.args.purchase).purchase.purchase_id}`;
|
|
82
|
+
if (r.method === 'claim_key')
|
|
83
|
+
return `claim:${r.args.purchase_id}:${r.args.grant ?? 0}`;
|
|
84
|
+
if (r.method === 'reopen')
|
|
85
|
+
return `reopen:${r.args.purchase_id}:${r.args.fresh_key}:${r.args.nonce}`;
|
|
86
|
+
if (r.method === 'register')
|
|
87
|
+
return `register:${r.args.terms.file_id}`;
|
|
88
|
+
if (r.method === 'fund_file_storage')
|
|
89
|
+
return `fund_file_storage:${r.args.file_id}:${r.args.nonce}`;
|
|
90
|
+
return `${r.method}:${r.args.file_id}:${r.args.key ?? ''}`;
|
|
91
|
+
};
|
|
92
|
+
const signedByItsWallet = (r, contract, assets) => {
|
|
93
|
+
if (r.method === 'register' &&
|
|
94
|
+
!verifyAuthorization(r.args.terms.seller, registerText(contract, r.args, assets), r.auth))
|
|
95
|
+
fail('bad_signature');
|
|
96
|
+
};
|
|
97
|
+
const quota = (r, p) => r.method === 'register'
|
|
98
|
+
? [`register:${identityJson(r.args.terms.seller)}`, p.registrationsPerSeller ?? 20]
|
|
99
|
+
: r.method === 'claim_key'
|
|
100
|
+
? [`claim:${r.args.purchase_id}:${r.args.grant ?? 0}`, p.claimsPerGrant ?? 3]
|
|
101
|
+
: r.method === 'reopen'
|
|
102
|
+
? [`reopen:${r.args.purchase_id}`, p.reopensPerWalletFile ?? 10]
|
|
103
|
+
: null;
|
|
104
|
+
const requestBinding = (r) => {
|
|
105
|
+
if (r.method !== 'register')
|
|
106
|
+
return r;
|
|
107
|
+
const { hostingReservation: _reservation, ...binding } = r;
|
|
108
|
+
return binding;
|
|
109
|
+
};
|
|
110
|
+
/** Runs jobs one at a time, in order: checks and nonces never interleave. */
|
|
111
|
+
function enqueue(job, scope = 'relay') {
|
|
112
|
+
const next = (queues.get(scope) ?? Promise.resolve()).then(job, job);
|
|
113
|
+
queues.set(scope, next.catch(() => { }));
|
|
114
|
+
return next;
|
|
115
|
+
}
|
|
116
|
+
/** Read an already admitted operation without charging admission limits or sending anything. */
|
|
117
|
+
export async function relayStatus(input, o) {
|
|
118
|
+
try {
|
|
119
|
+
const r = canonicalRequest(input, {
|
|
120
|
+
baseUsdc: o.usdc,
|
|
121
|
+
...(o.nearUsdc ? { nearUsdc: o.nearUsdc } : {}),
|
|
122
|
+
});
|
|
123
|
+
return o.ledger ? durableStatus(r, o) : null;
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
return asRelayError(e);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Every sponsored send reserves durable admission before signing. */
|
|
130
|
+
export async function relay(input, o) {
|
|
131
|
+
const r = canonicalRequest(input, {
|
|
132
|
+
baseUsdc: o.usdc,
|
|
133
|
+
...(o.nearUsdc ? { nearUsdc: o.nearUsdc } : {}),
|
|
134
|
+
});
|
|
135
|
+
if (!o.ledger)
|
|
136
|
+
return temporary(r.method === 'unlock');
|
|
137
|
+
try {
|
|
138
|
+
return await durableRelay(r, o, o.ledger);
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
if (r.method === 'unlock' &&
|
|
142
|
+
e instanceof RelayError &&
|
|
143
|
+
(isTransient(e) ||
|
|
144
|
+
['settlement_pending', 'relayer_unavailable', 'unavailable'].includes(e.code))) {
|
|
145
|
+
await deferPaidUnlock(r, o, e.retryAt ?? Date.now() + 30000);
|
|
146
|
+
if (e.code !== 'settlement_pending')
|
|
147
|
+
return temporary(true, e.retryAt ?? Date.now() + 30000);
|
|
148
|
+
}
|
|
149
|
+
throw e;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Retain bounded paid work while admission or the service is temporarily paused. */
|
|
153
|
+
export async function deferPaidUnlock(input, o, nextAt) {
|
|
154
|
+
if (!o.ledger)
|
|
155
|
+
return;
|
|
156
|
+
const r = canonicalRequest(input, {
|
|
157
|
+
baseUsdc: o.usdc,
|
|
158
|
+
...(o.nearUsdc ? { nearUsdc: o.nearUsdc } : {}),
|
|
159
|
+
});
|
|
160
|
+
if (r.method !== 'unlock')
|
|
161
|
+
return;
|
|
162
|
+
try {
|
|
163
|
+
await o.ledger.defer(identity(r), r, requestHash(requestBinding(r)), nextAt, o.policy.maxWaiting ?? 32);
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
o.alert?.('paid_work_full');
|
|
167
|
+
throw e;
|
|
168
|
+
}
|
|
169
|
+
o.alert?.('paid_unlock_delayed');
|
|
170
|
+
}
|
|
171
|
+
async function durableRelay(r, o, ledger) {
|
|
172
|
+
if (!(await ledger.cutover())) {
|
|
173
|
+
o.alert?.('cutover_unsealed');
|
|
174
|
+
return temporary(true);
|
|
175
|
+
}
|
|
176
|
+
const previous = await durableStatus(r, o);
|
|
177
|
+
if (previous === 'pending')
|
|
178
|
+
return fail('in_flight');
|
|
179
|
+
if (previous)
|
|
180
|
+
return previous;
|
|
181
|
+
const deferred = (await ledger.read()).work[identity(r)];
|
|
182
|
+
if (deferred && deferred.nextAt > Date.now()) {
|
|
183
|
+
if (deferred.requestHash !== requestHash(requestBinding(r)))
|
|
184
|
+
return fail('bad_request');
|
|
185
|
+
const e = new RelayError('settlement_pending');
|
|
186
|
+
e.retryAt = deferred.nextAt;
|
|
187
|
+
throw e;
|
|
188
|
+
}
|
|
189
|
+
if (waiting >= (o.policy.maxWaiting ?? MAX_WAITING) ||
|
|
190
|
+
checking >= (o.policy.maxChecking ?? MAX_WAITING))
|
|
191
|
+
return temporary(r.method === 'unlock');
|
|
192
|
+
checking++;
|
|
193
|
+
try {
|
|
194
|
+
await checkRelay(r, o);
|
|
195
|
+
await checkAdmissionFloor(r.method, {
|
|
196
|
+
...o,
|
|
197
|
+
admissionBalance: o.admissionBalance ?? (() => admissionBalance(o.nearRpc, o.sender.accountId)),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
if (r.method === 'unlock' &&
|
|
202
|
+
e instanceof RelayError &&
|
|
203
|
+
(isTransient(e) || ['unavailable', 'relayer_unavailable'].includes(e.code)))
|
|
204
|
+
return temporary(true);
|
|
205
|
+
throw e;
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
checking--;
|
|
209
|
+
}
|
|
210
|
+
waiting++;
|
|
211
|
+
try {
|
|
212
|
+
return await enqueue(async () => {
|
|
213
|
+
const previous = await durableStatus(r, o);
|
|
214
|
+
if (previous === 'pending')
|
|
215
|
+
return fail('in_flight');
|
|
216
|
+
if (previous)
|
|
217
|
+
return previous;
|
|
218
|
+
if ('deadline' in r.args && r.args.deadline < (o.now ?? Math.floor(Date.now() / 1000)))
|
|
219
|
+
return fail('bad_deadline');
|
|
220
|
+
if (r.method === 'register' &&
|
|
221
|
+
r.hostingReservation &&
|
|
222
|
+
r.hostingReservation.expiresAt <= (o.now ?? Math.floor(Date.now() / 1000)))
|
|
223
|
+
return fail('not_sponsored');
|
|
224
|
+
const purchase = r.method === 'unlock'
|
|
225
|
+
? parsePurchaseMessage(r.args.purchase, { baseUsdc: o.usdc })
|
|
226
|
+
: r.method === 'claim_key' || r.method === 'reopen'
|
|
227
|
+
? await getPurchase(o.nearRpc, o.contractId, r.args.purchase_id)
|
|
228
|
+
: null;
|
|
229
|
+
const fileId = r.method === 'register'
|
|
230
|
+
? r.args.terms.file_id
|
|
231
|
+
: 'file_id' in r.args
|
|
232
|
+
? r.args.file_id
|
|
233
|
+
: purchase?.file_id;
|
|
234
|
+
if (!fileId)
|
|
235
|
+
return fail('unknown_file');
|
|
236
|
+
const file = r.method === 'register' ? null : await getFile(o.nearRpc, o.contractId, fileId);
|
|
237
|
+
const terms = r.method === 'register' ? r.args.terms : file?.terms;
|
|
238
|
+
if (!terms)
|
|
239
|
+
return fail('unknown_file');
|
|
240
|
+
const hash = termsHash(terms, {
|
|
241
|
+
baseUsdc: o.usdc,
|
|
242
|
+
...(o.nearUsdc ? { nearUsdc: o.nearUsdc } : {}),
|
|
243
|
+
});
|
|
244
|
+
let decision = await ledger.decision(fileId, hash);
|
|
245
|
+
if (r.method === 'register') {
|
|
246
|
+
const listed = await o.policy.sellerAllowed?.(identityJson(terms.seller));
|
|
247
|
+
decision = await admitTerms(terms, o, listed ? 'listed' : 'pool');
|
|
248
|
+
}
|
|
249
|
+
const listed = decision?.listed ?? false;
|
|
250
|
+
const allocation = r.method === 'register'
|
|
251
|
+
? listed
|
|
252
|
+
? (o.policy.listedFileBudget ?? 5n * 10n ** 22n)
|
|
253
|
+
: (o.policy.fileBudget ?? 41n * 10n ** 21n)
|
|
254
|
+
: r.method === 'fund_file_storage'
|
|
255
|
+
? BigInt(r.args.amount)
|
|
256
|
+
: 0n;
|
|
257
|
+
if (r.method === 'register') {
|
|
258
|
+
// Query the unchanged contract's current admission floor before making an offer usable.
|
|
259
|
+
const protocol = await rpcReply(o.nearRpc, 'EXPERIMENTAL_protocol_config', {
|
|
260
|
+
finality: 'final',
|
|
261
|
+
});
|
|
262
|
+
const byteCost = protocol.result?.runtime_config?.storage_amount_per_byte;
|
|
263
|
+
if (protocol.error ||
|
|
264
|
+
typeof byteCost !== 'string' ||
|
|
265
|
+
!/^[0-9]+$/.test(byteCost) ||
|
|
266
|
+
BigInt(byteCost) <= 0n ||
|
|
267
|
+
allocation < BigInt(byteCost) * 4096n)
|
|
268
|
+
return temporary();
|
|
269
|
+
}
|
|
270
|
+
const write = ['register', 'unlock', 'reopen', 'reopen_seller', 'fund_file_storage'].includes(r.method);
|
|
271
|
+
const storage = write
|
|
272
|
+
? (r.method === 'fund_file_storage'
|
|
273
|
+
? 0n
|
|
274
|
+
: (o.policy.storagePerWrite ?? STORAGE_PER_WRITE)) + allocation
|
|
275
|
+
: 0n;
|
|
276
|
+
if (write) {
|
|
277
|
+
const balance = await view(o.nearRpc, o.contractId, 'storage_balance_of', {
|
|
278
|
+
account_id: o.sender.accountId,
|
|
279
|
+
});
|
|
280
|
+
const floor = r.method === 'unlock' ? 0n : (o.policy.prepaidUnlockFloor ?? 10n ** 24n);
|
|
281
|
+
if (typeof balance !== 'string' ||
|
|
282
|
+
!/^[0-9]+$/.test(balance) ||
|
|
283
|
+
BigInt(balance) < floor + storage + (await ledger.pendingStorage()))
|
|
284
|
+
return temporary(r.method === 'unlock');
|
|
285
|
+
}
|
|
286
|
+
if (decision && r.method === 'register')
|
|
287
|
+
decision.allocated = String(allocation);
|
|
288
|
+
const id = identity(r), binding = requestHash(requestBinding(r));
|
|
289
|
+
const q = quota(r, o.policy);
|
|
290
|
+
try {
|
|
291
|
+
const prepared = await ledger.prepare({
|
|
292
|
+
id,
|
|
293
|
+
requestHash: binding,
|
|
294
|
+
method: r.method,
|
|
295
|
+
fileId,
|
|
296
|
+
termsHash: hash,
|
|
297
|
+
seller: identityJson(terms.seller),
|
|
298
|
+
listed,
|
|
299
|
+
ip: o.ip ?? 'unknown',
|
|
300
|
+
storage: String(storage),
|
|
301
|
+
allocation: String(allocation),
|
|
302
|
+
limits: relayLimits(o),
|
|
303
|
+
...(decision && r.method === 'register' ? { decision } : {}),
|
|
304
|
+
...(q && r.method !== 'register' ? { quota: { key: q[0], limit: q[1] } } : {}),
|
|
305
|
+
...(r.method === 'register' && r.hostingReservation
|
|
306
|
+
? {
|
|
307
|
+
reservation: {
|
|
308
|
+
key: `${r.hostingReservation.hostOrigin}|${r.hostingReservation.uploadId}`,
|
|
309
|
+
binding,
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
: {}),
|
|
313
|
+
...(r.method === 'fund_file_storage'
|
|
314
|
+
? {
|
|
315
|
+
fundingBalanceBefore: String(file?.purchaseStorageBalance ?? 0n),
|
|
316
|
+
nonce: `${identityJson(terms.seller)}|${r.args.nonce}`,
|
|
317
|
+
nonceDeadline: r.args.deadline,
|
|
318
|
+
topup: {
|
|
319
|
+
lifetime: String(listed
|
|
320
|
+
? (o.policy.listedTopupPerFile ?? 15n * 10n ** 22n)
|
|
321
|
+
: (o.policy.topupPerFile ?? 82n * 10n ** 21n)),
|
|
322
|
+
fileDay: String(listed
|
|
323
|
+
? (o.policy.listedTopupPerFileDay ?? 5n * 10n ** 22n)
|
|
324
|
+
: (o.policy.topupPerFileDay ?? 41n * 10n ** 21n)),
|
|
325
|
+
totalDay: String(o.policy.topupPerDay ?? 5n * 10n ** 23n),
|
|
326
|
+
listedReserved: String(o.policy.topupListedReserved ?? 25n * 10n ** 22n),
|
|
327
|
+
},
|
|
328
|
+
}
|
|
329
|
+
: {}),
|
|
330
|
+
});
|
|
331
|
+
if (prepared.capacityReached)
|
|
332
|
+
o.alert?.('ledger_capacity');
|
|
333
|
+
if (!prepared.fresh)
|
|
334
|
+
return (prepared.operation.result ??
|
|
335
|
+
(prepared.operation.tx
|
|
336
|
+
? { txHash: prepared.operation.tx.txHash, uncertain: true }
|
|
337
|
+
: fail('in_flight')));
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
if (e instanceof RelayError && e.code === 'relayer_unavailable')
|
|
341
|
+
o.alert?.('ledger_capacity');
|
|
342
|
+
if (r.method === 'unlock' && e instanceof RelayError && e.code === 'limited') {
|
|
343
|
+
const pending = new RelayError('settlement_pending');
|
|
344
|
+
if (e.retryAt !== undefined)
|
|
345
|
+
pending.retryAt = e.retryAt;
|
|
346
|
+
throw pending;
|
|
347
|
+
}
|
|
348
|
+
throw e;
|
|
349
|
+
}
|
|
350
|
+
let signed = false;
|
|
351
|
+
try {
|
|
352
|
+
const args = r.method === 'register'
|
|
353
|
+
? {
|
|
354
|
+
args: {
|
|
355
|
+
...r.args,
|
|
356
|
+
...(allocation > 0n ? { file_budget: String(allocation) } : {}),
|
|
357
|
+
},
|
|
358
|
+
auth: r.auth,
|
|
359
|
+
}
|
|
360
|
+
: contractArgs(r);
|
|
361
|
+
const tx = await sign(o, senderKey(o), r.method, args);
|
|
362
|
+
await ledger.signed(id, { ...tx, signer: o.sender.accountId, args });
|
|
363
|
+
signed = true;
|
|
364
|
+
const result = await send(o, senderKey(o), tx.txHash, tx.signed);
|
|
365
|
+
if (result.rejected) {
|
|
366
|
+
await ledger.reject(id);
|
|
367
|
+
return temporary(r.method === 'unlock');
|
|
368
|
+
}
|
|
369
|
+
await ledger.result(id, result);
|
|
370
|
+
if (r.method === 'register') {
|
|
371
|
+
const final = await getFile(o.nearRpc, o.contractId, fileId).catch(() => null);
|
|
372
|
+
if (final?.termsHash === hash)
|
|
373
|
+
await ledger.registered(fileId, hash);
|
|
374
|
+
}
|
|
375
|
+
if (result.uncertain)
|
|
376
|
+
o.alert?.('uncertain');
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
if (!signed && !(await ledger.operation(id))?.tx)
|
|
381
|
+
await ledger.reject(id);
|
|
382
|
+
throw e;
|
|
383
|
+
}
|
|
384
|
+
}, senderKey(o));
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
waiting--;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/** Resume due paid work without creating a second purchase or losing its frozen policy. */
|
|
391
|
+
export async function retryPaidUnlocks(o) {
|
|
392
|
+
if (!o.ledger)
|
|
393
|
+
return;
|
|
394
|
+
for (const [id, work] of Object.entries((await o.ledger.read()).work)) {
|
|
395
|
+
if (work.nextAt > Date.now())
|
|
396
|
+
continue;
|
|
397
|
+
try {
|
|
398
|
+
await relay(work.request, o);
|
|
399
|
+
}
|
|
400
|
+
catch (e) {
|
|
401
|
+
if (e instanceof RelayError &&
|
|
402
|
+
[
|
|
403
|
+
'taken_down',
|
|
404
|
+
'sales_stopped',
|
|
405
|
+
'payment_used',
|
|
406
|
+
'payment_not_found',
|
|
407
|
+
'bad_request',
|
|
408
|
+
'not_sponsored',
|
|
409
|
+
'unknown_file',
|
|
410
|
+
'file_mismatch',
|
|
411
|
+
'bad_signature',
|
|
412
|
+
].includes(e.code))
|
|
413
|
+
await o.ledger.finishWork(id);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/** A dedicated account/key has its own nonce queue, independent from relay traffic. */
|
|
418
|
+
export async function keep(o) {
|
|
419
|
+
const key = senderKey(o);
|
|
420
|
+
return enqueue(() => runKeeper(o, {
|
|
421
|
+
sign: (method, args, gas) => sign(o, key, method, args, gas),
|
|
422
|
+
send: (hash, signed) => send(o, key, hash, signed, true),
|
|
423
|
+
}), key);
|
|
424
|
+
}
|
|
425
|
+
// Load-time declarations stay together in their original evaluation order.
|
|
426
|
+
export { accounts, asRelayError, enqueue, fail, GAS, HOLD, IN_BLOCK, identity, inBlock, isObject, lookUp, quota, RESENDS, RETRY_WAIT, reconciliationCursor, refusedBaseTerms, requestBinding, senderKey, signedByItsWallet, statusReads, verifiedBaseTerms, };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Identity } from '../settle-near/identity.js';
|
|
2
|
+
import { type Terms } from '../settle-near/terms.js';
|
|
3
|
+
export interface HostingReservation {
|
|
4
|
+
v: 1;
|
|
5
|
+
uploadId: string;
|
|
6
|
+
network: string;
|
|
7
|
+
contract: string;
|
|
8
|
+
fileId: string;
|
|
9
|
+
termsHash: string;
|
|
10
|
+
seller: Identity;
|
|
11
|
+
cipherHash: string;
|
|
12
|
+
byteSize: number;
|
|
13
|
+
plaintextSize: number;
|
|
14
|
+
headerSize: number;
|
|
15
|
+
metadataHash: string;
|
|
16
|
+
hostOrigin: string;
|
|
17
|
+
issuedAt: number;
|
|
18
|
+
expiresAt: number;
|
|
19
|
+
signature: string;
|
|
20
|
+
}
|
|
21
|
+
export declare const reservationText: (r: Omit<HostingReservation, "signature">) => string;
|
|
22
|
+
export declare function parseReservation(input: unknown): HostingReservation;
|
|
23
|
+
export declare function verifyReservation(input: unknown, terms: Terms, o: {
|
|
24
|
+
hostOrigin: string;
|
|
25
|
+
publicKey: string;
|
|
26
|
+
now: number;
|
|
27
|
+
fetch?: typeof fetch;
|
|
28
|
+
maxFileBytes?: number;
|
|
29
|
+
}): Promise<HostingReservation>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/** Verify host attestations against a fixed pinned origin and Ed25519 key. */
|
|
2
|
+
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
3
|
+
import { base64urlnopad } from '@scure/base';
|
|
4
|
+
import { bytes, exactKeys } from '../settle-near/codec.js';
|
|
5
|
+
import { identityJson, identityText } from '../settle-near/identity.js';
|
|
6
|
+
import { termsHash } from '../settle-near/terms.js';
|
|
7
|
+
export const reservationText = (r) => [
|
|
8
|
+
'Annon: hosting reservation v1',
|
|
9
|
+
`upload: ${r.uploadId}`,
|
|
10
|
+
`network: ${r.network}`,
|
|
11
|
+
`contract: ${r.contract}`,
|
|
12
|
+
`file: ${r.fileId}`,
|
|
13
|
+
`terms: ${r.termsHash}`,
|
|
14
|
+
`seller: ${identityText(r.seller)}`,
|
|
15
|
+
`cipher: ${r.cipherHash}`,
|
|
16
|
+
`bytes: ${r.byteSize}`,
|
|
17
|
+
`plaintext bytes: ${r.plaintextSize}`,
|
|
18
|
+
`header bytes: ${r.headerSize}`,
|
|
19
|
+
`metadata: ${r.metadataHash}`,
|
|
20
|
+
`host: ${r.hostOrigin}`,
|
|
21
|
+
`issued: ${r.issuedAt}`,
|
|
22
|
+
`expires: ${r.expiresAt}`,
|
|
23
|
+
].join('\n');
|
|
24
|
+
export function parseReservation(input) {
|
|
25
|
+
exactKeys(input, [
|
|
26
|
+
'v',
|
|
27
|
+
'uploadId',
|
|
28
|
+
'network',
|
|
29
|
+
'contract',
|
|
30
|
+
'fileId',
|
|
31
|
+
'termsHash',
|
|
32
|
+
'seller',
|
|
33
|
+
'cipherHash',
|
|
34
|
+
'byteSize',
|
|
35
|
+
'plaintextSize',
|
|
36
|
+
'headerSize',
|
|
37
|
+
'metadataHash',
|
|
38
|
+
'hostOrigin',
|
|
39
|
+
'issuedAt',
|
|
40
|
+
'expiresAt',
|
|
41
|
+
'signature',
|
|
42
|
+
]);
|
|
43
|
+
const r = input;
|
|
44
|
+
if (r.v !== 1 ||
|
|
45
|
+
!/^[0-9a-f]{64}$/.test(r.uploadId) ||
|
|
46
|
+
typeof r.signature !== 'string' ||
|
|
47
|
+
!/^[A-Za-z0-9_-]{86}$/.test(r.signature))
|
|
48
|
+
throw new TypeError('Invalid hosting reservation.');
|
|
49
|
+
for (const k of ['fileId', 'termsHash', 'cipherHash', 'metadataHash'])
|
|
50
|
+
bytes(r[k], 32);
|
|
51
|
+
for (const k of ['byteSize', 'plaintextSize', 'headerSize', 'issuedAt', 'expiresAt'])
|
|
52
|
+
if (!Number.isSafeInteger(r[k]) || r[k] < 0)
|
|
53
|
+
throw new TypeError('Invalid hosting reservation.');
|
|
54
|
+
const origin = new URL(r.hostOrigin);
|
|
55
|
+
if (origin.protocol !== 'https:' ||
|
|
56
|
+
origin.origin !== r.hostOrigin ||
|
|
57
|
+
r.byteSize < 1 ||
|
|
58
|
+
r.byteSize > 50086016 ||
|
|
59
|
+
r.plaintextSize > 50000000 ||
|
|
60
|
+
r.headerSize < 1 ||
|
|
61
|
+
r.headerSize > 65536 ||
|
|
62
|
+
r.byteSize <= r.headerSize ||
|
|
63
|
+
r.expiresAt !== r.issuedAt + 600)
|
|
64
|
+
throw new TypeError('Invalid hosting reservation.');
|
|
65
|
+
identityText(r.seller);
|
|
66
|
+
return r;
|
|
67
|
+
}
|
|
68
|
+
export async function verifyReservation(input, terms, o) {
|
|
69
|
+
const r = parseReservation(input);
|
|
70
|
+
if (r.plaintextSize > (o.maxFileBytes ?? 50000000) ||
|
|
71
|
+
r.hostOrigin !== o.hostOrigin ||
|
|
72
|
+
r.network !== terms.network ||
|
|
73
|
+
r.contract !== terms.contract ||
|
|
74
|
+
r.fileId !== terms.file_id ||
|
|
75
|
+
r.cipherHash !== terms.cipher_hash ||
|
|
76
|
+
r.termsHash !==
|
|
77
|
+
termsHash(terms, {
|
|
78
|
+
nearUsdc: terms.near.token,
|
|
79
|
+
...(terms.base ? { baseUsdc: terms.base.usdc } : {}),
|
|
80
|
+
}) ||
|
|
81
|
+
identityJson(r.seller) !== identityJson(terms.seller) ||
|
|
82
|
+
r.issuedAt > o.now ||
|
|
83
|
+
r.expiresAt <= o.now ||
|
|
84
|
+
!/^[0-9a-f]{64}$/.test(o.publicKey) ||
|
|
85
|
+
!ed25519.verify(base64urlnopad.decode(r.signature), new TextEncoder().encode(reservationText(r)), bytes(`0x${o.publicKey}`, 32), { zip215: false }))
|
|
86
|
+
throw new TypeError('Invalid hosting reservation.');
|
|
87
|
+
const response = await (o.fetch ?? fetch)(`${o.hostOrigin}/v1/upload-reservations/${r.uploadId}`, {
|
|
88
|
+
credentials: 'omit',
|
|
89
|
+
redirect: 'error',
|
|
90
|
+
signal: AbortSignal.timeout(5000),
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok ||
|
|
93
|
+
response.redirected ||
|
|
94
|
+
response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() !== 'application/json')
|
|
95
|
+
throw new TypeError('Hosting reservation unavailable.');
|
|
96
|
+
const reader = response.body?.getReader();
|
|
97
|
+
if (!reader)
|
|
98
|
+
throw new TypeError('Hosting reservation unavailable.');
|
|
99
|
+
const parts = [];
|
|
100
|
+
let size = 0;
|
|
101
|
+
try {
|
|
102
|
+
for (;;) {
|
|
103
|
+
const { done, value } = await reader.read();
|
|
104
|
+
if (done)
|
|
105
|
+
break;
|
|
106
|
+
size += value.length;
|
|
107
|
+
if (size > 16384)
|
|
108
|
+
throw new TypeError('Hosting reservation unavailable.');
|
|
109
|
+
parts.push(value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await reader.cancel();
|
|
114
|
+
}
|
|
115
|
+
const raw = new Uint8Array(size);
|
|
116
|
+
let at = 0;
|
|
117
|
+
for (const part of parts) {
|
|
118
|
+
raw.set(part, at);
|
|
119
|
+
at += part.length;
|
|
120
|
+
}
|
|
121
|
+
const live = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(raw));
|
|
122
|
+
exactKeys(live, ['state', 'fileId', 'termsHash', 'expiresAt', 'reservation']);
|
|
123
|
+
const liveReservation = parseReservation(live.reservation);
|
|
124
|
+
if (live.state !== 'reserved' ||
|
|
125
|
+
live.fileId !== r.fileId ||
|
|
126
|
+
live.termsHash !== r.termsHash ||
|
|
127
|
+
live.expiresAt !== r.expiresAt ||
|
|
128
|
+
reservationText(liveReservation) !== reservationText(r) ||
|
|
129
|
+
liveReservation.signature !== r.signature)
|
|
130
|
+
throw new TypeError('Hosting reservation is not live.');
|
|
131
|
+
return r;
|
|
132
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Durable sponsorship admission. A later policy change cannot revoke a checked listing. */
|
|
2
|
+
import { type FileInfo } from '../near/rpc.js';
|
|
3
|
+
import { type Terms } from '../settle-near/terms.js';
|
|
4
|
+
import type { RelayLimits, SponsorshipDecision } from './ledger.js';
|
|
5
|
+
import { type CheckOptions } from './relay.js';
|
|
6
|
+
export declare const ZERO_FEE: {
|
|
7
|
+
fixed: string;
|
|
8
|
+
bps: number;
|
|
9
|
+
treasuryNear: null;
|
|
10
|
+
treasuryEvm: null;
|
|
11
|
+
};
|
|
12
|
+
export declare const relayLimits: (o: CheckOptions) => RelayLimits;
|
|
13
|
+
export declare const temporary: (paid?: boolean, retryAt?: number) => never;
|
|
14
|
+
export declare function admitTerms(terms: Terms, o: CheckOptions, tier: SponsorshipDecision['tier'], skipFee?: boolean): Promise<SponsorshipDecision>;
|
|
15
|
+
export declare function checkedDecision(file: FileInfo, o: CheckOptions, paid?: boolean): Promise<SponsorshipDecision>;
|
|
16
|
+
export declare function baseEligibility(fileId: string, hash: string, o: CheckOptions, paid?: boolean): Promise<{
|
|
17
|
+
network: string;
|
|
18
|
+
contract: string;
|
|
19
|
+
fileId: string;
|
|
20
|
+
termsHash: string;
|
|
21
|
+
eligible: true;
|
|
22
|
+
tier: "legacy" | "listed" | "pool" | "external";
|
|
23
|
+
policyVersion: string;
|
|
24
|
+
minPrice: string;
|
|
25
|
+
fee: import("./fees.js").ProtocolFee;
|
|
26
|
+
decidedAt: number;
|
|
27
|
+
checkedAt: number;
|
|
28
|
+
reason?: never;
|
|
29
|
+
} | {
|
|
30
|
+
network: string;
|
|
31
|
+
contract: string;
|
|
32
|
+
fileId: string;
|
|
33
|
+
termsHash: string;
|
|
34
|
+
eligible: false;
|
|
35
|
+
reason: "not_sponsored";
|
|
36
|
+
tier: "external";
|
|
37
|
+
policyVersion: string;
|
|
38
|
+
minPrice: string;
|
|
39
|
+
fee: import("./fees.js").ProtocolFee;
|
|
40
|
+
decidedAt: number;
|
|
41
|
+
checkedAt: number;
|
|
42
|
+
}>;
|