@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 +86 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/crossmint/SKILL.md +198 -22
- package/src/amazon-order.test.ts +528 -0
- package/src/api.ts +162 -12
- package/src/tools.ts +5 -1
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 (
|
|
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
|
|
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
|
-
│
|
|
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
|
|
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 |
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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. **
|
|
272
|
-
2. **
|
|
273
|
-
3. **
|
|
274
|
-
4. **
|
|
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
|
|
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<
|
|
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 }
|
|
347
|
+
// API returns { clientSecret, order }
|
|
341
348
|
const data = await response.json();
|
|
342
|
-
return
|
|
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
|
-
*
|
|
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<
|
|
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
|
|
465
|
-
// Message is base58 encoded (Solana standard)
|
|
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
|
|
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
|
-
|
|
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:
|
|
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,
|