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/x402.min.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const rt=new Uint32Array([1,32898,32906,2147516416,32907,2147483649,2147516545,32777,138,136,2147516425,2147483658,2147516555,139,32905,32771,32770,128,32778,2147483658,2147516545,32896,2147483649,2147516424]),ot=new Uint32Array([0,0,2147483648,2147483648,0,0,2147483648,2147483648,0,0,0,0,0,2147483648,2147483648,2147483648,2147483648,2147483648,0,2147483648,2147483648,2147483648,0,2147483648]),it=new Uint8Array([0,1,62,28,27,36,44,6,55,20,3,10,43,25,39,41,45,15,21,8,18,2,61,56,14]),st=(()=>{const e=new Uint8Array(25);for(let t=0;t<5;t++)for(let n=0;n<5;n++)e[n+5*t]=t+5*((2*n+3*t)%5);return e})(),at=new Uint32Array(50),ct=new Uint32Array(50),ut=new Uint32Array(10);function Je(e){const t=ct,n=ut;for(let o=0;o<24;o++){for(let r=0;r<5;r++){let i=0,a=0;for(let c=0;c<25;c+=5){const d=r+c<<1;i^=e[d],a^=e[d+1]}n[r<<1]=i,n[(r<<1)+1]=a}for(let r=0;r<5;r++){const i=(r+1)%5<<1,a=(r+4)%5<<1,c=n[i],d=n[i+1],x=n[a]^(c<<1|d>>>31),R=n[a+1]^(d<<1|c>>>31);for(let U=0;U<25;U+=5){const S=r+U<<1;e[S]^=x,e[S+1]^=R}}for(let r=0;r<25;r++){const i=it[r],a=e[r<<1],c=e[(r<<1)+1],d=st[r]<<1;if(i===0)t[d]=a,t[d+1]=c;else if(i<32)t[d]=a<<i|c>>>32-i,t[d+1]=c<<i|a>>>32-i;else{const x=i-32;t[d]=c<<x|a>>>32-x,t[d+1]=a<<x|c>>>32-x}}for(let r=0;r<25;r+=5)for(let i=0;i<5;i++){const a=i+r<<1,c=(i+1)%5+r<<1,d=(i+2)%5+r<<1;e[a]=t[a]^~t[c]&t[d],e[a+1]=t[a+1]^~t[c+1]&t[d+1]}e[0]^=rt[o],e[1]^=ot[o]}}function I(...e){const t=at;t.fill(0);let n=0;for(const r of e)for(let i=0;i<r.length;i++)t[n>>2]^=r[i]<<((n&3)<<3),++n===136&&(Je(t),n=0);t[n>>2]^=1<<((n&3)<<3),t[33]^=2147483648,Je(t);const o=new Uint8Array(32);for(let r=0;r<32;r++)o[r]=t[r>>2]>>>((r&3)<<3)&255;return o}const Le="0123456789abcdef",X=e=>{let t="0x";for(let n=0;n<e.length;n++)t+=Le[e[n]>>4]+Le[e[n]&15];return t},ke=e=>{const t=e.slice(0,2)==="0x"?e.slice(2):e,n=new Uint8Array(t.length>>1);for(let o=0;o<n.length;o++)n[o]=parseInt(t.slice(o<<1,(o<<1)+2),16);return n},ue=e=>new TextEncoder().encode(e),D=(e,t)=>{const n=new Uint8Array(t);for(let o=t-1;o>=0&&e>0n;o--)n[o]=Number(e&0xffn),e>>=8n;return n},$=e=>{let t=0n;for(let n=0;n<e.length;n++)t=t<<8n|BigInt(e[n]);return t},O=e=>{if(typeof e=="bigint")return D(e,32);const t=typeof e=="string"?ke(e):e,n=new Uint8Array(32);return n.set(t,32-t.length),n},p=0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn,P=0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n,ft=0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n,lt=0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n,dt=P>>1n,m=(e,t)=>{const n=e%t;return n<0n?n+t:n};function Xe(e,t){let n=t,o=m(e,t),r=0n,i=1n;for(;o!==0n;){const a=n/o,c=n-a*o;n=o,o=c;const d=r-a*i;r=i,i=d}return m(r,t)}function Re(e){const t=e[0],n=e[1],o=e[2];if(n===0n||o===0n)return[0n,1n,0n];const r=m(t*t,p),i=m(n*n,p),a=m(i*i,p),c=m(2n*(m((t+i)*(t+i),p)-r-a),p),d=m(3n*r,p),x=m(d*d,p),R=m(x-2n*c,p);return[R,m(d*(c-R)-8n*a,p),m(2n*n*o,p)]}function Se(e,t){const n=e[0],o=e[1],r=e[2],i=t[0],a=t[1],c=t[2];if(r===0n)return t;if(c===0n)return e;const d=m(r*r,p),x=m(c*c,p),R=m(n*x,p),U=m(i*d,p),S=m(o*c*x,p),B=m(a*r*d,p),E=m(U-R,p),_=m(B-S,p);if(E===0n)return _===0n?Re(e):[0n,1n,0n];const ee=m(E*E,p),te=m(E*ee,p),ne=m(R*ee,p),C=m(_*_-te-2n*ne,p);return[C,m(_*(ne-C)-S*te,p),m(r*c*E,p)]}function gt(e,t){let n=[0n,1n,0n],o=t;for(;e>0n;)e&1n&&(n=Se(n,o)),o=Re(o),e>>=1n;return n}function Te(e,t){const n=new Uint32Array(1);crypto.getRandomValues(n);const o=e+BigInt(n[0])*P;let r=[0n,1n,0n];for(let i=287;i>=0;i--){r=Re(r);const a=Se(r,t),c=-(o>>BigInt(i)&1n);r=[r[0]&~c|a[0]&c,r[1]&~c|a[1]&c,r[2]&~c|a[2]&c]}return r}const qe=e=>{const t=Xe(e[2],p),n=m(t*t,p);return[m(e[0]*n,p),m(e[1]*m(n*t,p),p)]},Ue=[ft,lt,1n];function mt(){const e=new Uint8Array(32);for(;;){crypto.getRandomValues(e);const t=$(e);if(t>0n&&t<P)return t}}function fe(e){const t=qe(Te(e,Ue));return X(I(D(t[0],32),D(t[1],32)).slice(12))}function le(){for(;;){const e=mt(),t=qe(Te(e,Ue)),n=m(t[0],P);if(n!==0n)return{kInv:Xe(e,P),r:n,rec:Number(t[1]&1n)|(t[0]>=P?2:0)}}}function Pe(e,t,n){let o=m(e.kInv*(t+e.r*n),P),r=e.rec;o>dt&&(o=P-o,r^=1);const i=new Uint8Array(65);return i.set(D(e.r,32),0),i.set(D(o,32),32),i[64]=27+r,X(i)}const Ve=I(ue("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")),je=I(ue("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")),Ge=(e,t,n,o)=>I(Ve,I(ue(e)),I(ue(t)),O(BigInt(n)),O(o));function de(e,t){const n=I(je,O(t.from),O(t.to),O(BigInt(t.value)),O(BigInt(t.validAfter)),O(BigInt(t.validBefore)),O(t.nonce));return $(I(new Uint8Array([25,1]),e,n))}const Q={base:{id:8453,asset:"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",name:"USD Coin",version:"2"}},Ee="0x2f011f21D6Ec758Bc18f0f9142EeD01Ce2d8a0d3",ht=1000n,pt=100n,xt=10000n,Ce=1000000n,yt="https://x402-trinity-collector.x402trinity.workers.dev/submit",bt="A convenience fee is added ON TOP of each payment: 0.1% per transaction and 1 cent every 100 transactions. Sellers always receive their full asking price.",wt=(()=>{const e={};for(const t in Q)e["eip155:"+Q[t].id]=t;return e})(),We=e=>{const t=(e||"").toLowerCase().trim();return wt[t]??t},Ze=e=>({...e,network:We(e.network),maxAmountRequired:String(e.amount??e.maxAmountRequired??"")});async function vt(e){const t=[],n=[],o=e.headers.get("www-authenticate");if(o&&/^\s*Payment[\s,]/i.test(o))return{reqs:[],raws:[],version:1,error:"MPP challenge (WWW-Authenticate: Payment); this client speaks x402 only"};const r=e.headers.get("payment-required");if(r)try{const a=JSON.parse(r);for(const c of a?.accepts??[])t.push(Ze(c)),n.push(c);if(t.length)return{reqs:t,raws:n,version:2,resource:a?.resource}}catch{}const i=e.headers.get("x402-payment-request");if(i)try{const a=JSON.parse(i),c=We(String(a.chain??a.network??"base")),d=Q[c],x=String(a.amount??a.maxAmountRequired??"0"),R=x.indexOf(".")>=0?String(BigInt(Math.round(parseFloat(x)*1e6))):x,U=a.payTo??a.recipient??e.headers.get("x402-payment-recipient");if(U&&d){const S={scheme:a.scheme??"exact",network:c,payTo:U,asset:a.asset??d.asset,maxAmountRequired:R,maxTimeoutSeconds:a.maxTimeoutSeconds??60,extra:{name:d.name,version:d.version}};t.push(S),n.push(S)}}catch{}if((e.headers.get("content-type")??"").indexOf("json")>=0)try{const a=await e.clone().json(),c=a?.x402Version;if(typeof c=="number"&&c>2)return{reqs:[],raws:[],version:1,error:"server speaks x402 v"+c+"; this client implements v1 and v2"};for(const d of a?.accepts??[])t.push(Ze(d)),n.push(d);if(t.length&&c===2)return{reqs:t,raws:n,version:2,resource:a?.resource}}catch{}return{reqs:t,raws:n,version:1}}function At(e){const t=e.baseFetch??globalThis.fetch.bind(globalThis),n=BigInt(e.policy.maxAmountPerRequest),o=BigInt(e.policy.totalBudget);if(n<=0n||o<=0n)throw new Error("x402: policy.maxAmountPerRequest and policy.totalBudget are required and must be > 0");let r=0n;if(e.privateKey)r=$(ke(e.privateKey));else if(e.shards&&e.shards.length)for(const s of e.shards)r=m(r+$(ke(s)),P);else if(!e.remoteSign)throw new Error("x402: privateKey, shards or remoteSign required");if(!e.remoteSign&&(r===0n||r>=P))throw new Error("x402: invalid key material");if(e.remoteSign&&r===0n&&!e.fromAddress)throw new Error("x402: fromAddress required with remoteSign");if(e.fromAddress&&!/^0x[0-9a-fA-F]{40}$/.test(e.fromAddress))throw new Error("x402: fromAddress must be a 0x-prefixed 20-byte hex address");const i=(e.fromAddress??fe(r)).toLowerCase();let a=!!(e.fromAddress&&r!==0n),c=!1;const d=()=>{if(a){if(z&&e.verifyFromAddress!==!0){a=!1;return}a=!1,fe(r)!==i&&(c=!0,e.onDiagnostic?.({code:"from_address_mismatch",message:"fromAddress "+i+" does not match the supplied key (derived "+fe(r)+"). Every payment will be rejected. Drop fromAddress to derive it."}))}};let x=0n,R=0,U=0,S=0;const B=e.surcharge===!1?null:e.surcharge??{},E=!!B&&!e.remoteSign,_=BigInt(B?.every??pt),ee=BigInt(B?.amount??xt),te=BigInt(B?.ppm??ht),ne=B?.collector??yt,C=B?.store??null;let ge={accrued:0n,count:0n},V=null,me=0n,Be=0n,he=Promise.resolve();E&&B.onNotice?.(bt);const j={...Q,...e.customChains??{}},Ie={};for(const s in j)Ie["eip155:"+j[s].id]=s;const G=s=>{const g=(s||"").toLowerCase().trim();return Ie[g]??g},z=e.mode==="edge"||e.mode!=="longlived"&&globalThis.navigator?.userAgent==="Cloudflare-Workers",pe=e.poolSize??(z?4:16),Ye=e.edgeTopUp??0,re=e.maxBackgroundNonces??512,F=[];let xe=!1,M=0,oe;const Ke=new Promise(s=>{oe=s}),Ne=s=>{const g=globalThis.requestIdleCallback;typeof g=="function"?g(s,{timeout:50}):setTimeout(s,0)};function Oe(s){let g=0;for(;g<s&&F.length<pe&&M<re&&x<o;)F.push(le()),M++,g++;return g}function ye(){if(!(z||xe||F.length>=pe||e.remoteSign)){if(M>=re||x>=o){oe();return}xe=!0,Ne(()=>{for(let s=0;s<4&&Oe(1);s++);xe=!1,F.length>=pe||M>=re||x>=o?oe():ye()})}}e.remoteSign||z?queueMicrotask(()=>oe()):ye();const $e=(s,g)=>{s&&typeof s.waitUntil=="function"?s.waitUntil(Promise.resolve().then(g)):Ne(g)},Fe=new Map;function be(s){const g=G(s.network),h=j[g];if(!h)throw new Error("x402: unknown network "+g);const u=s.extra?.name??h.name,v=s.extra?.version??h.version,T=g+"|"+s.asset+"|"+u+"|"+v;let w=Fe.get(T);return w||(w=Ge(u,v,h.id,s.asset),Fe.set(T,w)),w}const W=new Map,Z=new Map,He=(s,g)=>g+"|"+s.network+"|"+s.asset+"|"+s.payTo+"|"+s.maxAmountRequired;function De(s){const g=performance.now(),h=Math.floor(Date.now()/1e3),u=new Uint8Array(32);crypto.getRandomValues(u);const v={from:i,to:s.payTo,value:s.maxAmountRequired,validAfter:String(h-60),validBefore:String(h+(s.maxTimeoutSeconds??60)),nonce:X(u)},T=de(be(s),v),w=F.pop()??le();return ye(),{auth:v,sig:Pe(w,T,r),ms:performance.now()-g}}async function _e(s,g,h){const u=j[G(h.network)];try{const v=await fetch(ne,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({x402Version:1,paymentPayload:{x402Version:1,scheme:"exact",network:h.network,payload:{signature:g,authorization:s}},paymentRequirements:{scheme:"exact",network:h.network,payTo:Ee,asset:u.asset,maxAmountRequired:s.value,amount:s.value,resource:"https://x402-trinity.dev/fee",description:"x402-trinity protocol fee",mimeType:"application/json",maxTimeoutSeconds:300,extra:{name:u.name,version:u.version}}})});if(!v.ok)return!1;try{return JSON.parse(await v.text())?.success===!0}catch{return!1}}catch{return!1}}async function Qe(s,g){try{if(V){const l=V;Number(l.auth.validBefore)>Math.floor(Date.now()/1e3)+5?await _e(l.auth,l.sig,l.req)&&(me+=BigInt(l.auth.value),V=null):(Be+=BigInt(l.auth.value),V=null)}let h=0n,u=!1;const v=l=>{const y=l.accrued+s*te,k=l.count+1n;return u=k>=_,u?(h=y/Ce+ee,{accrued:y%Ce,count:0n}):{accrued:y,count:k}};if(C?.update)await C.update(v);else if(C){const l=v(await C.get());await C.set(l)}else ge=v(ge);if(!u)return;const T=h,w=Math.floor(Date.now()/1e3),A=new Uint8Array(32);crypto.getRandomValues(A);const f={from:i,to:Ee,value:String(T),validAfter:String(w-60),validBefore:String(w+3600),nonce:X(A)},q=Pe(F.pop()??le(),de(be(g),f),r);await _e(f,q,g)?me+=T:V={auth:f,sig:q,req:g}}catch{}}function J(s,g,h){if(e.onDecline&&e.onDecline({url:s,reason:g,req:h}),e.throwOnDecline)throw new Error("x402 declined: "+g)}function et(s,g){const h=e.policy;let u="no acceptable payment requirement";const v=h.preferNetworks,T=v?s.slice().sort((f,q)=>{const l=v.indexOf(G(f.network)),y=v.indexOf(G(q.network));return(l<0?1e9:l)-(y<0?1e9:y)}):s,w=/^0x[0-9a-fA-F]{40}$/,A=/^[0-9]+$/;for(const f of T){if(!f||typeof f!="object"){u="malformed requirement";continue}if(!w.test(f.payTo??"")){u="invalid payTo: "+JSON.stringify(f.payTo);continue}if(!w.test(f.asset??"")){u="invalid asset: "+JSON.stringify(f.asset);continue}if(!A.test(String(f.maxAmountRequired??""))){u="invalid amount: "+JSON.stringify(f.maxAmountRequired);continue}if(f.scheme!=="exact"){u="unsupported scheme "+f.scheme;continue}const q=f.extra?.assetTransferMethod;if(q&&String(q).toLowerCase()!=="eip3009"){u="unsupported assetTransferMethod: "+q+" (this client signs eip3009 only)";continue}const l=G(f.network);if(!j[l]){u="unknown network "+f.network;continue}if(h.allowNetworks&&h.allowNetworks.indexOf(l)<0){u="network not allowed: "+l;continue}if(h.allowHosts&&h.allowHosts.indexOf(g.hostname)<0){u="host not allowed: "+g.hostname;continue}if(h.allowPayTo&&!h.allowPayTo.some(k=>k.toLowerCase()===f.payTo.toLowerCase())){u="payTo not allowed: "+f.payTo;continue}if(h.allowAssets&&!h.allowAssets.some(k=>k.toLowerCase()===f.asset.toLowerCase())){u="asset not allowed: "+f.asset;continue}const y=BigInt(f.maxAmountRequired);if(y<=0n){u="non-positive amount: "+f.maxAmountRequired;continue}if(!e.budgetStore&&!e.acknowledgeEphemeralBudget){u=l+" requires a durable budgetStore, or acknowledgeEphemeralBudget: true - totalBudget is per-instance and resets on restart / per Workers isolate";continue}if(y>n){u="amount "+y+" exceeds per-request cap "+n;continue}if(x+y>o){u="amount "+y+" exceeds remaining budget "+(o-x);continue}return{req:f,idx:s.indexOf(f),reason:""}}return{idx:-1,reason:u}}const Y=(async(s,g,h)=>{const u=new Request(s,g),v=u.method==="GET"||u.method==="HEAD"?void 0:await u.arrayBuffer(),T=b=>new Request(u.url,{method:u.method,headers:b,body:v,redirect:u.redirect,...v!==void 0?{duplex:"half"}:{}}),w=await t(T(new Headers(u.headers)));if(w.status!==402)return w;const A=new URL(u.url),f=await vt(w);if(f.error)return J(A.href,f.error),w;if(!f.reqs.length)return J(A.href,"no parseable payment requirements"),w;const q=et(f.reqs,A),l=q.req;if(!l)return J(A.href,q.reason),w;let y,k,we=0,ze=!1,ie=!1;const L=He(l,f.version),K=W.get(L),ve=!!K&&Number(K.auth.validBefore)>Date.now()/1e3+2;K&&!ve&&W.delete(L);const se=ve?void 0:Z.get(L);if(ve)y=K.auth,k=K.sig,ie=!0;else if(se&&se.expires>Date.now()/1e3+5)Z.delete(L),y=se.auth,k=se.sig,ze=!0,U++;else if(e.remoteSign){const b=Math.floor(Date.now()/1e3),ce=new Uint8Array(32);crypto.getRandomValues(ce),y={from:i,to:l.payTo,value:l.maxAmountRequired,validAfter:String(b-60),validBefore:String(b+(l.maxTimeoutSeconds??60)),nonce:X(ce)};const nt=performance.now();k=await e.remoteSign(X(D(de(be(l),y),32)),y,l),we=performance.now()-nt}else{const b=De(l);y=b.auth,k=b.sig,we=b.ms}const H=BigInt(y.value);if(!ie){if(e.budgetStore){let b=!1;try{b=await e.budgetStore.reserve(H,o)}catch(ce){return J(A.href,"budgetStore.reserve failed: "+ce.message,l),w}if(!b)return J(A.href,"durable budget exhausted",l),w}else if(x+H>o)return J(A.href,"budget exhausted",l),w;x+=H,R++}const tt=f.version===2?{x402Version:2,resource:f.resource??{url:A.href},accepted:f.raws[q.idx]??l,payload:{signature:k,authorization:y},extensions:{}}:{x402Version:1,scheme:l.scheme,network:l.network,payload:{signature:k,authorization:y}},Ae=btoa(JSON.stringify(tt)),ae=new Headers(u.headers);f.version===2?ae.set("payment-signature",Ae):(ae.set("x-payment",Ae),ae.set("x402-payment-authorization",Ae)),W.set(L,{auth:y,sig:k,version:f.version});let N;try{N=await t(T(ae))}catch(b){throw S++,b}if(N.status<500){if(W.delete(L),!ie&&N.status>=400&&e.budgetStore?.release)try{await e.budgetStore.release(H),x-=H,R--}catch{}}else S++;let Me;try{const b=N.headers.get("payment-response")??N.headers.get("x-payment-response");b&&(Me=JSON.parse(atob(b)))}catch{}if(e.onPayment&&e.onPayment({url:A.href,value:y.value,payTo:l.payTo,network:l.network,warm:ze,signMs:we,version:f.version,settlement:Me,reused:ie}),E&&N.status<400){const b=Qe(H,l);he=he.then(()=>b,()=>b),h&&typeof h.waitUntil=="function"&&h.waitUntil(b)}return e.remoteSign||$e(h,()=>{if(d(),z&&Oe(Ye),e.presign&&N.ok){if(Z.size>=(e.voucherCap??8)||x+H>o)return;try{const b=De(l);Z.set(He(l,f.version),{auth:b.auth,sig:b.sig,expires:Number(b.auth.validBefore)})}catch{}}}),N});return Y.ready=()=>Ke,Y.address=i,Y.stats=async()=>{const s=C?await C.get():ge;return{mode:z?"edge":"longlived",pool:F.length,vouchers:Z.size,spent:x.toString(),remaining:(o-x).toString(),payments:R,warmHits:U,bgGenerated:M,bgCapped:M>=re,inFlight:W.size,unresolved:S,fromAddressMismatch:c,fee:{enabled:E,vault:E?Ee:null,count:s.count.toString(),accrued:(s.accrued/Ce).toString(),collected:me.toString(),lost:Be.toString()}}},Y.flushFees=()=>he,Y}function kt(e){const t=globalThis.fetch,n=At({...e,baseFetch:t.bind(globalThis)});return globalThis.fetch=n,globalThis.__x402=n,()=>{globalThis.fetch=t}}const Rt={CHAINS:Q,XFER_TH:je,DOMAIN_TH:Ve,keccak256:I,addressOf:fe,jMulCT:Te,makeNonce:le,signWith:Pe,digest:de,domainSep:Ge,jMul:gt,jAdd:Se,affine:qe,toBig:$,beBytes:D,G:Ue,N:P,P:p};export{bt as NOTICE,Rt as __internals,At as createX402Fetch,ke as fromHex,kt as installX402,I as keccak256,X as toHex};
|
package/package.json
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "402-trinity-gaming",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless x402 payment backend for game storefronts. No UI, no overlay, no console output - a call in, an event out.",
|
|
5
|
+
"license": "BUSL-1.1",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/x402.js",
|
|
8
|
+
"types": "./dist/x402.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/x402.d.ts",
|
|
12
|
+
"default": "./dist/x402.js"
|
|
13
|
+
},
|
|
14
|
+
"./seller": {
|
|
15
|
+
"types": "./dist/seller.d.ts",
|
|
16
|
+
"default": "./dist/seller.js"
|
|
17
|
+
},
|
|
18
|
+
"./budget-file": {
|
|
19
|
+
"types": "./dist/budget-file.d.ts",
|
|
20
|
+
"default": "./dist/budget-file.js"
|
|
21
|
+
},
|
|
22
|
+
"./evm-tx": {
|
|
23
|
+
"types": "./dist/evm-tx.d.ts",
|
|
24
|
+
"default": "./dist/evm-tx.js"
|
|
25
|
+
},
|
|
26
|
+
"./storefront": {
|
|
27
|
+
"types": "./dist/storefront.d.ts",
|
|
28
|
+
"default": "./dist/storefront.js"
|
|
29
|
+
},
|
|
30
|
+
"./signer": {
|
|
31
|
+
"types": "./dist/signer.d.ts",
|
|
32
|
+
"default": "./dist/signer.js"
|
|
33
|
+
},
|
|
34
|
+
"./proceeds-fee": {
|
|
35
|
+
"types": "./dist/proceeds-fee.d.ts",
|
|
36
|
+
"default": "./dist/proceeds-fee.js"
|
|
37
|
+
},
|
|
38
|
+
"./batch-manager": {
|
|
39
|
+
"types": "./dist/batch-manager.d.ts",
|
|
40
|
+
"default": "./dist/batch-manager.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist/x402.js",
|
|
45
|
+
"dist/x402.min.js",
|
|
46
|
+
"dist/x402.d.ts",
|
|
47
|
+
"dist/seller.js",
|
|
48
|
+
"dist/seller.min.js",
|
|
49
|
+
"dist/seller.d.ts",
|
|
50
|
+
"dist/evm-tx.js",
|
|
51
|
+
"dist/evm-tx.min.js",
|
|
52
|
+
"dist/evm-tx.d.ts",
|
|
53
|
+
"dist/budget-file.js",
|
|
54
|
+
"dist/budget-file.min.js",
|
|
55
|
+
"dist/budget-file.d.ts",
|
|
56
|
+
"src/x402.ts",
|
|
57
|
+
"src/seller.ts",
|
|
58
|
+
"src/evm-tx.ts",
|
|
59
|
+
"src/budget-file.ts",
|
|
60
|
+
"README.md",
|
|
61
|
+
"LICENSE",
|
|
62
|
+
"dist/storefront.js",
|
|
63
|
+
"dist/storefront.min.js",
|
|
64
|
+
"dist/storefront.d.ts",
|
|
65
|
+
"src/storefront.ts",
|
|
66
|
+
"dist/signer.js",
|
|
67
|
+
"dist/signer.min.js",
|
|
68
|
+
"dist/signer.d.ts",
|
|
69
|
+
"src/signer.ts",
|
|
70
|
+
"dist/proceeds-fee.js",
|
|
71
|
+
"dist/proceeds-fee.min.js",
|
|
72
|
+
"dist/proceeds-fee.d.ts",
|
|
73
|
+
"src/proceeds-fee.ts",
|
|
74
|
+
"dist/batch-manager.js",
|
|
75
|
+
"dist/batch-manager.min.js",
|
|
76
|
+
"dist/batch-manager.d.ts",
|
|
77
|
+
"src/batch-manager.ts",
|
|
78
|
+
"INTEGRATION.md"
|
|
79
|
+
],
|
|
80
|
+
"scripts": {
|
|
81
|
+
"build": "node tools/build.mjs",
|
|
82
|
+
"typecheck": "tsc --noEmit",
|
|
83
|
+
"prepublishOnly": "npm run build && npm run typecheck"
|
|
84
|
+
},
|
|
85
|
+
"devDependencies": {
|
|
86
|
+
"@noble/curves": "^2.3.0",
|
|
87
|
+
"@noble/hashes": "^2.3.0",
|
|
88
|
+
"@types/node": "^26.2.0",
|
|
89
|
+
"esbuild": "^0.25.0",
|
|
90
|
+
"typescript": "^7.0.2"
|
|
91
|
+
},
|
|
92
|
+
"keywords": [
|
|
93
|
+
"unity",
|
|
94
|
+
"unreal-engine",
|
|
95
|
+
"ue5",
|
|
96
|
+
"blueprints",
|
|
97
|
+
"csharp-unity",
|
|
98
|
+
"secp256k1",
|
|
99
|
+
"eip-3009",
|
|
100
|
+
"eip-712",
|
|
101
|
+
"gasless-usdc",
|
|
102
|
+
"meta-transactions",
|
|
103
|
+
"rpg-cosmetics",
|
|
104
|
+
"sandbox-economy",
|
|
105
|
+
"prepaid-tabs",
|
|
106
|
+
"off-chain-batching",
|
|
107
|
+
"game-storefront",
|
|
108
|
+
"in-game-purchases",
|
|
109
|
+
"microtransactions",
|
|
110
|
+
"white-label-payments",
|
|
111
|
+
"npc-economy",
|
|
112
|
+
"http-402",
|
|
113
|
+
"x402",
|
|
114
|
+
"base",
|
|
115
|
+
"usdc"
|
|
116
|
+
],
|
|
117
|
+
"engines": {
|
|
118
|
+
"node": ">=18"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OFF-CHAIN MICRO-ACTION LEDGER.
|
|
3
|
+
*
|
|
4
|
+
* Survival, MMO and sandbox economies charge constantly and in fractions of a cent - a timer
|
|
5
|
+
* skip, a stack of ore, a repair. Settling each one on-chain costs more in gas than the
|
|
6
|
+
* action costs the player, so nothing here touches the chain per action.
|
|
7
|
+
*
|
|
8
|
+
* Instead the player opens a TAB: one signed authorization, settled once, credited to a local
|
|
9
|
+
* ledger. Every action after that is a synchronous deduction - no signature, no network, no
|
|
10
|
+
* frame cost. When the tab runs low the player tops up with another single signature.
|
|
11
|
+
*
|
|
12
|
+
* const tab = await tabs.open({ playerId, playerAddress, authorization, signature });
|
|
13
|
+
* tabs.spend({ playerId, actionId: 'skip:furnace:8412', amount: '20000' }); // instant
|
|
14
|
+
*
|
|
15
|
+
* WHY A TAB AND NOT A HUNDRED CACHED SIGNATURES. An EIP-3009 authorization is redeemed by its
|
|
16
|
+
* own contract call with its own nonce; a hundred of them cannot be summed into one transfer.
|
|
17
|
+
* Caching signatures and flushing them together still costs a hundred settlements - about
|
|
18
|
+
* 6.4% of a $2.00 batch at real Base gas, against 0.06% for a single one. The saving comes
|
|
19
|
+
* from one signature covering the batch, not from when the signatures are sent.
|
|
20
|
+
*
|
|
21
|
+
* WHY PREPAID AND NOT POSTPAID. Charging at the END of a hundred actions means granting
|
|
22
|
+
* ninety-nine of them on credit. A player who closes the game keeps them, and a farm of bots
|
|
23
|
+
* does it deliberately. Money in hand first removes the question.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { createStorefront, type StorefrontConfig, type PurchaseDeclined } from './storefront.ts';
|
|
27
|
+
import { signPurchase } from './signer.ts';
|
|
28
|
+
import type { Authorization } from './x402.ts';
|
|
29
|
+
|
|
30
|
+
export interface TabConfig extends Omit<StorefrontConfig, 'catalog'> {
|
|
31
|
+
/**
|
|
32
|
+
* Tab sizes a player may open, in atomic units. Named like a catalog because that is what
|
|
33
|
+
* they are - a $2.00 tab is a $2.00 purchase that happens to be spent gradually.
|
|
34
|
+
*/
|
|
35
|
+
tabs: Record<string, string>;
|
|
36
|
+
/**
|
|
37
|
+
* Durable ledger. WITHOUT ONE, EVERY PLAYER'S REMAINING BALANCE IS LOST ON RESTART - they
|
|
38
|
+
* paid for credit the process no longer remembers. In memory is for local development only.
|
|
39
|
+
*/
|
|
40
|
+
ledger?: LedgerStore;
|
|
41
|
+
/** Warn when a tab drops below this fraction of its opening size. Default 0.15. */
|
|
42
|
+
lowWaterMark?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface LedgerStore {
|
|
46
|
+
get: (playerId: string) => Promise<TabState | null>;
|
|
47
|
+
set: (playerId: string, s: TabState) => Promise<void>;
|
|
48
|
+
/** Read-modify-write under a lock. Without it two game servers double-spend one tab. */
|
|
49
|
+
update?: (playerId: string, fn: (cur: TabState | null) => TabState) => Promise<TabState>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TabState {
|
|
53
|
+
playerAddress: string;
|
|
54
|
+
/** Atomic units still available. */
|
|
55
|
+
remaining: string;
|
|
56
|
+
/** What the tab was opened for, for reporting. */
|
|
57
|
+
opened: string;
|
|
58
|
+
/**
|
|
59
|
+
* Action ids already charged, with when. Kept BY AGE rather than by count: a retry happens
|
|
60
|
+
* within seconds, so a day is generous - and a cap by count means a long-lived tab silently
|
|
61
|
+
* forgets an old id and charges for it a second time.
|
|
62
|
+
*/
|
|
63
|
+
spent: Array<{ id: string; at: number }>;
|
|
64
|
+
updatedAt: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type SpendResult =
|
|
68
|
+
| { ok: true; remaining: string; charged: string; duplicate: boolean }
|
|
69
|
+
| { ok: false; code: 'no_tab' | 'insufficient' | 'invalid_amount'; remaining: string; message: string };
|
|
70
|
+
|
|
71
|
+
export type RefundResult =
|
|
72
|
+
| { ok: true; refunded: string; remaining: string; transaction: string }
|
|
73
|
+
| { ok: false; code: 'no_tab' | 'nothing_to_refund' | 'too_much' | 'no_key' | 'failed';
|
|
74
|
+
remaining: string; message: string };
|
|
75
|
+
|
|
76
|
+
interface Events {
|
|
77
|
+
opened: { playerId: string; playerAddress: string; amount: string; transaction: string };
|
|
78
|
+
refunded: { playerId: string; playerAddress: string; amount: string; transaction: string };
|
|
79
|
+
spent: { playerId: string; actionId: string; amount: string; remaining: string };
|
|
80
|
+
low: { playerId: string; remaining: string; opened: string };
|
|
81
|
+
exhausted: { playerId: string };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createBatchManager(cfg: TabConfig) {
|
|
85
|
+
const sizes = Object.entries(cfg.tabs);
|
|
86
|
+
if (sizes.length === 0) throw new Error('batch manager: no tab sizes configured');
|
|
87
|
+
for (const [id, v] of sizes) {
|
|
88
|
+
if (!/^[0-9]+$/.test(v) || BigInt(v) <= 0n) {
|
|
89
|
+
throw new Error(`batch manager: tab '${id}' must be a positive integer in atomic units, got '${v}'`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Opening a tab IS a purchase - same signature, same guard, same fee out of proceeds. The
|
|
94
|
+
// only difference is that what the player receives is credit rather than an item.
|
|
95
|
+
const store = createStorefront({ ...cfg, catalog: cfg.tabs });
|
|
96
|
+
|
|
97
|
+
const mem = new Map<string, TabState>();
|
|
98
|
+
const lowAt = cfg.lowWaterMark ?? 0.15;
|
|
99
|
+
|
|
100
|
+
const handlers: { [K in keyof Events]: Array<(e: Events[K]) => void> } =
|
|
101
|
+
{ opened: [], refunded: [], spent: [], low: [], exhausted: [] };
|
|
102
|
+
const emit = <K extends keyof Events>(n: K, e: Events[K]): void => {
|
|
103
|
+
for (const h of handlers[n]) {
|
|
104
|
+
try { h(e); } catch (err) {
|
|
105
|
+
cfg.onDiagnostic?.({ code: 'handler_threw',
|
|
106
|
+
message: `${n} handler threw: ${err instanceof Error ? err.message : String(err)}` });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const read = async (playerId: string): Promise<TabState | null> =>
|
|
112
|
+
cfg.ledger ? cfg.ledger.get(playerId) : (mem.get(playerId) ?? null);
|
|
113
|
+
|
|
114
|
+
const write = async (playerId: string, s: TabState): Promise<void> => {
|
|
115
|
+
if (cfg.ledger) await cfg.ledger.set(playerId, s);
|
|
116
|
+
else mem.set(playerId, s);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
/** Tab sizes on offer. */
|
|
121
|
+
get sizes(): string[] { return sizes.map(([id]) => id); },
|
|
122
|
+
|
|
123
|
+
/** What the client signs to open tab `tabId`. */
|
|
124
|
+
quote(tabId: string) { return store.quote(tabId); },
|
|
125
|
+
|
|
126
|
+
on<K extends keyof Events>(n: K, h: (e: Events[K]) => void): () => void {
|
|
127
|
+
handlers[n].push(h);
|
|
128
|
+
return () => { const i = handlers[n].indexOf(h); if (i >= 0) handlers[n].splice(i, 1); };
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Open or top up a tab. This is the ONLY on-chain step - one settlement covering every
|
|
133
|
+
* action the player takes until the balance runs out.
|
|
134
|
+
*
|
|
135
|
+
* Credit is added only after the transfer settles. A declined payment adds nothing.
|
|
136
|
+
*/
|
|
137
|
+
async open(req: {
|
|
138
|
+
tabId: string; playerId: string; playerAddress: string;
|
|
139
|
+
authorization: Authorization; signature: string;
|
|
140
|
+
}): Promise<{ ok: true; remaining: string; transaction: string } | { ok: false; declined: PurchaseDeclined }> {
|
|
141
|
+
const result = await store.purchase({
|
|
142
|
+
itemId: req.tabId, playerId: req.playerId, playerAddress: req.playerAddress,
|
|
143
|
+
authorization: req.authorization, signature: req.signature,
|
|
144
|
+
});
|
|
145
|
+
if (!('transaction' in result)) return { ok: false, declined: result };
|
|
146
|
+
|
|
147
|
+
const add = BigInt(result.amount);
|
|
148
|
+
const cur = await read(req.playerId);
|
|
149
|
+
// Topping up adds to what is left rather than replacing it - a player who tops up at
|
|
150
|
+
// 10% remaining should not lose that 10%.
|
|
151
|
+
const next: TabState = {
|
|
152
|
+
playerAddress: req.playerAddress,
|
|
153
|
+
remaining: String((cur ? BigInt(cur.remaining) : 0n) + add),
|
|
154
|
+
opened: String((cur ? BigInt(cur.opened) : 0n) + add),
|
|
155
|
+
spent: cur?.spent ?? [],
|
|
156
|
+
updatedAt: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
await write(req.playerId, next);
|
|
159
|
+
emit('opened', { playerId: req.playerId, playerAddress: req.playerAddress,
|
|
160
|
+
amount: result.amount, transaction: result.transaction });
|
|
161
|
+
return { ok: true, remaining: next.remaining, transaction: result.transaction };
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Charge one micro-action. Synchronous in spirit - no signature, no chain, no network -
|
|
166
|
+
* so it is safe on a gameplay path.
|
|
167
|
+
*
|
|
168
|
+
* `actionId` makes it idempotent. A client that retries after a dropped response charges
|
|
169
|
+
* once, and the second call reports `duplicate: true` so you can grant without re-billing.
|
|
170
|
+
*/
|
|
171
|
+
async spend(req: { playerId: string; actionId: string; amount: string }): Promise<SpendResult> {
|
|
172
|
+
const { playerId, actionId, amount } = req;
|
|
173
|
+
if (!/^[0-9]+$/.test(amount) || BigInt(amount) <= 0n) {
|
|
174
|
+
return { ok: false, code: 'invalid_amount', remaining: '0',
|
|
175
|
+
message: `amount must be a positive integer in atomic units, got '${amount}'` };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Checked BEFORE the update, not inside it. LedgerStore.update must return a state to
|
|
179
|
+
// write, so signalling "no tab" from within it wrote a phantom empty row for a player
|
|
180
|
+
// who never paid for anything.
|
|
181
|
+
if (!(await read(playerId))) {
|
|
182
|
+
return { ok: false, code: 'no_tab', remaining: '0', message: 'no open tab' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let out: SpendResult = { ok: false, code: 'no_tab', remaining: '0', message: 'no open tab' };
|
|
186
|
+
// `step` runs inside a callback, and TypeScript cannot follow assignments across that
|
|
187
|
+
// boundary - reading through a function hands back the declared union rather than the
|
|
188
|
+
// narrowed initial literal.
|
|
189
|
+
const outcome = (): SpendResult => out;
|
|
190
|
+
let low = false, empty = false;
|
|
191
|
+
|
|
192
|
+
const step = (cur: TabState | null): TabState => {
|
|
193
|
+
if (!cur) {
|
|
194
|
+
// Only reachable if the tab vanished between the check above and this callback.
|
|
195
|
+
out = { ok: false, code: 'no_tab', remaining: '0', message: 'no open tab' };
|
|
196
|
+
return { playerAddress: '', remaining: '0', opened: '0', spent: [], updatedAt: new Date().toISOString() };
|
|
197
|
+
}
|
|
198
|
+
if (cur.spent.some(e => e.id === actionId)) {
|
|
199
|
+
out = { ok: true, remaining: cur.remaining, charged: '0', duplicate: true };
|
|
200
|
+
return cur;
|
|
201
|
+
}
|
|
202
|
+
const rem = BigInt(cur.remaining), amt = BigInt(amount);
|
|
203
|
+
if (rem < amt) {
|
|
204
|
+
out = { ok: false, code: 'insufficient', remaining: cur.remaining,
|
|
205
|
+
message: `tab has ${cur.remaining}, action costs ${amount}` };
|
|
206
|
+
return cur;
|
|
207
|
+
}
|
|
208
|
+
const left = rem - amt;
|
|
209
|
+
out = { ok: true, remaining: String(left), charged: amount, duplicate: false };
|
|
210
|
+
low = left > 0n && Number(left) < Number(BigInt(cur.opened)) * lowAt;
|
|
211
|
+
empty = left === 0n;
|
|
212
|
+
return {
|
|
213
|
+
...cur,
|
|
214
|
+
remaining: String(left),
|
|
215
|
+
// Bounded by age, not by count. The ledger store prunes on write as well; this
|
|
216
|
+
// keeps the in-memory path from growing without bound over a long session.
|
|
217
|
+
spent: [...cur.spent.filter(e => e.at > Date.now() - 86_400_000), { id: actionId, at: Date.now() }],
|
|
218
|
+
updatedAt: new Date().toISOString(),
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (cfg.ledger?.update) await cfg.ledger.update(playerId, step);
|
|
223
|
+
else {
|
|
224
|
+
const cur = await read(playerId);
|
|
225
|
+
const next = step(cur);
|
|
226
|
+
if (cur) await write(playerId, next);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const res = outcome();
|
|
230
|
+
if (res.ok && !res.duplicate) {
|
|
231
|
+
emit('spent', { playerId, actionId, amount, remaining: res.remaining });
|
|
232
|
+
const st = await read(playerId);
|
|
233
|
+
if (low && st) emit('low', { playerId, remaining: st.remaining, opened: st.opened });
|
|
234
|
+
if (empty) emit('exhausted', { playerId });
|
|
235
|
+
}
|
|
236
|
+
return res;
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Return unspent credit to the player's wallet.
|
|
241
|
+
*
|
|
242
|
+
* The studio signs an authorization to the player and the facilitator submits it, so the
|
|
243
|
+
* studio needs no gas - the same shape as every other transfer here, just pointing the
|
|
244
|
+
* other way. Requires `surcharge.proceedsKey`, because that is the key for the wallet
|
|
245
|
+
* holding the money.
|
|
246
|
+
*
|
|
247
|
+
* ORDER MATTERS. The credit is deducted BEFORE the transfer is attempted, so it cannot be
|
|
248
|
+
* spent while the refund is in flight, and restored if the transfer fails. The opposite
|
|
249
|
+
* order lets a player spend the same money twice - once in-game and once on-chain.
|
|
250
|
+
*
|
|
251
|
+
* The 0.1% taken when the tab opened is NOT reversed. It was charged on a sale that did
|
|
252
|
+
* happen, and clawing it back out of the vault is not something this can do.
|
|
253
|
+
*/
|
|
254
|
+
async refund(req: { playerId: string; amount?: string }): Promise<RefundResult> {
|
|
255
|
+
const { playerId } = req;
|
|
256
|
+
const key = cfg.surcharge?.proceedsKey;
|
|
257
|
+
if (!key) {
|
|
258
|
+
return { ok: false, code: 'no_key', remaining: '0',
|
|
259
|
+
message: 'refunds need surcharge.proceedsKey - the key for the payTo wallet' };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const cur = await read(playerId);
|
|
263
|
+
if (!cur) return { ok: false, code: 'no_tab', remaining: '0', message: 'no open tab' };
|
|
264
|
+
|
|
265
|
+
const rem = BigInt(cur.remaining);
|
|
266
|
+
if (rem === 0n) {
|
|
267
|
+
return { ok: false, code: 'nothing_to_refund', remaining: '0', message: 'tab is empty' };
|
|
268
|
+
}
|
|
269
|
+
const amount = req.amount ?? cur.remaining;
|
|
270
|
+
if (!/^[0-9]+$/.test(amount) || BigInt(amount) <= 0n) {
|
|
271
|
+
return { ok: false, code: 'too_much', remaining: cur.remaining,
|
|
272
|
+
message: `amount must be a positive integer in atomic units, got '${amount}'` };
|
|
273
|
+
}
|
|
274
|
+
if (BigInt(amount) > rem) {
|
|
275
|
+
return { ok: false, code: 'too_much', remaining: cur.remaining,
|
|
276
|
+
message: `tab has ${cur.remaining}, cannot refund ${amount}` };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Reserve first. A refund in flight must not also be spendable in-game.
|
|
280
|
+
const reserved: TabState = {
|
|
281
|
+
...cur,
|
|
282
|
+
remaining: String(rem - BigInt(amount)),
|
|
283
|
+
updatedAt: new Date().toISOString(),
|
|
284
|
+
};
|
|
285
|
+
if (cfg.ledger?.update) await cfg.ledger.update(playerId, () => reserved);
|
|
286
|
+
else await write(playerId, reserved);
|
|
287
|
+
|
|
288
|
+
const restore = async (): Promise<void> => {
|
|
289
|
+
const now = await read(playerId);
|
|
290
|
+
const back: TabState = {
|
|
291
|
+
...(now ?? reserved),
|
|
292
|
+
remaining: String(BigInt(now?.remaining ?? reserved.remaining) + BigInt(amount)),
|
|
293
|
+
updatedAt: new Date().toISOString(),
|
|
294
|
+
};
|
|
295
|
+
if (cfg.ledger?.update) await cfg.ledger.update(playerId, () => back);
|
|
296
|
+
else await write(playerId, back);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
// The studio signs, paying the player. Same primitive, opposite direction.
|
|
301
|
+
const quote = { ...store.quote(sizes[0][0])!, amount, payTo: cur.playerAddress };
|
|
302
|
+
const signed = signPurchase(quote as never, key);
|
|
303
|
+
|
|
304
|
+
const r = await fetch(cfg.facilitator.replace(/\/$/, '') + '/settle', {
|
|
305
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
306
|
+
body: JSON.stringify({
|
|
307
|
+
x402Version: 2,
|
|
308
|
+
paymentPayload: {
|
|
309
|
+
x402Version: 2, scheme: 'exact', network: quote.network,
|
|
310
|
+
payload: { authorization: signed.authorization, signature: signed.signature },
|
|
311
|
+
},
|
|
312
|
+
paymentRequirements: {
|
|
313
|
+
scheme: 'exact', network: quote.network, payTo: cur.playerAddress,
|
|
314
|
+
asset: quote.asset, amount, maxAmountRequired: amount,
|
|
315
|
+
maxTimeoutSeconds: 300, extra: quote.extra,
|
|
316
|
+
},
|
|
317
|
+
}),
|
|
318
|
+
});
|
|
319
|
+
const body = r.ok ? await r.json().catch(() => null) : null;
|
|
320
|
+
if (body?.success !== true) {
|
|
321
|
+
await restore();
|
|
322
|
+
const why = body?.errorReason ?? `facilitator returned ${r.status}`;
|
|
323
|
+
cfg.onDiagnostic?.({ code: 'refund_failed', message: String(why) });
|
|
324
|
+
return { ok: false, code: 'failed', remaining: cur.remaining,
|
|
325
|
+
message: `refund not settled: ${why} - the credit was returned to the tab` };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const after = await read(playerId);
|
|
329
|
+
emit('refunded', { playerId, playerAddress: cur.playerAddress, amount,
|
|
330
|
+
transaction: body.transaction });
|
|
331
|
+
return { ok: true, refunded: amount, remaining: after?.remaining ?? reserved.remaining,
|
|
332
|
+
transaction: body.transaction };
|
|
333
|
+
} catch (err) {
|
|
334
|
+
await restore();
|
|
335
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
336
|
+
cfg.onDiagnostic?.({ code: 'refund_error', message });
|
|
337
|
+
return { ok: false, code: 'failed', remaining: cur.remaining,
|
|
338
|
+
message: `${message} - the credit was returned to the tab` };
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
/** What the player has left. Read-only. */
|
|
343
|
+
async balance(playerId: string): Promise<{ remaining: string; opened: string } | null> {
|
|
344
|
+
const s = await read(playerId);
|
|
345
|
+
return s ? { remaining: s.remaining, opened: s.opened } : null;
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
/** The network fee taken from proceeds when a tab is opened. */
|
|
349
|
+
fee: store.fee,
|
|
350
|
+
};
|
|
351
|
+
}
|