@crossmint/openclaw-wallet 0.2.2 → 0.2.4

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 CHANGED
@@ -9,6 +9,7 @@ This plugin enables OpenClaw agents to:
9
9
  - Use Crossmint smart wallets on Solana
10
10
  - Check wallet balances (SOL, USDC, SPL tokens)
11
11
  - Send tokens to other addresses
12
+ - **Buy products from Amazon** with SOL or USDC
12
13
 
13
14
  The key innovation is **delegated signing**: the agent holds its own private key locally, and users authorize the agent to operate their Crossmint wallet through a web-based delegation flow.
14
15
 
@@ -38,7 +39,7 @@ Enable the plugin in `~/.openclaw/.openclaw.json5`:
38
39
 
39
40
  ## Usage
40
41
 
41
- ### Setting Up a Wallet (2-Step Process)
42
+ ### Setting Up a Wallet (3-Step Process)
42
43
 
43
44
  **Step 1: Generate keypair and get delegation URL**
44
45
 
@@ -46,13 +47,13 @@ Ask the agent: "Set up my Crossmint wallet"
46
47
 
47
48
  The agent will:
48
49
  1. Generate a local Solana keypair (ed25519)
49
- 2. Provide a URL with the public key for delegation setup
50
+ 2. Provide a delegation URL: `https://www.lobster.cash/configure?pubkey=<your-public-key>`
50
51
 
51
52
  **Step 2: Complete setup on the web app**
52
53
 
53
54
  1. Open the delegation URL in your browser
54
55
  2. The web app will:
55
- - Create a Crossmint smart wallet
56
+ - Create a Crossmint smart wallet on Solana devnet
56
57
  - Add the agent's public key as a delegated signer
57
58
  - Show you the **wallet address** and **API key**
58
59
 
@@ -75,6 +76,33 @@ The agent will:
75
76
  2. Sign the transaction locally using ed25519
76
77
  3. Submit to Crossmint for execution on Solana
77
78
 
79
+ ### Buying from Amazon
80
+
81
+ Ask the agent: "Buy me this Amazon product: B00O79SKV6"
82
+
83
+ The agent will:
84
+ 1. Ask for shipping address if not provided
85
+ 2. Create an order with Crossmint
86
+ 3. Sign the payment transaction locally
87
+ 4. Submit the payment and confirm on-chain
88
+ 5. Return the order ID and Solana explorer link
89
+
90
+ **Example purchase flow:**
91
+ ```
92
+ User: "Buy B00O79SKV6 and ship to John Doe, 123 Main St, New York NY 10001"
93
+
94
+ Agent: ✅ Purchase complete!
95
+
96
+ Product: AmazonBasics USB Cable
97
+ Price: 0.05 SOL
98
+ Order ID: order_abc123
99
+ Payment: completed
100
+
101
+ Transaction: https://explorer.solana.com/tx/5x...?cluster=devnet
102
+
103
+ Use crossmint_order_status to check delivery status.
104
+ ```
105
+
78
106
  ## Tools
79
107
 
80
108
  | Tool | Description |
@@ -88,6 +116,36 @@ The agent will:
88
116
  | `crossmint_buy` | Buy products from Amazon with SOL or USDC |
89
117
  | `crossmint_order_status` | Check Amazon order/delivery status |
90
118
 
119
+ ## Amazon Purchase Flow
120
+
121
+ When you use `crossmint_buy`, the plugin executes a complete delegated signer flow:
122
+
123
+ ```
124
+ ┌─────────────────────────────────────────────────────────────┐
125
+ │ 1. Create Order │
126
+ │ POST /orders → Returns serialized payment transaction │
127
+ ├─────────────────────────────────────────────────────────────┤
128
+ │ 2. Create Transaction │
129
+ │ POST /wallets/{address}/transactions │
130
+ │ → Returns approval message to sign │
131
+ ├─────────────────────────────────────────────────────────────┤
132
+ │ 3. Sign Approval (Local) │
133
+ │ Agent signs message with ed25519 keypair │
134
+ ├─────────────────────────────────────────────────────────────┤
135
+ │ 4. Submit Approval │
136
+ │ POST /wallets/{address}/transactions/{id}/approvals │
137
+ ├─────────────────────────────────────────────────────────────┤
138
+ │ 5. Wait for Broadcast │
139
+ │ Poll until on-chain txId is available │
140
+ ├─────────────────────────────────────────────────────────────┤
141
+ │ 6. Confirm Payment (CRITICAL) │
142
+ │ POST /orders/{orderId}/payment │
143
+ │ → Notifies Crossmint that payment is on-chain │
144
+ └─────────────────────────────────────────────────────────────┘
145
+ ```
146
+
147
+ All steps are handled automatically by the plugin.
148
+
91
149
  ## Architecture
92
150
 
93
151
  ```
@@ -102,7 +160,7 @@ The agent will:
102
160
 
103
161
 
104
162
  ┌─────────────────────────────────────────────────────────────┐
105
- Delegation Web App (external) │
163
+ Delegation Web App (lobster.cash) │
106
164
  ├─────────────────────────────────────────────────────────────┤
107
165
  │ - Receives agent's public key via URL │
108
166
  │ - Creates Crossmint smart wallet │
@@ -114,16 +172,17 @@ The agent will:
114
172
  ┌─────────────────────────────────────────────────────────────┐
115
173
  │ Crossmint Smart Wallet │
116
174
  ├─────────────────────────────────────────────────────────────┤
117
- │ - Deployed on Solana
175
+ │ - Deployed on Solana (devnet)
118
176
  │ - Agent's address registered as delegated signer │
119
177
  │ - User retains admin control │
178
+ │ - Holds SOL/USDC for purchases and transfers │
120
179
  └─────────────────────────────────────────────────────────────┘
121
180
  ```
122
181
 
123
182
  ## Security
124
183
 
125
- - Private keys are stored locally on the agent's machine with secure file permissions
126
- - Keys are never transmitted to Crossmint
184
+ - Private keys are stored locally on the agent's machine with secure file permissions (0600)
185
+ - Keys are never transmitted to Crossmint - only signatures
127
186
  - Uses ed25519 cryptography (Solana native)
128
187
  - API key is stored locally after user retrieves it from web app
129
188
  - Users maintain admin control and can revoke delegation at any time
@@ -135,13 +194,23 @@ The agent will:
135
194
  - Run `crossmint_setup` first to generate a keypair
136
195
 
137
196
  **"Wallet not fully configured"**
138
- - Complete the web setup flow and run `crossmint_configure` with wallet address and API key
197
+ - Complete the web setup flow at the delegation URL
198
+ - Run `crossmint_configure` with wallet address and API key from the web app
139
199
 
140
200
  **"Failed to get balance" or "Failed to send"**
141
- - Verify the API key is correct
201
+ - Verify the API key is correct (should start with `ck_staging_`)
142
202
  - Check that the wallet address matches the one shown in the web app
143
203
  - Ensure the wallet has sufficient balance
144
204
 
205
+ **"Insufficient funds" (Amazon purchase)**
206
+ - Check balance with `crossmint_balance`
207
+ - Fund the wallet with more SOL or USDC
208
+ - For devnet testing, use Solana faucets for test SOL
209
+
210
+ **"Timeout waiting for transaction to be broadcast"**
211
+ - Check transaction status with `crossmint_tx_status`
212
+ - Solana network may be congested - wait and retry
213
+
145
214
  ## Plugin Management
146
215
 
147
216
  ```bash
@@ -155,3 +224,11 @@ openclaw plugins info openclaw-wallet
155
224
  openclaw plugins enable openclaw-wallet
156
225
  openclaw plugins disable openclaw-wallet
157
226
  ```
227
+
228
+ ## Supported Currencies
229
+
230
+ | Currency | Token | Use Cases |
231
+ |----------|-------|-----------|
232
+ | SOL | Native Solana | Transfers, Amazon purchases |
233
+ | USDC | USD Coin | Transfers, Amazon purchases |
234
+ | SPL tokens | Any mint address | Transfers only |
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-wallet",
3
3
  "name": "Crossmint",
4
4
  "description": "Solana wallet integration using Crossmint smart wallets with delegated signing.",
5
- "version": "0.2.2",
5
+ "version": "0.2.4",
6
6
  "skills": ["skills/crossmint"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crossmint/openclaw-wallet",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "OpenClaw plugin for Solana wallet integration with Crossmint smart wallets",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -43,14 +43,14 @@ User: "Set up my Crossmint wallet"
43
43
  Agent: Use crossmint_setup
44
44
  ```
45
45
 
46
- This generates a local ed25519 keypair and returns a delegation URL.
46
+ This generates a local ed25519 keypair and returns a delegation URL pointing to `https://www.lobster.cash/configure?pubkey=<agent-public-key>`.
47
47
 
48
48
  ### Step 2: User completes web setup
49
49
 
50
- The user opens the delegation URL in their browser. The web app will:
51
- 1. Create a Crossmint smart wallet
50
+ The user opens the delegation URL in their browser. The web app (lobster.cash) will:
51
+ 1. Create a Crossmint smart wallet on Solana devnet
52
52
  2. Add the agent's public key as a delegated signer
53
- 3. Display the **wallet address** and **API key**
53
+ 3. Display the **wallet address** and **API key** for the user to copy
54
54
 
55
55
  ### Step 3: Configure the agent
56
56
 
@@ -59,7 +59,7 @@ User: "My wallet address is X and API key is Y"
59
59
  Agent: Use crossmint_configure with walletAddress and apiKey
60
60
  ```
61
61
 
62
- Now the wallet is ready to use.
62
+ Now the wallet is ready to use for balance checks, transfers, and Amazon purchases.
63
63
 
64
64
  ## Common Operations
65
65
 
@@ -70,7 +70,7 @@ User: "What's my wallet balance?"
70
70
  Agent: Use crossmint_balance
71
71
  ```
72
72
 
73
- Returns SOL, USDC, and other token balances.
73
+ Returns SOL, USDC, and other token balances on the smart wallet.
74
74
 
75
75
  ### Send tokens
76
76
 
@@ -112,6 +112,55 @@ Agent: Use crossmint_wallet_info
112
112
 
113
113
  Buy products from Amazon using SOL or USDC from the agent's wallet. Crossmint acts as Merchant of Record, handling payments, shipping, and taxes.
114
114
 
115
+ ### CRITICAL: Product Validation
116
+
117
+ **ALWAYS validate that the product matches what the user requested before completing a purchase.**
118
+
119
+ When a purchase completes, the response includes the product title. The agent MUST:
120
+
121
+ 1. **Compare the product title** with what the user asked for
122
+ 2. **If there's a mismatch**, inform the user immediately:
123
+ ```
124
+ ⚠️ The product found doesn't match your request.
125
+
126
+ You asked for: "Celsius Energy Drink"
127
+ Product found: "USB Cable Organizer"
128
+
129
+ This might be the wrong product. Would you like me to:
130
+ 1. Search for the correct product on Amazon
131
+ 2. Cancel and try a different ASIN
132
+ 3. Proceed anyway (if this is actually correct)
133
+ ```
134
+ 3. **Never assume** - If the user says "buy Celsius" without an ASIN, search Amazon first to find the correct product
135
+ 4. **Confirm before payment** - For vague requests, always confirm the exact product before purchasing
136
+
137
+ **Best Practice Flow:**
138
+ ```
139
+ User: "Buy me some Celsius energy drinks"
140
+ Agent:
141
+ 1. First, search Amazon for "Celsius energy drink" to find the correct ASIN
142
+ 2. Present options: "I found these Celsius products:
143
+ - B08P5H1FLX: Celsius Sparkling Orange (12-pack) - ~$25
144
+ - B07GX3GDN5: Celsius Variety Pack (12-pack) - ~$30
145
+ Which one would you like?"
146
+ 3. User confirms: "The variety pack"
147
+ 4. Agent uses crossmint_buy with the confirmed ASIN
148
+ 5. After purchase, verify the product title in the response matches
149
+ ```
150
+
151
+ ### How It Works (Under the Hood)
152
+
153
+ When you use `crossmint_buy`, the plugin executes a 6-step delegated signer flow:
154
+
155
+ 1. **Create Order** - Crossmint creates an Amazon order and returns a payment transaction
156
+ 2. **Create Transaction** - The serialized transaction is submitted to Crossmint's wallet API
157
+ 3. **Sign Approval** - The agent signs an approval message locally using ed25519
158
+ 4. **Submit Approval** - The signed approval is sent to Crossmint
159
+ 5. **Wait for Broadcast** - Poll until the transaction is confirmed on-chain
160
+ 6. **Confirm Payment** - Submit the on-chain transaction ID to complete the order
161
+
162
+ The entire flow happens automatically - the agent handles all signing and API calls.
163
+
115
164
  ### Buy a product
116
165
 
117
166
  ```
@@ -121,9 +170,37 @@ Agent: Use crossmint_buy with product ASIN and shipping address
121
170
 
122
171
  Required information:
123
172
  - Amazon product ASIN or URL
124
- - Recipient email
173
+ - Recipient email (for order confirmation)
125
174
  - Full shipping address (name, street, city, postal code, country)
126
175
 
176
+ ### Successful Purchase Response
177
+
178
+ When a purchase completes successfully, you'll receive:
179
+ - **Order ID** - Use with `crossmint_order_status` to track delivery
180
+ - **Transaction Explorer Link** - Solana explorer URL to verify the payment on-chain
181
+ - **Payment Status** - Confirms payment was processed
182
+ - **Product Details** - Title and price of the purchased item
183
+
184
+ Example response:
185
+ ```
186
+ ✅ Purchase complete!
187
+
188
+ Product: AmazonBasics USB Cable
189
+ Price: 0.05 SOL
190
+ Order ID: order_abc123
191
+ Payment: completed
192
+
193
+ Transaction: https://explorer.solana.com/tx/5x...?cluster=devnet
194
+
195
+ Shipping to:
196
+ John Doe
197
+ 123 Main St
198
+ New York, NY 10001
199
+ US
200
+
201
+ Use crossmint_order_status to check delivery status.
202
+ ```
203
+
127
204
  ### Check order status
128
205
 
129
206
  ```
@@ -131,18 +208,47 @@ User: "What's the status of my order?"
131
208
  Agent: Use crossmint_order_status with the order ID
132
209
  ```
133
210
 
211
+ Returns:
212
+ - Order phase (quote, payment, delivery, completed)
213
+ - Payment status
214
+ - Delivery status
215
+ - Tracking information (when available)
216
+
217
+ ### Finding the Right Product
218
+
219
+ When the user doesn't provide an ASIN:
220
+
221
+ 1. **Search Amazon first** - Use web search to find the product: `"[product name] site:amazon.com"`
222
+ 2. **Extract the ASIN** - Look for the 10-character alphanumeric code (e.g., B08P5H1FLX) in the URL
223
+ 3. **Confirm with user** - Show the product name, price estimate, and ask for confirmation
224
+ 4. **Then purchase** - Only call `crossmint_buy` after user confirms the correct product
225
+
226
+ **Example:**
227
+ ```
228
+ User: "Buy me Celsius energy drinks"
229
+ Agent: Let me search Amazon for Celsius energy drinks...
230
+ [searches "Celsius energy drink site:amazon.com"]
231
+ I found: "CELSIUS Sparkling Orange Fitness Drink (12-pack)" - ASIN: B08P5H1FLX
232
+ Is this what you want? (Yes/No, or tell me a different product)
233
+ User: "Yes"
234
+ Agent: [calls crossmint_buy with B08P5H1FLX]
235
+ ```
236
+
134
237
  ### Amazon Product Locator Formats
135
238
 
136
- - ASIN: `B00O79SKV6`
239
+ All of these formats work:
240
+ - ASIN only: `B00O79SKV6`
137
241
  - Full URL: `https://www.amazon.com/dp/B00O79SKV6`
242
+ - With amazon: prefix: `amazon:B00O79SKV6`
138
243
 
139
244
  ### Amazon Order Restrictions
140
245
 
141
246
  Orders may fail if:
142
247
  - Item not sold by Amazon or verified seller
143
- - Item requires special shipping
144
- - Item is digital (ebooks, software, etc.)
248
+ - Item requires special shipping (hazmat, oversized)
249
+ - Item is digital (ebooks, software, music, etc.)
145
250
  - Item is from Amazon Fresh, Pantry, Pharmacy, or Subscribe & Save
251
+ - Item is out of stock or unavailable for shipping to the address
146
252
 
147
253
  ## Tool Parameters
148
254
 
@@ -214,9 +320,9 @@ Orders may fail if:
214
320
  "addressLine1": "required - street address",
215
321
  "addressLine2": "optional - apt, suite, etc.",
216
322
  "city": "required",
217
- "state": "optional - state/province code",
323
+ "state": "optional - state/province code (e.g., 'CA', 'NY')",
218
324
  "postalCode": "required",
219
- "country": "required - e.g., 'US'",
325
+ "country": "required - ISO country code (e.g., 'US')",
220
326
  "currency": "optional - 'sol' or 'usdc' (default: 'usdc')",
221
327
  "agentId": "optional"
222
328
  }
@@ -248,27 +354,97 @@ Keypair exists but web setup wasn't completed.
248
354
  ```
249
355
  Agent:
250
356
  1. Use crossmint_wallet_info to get the delegation URL
251
- 2. Ask user to complete web setup
252
- 3. Use crossmint_configure with the wallet address and API key
357
+ 2. Ask user to complete web setup at the URL
358
+ 3. Use crossmint_configure with the wallet address and API key from the web app
253
359
  ```
254
360
 
255
361
  ### "Failed to get balance" or "Failed to send"
256
362
 
257
- - Verify the API key is correct
258
- - Check wallet address matches the web app
363
+ - Verify the API key is correct (should start with `ck_staging_` for devnet)
364
+ - Check wallet address matches the one shown in the web app
259
365
  - Ensure sufficient balance for transfers
260
366
 
367
+ ### "Insufficient funds" (Amazon purchase)
368
+
369
+ The wallet doesn't have enough SOL or USDC for the purchase.
370
+
371
+ ```
372
+ Agent:
373
+ 1. Use crossmint_balance to check current balance
374
+ 2. Ask user to fund the wallet with more SOL/USDC
375
+ 3. For devnet testing, use Solana faucets to get test SOL
376
+ ```
377
+
378
+ ### "Wrong product was purchased"
379
+
380
+ The product title doesn't match what the user requested.
381
+
382
+ **Prevention:**
383
+ 1. Always search for the correct ASIN before purchasing
384
+ 2. Confirm the product with the user before calling `crossmint_buy`
385
+ 3. After purchase, compare the product title in the response with user's request
386
+
387
+ **If it happens:**
388
+ ```
389
+ Agent:
390
+ ⚠️ I notice the product purchased doesn't match your request.
391
+
392
+ You asked for: "Celsius Energy Drink"
393
+ Product purchased: "USB Cable Organizer" (Order ID: xxx)
394
+
395
+ Unfortunately, the payment has already been processed. You may need to:
396
+ 1. Contact Crossmint support to request a cancellation/refund
397
+ 2. Wait for delivery and return the item
398
+
399
+ I apologize for this error. In the future, I'll confirm the exact product with you before purchasing.
400
+ ```
401
+
402
+ ### "Order created but no serialized transaction returned"
403
+
404
+ The order was created but payment couldn't be prepared. This usually means:
405
+ - Product is unavailable or restricted
406
+ - Shipping address is invalid
407
+ - Price changed during checkout
408
+
409
+ ### "Timeout waiting for transaction to be broadcast"
410
+
411
+ The transaction was signed but didn't confirm on-chain within 30 seconds.
412
+
413
+ ```
414
+ Agent:
415
+ 1. Use crossmint_tx_status to check the transaction status
416
+ 2. If still pending, wait longer or retry
417
+ 3. Check Solana network status for congestion
418
+ ```
419
+
261
420
  ## Security Notes
262
421
 
263
422
  - Private keys are stored locally at `~/.openclaw/crossmint-wallets/`
264
- - Keys never leave the agent's machine
423
+ - Keys never leave the agent's machine - only signatures are sent to Crossmint
265
424
  - Uses ed25519 cryptography (Solana native)
266
- - Users retain admin control and can revoke delegation anytime
425
+ - Users retain admin control and can revoke delegation anytime via the Crossmint dashboard
267
426
  - Always verify recipient addresses before sending
427
+ - The API key grants limited permissions - only what the user authorized during delegation
268
428
 
269
429
  ## Best Practices
270
430
 
271
- 1. **Always check balance before sending** - Avoid failed transactions
272
- 2. **Confirm recipient with user** - Double-check addresses for large transfers
273
- 3. **Get devnet tokens for testing** - Use Solana devnet faucets to get test SOL
274
- 4. **One wallet per agent** - Each agent ID gets its own keypair
431
+ 1. **ALWAYS validate products before purchasing** - Never buy without confirming the product matches user intent
432
+ 2. **Search Amazon first for vague requests** - If user says "buy X" without an ASIN, search for the correct product first
433
+ 3. **Confirm product title after purchase** - Check that the returned product title matches what was requested
434
+ 4. **Always check balance before purchasing** - Avoid failed transactions due to insufficient funds
435
+ 5. **Confirm shipping address with user** - Double-check addresses for Amazon purchases
436
+ 6. **Get devnet tokens for testing** - Use Solana devnet faucets to get test SOL before trying purchases
437
+ 7. **One wallet per agent** - Each agent ID gets its own keypair and wallet
438
+ 8. **Save the order ID** - Users should note the order ID to track delivery status later
439
+ 9. **Verify on-chain** - The explorer link lets users verify the payment transaction on Solana
440
+
441
+ ## Supported Currencies
442
+
443
+ For Amazon purchases:
444
+ - **SOL** - Native Solana token
445
+ - **USDC** - USD Coin stablecoin on Solana
446
+
447
+ For transfers:
448
+ - **SOL** - Native Solana token
449
+ - **USDC** - USD Coin stablecoin
450
+ - **Any SPL token** - Specify the token mint address
@@ -0,0 +1,528 @@
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 (staging = devnet)
31
+ const CROSSMINT_ORDERS_API = "https://staging.crossmint.com/api/2022-06-09";
32
+ const CROSSMINT_WALLETS_API = "https://staging.crossmint.com/api/2025-06-09";
33
+
34
+ // Test product - can be overridden via environment variable
35
+ const TEST_AMAZON_ASIN = process.env.TEST_AMAZON_ASIN || "B00AATAHY0";
36
+
37
+ // Skip tests if credentials not provided
38
+ const LIVE = API_KEY && PAYER_ADDRESS && SIGNER_PRIVATE_KEY;
39
+
40
+ /**
41
+ * Step 1: Create the Amazon Order
42
+ */
43
+ async function createAmazonOrder(amazonASIN: string, payerAddress: string, currency: string = "sol") {
44
+ const response = await fetch(`${CROSSMINT_ORDERS_API}/orders`, {
45
+ method: "POST",
46
+ headers: {
47
+ "Content-Type": "application/json",
48
+ "X-API-KEY": API_KEY,
49
+ },
50
+ body: JSON.stringify({
51
+ recipient: {
52
+ email: "buyer@example.com",
53
+ physicalAddress: {
54
+ name: "John Doe",
55
+ line1: "350 5th Ave",
56
+ line2: "Suite 400",
57
+ city: "New York",
58
+ state: "NY",
59
+ postalCode: "10118",
60
+ country: "US",
61
+ },
62
+ },
63
+ payment: {
64
+ method: "solana",
65
+ currency: currency,
66
+ payerAddress: payerAddress,
67
+ receiptEmail: "buyer@example.com",
68
+ },
69
+ lineItems: [
70
+ {
71
+ productLocator: `amazon:${amazonASIN}`,
72
+ },
73
+ ],
74
+ }),
75
+ });
76
+
77
+ if (!response.ok) {
78
+ const error = await response.text();
79
+ throw new Error(`Failed to create order: ${error}`);
80
+ }
81
+
82
+ return await response.json();
83
+ }
84
+
85
+ /**
86
+ * Step 2: Create Crossmint transaction from serialized transaction
87
+ * Returns transactionId and message to sign
88
+ */
89
+ async function createCrossmintTransaction(
90
+ payerAddress: string,
91
+ serializedTransaction: string,
92
+ signerAddress: string
93
+ ) {
94
+ const response = await fetch(
95
+ `${CROSSMINT_WALLETS_API}/wallets/${encodeURIComponent(payerAddress)}/transactions`,
96
+ {
97
+ method: "POST",
98
+ headers: {
99
+ "Content-Type": "application/json",
100
+ "X-API-KEY": API_KEY,
101
+ },
102
+ body: JSON.stringify({
103
+ params: {
104
+ transaction: serializedTransaction,
105
+ signer: `external-wallet:${signerAddress}`,
106
+ },
107
+ }),
108
+ }
109
+ );
110
+
111
+ if (!response.ok) {
112
+ const error = await response.text();
113
+ throw new Error(`Failed to create transaction: ${error}`);
114
+ }
115
+
116
+ return await response.json();
117
+ }
118
+
119
+ /**
120
+ * Step 3: Sign the approval message with delegated signer
121
+ */
122
+ function signApprovalMessage(message: string, privateKeyBase58: string): string {
123
+ const secretKey = bs58.decode(privateKeyBase58);
124
+ const messageBytes = bs58.decode(message);
125
+ const signature = nacl.sign.detached(messageBytes, secretKey);
126
+ return bs58.encode(signature);
127
+ }
128
+
129
+ /**
130
+ * Step 4: Submit approval to Crossmint
131
+ */
132
+ async function submitApproval(
133
+ payerAddress: string,
134
+ transactionId: string,
135
+ signerAddress: string,
136
+ signature: string
137
+ ) {
138
+ const response = await fetch(
139
+ `${CROSSMINT_WALLETS_API}/wallets/${encodeURIComponent(payerAddress)}/transactions/${encodeURIComponent(transactionId)}/approvals`,
140
+ {
141
+ method: "POST",
142
+ headers: {
143
+ "Content-Type": "application/json",
144
+ "X-API-KEY": API_KEY,
145
+ },
146
+ body: JSON.stringify({
147
+ approvals: [
148
+ {
149
+ signer: `external-wallet:${signerAddress}`,
150
+ signature: signature,
151
+ },
152
+ ],
153
+ }),
154
+ }
155
+ );
156
+
157
+ if (!response.ok) {
158
+ const error = await response.text();
159
+ throw new Error(`Failed to submit approval: ${error}`);
160
+ }
161
+
162
+ return await response.json();
163
+ }
164
+
165
+ /**
166
+ * Step 5: Get transaction status from Crossmint Wallets API
167
+ * Poll until we get the on-chain txId
168
+ */
169
+ async function getTransactionStatus(payerAddress: string, transactionId: string) {
170
+ const response = await fetch(
171
+ `${CROSSMINT_WALLETS_API}/wallets/${encodeURIComponent(payerAddress)}/transactions/${encodeURIComponent(transactionId)}`,
172
+ {
173
+ method: "GET",
174
+ headers: {
175
+ "Content-Type": "application/json",
176
+ "X-API-KEY": API_KEY,
177
+ },
178
+ }
179
+ );
180
+
181
+ if (!response.ok) {
182
+ const error = await response.text();
183
+ throw new Error(`Failed to get transaction status: ${error}`);
184
+ }
185
+
186
+ return await response.json();
187
+ }
188
+
189
+ /**
190
+ * Step 5b: Poll for transaction to be broadcast and get on-chain txId
191
+ */
192
+ async function waitForTransactionBroadcast(
193
+ payerAddress: string,
194
+ transactionId: string,
195
+ timeoutMs: number = 30000
196
+ ): Promise<string | null> {
197
+ const startTime = Date.now();
198
+
199
+ while (Date.now() - startTime < timeoutMs) {
200
+ const txStatus = await getTransactionStatus(payerAddress, transactionId);
201
+ console.log("Transaction status:", txStatus.status);
202
+
203
+ // Check if transaction has been broadcast and we have the on-chain txId
204
+ if (txStatus.onChain?.txId) {
205
+ console.log("On-chain txId:", txStatus.onChain.txId);
206
+ return txStatus.onChain.txId;
207
+ }
208
+
209
+ // Also check for txId directly on the response
210
+ if (txStatus.txId) {
211
+ console.log("txId from response:", txStatus.txId);
212
+ return txStatus.txId;
213
+ }
214
+
215
+ // Check if status indicates completion
216
+ if (txStatus.status === "success" || txStatus.status === "completed") {
217
+ // Try to find txId in various places
218
+ const txId = txStatus.onChain?.txId || txStatus.txId || txStatus.hash;
219
+ if (txId) {
220
+ return txId;
221
+ }
222
+ }
223
+
224
+ // Check for failure
225
+ if (txStatus.status === "failed") {
226
+ throw new Error(`Transaction failed: ${JSON.stringify(txStatus)}`);
227
+ }
228
+
229
+ // Wait before polling again
230
+ await new Promise(resolve => setTimeout(resolve, 2000));
231
+ }
232
+
233
+ console.log("Timeout waiting for transaction broadcast");
234
+ return null;
235
+ }
236
+
237
+ /**
238
+ * Step 6: Submit payment to Crossmint Orders API (CRITICAL!)
239
+ * This notifies Crossmint that the payment transaction has been submitted
240
+ */
241
+ async function processPayment(orderId: string, txId: string, clientSecret?: string) {
242
+ const headers: Record<string, string> = {
243
+ "Content-Type": "application/json",
244
+ "X-API-KEY": API_KEY,
245
+ };
246
+ if (clientSecret) {
247
+ headers["Authorization"] = clientSecret;
248
+ }
249
+
250
+ const response = await fetch(`${CROSSMINT_ORDERS_API}/orders/${orderId}/payment`, {
251
+ method: "POST",
252
+ headers,
253
+ body: JSON.stringify({
254
+ type: "crypto-tx-id",
255
+ txId: txId,
256
+ }),
257
+ });
258
+
259
+ if (!response.ok) {
260
+ const error = await response.text();
261
+ throw new Error(`Failed to process payment: ${error}`);
262
+ }
263
+
264
+ return await response.json();
265
+ }
266
+
267
+ /**
268
+ * Step 7: Poll for Order Completion
269
+ */
270
+ async function pollOrderStatus(
271
+ orderId: string,
272
+ clientSecret: string,
273
+ timeoutMs: number = 60000
274
+ ): Promise<{ paymentStatus: string; deliveryStatus: string }> {
275
+ return new Promise((resolve) => {
276
+ const intervalId = setInterval(async () => {
277
+ try {
278
+ const response = await fetch(`${CROSSMINT_ORDERS_API}/orders/${orderId}`, {
279
+ method: "GET",
280
+ headers: {
281
+ "Content-Type": "application/json",
282
+ "X-API-KEY": API_KEY,
283
+ Authorization: clientSecret,
284
+ },
285
+ });
286
+
287
+ if (!response.ok) {
288
+ console.log("Failed to get order status:", await response.text());
289
+ return;
290
+ }
291
+
292
+ const orderStatus = await response.json();
293
+ const paymentStatus = orderStatus.payment?.status || "unknown";
294
+ const deliveryStatus = orderStatus.lineItems?.[0]?.delivery?.status || "pending";
295
+
296
+ console.log("Payment:", paymentStatus);
297
+ console.log("Delivery:", deliveryStatus);
298
+
299
+ if (paymentStatus === "completed") {
300
+ clearInterval(intervalId);
301
+ console.log("Payment completed! Amazon order is being fulfilled.");
302
+ resolve({ paymentStatus, deliveryStatus });
303
+ }
304
+ } catch (error) {
305
+ console.error("Error polling order status:", error);
306
+ }
307
+ }, 2500);
308
+
309
+ setTimeout(() => {
310
+ clearInterval(intervalId);
311
+ console.log("Timeout - check order manually");
312
+ resolve({ paymentStatus: "timeout", deliveryStatus: "unknown" });
313
+ }, timeoutMs);
314
+ });
315
+ }
316
+
317
+ /**
318
+ * Get order status (single call)
319
+ */
320
+ async function getOrderStatus(orderId: string, clientSecret?: string) {
321
+ const headers: Record<string, string> = {
322
+ "Content-Type": "application/json",
323
+ "X-API-KEY": API_KEY,
324
+ };
325
+ if (clientSecret) {
326
+ headers["Authorization"] = clientSecret;
327
+ }
328
+
329
+ const response = await fetch(`${CROSSMINT_ORDERS_API}/orders/${orderId}`, {
330
+ method: "GET",
331
+ headers,
332
+ });
333
+
334
+ if (!response.ok) {
335
+ const error = await response.text();
336
+ throw new Error(`Failed to get order: ${error}`);
337
+ }
338
+
339
+ return await response.json();
340
+ }
341
+
342
+ /**
343
+ * Get signer public key from private key
344
+ */
345
+ function getSignerAddress(privateKeyBase58: string): string {
346
+ const secretKey = bs58.decode(privateKeyBase58);
347
+ const keypair = Keypair.fromSecretKey(secretKey);
348
+ return keypair.publicKey.toBase58();
349
+ }
350
+
351
+ describe("Amazon Order E2E Test", () => {
352
+ describe.skipIf(!LIVE)("live: delegated signer purchase flow", () => {
353
+ it("creates order and pays with delegated signer approval", async () => {
354
+ const signerAddress = getSignerAddress(SIGNER_PRIVATE_KEY);
355
+
356
+ console.log("\n=== Starting Amazon Order E2E Test (Delegated Signer Flow) ===\n");
357
+ console.log("Smart Wallet (Payer):", PAYER_ADDRESS);
358
+ console.log("Delegated Signer:", signerAddress);
359
+ console.log("Amazon ASIN:", TEST_AMAZON_ASIN);
360
+
361
+ // Step 1: Create the order
362
+ console.log("\n--- Step 1: Creating Amazon order ---");
363
+ const { order, clientSecret } = await createAmazonOrder(
364
+ TEST_AMAZON_ASIN,
365
+ PAYER_ADDRESS,
366
+ "sol"
367
+ );
368
+
369
+ console.log("Order ID:", order.orderId);
370
+ console.log("Order Phase:", order.phase);
371
+ console.log("Client Secret:", clientSecret ? "✓ received" : "✗ missing");
372
+
373
+ expect(order.orderId).toBeDefined();
374
+ expect(order.phase).toBeDefined();
375
+
376
+ // Check if we have a serialized transaction
377
+ const serializedTransaction = order.payment?.preparation?.serializedTransaction;
378
+ console.log(
379
+ "Serialized Transaction:",
380
+ serializedTransaction ? `✓ received (${serializedTransaction.length} chars)` : "✗ missing"
381
+ );
382
+
383
+ if (!serializedTransaction) {
384
+ console.log("\nOrder created but no serialized transaction returned.");
385
+ console.log("Full order response:", JSON.stringify(order, null, 2));
386
+ return;
387
+ }
388
+
389
+ // Check for insufficient funds
390
+ if (order.payment?.failureReason?.code === "insufficient-funds") {
391
+ console.log("\n⚠️ Insufficient funds:", order.payment.failureReason.message);
392
+ console.log("Please fund the wallet and try again.");
393
+ return;
394
+ }
395
+
396
+ // Step 2: Create Crossmint transaction
397
+ console.log("\n--- Step 2: Creating Crossmint transaction ---");
398
+ const txResponse = await createCrossmintTransaction(
399
+ PAYER_ADDRESS,
400
+ serializedTransaction,
401
+ signerAddress
402
+ );
403
+
404
+ console.log("Transaction ID:", txResponse.id);
405
+ console.log("Transaction Status:", txResponse.status);
406
+
407
+ const messageToSign = txResponse.approvals?.pending?.[0]?.message;
408
+ console.log("Message to sign:", messageToSign ? `✓ received (${messageToSign.length} chars)` : "✗ missing");
409
+
410
+ if (!messageToSign) {
411
+ console.log("\nNo message to sign. Transaction response:", JSON.stringify(txResponse, null, 2));
412
+ return;
413
+ }
414
+
415
+ // Step 3: Sign the approval message
416
+ console.log("\n--- Step 3: Signing approval message ---");
417
+ const signature = signApprovalMessage(messageToSign, SIGNER_PRIVATE_KEY);
418
+ console.log("Signature:", `✓ generated (${signature.length} chars)`);
419
+
420
+ // Step 4: Submit approval
421
+ console.log("\n--- Step 4: Submitting approval to Crossmint ---");
422
+ const approvalResponse = await submitApproval(
423
+ PAYER_ADDRESS,
424
+ txResponse.id,
425
+ signerAddress,
426
+ signature
427
+ );
428
+
429
+ console.log("Approval Response:", JSON.stringify(approvalResponse, null, 2));
430
+
431
+ // Step 5: Wait for transaction to be broadcast and get on-chain txId
432
+ console.log("\n--- Step 5: Waiting for transaction broadcast ---");
433
+ const onChainTxId = await waitForTransactionBroadcast(PAYER_ADDRESS, txResponse.id, 30000);
434
+
435
+ if (!onChainTxId) {
436
+ console.log("Could not get on-chain txId. Checking approval response for txId...");
437
+ // Try to get txId from approval response
438
+ const fallbackTxId = approvalResponse.onChain?.txId || approvalResponse.txId || approvalResponse.hash;
439
+ if (!fallbackTxId) {
440
+ console.log("No txId available. Cannot call /payment endpoint.");
441
+ console.log("Full approval response:", JSON.stringify(approvalResponse, null, 2));
442
+ return;
443
+ }
444
+ }
445
+
446
+ const txIdToSubmit = onChainTxId || approvalResponse.onChain?.txId || approvalResponse.txId;
447
+
448
+ // Step 6: Submit payment to Crossmint (CRITICAL!)
449
+ console.log("\n--- Step 6: Submitting payment to Crossmint /payment endpoint ---");
450
+ console.log("Submitting txId:", txIdToSubmit);
451
+
452
+ const paymentResponse = await processPayment(order.orderId, txIdToSubmit!, clientSecret);
453
+ console.log("Payment Response:", JSON.stringify(paymentResponse, null, 2));
454
+
455
+ // Step 7: Poll for order completion
456
+ console.log("\n--- Step 7: Polling for order completion ---");
457
+ const { paymentStatus, deliveryStatus } = await pollOrderStatus(
458
+ order.orderId,
459
+ clientSecret,
460
+ 60000
461
+ );
462
+
463
+ console.log("\n=== Final Status ===");
464
+ console.log("Payment Status:", paymentStatus);
465
+ console.log("Delivery Status:", deliveryStatus);
466
+
467
+ // Payment should eventually complete
468
+ expect(["completed", "timeout", "crypto-payer-insufficient-funds"]).toContain(paymentStatus);
469
+ }, 180000); // 3 minute timeout
470
+
471
+ it("creates order only (inspect response)", async () => {
472
+ console.log("\n=== Create Order Only Test ===\n");
473
+
474
+ const { order, clientSecret } = await createAmazonOrder(
475
+ TEST_AMAZON_ASIN,
476
+ PAYER_ADDRESS,
477
+ "sol"
478
+ );
479
+
480
+ console.log("Order ID:", order.orderId);
481
+ console.log("Order Phase:", order.phase);
482
+ console.log("Quote:", JSON.stringify(order.quote, null, 2));
483
+ console.log("Payment:", JSON.stringify(order.payment, null, 2));
484
+
485
+ expect(order.orderId).toBeDefined();
486
+
487
+ console.log("\n--- Full Response ---");
488
+ console.log(JSON.stringify({ order, clientSecret }, null, 2));
489
+ }, 30000);
490
+ });
491
+
492
+ describe("unit tests", () => {
493
+ it("validates keypair creation from base58", () => {
494
+ const testKeypair = Keypair.generate();
495
+ const secretKeyBase58 = bs58.encode(testKeypair.secretKey);
496
+ const recreatedKeypair = Keypair.fromSecretKey(bs58.decode(secretKeyBase58));
497
+ expect(recreatedKeypair.publicKey.toBase58()).toBe(testKeypair.publicKey.toBase58());
498
+ });
499
+
500
+ it("validates signature generation", () => {
501
+ const testKeypair = Keypair.generate();
502
+ const secretKeyBase58 = bs58.encode(testKeypair.secretKey);
503
+ const testMessage = bs58.encode(Buffer.from("test message"));
504
+
505
+ const signature = signApprovalMessage(testMessage, secretKeyBase58);
506
+ expect(signature).toBeDefined();
507
+ expect(signature.length).toBeGreaterThan(0);
508
+
509
+ // Verify signature
510
+ const sigBytes = bs58.decode(signature);
511
+ const msgBytes = bs58.decode(testMessage);
512
+ const isValid = nacl.sign.detached.verify(msgBytes, sigBytes, testKeypair.publicKey.toBytes());
513
+ expect(isValid).toBe(true);
514
+ });
515
+ });
516
+ });
517
+
518
+ export {
519
+ createAmazonOrder,
520
+ createCrossmintTransaction,
521
+ signApprovalMessage,
522
+ submitApproval,
523
+ waitForTransactionBroadcast,
524
+ processPayment,
525
+ pollOrderStatus,
526
+ getOrderStatus,
527
+ getTransactionStatus,
528
+ };
package/src/api.ts CHANGED
@@ -17,6 +17,7 @@ export type CrossmintTransaction = {
17
17
  id: string;
18
18
  status: string;
19
19
  hash?: string;
20
+ txId?: string; // Top-level txId from API response
20
21
  explorerLink?: string;
21
22
  onChain?: {
22
23
  status?: string;
@@ -109,6 +110,7 @@ export async function getTransactionStatus(
109
110
  id: data.id,
110
111
  status: data.status,
111
112
  hash: data.onChain?.txId || data.hash,
113
+ txId: data.txId, // Top-level txId from API response
112
114
  explorerLink: data.onChain?.txId
113
115
  ? `https://explorer.solana.com/tx/${data.onChain.txId}?cluster=devnet`
114
116
  : undefined,
@@ -319,14 +321,19 @@ export type TransactionResponse = {
319
321
  // Headless Checkout API Functions (Delegated Signer Flow)
320
322
  // ============================================================================
321
323
 
324
+ export type CreateOrderResponse = {
325
+ order: CrossmintOrder;
326
+ clientSecret: string;
327
+ };
328
+
322
329
  /**
323
330
  * Step 1: Create an order for purchasing products (e.g., from Amazon)
324
- * Returns order with serializedTransaction to use in step 2
331
+ * Returns order with serializedTransaction and clientSecret for subsequent API calls
325
332
  */
326
333
  export async function createOrder(
327
334
  config: CrossmintApiConfig,
328
335
  request: CreateOrderRequest,
329
- ): Promise<CrossmintOrder> {
336
+ ): Promise<CreateOrderResponse> {
330
337
  const response = await fetchCrossmint(config, "/2022-06-09/orders", {
331
338
  method: "POST",
332
339
  body: JSON.stringify(request),
@@ -337,9 +344,12 @@ export async function createOrder(
337
344
  throw new Error(`Failed to create order: ${error}`);
338
345
  }
339
346
 
340
- // API returns { clientSecret, order } - extract the order
347
+ // API returns { clientSecret, order }
341
348
  const data = await response.json();
342
- return data.order;
349
+ return {
350
+ order: data.order,
351
+ clientSecret: data.clientSecret,
352
+ };
343
353
  }
344
354
 
345
355
  /**
@@ -414,11 +424,17 @@ export async function submitApproval(
414
424
  export async function getOrder(
415
425
  config: CrossmintApiConfig,
416
426
  orderId: string,
427
+ clientSecret?: string,
417
428
  ): Promise<CrossmintOrder> {
429
+ const headers: Record<string, string> = {};
430
+ if (clientSecret) {
431
+ headers["Authorization"] = clientSecret;
432
+ }
433
+
418
434
  const response = await fetchCrossmint(
419
435
  config,
420
436
  `/2022-06-09/orders/${encodeURIComponent(orderId)}`,
421
- { method: "GET" },
437
+ { method: "GET", headers },
422
438
  );
423
439
 
424
440
  if (!response.ok) {
@@ -429,20 +445,124 @@ export async function getOrder(
429
445
  return response.json();
430
446
  }
431
447
 
448
+ /**
449
+ * Step 5: Wait for transaction to be broadcast and get on-chain txId
450
+ * Polls the transaction status until we get the on-chain transaction ID
451
+ */
452
+ export async function waitForTransactionBroadcast(
453
+ config: CrossmintApiConfig,
454
+ walletAddress: string,
455
+ transactionId: string,
456
+ timeoutMs: number = 30000,
457
+ pollIntervalMs: number = 2000,
458
+ ): Promise<string | null> {
459
+ const startTime = Date.now();
460
+
461
+ while (Date.now() - startTime < timeoutMs) {
462
+ const txStatus = await getTransactionStatus(config, walletAddress, transactionId);
463
+
464
+ // Check if transaction has been broadcast and we have the on-chain txId
465
+ if (txStatus.onChain?.txId) {
466
+ return txStatus.onChain.txId;
467
+ }
468
+
469
+ // Also check for txId directly on the response
470
+ if (txStatus.txId) {
471
+ return txStatus.txId;
472
+ }
473
+
474
+ // Check if status indicates completion
475
+ if (txStatus.status === "success" || txStatus.status === "completed") {
476
+ // Try to find txId in various places
477
+ const txId = txStatus.onChain?.txId || txStatus.txId || txStatus.hash;
478
+ if (txId) {
479
+ return txId;
480
+ }
481
+ }
482
+
483
+ // Check for failure
484
+ if (txStatus.status === "failed") {
485
+ throw new Error(`Transaction failed: ${JSON.stringify(txStatus)}`);
486
+ }
487
+
488
+ // Wait before polling again
489
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
490
+ }
491
+
492
+ return null;
493
+ }
494
+
495
+ /**
496
+ * Step 6: Submit payment confirmation to Crossmint Orders API (CRITICAL!)
497
+ * This notifies Crossmint that the payment transaction has been submitted on-chain
498
+ */
499
+ export async function submitPaymentConfirmation(
500
+ config: CrossmintApiConfig,
501
+ orderId: string,
502
+ onChainTxId: string,
503
+ clientSecret?: string,
504
+ ): Promise<CrossmintOrder> {
505
+ const headers: Record<string, string> = {};
506
+ if (clientSecret) {
507
+ headers["Authorization"] = clientSecret;
508
+ }
509
+
510
+ const response = await fetchCrossmint(
511
+ config,
512
+ `/2022-06-09/orders/${encodeURIComponent(orderId)}/payment`,
513
+ {
514
+ method: "POST",
515
+ headers,
516
+ body: JSON.stringify({
517
+ type: "crypto-tx-id",
518
+ txId: onChainTxId,
519
+ }),
520
+ },
521
+ );
522
+
523
+ if (!response.ok) {
524
+ const error = await response.text();
525
+ throw new Error(`Failed to submit payment confirmation: ${error}`);
526
+ }
527
+
528
+ return response.json();
529
+ }
530
+
531
+ export type PurchaseResult = {
532
+ order: CrossmintOrder;
533
+ transactionId: string;
534
+ onChainTxId: string;
535
+ explorerLink: string;
536
+ };
537
+
432
538
  /**
433
539
  * Complete Amazon purchase flow with delegated signer
434
- * Combines all 3 API calls + local signing
540
+ *
541
+ * Steps:
542
+ * 1. Create order → returns serializedTransaction and clientSecret
543
+ * 2a. Create Crossmint transaction with serialized tx
544
+ * 2b. Sign the approval message locally with ed25519
545
+ * 2c. Submit approval to Crossmint
546
+ * 5. Wait for transaction to be broadcast → get on-chain txId
547
+ * 6. Submit txId to /payment endpoint (CRITICAL!)
548
+ *
549
+ * Returns the final order state after payment confirmation.
435
550
  */
436
551
  export async function purchaseProduct(
437
552
  config: CrossmintApiConfig,
438
553
  request: CreateOrderRequest,
439
554
  keypair: Keypair,
440
- ): Promise<{ order: CrossmintOrder; transactionId: string }> {
555
+ ): Promise<PurchaseResult> {
441
556
  // Step 1: Create order
442
- const order = await createOrder(config, request);
557
+ const { order, clientSecret } = await createOrder(config, request);
443
558
 
444
559
  const serializedTransaction = order.payment?.preparation?.serializedTransaction;
445
560
  if (!serializedTransaction) {
561
+ // Check for insufficient funds
562
+ const failureReason = (order.payment as { failureReason?: { code: string; message: string } })?.failureReason;
563
+ if (failureReason?.code === "insufficient-funds") {
564
+ throw new Error(`Insufficient funds: ${failureReason.message}`);
565
+ }
446
566
  throw new Error(
447
567
  `Order created but no serialized transaction returned. Payment status: ${order.payment?.status || "unknown"}`,
448
568
  );
@@ -461,14 +581,14 @@ export async function purchaseProduct(
461
581
  throw new Error("Transaction created but no message to sign");
462
582
  }
463
583
 
464
- // Step 2 (local): Sign the message with ed25519
465
- // Message is base58 encoded (Solana standard) - same as transfers
584
+ // Step 2b (local): Sign the message with ed25519
585
+ // Message is base58 encoded (Solana standard)
466
586
  const messageBytes = bs58.decode(messageToSign);
467
587
  const nacl = (await import("tweetnacl")).default;
468
588
  const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
469
589
  const signatureBase58 = bs58.encode(signature);
470
590
 
471
- // Step 2b: Submit approval
591
+ // Step 2c: Submit approval
472
592
  await submitApproval(
473
593
  config,
474
594
  request.payment.payerAddress,
@@ -477,7 +597,37 @@ export async function purchaseProduct(
477
597
  signatureBase58,
478
598
  );
479
599
 
480
- return { order, transactionId: txResponse.id };
600
+ // Step 5: Wait for transaction to be broadcast and get on-chain txId
601
+ const onChainTxId = await waitForTransactionBroadcast(
602
+ config,
603
+ request.payment.payerAddress,
604
+ txResponse.id,
605
+ 30000, // 30 second timeout
606
+ );
607
+
608
+ if (!onChainTxId) {
609
+ throw new Error("Timeout waiting for transaction to be broadcast on-chain");
610
+ }
611
+
612
+ // Step 6: Submit payment confirmation (CRITICAL!)
613
+ const updatedOrder = await submitPaymentConfirmation(
614
+ config,
615
+ order.orderId,
616
+ onChainTxId,
617
+ clientSecret,
618
+ );
619
+
620
+ const explorerLink =
621
+ config.environment === "staging"
622
+ ? `https://explorer.solana.com/tx/${onChainTxId}?cluster=devnet`
623
+ : `https://explorer.solana.com/tx/${onChainTxId}`;
624
+
625
+ return {
626
+ order: updatedOrder,
627
+ transactionId: txResponse.id,
628
+ onChainTxId,
629
+ explorerLink,
630
+ };
481
631
  }
482
632
 
483
633
  /**
package/src/tools.ts CHANGED
@@ -675,18 +675,22 @@ export function createCrossmintBuyTool(_api: OpenClawPluginApi, _config: Crossmi
675
675
  const productTitle = result.order.lineItems?.[0]?.metadata?.title || "Product";
676
676
  const totalPrice = result.order.quote?.totalPrice;
677
677
  const priceText = totalPrice ? `${totalPrice.amount} ${totalPrice.currency}` : "See order details";
678
+ const paymentStatus = result.order.payment?.status || result.order.phase;
678
679
 
679
680
  return {
680
681
  content: [
681
682
  {
682
683
  type: "text",
683
- text: `Purchase initiated!\n\nProduct: ${productTitle}\nPrice: ${priceText}\nOrder ID: ${result.order.orderId}\nStatus: ${result.order.phase}\n\nShipping to:\n${recipientName}\n${addressLine1}${addressLine2 ? `\n${addressLine2}` : ""}\n${city}${state ? `, ${state}` : ""} ${postalCode}\n${country}\n\nUse crossmint_order_status to check delivery status.`,
684
+ text: `✅ Purchase complete!\n\nProduct: ${productTitle}\nPrice: ${priceText}\nOrder ID: ${result.order.orderId}\nPayment: ${paymentStatus}\n\nTransaction: ${result.explorerLink}\n\nShipping to:\n${recipientName}\n${addressLine1}${addressLine2 ? `\n${addressLine2}` : ""}\n${city}${state ? `, ${state}` : ""} ${postalCode}\n${country}\n\nUse crossmint_order_status to check delivery status.`,
684
685
  },
685
686
  ],
686
687
  details: {
687
688
  orderId: result.order.orderId,
688
689
  transactionId: result.transactionId,
690
+ onChainTxId: result.onChainTxId,
691
+ explorerLink: result.explorerLink,
689
692
  phase: result.order.phase,
693
+ paymentStatus,
690
694
  productLocator,
691
695
  totalPrice,
692
696
  recipient: orderRequest.recipient,