@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,1152 @@
|
|
|
1
|
+
import { AnnonError, toPublicError } from '../errors.js';
|
|
2
|
+
import { HostedSelling, shareSize } from './sharing.js';
|
|
3
|
+
/** Sell, buy, open, recover and stop, using the single purchase/grant settlement model. */
|
|
4
|
+
import { DownloadError } from '../download/download.js';
|
|
5
|
+
import { concat, inspectHeader, open as openPay2, sha256, } from '../envelope/pay2.js';
|
|
6
|
+
import { prepareOpen, prepareSeal, withReplay, } from '../envelope/replay.js';
|
|
7
|
+
import { SourceChangedError } from '../envelope/stream.js';
|
|
8
|
+
import { fileAppId, keyFromSecret, oneTimeKey, openAnswer } from '../keys/near-ckd.js';
|
|
9
|
+
import { MAX_PAY2_BYTES, MAX_PLAINTEXT_BYTES } from '../limits.js';
|
|
10
|
+
import { fileStorageBalance, getConfig, getFile, getGrant, getPurchase, isStorageRequired, purchasesOf, readOutcome, view, } from '../near/rpc.js';
|
|
11
|
+
import { fileFundingText, registerText, reopenSellerText, reopenText, stopSalesText, } from '../near/texts.js';
|
|
12
|
+
import { OneClick, OneClickQuoteExpired, validateOrigin } from '../oneclick/oneclick.js';
|
|
13
|
+
import { encryptedPurchaseStore, encryptedRecoveryStore, encryptedSellerKeyStore, encryptedSellStore, exportPurchase, exportRecovery, exportSell, exportSellerKey, validatePurchaseRecord, } from '../pay/backup.js';
|
|
14
|
+
import { balanceOf, hasCode, head, PayError } from '../pay/base.js';
|
|
15
|
+
import { authorize, preflight } from '../pay/pay.js';
|
|
16
|
+
import { purchaseKey, randomHex } from '../pay/purchase.js';
|
|
17
|
+
import { nextStep } from '../pay/recover.js';
|
|
18
|
+
import { createSplit } from '../pay/split.js';
|
|
19
|
+
import { checkBaseSplit, checkBaseSponsorship, validBaseSponsorship, } from '../relay/capabilities.js';
|
|
20
|
+
import { applySponsoredFee, checkProtocolFee, protocolFeeAboveMax, } from '../relay/fees.js';
|
|
21
|
+
import { account, bytes, compare, hex, network, usdc } from '../settle-near/codec.js';
|
|
22
|
+
import { encodeIdentity, identityJson, identityKind, identityText, parseIdentity, verifyAuthorization, } from '../settle-near/identity.js';
|
|
23
|
+
import { buildPurchaseMessage, parsePurchaseMessage, purchaseId, } from '../settle-near/purchase.js';
|
|
24
|
+
import { termsHash } from '../settle-near/terms.js';
|
|
25
|
+
import { TransactionUncertain } from '../wallet/uncertain.js';
|
|
26
|
+
import { FileFundingRefused, fundFileStorage } from '../wallet/wallet.js';
|
|
27
|
+
const asNear = (w) => {
|
|
28
|
+
if (w.identity.kind !== 'near' || !('call' in w) || typeof w.call !== 'function')
|
|
29
|
+
throw new AnnonError('near_wallet_required', 'a NEAR transaction wallet is required');
|
|
30
|
+
return w;
|
|
31
|
+
};
|
|
32
|
+
const assetsFor = (terms) => ({
|
|
33
|
+
nearUsdc: terms.near.token,
|
|
34
|
+
...(terms.base ? { baseUsdc: terms.base.usdc } : {}),
|
|
35
|
+
});
|
|
36
|
+
const listing = (fileId, f) => ({
|
|
37
|
+
fileId,
|
|
38
|
+
cipherHash: f.cipherHash,
|
|
39
|
+
seller: f.seller,
|
|
40
|
+
...(f.headerHash ? { publishedHeaderHash: f.headerHash } : {}),
|
|
41
|
+
});
|
|
42
|
+
export class SellError extends AnnonError {
|
|
43
|
+
fileId;
|
|
44
|
+
txHash;
|
|
45
|
+
retryAfterSeconds;
|
|
46
|
+
constructor(fileId, options) {
|
|
47
|
+
const cause = options?.cause;
|
|
48
|
+
const code = cause instanceof AnnonError || cause instanceof SourceChangedError
|
|
49
|
+
? cause.code
|
|
50
|
+
: 'sell_incomplete';
|
|
51
|
+
super(code, cause instanceof Error ? cause.message : 'Sell incomplete; resume with fileId.', options);
|
|
52
|
+
this.fileId = fileId;
|
|
53
|
+
this.name = 'SellError';
|
|
54
|
+
const safe = toPublicError(cause);
|
|
55
|
+
if (safe.retryAfterSeconds !== undefined)
|
|
56
|
+
this.retryAfterSeconds = safe.retryAfterSeconds;
|
|
57
|
+
if (cause instanceof AnnonError && 'txHash' in cause && typeof cause.txHash === 'string')
|
|
58
|
+
this.txHash = cause.txHash;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export class Annon {
|
|
62
|
+
o;
|
|
63
|
+
store;
|
|
64
|
+
hosted;
|
|
65
|
+
sellerKeys;
|
|
66
|
+
sells;
|
|
67
|
+
recoveries;
|
|
68
|
+
selfUnlockWallets = new Map();
|
|
69
|
+
unlocked = new Map();
|
|
70
|
+
constructor(o) {
|
|
71
|
+
this.o = o;
|
|
72
|
+
account(o.contractId);
|
|
73
|
+
network(o.network);
|
|
74
|
+
this.hosted = new HostedSelling(o, {
|
|
75
|
+
config: () => getConfig(o.nearRpc, o.contractId),
|
|
76
|
+
file: (id) => getFile(o.nearRpc, o.contractId, id),
|
|
77
|
+
call: (method, args, seller) => this.call(method, args, seller),
|
|
78
|
+
answer: (value, secret, id) => this.answer(value, secret, id),
|
|
79
|
+
reopen: (id, seller) => this.reopenSeller({ fileId: id, seller }),
|
|
80
|
+
});
|
|
81
|
+
this.store = this.withBackup(encryptedPurchaseStore(o.rawStore, o.backupSecret), (r) => exportPurchase(r, o.backupSecret));
|
|
82
|
+
this.sellerKeys = this.withBackup(encryptedSellerKeyStore(o.rawStore, o.backupSecret), (r) => exportSellerKey(r, o.backupSecret));
|
|
83
|
+
this.sells = this.withBackup(encryptedSellStore(o.rawStore, o.backupSecret), (r) => exportSell(r, o.backupSecret));
|
|
84
|
+
this.recoveries = this.withBackup(encryptedRecoveryStore(o.rawStore, o.backupSecret), (r) => exportRecovery(r, o.backupSecret));
|
|
85
|
+
}
|
|
86
|
+
withBackup(store, encode) {
|
|
87
|
+
return {
|
|
88
|
+
load: (key) => store.load(key),
|
|
89
|
+
compareAndSet: async (key, expected, next) => {
|
|
90
|
+
const ok = await store.compareAndSet(key, expected, next);
|
|
91
|
+
if (ok && next)
|
|
92
|
+
await this.o.writeBackup(next.id, await encode(next));
|
|
93
|
+
return ok;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
now() {
|
|
98
|
+
return (this.o.now ?? (() => Math.floor(Date.now() / 1000)))();
|
|
99
|
+
}
|
|
100
|
+
async auth(wallet, text) {
|
|
101
|
+
const id = parseIdentity(wallet.identity);
|
|
102
|
+
const auth = await wallet.authorize(text);
|
|
103
|
+
if (!verifyAuthorization(id, text, auth, id.kind === 'near' ? id.id : undefined))
|
|
104
|
+
throw new AnnonError('invalid_signature', 'wallet did not authorize the exact text');
|
|
105
|
+
return auth;
|
|
106
|
+
}
|
|
107
|
+
async call(method, args, wallet) {
|
|
108
|
+
const near = wallet?.identity.kind === 'near' ? asNear(wallet) : undefined;
|
|
109
|
+
const result = this.o.send
|
|
110
|
+
? await this.o.send(method, args, near)
|
|
111
|
+
: near
|
|
112
|
+
? await near.call({ receiverId: this.o.contractId, method, args })
|
|
113
|
+
: (() => {
|
|
114
|
+
throw new AnnonError('transport_required', 'configure a transaction transport for relayed calls');
|
|
115
|
+
})();
|
|
116
|
+
if (result && typeof result === 'object' && 'transaction' in result) {
|
|
117
|
+
const transaction = result
|
|
118
|
+
.transaction;
|
|
119
|
+
const outcome = readOutcome(result, this.o.contractId, {
|
|
120
|
+
txHash: transaction.hash,
|
|
121
|
+
signer: transaction.signer_id,
|
|
122
|
+
method,
|
|
123
|
+
args: method === 'fund_file_storage'
|
|
124
|
+
? {
|
|
125
|
+
file_id: args.args.file_id,
|
|
126
|
+
amount: args.args.amount,
|
|
127
|
+
}
|
|
128
|
+
: method === 'register'
|
|
129
|
+
? // Relayer-only fields never reach the contract call.
|
|
130
|
+
Object.fromEntries(Object.entries(args).filter(([key]) => key !== 'baseSplit' && key !== 'hostingReservation'))
|
|
131
|
+
: args,
|
|
132
|
+
});
|
|
133
|
+
if (outcome.state === 'released')
|
|
134
|
+
return outcome.answer;
|
|
135
|
+
if (outcome.state === 'done')
|
|
136
|
+
return outcome.value;
|
|
137
|
+
if (isStorageRequired('reason' in outcome ? outcome.reason : ''))
|
|
138
|
+
throw new PayError('storage_required');
|
|
139
|
+
throw new AnnonError(outcome.state === 'pending' ? 'settlement_pending' : 'refused');
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
/** Allocate file storage directly with NEAR, or request a seller-signed sponsored top-up. */
|
|
144
|
+
async fund(fileId, amount, seller, options = {}) {
|
|
145
|
+
bytes(fileId, 32);
|
|
146
|
+
if (amount <= 0n || amount >= 2n ** 128n)
|
|
147
|
+
throw new PayError('bad_requirements');
|
|
148
|
+
const file = await getFile(this.o.nearRpc, this.o.contractId, fileId);
|
|
149
|
+
if (!file)
|
|
150
|
+
throw new PayError('unknown_file');
|
|
151
|
+
if (file.terms.network !== this.o.network || file.terms.contract !== this.o.contractId)
|
|
152
|
+
throw new PayError('network_mismatch');
|
|
153
|
+
if (identityJson(parseIdentity(seller.identity)) !== identityJson(file.seller))
|
|
154
|
+
throw new PayError('wrong_identity');
|
|
155
|
+
if (file.takenDown !== null)
|
|
156
|
+
throw new PayError('taken_down');
|
|
157
|
+
if (file.salesStoppedAt !== null)
|
|
158
|
+
throw new PayError('sales_stopped');
|
|
159
|
+
if (seller.identity.kind === 'near')
|
|
160
|
+
return fundFileStorage(asNear(seller), this.o.contractId, fileId, amount);
|
|
161
|
+
const deadline = options.deadline ?? this.now() + 600;
|
|
162
|
+
if (!Number.isSafeInteger(deadline) || deadline <= this.now() || deadline > this.now() + 600)
|
|
163
|
+
throw new PayError('bad_requirements');
|
|
164
|
+
const args = {
|
|
165
|
+
file_id: fileId,
|
|
166
|
+
amount: String(amount),
|
|
167
|
+
nonce: options.nonce ?? `0x${randomHex(32)}`,
|
|
168
|
+
deadline,
|
|
169
|
+
};
|
|
170
|
+
const text = fileFundingText({
|
|
171
|
+
...args,
|
|
172
|
+
network: this.o.network,
|
|
173
|
+
contract: this.o.contractId,
|
|
174
|
+
seller: file.seller,
|
|
175
|
+
});
|
|
176
|
+
const auth = await this.auth(seller, text);
|
|
177
|
+
return this.call('fund_file_storage', { args, auth }, seller);
|
|
178
|
+
}
|
|
179
|
+
async answer(value, secret, fileId) {
|
|
180
|
+
const answer = value && typeof value === 'object' && 'answer' in value ? value.answer : value;
|
|
181
|
+
const config = await getConfig(this.o.nearRpc, this.o.contractId);
|
|
182
|
+
const mpcKey = await view(this.o.nearRpc, config.mpc, 'public_key', {
|
|
183
|
+
domain_id: config.ckdDomain,
|
|
184
|
+
});
|
|
185
|
+
if (typeof mpcKey !== 'string' || !mpcKey.startsWith('bls12381g2:'))
|
|
186
|
+
throw new AnnonError('ckd_invalid', 'MPC domain has no CKD key');
|
|
187
|
+
if (this.o.mpcKey && this.o.mpcKey !== mpcKey)
|
|
188
|
+
throw new AnnonError('ckd_invalid', 'configured MPC key differs from the contract MPC domain');
|
|
189
|
+
return openAnswer(answer, {
|
|
190
|
+
secret,
|
|
191
|
+
appId: fileAppId(this.o.contractId, bytes(fileId, 32)),
|
|
192
|
+
mpcKey,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async file(fileId, forSale = false) {
|
|
196
|
+
const f = await getFile(this.o.nearRpc, this.o.contractId, fileId);
|
|
197
|
+
if (!f)
|
|
198
|
+
throw new PayError('unknown_file');
|
|
199
|
+
if (f.takenDown)
|
|
200
|
+
throw new PayError('taken_down');
|
|
201
|
+
if (forSale && f.salesStoppedAt !== null)
|
|
202
|
+
throw new PayError('sales_stopped');
|
|
203
|
+
const config = await getConfig(this.o.nearRpc, this.o.contractId);
|
|
204
|
+
if (f.terms.network !== this.o.network ||
|
|
205
|
+
config.network !== this.o.network ||
|
|
206
|
+
f.terms.near.token !== config.nearUsdc ||
|
|
207
|
+
(f.terms.base && f.terms.base.usdc !== config.usdc))
|
|
208
|
+
throw new PayError('network_mismatch');
|
|
209
|
+
return f;
|
|
210
|
+
}
|
|
211
|
+
/** Hosted sale with a pending backup saved before the first approval. */
|
|
212
|
+
async startShare(options) {
|
|
213
|
+
const quote = await this.hosted.quote(options);
|
|
214
|
+
await options.onSplit?.(structuredClone(quote.terms.near), quote.fee ?? undefined, quote.fee ? protocolFeeAboveMax(options.price, quote.fee, this.o.maxProtocolFee) : false);
|
|
215
|
+
try {
|
|
216
|
+
const job = await this.hosted.begin(quote.quoteId);
|
|
217
|
+
if (!options.confirmBackup)
|
|
218
|
+
throw new AnnonError('backup_secret_required');
|
|
219
|
+
await options.confirmBackup({
|
|
220
|
+
fileId: quote.fileId,
|
|
221
|
+
text: await this.hosted.backup(quote.fileId),
|
|
222
|
+
});
|
|
223
|
+
await this.hosted.confirmBackup(job.terms.file_id);
|
|
224
|
+
return job;
|
|
225
|
+
}
|
|
226
|
+
catch (cause) {
|
|
227
|
+
if (cause instanceof SellError)
|
|
228
|
+
throw cause;
|
|
229
|
+
throw new SellError(quote.fileId, { cause });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async share(options) {
|
|
233
|
+
const job = await this.startShare(options);
|
|
234
|
+
return this.resumeShare({ ...options, fileId: job.terms.file_id });
|
|
235
|
+
}
|
|
236
|
+
async resumeShare(options) {
|
|
237
|
+
try {
|
|
238
|
+
shareSize(options.data);
|
|
239
|
+
for (;;) {
|
|
240
|
+
const job = await this.hosted.load(options.fileId);
|
|
241
|
+
if (job.hosting?.result)
|
|
242
|
+
return job.hosting.result;
|
|
243
|
+
await this.hosted.next(options.fileId, options);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (cause) {
|
|
247
|
+
throw new SellError(options.fileId, { cause });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async hostedFile(original, data, publish) {
|
|
251
|
+
const job = await this.hosted.load(original.terms.file_id);
|
|
252
|
+
if (!job.hosting.result || !job.hosting.header || !original.fileKey)
|
|
253
|
+
throw new AnnonError('invalid_record');
|
|
254
|
+
const header = bytes(job.hosting.header, (job.hosting.header.length - 2) / 2);
|
|
255
|
+
const prepared = await prepareSeal(bytes(original.fileKey, 32), data, {
|
|
256
|
+
fileId: bytes(job.terms.file_id, 32),
|
|
257
|
+
name: job.name,
|
|
258
|
+
mime: job.mime,
|
|
259
|
+
authKind: identityKind(job.terms.seller),
|
|
260
|
+
});
|
|
261
|
+
if (hex(prepared.cipherHash) !== job.terms.cipher_hash)
|
|
262
|
+
throw new SourceChangedError();
|
|
263
|
+
let consumed = false;
|
|
264
|
+
let file;
|
|
265
|
+
if (data instanceof Uint8Array) {
|
|
266
|
+
file = concat(header, new Uint8Array(await new Response(prepared.body()).arrayBuffer()));
|
|
267
|
+
consumed = true;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
const reader = prepared.body().getReader();
|
|
271
|
+
let first = true;
|
|
272
|
+
file = new ReadableStream({
|
|
273
|
+
async pull(controller) {
|
|
274
|
+
if (first) {
|
|
275
|
+
first = false;
|
|
276
|
+
controller.enqueue(header);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const part = await reader.read();
|
|
281
|
+
if (part.done) {
|
|
282
|
+
consumed = true;
|
|
283
|
+
controller.close();
|
|
284
|
+
reader.releaseLock();
|
|
285
|
+
}
|
|
286
|
+
else
|
|
287
|
+
controller.enqueue(part.value);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
controller.error(error);
|
|
291
|
+
reader.releaseLock();
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
async cancel(reason) {
|
|
295
|
+
try {
|
|
296
|
+
await reader.cancel(reason);
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
reader.releaseLock();
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
}, { highWaterMark: 0 });
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await publish({ file, header, terms: job.terms });
|
|
306
|
+
if (!consumed)
|
|
307
|
+
throw new AnnonError('upload_incomplete');
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
if (file instanceof ReadableStream)
|
|
311
|
+
await file.cancel(error).catch(() => { });
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
terms: job.terms,
|
|
316
|
+
...(job.baseSplit ? { baseSplit: job.baseSplit } : {}),
|
|
317
|
+
file,
|
|
318
|
+
header,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
async sell(o) {
|
|
322
|
+
if (o.base && parseIdentity(o.seller.identity).kind !== 'evm')
|
|
323
|
+
throw new AnnonError('route_unavailable');
|
|
324
|
+
if (o.hosting) {
|
|
325
|
+
if (o.fileId ||
|
|
326
|
+
o.moderator ||
|
|
327
|
+
(o.previewLen ?? 0) !== 0 ||
|
|
328
|
+
(o.base && o.base.price !== o.near.price))
|
|
329
|
+
throw new AnnonError('invalid_arguments');
|
|
330
|
+
const options = {
|
|
331
|
+
data: o.data,
|
|
332
|
+
name: o.name,
|
|
333
|
+
mime: o.mime,
|
|
334
|
+
seller: o.seller,
|
|
335
|
+
price: o.near.price,
|
|
336
|
+
near: { recipients: o.near.recipients, remainder: o.near.remainder },
|
|
337
|
+
hosting: o.hosting,
|
|
338
|
+
...(o.storageBudget !== undefined ? { storageBudget: o.storageBudget } : {}),
|
|
339
|
+
...(o.hosting.listingStorage !== undefined
|
|
340
|
+
? { listingStorage: o.hosting.listingStorage }
|
|
341
|
+
: {}),
|
|
342
|
+
...(o.hosting.confirmBackup ? { confirmBackup: o.hosting.confirmBackup } : {}),
|
|
343
|
+
...(o.onSplit ? { onSplit: o.onSplit } : {}),
|
|
344
|
+
};
|
|
345
|
+
const job = await this.startShare(options);
|
|
346
|
+
await this.resumeShare({ ...options, fileId: job.terms.file_id });
|
|
347
|
+
return this.hostedFile(job, o.data, o.publish);
|
|
348
|
+
}
|
|
349
|
+
if ((o.data instanceof Uint8Array ? o.data.length : o.data.size) > MAX_PLAINTEXT_BYTES)
|
|
350
|
+
throw new DownloadError('too_large');
|
|
351
|
+
if (o.storageBudget !== undefined && (o.storageBudget <= 0n || o.storageBudget >= 2n ** 128n))
|
|
352
|
+
throw new AnnonError('invalid_arguments', 'invalid storage budget');
|
|
353
|
+
if (o.storageBudget !== undefined && o.seller.identity.kind !== 'near')
|
|
354
|
+
throw new AnnonError('near_wallet_required', 'storage budget requires a NEAR wallet');
|
|
355
|
+
const config = await getConfig(this.o.nearRpc, this.o.contractId);
|
|
356
|
+
if (config.network !== this.o.network)
|
|
357
|
+
throw new PayError('network_mismatch');
|
|
358
|
+
const assets = { nearUsdc: config.nearUsdc, baseUsdc: config.usdc };
|
|
359
|
+
const seller = parseIdentity(o.seller.identity);
|
|
360
|
+
const fileId = o.fileId ?? `0x${randomHex(32)}`;
|
|
361
|
+
const fileKey = crypto.getRandomValues(new Uint8Array(32));
|
|
362
|
+
const s = await prepareSeal(fileKey, o.data, {
|
|
363
|
+
fileId: bytes(fileId, 32),
|
|
364
|
+
name: o.name,
|
|
365
|
+
mime: o.mime,
|
|
366
|
+
authKind: identityKind(seller),
|
|
367
|
+
...(o.previewLen === undefined ? {} : { previewLen: o.previewLen }),
|
|
368
|
+
});
|
|
369
|
+
let near = {
|
|
370
|
+
token: config.nearUsdc,
|
|
371
|
+
...o.near,
|
|
372
|
+
remainder: o.near.recipients.findIndex((r) => identityJson(r.identity) === identityJson(o.near.remainder)),
|
|
373
|
+
};
|
|
374
|
+
let fee;
|
|
375
|
+
if (this.o.sponsorship && seller.kind !== 'near') {
|
|
376
|
+
const advertised = (await this.o.sponsorship()).fee;
|
|
377
|
+
fee = advertised;
|
|
378
|
+
if (!o.onSplit &&
|
|
379
|
+
[near.price, ...(o.base ? [o.base.price] : [])].some((price) => protocolFeeAboveMax(price, advertised, this.o.maxProtocolFee)))
|
|
380
|
+
throw new PayError('protocol_fee_above_max');
|
|
381
|
+
near = applySponsoredFee(near, fee, o.base?.price);
|
|
382
|
+
// The shared treasury share must meet both route requirements.
|
|
383
|
+
checkProtocolFee({ near, base: o.base ? { price: o.base.price } : null }, fee);
|
|
384
|
+
}
|
|
385
|
+
const aboveMaximum = fee !== undefined &&
|
|
386
|
+
[near.price, ...(o.base ? [o.base.price] : [])].some((price) => protocolFeeAboveMax(price, fee, this.o.maxProtocolFee));
|
|
387
|
+
if (aboveMaximum && !o.onSplit)
|
|
388
|
+
throw new PayError('protocol_fee_above_max');
|
|
389
|
+
await o.onSplit?.(near, fee, aboveMaximum);
|
|
390
|
+
const remainderIdentity = near.recipients[near.remainder]?.identity;
|
|
391
|
+
const recipients = near.recipients
|
|
392
|
+
.map((r) => ({ ...r, identity: parseIdentity(r.identity) }))
|
|
393
|
+
.sort((a, b) => compare(encodeIdentity(a.identity), encodeIdentity(b.identity)));
|
|
394
|
+
const remainder = recipients.findIndex((r) => identityJson(r.identity) === identityJson(remainderIdentity));
|
|
395
|
+
const terms = {
|
|
396
|
+
network: this.o.network,
|
|
397
|
+
contract: this.o.contractId,
|
|
398
|
+
file_id: fileId,
|
|
399
|
+
seller,
|
|
400
|
+
pay2_version: 2,
|
|
401
|
+
cipher_hash: hex(s.cipherHash),
|
|
402
|
+
moderator: o.moderator ?? null,
|
|
403
|
+
near: { token: config.nearUsdc, price: o.near.price, recipients, remainder },
|
|
404
|
+
base: null,
|
|
405
|
+
};
|
|
406
|
+
termsHash(terms, assets); // Validate all local terms before even creating a Base split.
|
|
407
|
+
let baseSplit = null;
|
|
408
|
+
if (o.base) {
|
|
409
|
+
if (recipients.some((r) => r.identity.kind !== 'evm'))
|
|
410
|
+
throw new AnnonError('bad_requirements', 'Base split requires only EVM recipients');
|
|
411
|
+
const kind = o.base.kind ?? 'push';
|
|
412
|
+
const salt = `0x${randomHex(32)}`;
|
|
413
|
+
const split = createSplit(kind, {
|
|
414
|
+
recipients: recipients.map((r) => r.identity.id),
|
|
415
|
+
allocations: recipients.map((r) => BigInt(r.bps)),
|
|
416
|
+
totalAllocation: 10000n,
|
|
417
|
+
distributionIncentive: 0,
|
|
418
|
+
}, salt);
|
|
419
|
+
baseSplit = { address: split.address, kind, salt };
|
|
420
|
+
terms.base = { usdc: config.usdc, price: o.base.price, pay_to: split.address };
|
|
421
|
+
termsHash(terms, assets);
|
|
422
|
+
await o.base.sendTransaction(split);
|
|
423
|
+
}
|
|
424
|
+
const key = oneTimeKey();
|
|
425
|
+
const job = {
|
|
426
|
+
kind: 'sell',
|
|
427
|
+
...(o.storageBudget !== undefined ? { storageBudget: o.storageBudget.toString() } : {}),
|
|
428
|
+
id: `sell:${fileId}`,
|
|
429
|
+
revision: 0,
|
|
430
|
+
terms,
|
|
431
|
+
baseSplit,
|
|
432
|
+
fileKey: hex(fileKey),
|
|
433
|
+
sellerSecret: hex(key.secret),
|
|
434
|
+
deadline: this.now() + 540,
|
|
435
|
+
name: o.name,
|
|
436
|
+
mime: o.mime,
|
|
437
|
+
previewLen: o.previewLen ?? 0,
|
|
438
|
+
ckdSecret: null,
|
|
439
|
+
};
|
|
440
|
+
try {
|
|
441
|
+
if (!(await this.sells.compareAndSet(`annon/sell/${fileId}`, null, job)))
|
|
442
|
+
throw new PayError('store_conflict');
|
|
443
|
+
return await this.finishSell(job, s, o.seller, o.publish, false);
|
|
444
|
+
}
|
|
445
|
+
catch (cause) {
|
|
446
|
+
throw new SellError(fileId, { cause });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/** Resume with the original plaintext; the saved file key reproduces the registered cipher hash. */
|
|
450
|
+
async resumeSell(o) {
|
|
451
|
+
if ((o.data instanceof Uint8Array ? o.data.length : o.data.size) > MAX_PLAINTEXT_BYTES)
|
|
452
|
+
throw new DownloadError('too_large');
|
|
453
|
+
const job = await this.sells.load(`annon/sell/${o.fileId}`);
|
|
454
|
+
if (!job)
|
|
455
|
+
throw new AnnonError('invalid_record', 'no saved sell job');
|
|
456
|
+
if (job.hosting && !job.published) {
|
|
457
|
+
if (!o.hosting)
|
|
458
|
+
throw new AnnonError('transport_required');
|
|
459
|
+
await this.resumeShare({
|
|
460
|
+
fileId: o.fileId,
|
|
461
|
+
data: o.data,
|
|
462
|
+
seller: o.seller,
|
|
463
|
+
hosting: o.hosting,
|
|
464
|
+
});
|
|
465
|
+
return this.hostedFile(job, o.data, o.publish);
|
|
466
|
+
}
|
|
467
|
+
if (job.published) {
|
|
468
|
+
await this.o.writeBackup(job.id, await exportSell(job, this.o.backupSecret));
|
|
469
|
+
throw new AnnonError('invalid_record', 'sell already published; secrets cleared');
|
|
470
|
+
}
|
|
471
|
+
if (!job.fileKey || !job.sellerSecret)
|
|
472
|
+
throw new AnnonError('invalid_record', 'sell job has no file key');
|
|
473
|
+
if (job.terms.contract !== this.o.contractId ||
|
|
474
|
+
identityJson(job.terms.seller) !== identityJson(o.seller.identity))
|
|
475
|
+
throw new AnnonError('wrong_identity', 'sell job identity mismatch');
|
|
476
|
+
await this.o.writeBackup(job.id, await exportSell(job, this.o.backupSecret));
|
|
477
|
+
const s = await prepareSeal(bytes(job.fileKey, 32), o.data, {
|
|
478
|
+
fileId: bytes(o.fileId, 32),
|
|
479
|
+
name: job.name,
|
|
480
|
+
mime: job.mime,
|
|
481
|
+
authKind: identityKind(job.terms.seller),
|
|
482
|
+
previewLen: job.previewLen,
|
|
483
|
+
});
|
|
484
|
+
if (hex(s.cipherHash) !== job.terms.cipher_hash)
|
|
485
|
+
throw new AnnonError('cipher_hash_mismatch', 'original data cipher hash mismatch');
|
|
486
|
+
const registered = await getFile(this.o.nearRpc, this.o.contractId, o.fileId);
|
|
487
|
+
if (registered && registered.termsHash !== termsHash(job.terms, assetsFor(job.terms)))
|
|
488
|
+
throw new AnnonError('file_mismatch', 'registered sell job mismatch');
|
|
489
|
+
try {
|
|
490
|
+
return await this.finishSell(job, s, o.seller, o.publish, registered !== null);
|
|
491
|
+
}
|
|
492
|
+
catch (cause) {
|
|
493
|
+
throw new SellError(o.fileId, { cause });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/** A new seller one-time key is backed up before the single-use reopen_seller call. */
|
|
497
|
+
async reopenSeller(o) {
|
|
498
|
+
const f = await this.file(o.fileId);
|
|
499
|
+
if (identityJson(f.seller) !== identityJson(o.seller.identity))
|
|
500
|
+
throw new AnnonError('wrong_identity', 'not the registered seller');
|
|
501
|
+
const keyName = `annon/seller-key/${o.fileId}`;
|
|
502
|
+
const previous = await this.sellerKeys.load(keyName);
|
|
503
|
+
const key = oneTimeKey();
|
|
504
|
+
const next = {
|
|
505
|
+
kind: 'seller-key',
|
|
506
|
+
id: `seller-key:${o.fileId}`,
|
|
507
|
+
file_id: o.fileId,
|
|
508
|
+
seller: f.seller,
|
|
509
|
+
revision: (previous?.revision ?? -1) + 1,
|
|
510
|
+
buyerSecret: hex(key.secret),
|
|
511
|
+
deadline: this.now() + 540,
|
|
512
|
+
};
|
|
513
|
+
if (!(await this.sellerKeys.compareAndSet(keyName, previous, next)))
|
|
514
|
+
throw new PayError('store_conflict');
|
|
515
|
+
const args = { file_id: o.fileId, key: hex(key.publicKeyBytes), deadline: next.deadline };
|
|
516
|
+
const auth = await this.auth(o.seller, reopenSellerText(this.o.contractId, args, f.seller));
|
|
517
|
+
const answer = await this.answer(await this.call('reopen_seller', { args, ...(auth ? { auth } : {}) }, o.seller), key.secret, o.fileId);
|
|
518
|
+
if (!(await this.sellerKeys.compareAndSet(keyName, next, {
|
|
519
|
+
...next,
|
|
520
|
+
revision: next.revision + 1,
|
|
521
|
+
buyerSecret: null,
|
|
522
|
+
})))
|
|
523
|
+
throw new PayError('store_conflict');
|
|
524
|
+
key.secret.fill(0);
|
|
525
|
+
return answer;
|
|
526
|
+
}
|
|
527
|
+
async finishSell(job, s, seller, publish, registered) {
|
|
528
|
+
const fileId = job.terms.file_id;
|
|
529
|
+
let ckd;
|
|
530
|
+
if (job.ckdSecret)
|
|
531
|
+
ckd = bytes(job.ckdSecret, 48);
|
|
532
|
+
else {
|
|
533
|
+
if (registered)
|
|
534
|
+
ckd = await this.reopenSeller({ fileId, seller });
|
|
535
|
+
else {
|
|
536
|
+
const key = keyFromSecret(bytes(job.sellerSecret, 32));
|
|
537
|
+
const args = {
|
|
538
|
+
terms: job.terms,
|
|
539
|
+
seller_key: hex(key.publicKeyBytes),
|
|
540
|
+
deadline: this.now() + 540,
|
|
541
|
+
};
|
|
542
|
+
const auth = await this.auth(seller, registerText(this.o.contractId, args, assetsFor(job.terms)));
|
|
543
|
+
ckd = await this.answer(await this.call('register', {
|
|
544
|
+
args,
|
|
545
|
+
...(auth ? { auth } : {}),
|
|
546
|
+
...(auth && job.baseSplit
|
|
547
|
+
? { baseSplit: { kind: job.baseSplit.kind, salt: job.baseSplit.salt } }
|
|
548
|
+
: {}),
|
|
549
|
+
}, seller), key.secret, fileId);
|
|
550
|
+
}
|
|
551
|
+
const current = await this.sells.load(`annon/sell/${fileId}`);
|
|
552
|
+
if (!current)
|
|
553
|
+
throw new AnnonError('invalid_record', 'sell job missing');
|
|
554
|
+
const next = { ...current, revision: current.revision + 1, ckdSecret: hex(ckd) };
|
|
555
|
+
if (!(await this.sells.compareAndSet(`annon/sell/${fileId}`, current, next)))
|
|
556
|
+
throw new PayError('store_conflict');
|
|
557
|
+
}
|
|
558
|
+
const { file, header, consumed, cancel } = await s.finish({
|
|
559
|
+
contractId: this.o.contractId,
|
|
560
|
+
ckdSecret: ckd,
|
|
561
|
+
seller: job.terms.seller,
|
|
562
|
+
...(seller.identity.kind === 'near'
|
|
563
|
+
? {}
|
|
564
|
+
: {
|
|
565
|
+
sign: async (text) => {
|
|
566
|
+
const a = await this.auth(seller, text);
|
|
567
|
+
if (!a)
|
|
568
|
+
throw new AnnonError('invalid_signature', 'missing header authorization');
|
|
569
|
+
const signature = bytes(a.sig, seller.identity.kind === 'evm' ? 65 : 64);
|
|
570
|
+
return seller.identity.kind === 'solana'
|
|
571
|
+
? { signature, signedMessage: new TextEncoder().encode(text) }
|
|
572
|
+
: signature;
|
|
573
|
+
},
|
|
574
|
+
}),
|
|
575
|
+
});
|
|
576
|
+
const headerLen = header.length;
|
|
577
|
+
try {
|
|
578
|
+
if (seller.identity.kind === 'near')
|
|
579
|
+
await this.call('publish_header', { file_id: fileId, header_hash: hex(await sha256(header.subarray(0, headerLen - 65))) }, seller);
|
|
580
|
+
if (job.storageBudget) {
|
|
581
|
+
const name = `annon/sell/${fileId}`;
|
|
582
|
+
let current = await this.sells.load(name);
|
|
583
|
+
if (!current)
|
|
584
|
+
throw new AnnonError('invalid_record', 'sell job missing');
|
|
585
|
+
const target = BigInt(job.storageBudget);
|
|
586
|
+
if (current.storageFunding === 'pending') {
|
|
587
|
+
// The call may have landed and its allocation may already have been spent.
|
|
588
|
+
// A low current balance cannot justify automatically allocating the amount again.
|
|
589
|
+
const budget = await fileStorageBalance(this.o.nearRpc, this.o.contractId, fileId);
|
|
590
|
+
if (budget.available < target)
|
|
591
|
+
throw new PayError('storage_required', 'Previous file funding is uncertain; inspect it and explicitly fund before resuming.');
|
|
592
|
+
}
|
|
593
|
+
else if (current.storageFunding !== 'funded') {
|
|
594
|
+
const pending = {
|
|
595
|
+
...current,
|
|
596
|
+
revision: current.revision + 1,
|
|
597
|
+
storageFunding: 'pending',
|
|
598
|
+
};
|
|
599
|
+
if (!(await this.sells.compareAndSet(name, current, pending)))
|
|
600
|
+
throw new PayError('store_conflict');
|
|
601
|
+
current = pending;
|
|
602
|
+
try {
|
|
603
|
+
await fundFileStorage(asNear(seller), this.o.contractId, fileId, target);
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
if (error instanceof FileFundingRefused &&
|
|
607
|
+
!(await this.sells.compareAndSet(name, current, {
|
|
608
|
+
...current,
|
|
609
|
+
revision: current.revision + 1,
|
|
610
|
+
storageFunding: 'refused',
|
|
611
|
+
})))
|
|
612
|
+
throw new PayError('store_conflict');
|
|
613
|
+
throw error;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (current.storageFunding !== 'funded' &&
|
|
617
|
+
!(await this.sells.compareAndSet(name, current, {
|
|
618
|
+
...current,
|
|
619
|
+
revision: current.revision + 1,
|
|
620
|
+
storageFunding: 'funded',
|
|
621
|
+
})))
|
|
622
|
+
throw new PayError('store_conflict');
|
|
623
|
+
}
|
|
624
|
+
await publish({ file, header, terms: job.terms });
|
|
625
|
+
if (!consumed())
|
|
626
|
+
throw new AnnonError('upload_incomplete', 'publish did not consume the complete file stream');
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
await cancel(error).catch(() => { });
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
const sellerKeyName = `annon/seller-key/${fileId}`;
|
|
633
|
+
const sellerKey = await this.sellerKeys.load(sellerKeyName);
|
|
634
|
+
if (sellerKey?.buyerSecret &&
|
|
635
|
+
!(await this.sellerKeys.compareAndSet(sellerKeyName, sellerKey, {
|
|
636
|
+
...sellerKey,
|
|
637
|
+
revision: sellerKey.revision + 1,
|
|
638
|
+
buyerSecret: null,
|
|
639
|
+
})))
|
|
640
|
+
throw new PayError('store_conflict');
|
|
641
|
+
const current = await this.sells.load(`annon/sell/${fileId}`);
|
|
642
|
+
if (!current ||
|
|
643
|
+
!(await this.sells.compareAndSet(`annon/sell/${fileId}`, current, {
|
|
644
|
+
...current,
|
|
645
|
+
revision: current.revision + 1,
|
|
646
|
+
published: true,
|
|
647
|
+
fileKey: null,
|
|
648
|
+
sellerSecret: null,
|
|
649
|
+
ckdSecret: null,
|
|
650
|
+
})))
|
|
651
|
+
throw new PayError('store_conflict');
|
|
652
|
+
ckd.fill(0);
|
|
653
|
+
return {
|
|
654
|
+
terms: job.terms,
|
|
655
|
+
...(job.baseSplit ? { baseSplit: job.baseSplit } : {}),
|
|
656
|
+
file,
|
|
657
|
+
header,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
async buy(o) {
|
|
661
|
+
if (typeof o.maxPrice !== 'bigint' || o.maxPrice <= 0n || o.maxPrice >= 2n ** 128n)
|
|
662
|
+
throw new AnnonError('max_price_required', 'maxPrice must be a positive human-approved cap');
|
|
663
|
+
const f = await this.file(o.fileId, true);
|
|
664
|
+
if (f.moderator !== null && !(o.moderators ?? []).includes(f.moderator))
|
|
665
|
+
throw new PayError('unknown_moderator');
|
|
666
|
+
const checked = await inspectHeader(o.header, listing(o.fileId, f), this.o.contractId);
|
|
667
|
+
if (!checked.usable)
|
|
668
|
+
throw new PayError('file_invalid');
|
|
669
|
+
if (o.route !== undefined && !['near', 'base', '1click'].includes(o.route))
|
|
670
|
+
throw new AnnonError('invalid_arguments', 'invalid purchase route');
|
|
671
|
+
if (o.route === 'base' && !f.terms.base)
|
|
672
|
+
throw new AnnonError('route_unavailable');
|
|
673
|
+
if (o.route === 'near' && o.payer?.identity.kind !== 'near')
|
|
674
|
+
throw new AnnonError('near_wallet_required', 'direct NEAR requires a NEAR payment wallet');
|
|
675
|
+
if (o.route === '1click' && !o.origin)
|
|
676
|
+
throw new AnnonError('origin_required', 'a 1Click origin is required');
|
|
677
|
+
if (o.route === 'base' && (o.payer?.identity.kind !== 'evm' || !('signTypedData' in o.payer)))
|
|
678
|
+
throw new AnnonError('base_wallet_required', 'Base requires Base terms and an EVM payment wallet');
|
|
679
|
+
const oneclick = o.route === '1click' || (o.route === undefined && o.payer?.identity.kind !== 'near');
|
|
680
|
+
const store = {
|
|
681
|
+
load: (name) => this.store.load(name),
|
|
682
|
+
compareAndSet: async (name, expected, next) => {
|
|
683
|
+
if (next)
|
|
684
|
+
await o.beforeSave?.(next);
|
|
685
|
+
const ok = await this.store.compareAndSet(name, expected, next);
|
|
686
|
+
if (ok && next)
|
|
687
|
+
await o.onSaved?.(next);
|
|
688
|
+
return ok;
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
let wireRoute = o.route === 'base' ? 'base-usdc' : 'near-usdc';
|
|
692
|
+
if (o.route === undefined &&
|
|
693
|
+
o.payer?.identity.kind === 'evm' &&
|
|
694
|
+
f.terms.base &&
|
|
695
|
+
'signTypedData' in o.payer) {
|
|
696
|
+
const h = await head(this.o.baseRpc);
|
|
697
|
+
if (!(await hasCode(this.o.baseRpc, o.payer.identity.id, h.number)) &&
|
|
698
|
+
(await balanceOf(this.o.baseRpc, f.terms.base.usdc, o.payer.identity.id, h.number)) >=
|
|
699
|
+
BigInt(f.terms.base.price))
|
|
700
|
+
wireRoute = 'base-usdc';
|
|
701
|
+
}
|
|
702
|
+
const route = wireRoute === 'base-usdc' ? 'base' : oneclick ? '1click' : 'near';
|
|
703
|
+
if (wireRoute === 'near-usdc' && oneclick && o.origin)
|
|
704
|
+
validateOrigin(o.origin);
|
|
705
|
+
const price = wireRoute === 'base-usdc' && f.terms.base ? f.terms.base.price : f.terms.near.price;
|
|
706
|
+
if (BigInt(price) > o.maxPrice)
|
|
707
|
+
throw new PayError('price_above_max');
|
|
708
|
+
let ensureBase = async () => { };
|
|
709
|
+
if (wireRoute === 'base-usdc') {
|
|
710
|
+
if (o.baseUnlock !== undefined && !['relayer', 'self'].includes(o.baseUnlock))
|
|
711
|
+
throw new AnnonError('invalid_arguments', 'invalid Base unlock transport');
|
|
712
|
+
if (o.baseUnlock === 'self') {
|
|
713
|
+
asNear(o.recovery);
|
|
714
|
+
ensureBase = async () => checkBaseSplit(f, this.o.baseRpc);
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
if (!this.o.relayer)
|
|
718
|
+
throw new AnnonError('transport_required');
|
|
719
|
+
const config = await getConfig(this.o.nearRpc, this.o.contractId);
|
|
720
|
+
const options = {
|
|
721
|
+
deployment: {
|
|
722
|
+
network: this.o.network,
|
|
723
|
+
contractId: this.o.contractId,
|
|
724
|
+
nearUsdc: config.nearUsdc,
|
|
725
|
+
baseUsdc: config.usdc,
|
|
726
|
+
},
|
|
727
|
+
file: f,
|
|
728
|
+
baseRpc: this.o.baseRpc,
|
|
729
|
+
relayer: this.o.relayer,
|
|
730
|
+
...(this.o.fetch ? { fetch: this.o.fetch } : {}),
|
|
731
|
+
now: () => this.now(),
|
|
732
|
+
};
|
|
733
|
+
let checked = o.baseSponsorship;
|
|
734
|
+
ensureBase = async () => {
|
|
735
|
+
if (!validBaseSponsorship(checked, options))
|
|
736
|
+
checked = await checkBaseSponsorship(options);
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
await ensureBase();
|
|
741
|
+
if (wireRoute === 'near-usdc') {
|
|
742
|
+
const budget = await fileStorageBalance(this.o.nearRpc, this.o.contractId, o.fileId);
|
|
743
|
+
if (budget.available < budget.admissionRequirement)
|
|
744
|
+
throw new PayError('file_storage_low');
|
|
745
|
+
}
|
|
746
|
+
if (wireRoute === 'near-usdc' && o.payer?.identity.kind !== 'near' && !o.origin)
|
|
747
|
+
throw new AnnonError('origin_required', 'a 1Click origin is required for cross-chain payment');
|
|
748
|
+
const recovery = parseIdentity(o.recovery.identity);
|
|
749
|
+
const key = oneTimeKey();
|
|
750
|
+
const p = {
|
|
751
|
+
version: 2,
|
|
752
|
+
refund: parseIdentity(o.refund),
|
|
753
|
+
network: wireRoute === 'base-usdc' ? 'eip155:8453' : this.o.network,
|
|
754
|
+
contract: this.o.contractId,
|
|
755
|
+
route: wireRoute,
|
|
756
|
+
asset: wireRoute === 'base-usdc' && f.terms.base ? f.terms.base.usdc : f.terms.near.token,
|
|
757
|
+
file_id: o.fileId,
|
|
758
|
+
terms_hash: f.termsHash,
|
|
759
|
+
price,
|
|
760
|
+
nonce: `0x${randomHex(32)}`,
|
|
761
|
+
buyer_key: hex(key.publicKeyBytes),
|
|
762
|
+
recovery,
|
|
763
|
+
expires_at: String(this.now() + 3600),
|
|
764
|
+
};
|
|
765
|
+
const assets = {
|
|
766
|
+
nearUsdc: f.terms.near.token,
|
|
767
|
+
...(f.terms.base ? { baseUsdc: f.terms.base.usdc } : {}),
|
|
768
|
+
};
|
|
769
|
+
const id = purchaseId(p, assets);
|
|
770
|
+
const message = buildPurchaseMessage(p, assets);
|
|
771
|
+
parsePurchaseMessage(message, assets);
|
|
772
|
+
const proofText = `Annon: verify purchase recovery\ncontract: ${this.o.contractId}\npurchase: ${id}\nrecovery: ${identityText(recovery)}`;
|
|
773
|
+
const proveRecovery = async () => {
|
|
774
|
+
if (recovery.kind === 'near') {
|
|
775
|
+
const w = asNear(o.recovery);
|
|
776
|
+
if (!w.proveControl)
|
|
777
|
+
throw new AnnonError('missing_method', 'NEAR recovery requires an account ownership proof before payment');
|
|
778
|
+
await w.proveControl(proofText, { contractId: this.o.contractId, nearRpc: this.o.nearRpc });
|
|
779
|
+
}
|
|
780
|
+
else
|
|
781
|
+
await this.auth(o.recovery, proofText);
|
|
782
|
+
};
|
|
783
|
+
const deferProof = wireRoute === 'near-usdc' && oneclick && o.onQuote;
|
|
784
|
+
if (!deferProof)
|
|
785
|
+
await proveRecovery();
|
|
786
|
+
if (wireRoute === 'base-usdc') {
|
|
787
|
+
if (!o.paymentRequired || !o.url || !o.payer || !('signTypedData' in o.payer))
|
|
788
|
+
throw new TypeError('Base requires paymentRequired, resource URL and an EVM payment wallet');
|
|
789
|
+
const payer = o.payer;
|
|
790
|
+
const q = await preflight({
|
|
791
|
+
nearRpc: this.o.nearRpc,
|
|
792
|
+
baseRpc: this.o.baseRpc,
|
|
793
|
+
contractId: this.o.contractId,
|
|
794
|
+
fileId: o.fileId,
|
|
795
|
+
buyer: payer.identity.id,
|
|
796
|
+
paymentRequired: o.paymentRequired,
|
|
797
|
+
url: o.url,
|
|
798
|
+
pay2: o.header,
|
|
799
|
+
maxPrice: o.maxPrice,
|
|
800
|
+
...(o.moderators ? { moderators: o.moderators } : {}),
|
|
801
|
+
});
|
|
802
|
+
await ensureBase();
|
|
803
|
+
const result = await authorize(q, {
|
|
804
|
+
purchase: p,
|
|
805
|
+
buyerSecret: key.secret,
|
|
806
|
+
wallet: { address: payer.identity.id, signTypedData: (t) => payer.signTypedData(t) },
|
|
807
|
+
store,
|
|
808
|
+
});
|
|
809
|
+
const proved = {
|
|
810
|
+
...result.purchase,
|
|
811
|
+
revision: result.purchase.revision + 1,
|
|
812
|
+
recoveryProved: true,
|
|
813
|
+
};
|
|
814
|
+
if (!(await store.compareAndSet(purchaseKey(id), result.purchase, proved)))
|
|
815
|
+
throw new PayError('store_conflict');
|
|
816
|
+
await o.beforePayment?.(proved);
|
|
817
|
+
if (o.baseUnlock === 'self')
|
|
818
|
+
this.selfUnlockWallets.set(id, asNear(o.recovery));
|
|
819
|
+
if (o.submitBase)
|
|
820
|
+
await o.submitBase(result.header);
|
|
821
|
+
return { route, record: proved, payment: result.header };
|
|
822
|
+
}
|
|
823
|
+
const headerHash = hex(checked.headerHash);
|
|
824
|
+
let record = {
|
|
825
|
+
schemaVersion: 2,
|
|
826
|
+
payment: {
|
|
827
|
+
route,
|
|
828
|
+
amountUsdc: usdc(BigInt(price)),
|
|
829
|
+
amountBaseUnits: price,
|
|
830
|
+
asset: { symbol: 'USDC', id: p.asset },
|
|
831
|
+
network: p.network,
|
|
832
|
+
},
|
|
833
|
+
recoveryProved: !deferProof,
|
|
834
|
+
id,
|
|
835
|
+
revision: 0,
|
|
836
|
+
purchase: p,
|
|
837
|
+
message,
|
|
838
|
+
buyerSecret: hex(key.secret),
|
|
839
|
+
quote: null,
|
|
840
|
+
recovery,
|
|
841
|
+
checkedHeaderHash: headerHash,
|
|
842
|
+
terms: {
|
|
843
|
+
network: p.network,
|
|
844
|
+
contractId: p.contract,
|
|
845
|
+
fileId: p.file_id,
|
|
846
|
+
buyer: o.payer?.identity.id ?? recovery.id,
|
|
847
|
+
seller: f.seller,
|
|
848
|
+
to: p.contract,
|
|
849
|
+
usdc: p.asset,
|
|
850
|
+
price: p.price,
|
|
851
|
+
buyerKey: p.buyer_key,
|
|
852
|
+
nonce: p.nonce,
|
|
853
|
+
headerHash,
|
|
854
|
+
cipherHash: f.cipherHash,
|
|
855
|
+
moderator: f.moderator,
|
|
856
|
+
},
|
|
857
|
+
attempts: [],
|
|
858
|
+
};
|
|
859
|
+
if (!(await store.compareAndSet(purchaseKey(id), null, record)))
|
|
860
|
+
throw new PayError('store_conflict');
|
|
861
|
+
if (!oneclick && o.payer?.identity.kind === 'near') {
|
|
862
|
+
await o.beforePayment?.(record);
|
|
863
|
+
try {
|
|
864
|
+
await asNear(o.payer).transferPurchase(message, assets);
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
if (!(error instanceof TransactionUncertain))
|
|
868
|
+
throw error;
|
|
869
|
+
const next = {
|
|
870
|
+
...record,
|
|
871
|
+
revision: record.revision + 1,
|
|
872
|
+
nearSubmission: { state: 'uncertain', txHash: error.txHash },
|
|
873
|
+
};
|
|
874
|
+
if (!(await store.compareAndSet(purchaseKey(id), record, next)))
|
|
875
|
+
throw new TransactionUncertain(error.txHash, id);
|
|
876
|
+
return { route, record: next, uncertain: { purchaseId: id, txHash: error.txHash } };
|
|
877
|
+
}
|
|
878
|
+
return { route, record };
|
|
879
|
+
}
|
|
880
|
+
if (!o.origin)
|
|
881
|
+
throw new AnnonError('origin_required', 'a 1Click origin is required for cross-chain payment');
|
|
882
|
+
const quote = await (o.oneClick ?? new OneClick()).quote(message, o.origin);
|
|
883
|
+
const next = { ...record, revision: record.revision + 1, quote };
|
|
884
|
+
if (!(await store.compareAndSet(purchaseKey(id), record, next)))
|
|
885
|
+
throw new PayError('store_conflict');
|
|
886
|
+
record = next;
|
|
887
|
+
if (o.onQuote) {
|
|
888
|
+
await o.onQuote(quote);
|
|
889
|
+
const checkDeadline = () => {
|
|
890
|
+
const cutoff = Date.parse(quote.deposit.depositBefore);
|
|
891
|
+
if (!Number.isFinite(cutoff) || cutoff - this.now() * 1000 < 600000)
|
|
892
|
+
throw new OneClickQuoteExpired();
|
|
893
|
+
};
|
|
894
|
+
checkDeadline();
|
|
895
|
+
await proveRecovery();
|
|
896
|
+
checkDeadline();
|
|
897
|
+
}
|
|
898
|
+
if (!record.recoveryProved) {
|
|
899
|
+
const proved = { ...record, revision: record.revision + 1, recoveryProved: true };
|
|
900
|
+
if (!(await store.compareAndSet(purchaseKey(id), record, proved)))
|
|
901
|
+
throw new PayError('store_conflict');
|
|
902
|
+
record = proved;
|
|
903
|
+
}
|
|
904
|
+
await o.beforePayment?.(record);
|
|
905
|
+
return { route, record, instructions: quote.deposit };
|
|
906
|
+
}
|
|
907
|
+
/** Reprove the same saved purchase after reload, without preparing or submitting a payment. */
|
|
908
|
+
async proveRecovery(record, wallet) {
|
|
909
|
+
const current = await this.store.load(purchaseKey(record.id));
|
|
910
|
+
if (!current || current.revision !== record.revision)
|
|
911
|
+
throw new PayError('store_conflict');
|
|
912
|
+
if (current.purchase.contract !== this.o.contractId ||
|
|
913
|
+
current.purchase.network !==
|
|
914
|
+
(current.purchase.route === 'base-usdc' ? 'eip155:8453' : this.o.network))
|
|
915
|
+
throw new AnnonError('network_mismatch', 'saved purchase is outside this SDK scope');
|
|
916
|
+
if (identityJson(parseIdentity(wallet.identity)) !== identityJson(current.recovery))
|
|
917
|
+
throw new AnnonError('wrong_identity', 'purchase recovery identity mismatch');
|
|
918
|
+
const text = `Annon: verify purchase recovery\ncontract: ${this.o.contractId}\npurchase: ${current.id}\nrecovery: ${identityText(current.recovery)}`;
|
|
919
|
+
if (current.recovery.kind === 'near') {
|
|
920
|
+
const w = asNear(wallet);
|
|
921
|
+
if (!w.proveControl)
|
|
922
|
+
throw new AnnonError('missing_method', 'NEAR recovery requires an account ownership proof before payment');
|
|
923
|
+
await w.proveControl(text, { contractId: this.o.contractId, nearRpc: this.o.nearRpc });
|
|
924
|
+
}
|
|
925
|
+
else
|
|
926
|
+
await this.auth(wallet, text);
|
|
927
|
+
const next = { ...current, revision: current.revision + 1, recoveryProved: true };
|
|
928
|
+
if (!(await this.store.compareAndSet(purchaseKey(current.id), current, next)))
|
|
929
|
+
throw new PayError('store_conflict');
|
|
930
|
+
return next;
|
|
931
|
+
}
|
|
932
|
+
/** Resume a paid Base purchase before any host download, which requires a final grant. */
|
|
933
|
+
async accept(record, id = record?.id, options = {}) {
|
|
934
|
+
if (record)
|
|
935
|
+
validatePurchaseRecord(record);
|
|
936
|
+
if (!id)
|
|
937
|
+
throw new AnnonError('purchase_required', 'missing purchase id');
|
|
938
|
+
if (await getPurchase(this.o.nearRpc, this.o.contractId, id))
|
|
939
|
+
return true;
|
|
940
|
+
if (record?.purchase.route !== 'base-usdc')
|
|
941
|
+
return false;
|
|
942
|
+
if (!this.o.baseRpc)
|
|
943
|
+
throw new AnnonError('base_rpc_required', 'configure baseRpc to resume a Base purchase');
|
|
944
|
+
const next = await nextStep(record, {
|
|
945
|
+
nearRpc: this.o.nearRpc,
|
|
946
|
+
baseRpc: this.o.baseRpc,
|
|
947
|
+
store: this.store,
|
|
948
|
+
});
|
|
949
|
+
if (next.step === 'unlock') {
|
|
950
|
+
const answer = await this.call('unlock', { args: next.args }, options.unlockWallet ?? this.selfUnlockWallets.get(id));
|
|
951
|
+
this.unlocked.set(id, answer);
|
|
952
|
+
// Bound answers retained by status-only callers; evicted entries use claim_key.
|
|
953
|
+
if (this.unlocked.size > 64) {
|
|
954
|
+
const oldest = this.unlocked.keys().next().value;
|
|
955
|
+
if (oldest !== undefined)
|
|
956
|
+
this.unlocked.delete(oldest);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return (await getPurchase(this.o.nearRpc, this.o.contractId, id)) !== null;
|
|
960
|
+
}
|
|
961
|
+
async open(o) {
|
|
962
|
+
return this.openSource(o);
|
|
963
|
+
}
|
|
964
|
+
/** Replay PAY2 to a provisional sink; resolve after every check. Single-use streams require a ciphertext spool. */
|
|
965
|
+
async openTo(o) {
|
|
966
|
+
return withReplay(o.file, (file) => this.openSource({ ...o, file }), o.spool);
|
|
967
|
+
}
|
|
968
|
+
async openSource(o) {
|
|
969
|
+
if ((o.file instanceof Uint8Array ? o.file.length : o.file.size) > MAX_PAY2_BYTES)
|
|
970
|
+
throw new DownloadError('too_large');
|
|
971
|
+
if (!o.record && !o.recovery)
|
|
972
|
+
throw new AnnonError('purchase_required', 'a saved purchase or recovery grant is required');
|
|
973
|
+
const r = o.recovery;
|
|
974
|
+
if (r && r.grant === null)
|
|
975
|
+
throw new AnnonError('recovery_not_final', 'recovery grant is not final');
|
|
976
|
+
const id = r?.purchase_id ?? o.record?.id;
|
|
977
|
+
if (!id)
|
|
978
|
+
throw new AnnonError('purchase_required', 'missing purchase id');
|
|
979
|
+
if (!(await this.accept(o.record, id)))
|
|
980
|
+
throw new PayError('settlement_pending', 'purchase not accepted; check status and resume');
|
|
981
|
+
const fileId = r?.file_id ?? o.record?.purchase.file_id;
|
|
982
|
+
if (!fileId)
|
|
983
|
+
throw new AnnonError('file_required', 'missing file id');
|
|
984
|
+
const grant = r?.grant ?? 0;
|
|
985
|
+
const secretHex = r?.buyerSecret ?? o.record?.buyerSecret;
|
|
986
|
+
if (!secretHex)
|
|
987
|
+
throw new AnnonError('invalid_record', 'missing buyer secret');
|
|
988
|
+
const secret = bytes(secretHex, 32);
|
|
989
|
+
const final = await getPurchase(this.o.nearRpc, this.o.contractId, id);
|
|
990
|
+
const g = await getGrant(this.o.nearRpc, this.o.contractId, id, grant);
|
|
991
|
+
if (!final ||
|
|
992
|
+
final.file_id !== fileId ||
|
|
993
|
+
!g ||
|
|
994
|
+
hex(keyFromSecret(secret).publicKeyBytes) !== g.buyer_key)
|
|
995
|
+
throw new AnnonError('invalid_record', 'saved key does not match the final grant');
|
|
996
|
+
const f = await this.file(fileId);
|
|
997
|
+
const prepared = o.file instanceof Uint8Array
|
|
998
|
+
? undefined
|
|
999
|
+
: await prepareOpen(o.file, {
|
|
1000
|
+
listing: listing(fileId, f),
|
|
1001
|
+
contractId: this.o.contractId,
|
|
1002
|
+
});
|
|
1003
|
+
const inspected = await inspectHeader(prepared
|
|
1004
|
+
? prepared.header
|
|
1005
|
+
: o.file.subarray(0, new DataView(o.file.buffer, o.file.byteOffset).getUint32(8)), listing(fileId, f), this.o.contractId);
|
|
1006
|
+
if (o.record && hex(inspected.headerHash) !== o.record.checkedHeaderHash)
|
|
1007
|
+
throw new AnnonError('file_mismatch', 'header differs from the checked purchase header');
|
|
1008
|
+
const cached = grant === 0 ? this.unlocked.get(id) : undefined;
|
|
1009
|
+
if (grant === 0)
|
|
1010
|
+
this.unlocked.delete(id);
|
|
1011
|
+
let ckd;
|
|
1012
|
+
if (cached) {
|
|
1013
|
+
try {
|
|
1014
|
+
ckd = await this.answer(cached, secret, fileId);
|
|
1015
|
+
}
|
|
1016
|
+
catch {
|
|
1017
|
+
/* A missing or invalid unlock answer falls back to the usual verified claim. */
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (!ckd)
|
|
1021
|
+
ckd = await this.answer(await this.call('claim_key', { purchase_id: id, grant }), secret, fileId);
|
|
1022
|
+
if (prepared) {
|
|
1023
|
+
if (!o.sink)
|
|
1024
|
+
throw new AnnonError('invalid_arguments', 'a plaintext sink is required');
|
|
1025
|
+
const meta = await prepared.writeTo(o.sink, async () => ckd);
|
|
1026
|
+
if (meta.size > MAX_PLAINTEXT_BYTES)
|
|
1027
|
+
throw new DownloadError('too_large');
|
|
1028
|
+
return { meta };
|
|
1029
|
+
}
|
|
1030
|
+
const opened = await openPay2(o.file, {
|
|
1031
|
+
contractId: this.o.contractId,
|
|
1032
|
+
listing: listing(fileId, f),
|
|
1033
|
+
ckdSecret: async () => ckd,
|
|
1034
|
+
});
|
|
1035
|
+
if (opened.meta.size > MAX_PLAINTEXT_BYTES)
|
|
1036
|
+
throw new DownloadError('too_large');
|
|
1037
|
+
return opened;
|
|
1038
|
+
}
|
|
1039
|
+
async recover(o) {
|
|
1040
|
+
const recovery = parseIdentity(o.wallet.identity);
|
|
1041
|
+
const ids = [];
|
|
1042
|
+
if (o.purchaseId)
|
|
1043
|
+
ids.push(o.purchaseId);
|
|
1044
|
+
else {
|
|
1045
|
+
const limit = o.limit ?? 50;
|
|
1046
|
+
for (let from = 0;; from += limit) {
|
|
1047
|
+
const page = await purchasesOf(this.o.nearRpc, this.o.contractId, recovery, from, limit);
|
|
1048
|
+
ids.push(...page);
|
|
1049
|
+
if (page.length < limit)
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const recovered = [];
|
|
1054
|
+
for (const id of ids) {
|
|
1055
|
+
try {
|
|
1056
|
+
const p = await getPurchase(this.o.nearRpc, this.o.contractId, id);
|
|
1057
|
+
if (!p || identityJson(p.recovery) !== identityJson(recovery))
|
|
1058
|
+
throw new AnnonError('wrong_identity', 'purchase recovery identity mismatch');
|
|
1059
|
+
await this.file(p.file_id);
|
|
1060
|
+
const keyName = `annon/recovery/${id}`;
|
|
1061
|
+
let r = await this.recoveries.load(keyName);
|
|
1062
|
+
if (r?.grant !== null && r?.grant !== undefined) {
|
|
1063
|
+
recovered.push(r);
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
if (!r) {
|
|
1067
|
+
const key = oneTimeKey();
|
|
1068
|
+
r = {
|
|
1069
|
+
kind: 'recovery',
|
|
1070
|
+
id: `recovery:${id}`,
|
|
1071
|
+
revision: 0,
|
|
1072
|
+
purchase_id: id,
|
|
1073
|
+
file_id: p.file_id,
|
|
1074
|
+
recovery,
|
|
1075
|
+
buyerSecret: hex(key.secret),
|
|
1076
|
+
fresh_key: hex(key.publicKeyBytes),
|
|
1077
|
+
nonce: `0x${randomHex(32)}`,
|
|
1078
|
+
deadline: this.now() + 540,
|
|
1079
|
+
grant: null,
|
|
1080
|
+
};
|
|
1081
|
+
if (!(await this.recoveries.compareAndSet(keyName, null, r)))
|
|
1082
|
+
throw new PayError('store_conflict');
|
|
1083
|
+
}
|
|
1084
|
+
let current = r;
|
|
1085
|
+
const findGrant = async (count) => {
|
|
1086
|
+
for (let i = 1; i < count; i++) {
|
|
1087
|
+
const g = await getGrant(this.o.nearRpc, this.o.contractId, id, i);
|
|
1088
|
+
if (g?.buyer_key === current.fresh_key)
|
|
1089
|
+
return i;
|
|
1090
|
+
}
|
|
1091
|
+
return null;
|
|
1092
|
+
};
|
|
1093
|
+
// A previously accepted grant needs no new transaction, even if its request deadline passed.
|
|
1094
|
+
let grant = await findGrant(p.grants);
|
|
1095
|
+
let result;
|
|
1096
|
+
if (grant === null) {
|
|
1097
|
+
if (r.deadline <= this.now()) {
|
|
1098
|
+
// Preserve the old key before replacing it; its request may have landed near expiry.
|
|
1099
|
+
await this.o.writeBackup(`${r.id}:revision:${r.revision}`, await exportRecovery(r, this.o.backupSecret));
|
|
1100
|
+
const key = oneTimeKey();
|
|
1101
|
+
const next = {
|
|
1102
|
+
...r,
|
|
1103
|
+
revision: r.revision + 1,
|
|
1104
|
+
buyerSecret: hex(key.secret),
|
|
1105
|
+
fresh_key: hex(key.publicKeyBytes),
|
|
1106
|
+
nonce: `0x${randomHex(32)}`,
|
|
1107
|
+
deadline: this.now() + 540,
|
|
1108
|
+
};
|
|
1109
|
+
if (!(await this.recoveries.compareAndSet(keyName, r, next)))
|
|
1110
|
+
throw new PayError('store_conflict');
|
|
1111
|
+
r = next;
|
|
1112
|
+
current = next;
|
|
1113
|
+
}
|
|
1114
|
+
await this.o.writeBackup(r.id, await exportRecovery(r, this.o.backupSecret));
|
|
1115
|
+
const args = {
|
|
1116
|
+
purchase_id: id,
|
|
1117
|
+
fresh_key: r.fresh_key,
|
|
1118
|
+
nonce: r.nonce,
|
|
1119
|
+
deadline: r.deadline,
|
|
1120
|
+
};
|
|
1121
|
+
const auth = await this.auth(o.wallet, reopenText({ ...args, contract: this.o.contractId, file_id: p.file_id, recovery }));
|
|
1122
|
+
result = await this.call('reopen', { args, ...(auth ? { auth } : {}) }, o.wallet);
|
|
1123
|
+
const final = await getPurchase(this.o.nearRpc, this.o.contractId, id);
|
|
1124
|
+
grant = await findGrant(final?.grants ?? p.grants);
|
|
1125
|
+
}
|
|
1126
|
+
if (grant === null)
|
|
1127
|
+
throw new AnnonError('recovery_not_final', 'reopen grant is not final; resume the saved recovery');
|
|
1128
|
+
// Validate any returned CKD answer; claim_key is repeatable if it was absent/failed.
|
|
1129
|
+
if (result && typeof result === 'object' && ('big_y' in result || 'answer' in result))
|
|
1130
|
+
await this.answer(result, bytes(r.buyerSecret, 32), p.file_id);
|
|
1131
|
+
const next = { ...r, revision: r.revision + 1, grant };
|
|
1132
|
+
if (!(await this.recoveries.compareAndSet(keyName, r, next)))
|
|
1133
|
+
throw new PayError('store_conflict');
|
|
1134
|
+
recovered.push(next);
|
|
1135
|
+
}
|
|
1136
|
+
catch (error) {
|
|
1137
|
+
if (o.purchaseId)
|
|
1138
|
+
throw error;
|
|
1139
|
+
o.onError?.(id, error);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return recovered;
|
|
1143
|
+
}
|
|
1144
|
+
async stop(o) {
|
|
1145
|
+
const f = await this.file(o.fileId);
|
|
1146
|
+
if (identityJson(f.seller) !== identityJson(o.seller.identity))
|
|
1147
|
+
throw new AnnonError('wrong_identity', 'not the registered seller');
|
|
1148
|
+
const args = { file_id: o.fileId, key: null, deadline: this.now() + 540 };
|
|
1149
|
+
const auth = await this.auth(o.seller, stopSalesText(this.o.contractId, args, f.seller));
|
|
1150
|
+
await this.call('stop_sales', { args, ...(auth ? { auth } : {}) }, o.seller);
|
|
1151
|
+
}
|
|
1152
|
+
}
|