bsv-mcp 0.2.9 → 0.2.10

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.
@@ -1,326 +1,125 @@
1
- import { PrivateKey } from "@bsv/sdk";
2
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4
- import {
5
- type ChangeResult,
6
- type ExistingListing,
7
- type LocalSigner,
8
- oneSatBroadcaster,
9
- type Payment,
10
- type PurchaseOrdListingConfig,
11
- purchaseOrdListing,
12
- purchaseOrdTokenListing,
13
- type Royalty,
14
- TokenType,
15
- type TokenUtxo,
16
- type Utxo,
17
- } from "js-1sat-ord";
1
+ import type { OneSatContext } from '@1sat/actions'
2
+ import { purchaseBsv21, purchaseOrdinal } from '@1sat/actions'
3
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
4
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
18
5
  import {
19
6
  MARKET_FEE_PERCENTAGE,
20
7
  MARKET_WALLET_ADDRESS,
21
8
  MINIMUM_MARKET_FEE_SATOSHIS,
22
- } from "../constants";
23
- import { purchaseListingArgsSchema } from "./schemas";
24
- import type { Wallet } from "./wallet";
25
-
26
- // Define types for 1Sat API response
27
- interface OrdUtxo {
28
- txid: string;
29
- vout: number;
30
- satoshis: number;
31
- script: string;
32
- origin?: {
33
- outpoint: string;
34
- data?: {
35
- map?: {
36
- royalties?: string;
37
- [key: string]: string | number | boolean | null | undefined;
38
- };
39
- insc?: {
40
- text?: string;
41
- file?: {
42
- type?: string;
43
- size?: number;
44
- };
45
- };
46
- };
47
- };
48
- data?: {
49
- list?: {
50
- price: number;
51
- payout: string;
52
- };
53
- bsv20?: {
54
- amt?: string;
55
- tick?: string;
56
- id?: string;
57
- };
58
- };
59
- }
60
-
61
- /**
62
- * Register the purchaseListing tool
63
- *
64
- * This tool enables purchasing listed ordinals (NFTs or tokens) from the marketplace:
65
- * 1. Parses the listing outpoint to get the txid and vout
66
- * 2. Fetches the listing UTXO from the ordinals API
67
- * 3. Gets the wallet's payment UTXOs (using the wallet's internal UTXO management)
68
- * 4. Uses purchaseOrdListing or purchaseOrdTokenListing based on the listing type
69
- * 5. For NFTs, automatically detects and processes royalty payments to original creators
70
- * 6. Broadcasts the transaction
71
- * 7. Returns the transaction details including success status and txid
72
- *
73
- * The tool supports both NFT and token listings with appropriate type-specific handling.
74
- * Royalty payments are supported for NFT purchases only (based on creator-defined metadata).
75
- */
76
- export function registerPurchaseListingTool(server: McpServer, wallet: Wallet) {
77
- // Store a reference to check if wallet is persistent
9
+ } from '../constants'
10
+ import { purchaseListingArgsSchema } from './schemas'
78
11
 
12
+ export function registerPurchaseListingTool(
13
+ server: McpServer,
14
+ ctx: OneSatContext | undefined,
15
+ ) {
79
16
  server.tool(
80
- "wallet_purchaseListing",
81
- "Purchases a listing from the Bitcoin SV ordinals marketplace. Supports both NFT purchases (with royalty payments to original creators) and BSV-20/BSV-21 token purchases. The tool handles all aspects of the transaction - from fetching listing details, calculating fees, creating and broadcasting the transaction.",
17
+ 'wallet_purchaseListing',
18
+ 'Purchases a listing from the Bitcoin SV ordinals marketplace. Supports both NFT purchases and BSV21 token purchases.',
82
19
  { ...purchaseListingArgsSchema.shape },
83
- async ({ listingOutpoint, ordAddress, listingType, tokenProtocol, tokenID, description }): Promise<CallToolResult> => {
84
- try {
85
- // Load optional identity key for sigma signing
86
- const identityKeyWif = process.env.IDENTITY_KEY_WIF;
87
- let identityPk: PrivateKey | undefined;
88
- if (identityKeyWif) {
89
- try {
90
- identityPk = PrivateKey.fromWif(identityKeyWif);
91
- } catch (e) {
92
- console.warn(
93
- "Warning: Invalid IDENTITY_KEY_WIF environment variable; sigma signing disabled",
94
- e,
95
- );
96
- }
97
- }
98
-
99
- // Fetch the listing info directly from the API
100
- const response = await fetch(
101
- `https://ordinals.gorillapool.io/api/txos/${listingOutpoint}?script=true`,
102
- );
103
- if (!response.ok) {
104
- throw new Error(
105
- `Failed to fetch listing data: ${response.statusText}`,
106
- );
107
- }
108
-
109
- const listingData = (await response.json()) as OrdUtxo;
110
-
111
- // Check if the listing is valid and has a price
112
- if (!listingData.data?.list?.price) {
113
- throw new Error("Listing is either not for sale or invalid");
114
- }
115
-
116
- // Check if payout is available
117
- if (!listingData.data.list.payout) {
118
- throw new Error("Listing doesn't have payout information");
119
- }
120
-
121
- // Calculate the market fee (3% of listing price)
122
- const listingPrice = listingData.data.list.price;
123
- let marketFee = Math.round(listingPrice * MARKET_FEE_PERCENTAGE);
124
-
125
- // Ensure minimum fee
126
- if (marketFee < MINIMUM_MARKET_FEE_SATOSHIS) {
127
- marketFee = MINIMUM_MARKET_FEE_SATOSHIS;
128
- }
129
-
130
- // Parse the listing outpoint to get txid and vout
131
- const [txid, voutStr] = listingOutpoint.split("_");
132
- if (!txid) {
133
- throw new Error("Invalid outpoint format. Expected txid_vout");
134
- }
135
- const vout = Number.parseInt(voutStr || "0", 10);
136
-
137
- // Get private key from the wallet
138
- const paymentPk = wallet.getPrivateKey();
139
- if (!paymentPk) {
140
- throw new Error("No private key available in wallet");
141
- }
142
-
143
- // Get payment address
144
- const paymentAddress = paymentPk.toAddress().toString();
145
-
146
- // Get payment UTXOs from the wallet's managed UTXOs
147
- const { paymentUtxos } = await wallet.getUtxos();
148
- if (!paymentUtxos || paymentUtxos.length === 0) {
149
- // Provide more helpful error message with instructions
150
- throw new Error(
151
- `No payment UTXOs available for address ${paymentAddress}.
152
- Please fund this wallet address with enough BSV to cover the purchase price
153
- (${listingData.data.list.price} satoshis) plus market fee (${marketFee} satoshis) and transaction fees.`,
154
- );
20
+ async ({ listingOutpoint, listingType, tokenID, tokenAmount, description }): Promise<CallToolResult> => {
21
+ if (!ctx) {
22
+ return {
23
+ content: [
24
+ {
25
+ type: 'text',
26
+ text: 'Wallet not initialized. Please configure a wallet before purchasing.',
27
+ },
28
+ ],
29
+ isError: true,
155
30
  }
31
+ }
156
32
 
157
- // Define market fee payment
158
- const additionalPayments: Payment[] = [
159
- {
160
- to: MARKET_WALLET_ADDRESS,
161
- amount: marketFee,
162
- },
163
- ];
164
-
165
- // Define metadata for the transaction
166
- const metaData = {
167
- app: "bsv-mcp",
168
- type: "ord",
169
- op: "purchase",
170
- };
171
-
172
- // Create the purchase transaction based on listing type
173
- let transaction: ChangeResult;
174
-
175
- if (listingType === "token") {
176
- if (!tokenProtocol) {
177
- throw new Error("tokenProtocol is required for token listings");
178
- }
33
+ try {
34
+ let marketplaceRate = MARKET_FEE_PERCENTAGE
179
35
 
36
+ if (listingType === 'token') {
180
37
  if (!tokenID) {
181
- throw new Error("tokenID is required for token listings");
38
+ throw new Error('tokenID is required for token listings')
182
39
  }
183
40
 
184
- // Validate token data from the listing
185
- if (!listingData.data.bsv20) {
186
- throw new Error("This is not a valid BSV-20 token listing");
187
- }
41
+ if (!tokenAmount) {
42
+ const response = await fetch(
43
+ `https://ordinals.gorillapool.io/api/txos/${listingOutpoint}?script=true`,
44
+ )
45
+ if (!response.ok) {
46
+ throw new Error(`Failed to fetch listing data: ${response.statusText}`)
47
+ }
48
+ const listingData = (await response.json()) as {
49
+ data?: { list?: { price: number }; bsv20?: { amt?: string } }
50
+ }
51
+ if (!listingData.data?.bsv20?.amt) {
52
+ throw new Error('Token listing does not have an amount')
53
+ }
54
+ tokenAmount = listingData.data.bsv20.amt
188
55
 
189
- // For BSV-20, the amount should be included in the listing data
190
- if (!listingData.data.bsv20.amt) {
191
- throw new Error("Token listing doesn't have an amount specified");
56
+ if (listingData.data?.list?.price) {
57
+ const fee = Math.round(listingData.data.list.price * MARKET_FEE_PERCENTAGE)
58
+ marketplaceRate = Math.max(fee, MINIMUM_MARKET_FEE_SATOSHIS) / listingData.data.list.price
59
+ }
192
60
  }
193
61
 
194
- // Convert the token protocol to the enum type expected by js-1sat-ord
195
- const protocol =
196
- tokenProtocol === "bsv-20" ? TokenType.BSV20 : TokenType.BSV21;
197
-
198
- // Create a TokenUtxo with the required fields
199
- const listingUtxo: TokenUtxo = {
200
- txid,
201
- vout,
202
- script: listingData.script,
203
- satoshis: 1, // TokenUtxo's satoshis must be exactly 1
204
- amt: listingData.data.bsv20.amt,
205
- id: tokenID,
206
- payout: listingData.data.list.payout,
207
- };
208
-
209
- transaction = await purchaseOrdTokenListing({
210
- protocol,
211
- tokenID: tokenID,
212
- utxos: paymentUtxos,
213
- paymentPk,
214
- listingUtxo,
215
- ordAddress: ordAddress,
216
- additionalPayments,
217
- metaData,
218
- });
219
- } else {
220
- // Create a regular Utxo for NFT listing
221
- const listingUtxo: Utxo = {
222
- txid,
223
- vout,
224
- script: listingData.script,
225
- satoshis: listingData.satoshis,
226
- };
227
-
228
- // Create the ExistingListing object for NFT listings
229
- const listing: ExistingListing = {
230
- payout: listingData.data.list.payout,
231
- listingUtxo,
232
- };
233
-
234
- // Check for royalties in the NFT origin data
235
- // Royalties are only supported for NFTs, not for tokens
236
- // The royalties are defined by the original creator as a JSON string
237
- // in the NFT's metadata and parsed into a Royalty[] array
238
- let royalties: Royalty[] = [];
239
- if (listingData.origin?.data?.map?.royalties) {
240
- try {
241
- royalties = JSON.parse(listingData.origin.data.map.royalties);
242
- } catch (error) {
243
- // Remove console.warn
62
+ const result = await purchaseBsv21.execute(ctx, {
63
+ tokenId: tokenID,
64
+ outpoint: listingOutpoint,
65
+ amount: tokenAmount,
66
+ marketplaceAddress: MARKET_WALLET_ADDRESS,
67
+ marketplaceRate,
68
+ })
69
+
70
+ if (result.error) {
71
+ return {
72
+ content: [{ type: 'text', text: result.error }],
73
+ isError: true,
244
74
  }
245
75
  }
246
76
 
247
- const purchaseOrdListingConfig: PurchaseOrdListingConfig = {
248
- utxos: paymentUtxos,
249
- paymentPk,
250
- ordAddress: ordAddress,
251
- listing,
252
- additionalPayments,
253
- metaData,
254
- royalties,
255
- };
256
-
257
- transaction = await purchaseOrdListing(purchaseOrdListingConfig);
258
- }
259
-
260
- // After successful transaction creation, refresh the wallet's UTXOs
261
- // This ensures the wallet doesn't try to reuse spent UTXOs
262
- try {
263
- await wallet.refreshUtxos();
264
- } catch (refreshError) {
265
- // Remove console.warn
266
- }
267
-
268
- // Optionally sign with identity key then broadcast the transaction
269
-
270
- const disableBroadcasting = process.env.DISABLE_BROADCASTING === "true";
271
- if (!disableBroadcasting) {
272
- const broadcastResult = await transaction.tx.broadcast(
273
- oneSatBroadcaster(),
274
- );
275
- // Handle broadcast response
276
- const resultStatus =
277
- typeof broadcastResult === "object" && "status" in broadcastResult
278
- ? broadcastResult.status
279
- : "unknown";
280
-
281
- const resultMessage =
282
- typeof broadcastResult === "object" && "error" in broadcastResult
283
- ? broadcastResult.error
284
- : "Transaction broadcast successful";
285
-
286
77
  return {
287
78
  content: [
288
79
  {
289
- type: "text",
80
+ type: 'text',
290
81
  text: JSON.stringify({
291
- status: resultStatus,
292
- message: resultMessage,
293
- txid: transaction.tx.id("hex"),
294
- listingOutpoint: listingOutpoint,
295
- destinationAddress: ordAddress,
296
- listingType: listingType,
297
- tokenProtocol: tokenID ? tokenProtocol : undefined,
298
- tokenID: tokenID,
299
- price: listingData.data.list.price,
300
- marketFee,
301
- marketFeeAddress: MARKET_WALLET_ADDRESS,
302
- royaltiesPaid:
303
- listingType === "nft" &&
304
- listingData.origin?.data?.map?.royalties
305
- ? JSON.parse(listingData.origin.data.map.royalties)
306
- : undefined,
82
+ status: 'success',
83
+ txid: result.txid,
84
+ listingOutpoint,
85
+ listingType,
86
+ tokenID,
307
87
  }),
308
88
  },
309
89
  ],
310
- };
90
+ }
311
91
  }
92
+
93
+ const result = await purchaseOrdinal.execute(ctx, {
94
+ outpoint: listingOutpoint,
95
+ marketplaceAddress: MARKET_WALLET_ADDRESS,
96
+ marketplaceRate,
97
+ })
98
+
99
+ if (result.error) {
100
+ return {
101
+ content: [{ type: 'text', text: result.error }],
102
+ isError: true,
103
+ }
104
+ }
105
+
312
106
  return {
313
107
  content: [
314
108
  {
315
- type: "text",
316
- text: transaction.tx.toHex(),
109
+ type: 'text',
110
+ text: JSON.stringify({
111
+ status: 'success',
112
+ txid: result.txid,
113
+ listingOutpoint,
114
+ listingType,
115
+ }),
317
116
  },
318
117
  ],
319
- };
118
+ }
320
119
  } catch (err: unknown) {
321
- const msg = err instanceof Error ? err.message : String(err);
322
- return { content: [{ type: "text", text: msg }], isError: true };
120
+ const msg = err instanceof Error ? err.message : String(err)
121
+ return { content: [{ type: 'text', text: msg }], isError: true }
323
122
  }
324
123
  },
325
- );
124
+ )
326
125
  }
@@ -64,27 +64,23 @@ export const purchaseListingArgsSchema = z
64
64
  listingOutpoint: z
65
65
  .string()
66
66
  .describe("The outpoint of the listing to purchase (txid_vout format)"),
67
- ordAddress: z
68
- .string()
69
- .describe("The ordinal address to receive the purchased item"),
70
67
  listingType: z
71
68
  .enum(["nft", "token"])
72
69
  .default("nft")
73
70
  .describe(
74
- "Type of listing: 'nft' for ordinal NFTs, 'token' for BSV-20 tokens",
71
+ "Type of listing: 'nft' for ordinal NFTs, 'token' for BSV21 tokens",
75
72
  ),
76
- tokenProtocol: z
77
- .enum(["bsv-20", "bsv-21"])
73
+ tokenID: z
74
+ .string()
78
75
  .optional()
79
- .default("bsv-21")
80
76
  .describe(
81
- "Token protocol for token listings (required when listingType is 'token')",
77
+ "Token ID (txid_vout of deploy transaction) — required when listingType is 'token'",
82
78
  ),
83
- tokenID: z
79
+ tokenAmount: z
84
80
  .string()
85
81
  .optional()
86
82
  .describe(
87
- "Token ID for BSV-21 tokens or ticker for BSV-20 tokens (required when listingType is 'token')",
83
+ "Amount of tokens in the listing (as string) — required when listingType is 'token'",
88
84
  ),
89
85
  description: z
90
86
  .string()
@@ -92,7 +88,7 @@ export const purchaseListingArgsSchema = z
92
88
  .describe("Optional description for the transaction"),
93
89
  })
94
90
  .describe(
95
- "Schema for the wallet_purchaseListing tool arguments (purchase NFTs or tokens), with detailed field descriptions.",
91
+ "Schema for the wallet_purchaseListing tool arguments (purchase NFTs or BSV21 tokens).",
96
92
  );
97
93
 
98
94
  // Export types
@@ -1,75 +1,74 @@
1
- import { P2PKH } from "@bsv/sdk";
2
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { toSatoshi } from "satoshi-token";
4
- import { getBsvPriceWithCache } from "../bsv/getPrice";
5
- import { sendToAddressArgsSchema } from "./schemas";
6
- import type { Wallet } from "./wallet";
1
+ import type { OneSatContext } from '@1sat/actions'
2
+ import { sendBsv } from '@1sat/actions'
3
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
4
+ import { toSatoshi } from 'satoshi-token'
5
+ import { getBsvPriceWithCache } from '../bsv/getPrice'
6
+ import { sendToAddressArgsSchema } from './schemas'
7
7
 
8
- /**
9
- * Register the sendToAddress tool
10
- */
11
- export function registerSendToAddressTool(server: McpServer, wallet: Wallet) {
8
+ export function registerSendToAddressTool(
9
+ server: McpServer,
10
+ ctx: OneSatContext | undefined,
11
+ ) {
12
12
  server.tool(
13
- "wallet_sendToAddress",
14
- "Sends Bitcoin SV (BSV) to a specified address. This tool supports payments in both BSV and USD amounts (with automatic conversion using current exchange rates). Transaction fees are automatically calculated and a confirmation with transaction ID is returned upon success.",
13
+ 'wallet_sendToAddress',
14
+ 'Sends Bitcoin SV (BSV) to a specified address. This tool supports payments in both BSV and USD amounts (with automatic conversion using current exchange rates). Transaction fees are automatically calculated and a confirmation with transaction ID is returned upon success.',
15
15
  { ...sendToAddressArgsSchema.shape },
16
- async ({ address, amount, currency = "BSV", description = "Send to address" }) => {
17
- try {
18
-
19
- // Convert to satoshis
20
- let satoshis: number;
21
- if (currency === "USD") {
22
- // Get current BSV price
23
- const bsvPriceUsd = await getBsvPriceWithCache();
24
-
25
- // Convert USD to BSV
26
- const bsvAmount = amount / bsvPriceUsd;
16
+ async ({ address, amount, currency = 'BSV', description = 'Send to address' }) => {
17
+ if (!ctx) {
18
+ return {
19
+ content: [
20
+ {
21
+ type: 'text',
22
+ text: 'Wallet not initialized. Please configure a wallet before sending.',
23
+ },
24
+ ],
25
+ isError: true,
26
+ }
27
+ }
27
28
 
28
- // Convert BSV to satoshis using the library
29
- satoshis = toSatoshi(bsvAmount);
29
+ try {
30
+ let satoshis: number
31
+ if (currency === 'USD') {
32
+ const bsvPriceUsd = await getBsvPriceWithCache()
33
+ satoshis = toSatoshi(amount / bsvPriceUsd)
30
34
  } else {
31
- // Convert BSV to satoshis using the library
32
- satoshis = toSatoshi(amount);
35
+ satoshis = toSatoshi(amount)
33
36
  }
34
37
 
35
- // Create P2PKH script from address
36
- const lockingScript = new P2PKH().lock(address);
38
+ const result = await sendBsv.execute(ctx, {
39
+ requests: [{ address, satoshis }],
40
+ })
37
41
 
38
- // Create the transaction
39
- const tx = await wallet.createAction({
40
- description,
41
- outputs: [
42
- {
43
- lockingScript: lockingScript.toHex(),
44
- satoshis,
45
- outputDescription: `Payment to ${address}`,
46
- },
47
- ],
48
- });
42
+ if (result.error) {
43
+ return {
44
+ content: [{ type: 'text', text: result.error }],
45
+ isError: true,
46
+ }
47
+ }
49
48
 
50
49
  return {
51
50
  content: [
52
51
  {
53
- type: "text",
52
+ type: 'text',
54
53
  text: JSON.stringify({
55
- status: "success",
56
- txid: tx.txid,
54
+ status: 'success',
55
+ txid: result.txid,
57
56
  satoshis,
58
57
  }),
59
58
  },
60
59
  ],
61
- };
60
+ }
62
61
  } catch (error) {
63
62
  return {
64
63
  content: [
65
64
  {
66
- type: "text",
65
+ type: 'text',
67
66
  text: error instanceof Error ? error.message : String(error),
68
67
  },
69
68
  ],
70
69
  isError: true,
71
- };
70
+ }
72
71
  }
73
72
  },
74
- );
73
+ )
75
74
  }