bsv-mcp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -0
- package/biome.json +30 -0
- package/index.ts +34 -0
- package/package.json +31 -0
- package/tests/integration/wallet-server.test.ts +216 -0
- package/tools/bsv/decodeTransaction.ts +299 -0
- package/tools/bsv/getPrice.ts +44 -0
- package/tools/bsv/index.ts +13 -0
- package/tools/bsv/token.ts +169 -0
- package/tools/index.ts +21 -0
- package/tools/ordinals/bsv20MarketSales.ts +118 -0
- package/tools/ordinals/getBsv20ById.ts +94 -0
- package/tools/ordinals/getInscription.ts +103 -0
- package/tools/ordinals/index.ts +19 -0
- package/tools/ordinals/marketListings.ts +137 -0
- package/tools/ordinals/searchInscriptions.ts +118 -0
- package/tools/utils/conversion.ts +58 -0
- package/tools/utils/index.ts +50 -0
- package/tools/wallet/getAddress.ts +38 -0
- package/tools/wallet/purchaseListing.ts +175 -0
- package/tools/wallet/schemas.ts +284 -0
- package/tools/wallet/sendToAddress.ts +112 -0
- package/tools/wallet/tools.test.ts +76 -0
- package/tools/wallet/tools.ts +206 -0
- package/tools/wallet/wallet.ts +290 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { PrivateKey } from "@bsv/sdk";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
|
4
|
+
import {
|
|
5
|
+
type ExistingListing,
|
|
6
|
+
type Utxo,
|
|
7
|
+
oneSatBroadcaster,
|
|
8
|
+
purchaseOrdListing,
|
|
9
|
+
} from "js-1sat-ord";
|
|
10
|
+
import type { z } from "zod";
|
|
11
|
+
import { purchaseListingArgsSchema } from "./schemas";
|
|
12
|
+
import type { Wallet } from "./wallet";
|
|
13
|
+
|
|
14
|
+
// Define types for 1Sat API response
|
|
15
|
+
interface ListingResponse {
|
|
16
|
+
txid: string;
|
|
17
|
+
vout: number;
|
|
18
|
+
satoshis: number;
|
|
19
|
+
script: string;
|
|
20
|
+
data?: {
|
|
21
|
+
list?: {
|
|
22
|
+
price: number;
|
|
23
|
+
payout: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register the purchaseListing tool
|
|
30
|
+
*
|
|
31
|
+
* This tool:
|
|
32
|
+
* 1. Parses the listing outpoint to get the txid and vout
|
|
33
|
+
* 2. Fetches the listing UTXO from the ordinals API
|
|
34
|
+
* 3. Gets the wallet's payment UTXOs (using the wallet's internal UTXO management)
|
|
35
|
+
* 4. Uses purchaseOrdListing to create a purchase transaction
|
|
36
|
+
* 5. Broadcasts the transaction
|
|
37
|
+
* 6. Returns the transaction details
|
|
38
|
+
*/
|
|
39
|
+
export function registerPurchaseListingTool(server: McpServer, wallet: Wallet) {
|
|
40
|
+
// Store a reference to check if wallet is persistent
|
|
41
|
+
console.log("Registering purchaseListing tool with wallet:", wallet);
|
|
42
|
+
|
|
43
|
+
server.tool(
|
|
44
|
+
"wallet_purchaseListing",
|
|
45
|
+
{ args: purchaseListingArgsSchema },
|
|
46
|
+
async (
|
|
47
|
+
{ args }: { args: z.infer<typeof purchaseListingArgsSchema> },
|
|
48
|
+
extra: RequestHandlerExtra,
|
|
49
|
+
) => {
|
|
50
|
+
try {
|
|
51
|
+
console.log(`Attempting to purchase listing: ${args.listingOutpoint}`);
|
|
52
|
+
console.log("Using wallet instance:", wallet);
|
|
53
|
+
console.log("Wallet has UTXOs:", await wallet.getUtxos());
|
|
54
|
+
|
|
55
|
+
// Fetch the listing info directly from the API
|
|
56
|
+
const response = await fetch(
|
|
57
|
+
`https://ordinals.gorillapool.io/api/txos/${args.listingOutpoint}?script=true`,
|
|
58
|
+
);
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Failed to fetch listing data: ${response.statusText}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const listingData = (await response.json()) as ListingResponse;
|
|
66
|
+
|
|
67
|
+
// Check if the listing is valid and has a price
|
|
68
|
+
if (!listingData.data?.list?.price) {
|
|
69
|
+
throw new Error("Listing is either not for sale or invalid");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check if payout is available
|
|
73
|
+
if (!listingData.data.list.payout) {
|
|
74
|
+
throw new Error("Listing doesn't have payout information");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Parse the listing outpoint to get txid and vout
|
|
78
|
+
const [txid, voutStr] = args.listingOutpoint.split("_");
|
|
79
|
+
if (!txid) {
|
|
80
|
+
throw new Error("Invalid outpoint format. Expected txid_vout");
|
|
81
|
+
}
|
|
82
|
+
const vout = Number.parseInt(voutStr || "0", 10);
|
|
83
|
+
|
|
84
|
+
// Create listing UTXO object in the format required by js-1sat-ord
|
|
85
|
+
const listingUtxo: Utxo = {
|
|
86
|
+
txid,
|
|
87
|
+
vout,
|
|
88
|
+
script: listingData.script,
|
|
89
|
+
satoshis: listingData.satoshis,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Create the ExistingListing object
|
|
93
|
+
const listing: ExistingListing = {
|
|
94
|
+
payout: listingData.data.list.payout,
|
|
95
|
+
listingUtxo,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Get private key from the wallet
|
|
99
|
+
const paymentPk = wallet.getPrivateKey();
|
|
100
|
+
if (!paymentPk) {
|
|
101
|
+
throw new Error("No private key available in wallet");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Get payment address
|
|
105
|
+
const paymentAddress = paymentPk.toAddress().toString();
|
|
106
|
+
console.log(`Using payment address: ${paymentAddress}`);
|
|
107
|
+
|
|
108
|
+
// Get payment UTXOs from the wallet's managed UTXOs
|
|
109
|
+
const { paymentUtxos } = await wallet.getUtxos();
|
|
110
|
+
if (!paymentUtxos || paymentUtxos.length === 0) {
|
|
111
|
+
// Provide more helpful error message with instructions
|
|
112
|
+
throw new Error(
|
|
113
|
+
`No payment UTXOs available for address ${paymentAddress}.
|
|
114
|
+
Please fund this wallet address with enough BSV to cover the purchase price
|
|
115
|
+
(${listingData.data.list.price} satoshis) plus transaction fees.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Create the purchase transaction using the library's config type
|
|
120
|
+
const transaction = await purchaseOrdListing({
|
|
121
|
+
utxos: paymentUtxos,
|
|
122
|
+
paymentPk,
|
|
123
|
+
ordAddress: args.ordAddress,
|
|
124
|
+
listing,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// After successful transaction creation, refresh the wallet's UTXOs
|
|
128
|
+
// This ensures the wallet doesn't try to reuse spent UTXOs
|
|
129
|
+
try {
|
|
130
|
+
await wallet.refreshUtxos();
|
|
131
|
+
} catch (refreshError) {
|
|
132
|
+
console.warn(
|
|
133
|
+
"Failed to refresh UTXOs after transaction:",
|
|
134
|
+
refreshError,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Broadcast the transaction
|
|
139
|
+
const broadcastResult = await transaction.tx.broadcast(
|
|
140
|
+
oneSatBroadcaster(),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Handle broadcast response
|
|
144
|
+
const resultStatus =
|
|
145
|
+
typeof broadcastResult === "object" && "status" in broadcastResult
|
|
146
|
+
? broadcastResult.status
|
|
147
|
+
: "unknown";
|
|
148
|
+
|
|
149
|
+
const resultMessage =
|
|
150
|
+
typeof broadcastResult === "object" && "error" in broadcastResult
|
|
151
|
+
? broadcastResult.error
|
|
152
|
+
: "Transaction broadcast successful";
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
content: [
|
|
156
|
+
{
|
|
157
|
+
type: "text",
|
|
158
|
+
text: JSON.stringify({
|
|
159
|
+
status: resultStatus,
|
|
160
|
+
message: resultMessage,
|
|
161
|
+
txid: transaction.tx.id("hex"),
|
|
162
|
+
listingOutpoint: args.listingOutpoint,
|
|
163
|
+
destinationAddress: args.ordAddress,
|
|
164
|
+
price: listingData.data.list.price,
|
|
165
|
+
}),
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
} catch (err: unknown) {
|
|
170
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
171
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type { SecurityLevel } from "@bsv/sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// Define SecurityLevel to match the BSV SDK exactly
|
|
5
|
+
const SecurityLevelEnum = z.union([z.literal(0), z.literal(1), z.literal(2)]);
|
|
6
|
+
|
|
7
|
+
// Create a custom validator without tuples
|
|
8
|
+
export const walletProtocolSchema = z.custom<[SecurityLevel, string]>((val) => {
|
|
9
|
+
return (
|
|
10
|
+
Array.isArray(val) &&
|
|
11
|
+
val.length === 2 &&
|
|
12
|
+
(val[0] === 0 || val[0] === 1 || val[0] === 2) &&
|
|
13
|
+
typeof val[1] === "string"
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Empty args schema for functions that don't take arguments
|
|
18
|
+
export const emptyArgsSchema = z.object({});
|
|
19
|
+
|
|
20
|
+
// Get public key arguments
|
|
21
|
+
export const getPublicKeyArgsSchema = z.object({});
|
|
22
|
+
|
|
23
|
+
// Create signature arguments
|
|
24
|
+
export const createSignatureArgsSchema = z.object({
|
|
25
|
+
data: z.array(z.number()).optional(),
|
|
26
|
+
hashToDirectlySign: z.array(z.number()).optional(),
|
|
27
|
+
protocolID: walletProtocolSchema,
|
|
28
|
+
keyID: z.string(),
|
|
29
|
+
privilegedReason: z.string().optional(),
|
|
30
|
+
counterparty: z
|
|
31
|
+
.union([z.string(), z.literal("self"), z.literal("anyone")])
|
|
32
|
+
.optional(),
|
|
33
|
+
privileged: z.boolean().optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Verify signature arguments
|
|
37
|
+
export const verifySignatureArgsSchema = z.object({
|
|
38
|
+
data: z.array(z.number()).optional(),
|
|
39
|
+
hashToDirectlyVerify: z.array(z.number()).optional(),
|
|
40
|
+
signature: z.array(z.number()),
|
|
41
|
+
protocolID: walletProtocolSchema,
|
|
42
|
+
keyID: z.string(),
|
|
43
|
+
privilegedReason: z.string().optional(),
|
|
44
|
+
counterparty: z
|
|
45
|
+
.union([z.string(), z.literal("self"), z.literal("anyone")])
|
|
46
|
+
.optional(),
|
|
47
|
+
forSelf: z.boolean().optional(),
|
|
48
|
+
privileged: z.boolean().optional(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Wallet encryption args
|
|
52
|
+
export const walletEncryptArgsSchema = z.object({
|
|
53
|
+
plaintext: z.array(z.number()),
|
|
54
|
+
protocolID: walletProtocolSchema,
|
|
55
|
+
keyID: z.string(),
|
|
56
|
+
privilegedReason: z.string().optional(),
|
|
57
|
+
counterparty: z
|
|
58
|
+
.union([z.string(), z.literal("self"), z.literal("anyone")])
|
|
59
|
+
.optional(),
|
|
60
|
+
privileged: z.boolean().optional(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Wallet decryption args
|
|
64
|
+
export const walletDecryptArgsSchema = z.object({
|
|
65
|
+
ciphertext: z.array(z.number()),
|
|
66
|
+
protocolID: walletProtocolSchema,
|
|
67
|
+
keyID: z.string(),
|
|
68
|
+
privilegedReason: z.string().optional(),
|
|
69
|
+
counterparty: z
|
|
70
|
+
.union([z.string(), z.literal("self"), z.literal("anyone")])
|
|
71
|
+
.optional(),
|
|
72
|
+
privileged: z.boolean().optional(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Create HMAC arguments
|
|
76
|
+
export const createHmacArgsSchema = z.object({
|
|
77
|
+
message: z.string(),
|
|
78
|
+
encoding: z.enum(["utf8", "hex", "base64"]).optional(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Verify HMAC arguments
|
|
82
|
+
export const verifyHmacArgsSchema = z.object({
|
|
83
|
+
message: z.string(),
|
|
84
|
+
hmac: z.string(),
|
|
85
|
+
publicKey: z.string(),
|
|
86
|
+
encoding: z.enum(["utf8", "hex", "base64"]).optional(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Transaction input schema
|
|
90
|
+
export const transactionInputSchema = z.object({
|
|
91
|
+
outpoint: z.string(),
|
|
92
|
+
inputDescription: z.string(), // Required field
|
|
93
|
+
sequence: z.number().optional(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Transaction output schema
|
|
97
|
+
export const transactionOutputSchema = z.object({
|
|
98
|
+
lockingScript: z.string(),
|
|
99
|
+
satoshis: z.number(),
|
|
100
|
+
outputDescription: z.string(), // Required field
|
|
101
|
+
change: z.boolean().optional(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Create action args
|
|
105
|
+
export const createActionArgsSchema = z.object({
|
|
106
|
+
description: z.string(),
|
|
107
|
+
labels: z.array(z.string()).optional(),
|
|
108
|
+
lockTime: z.number().optional(),
|
|
109
|
+
version: z.number().optional(),
|
|
110
|
+
inputBEEF: z.array(z.number()).optional(),
|
|
111
|
+
inputs: z.array(transactionInputSchema).optional(),
|
|
112
|
+
outputs: z.array(transactionOutputSchema).optional(),
|
|
113
|
+
options: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Sign action args
|
|
117
|
+
export const signActionArgsSchema = z.object({
|
|
118
|
+
reference: z.string(),
|
|
119
|
+
spends: z.record(z.any()).optional(),
|
|
120
|
+
options: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// List actions args
|
|
124
|
+
export const listActionsArgsSchema = z.object({
|
|
125
|
+
labels: z.array(z.string()),
|
|
126
|
+
labelQueryMode: z.enum(["any", "all"]).optional(),
|
|
127
|
+
limit: z.number().optional(),
|
|
128
|
+
offset: z.number().optional(),
|
|
129
|
+
includeInputs: z.boolean().optional(),
|
|
130
|
+
includeOutputs: z.boolean().optional(),
|
|
131
|
+
includeLabels: z.boolean().optional(),
|
|
132
|
+
includeInputSourceLockingScripts: z.boolean().optional(),
|
|
133
|
+
includeInputUnlockingScripts: z.boolean().optional(),
|
|
134
|
+
seekPermission: z.boolean().optional(),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// List outputs args
|
|
138
|
+
export const listOutputsArgsSchema = z.object({
|
|
139
|
+
basket: z.string(),
|
|
140
|
+
tags: z.array(z.string()).optional(),
|
|
141
|
+
tagQueryMode: z.enum(["all", "any"]).optional(),
|
|
142
|
+
limit: z.number().optional(),
|
|
143
|
+
offset: z.number().optional(),
|
|
144
|
+
include: z.enum(["locking scripts", "entire transactions"]).optional(),
|
|
145
|
+
includeLabels: z.boolean().optional(),
|
|
146
|
+
includeTags: z.boolean().optional(),
|
|
147
|
+
includeCustomInstructions: z.boolean().optional(),
|
|
148
|
+
seekPermission: z.boolean().optional(),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Reveal counterparty key linkage args
|
|
152
|
+
export const revealCounterpartyKeyLinkageArgsSchema = z.object({
|
|
153
|
+
counterparty: z.string(),
|
|
154
|
+
verifier: z.string(),
|
|
155
|
+
privileged: z.boolean().optional(),
|
|
156
|
+
privilegedReason: z.string().optional(),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Reveal specific key linkage args
|
|
160
|
+
export const revealSpecificKeyLinkageArgsSchema = z.object({
|
|
161
|
+
keyID: z.number(),
|
|
162
|
+
verifier: z.string(),
|
|
163
|
+
privileged: z.boolean().optional(),
|
|
164
|
+
privilegedReason: z.string().optional(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Abort action args
|
|
168
|
+
export const abortActionArgsSchema = z.object({
|
|
169
|
+
reference: z.string(),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Internalize action args
|
|
173
|
+
export const internalizeActionArgsSchema = z.object({
|
|
174
|
+
tx: z.array(z.number()),
|
|
175
|
+
outputs: z.array(
|
|
176
|
+
z.object({
|
|
177
|
+
outputIndex: z.number(),
|
|
178
|
+
protocol: z.enum(["wallet payment", "basket insertion"]),
|
|
179
|
+
lockingScript: z.string(),
|
|
180
|
+
satoshis: z.number(),
|
|
181
|
+
}),
|
|
182
|
+
),
|
|
183
|
+
description: z.string(),
|
|
184
|
+
labels: z.array(z.string()).optional(),
|
|
185
|
+
seekPermission: z.boolean().optional(),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Relinquish output args
|
|
189
|
+
export const relinquishOutputArgsSchema = z.object({
|
|
190
|
+
basket: z.string(),
|
|
191
|
+
output: z.string(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Acquire certificate args
|
|
195
|
+
export const acquireCertificateArgsSchema = z.object({
|
|
196
|
+
type: z.string(),
|
|
197
|
+
certifier: z.string(),
|
|
198
|
+
acquisitionProtocol: z.enum(["direct", "issuance"]),
|
|
199
|
+
fields: z.record(z.string()),
|
|
200
|
+
certifierUrl: z.string().optional(),
|
|
201
|
+
serialNumber: z.string().optional(),
|
|
202
|
+
signature: z.string().optional(),
|
|
203
|
+
revocationOutpoint: z.string().optional(),
|
|
204
|
+
keyringForSubject: z.record(z.string()).optional(),
|
|
205
|
+
keyringRevealer: z.string().optional(),
|
|
206
|
+
privileged: z.boolean().optional(),
|
|
207
|
+
privilegedReason: z.string().optional(),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// List certificates args
|
|
211
|
+
export const listCertificatesArgsSchema = z.object({
|
|
212
|
+
certifiers: z.array(z.string()),
|
|
213
|
+
types: z.array(z.string()),
|
|
214
|
+
limit: z.number().optional(),
|
|
215
|
+
offset: z.number().optional(),
|
|
216
|
+
privileged: z.boolean().optional(),
|
|
217
|
+
privilegedReason: z.string().optional(),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Prove certificate args
|
|
221
|
+
export const proveCertificateArgsSchema = z.object({
|
|
222
|
+
certificate: z.object({}),
|
|
223
|
+
fieldsToReveal: z.array(z.string()),
|
|
224
|
+
verifier: z.string(),
|
|
225
|
+
privileged: z.boolean().optional(),
|
|
226
|
+
privilegedReason: z.string().optional(),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Relinquish certificate args
|
|
230
|
+
export const relinquishCertificateArgsSchema = z.object({
|
|
231
|
+
type: z.string(),
|
|
232
|
+
serialNumber: z.string(),
|
|
233
|
+
certifier: z.string(),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Discover by identity key args
|
|
237
|
+
export const discoverByIdentityKeyArgsSchema = z.object({
|
|
238
|
+
identityKey: z.string(),
|
|
239
|
+
limit: z.number().optional(),
|
|
240
|
+
offset: z.number().optional(),
|
|
241
|
+
seekPermission: z.boolean().optional(),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Discover by attributes args
|
|
245
|
+
export const discoverByAttributesArgsSchema = z.object({
|
|
246
|
+
attributes: z.record(z.string()),
|
|
247
|
+
limit: z.number().optional(),
|
|
248
|
+
offset: z.number().optional(),
|
|
249
|
+
seekPermission: z.boolean().optional(),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Get header for height args
|
|
253
|
+
export const getHeaderArgsSchema = z.object({
|
|
254
|
+
height: z.number(),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Get address args
|
|
258
|
+
export const getAddressArgsSchema = z.object({});
|
|
259
|
+
|
|
260
|
+
// Send to address args
|
|
261
|
+
export const sendToAddressArgsSchema = z.object({
|
|
262
|
+
address: z.string(),
|
|
263
|
+
amount: z.number(),
|
|
264
|
+
currency: z.enum(["BSV", "USD"]).optional(),
|
|
265
|
+
description: z.string().optional(),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Schema for purchase listing arguments
|
|
270
|
+
*/
|
|
271
|
+
export const purchaseListingArgsSchema = z.object({
|
|
272
|
+
listingOutpoint: z
|
|
273
|
+
.string()
|
|
274
|
+
.describe("The outpoint of the listing to purchase (txid_vout format)"),
|
|
275
|
+
ordAddress: z
|
|
276
|
+
.string()
|
|
277
|
+
.describe("The ordinal address to receive the purchased item"),
|
|
278
|
+
description: z
|
|
279
|
+
.string()
|
|
280
|
+
.optional()
|
|
281
|
+
.describe("Optional description for the transaction"),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
export type PurchaseListingArgs = z.infer<typeof purchaseListingArgsSchema>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { P2PKH } from "@bsv/sdk";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
|
4
|
+
import type { z } from "zod";
|
|
5
|
+
import { sendToAddressArgsSchema } from "./schemas";
|
|
6
|
+
import type { Wallet } from "./wallet";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fetch the current BSV price from whatsonchain API
|
|
10
|
+
* @returns The BSV price in USD
|
|
11
|
+
*/
|
|
12
|
+
async function getBsvPrice(): Promise<number> {
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(
|
|
15
|
+
"https://api.whatsonchain.com/v1/bsv/main/exchangerate",
|
|
16
|
+
);
|
|
17
|
+
if (!res.ok) throw new Error("Failed to fetch BSV price");
|
|
18
|
+
|
|
19
|
+
// Parse the response with proper type casting
|
|
20
|
+
const data = (await res.json()) as {
|
|
21
|
+
rate: string;
|
|
22
|
+
currency: string;
|
|
23
|
+
time: number;
|
|
24
|
+
};
|
|
25
|
+
const price = Number(data.rate);
|
|
26
|
+
|
|
27
|
+
if (Number.isNaN(price) || price <= 0) throw new Error("Invalid BSV price");
|
|
28
|
+
return price;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error("BSV price fetch error:", error);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Use the schema imported from schemas.ts
|
|
36
|
+
export type SendToAddressArgs = z.infer<typeof sendToAddressArgsSchema>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register the sendToAddress tool
|
|
40
|
+
*/
|
|
41
|
+
export function registerSendToAddressTool(server: McpServer, wallet: Wallet) {
|
|
42
|
+
server.tool(
|
|
43
|
+
"wallet_sendToAddress",
|
|
44
|
+
{
|
|
45
|
+
args: sendToAddressArgsSchema,
|
|
46
|
+
},
|
|
47
|
+
async (
|
|
48
|
+
{ args }: { args: SendToAddressArgs },
|
|
49
|
+
extra: RequestHandlerExtra,
|
|
50
|
+
) => {
|
|
51
|
+
try {
|
|
52
|
+
const {
|
|
53
|
+
address,
|
|
54
|
+
amount,
|
|
55
|
+
currency = "BSV",
|
|
56
|
+
description = "Send to address",
|
|
57
|
+
} = args;
|
|
58
|
+
|
|
59
|
+
// Convert USD to satoshis if needed
|
|
60
|
+
let satoshis = amount;
|
|
61
|
+
if (currency === "USD") {
|
|
62
|
+
// Get current BSV price
|
|
63
|
+
const bsvPriceUsd = await getBsvPrice();
|
|
64
|
+
|
|
65
|
+
// Convert USD to BSV, then to satoshis
|
|
66
|
+
satoshis = Math.floor((amount / bsvPriceUsd) * 100000000);
|
|
67
|
+
} else {
|
|
68
|
+
// Convert BSV to satoshis
|
|
69
|
+
satoshis = Math.floor(amount * 100000000);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Create P2PKH script from address
|
|
73
|
+
const lockingScript = new P2PKH().lock(address);
|
|
74
|
+
|
|
75
|
+
// Create the transaction
|
|
76
|
+
const tx = await wallet.createAction({
|
|
77
|
+
description,
|
|
78
|
+
outputs: [
|
|
79
|
+
{
|
|
80
|
+
lockingScript: lockingScript.toHex(),
|
|
81
|
+
satoshis,
|
|
82
|
+
outputDescription: `Payment to ${address}`,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: JSON.stringify({
|
|
92
|
+
status: "success",
|
|
93
|
+
txid: tx.txid,
|
|
94
|
+
satoshis,
|
|
95
|
+
}),
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: "text",
|
|
104
|
+
text: error instanceof Error ? error.message : String(error),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
isError: true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { PrivateKey } from "@bsv/sdk";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
|
5
|
+
import { getPublicKeyArgsSchema } from "./schemas";
|
|
6
|
+
import { registerWalletTools } from "./tools";
|
|
7
|
+
import { Wallet } from "./wallet";
|
|
8
|
+
|
|
9
|
+
// Define type for tool names to ensure they match what's in tools.ts
|
|
10
|
+
type WalletToolName =
|
|
11
|
+
| "wallet_getPublicKey"
|
|
12
|
+
| "wallet_createSignature"
|
|
13
|
+
| "wallet_verifySignature"
|
|
14
|
+
| "wallet_encrypt"
|
|
15
|
+
| "wallet_decrypt";
|
|
16
|
+
|
|
17
|
+
const toolNames: WalletToolName[] = [
|
|
18
|
+
"wallet_getPublicKey",
|
|
19
|
+
"wallet_createSignature",
|
|
20
|
+
"wallet_verifySignature",
|
|
21
|
+
"wallet_encrypt",
|
|
22
|
+
"wallet_decrypt",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Helper function to get dummy arguments for each tool
|
|
26
|
+
function getDummyArgs(tool: WalletToolName): Record<string, unknown> {
|
|
27
|
+
switch (tool) {
|
|
28
|
+
case "wallet_getPublicKey":
|
|
29
|
+
return getPublicKeyArgsSchema.parse({});
|
|
30
|
+
case "wallet_createSignature":
|
|
31
|
+
return { data: "test", keyType: "identity" };
|
|
32
|
+
case "wallet_verifySignature":
|
|
33
|
+
return {
|
|
34
|
+
data: "test",
|
|
35
|
+
keyType: "identity",
|
|
36
|
+
signature: "sig",
|
|
37
|
+
publicKey: "pubkey",
|
|
38
|
+
};
|
|
39
|
+
case "wallet_encrypt":
|
|
40
|
+
return { data: "test", publicKey: "pubkey" };
|
|
41
|
+
case "wallet_decrypt":
|
|
42
|
+
return { encryptedData: "data" };
|
|
43
|
+
default:
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Bun test
|
|
49
|
+
for (const tool of toolNames) {
|
|
50
|
+
test(`tool ${tool} returns not implemented error`, async () => {
|
|
51
|
+
const server = new McpServer({ name: "Test", version: "0.0.1" });
|
|
52
|
+
const wallet = new Wallet(
|
|
53
|
+
PrivateKey.fromWif(
|
|
54
|
+
"KyqU1boXYdksJKyxfsCtvBxfbt2a8XQd2aPhVZHNxMzvms9hRAvz",
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
const handlers = registerWalletTools(server, wallet);
|
|
58
|
+
|
|
59
|
+
// Get the handler for this tool
|
|
60
|
+
const handler = handlers[tool];
|
|
61
|
+
if (!handler) {
|
|
62
|
+
throw new Error(`Tool ${tool} not registered`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Create a mock RequestHandlerExtra with required properties
|
|
66
|
+
const mockExtra: RequestHandlerExtra = {
|
|
67
|
+
signal: new AbortController().signal,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Call the handler with dummy arguments
|
|
71
|
+
const result = await handler({ args: getDummyArgs(tool) }, mockExtra);
|
|
72
|
+
|
|
73
|
+
expect(result.isError).toBe(true);
|
|
74
|
+
expect(result.content?.[0]?.text ?? "").toMatch(/not implemented/i);
|
|
75
|
+
});
|
|
76
|
+
}
|