@wopr-network/platform-ui-core 1.7.0 → 1.8.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wopr-network/platform-ui-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Brand-agnostic AI agent platform UI — deploy as any brand via env vars",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@hookform/resolvers": "^5.2.2",
|
|
47
|
+
"@noble/hashes": "^2.0.1",
|
|
48
|
+
"@scure/bip32": "^2.0.1",
|
|
49
|
+
"@scure/bip39": "^2.0.1",
|
|
47
50
|
"@stripe/react-stripe-js": "^5.6.0",
|
|
48
51
|
"@stripe/stripe-js": "^8.7.0",
|
|
49
52
|
"@tanstack/react-query": "^5.90.21",
|
|
@@ -2,24 +2,16 @@ import { render, screen, waitFor } from "@testing-library/react";
|
|
|
2
2
|
import userEvent from "@testing-library/user-event";
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
|
|
5
|
-
const {
|
|
6
|
-
vi.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
mockGetSupportedPaymentMethods: vi.fn(),
|
|
10
|
-
}));
|
|
5
|
+
const { mockCreateCheckout, mockGetSupportedPaymentMethods } = vi.hoisted(() => ({
|
|
6
|
+
mockCreateCheckout: vi.fn(),
|
|
7
|
+
mockGetSupportedPaymentMethods: vi.fn(),
|
|
8
|
+
}));
|
|
11
9
|
|
|
12
10
|
vi.mock("@/lib/api", () => ({
|
|
13
|
-
|
|
14
|
-
createEthCheckout: vi.fn(),
|
|
15
|
-
createStablecoinCheckout: vi.fn(),
|
|
11
|
+
createCheckout: (...args: unknown[]) => mockCreateCheckout(...args),
|
|
16
12
|
getSupportedPaymentMethods: (...args: unknown[]) => mockGetSupportedPaymentMethods(...args),
|
|
17
13
|
}));
|
|
18
14
|
|
|
19
|
-
vi.mock("@/lib/validate-redirect-url", () => ({
|
|
20
|
-
isAllowedRedirectUrl: (...args: unknown[]) => mockIsAllowedRedirectUrl(...args),
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
15
|
vi.mock("framer-motion", () => ({
|
|
24
16
|
motion: {
|
|
25
17
|
button: ({
|
|
@@ -65,33 +57,34 @@ vi.mock("lucide-react", async (importOriginal) => {
|
|
|
65
57
|
|
|
66
58
|
const MOCK_USDC_METHOD = {
|
|
67
59
|
id: "usdc:base",
|
|
68
|
-
type: "
|
|
60
|
+
type: "erc20",
|
|
69
61
|
label: "USDC on Base",
|
|
70
62
|
token: "USDC",
|
|
71
63
|
chain: "base",
|
|
72
64
|
};
|
|
73
|
-
const
|
|
74
|
-
id: "
|
|
75
|
-
type: "
|
|
76
|
-
label: "
|
|
77
|
-
token: "
|
|
78
|
-
chain: "
|
|
65
|
+
const MOCK_ETH_METHOD = {
|
|
66
|
+
id: "eth:base",
|
|
67
|
+
type: "native",
|
|
68
|
+
label: "ETH on Base",
|
|
69
|
+
token: "ETH",
|
|
70
|
+
chain: "base",
|
|
79
71
|
};
|
|
80
72
|
|
|
81
73
|
afterEach(() => {
|
|
82
74
|
vi.restoreAllMocks();
|
|
83
|
-
|
|
84
|
-
mockCreateCryptoCheckout.mockReset();
|
|
75
|
+
mockCreateCheckout.mockReset();
|
|
85
76
|
mockGetSupportedPaymentMethods.mockReset();
|
|
86
77
|
});
|
|
87
78
|
|
|
88
79
|
describe("BuyCryptoCreditPanel", () => {
|
|
89
80
|
it("renders crypto amount buttons ($10, $25, $50, $100)", async () => {
|
|
90
|
-
mockGetSupportedPaymentMethods.mockResolvedValue([MOCK_USDC_METHOD,
|
|
81
|
+
mockGetSupportedPaymentMethods.mockResolvedValue([MOCK_USDC_METHOD, MOCK_ETH_METHOD]);
|
|
91
82
|
const { BuyCryptoCreditPanel } = await import("../components/billing/buy-crypto-credits-panel");
|
|
92
83
|
render(<BuyCryptoCreditPanel />);
|
|
93
84
|
|
|
94
|
-
|
|
85
|
+
await waitFor(() => {
|
|
86
|
+
expect(screen.getByText("Pay with Crypto")).toBeInTheDocument();
|
|
87
|
+
});
|
|
95
88
|
expect(screen.getByText("$10")).toBeInTheDocument();
|
|
96
89
|
expect(screen.getByText("$25")).toBeInTheDocument();
|
|
97
90
|
expect(screen.getByText("$50")).toBeInTheDocument();
|
|
@@ -99,13 +92,12 @@ describe("BuyCryptoCreditPanel", () => {
|
|
|
99
92
|
});
|
|
100
93
|
|
|
101
94
|
it("Pay button is disabled when no amount selected", async () => {
|
|
102
|
-
mockGetSupportedPaymentMethods.mockResolvedValue([MOCK_USDC_METHOD,
|
|
95
|
+
mockGetSupportedPaymentMethods.mockResolvedValue([MOCK_USDC_METHOD, MOCK_ETH_METHOD]);
|
|
103
96
|
const { BuyCryptoCreditPanel } = await import("../components/billing/buy-crypto-credits-panel");
|
|
104
97
|
render(<BuyCryptoCreditPanel />);
|
|
105
98
|
|
|
106
|
-
// Wait for methods to load
|
|
107
99
|
await waitFor(() => {
|
|
108
|
-
expect(screen.getByText("
|
|
100
|
+
expect(screen.getByText("USDC")).toBeInTheDocument();
|
|
109
101
|
});
|
|
110
102
|
|
|
111
103
|
const payBtn = screen.getByRole("button", { name: "Pay with USDC" });
|
|
@@ -113,120 +105,110 @@ describe("BuyCryptoCreditPanel", () => {
|
|
|
113
105
|
});
|
|
114
106
|
|
|
115
107
|
it("Pay button is enabled after selecting an amount", async () => {
|
|
116
|
-
mockGetSupportedPaymentMethods.mockResolvedValue([MOCK_USDC_METHOD,
|
|
108
|
+
mockGetSupportedPaymentMethods.mockResolvedValue([MOCK_USDC_METHOD, MOCK_ETH_METHOD]);
|
|
117
109
|
const user = userEvent.setup();
|
|
118
110
|
const { BuyCryptoCreditPanel } = await import("../components/billing/buy-crypto-credits-panel");
|
|
119
111
|
render(<BuyCryptoCreditPanel />);
|
|
120
112
|
|
|
121
113
|
await waitFor(() => {
|
|
122
|
-
expect(screen.getByText("
|
|
114
|
+
expect(screen.getByText("USDC")).toBeInTheDocument();
|
|
123
115
|
});
|
|
124
116
|
|
|
125
117
|
await user.click(screen.getByText("$25"));
|
|
126
118
|
expect(screen.getByRole("button", { name: "Pay with USDC" })).toBeEnabled();
|
|
127
119
|
});
|
|
128
120
|
|
|
129
|
-
it("calls
|
|
130
|
-
mockGetSupportedPaymentMethods.mockResolvedValue([MOCK_USDC_METHOD,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
});
|
|
137
|
-
Object.defineProperty(window.location, "href", {
|
|
138
|
-
set: hrefSetter,
|
|
139
|
-
configurable: true,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
mockCreateCryptoCheckout.mockResolvedValue({
|
|
143
|
-
url: "https://btcpay.example.com/i/abc123",
|
|
144
|
-
referenceId: "ref-abc123",
|
|
121
|
+
it("calls createCheckout and shows deposit address", async () => {
|
|
122
|
+
mockGetSupportedPaymentMethods.mockResolvedValue([MOCK_USDC_METHOD, MOCK_ETH_METHOD]);
|
|
123
|
+
mockCreateCheckout.mockResolvedValue({
|
|
124
|
+
depositAddress: "0xabc123def456",
|
|
125
|
+
displayAmount: "50.000000 USDC",
|
|
126
|
+
token: "USDC",
|
|
127
|
+
chain: "base",
|
|
145
128
|
});
|
|
146
|
-
mockIsAllowedRedirectUrl.mockReturnValue(true);
|
|
147
129
|
|
|
148
130
|
const user = userEvent.setup();
|
|
149
131
|
const { BuyCryptoCreditPanel } = await import("../components/billing/buy-crypto-credits-panel");
|
|
150
132
|
render(<BuyCryptoCreditPanel />);
|
|
151
133
|
|
|
152
134
|
await waitFor(() => {
|
|
153
|
-
expect(screen.getByText("
|
|
135
|
+
expect(screen.getByText("USDC")).toBeInTheDocument();
|
|
154
136
|
});
|
|
155
137
|
|
|
156
|
-
// Switch to BTC tab
|
|
157
|
-
await user.click(screen.getByText("BTC"));
|
|
158
138
|
await user.click(screen.getByText("$50"));
|
|
159
|
-
await user.click(screen.getByRole("button", { name: "Pay with
|
|
139
|
+
await user.click(screen.getByRole("button", { name: "Pay with USDC" }));
|
|
160
140
|
|
|
161
|
-
expect(
|
|
162
|
-
expect(
|
|
163
|
-
expect(
|
|
141
|
+
expect(mockCreateCheckout).toHaveBeenCalledWith("usdc:base", 50);
|
|
142
|
+
expect(await screen.findByText("0xabc123def456")).toBeInTheDocument();
|
|
143
|
+
expect(screen.getByText("50.000000 USDC")).toBeInTheDocument();
|
|
164
144
|
});
|
|
165
145
|
|
|
166
|
-
it("shows
|
|
167
|
-
mockGetSupportedPaymentMethods.mockResolvedValue([MOCK_USDC_METHOD,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
146
|
+
it("shows Creating checkout... while checkout is in progress", async () => {
|
|
147
|
+
mockGetSupportedPaymentMethods.mockResolvedValue([MOCK_USDC_METHOD, MOCK_ETH_METHOD]);
|
|
148
|
+
mockCreateCheckout.mockReturnValue(
|
|
149
|
+
new Promise(() => {
|
|
150
|
+
/* intentionally pending */
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
173
153
|
|
|
174
154
|
const user = userEvent.setup();
|
|
175
155
|
const { BuyCryptoCreditPanel } = await import("../components/billing/buy-crypto-credits-panel");
|
|
176
156
|
render(<BuyCryptoCreditPanel />);
|
|
177
157
|
|
|
178
158
|
await waitFor(() => {
|
|
179
|
-
expect(screen.getByText("
|
|
159
|
+
expect(screen.getByText("USDC")).toBeInTheDocument();
|
|
180
160
|
});
|
|
181
161
|
|
|
182
|
-
await user.click(screen.getByText("
|
|
183
|
-
await user.click(screen.
|
|
184
|
-
await user.click(screen.getByRole("button", { name: "Pay with BTC" }));
|
|
162
|
+
await user.click(screen.getByText("$50"));
|
|
163
|
+
await user.click(screen.getByRole("button", { name: "Pay with USDC" }));
|
|
185
164
|
|
|
186
|
-
expect(
|
|
187
|
-
await screen.findByText("Unexpected checkout URL. Please contact support."),
|
|
188
|
-
).toBeInTheDocument();
|
|
165
|
+
expect(await screen.findByText("Creating checkout...")).toBeInTheDocument();
|
|
189
166
|
});
|
|
190
167
|
|
|
191
|
-
it("shows
|
|
192
|
-
mockGetSupportedPaymentMethods.mockResolvedValue([MOCK_USDC_METHOD,
|
|
193
|
-
|
|
194
|
-
new Promise(() => {
|
|
195
|
-
/* intentionally pending */
|
|
196
|
-
}),
|
|
197
|
-
);
|
|
168
|
+
it("shows error when checkout API call fails", async () => {
|
|
169
|
+
mockGetSupportedPaymentMethods.mockResolvedValue([MOCK_USDC_METHOD, MOCK_ETH_METHOD]);
|
|
170
|
+
mockCreateCheckout.mockRejectedValue(new Error("API down"));
|
|
198
171
|
|
|
199
172
|
const user = userEvent.setup();
|
|
200
173
|
const { BuyCryptoCreditPanel } = await import("../components/billing/buy-crypto-credits-panel");
|
|
201
174
|
render(<BuyCryptoCreditPanel />);
|
|
202
175
|
|
|
203
176
|
await waitFor(() => {
|
|
204
|
-
expect(screen.getByText("
|
|
177
|
+
expect(screen.getByText("USDC")).toBeInTheDocument();
|
|
205
178
|
});
|
|
206
179
|
|
|
207
|
-
await user.click(screen.getByText("
|
|
208
|
-
await user.click(screen.
|
|
209
|
-
await user.click(screen.getByRole("button", { name: "Pay with BTC" }));
|
|
180
|
+
await user.click(screen.getByText("$25"));
|
|
181
|
+
await user.click(screen.getByRole("button", { name: "Pay with USDC" }));
|
|
210
182
|
|
|
211
|
-
expect(await screen.findByText("
|
|
183
|
+
expect(await screen.findByText("Checkout failed. Please try again.")).toBeInTheDocument();
|
|
212
184
|
});
|
|
213
185
|
|
|
214
|
-
it("
|
|
215
|
-
mockGetSupportedPaymentMethods.mockResolvedValue([
|
|
216
|
-
|
|
186
|
+
it("hides panel when no payment methods available", async () => {
|
|
187
|
+
mockGetSupportedPaymentMethods.mockResolvedValue([]);
|
|
188
|
+
const { BuyCryptoCreditPanel } = await import("../components/billing/buy-crypto-credits-panel");
|
|
189
|
+
const { container } = render(<BuyCryptoCreditPanel />);
|
|
190
|
+
|
|
191
|
+
// Component returns null when no methods
|
|
192
|
+
await waitFor(() => {
|
|
193
|
+
expect(container.innerHTML).toBe("");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
217
196
|
|
|
197
|
+
it("switches between payment methods", async () => {
|
|
198
|
+
mockGetSupportedPaymentMethods.mockResolvedValue([MOCK_USDC_METHOD, MOCK_ETH_METHOD]);
|
|
218
199
|
const user = userEvent.setup();
|
|
219
200
|
const { BuyCryptoCreditPanel } = await import("../components/billing/buy-crypto-credits-panel");
|
|
220
201
|
render(<BuyCryptoCreditPanel />);
|
|
221
202
|
|
|
222
203
|
await waitFor(() => {
|
|
223
|
-
expect(screen.getByText("
|
|
204
|
+
expect(screen.getByText("USDC")).toBeInTheDocument();
|
|
224
205
|
});
|
|
225
206
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
await user.click(screen.getByRole("button", { name: "Pay with BTC" }));
|
|
207
|
+
// Default is first method (USDC)
|
|
208
|
+
expect(screen.getByRole("button", { name: "Pay with USDC" })).toBeInTheDocument();
|
|
229
209
|
|
|
230
|
-
|
|
210
|
+
// Switch to ETH
|
|
211
|
+
await user.click(screen.getByText("ETH"));
|
|
212
|
+
expect(screen.getByRole("button", { name: "Pay with ETH" })).toBeInTheDocument();
|
|
231
213
|
});
|
|
232
214
|
});
|
|
@@ -10,6 +10,107 @@ import {
|
|
|
10
10
|
type PaymentMethodAdmin,
|
|
11
11
|
} from "@/lib/api";
|
|
12
12
|
|
|
13
|
+
/** BIP-44 coin types by chain name. */
|
|
14
|
+
const COIN_TYPES: Record<string, number> = {
|
|
15
|
+
base: 60,
|
|
16
|
+
ethereum: 60,
|
|
17
|
+
polygon: 60,
|
|
18
|
+
optimism: 60,
|
|
19
|
+
arbitrum: 60,
|
|
20
|
+
bitcoin: 0,
|
|
21
|
+
litecoin: 2,
|
|
22
|
+
dogecoin: 3,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Derive xpub from mnemonic client-side. Mnemonic NEVER leaves the browser.
|
|
27
|
+
* Returns the extended public key for the given BIP-44 coin type.
|
|
28
|
+
* Path: m/44'/<coinType>'/0'
|
|
29
|
+
*/
|
|
30
|
+
async function deriveXpubFromMnemonic(mnemonic: string, coinType: number): Promise<string> {
|
|
31
|
+
const { HDKey } = await import("@scure/bip32");
|
|
32
|
+
const { mnemonicToSeedSync } = await import("@scure/bip39");
|
|
33
|
+
const seed = mnemonicToSeedSync(mnemonic.trim());
|
|
34
|
+
const master = HDKey.fromMasterSeed(seed);
|
|
35
|
+
const account = master.derive(`m/44'/${coinType}'/0'`);
|
|
36
|
+
const xpub = account.publicExtendedKey;
|
|
37
|
+
if (!xpub) throw new Error("Failed to derive xpub");
|
|
38
|
+
return xpub;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function MnemonicDeriveSection({
|
|
42
|
+
chain,
|
|
43
|
+
onXpubDerived,
|
|
44
|
+
}: {
|
|
45
|
+
chain: string;
|
|
46
|
+
onXpubDerived: (xpub: string) => void;
|
|
47
|
+
}) {
|
|
48
|
+
const [mnemonic, setMnemonic] = useState("");
|
|
49
|
+
const [deriving, setDeriving] = useState(false);
|
|
50
|
+
const [error, setError] = useState<string | null>(null);
|
|
51
|
+
const [derived, setDerived] = useState(false);
|
|
52
|
+
|
|
53
|
+
const coinType = COIN_TYPES[chain.toLowerCase()] ?? 60;
|
|
54
|
+
const path = `m/44'/${coinType}'/0'`;
|
|
55
|
+
|
|
56
|
+
async function handleDerive() {
|
|
57
|
+
if (!mnemonic.trim()) return;
|
|
58
|
+
setDeriving(true);
|
|
59
|
+
setError(null);
|
|
60
|
+
try {
|
|
61
|
+
const xpub = await deriveXpubFromMnemonic(mnemonic, coinType);
|
|
62
|
+
onXpubDerived(xpub);
|
|
63
|
+
setMnemonic(""); // Clear mnemonic immediately
|
|
64
|
+
setDerived(true);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
setError((err as Error).message);
|
|
67
|
+
} finally {
|
|
68
|
+
setDeriving(false);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (derived) {
|
|
73
|
+
return (
|
|
74
|
+
<div className="col-span-2 rounded-md border border-green-500/30 bg-green-500/5 p-3">
|
|
75
|
+
<p className="text-xs text-green-500">
|
|
76
|
+
xpub derived and set. Mnemonic was cleared from memory.
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="col-span-2 space-y-2 rounded-md border border-dashed border-muted-foreground/30 p-3">
|
|
84
|
+
<div className="flex items-center justify-between">
|
|
85
|
+
<span className="text-xs font-medium text-muted-foreground">Derive xpub from mnemonic</span>
|
|
86
|
+
<span className="text-xs text-muted-foreground/60">Path: {path}</span>
|
|
87
|
+
</div>
|
|
88
|
+
<p className="text-xs text-muted-foreground/60">
|
|
89
|
+
Your mnemonic never leaves this browser. Only the xpub (public key) is sent to the server.
|
|
90
|
+
</p>
|
|
91
|
+
<input
|
|
92
|
+
type="password"
|
|
93
|
+
className="w-full rounded-md border bg-background px-3 py-1.5 text-sm font-mono"
|
|
94
|
+
placeholder="word1 word2 word3 ... (12 or 24 words)"
|
|
95
|
+
value={mnemonic}
|
|
96
|
+
onChange={(e) => setMnemonic(e.target.value)}
|
|
97
|
+
autoComplete="off"
|
|
98
|
+
spellCheck={false}
|
|
99
|
+
/>
|
|
100
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
101
|
+
<Button
|
|
102
|
+
type="button"
|
|
103
|
+
variant="outline"
|
|
104
|
+
size="sm"
|
|
105
|
+
onClick={handleDerive}
|
|
106
|
+
disabled={deriving || !mnemonic.trim()}
|
|
107
|
+
>
|
|
108
|
+
{deriving ? "Deriving..." : "Derive xpub"}
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
13
114
|
function AddMethodForm({ onSaved }: { onSaved: () => void }) {
|
|
14
115
|
const [open, setOpen] = useState(false);
|
|
15
116
|
const [saving, setSaving] = useState(false);
|
|
@@ -23,6 +124,8 @@ function AddMethodForm({ onSaved }: { onSaved: () => void }) {
|
|
|
23
124
|
displayOrder: 0,
|
|
24
125
|
confirmations: 1,
|
|
25
126
|
rpcUrl: "",
|
|
127
|
+
oracleAddress: "",
|
|
128
|
+
xpub: "",
|
|
26
129
|
});
|
|
27
130
|
|
|
28
131
|
async function handleSubmit(e: React.FormEvent) {
|
|
@@ -40,6 +143,8 @@ function AddMethodForm({ onSaved }: { onSaved: () => void }) {
|
|
|
40
143
|
enabled: true,
|
|
41
144
|
displayOrder: form.displayOrder,
|
|
42
145
|
rpcUrl: form.rpcUrl || null,
|
|
146
|
+
oracleAddress: form.oracleAddress || null,
|
|
147
|
+
xpub: form.xpub || null,
|
|
43
148
|
confirmations: form.confirmations,
|
|
44
149
|
});
|
|
45
150
|
setSaving(false);
|
|
@@ -54,6 +159,8 @@ function AddMethodForm({ onSaved }: { onSaved: () => void }) {
|
|
|
54
159
|
displayOrder: 0,
|
|
55
160
|
confirmations: 1,
|
|
56
161
|
rpcUrl: "",
|
|
162
|
+
oracleAddress: "",
|
|
163
|
+
xpub: "",
|
|
57
164
|
});
|
|
58
165
|
onSaved();
|
|
59
166
|
}
|
|
@@ -118,6 +225,44 @@ function AddMethodForm({ onSaved }: { onSaved: () => void }) {
|
|
|
118
225
|
onChange={(e) => setForm({ ...form, contractAddress: e.target.value })}
|
|
119
226
|
/>
|
|
120
227
|
</label>
|
|
228
|
+
<label className="col-span-2 space-y-1">
|
|
229
|
+
<span className="text-xs text-muted-foreground">RPC URL (chain node endpoint)</span>
|
|
230
|
+
<input
|
|
231
|
+
className="w-full rounded-md border bg-background px-3 py-1.5 text-sm font-mono"
|
|
232
|
+
placeholder="http://op-geth:8545"
|
|
233
|
+
value={form.rpcUrl}
|
|
234
|
+
onChange={(e) => setForm({ ...form, rpcUrl: e.target.value })}
|
|
235
|
+
/>
|
|
236
|
+
</label>
|
|
237
|
+
<label className="col-span-2 space-y-1">
|
|
238
|
+
<span className="text-xs text-muted-foreground">
|
|
239
|
+
Oracle Address (Chainlink feed — empty for stablecoins)
|
|
240
|
+
</span>
|
|
241
|
+
<input
|
|
242
|
+
className="w-full rounded-md border bg-background px-3 py-1.5 text-sm font-mono"
|
|
243
|
+
placeholder="0x..."
|
|
244
|
+
value={form.oracleAddress}
|
|
245
|
+
onChange={(e) => setForm({ ...form, oracleAddress: e.target.value })}
|
|
246
|
+
/>
|
|
247
|
+
</label>
|
|
248
|
+
|
|
249
|
+
{/* --- xpub: paste directly or derive from mnemonic --- */}
|
|
250
|
+
<MnemonicDeriveSection
|
|
251
|
+
chain={form.chain}
|
|
252
|
+
onXpubDerived={(xpub) => setForm((f) => ({ ...f, xpub }))}
|
|
253
|
+
/>
|
|
254
|
+
<label className="col-span-2 space-y-1">
|
|
255
|
+
<span className="text-xs text-muted-foreground">
|
|
256
|
+
xpub (auto-filled from mnemonic above, or paste directly)
|
|
257
|
+
</span>
|
|
258
|
+
<input
|
|
259
|
+
className="w-full rounded-md border bg-background px-3 py-1.5 text-xs font-mono"
|
|
260
|
+
placeholder="xpub6..."
|
|
261
|
+
value={form.xpub}
|
|
262
|
+
onChange={(e) => setForm({ ...form, xpub: e.target.value })}
|
|
263
|
+
/>
|
|
264
|
+
</label>
|
|
265
|
+
|
|
121
266
|
<label className="space-y-1">
|
|
122
267
|
<span className="text-xs text-muted-foreground">Display Order</span>
|
|
123
268
|
<input
|
|
@@ -199,8 +344,8 @@ export default function AdminPaymentMethodsPage() {
|
|
|
199
344
|
<th className="pb-2 pr-4">Chain</th>
|
|
200
345
|
<th className="pb-2 pr-4">Type</th>
|
|
201
346
|
<th className="pb-2 pr-4">Contract</th>
|
|
202
|
-
<th className="pb-2 pr-4">
|
|
203
|
-
<th className="pb-2 pr-4">
|
|
347
|
+
<th className="pb-2 pr-4">xpub</th>
|
|
348
|
+
<th className="pb-2 pr-4">RPC</th>
|
|
204
349
|
<th className="pb-2 pr-4">Status</th>
|
|
205
350
|
<th className="pb-2">Actions</th>
|
|
206
351
|
</tr>
|
|
@@ -214,8 +359,12 @@ export default function AdminPaymentMethodsPage() {
|
|
|
214
359
|
<td className="py-2 pr-4 font-mono text-xs">
|
|
215
360
|
{m.contractAddress ? `${m.contractAddress.slice(0, 10)}...` : "—"}
|
|
216
361
|
</td>
|
|
217
|
-
<td className="py-2 pr-4">
|
|
218
|
-
|
|
362
|
+
<td className="py-2 pr-4 font-mono text-xs">
|
|
363
|
+
{m.xpub ? `${m.xpub.slice(0, 12)}...` : "—"}
|
|
364
|
+
</td>
|
|
365
|
+
<td className="py-2 pr-4 font-mono text-xs">
|
|
366
|
+
{m.rpcUrl ? `${m.rpcUrl.slice(0, 20)}...` : "—"}
|
|
367
|
+
</td>
|
|
219
368
|
<td className="py-2 pr-4">
|
|
220
369
|
<span
|
|
221
370
|
className={
|
package/src/lib/api.ts
CHANGED
|
@@ -1330,45 +1330,6 @@ export async function createCreditCheckout(priceId: string): Promise<CheckoutRes
|
|
|
1330
1330
|
return { checkoutUrl: url };
|
|
1331
1331
|
}
|
|
1332
1332
|
|
|
1333
|
-
export async function createCryptoCheckout(
|
|
1334
|
-
amountUsd: number,
|
|
1335
|
-
): Promise<{ url: string; referenceId: string }> {
|
|
1336
|
-
return trpcVanilla.billing.cryptoCheckout.mutate({ amountUsd });
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
export interface StablecoinCheckoutResult {
|
|
1340
|
-
depositAddress: string;
|
|
1341
|
-
amountRaw: string;
|
|
1342
|
-
amountUsd: number;
|
|
1343
|
-
chain: string;
|
|
1344
|
-
token: string;
|
|
1345
|
-
referenceId: string;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
export async function createStablecoinCheckout(
|
|
1349
|
-
amountUsd: number,
|
|
1350
|
-
token: string,
|
|
1351
|
-
chain: string,
|
|
1352
|
-
): Promise<StablecoinCheckoutResult> {
|
|
1353
|
-
return trpcVanilla.billing.stablecoinCheckout.mutate({ amountUsd, token, chain });
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
export interface EthCheckoutResult {
|
|
1357
|
-
depositAddress: string;
|
|
1358
|
-
expectedWei: string;
|
|
1359
|
-
amountUsd: number;
|
|
1360
|
-
priceCents: number;
|
|
1361
|
-
chain: string;
|
|
1362
|
-
referenceId: string;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
export async function createEthCheckout(
|
|
1366
|
-
amountUsd: number,
|
|
1367
|
-
chain: string,
|
|
1368
|
-
): Promise<EthCheckoutResult> {
|
|
1369
|
-
return trpcVanilla.billing.ethCheckout.mutate({ amountUsd, chain });
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
1333
|
// --- Supported payment methods (runtime-configured) ---
|
|
1373
1334
|
|
|
1374
1335
|
export interface SupportedPaymentMethod {
|
|
@@ -1414,6 +1375,8 @@ export interface PaymentMethodAdmin {
|
|
|
1414
1375
|
enabled: boolean;
|
|
1415
1376
|
displayOrder: number;
|
|
1416
1377
|
rpcUrl: string | null;
|
|
1378
|
+
oracleAddress: string | null;
|
|
1379
|
+
xpub: string | null;
|
|
1417
1380
|
confirmations: number;
|
|
1418
1381
|
}
|
|
1419
1382
|
|
package/src/lib/trpc-types.ts
CHANGED
|
@@ -112,10 +112,8 @@ type AppRouterRecord = {
|
|
|
112
112
|
setDefaultPaymentMethod: AnyTRPCMutationProcedure;
|
|
113
113
|
affiliateStats: AnyTRPCQueryProcedure;
|
|
114
114
|
affiliateReferrals: AnyTRPCQueryProcedure;
|
|
115
|
-
cryptoCheckout: AnyTRPCMutationProcedure;
|
|
116
|
-
stablecoinCheckout: AnyTRPCMutationProcedure;
|
|
117
|
-
ethCheckout: AnyTRPCMutationProcedure;
|
|
118
115
|
checkout: AnyTRPCMutationProcedure;
|
|
116
|
+
chargeStatus: AnyTRPCQueryProcedure;
|
|
119
117
|
supportedPaymentMethods: AnyTRPCQueryProcedure;
|
|
120
118
|
adminListPaymentMethods: AnyTRPCQueryProcedure;
|
|
121
119
|
adminUpsertPaymentMethod: AnyTRPCMutationProcedure;
|