@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,879 @@
|
|
|
1
|
+
/** Durable hosted selling. A pending operation is evidence, not permission to send again. */
|
|
2
|
+
import { metadataHash, normalizeMetadata, UploadClient, uploadOrigin, uploadText, verifyUploadAuthorization, } from '../download/upload.js';
|
|
3
|
+
import { sha256 } from '../envelope/pay2.js';
|
|
4
|
+
import { prepareSeal } from '../envelope/replay.js';
|
|
5
|
+
import { AnnonError, present } from '../errors.js';
|
|
6
|
+
import { keyFromSecret, oneTimeKey } from '../keys/near-ckd.js';
|
|
7
|
+
import { MAX_PLAINTEXT_BYTES } from '../limits.js';
|
|
8
|
+
import { readOutcome, txOutcome, view } from '../near/rpc.js';
|
|
9
|
+
import { encryptedSellStore, exportSell } from '../pay/backup.js';
|
|
10
|
+
import { createSplit } from '../pay/split.js';
|
|
11
|
+
import { applySponsoredFee, checkProtocolFee, protocolFeeAboveMax, } from '../relay/fees.js';
|
|
12
|
+
// Bare import: relay/reservation still initializes at its original place in this list.
|
|
13
|
+
import '../relay/reservation.js';
|
|
14
|
+
import { bytes, compare, hex } from '../settle-near/codec.js';
|
|
15
|
+
import { encodeIdentity, identityJson, identityKind, parseIdentity, verifyAuthorization, } from '../settle-near/identity.js';
|
|
16
|
+
import { registerTextV2, termsHash } from '../settle-near/terms.js';
|
|
17
|
+
import { TransactionUncertain } from '../wallet/uncertain.js';
|
|
18
|
+
// Modules split out of this file load after its original imports, so initialization order is unchanged.
|
|
19
|
+
import { assets, salt, shareSteps, validateHostedRecord } from './hosted-record.js';
|
|
20
|
+
export { shareSteps, validateHostedRecord } from './hosted-record.js';
|
|
21
|
+
export function shareSize(data) {
|
|
22
|
+
const size = data instanceof Uint8Array ? data.length : data.size;
|
|
23
|
+
if (!Number.isSafeInteger(size) || size < 0)
|
|
24
|
+
throw new AnnonError('invalid_arguments');
|
|
25
|
+
if (size > MAX_PLAINTEXT_BYTES)
|
|
26
|
+
throw new AnnonError('too_large');
|
|
27
|
+
return size;
|
|
28
|
+
}
|
|
29
|
+
const nearValiditySeconds = 172800;
|
|
30
|
+
function result(r) {
|
|
31
|
+
if (!r.ok)
|
|
32
|
+
throw r.error;
|
|
33
|
+
return r.value;
|
|
34
|
+
}
|
|
35
|
+
function nearWallet(seller) {
|
|
36
|
+
if (seller.identity.kind !== 'near' || !('call' in seller))
|
|
37
|
+
throw new AnnonError('near_wallet_required');
|
|
38
|
+
return seller;
|
|
39
|
+
}
|
|
40
|
+
export class HostedSelling {
|
|
41
|
+
o;
|
|
42
|
+
context;
|
|
43
|
+
store;
|
|
44
|
+
quotes = new Map();
|
|
45
|
+
constructor(o, context) {
|
|
46
|
+
this.o = o;
|
|
47
|
+
this.context = context;
|
|
48
|
+
this.store = encryptedSellStore(o.rawStore, o.backupSecret);
|
|
49
|
+
}
|
|
50
|
+
async direct(seller, method, args, deposit) {
|
|
51
|
+
const value = await nearWallet(seller).call({
|
|
52
|
+
receiverId: this.o.contractId,
|
|
53
|
+
method,
|
|
54
|
+
args,
|
|
55
|
+
deposit,
|
|
56
|
+
});
|
|
57
|
+
if (!value || typeof value !== 'object' || !('transaction' in value))
|
|
58
|
+
return value;
|
|
59
|
+
const tx = value.transaction;
|
|
60
|
+
try {
|
|
61
|
+
const outcome = readOutcome(value, this.o.contractId, {
|
|
62
|
+
txHash: tx.hash,
|
|
63
|
+
signer: seller.identity.id,
|
|
64
|
+
method,
|
|
65
|
+
args,
|
|
66
|
+
});
|
|
67
|
+
if (outcome.state === 'released')
|
|
68
|
+
return outcome.answer;
|
|
69
|
+
if (outcome.state === 'done')
|
|
70
|
+
return outcome.value;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw new TransactionUncertain(tx.hash);
|
|
74
|
+
}
|
|
75
|
+
throw new TransactionUncertain(tx.hash);
|
|
76
|
+
}
|
|
77
|
+
async fundingBalance(job, step) {
|
|
78
|
+
const value = step === 'deposit'
|
|
79
|
+
? await view(this.o.nearRpc, this.o.contractId, 'storage_balance_of', {
|
|
80
|
+
account_id: job.terms.seller.id,
|
|
81
|
+
})
|
|
82
|
+
: (await this.context.file(job.terms.file_id))?.purchaseStorageBalance?.toString();
|
|
83
|
+
if (value === null && step === 'deposit')
|
|
84
|
+
return null;
|
|
85
|
+
if (typeof value !== 'string' ||
|
|
86
|
+
!/^(0|[1-9][0-9]*)$/.test(value) ||
|
|
87
|
+
BigInt(value) >= 2n ** 128n)
|
|
88
|
+
throw new AnnonError('invalid_response');
|
|
89
|
+
return BigInt(value);
|
|
90
|
+
}
|
|
91
|
+
now() {
|
|
92
|
+
return (this.o.now ?? (() => Math.floor(Date.now() / 1000)))();
|
|
93
|
+
}
|
|
94
|
+
key(id) {
|
|
95
|
+
bytes(id, 32);
|
|
96
|
+
return `annon/sell/${id}`;
|
|
97
|
+
}
|
|
98
|
+
async load(id) {
|
|
99
|
+
try {
|
|
100
|
+
const job = await this.store.load(this.key(id));
|
|
101
|
+
if (!job?.hosting ||
|
|
102
|
+
job.terms.network !== this.o.network ||
|
|
103
|
+
job.terms.contract !== this.o.contractId)
|
|
104
|
+
throw new Error();
|
|
105
|
+
validateHostedRecord(job);
|
|
106
|
+
if ((await metadataHash(job.hosting.metadata)) !== job.hosting.intent.metadataHash)
|
|
107
|
+
throw new Error();
|
|
108
|
+
return job;
|
|
109
|
+
}
|
|
110
|
+
catch (cause) {
|
|
111
|
+
throw new AnnonError('invalid_record', undefined, { cause });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async save(old, job) {
|
|
115
|
+
validateHostedRecord(job);
|
|
116
|
+
if (!(await this.store.compareAndSet(this.key(job.terms.file_id), old, job)))
|
|
117
|
+
throw new AnnonError('store_conflict');
|
|
118
|
+
await this.o.writeBackup(job.id, await exportSell(job, this.o.backupSecret));
|
|
119
|
+
return job;
|
|
120
|
+
}
|
|
121
|
+
async quote(input) {
|
|
122
|
+
if (!input.hosting?.origin)
|
|
123
|
+
throw new AnnonError('transport_required');
|
|
124
|
+
const origin = uploadOrigin(input.hosting.origin);
|
|
125
|
+
const size = shareSize(input.data);
|
|
126
|
+
const seller = parseIdentity(input.seller.identity);
|
|
127
|
+
if (seller.kind === 'near' && (!input.listingStorage || !input.storageBudget))
|
|
128
|
+
throw new AnnonError('storage_required');
|
|
129
|
+
for (const v of [input.listingStorage, input.storageBudget])
|
|
130
|
+
if (v !== undefined && (v <= 0n || v >= 2n ** 128n))
|
|
131
|
+
throw new AnnonError('invalid_arguments');
|
|
132
|
+
if (seller.kind !== 'near' &&
|
|
133
|
+
(input.listingStorage !== undefined || input.storageBudget !== undefined))
|
|
134
|
+
throw new AnnonError('near_wallet_required');
|
|
135
|
+
const metadata = normalizeMetadata({
|
|
136
|
+
filename: input.name,
|
|
137
|
+
title: input.title ?? input.name,
|
|
138
|
+
description: input.description ?? '',
|
|
139
|
+
});
|
|
140
|
+
const c = await this.context.config();
|
|
141
|
+
if (c.network !== this.o.network)
|
|
142
|
+
throw new AnnonError('network_mismatch');
|
|
143
|
+
const recipients = input.near?.recipients ?? [{ identity: seller, bps: 10000 }];
|
|
144
|
+
const remainderId = input.near?.remainder ?? seller;
|
|
145
|
+
let near = {
|
|
146
|
+
token: c.nearUsdc,
|
|
147
|
+
price: input.price,
|
|
148
|
+
recipients: structuredClone(recipients),
|
|
149
|
+
remainder: recipients.findIndex((r) => identityJson(r.identity) === identityJson(remainderId)),
|
|
150
|
+
};
|
|
151
|
+
let fee = null;
|
|
152
|
+
if (seller.kind !== 'near' && this.o.sponsorship) {
|
|
153
|
+
fee = (await this.o.sponsorship()).fee;
|
|
154
|
+
if (protocolFeeAboveMax(input.price, fee, this.o.maxProtocolFee) && !input.onSplit)
|
|
155
|
+
throw new AnnonError('protocol_fee_above_max');
|
|
156
|
+
near = applySponsoredFee(near, fee, seller.kind === 'evm' ? input.price : undefined);
|
|
157
|
+
}
|
|
158
|
+
const remainder = near.recipients[near.remainder]?.identity;
|
|
159
|
+
near.recipients.sort((a, b) => compare(encodeIdentity(a.identity), encodeIdentity(b.identity)));
|
|
160
|
+
near.remainder = near.recipients.findIndex((r) => identityJson(r.identity) === identityJson(present(remainder)));
|
|
161
|
+
const fileId = `0x${hex(crypto.getRandomValues(new Uint8Array(32))).slice(2)}`;
|
|
162
|
+
const fileKey = crypto.getRandomValues(new Uint8Array(32));
|
|
163
|
+
const prepared = await prepareSeal(fileKey, input.data, {
|
|
164
|
+
fileId: bytes(fileId, 32),
|
|
165
|
+
name: metadata.filename,
|
|
166
|
+
mime: input.mime,
|
|
167
|
+
authKind: identityKind(seller),
|
|
168
|
+
});
|
|
169
|
+
const terms = {
|
|
170
|
+
network: c.network,
|
|
171
|
+
contract: this.o.contractId,
|
|
172
|
+
file_id: fileId,
|
|
173
|
+
seller,
|
|
174
|
+
pay2_version: 2,
|
|
175
|
+
cipher_hash: hex(prepared.cipherHash),
|
|
176
|
+
moderator: null,
|
|
177
|
+
near,
|
|
178
|
+
base: null,
|
|
179
|
+
};
|
|
180
|
+
let baseSplit = null;
|
|
181
|
+
if (seller.kind === 'evm') {
|
|
182
|
+
if (near.recipients.some((r) => r.identity.kind !== 'evm'))
|
|
183
|
+
throw new AnnonError('bad_requirements');
|
|
184
|
+
const split = createSplit('push', {
|
|
185
|
+
recipients: near.recipients.map((r) => r.identity.id),
|
|
186
|
+
allocations: near.recipients.map((r) => BigInt(r.bps)),
|
|
187
|
+
totalAllocation: 10000n,
|
|
188
|
+
distributionIncentive: 0,
|
|
189
|
+
}, salt);
|
|
190
|
+
baseSplit = { address: split.address, kind: 'push', salt };
|
|
191
|
+
terms.base = { usdc: c.usdc, price: input.price, pay_to: split.address };
|
|
192
|
+
}
|
|
193
|
+
const hash = termsHash(terms, { nearUsdc: c.nearUsdc, baseUsdc: c.usdc });
|
|
194
|
+
if (fee)
|
|
195
|
+
checkProtocolFee(terms, fee);
|
|
196
|
+
const headerSize = prepared.headerSize(this.o.contractId);
|
|
197
|
+
const intent = {
|
|
198
|
+
network: c.network,
|
|
199
|
+
contract: this.o.contractId,
|
|
200
|
+
seller,
|
|
201
|
+
fileId,
|
|
202
|
+
cipherHash: terms.cipher_hash,
|
|
203
|
+
termsHash: hash,
|
|
204
|
+
byteSize: headerSize + size + prepared.manifest.metaLen + 16 * prepared.manifest.n,
|
|
205
|
+
plaintextSize: size,
|
|
206
|
+
headerSize,
|
|
207
|
+
hostOrigin: origin,
|
|
208
|
+
metadataHash: await metadataHash(metadata),
|
|
209
|
+
nonce: hex(crypto.getRandomValues(new Uint8Array(32))),
|
|
210
|
+
expiresAt: this.now() + 600,
|
|
211
|
+
};
|
|
212
|
+
const job = {
|
|
213
|
+
kind: 'sell',
|
|
214
|
+
id: `sell:${fileId}`,
|
|
215
|
+
revision: 0,
|
|
216
|
+
terms,
|
|
217
|
+
baseSplit,
|
|
218
|
+
fileKey: hex(fileKey),
|
|
219
|
+
sellerSecret: hex(oneTimeKey().secret),
|
|
220
|
+
deadline: this.now() + 540,
|
|
221
|
+
name: metadata.filename,
|
|
222
|
+
mime: input.mime,
|
|
223
|
+
previewLen: 0,
|
|
224
|
+
ckdSecret: null,
|
|
225
|
+
...(input.storageBudget ? { storageBudget: String(input.storageBudget) } : {}),
|
|
226
|
+
hosting: {
|
|
227
|
+
origin,
|
|
228
|
+
metadata,
|
|
229
|
+
fee,
|
|
230
|
+
intent,
|
|
231
|
+
manifest: prepared.manifest,
|
|
232
|
+
authorization: null,
|
|
233
|
+
uploadId: null,
|
|
234
|
+
reservation: null,
|
|
235
|
+
split: null,
|
|
236
|
+
steps: shareSteps(seller),
|
|
237
|
+
completed: [],
|
|
238
|
+
pending: null,
|
|
239
|
+
lease: null,
|
|
240
|
+
backupConfirmed: false,
|
|
241
|
+
listingStorage: input.listingStorage ? String(input.listingStorage) : null,
|
|
242
|
+
header: null,
|
|
243
|
+
result: null,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
const quoteId = crypto.randomUUID();
|
|
247
|
+
this.quotes.set(quoteId, { job: structuredClone(job), input });
|
|
248
|
+
return structuredClone({
|
|
249
|
+
quoteId,
|
|
250
|
+
fileId,
|
|
251
|
+
terms,
|
|
252
|
+
fee,
|
|
253
|
+
steps: job.hosting.steps,
|
|
254
|
+
metadata,
|
|
255
|
+
listingStorage: job.hosting.listingStorage,
|
|
256
|
+
storageBudget: job.storageBudget ?? null,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async begin(quoteId) {
|
|
260
|
+
const quote = this.quotes.get(quoteId);
|
|
261
|
+
if (!quote)
|
|
262
|
+
throw new AnnonError('invalid_arguments');
|
|
263
|
+
const job = await this.save(null, structuredClone(quote.job));
|
|
264
|
+
this.quotes.delete(quoteId);
|
|
265
|
+
return job;
|
|
266
|
+
}
|
|
267
|
+
async confirmBackup(id) {
|
|
268
|
+
const job = await this.load(id), h = job.hosting;
|
|
269
|
+
if (h.backupConfirmed)
|
|
270
|
+
return job;
|
|
271
|
+
if (h.pending || h.lease)
|
|
272
|
+
throw Object.assign(new AnnonError('lease_busy'), { retryAfterSeconds: 5 });
|
|
273
|
+
return this.save(job, {
|
|
274
|
+
...job,
|
|
275
|
+
revision: job.revision + 1,
|
|
276
|
+
hosting: { ...h, backupConfirmed: true },
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
async discard(id) {
|
|
280
|
+
const job = await this.load(id);
|
|
281
|
+
if (job.hosting.lease && job.hosting.lease.until > this.now())
|
|
282
|
+
throw Object.assign(new AnnonError('lease_busy'), { retryAfterSeconds: 5 });
|
|
283
|
+
if (!(await this.store.compareAndSet(this.key(id), job, null)))
|
|
284
|
+
throw new AnnonError('store_conflict');
|
|
285
|
+
}
|
|
286
|
+
async backup(id) {
|
|
287
|
+
return exportSell(await this.load(id), this.o.backupSecret);
|
|
288
|
+
}
|
|
289
|
+
async prepared(job, input) {
|
|
290
|
+
const size = shareSize(input.data);
|
|
291
|
+
if (!job.fileKey)
|
|
292
|
+
throw new AnnonError('invalid_record');
|
|
293
|
+
if (size !== job.hosting?.intent.plaintextSize)
|
|
294
|
+
throw new AnnonError('source_changed');
|
|
295
|
+
const s = await prepareSeal(bytes(job.fileKey, 32), input.data, {
|
|
296
|
+
fileId: bytes(job.terms.file_id, 32),
|
|
297
|
+
name: job.name,
|
|
298
|
+
mime: job.mime,
|
|
299
|
+
authKind: identityKind(job.terms.seller),
|
|
300
|
+
});
|
|
301
|
+
if (hex(s.cipherHash) !== job.terms.cipher_hash)
|
|
302
|
+
throw new AnnonError('source_changed');
|
|
303
|
+
return s;
|
|
304
|
+
}
|
|
305
|
+
async verifySource(id, data) {
|
|
306
|
+
await this.prepared(await this.load(id), { data });
|
|
307
|
+
}
|
|
308
|
+
async next(id, input) {
|
|
309
|
+
await this.advance(id, input);
|
|
310
|
+
return this.load(id);
|
|
311
|
+
}
|
|
312
|
+
async advance(id, input) {
|
|
313
|
+
let job = await this.load(id);
|
|
314
|
+
if (input.expectedRevision !== undefined && input.expectedRevision !== job.revision)
|
|
315
|
+
throw new AnnonError('store_conflict');
|
|
316
|
+
if (identityJson(parseIdentity(input.seller.identity)) !== identityJson(job.terms.seller))
|
|
317
|
+
throw new AnnonError('wrong_identity');
|
|
318
|
+
if (uploadOrigin(input.hosting.origin) !== job.hosting.origin)
|
|
319
|
+
throw new AnnonError('config_conflict');
|
|
320
|
+
if (job.published)
|
|
321
|
+
return job;
|
|
322
|
+
if (!job.hosting.backupConfirmed)
|
|
323
|
+
throw new AnnonError('backup_secret_required');
|
|
324
|
+
if (job.hosting.lease && job.hosting.lease.until > this.now())
|
|
325
|
+
throw Object.assign(new AnnonError('lease_busy'), { retryAfterSeconds: 5 });
|
|
326
|
+
const owner = crypto.randomUUID();
|
|
327
|
+
job = await this.save(job, {
|
|
328
|
+
...job,
|
|
329
|
+
revision: job.revision + 1,
|
|
330
|
+
hosting: { ...job.hosting, lease: { owner, until: this.now() + 120 } },
|
|
331
|
+
});
|
|
332
|
+
const fence = async () => {
|
|
333
|
+
const current = await this.load(id);
|
|
334
|
+
if (renewalFailed ||
|
|
335
|
+
current.revision !== job.revision ||
|
|
336
|
+
current.hosting.lease?.owner !== owner ||
|
|
337
|
+
current.hosting.lease.until <= this.now())
|
|
338
|
+
throw Object.assign(new AnnonError('lease_busy'), { retryAfterSeconds: 5 });
|
|
339
|
+
if (identityJson(parseIdentity(input.seller.identity)) !== identityJson(job.terms.seller))
|
|
340
|
+
throw new AnnonError('wrong_identity');
|
|
341
|
+
};
|
|
342
|
+
let awaitingSignature = false;
|
|
343
|
+
let awaitingTransaction = false;
|
|
344
|
+
let freshAttempt = false;
|
|
345
|
+
let journal = null;
|
|
346
|
+
let journalKey = '';
|
|
347
|
+
let updates = Promise.resolve();
|
|
348
|
+
let renewalFailed = false;
|
|
349
|
+
const writeUpdate = async (change, h = {}) => {
|
|
350
|
+
await fence();
|
|
351
|
+
job = await this.save(job, {
|
|
352
|
+
...job,
|
|
353
|
+
...change,
|
|
354
|
+
revision: job.revision + 1,
|
|
355
|
+
hosting: {
|
|
356
|
+
...job.hosting,
|
|
357
|
+
...h,
|
|
358
|
+
lease: h.lease === null ? null : { owner, until: this.now() + 120 },
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
if (journal && h.lease !== null) {
|
|
362
|
+
await fence();
|
|
363
|
+
const next = {
|
|
364
|
+
...job,
|
|
365
|
+
revision: journal.revision + 1,
|
|
366
|
+
hosting: { ...job.hosting, lease: null },
|
|
367
|
+
};
|
|
368
|
+
if (!(await this.store.compareAndSet(journalKey, journal, next)))
|
|
369
|
+
throw new AnnonError('store_conflict');
|
|
370
|
+
journal = next;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const update = (change, h = {}) => {
|
|
374
|
+
const next = updates.then(() => writeUpdate(change, h));
|
|
375
|
+
updates = next.catch(() => { });
|
|
376
|
+
return next;
|
|
377
|
+
};
|
|
378
|
+
const heartbeat = setInterval(() => {
|
|
379
|
+
void update({}, { lease: { owner, until: this.now() + 120 } }).catch(() => {
|
|
380
|
+
renewalFailed = true;
|
|
381
|
+
});
|
|
382
|
+
}, 30000);
|
|
383
|
+
const effect = async (run) => {
|
|
384
|
+
await updates;
|
|
385
|
+
await fence();
|
|
386
|
+
const value = await run();
|
|
387
|
+
await updates;
|
|
388
|
+
await fence();
|
|
389
|
+
return value;
|
|
390
|
+
};
|
|
391
|
+
const client = new UploadClient(job.hosting.origin, this.o.fetch ? { fetch: this.o.fetch } : {});
|
|
392
|
+
const authorize = async () => {
|
|
393
|
+
const intent = { ...job.hosting.intent, expiresAt: this.now() + 600 };
|
|
394
|
+
awaitingSignature = true;
|
|
395
|
+
const auth = await effect(async () => {
|
|
396
|
+
if (job.terms.seller.kind === 'near') {
|
|
397
|
+
if (!input.hosting.signUpload)
|
|
398
|
+
throw new AnnonError('missing_method');
|
|
399
|
+
return input.hosting.signUpload(structuredClone(intent));
|
|
400
|
+
}
|
|
401
|
+
const value = await input.seller.authorize(uploadText(intent));
|
|
402
|
+
if (!value || !verifyAuthorization(job.terms.seller, uploadText(intent), value))
|
|
403
|
+
throw new AnnonError('invalid_signature');
|
|
404
|
+
return value;
|
|
405
|
+
});
|
|
406
|
+
awaitingSignature = false;
|
|
407
|
+
await effect(() => verifyUploadAuthorization({ intent, auth }, {
|
|
408
|
+
network: this.o.network,
|
|
409
|
+
contract: this.o.contractId,
|
|
410
|
+
hostOrigin: job.hosting.origin,
|
|
411
|
+
now: this.now(),
|
|
412
|
+
nearRpc: this.o.nearRpc,
|
|
413
|
+
}));
|
|
414
|
+
await update({}, { intent, authorization: { intent, auth } });
|
|
415
|
+
};
|
|
416
|
+
const body = async (prepared, uploadId) => {
|
|
417
|
+
if (!input.hosting.spool)
|
|
418
|
+
throw new AnnonError('transport_required', 'A ciphertext spool is required.');
|
|
419
|
+
await prepared.stage(input.hosting.spool, async (source) => {
|
|
420
|
+
const wire = input.hosting.body ? await input.hosting.body(source) : source.read();
|
|
421
|
+
const reserved = result(await effect(() => client.body(uploadId, present(job.hosting.authorization), wire)));
|
|
422
|
+
await update({}, { reservation: reserved.reservation });
|
|
423
|
+
});
|
|
424
|
+
};
|
|
425
|
+
const create = async () => {
|
|
426
|
+
const h = job.hosting;
|
|
427
|
+
const created = result(await effect(() => client.create({
|
|
428
|
+
...present(h.authorization),
|
|
429
|
+
terms: job.terms,
|
|
430
|
+
manifest: h.manifest,
|
|
431
|
+
metadata: h.metadata,
|
|
432
|
+
})));
|
|
433
|
+
if (h.uploadId && h.uploadId !== created.uploadId)
|
|
434
|
+
throw new AnnonError('upload_conflict');
|
|
435
|
+
await update({}, {
|
|
436
|
+
uploadId: created.uploadId,
|
|
437
|
+
...(created.state === 'reserved' ? { reservation: created.reservation } : {}),
|
|
438
|
+
});
|
|
439
|
+
return created;
|
|
440
|
+
};
|
|
441
|
+
const refresh = async () => {
|
|
442
|
+
if (job.hosting.authorization && job.hosting.intent.expiresAt <= this.now()) {
|
|
443
|
+
await authorize();
|
|
444
|
+
return create();
|
|
445
|
+
}
|
|
446
|
+
return null;
|
|
447
|
+
};
|
|
448
|
+
const complete = async () => update({}, { completed: [...job.hosting.completed, present(job.hosting.pending).step], pending: null });
|
|
449
|
+
const resetPending = async () => {
|
|
450
|
+
await fence();
|
|
451
|
+
if (journal && !(await this.store.compareAndSet(journalKey, journal, null)))
|
|
452
|
+
throw new AnnonError('store_conflict');
|
|
453
|
+
journal = null;
|
|
454
|
+
await update({}, { pending: null });
|
|
455
|
+
};
|
|
456
|
+
const direct = async (method, args, deposit) => {
|
|
457
|
+
awaitingTransaction = true;
|
|
458
|
+
const value = await this.direct(input.seller, method, args, deposit);
|
|
459
|
+
awaitingTransaction = false;
|
|
460
|
+
return value;
|
|
461
|
+
};
|
|
462
|
+
const uncertain = (retryAfterSeconds = 30) => Object.assign(new AnnonError('transaction_uncertain'), { retryAfterSeconds });
|
|
463
|
+
const checkedOutcome = async (operation, txHash) => {
|
|
464
|
+
const pending = present(job.hosting.pending);
|
|
465
|
+
const candidate = !pending.txHash;
|
|
466
|
+
// Funding already persists its attempt time through the wallet validity bound.
|
|
467
|
+
const notBefore = pending.validUntil === undefined
|
|
468
|
+
? pending.startedAt
|
|
469
|
+
: pending.validUntil - nearValiditySeconds;
|
|
470
|
+
if (candidate && notBefore === undefined)
|
|
471
|
+
throw uncertain();
|
|
472
|
+
return effect(async () => {
|
|
473
|
+
try {
|
|
474
|
+
return await txOutcome(this.o.nearRpc, txHash, job.terms.seller.id, this.o.contractId, 'FINAL', {
|
|
475
|
+
...operation,
|
|
476
|
+
...(candidate ? { notBefore: present(notBefore) } : {}),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
// Invalid candidate evidence says nothing about the outstanding wallet call.
|
|
481
|
+
if (candidate)
|
|
482
|
+
throw uncertain();
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
};
|
|
487
|
+
try {
|
|
488
|
+
let h = job.hosting;
|
|
489
|
+
const step = h.steps[h.completed.length];
|
|
490
|
+
if (!step)
|
|
491
|
+
throw new AnnonError('invalid_record');
|
|
492
|
+
journalKey = `annon/share-operation/${id}/${step}`;
|
|
493
|
+
journal = await this.store.load(journalKey);
|
|
494
|
+
await fence();
|
|
495
|
+
if (journal && !h.pending) {
|
|
496
|
+
if (termsHash(journal.terms, assets(journal.terms)) !== h.intent.termsHash)
|
|
497
|
+
throw new AnnonError('invalid_record');
|
|
498
|
+
// Restore the durable operation instead of trusting a rolled-back progress list.
|
|
499
|
+
await update({ ...journal, revision: job.revision }, { ...present(journal.hosting), lease: h.lease });
|
|
500
|
+
if (job.hosting.completed.includes(step) || job.published)
|
|
501
|
+
return job;
|
|
502
|
+
h = job.hosting;
|
|
503
|
+
}
|
|
504
|
+
if (input.txHash !== undefined &&
|
|
505
|
+
(typeof input.txHash !== 'string' ||
|
|
506
|
+
!input.txHash ||
|
|
507
|
+
job.terms.seller.kind !== 'near' ||
|
|
508
|
+
!h.pending?.operation ||
|
|
509
|
+
!['deposit', 'fund', 'publish'].includes(step)))
|
|
510
|
+
throw new AnnonError('invalid_arguments');
|
|
511
|
+
// Unsaved signatures cannot have reached the host or the chain.
|
|
512
|
+
if (h.pending &&
|
|
513
|
+
((step === 'stage' && !h.authorization) ||
|
|
514
|
+
(step === 'publish' && !h.header) ||
|
|
515
|
+
(step === 'register' && job.terms.seller.kind !== 'near' && !h.pending.auth))) {
|
|
516
|
+
await resetPending();
|
|
517
|
+
h = job.hosting;
|
|
518
|
+
}
|
|
519
|
+
// Reconcile saved evidence. A relayer replay uses exactly the original envelope.
|
|
520
|
+
if (h.pending) {
|
|
521
|
+
if (step === 'register') {
|
|
522
|
+
const f = await effect(() => this.context.file(id));
|
|
523
|
+
if (f) {
|
|
524
|
+
if (f.termsHash !== h.intent.termsHash)
|
|
525
|
+
throw new AnnonError('file_mismatch');
|
|
526
|
+
await complete();
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
const pending = h.pending;
|
|
530
|
+
const args = pending.operation?.args;
|
|
531
|
+
const deadline = args?.args?.deadline;
|
|
532
|
+
if (typeof deadline === 'number' && this.now() > deadline + 60) {
|
|
533
|
+
await resetPending();
|
|
534
|
+
}
|
|
535
|
+
else if (pending.operation?.method === 'register' && pending.auth) {
|
|
536
|
+
const value = await effect(() => this.context.call('register', {
|
|
537
|
+
...present(pending.operation).args,
|
|
538
|
+
auth: pending.auth,
|
|
539
|
+
hostingReservation: h.reservation,
|
|
540
|
+
...(job.baseSplit ? { baseSplit: { kind: 'push', salt } } : {}),
|
|
541
|
+
}, input.seller));
|
|
542
|
+
const ckd = await effect(() => this.context.answer(value, keyFromSecret(bytes(present(job.sellerSecret), 32)).secret, id));
|
|
543
|
+
await update({ ckdSecret: hex(ckd) });
|
|
544
|
+
await complete();
|
|
545
|
+
}
|
|
546
|
+
else
|
|
547
|
+
throw new AnnonError('transaction_uncertain');
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
else if (['deposit', 'fund'].includes(step)) {
|
|
551
|
+
const pending = h.pending;
|
|
552
|
+
if (!pending.operation) {
|
|
553
|
+
await resetPending();
|
|
554
|
+
}
|
|
555
|
+
else if (pending.txHash || input.txHash) {
|
|
556
|
+
const outcome = await checkedOutcome(pending.operation, present(pending.txHash ?? input.txHash));
|
|
557
|
+
if (outcome.state === 'refused')
|
|
558
|
+
await resetPending();
|
|
559
|
+
else {
|
|
560
|
+
if (outcome.state !== 'done')
|
|
561
|
+
throw uncertain();
|
|
562
|
+
await complete();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
const balance = await effect(() => this.fundingBalance(job, step));
|
|
567
|
+
if (step === 'deposit' && pending.storageAccountExisted === false
|
|
568
|
+
? (balance ?? 0n) > 0n
|
|
569
|
+
: (balance ?? 0n) >=
|
|
570
|
+
BigInt(present(pending.snapshot)) + BigInt(pending.operation.deposit))
|
|
571
|
+
await complete();
|
|
572
|
+
else if (this.now() > present(pending.validUntil))
|
|
573
|
+
await resetPending();
|
|
574
|
+
else
|
|
575
|
+
throw uncertain(Math.max(1, present(pending.validUntil) - this.now()));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
else if (h.authorization && ['stage', 'split', 'publish'].includes(step)) {
|
|
579
|
+
await refresh();
|
|
580
|
+
if (!job.hosting.uploadId)
|
|
581
|
+
await create();
|
|
582
|
+
h = job.hosting;
|
|
583
|
+
const status = result(await effect(() => client.status(present(h.uploadId), present(h.authorization))));
|
|
584
|
+
if (step === 'stage' && status.state !== 'published') {
|
|
585
|
+
if (status.state === 'receiving')
|
|
586
|
+
await body(await this.prepared(job, input), present(h.uploadId));
|
|
587
|
+
else
|
|
588
|
+
await update({}, { reservation: status.reservation });
|
|
589
|
+
await complete();
|
|
590
|
+
}
|
|
591
|
+
else if (step === 'split') {
|
|
592
|
+
const split = status.baseSplit?.state === 'ready'
|
|
593
|
+
? status.baseSplit
|
|
594
|
+
: result(await effect(() => client.baseSplit(present(h.uploadId), present(h.authorization), {
|
|
595
|
+
kind: 'push',
|
|
596
|
+
salt,
|
|
597
|
+
}))).baseSplit;
|
|
598
|
+
this.checkSplit(job, split);
|
|
599
|
+
await update({}, { split });
|
|
600
|
+
if (split?.state !== 'ready')
|
|
601
|
+
throw new AnnonError('settlement_pending');
|
|
602
|
+
await complete();
|
|
603
|
+
}
|
|
604
|
+
else if (step === 'publish' && status.state === 'published')
|
|
605
|
+
await this.finishPublication(status.result, job, update);
|
|
606
|
+
else if (step === 'publish' && h.header) {
|
|
607
|
+
if (job.terms.seller.kind === 'near') {
|
|
608
|
+
const f = await effect(() => this.context.file(id));
|
|
609
|
+
const header = bytes(h.header, (h.header.length - 2) / 2);
|
|
610
|
+
if (f?.headerHash !== hex(await sha256(header.subarray(0, header.length - 65)))) {
|
|
611
|
+
const pending = present(h.pending);
|
|
612
|
+
const txHash = pending.txHash ?? input.txHash;
|
|
613
|
+
if (txHash) {
|
|
614
|
+
const operation = present(pending.operation);
|
|
615
|
+
const outcome = await checkedOutcome(operation, txHash);
|
|
616
|
+
if (outcome.state === 'refused') {
|
|
617
|
+
await resetPending();
|
|
618
|
+
return job;
|
|
619
|
+
}
|
|
620
|
+
if (outcome.state !== 'done')
|
|
621
|
+
throw uncertain();
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
await resetPending();
|
|
625
|
+
return job;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const published = result(await effect(() => client.header(present(h.uploadId), present(h.authorization), new Uint8Array(bytes(present(h.header), (present(h.header).length - 2) / 2)))));
|
|
630
|
+
await this.finishPublication(published, job, update);
|
|
631
|
+
}
|
|
632
|
+
else
|
|
633
|
+
throw new AnnonError('transaction_uncertain');
|
|
634
|
+
}
|
|
635
|
+
else
|
|
636
|
+
throw new AnnonError('transaction_uncertain');
|
|
637
|
+
return job;
|
|
638
|
+
}
|
|
639
|
+
freshAttempt = true;
|
|
640
|
+
const prepared = await this.prepared(job, input);
|
|
641
|
+
await fence();
|
|
642
|
+
await update({}, { pending: { step, token: crypto.randomUUID(), startedAt: this.now() } });
|
|
643
|
+
if (!journal) {
|
|
644
|
+
const claim = { ...job, revision: 0, hosting: { ...job.hosting, lease: null } };
|
|
645
|
+
await fence();
|
|
646
|
+
if (!(await this.store.compareAndSet(journalKey, null, claim)))
|
|
647
|
+
throw new AnnonError('store_conflict');
|
|
648
|
+
journal = claim;
|
|
649
|
+
}
|
|
650
|
+
if (step !== 'stage')
|
|
651
|
+
await refresh();
|
|
652
|
+
h = job.hosting;
|
|
653
|
+
if (step === 'stage') {
|
|
654
|
+
await authorize();
|
|
655
|
+
const created = await create();
|
|
656
|
+
if (created.state === 'published') {
|
|
657
|
+
await this.finishPublication(created.result, job, update);
|
|
658
|
+
return job;
|
|
659
|
+
}
|
|
660
|
+
if (created.state === 'receiving')
|
|
661
|
+
await body(prepared, created.uploadId);
|
|
662
|
+
}
|
|
663
|
+
else if (step === 'split') {
|
|
664
|
+
if (job.terms.seller.kind !== 'evm' || !job.terms.base)
|
|
665
|
+
throw new AnnonError('route_unavailable');
|
|
666
|
+
await this.live(job, client, effect);
|
|
667
|
+
const split = result(await effect(() => client.baseSplit(present(h.uploadId), present(h.authorization), { kind: 'push', salt }))).baseSplit;
|
|
668
|
+
this.checkSplit(job, split);
|
|
669
|
+
await update({}, { split });
|
|
670
|
+
if (split?.state !== 'ready')
|
|
671
|
+
throw new AnnonError('settlement_pending');
|
|
672
|
+
}
|
|
673
|
+
else if (step === 'deposit' || step === 'fund') {
|
|
674
|
+
await this.live(job, client, effect);
|
|
675
|
+
const seller = nearWallet(input.seller);
|
|
676
|
+
const operation = {
|
|
677
|
+
method: step === 'deposit' ? 'storage_deposit' : 'fund_file_storage',
|
|
678
|
+
args: step === 'deposit' ? { account_id: seller.identity.id } : { file_id: id },
|
|
679
|
+
deposit: step === 'deposit' ? present(h.listingStorage) : present(job.storageBudget),
|
|
680
|
+
};
|
|
681
|
+
const snapshot = await effect(() => this.fundingBalance(job, step));
|
|
682
|
+
await update({}, {
|
|
683
|
+
pending: {
|
|
684
|
+
...present(job.hosting.pending),
|
|
685
|
+
operation,
|
|
686
|
+
snapshot: (snapshot ?? 0n).toString(),
|
|
687
|
+
...(step === 'deposit' ? { storageAccountExisted: snapshot !== null } : {}),
|
|
688
|
+
validUntil: this.now() + nearValiditySeconds,
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
await effect(() => direct(operation.method, operation.args, BigInt(operation.deposit)));
|
|
692
|
+
}
|
|
693
|
+
else if (step === 'register') {
|
|
694
|
+
const existing = await effect(() => this.context.file(id));
|
|
695
|
+
if (existing) {
|
|
696
|
+
if (existing.termsHash !== h.intent.termsHash)
|
|
697
|
+
throw new AnnonError('file_mismatch');
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
await this.live(job, client, effect);
|
|
701
|
+
const key = keyFromSecret(bytes(present(job.sellerSecret), 32));
|
|
702
|
+
const args = {
|
|
703
|
+
terms: job.terms,
|
|
704
|
+
seller_key: hex(key.publicKeyBytes),
|
|
705
|
+
deadline: this.now() + 540,
|
|
706
|
+
};
|
|
707
|
+
await update({}, {
|
|
708
|
+
pending: {
|
|
709
|
+
...present(job.hosting.pending),
|
|
710
|
+
operation: { method: 'register', args: { args }, deposit: '0' },
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
const value = await effect(async () => {
|
|
714
|
+
if (job.terms.seller.kind === 'near')
|
|
715
|
+
return direct('register', { args }, 0n);
|
|
716
|
+
awaitingSignature = true;
|
|
717
|
+
const auth = await input.seller.authorize(registerTextV2(job.terms, args.seller_key, args.deadline, assets(job.terms)));
|
|
718
|
+
awaitingSignature = false;
|
|
719
|
+
if (!auth ||
|
|
720
|
+
!verifyAuthorization(job.terms.seller, registerTextV2(job.terms, args.seller_key, args.deadline, assets(job.terms)), auth))
|
|
721
|
+
throw new AnnonError('invalid_signature');
|
|
722
|
+
await update({}, { pending: { ...present(job.hosting.pending), auth } });
|
|
723
|
+
await fence();
|
|
724
|
+
return this.context.call('register', {
|
|
725
|
+
args,
|
|
726
|
+
auth,
|
|
727
|
+
hostingReservation: job.hosting.reservation,
|
|
728
|
+
...(job.baseSplit ? { baseSplit: { kind: 'push', salt } } : {}),
|
|
729
|
+
}, input.seller);
|
|
730
|
+
});
|
|
731
|
+
const ckd = await effect(() => this.context.answer(value, key.secret, id));
|
|
732
|
+
await update({ ckdSecret: hex(ckd) });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
else if (step === 'publish') {
|
|
736
|
+
if (!job.ckdSecret) {
|
|
737
|
+
const ckd = await effect(() => this.context.reopen(id, input.seller));
|
|
738
|
+
await update({ ckdSecret: hex(ckd) });
|
|
739
|
+
}
|
|
740
|
+
const finished = await effect(() => prepared.finish({
|
|
741
|
+
contractId: this.o.contractId,
|
|
742
|
+
seller: job.terms.seller,
|
|
743
|
+
ckdSecret: bytes(present(job.ckdSecret), 48),
|
|
744
|
+
...(job.terms.seller.kind === 'near'
|
|
745
|
+
? {}
|
|
746
|
+
: {
|
|
747
|
+
sign: async (text) => {
|
|
748
|
+
awaitingSignature = true;
|
|
749
|
+
const auth = await effect(() => input.seller.authorize(text));
|
|
750
|
+
awaitingSignature = false;
|
|
751
|
+
if (!auth || !verifyAuthorization(job.terms.seller, text, auth))
|
|
752
|
+
throw new AnnonError('invalid_signature');
|
|
753
|
+
const signature = bytes(auth.sig, job.terms.seller.kind === 'evm' ? 65 : 64);
|
|
754
|
+
return job.terms.seller.kind === 'solana'
|
|
755
|
+
? { signature, signedMessage: new TextEncoder().encode(text) }
|
|
756
|
+
: signature;
|
|
757
|
+
},
|
|
758
|
+
}),
|
|
759
|
+
}));
|
|
760
|
+
try {
|
|
761
|
+
// Exhaust the checked replay even though only the header is sent in this step.
|
|
762
|
+
if (finished.file instanceof ReadableStream) {
|
|
763
|
+
const reader = finished.file.getReader();
|
|
764
|
+
try {
|
|
765
|
+
while (!(await reader.read()).done) { }
|
|
766
|
+
}
|
|
767
|
+
finally {
|
|
768
|
+
reader.releaseLock();
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (!finished.consumed())
|
|
772
|
+
throw new AnnonError('upload_incomplete');
|
|
773
|
+
await update({}, { header: hex(finished.header) });
|
|
774
|
+
if (job.terms.seller.kind === 'near') {
|
|
775
|
+
const operation = {
|
|
776
|
+
method: 'publish_header',
|
|
777
|
+
args: {
|
|
778
|
+
file_id: id,
|
|
779
|
+
header_hash: hex(await sha256(finished.header.subarray(0, finished.header.length - 65))),
|
|
780
|
+
},
|
|
781
|
+
deposit: '0',
|
|
782
|
+
};
|
|
783
|
+
await update({}, { pending: { ...present(job.hosting.pending), operation } });
|
|
784
|
+
await effect(() => direct(operation.method, operation.args, 0n));
|
|
785
|
+
}
|
|
786
|
+
const published = result(await effect(() => client.header(present(h.uploadId), present(h.authorization), new Uint8Array(finished.header))));
|
|
787
|
+
await this.finishPublication(published, job, update);
|
|
788
|
+
return job;
|
|
789
|
+
}
|
|
790
|
+
catch (e) {
|
|
791
|
+
await finished.cancel(e).catch(() => { });
|
|
792
|
+
throw e;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
await complete();
|
|
796
|
+
return job;
|
|
797
|
+
}
|
|
798
|
+
catch (error) {
|
|
799
|
+
const refusedRegistration = freshAttempt &&
|
|
800
|
+
job.hosting.pending?.step === 'register' &&
|
|
801
|
+
error instanceof AnnonError &&
|
|
802
|
+
[
|
|
803
|
+
'budget',
|
|
804
|
+
'limited',
|
|
805
|
+
'rate_limited',
|
|
806
|
+
'not_sponsored',
|
|
807
|
+
'bad_request',
|
|
808
|
+
'bad_signature',
|
|
809
|
+
'bad_deadline',
|
|
810
|
+
'upload_expired',
|
|
811
|
+
].includes(error.code) &&
|
|
812
|
+
!('txHash' in error);
|
|
813
|
+
if (refusedRegistration ||
|
|
814
|
+
(freshAttempt &&
|
|
815
|
+
(awaitingSignature || awaitingTransaction) &&
|
|
816
|
+
error instanceof AnnonError &&
|
|
817
|
+
error.code === 'rejected' &&
|
|
818
|
+
!('txHash' in error))) {
|
|
819
|
+
await resetPending();
|
|
820
|
+
}
|
|
821
|
+
if (error instanceof AnnonError && 'txHash' in error && typeof error.txHash === 'string') {
|
|
822
|
+
try {
|
|
823
|
+
await update({}, { pending: { ...present(job.hosting.pending), txHash: error.txHash } });
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
/* A new owner retains the original operation. */
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
throw error;
|
|
830
|
+
}
|
|
831
|
+
finally {
|
|
832
|
+
clearInterval(heartbeat);
|
|
833
|
+
await updates;
|
|
834
|
+
// A former owner never adopts the revision or lease of its successor.
|
|
835
|
+
try {
|
|
836
|
+
await fence();
|
|
837
|
+
await update({}, { lease: null });
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
/* The durable pending operation remains available to its new owner. */
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
checkSplit(job, value) {
|
|
845
|
+
if (!value ||
|
|
846
|
+
job.terms.seller.kind !== 'evm' ||
|
|
847
|
+
value.address.toLowerCase() !== job.terms.base?.pay_to ||
|
|
848
|
+
value.kind !== 'push' ||
|
|
849
|
+
value.salt !== salt)
|
|
850
|
+
throw new AnnonError('bad_response');
|
|
851
|
+
}
|
|
852
|
+
async live(job, client, effect) {
|
|
853
|
+
const h = job.hosting;
|
|
854
|
+
if (!h)
|
|
855
|
+
throw new AnnonError('invalid_record');
|
|
856
|
+
if (!h.uploadId || !h.authorization)
|
|
857
|
+
throw new AnnonError('upload_incomplete');
|
|
858
|
+
const status = result(await effect(() => client.status(present(h.uploadId), present(h.authorization))));
|
|
859
|
+
if (status.state !== 'reserved' ||
|
|
860
|
+
status.reservation.expiresAt <= this.now() ||
|
|
861
|
+
status.reservation.termsHash !== h.intent.termsHash)
|
|
862
|
+
throw new AnnonError('upload_expired');
|
|
863
|
+
}
|
|
864
|
+
async finishPublication(published, job, update) {
|
|
865
|
+
const expiry = published.expiresAt;
|
|
866
|
+
if (expiry !== null) {
|
|
867
|
+
if (!Number.isSafeInteger(expiry) || expiry <= 0)
|
|
868
|
+
throw new AnnonError('bad_response');
|
|
869
|
+
const now = this.now();
|
|
870
|
+
if (expiry <= now)
|
|
871
|
+
throw new AnnonError('file_expired');
|
|
872
|
+
}
|
|
873
|
+
await update({ published: true, fileKey: null, sellerSecret: null, ckdSecret: null }, {
|
|
874
|
+
result: published,
|
|
875
|
+
completed: [...(job.hosting?.steps ?? [])],
|
|
876
|
+
pending: null,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|