@crossmint/openclaw-wallet 0.2.2
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 +157 -0
- package/index.ts +72 -0
- package/openclaw.plugin.json +12 -0
- package/package.json +42 -0
- package/skills/crossmint/SKILL.md +274 -0
- package/src/api.test.ts +211 -0
- package/src/api.ts +495 -0
- package/src/config.ts +11 -0
- package/src/tools.ts +787 -0
- package/src/wallet.test.ts +291 -0
- package/src/wallet.ts +154 -0
package/src/api.test.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { Keypair } from "@solana/web3.js";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
buildAmazonProductLocator,
|
|
5
|
+
buildDelegationUrl,
|
|
6
|
+
createOrder,
|
|
7
|
+
getOrder,
|
|
8
|
+
type CreateOrderRequest,
|
|
9
|
+
type CrossmintApiConfig,
|
|
10
|
+
} from "./api.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Live integration tests for Crossmint Amazon purchase API.
|
|
14
|
+
*
|
|
15
|
+
* Run with: CROSSMINT_API_KEY=your-key pnpm test extensions/crossmint/src/api.test.ts
|
|
16
|
+
*
|
|
17
|
+
* These tests make real API calls to Crossmint staging (devnet).
|
|
18
|
+
* They require a valid client-side API key with orders.create scope.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const LIVE = process.env.CROSSMINT_API_KEY || process.env.LIVE;
|
|
22
|
+
|
|
23
|
+
describe("crossmint api", () => {
|
|
24
|
+
describe("buildDelegationUrl", () => {
|
|
25
|
+
it("builds URL with public key parameter", () => {
|
|
26
|
+
const url = buildDelegationUrl("https://example.com/delegate", "ABC123");
|
|
27
|
+
expect(url).toBe("https://example.com/delegate/configure?pubkey=ABC123");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("removes trailing slashes from base URL", () => {
|
|
31
|
+
const url = buildDelegationUrl("https://example.com/delegate///", "ABC123");
|
|
32
|
+
expect(url).toBe("https://example.com/delegate/configure?pubkey=ABC123");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("buildAmazonProductLocator", () => {
|
|
37
|
+
it("returns existing amazon: locator unchanged", () => {
|
|
38
|
+
const result = buildAmazonProductLocator("amazon:B00O79SKV6");
|
|
39
|
+
expect(result).toBe("amazon:B00O79SKV6");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("wraps Amazon URL with amazon: prefix", () => {
|
|
43
|
+
const result = buildAmazonProductLocator("https://www.amazon.com/dp/B00O79SKV6");
|
|
44
|
+
expect(result).toBe("amazon:https://www.amazon.com/dp/B00O79SKV6");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("wraps ASIN with amazon: prefix", () => {
|
|
48
|
+
const result = buildAmazonProductLocator("B00O79SKV6");
|
|
49
|
+
expect(result).toBe("amazon:B00O79SKV6");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe.skipIf(!LIVE)("live: createOrder", () => {
|
|
54
|
+
const config: CrossmintApiConfig = {
|
|
55
|
+
apiKey: process.env.CROSSMINT_API_KEY!,
|
|
56
|
+
environment: "staging",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Generate a test keypair for the payer address
|
|
60
|
+
const testKeypair = Keypair.generate();
|
|
61
|
+
|
|
62
|
+
it("creates an order for an Amazon product", async () => {
|
|
63
|
+
const request: CreateOrderRequest = {
|
|
64
|
+
recipient: {
|
|
65
|
+
email: "test@example.com",
|
|
66
|
+
physicalAddress: {
|
|
67
|
+
name: "Test User",
|
|
68
|
+
line1: "123 Test Street",
|
|
69
|
+
city: "San Francisco",
|
|
70
|
+
state: "CA",
|
|
71
|
+
postalCode: "94102",
|
|
72
|
+
country: "US",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
payment: {
|
|
76
|
+
receiptEmail: "test@example.com",
|
|
77
|
+
method: "solana",
|
|
78
|
+
currency: "usdc",
|
|
79
|
+
payerAddress: testKeypair.publicKey.toBase58(),
|
|
80
|
+
},
|
|
81
|
+
lineItems: [
|
|
82
|
+
{
|
|
83
|
+
// Amazon Basics product - commonly available
|
|
84
|
+
productLocator: "amazon:B00O79SKV6",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const order = await createOrder(config, request);
|
|
90
|
+
|
|
91
|
+
console.log("Created order:", JSON.stringify(order, null, 2));
|
|
92
|
+
|
|
93
|
+
// Verify order was created
|
|
94
|
+
expect(order.orderId).toBeDefined();
|
|
95
|
+
expect(order.phase).toBeDefined();
|
|
96
|
+
|
|
97
|
+
// The order should have a quote or be in quote phase
|
|
98
|
+
expect(["quote", "payment"]).toContain(order.phase);
|
|
99
|
+
|
|
100
|
+
// For headless checkout with delegated signer, we expect serializedTransaction
|
|
101
|
+
// Note: This may not always be present depending on quote status
|
|
102
|
+
if (order.phase === "payment") {
|
|
103
|
+
expect(order.payment?.preparation?.serializedTransaction).toBeDefined();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("creates an order with SOL currency", async () => {
|
|
108
|
+
const request: CreateOrderRequest = {
|
|
109
|
+
recipient: {
|
|
110
|
+
email: "test@example.com",
|
|
111
|
+
physicalAddress: {
|
|
112
|
+
name: "Test User",
|
|
113
|
+
line1: "456 Test Avenue",
|
|
114
|
+
city: "Los Angeles",
|
|
115
|
+
state: "CA",
|
|
116
|
+
postalCode: "90001",
|
|
117
|
+
country: "US",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
payment: {
|
|
121
|
+
receiptEmail: "test@example.com",
|
|
122
|
+
method: "solana",
|
|
123
|
+
currency: "sol",
|
|
124
|
+
payerAddress: testKeypair.publicKey.toBase58(),
|
|
125
|
+
},
|
|
126
|
+
lineItems: [
|
|
127
|
+
{
|
|
128
|
+
productLocator: "amazon:B00O79SKV6",
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const order = await createOrder(config, request);
|
|
134
|
+
|
|
135
|
+
console.log("Created SOL order:", JSON.stringify(order, null, 2));
|
|
136
|
+
|
|
137
|
+
expect(order.orderId).toBeDefined();
|
|
138
|
+
expect(order.phase).toBeDefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("retrieves order status after creation", async () => {
|
|
142
|
+
// First create an order
|
|
143
|
+
const request: CreateOrderRequest = {
|
|
144
|
+
recipient: {
|
|
145
|
+
email: "test@example.com",
|
|
146
|
+
physicalAddress: {
|
|
147
|
+
name: "Test User",
|
|
148
|
+
line1: "789 Test Blvd",
|
|
149
|
+
city: "New York",
|
|
150
|
+
state: "NY",
|
|
151
|
+
postalCode: "10001",
|
|
152
|
+
country: "US",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
payment: {
|
|
156
|
+
receiptEmail: "test@example.com",
|
|
157
|
+
method: "solana",
|
|
158
|
+
currency: "usdc",
|
|
159
|
+
payerAddress: testKeypair.publicKey.toBase58(),
|
|
160
|
+
},
|
|
161
|
+
lineItems: [
|
|
162
|
+
{
|
|
163
|
+
productLocator: "amazon:B00O79SKV6",
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const createdOrder = await createOrder(config, request);
|
|
169
|
+
expect(createdOrder.orderId).toBeDefined();
|
|
170
|
+
|
|
171
|
+
// Now retrieve the order
|
|
172
|
+
const retrievedOrder = await getOrder(config, createdOrder.orderId);
|
|
173
|
+
|
|
174
|
+
console.log("Retrieved order:", JSON.stringify(retrievedOrder, null, 2));
|
|
175
|
+
|
|
176
|
+
expect(retrievedOrder.orderId).toBe(createdOrder.orderId);
|
|
177
|
+
expect(retrievedOrder.phase).toBeDefined();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("handles invalid product gracefully", async () => {
|
|
181
|
+
const request: CreateOrderRequest = {
|
|
182
|
+
recipient: {
|
|
183
|
+
email: "test@example.com",
|
|
184
|
+
physicalAddress: {
|
|
185
|
+
name: "Test User",
|
|
186
|
+
line1: "123 Test Street",
|
|
187
|
+
city: "San Francisco",
|
|
188
|
+
state: "CA",
|
|
189
|
+
postalCode: "94102",
|
|
190
|
+
country: "US",
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
payment: {
|
|
194
|
+
receiptEmail: "test@example.com",
|
|
195
|
+
method: "solana",
|
|
196
|
+
currency: "usdc",
|
|
197
|
+
payerAddress: testKeypair.publicKey.toBase58(),
|
|
198
|
+
},
|
|
199
|
+
lineItems: [
|
|
200
|
+
{
|
|
201
|
+
// Invalid ASIN
|
|
202
|
+
productLocator: "amazon:INVALID123",
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Should throw an error for invalid product
|
|
208
|
+
await expect(createOrder(config, request)).rejects.toThrow();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import type { Keypair } from "@solana/web3.js";
|
|
2
|
+
import bs58 from "bs58";
|
|
3
|
+
|
|
4
|
+
export type CrossmintApiConfig = {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
environment: "staging"; // Only staging (Solana devnet) supported for now
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type CrossmintBalance = {
|
|
10
|
+
token: string;
|
|
11
|
+
amount: string;
|
|
12
|
+
decimals: number;
|
|
13
|
+
rawAmount?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type CrossmintTransaction = {
|
|
17
|
+
id: string;
|
|
18
|
+
status: string;
|
|
19
|
+
hash?: string;
|
|
20
|
+
explorerLink?: string;
|
|
21
|
+
onChain?: {
|
|
22
|
+
status?: string;
|
|
23
|
+
chain?: string;
|
|
24
|
+
txId?: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Only staging (Solana devnet) is supported for now
|
|
29
|
+
function getBaseUrl(_env: "staging"): string {
|
|
30
|
+
// Production URL reserved for future use: https://www.crossmint.com/api
|
|
31
|
+
return "https://staging.crossmint.com/api";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function fetchCrossmint(
|
|
35
|
+
config: CrossmintApiConfig,
|
|
36
|
+
endpoint: string,
|
|
37
|
+
options: RequestInit = {},
|
|
38
|
+
): Promise<Response> {
|
|
39
|
+
const baseUrl = getBaseUrl(config.environment);
|
|
40
|
+
const url = `${baseUrl}${endpoint}`;
|
|
41
|
+
|
|
42
|
+
const headers: Record<string, string> = {
|
|
43
|
+
"X-API-KEY": config.apiKey,
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
...(options.headers as Record<string, string>),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const response = await fetch(url, {
|
|
49
|
+
...options,
|
|
50
|
+
headers,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return response;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function getWalletBalance(
|
|
57
|
+
config: CrossmintApiConfig,
|
|
58
|
+
walletAddress: string,
|
|
59
|
+
): Promise<CrossmintBalance[]> {
|
|
60
|
+
const response = await fetchCrossmint(
|
|
61
|
+
config,
|
|
62
|
+
`/2025-06-09/wallets/${encodeURIComponent(walletAddress)}/balances?tokens=sol,usdc&chains=solana`,
|
|
63
|
+
{ method: "GET" },
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const error = await response.text();
|
|
68
|
+
throw new Error(`Failed to get balance: ${error}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const data = await response.json();
|
|
72
|
+
|
|
73
|
+
const balances: CrossmintBalance[] = [];
|
|
74
|
+
|
|
75
|
+
// Parse the response array
|
|
76
|
+
if (Array.isArray(data)) {
|
|
77
|
+
for (const token of data) {
|
|
78
|
+
balances.push({
|
|
79
|
+
token: token.symbol || "Unknown",
|
|
80
|
+
amount: token.amount || "0",
|
|
81
|
+
decimals: token.decimals || 9,
|
|
82
|
+
rawAmount: token.rawAmount,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return balances;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function getTransactionStatus(
|
|
91
|
+
config: CrossmintApiConfig,
|
|
92
|
+
walletAddress: string,
|
|
93
|
+
transactionId: string,
|
|
94
|
+
): Promise<CrossmintTransaction> {
|
|
95
|
+
const response = await fetchCrossmint(
|
|
96
|
+
config,
|
|
97
|
+
`/2025-06-09/wallets/${encodeURIComponent(walletAddress)}/transactions/${encodeURIComponent(transactionId)}`,
|
|
98
|
+
{ method: "GET" },
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const error = await response.text();
|
|
103
|
+
throw new Error(`Failed to get transaction status: ${error}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
id: data.id,
|
|
110
|
+
status: data.status,
|
|
111
|
+
hash: data.onChain?.txId || data.hash,
|
|
112
|
+
explorerLink: data.onChain?.txId
|
|
113
|
+
? `https://explorer.solana.com/tx/${data.onChain.txId}?cluster=devnet`
|
|
114
|
+
: undefined,
|
|
115
|
+
onChain: data.onChain,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function waitForTransaction(
|
|
120
|
+
config: CrossmintApiConfig,
|
|
121
|
+
walletAddress: string,
|
|
122
|
+
transactionId: string,
|
|
123
|
+
timeoutMs: number = 60000,
|
|
124
|
+
pollIntervalMs: number = 2000,
|
|
125
|
+
): Promise<CrossmintTransaction> {
|
|
126
|
+
const startTime = Date.now();
|
|
127
|
+
|
|
128
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
129
|
+
const tx = await getTransactionStatus(config, walletAddress, transactionId);
|
|
130
|
+
|
|
131
|
+
// Terminal states
|
|
132
|
+
if (tx.status === "success" || tx.status === "completed") {
|
|
133
|
+
return tx;
|
|
134
|
+
}
|
|
135
|
+
if (tx.status === "failed" || tx.status === "rejected" || tx.status === "cancelled") {
|
|
136
|
+
return tx;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Still pending, wait and retry
|
|
140
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Timeout - return last known status
|
|
144
|
+
return getTransactionStatus(config, walletAddress, transactionId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function createTransfer(
|
|
148
|
+
config: CrossmintApiConfig,
|
|
149
|
+
walletAddress: string,
|
|
150
|
+
recipient: string,
|
|
151
|
+
token: string,
|
|
152
|
+
amount: string,
|
|
153
|
+
keypair: Keypair,
|
|
154
|
+
): Promise<CrossmintTransaction> {
|
|
155
|
+
// Token locator format for Solana: solana:tokenAddress or solana:sol for native
|
|
156
|
+
const tokenLocator = token.toLowerCase() === "sol" ? "solana:sol" : `solana:${token}`;
|
|
157
|
+
|
|
158
|
+
// Step 1: Create the transfer transaction
|
|
159
|
+
const createResponse = await fetchCrossmint(
|
|
160
|
+
config,
|
|
161
|
+
`/2025-06-09/wallets/${encodeURIComponent(walletAddress)}/tokens/${encodeURIComponent(tokenLocator)}/transfers`,
|
|
162
|
+
{
|
|
163
|
+
method: "POST",
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
recipient,
|
|
166
|
+
amount,
|
|
167
|
+
signer: `external-wallet:${keypair.publicKey.toBase58()}`,
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (!createResponse.ok) {
|
|
173
|
+
const error = await createResponse.text();
|
|
174
|
+
throw new Error(`Failed to create transfer: ${error}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const txData = await createResponse.json();
|
|
178
|
+
|
|
179
|
+
// Step 2: If approval is needed, sign and submit
|
|
180
|
+
if (txData.status === "awaiting-approval" && txData.approvals?.pending?.length > 0) {
|
|
181
|
+
const approval = txData.approvals.pending[0];
|
|
182
|
+
if (approval?.message) {
|
|
183
|
+
// Sign the approval message using ed25519
|
|
184
|
+
// Message is base58 encoded (Solana standard), not hex
|
|
185
|
+
const messageBytes = bs58.decode(approval.message);
|
|
186
|
+
const nacl = (await import("tweetnacl")).default;
|
|
187
|
+
const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
|
|
188
|
+
const signatureBase58 = bs58.encode(signature);
|
|
189
|
+
|
|
190
|
+
// Submit approval
|
|
191
|
+
const approveResponse = await fetchCrossmint(
|
|
192
|
+
config,
|
|
193
|
+
`/2025-06-09/wallets/${encodeURIComponent(walletAddress)}/transactions/${txData.id}/approvals`,
|
|
194
|
+
{
|
|
195
|
+
method: "POST",
|
|
196
|
+
body: JSON.stringify({
|
|
197
|
+
approvals: [
|
|
198
|
+
{
|
|
199
|
+
signer: `external-wallet:${keypair.publicKey.toBase58()}`,
|
|
200
|
+
signature: signatureBase58,
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
}),
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (!approveResponse.ok) {
|
|
208
|
+
const error = await approveResponse.text();
|
|
209
|
+
throw new Error(`Failed to approve transfer: ${error}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const approvedData = await approveResponse.json();
|
|
213
|
+
return {
|
|
214
|
+
id: approvedData.id || txData.id,
|
|
215
|
+
status: approvedData.status || "pending",
|
|
216
|
+
hash: approvedData.hash,
|
|
217
|
+
explorerLink: approvedData.explorerLink,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
id: txData.id,
|
|
224
|
+
status: txData.status,
|
|
225
|
+
hash: txData.hash,
|
|
226
|
+
explorerLink: txData.explorerLink,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function buildDelegationUrl(
|
|
231
|
+
delegationBaseUrl: string,
|
|
232
|
+
publicAddress: string,
|
|
233
|
+
): string {
|
|
234
|
+
// URL format: {baseUrl}/configure?pubkey={publicKey}
|
|
235
|
+
const baseUrl = delegationBaseUrl.replace(/\/+$/, ""); // Remove trailing slashes
|
|
236
|
+
return `${baseUrl}/configure?pubkey=${publicAddress}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ============================================================================
|
|
240
|
+
// Headless Checkout Types (Amazon purchases with delegated signer)
|
|
241
|
+
// ============================================================================
|
|
242
|
+
|
|
243
|
+
export type OrderRecipient = {
|
|
244
|
+
email: string;
|
|
245
|
+
physicalAddress: {
|
|
246
|
+
name: string;
|
|
247
|
+
line1: string;
|
|
248
|
+
line2?: string;
|
|
249
|
+
city: string;
|
|
250
|
+
state?: string;
|
|
251
|
+
postalCode: string;
|
|
252
|
+
country: string; // ISO 3166-1 alpha-2 (e.g., "US")
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export type OrderLineItem = {
|
|
257
|
+
productLocator: string; // e.g., "amazon:B00O79SKV6"
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export type OrderPayment = {
|
|
261
|
+
receiptEmail: string;
|
|
262
|
+
method: "solana";
|
|
263
|
+
currency: string; // e.g., "usdc"
|
|
264
|
+
payerAddress: string; // Smart wallet address that pays
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export type CreateOrderRequest = {
|
|
268
|
+
recipient: OrderRecipient;
|
|
269
|
+
payment: OrderPayment;
|
|
270
|
+
lineItems: OrderLineItem[];
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export type OrderPhase = "quote" | "payment" | "delivery" | "completed" | "failed";
|
|
274
|
+
|
|
275
|
+
export type CrossmintOrder = {
|
|
276
|
+
orderId: string;
|
|
277
|
+
phase: OrderPhase;
|
|
278
|
+
quote?: {
|
|
279
|
+
status: string;
|
|
280
|
+
totalPrice?: { amount: string; currency: string };
|
|
281
|
+
};
|
|
282
|
+
payment?: {
|
|
283
|
+
status: string;
|
|
284
|
+
preparation?: {
|
|
285
|
+
serializedTransaction?: string; // Transaction to sign for payment
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
delivery?: {
|
|
289
|
+
status: string;
|
|
290
|
+
items?: Array<{
|
|
291
|
+
status: string;
|
|
292
|
+
packageTracking?: {
|
|
293
|
+
carrierName: string;
|
|
294
|
+
carrierTrackingNumber: string;
|
|
295
|
+
};
|
|
296
|
+
}>;
|
|
297
|
+
};
|
|
298
|
+
lineItems?: Array<{
|
|
299
|
+
metadata?: {
|
|
300
|
+
title?: string;
|
|
301
|
+
imageUrl?: string;
|
|
302
|
+
price?: { amount: string; currency: string };
|
|
303
|
+
};
|
|
304
|
+
}>;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
export type TransactionResponse = {
|
|
308
|
+
id: string;
|
|
309
|
+
status: string;
|
|
310
|
+
approvals?: {
|
|
311
|
+
pending?: Array<{
|
|
312
|
+
signer: string;
|
|
313
|
+
message: string; // Message to sign
|
|
314
|
+
}>;
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// ============================================================================
|
|
319
|
+
// Headless Checkout API Functions (Delegated Signer Flow)
|
|
320
|
+
// ============================================================================
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Step 1: Create an order for purchasing products (e.g., from Amazon)
|
|
324
|
+
* Returns order with serializedTransaction to use in step 2
|
|
325
|
+
*/
|
|
326
|
+
export async function createOrder(
|
|
327
|
+
config: CrossmintApiConfig,
|
|
328
|
+
request: CreateOrderRequest,
|
|
329
|
+
): Promise<CrossmintOrder> {
|
|
330
|
+
const response = await fetchCrossmint(config, "/2022-06-09/orders", {
|
|
331
|
+
method: "POST",
|
|
332
|
+
body: JSON.stringify(request),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (!response.ok) {
|
|
336
|
+
const error = await response.text();
|
|
337
|
+
throw new Error(`Failed to create order: ${error}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// API returns { clientSecret, order } - extract the order
|
|
341
|
+
const data = await response.json();
|
|
342
|
+
return data.order;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Step 2a: Create transaction from serialized transaction
|
|
347
|
+
* Returns transactionId and message to sign
|
|
348
|
+
*/
|
|
349
|
+
export async function createTransaction(
|
|
350
|
+
config: CrossmintApiConfig,
|
|
351
|
+
payerAddress: string,
|
|
352
|
+
serializedTransaction: string,
|
|
353
|
+
signerAddress?: string,
|
|
354
|
+
): Promise<TransactionResponse> {
|
|
355
|
+
const response = await fetchCrossmint(
|
|
356
|
+
config,
|
|
357
|
+
`/2025-06-09/wallets/${encodeURIComponent(payerAddress)}/transactions`,
|
|
358
|
+
{
|
|
359
|
+
method: "POST",
|
|
360
|
+
body: JSON.stringify({
|
|
361
|
+
params: {
|
|
362
|
+
transaction: serializedTransaction,
|
|
363
|
+
...(signerAddress && { signer: `external-wallet:${signerAddress}` }),
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
if (!response.ok) {
|
|
370
|
+
const error = await response.text();
|
|
371
|
+
throw new Error(`Failed to create transaction: ${error}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return response.json();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Step 2b: Submit approval with signed message
|
|
379
|
+
*/
|
|
380
|
+
export async function submitApproval(
|
|
381
|
+
config: CrossmintApiConfig,
|
|
382
|
+
payerAddress: string,
|
|
383
|
+
transactionId: string,
|
|
384
|
+
signerAddress: string,
|
|
385
|
+
signature: string,
|
|
386
|
+
): Promise<TransactionResponse> {
|
|
387
|
+
const response = await fetchCrossmint(
|
|
388
|
+
config,
|
|
389
|
+
`/2025-06-09/wallets/${encodeURIComponent(payerAddress)}/transactions/${encodeURIComponent(transactionId)}/approvals`,
|
|
390
|
+
{
|
|
391
|
+
method: "POST",
|
|
392
|
+
body: JSON.stringify({
|
|
393
|
+
approvals: [
|
|
394
|
+
{
|
|
395
|
+
signer: `external-wallet:${signerAddress}`,
|
|
396
|
+
signature,
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
}),
|
|
400
|
+
},
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
if (!response.ok) {
|
|
404
|
+
const error = await response.text();
|
|
405
|
+
throw new Error(`Failed to submit approval: ${error}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return response.json();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Get order status
|
|
413
|
+
*/
|
|
414
|
+
export async function getOrder(
|
|
415
|
+
config: CrossmintApiConfig,
|
|
416
|
+
orderId: string,
|
|
417
|
+
): Promise<CrossmintOrder> {
|
|
418
|
+
const response = await fetchCrossmint(
|
|
419
|
+
config,
|
|
420
|
+
`/2022-06-09/orders/${encodeURIComponent(orderId)}`,
|
|
421
|
+
{ method: "GET" },
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
if (!response.ok) {
|
|
425
|
+
const error = await response.text();
|
|
426
|
+
throw new Error(`Failed to get order: ${error}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return response.json();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Complete Amazon purchase flow with delegated signer
|
|
434
|
+
* Combines all 3 API calls + local signing
|
|
435
|
+
*/
|
|
436
|
+
export async function purchaseProduct(
|
|
437
|
+
config: CrossmintApiConfig,
|
|
438
|
+
request: CreateOrderRequest,
|
|
439
|
+
keypair: Keypair,
|
|
440
|
+
): Promise<{ order: CrossmintOrder; transactionId: string }> {
|
|
441
|
+
// Step 1: Create order
|
|
442
|
+
const order = await createOrder(config, request);
|
|
443
|
+
|
|
444
|
+
const serializedTransaction = order.payment?.preparation?.serializedTransaction;
|
|
445
|
+
if (!serializedTransaction) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`Order created but no serialized transaction returned. Payment status: ${order.payment?.status || "unknown"}`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Step 2a: Create transaction with delegated signer
|
|
452
|
+
const txResponse = await createTransaction(
|
|
453
|
+
config,
|
|
454
|
+
request.payment.payerAddress,
|
|
455
|
+
serializedTransaction,
|
|
456
|
+
keypair.publicKey.toBase58(),
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
const messageToSign = txResponse.approvals?.pending?.[0]?.message;
|
|
460
|
+
if (!messageToSign) {
|
|
461
|
+
throw new Error("Transaction created but no message to sign");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Step 2 (local): Sign the message with ed25519
|
|
465
|
+
// Message is base58 encoded (Solana standard) - same as transfers
|
|
466
|
+
const messageBytes = bs58.decode(messageToSign);
|
|
467
|
+
const nacl = (await import("tweetnacl")).default;
|
|
468
|
+
const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
|
|
469
|
+
const signatureBase58 = bs58.encode(signature);
|
|
470
|
+
|
|
471
|
+
// Step 2b: Submit approval
|
|
472
|
+
await submitApproval(
|
|
473
|
+
config,
|
|
474
|
+
request.payment.payerAddress,
|
|
475
|
+
txResponse.id,
|
|
476
|
+
keypair.publicKey.toBase58(),
|
|
477
|
+
signatureBase58,
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
return { order, transactionId: txResponse.id };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Build Amazon product locator from ASIN or URL
|
|
485
|
+
*/
|
|
486
|
+
export function buildAmazonProductLocator(productIdOrUrl: string): string {
|
|
487
|
+
if (productIdOrUrl.startsWith("amazon:")) {
|
|
488
|
+
return productIdOrUrl;
|
|
489
|
+
}
|
|
490
|
+
if (productIdOrUrl.includes("amazon.com")) {
|
|
491
|
+
return `amazon:${productIdOrUrl}`;
|
|
492
|
+
}
|
|
493
|
+
// Assume it's an ASIN
|
|
494
|
+
return `amazon:${productIdOrUrl}`;
|
|
495
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Hardcoded configuration - no user config needed
|
|
2
|
+
export const DELEGATION_URL = "https://www.lobster.cash/";
|
|
3
|
+
export const ENVIRONMENT = "staging" as const;
|
|
4
|
+
|
|
5
|
+
export type CrossmintPluginConfig = Record<string, never>;
|
|
6
|
+
|
|
7
|
+
export const crossmintConfigSchema = {
|
|
8
|
+
parse(_value: unknown): CrossmintPluginConfig {
|
|
9
|
+
return {};
|
|
10
|
+
},
|
|
11
|
+
};
|