@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/tools.ts
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { OpenClawPluginToolContext } from "../../../src/plugins/types.js";
|
|
4
|
+
import {
|
|
5
|
+
getOrCreateWallet,
|
|
6
|
+
getWallet,
|
|
7
|
+
getKeypair,
|
|
8
|
+
configureWallet,
|
|
9
|
+
isWalletConfigured,
|
|
10
|
+
} from "./wallet.js";
|
|
11
|
+
import {
|
|
12
|
+
getWalletBalance,
|
|
13
|
+
createTransfer,
|
|
14
|
+
getTransactionStatus,
|
|
15
|
+
waitForTransaction,
|
|
16
|
+
buildDelegationUrl,
|
|
17
|
+
purchaseProduct,
|
|
18
|
+
getOrder,
|
|
19
|
+
buildAmazonProductLocator,
|
|
20
|
+
type CrossmintApiConfig,
|
|
21
|
+
type CreateOrderRequest,
|
|
22
|
+
} from "./api.js";
|
|
23
|
+
import { DELEGATION_URL, ENVIRONMENT, type CrossmintPluginConfig } from "./config.js";
|
|
24
|
+
|
|
25
|
+
function getAgentId(ctx: OpenClawPluginToolContext): string {
|
|
26
|
+
return ctx.agentId || "main";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getApiConfig(walletData: { apiKey?: string }, environment: "staging"): CrossmintApiConfig {
|
|
30
|
+
if (!walletData.apiKey) {
|
|
31
|
+
throw new Error("Wallet not configured. Run crossmint_setup first and provide the API key.");
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
apiKey: walletData.apiKey,
|
|
35
|
+
environment,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createCrossmintSetupTool(_api: OpenClawPluginApi, _config: CrossmintPluginConfig) {
|
|
40
|
+
return {
|
|
41
|
+
name: "crossmint_setup",
|
|
42
|
+
description:
|
|
43
|
+
"Set up the Crossmint Solana wallet for this agent. Generates a local keypair and provides a URL for the user to complete wallet setup. After the user completes setup on the web, use crossmint_configure to save the wallet address and API key.",
|
|
44
|
+
parameters: Type.Object({
|
|
45
|
+
agentId: Type.Optional(
|
|
46
|
+
Type.String({ description: "Agent ID for the wallet. Defaults to current agent." }),
|
|
47
|
+
),
|
|
48
|
+
}),
|
|
49
|
+
|
|
50
|
+
async execute(_id: string, params: Record<string, unknown>, ctx: OpenClawPluginToolContext) {
|
|
51
|
+
const agentId =
|
|
52
|
+
typeof params.agentId === "string" ? params.agentId : getAgentId(ctx);
|
|
53
|
+
|
|
54
|
+
// Get or create the local Solana wallet (generates keypair)
|
|
55
|
+
const walletData = getOrCreateWallet(agentId);
|
|
56
|
+
|
|
57
|
+
// Check if already configured
|
|
58
|
+
if (isWalletConfigured(agentId)) {
|
|
59
|
+
return {
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: "text",
|
|
63
|
+
text: `Wallet already configured for agent "${agentId}":\n\nLocal signer: ${walletData.address}\nSmart wallet: ${walletData.smartWalletAddress}\n\nTo reconfigure, delete the wallet first.`,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
details: {
|
|
67
|
+
status: "already_configured",
|
|
68
|
+
agentId,
|
|
69
|
+
localSignerAddress: walletData.address,
|
|
70
|
+
smartWalletAddress: walletData.smartWalletAddress,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Build the delegation URL with the public key
|
|
76
|
+
const delegationUrl = buildDelegationUrl(
|
|
77
|
+
DELEGATION_URL,
|
|
78
|
+
walletData.address,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
content: [
|
|
83
|
+
{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: `Solana wallet setup for agent "${agentId}":\n\n**Step 1:** Open this URL to set up the wallet:\n${delegationUrl}\n\n**Step 2:** The web app will:\n- Create a Crossmint smart wallet\n- Add this agent as a delegated signer\n- Show you the wallet address and API key\n\n**Step 3:** After completing setup, use crossmint_configure with:\n- The wallet address shown on the web\n- The API key shown on the web\n\nLocal signer address: ${walletData.address}`,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
details: {
|
|
89
|
+
status: "pending_configuration",
|
|
90
|
+
agentId,
|
|
91
|
+
localSignerAddress: walletData.address,
|
|
92
|
+
delegationUrl,
|
|
93
|
+
nextStep: "crossmint_configure",
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createCrossmintConfigureTool(_api: OpenClawPluginApi, _config: CrossmintPluginConfig) {
|
|
101
|
+
return {
|
|
102
|
+
name: "crossmint_configure",
|
|
103
|
+
description:
|
|
104
|
+
"Complete wallet setup by providing the wallet address and API key from the Crossmint web app. Run this after completing the setup flow from crossmint_setup.",
|
|
105
|
+
parameters: Type.Object({
|
|
106
|
+
walletAddress: Type.String({
|
|
107
|
+
description: "The smart wallet address shown on the Crossmint web app",
|
|
108
|
+
}),
|
|
109
|
+
apiKey: Type.String({
|
|
110
|
+
description: "The client-side API key shown on the Crossmint web app",
|
|
111
|
+
}),
|
|
112
|
+
agentId: Type.Optional(
|
|
113
|
+
Type.String({ description: "Agent ID for the wallet. Defaults to current agent." }),
|
|
114
|
+
),
|
|
115
|
+
}),
|
|
116
|
+
|
|
117
|
+
async execute(_id: string, params: Record<string, unknown>, ctx: OpenClawPluginToolContext) {
|
|
118
|
+
const agentId =
|
|
119
|
+
typeof params.agentId === "string" ? params.agentId : getAgentId(ctx);
|
|
120
|
+
const walletAddress = params.walletAddress as string;
|
|
121
|
+
const apiKey = params.apiKey as string;
|
|
122
|
+
|
|
123
|
+
if (!walletAddress) {
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: "text", text: "Wallet address is required." }],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!apiKey) {
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: "text", text: "API key is required." }],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if keypair exists
|
|
136
|
+
const existing = getWallet(agentId);
|
|
137
|
+
if (!existing) {
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: `No keypair found for agent "${agentId}". Run crossmint_setup first to generate a keypair.`,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
// Save the wallet configuration
|
|
150
|
+
const walletData = configureWallet(agentId, walletAddress, apiKey);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: `Wallet configured successfully for agent "${agentId}"!\n\nLocal signer: ${walletData.address}\nSmart wallet: ${walletData.smartWalletAddress}\nEnvironment: ${ENVIRONMENT}\n\nYou can now use crossmint_balance and crossmint_send.`,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
details: {
|
|
160
|
+
status: "configured",
|
|
161
|
+
agentId,
|
|
162
|
+
localSignerAddress: walletData.address,
|
|
163
|
+
smartWalletAddress: walletData.smartWalletAddress,
|
|
164
|
+
environment: ENVIRONMENT,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return {
|
|
169
|
+
content: [
|
|
170
|
+
{
|
|
171
|
+
type: "text",
|
|
172
|
+
text: `Failed to configure wallet: ${(error as Error).message}`,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function createCrossmintBalanceTool(_api: OpenClawPluginApi, _config: CrossmintPluginConfig) {
|
|
182
|
+
return {
|
|
183
|
+
name: "crossmint_balance",
|
|
184
|
+
description: "Check the balance of the Crossmint Solana wallet for this agent.",
|
|
185
|
+
parameters: Type.Object({
|
|
186
|
+
agentId: Type.Optional(
|
|
187
|
+
Type.String({ description: "Agent ID for the wallet. Defaults to current agent." }),
|
|
188
|
+
),
|
|
189
|
+
}),
|
|
190
|
+
|
|
191
|
+
async execute(_id: string, params: Record<string, unknown>, ctx: OpenClawPluginToolContext) {
|
|
192
|
+
const agentId =
|
|
193
|
+
typeof params.agentId === "string" ? params.agentId : getAgentId(ctx);
|
|
194
|
+
|
|
195
|
+
const walletData = getWallet(agentId);
|
|
196
|
+
if (!walletData) {
|
|
197
|
+
return {
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: `No wallet found for agent "${agentId}". Run crossmint_setup first.`,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!isWalletConfigured(agentId)) {
|
|
208
|
+
return {
|
|
209
|
+
content: [
|
|
210
|
+
{
|
|
211
|
+
type: "text",
|
|
212
|
+
text: `Wallet not fully configured for agent "${agentId}". Complete setup with crossmint_configure.`,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const apiConfig = getApiConfig(walletData, ENVIRONMENT);
|
|
220
|
+
const balances = await getWalletBalance(apiConfig, walletData.smartWalletAddress!);
|
|
221
|
+
|
|
222
|
+
const balanceText = balances
|
|
223
|
+
.map((b) => `${b.token}: ${b.amount}`)
|
|
224
|
+
.join("\n");
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
content: [
|
|
228
|
+
{
|
|
229
|
+
type: "text",
|
|
230
|
+
text: `Wallet balance for agent "${agentId}":\n\nSmart Wallet: ${walletData.smartWalletAddress}\nChain: Solana\n\n${balanceText || "No balances found"}`,
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
details: {
|
|
234
|
+
agentId,
|
|
235
|
+
smartWalletAddress: walletData.smartWalletAddress,
|
|
236
|
+
localSignerAddress: walletData.address,
|
|
237
|
+
chain: "solana",
|
|
238
|
+
balances,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: "text",
|
|
246
|
+
text: `Failed to get balance: ${(error as Error).message}`,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function createCrossmintSendTool(_api: OpenClawPluginApi, _config: CrossmintPluginConfig) {
|
|
256
|
+
return {
|
|
257
|
+
name: "crossmint_send",
|
|
258
|
+
description:
|
|
259
|
+
"Send tokens from the agent's Crossmint Solana wallet to another address. Supports SOL, USDC, and other SPL tokens.",
|
|
260
|
+
parameters: Type.Object({
|
|
261
|
+
to: Type.String({ description: "Recipient Solana address or email locator" }),
|
|
262
|
+
amount: Type.String({ description: "Amount to send (e.g., '10', '0.5')" }),
|
|
263
|
+
token: Type.Optional(
|
|
264
|
+
Type.String({
|
|
265
|
+
description: "Token to send: 'sol', 'usdc', or SPL token address. Defaults to 'usdc'.",
|
|
266
|
+
}),
|
|
267
|
+
),
|
|
268
|
+
wait: Type.Optional(
|
|
269
|
+
Type.Boolean({
|
|
270
|
+
description: "If true, wait for transaction confirmation before returning. Default: false",
|
|
271
|
+
}),
|
|
272
|
+
),
|
|
273
|
+
timeoutMs: Type.Optional(
|
|
274
|
+
Type.Number({
|
|
275
|
+
description: "Maximum time to wait for confirmation in milliseconds. Default: 60000 (60 seconds)",
|
|
276
|
+
}),
|
|
277
|
+
),
|
|
278
|
+
agentId: Type.Optional(
|
|
279
|
+
Type.String({ description: "Agent ID for the wallet. Defaults to current agent." }),
|
|
280
|
+
),
|
|
281
|
+
}),
|
|
282
|
+
|
|
283
|
+
async execute(_id: string, params: Record<string, unknown>, ctx: OpenClawPluginToolContext) {
|
|
284
|
+
const agentId =
|
|
285
|
+
typeof params.agentId === "string" ? params.agentId : getAgentId(ctx);
|
|
286
|
+
const to = params.to as string;
|
|
287
|
+
const amount = params.amount as string;
|
|
288
|
+
const token = (params.token as string) || "usdc";
|
|
289
|
+
const wait = params.wait === true;
|
|
290
|
+
const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : 60000;
|
|
291
|
+
|
|
292
|
+
if (!to) {
|
|
293
|
+
return {
|
|
294
|
+
content: [{ type: "text", text: "Recipient address is required." }],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!amount) {
|
|
299
|
+
return {
|
|
300
|
+
content: [{ type: "text", text: "Amount is required." }],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const walletData = getWallet(agentId);
|
|
305
|
+
if (!walletData) {
|
|
306
|
+
return {
|
|
307
|
+
content: [
|
|
308
|
+
{
|
|
309
|
+
type: "text",
|
|
310
|
+
text: `No wallet found for agent "${agentId}". Run crossmint_setup first.`,
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!isWalletConfigured(agentId)) {
|
|
317
|
+
return {
|
|
318
|
+
content: [
|
|
319
|
+
{
|
|
320
|
+
type: "text",
|
|
321
|
+
text: `Wallet not fully configured for agent "${agentId}". Complete setup with crossmint_configure.`,
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const apiConfig = getApiConfig(walletData, ENVIRONMENT);
|
|
329
|
+
|
|
330
|
+
// Get Solana keypair for signing
|
|
331
|
+
const keypair = getKeypair(agentId);
|
|
332
|
+
if (!keypair) {
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: "text", text: "Failed to load wallet for signing." }],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Create and sign transfer
|
|
339
|
+
let tx = await createTransfer(
|
|
340
|
+
apiConfig,
|
|
341
|
+
walletData.smartWalletAddress!,
|
|
342
|
+
to,
|
|
343
|
+
token,
|
|
344
|
+
amount,
|
|
345
|
+
keypair,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// If wait is requested, poll until terminal state
|
|
349
|
+
if (wait && tx.id) {
|
|
350
|
+
tx = await waitForTransaction(apiConfig, walletData.smartWalletAddress!, tx.id, timeoutMs);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const statusEmoji = tx.status === "success" || tx.status === "completed" ? "✅" :
|
|
354
|
+
tx.status === "failed" || tx.status === "rejected" ? "❌" :
|
|
355
|
+
tx.status === "pending" ? "⏳" : "🔄";
|
|
356
|
+
|
|
357
|
+
const actionWord = (tx.status === "success" || tx.status === "completed") ? "completed" :
|
|
358
|
+
(tx.status === "failed" || tx.status === "rejected") ? "failed" : "initiated";
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
content: [
|
|
362
|
+
{
|
|
363
|
+
type: "text",
|
|
364
|
+
text: `${statusEmoji} Transfer ${actionWord}!\n\nFrom: ${walletData.smartWalletAddress}\nTo: ${to}\nAmount: ${amount} ${token.toUpperCase()}\n\nTransaction ID: ${tx.id}\nStatus: ${tx.status}${tx.hash ? `\nHash: ${tx.hash}` : ""}${tx.explorerLink ? `\nExplorer: ${tx.explorerLink}` : ""}`,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
details: {
|
|
368
|
+
agentId,
|
|
369
|
+
from: walletData.smartWalletAddress,
|
|
370
|
+
to,
|
|
371
|
+
amount,
|
|
372
|
+
token,
|
|
373
|
+
transaction: tx,
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return {
|
|
378
|
+
content: [
|
|
379
|
+
{
|
|
380
|
+
type: "text",
|
|
381
|
+
text: `Failed to send: ${(error as Error).message}`,
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function createCrossmintWalletInfoTool(_api: OpenClawPluginApi, _config: CrossmintPluginConfig) {
|
|
391
|
+
return {
|
|
392
|
+
name: "crossmint_wallet_info",
|
|
393
|
+
description: "Get detailed information about the agent's Crossmint Solana wallet.",
|
|
394
|
+
parameters: Type.Object({
|
|
395
|
+
agentId: Type.Optional(
|
|
396
|
+
Type.String({ description: "Agent ID for the wallet. Defaults to current agent." }),
|
|
397
|
+
),
|
|
398
|
+
}),
|
|
399
|
+
|
|
400
|
+
async execute(_id: string, params: Record<string, unknown>, ctx: OpenClawPluginToolContext) {
|
|
401
|
+
const agentId =
|
|
402
|
+
typeof params.agentId === "string" ? params.agentId : getAgentId(ctx);
|
|
403
|
+
|
|
404
|
+
const walletData = getWallet(agentId);
|
|
405
|
+
if (!walletData) {
|
|
406
|
+
return {
|
|
407
|
+
content: [
|
|
408
|
+
{
|
|
409
|
+
type: "text",
|
|
410
|
+
text: `No wallet found for agent "${agentId}". Run crossmint_setup first.`,
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const configured = isWalletConfigured(agentId);
|
|
417
|
+
|
|
418
|
+
if (!configured) {
|
|
419
|
+
const delegationUrl = buildDelegationUrl(
|
|
420
|
+
DELEGATION_URL,
|
|
421
|
+
walletData.address,
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
content: [
|
|
426
|
+
{
|
|
427
|
+
type: "text",
|
|
428
|
+
text: `Wallet info for agent "${agentId}" (not fully configured):\n\nLocal signer: ${walletData.address}\nCreated: ${walletData.createdAt}\n\nComplete setup at: ${delegationUrl}\nThen run crossmint_configure with the wallet address and API key.`,
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
details: {
|
|
432
|
+
agentId,
|
|
433
|
+
localSignerAddress: walletData.address,
|
|
434
|
+
createdAt: walletData.createdAt,
|
|
435
|
+
configured: false,
|
|
436
|
+
delegationUrl,
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
content: [
|
|
443
|
+
{
|
|
444
|
+
type: "text",
|
|
445
|
+
text: `Wallet info for agent "${agentId}":\n\nLocal signer: ${walletData.address}\nSmart wallet: ${walletData.smartWalletAddress}\nChain: Solana\nEnvironment: ${ENVIRONMENT}\nCreated: ${walletData.createdAt}\nConfigured: ${walletData.configuredAt}`,
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
details: {
|
|
449
|
+
agentId,
|
|
450
|
+
localSignerAddress: walletData.address,
|
|
451
|
+
smartWalletAddress: walletData.smartWalletAddress,
|
|
452
|
+
chain: "solana",
|
|
453
|
+
environment: ENVIRONMENT,
|
|
454
|
+
createdAt: walletData.createdAt,
|
|
455
|
+
configuredAt: walletData.configuredAt,
|
|
456
|
+
configured: true,
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function createCrossmintTxStatusTool(_api: OpenClawPluginApi, _config: CrossmintPluginConfig) {
|
|
464
|
+
return {
|
|
465
|
+
name: "crossmint_tx_status",
|
|
466
|
+
description:
|
|
467
|
+
"Check the status of a Crossmint transaction. Can optionally wait for the transaction to complete.",
|
|
468
|
+
parameters: Type.Object({
|
|
469
|
+
transactionId: Type.String({
|
|
470
|
+
description: "The transaction ID returned from crossmint_send",
|
|
471
|
+
}),
|
|
472
|
+
wait: Type.Optional(
|
|
473
|
+
Type.Boolean({
|
|
474
|
+
description: "If true, wait for the transaction to reach a terminal state (success/failed). Default: false",
|
|
475
|
+
}),
|
|
476
|
+
),
|
|
477
|
+
timeoutMs: Type.Optional(
|
|
478
|
+
Type.Number({
|
|
479
|
+
description: "Maximum time to wait in milliseconds. Default: 60000 (60 seconds)",
|
|
480
|
+
}),
|
|
481
|
+
),
|
|
482
|
+
agentId: Type.Optional(
|
|
483
|
+
Type.String({ description: "Agent ID for the wallet. Defaults to current agent." }),
|
|
484
|
+
),
|
|
485
|
+
}),
|
|
486
|
+
|
|
487
|
+
async execute(_id: string, params: Record<string, unknown>, ctx: OpenClawPluginToolContext) {
|
|
488
|
+
const agentId =
|
|
489
|
+
typeof params.agentId === "string" ? params.agentId : getAgentId(ctx);
|
|
490
|
+
const transactionId = params.transactionId as string;
|
|
491
|
+
const wait = params.wait === true;
|
|
492
|
+
const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : 60000;
|
|
493
|
+
|
|
494
|
+
if (!transactionId) {
|
|
495
|
+
return {
|
|
496
|
+
content: [{ type: "text", text: "Transaction ID is required." }],
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const walletData = getWallet(agentId);
|
|
501
|
+
if (!walletData) {
|
|
502
|
+
return {
|
|
503
|
+
content: [
|
|
504
|
+
{
|
|
505
|
+
type: "text",
|
|
506
|
+
text: `No wallet found for agent "${agentId}". Run crossmint_setup first.`,
|
|
507
|
+
},
|
|
508
|
+
],
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!isWalletConfigured(agentId)) {
|
|
513
|
+
return {
|
|
514
|
+
content: [
|
|
515
|
+
{
|
|
516
|
+
type: "text",
|
|
517
|
+
text: `Wallet not fully configured for agent "${agentId}". Complete setup with crossmint_configure.`,
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
const apiConfig = getApiConfig(walletData, ENVIRONMENT);
|
|
525
|
+
|
|
526
|
+
const tx = wait
|
|
527
|
+
? await waitForTransaction(apiConfig, walletData.smartWalletAddress!, transactionId, timeoutMs)
|
|
528
|
+
: await getTransactionStatus(apiConfig, walletData.smartWalletAddress!, transactionId);
|
|
529
|
+
|
|
530
|
+
const statusEmoji = tx.status === "success" || tx.status === "completed" ? "✅" :
|
|
531
|
+
tx.status === "failed" || tx.status === "rejected" ? "❌" :
|
|
532
|
+
tx.status === "pending" ? "⏳" : "🔄";
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
content: [
|
|
536
|
+
{
|
|
537
|
+
type: "text",
|
|
538
|
+
text: `${statusEmoji} Transaction ${transactionId}\n\nStatus: ${tx.status}${tx.hash ? `\nHash: ${tx.hash}` : ""}${tx.explorerLink ? `\nExplorer: ${tx.explorerLink}` : ""}${tx.onChain ? `\nOn-chain: ${JSON.stringify(tx.onChain)}` : ""}`,
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
details: {
|
|
542
|
+
agentId,
|
|
543
|
+
transaction: tx,
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
} catch (error) {
|
|
547
|
+
return {
|
|
548
|
+
content: [
|
|
549
|
+
{
|
|
550
|
+
type: "text",
|
|
551
|
+
text: `Failed to get transaction status: ${(error as Error).message}`,
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function createCrossmintBuyTool(_api: OpenClawPluginApi, _config: CrossmintPluginConfig) {
|
|
561
|
+
return {
|
|
562
|
+
name: "crossmint_buy",
|
|
563
|
+
description:
|
|
564
|
+
"Buy a product from Amazon using the agent's Crossmint wallet. Pays with SOL or USDC. Requires shipping address.",
|
|
565
|
+
parameters: Type.Object({
|
|
566
|
+
productId: Type.String({
|
|
567
|
+
description: "Amazon product ASIN (e.g., 'B00O79SKV6') or full Amazon URL",
|
|
568
|
+
}),
|
|
569
|
+
recipientEmail: Type.String({
|
|
570
|
+
description: "Email address for order confirmation and receipt",
|
|
571
|
+
}),
|
|
572
|
+
recipientName: Type.String({
|
|
573
|
+
description: "Full name for shipping",
|
|
574
|
+
}),
|
|
575
|
+
addressLine1: Type.String({
|
|
576
|
+
description: "Street address line 1",
|
|
577
|
+
}),
|
|
578
|
+
addressLine2: Type.Optional(
|
|
579
|
+
Type.String({ description: "Street address line 2 (apt, suite, etc.)" }),
|
|
580
|
+
),
|
|
581
|
+
city: Type.String({
|
|
582
|
+
description: "City name",
|
|
583
|
+
}),
|
|
584
|
+
state: Type.Optional(
|
|
585
|
+
Type.String({ description: "State/province code (e.g., 'CA', 'NY')" }),
|
|
586
|
+
),
|
|
587
|
+
postalCode: Type.String({
|
|
588
|
+
description: "Postal/ZIP code",
|
|
589
|
+
}),
|
|
590
|
+
country: Type.String({
|
|
591
|
+
description: "Country code (e.g., 'US')",
|
|
592
|
+
}),
|
|
593
|
+
currency: Type.Optional(
|
|
594
|
+
Type.String({ description: "Payment currency: 'sol' or 'usdc'. Defaults to 'usdc'." }),
|
|
595
|
+
),
|
|
596
|
+
agentId: Type.Optional(
|
|
597
|
+
Type.String({ description: "Agent ID for the wallet. Defaults to current agent." }),
|
|
598
|
+
),
|
|
599
|
+
}),
|
|
600
|
+
|
|
601
|
+
async execute(_id: string, params: Record<string, unknown>, ctx: OpenClawPluginToolContext) {
|
|
602
|
+
const agentId =
|
|
603
|
+
typeof params.agentId === "string" ? params.agentId : getAgentId(ctx);
|
|
604
|
+
|
|
605
|
+
// Validate required params
|
|
606
|
+
const productId = params.productId as string;
|
|
607
|
+
const recipientEmail = params.recipientEmail as string;
|
|
608
|
+
const recipientName = params.recipientName as string;
|
|
609
|
+
const addressLine1 = params.addressLine1 as string;
|
|
610
|
+
const addressLine2 = params.addressLine2 as string | undefined;
|
|
611
|
+
const city = params.city as string;
|
|
612
|
+
const state = params.state as string | undefined;
|
|
613
|
+
const postalCode = params.postalCode as string;
|
|
614
|
+
const country = params.country as string;
|
|
615
|
+
const currency = (params.currency as string)?.toLowerCase() || "usdc";
|
|
616
|
+
|
|
617
|
+
if (!productId || !recipientEmail || !recipientName || !addressLine1 || !city || !postalCode || !country) {
|
|
618
|
+
return {
|
|
619
|
+
content: [{ type: "text", text: "Missing required fields. Need: productId, recipientEmail, recipientName, addressLine1, city, postalCode, country" }],
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const walletData = getWallet(agentId);
|
|
624
|
+
if (!walletData) {
|
|
625
|
+
return {
|
|
626
|
+
content: [
|
|
627
|
+
{ type: "text", text: `No wallet found for agent "${agentId}". Run crossmint_setup first.` },
|
|
628
|
+
],
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (!isWalletConfigured(agentId)) {
|
|
633
|
+
return {
|
|
634
|
+
content: [
|
|
635
|
+
{ type: "text", text: `Wallet not fully configured for agent "${agentId}". Complete setup with crossmint_configure.` },
|
|
636
|
+
],
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const keypair = getKeypair(agentId);
|
|
641
|
+
if (!keypair) {
|
|
642
|
+
return {
|
|
643
|
+
content: [{ type: "text", text: "Failed to load wallet for signing." }],
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
try {
|
|
648
|
+
const apiConfig = getApiConfig(walletData, ENVIRONMENT);
|
|
649
|
+
const productLocator = buildAmazonProductLocator(productId);
|
|
650
|
+
|
|
651
|
+
const orderRequest: CreateOrderRequest = {
|
|
652
|
+
recipient: {
|
|
653
|
+
email: recipientEmail,
|
|
654
|
+
physicalAddress: {
|
|
655
|
+
name: recipientName,
|
|
656
|
+
line1: addressLine1,
|
|
657
|
+
line2: addressLine2,
|
|
658
|
+
city,
|
|
659
|
+
state,
|
|
660
|
+
postalCode,
|
|
661
|
+
country,
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
payment: {
|
|
665
|
+
receiptEmail: recipientEmail,
|
|
666
|
+
method: "solana",
|
|
667
|
+
currency,
|
|
668
|
+
payerAddress: walletData.smartWalletAddress!,
|
|
669
|
+
},
|
|
670
|
+
lineItems: [{ productLocator }],
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const result = await purchaseProduct(apiConfig, orderRequest, keypair);
|
|
674
|
+
|
|
675
|
+
const productTitle = result.order.lineItems?.[0]?.metadata?.title || "Product";
|
|
676
|
+
const totalPrice = result.order.quote?.totalPrice;
|
|
677
|
+
const priceText = totalPrice ? `${totalPrice.amount} ${totalPrice.currency}` : "See order details";
|
|
678
|
+
|
|
679
|
+
return {
|
|
680
|
+
content: [
|
|
681
|
+
{
|
|
682
|
+
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
|
+
},
|
|
685
|
+
],
|
|
686
|
+
details: {
|
|
687
|
+
orderId: result.order.orderId,
|
|
688
|
+
transactionId: result.transactionId,
|
|
689
|
+
phase: result.order.phase,
|
|
690
|
+
productLocator,
|
|
691
|
+
totalPrice,
|
|
692
|
+
recipient: orderRequest.recipient,
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
} catch (error) {
|
|
696
|
+
return {
|
|
697
|
+
content: [
|
|
698
|
+
{ type: "text", text: `Failed to purchase: ${(error as Error).message}` },
|
|
699
|
+
],
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export function createCrossmintOrderStatusTool(_api: OpenClawPluginApi, _config: CrossmintPluginConfig) {
|
|
707
|
+
return {
|
|
708
|
+
name: "crossmint_order_status",
|
|
709
|
+
description: "Check the status of a Crossmint order (Amazon purchase).",
|
|
710
|
+
parameters: Type.Object({
|
|
711
|
+
orderId: Type.String({
|
|
712
|
+
description: "The order ID returned from crossmint_buy",
|
|
713
|
+
}),
|
|
714
|
+
agentId: Type.Optional(
|
|
715
|
+
Type.String({ description: "Agent ID for the wallet. Defaults to current agent." }),
|
|
716
|
+
),
|
|
717
|
+
}),
|
|
718
|
+
|
|
719
|
+
async execute(_id: string, params: Record<string, unknown>, ctx: OpenClawPluginToolContext) {
|
|
720
|
+
const agentId =
|
|
721
|
+
typeof params.agentId === "string" ? params.agentId : getAgentId(ctx);
|
|
722
|
+
const orderId = params.orderId as string;
|
|
723
|
+
|
|
724
|
+
if (!orderId) {
|
|
725
|
+
return {
|
|
726
|
+
content: [{ type: "text", text: "Order ID is required." }],
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const walletData = getWallet(agentId);
|
|
731
|
+
if (!walletData) {
|
|
732
|
+
return {
|
|
733
|
+
content: [
|
|
734
|
+
{ type: "text", text: `No wallet found for agent "${agentId}". Run crossmint_setup first.` },
|
|
735
|
+
],
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (!isWalletConfigured(agentId)) {
|
|
740
|
+
return {
|
|
741
|
+
content: [
|
|
742
|
+
{ type: "text", text: `Wallet not fully configured for agent "${agentId}". Complete setup with crossmint_configure.` },
|
|
743
|
+
],
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
const apiConfig = getApiConfig(walletData, ENVIRONMENT);
|
|
749
|
+
const order = await getOrder(apiConfig, orderId);
|
|
750
|
+
|
|
751
|
+
const productTitle = order.lineItems?.[0]?.metadata?.title || "Product";
|
|
752
|
+
const deliveryStatus = order.delivery?.status || "pending";
|
|
753
|
+
const deliveryItems = order.delivery?.items || [];
|
|
754
|
+
|
|
755
|
+
let trackingInfo = "";
|
|
756
|
+
for (const item of deliveryItems) {
|
|
757
|
+
if (item.packageTracking) {
|
|
758
|
+
trackingInfo += `\nCarrier: ${item.packageTracking.carrierName}\nTracking: ${item.packageTracking.carrierTrackingNumber}`;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return {
|
|
763
|
+
content: [
|
|
764
|
+
{
|
|
765
|
+
type: "text",
|
|
766
|
+
text: `Order Status: ${order.phase}\n\nOrder ID: ${orderId}\nProduct: ${productTitle}\nPayment: ${order.payment?.status || "unknown"}\nDelivery: ${deliveryStatus}${trackingInfo}`,
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
details: {
|
|
770
|
+
orderId,
|
|
771
|
+
phase: order.phase,
|
|
772
|
+
quote: order.quote,
|
|
773
|
+
payment: order.payment,
|
|
774
|
+
delivery: order.delivery,
|
|
775
|
+
lineItems: order.lineItems,
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
} catch (error) {
|
|
779
|
+
return {
|
|
780
|
+
content: [
|
|
781
|
+
{ type: "text", text: `Failed to get order status: ${(error as Error).message}` },
|
|
782
|
+
],
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
}
|