@defdone/annon 0.0.0-stage → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/CLI.md +378 -0
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +581 -2
- package/dist/api/annon-types.d.ts +108 -0
- package/dist/api/annon-types.js +2 -0
- package/dist/api/annon.d.ts +117 -0
- package/dist/api/annon.js +1152 -0
- package/dist/api/hosted-record.d.ts +13 -0
- package/dist/api/hosted-record.js +149 -0
- package/dist/api/sharing-types.d.ts +109 -0
- package/dist/api/sharing-types.js +1 -0
- package/dist/api/sharing.d.ts +36 -0
- package/dist/api/sharing.js +879 -0
- package/dist/browser/download.d.ts +19 -0
- package/dist/browser/download.js +52 -0
- package/dist/browser/index.d.ts +23 -0
- package/dist/browser/index.js +22 -0
- package/dist/browser/login-bundle.js +116 -0
- package/dist/browser/login.d.ts +1 -0
- package/dist/browser/login.js +177 -0
- package/dist/browser/purchase-bundle.js +84 -0
- package/dist/browser/purchases.d.ts +98 -0
- package/dist/browser/purchases.js +735 -0
- package/dist/browser/record.d.ts +55 -0
- package/dist/browser/record.js +16 -0
- package/dist/browser/selling.d.ts +146 -0
- package/dist/browser/selling.js +418 -0
- package/dist/browser/wallets.d.ts +53 -0
- package/dist/browser/wallets.js +286 -0
- package/dist/bytes.d.ts +4 -0
- package/dist/bytes.js +16 -0
- package/dist/download/download.d.ts +59 -0
- package/dist/download/download.js +384 -0
- package/dist/download/host.d.ts +56 -0
- package/dist/download/host.js +161 -0
- package/dist/download/http.d.ts +23 -0
- package/dist/download/http.js +87 -0
- package/dist/download/proof.d.ts +20 -0
- package/dist/download/proof.js +79 -0
- package/dist/download/upload.d.ts +130 -0
- package/dist/download/upload.js +408 -0
- package/dist/envelope/evm.d.ts +4 -0
- package/dist/envelope/evm.js +32 -0
- package/dist/envelope/pay2.d.ts +176 -0
- package/dist/envelope/pay2.js +541 -0
- package/dist/envelope/replay.d.ts +45 -0
- package/dist/envelope/replay.js +186 -0
- package/dist/envelope/stream.d.ts +78 -0
- package/dist/envelope/stream.js +372 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +300 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +23 -0
- package/dist/keys/near-ckd.d.ts +47 -0
- package/dist/keys/near-ckd.js +96 -0
- package/dist/limits.d.ts +4 -0
- package/dist/limits.js +4 -0
- package/dist/near/nep413.d.ts +22 -0
- package/dist/near/nep413.js +39 -0
- package/dist/near/rpc.d.ts +170 -0
- package/dist/near/rpc.js +423 -0
- package/dist/near/texts.d.ts +39 -0
- package/dist/near/texts.js +60 -0
- package/dist/net/ip.d.ts +7 -0
- package/dist/net/ip.js +62 -0
- package/dist/net/retry.d.ts +3 -0
- package/dist/net/retry.js +31 -0
- package/dist/node/adapter-checks.d.ts +6 -0
- package/dist/node/adapter-checks.js +13 -0
- package/dist/node/adapter.d.ts +8 -0
- package/dist/node/adapter.js +99 -0
- package/dist/node/agent.d.ts +49 -0
- package/dist/node/agent.js +266 -0
- package/dist/node/amounts.d.ts +2 -0
- package/dist/node/amounts.js +20 -0
- package/dist/node/browser-login.d.ts +18 -0
- package/dist/node/browser-login.js +212 -0
- package/dist/node/budget.d.ts +24 -0
- package/dist/node/budget.js +59 -0
- package/dist/node/buy-commands.d.ts +24 -0
- package/dist/node/buy-commands.js +301 -0
- package/dist/node/cli.d.ts +60 -0
- package/dist/node/cli.js +201 -0
- package/dist/node/command-context.d.ts +82 -0
- package/dist/node/command-context.js +77 -0
- package/dist/node/config.d.ts +37 -0
- package/dist/node/config.js +189 -0
- package/dist/node/core-adapter.d.ts +43 -0
- package/dist/node/core-adapter.js +122 -0
- package/dist/node/errors.d.ts +8 -0
- package/dist/node/errors.js +12 -0
- package/dist/node/index.d.ts +7 -0
- package/dist/node/index.js +5 -0
- package/dist/node/io.d.ts +13 -0
- package/dist/node/io.js +112 -0
- package/dist/node/keychain.d.ts +10 -0
- package/dist/node/keychain.js +67 -0
- package/dist/node/main.d.ts +2 -0
- package/dist/node/main.js +21 -0
- package/dist/node/offers.d.ts +16 -0
- package/dist/node/offers.js +91 -0
- package/dist/node/origin.d.ts +11 -0
- package/dist/node/origin.js +80 -0
- package/dist/node/paths.d.ts +3 -0
- package/dist/node/paths.js +71 -0
- package/dist/node/payment-terms.d.ts +26 -0
- package/dist/node/payment-terms.js +127 -0
- package/dist/node/purchase-commands.d.ts +16 -0
- package/dist/node/purchase-commands.js +237 -0
- package/dist/node/purchase-guard.d.ts +19 -0
- package/dist/node/purchase-guard.js +53 -0
- package/dist/node/relay-transport.d.ts +6 -0
- package/dist/node/relay-transport.js +173 -0
- package/dist/node/resources.d.ts +2 -0
- package/dist/node/resources.js +26 -0
- package/dist/node/retry-gate.d.ts +15 -0
- package/dist/node/retry-gate.js +53 -0
- package/dist/node/seller-commands.d.ts +12 -0
- package/dist/node/seller-commands.js +207 -0
- package/dist/node/sharing.d.ts +3 -0
- package/dist/node/sharing.js +28 -0
- package/dist/node/shops.d.ts +17 -0
- package/dist/node/shops.js +74 -0
- package/dist/node/signer-mock.d.ts +11 -0
- package/dist/node/signer-mock.js +79 -0
- package/dist/node/signer-wallet.d.ts +8 -0
- package/dist/node/signer-wallet.js +147 -0
- package/dist/node/signing.d.ts +29 -0
- package/dist/node/signing.js +200 -0
- package/dist/node/storage-commands.d.ts +11 -0
- package/dist/node/storage-commands.js +50 -0
- package/dist/node/sync-dir.d.ts +1 -0
- package/dist/node/sync-dir.js +11 -0
- package/dist/node/wallets.d.ts +13 -0
- package/dist/node/wallets.js +146 -0
- package/dist/oneclick/oneclick.d.ts +132 -0
- package/dist/oneclick/oneclick.js +342 -0
- package/dist/pay/backup.d.ts +70 -0
- package/dist/pay/backup.js +402 -0
- package/dist/pay/base.d.ts +135 -0
- package/dist/pay/base.js +393 -0
- package/dist/pay/eip3009.d.ts +59 -0
- package/dist/pay/eip3009.js +72 -0
- package/dist/pay/pay.d.ts +86 -0
- package/dist/pay/pay.js +297 -0
- package/dist/pay/purchase.d.ts +103 -0
- package/dist/pay/purchase.js +20 -0
- package/dist/pay/recover.d.ts +31 -0
- package/dist/pay/recover.js +34 -0
- package/dist/pay/seller.d.ts +130 -0
- package/dist/pay/seller.js +444 -0
- package/dist/pay/settle.d.ts +96 -0
- package/dist/pay/settle.js +308 -0
- package/dist/pay/split.d.ts +115 -0
- package/dist/pay/split.js +256 -0
- package/dist/pay/store-node.d.ts +8 -0
- package/dist/pay/store-node.js +224 -0
- package/dist/pay/store.d.ts +54 -0
- package/dist/pay/store.js +135 -0
- package/dist/pay/x402.d.ts +95 -0
- package/dist/pay/x402.js +136 -0
- package/dist/relay/admission.d.ts +8 -0
- package/dist/relay/admission.js +58 -0
- package/dist/relay/capabilities.d.ts +36 -0
- package/dist/relay/capabilities.js +232 -0
- package/dist/relay/fees.d.ts +17 -0
- package/dist/relay/fees.js +105 -0
- package/dist/relay/index.d.ts +10 -0
- package/dist/relay/index.js +8 -0
- package/dist/relay/keeper-balance.d.ts +17 -0
- package/dist/relay/keeper-balance.js +46 -0
- package/dist/relay/keeper-runner.d.ts +114 -0
- package/dist/relay/keeper-runner.js +703 -0
- package/dist/relay/keeper.d.ts +38 -0
- package/dist/relay/keeper.js +73 -0
- package/dist/relay/ledger.d.ts +155 -0
- package/dist/relay/ledger.js +392 -0
- package/dist/relay/relay-checks.d.ts +3 -0
- package/dist/relay/relay-checks.js +244 -0
- package/dist/relay/relay-reconciliation.d.ts +15 -0
- package/dist/relay/relay-reconciliation.js +293 -0
- package/dist/relay/relay-request.d.ts +6 -0
- package/dist/relay/relay-request.js +139 -0
- package/dist/relay/relay-transactions.d.ts +23 -0
- package/dist/relay/relay-transactions.js +182 -0
- package/dist/relay/relay-types.d.ts +128 -0
- package/dist/relay/relay-types.js +17 -0
- package/dist/relay/relay.d.ts +102 -0
- package/dist/relay/relay.js +426 -0
- package/dist/relay/reservation.d.ts +29 -0
- package/dist/relay/reservation.js +132 -0
- package/dist/relay/sponsorship.d.ts +42 -0
- package/dist/relay/sponsorship.js +184 -0
- package/dist/routes.d.ts +2 -0
- package/dist/routes.js +1 -0
- package/dist/settle-near/codec.d.ts +17 -0
- package/dist/settle-near/codec.js +86 -0
- package/dist/settle-near/identity.d.ts +27 -0
- package/dist/settle-near/identity.js +80 -0
- package/dist/settle-near/purchase.d.ts +40 -0
- package/dist/settle-near/purchase.js +153 -0
- package/dist/settle-near/terms.d.ts +37 -0
- package/dist/settle-near/terms.js +89 -0
- package/dist/shops/discovery.d.ts +95 -0
- package/dist/shops/discovery.js +384 -0
- package/dist/wallet/account-only.d.ts +11 -0
- package/dist/wallet/account-only.js +6 -0
- package/dist/wallet/uncertain.d.ts +8 -0
- package/dist/wallet/uncertain.js +13 -0
- package/dist/wallet/wallet.d.ts +100 -0
- package/dist/wallet/wallet.js +204 -0
- package/package.json +135 -3
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A seller settles each purchase once (x402-annon.md section 10). Two records in the seller's store: the
|
|
3
|
+
* payment id bound to the payload it first came with, and the purchase `(network, asset, from,
|
|
4
|
+
* nonce)` with its state. The answer always follows the purchase's state now; a purchase the store
|
|
5
|
+
* doesn't know, or whose settlement is unclear, is read from Base by its nonce before anything is
|
|
6
|
+
* decided. Settlement itself goes through the server's facilitator, any standard x402 one.
|
|
7
|
+
*/
|
|
8
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
9
|
+
import { getConfig, getFile } from '../near/rpc.js';
|
|
10
|
+
import { authorizationState, balanceOf, block, fail, head, PayError, paymentStatus, } from './base.js';
|
|
11
|
+
import { toHex } from './eip3009.js';
|
|
12
|
+
import { checkTerms, forFacilitator, readPayload, verifyPayment, } from './seller.js';
|
|
13
|
+
import { BASE_NETWORK } from './x402.js';
|
|
14
|
+
/** A facilitator call is aborted after this; the lease outlasts it, so no two overlap. */
|
|
15
|
+
const FACILITATOR_TIMEOUT_MS = 50_000;
|
|
16
|
+
const LEASE_MARGIN_SECONDS = 10;
|
|
17
|
+
/** After a facilitator call that did not settle, the next one waits this long. */
|
|
18
|
+
export const RETRY_SECONDS = 10;
|
|
19
|
+
/** The longest window a payload this seller accepts can have (x402-annon.md section 4). */
|
|
20
|
+
const MAX_WINDOW_SECONDS = 240;
|
|
21
|
+
const canonical = (v) => Array.isArray(v)
|
|
22
|
+
? v.map(canonical)
|
|
23
|
+
: v !== null && typeof v === 'object'
|
|
24
|
+
? Object.fromEntries(Object.keys(v)
|
|
25
|
+
.sort()
|
|
26
|
+
.map((k) => [k, canonical(v[k])]))
|
|
27
|
+
: v;
|
|
28
|
+
/** The payload as data, key order aside: the same payload always gives the same fingerprint. */
|
|
29
|
+
const fingerprint = (header) => {
|
|
30
|
+
const json = new TextDecoder().decode(Uint8Array.from(atob(header.trim()), (c) => c.charCodeAt(0)));
|
|
31
|
+
return toHex(sha256(new TextEncoder().encode(JSON.stringify(canonical(JSON.parse(json))))));
|
|
32
|
+
};
|
|
33
|
+
const now = () => Math.floor(Date.now() / 1000);
|
|
34
|
+
/** The first block at or before Unix time `t`, from the head back at about five blocks a second. */
|
|
35
|
+
async function blockAt(url, t) {
|
|
36
|
+
const h = await head(url);
|
|
37
|
+
let n = Math.max(0, h.number - Math.max(0, h.timestamp - t) * 5);
|
|
38
|
+
for (;;) {
|
|
39
|
+
const b = await block(url, n);
|
|
40
|
+
if (b.timestamp <= t || n === 0)
|
|
41
|
+
return n;
|
|
42
|
+
n = Math.max(0, n - (b.timestamp - t) * 5 - 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Settles a payload, or answers it from the state of its purchase. Throws `PayError`:
|
|
47
|
+
* `taken_down` (410) first, whatever the state; `bad_requirements`, `bad_signature` for a payload
|
|
48
|
+
* whose purchase terms are wrong; `payment_id_conflict` (409); the refusals of `verifyPayment` for
|
|
49
|
+
* a new payment (`sales_stopped`, a deadline too long, a changed offer); `payment_expired` for a
|
|
50
|
+
* purchase that can no longer be paid; `settlement_pending` when it is under way or unclear: send
|
|
51
|
+
* the same payload again later.
|
|
52
|
+
*/
|
|
53
|
+
/** Where `settle` keeps a purchase: its identity (x402-annon.md section 10). */
|
|
54
|
+
export const purchaseRecordKey = (usdc, payer, nonce) => `annon/seller/purchase/${BASE_NETWORK}/${usdc}/${payer}/${nonce}`;
|
|
55
|
+
export async function settle(header, o) {
|
|
56
|
+
const file = await getFile(o.nearRpc, o.contractId, o.fileId);
|
|
57
|
+
if (!file)
|
|
58
|
+
return fail('unknown_file', 'the file is not registered on the Annon contract');
|
|
59
|
+
if (file.takenDown)
|
|
60
|
+
fail('taken_down', `the file was taken down (${file.takenDown})`);
|
|
61
|
+
if (!file.terms.base)
|
|
62
|
+
fail('route_unavailable', 'This file does not support the selected payment route.');
|
|
63
|
+
const { usdc } = await getConfig(o.nearRpc, o.contractId);
|
|
64
|
+
const c = checkTerms(header, {
|
|
65
|
+
contractId: o.contractId,
|
|
66
|
+
fileId: o.fileId,
|
|
67
|
+
usdc,
|
|
68
|
+
price: file.price,
|
|
69
|
+
payTo: file.payTo,
|
|
70
|
+
termsHash: file.termsHash,
|
|
71
|
+
});
|
|
72
|
+
const scope = o.scope ?? `${o.contractId}/${o.fileId}/${o.url}`;
|
|
73
|
+
const bindId = async () => {
|
|
74
|
+
if (c.paymentId)
|
|
75
|
+
await bind(o, `annon/seller/id/${scope}/${c.paymentId}`, header, c, purchaseRecordKey(usdc, c.payer, c.nonce));
|
|
76
|
+
};
|
|
77
|
+
const timeout = o.facilitatorTimeoutMs ?? FACILITATOR_TIMEOUT_MS;
|
|
78
|
+
const lease = Math.ceil(timeout / 1000) + LEASE_MARGIN_SECONDS;
|
|
79
|
+
const key = purchaseRecordKey(usdc, c.payer, c.nonce);
|
|
80
|
+
const payTo = file.payTo;
|
|
81
|
+
const price = file.price;
|
|
82
|
+
const onBase = (r) => paymentStatus(o.baseRpc, {
|
|
83
|
+
usdc,
|
|
84
|
+
to: payTo,
|
|
85
|
+
from: c.payer,
|
|
86
|
+
nonce: c.nonce,
|
|
87
|
+
price,
|
|
88
|
+
block: r.searchFromBlock,
|
|
89
|
+
validBefore: BigInt(r.validBefore),
|
|
90
|
+
});
|
|
91
|
+
const answerOf = (r) => r.state === 'settled'
|
|
92
|
+
? {
|
|
93
|
+
state: 'settled',
|
|
94
|
+
transaction: r.transaction,
|
|
95
|
+
network: BASE_NETWORK,
|
|
96
|
+
payer: c.payer,
|
|
97
|
+
buyerKey: c.buyerKey,
|
|
98
|
+
nonce: c.nonce,
|
|
99
|
+
}
|
|
100
|
+
: { state: 'pending', transaction: r.transaction, network: BASE_NETWORK };
|
|
101
|
+
/**
|
|
102
|
+
* The answer a record gives without another facilitator call: settled, pending with its hash, or
|
|
103
|
+
* expired; anything else is still being settled.
|
|
104
|
+
*/
|
|
105
|
+
const answerFrom = (r, why = 'this purchase is being settled') => {
|
|
106
|
+
if (r?.state === 'settled' || (r?.state === 'submitted' && r.transaction))
|
|
107
|
+
return answerOf(r);
|
|
108
|
+
if (r?.state === 'unpaid')
|
|
109
|
+
return fail('payment_expired', 'this purchase can no longer be paid');
|
|
110
|
+
return fail('settlement_pending', `${why}; send it again shortly`);
|
|
111
|
+
};
|
|
112
|
+
/** After a write that lost: the record that won decides the answer. */
|
|
113
|
+
const winner = async () => answerFrom((await o.store.load(key)));
|
|
114
|
+
/** The purchase as Base shows it, written to the store; null if another writer came first. */
|
|
115
|
+
const fromChain = async (r, s, base) => {
|
|
116
|
+
let next;
|
|
117
|
+
if (s.state === 'paid') {
|
|
118
|
+
// Settled only if the payment's own block is on the chain at or below Base safe: its height
|
|
119
|
+
// alone could belong to a block a reorg replaced.
|
|
120
|
+
const safe = await block(o.baseRpc, 'safe');
|
|
121
|
+
const log = s.logs[0];
|
|
122
|
+
const canonical = log && log.block_number <= safe.number ? await block(o.baseRpc, log.block_number) : null;
|
|
123
|
+
const onSafe = !!log && canonical?.hash === `0x${log.block_hash}`;
|
|
124
|
+
next = { ...base, state: onSafe ? 'settled' : 'submitted', transaction: s.txHash };
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
next = { ...base, state: 'unpaid' };
|
|
128
|
+
}
|
|
129
|
+
next = { ...next, revision: r ? r.revision + 1 : 0, leaseUntil: base.leaseUntil };
|
|
130
|
+
// A purchase first seen already on Base is recorded here: indexed first, as in the loop.
|
|
131
|
+
if (s.state === 'paid')
|
|
132
|
+
await bindId();
|
|
133
|
+
if (!r)
|
|
134
|
+
await o.beforeRecord?.(key, c.payer);
|
|
135
|
+
if (!(await o.store.compareAndSet(key, r, next)))
|
|
136
|
+
return null;
|
|
137
|
+
if (next.state === 'unpaid')
|
|
138
|
+
fail('payment_expired', 'this purchase can no longer be paid');
|
|
139
|
+
return answerOf(next);
|
|
140
|
+
};
|
|
141
|
+
for (let tries = 0; tries < 4; tries++) {
|
|
142
|
+
const r = (await o.store.load(key));
|
|
143
|
+
if (r)
|
|
144
|
+
await bindId();
|
|
145
|
+
if (r?.state === 'settled')
|
|
146
|
+
return answerOf(r);
|
|
147
|
+
if (r?.state === 'submitted' && r.leaseUntil > now())
|
|
148
|
+
return answerOf(r);
|
|
149
|
+
if (r?.state === 'unpaid')
|
|
150
|
+
return fail('payment_expired', 'this purchase can no longer be paid');
|
|
151
|
+
// A purchase already recorded is settled with its recorded payload, never another one: another
|
|
152
|
+
// payload of the same nonce has not passed the checks of a new payment (x402-annon.md section 10).
|
|
153
|
+
const base = {
|
|
154
|
+
kind: 'purchase',
|
|
155
|
+
id: key,
|
|
156
|
+
revision: 0,
|
|
157
|
+
state: 'processing',
|
|
158
|
+
header: r ? r.header : header,
|
|
159
|
+
validBefore: r ? r.validBefore : c.validBefore.toString(),
|
|
160
|
+
searchFromBlock: r?.searchFromBlock ??
|
|
161
|
+
(await blockAt(o.baseRpc, Number(c.validBefore) - MAX_WINDOW_SECONDS)),
|
|
162
|
+
leaseUntil: 0,
|
|
163
|
+
submissionAttempted: r?.submissionAttempted ?? false,
|
|
164
|
+
...(r?.transaction ? { transaction: r.transaction } : {}),
|
|
165
|
+
};
|
|
166
|
+
const s = await onBase(base);
|
|
167
|
+
if (!r && s.state !== 'paid' && s.state !== 'pending')
|
|
168
|
+
fail('payment_expired');
|
|
169
|
+
if (s.state !== 'pending') {
|
|
170
|
+
const answer = await fromChain(r, s, { ...base, leaseUntil: r?.leaseUntil ?? 0 });
|
|
171
|
+
if (answer)
|
|
172
|
+
return answer;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// Another request's facilitator call may be running: not a second one now.
|
|
176
|
+
if (r && r.leaseUntil > now())
|
|
177
|
+
return answerFrom(r);
|
|
178
|
+
// A purchase never seen before is a new payment: every check of section 4.
|
|
179
|
+
if (!r) {
|
|
180
|
+
await verifyPayment(header, o);
|
|
181
|
+
const checkedHead = await head(o.baseRpc);
|
|
182
|
+
if ((await balanceOf(o.baseRpc, usdc, c.payer, checkedHead.number)) < price)
|
|
183
|
+
fail('insufficient_balance');
|
|
184
|
+
if (await authorizationState(o.baseRpc, usdc, c.payer, c.nonce, checkedHead.number))
|
|
185
|
+
fail('payment_rejected');
|
|
186
|
+
await o.beforeSettle?.(file);
|
|
187
|
+
await bindId();
|
|
188
|
+
await o.beforeRecord?.(key, c.payer);
|
|
189
|
+
}
|
|
190
|
+
const claimed = {
|
|
191
|
+
...base,
|
|
192
|
+
revision: r ? r.revision + 1 : 0,
|
|
193
|
+
state: r?.state ?? 'processing',
|
|
194
|
+
leaseUntil: now() + lease,
|
|
195
|
+
};
|
|
196
|
+
if (!(await o.store.compareAndSet(key, r, claimed)))
|
|
197
|
+
continue;
|
|
198
|
+
// The lease started before the write: what is left of it, less the margin, bounds the call.
|
|
199
|
+
const left = (claimed.leaseUntil - LEASE_MARGIN_SECONDS - now()) * 1000;
|
|
200
|
+
if (left <= 0)
|
|
201
|
+
return answerFrom(claimed);
|
|
202
|
+
return submit(claimed, Math.min(timeout, left));
|
|
203
|
+
}
|
|
204
|
+
// Every write lost to another request: its record decides the answer.
|
|
205
|
+
return winner();
|
|
206
|
+
async function submit(r, budget) {
|
|
207
|
+
const attempted = { ...r, revision: r.revision + 1, submissionAttempted: true };
|
|
208
|
+
if (!(await o.store.compareAndSet(key, r, attempted)))
|
|
209
|
+
return winner();
|
|
210
|
+
r = attempted;
|
|
211
|
+
let res;
|
|
212
|
+
// The recorded payload with its own offer, exactly as it was accepted.
|
|
213
|
+
const offer = readPayload(r.header).accepted;
|
|
214
|
+
const abort = new AbortController();
|
|
215
|
+
const timer = setTimeout(() => abort.abort(new Error('facilitator timed out')), budget);
|
|
216
|
+
try {
|
|
217
|
+
res = await Promise.race([
|
|
218
|
+
o.facilitator(forFacilitator(r.header, offer), { signal: abort.signal }),
|
|
219
|
+
new Promise((_, no) => abort.signal.addEventListener('abort', () => no(abort.signal.reason))),
|
|
220
|
+
]);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
abort.abort();
|
|
227
|
+
}
|
|
228
|
+
const hash = res?.transaction;
|
|
229
|
+
if (hash && (res?.success || res?.errorReason === 'settlement_pending')) {
|
|
230
|
+
const submitted = {
|
|
231
|
+
...r,
|
|
232
|
+
revision: r.revision + 1,
|
|
233
|
+
state: 'submitted',
|
|
234
|
+
leaseUntil: now() + (o.settlementPollSeconds ?? RETRY_SECONDS),
|
|
235
|
+
transaction: hash,
|
|
236
|
+
};
|
|
237
|
+
if (!(await o.store.compareAndSet(key, r, submitted)))
|
|
238
|
+
return winner();
|
|
239
|
+
const s = await onBase(submitted);
|
|
240
|
+
if (s.state === 'pending')
|
|
241
|
+
return answerOf(submitted);
|
|
242
|
+
return (await fromChain(submitted, s, submitted)) ?? winner();
|
|
243
|
+
}
|
|
244
|
+
// Unclear or refused: Base is still read on every request, but the facilitator is called
|
|
245
|
+
// again only after RETRY_SECONDS, the Retry-After a host sends, so retries and repeated downloads can't
|
|
246
|
+
// turn one purchase into a call per request. A purchase already broadcast keeps its answer:
|
|
247
|
+
// pending, with its hash.
|
|
248
|
+
const released = {
|
|
249
|
+
...r,
|
|
250
|
+
revision: r.revision + 1,
|
|
251
|
+
leaseUntil: now() + RETRY_SECONDS,
|
|
252
|
+
submissionAttempted: res?.submissionAttempted === false ? false : (r.submissionAttempted ?? true),
|
|
253
|
+
};
|
|
254
|
+
if (!(await o.store.compareAndSet(key, r, released)))
|
|
255
|
+
return winner();
|
|
256
|
+
if (res?.errorReason === 'facilitator_busy' || res?.errorReason === 'payer_busy')
|
|
257
|
+
fail('facilitator_busy');
|
|
258
|
+
if (res?.errorReason === 'unavailable' || res?.errorReason === 'gas_price_too_high') {
|
|
259
|
+
const error = new PayError('unavailable');
|
|
260
|
+
if (typeof res.retryAfterSeconds === 'number' &&
|
|
261
|
+
Number.isSafeInteger(res.retryAfterSeconds) &&
|
|
262
|
+
res.retryAfterSeconds >= 0)
|
|
263
|
+
error.retryAfterSeconds = res.retryAfterSeconds;
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
if (res?.errorReason === 'payment_expired')
|
|
267
|
+
fail('payment_expired');
|
|
268
|
+
if (res?.errorReason === 'payment_rejected' ||
|
|
269
|
+
res?.errorReason === 'invalid_authorization' ||
|
|
270
|
+
res?.errorReason === 'authorization_used')
|
|
271
|
+
fail('payment_rejected');
|
|
272
|
+
return answerFrom(released, `the facilitator did not settle it${res?.errorReason ? ` (${res.errorReason})` : ''}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** The payment id bound to this payload, or `payment_id_conflict` if it came with another. */
|
|
276
|
+
async function bind(o, key, header, c, purchaseKey) {
|
|
277
|
+
const fp = fingerprint(header);
|
|
278
|
+
for (;;) {
|
|
279
|
+
const b = (await o.store.load(key));
|
|
280
|
+
if (b) {
|
|
281
|
+
if (b.fingerprint !== fp) {
|
|
282
|
+
throw new PayError('payment_id_conflict', 'this payment id came with another payload');
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const created = {
|
|
287
|
+
kind: 'id',
|
|
288
|
+
id: key,
|
|
289
|
+
revision: 0,
|
|
290
|
+
fingerprint: fp,
|
|
291
|
+
keepUntil: c.validBefore.toString(),
|
|
292
|
+
purchaseKey,
|
|
293
|
+
};
|
|
294
|
+
if (await o.store.compareAndSet(key, null, created))
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** The x402 v2 `SettleResponse` for a settlement: the `PAYMENT-RESPONSE` header's content. */
|
|
299
|
+
export function paymentResponse(s) {
|
|
300
|
+
return s.state === 'settled'
|
|
301
|
+
? { success: true, transaction: s.transaction, network: s.network, payer: s.payer }
|
|
302
|
+
: {
|
|
303
|
+
success: false,
|
|
304
|
+
errorReason: 'settlement_pending',
|
|
305
|
+
transaction: s.transaction,
|
|
306
|
+
network: s.network,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revenue splits through the registered Base payment address: when a file's `pay_to` is
|
|
3
|
+
* an 0xSplits v2 split, the integrator shows the seller where the money goes before they sign. The
|
|
4
|
+
* split keeps only `keccak256(abi.encode(split))` on chain, so the caller supplies the split and how
|
|
5
|
+
* it was created (both from its `SplitCreated` event, or its own config).
|
|
6
|
+
*
|
|
7
|
+
* The address alone proves the rest: an 0xSplits V2.2 factory deploys each split with CREATE2 from
|
|
8
|
+
* a salt that commits to the split and its owner, and initializes it in the same transaction. If the
|
|
9
|
+
* address recomputed for `owner = 0` is `pay_to` and its code is the factory's clone, the canonical
|
|
10
|
+
* factory made it with these terms and nobody ever controlled it (no left-over USDC approvals from a
|
|
11
|
+
* former owner). Code that merely answers `splitHash()` and `owner()` correctly does not pass.
|
|
12
|
+
*/
|
|
13
|
+
/** An 0xSplits v2 split, as `SplitV2Lib.Split`. */
|
|
14
|
+
export interface Split {
|
|
15
|
+
recipients: string[];
|
|
16
|
+
/** Shares, in any unit below 2^128 in total; each recipient gets `amount * allocation / total`. */
|
|
17
|
+
allocations: bigint[];
|
|
18
|
+
totalAllocation: bigint;
|
|
19
|
+
/** Reward to the distributor the `distribute` caller names, in millionths (at most 65 535, 6.5535%). */
|
|
20
|
+
distributionIncentive: number;
|
|
21
|
+
}
|
|
22
|
+
/** `abi.encode(split)`: the struct as one dynamic tuple (offset, then head, then the two arrays). */
|
|
23
|
+
export declare function encodeSplit(s: Split): Uint8Array;
|
|
24
|
+
export declare const splitHash: (s: Split) => string;
|
|
25
|
+
export interface SplitTerms {
|
|
26
|
+
/** `push`: `distribute` pays the wallets; `pull`: it credits them in the SplitsWarehouse. */
|
|
27
|
+
kind: 'push' | 'pull';
|
|
28
|
+
/** Each recipient and its share of a payment, after the distribution reward, in percent. */
|
|
29
|
+
shares: {
|
|
30
|
+
recipient: string;
|
|
31
|
+
percent: number;
|
|
32
|
+
}[];
|
|
33
|
+
/** The reward to the distributor the `distribute` caller names, in percent. */
|
|
34
|
+
distributorPercent: number;
|
|
35
|
+
}
|
|
36
|
+
/** How the factory named the split: `createSplitDeterministic`'s salt or `createSplit`'s nonce. */
|
|
37
|
+
export type SplitOrigin = {
|
|
38
|
+
salt: string;
|
|
39
|
+
} | {
|
|
40
|
+
nonce: bigint;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The 0xSplits V2.2 factories on Base (`deployments/8453.json` at 0159376) and the implementation
|
|
44
|
+
* each clones (`SPLIT_WALLET_IMPLEMENTATION()`, read 6 Nov 2026). Push pays recipients' wallets at
|
|
45
|
+
* `distribute`; pull credits them in the SplitsWarehouse, from which they withdraw.
|
|
46
|
+
* Note: V2.2 only; older factories after their clones are checked on chain.
|
|
47
|
+
*/
|
|
48
|
+
export declare const SPLIT_FACTORIES: readonly [{
|
|
49
|
+
readonly kind: "push";
|
|
50
|
+
readonly factory: "0x8e8eb0cc6ae34a38b67d5cf91aca38f60bc3ecf4";
|
|
51
|
+
readonly implementation: "0x1e2086a7e84a32482ac03000d56925f607ccb708";
|
|
52
|
+
}, {
|
|
53
|
+
readonly kind: "pull";
|
|
54
|
+
readonly factory: "0x6b9118074ab15142d7524e8c4ea8f62a3bdb98f1";
|
|
55
|
+
readonly implementation: "0x98254aedb6b2c30b70483064367f0ba24ca86244";
|
|
56
|
+
}];
|
|
57
|
+
/** The 89-byte runtime of `Clone.sol` (a minimal proxy with an ETH `receive`) for `implementation`. */
|
|
58
|
+
export declare const cloneCode: (implementation: string) => string;
|
|
59
|
+
/** Where `factory` deploys `split` for `owner` (`SplitFactoryV2._getSalt`, `Clone.initCodeHash`). */
|
|
60
|
+
export declare function splitAddress(f: {
|
|
61
|
+
factory: string;
|
|
62
|
+
implementation: string;
|
|
63
|
+
}, split: Split, owner: string, origin: SplitOrigin): string;
|
|
64
|
+
/**
|
|
65
|
+
* Most recipients a split may have here. A push `distribute` makes one USDC transfer (about 30k to
|
|
66
|
+
* 60k gas) per recipient in one transaction; 100 stays far below Base's gas limits.
|
|
67
|
+
*/
|
|
68
|
+
export declare const MAX_RECIPIENTS = 100;
|
|
69
|
+
/** Canonical hosted v1 salt. Complete ordered split parameters determine reuse. */
|
|
70
|
+
export declare function hostedSplitSalt(): string;
|
|
71
|
+
/** A transaction to send from any wallet on Base. */
|
|
72
|
+
export interface SplitCall {
|
|
73
|
+
to: string;
|
|
74
|
+
data: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The transaction that creates `split` as an ownerless (immutable) split of `kind` at the address
|
|
78
|
+
* it returns. Send it from any wallet on Base. Keep `split` and `salt`: `readSplit` needs them, and
|
|
79
|
+
* the same terms and salt always give the same address (sent again, the factory just returns it).
|
|
80
|
+
*/
|
|
81
|
+
export declare function createSplit(kind: 'push' | 'pull', split: Split, salt: string): SplitCall & {
|
|
82
|
+
address: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The calls that pay out what the split at `address` holds in `token`: `distribute` first, then
|
|
86
|
+
* for a pull split one SplitsWarehouse withdrawal per recipient (and for `distributor`, if the
|
|
87
|
+
* split pays a reward), each paying that address's wallet. Anyone may send them; the sender pays
|
|
88
|
+
* the gas and `distributor` gets the split's distribution reward. `payoutCall` sends them as one
|
|
89
|
+
* transaction. Sent one by one, each goes after the one before it is in a block: a wallet
|
|
90
|
+
* estimates a withdrawal on the current state, and before `distribute` lands there is nothing to
|
|
91
|
+
* withdraw.
|
|
92
|
+
*/
|
|
93
|
+
/** Multicall3, at the same address on Base as on every chain it is deployed to. */
|
|
94
|
+
export declare const MULTICALL3 = "0xca11bde05977b3631167028862be2a173976ca11";
|
|
95
|
+
/**
|
|
96
|
+
* The payout as one transaction through Multicall3 `aggregate3`. `distribute` must succeed or the
|
|
97
|
+
* whole transaction reverts; each withdrawal may fail on its own (nothing to pay, a USDC-blacklisted
|
|
98
|
+
* or paused recipient) without stopping the others. A push split's payout is already one call.
|
|
99
|
+
* Check the receipt's warehouse events for who was paid: a transaction that succeeded may still
|
|
100
|
+
* hold a failed withdrawal.
|
|
101
|
+
*/
|
|
102
|
+
export declare function payoutCall(kind: 'push' | 'pull', address: string, split: Split, token: string, distributor: string): SplitCall;
|
|
103
|
+
export declare function payoutCalls(kind: 'push' | 'pull', address: string, split: Split, token: string, distributor: string): SplitCall[];
|
|
104
|
+
/**
|
|
105
|
+
* Checks that `address` on Base is the ownerless 0xSplits V2.2 split `split`, created as `origin`
|
|
106
|
+
* says, and returns what it pays whom. Throws `PayError` `bad_requirements` otherwise.
|
|
107
|
+
* It proves the terms, not that a payout will go through: Circle can blacklist a recipient (a push
|
|
108
|
+
* split then pays nobody, a pull split only not that one) or the split itself, and shares round down.
|
|
109
|
+
*/
|
|
110
|
+
export declare function readSplit(baseRpc: string, address: string, split: Split, origin: SplitOrigin): Promise<SplitTerms>;
|
|
111
|
+
/**
|
|
112
|
+
* Reads an ownerless, unpaused V2.2 clone with these exact terms without its creation salt or
|
|
113
|
+
* nonce. This checks current code and state only; `readSplit` also proves ownerless creation.
|
|
114
|
+
*/
|
|
115
|
+
export declare function readSplitState(baseRpc: string, address: string, split: Split): Promise<SplitTerms>;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revenue splits through the registered Base payment address: when a file's `pay_to` is
|
|
3
|
+
* an 0xSplits v2 split, the integrator shows the seller where the money goes before they sign. The
|
|
4
|
+
* split keeps only `keccak256(abi.encode(split))` on chain, so the caller supplies the split and how
|
|
5
|
+
* it was created (both from its `SplitCreated` event, or its own config).
|
|
6
|
+
*
|
|
7
|
+
* The address alone proves the rest: an 0xSplits V2.2 factory deploys each split with CREATE2 from
|
|
8
|
+
* a salt that commits to the split and its owner, and initializes it in the same transaction. If the
|
|
9
|
+
* address recomputed for `owner = 0` is `pay_to` and its code is the factory's clone, the canonical
|
|
10
|
+
* factory made it with these terms and nobody ever controlled it (no left-over USDC approvals from a
|
|
11
|
+
* former owner). Code that merely answers `splitHash()` and `owner()` correctly does not pass.
|
|
12
|
+
*/
|
|
13
|
+
import { keccak_256 } from '@noble/hashes/sha3.js';
|
|
14
|
+
import { concatParts as concat } from '../bytes.js';
|
|
15
|
+
import { fail, getCode, head, PayError, viewWord } from './base.js';
|
|
16
|
+
import { addressWord, fromHex, toHex, word } from './eip3009.js';
|
|
17
|
+
/** `abi.encode(split)`: the struct as one dynamic tuple (offset, then head, then the two arrays). */
|
|
18
|
+
export function encodeSplit(s) {
|
|
19
|
+
const n = s.recipients.length;
|
|
20
|
+
if (s.allocations.length !== n)
|
|
21
|
+
throw new TypeError('one allocation per recipient');
|
|
22
|
+
if (!Number.isInteger(s.distributionIncentive) ||
|
|
23
|
+
s.distributionIncentive < 0 ||
|
|
24
|
+
s.distributionIncentive > 0xffff) {
|
|
25
|
+
throw new RangeError('distributionIncentive is a uint16');
|
|
26
|
+
}
|
|
27
|
+
const recipients = concat([word(BigInt(n)), ...s.recipients.map(addressWord)]);
|
|
28
|
+
const allocations = concat([word(BigInt(n)), ...s.allocations.map(word)]);
|
|
29
|
+
const head = concat([
|
|
30
|
+
word(4n * 32n),
|
|
31
|
+
word(4n * 32n + BigInt(recipients.length)),
|
|
32
|
+
word(s.totalAllocation),
|
|
33
|
+
word(BigInt(s.distributionIncentive)),
|
|
34
|
+
]);
|
|
35
|
+
return concat([word(32n), head, recipients, allocations]);
|
|
36
|
+
}
|
|
37
|
+
export const splitHash = (s) => toHex(keccak_256(encodeSplit(s)));
|
|
38
|
+
/**
|
|
39
|
+
* The 0xSplits V2.2 factories on Base (`deployments/8453.json` at 0159376) and the implementation
|
|
40
|
+
* each clones (`SPLIT_WALLET_IMPLEMENTATION()`, read 6 Nov 2026). Push pays recipients' wallets at
|
|
41
|
+
* `distribute`; pull credits them in the SplitsWarehouse, from which they withdraw.
|
|
42
|
+
* Note: V2.2 only; older factories after their clones are checked on chain.
|
|
43
|
+
*/
|
|
44
|
+
export const SPLIT_FACTORIES = [
|
|
45
|
+
{
|
|
46
|
+
kind: 'push',
|
|
47
|
+
factory: '0x8e8eb0cc6ae34a38b67d5cf91aca38f60bc3ecf4',
|
|
48
|
+
implementation: '0x1e2086a7e84a32482ac03000d56925f607ccb708',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
kind: 'pull',
|
|
52
|
+
factory: '0x6b9118074ab15142d7524e8c4ea8f62a3bdb98f1',
|
|
53
|
+
implementation: '0x98254aedb6b2c30b70483064367f0ba24ca86244',
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
/** The 89-byte runtime of `Clone.sol` (a minimal proxy with an ETH `receive`) for `implementation`. */
|
|
57
|
+
export const cloneCode = (implementation) => `0x36602c57343d527f9e4ac34f21c619cefc926c8bd93b54bf5a39c7ab2127a895af1cc0691d7e3dff593da1005b3d3d3d3d363d3d37363d73${implementation.slice(2).toLowerCase()}5af43d3d93803e605757fd5bf3`;
|
|
58
|
+
/** Where `factory` deploys `split` for `owner` (`SplitFactoryV2._getSalt`, `Clone.initCodeHash`). */
|
|
59
|
+
export function splitAddress(f, split, owner, origin) {
|
|
60
|
+
// abi.encode(split, owner): the tuple's offset, the owner, then the tuple.
|
|
61
|
+
const params = concat([word(64n), addressWord(owner), encodeSplit(split).subarray(32)]);
|
|
62
|
+
let salt;
|
|
63
|
+
if ('salt' in origin) {
|
|
64
|
+
salt = keccak_256(concat([params, fromHex(origin.salt, 32, 'salt')]));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
salt = keccak_256(concat([keccak_256(params), word(origin.nonce)]));
|
|
68
|
+
}
|
|
69
|
+
// CREATE (9 bytes) + runtime (89) + 15 zero bytes: Clone.sol hashes 0x71 bytes from 0x0f.
|
|
70
|
+
const init = concat([
|
|
71
|
+
fromHex('0x60593d8160093d39f3', 9, 'init'),
|
|
72
|
+
fromHex(cloneCode(f.implementation), 89, 'clone'),
|
|
73
|
+
new Uint8Array(15),
|
|
74
|
+
]);
|
|
75
|
+
const h = keccak_256(concat([Uint8Array.of(0xff), fromHex(f.factory, 20, 'factory'), salt, keccak_256(init)]));
|
|
76
|
+
return toHex(h.subarray(12));
|
|
77
|
+
}
|
|
78
|
+
const ZERO = `0x${'00'.repeat(20)}`;
|
|
79
|
+
const selector = (sig) => toHex(keccak_256(new TextEncoder().encode(sig)).subarray(0, 4));
|
|
80
|
+
const calldata = (sig, ...parts) => selector(sig) + toHex(concat(parts)).slice(2);
|
|
81
|
+
const SPLIT_TUPLE = '(address[],uint256[],uint256,uint16)';
|
|
82
|
+
/** The split's tuple without the leading offset word, for a call that has other arguments. */
|
|
83
|
+
const tuple = (s) => encodeSplit(s).subarray(32);
|
|
84
|
+
/** The SplitsWarehouse on Base, where a pull split credits its recipients. */
|
|
85
|
+
const SPLITS_WAREHOUSE = '0x8fb66f38cf86a3d5e8768f8f1754a24a6c661fb8';
|
|
86
|
+
/**
|
|
87
|
+
* Most recipients a split may have here. A push `distribute` makes one USDC transfer (about 30k to
|
|
88
|
+
* 60k gas) per recipient in one transaction; 100 stays far below Base's gas limits.
|
|
89
|
+
*/
|
|
90
|
+
export const MAX_RECIPIENTS = 100;
|
|
91
|
+
/** Terms the SDK accepts, whether it creates the split or reads one. */
|
|
92
|
+
function checkTerms(s) {
|
|
93
|
+
if (s.recipients.length > MAX_RECIPIENTS) {
|
|
94
|
+
fail('bad_requirements', `the split has more than ${MAX_RECIPIENTS} recipients`);
|
|
95
|
+
}
|
|
96
|
+
// USDC refuses any transfer to address 0: a push split could never pay anyone, a pull split
|
|
97
|
+
// would keep that share for good.
|
|
98
|
+
if (s.recipients.some((r) => BigInt(r) === 0n)) {
|
|
99
|
+
fail('bad_requirements', 'the split pays the zero address');
|
|
100
|
+
}
|
|
101
|
+
if (s.totalAllocation < 1n)
|
|
102
|
+
fail('bad_requirements', 'the split allocates nothing');
|
|
103
|
+
// `distribute` computes amount * allocation / total in uint256. With the total below 2^128 that
|
|
104
|
+
// cannot overflow for any amount below 2^128 (USDC's supply is far smaller); above, a large
|
|
105
|
+
// enough payment would, and the money would stay in the split.
|
|
106
|
+
if (s.totalAllocation >= 2n ** 128n) {
|
|
107
|
+
fail('bad_requirements', 'the split allocations are too large to distribute');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Canonical hosted v1 salt. Complete ordered split parameters determine reuse. */
|
|
111
|
+
export function hostedSplitSalt() {
|
|
112
|
+
return `0x${'00'.repeat(32)}`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The transaction that creates `split` as an ownerless (immutable) split of `kind` at the address
|
|
116
|
+
* it returns. Send it from any wallet on Base. Keep `split` and `salt`: `readSplit` needs them, and
|
|
117
|
+
* the same terms and salt always give the same address (sent again, the factory just returns it).
|
|
118
|
+
*/
|
|
119
|
+
export function createSplit(kind, split, salt) {
|
|
120
|
+
checkTerms(split);
|
|
121
|
+
if (split.allocations.reduce((a, b) => a + b, 0n) !== split.totalAllocation) {
|
|
122
|
+
throw new RangeError('the allocations must add up to totalAllocation');
|
|
123
|
+
}
|
|
124
|
+
const f = SPLIT_FACTORIES.find((f) => f.kind === kind);
|
|
125
|
+
if (!f)
|
|
126
|
+
throw new TypeError('kind is push or pull');
|
|
127
|
+
const data = calldata(`createSplitDeterministic(${SPLIT_TUPLE},address,address,bytes32)`, word(128n), addressWord(ZERO), addressWord(ZERO), fromHex(salt, 32, 'salt'), tuple(split));
|
|
128
|
+
return { to: f.factory, data, address: splitAddress(f, split, ZERO, { salt }) };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The calls that pay out what the split at `address` holds in `token`: `distribute` first, then
|
|
132
|
+
* for a pull split one SplitsWarehouse withdrawal per recipient (and for `distributor`, if the
|
|
133
|
+
* split pays a reward), each paying that address's wallet. Anyone may send them; the sender pays
|
|
134
|
+
* the gas and `distributor` gets the split's distribution reward. `payoutCall` sends them as one
|
|
135
|
+
* transaction. Sent one by one, each goes after the one before it is in a block: a wallet
|
|
136
|
+
* estimates a withdrawal on the current state, and before `distribute` lands there is nothing to
|
|
137
|
+
* withdraw.
|
|
138
|
+
*/
|
|
139
|
+
/** Multicall3, at the same address on Base as on every chain it is deployed to. */
|
|
140
|
+
export const MULTICALL3 = '0xca11bde05977b3631167028862be2a173976ca11';
|
|
141
|
+
/**
|
|
142
|
+
* The payout as one transaction through Multicall3 `aggregate3`. `distribute` must succeed or the
|
|
143
|
+
* whole transaction reverts; each withdrawal may fail on its own (nothing to pay, a USDC-blacklisted
|
|
144
|
+
* or paused recipient) without stopping the others. A push split's payout is already one call.
|
|
145
|
+
* Check the receipt's warehouse events for who was paid: a transaction that succeeded may still
|
|
146
|
+
* hold a failed withdrawal.
|
|
147
|
+
*/
|
|
148
|
+
export function payoutCall(kind, address, split, token, distributor) {
|
|
149
|
+
const calls = payoutCalls(kind, address, split, token, distributor);
|
|
150
|
+
if (kind === 'push')
|
|
151
|
+
return calls[0];
|
|
152
|
+
// aggregate3((address target, bool allowFailure, bytes callData)[]): the array's offset, its
|
|
153
|
+
// length, one offset per element (from after the length), then each element.
|
|
154
|
+
const elements = calls.map((c, i) => {
|
|
155
|
+
const data = fromHex(c.data, (c.data.length - 2) / 2, 'callData');
|
|
156
|
+
const padded = new Uint8Array(Math.ceil(data.length / 32) * 32);
|
|
157
|
+
padded.set(data);
|
|
158
|
+
return concat([
|
|
159
|
+
addressWord(c.to),
|
|
160
|
+
word(i === 0 ? 0n : 1n),
|
|
161
|
+
word(96n),
|
|
162
|
+
word(BigInt(data.length)),
|
|
163
|
+
padded,
|
|
164
|
+
]);
|
|
165
|
+
});
|
|
166
|
+
const offsets = [];
|
|
167
|
+
let at = 32n * BigInt(elements.length);
|
|
168
|
+
for (const e of elements) {
|
|
169
|
+
offsets.push(word(at));
|
|
170
|
+
at += BigInt(e.length);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
to: MULTICALL3,
|
|
174
|
+
data: calldata(`aggregate3((address,bool,bytes)[])`, word(32n), word(BigInt(elements.length)), ...offsets, ...elements),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export function payoutCalls(kind, address, split, token, distributor) {
|
|
178
|
+
// A pull split would credit the reward to address 0 for good; push could never distribute.
|
|
179
|
+
if (BigInt(distributor) === 0n)
|
|
180
|
+
throw new TypeError('distributor is the address that sends them');
|
|
181
|
+
const calls = [
|
|
182
|
+
{
|
|
183
|
+
to: address.toLowerCase(),
|
|
184
|
+
data: calldata(`distribute(${SPLIT_TUPLE},address,address)`, word(96n), addressWord(token), addressWord(distributor), tuple(split)),
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
if (kind === 'pull') {
|
|
188
|
+
const paid = [...split.recipients, ...(split.distributionIncentive > 0 ? [distributor] : [])];
|
|
189
|
+
for (const r of new Set(paid.map((r) => r.toLowerCase()))) {
|
|
190
|
+
calls.push({
|
|
191
|
+
to: SPLITS_WAREHOUSE,
|
|
192
|
+
data: calldata('withdraw(address,address)', addressWord(r), addressWord(token)),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return calls;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Checks that `address` on Base is the ownerless 0xSplits V2.2 split `split`, created as `origin`
|
|
200
|
+
* says, and returns what it pays whom. Throws `PayError` `bad_requirements` otherwise.
|
|
201
|
+
* It proves the terms, not that a payout will go through: Circle can blacklist a recipient (a push
|
|
202
|
+
* split then pays nobody, a pull split only not that one) or the split itself, and shares round down.
|
|
203
|
+
*/
|
|
204
|
+
export async function readSplit(baseRpc, address, split, origin) {
|
|
205
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address))
|
|
206
|
+
throw new TypeError('address must be 0x and 20 bytes');
|
|
207
|
+
checkTerms(split);
|
|
208
|
+
const to = address.toLowerCase();
|
|
209
|
+
const f = SPLIT_FACTORIES.find((f) => splitAddress(f, split, ZERO, origin) === to);
|
|
210
|
+
if (!f) {
|
|
211
|
+
return fail('bad_requirements', `${address} is not an ownerless 0xSplits V2.2 split of these terms`);
|
|
212
|
+
}
|
|
213
|
+
return inspectSplit(baseRpc, to, split, [f]);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Reads an ownerless, unpaused V2.2 clone with these exact terms without its creation salt or
|
|
217
|
+
* nonce. This checks current code and state only; `readSplit` also proves ownerless creation.
|
|
218
|
+
*/
|
|
219
|
+
export async function readSplitState(baseRpc, address, split) {
|
|
220
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address))
|
|
221
|
+
throw new TypeError('address must be 0x and 20 bytes');
|
|
222
|
+
checkTerms(split);
|
|
223
|
+
return inspectSplit(baseRpc, address.toLowerCase(), split, SPLIT_FACTORIES);
|
|
224
|
+
}
|
|
225
|
+
async function inspectSplit(baseRpc, address, split, factories) {
|
|
226
|
+
const at = (await head(baseRpc)).number;
|
|
227
|
+
const code = await getCode(baseRpc, address, at);
|
|
228
|
+
const f = factories.find((f) => code === cloneCode(f.implementation));
|
|
229
|
+
if (!f)
|
|
230
|
+
return fail('bad_requirements', `${address} is not deployed as a known V2.2 clone`);
|
|
231
|
+
const read = (sig) => viewWord(baseRpc, address, selector(sig), at).catch((e) => {
|
|
232
|
+
// A reverted view is an invalid split; malformed or unavailable RPC replies stay retryable.
|
|
233
|
+
if (e instanceof PayError && e.code === 'refused') {
|
|
234
|
+
fail('bad_requirements', `${address} does not answer ${sig}: not an 0xSplits v2 split`);
|
|
235
|
+
}
|
|
236
|
+
throw e;
|
|
237
|
+
});
|
|
238
|
+
if ((await read('splitHash()')) !== splitHash(split)) {
|
|
239
|
+
fail('bad_requirements', 'the split on chain is not the one described');
|
|
240
|
+
}
|
|
241
|
+
if (BigInt(await read('owner()')) !== 0n) {
|
|
242
|
+
fail('bad_requirements', 'the split has an owner who could change where the money goes');
|
|
243
|
+
}
|
|
244
|
+
if (BigInt(await read('paused()')) !== 0n)
|
|
245
|
+
fail('bad_requirements', 'the split is paused');
|
|
246
|
+
const pct = (part, whole) => Number((part * 1000000n) / whole) / 10_000;
|
|
247
|
+
const kept = 1_000_000 - split.distributionIncentive;
|
|
248
|
+
return {
|
|
249
|
+
kind: f.kind,
|
|
250
|
+
shares: split.recipients.map((r, i) => ({
|
|
251
|
+
recipient: r.toLowerCase(),
|
|
252
|
+
percent: (pct(split.allocations[i] ?? 0n, split.totalAllocation) * kept) / 1_000_000,
|
|
253
|
+
})),
|
|
254
|
+
distributorPercent: split.distributionIncentive / 10_000,
|
|
255
|
+
};
|
|
256
|
+
}
|