@three-ws/x402-payment-modal 1.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/CHANGELOG.md +57 -0
- package/LICENSE +180 -0
- package/README.md +347 -0
- package/TUTORIAL.md +188 -0
- package/dist/index.d.ts +141 -0
- package/dist/x402.js +1777 -0
- package/dist/x402.min.js +375 -0
- package/docs/api-reference.md +227 -0
- package/docs/architecture.md +171 -0
- package/docs/server-setup.md +239 -0
- package/docs/siwx.md +116 -0
- package/docs/spending-caps.md +92 -0
- package/docs/theming.md +124 -0
- package/examples/README.md +22 -0
- package/examples/plain-html/index.html +229 -0
- package/examples/react/README.md +69 -0
- package/examples/react/X402Button.jsx +84 -0
- package/examples/server-express/package.json +16 -0
- package/examples/server-express/public/index.html +89 -0
- package/examples/server-express/server.js +89 -0
- package/package.json +113 -0
- package/server/README.md +68 -0
- package/server/checkout.js +392 -0
- package/server/express.js +44 -0
- package/server/vercel.js +54 -0
- package/src/index.js +1776 -0
- package/types/index.d.ts +141 -0
- package/types/server.d.ts +109 -0
package/dist/x402.min.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/*! @three-ws/x402-payment-modal v1.1.0 — Apache-2.0 — https://three.ws */
|
|
2
|
+
const te="1.1.0",h={checkoutOrigin:null,checkoutPath:"/api/x402-checkout",brand:{name:"three.ws",url:"https://three.ws"},footerNote:"x402 \xB7 onchain settled",builderCode:{wallet:"3d_agent",service:"3d_agent_modal"},esm:{solanaWeb3:"https://esm.sh/@solana/web3.js@1.95.3?bundle",nobleHashesSha3:"https://esm.sh/@noble/hashes@1.4.0/sha3?bundle"}};function j(n={}){if(!n||typeof n!="object")return h;for(const e of["checkoutOrigin","checkoutPath","footerNote"])n[e]!==void 0&&(h[e]=n[e]);return n.brand&&typeof n.brand=="object"&&Object.assign(h.brand,n.brand),n.builderCode&&typeof n.builderCode=="object"&&Object.assign(h.builderCode,n.builderCode),n.esm&&typeof n.esm=="object"&&Object.assign(h.esm,n.esm),h}function L(){return`${h.checkoutOrigin||oe}${h.checkoutPath}`}const ne="SIGN-IN-WITH-X",re="sign-in-with-x",oe=(()=>{try{const n=document.currentScript;if(n?.src)return new URL(n.src).origin;const e=document.querySelector('script[src*="/x402.js"]');if(e?.src)return new URL(e.src).origin}catch{}return location.origin})(),C={"eip155:8453":{chainId:8453,name:"Base",explorer:"https://basescan.org/tx/"},"eip155:84532":{chainId:84532,name:"Base Sepolia",explorer:"https://sepolia.basescan.org/tx/"},"eip155:42161":{chainId:42161,name:"Arbitrum",explorer:"https://arbiscan.io/tx/"},"eip155:10":{chainId:10,name:"Optimism",explorer:"https://optimistic.etherscan.io/tx/"}};function Be(n){if(!n||typeof n!="object")return n;const e=n.amount??n.maxAmountRequired;return e!=null&&n.amount==null?{...n,amount:String(e)}:n}const T="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",P="FeMbDoX7R1Psc4GEcvJdsbNbZA3bfztcyDCatJVJpump",B=Object.freeze({[T]:{symbol:"USDC",name:"USD Coin",decimals:6,stable:!0},[P]:{symbol:"THREE",name:"THREE",decimals:6,accent:"#7aa2ff",glyph:"\u25C6"}});function y(n){const e=n&&n.asset&&B[n.asset]||null,t=n?.extra?.name,r=(t?String(t).replace(/^USD Coin$/,"USDC"):e?.symbol)||"USDC",o=Number(n?.extra?.decimals??e?.decimals??6),s=e?.stable||U.has(String(t||"").toLowerCase());return{symbol:r,decimals:o,stable:s,accent:e?.accent||null,glyph:e?.glyph||null}}function Ne(n){return n?.asset===P}function q(n){return(n?.accepts||[]).filter(e=>N(e.network))}function R(n){return n.find(e=>e.asset===T)||n.find(e=>y(e).stable)||n[0]||null}function N(n){return typeof n=="string"&&(n==="solana"||n.startsWith("solana:"))}function se(n){return typeof n=="string"&&n.startsWith("eip155:")}function D(n){if(!se(n?.network))return!1;const e=n?.extra?.assetTransferMethod;return!e||e==="eip3009"}function b(n,e){return N(n)?"Solana":C[n]?.name||e?.extra?.name||n}function ae(n,e){if(!e)return null;if(N(n))return`https://solscan.io/tx/${e}`;const t=C[n];return t?`${t.explorer}${e}`:null}function v(n,e=6){const t=Number(n)/10**e;return t<.01?t.toFixed(6).replace(/0+$/,"").replace(/\.$/,""):t<1?t.toFixed(4).replace(/0+$/,"").replace(/\.$/,""):t.toFixed(2)}function ie(n){const e=JSON.stringify(n);return typeof Buffer<"u"?Buffer.from(e,"utf8").toString("base64"):btoa(unescape(encodeURIComponent(e)))}function I(n){if(!n)return null;try{const e=typeof Buffer<"u"?Buffer.from(n,"base64").toString("utf8"):decodeURIComponent(escape(atob(n)));return JSON.parse(e)}catch{return null}}const ce="x402.spend.",U=new Set(["usdc","usd coin","usdt","tether","binance-peg usd coin","dai"]);function de(n=Date.now()){const e=Math.floor(n/36e5),t=Math.floor(n/864e5);return{hour:e,day:t}}function H(n,e,t){return`${ce}${e}.${n.toLowerCase()}.${t}`}function k(n,e,t){try{const r=localStorage.getItem(H(n,e,t));return r?BigInt(r):0n}catch{return 0n}}function S(n,e,t,r){try{localStorage.setItem(H(n,e,t),r.toString())}catch{}}function le(n,e){const t=BigInt(n),r=e&&e.asset&&B[e.asset]||null,o=Number(e?.extra?.decimals??r?.decimals??6),s=String(e?.extra?.name||"").toLowerCase();return U.has(s)||r?.stable?o===6?t:o>6?t/10n**BigInt(o-6):t*10n**BigInt(6-o):t}function F({accept:n,caps:e,address:t}){if(!e||!t)return{abort:!1};const r=le(n.amount,n),o=e.maxPerCall!=null?BigInt(e.maxPerCall):null,s=e.maxPerHour!=null?BigInt(e.maxPerHour):null,i=e.maxPerDay!=null?BigInt(e.maxPerDay):null;if(o!=null&&r>o)return{abort:!0,reason:`Per-call cap exceeded (${r} > ${o} \xB5USD)`};const a=de(),l=k(t,"hr",a.hour)+r,d=k(t,"day",a.day)+r;return s!=null&&l>s?{abort:!0,reason:`Hourly cap exceeded (${l} > ${s} \xB5USD)`}:i!=null&&d>i?{abort:!0,reason:`Daily cap exceeded (${d} > ${i} \xB5USD)`}:(S(t,"hr",a.hour,l),S(t,"day",a.day,d),{abort:!1,reservation:{address:t,microUsd:r,buckets:a}})}function z(n){if(!n)return;const{address:e,microUsd:t,buckets:r}=n,o=k(e,"hr",r.hour),s=k(e,"day",r.day),i=o-t,a=s-t;S(e,"hr",r.hour,i<0n?0n:i),S(e,"day",r.day,a<0n?0n:a)}const ue="builder-code",_=/^[a-z0-9_]{1,32}$/;function W(n){const t=n?.extensions?.[ue]?.info?.a;if(!t||!_.test(t))return null;const r={a:t},o=h.builderCode.service,s=h.builderCode.wallet;return o&&_.test(o)&&(r.s=[o]),s&&_.test(s)&&(r.w=s),r}function fe(n){const e=n?.extensions?.[re];return!e||!e.info||!Array.isArray(e.supportedChains)||!e.supportedChains.length?null:e}function J(n,e){for(const t of n.supportedChains){if(e==="evm"&&t.type==="eip191")return{chain:t,kind:"evm"};if(e==="solana"&&t.type==="ed25519")return{chain:t,kind:"solana"}}return null}function V(n,e,t){const r=e.type==="eip191",o=r?`${n.domain} wants you to sign in with your Ethereum account:`:`${n.domain} wants you to sign in with your Solana account:`,[,s=""]=String(e.chainId).split(":"),i=r?String(parseInt(s,10)):s,a=[o,t,""];if(n.statement?a.push(n.statement,""):r&&a.push(""),a.push(`URI: ${n.uri}`),a.push(`Version: ${n.version||"1"}`),a.push(`Chain ID: ${i}`),a.push(`Nonce: ${n.nonce}`),a.push(`Issued At: ${n.issuedAt}`),n.expirationTime&&a.push(`Expiration Time: ${n.expirationTime}`),n.notBefore&&a.push(`Not Before: ${n.notBefore}`),n.requestId!==void 0&&n.requestId!==null&&a.push(`Request ID: ${n.requestId}`),Array.isArray(n.resources)&&n.resources.length){a.push("Resources:");for(const l of n.resources)a.push(`- ${l}`)}return a.join(`
|
|
3
|
+
`)}function pe(n){const e=JSON.stringify(n);return typeof Buffer<"u"?Buffer.from(e,"utf8").toString("base64"):btoa(unescape(encodeURIComponent(e)))}const K="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";function he(n){if(!n||n.length===0)return"";let e=0;for(;e<n.length&&n[e]===0;)e++;let t=0n;for(let o=0;o<n.length;o++)t=t<<8n|BigInt(n[o]);let r="";for(;t>0n;)r=K[Number(t%58n)]+r,t/=58n;for(let o=0;o<e;o++)r=K[0]+r;return r}let E=null;async function xe(){if(E)return E;const e=(await import(h.esm.nobleHashesSha3)).keccak_256;return E=t=>{const r=String(t).toLowerCase().replace(/^0x/,"");if(!/^[0-9a-f]{40}$/.test(r))throw new Error(`invalid EVM address: ${t}`);const o=e(new TextEncoder().encode(r));let s="";for(let a=0;a<o.length;a++)s+=o[a].toString(16).padStart(2,"0");let i="0x";for(let a=0;a<40;a++)i+=parseInt(s[a],16)>=8?r[a].toUpperCase():r[a];return i},E}const G="x402-styles",me=`
|
|
4
|
+
:root {
|
|
5
|
+
--x402-z: 2147483600;
|
|
6
|
+
}
|
|
7
|
+
.x402-overlay {
|
|
8
|
+
position: fixed; inset: 0;
|
|
9
|
+
background: rgba(8, 10, 18, 0.55);
|
|
10
|
+
backdrop-filter: blur(10px);
|
|
11
|
+
-webkit-backdrop-filter: blur(10px);
|
|
12
|
+
display: flex; align-items: center; justify-content: center;
|
|
13
|
+
z-index: var(--x402-z);
|
|
14
|
+
opacity: 0; transition: opacity 0.16s ease-out;
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
16
|
+
-webkit-font-smoothing: antialiased;
|
|
17
|
+
color: #0f0f0f;
|
|
18
|
+
}
|
|
19
|
+
.x402-overlay.x402-open { opacity: 1; }
|
|
20
|
+
.x402-overlay * { box-sizing: border-box; }
|
|
21
|
+
.x402-modal {
|
|
22
|
+
width: calc(100% - 32px); max-width: 420px;
|
|
23
|
+
background: #ffffff;
|
|
24
|
+
border-radius: 18px;
|
|
25
|
+
box-shadow: 0 24px 80px rgba(8, 10, 18, 0.28), 0 4px 16px rgba(8, 10, 18, 0.12);
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
transform: translateY(8px) scale(0.985);
|
|
28
|
+
transition: transform 0.18s ease-out;
|
|
29
|
+
display: flex; flex-direction: column;
|
|
30
|
+
max-height: calc(100dvh - 32px);
|
|
31
|
+
}
|
|
32
|
+
.x402-overlay.x402-open .x402-modal { transform: translateY(0) scale(1); }
|
|
33
|
+
.x402-head {
|
|
34
|
+
padding: 18px 20px 14px;
|
|
35
|
+
border-bottom: 1px solid #eef0f4;
|
|
36
|
+
display: flex; align-items: center; gap: 12px;
|
|
37
|
+
}
|
|
38
|
+
.x402-head .x402-merchant {
|
|
39
|
+
flex: 1; min-width: 0;
|
|
40
|
+
}
|
|
41
|
+
.x402-merchant .x402-name {
|
|
42
|
+
font-size: 12px; color: #5a6378; font-weight: 600; letter-spacing: 0.02em; text-transform: uppercase;
|
|
43
|
+
margin-bottom: 2px;
|
|
44
|
+
}
|
|
45
|
+
.x402-merchant .x402-action {
|
|
46
|
+
font-size: 17px; font-weight: 700; color: #0f0f0f;
|
|
47
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
48
|
+
letter-spacing: -0.01em;
|
|
49
|
+
}
|
|
50
|
+
.x402-close {
|
|
51
|
+
width: 32px; height: 32px;
|
|
52
|
+
border-radius: 8px; border: none; background: #f3f4f7;
|
|
53
|
+
font-size: 16px; color: #5a6378; cursor: pointer;
|
|
54
|
+
display: flex; align-items: center; justify-content: center;
|
|
55
|
+
transition: background 0.12s;
|
|
56
|
+
}
|
|
57
|
+
.x402-close:hover { background: #e7e9ee; color: #0f0f0f; }
|
|
58
|
+
|
|
59
|
+
.x402-price-row {
|
|
60
|
+
padding: 18px 20px;
|
|
61
|
+
display: flex; align-items: baseline; justify-content: space-between;
|
|
62
|
+
background: linear-gradient(180deg, #fafbfc 0%, #ffffff 100%);
|
|
63
|
+
border-bottom: 1px solid #eef0f4;
|
|
64
|
+
}
|
|
65
|
+
.x402-price {
|
|
66
|
+
font-size: 32px; font-weight: 700; letter-spacing: -0.02em; color: #0f0f0f;
|
|
67
|
+
font-variant-numeric: tabular-nums;
|
|
68
|
+
}
|
|
69
|
+
.x402-price .x402-currency { font-size: 14px; color: #5a6378; font-weight: 600; margin-left: 6px; letter-spacing: 0; }
|
|
70
|
+
.x402-network {
|
|
71
|
+
font-size: 12px; color: #5a6378; font-weight: 500;
|
|
72
|
+
background: #f3f4f7; padding: 5px 10px; border-radius: 99px;
|
|
73
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
74
|
+
}
|
|
75
|
+
.x402-network::before {
|
|
76
|
+
content: ''; width: 6px; height: 6px; border-radius: 50%;
|
|
77
|
+
background: #22c55e;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.x402-body {
|
|
81
|
+
padding: 16px 20px 18px;
|
|
82
|
+
flex: 1 1 auto; overflow-y: auto;
|
|
83
|
+
display: flex; flex-direction: column; gap: 10px;
|
|
84
|
+
}
|
|
85
|
+
.x402-step {
|
|
86
|
+
display: flex; gap: 12px; align-items: flex-start;
|
|
87
|
+
padding: 10px 0;
|
|
88
|
+
}
|
|
89
|
+
.x402-step + .x402-step { border-top: 1px solid #f3f4f7; }
|
|
90
|
+
.x402-step-num {
|
|
91
|
+
width: 22px; height: 22px; flex: 0 0 auto;
|
|
92
|
+
border-radius: 50%; border: 1.5px solid #d0d4dd; background: #fff;
|
|
93
|
+
color: #5a6378;
|
|
94
|
+
font-size: 11px; font-weight: 700;
|
|
95
|
+
display: flex; align-items: center; justify-content: center;
|
|
96
|
+
}
|
|
97
|
+
.x402-step.x402-active .x402-step-num {
|
|
98
|
+
border-color: #0a84ff; background: #0a84ff; color: #fff;
|
|
99
|
+
animation: x402-spin 1.2s linear infinite;
|
|
100
|
+
}
|
|
101
|
+
.x402-step.x402-done .x402-step-num {
|
|
102
|
+
border-color: #22c55e; background: #22c55e; color: #fff;
|
|
103
|
+
}
|
|
104
|
+
.x402-step.x402-error .x402-step-num {
|
|
105
|
+
border-color: #ef4444; background: #ef4444; color: #fff;
|
|
106
|
+
}
|
|
107
|
+
@keyframes x402-spin {
|
|
108
|
+
from { box-shadow: 0 0 0 0 rgba(10, 132, 255, 0.4); }
|
|
109
|
+
to { box-shadow: 0 0 0 8px rgba(10, 132, 255, 0); }
|
|
110
|
+
}
|
|
111
|
+
.x402-step-body { flex: 1; min-width: 0; }
|
|
112
|
+
.x402-step-label { font-size: 14px; font-weight: 600; color: #0f0f0f; line-height: 1.35; }
|
|
113
|
+
.x402-step-meta { font-size: 12px; color: #5a6378; margin-top: 2px; font-feature-settings: 'tnum' 1; }
|
|
114
|
+
.x402-step.x402-error .x402-step-meta { color: #ef4444; }
|
|
115
|
+
|
|
116
|
+
.x402-wallet-buttons {
|
|
117
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
118
|
+
margin-top: 4px;
|
|
119
|
+
}
|
|
120
|
+
.x402-wallet-btn {
|
|
121
|
+
width: 100%; padding: 13px 14px;
|
|
122
|
+
background: #ffffff; border: 1.5px solid #e2e5ec; border-radius: 11px;
|
|
123
|
+
font-size: 14px; font-weight: 600; color: #0f0f0f;
|
|
124
|
+
cursor: pointer; font-family: inherit;
|
|
125
|
+
display: flex; align-items: center; gap: 12px;
|
|
126
|
+
transition: border-color 0.12s, background 0.12s, transform 0.05s;
|
|
127
|
+
}
|
|
128
|
+
.x402-wallet-btn:hover:not(:disabled) { border-color: #0a84ff; background: #f7faff; }
|
|
129
|
+
.x402-wallet-btn:active:not(:disabled) { transform: translateY(1px); }
|
|
130
|
+
.x402-wallet-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
131
|
+
.x402-wallet-icon {
|
|
132
|
+
width: 28px; height: 28px; flex: 0 0 auto;
|
|
133
|
+
border-radius: 7px;
|
|
134
|
+
display: flex; align-items: center; justify-content: center;
|
|
135
|
+
font-size: 16px;
|
|
136
|
+
background: #f3f4f7;
|
|
137
|
+
}
|
|
138
|
+
.x402-wallet-icon.x402-phantom { background: linear-gradient(135deg, #ab9ff2, #534bb1); color: #fff; }
|
|
139
|
+
.x402-wallet-icon.x402-metamask { background: linear-gradient(135deg, #f6851b, #e2761b); color: #fff; }
|
|
140
|
+
.x402-wallet-name { flex: 1; text-align: left; }
|
|
141
|
+
.x402-wallet-meta { font-size: 11px; color: #8a90a8; font-weight: 500; }
|
|
142
|
+
|
|
143
|
+
.x402-token-row {
|
|
144
|
+
display: flex; gap: 8px; margin-bottom: 10px;
|
|
145
|
+
}
|
|
146
|
+
.x402-token-pill {
|
|
147
|
+
flex: 1; min-width: 0; cursor: pointer; font-family: inherit;
|
|
148
|
+
padding: 9px 12px; border-radius: 10px;
|
|
149
|
+
background: #ffffff; border: 1.5px solid #e2e5ec;
|
|
150
|
+
display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
|
|
151
|
+
transition: border-color 0.12s, background 0.12s, transform 0.05s;
|
|
152
|
+
}
|
|
153
|
+
.x402-token-pill:hover { border-color: #c3c9d6; background: #f9fafc; }
|
|
154
|
+
.x402-token-pill:active { transform: translateY(1px); }
|
|
155
|
+
.x402-token-pill.x402-on { border-color: #0f0f0f; background: #f7faff; }
|
|
156
|
+
.x402-token-sym { font-size: 13px; font-weight: 700; color: #0f0f0f; letter-spacing: -0.005em; }
|
|
157
|
+
.x402-token-amt { font-size: 11px; color: #8a90a8; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
158
|
+
|
|
159
|
+
.x402-pay-btn {
|
|
160
|
+
width: 100%; padding: 14px 16px;
|
|
161
|
+
background: #0f0f0f; color: #fff; border: none;
|
|
162
|
+
border-radius: 12px;
|
|
163
|
+
font-size: 15px; font-weight: 700; font-family: inherit;
|
|
164
|
+
cursor: pointer; letter-spacing: -0.005em;
|
|
165
|
+
transition: background 0.12s, transform 0.05s;
|
|
166
|
+
margin-top: 4px;
|
|
167
|
+
display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
168
|
+
}
|
|
169
|
+
.x402-pay-btn:hover:not(:disabled) { background: #1d1d1d; }
|
|
170
|
+
.x402-pay-btn:active:not(:disabled) { transform: translateY(1px); }
|
|
171
|
+
.x402-pay-btn:disabled { background: #c8ccd4; cursor: not-allowed; }
|
|
172
|
+
|
|
173
|
+
.x402-pay-secondary {
|
|
174
|
+
width: 100%; padding: 12px 14px;
|
|
175
|
+
background: #ffffff; color: #0f0f0f;
|
|
176
|
+
border: 1.5px solid #e2e5ec; border-radius: 11px;
|
|
177
|
+
font-size: 14px; font-weight: 600; font-family: inherit;
|
|
178
|
+
cursor: pointer; letter-spacing: -0.005em;
|
|
179
|
+
margin-top: 6px;
|
|
180
|
+
transition: border-color 0.12s, background 0.12s, transform 0.05s;
|
|
181
|
+
}
|
|
182
|
+
.x402-pay-secondary:hover:not(:disabled) { border-color: #0a84ff; background: #f7faff; }
|
|
183
|
+
.x402-pay-secondary:active:not(:disabled) { transform: translateY(1px); }
|
|
184
|
+
|
|
185
|
+
.x402-siwx-hint {
|
|
186
|
+
font-size: 11px; color: #5a6378; text-align: center;
|
|
187
|
+
margin-top: 8px; line-height: 1.4;
|
|
188
|
+
}
|
|
189
|
+
.x402-siwx-fallback {
|
|
190
|
+
font-size: 12px; color: #b45309; line-height: 1.45;
|
|
191
|
+
padding: 8px 10px; border-radius: 8px;
|
|
192
|
+
background: #fffbeb; border: 1px solid #fde68a;
|
|
193
|
+
margin-bottom: 6px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.x402-error-box {
|
|
197
|
+
padding: 12px 14px; border-radius: 10px;
|
|
198
|
+
background: #fef2f2; border: 1px solid #fecaca; color: #b91c1c;
|
|
199
|
+
font-size: 13px; line-height: 1.45;
|
|
200
|
+
font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
|
|
201
|
+
word-break: break-word;
|
|
202
|
+
}
|
|
203
|
+
.x402-error-box strong { font-weight: 700; }
|
|
204
|
+
|
|
205
|
+
.x402-receipt {
|
|
206
|
+
padding: 14px 16px; border-radius: 12px;
|
|
207
|
+
background: linear-gradient(180deg, #f0fdf4 0%, #ffffff 100%);
|
|
208
|
+
border: 1px solid #bbf7d0;
|
|
209
|
+
}
|
|
210
|
+
.x402-receipt-title {
|
|
211
|
+
font-size: 11px; font-weight: 700; color: #15803d;
|
|
212
|
+
text-transform: uppercase; letter-spacing: 0.06em;
|
|
213
|
+
margin-bottom: 8px;
|
|
214
|
+
display: flex; align-items: center; gap: 6px;
|
|
215
|
+
}
|
|
216
|
+
.x402-receipt-title::before { content: '\u2713'; font-size: 14px; }
|
|
217
|
+
.x402-receipt-row {
|
|
218
|
+
display: flex; justify-content: space-between; gap: 12px;
|
|
219
|
+
font-size: 12px; padding: 2px 0;
|
|
220
|
+
font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
|
|
221
|
+
}
|
|
222
|
+
.x402-receipt-row .x402-k { color: #5a6378; }
|
|
223
|
+
.x402-receipt-row .x402-v { color: #0f0f0f; text-align: right; word-break: break-all; }
|
|
224
|
+
.x402-receipt-row a { color: #0a84ff; text-decoration: none; }
|
|
225
|
+
.x402-receipt-row a:hover { text-decoration: underline; }
|
|
226
|
+
|
|
227
|
+
.x402-result {
|
|
228
|
+
padding: 12px 14px; border-radius: 10px;
|
|
229
|
+
background: #fafbfc; border: 1px solid #e2e5ec;
|
|
230
|
+
max-height: 240px; overflow: auto;
|
|
231
|
+
font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
|
|
232
|
+
font-size: 12px; line-height: 1.5; color: #0f0f0f;
|
|
233
|
+
white-space: pre-wrap; word-break: break-word;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.x402-foot {
|
|
237
|
+
padding: 10px 20px 14px;
|
|
238
|
+
border-top: 1px solid #eef0f4;
|
|
239
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
240
|
+
font-size: 11px; color: #8a90a8;
|
|
241
|
+
}
|
|
242
|
+
.x402-foot a { color: #5a6378; text-decoration: none; font-weight: 600; }
|
|
243
|
+
.x402-foot a:hover { color: #0f0f0f; }
|
|
244
|
+
.x402-foot .x402-secure { display: flex; align-items: center; gap: 5px; }
|
|
245
|
+
.x402-foot .x402-secure::before { content: '\u{1F512}'; font-size: 10px; }
|
|
246
|
+
|
|
247
|
+
@media (max-width: 480px) {
|
|
248
|
+
.x402-modal { max-width: none; width: calc(100% - 16px); border-radius: 16px; }
|
|
249
|
+
.x402-price { font-size: 26px; }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@media (prefers-color-scheme: dark) {
|
|
253
|
+
.x402-overlay { color: #e6e8f0; }
|
|
254
|
+
.x402-modal { background: #161616; box-shadow: 0 24px 80px rgba(0, 0, 0, 0.6); }
|
|
255
|
+
.x402-head, .x402-price-row, .x402-foot { border-color: #272727; }
|
|
256
|
+
.x402-step + .x402-step { border-top-color: #272727; }
|
|
257
|
+
.x402-merchant .x402-name { color: #8a90a8; }
|
|
258
|
+
.x402-merchant .x402-action, .x402-price, .x402-step-label { color: #e6e8f0; }
|
|
259
|
+
.x402-step-meta { color: #8a90a8; }
|
|
260
|
+
.x402-close { background: #222222; color: #8a90a8; }
|
|
261
|
+
.x402-close:hover { background: #2e2e2e; color: #e6e8f0; }
|
|
262
|
+
.x402-price-row { background: linear-gradient(180deg, #1d1d1d 0%, #161616 100%); }
|
|
263
|
+
.x402-network { background: #222222; color: #b0b6cc; }
|
|
264
|
+
.x402-wallet-btn { background: #1d1d1d; border-color: #2e2e2e; color: #e6e8f0; }
|
|
265
|
+
.x402-wallet-btn:hover:not(:disabled) { background: #252525; border-color: #0a84ff; }
|
|
266
|
+
.x402-wallet-icon { background: #2e2e2e; }
|
|
267
|
+
.x402-wallet-meta { color: #6b7088; }
|
|
268
|
+
.x402-pay-btn { background: #ffffff; color: #0f0f0f; }
|
|
269
|
+
.x402-pay-btn:hover:not(:disabled) { background: #e7e9ee; }
|
|
270
|
+
.x402-pay-btn:disabled { background: #2e2e2e; color: #5a6378; }
|
|
271
|
+
.x402-pay-secondary { background: #1d1d1d; border-color: #2e2e2e; color: #e6e8f0; }
|
|
272
|
+
.x402-pay-secondary:hover:not(:disabled) { background: #252525; border-color: #0a84ff; }
|
|
273
|
+
.x402-siwx-hint { color: #8a90a8; }
|
|
274
|
+
.x402-siwx-fallback { background: #2a1d10; border-color: #78350f; color: #fcd34d; }
|
|
275
|
+
.x402-step-num { background: #161616; border-color: #2e2e2e; color: #8a90a8; }
|
|
276
|
+
.x402-result { background: #1d1d1d; border-color: #2e2e2e; color: #e6e8f0; }
|
|
277
|
+
.x402-receipt { background: linear-gradient(180deg, #0b1f17 0%, #161616 100%); border-color: #14532d; }
|
|
278
|
+
.x402-receipt-title { color: #4ade80; }
|
|
279
|
+
.x402-receipt-row .x402-k { color: #8a90a8; }
|
|
280
|
+
.x402-receipt-row .x402-v { color: #e6e8f0; }
|
|
281
|
+
.x402-receipt-row a { color: #60a5fa; }
|
|
282
|
+
.x402-error-box { background: #1f1416; border-color: #7f1d1d; color: #fca5a5; }
|
|
283
|
+
.x402-foot a { color: #b0b6cc; }
|
|
284
|
+
.x402-foot a:hover { color: #ffffff; }
|
|
285
|
+
}
|
|
286
|
+
`;function be(){if(document.getElementById(G))return;const n=document.createElement("style");n.id=G,n.textContent=me,document.head.appendChild(n)}class ye{constructor(e){this.opts=e,this.steps=[{id:"discover",label:"Confirming price"},{id:"connect",label:"Connect wallet"},{id:"authorize",label:"Authorize payment"},{id:"verify",label:"Verify & complete"}],this.activeNetwork=null,this.payerAddress=null,this.accept=null,this.challenge=null,this.disposed=!1,this.autoConnectTried=!1}mount(){be();const e=document.createElement("div");return e.className="x402-overlay",e.innerHTML=`
|
|
287
|
+
<div class="x402-modal" role="dialog" aria-modal="true" aria-label="x402 payment">
|
|
288
|
+
<div class="x402-head">
|
|
289
|
+
<div class="x402-merchant">
|
|
290
|
+
<div class="x402-name" data-merchant>${f(this.opts.merchant||"Payment")}</div>
|
|
291
|
+
<div class="x402-action" data-action>${f(this.opts.action||"Pay-per-call")}</div>
|
|
292
|
+
</div>
|
|
293
|
+
<button class="x402-close" data-close aria-label="Close">\u2715</button>
|
|
294
|
+
</div>
|
|
295
|
+
<div class="x402-price-row">
|
|
296
|
+
<div class="x402-price" data-price>\u2014<span class="x402-currency"> USDC</span></div>
|
|
297
|
+
<div class="x402-network" data-network>resolving\u2026</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="x402-body" data-body></div>
|
|
300
|
+
<div class="x402-foot">
|
|
301
|
+
<span class="x402-secure">${f(h.footerNote)}</span>
|
|
302
|
+
${h.brand?.name?`<a href="${f(h.brand.url||"#")}" target="_blank" rel="noopener">Powered by ${f(h.brand.name)}</a>`:""}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
`,document.body.appendChild(e),this.overlay=e,this.bodyEl=e.querySelector("[data-body]"),this.priceEl=e.querySelector("[data-price]"),this.networkEl=e.querySelector("[data-network]"),e.querySelector("[data-close]").addEventListener("click",()=>this.close("cancelled")),e.addEventListener("click",t=>{t.target===e&&this.close("cancelled")}),this.onKey=t=>{t.key==="Escape"&&this.close("cancelled")},document.addEventListener("keydown",this.onKey),requestAnimationFrame(()=>e.classList.add("x402-open")),new Promise((t,r)=>{this.resolve=t,this.reject=r})}close(e){if(!this.disposed&&(this.disposed=!0,document.removeEventListener("keydown",this.onKey),this.overlay.classList.remove("x402-open"),setTimeout(()=>this.overlay.remove(),180),e==="cancelled"&&this.reject)){const t=new Error("cancelled");t.code="cancelled",this.reject(t)}}renderSteps(e,t={}){return this.steps.map(o=>{const s=t[o.id]||(o.id===e?"active":"idle"),i=s==="active"?"x402-active":s==="done"?"x402-done":s==="error"?"x402-error":"",a=t[`${o.id}_meta`]||"",l=s==="done"?"\u2713":s==="error"?"!":o.id===e&&s==="active"?" ":this.steps.findIndex(d=>d.id===o.id)+1;return`<div class="x402-step ${i}">
|
|
306
|
+
<div class="x402-step-num">${l}</div>
|
|
307
|
+
<div class="x402-step-body">
|
|
308
|
+
<div class="x402-step-label">${o.label}</div>
|
|
309
|
+
${a?`<div class="x402-step-meta">${f(a)}</div>`:""}
|
|
310
|
+
</div>
|
|
311
|
+
</div>`}).join("")}setPrice(e){const t=y(e),r=v(e.amount,t.decimals),o=t.glyph?`${t.glyph} `:"";this.priceEl.innerHTML=`${r}<span class="x402-currency"> ${o}${f(t.symbol)}</span>`,this.networkEl.textContent=b(e.network,e)}renderConnect(){const e=typeof window<"u"&&(window.solana?.isPhantom||window.phantom?.solana),t=typeof window<"u"&&window.ethereum,r=q(this.challenge);let o=r.find(u=>u===this.solanaAccept)||R(r);this.solanaAccept=o;const s=this.challenge?.accepts.find(D);if(this.siwx&&!this.payFlowOverride){const u=e?J(this.siwx,"solana"):null,c=t?J(this.siwx,"evm"):null;if(u||c){this.renderSiwxChoice({siwxSolana:u,siwxEvm:c});return}}if(this.opts.autoConnect&&!this.autoConnectTried&&!this.siwxFallbackNotice){this.autoConnectTried=!0;const u=!!(o&&e),c=!!(s&&t);if(u&&!c){this.runSolana(o);return}if(c&&!u){this.runEvm(s);return}}const i=r.length>1?`<div class="x402-token-row" role="group" aria-label="Choose payment token">
|
|
312
|
+
${r.map(u=>{const c=y(u),p=u===o,x=v(u.amount,c.decimals);return`<button type="button" class="x402-token-pill${p?" x402-on":""}" data-token-asset="${f(u.asset)}" aria-pressed="${p}"${c.accent&&p?` style="border-color:${f(c.accent)}"`:""}>
|
|
313
|
+
<span class="x402-token-sym">${c.glyph?f(c.glyph)+" ":""}${f(c.symbol)}</span>
|
|
314
|
+
<span class="x402-token-amt">${x}</span>
|
|
315
|
+
</button>`}).join("")}
|
|
316
|
+
</div>`:"",a=[];if(o){const u=y(o),c=r.length>1?`${b(o.network,o)} \xB7 ${u.symbol}`:b(o.network,o);a.push(`
|
|
317
|
+
<button class="x402-wallet-btn" data-wallet="phantom" ${e?"":"disabled"}>
|
|
318
|
+
<div class="x402-wallet-icon x402-phantom">P</div>
|
|
319
|
+
<span class="x402-wallet-name">${e?"Phantom":"Phantom (not detected)"}</span>
|
|
320
|
+
<span class="x402-wallet-meta" data-sol-meta>${f(c)}</span>
|
|
321
|
+
</button>
|
|
322
|
+
`)}s&&a.push(`
|
|
323
|
+
<button class="x402-wallet-btn" data-wallet="evm" ${t?"":"disabled"}>
|
|
324
|
+
<div class="x402-wallet-icon x402-metamask">M</div>
|
|
325
|
+
<span class="x402-wallet-name">${t?"Browser wallet":"No EVM wallet detected"}</span>
|
|
326
|
+
<span class="x402-wallet-meta">${b(s.network,s)}</span>
|
|
327
|
+
</button>
|
|
328
|
+
`);const l=this.siwxFallbackNotice?`<div class="x402-siwx-fallback">${f(this.siwxFallbackNotice)}</div>`:"";this.bodyEl.innerHTML=`
|
|
329
|
+
${this.renderSteps("connect",{discover:"done"})}
|
|
330
|
+
${l}
|
|
331
|
+
${i}
|
|
332
|
+
<div class="x402-wallet-buttons">${a.join("")}</div>
|
|
333
|
+
`;const d=u=>{const c=u.target.closest("[data-wallet]");if(!c||c.disabled)return;const p=c.dataset.wallet;p==="phantom"?this.runSolana(this.solanaAccept):p==="evm"&&this.runEvm(s)};this.bodyEl.querySelectorAll("[data-wallet]").forEach(u=>u.addEventListener("click",d)),this.bodyEl.querySelectorAll("[data-token-asset]").forEach(u=>{u.addEventListener("click",()=>{const c=r.find(m=>m.asset===u.dataset.tokenAsset);if(!c||c===this.solanaAccept)return;this.solanaAccept=c,this.accept=c,this.setPrice(c);const p=y(c),x=this.bodyEl.querySelector("[data-sol-meta]");x&&(x.textContent=`${b(c.network,c)} \xB7 ${p.symbol}`),this.bodyEl.querySelectorAll("[data-token-asset]").forEach(m=>{const w=m===u;m.classList.toggle("x402-on",w),m.setAttribute("aria-pressed",String(w)),m.style.borderColor=w&&p.accent?p.accent:""})})})}renderSiwxChoice({siwxSolana:e,siwxEvm:t}){const r=y(this.accept),o=v(this.accept.amount,r.decimals),s=e?{kind:"solana",chain:e.chain}:{kind:"evm",chain:t.chain},i=s.kind==="solana"?"Sign in with Phantom":"Sign in with wallet";this.bodyEl.innerHTML=`
|
|
334
|
+
${this.renderSteps("connect",{discover:"done"})}
|
|
335
|
+
<button class="x402-pay-btn" data-action="siwx">${i}</button>
|
|
336
|
+
<button class="x402-pay-secondary" data-action="pay">Pay ${o} ${f(r.symbol)} instead</button>
|
|
337
|
+
<div class="x402-siwx-hint">Already paid for this once? Sign in to re-enter without paying again.</div>
|
|
338
|
+
`;const a=this.bodyEl.querySelector('[data-action="siwx"]'),l=this.bodyEl.querySelector('[data-action="pay"]');a.addEventListener("click",()=>{s.kind==="solana"?this.runSiwxSolana(s.chain):this.runSiwxEvm(s.chain)}),l.addEventListener("click",()=>{this.payFlowOverride=!0,this.renderConnect()}),requestAnimationFrame(()=>a.focus())}renderProgress(e,t={}){this.bodyEl.innerHTML=this.renderSteps(e,{discover:"done",connect:"done",...e==="verify"?{authorize:"done"}:{},[`${e}_meta`]:t.text||"",...t.statuses})}renderError(e,t){this.bodyEl.innerHTML=`
|
|
339
|
+
${this.renderSteps(e,{...e!=="discover"?{discover:"done"}:{},...e==="authorize"||e==="verify"?{connect:"done"}:{},...e==="verify"?{authorize:"done"}:{},[e]:"error",[`${e}_meta`]:"failed"})}
|
|
340
|
+
<div class="x402-error-box"><strong>${f(e)}:</strong> ${f(t)}</div>
|
|
341
|
+
<button class="x402-pay-btn" data-retry>Try again</button>
|
|
342
|
+
`,this.bodyEl.querySelector("[data-retry]").addEventListener("click",()=>this.start())}renderDone({result:e,payment:t,siwx:r}){const o=typeof e=="string"?e:JSON.stringify(e,null,2);let s;if(r){const i=r.address?`${r.address.slice(0,8)}\u2026${r.address.slice(-6)}`:"\u2014";s=`
|
|
343
|
+
<div class="x402-receipt">
|
|
344
|
+
<div class="x402-receipt-title">Welcome back!</div>
|
|
345
|
+
<div class="x402-receipt-row">
|
|
346
|
+
<span class="x402-k">network</span>
|
|
347
|
+
<span class="x402-v">${f(b(r.network)||r.network||"\u2014")}</span>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="x402-receipt-row">
|
|
350
|
+
<span class="x402-k">wallet</span>
|
|
351
|
+
<span class="x402-v">${f(i)}</span>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="x402-receipt-row">
|
|
354
|
+
<span class="x402-k">paid</span>
|
|
355
|
+
<span class="x402-v">previously \xB7 re-entered free</span>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
`}else{const i=ae(t?.network,t?.transaction),a=t?.transaction?`${t.transaction.slice(0,8)}\u2026${t.transaction.slice(-6)}`:"\u2014";s=`
|
|
359
|
+
<div class="x402-receipt">
|
|
360
|
+
<div class="x402-receipt-title">Payment confirmed!</div>
|
|
361
|
+
<div class="x402-receipt-row">
|
|
362
|
+
<span class="x402-k">network</span>
|
|
363
|
+
<span class="x402-v">${f(b(t?.network)||"\u2014")}</span>
|
|
364
|
+
</div>
|
|
365
|
+
<div class="x402-receipt-row">
|
|
366
|
+
<span class="x402-k">payer</span>
|
|
367
|
+
<span class="x402-v">${f(t?.payer?`${t.payer.slice(0,8)}\u2026${t.payer.slice(-6)}`:"\u2014")}</span>
|
|
368
|
+
</div>
|
|
369
|
+
${t?.transaction?`<div class="x402-receipt-row"><span class="x402-k">tx</span><span class="x402-v">${i?`<a href="${i}" target="_blank" rel="noopener">${a} \u2197</a>`:a}</span></div>`:""}
|
|
370
|
+
</div>
|
|
371
|
+
`}this.bodyEl.innerHTML=`
|
|
372
|
+
${s}
|
|
373
|
+
<div class="x402-result">${f(o).slice(0,4e3)}</div>
|
|
374
|
+
<button class="x402-pay-btn" data-done>Done</button>
|
|
375
|
+
`,this.bodyEl.querySelector("[data-done]").addEventListener("click",()=>{this.disposed=!0,document.removeEventListener("keydown",this.onKey),this.overlay.classList.remove("x402-open"),setTimeout(()=>this.overlay.remove(),180)})}async start(){this.bodyEl.innerHTML=this.renderSteps("discover");try{const e=await $e(this.opts);this.challenge=e,this.siwx=fe(e),this.payFlowOverride=!1,this.siwxFallbackNotice=null;const t=q(e),r=R(t);this.solanaAccept=r;const o=e.accepts.find(D),s=typeof window<"u"&&(window.solana?.isPhantom||window.phantom?.solana);this.accept=s&&r||o||e.accepts[0],this.setPrice(this.accept),this.renderConnect()}catch(e){this.renderError("discover",e.message||String(e))}}async runSolana(e){this.accept=e,this.setPrice(e),this.renderProgress("connect",{text:"Opening Phantom\u2026"});try{const t=window.phantom?.solana||window.solana;if(!t)throw new Error("Phantom wallet not detected");const o=((await t.connect())?.publicKey||t.publicKey)?.toString();if(!o)throw new Error("Phantom did not return a public key");this.payerAddress=o;const s=F({accept:e,caps:this.opts.caps,address:o});if(s.abort){this.renderError("authorize",s.reason);return}this.spendReservation=s.reservation||null,this.renderProgress("authorize",{text:`Building Solana payment for ${o.slice(0,6)}\u2026${o.slice(-4)}`});const i=await X(`${L()}?action=prepare`,{accept:e,buyer:o});this.renderProgress("authorize",{text:"Confirm in Phantom\u2026"});const a=ve(i.tx_base64),d=(await Ee()).VersionedTransaction.deserialize(a),u=await t.signTransaction(d),c=ke(u.serialize()),p=W(this.challenge),x=await X(`${L()}?action=encode`,{accept:e,signed_tx_base64:c,resource_url:new URL(this.opts.endpoint,location.href).href,...p?{builder_code:p}:{}});await this.executePaid(x.x_payment)}catch(t){this.spendReservation&&(z(this.spendReservation),this.spendReservation=null),this.renderError(this.payerAddress?"authorize":"connect",g(t))}}async runEvm(e){this.accept=e,this.setPrice(e),this.renderProgress("connect",{text:"Opening browser wallet\u2026"});try{const t=window.ethereum;if(!t)throw new Error("No EVM wallet detected");const o=(await t.request({method:"eth_requestAccounts"}))?.[0];if(!o)throw new Error("Wallet did not return an account");this.payerAddress=o;const s=F({accept:e,caps:this.opts.caps,address:o});if(s.abort){this.renderError("authorize",s.reason);return}this.spendReservation=s.reservation||null;const i=C[e.network];if(!i)throw new Error(`Unknown EVM network ${e.network}`);const a=await t.request({method:"eth_chainId"}),l="0x"+i.chainId.toString(16);if(a!==l){this.renderProgress("connect",{text:`Switch wallet to ${i.name}\u2026`});try{await t.request({method:"wallet_switchEthereumChain",params:[{chainId:l}]})}catch{throw new Error(`Wallet is on ${a}; please switch to ${i.name} (${l}) and retry`)}}this.renderProgress("authorize",{text:`Authorize ${v(e.amount)} USDC\u2026`});const d=0,u=Math.floor(Date.now()/1e3)+(e.maxTimeoutSeconds||600),c="0x"+Se(32),p={name:e.extra?.name||"USD Coin",version:e.extra?.version||"2",chainId:i.chainId,verifyingContract:e.asset},x={EIP712Domain:[{name:"name",type:"string"},{name:"version",type:"string"},{name:"chainId",type:"uint256"},{name:"verifyingContract",type:"address"}],TransferWithAuthorization:[{name:"from",type:"address"},{name:"to",type:"address"},{name:"value",type:"uint256"},{name:"validAfter",type:"uint256"},{name:"validBefore",type:"uint256"},{name:"nonce",type:"bytes32"}]},m={from:o,to:e.payTo,value:e.amount,validAfter:d,validBefore:u,nonce:c},w={primaryType:"TransferWithAuthorization",types:x,domain:p,message:m},Q=await t.request({method:"eth_signTypedData_v4",params:[o,JSON.stringify(w)]}),O={x402Version:2,scheme:"exact",network:e.network,resource:{url:this.opts.endpoint,mimeType:"application/json"},accepted:e,payload:{signature:Q,authorization:{from:o,to:e.payTo,value:e.amount,validAfter:String(d),validBefore:String(u),nonce:c}}},M=W(this.challenge);M&&(O.extensions={"builder-code":M});const ee=ie(O);await this.executePaid(ee)}catch(t){this.spendReservation&&(z(this.spendReservation),this.spendReservation=null),this.renderError(this.payerAddress?"authorize":"connect",g(t))}}async executePaid(e,t=0){this.renderProgress("verify",{text:t?"Retrying after upstream throttle\u2026":"Calling merchant endpoint\u2026"});try{const r=await fetch(this.opts.endpoint,{method:this.opts.method||"GET",headers:{...this.opts.headers||{},...this.opts.body&&!this.opts.headers?.["content-type"]?{"content-type":"application/json"}:{},"X-PAYMENT":e},body:this.opts.body?typeof this.opts.body=="string"?this.opts.body:JSON.stringify(this.opts.body):void 0}),o=r.headers.get("content-type")||"",s=await r.text();let i;if(o.includes("json"))try{i=JSON.parse(s)}catch{i=s}else i=s;if(!r.ok){if(r.status===429&&t<ge)return await this.waitForThrottle(we(r,i)),this.executePaid(e,t+1);const d=i&&typeof i=="object"&&(i.error_description||i.error)||`HTTP ${r.status}`;throw new Error(d)}const a=r.headers.get("x-payment-response"),l=I(a)||{};this.spendReservation=null,this.renderDone({result:i,payment:l}),this.resolve?.({ok:!0,result:i,payment:l,response:{status:r.status,headers:Y(r.headers)}})}catch(r){this.spendReservation&&(z(this.spendReservation),this.spendReservation=null),this.renderError("verify",g(r))}}async waitForThrottle(e){const t=Math.max(1,Math.min(30,Math.round(e)||6));for(let r=t;r>0;r--)this.renderProgress("verify",{text:`Generator is busy \u2014 retrying in ${r}s\u2026`}),await new Promise(o=>setTimeout(o,1e3));this.renderProgress("verify",{text:"Retrying\u2026"})}async runSiwxEvm(e){this.renderProgress("connect",{text:"Opening browser wallet\u2026"});try{const t=window.ethereum;if(!t)throw new Error("No EVM wallet detected");const o=(await t.request({method:"eth_requestAccounts"}))?.[0];if(!o)throw new Error("Wallet did not return an account");const i=(await xe())(o);this.payerAddress=i,this.renderProgress("authorize",{text:`Sign sign-in message as ${i.slice(0,6)}\u2026${i.slice(-4)}`});const a=V(this.siwx.info,e,i),l=await t.request({method:"personal_sign",params:[a,i]}),d=this.siwx.info,u={domain:d.domain,address:i,...d.statement?{statement:d.statement}:{},uri:d.uri,version:d.version||"1",chainId:e.chainId,type:"eip191",nonce:d.nonce,issuedAt:d.issuedAt,...d.expirationTime?{expirationTime:d.expirationTime}:{},...d.notBefore?{notBefore:d.notBefore}:{},...d.requestId!==void 0&&d.requestId!==null?{requestId:d.requestId}:{},...Array.isArray(d.resources)?{resources:d.resources}:{},signatureScheme:"eip191",signature:l};await this.executeSiwx(u,e.chainId)}catch(t){this.renderError(this.payerAddress?"authorize":"connect",g(t))}}async runSiwxSolana(e){this.renderProgress("connect",{text:"Opening Phantom\u2026"});try{const t=window.phantom?.solana||window.solana;if(!t)throw new Error("Phantom wallet not detected");const s=((await t.connect())?.publicKey||t.publicKey)?.toString();if(!s)throw new Error("Phantom did not return a public key");this.payerAddress=s,this.renderProgress("authorize",{text:`Sign sign-in message as ${s.slice(0,6)}\u2026${s.slice(-4)}`});const i=V(this.siwx.info,e,s),a=new TextEncoder().encode(i),l=await t.signMessage(a,"utf8"),d=l?.signature instanceof Uint8Array?l.signature:new Uint8Array(l?.signature||l);if(!d||!d.length)throw new Error("Phantom did not return a signature");const u=he(d),c=this.siwx.info,p={domain:c.domain,address:s,...c.statement?{statement:c.statement}:{},uri:c.uri,version:c.version||"1",chainId:e.chainId,type:"ed25519",nonce:c.nonce,issuedAt:c.issuedAt,...c.expirationTime?{expirationTime:c.expirationTime}:{},...c.notBefore?{notBefore:c.notBefore}:{},...c.requestId!==void 0&&c.requestId!==null?{requestId:c.requestId}:{},...Array.isArray(c.resources)?{resources:c.resources}:{},signatureScheme:"siws",signature:u};await this.executeSiwx(p,e.chainId)}catch(t){this.renderError(this.payerAddress?"authorize":"connect",g(t))}}async executeSiwx(e,t){this.renderProgress("verify",{text:"Verifying sign-in\u2026"});const r=pe(e);let o;try{o=await fetch(this.opts.endpoint,{method:this.opts.method||"GET",headers:{...this.opts.headers||{},...this.opts.body&&!this.opts.headers?.["content-type"]?{"content-type":"application/json"}:{},[ne]:r},body:this.opts.body?typeof this.opts.body=="string"?this.opts.body:JSON.stringify(this.opts.body):void 0})}catch(i){this.renderError("verify",g(i));return}if(o.status===200){const i=o.headers.get("content-type")||"",a=await o.text();let l;if(i.includes("json"))try{l=JSON.parse(a)}catch{l=a}else l=a;const d={address:e.address,network:t};this.renderDone({result:l,siwx:d}),this.resolve?.({ok:!0,result:l,siwx:d,response:{status:o.status,headers:Y(o.headers)}});return}if(o.status===401||o.status===402){let i=null;try{i=await o.clone().json()}catch{}const a=i?.code||i?.error;this.siwx=null,this.payerAddress=null,this.payFlowOverride=!1,this.siwxFallbackNotice=a==="siwx_not_paid"||o.status===402?"You haven't paid for this yet \u2014 pay now to unlock re-entry.":"Sign-in not accepted \u2014 please pay to continue.",!this.challenge||!Array.isArray(this.challenge.accepts)||!this.challenge.accepts.length?this.start():this.renderConnect();return}const s=await o.text().catch(()=>"");this.renderError("verify",`SIWX retry failed: HTTP ${o.status}${s?` \xB7 ${s.slice(0,120)}`:""}`)}}function f(n){return String(n??"").replace(/[&<>"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}function Y(n){const e={};return n.forEach((t,r)=>e[r]=t),e}const ge=2;function we(n,e,t=6){const r=Number.parseInt(n.headers.get("retry-after")||"",10);if(Number.isFinite(r)&&r>0)return r;const o=e&&typeof e=="object"?Number(e.retry_after):NaN;return Number.isFinite(o)&&o>0?o:t}function g(n){const e=n?.shortMessage||n?.message||String(n);return/user rejected|user denied|reject/i.test(e)?"cancelled in wallet":/throttl|rate.?limit|too many requests|less than \$|in credit|\b429\b/i.test(e)?"The service is briefly busy and your payment was not taken \u2014 retry in a few seconds.":/dynamically imported module|esm\.sh|module script failed/i.test(e)?"A component this wallet path needs (loaded from esm.sh) was blocked \u2014 often by a strict host security policy. Pay with MetaMask on Base instead; it needs no third-party code.":e.slice(0,240)}function ve(n){if(typeof Buffer<"u")return new Uint8Array(Buffer.from(n,"base64"));const e=atob(n),t=new Uint8Array(e.length);for(let r=0;r<e.length;r++)t[r]=e.charCodeAt(r);return t}function ke(n){if(typeof Buffer<"u")return Buffer.from(n).toString("base64");let e="";for(let t=0;t<n.length;t++)e+=String.fromCharCode(n[t]);return btoa(e)}function Se(n){const e=new Uint8Array(n);return crypto.getRandomValues(e),Array.from(e).map(t=>t.toString(16).padStart(2,"0")).join("")}let $=null;async function Ee(){return $||($=await import(h.esm.solanaWeb3),$)}async function X(n,e){const t=await fetch(n,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(e)}),r=await t.text();let o;try{o=JSON.parse(r)}catch{o={error:"parse_error",error_description:r.slice(0,200)}}if(!t.ok){const s=new Error(o.error_description||o.error||`HTTP ${t.status}`);throw s.status=t.status,s.data=o,s}return o}async function $e(n){const e={...n.headers||{}},t={method:n.method||"GET",headers:e,body:n.body?typeof n.body=="string"?n.body:JSON.stringify(n.body):void 0};t.body&&!e["content-type"]&&(e["content-type"]="application/json");const r=await fetch(n.endpoint,t),o=r.headers.get("payment-required"),s=r.status===401&&!!o;if(r.status!==402&&!s){const a=await r.text();throw new Error(`Endpoint did not return 402 (got ${r.status}). Body: ${a.slice(0,120)}`)}let i=s?I(o):await r.json().catch(()=>null);if(!i||!Array.isArray(i.accepts)||!i.accepts.length){const a=I(o);a&&Array.isArray(a.accepts)&&a.accepts.length&&(i=a)}if(!i||!Array.isArray(i.accepts)||!i.accepts.length)throw new Error("Endpoint returned 402 but no `accepts` array could be found in body or header");return i}async function Z(n){if(!n?.endpoint)throw new Error("X402.pay: endpoint is required");const e=new ye(n),t=e.mount();return queueMicrotask(()=>e.start()),t}function Ae(n){n.dataset.x402Bound!=="1"&&(n.dataset.x402Bound="1",n.addEventListener("click",async e=>{e.preventDefault();const t=Ce(n);try{const r=await Z(t);r?.siwx&&n.dispatchEvent(new CustomEvent("x402:siwx-signed",{detail:r.siwx,bubbles:!0})),n.dispatchEvent(new CustomEvent("x402:result",{detail:r,bubbles:!0}))}catch(r){if(r?.code==="cancelled")return;n.dispatchEvent(new CustomEvent("x402:error",{detail:{error:r?.message||String(r)},bubbles:!0}))}}))}function Ce(n){const e=n.dataset;let t=e.x402Body;if(t)try{t=JSON.parse(t)}catch{}let r=e.x402Headers;if(r)try{r=JSON.parse(r)}catch{r=void 0}return{endpoint:e.x402Endpoint,method:e.x402Method||(t?"POST":"GET"),body:t,headers:r,merchant:e.x402Merchant,action:e.x402Action||n.textContent?.trim().slice(0,60)}}function A(){document.querySelectorAll("[data-x402-endpoint]").forEach(Ae)}function Te(){try{const e=(document.currentScript||document.querySelector('script[src*="x402"][type="module"]')||document.querySelector("script[data-x402-checkout-origin]"))?.dataset;if(!e)return;const t={};e.x402CheckoutOrigin&&(t.checkoutOrigin=e.x402CheckoutOrigin),e.x402CheckoutPath&&(t.checkoutPath=e.x402CheckoutPath),e.x402FooterNote&&(t.footerNote=e.x402FooterNote),(e.x402BrandName||e.x402BrandUrl)&&(t.brand={},e.x402BrandName&&(t.brand.name=e.x402BrandName),e.x402BrandUrl&&(t.brand.url=e.x402BrandUrl)),(e.x402BuilderWallet||e.x402BuilderService)&&(t.builderCode={},e.x402BuilderWallet&&(t.builderCode.wallet=e.x402BuilderWallet),e.x402BuilderService&&(t.builderCode.service=e.x402BuilderService)),Object.keys(t).length&&j(t)}catch{}}typeof document<"u"&&(Te(),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",A,{once:!0}):A(),new MutationObserver(()=>A()).observe(document.documentElement,{childList:!0,subtree:!0})),typeof window<"u"&&(window.X402=Object.freeze({pay:Z,init:A,configure:j,version:te,tokens:Object.freeze({USDC_MINT_SOLANA:T,THREE_MINT:P,KNOWN_SOLANA_TOKENS:B})}));export{B as KNOWN_SOLANA_TOKENS,P as THREE_MINT,T as USDC_MINT_SOLANA,j as configure,A as init,Z as pay};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Client API reference
|
|
2
|
+
|
|
3
|
+
The browser entry point. Import it as an ES module, or drop the bundle on a page
|
|
4
|
+
and use `window.X402`. For how these pieces fit together see
|
|
5
|
+
[architecture](./architecture.md); for the Solana backend see
|
|
6
|
+
[server setup](./server-setup.md).
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import { pay, configure, init, version } from '@three-ws/x402-payment-modal';
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
When loaded as a script, the same functions are available as
|
|
13
|
+
`window.X402.{ pay, init, configure, version }`. The module also auto-binds
|
|
14
|
+
`[data-x402-endpoint]` elements on load (and re-binds new ones via a
|
|
15
|
+
`MutationObserver`).
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<script type="module" src="https://unpkg.com/@three-ws/x402-payment-modal"></script>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## `pay(opts) => Promise<PayResult>`
|
|
24
|
+
|
|
25
|
+
Opens the modal and drives the full pay flow against one endpoint. Resolves once
|
|
26
|
+
the merchant settles and returns a result. Rejects with an `Error` whose
|
|
27
|
+
`.code === 'cancelled'` if the user closes the modal.
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
try {
|
|
31
|
+
const res = await pay({
|
|
32
|
+
endpoint: 'https://api.example.com/premium',
|
|
33
|
+
method: 'POST',
|
|
34
|
+
body: { prompt: 'Summarize this article' },
|
|
35
|
+
merchant: 'Example API',
|
|
36
|
+
action: 'Generate summary',
|
|
37
|
+
autoConnect: true,
|
|
38
|
+
});
|
|
39
|
+
console.log(res.result); // the paid endpoint's response
|
|
40
|
+
console.log(res.payment); // { network, transaction, payer }
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err.code === 'cancelled') return; // user closed the modal
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### PayOptions
|
|
48
|
+
|
|
49
|
+
| Field | Type | Required | Description |
|
|
50
|
+
|---------------|---------------------------|----------|-----------------------------------------------------------------------------|
|
|
51
|
+
| `endpoint` | `string` | yes | The x402-protected URL to call. |
|
|
52
|
+
| `method` | `string` | no | HTTP method. Defaults to `GET` (or `POST` when a `body` is supplied). |
|
|
53
|
+
| `body` | `object \| string` | no | Request body. An object is JSON-stringified; a string is sent as-is. |
|
|
54
|
+
| `headers` | `object` | no | Extra request headers merged into both the probe and retry requests. |
|
|
55
|
+
| `merchant` | `string` | no | Merchant name shown in the modal header. |
|
|
56
|
+
| `action` | `string` | no | Short action label shown in the modal header (e.g. "Generate summary"). |
|
|
57
|
+
| `autoConnect` | `boolean` | no | Skip the wallet picker when exactly one wallet is detected. |
|
|
58
|
+
| `caps` | [`SpendingCaps`](./spending-caps.md) | no | Client-side spending limits, atomic micro-USD. |
|
|
59
|
+
|
|
60
|
+
### PayResult
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
{
|
|
64
|
+
ok: true,
|
|
65
|
+
result: unknown, // parsed JSON, or text, from the paid endpoint
|
|
66
|
+
payment?: {
|
|
67
|
+
network: string,
|
|
68
|
+
transaction: string, // tx hash / signature
|
|
69
|
+
payer: string, // payer address
|
|
70
|
+
},
|
|
71
|
+
siwx?: { // present when re-entry used Sign-In-With-X
|
|
72
|
+
address: string,
|
|
73
|
+
network: string,
|
|
74
|
+
},
|
|
75
|
+
response: {
|
|
76
|
+
status: number,
|
|
77
|
+
headers: Record<string, string>,
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## `configure(opts) => Config`
|
|
85
|
+
|
|
86
|
+
Sets global defaults. All fields are optional; nested objects are
|
|
87
|
+
**shallow-merged** into the current config. Returns the resulting config.
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
import { configure } from '@three-ws/x402-payment-modal';
|
|
91
|
+
|
|
92
|
+
configure({
|
|
93
|
+
checkoutOrigin: 'https://pay.example.com',
|
|
94
|
+
checkoutPath: '/api/x402-checkout',
|
|
95
|
+
brand: { name: 'Example', url: 'https://example.com' },
|
|
96
|
+
footerNote: 'Secured by x402',
|
|
97
|
+
builderCode: { wallet: 'examplewallet', service: 'example_api' },
|
|
98
|
+
esm: {
|
|
99
|
+
solanaWeb3: 'https://esm.sh/@solana/web3.js@1.95.0',
|
|
100
|
+
nobleHashesSha3: 'https://esm.sh/@noble/hashes@1/sha3',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
| Field | Type | Description |
|
|
106
|
+
|------------------|---------------------------------------|-----------------------------------------------------------------------------------------------|
|
|
107
|
+
| `checkoutOrigin` | `string \| null` | Origin serving the Solana checkout endpoint. `null` resolves it from the script `src` or page origin. |
|
|
108
|
+
| `checkoutPath` | `string` | Checkout path. Default `'/api/x402-checkout'`. |
|
|
109
|
+
| `brand` | `{ name, url }` | Footer attribution. |
|
|
110
|
+
| `footerNote` | `string` | Text on the left side of the footer. |
|
|
111
|
+
| `builderCode` | `{ wallet, service }` | ERC-8021 builder-code echo. Each value lowercase `[a-z0-9_]{1,32}`. |
|
|
112
|
+
| `esm` | `{ solanaWeb3, nobleHashesSha3 }` | CDN URLs for crypto helpers loaded on demand. Repoint for strict CSP / self-hosting. |
|
|
113
|
+
|
|
114
|
+
> The `checkoutOrigin` / `checkoutPath` settings only matter for the Solana rail.
|
|
115
|
+
> EVM-only sites can ignore them.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## `init()`
|
|
120
|
+
|
|
121
|
+
Re-scans the document and binds every `[data-x402-endpoint]` element. This is
|
|
122
|
+
called automatically on load and whenever the `MutationObserver` sees new
|
|
123
|
+
matching elements, so you rarely need it — call it after injecting markup into a
|
|
124
|
+
context the observer doesn't cover (e.g. inside a shadow root you control).
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import { init } from '@three-ws/x402-payment-modal';
|
|
128
|
+
init();
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## `version`
|
|
134
|
+
|
|
135
|
+
The package version string.
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
import { version } from '@three-ws/x402-payment-modal';
|
|
139
|
+
console.log(version); // "1.1.0"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## HTML data attributes
|
|
145
|
+
|
|
146
|
+
You can drive the modal declaratively without writing JavaScript. Add attributes
|
|
147
|
+
to any clickable element and the auto-binder wires the click handler.
|
|
148
|
+
|
|
149
|
+
### On a clickable element
|
|
150
|
+
|
|
151
|
+
| Attribute | Maps to | Notes |
|
|
152
|
+
|------------------------|--------------------|--------------------------------|
|
|
153
|
+
| `data-x402-endpoint` | `endpoint` | **Required.** |
|
|
154
|
+
| `data-x402-method` | `method` | |
|
|
155
|
+
| `data-x402-body` | `body` | JSON string. |
|
|
156
|
+
| `data-x402-headers` | `headers` | JSON string. |
|
|
157
|
+
| `data-x402-merchant` | `merchant` | |
|
|
158
|
+
| `data-x402-action` | `action` | |
|
|
159
|
+
|
|
160
|
+
```html
|
|
161
|
+
<button
|
|
162
|
+
data-x402-endpoint="https://api.example.com/premium"
|
|
163
|
+
data-x402-method="POST"
|
|
164
|
+
data-x402-body='{"prompt":"Summarize this article"}'
|
|
165
|
+
data-x402-headers='{"X-Client":"web"}'
|
|
166
|
+
data-x402-merchant="Example API"
|
|
167
|
+
data-x402-action="Generate summary">
|
|
168
|
+
Unlock for 0.05 USDC
|
|
169
|
+
</button>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### On the `<script>` tag (read once at load)
|
|
173
|
+
|
|
174
|
+
These configure global defaults from the script element that loads the module —
|
|
175
|
+
equivalent to calling `configure()`.
|
|
176
|
+
|
|
177
|
+
| Attribute | Maps to |
|
|
178
|
+
|-------------------------------|--------------------------|
|
|
179
|
+
| `data-x402-checkout-origin` | `checkoutOrigin` |
|
|
180
|
+
| `data-x402-checkout-path` | `checkoutPath` |
|
|
181
|
+
| `data-x402-footer-note` | `footerNote` |
|
|
182
|
+
| `data-x402-brand-name` | `brand.name` |
|
|
183
|
+
| `data-x402-brand-url` | `brand.url` |
|
|
184
|
+
| `data-x402-builder-wallet` | `builderCode.wallet` |
|
|
185
|
+
| `data-x402-builder-service` | `builderCode.service` |
|
|
186
|
+
|
|
187
|
+
```html
|
|
188
|
+
<script
|
|
189
|
+
type="module"
|
|
190
|
+
src="https://unpkg.com/@three-ws/x402-payment-modal"
|
|
191
|
+
data-x402-checkout-origin="https://pay.example.com"
|
|
192
|
+
data-x402-checkout-path="/api/x402-checkout"
|
|
193
|
+
data-x402-brand-name="Example"
|
|
194
|
+
data-x402-brand-url="https://example.com"
|
|
195
|
+
data-x402-footer-note="Secured by x402"
|
|
196
|
+
data-x402-builder-wallet="examplewallet"
|
|
197
|
+
data-x402-builder-service="example_api"></script>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## DOM events
|
|
203
|
+
|
|
204
|
+
The modal dispatches bubbling `CustomEvent`s on the **clicked element** — useful
|
|
205
|
+
for declarative integrations where you never called `pay()` directly.
|
|
206
|
+
|
|
207
|
+
| Event | `detail` | When |
|
|
208
|
+
|--------------------|---------------------------------------|---------------------------------------|
|
|
209
|
+
| `x402:result` | the [`PayResult`](#payresult) | Payment settled and result returned. |
|
|
210
|
+
| `x402:error` | `{ error: string }` | The flow failed (not on cancellation).|
|
|
211
|
+
| `x402:siwx-signed` | `{ address, network }` | A [SIWX](./siwx.md) sign-in succeeded.|
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
const btn = document.querySelector('[data-x402-endpoint]');
|
|
215
|
+
|
|
216
|
+
btn.addEventListener('x402:result', (e) => {
|
|
217
|
+
console.log('paid result:', e.detail.result);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
btn.addEventListener('x402:error', (e) => {
|
|
221
|
+
console.error('payment failed:', e.detail.error);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
btn.addEventListener('x402:siwx-signed', (e) => {
|
|
225
|
+
console.log('re-entered via SIWX:', e.detail.address, e.detail.network);
|
|
226
|
+
});
|
|
227
|
+
```
|