@settlr/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/index.d.mts +635 -0
- package/dist/index.d.ts +635 -0
- package/dist/index.js +974 -0
- package/dist/index.mjs +936 -0
- package/package.json +65 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import {
|
|
3
|
+
Connection,
|
|
4
|
+
PublicKey as PublicKey2,
|
|
5
|
+
Transaction,
|
|
6
|
+
TransactionInstruction
|
|
7
|
+
} from "@solana/web3.js";
|
|
8
|
+
import {
|
|
9
|
+
getAssociatedTokenAddress,
|
|
10
|
+
createAssociatedTokenAccountInstruction,
|
|
11
|
+
createTransferInstruction,
|
|
12
|
+
getAccount,
|
|
13
|
+
TokenAccountNotFoundError
|
|
14
|
+
} from "@solana/spl-token";
|
|
15
|
+
|
|
16
|
+
// src/constants.ts
|
|
17
|
+
import { PublicKey } from "@solana/web3.js";
|
|
18
|
+
var USDC_MINT_DEVNET = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
|
|
19
|
+
var USDC_MINT_MAINNET = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
|
|
20
|
+
var SETTLR_API_URL = {
|
|
21
|
+
production: "https://settlr.dev/api",
|
|
22
|
+
development: "http://localhost:3000/api"
|
|
23
|
+
};
|
|
24
|
+
var SETTLR_CHECKOUT_URL = {
|
|
25
|
+
production: "https://settlr.dev/pay",
|
|
26
|
+
development: "http://localhost:3000/pay"
|
|
27
|
+
};
|
|
28
|
+
var SUPPORTED_NETWORKS = ["devnet", "mainnet-beta"];
|
|
29
|
+
var USDC_DECIMALS = 6;
|
|
30
|
+
var DEFAULT_RPC_ENDPOINTS = {
|
|
31
|
+
devnet: "https://api.devnet.solana.com",
|
|
32
|
+
"mainnet-beta": "https://api.mainnet-beta.solana.com"
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// src/utils.ts
|
|
36
|
+
function formatUSDC(lamports, decimals = 2) {
|
|
37
|
+
const amount = Number(lamports) / Math.pow(10, USDC_DECIMALS);
|
|
38
|
+
return amount.toFixed(decimals);
|
|
39
|
+
}
|
|
40
|
+
function parseUSDC(amount) {
|
|
41
|
+
const num = typeof amount === "string" ? parseFloat(amount) : amount;
|
|
42
|
+
return BigInt(Math.round(num * Math.pow(10, USDC_DECIMALS)));
|
|
43
|
+
}
|
|
44
|
+
function shortenAddress(address, chars = 4) {
|
|
45
|
+
if (address.length <= chars * 2 + 3) return address;
|
|
46
|
+
return `${address.slice(0, chars)}...${address.slice(-chars)}`;
|
|
47
|
+
}
|
|
48
|
+
function generatePaymentId() {
|
|
49
|
+
const timestamp = Date.now().toString(36);
|
|
50
|
+
const random = Math.random().toString(36).substring(2, 10);
|
|
51
|
+
return `pay_${timestamp}${random}`;
|
|
52
|
+
}
|
|
53
|
+
function isValidSolanaAddress(address) {
|
|
54
|
+
try {
|
|
55
|
+
const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
56
|
+
return base58Regex.test(address);
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function sleep(ms) {
|
|
62
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
63
|
+
}
|
|
64
|
+
async function retry(fn, maxRetries = 3, baseDelay = 1e3) {
|
|
65
|
+
let lastError;
|
|
66
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
67
|
+
try {
|
|
68
|
+
return await fn();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
lastError = error;
|
|
71
|
+
if (i < maxRetries - 1) {
|
|
72
|
+
await sleep(baseDelay * Math.pow(2, i));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
throw lastError;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/client.ts
|
|
80
|
+
var Settlr = class {
|
|
81
|
+
constructor(config) {
|
|
82
|
+
this.validated = false;
|
|
83
|
+
if (!config.apiKey) {
|
|
84
|
+
throw new Error("API key is required. Get one at https://settlr.dev/dashboard");
|
|
85
|
+
}
|
|
86
|
+
const walletAddress = typeof config.merchant.walletAddress === "string" ? config.merchant.walletAddress : config.merchant.walletAddress.toBase58();
|
|
87
|
+
if (!isValidSolanaAddress(walletAddress)) {
|
|
88
|
+
throw new Error("Invalid merchant wallet address");
|
|
89
|
+
}
|
|
90
|
+
const network = config.network ?? "devnet";
|
|
91
|
+
const testMode = config.testMode ?? network === "devnet";
|
|
92
|
+
this.config = {
|
|
93
|
+
merchant: {
|
|
94
|
+
...config.merchant,
|
|
95
|
+
walletAddress
|
|
96
|
+
},
|
|
97
|
+
apiKey: config.apiKey,
|
|
98
|
+
network,
|
|
99
|
+
rpcEndpoint: config.rpcEndpoint ?? DEFAULT_RPC_ENDPOINTS[network],
|
|
100
|
+
testMode
|
|
101
|
+
};
|
|
102
|
+
this.apiBaseUrl = testMode ? SETTLR_API_URL.development : SETTLR_API_URL.production;
|
|
103
|
+
this.connection = new Connection(this.config.rpcEndpoint, "confirmed");
|
|
104
|
+
this.usdcMint = network === "devnet" ? USDC_MINT_DEVNET : USDC_MINT_MAINNET;
|
|
105
|
+
this.merchantWallet = new PublicKey2(walletAddress);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Validate API key with Settlr backend
|
|
109
|
+
*/
|
|
110
|
+
async validateApiKey() {
|
|
111
|
+
if (this.validated) return;
|
|
112
|
+
try {
|
|
113
|
+
const response = await fetch(`${this.apiBaseUrl}/sdk/validate`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
"Content-Type": "application/json",
|
|
117
|
+
"X-API-Key": this.config.apiKey
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
walletAddress: this.config.merchant.walletAddress
|
|
121
|
+
})
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const error = await response.json().catch(() => ({ error: "Invalid API key" }));
|
|
125
|
+
throw new Error(error.error || "API key validation failed");
|
|
126
|
+
}
|
|
127
|
+
const data = await response.json();
|
|
128
|
+
if (!data.valid) {
|
|
129
|
+
throw new Error(data.error || "Invalid API key");
|
|
130
|
+
}
|
|
131
|
+
this.validated = true;
|
|
132
|
+
this.merchantId = data.merchantId;
|
|
133
|
+
this.tier = data.tier;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof Error && error.message.includes("fetch")) {
|
|
136
|
+
if (this.config.apiKey.startsWith("sk_test_")) {
|
|
137
|
+
this.validated = true;
|
|
138
|
+
this.tier = "free";
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get the current tier
|
|
147
|
+
*/
|
|
148
|
+
getTier() {
|
|
149
|
+
return this.tier;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create a payment link
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```typescript
|
|
156
|
+
* const payment = await settlr.createPayment({
|
|
157
|
+
* amount: 29.99,
|
|
158
|
+
* memo: 'Order #1234',
|
|
159
|
+
* successUrl: 'https://mystore.com/success',
|
|
160
|
+
* });
|
|
161
|
+
*
|
|
162
|
+
* console.log(payment.checkoutUrl);
|
|
163
|
+
* // https://settlr.dev/pay?amount=29.99&merchant=...
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
async createPayment(options) {
|
|
167
|
+
await this.validateApiKey();
|
|
168
|
+
const { amount, memo, orderId, metadata, successUrl, cancelUrl, expiresIn = 3600 } = options;
|
|
169
|
+
if (amount <= 0) {
|
|
170
|
+
throw new Error("Amount must be greater than 0");
|
|
171
|
+
}
|
|
172
|
+
const paymentId = generatePaymentId();
|
|
173
|
+
const amountLamports = parseUSDC(amount);
|
|
174
|
+
const createdAt = /* @__PURE__ */ new Date();
|
|
175
|
+
const expiresAt = new Date(createdAt.getTime() + expiresIn * 1e3);
|
|
176
|
+
const baseUrl = this.config.testMode ? SETTLR_CHECKOUT_URL.development : SETTLR_CHECKOUT_URL.production;
|
|
177
|
+
const params = new URLSearchParams({
|
|
178
|
+
amount: amount.toString(),
|
|
179
|
+
merchant: this.config.merchant.name,
|
|
180
|
+
to: this.config.merchant.walletAddress
|
|
181
|
+
});
|
|
182
|
+
if (memo) params.set("memo", memo);
|
|
183
|
+
if (orderId) params.set("orderId", orderId);
|
|
184
|
+
if (successUrl) params.set("successUrl", successUrl);
|
|
185
|
+
if (cancelUrl) params.set("cancelUrl", cancelUrl);
|
|
186
|
+
if (paymentId) params.set("paymentId", paymentId);
|
|
187
|
+
const checkoutUrl = `${baseUrl}?${params.toString()}`;
|
|
188
|
+
const qrCode = await this.generateQRCode(checkoutUrl);
|
|
189
|
+
const payment = {
|
|
190
|
+
id: paymentId,
|
|
191
|
+
amount,
|
|
192
|
+
amountLamports,
|
|
193
|
+
status: "pending",
|
|
194
|
+
merchantAddress: this.config.merchant.walletAddress,
|
|
195
|
+
checkoutUrl,
|
|
196
|
+
qrCode,
|
|
197
|
+
memo,
|
|
198
|
+
orderId,
|
|
199
|
+
metadata,
|
|
200
|
+
createdAt,
|
|
201
|
+
expiresAt
|
|
202
|
+
};
|
|
203
|
+
return payment;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Build a transaction for direct payment (for wallet integration)
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```typescript
|
|
210
|
+
* const tx = await settlr.buildTransaction({
|
|
211
|
+
* payerPublicKey: wallet.publicKey,
|
|
212
|
+
* amount: 29.99,
|
|
213
|
+
* });
|
|
214
|
+
*
|
|
215
|
+
* const signature = await wallet.sendTransaction(tx, connection);
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
async buildTransaction(options) {
|
|
219
|
+
await this.validateApiKey();
|
|
220
|
+
const { payerPublicKey, amount, memo } = options;
|
|
221
|
+
const amountLamports = parseUSDC(amount);
|
|
222
|
+
const payerAta = await getAssociatedTokenAddress(this.usdcMint, payerPublicKey);
|
|
223
|
+
const merchantAta = await getAssociatedTokenAddress(this.usdcMint, this.merchantWallet);
|
|
224
|
+
const instructions = [];
|
|
225
|
+
try {
|
|
226
|
+
await getAccount(this.connection, merchantAta);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error instanceof TokenAccountNotFoundError) {
|
|
229
|
+
instructions.push(
|
|
230
|
+
createAssociatedTokenAccountInstruction(
|
|
231
|
+
payerPublicKey,
|
|
232
|
+
merchantAta,
|
|
233
|
+
this.merchantWallet,
|
|
234
|
+
this.usdcMint
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
instructions.push(
|
|
242
|
+
createTransferInstruction(
|
|
243
|
+
payerAta,
|
|
244
|
+
merchantAta,
|
|
245
|
+
payerPublicKey,
|
|
246
|
+
BigInt(amountLamports)
|
|
247
|
+
)
|
|
248
|
+
);
|
|
249
|
+
if (memo) {
|
|
250
|
+
const MEMO_PROGRAM_ID = new PublicKey2("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
|
|
251
|
+
instructions.push(
|
|
252
|
+
new TransactionInstruction({
|
|
253
|
+
keys: [{ pubkey: payerPublicKey, isSigner: true, isWritable: false }],
|
|
254
|
+
programId: MEMO_PROGRAM_ID,
|
|
255
|
+
data: Buffer.from(memo, "utf-8")
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const { blockhash } = await this.connection.getLatestBlockhash();
|
|
260
|
+
const transaction = new Transaction();
|
|
261
|
+
transaction.recentBlockhash = blockhash;
|
|
262
|
+
transaction.feePayer = payerPublicKey;
|
|
263
|
+
transaction.add(...instructions);
|
|
264
|
+
return transaction;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Execute a direct payment (requires wallet adapter)
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```typescript
|
|
271
|
+
* const result = await settlr.pay({
|
|
272
|
+
* wallet,
|
|
273
|
+
* amount: 29.99,
|
|
274
|
+
* memo: 'Order #1234',
|
|
275
|
+
* });
|
|
276
|
+
*
|
|
277
|
+
* if (result.success) {
|
|
278
|
+
* console.log('Paid!', result.signature);
|
|
279
|
+
* }
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
async pay(options) {
|
|
283
|
+
const { wallet, amount, memo, txOptions } = options;
|
|
284
|
+
try {
|
|
285
|
+
const transaction = await this.buildTransaction({
|
|
286
|
+
payerPublicKey: wallet.publicKey,
|
|
287
|
+
amount,
|
|
288
|
+
memo
|
|
289
|
+
});
|
|
290
|
+
const signedTx = await wallet.signTransaction(transaction);
|
|
291
|
+
const signature = await retry(
|
|
292
|
+
() => this.connection.sendRawTransaction(signedTx.serialize(), {
|
|
293
|
+
skipPreflight: txOptions?.skipPreflight ?? false,
|
|
294
|
+
preflightCommitment: txOptions?.commitment ?? "confirmed",
|
|
295
|
+
maxRetries: txOptions?.maxRetries ?? 3
|
|
296
|
+
}),
|
|
297
|
+
3
|
|
298
|
+
);
|
|
299
|
+
const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash();
|
|
300
|
+
await this.connection.confirmTransaction({
|
|
301
|
+
blockhash,
|
|
302
|
+
lastValidBlockHeight,
|
|
303
|
+
signature
|
|
304
|
+
});
|
|
305
|
+
return {
|
|
306
|
+
success: true,
|
|
307
|
+
signature,
|
|
308
|
+
amount,
|
|
309
|
+
merchantAddress: this.merchantWallet.toBase58()
|
|
310
|
+
};
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return {
|
|
313
|
+
success: false,
|
|
314
|
+
signature: "",
|
|
315
|
+
amount,
|
|
316
|
+
merchantAddress: this.merchantWallet.toBase58(),
|
|
317
|
+
error: error instanceof Error ? error.message : "Payment failed"
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Check payment status by transaction signature
|
|
323
|
+
*/
|
|
324
|
+
async getPaymentStatus(signature) {
|
|
325
|
+
try {
|
|
326
|
+
const status = await this.connection.getSignatureStatus(signature);
|
|
327
|
+
if (!status.value) {
|
|
328
|
+
return "pending";
|
|
329
|
+
}
|
|
330
|
+
if (status.value.err) {
|
|
331
|
+
return "failed";
|
|
332
|
+
}
|
|
333
|
+
if (status.value.confirmationStatus === "confirmed" || status.value.confirmationStatus === "finalized") {
|
|
334
|
+
return "completed";
|
|
335
|
+
}
|
|
336
|
+
return "pending";
|
|
337
|
+
} catch {
|
|
338
|
+
return "failed";
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Create a hosted checkout session (like Stripe Checkout)
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* ```typescript
|
|
346
|
+
* const session = await settlr.createCheckoutSession({
|
|
347
|
+
* amount: 29.99,
|
|
348
|
+
* description: 'Premium Plan',
|
|
349
|
+
* successUrl: 'https://mystore.com/success',
|
|
350
|
+
* cancelUrl: 'https://mystore.com/cancel',
|
|
351
|
+
* webhookUrl: 'https://mystore.com/api/webhooks/settlr',
|
|
352
|
+
* });
|
|
353
|
+
*
|
|
354
|
+
* // Redirect customer to hosted checkout
|
|
355
|
+
* window.location.href = session.url;
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
async createCheckoutSession(options) {
|
|
359
|
+
const { amount, description, metadata, successUrl, cancelUrl, webhookUrl } = options;
|
|
360
|
+
if (amount <= 0) {
|
|
361
|
+
throw new Error("Amount must be greater than 0");
|
|
362
|
+
}
|
|
363
|
+
const baseUrl = this.config.testMode ? "http://localhost:3000" : "https://settlr.dev";
|
|
364
|
+
const response = await fetch(`${baseUrl}/api/checkout/sessions`, {
|
|
365
|
+
method: "POST",
|
|
366
|
+
headers: {
|
|
367
|
+
"Content-Type": "application/json",
|
|
368
|
+
...this.config.apiKey && { "Authorization": `Bearer ${this.config.apiKey}` }
|
|
369
|
+
},
|
|
370
|
+
body: JSON.stringify({
|
|
371
|
+
merchantId: this.config.merchant.name.toLowerCase().replace(/\s+/g, "-"),
|
|
372
|
+
merchantName: this.config.merchant.name,
|
|
373
|
+
merchantWallet: this.config.merchant.walletAddress,
|
|
374
|
+
amount,
|
|
375
|
+
description,
|
|
376
|
+
metadata,
|
|
377
|
+
successUrl,
|
|
378
|
+
cancelUrl,
|
|
379
|
+
webhookUrl
|
|
380
|
+
})
|
|
381
|
+
});
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
384
|
+
throw new Error(error.error || "Failed to create checkout session");
|
|
385
|
+
}
|
|
386
|
+
return response.json();
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Get merchant's USDC balance
|
|
390
|
+
*/
|
|
391
|
+
async getMerchantBalance() {
|
|
392
|
+
try {
|
|
393
|
+
const ata = await getAssociatedTokenAddress(this.usdcMint, this.merchantWallet);
|
|
394
|
+
const account = await getAccount(this.connection, ata);
|
|
395
|
+
return Number(account.amount) / 1e6;
|
|
396
|
+
} catch {
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Generate QR code for payment URL
|
|
402
|
+
*/
|
|
403
|
+
async generateQRCode(url) {
|
|
404
|
+
const encoded = encodeURIComponent(url);
|
|
405
|
+
return `data:image/svg+xml,${encoded}`;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Get the connection instance
|
|
409
|
+
*/
|
|
410
|
+
getConnection() {
|
|
411
|
+
return this.connection;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Get merchant wallet address
|
|
415
|
+
*/
|
|
416
|
+
getMerchantAddress() {
|
|
417
|
+
return this.merchantWallet;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Get USDC mint address
|
|
421
|
+
*/
|
|
422
|
+
getUsdcMint() {
|
|
423
|
+
return this.usdcMint;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/react.tsx
|
|
428
|
+
import { createContext, useContext, useMemo } from "react";
|
|
429
|
+
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
|
|
430
|
+
import { jsx } from "react/jsx-runtime";
|
|
431
|
+
var SettlrContext = createContext(null);
|
|
432
|
+
function SettlrProvider({ children, config }) {
|
|
433
|
+
const { connection } = useConnection();
|
|
434
|
+
const wallet = useWallet();
|
|
435
|
+
const settlr = useMemo(() => {
|
|
436
|
+
return new Settlr({
|
|
437
|
+
...config,
|
|
438
|
+
rpcEndpoint: connection.rpcEndpoint
|
|
439
|
+
});
|
|
440
|
+
}, [config, connection.rpcEndpoint]);
|
|
441
|
+
const value = useMemo(
|
|
442
|
+
() => ({
|
|
443
|
+
settlr,
|
|
444
|
+
connected: wallet.connected,
|
|
445
|
+
createPayment: (options) => {
|
|
446
|
+
return settlr.createPayment(options);
|
|
447
|
+
},
|
|
448
|
+
pay: async (options) => {
|
|
449
|
+
if (!wallet.publicKey || !wallet.signTransaction) {
|
|
450
|
+
return {
|
|
451
|
+
success: false,
|
|
452
|
+
signature: "",
|
|
453
|
+
amount: options.amount,
|
|
454
|
+
merchantAddress: settlr.getMerchantAddress().toBase58(),
|
|
455
|
+
error: "Wallet not connected"
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return settlr.pay({
|
|
459
|
+
wallet: {
|
|
460
|
+
publicKey: wallet.publicKey,
|
|
461
|
+
signTransaction: wallet.signTransaction
|
|
462
|
+
},
|
|
463
|
+
amount: options.amount,
|
|
464
|
+
memo: options.memo
|
|
465
|
+
});
|
|
466
|
+
},
|
|
467
|
+
getBalance: () => {
|
|
468
|
+
return settlr.getMerchantBalance();
|
|
469
|
+
}
|
|
470
|
+
}),
|
|
471
|
+
[settlr, wallet]
|
|
472
|
+
);
|
|
473
|
+
return /* @__PURE__ */ jsx(SettlrContext.Provider, { value, children });
|
|
474
|
+
}
|
|
475
|
+
function useSettlr() {
|
|
476
|
+
const context = useContext(SettlrContext);
|
|
477
|
+
if (!context) {
|
|
478
|
+
throw new Error("useSettlr must be used within a SettlrProvider");
|
|
479
|
+
}
|
|
480
|
+
return context;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/components.tsx
|
|
484
|
+
import {
|
|
485
|
+
useState,
|
|
486
|
+
useCallback
|
|
487
|
+
} from "react";
|
|
488
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
489
|
+
var defaultStyles = {
|
|
490
|
+
base: {
|
|
491
|
+
display: "inline-flex",
|
|
492
|
+
alignItems: "center",
|
|
493
|
+
justifyContent: "center",
|
|
494
|
+
gap: "8px",
|
|
495
|
+
fontWeight: 600,
|
|
496
|
+
borderRadius: "12px",
|
|
497
|
+
cursor: "pointer",
|
|
498
|
+
transition: "all 0.2s ease",
|
|
499
|
+
border: "none",
|
|
500
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
|
501
|
+
},
|
|
502
|
+
primary: {
|
|
503
|
+
background: "linear-gradient(135deg, #f472b6 0%, #67e8f9 100%)",
|
|
504
|
+
color: "white"
|
|
505
|
+
},
|
|
506
|
+
secondary: {
|
|
507
|
+
background: "#12121a",
|
|
508
|
+
color: "white",
|
|
509
|
+
border: "1px solid rgba(255,255,255,0.1)"
|
|
510
|
+
},
|
|
511
|
+
outline: {
|
|
512
|
+
background: "transparent",
|
|
513
|
+
color: "#f472b6",
|
|
514
|
+
border: "2px solid #f472b6"
|
|
515
|
+
},
|
|
516
|
+
sm: {
|
|
517
|
+
padding: "8px 16px",
|
|
518
|
+
fontSize: "14px"
|
|
519
|
+
},
|
|
520
|
+
md: {
|
|
521
|
+
padding: "12px 24px",
|
|
522
|
+
fontSize: "16px"
|
|
523
|
+
},
|
|
524
|
+
lg: {
|
|
525
|
+
padding: "16px 32px",
|
|
526
|
+
fontSize: "18px"
|
|
527
|
+
},
|
|
528
|
+
disabled: {
|
|
529
|
+
opacity: 0.5,
|
|
530
|
+
cursor: "not-allowed"
|
|
531
|
+
},
|
|
532
|
+
loading: {
|
|
533
|
+
opacity: 0.8
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
function BuyButton({
|
|
537
|
+
amount,
|
|
538
|
+
memo,
|
|
539
|
+
orderId,
|
|
540
|
+
children,
|
|
541
|
+
onSuccess,
|
|
542
|
+
onError,
|
|
543
|
+
onProcessing,
|
|
544
|
+
useRedirect = false,
|
|
545
|
+
successUrl,
|
|
546
|
+
cancelUrl,
|
|
547
|
+
className,
|
|
548
|
+
style,
|
|
549
|
+
disabled = false,
|
|
550
|
+
variant = "primary",
|
|
551
|
+
size = "md"
|
|
552
|
+
}) {
|
|
553
|
+
const { pay, createPayment, connected } = useSettlr();
|
|
554
|
+
const [loading, setLoading] = useState(false);
|
|
555
|
+
const [status, setStatus] = useState("idle");
|
|
556
|
+
const handleClick = useCallback(async () => {
|
|
557
|
+
if (disabled || loading) return;
|
|
558
|
+
setLoading(true);
|
|
559
|
+
setStatus("processing");
|
|
560
|
+
onProcessing?.();
|
|
561
|
+
try {
|
|
562
|
+
if (useRedirect) {
|
|
563
|
+
const payment = await createPayment({
|
|
564
|
+
amount,
|
|
565
|
+
memo,
|
|
566
|
+
orderId,
|
|
567
|
+
successUrl,
|
|
568
|
+
cancelUrl
|
|
569
|
+
});
|
|
570
|
+
window.location.href = payment.checkoutUrl;
|
|
571
|
+
} else {
|
|
572
|
+
const result = await pay({ amount, memo });
|
|
573
|
+
if (result.success) {
|
|
574
|
+
setStatus("success");
|
|
575
|
+
onSuccess?.({
|
|
576
|
+
signature: result.signature,
|
|
577
|
+
amount: result.amount,
|
|
578
|
+
merchantAddress: result.merchantAddress
|
|
579
|
+
});
|
|
580
|
+
} else {
|
|
581
|
+
throw new Error(result.error || "Payment failed");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
} catch (error) {
|
|
585
|
+
setStatus("error");
|
|
586
|
+
onError?.(error instanceof Error ? error : new Error("Payment failed"));
|
|
587
|
+
} finally {
|
|
588
|
+
setLoading(false);
|
|
589
|
+
}
|
|
590
|
+
}, [
|
|
591
|
+
amount,
|
|
592
|
+
memo,
|
|
593
|
+
orderId,
|
|
594
|
+
disabled,
|
|
595
|
+
loading,
|
|
596
|
+
useRedirect,
|
|
597
|
+
successUrl,
|
|
598
|
+
cancelUrl,
|
|
599
|
+
pay,
|
|
600
|
+
createPayment,
|
|
601
|
+
onSuccess,
|
|
602
|
+
onError,
|
|
603
|
+
onProcessing
|
|
604
|
+
]);
|
|
605
|
+
const buttonStyle = {
|
|
606
|
+
...defaultStyles.base,
|
|
607
|
+
...defaultStyles[variant],
|
|
608
|
+
...defaultStyles[size],
|
|
609
|
+
...disabled ? defaultStyles.disabled : {},
|
|
610
|
+
...loading ? defaultStyles.loading : {},
|
|
611
|
+
...style
|
|
612
|
+
};
|
|
613
|
+
const buttonContent = loading ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
614
|
+
/* @__PURE__ */ jsx2(Spinner, {}),
|
|
615
|
+
"Processing..."
|
|
616
|
+
] }) : children || `Pay $${amount.toFixed(2)}`;
|
|
617
|
+
return /* @__PURE__ */ jsx2(
|
|
618
|
+
"button",
|
|
619
|
+
{
|
|
620
|
+
onClick: handleClick,
|
|
621
|
+
disabled: disabled || loading || !connected,
|
|
622
|
+
className,
|
|
623
|
+
style: buttonStyle,
|
|
624
|
+
type: "button",
|
|
625
|
+
children: !connected ? "Connect Wallet" : buttonContent
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
function Spinner() {
|
|
630
|
+
return /* @__PURE__ */ jsxs(
|
|
631
|
+
"svg",
|
|
632
|
+
{
|
|
633
|
+
width: "16",
|
|
634
|
+
height: "16",
|
|
635
|
+
viewBox: "0 0 16 16",
|
|
636
|
+
fill: "none",
|
|
637
|
+
style: { animation: "spin 1s linear infinite" },
|
|
638
|
+
children: [
|
|
639
|
+
/* @__PURE__ */ jsx2("style", { children: `@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }` }),
|
|
640
|
+
/* @__PURE__ */ jsx2(
|
|
641
|
+
"circle",
|
|
642
|
+
{
|
|
643
|
+
cx: "8",
|
|
644
|
+
cy: "8",
|
|
645
|
+
r: "6",
|
|
646
|
+
stroke: "currentColor",
|
|
647
|
+
strokeWidth: "2",
|
|
648
|
+
strokeLinecap: "round",
|
|
649
|
+
strokeDasharray: "32",
|
|
650
|
+
strokeDashoffset: "12"
|
|
651
|
+
}
|
|
652
|
+
)
|
|
653
|
+
]
|
|
654
|
+
}
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
var widgetStyles = {
|
|
658
|
+
container: {
|
|
659
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
660
|
+
borderRadius: "16px",
|
|
661
|
+
overflow: "hidden",
|
|
662
|
+
maxWidth: "400px",
|
|
663
|
+
width: "100%"
|
|
664
|
+
},
|
|
665
|
+
containerDark: {
|
|
666
|
+
background: "#12121a",
|
|
667
|
+
border: "1px solid rgba(255,255,255,0.1)",
|
|
668
|
+
color: "white"
|
|
669
|
+
},
|
|
670
|
+
containerLight: {
|
|
671
|
+
background: "white",
|
|
672
|
+
border: "1px solid #e5e7eb",
|
|
673
|
+
color: "#111827"
|
|
674
|
+
},
|
|
675
|
+
header: {
|
|
676
|
+
padding: "24px",
|
|
677
|
+
borderBottom: "1px solid rgba(255,255,255,0.1)"
|
|
678
|
+
},
|
|
679
|
+
productImage: {
|
|
680
|
+
width: "64px",
|
|
681
|
+
height: "64px",
|
|
682
|
+
borderRadius: "12px",
|
|
683
|
+
objectFit: "cover",
|
|
684
|
+
marginBottom: "16px"
|
|
685
|
+
},
|
|
686
|
+
productName: {
|
|
687
|
+
fontSize: "20px",
|
|
688
|
+
fontWeight: 600,
|
|
689
|
+
margin: "0 0 4px 0"
|
|
690
|
+
},
|
|
691
|
+
productDescription: {
|
|
692
|
+
fontSize: "14px",
|
|
693
|
+
opacity: 0.7,
|
|
694
|
+
margin: 0
|
|
695
|
+
},
|
|
696
|
+
body: {
|
|
697
|
+
padding: "24px"
|
|
698
|
+
},
|
|
699
|
+
row: {
|
|
700
|
+
display: "flex",
|
|
701
|
+
justifyContent: "space-between",
|
|
702
|
+
alignItems: "center",
|
|
703
|
+
marginBottom: "12px"
|
|
704
|
+
},
|
|
705
|
+
label: {
|
|
706
|
+
fontSize: "14px",
|
|
707
|
+
opacity: 0.7
|
|
708
|
+
},
|
|
709
|
+
value: {
|
|
710
|
+
fontSize: "14px",
|
|
711
|
+
fontWeight: 500
|
|
712
|
+
},
|
|
713
|
+
total: {
|
|
714
|
+
fontSize: "24px",
|
|
715
|
+
fontWeight: 700
|
|
716
|
+
},
|
|
717
|
+
divider: {
|
|
718
|
+
height: "1px",
|
|
719
|
+
background: "rgba(255,255,255,0.1)",
|
|
720
|
+
margin: "16px 0"
|
|
721
|
+
},
|
|
722
|
+
footer: {
|
|
723
|
+
padding: "24px",
|
|
724
|
+
paddingTop: "0"
|
|
725
|
+
},
|
|
726
|
+
branding: {
|
|
727
|
+
textAlign: "center",
|
|
728
|
+
fontSize: "12px",
|
|
729
|
+
opacity: 0.5,
|
|
730
|
+
marginTop: "16px"
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
function CheckoutWidget({
|
|
734
|
+
amount,
|
|
735
|
+
productName,
|
|
736
|
+
productDescription,
|
|
737
|
+
productImage,
|
|
738
|
+
merchantName,
|
|
739
|
+
memo,
|
|
740
|
+
orderId,
|
|
741
|
+
onSuccess,
|
|
742
|
+
onError,
|
|
743
|
+
onCancel,
|
|
744
|
+
className,
|
|
745
|
+
style,
|
|
746
|
+
theme = "dark",
|
|
747
|
+
showBranding = true
|
|
748
|
+
}) {
|
|
749
|
+
const { connected } = useSettlr();
|
|
750
|
+
const [status, setStatus] = useState("idle");
|
|
751
|
+
const containerStyle = {
|
|
752
|
+
...widgetStyles.container,
|
|
753
|
+
...theme === "dark" ? widgetStyles.containerDark : widgetStyles.containerLight,
|
|
754
|
+
...style
|
|
755
|
+
};
|
|
756
|
+
const dividerStyle = {
|
|
757
|
+
...widgetStyles.divider,
|
|
758
|
+
background: theme === "dark" ? "rgba(255,255,255,0.1)" : "#e5e7eb"
|
|
759
|
+
};
|
|
760
|
+
return /* @__PURE__ */ jsxs("div", { className, style: containerStyle, children: [
|
|
761
|
+
/* @__PURE__ */ jsxs("div", { style: widgetStyles.header, children: [
|
|
762
|
+
productImage && /* @__PURE__ */ jsx2(
|
|
763
|
+
"img",
|
|
764
|
+
{
|
|
765
|
+
src: productImage,
|
|
766
|
+
alt: productName,
|
|
767
|
+
style: widgetStyles.productImage
|
|
768
|
+
}
|
|
769
|
+
),
|
|
770
|
+
/* @__PURE__ */ jsx2("h2", { style: widgetStyles.productName, children: productName }),
|
|
771
|
+
productDescription && /* @__PURE__ */ jsx2("p", { style: widgetStyles.productDescription, children: productDescription })
|
|
772
|
+
] }),
|
|
773
|
+
/* @__PURE__ */ jsxs("div", { style: widgetStyles.body, children: [
|
|
774
|
+
/* @__PURE__ */ jsxs("div", { style: widgetStyles.row, children: [
|
|
775
|
+
/* @__PURE__ */ jsx2("span", { style: widgetStyles.label, children: "Subtotal" }),
|
|
776
|
+
/* @__PURE__ */ jsxs("span", { style: widgetStyles.value, children: [
|
|
777
|
+
"$",
|
|
778
|
+
amount.toFixed(2)
|
|
779
|
+
] })
|
|
780
|
+
] }),
|
|
781
|
+
/* @__PURE__ */ jsxs("div", { style: widgetStyles.row, children: [
|
|
782
|
+
/* @__PURE__ */ jsx2("span", { style: widgetStyles.label, children: "Network Fee" }),
|
|
783
|
+
/* @__PURE__ */ jsx2("span", { style: widgetStyles.value, children: "$0.01" })
|
|
784
|
+
] }),
|
|
785
|
+
/* @__PURE__ */ jsx2("div", { style: dividerStyle }),
|
|
786
|
+
/* @__PURE__ */ jsxs("div", { style: widgetStyles.row, children: [
|
|
787
|
+
/* @__PURE__ */ jsx2("span", { style: widgetStyles.label, children: "Total" }),
|
|
788
|
+
/* @__PURE__ */ jsxs("span", { style: widgetStyles.total, children: [
|
|
789
|
+
"$",
|
|
790
|
+
(amount + 0.01).toFixed(2),
|
|
791
|
+
" USDC"
|
|
792
|
+
] })
|
|
793
|
+
] })
|
|
794
|
+
] }),
|
|
795
|
+
/* @__PURE__ */ jsxs("div", { style: widgetStyles.footer, children: [
|
|
796
|
+
/* @__PURE__ */ jsx2(
|
|
797
|
+
BuyButton,
|
|
798
|
+
{
|
|
799
|
+
amount,
|
|
800
|
+
memo: memo || productName,
|
|
801
|
+
orderId,
|
|
802
|
+
onSuccess: (result) => {
|
|
803
|
+
setStatus("success");
|
|
804
|
+
onSuccess?.(result);
|
|
805
|
+
},
|
|
806
|
+
onError: (error) => {
|
|
807
|
+
setStatus("error");
|
|
808
|
+
onError?.(error);
|
|
809
|
+
},
|
|
810
|
+
onProcessing: () => setStatus("processing"),
|
|
811
|
+
size: "lg",
|
|
812
|
+
style: { width: "100%" },
|
|
813
|
+
children: status === "success" ? "\u2713 Payment Complete" : status === "error" ? "Payment Failed - Retry" : `Pay $${(amount + 0.01).toFixed(2)} USDC`
|
|
814
|
+
}
|
|
815
|
+
),
|
|
816
|
+
showBranding && /* @__PURE__ */ jsxs("p", { style: widgetStyles.branding, children: [
|
|
817
|
+
"Secured by ",
|
|
818
|
+
/* @__PURE__ */ jsx2("strong", { children: "Settlr" }),
|
|
819
|
+
" \u2022 Powered by Solana"
|
|
820
|
+
] })
|
|
821
|
+
] })
|
|
822
|
+
] });
|
|
823
|
+
}
|
|
824
|
+
function usePaymentLink(config) {
|
|
825
|
+
const {
|
|
826
|
+
merchantWallet,
|
|
827
|
+
merchantName,
|
|
828
|
+
baseUrl = "https://settlr.dev/pay"
|
|
829
|
+
} = config;
|
|
830
|
+
const generateLink = useCallback(
|
|
831
|
+
(options) => {
|
|
832
|
+
const params = new URLSearchParams({
|
|
833
|
+
amount: options.amount.toString(),
|
|
834
|
+
merchant: merchantName,
|
|
835
|
+
to: merchantWallet
|
|
836
|
+
});
|
|
837
|
+
if (options.memo) params.set("memo", options.memo);
|
|
838
|
+
if (options.orderId) params.set("orderId", options.orderId);
|
|
839
|
+
if (options.successUrl) params.set("successUrl", options.successUrl);
|
|
840
|
+
if (options.cancelUrl) params.set("cancelUrl", options.cancelUrl);
|
|
841
|
+
return `${baseUrl}?${params.toString()}`;
|
|
842
|
+
},
|
|
843
|
+
[merchantWallet, merchantName, baseUrl]
|
|
844
|
+
);
|
|
845
|
+
const generateQRCode = useCallback(
|
|
846
|
+
async (options) => {
|
|
847
|
+
const link = generateLink(options);
|
|
848
|
+
const qrUrl = `https://chart.googleapis.com/chart?chs=300x300&cht=qr&chl=${encodeURIComponent(
|
|
849
|
+
link
|
|
850
|
+
)}&choe=UTF-8`;
|
|
851
|
+
return qrUrl;
|
|
852
|
+
},
|
|
853
|
+
[generateLink]
|
|
854
|
+
);
|
|
855
|
+
return {
|
|
856
|
+
generateLink,
|
|
857
|
+
generateQRCode
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/webhooks.ts
|
|
862
|
+
import crypto from "crypto";
|
|
863
|
+
function generateWebhookSignature(payload, secret) {
|
|
864
|
+
return crypto.createHmac("sha256", secret).update(payload).digest("hex");
|
|
865
|
+
}
|
|
866
|
+
function verifyWebhookSignature(payload, signature, secret) {
|
|
867
|
+
const expectedSignature = generateWebhookSignature(payload, secret);
|
|
868
|
+
try {
|
|
869
|
+
return crypto.timingSafeEqual(
|
|
870
|
+
Buffer.from(signature),
|
|
871
|
+
Buffer.from(expectedSignature)
|
|
872
|
+
);
|
|
873
|
+
} catch {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function parseWebhookPayload(rawBody, signature, secret) {
|
|
878
|
+
if (!verifyWebhookSignature(rawBody, signature, secret)) {
|
|
879
|
+
throw new Error("Invalid webhook signature");
|
|
880
|
+
}
|
|
881
|
+
const payload = JSON.parse(rawBody);
|
|
882
|
+
return payload;
|
|
883
|
+
}
|
|
884
|
+
function createWebhookHandler(options) {
|
|
885
|
+
const { secret, handlers, onError } = options;
|
|
886
|
+
return async (req, res) => {
|
|
887
|
+
try {
|
|
888
|
+
let rawBody;
|
|
889
|
+
if (typeof req.body === "string") {
|
|
890
|
+
rawBody = req.body;
|
|
891
|
+
} else if (Buffer.isBuffer(req.body)) {
|
|
892
|
+
rawBody = req.body.toString("utf8");
|
|
893
|
+
} else {
|
|
894
|
+
rawBody = JSON.stringify(req.body);
|
|
895
|
+
}
|
|
896
|
+
const signature = req.headers["x-settlr-signature"];
|
|
897
|
+
if (!signature) {
|
|
898
|
+
res.status(400).json({ error: "Missing signature header" });
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const event = parseWebhookPayload(rawBody, signature, secret);
|
|
902
|
+
const handler = handlers[event.type];
|
|
903
|
+
if (handler) {
|
|
904
|
+
await handler(event);
|
|
905
|
+
}
|
|
906
|
+
res.status(200).json({ received: true });
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if (onError && error instanceof Error) {
|
|
909
|
+
onError(error);
|
|
910
|
+
}
|
|
911
|
+
if (error instanceof Error && error.message === "Invalid webhook signature") {
|
|
912
|
+
res.status(401).json({ error: "Invalid signature" });
|
|
913
|
+
} else {
|
|
914
|
+
res.status(500).json({ error: "Webhook processing failed" });
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
export {
|
|
920
|
+
BuyButton,
|
|
921
|
+
CheckoutWidget,
|
|
922
|
+
SETTLR_CHECKOUT_URL,
|
|
923
|
+
SUPPORTED_NETWORKS,
|
|
924
|
+
Settlr,
|
|
925
|
+
SettlrProvider,
|
|
926
|
+
USDC_MINT_DEVNET,
|
|
927
|
+
USDC_MINT_MAINNET,
|
|
928
|
+
createWebhookHandler,
|
|
929
|
+
formatUSDC,
|
|
930
|
+
parseUSDC,
|
|
931
|
+
parseWebhookPayload,
|
|
932
|
+
shortenAddress,
|
|
933
|
+
usePaymentLink,
|
|
934
|
+
useSettlr,
|
|
935
|
+
verifyWebhookSignature
|
|
936
|
+
};
|