@crossmint/lobster.cash 0.1.1

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.
@@ -0,0 +1,548 @@
1
+ import { Keypair } from "@solana/web3.js";
2
+ import bs58 from "bs58";
3
+ import nacl from "tweetnacl";
4
+ import { describe, it, expect } from "vitest";
5
+
6
+ /**
7
+ * End-to-end test for Amazon order purchase via Crossmint.
8
+ *
9
+ * This test demonstrates the complete delegated signer flow:
10
+ * 1. Create an Amazon order via Crossmint API
11
+ * 2. Create a Crossmint transaction with the serialized transaction
12
+ * 3. Sign the approval message with the delegated signer
13
+ * 4. Submit the approval to Crossmint
14
+ * 5. Wait for transaction to be broadcast and get txId
15
+ * 6. Submit txId to /payment endpoint (CRITICAL STEP!)
16
+ * 7. Poll for order completion
17
+ *
18
+ * Run with:
19
+ * CROSSMINT_API_KEY=your-key \
20
+ * PAYER_ADDRESS=your-smart-wallet-address \
21
+ * SIGNER_PRIVATE_KEY=your-delegated-signer-base58-private-key \
22
+ * pnpm test src/amazon-order.test.ts
23
+ */
24
+
25
+ // Configuration from environment
26
+ const API_KEY = process.env.CROSSMINT_API_KEY || "";
27
+ const PAYER_ADDRESS = process.env.PAYER_ADDRESS || ""; // Smart wallet address
28
+ const SIGNER_PRIVATE_KEY = process.env.PAYER_PRIVATE_KEY || ""; // Delegated signer private key
29
+
30
+ // Crossmint API base URLs - depends on environment
31
+ const CROSSMINT_ENV = process.env.CROSSMINT_ENV || "production";
32
+ const CROSSMINT_BASE_URL =
33
+ CROSSMINT_ENV === "staging"
34
+ ? "https://staging.crossmint.com/api"
35
+ : "https://www.crossmint.com/api";
36
+ const CROSSMINT_ORDERS_API = `${CROSSMINT_BASE_URL}/2022-06-09`;
37
+ const CROSSMINT_WALLETS_API = `${CROSSMINT_BASE_URL}/2025-06-09`;
38
+
39
+ // Test product - can be overridden via environment variable
40
+ const TEST_AMAZON_ASIN = process.env.TEST_AMAZON_ASIN || "B00AATAHY0";
41
+
42
+ // Skip tests if credentials not provided
43
+ const LIVE = API_KEY && PAYER_ADDRESS && SIGNER_PRIVATE_KEY;
44
+
45
+ /**
46
+ * Step 1: Create the Amazon Order
47
+ */
48
+ async function createAmazonOrder(
49
+ amazonASIN: string,
50
+ payerAddress: string,
51
+ currency: string = "sol"
52
+ ) {
53
+ const response = await fetch(`${CROSSMINT_ORDERS_API}/orders`, {
54
+ method: "POST",
55
+ headers: {
56
+ "Content-Type": "application/json",
57
+ "X-API-KEY": API_KEY,
58
+ },
59
+ body: JSON.stringify({
60
+ recipient: {
61
+ email: "buyer@example.com",
62
+ physicalAddress: {
63
+ name: "John Doe",
64
+ line1: "350 5th Ave",
65
+ line2: "Suite 400",
66
+ city: "New York",
67
+ state: "NY",
68
+ postalCode: "10118",
69
+ country: "US",
70
+ },
71
+ },
72
+ payment: {
73
+ method: "solana",
74
+ currency: currency,
75
+ payerAddress: payerAddress,
76
+ receiptEmail: "buyer@example.com",
77
+ },
78
+ lineItems: [
79
+ {
80
+ productLocator: `amazon:${amazonASIN}`,
81
+ },
82
+ ],
83
+ }),
84
+ });
85
+
86
+ if (!response.ok) {
87
+ const error = await response.text();
88
+ throw new Error(`Failed to create order: ${error}`);
89
+ }
90
+
91
+ return await response.json();
92
+ }
93
+
94
+ /**
95
+ * Step 2: Create Crossmint transaction from serialized transaction
96
+ * Returns transactionId and message to sign
97
+ */
98
+ async function createCrossmintTransaction(
99
+ payerAddress: string,
100
+ serializedTransaction: string,
101
+ signerAddress: string
102
+ ) {
103
+ const response = await fetch(
104
+ `${CROSSMINT_WALLETS_API}/wallets/${encodeURIComponent(payerAddress)}/transactions`,
105
+ {
106
+ method: "POST",
107
+ headers: {
108
+ "Content-Type": "application/json",
109
+ "X-API-KEY": API_KEY,
110
+ },
111
+ body: JSON.stringify({
112
+ params: {
113
+ transaction: serializedTransaction,
114
+ signer: `external-wallet:${signerAddress}`,
115
+ },
116
+ }),
117
+ }
118
+ );
119
+
120
+ if (!response.ok) {
121
+ const error = await response.text();
122
+ throw new Error(`Failed to create transaction: ${error}`);
123
+ }
124
+
125
+ return await response.json();
126
+ }
127
+
128
+ /**
129
+ * Step 3: Sign the approval message with delegated signer
130
+ */
131
+ function signApprovalMessage(message: string, privateKeyBase58: string): string {
132
+ const secretKey = bs58.decode(privateKeyBase58);
133
+ const messageBytes = bs58.decode(message);
134
+ const signature = nacl.sign.detached(messageBytes, secretKey);
135
+ return bs58.encode(signature);
136
+ }
137
+
138
+ /**
139
+ * Step 4: Submit approval to Crossmint
140
+ */
141
+ async function submitApproval(
142
+ payerAddress: string,
143
+ transactionId: string,
144
+ signerAddress: string,
145
+ signature: string
146
+ ) {
147
+ const response = await fetch(
148
+ `${CROSSMINT_WALLETS_API}/wallets/${encodeURIComponent(payerAddress)}/transactions/${encodeURIComponent(transactionId)}/approvals`,
149
+ {
150
+ method: "POST",
151
+ headers: {
152
+ "Content-Type": "application/json",
153
+ "X-API-KEY": API_KEY,
154
+ },
155
+ body: JSON.stringify({
156
+ approvals: [
157
+ {
158
+ signer: `external-wallet:${signerAddress}`,
159
+ signature: signature,
160
+ },
161
+ ],
162
+ }),
163
+ }
164
+ );
165
+
166
+ if (!response.ok) {
167
+ const error = await response.text();
168
+ throw new Error(`Failed to submit approval: ${error}`);
169
+ }
170
+
171
+ return await response.json();
172
+ }
173
+
174
+ /**
175
+ * Step 5: Get transaction status from Crossmint Wallets API
176
+ * Poll until we get the on-chain txId
177
+ */
178
+ async function getTransactionStatus(payerAddress: string, transactionId: string) {
179
+ const response = await fetch(
180
+ `${CROSSMINT_WALLETS_API}/wallets/${encodeURIComponent(payerAddress)}/transactions/${encodeURIComponent(transactionId)}`,
181
+ {
182
+ method: "GET",
183
+ headers: {
184
+ "Content-Type": "application/json",
185
+ "X-API-KEY": API_KEY,
186
+ },
187
+ }
188
+ );
189
+
190
+ if (!response.ok) {
191
+ const error = await response.text();
192
+ throw new Error(`Failed to get transaction status: ${error}`);
193
+ }
194
+
195
+ return await response.json();
196
+ }
197
+
198
+ /**
199
+ * Step 5b: Poll for transaction to be broadcast and get on-chain txId
200
+ */
201
+ async function waitForTransactionBroadcast(
202
+ payerAddress: string,
203
+ transactionId: string,
204
+ timeoutMs: number = 30000
205
+ ): Promise<string | null> {
206
+ const startTime = Date.now();
207
+
208
+ while (Date.now() - startTime < timeoutMs) {
209
+ const txStatus = await getTransactionStatus(payerAddress, transactionId);
210
+ console.log("Transaction status:", txStatus.status);
211
+
212
+ // Check if transaction has been broadcast and we have the on-chain txId
213
+ if (txStatus.onChain?.txId) {
214
+ console.log("On-chain txId:", txStatus.onChain.txId);
215
+ return txStatus.onChain.txId;
216
+ }
217
+
218
+ // Also check for txId directly on the response
219
+ if (txStatus.txId) {
220
+ console.log("txId from response:", txStatus.txId);
221
+ return txStatus.txId;
222
+ }
223
+
224
+ // Check if status indicates completion
225
+ if (txStatus.status === "success" || txStatus.status === "completed") {
226
+ // Try to find txId in various places
227
+ const txId = txStatus.onChain?.txId || txStatus.txId || txStatus.hash;
228
+ if (txId) {
229
+ return txId;
230
+ }
231
+ }
232
+
233
+ // Check for failure
234
+ if (txStatus.status === "failed") {
235
+ throw new Error(`Transaction failed: ${JSON.stringify(txStatus)}`);
236
+ }
237
+
238
+ // Wait before polling again
239
+ await new Promise((resolve) => setTimeout(resolve, 2000));
240
+ }
241
+
242
+ console.log("Timeout waiting for transaction broadcast");
243
+ return null;
244
+ }
245
+
246
+ /**
247
+ * Step 6: Submit payment to Crossmint Orders API (CRITICAL!)
248
+ * This notifies Crossmint that the payment transaction has been submitted
249
+ */
250
+ async function processPayment(orderId: string, txId: string, clientSecret?: string) {
251
+ const headers: Record<string, string> = {
252
+ "Content-Type": "application/json",
253
+ "X-API-KEY": API_KEY,
254
+ };
255
+ if (clientSecret) {
256
+ headers["Authorization"] = clientSecret;
257
+ }
258
+
259
+ const response = await fetch(`${CROSSMINT_ORDERS_API}/orders/${orderId}/payment`, {
260
+ method: "POST",
261
+ headers,
262
+ body: JSON.stringify({
263
+ type: "crypto-tx-id",
264
+ txId: txId,
265
+ }),
266
+ });
267
+
268
+ if (!response.ok) {
269
+ const error = await response.text();
270
+ throw new Error(`Failed to process payment: ${error}`);
271
+ }
272
+
273
+ return await response.json();
274
+ }
275
+
276
+ /**
277
+ * Step 7: Poll for Order Completion
278
+ */
279
+ async function pollOrderStatus(
280
+ orderId: string,
281
+ clientSecret: string,
282
+ timeoutMs: number = 60000
283
+ ): Promise<{ paymentStatus: string; deliveryStatus: string }> {
284
+ return new Promise((resolve) => {
285
+ const intervalId = setInterval(async () => {
286
+ try {
287
+ const response = await fetch(`${CROSSMINT_ORDERS_API}/orders/${orderId}`, {
288
+ method: "GET",
289
+ headers: {
290
+ "Content-Type": "application/json",
291
+ "X-API-KEY": API_KEY,
292
+ Authorization: clientSecret,
293
+ },
294
+ });
295
+
296
+ if (!response.ok) {
297
+ console.log("Failed to get order status:", await response.text());
298
+ return;
299
+ }
300
+
301
+ const orderStatus = await response.json();
302
+ const paymentStatus = orderStatus.payment?.status || "unknown";
303
+ const deliveryStatus = orderStatus.lineItems?.[0]?.delivery?.status || "pending";
304
+
305
+ console.log("Payment:", paymentStatus);
306
+ console.log("Delivery:", deliveryStatus);
307
+
308
+ if (paymentStatus === "completed") {
309
+ clearInterval(intervalId);
310
+ console.log("Payment completed! Amazon order is being fulfilled.");
311
+ resolve({ paymentStatus, deliveryStatus });
312
+ }
313
+ } catch (error) {
314
+ console.error("Error polling order status:", error);
315
+ }
316
+ }, 2500);
317
+
318
+ setTimeout(() => {
319
+ clearInterval(intervalId);
320
+ console.log("Timeout - check order manually");
321
+ resolve({ paymentStatus: "timeout", deliveryStatus: "unknown" });
322
+ }, timeoutMs);
323
+ });
324
+ }
325
+
326
+ /**
327
+ * Get order status (single call)
328
+ */
329
+ async function getOrderStatus(orderId: string, clientSecret?: string) {
330
+ const headers: Record<string, string> = {
331
+ "Content-Type": "application/json",
332
+ "X-API-KEY": API_KEY,
333
+ };
334
+ if (clientSecret) {
335
+ headers["Authorization"] = clientSecret;
336
+ }
337
+
338
+ const response = await fetch(`${CROSSMINT_ORDERS_API}/orders/${orderId}`, {
339
+ method: "GET",
340
+ headers,
341
+ });
342
+
343
+ if (!response.ok) {
344
+ const error = await response.text();
345
+ throw new Error(`Failed to get order: ${error}`);
346
+ }
347
+
348
+ return await response.json();
349
+ }
350
+
351
+ /**
352
+ * Get signer public key from private key
353
+ */
354
+ function getSignerAddress(privateKeyBase58: string): string {
355
+ const secretKey = bs58.decode(privateKeyBase58);
356
+ const keypair = Keypair.fromSecretKey(secretKey);
357
+ return keypair.publicKey.toBase58();
358
+ }
359
+
360
+ describe("Amazon Order E2E Test", () => {
361
+ describe.skipIf(!LIVE)("live: delegated signer purchase flow", () => {
362
+ it("creates order and pays with delegated signer approval", async () => {
363
+ const signerAddress = getSignerAddress(SIGNER_PRIVATE_KEY);
364
+
365
+ console.log("\n=== Starting Amazon Order E2E Test (Delegated Signer Flow) ===\n");
366
+ console.log("Smart Wallet (Payer):", PAYER_ADDRESS);
367
+ console.log("Delegated Signer:", signerAddress);
368
+ console.log("Amazon ASIN:", TEST_AMAZON_ASIN);
369
+
370
+ // Step 1: Create the order
371
+ console.log("\n--- Step 1: Creating Amazon order ---");
372
+ const { order, clientSecret } = await createAmazonOrder(
373
+ TEST_AMAZON_ASIN,
374
+ PAYER_ADDRESS,
375
+ "sol"
376
+ );
377
+
378
+ console.log("Order ID:", order.orderId);
379
+ console.log("Order Phase:", order.phase);
380
+ console.log("Client Secret:", clientSecret ? "✓ received" : "✗ missing");
381
+
382
+ expect(order.orderId).toBeDefined();
383
+ expect(order.phase).toBeDefined();
384
+
385
+ // Check if we have a serialized transaction
386
+ const serializedTransaction = order.payment?.preparation?.serializedTransaction;
387
+ console.log(
388
+ "Serialized Transaction:",
389
+ serializedTransaction ? `✓ received (${serializedTransaction.length} chars)` : "✗ missing"
390
+ );
391
+
392
+ if (!serializedTransaction) {
393
+ console.log("\nOrder created but no serialized transaction returned.");
394
+ console.log("Full order response:", JSON.stringify(order, null, 2));
395
+ return;
396
+ }
397
+
398
+ // Check for insufficient funds
399
+ if (order.payment?.failureReason?.code === "insufficient-funds") {
400
+ console.log("\n⚠️ Insufficient funds:", order.payment.failureReason.message);
401
+ console.log("Please fund the wallet and try again.");
402
+ return;
403
+ }
404
+
405
+ // Step 2: Create Crossmint transaction
406
+ console.log("\n--- Step 2: Creating Crossmint transaction ---");
407
+ const txResponse = await createCrossmintTransaction(
408
+ PAYER_ADDRESS,
409
+ serializedTransaction,
410
+ signerAddress
411
+ );
412
+
413
+ console.log("Transaction ID:", txResponse.id);
414
+ console.log("Transaction Status:", txResponse.status);
415
+
416
+ const messageToSign = txResponse.approvals?.pending?.[0]?.message;
417
+ console.log(
418
+ "Message to sign:",
419
+ messageToSign ? `✓ received (${messageToSign.length} chars)` : "✗ missing"
420
+ );
421
+
422
+ if (!messageToSign) {
423
+ console.log(
424
+ "\nNo message to sign. Transaction response:",
425
+ JSON.stringify(txResponse, null, 2)
426
+ );
427
+ return;
428
+ }
429
+
430
+ // Step 3: Sign the approval message
431
+ console.log("\n--- Step 3: Signing approval message ---");
432
+ const signature = signApprovalMessage(messageToSign, SIGNER_PRIVATE_KEY);
433
+ console.log("Signature:", `✓ generated (${signature.length} chars)`);
434
+
435
+ // Step 4: Submit approval
436
+ console.log("\n--- Step 4: Submitting approval to Crossmint ---");
437
+ const approvalResponse = await submitApproval(
438
+ PAYER_ADDRESS,
439
+ txResponse.id,
440
+ signerAddress,
441
+ signature
442
+ );
443
+
444
+ console.log("Approval Response:", JSON.stringify(approvalResponse, null, 2));
445
+
446
+ // Step 5: Wait for transaction to be broadcast and get on-chain txId
447
+ console.log("\n--- Step 5: Waiting for transaction broadcast ---");
448
+ const onChainTxId = await waitForTransactionBroadcast(PAYER_ADDRESS, txResponse.id, 30000);
449
+
450
+ if (!onChainTxId) {
451
+ console.log("Could not get on-chain txId. Checking approval response for txId...");
452
+ // Try to get txId from approval response
453
+ const fallbackTxId =
454
+ approvalResponse.onChain?.txId || approvalResponse.txId || approvalResponse.hash;
455
+ if (!fallbackTxId) {
456
+ console.log("No txId available. Cannot call /payment endpoint.");
457
+ console.log("Full approval response:", JSON.stringify(approvalResponse, null, 2));
458
+ return;
459
+ }
460
+ }
461
+
462
+ const txIdToSubmit = onChainTxId || approvalResponse.onChain?.txId || approvalResponse.txId;
463
+
464
+ // Step 6: Submit payment to Crossmint (CRITICAL!)
465
+ console.log("\n--- Step 6: Submitting payment to Crossmint /payment endpoint ---");
466
+ console.log("Submitting txId:", txIdToSubmit);
467
+
468
+ const paymentResponse = await processPayment(order.orderId, txIdToSubmit!, clientSecret);
469
+ console.log("Payment Response:", JSON.stringify(paymentResponse, null, 2));
470
+
471
+ // Step 7: Poll for order completion
472
+ console.log("\n--- Step 7: Polling for order completion ---");
473
+ const { paymentStatus, deliveryStatus } = await pollOrderStatus(
474
+ order.orderId,
475
+ clientSecret,
476
+ 60000
477
+ );
478
+
479
+ console.log("\n=== Final Status ===");
480
+ console.log("Payment Status:", paymentStatus);
481
+ console.log("Delivery Status:", deliveryStatus);
482
+
483
+ // Payment should eventually complete
484
+ expect(["completed", "timeout", "crypto-payer-insufficient-funds"]).toContain(paymentStatus);
485
+ }, 180000); // 3 minute timeout
486
+
487
+ it("creates order only (inspect response)", async () => {
488
+ console.log("\n=== Create Order Only Test ===\n");
489
+
490
+ const { order, clientSecret } = await createAmazonOrder(
491
+ TEST_AMAZON_ASIN,
492
+ PAYER_ADDRESS,
493
+ "sol"
494
+ );
495
+
496
+ console.log("Order ID:", order.orderId);
497
+ console.log("Order Phase:", order.phase);
498
+ console.log("Quote:", JSON.stringify(order.quote, null, 2));
499
+ console.log("Payment:", JSON.stringify(order.payment, null, 2));
500
+
501
+ expect(order.orderId).toBeDefined();
502
+
503
+ console.log("\n--- Full Response ---");
504
+ console.log(JSON.stringify({ order, clientSecret }, null, 2));
505
+ }, 30000);
506
+ });
507
+
508
+ describe("unit tests", () => {
509
+ it("validates keypair creation from base58", () => {
510
+ const testKeypair = Keypair.generate();
511
+ const secretKeyBase58 = bs58.encode(testKeypair.secretKey);
512
+ const recreatedKeypair = Keypair.fromSecretKey(bs58.decode(secretKeyBase58));
513
+ expect(recreatedKeypair.publicKey.toBase58()).toBe(testKeypair.publicKey.toBase58());
514
+ });
515
+
516
+ it("validates signature generation", () => {
517
+ const testKeypair = Keypair.generate();
518
+ const secretKeyBase58 = bs58.encode(testKeypair.secretKey);
519
+ const testMessage = bs58.encode(Buffer.from("test message"));
520
+
521
+ const signature = signApprovalMessage(testMessage, secretKeyBase58);
522
+ expect(signature).toBeDefined();
523
+ expect(signature.length).toBeGreaterThan(0);
524
+
525
+ // Verify signature
526
+ const sigBytes = bs58.decode(signature);
527
+ const msgBytes = bs58.decode(testMessage);
528
+ const isValid = nacl.sign.detached.verify(
529
+ msgBytes,
530
+ sigBytes,
531
+ testKeypair.publicKey.toBytes()
532
+ );
533
+ expect(isValid).toBe(true);
534
+ });
535
+ });
536
+ });
537
+
538
+ export {
539
+ createAmazonOrder,
540
+ createCrossmintTransaction,
541
+ signApprovalMessage,
542
+ submitApproval,
543
+ waitForTransactionBroadcast,
544
+ processPayment,
545
+ pollOrderStatus,
546
+ getOrderStatus,
547
+ getTransactionStatus,
548
+ };