@phala/pay 0.8.3 → 0.9.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/README.md +20 -83
- package/dist/checkout.d.ts +4 -2
- package/dist/checkout.d.ts.map +1 -1
- package/dist/checkout.js +16 -1
- package/dist/checkout.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/shared/checkout-params.d.ts +7 -0
- package/dist/shared/checkout-params.d.ts.map +1 -0
- package/dist/shared/checkout-params.js +2 -0
- package/dist/shared/checkout-params.js.map +1 -0
- package/package.json +12 -30
- package/src/checkout.ts +19 -2
- package/src/index.ts +5 -2
- package/src/shared/checkout-params.ts +6 -0
- package/dist/react/Checkout.d.ts +0 -43
- package/dist/react/Checkout.d.ts.map +0 -1
- package/dist/react/Checkout.js +0 -195
- package/dist/react/Checkout.js.map +0 -1
- package/dist/react/DepositAddress.d.ts +0 -32
- package/dist/react/DepositAddress.d.ts.map +0 -1
- package/dist/react/DepositAddress.js +0 -131
- package/dist/react/DepositAddress.js.map +0 -1
- package/dist/react/Field.d.ts +0 -7
- package/dist/react/Field.d.ts.map +0 -1
- package/dist/react/Field.js +0 -21
- package/dist/react/Field.js.map +0 -1
- package/dist/react/Icons.d.ts +0 -17
- package/dist/react/Icons.d.ts.map +0 -1
- package/dist/react/Icons.js +0 -18
- package/dist/react/Icons.js.map +0 -1
- package/dist/react/QrCode.d.ts +0 -8
- package/dist/react/QrCode.d.ts.map +0 -1
- package/dist/react/QrCode.js +0 -20
- package/dist/react/QrCode.js.map +0 -1
- package/dist/react/appearance.d.ts +0 -5
- package/dist/react/appearance.d.ts.map +0 -1
- package/dist/react/appearance.js +0 -2
- package/dist/react/appearance.js.map +0 -1
- package/dist/react/icon-paths.d.ts +0 -13
- package/dist/react/icon-paths.d.ts.map +0 -1
- package/dist/react/icon-paths.js +0 -14
- package/dist/react/icon-paths.js.map +0 -1
- package/dist/react/index.d.ts +0 -6
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -6
- package/dist/react/index.js.map +0 -1
- package/dist/react/useCheckout.d.ts +0 -9
- package/dist/react/useCheckout.d.ts.map +0 -1
- package/dist/react/useCheckout.js +0 -32
- package/dist/react/useCheckout.js.map +0 -1
- package/dist/server/addresses.d.ts +0 -78
- package/dist/server/addresses.d.ts.map +0 -1
- package/dist/server/addresses.js +0 -118
- package/dist/server/addresses.js.map +0 -1
- package/dist/server/index.d.ts +0 -12
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -12
- package/dist/server/index.js.map +0 -1
- package/dist/server/sweeps.d.ts +0 -78
- package/dist/server/sweeps.d.ts.map +0 -1
- package/dist/server/sweeps.js +0 -117
- package/dist/server/sweeps.js.map +0 -1
- package/dist/server/webhook.d.ts +0 -61
- package/dist/server/webhook.d.ts.map +0 -1
- package/dist/server/webhook.js +0 -154
- package/dist/server/webhook.js.map +0 -1
- package/src/css.d.ts +0 -1
- package/src/react/Checkout.tsx +0 -475
- package/src/react/DepositAddress.tsx +0 -240
- package/src/react/Field.tsx +0 -46
- package/src/react/Icons.tsx +0 -43
- package/src/react/QrCode.tsx +0 -39
- package/src/react/appearance.ts +0 -4
- package/src/react/icon-paths.tsx +0 -13
- package/src/react/index.ts +0 -7
- package/src/react/useCheckout.ts +0 -58
- package/src/server/addresses.ts +0 -223
- package/src/server/index.ts +0 -38
- package/src/server/sweeps.ts +0 -197
- package/src/server/webhook.ts +0 -208
- package/src/styles.css +0 -80
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useId, useState } from "react";
|
|
4
|
-
import { formatUnits } from "viem";
|
|
5
|
-
import { networkName } from "../chains.js";
|
|
6
|
-
import {
|
|
7
|
-
retrieveDepositAddress,
|
|
8
|
-
type ClientDepositAddress,
|
|
9
|
-
type DepositAddressPayment,
|
|
10
|
-
} from "../deposit-address.js";
|
|
11
|
-
import { CheckoutError, pollDelay } from "../checkout.js";
|
|
12
|
-
import { formatWait } from "../format.js";
|
|
13
|
-
import { depositAddressTransfer, type DepositAddressDetails } from "../payment.js";
|
|
14
|
-
import type { Appearance } from "./appearance.js";
|
|
15
|
-
import { AssetIcon, NetworkIcon } from "./Icons.js";
|
|
16
|
-
import { Field } from "./Field.js";
|
|
17
|
-
import { QrCode } from "./QrCode.js";
|
|
18
|
-
|
|
19
|
-
export interface DepositAddressProps {
|
|
20
|
-
/** `address` and `networks` from your backend's `POST /v1/deposit_addresses`. */
|
|
21
|
-
depositAddress: DepositAddressDetails;
|
|
22
|
-
/** The network shown first; the first of `networks` by default. */
|
|
23
|
-
chainId?: number;
|
|
24
|
-
/** The token shown first, for example `pha`; the network's first token by default. */
|
|
25
|
-
asset?: string;
|
|
26
|
-
/**
|
|
27
|
-
* The `client_secret` of the same response. With `apiBase`, the component follows the
|
|
28
|
-
* address's payments and shows each one within about a block of arriving: received with its
|
|
29
|
-
* confirmations, then credited, and states each network's typical credit time. Display only;
|
|
30
|
-
* credit from your `deposit.credited` webhook.
|
|
31
|
-
*/
|
|
32
|
-
clientSecret?: string;
|
|
33
|
-
/** The service origin, for example `https://pay.example.com`; needed with `clientSecret`. */
|
|
34
|
-
apiBase?: string;
|
|
35
|
-
/** Milliseconds between payment reads; default 3000. */
|
|
36
|
-
pollInterval?: number;
|
|
37
|
-
appearance?: Appearance;
|
|
38
|
-
className?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* A customer's persistent deposit address: one address for every supported token on every
|
|
43
|
-
* supported network. The payer picks a network and a token; the component shows a QR code of that
|
|
44
|
-
* EIP-681 transfer request and the address and token contract to copy. Any amount of a supported
|
|
45
|
-
* token sent is credited at the market rate when it arrives; follow it from your backend's
|
|
46
|
-
* `deposit.credited` webhook.
|
|
47
|
-
*/
|
|
48
|
-
export function DepositAddress({
|
|
49
|
-
depositAddress,
|
|
50
|
-
chainId,
|
|
51
|
-
asset,
|
|
52
|
-
clientSecret,
|
|
53
|
-
apiBase,
|
|
54
|
-
pollInterval,
|
|
55
|
-
appearance,
|
|
56
|
-
className,
|
|
57
|
-
}: DepositAddressProps) {
|
|
58
|
-
const { networks } = depositAddress;
|
|
59
|
-
const id = useId();
|
|
60
|
-
const view = useClientView(clientSecret, apiBase, pollInterval ?? 3000);
|
|
61
|
-
const payments = view?.payments ?? [];
|
|
62
|
-
const [selectedChain, setSelectedChain] = useState(chainId ?? networks[0]?.chain_id);
|
|
63
|
-
const [selectedAsset, setSelectedAsset] = useState(asset);
|
|
64
|
-
const network = networks.find((candidate) => candidate.chain_id === selectedChain) ?? networks[0];
|
|
65
|
-
if (network === undefined) {
|
|
66
|
-
throw new TypeError("the deposit address has no network");
|
|
67
|
-
}
|
|
68
|
-
const token =
|
|
69
|
-
network.assets.find((candidate) => candidate.asset === selectedAsset) ?? network.assets[0];
|
|
70
|
-
if (token === undefined) {
|
|
71
|
-
throw new TypeError(`the deposit address takes no token on chain ${network.chain_id}`);
|
|
72
|
-
}
|
|
73
|
-
const { token: contract, to } = depositAddressTransfer(network, token);
|
|
74
|
-
const symbol = token.asset.toUpperCase();
|
|
75
|
-
const name = networkName(network.chain_id);
|
|
76
|
-
return (
|
|
77
|
-
<div
|
|
78
|
-
className={className === undefined ? "pp-root" : `pp-root ${className}`}
|
|
79
|
-
data-theme={appearance?.theme ?? "light"}
|
|
80
|
-
>
|
|
81
|
-
<p className="pp-subtitle">
|
|
82
|
-
{depositAddress.address === null
|
|
83
|
-
? "Reusable deposit address; varies by network"
|
|
84
|
-
: "One reusable address for supported tokens and networks"}
|
|
85
|
-
</p>
|
|
86
|
-
{networks.length > 1 && (
|
|
87
|
-
<fieldset className="pp-choices">
|
|
88
|
-
<legend>Network</legend>
|
|
89
|
-
{networks.map((each) => (
|
|
90
|
-
<label key={each.chain_id}>
|
|
91
|
-
<input
|
|
92
|
-
type="radio"
|
|
93
|
-
name={`${id}-network`}
|
|
94
|
-
checked={each.chain_id === network.chain_id}
|
|
95
|
-
onChange={() => setSelectedChain(each.chain_id)}
|
|
96
|
-
/>
|
|
97
|
-
<NetworkIcon chainId={each.chain_id} size={16} />{" "}
|
|
98
|
-
{networkName(each.chain_id)}
|
|
99
|
-
</label>
|
|
100
|
-
))}
|
|
101
|
-
</fieldset>
|
|
102
|
-
)}
|
|
103
|
-
{network.assets.length > 1 && (
|
|
104
|
-
<fieldset className="pp-choices">
|
|
105
|
-
<legend>Token</legend>
|
|
106
|
-
{network.assets.map((each) => (
|
|
107
|
-
<label key={each.asset}>
|
|
108
|
-
<input
|
|
109
|
-
type="radio"
|
|
110
|
-
name={`${id}-token`}
|
|
111
|
-
checked={each.asset === token.asset}
|
|
112
|
-
onChange={() => setSelectedAsset(each.asset)}
|
|
113
|
-
/>
|
|
114
|
-
<AssetIcon asset={each.asset} size={16} />{" "}
|
|
115
|
-
{each.asset.toUpperCase()}
|
|
116
|
-
</label>
|
|
117
|
-
))}
|
|
118
|
-
</fieldset>
|
|
119
|
-
)}
|
|
120
|
-
<div className="pp-qr-panel">
|
|
121
|
-
<QrCode value={token.payment_uri} label={`Deposit address for ${symbol} on ${name}`} />
|
|
122
|
-
</div>
|
|
123
|
-
<dl className="pp-fields">
|
|
124
|
-
<Field label="Network" value={`${name} (chain ID ${network.chain_id})`} />
|
|
125
|
-
<Field label={`Token (${symbol}) contract`} value={contract} copy />
|
|
126
|
-
<Field label="Deposit address" value={to} copy />
|
|
127
|
-
</dl>
|
|
128
|
-
{payments.length > 0 && (
|
|
129
|
-
<ul className="pp-payments" aria-live="polite" aria-label="Payments">
|
|
130
|
-
{payments.map((payment) => (
|
|
131
|
-
<li key={`${payment.chain_id}:${payment.tx_hash}:${payment.created}`}>
|
|
132
|
-
{paymentMessage(payment)}
|
|
133
|
-
</li>
|
|
134
|
-
))}
|
|
135
|
-
</ul>
|
|
136
|
-
)}
|
|
137
|
-
<p className="pp-message">
|
|
138
|
-
Send {symbol} on {name}. Unsupported tokens or networks are not credited.{" "}
|
|
139
|
-
{creditMessage(networks.map((each) => each.chain_id), view)}
|
|
140
|
-
</p>
|
|
141
|
-
</div>
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** The address's public view, read every `interval` while mounted; `null` without a secret. */
|
|
146
|
-
function useClientView(
|
|
147
|
-
clientSecret: string | undefined,
|
|
148
|
-
apiBase: string | undefined,
|
|
149
|
-
interval: number,
|
|
150
|
-
): ClientDepositAddress | null {
|
|
151
|
-
const key = clientSecret === undefined || apiBase === undefined ? "" : `${apiBase} ${clientSecret}`;
|
|
152
|
-
const [current, setCurrent] = useState<{ key: string; view: ClientDepositAddress | null }>({
|
|
153
|
-
key: "",
|
|
154
|
-
view: null,
|
|
155
|
-
});
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
if (clientSecret === undefined || apiBase === undefined) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
const controller = new AbortController();
|
|
161
|
-
let stopped = false;
|
|
162
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
163
|
-
let failures = 0;
|
|
164
|
-
const load = async () => {
|
|
165
|
-
let failure: CheckoutError | null = null;
|
|
166
|
-
try {
|
|
167
|
-
const view = await retrieveDepositAddress({ clientSecret, apiBase, signal: controller.signal });
|
|
168
|
-
failures = 0;
|
|
169
|
-
if (!stopped) {
|
|
170
|
-
setCurrent({ key, view });
|
|
171
|
-
}
|
|
172
|
-
} catch (error) {
|
|
173
|
-
failures += 1;
|
|
174
|
-
// A secret that is not valid will not become valid: stop, keep showing the address.
|
|
175
|
-
if (error instanceof CheckoutError && error.code === "invalid_client_secret") {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
failure = error instanceof CheckoutError ? error : null;
|
|
179
|
-
}
|
|
180
|
-
if (!stopped) {
|
|
181
|
-
timer = setTimeout(() => void load(), pollDelay(interval, failures, failure));
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
void load();
|
|
185
|
-
return () => {
|
|
186
|
-
stopped = true;
|
|
187
|
-
controller.abort();
|
|
188
|
-
clearTimeout(timer);
|
|
189
|
-
};
|
|
190
|
-
}, [key, clientSecret, apiBase, interval]);
|
|
191
|
-
// Another address starts without the previous one's view.
|
|
192
|
-
return current.key === key && key !== "" ? current.view : null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* When a payment on each of `chainIds` is credited, from `view`'s typical credit times, for
|
|
197
|
-
* example "usually in about 30 seconds on Ethereum and about 7 seconds on Base". Before the view
|
|
198
|
-
* is loaded it names no time.
|
|
199
|
-
*/
|
|
200
|
-
function creditMessage(chainIds: number[], view: ClientDepositAddress | null): string {
|
|
201
|
-
const names = new Map<number, string[]>();
|
|
202
|
-
for (const network of view?.networks ?? []) {
|
|
203
|
-
if (chainIds.includes(network.chain_id)) {
|
|
204
|
-
const seconds = network.typical_credit_seconds;
|
|
205
|
-
names.set(seconds, [...(names.get(seconds) ?? []), networkName(network.chain_id)]);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
if (names.size === 0) {
|
|
209
|
-
return "Any amount is credited at the market rate after confirmation.";
|
|
210
|
-
}
|
|
211
|
-
const list = new Intl.ListFormat("en", { type: "conjunction" });
|
|
212
|
-
const [only, ...others] = names.keys();
|
|
213
|
-
const usually =
|
|
214
|
-
only !== undefined && others.length === 0
|
|
215
|
-
? formatWait(only)
|
|
216
|
-
: list.format([...names].map(([seconds, on]) => `${formatWait(seconds)} on ${list.format(on)}`));
|
|
217
|
-
return `Any amount is credited at the market rate on arrival, usually in ${usually}.`;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function paymentMessage(payment: DepositAddressPayment): string {
|
|
221
|
-
const amount =
|
|
222
|
-
payment.asset === null || payment.decimals === null
|
|
223
|
-
? "A payment"
|
|
224
|
-
: `${formatUnits(BigInt(payment.amount_atomic), payment.decimals)} ${payment.asset.toUpperCase()}`;
|
|
225
|
-
const on = `on ${networkName(payment.chain_id)}`;
|
|
226
|
-
switch (payment.status) {
|
|
227
|
-
case "seen":
|
|
228
|
-
return payment.confirmations === null
|
|
229
|
-
? `${amount} received ${on}, crediting shortly`
|
|
230
|
-
: `${amount} received ${on}, ${payment.confirmations} confirmation${payment.confirmations === 1 ? "" : "s"}`;
|
|
231
|
-
case "confirming":
|
|
232
|
-
return `${amount} confirmed ${on}, crediting…`;
|
|
233
|
-
case "credited":
|
|
234
|
-
return `${amount} ${on} credited`;
|
|
235
|
-
case "rejected":
|
|
236
|
-
return `${amount} ${on} cannot be credited. Contact support with your transaction.`;
|
|
237
|
-
case "reversed":
|
|
238
|
-
return `${amount} ${on} was reversed: a chain reorganization replaced it and its credit was removed.`;
|
|
239
|
-
}
|
|
240
|
-
}
|
package/src/react/Field.tsx
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
|
|
3
|
-
/** `copy` adds a copy button, copying `value`, or the given string when it differs from it. */
|
|
4
|
-
export function Field({
|
|
5
|
-
label,
|
|
6
|
-
value,
|
|
7
|
-
copy = false,
|
|
8
|
-
}: {
|
|
9
|
-
label: string;
|
|
10
|
-
value: string;
|
|
11
|
-
copy?: boolean | string;
|
|
12
|
-
}) {
|
|
13
|
-
return (
|
|
14
|
-
<div className="pp-field">
|
|
15
|
-
<dt>{label}</dt>
|
|
16
|
-
<dd>
|
|
17
|
-
<span className={copy === false ? undefined : "pp-value"}>{value}</span>
|
|
18
|
-
{copy !== false && (
|
|
19
|
-
<CopyButton value={typeof copy === "string" ? copy : value} label={label} />
|
|
20
|
-
)}
|
|
21
|
-
</dd>
|
|
22
|
-
</div>
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function CopyButton({ value, label }: { value: string; label: string }) {
|
|
27
|
-
const [copied, setCopied] = useState<boolean | null>(null);
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
if (copied === null) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const timer = setTimeout(() => setCopied(null), 2000);
|
|
33
|
-
return () => clearTimeout(timer);
|
|
34
|
-
}, [copied]);
|
|
35
|
-
const onClick = () => {
|
|
36
|
-
navigator.clipboard.writeText(value).then(
|
|
37
|
-
() => setCopied(true),
|
|
38
|
-
() => setCopied(false),
|
|
39
|
-
);
|
|
40
|
-
};
|
|
41
|
-
return (
|
|
42
|
-
<button type="button" className="pp-copy" onClick={onClick} aria-label={`Copy ${label}`}>
|
|
43
|
-
<span aria-live="polite">{copied === null ? "Copy" : copied ? "Copied" : "Copy failed"}</span>
|
|
44
|
-
</button>
|
|
45
|
-
);
|
|
46
|
-
}
|
package/src/react/Icons.tsx
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { networkName, networkIconName } from "../chains.js";
|
|
4
|
-
import { assetIconName, iconMonogram, type IconName } from "../icon-names.js";
|
|
5
|
-
import { ICON_PATHS } from "./icon-paths.js";
|
|
6
|
-
|
|
7
|
-
export interface IconProps {
|
|
8
|
-
/** Width and height in pixels; default 18. No inline styles are injected. */
|
|
9
|
-
size?: number;
|
|
10
|
-
/** Default true next to visible text. Set false when the icon is used alone. */
|
|
11
|
-
decorative?: boolean;
|
|
12
|
-
}
|
|
13
|
-
export interface NetworkIconProps extends IconProps { chainId: number }
|
|
14
|
-
export interface AssetIconProps extends IconProps { asset: string }
|
|
15
|
-
|
|
16
|
-
function Icon({ name, label, size = 18, decorative = true }: IconProps & {
|
|
17
|
-
name: IconName | undefined;
|
|
18
|
-
label: string;
|
|
19
|
-
}) {
|
|
20
|
-
const pixels = Number.isFinite(size) && size > 0 ? size : 18;
|
|
21
|
-
return (
|
|
22
|
-
<span className="pp-icon-label" role={decorative ? undefined : "img"}
|
|
23
|
-
aria-label={decorative ? undefined : label || "Unknown asset"}>
|
|
24
|
-
<svg className="pp-icon" xmlns="http://www.w3.org/2000/svg" width={pixels} height={pixels}
|
|
25
|
-
viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">
|
|
26
|
-
{name === undefined ? <>
|
|
27
|
-
<circle cx="12" cy="12" r="11" fill="currentColor" opacity="0.12" />
|
|
28
|
-
<text x="12" y="16" textAnchor="middle" fontFamily="sans-serif" fontSize="12" fill="currentColor">{iconMonogram(label)}</text>
|
|
29
|
-
</> : ICON_PATHS[name]}
|
|
30
|
-
</svg>
|
|
31
|
-
</span>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Branded network icon; testnets reuse their mainnet family artwork. */
|
|
36
|
-
export function NetworkIcon({ chainId, ...props }: NetworkIconProps) {
|
|
37
|
-
return <Icon {...props} name={networkIconName(chainId)} label={networkName(chainId)} />;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Branded token icon, or a neutral first-letter monogram for unknown symbols. */
|
|
41
|
-
export function AssetIcon({ asset, ...props }: AssetIconProps) {
|
|
42
|
-
return <Icon {...props} name={assetIconName(asset)} label={asset.trim().toUpperCase()} />;
|
|
43
|
-
}
|
package/src/react/QrCode.tsx
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
|
-
import { encode } from "uqr";
|
|
3
|
-
|
|
4
|
-
export interface QrCodeProps {
|
|
5
|
-
value: string;
|
|
6
|
-
label: string;
|
|
7
|
-
size?: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** A QR code as an SVG path, one unit per module. */
|
|
11
|
-
export function QrCode({ value, label, size = 208 }: QrCodeProps) {
|
|
12
|
-
const { path, width } = useMemo(() => {
|
|
13
|
-
const { data, size: modules } = encode(value, { ecc: "M", border: 2 });
|
|
14
|
-
let d = "";
|
|
15
|
-
data.forEach((row, y) => {
|
|
16
|
-
row.forEach((dark, x) => {
|
|
17
|
-
if (dark) {
|
|
18
|
-
d += `M${x} ${y}h1v1h-1z`;
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
return { path: d, width: modules };
|
|
23
|
-
}, [value]);
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<svg
|
|
27
|
-
className="pp-qr"
|
|
28
|
-
role="img"
|
|
29
|
-
aria-label={label}
|
|
30
|
-
width={size}
|
|
31
|
-
height={size}
|
|
32
|
-
viewBox={`0 0 ${width} ${width}`}
|
|
33
|
-
shapeRendering="crispEdges"
|
|
34
|
-
>
|
|
35
|
-
<rect width={width} height={width} fill="#fff" />
|
|
36
|
-
<path d={path} fill="#000" />
|
|
37
|
-
</svg>
|
|
38
|
-
);
|
|
39
|
-
}
|
package/src/react/appearance.ts
DELETED
package/src/react/icon-paths.tsx
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/** Generated from @web3icons/core@4.0.57 by scripts/generate-icons.mjs.
|
|
2
|
-
* SPDX-License-Identifier: MIT; Copyright (c) 2024 0xa3k5.
|
|
3
|
-
* See THIRD_PARTY_NOTICES for the full license. Do not edit.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const ICON_PATHS = {
|
|
7
|
-
usdc: <><path fill="#0B53BF" d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18"/><path fill="#fff" d="M13.62 5.45v1.159a5.64 5.64 0 0 1 4.005 5.394 5.64 5.64 0 0 1-4.005 5.394v1.16a6.74 6.74 0 0 0 5.13-6.554 6.74 6.74 0 0 0-5.13-6.553m-7.245 6.553a5.64 5.64 0 0 1 4.005-5.394V5.45a6.74 6.74 0 0 0-5.13 6.553 6.74 6.74 0 0 0 5.13 6.553v-1.159a5.63 5.63 0 0 1-4.005-5.394"/><path fill="#fff" d="M14.419 13.258c0-2.301-3.606-1.356-3.606-2.627 0-.456.366-.748 1.063-.748.833 0 1.12.405 1.21.95h1.147c-.102-1.024-.69-1.67-1.67-1.863v-.904h-1.125v.872c-1.075.137-1.75.762-1.75 1.693 0 2.312 3.611 1.445 3.611 2.694 0 .472-.455.787-1.226.787-1.007 0-1.339-.444-1.462-1.057H9.49c.073 1.122.764 1.823 1.947 1.999v.886h1.125v-.875c1.153-.149 1.856-.82 1.856-1.807"/></>,
|
|
8
|
-
usdt: <><path fill="#009393" d="m12 19.2-9-8.88L6.433 4.8h11.134L21 10.32zm.9-8.1v-1.098c1.62.08 3.132.396 3.6.805-.544.477-2.493.824-4.5.824s-3.956-.347-4.5-.824c.463-.41 1.98-.72 3.6-.81V11.1zm-5.4-.297v.661c.463.41 1.975.72 3.6.81V14.7h1.8v-2.43c1.62-.081 3.136-.396 3.6-.806v-1.318c-.464-.41-1.98-.725-3.6-.81V8.4h2.7V7.05H8.4V8.4h2.7v.936c-1.625.085-3.137.4-3.6.81z"/></>,
|
|
9
|
-
pha: <><circle cx="12" cy="12" r="12" fill="#252525"/><path fill="#D1FF52" fillRule="evenodd" d="M3 3h14.143v3.857H21V12h-3.857V6.857H6.857V12H3zm3.857 12.857h10.286V12H6.857zm0 0V21H3v-5.143z" clipRule="evenodd"/></>,
|
|
10
|
-
eth: <><path fill="#8FFCF3" d="M12 3v6.65l5.625 2.516z"/><path fill="#CABCF8" d="m12 3-5.625 9.166L12 9.651z"/><path fill="#CBA7F5" d="M12 16.477v4.522l5.625-7.784z"/><path fill="#74A0F3" d="M12 21v-4.523l-5.625-3.262z"/><path fill="#CBA7F5" d="m12 15.43 5.625-3.263L12 9.65z"/><path fill="#74A0F3" d="M6.375 12.167 12 15.429V9.651z"/><path fill="#202699" fillRule="evenodd" d="m12 15.429-5.625-3.263L12 3l5.625 9.166zM6.749 11.9l5.16-8.41v6.115zm-.077.23 5.238-2.327v5.364zm5.418-2.327v5.364l5.233-3.038zm0-.198 5.16 2.295-5.16-8.41z" clipRule="evenodd"/><path fill="#202699" fillRule="evenodd" d="M12 16.406 6.375 13.21 12 21l5.625-7.79zm-4.995-2.633 4.905 2.79v4.005zm5.085 2.79v4.005l4.905-6.795z" clipRule="evenodd"/></>,
|
|
11
|
-
ethereum: <><path fill="#8FFCF3" d="M12 3v6.651l5.625 2.516z"/><path fill="#CABCF8" d="m12 3-5.625 9.166L12 9.653z"/><path fill="#CBA7F5" d="M12 16.478V21l5.625-7.784z"/><path fill="#74A0F3" d="M12 21v-4.522l-5.625-3.262z"/><path fill="#CBA7F5" d="m12 15.43 5.625-3.263L12 9.652z"/><path fill="#74A0F3" d="M6.375 12.167 12 15.43V9.652z"/><path fill="#202699" fillRule="evenodd" d="m12 15.43-5.625-3.263L12 3l5.624 9.166zm-5.252-3.528 5.161-8.41v6.114zm-.077.229 5.238-2.327v5.364zm5.418-2.327v5.364l5.234-3.037zm0-.198 5.161 2.296-5.161-8.41z" clipRule="evenodd"/><path fill="#202699" fillRule="evenodd" d="m12 16.406-5.625-3.195L12 21l5.624-7.79zm-4.995-2.633 4.904 2.79v4.005zm5.084 2.79v4.005l4.905-6.795z" clipRule="evenodd"/></>,
|
|
12
|
-
base: <><path fill="#00F" d="M3 4.706c0-.585 0-.877.11-1.101.106-.215.28-.39.496-.495C3.83 3 4.122 3 4.706 3h14.588c.585 0 .876 0 1.101.11.215.105.389.28.494.495.111.225.111.517.111 1.101v14.588c0 .585 0 .876-.11 1.101-.106.215-.28.389-.495.494-.225.111-.517.111-1.101.111H4.706c-.585 0-.876 0-1.101-.11a1.08 1.08 0 0 1-.494-.495C3 20.17 3 19.878 3 19.294z"/></>,
|
|
13
|
-
};
|
package/src/react/index.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
export { Checkout, type CheckoutProps } from "./Checkout.js";
|
|
4
|
-
export { DepositAddress, type DepositAddressProps } from "./DepositAddress.js";
|
|
5
|
-
export { useCheckout, type UseCheckoutOptions, type UseCheckoutResult } from "./useCheckout.js";
|
|
6
|
-
export type { Appearance } from "./appearance.js";
|
|
7
|
-
export { NetworkIcon, AssetIcon, type IconProps, type NetworkIconProps, type AssetIconProps } from "./Icons.js";
|
package/src/react/useCheckout.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
createCheckout,
|
|
4
|
-
type CheckoutOptions,
|
|
5
|
-
type CheckoutSession,
|
|
6
|
-
type CheckoutState,
|
|
7
|
-
} from "../checkout.js";
|
|
8
|
-
|
|
9
|
-
export type UseCheckoutOptions = Pick<
|
|
10
|
-
CheckoutOptions,
|
|
11
|
-
"clientSecret" | "expectedAddress" | "apiBase" | "pollInterval" | "signal" | "requestTimeout"
|
|
12
|
-
>;
|
|
13
|
-
|
|
14
|
-
export interface UseCheckoutResult extends CheckoutState {
|
|
15
|
-
/** Reads the quote now, for example right after the wallet broadcast the payment. */
|
|
16
|
-
refresh: () => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const LOADING: CheckoutState = { status: "loading", quote: null, error: null };
|
|
20
|
-
|
|
21
|
-
/** Follows a quote's public view for as long as the component is mounted. */
|
|
22
|
-
export function useCheckout({
|
|
23
|
-
clientSecret,
|
|
24
|
-
expectedAddress,
|
|
25
|
-
apiBase,
|
|
26
|
-
pollInterval,
|
|
27
|
-
signal,
|
|
28
|
-
requestTimeout,
|
|
29
|
-
}: UseCheckoutOptions): UseCheckoutResult {
|
|
30
|
-
const [current, setCurrent] = useState({ key: "", state: LOADING });
|
|
31
|
-
const checkout = useRef<CheckoutSession | null>(null);
|
|
32
|
-
const key = `${apiBase} ${clientSecret} ${expectedAddress}`;
|
|
33
|
-
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
const instance = createCheckout({
|
|
36
|
-
clientSecret,
|
|
37
|
-
expectedAddress,
|
|
38
|
-
apiBase,
|
|
39
|
-
...(pollInterval === undefined ? {} : { pollInterval }),
|
|
40
|
-
...(signal === undefined ? {} : { signal }),
|
|
41
|
-
...(requestTimeout === undefined ? {} : { requestTimeout }),
|
|
42
|
-
});
|
|
43
|
-
checkout.current = instance;
|
|
44
|
-
const unsubscribe = instance.subscribe((state) => setCurrent({ key, state }));
|
|
45
|
-
return () => {
|
|
46
|
-
unsubscribe();
|
|
47
|
-
instance.destroy();
|
|
48
|
-
checkout.current = null;
|
|
49
|
-
};
|
|
50
|
-
}, [key, clientSecret, expectedAddress, apiBase, pollInterval, signal, requestTimeout]);
|
|
51
|
-
|
|
52
|
-
const refresh = useCallback(() => {
|
|
53
|
-
void checkout.current?.refresh();
|
|
54
|
-
}, []);
|
|
55
|
-
|
|
56
|
-
// A new client secret starts from `loading`, not from the previous quote's state.
|
|
57
|
-
return { ...(current.key === key ? current.state : LOADING), refresh };
|
|
58
|
-
}
|
package/src/server/addresses.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
encodeAbiParameters,
|
|
3
|
-
getAddress,
|
|
4
|
-
getContractAddress,
|
|
5
|
-
keccak256,
|
|
6
|
-
concat,
|
|
7
|
-
type Address,
|
|
8
|
-
type Hex,
|
|
9
|
-
} from "viem";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* The forwarder `factory` deploys for `treasury` and `salt`: OpenZeppelin 5.x
|
|
13
|
-
* `Clones.predictDeterministicAddressWithImmutableArgs` with `abi.encodePacked(treasury)`, so the
|
|
14
|
-
* address commits to the factory, the implementation, the treasury, and the salt.
|
|
15
|
-
*/
|
|
16
|
-
export function forwarderAddress(
|
|
17
|
-
factory: string,
|
|
18
|
-
implementation: string,
|
|
19
|
-
treasury: string,
|
|
20
|
-
salt: Hex,
|
|
21
|
-
): Address {
|
|
22
|
-
const initCode = concat([
|
|
23
|
-
"0x61",
|
|
24
|
-
"0x0041", // runtime length: the 45-byte proxy and the 20 treasury bytes
|
|
25
|
-
"0x3d81600a3d39f3",
|
|
26
|
-
"0x363d3d373d3d3d363d73",
|
|
27
|
-
getAddress(implementation),
|
|
28
|
-
"0x5af43d82803e903d91602b57fd5bf3",
|
|
29
|
-
getAddress(treasury),
|
|
30
|
-
]);
|
|
31
|
-
return getContractAddress({
|
|
32
|
-
opcode: "CREATE2",
|
|
33
|
-
from: getAddress(factory),
|
|
34
|
-
salt,
|
|
35
|
-
bytecodeHash: keccak256(initCode),
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** `keccak256(abi.encode(account, client_reference_id, "quote", quote_id))`: a quote's salt. */
|
|
40
|
-
export function quoteSalt(account: string, clientReferenceId: string, quoteId: string): Hex {
|
|
41
|
-
return keccak256(
|
|
42
|
-
encodeAbiParameters(
|
|
43
|
-
[{ type: "string" }, { type: "string" }, { type: "string" }, { type: "string" }],
|
|
44
|
-
[account, clientReferenceId, "quote", quoteId],
|
|
45
|
-
),
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* `keccak256(abi.encode(account, livemode, client_reference_id, "deposit_address", version))`:
|
|
51
|
-
* a deposit address's salt, the same on every chain.
|
|
52
|
-
*/
|
|
53
|
-
export function depositAddressSalt(
|
|
54
|
-
account: string,
|
|
55
|
-
livemode: boolean,
|
|
56
|
-
clientReferenceId: string,
|
|
57
|
-
version: number | bigint,
|
|
58
|
-
): Hex {
|
|
59
|
-
return keccak256(
|
|
60
|
-
encodeAbiParameters(
|
|
61
|
-
[
|
|
62
|
-
{ type: "string" },
|
|
63
|
-
{ type: "bool" },
|
|
64
|
-
{ type: "string" },
|
|
65
|
-
{ type: "string" },
|
|
66
|
-
{ type: "uint256" },
|
|
67
|
-
],
|
|
68
|
-
[account, livemode, clientReferenceId, "deposit_address", BigInt(version)],
|
|
69
|
-
),
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface Forwarder {
|
|
74
|
-
factory: string;
|
|
75
|
-
implementation: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** The address of a quote over `treasury`, which you pin yourself (see `verifyQuoteAddress`). */
|
|
79
|
-
export function quoteAddress(
|
|
80
|
-
forwarder: Forwarder,
|
|
81
|
-
quote: { client_reference_id: string; id: string },
|
|
82
|
-
treasury: string,
|
|
83
|
-
account: string,
|
|
84
|
-
): Address {
|
|
85
|
-
return forwarderAddress(
|
|
86
|
-
forwarder.factory,
|
|
87
|
-
forwarder.implementation,
|
|
88
|
-
treasury,
|
|
89
|
-
quoteSalt(account, quote.client_reference_id, quote.id),
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** A deposit address on the network whose treasury is `treasury`, which you pin yourself. */
|
|
94
|
-
export function depositAddress(
|
|
95
|
-
forwarder: Forwarder,
|
|
96
|
-
address: { livemode: boolean; client_reference_id: string; version: number },
|
|
97
|
-
treasury: string,
|
|
98
|
-
account: string,
|
|
99
|
-
): Address {
|
|
100
|
-
return forwarderAddress(
|
|
101
|
-
forwarder.factory,
|
|
102
|
-
forwarder.implementation,
|
|
103
|
-
treasury,
|
|
104
|
-
depositAddressSalt(account, address.livemode, address.client_reference_id, address.version),
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** An address the service returned is not the one your pins derive: show nothing to pay. */
|
|
109
|
-
export class AddressMismatchError extends Error {
|
|
110
|
-
override readonly name = "AddressMismatchError";
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* What every address is recomputed from, configured on your server and never read from the
|
|
115
|
-
* service: your account id (`acct_…`), the forwarder `factory` and `implementation` of the
|
|
116
|
-
* attested deployment, and `treasuries`, your own treasury per chain id as you proved it. A
|
|
117
|
-
* compromised service could return another treasury with the address that really derives from
|
|
118
|
-
* it, so the response's `treasury` is never trusted.
|
|
119
|
-
*
|
|
120
|
-
* Live mode requires `treasuries`: without the chain's treasury the check fails closed. In test
|
|
121
|
-
* mode an unpinned chain falls back to the response's treasury with a console warning.
|
|
122
|
-
*/
|
|
123
|
-
export interface AddressPins extends Forwarder {
|
|
124
|
-
account: string;
|
|
125
|
-
treasuries?: Readonly<Record<number, string>> | undefined;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* The treasury an address on `chainId` must pay: the pinned one, which the response must name.
|
|
130
|
-
*/
|
|
131
|
-
function pinnedTreasury(
|
|
132
|
-
pins: AddressPins,
|
|
133
|
-
livemode: boolean,
|
|
134
|
-
chainId: number,
|
|
135
|
-
treasury: string,
|
|
136
|
-
where: string,
|
|
137
|
-
): string {
|
|
138
|
-
const pinned = pins.treasuries?.[chainId];
|
|
139
|
-
if (pinned === undefined) {
|
|
140
|
-
if (livemode) {
|
|
141
|
-
throw new AddressMismatchError(`${where}: live mode requires your pinned treasury of chain ${chainId}`);
|
|
142
|
-
}
|
|
143
|
-
console.warn(
|
|
144
|
-
`${where}: no pinned treasury of chain ${chainId}; the service's ${treasury} is trusted (test mode only)`,
|
|
145
|
-
);
|
|
146
|
-
return treasury;
|
|
147
|
-
}
|
|
148
|
-
if (!sameAddress(pinned, treasury)) {
|
|
149
|
-
throw new AddressMismatchError(`${where} pays a treasury that is not the pinned one`);
|
|
150
|
-
}
|
|
151
|
-
return pinned;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function requirePins(pins: AddressPins, where: string): void {
|
|
155
|
-
for (const field of ["account", "factory", "implementation"] as const) {
|
|
156
|
-
if (typeof pins[field] !== "string" || pins[field] === "") {
|
|
157
|
-
throw new AddressMismatchError(`${where}: the pinned ${field} is missing`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function sameAddress(a: string, b: string): boolean {
|
|
163
|
-
return a.toLowerCase() === b.toLowerCase();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Recomputes an open quote's address from your pins and returns it, the `expectedAddress` to
|
|
168
|
-
* pass to `<Checkout>`. Throws `AddressMismatchError` unless the quote names your pinned
|
|
169
|
-
* treasury of its chain and its address derives from it; in live mode also when the chain has no
|
|
170
|
-
* pinned treasury.
|
|
171
|
-
*/
|
|
172
|
-
export function verifyQuoteAddress(
|
|
173
|
-
pins: AddressPins,
|
|
174
|
-
quote: {
|
|
175
|
-
id: string;
|
|
176
|
-
livemode: boolean;
|
|
177
|
-
chain_id: number;
|
|
178
|
-
treasury: string;
|
|
179
|
-
address: string;
|
|
180
|
-
client_reference_id: string;
|
|
181
|
-
},
|
|
182
|
-
): Address {
|
|
183
|
-
const where = `quote ${quote.id}`;
|
|
184
|
-
requirePins(pins, where);
|
|
185
|
-
const treasury = pinnedTreasury(pins, quote.livemode, quote.chain_id, quote.treasury, where);
|
|
186
|
-
const derived = quoteAddress(pins, quote, treasury, pins.account);
|
|
187
|
-
if (!sameAddress(derived, quote.address)) {
|
|
188
|
-
throw new AddressMismatchError(`${where} has an address the account cannot derive`);
|
|
189
|
-
}
|
|
190
|
-
return derived;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Recomputes every network of an active deposit address from your pins and returns the address,
|
|
195
|
-
* the same on every network. Throws `AddressMismatchError` as `verifyQuoteAddress` does, for any
|
|
196
|
-
* network.
|
|
197
|
-
*/
|
|
198
|
-
export function verifyDepositAddress(
|
|
199
|
-
pins: AddressPins,
|
|
200
|
-
address: {
|
|
201
|
-
id: string;
|
|
202
|
-
livemode: boolean;
|
|
203
|
-
client_reference_id: string;
|
|
204
|
-
version: number;
|
|
205
|
-
networks: readonly { chain_id: number; treasury: string; address: string }[];
|
|
206
|
-
},
|
|
207
|
-
): Address {
|
|
208
|
-
requirePins(pins, `deposit address ${address.id}`);
|
|
209
|
-
let first: Address | undefined;
|
|
210
|
-
for (const network of address.networks) {
|
|
211
|
-
const where = `deposit address ${address.id} on chain ${network.chain_id}`;
|
|
212
|
-
const treasury = pinnedTreasury(pins, address.livemode, network.chain_id, network.treasury, where);
|
|
213
|
-
const derived = depositAddress(pins, address, treasury, pins.account);
|
|
214
|
-
if (!sameAddress(derived, network.address)) {
|
|
215
|
-
throw new AddressMismatchError(`${where} is not one the account can derive`);
|
|
216
|
-
}
|
|
217
|
-
first ??= derived;
|
|
218
|
-
}
|
|
219
|
-
if (first === undefined) {
|
|
220
|
-
throw new AddressMismatchError(`deposit address ${address.id} has no network`);
|
|
221
|
-
}
|
|
222
|
-
return first;
|
|
223
|
-
}
|