transferOrdToken.ts•4.27 kB
import { PrivateKey } from "@bsv/sdk";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
import type {
ServerNotification,
ServerRequest,
} from "@modelcontextprotocol/sdk/types.js";
import {
type Distribution,
type LocalSigner,
type Payment,
type TokenChangeResult,
TokenInputMode,
TokenSelectionStrategy,
TokenType,
type TokenUtxo,
type TransferOrdTokensConfig,
type Utxo,
selectTokenUtxos,
transferOrdTokens,
} from "js-1sat-ord";
import { z } from "zod";
import type { Wallet } from "./wallet";
// Schema for BSV-20/BSV-21 token transfer arguments
export const transferOrdTokenArgsSchema = z.object({
protocol: z.enum(["bsv-20", "bsv-21"]),
tokenID: z.string(),
sendAmount: z.number(),
paymentUtxos: z.array(
z.object({
txid: z.string(),
vout: z.number(),
satoshis: z.number(),
script: z.string(),
}),
),
tokenUtxos: z.array(
z.object({
txid: z.string(),
vout: z.number(),
satoshis: z.literal(1),
script: z.string(),
amt: z.string(),
id: z.string(),
}),
),
distributions: z.array(z.object({ address: z.string(), tokens: z.number() })),
decimals: z.number(),
additionalPayments: z
.array(z.object({ to: z.string(), amount: z.number() }))
.optional(),
});
export type TransferOrdTokenArgs = z.infer<typeof transferOrdTokenArgsSchema>;
/**
* Register the wallet_transferOrdToken tool for transferring BSV tokens.
*/
export function registerTransferOrdTokenTool(
server: McpServer,
wallet: Wallet,
) {
server.tool(
"wallet_transferOrdToken",
"Transfers BSV-20 or BSV-21 tokens from your wallet via js-1sat-ord transferOrdTokens.",
{ args: transferOrdTokenArgsSchema },
async (
{ args }: { args: TransferOrdTokenArgs },
extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
) => {
try {
// fetch keys
const paymentPk = wallet.getPrivateKey();
if (!paymentPk) throw new Error("No private key available");
const ordPk = paymentPk;
const changeAddress = paymentPk.toAddress().toString();
const ordAddress = changeAddress;
// select token UTXOs
const { selectedUtxos: inputTokens } = selectTokenUtxos(
args.tokenUtxos as TokenUtxo[],
args.sendAmount,
args.decimals,
{
inputStrategy: TokenSelectionStrategy.SmallestFirst,
outputStrategy: TokenSelectionStrategy.LargestFirst,
},
);
// build config
const config: TransferOrdTokensConfig = {
protocol:
args.protocol === "bsv-20" ? TokenType.BSV20 : TokenType.BSV21,
tokenID: args.tokenID,
utxos: args.paymentUtxos as Utxo[],
inputTokens,
distributions: args.distributions as Distribution[],
tokenChangeAddress: ordAddress,
changeAddress,
paymentPk,
ordPk,
additionalPayments: (args.additionalPayments as Payment[]) || [],
decimals: args.decimals,
inputMode: TokenInputMode.Needed,
splitConfig: {
outputs: inputTokens.length === 1 ? 2 : 1,
threshold: args.sendAmount,
},
};
const identityPk = process.env.IDENTITY_KEY_WIF
? PrivateKey.fromWif(process.env.IDENTITY_KEY_WIF)
: undefined;
if (identityPk) {
config.signer = {
idKey: identityPk,
} as LocalSigner;
}
// execute transfer
const result: TokenChangeResult = await transferOrdTokens(config);
const disableBroadcasting = process.env.DISABLE_BROADCASTING === "true";
if (!disableBroadcasting) {
await result.tx.broadcast();
// refresh UTXOs
try {
await wallet.refreshUtxos();
} catch {}
// respond
return {
content: [
{
type: "text",
text: JSON.stringify({
txid: result.tx.id("hex"),
spentOutpoints: result.spentOutpoints,
payChange: result.payChange,
tokenChange: result.tokenChange,
}),
},
],
};
}
return {
content: [
{
type: "text",
text: result.tx.toHex(),
},
],
};
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text", text: msg }], isError: true };
}
},
);
}