402-trinity-gaming 0.1.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/INTEGRATION.md +188 -0
- package/LICENSE +110 -0
- package/README.md +105 -0
- package/dist/batch-manager.d.ts +201 -0
- package/dist/batch-manager.js +291 -0
- package/dist/batch-manager.min.js +1 -0
- package/dist/budget-file.d.ts +106 -0
- package/dist/budget-file.js +270 -0
- package/dist/budget-file.min.js +1 -0
- package/dist/evm-tx.d.ts +55 -0
- package/dist/evm-tx.js +195 -0
- package/dist/evm-tx.min.js +1 -0
- package/dist/proceeds-fee.d.ts +87 -0
- package/dist/proceeds-fee.js +158 -0
- package/dist/proceeds-fee.min.js +1 -0
- package/dist/seller.d.ts +121 -0
- package/dist/seller.js +136 -0
- package/dist/seller.min.js +1 -0
- package/dist/signer.d.ts +64 -0
- package/dist/signer.js +61 -0
- package/dist/signer.min.js +1 -0
- package/dist/storefront.d.ts +143 -0
- package/dist/storefront.js +173 -0
- package/dist/storefront.min.js +1 -0
- package/dist/x402.d.ts +391 -0
- package/dist/x402.js +930 -0
- package/dist/x402.min.js +1 -0
- package/package.json +120 -0
- package/src/batch-manager.ts +351 -0
- package/src/budget-file.ts +314 -0
- package/src/evm-tx.ts +244 -0
- package/src/proceeds-fee.ts +202 -0
- package/src/seller.ts +252 -0
- package/src/signer.ts +129 -0
- package/src/storefront.ts +286 -0
- package/src/x402.ts +1255 -0
package/dist/evm-tx.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
type RlpInput = Uint8Array | RlpInput[];
|
|
2
|
+
export declare function rlp(x: RlpInput): Uint8Array;
|
|
3
|
+
/**
|
|
4
|
+
* Recover the signing address from a digest and a 65-byte r||s||v signature.
|
|
5
|
+
* This is exactly what EIP-3009 does on-chain, so it is the right local check before
|
|
6
|
+
* spending gas submitting something that would revert.
|
|
7
|
+
*/
|
|
8
|
+
export declare function recoverSigner(digest: bigint, signature: string): string | null;
|
|
9
|
+
export interface RpcConfig {
|
|
10
|
+
urls: string[];
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
retries?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function createRpc(cfg: RpcConfig): (method: string, params?: unknown[]) => Promise<any>;
|
|
15
|
+
export interface TxRequest {
|
|
16
|
+
chainId: number;
|
|
17
|
+
to: string;
|
|
18
|
+
data: string;
|
|
19
|
+
gasLimit?: bigint;
|
|
20
|
+
value?: bigint;
|
|
21
|
+
/** Multiplier applied to the observed gas price for maxFeePerGas. Default 4. */
|
|
22
|
+
feeMultiplier?: bigint;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Sign and broadcast an EIP-1559 transaction.
|
|
26
|
+
*
|
|
27
|
+
* Re-submitting the SAME signed transaction is idempotent at the network layer: identical
|
|
28
|
+
* nonce plus identical signature yields an identical hash. That makes RPC failover safe here.
|
|
29
|
+
*/
|
|
30
|
+
export declare function sendTransaction(rpc: (m: string, p?: unknown[]) => Promise<any>, privateKey: string, tx: TxRequest): Promise<{
|
|
31
|
+
hash: string;
|
|
32
|
+
from: string;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function waitForReceipt(rpc: (m: string, p?: unknown[]) => Promise<any>, hash: string, opts?: {
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
pollMs?: number;
|
|
37
|
+
}): Promise<{
|
|
38
|
+
ok: boolean;
|
|
39
|
+
gasUsed: number;
|
|
40
|
+
blockNumber: number;
|
|
41
|
+
} | null>;
|
|
42
|
+
/** abi.encode word: bigint, 0x-hex, or bytes. */
|
|
43
|
+
export declare const word: (v: bigint | string | Uint8Array) => string;
|
|
44
|
+
/** 4-byte selector for a solidity signature. */
|
|
45
|
+
export declare const selector: (sig: string) => string;
|
|
46
|
+
/** Calldata for USDC's transferWithAuthorization. */
|
|
47
|
+
export declare function transferWithAuthorizationData(auth: {
|
|
48
|
+
from: string;
|
|
49
|
+
to: string;
|
|
50
|
+
value: string;
|
|
51
|
+
validAfter: string;
|
|
52
|
+
validBefore: string;
|
|
53
|
+
nonce: string;
|
|
54
|
+
}, signature: string): string;
|
|
55
|
+
export {};
|
package/dist/evm-tx.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { keccak256, toHex, fromHex, __internals } from "./x402.js";
|
|
2
|
+
const {
|
|
3
|
+
toBig,
|
|
4
|
+
beBytes,
|
|
5
|
+
signWith,
|
|
6
|
+
makeNonce,
|
|
7
|
+
addressOf,
|
|
8
|
+
jMul,
|
|
9
|
+
jAdd,
|
|
10
|
+
affine,
|
|
11
|
+
G,
|
|
12
|
+
N,
|
|
13
|
+
P
|
|
14
|
+
} = __internals;
|
|
15
|
+
const cat = (...a) => {
|
|
16
|
+
const t = new Uint8Array(a.reduce((n, x) => n + x.length, 0));
|
|
17
|
+
let o = 0;
|
|
18
|
+
for (const x of a) {
|
|
19
|
+
t.set(x, o);
|
|
20
|
+
o += x.length;
|
|
21
|
+
}
|
|
22
|
+
return t;
|
|
23
|
+
};
|
|
24
|
+
const minimal = (v) => {
|
|
25
|
+
if (v === 0n) return new Uint8Array(0);
|
|
26
|
+
let h = v.toString(16);
|
|
27
|
+
if (h.length % 2) h = "0" + h;
|
|
28
|
+
return fromHex("0x" + h);
|
|
29
|
+
};
|
|
30
|
+
const rlpLen = (len, offset) => {
|
|
31
|
+
if (len < 56) return new Uint8Array([offset + len]);
|
|
32
|
+
const lb = minimal(BigInt(len));
|
|
33
|
+
return cat(new Uint8Array([offset + 55 + lb.length]), lb);
|
|
34
|
+
};
|
|
35
|
+
function rlp(x) {
|
|
36
|
+
if (Array.isArray(x)) {
|
|
37
|
+
const payload = cat(...x.map(rlp));
|
|
38
|
+
return cat(rlpLen(payload.length, 192), payload);
|
|
39
|
+
}
|
|
40
|
+
if (x.length === 1 && x[0] < 128) return x;
|
|
41
|
+
return cat(rlpLen(x.length, 128), x);
|
|
42
|
+
}
|
|
43
|
+
const mod = (a, m) => {
|
|
44
|
+
const r = a % m;
|
|
45
|
+
return r < 0n ? r + m : r;
|
|
46
|
+
};
|
|
47
|
+
const modPow = (b, e, m) => {
|
|
48
|
+
let r = 1n;
|
|
49
|
+
b = mod(b, m);
|
|
50
|
+
while (e > 0n) {
|
|
51
|
+
if (e & 1n) r = mod(r * b, m);
|
|
52
|
+
b = mod(b * b, m);
|
|
53
|
+
e >>= 1n;
|
|
54
|
+
}
|
|
55
|
+
return r;
|
|
56
|
+
};
|
|
57
|
+
const inv = (a, m) => {
|
|
58
|
+
let r = m, nr = mod(a, m), s = 0n, ns = 1n;
|
|
59
|
+
while (nr !== 0n) {
|
|
60
|
+
const q = r / nr;
|
|
61
|
+
[r, nr] = [nr, r - q * nr];
|
|
62
|
+
[s, ns] = [ns, s - q * ns];
|
|
63
|
+
}
|
|
64
|
+
return mod(s, m);
|
|
65
|
+
};
|
|
66
|
+
function recoverSigner(digest, signature) {
|
|
67
|
+
try {
|
|
68
|
+
const sig = fromHex(signature);
|
|
69
|
+
if (sig.length !== 65) return null;
|
|
70
|
+
const r = toBig(sig.slice(0, 32)), s = toBig(sig.slice(32, 64)), v = sig[64] - 27;
|
|
71
|
+
if (r === 0n || r >= N || s === 0n || s >= N || v < 0 || v > 3) return null;
|
|
72
|
+
const x = r + (v >> 1 ? N : 0n);
|
|
73
|
+
if (x >= P) return null;
|
|
74
|
+
let y = modPow(mod(x * x * x + 7n, P), (P + 1n) / 4n, P);
|
|
75
|
+
if (mod(y * y, P) !== mod(x * x * x + 7n, P)) return null;
|
|
76
|
+
if ((y & 1n) !== BigInt(v & 1)) y = P - y;
|
|
77
|
+
const Q = jMul(inv(r, N), jAdd(jMul(s, [x, y, 1n]), jMul(mod(-digest, N), G)));
|
|
78
|
+
if (Q[2] === 0n) return null;
|
|
79
|
+
const xy = affine(Q);
|
|
80
|
+
return toHex(keccak256(beBytes(xy[0], 32), beBytes(xy[1], 32)).slice(12));
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function createRpc(cfg) {
|
|
86
|
+
const urls = cfg.urls;
|
|
87
|
+
const timeoutMs = cfg.timeoutMs ?? 15e3;
|
|
88
|
+
const retries = cfg.retries ?? 3;
|
|
89
|
+
if (!urls.length) throw new Error("evm-tx: at least one RPC url is required");
|
|
90
|
+
return async function rpc(method, params = []) {
|
|
91
|
+
let last;
|
|
92
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
93
|
+
for (const url of urls) {
|
|
94
|
+
try {
|
|
95
|
+
const r = await fetch(url, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "content-type": "application/json" },
|
|
98
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
99
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
100
|
+
});
|
|
101
|
+
const j = await r.json();
|
|
102
|
+
if (j.error) {
|
|
103
|
+
const msg = String(j.error.message ?? "");
|
|
104
|
+
if (!/healthy|unavailable|rate|limit|timeout|busy|capacity/i.test(msg)) {
|
|
105
|
+
throw new Error(`${method}: ${msg}`);
|
|
106
|
+
}
|
|
107
|
+
last = new Error(`${method}: ${msg}`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
return j.result;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
if (e instanceof Error && e.message.startsWith(method + ":")) throw e;
|
|
113
|
+
last = e;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await new Promise((r) => setTimeout(r, 800 * (attempt + 1)));
|
|
117
|
+
}
|
|
118
|
+
throw last instanceof Error ? last : new Error(`${method}: all RPCs failed`);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function sendTransaction(rpc, privateKey, tx) {
|
|
122
|
+
const d = toBig(fromHex(privateKey));
|
|
123
|
+
if (d === 0n || d >= N) throw new Error("evm-tx: invalid key material");
|
|
124
|
+
const from = addressOf(d);
|
|
125
|
+
const [nonceHex, gasPriceHex] = await Promise.all([
|
|
126
|
+
rpc("eth_getTransactionCount", [from, "pending"]),
|
|
127
|
+
rpc("eth_gasPrice", [])
|
|
128
|
+
]);
|
|
129
|
+
const tip = 1000000n;
|
|
130
|
+
const maxFee = BigInt(gasPriceHex) * (tx.feeMultiplier ?? 4n) + tip;
|
|
131
|
+
const fields = [
|
|
132
|
+
minimal(BigInt(tx.chainId)),
|
|
133
|
+
minimal(BigInt(nonceHex)),
|
|
134
|
+
minimal(tip),
|
|
135
|
+
minimal(maxFee),
|
|
136
|
+
minimal(tx.gasLimit ?? 200000n),
|
|
137
|
+
fromHex(tx.to),
|
|
138
|
+
minimal(tx.value ?? 0n),
|
|
139
|
+
fromHex(tx.data),
|
|
140
|
+
[]
|
|
141
|
+
];
|
|
142
|
+
const sigHash = toBig(keccak256(new Uint8Array([2]), rlp(fields)));
|
|
143
|
+
const sig = fromHex(signWith(makeNonce(), sigHash, d));
|
|
144
|
+
if (recoverSigner(sigHash, toHex(sig)) !== from) {
|
|
145
|
+
throw new Error("evm-tx: signed transaction does not recover to the sender; refusing to broadcast");
|
|
146
|
+
}
|
|
147
|
+
const trimZeros = (b) => {
|
|
148
|
+
let i = 0;
|
|
149
|
+
while (i < b.length && b[i] === 0) i++;
|
|
150
|
+
return b.subarray(i);
|
|
151
|
+
};
|
|
152
|
+
const raw = toHex(cat(
|
|
153
|
+
new Uint8Array([2]),
|
|
154
|
+
rlp([
|
|
155
|
+
...fields,
|
|
156
|
+
minimal(BigInt(sig[64] - 27)),
|
|
157
|
+
trimZeros(sig.slice(0, 32)),
|
|
158
|
+
trimZeros(sig.slice(32, 64))
|
|
159
|
+
])
|
|
160
|
+
));
|
|
161
|
+
const hash = await rpc("eth_sendRawTransaction", [raw]);
|
|
162
|
+
return { hash, from };
|
|
163
|
+
}
|
|
164
|
+
async function waitForReceipt(rpc, hash, opts = {}) {
|
|
165
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 12e4);
|
|
166
|
+
const poll = opts.pollMs ?? 2500;
|
|
167
|
+
while (Date.now() < deadline) {
|
|
168
|
+
await new Promise((r) => setTimeout(r, poll));
|
|
169
|
+
const rc = await rpc("eth_getTransactionReceipt", [hash]).catch(() => null);
|
|
170
|
+
if (rc) return { ok: rc.status === "0x1", gasUsed: Number(BigInt(rc.gasUsed)), blockNumber: Number(BigInt(rc.blockNumber)) };
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const word = (v) => {
|
|
175
|
+
if (typeof v === "bigint") return toHex(beBytes(v, 32)).slice(2);
|
|
176
|
+
const b = typeof v === "string" ? fromHex(v) : v;
|
|
177
|
+
const w = new Uint8Array(32);
|
|
178
|
+
w.set(b, 32 - b.length);
|
|
179
|
+
return toHex(w).slice(2);
|
|
180
|
+
};
|
|
181
|
+
const selector = (sig) => toHex(keccak256(new TextEncoder().encode(sig))).slice(0, 10);
|
|
182
|
+
function transferWithAuthorizationData(auth, signature) {
|
|
183
|
+
const sig = fromHex(signature);
|
|
184
|
+
return selector("transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32)") + word(auth.from) + word(auth.to) + word(BigInt(auth.value)) + word(BigInt(auth.validAfter)) + word(BigInt(auth.validBefore)) + word(auth.nonce) + word(BigInt(sig[64])) + word(sig.slice(0, 32)) + word(sig.slice(32, 64));
|
|
185
|
+
}
|
|
186
|
+
export {
|
|
187
|
+
createRpc,
|
|
188
|
+
recoverSigner,
|
|
189
|
+
rlp,
|
|
190
|
+
selector,
|
|
191
|
+
sendTransaction,
|
|
192
|
+
transferWithAuthorizationData,
|
|
193
|
+
waitForReceipt,
|
|
194
|
+
word
|
|
195
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{keccak256 as B,toHex as d,fromHex as m,__internals as N}from"./x402.js";const{toBig:x,beBytes:I,signWith:T,makeNonce:E,addressOf:J,jMul:P,jAdd:_,affine:j,G:S,N:h,P:y}=N,A=(...n)=>{const r=new Uint8Array(n.reduce((i,o)=>i+o.length,0));let t=0;for(const i of n)r.set(i,t),t+=i.length;return r},w=n=>{if(n===0n)return new Uint8Array(0);let r=n.toString(16);return r.length%2&&(r="0"+r),m("0x"+r)},v=(n,r)=>{if(n<56)return new Uint8Array([r+n]);const t=w(BigInt(n));return A(new Uint8Array([r+55+t.length]),t)};function R(n){if(Array.isArray(n)){const r=A(...n.map(R));return A(v(r.length,192),r)}return n.length===1&&n[0]<128?n:A(v(n.length,128),n)}const l=(n,r)=>{const t=n%r;return t<0n?t+r:t},C=(n,r,t)=>{let i=1n;for(n=l(n,t);r>0n;)r&1n&&(i=l(i*n,t)),n=l(n*n,t),r>>=1n;return i},H=(n,r)=>{let t=r,i=l(n,r),o=0n,e=1n;for(;i!==0n;){const s=t/i;[t,i]=[i,t-s*i],[o,e]=[e,o-s*e]}return l(o,r)};function W(n,r){try{const t=m(r);if(t.length!==65)return null;const i=x(t.slice(0,32)),o=x(t.slice(32,64)),e=t[64]-27;if(i===0n||i>=h||o===0n||o>=h||e<0||e>3)return null;const s=i+(e>>1?h:0n);if(s>=y)return null;let a=C(l(s*s*s+7n,y),(y+1n)/4n,y);if(l(a*a,y)!==l(s*s*s+7n,y))return null;(a&1n)!==BigInt(e&1)&&(a=y-a);const u=P(H(i,h),_(P(o,[s,a,1n]),P(l(-n,h),S)));if(u[2]===0n)return null;const c=j(u);return d(B(I(c[0],32),I(c[1],32)).slice(12))}catch{return null}}function z(n){const r=n.urls,t=n.timeoutMs??15e3,i=n.retries??3;if(!r.length)throw new Error("evm-tx: at least one RPC url is required");return async function(e,s=[]){let a;for(let u=0;u<i;u++){for(const c of r)try{const b=await(await fetch(c,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:1,method:e,params:s}),signal:AbortSignal.timeout(t)})).json();if(b.error){const p=String(b.error.message??"");if(!/healthy|unavailable|rate|limit|timeout|busy|capacity/i.test(p))throw new Error(`${e}: ${p}`);a=new Error(`${e}: ${p}`);continue}return b.result}catch(f){if(f instanceof Error&&f.message.startsWith(e+":"))throw f;a=f}await new Promise(c=>setTimeout(c,800*(u+1)))}throw a instanceof Error?a:new Error(`${e}: all RPCs failed`)}}async function D(n,r,t){const i=x(m(r));if(i===0n||i>=h)throw new Error("evm-tx: invalid key material");const o=J(i),[e,s]=await Promise.all([n("eth_getTransactionCount",[o,"pending"]),n("eth_gasPrice",[])]),a=1000000n,u=BigInt(s)*(t.feeMultiplier??4n)+a,c=[w(BigInt(t.chainId)),w(BigInt(e)),w(a),w(u),w(t.gasLimit??200000n),m(t.to),w(t.value??0n),m(t.data),[]],f=x(B(new Uint8Array([2]),R(c))),b=m(T(E(),f,i));if(W(f,d(b))!==o)throw new Error("evm-tx: signed transaction does not recover to the sender; refusing to broadcast");const p=k=>{let U=0;for(;U<k.length&&k[U]===0;)U++;return k.subarray(U)},M=d(A(new Uint8Array([2]),R([...c,w(BigInt(b[64]-27)),p(b.slice(0,32)),p(b.slice(32,64))])));return{hash:await n("eth_sendRawTransaction",[M]),from:o}}async function L(n,r,t={}){const i=Date.now()+(t.timeoutMs??12e4),o=t.pollMs??2500;for(;Date.now()<i;){await new Promise(s=>setTimeout(s,o));const e=await n("eth_getTransactionReceipt",[r]).catch(()=>null);if(e)return{ok:e.status==="0x1",gasUsed:Number(BigInt(e.gasUsed)),blockNumber:Number(BigInt(e.blockNumber))}}return null}const g=n=>{if(typeof n=="bigint")return d(I(n,32)).slice(2);const r=typeof n=="string"?m(n):n,t=new Uint8Array(32);return t.set(r,32-r.length),d(t).slice(2)},$=n=>d(B(new TextEncoder().encode(n))).slice(0,10);function F(n,r){const t=m(r);return $("transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32)")+g(n.from)+g(n.to)+g(BigInt(n.value))+g(BigInt(n.validAfter))+g(BigInt(n.validBefore))+g(n.nonce)+g(BigInt(t[64]))+g(t.slice(0,32))+g(t.slice(32,64))}export{z as createRpc,W as recoverSigner,R as rlp,$ as selector,D as sendTransaction,F as transferWithAuthorizationData,L as waitForReceipt,g as word};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE MERCHANT-SIDE FEE.
|
|
3
|
+
*
|
|
4
|
+
* The player is debited exactly the sticker price - nothing is added on top of what the
|
|
5
|
+
* store shows. The fee comes out of the studio's proceeds instead, the way a card processor
|
|
6
|
+
* or a platform cut works, and settles in a batch rather than on every sale.
|
|
7
|
+
*
|
|
8
|
+
* Two things are owed: 0.1% of each sale, and a flat charge once every hundred. Both accrue
|
|
9
|
+
* and go out TOGETHER in one authorization when the hundredth sale lands - one settlement
|
|
10
|
+
* per hundred rather than a hundred dust transfers that would cost more in gas than they
|
|
11
|
+
* collect.
|
|
12
|
+
*
|
|
13
|
+
* WHY THE STUDIO MUST SUPPLY A KEY. Moving USDC out of the studio's wallet requires the
|
|
14
|
+
* studio to authorize it. There is no way around that and no way for us to do it for them.
|
|
15
|
+
* The key signs one thing only - a transfer of the accrued fee to the vault - and it is the
|
|
16
|
+
* studio's own treasury wallet on the studio's own server. If no key is supplied, no fee is
|
|
17
|
+
* charged and `enabled` reads false; nothing silently half-works.
|
|
18
|
+
*
|
|
19
|
+
* The accrual below mirrors the buyer-side implementation that has been settling on mainnet:
|
|
20
|
+
* read-modify-write inside the lock, tally reset BEFORE the authorization is signed, and a
|
|
21
|
+
* failed hand-off held and re-sent with the SAME nonce rather than re-minted.
|
|
22
|
+
*/
|
|
23
|
+
export declare const NOTICE: string;
|
|
24
|
+
export interface FeeStore {
|
|
25
|
+
get: () => Promise<{
|
|
26
|
+
accrued: bigint;
|
|
27
|
+
count: bigint;
|
|
28
|
+
}>;
|
|
29
|
+
set: (v: {
|
|
30
|
+
accrued: bigint;
|
|
31
|
+
count: bigint;
|
|
32
|
+
}) => Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Read, modify and write while holding a lock. Without it two backend instances sharing a
|
|
35
|
+
* tally both read the same count and both write count+1, and sales stop counting.
|
|
36
|
+
*/
|
|
37
|
+
update?: (fn: (c: {
|
|
38
|
+
accrued: bigint;
|
|
39
|
+
count: bigint;
|
|
40
|
+
}) => {
|
|
41
|
+
accrued: bigint;
|
|
42
|
+
count: bigint;
|
|
43
|
+
}) => Promise<{
|
|
44
|
+
accrued: bigint;
|
|
45
|
+
count: bigint;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
export interface ProceedsFeeConfig {
|
|
49
|
+
/**
|
|
50
|
+
* Key for the wallet named in the storefront's `payTo`. Signs ONLY fee authorizations to
|
|
51
|
+
* the vault. Omit it and no fee is charged.
|
|
52
|
+
*/
|
|
53
|
+
proceedsKey?: string;
|
|
54
|
+
/** Durable tally. In memory the count resets on restart and the hundredth never lands. */
|
|
55
|
+
store?: FeeStore;
|
|
56
|
+
network?: string;
|
|
57
|
+
/** Point the batch somewhere else - a studio may prefer their own facilitator. */
|
|
58
|
+
collector?: string;
|
|
59
|
+
onNotice?: (msg: string) => void;
|
|
60
|
+
onDiagnostic?: (d: {
|
|
61
|
+
code: string;
|
|
62
|
+
message: string;
|
|
63
|
+
}) => void;
|
|
64
|
+
}
|
|
65
|
+
export declare function createProceedsFee(cfg: ProceedsFeeConfig): {
|
|
66
|
+
/** False when no key was supplied - nothing is being charged. */
|
|
67
|
+
readonly enabled: boolean;
|
|
68
|
+
/** The wallet the fee is debited from. Empty when disabled. */
|
|
69
|
+
readonly from: string;
|
|
70
|
+
/**
|
|
71
|
+
* Record one settled sale. Sweeps on the hundredth.
|
|
72
|
+
*
|
|
73
|
+
* Never throws and never rejects: a fee problem must not undo a sale that has already
|
|
74
|
+
* settled on-chain. Failures surface through `onDiagnostic` and `stats()`.
|
|
75
|
+
*/
|
|
76
|
+
record(saleValue: bigint): Promise<void>;
|
|
77
|
+
stats(): Promise<{
|
|
78
|
+
enabled: boolean;
|
|
79
|
+
salesSinceLastSweep: string;
|
|
80
|
+
accrued: string;
|
|
81
|
+
collected: string;
|
|
82
|
+
/** Held and awaiting re-send. Not lost - the same authorization goes out next sale. */
|
|
83
|
+
held: string;
|
|
84
|
+
/** Expired before it could be collected. This is genuinely gone. */
|
|
85
|
+
lost: string;
|
|
86
|
+
}>;
|
|
87
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { toHex, fromHex, __internals } from "./x402.js";
|
|
2
|
+
const { addressOf, digest, domainSep, makeNonce, signWith, toBig, CHAINS, N } = __internals;
|
|
3
|
+
const FEE_VAULT = "0x2f011f21D6Ec758Bc18f0f9142EeD01Ce2d8a0d3";
|
|
4
|
+
const FEE_PPM = 1000n;
|
|
5
|
+
const FEE_EVERY = 100n;
|
|
6
|
+
const FEE_AMOUNT = 10000n;
|
|
7
|
+
const FEE_SCALE = 1000000n;
|
|
8
|
+
const FEE_COLLECTOR = "https://x402-trinity-collector.x402trinity.workers.dev/submit";
|
|
9
|
+
const NOTICE = "Merchant proceeds are settled net of a 0.1% network fee, plus a flat charge once every hundred sales. Players are debited exactly the price shown.";
|
|
10
|
+
function createProceedsFee(cfg) {
|
|
11
|
+
const enabled = typeof cfg.proceedsKey === "string" && cfg.proceedsKey.length > 0;
|
|
12
|
+
const chainKey = (cfg.network ?? "base").toLowerCase();
|
|
13
|
+
const chain = CHAINS[chainKey] ?? Object.values(CHAINS).find((c) => c.caip2 === chainKey);
|
|
14
|
+
let d = 0n, from = "";
|
|
15
|
+
if (enabled) {
|
|
16
|
+
d = toBig(fromHex(cfg.proceedsKey));
|
|
17
|
+
if (d === 0n || d >= N) throw new Error("proceeds fee: invalid key material");
|
|
18
|
+
from = addressOf(d);
|
|
19
|
+
cfg.onNotice?.(NOTICE);
|
|
20
|
+
}
|
|
21
|
+
const collector = cfg.collector ?? FEE_COLLECTOR;
|
|
22
|
+
let mem = { accrued: 0n, count: 0n };
|
|
23
|
+
let pending = null;
|
|
24
|
+
let collected = 0n, lost = 0n;
|
|
25
|
+
const handOff = async (auth, sig) => {
|
|
26
|
+
try {
|
|
27
|
+
const r = await fetch(collector, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "content-type": "application/json" },
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
x402Version: 1,
|
|
32
|
+
paymentPayload: {
|
|
33
|
+
x402Version: 1,
|
|
34
|
+
scheme: "exact",
|
|
35
|
+
network: chain.caip2,
|
|
36
|
+
payload: { signature: sig, authorization: auth }
|
|
37
|
+
},
|
|
38
|
+
paymentRequirements: {
|
|
39
|
+
scheme: "exact",
|
|
40
|
+
network: chain.caip2,
|
|
41
|
+
payTo: FEE_VAULT,
|
|
42
|
+
asset: chain.asset,
|
|
43
|
+
maxAmountRequired: auth.value,
|
|
44
|
+
amount: auth.value,
|
|
45
|
+
resource: "https://x402-trinity.dev/fee",
|
|
46
|
+
description: "network fee",
|
|
47
|
+
mimeType: "application/json",
|
|
48
|
+
maxTimeoutSeconds: 300,
|
|
49
|
+
extra: { name: chain.name, version: chain.version }
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
if (!r.ok) return false;
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(await r.text())?.success === true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
/** False when no key was supplied - nothing is being charged. */
|
|
65
|
+
get enabled() {
|
|
66
|
+
return enabled;
|
|
67
|
+
},
|
|
68
|
+
/** The wallet the fee is debited from. Empty when disabled. */
|
|
69
|
+
get from() {
|
|
70
|
+
return from;
|
|
71
|
+
},
|
|
72
|
+
/**
|
|
73
|
+
* Record one settled sale. Sweeps on the hundredth.
|
|
74
|
+
*
|
|
75
|
+
* Never throws and never rejects: a fee problem must not undo a sale that has already
|
|
76
|
+
* settled on-chain. Failures surface through `onDiagnostic` and `stats()`.
|
|
77
|
+
*/
|
|
78
|
+
async record(saleValue) {
|
|
79
|
+
if (!enabled) return;
|
|
80
|
+
try {
|
|
81
|
+
if (pending) {
|
|
82
|
+
const stuck = pending;
|
|
83
|
+
if (Number(stuck.auth.validBefore) > Math.floor(Date.now() / 1e3) + 5) {
|
|
84
|
+
if (await handOff(stuck.auth, stuck.sig)) {
|
|
85
|
+
collected += BigInt(stuck.auth.value);
|
|
86
|
+
pending = null;
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
lost += BigInt(stuck.auth.value);
|
|
90
|
+
pending = null;
|
|
91
|
+
cfg.onDiagnostic?.({
|
|
92
|
+
code: "fee_expired",
|
|
93
|
+
message: `a held fee authorization for ${stuck.auth.value} expired uncollected`
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
let owed = 0n, crossed = false;
|
|
98
|
+
const step = (cur) => {
|
|
99
|
+
const a = cur.accrued + saleValue * FEE_PPM;
|
|
100
|
+
const c = cur.count + 1n;
|
|
101
|
+
crossed = c >= FEE_EVERY;
|
|
102
|
+
if (!crossed) return { accrued: a, count: c };
|
|
103
|
+
owed = a / FEE_SCALE + FEE_AMOUNT;
|
|
104
|
+
return { accrued: a % FEE_SCALE, count: 0n };
|
|
105
|
+
};
|
|
106
|
+
if (cfg.store?.update) await cfg.store.update(step);
|
|
107
|
+
else if (cfg.store) {
|
|
108
|
+
const next = step(await cfg.store.get());
|
|
109
|
+
await cfg.store.set(next);
|
|
110
|
+
} else mem = step(mem);
|
|
111
|
+
if (!crossed) return;
|
|
112
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
113
|
+
const n32 = new Uint8Array(32);
|
|
114
|
+
crypto.getRandomValues(n32);
|
|
115
|
+
const auth = {
|
|
116
|
+
from,
|
|
117
|
+
to: FEE_VAULT,
|
|
118
|
+
value: String(owed),
|
|
119
|
+
validAfter: String(now - 60),
|
|
120
|
+
validBefore: String(now + 3600),
|
|
121
|
+
nonce: toHex(n32)
|
|
122
|
+
};
|
|
123
|
+
const dsep = domainSep(chain.name, chain.version, chain.id, chain.asset);
|
|
124
|
+
const sig = signWith(makeNonce(), digest(dsep, auth), d);
|
|
125
|
+
if (await handOff(auth, sig)) collected += owed;
|
|
126
|
+
else {
|
|
127
|
+
pending = { auth, sig };
|
|
128
|
+
cfg.onDiagnostic?.({
|
|
129
|
+
code: "fee_handoff_failed",
|
|
130
|
+
message: `holding a fee authorization for ${owed} to re-send with the same nonce`
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
cfg.onDiagnostic?.({
|
|
135
|
+
code: "fee_error",
|
|
136
|
+
message: err instanceof Error ? err.message : String(err)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
async stats() {
|
|
141
|
+
const cur = cfg.store ? await cfg.store.get() : mem;
|
|
142
|
+
return {
|
|
143
|
+
enabled,
|
|
144
|
+
salesSinceLastSweep: String(cur.count),
|
|
145
|
+
accrued: String(cur.accrued / FEE_SCALE),
|
|
146
|
+
collected: String(collected),
|
|
147
|
+
/** Held and awaiting re-send. Not lost - the same authorization goes out next sale. */
|
|
148
|
+
held: pending ? pending.auth.value : "0",
|
|
149
|
+
/** Expired before it could be collected. This is genuinely gone. */
|
|
150
|
+
lost: String(lost)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export {
|
|
156
|
+
NOTICE,
|
|
157
|
+
createProceedsFee
|
|
158
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{toHex as A,fromHex as F,__internals as N}from"./x402.js";const{addressOf:O,digest:C,domainSep:D,makeNonce:T,signWith:z,toBig:B,CHAINS:S,N:L}=N,_="0x2f011f21D6Ec758Bc18f0f9142EeD01Ce2d8a0d3",K=1000n,M=100n,R=10000n,h=1000000n,V="https://x402-trinity-collector.x402trinity.workers.dev/submit",I="Merchant proceeds are settled net of a 0.1% network fee, plus a flat charge once every hundred sales. Players are debited exactly the price shown.";function H(e){const c=typeof e.proceedsKey=="string"&&e.proceedsKey.length>0,y=(e.network??"base").toLowerCase(),a=S[y]??Object.values(S).find(n=>n.caip2===y);let s=0n,d="";if(c){if(s=B(F(e.proceedsKey)),s===0n||s>=L)throw new Error("proceeds fee: invalid key material");d=O(s),e.onNotice?.(I)}const P=e.collector??V;let u={accrued:0n,count:0n},r=null,l=0n,m=0n;const w=async(n,o)=>{try{const i=await fetch(P,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({x402Version:1,paymentPayload:{x402Version:1,scheme:"exact",network:a.caip2,payload:{signature:o,authorization:n}},paymentRequirements:{scheme:"exact",network:a.caip2,payTo:_,asset:a.asset,maxAmountRequired:n.value,amount:n.value,resource:"https://x402-trinity.dev/fee",description:"network fee",mimeType:"application/json",maxTimeoutSeconds:300,extra:{name:a.name,version:a.version}}})});if(!i.ok)return!1;try{return JSON.parse(await i.text())?.success===!0}catch{return!1}}catch{return!1}};return{get enabled(){return c},get from(){return d},async record(n){if(c)try{if(r){const t=r;Number(t.auth.validBefore)>Math.floor(Date.now()/1e3)+5?await w(t.auth,t.sig)&&(l+=BigInt(t.auth.value),r=null):(m+=BigInt(t.auth.value),r=null,e.onDiagnostic?.({code:"fee_expired",message:`a held fee authorization for ${t.auth.value} expired uncollected`}))}let o=0n,i=!1;const g=t=>{const p=t.accrued+n*K,E=t.count+1n;return i=E>=M,i?(o=p/h+R,{accrued:p%h,count:0n}):{accrued:p,count:E}};if(e.store?.update)await e.store.update(g);else if(e.store){const t=g(await e.store.get());await e.store.set(t)}else u=g(u);if(!i)return;const b=Math.floor(Date.now()/1e3),v=new Uint8Array(32);crypto.getRandomValues(v);const f={from:d,to:_,value:String(o),validAfter:String(b-60),validBefore:String(b+3600),nonce:A(v)},k=D(a.name,a.version,a.id,a.asset),x=z(T(),C(k,f),s);await w(f,x)?l+=o:(r={auth:f,sig:x},e.onDiagnostic?.({code:"fee_handoff_failed",message:`holding a fee authorization for ${o} to re-send with the same nonce`}))}catch(o){e.onDiagnostic?.({code:"fee_error",message:o instanceof Error?o.message:String(o)})}},async stats(){const n=e.store?await e.store.get():u;return{enabled:c,salesSinceLastSweep:String(n.count),accrued:String(n.accrued/h),collected:String(l),held:r?r.auth.value:"0",lost:String(m)}}}}export{I as NOTICE,H as createProceedsFee};
|
package/dist/seller.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402-trinity/seller - the OTHER half of the protocol: charge for a resource and get paid.
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies. Optional module - not part of the buyer core, so it does not count
|
|
5
|
+
* against the wrapper's footprint.
|
|
6
|
+
*
|
|
7
|
+
* import { createX402Seller } from './seller.js';
|
|
8
|
+
*
|
|
9
|
+
* const seller = createX402Seller({
|
|
10
|
+
* payTo: '0xYourWallet', // 100% of every payment lands here
|
|
11
|
+
* price: '1000', // atomic units (USDC = 6dp) -> 0.001 USDC
|
|
12
|
+
* network: 'base',
|
|
13
|
+
* facilitator: 'https://your-facilitator.example',
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // in any fetch-style handler:
|
|
17
|
+
* const gate = await seller.guard(request);
|
|
18
|
+
* if (gate.response) return gate.response; // unpaid or rejected
|
|
19
|
+
* return new Response(mySecretData); // paid; gate.settlement has the tx
|
|
20
|
+
*
|
|
21
|
+
* The seller never holds funds and never needs a private key. It quotes a price, then
|
|
22
|
+
* asks a facilitator to verify and settle. Money moves buyer -> payTo directly on-chain.
|
|
23
|
+
*/
|
|
24
|
+
export interface SellerConfig {
|
|
25
|
+
/** Your wallet. Receives 100% of each payment. */
|
|
26
|
+
payTo: string;
|
|
27
|
+
/** Price in atomic units of the asset (USDC has 6 decimals). */
|
|
28
|
+
price: string;
|
|
29
|
+
/** 'base', or the CAIP-2 id 'eip155:8453'. Other chains need explicit asset + extra. */
|
|
30
|
+
network: string;
|
|
31
|
+
/** Token contract. Defaults to USDC for the network. */
|
|
32
|
+
asset?: string;
|
|
33
|
+
/** EIP-712 domain for the asset. Defaults to USDC's. */
|
|
34
|
+
extra?: {
|
|
35
|
+
name: string;
|
|
36
|
+
version: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Facilitator base URL. REQUIRED - there is no safe default.
|
|
40
|
+
*
|
|
41
|
+
* The public facilitator at x402.org settles TESTNET ONLY on EVM. Defaulting to it in a
|
|
42
|
+
* mainnet package would mean every payment verifies and then fails to settle, which looks
|
|
43
|
+
* like your service is broken. Supply one that settles on your network:
|
|
44
|
+
* - Coinbase CDP (needs an API key)
|
|
45
|
+
* - your own, if you run settlement yourself
|
|
46
|
+
*/
|
|
47
|
+
facilitator: string;
|
|
48
|
+
/** How long a quote stays valid. Default 600s. */
|
|
49
|
+
maxTimeoutSeconds?: number;
|
|
50
|
+
/** Human-readable description surfaced in the challenge. */
|
|
51
|
+
description?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Replay guard. An authorization nonce redeems once ON-CHAIN, but nothing stops a buyer
|
|
54
|
+
* re-presenting an already-settled payment to get the resource a second time for free.
|
|
55
|
+
* That is revenue loss, and the default in-memory guard forgets everything on restart.
|
|
56
|
+
*
|
|
57
|
+
* `add` receives the authorization's `validBefore`, so a store only has to remember a
|
|
58
|
+
* nonce until it expires - after that the authorization is dead on-chain anyway and the
|
|
59
|
+
* entry can be pruned. Without that, the guard grows without bound.
|
|
60
|
+
*
|
|
61
|
+
* REQUIRED on mainnet unless `acknowledgeEphemeralReplayGuard` is set.
|
|
62
|
+
*/
|
|
63
|
+
nonceStore?: {
|
|
64
|
+
seen: (nonce: string) => Promise<boolean>;
|
|
65
|
+
add: (nonce: string, expiresAtUnix: number) => Promise<void>;
|
|
66
|
+
};
|
|
67
|
+
/** Accept an in-memory replay guard. Only sane for local development. */
|
|
68
|
+
acknowledgeEphemeralReplayGuard?: boolean;
|
|
69
|
+
onSettled?: (i: {
|
|
70
|
+
transaction: string;
|
|
71
|
+
payer: string;
|
|
72
|
+
amount: string;
|
|
73
|
+
network: string;
|
|
74
|
+
}) => void;
|
|
75
|
+
/** Settlement attempts before giving up. Default 3. Public facilitators are flaky. */
|
|
76
|
+
settleRetries?: number;
|
|
77
|
+
/** Base backoff between settlement attempts, ms. Default 1500 (then 3000, 4500...). */
|
|
78
|
+
settleBackoffMs?: number;
|
|
79
|
+
onSettleFailure?: (i: {
|
|
80
|
+
reason: string;
|
|
81
|
+
attempts: number;
|
|
82
|
+
nonce: string;
|
|
83
|
+
}) => void;
|
|
84
|
+
}
|
|
85
|
+
export interface GateResult {
|
|
86
|
+
/** Non-null when the caller must return this instead of serving the resource. */
|
|
87
|
+
response: Response | null;
|
|
88
|
+
settlement?: {
|
|
89
|
+
transaction: string;
|
|
90
|
+
payer: string;
|
|
91
|
+
network: string;
|
|
92
|
+
};
|
|
93
|
+
reason?: string;
|
|
94
|
+
/**
|
|
95
|
+
* True when the payment was VALID but settlement failed on our side. The response is a
|
|
96
|
+
* 503, not a 402 - see the note on `guard`.
|
|
97
|
+
*/
|
|
98
|
+
settlementFailed?: boolean;
|
|
99
|
+
}
|
|
100
|
+
export declare function createX402Seller(cfg: SellerConfig): {
|
|
101
|
+
requirements: {
|
|
102
|
+
scheme: string;
|
|
103
|
+
network: string;
|
|
104
|
+
amount: string;
|
|
105
|
+
asset: string;
|
|
106
|
+
payTo: string;
|
|
107
|
+
maxTimeoutSeconds: number;
|
|
108
|
+
extra: {
|
|
109
|
+
name: string;
|
|
110
|
+
version: string;
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
/** Returns {response} when the caller must NOT serve the resource. */
|
|
114
|
+
guard(request: Request): Promise<GateResult>;
|
|
115
|
+
/** Header a paid response should carry, so the buyer can read the receipt. */
|
|
116
|
+
receiptHeader(settlement: {
|
|
117
|
+
transaction: string;
|
|
118
|
+
payer: string;
|
|
119
|
+
network: string;
|
|
120
|
+
}): Record<string, string>;
|
|
121
|
+
};
|