@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.
- package/CHANGELOG.md +63 -0
- package/CLI.md +378 -0
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +591 -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 +151 -0
- package/dist/browser/login.d.ts +1 -0
- package/dist/browser/login.js +216 -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 +98 -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 +25 -0
- package/dist/node/browser-login.js +219 -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 +149 -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 +386 -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 +136 -3
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Identity } from '../settle-near/identity.js';
|
|
2
|
+
import type { NearWallet } from '../wallet/wallet.js';
|
|
3
|
+
export interface DeliveryStep {
|
|
4
|
+
state: 0 | 1 | 2;
|
|
5
|
+
attempt: number;
|
|
6
|
+
}
|
|
7
|
+
export interface DeliveryInfo {
|
|
8
|
+
id: number;
|
|
9
|
+
amount: string;
|
|
10
|
+
sender: string;
|
|
11
|
+
purchase: string | null;
|
|
12
|
+
reason: number;
|
|
13
|
+
fund: DeliveryStep;
|
|
14
|
+
funded: string;
|
|
15
|
+
slots: {
|
|
16
|
+
to: Identity;
|
|
17
|
+
amount: string;
|
|
18
|
+
step: DeliveryStep;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
export interface MaterializeRefundArgs {
|
|
22
|
+
leaf: {
|
|
23
|
+
delivery_id: number;
|
|
24
|
+
refund: Identity;
|
|
25
|
+
amount: string;
|
|
26
|
+
};
|
|
27
|
+
index: number;
|
|
28
|
+
proof: string[];
|
|
29
|
+
}
|
|
30
|
+
export declare function getDelivery(url: string, contract: string, deliveryId: number): Promise<DeliveryInfo | null>;
|
|
31
|
+
/** Unordered positions, capped at 50. Pin every page and count to ONE finalized block ID;
|
|
32
|
+
* removal swaps the last entry into the hole. Moving-block forward scans can skip entries.
|
|
33
|
+
* New deliveries after the snapshot are discovered on the next sweep. */
|
|
34
|
+
export declare function listOpenDeliveries(url: string, contract: string, from?: number, limit?: number, blockId?: string | number): Promise<DeliveryInfo[]>;
|
|
35
|
+
/** Count includes Pending and InFlight records. Use the same block ID as every sweep page. */
|
|
36
|
+
export declare function openDeliveryCount(url: string, contract: string, blockId?: string | number): Promise<number>;
|
|
37
|
+
/** Attach storage funding, or pass zero to spend the caller's prepaid contract balance. */
|
|
38
|
+
export declare function materializeRefund(wallet: Pick<NearWallet, 'call'>, contract: string, args: MaterializeRefundArgs, deposit: bigint): Promise<unknown>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Typed keeper calls only; scheduling, retry and accounting belong to the caller. */
|
|
2
|
+
import { view } from '../near/rpc.js';
|
|
3
|
+
import { account, bytes, uint } from '../settle-near/codec.js';
|
|
4
|
+
import { parseIdentity } from '../settle-near/identity.js';
|
|
5
|
+
const integer = (v, max = Number.MAX_SAFE_INTEGER) => {
|
|
6
|
+
if (!Number.isSafeInteger(v) || v < 0 || v > max)
|
|
7
|
+
throw new TypeError('invalid contract integer');
|
|
8
|
+
};
|
|
9
|
+
const delivery = (v) => {
|
|
10
|
+
const d = v;
|
|
11
|
+
integer(d.id);
|
|
12
|
+
account(d.sender);
|
|
13
|
+
uint(d.amount, 128);
|
|
14
|
+
uint(d.funded, 128);
|
|
15
|
+
if (d.purchase !== null)
|
|
16
|
+
bytes(d.purchase, 32);
|
|
17
|
+
integer(d.reason, 255);
|
|
18
|
+
const step = (s) => {
|
|
19
|
+
integer(s.state, 2);
|
|
20
|
+
integer(s.attempt, 0xffffffff);
|
|
21
|
+
};
|
|
22
|
+
step(d.fund);
|
|
23
|
+
if (!Array.isArray(d.slots) || d.slots.length > 9)
|
|
24
|
+
throw new TypeError('invalid delivery slots');
|
|
25
|
+
for (const slot of d.slots) {
|
|
26
|
+
parseIdentity(slot.to);
|
|
27
|
+
uint(slot.amount, 128);
|
|
28
|
+
step(slot.step);
|
|
29
|
+
}
|
|
30
|
+
return d;
|
|
31
|
+
};
|
|
32
|
+
export async function getDelivery(url, contract, deliveryId) {
|
|
33
|
+
integer(deliveryId);
|
|
34
|
+
const d = await view(url, contract, 'get_delivery', { delivery_id: deliveryId });
|
|
35
|
+
return d === null ? null : delivery(d);
|
|
36
|
+
}
|
|
37
|
+
/** Unordered positions, capped at 50. Pin every page and count to ONE finalized block ID;
|
|
38
|
+
* removal swaps the last entry into the hole. Moving-block forward scans can skip entries.
|
|
39
|
+
* New deliveries after the snapshot are discovered on the next sweep. */
|
|
40
|
+
export async function listOpenDeliveries(url, contract, from = 0, limit = 50, blockId) {
|
|
41
|
+
integer(from);
|
|
42
|
+
integer(limit, 50);
|
|
43
|
+
const ds = await view(url, contract, 'list_open_deliveries', { from, limit }, blockId);
|
|
44
|
+
if (!Array.isArray(ds) || ds.length > limit)
|
|
45
|
+
throw new TypeError('invalid deliveries');
|
|
46
|
+
return ds.map(delivery);
|
|
47
|
+
}
|
|
48
|
+
/** Count includes Pending and InFlight records. Use the same block ID as every sweep page. */
|
|
49
|
+
export async function openDeliveryCount(url, contract, blockId) {
|
|
50
|
+
const n = await view(url, contract, 'open_delivery_count', {}, blockId);
|
|
51
|
+
integer(n);
|
|
52
|
+
return n;
|
|
53
|
+
}
|
|
54
|
+
/** Attach storage funding, or pass zero to spend the caller's prepaid contract balance. */
|
|
55
|
+
export function materializeRefund(wallet, contract, args, deposit) {
|
|
56
|
+
integer(args.leaf.delivery_id);
|
|
57
|
+
integer(args.index, 0xffffffff);
|
|
58
|
+
uint(args.leaf.amount, 128);
|
|
59
|
+
parseIdentity(args.leaf.refund);
|
|
60
|
+
if (args.proof.length !== 32)
|
|
61
|
+
throw new TypeError('refund proof must have 32 siblings');
|
|
62
|
+
for (const sibling of args.proof)
|
|
63
|
+
bytes(sibling, 32);
|
|
64
|
+
if (deposit < 0n)
|
|
65
|
+
throw new TypeError('invalid storage deposit');
|
|
66
|
+
return wallet.call({
|
|
67
|
+
receiverId: account(contract),
|
|
68
|
+
method: 'materialize_refund',
|
|
69
|
+
args,
|
|
70
|
+
gas: 100000000000000n,
|
|
71
|
+
deposit,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { Store, Versioned } from '../pay/store.js';
|
|
2
|
+
import type { Terms } from '../settle-near/terms.js';
|
|
3
|
+
import type { ProtocolFee } from './fees.js';
|
|
4
|
+
import { type RelayResult } from './relay.js';
|
|
5
|
+
export interface SponsorshipDecision {
|
|
6
|
+
network: string;
|
|
7
|
+
contract: string;
|
|
8
|
+
fileId: string;
|
|
9
|
+
termsHash: string;
|
|
10
|
+
terms: Terms;
|
|
11
|
+
eligible: true;
|
|
12
|
+
tier: 'listed' | 'pool' | 'legacy' | 'external';
|
|
13
|
+
listed: boolean;
|
|
14
|
+
policyVersion: string;
|
|
15
|
+
minPrice: string;
|
|
16
|
+
fee: ProtocolFee;
|
|
17
|
+
decidedAt: number;
|
|
18
|
+
allocated: string | null;
|
|
19
|
+
outcome: 'prepared' | 'registered';
|
|
20
|
+
}
|
|
21
|
+
export interface RelayLimits {
|
|
22
|
+
perIpHour?: number;
|
|
23
|
+
perDay: number;
|
|
24
|
+
unlock: number;
|
|
25
|
+
unlockReserved: number;
|
|
26
|
+
unlockListedReserved: number;
|
|
27
|
+
unlocksPerFile: number;
|
|
28
|
+
unlocksPerSeller: number;
|
|
29
|
+
listedUnlocksPerFile: number;
|
|
30
|
+
listedUnlocksPerSeller: number;
|
|
31
|
+
files: number;
|
|
32
|
+
filesPerIp: number;
|
|
33
|
+
registrationsPerSeller: number;
|
|
34
|
+
listedRegistrationsPerSeller: number;
|
|
35
|
+
methods?: Partial<Record<string, number>>;
|
|
36
|
+
claimsPerGrant?: number;
|
|
37
|
+
reopensPerPurchase?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface Admission {
|
|
40
|
+
id: string;
|
|
41
|
+
requestHash: string;
|
|
42
|
+
method: string;
|
|
43
|
+
fileId: string;
|
|
44
|
+
termsHash: string;
|
|
45
|
+
seller: string;
|
|
46
|
+
listed: boolean;
|
|
47
|
+
ip: string;
|
|
48
|
+
storage: string;
|
|
49
|
+
allocation: string;
|
|
50
|
+
/** Final file balance observed before a sponsored top-up is prepared. */
|
|
51
|
+
fundingBalanceBefore?: string;
|
|
52
|
+
limits: RelayLimits;
|
|
53
|
+
decision?: SponsorshipDecision;
|
|
54
|
+
reservation?: {
|
|
55
|
+
key: string;
|
|
56
|
+
binding: string;
|
|
57
|
+
};
|
|
58
|
+
nonce?: string;
|
|
59
|
+
nonceDeadline?: number;
|
|
60
|
+
quota?: {
|
|
61
|
+
key: string;
|
|
62
|
+
limit: number;
|
|
63
|
+
};
|
|
64
|
+
topup?: {
|
|
65
|
+
lifetime: string;
|
|
66
|
+
fileDay: string;
|
|
67
|
+
totalDay: string;
|
|
68
|
+
listedReserved: string;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export interface RelayOperation extends Admission {
|
|
72
|
+
at: number;
|
|
73
|
+
compact?: true;
|
|
74
|
+
finishedAt?: number;
|
|
75
|
+
charges: {
|
|
76
|
+
key: string;
|
|
77
|
+
amount: string;
|
|
78
|
+
}[];
|
|
79
|
+
stage: 'prepared' | 'submitted' | 'rejected';
|
|
80
|
+
tx?: {
|
|
81
|
+
txHash: string;
|
|
82
|
+
signed: string;
|
|
83
|
+
signer: string;
|
|
84
|
+
args: object;
|
|
85
|
+
referenceHash?: string;
|
|
86
|
+
};
|
|
87
|
+
result?: RelayResult;
|
|
88
|
+
storageFinalized?: boolean;
|
|
89
|
+
finalState?: 'released' | 'not_released' | 'done' | 'refused';
|
|
90
|
+
}
|
|
91
|
+
export interface Cutover {
|
|
92
|
+
policyVersion: 'legacy-v0';
|
|
93
|
+
blockHash: string;
|
|
94
|
+
blockHeight: number;
|
|
95
|
+
minPrice: '100000';
|
|
96
|
+
fee: ProtocolFee;
|
|
97
|
+
scannedThrough: number;
|
|
98
|
+
inventory: string[];
|
|
99
|
+
}
|
|
100
|
+
export interface RelayState extends Versioned {
|
|
101
|
+
schema: 1;
|
|
102
|
+
decisions: Record<string, SponsorshipDecision>;
|
|
103
|
+
operations: Record<string, RelayOperation>;
|
|
104
|
+
history: Record<string, RelayOperation>;
|
|
105
|
+
counters: Record<string, string>;
|
|
106
|
+
reservations: Record<string, {
|
|
107
|
+
binding: string;
|
|
108
|
+
expiresAt: number;
|
|
109
|
+
}>;
|
|
110
|
+
nonces: Record<string, {
|
|
111
|
+
binding: string;
|
|
112
|
+
expiresAt: number;
|
|
113
|
+
}>;
|
|
114
|
+
cutover?: Cutover;
|
|
115
|
+
work: Record<string, {
|
|
116
|
+
request: unknown;
|
|
117
|
+
requestHash: string;
|
|
118
|
+
nextAt: number;
|
|
119
|
+
}>;
|
|
120
|
+
}
|
|
121
|
+
export declare const requestHash: (v: unknown) => string;
|
|
122
|
+
export declare class RelayLedger {
|
|
123
|
+
private store;
|
|
124
|
+
private scope;
|
|
125
|
+
private holdMs;
|
|
126
|
+
constructor(store: Store<RelayState>, scope: string, holdMs?: number);
|
|
127
|
+
read(): Promise<RelayState>;
|
|
128
|
+
private change;
|
|
129
|
+
private decisionStore;
|
|
130
|
+
private recordKey;
|
|
131
|
+
private loadDecision;
|
|
132
|
+
/** The registry is the write-ahead journal; frozen per-file records outlive traffic. */
|
|
133
|
+
private flushDecisions;
|
|
134
|
+
decision(file: string, terms: string): Promise<SponsorshipDecision | null>;
|
|
135
|
+
remember(d: SponsorshipDecision): Promise<SponsorshipDecision>;
|
|
136
|
+
cutover(): Promise<Cutover | undefined>;
|
|
137
|
+
seal(cutover: Cutover, decisions: SponsorshipDecision[]): Promise<void>;
|
|
138
|
+
operation(id: string): Promise<RelayOperation | null>;
|
|
139
|
+
pendingStorage(_now?: number): Promise<bigint>;
|
|
140
|
+
finalizeStorage(id: string, hash: string, state?: RelayOperation['finalState']): Promise<void>;
|
|
141
|
+
prepare(a: Admission, now?: number): Promise<{
|
|
142
|
+
fresh: boolean;
|
|
143
|
+
operation: RelayOperation;
|
|
144
|
+
capacityReached?: boolean;
|
|
145
|
+
}>;
|
|
146
|
+
signed(id: string, tx: NonNullable<RelayOperation['tx']>): Promise<void>;
|
|
147
|
+
result(id: string, result: RelayResult): Promise<void>;
|
|
148
|
+
defer(id: string, request: unknown, hash: string, nextAt: number, max?: number): Promise<void>;
|
|
149
|
+
finishWork(id: string): Promise<void>;
|
|
150
|
+
registered(fileId: string, hash: string): Promise<void>;
|
|
151
|
+
retire(id: string, hash: string): Promise<boolean>;
|
|
152
|
+
recoverPrepared(): Promise<void>;
|
|
153
|
+
reject(id: string, refundCounters?: boolean): Promise<void>;
|
|
154
|
+
unlockAvailable(d: SponsorshipDecision, p: RelayLimits, now?: number): Promise<boolean>;
|
|
155
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/** One CAS transaction binds eligibility, operation holds, allocations and UTC quotas. */
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
3
|
+
import { hex } from '../settle-near/codec.js';
|
|
4
|
+
import { identityJson } from '../settle-near/identity.js';
|
|
5
|
+
import { RelayError } from './relay.js';
|
|
6
|
+
const DAY = 86400000;
|
|
7
|
+
const RECORD_CAP = 4096;
|
|
8
|
+
const HISTORY_CAP = 128;
|
|
9
|
+
const decisionKey = (file, terms) => `${file}|${terms}`;
|
|
10
|
+
const canonical = (v) => Array.isArray(v)
|
|
11
|
+
? v.map(canonical)
|
|
12
|
+
: v !== null && typeof v === 'object'
|
|
13
|
+
? Object.fromEntries(Object.entries(v)
|
|
14
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
15
|
+
.map(([k, x]) => [k, canonical(x)]))
|
|
16
|
+
: v;
|
|
17
|
+
export const requestHash = (v) => hex(sha256(new TextEncoder().encode(JSON.stringify(canonical(v)))));
|
|
18
|
+
const amount = (v) => typeof v === 'string' && /^(0|[1-9][0-9]*)$/.test(v);
|
|
19
|
+
export class RelayLedger {
|
|
20
|
+
store;
|
|
21
|
+
scope;
|
|
22
|
+
holdMs;
|
|
23
|
+
constructor(store, scope, holdMs = 900000) {
|
|
24
|
+
this.store = store;
|
|
25
|
+
this.scope = scope;
|
|
26
|
+
this.holdMs = holdMs;
|
|
27
|
+
}
|
|
28
|
+
async read() {
|
|
29
|
+
const s = await this.store.load(this.scope);
|
|
30
|
+
if (s &&
|
|
31
|
+
(s.schema !== 1 ||
|
|
32
|
+
!s.decisions ||
|
|
33
|
+
!s.operations ||
|
|
34
|
+
!s.history ||
|
|
35
|
+
!s.counters ||
|
|
36
|
+
!s.reservations ||
|
|
37
|
+
!s.nonces ||
|
|
38
|
+
!s.work ||
|
|
39
|
+
Object.values(s.counters).some((v) => !amount(v)) ||
|
|
40
|
+
Object.values(s.operations).some((op) => !amount(op.storage) ||
|
|
41
|
+
!amount(op.allocation) ||
|
|
42
|
+
(op.fundingBalanceBefore !== undefined && !amount(op.fundingBalanceBefore))) ||
|
|
43
|
+
Object.values(s.decisions).some((d) => d.allocated !== null && !amount(d.allocated))))
|
|
44
|
+
throw new RelayError('unavailable', 'Sponsorship state is unavailable.');
|
|
45
|
+
return (s ?? {
|
|
46
|
+
id: this.scope,
|
|
47
|
+
revision: -1,
|
|
48
|
+
schema: 1,
|
|
49
|
+
decisions: {},
|
|
50
|
+
operations: {},
|
|
51
|
+
history: {},
|
|
52
|
+
counters: {},
|
|
53
|
+
reservations: {},
|
|
54
|
+
nonces: {},
|
|
55
|
+
work: {},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async change(fn) {
|
|
59
|
+
for (;;) {
|
|
60
|
+
const s = await this.read();
|
|
61
|
+
const next = structuredClone(s);
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
for (const [id, op] of Object.entries(next.operations)) {
|
|
64
|
+
if (op.finishedAt !== undefined && op.finishedAt + this.holdMs <= now)
|
|
65
|
+
delete next.operations[id];
|
|
66
|
+
// Records finalized before retention timestamps were introduced have already held.
|
|
67
|
+
else if (op.compact &&
|
|
68
|
+
op.at + this.holdMs <= now &&
|
|
69
|
+
(op.stage === 'rejected' ||
|
|
70
|
+
(op.storageFinalized && ['released', 'done'].includes(op.finalState ?? ''))))
|
|
71
|
+
delete next.operations[id];
|
|
72
|
+
}
|
|
73
|
+
for (const entries of [next.nonces, next.reservations])
|
|
74
|
+
for (const [key, binding] of Object.entries(entries))
|
|
75
|
+
if (binding.expiresAt <= now)
|
|
76
|
+
delete entries[key];
|
|
77
|
+
const result = await fn(next);
|
|
78
|
+
const history = Object.keys(next.history);
|
|
79
|
+
for (const key of history.slice(0, Math.max(0, history.length - HISTORY_CAP)))
|
|
80
|
+
delete next.history[key];
|
|
81
|
+
for (const key of Object.keys(next.counters)) {
|
|
82
|
+
const [window = ''] = key.split('|');
|
|
83
|
+
if (window.startsWith('hour:')
|
|
84
|
+
? Number(window.slice(5)) < Math.floor(now / 3600000)
|
|
85
|
+
: Number(window) < Math.floor(now / DAY) - 1)
|
|
86
|
+
delete next.counters[key];
|
|
87
|
+
}
|
|
88
|
+
next.revision++;
|
|
89
|
+
if (await this.store.compareAndSet(this.scope, s.revision < 0 ? null : s, next)) {
|
|
90
|
+
await this.flushDecisions(next);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
decisionStore() {
|
|
96
|
+
return this.store;
|
|
97
|
+
}
|
|
98
|
+
recordKey(file, terms) {
|
|
99
|
+
return `${this.scope}|decision|${file}|${terms}`;
|
|
100
|
+
}
|
|
101
|
+
async loadDecision(s, file, terms) {
|
|
102
|
+
let d = s.decisions[decisionKey(file, terms)] ?? null;
|
|
103
|
+
if (!d) {
|
|
104
|
+
const record = await this.decisionStore().load(this.recordKey(file, terms));
|
|
105
|
+
if (record && (!record.decision || !Number.isSafeInteger(record.sourceRevision)))
|
|
106
|
+
throw new RelayError('unavailable');
|
|
107
|
+
d = record?.decision ?? null;
|
|
108
|
+
}
|
|
109
|
+
if (d &&
|
|
110
|
+
(d.fileId !== file || d.termsHash !== terms || (d.allocated !== null && !amount(d.allocated))))
|
|
111
|
+
throw new RelayError('unavailable');
|
|
112
|
+
return d;
|
|
113
|
+
}
|
|
114
|
+
/** The registry is the write-ahead journal; frozen per-file records outlive traffic. */
|
|
115
|
+
async flushDecisions(committed) {
|
|
116
|
+
const flushed = {};
|
|
117
|
+
const store = this.decisionStore();
|
|
118
|
+
for (const [key, d] of Object.entries(committed.decisions)) {
|
|
119
|
+
if (d.outcome !== 'registered')
|
|
120
|
+
continue;
|
|
121
|
+
const id = this.recordKey(d.fileId, d.termsHash);
|
|
122
|
+
for (;;) {
|
|
123
|
+
const old = await store.load(id);
|
|
124
|
+
if (old && old.sourceRevision >= committed.revision)
|
|
125
|
+
break;
|
|
126
|
+
if (await store.compareAndSet(id, old, {
|
|
127
|
+
id,
|
|
128
|
+
revision: (old?.revision ?? -1) + 1,
|
|
129
|
+
sourceRevision: committed.revision,
|
|
130
|
+
decision: d,
|
|
131
|
+
}))
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
flushed[key] = d;
|
|
135
|
+
}
|
|
136
|
+
if (!Object.keys(flushed).length)
|
|
137
|
+
return;
|
|
138
|
+
for (;;) {
|
|
139
|
+
const current = await this.read();
|
|
140
|
+
const next = structuredClone(current);
|
|
141
|
+
let changed = false;
|
|
142
|
+
for (const [key, d] of Object.entries(flushed)) {
|
|
143
|
+
if (next.decisions[key] && requestHash(next.decisions[key]) === requestHash(d)) {
|
|
144
|
+
delete next.decisions[key];
|
|
145
|
+
changed = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!changed)
|
|
149
|
+
return;
|
|
150
|
+
next.revision++;
|
|
151
|
+
if (await this.store.compareAndSet(this.scope, current, next))
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async decision(file, terms) {
|
|
156
|
+
return this.loadDecision(await this.read(), file, terms);
|
|
157
|
+
}
|
|
158
|
+
async remember(d) {
|
|
159
|
+
return this.change(async (s) => {
|
|
160
|
+
const key = decisionKey(d.fileId, d.termsHash);
|
|
161
|
+
const old = await this.loadDecision(s, d.fileId, d.termsHash);
|
|
162
|
+
s.decisions[key] = old ?? structuredClone(d);
|
|
163
|
+
return s.decisions[key];
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async cutover() {
|
|
167
|
+
return (await this.read()).cutover;
|
|
168
|
+
}
|
|
169
|
+
async seal(cutover, decisions) {
|
|
170
|
+
if (cutover.scannedThrough !== cutover.blockHeight)
|
|
171
|
+
throw new RelayError('unavailable');
|
|
172
|
+
return this.change((s) => {
|
|
173
|
+
if (s.cutover) {
|
|
174
|
+
if (requestHash(s.cutover) !== requestHash(cutover))
|
|
175
|
+
throw new RelayError('bad_request');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
for (const d of decisions)
|
|
179
|
+
s.decisions[decisionKey(d.fileId, d.termsHash)] ??= structuredClone(d);
|
|
180
|
+
s.cutover = structuredClone(cutover);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async operation(id) {
|
|
184
|
+
return (await this.read()).operations[id] ?? null;
|
|
185
|
+
}
|
|
186
|
+
async pendingStorage(_now = Date.now()) {
|
|
187
|
+
return Object.values((await this.read()).operations).reduce((n, op) => n + (op.stage !== 'rejected' && !op.storageFinalized ? BigInt(op.storage) : 0n), 0n);
|
|
188
|
+
}
|
|
189
|
+
async finalizeStorage(id, hash, state) {
|
|
190
|
+
return this.change((s) => {
|
|
191
|
+
const op = s.operations[id];
|
|
192
|
+
if (op?.tx?.txHash === hash) {
|
|
193
|
+
op.storageFinalized = true;
|
|
194
|
+
if (state !== undefined) {
|
|
195
|
+
op.finalState = state;
|
|
196
|
+
op.finishedAt ??= Date.now();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async prepare(a, now = Date.now()) {
|
|
202
|
+
return this.change(async (s) => {
|
|
203
|
+
const old = s.operations[a.id];
|
|
204
|
+
if (old) {
|
|
205
|
+
if (old.stage !== 'rejected') {
|
|
206
|
+
if (old.requestHash !== a.requestHash)
|
|
207
|
+
throw new RelayError('bad_request');
|
|
208
|
+
return { fresh: false, operation: old };
|
|
209
|
+
}
|
|
210
|
+
s.history[`${a.id}|${s.revision}`] = structuredClone(old);
|
|
211
|
+
}
|
|
212
|
+
let capacityReached = false;
|
|
213
|
+
for (const [entries, key] of [
|
|
214
|
+
[s.operations, a.id],
|
|
215
|
+
[s.reservations, a.reservation?.key],
|
|
216
|
+
[s.nonces, a.nonce],
|
|
217
|
+
]) {
|
|
218
|
+
if (key && !entries[key] && Object.keys(entries).length >= RECORD_CAP) {
|
|
219
|
+
capacityReached = true;
|
|
220
|
+
if (a.method !== 'unlock')
|
|
221
|
+
throw new RelayError('relayer_unavailable');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (a.reservation && a.reservation.binding !== a.requestHash)
|
|
225
|
+
throw new RelayError('bad_request');
|
|
226
|
+
if (a.reservation &&
|
|
227
|
+
s.reservations[a.reservation.key] &&
|
|
228
|
+
s.reservations[a.reservation.key]?.binding !== a.reservation.binding)
|
|
229
|
+
throw new RelayError('bad_request');
|
|
230
|
+
if (a.nonce && s.nonces[a.nonce] && s.nonces[a.nonce]?.binding !== a.requestHash)
|
|
231
|
+
throw new RelayError('bad_request');
|
|
232
|
+
const day = Math.floor(now / DAY), p = a.limits;
|
|
233
|
+
const charges = [];
|
|
234
|
+
const charge = (name, limit, amount = 1n) => {
|
|
235
|
+
const key = `${day}|${name}`;
|
|
236
|
+
const n = BigInt(s.counters[key] ?? '0') + amount;
|
|
237
|
+
if (n > BigInt(limit)) {
|
|
238
|
+
const e = new RelayError('limited', 'The daily sponsorship limit is reached.');
|
|
239
|
+
e.retryAt = (day + 1) * DAY;
|
|
240
|
+
throw e;
|
|
241
|
+
}
|
|
242
|
+
s.counters[key] = String(n);
|
|
243
|
+
charges.push({ key, amount: String(amount) });
|
|
244
|
+
};
|
|
245
|
+
const hourKey = `hour:${Math.floor(now / 3600000)}|ip:${a.ip}`;
|
|
246
|
+
const admissions = BigInt(s.counters[hourKey] ?? '0') + 1n;
|
|
247
|
+
if (admissions > BigInt(p.perIpHour ?? 600)) {
|
|
248
|
+
const e = new RelayError('limited');
|
|
249
|
+
e.retryAt = (Math.floor(now / 3600000) + 1) * 3600000;
|
|
250
|
+
throw e;
|
|
251
|
+
}
|
|
252
|
+
s.counters[hourKey] = String(admissions);
|
|
253
|
+
charges.push({ key: hourKey, amount: '1' });
|
|
254
|
+
charge('all', p.perDay);
|
|
255
|
+
if (a.method === 'unlock') {
|
|
256
|
+
charge('unlock', p.unlock);
|
|
257
|
+
if (!a.listed)
|
|
258
|
+
charge('unlock:pool', Math.max(0, p.unlock - p.unlockListedReserved));
|
|
259
|
+
charge(`unlock:file:${a.fileId}`, a.listed ? p.listedUnlocksPerFile : p.unlocksPerFile);
|
|
260
|
+
charge(`unlock:seller:${a.seller}`, a.listed ? p.listedUnlocksPerSeller : p.unlocksPerSeller);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
charge('other', Math.max(0, p.perDay - p.unlockReserved));
|
|
264
|
+
if (a.method !== 'register' || !a.listed)
|
|
265
|
+
charge(`method:${a.method}`, p.methods?.[a.method] ?? p.perDay);
|
|
266
|
+
}
|
|
267
|
+
if (a.quota)
|
|
268
|
+
charge(a.quota.key, a.quota.limit);
|
|
269
|
+
if (a.method === 'register') {
|
|
270
|
+
if (!a.listed) {
|
|
271
|
+
charge('files', p.files);
|
|
272
|
+
charge(`files:ip:${a.ip}`, p.filesPerIp);
|
|
273
|
+
}
|
|
274
|
+
charge(`files:seller:${a.seller}`, a.listed ? p.listedRegistrationsPerSeller : p.registrationsPerSeller);
|
|
275
|
+
}
|
|
276
|
+
if (a.topup) {
|
|
277
|
+
const d = await this.loadDecision(s, a.fileId, a.termsHash);
|
|
278
|
+
if (!d || d.allocated === null)
|
|
279
|
+
throw new RelayError('not_sponsored');
|
|
280
|
+
if (BigInt(d.allocated) + BigInt(a.allocation) > BigInt(a.topup.lifetime))
|
|
281
|
+
throw new RelayError('limited');
|
|
282
|
+
charge(`topup:file:${a.fileId}`, BigInt(a.topup.fileDay), BigInt(a.allocation));
|
|
283
|
+
charge('topup', BigInt(a.topup.totalDay), BigInt(a.allocation));
|
|
284
|
+
if (!a.listed)
|
|
285
|
+
charge('topup:pool', BigInt(a.topup.totalDay) - BigInt(a.topup.listedReserved), BigInt(a.allocation));
|
|
286
|
+
d.allocated = String(BigInt(d.allocated) + BigInt(a.allocation));
|
|
287
|
+
s.decisions[decisionKey(a.fileId, a.termsHash)] = d;
|
|
288
|
+
}
|
|
289
|
+
if (a.decision)
|
|
290
|
+
s.decisions[decisionKey(a.fileId, a.termsHash)] ??= {
|
|
291
|
+
...structuredClone(a.decision),
|
|
292
|
+
outcome: 'prepared',
|
|
293
|
+
};
|
|
294
|
+
if (a.reservation)
|
|
295
|
+
s.reservations[a.reservation.key] = {
|
|
296
|
+
binding: a.reservation.binding,
|
|
297
|
+
expiresAt: now + DAY,
|
|
298
|
+
};
|
|
299
|
+
if (a.nonce)
|
|
300
|
+
s.nonces[a.nonce] = {
|
|
301
|
+
binding: a.requestHash,
|
|
302
|
+
expiresAt: a.nonceDeadline === undefined ? now + 601000 : (a.nonceDeadline + 1) * 1000,
|
|
303
|
+
};
|
|
304
|
+
const operation = { ...a, at: now, charges, stage: 'prepared' };
|
|
305
|
+
s.operations[a.id] = operation;
|
|
306
|
+
return { fresh: true, operation, capacityReached };
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async signed(id, tx) {
|
|
310
|
+
return this.change((s) => {
|
|
311
|
+
const op = s.operations[id];
|
|
312
|
+
if (op?.stage !== 'prepared')
|
|
313
|
+
throw new RelayError('in_flight');
|
|
314
|
+
op.tx = tx;
|
|
315
|
+
op.stage = 'submitted';
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
async result(id, result) {
|
|
319
|
+
return this.change((s) => {
|
|
320
|
+
const op = s.operations[id];
|
|
321
|
+
if (op?.tx?.txHash !== result.txHash)
|
|
322
|
+
return;
|
|
323
|
+
op.result = result;
|
|
324
|
+
delete s.work[id];
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async defer(id, request, hash, nextAt, max = 32) {
|
|
328
|
+
return this.change((s) => {
|
|
329
|
+
if (s.work[id]?.requestHash && s.work[id]?.requestHash !== hash)
|
|
330
|
+
throw new RelayError('bad_request');
|
|
331
|
+
if (!s.work[id] && Object.keys(s.work).length >= max)
|
|
332
|
+
throw new RelayError('settlement_pending', 'Paid work capacity is reached. Retry later.');
|
|
333
|
+
s.work[id] = { request: structuredClone(request), requestHash: hash, nextAt };
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
async finishWork(id) {
|
|
337
|
+
return this.change((s) => {
|
|
338
|
+
delete s.work[id];
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async registered(fileId, hash) {
|
|
342
|
+
return this.change(async (s) => {
|
|
343
|
+
const d = await this.loadDecision(s, fileId, hash);
|
|
344
|
+
if (d)
|
|
345
|
+
s.decisions[decisionKey(fileId, hash)] = { ...d, outcome: 'registered' };
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
async retire(id, hash) {
|
|
349
|
+
return this.change((s) => {
|
|
350
|
+
const op = s.operations[id];
|
|
351
|
+
if (op?.tx?.txHash !== hash)
|
|
352
|
+
return false;
|
|
353
|
+
op.stage = 'rejected';
|
|
354
|
+
op.finishedAt ??= Date.now();
|
|
355
|
+
return true;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
async recoverPrepared() {
|
|
359
|
+
for (const op of Object.values((await this.read()).operations))
|
|
360
|
+
if (op.stage === 'prepared' && !op.tx)
|
|
361
|
+
await this.reject(op.id);
|
|
362
|
+
}
|
|
363
|
+
async reject(id, refundCounters = true) {
|
|
364
|
+
return this.change(async (s) => {
|
|
365
|
+
const op = s.operations[id];
|
|
366
|
+
if (!op || op.stage === 'rejected')
|
|
367
|
+
return;
|
|
368
|
+
for (const c of refundCounters ? op.charges : [])
|
|
369
|
+
if (s.counters[c.key] !== undefined)
|
|
370
|
+
s.counters[c.key] = String(BigInt(s.counters[c.key] ?? '0') - BigInt(c.amount));
|
|
371
|
+
const key = decisionKey(op.fileId, op.termsHash), d = await this.loadDecision(s, op.fileId, op.termsHash);
|
|
372
|
+
if (op.method === 'register' && d?.outcome === 'prepared')
|
|
373
|
+
delete s.decisions[key];
|
|
374
|
+
if (op.topup && d?.allocated) {
|
|
375
|
+
d.allocated = String(BigInt(d.allocated) - BigInt(op.allocation));
|
|
376
|
+
s.decisions[key] = d;
|
|
377
|
+
}
|
|
378
|
+
op.stage = 'rejected';
|
|
379
|
+
op.finishedAt ??= Date.now();
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async unlockAvailable(d, p, now = Date.now()) {
|
|
383
|
+
const s = await this.read(), day = Math.floor(now / DAY);
|
|
384
|
+
const n = (key) => Number(s.counters[`${day}|${key}`] ?? '0');
|
|
385
|
+
return (n('all') < p.perDay &&
|
|
386
|
+
n('unlock') < p.unlock &&
|
|
387
|
+
(d.listed || n('unlock:pool') < Math.max(0, p.unlock - p.unlockListedReserved)) &&
|
|
388
|
+
n(`unlock:file:${d.fileId}`) < (d.listed ? p.listedUnlocksPerFile : p.unlocksPerFile) &&
|
|
389
|
+
n(`unlock:seller:${identityJson(d.terms.seller)}`) <
|
|
390
|
+
(d.listed ? p.listedUnlocksPerSeller : p.unlocksPerSeller));
|
|
391
|
+
}
|
|
392
|
+
}
|