diff --git a/src/Methods/Order/CreateSignablePsbt.ts b/src/Methods/Order/CreateSignablePsbt.ts index 23fd586..e5a565a 100644 --- a/src/Methods/Order/CreateSignablePsbt.ts +++ b/src/Methods/Order/CreateSignablePsbt.ts @@ -14,16 +14,16 @@ export const createSignablePsbt = method({ pubkey: string.optional(), }), handler: async ({ network, location, maker, pubkey }) => { + const lookup = new Lookup(network); + + const psbt = new Psbt({ network: lookup.btcnetwork }); const [hash, index] = utils.parse.location(location); - const tapInternalKey = pubkey ? Buffer.from(pubkey, "hex") : undefined; - const btcNetwork = utils.bitcoin.getBitcoinNetwork(network); - const address = utils.bitcoin.getBitcoinAddress(maker); - if (address === undefined) { - throw new BadRequestError("Provided maker address is invalid"); - } + // ### Location + // Ensure that the location the signature is being created for exists and + // belongs to the maker. - const tx = await new Lookup(network).getTransaction(hash); + const tx = await lookup.getTransaction(hash); if (tx === undefined) { throw new BadRequestError("Could not find transaction"); } @@ -33,35 +33,58 @@ export const createSignablePsbt = method({ throw new BadRequestError("Provided maker address does not match location output"); } - const psbt = new Psbt({ network: btcNetwork }); + // ### Input + // Add transaction input to the PSBT. Determine the input structure based + // on the address type of the provided maker. + const type = utils.bitcoin.getAddressType(maker); - if (type === "bech32") { - psbt.addInput({ - hash, - index, - witnessUtxo: { - script: Buffer.from(vout.scriptPubKey.hex, "hex"), - value: 0, - }, - }); - } else if (type === "taproot") { - if (tapInternalKey === undefined) { - throw new BadRequestError("Taproot address requires a pubkey"); + if (type === undefined) { + throw new BadRequestError("Provided maker address does not match supported address types."); + } + + switch (type) { + case "taproot": { + if (pubkey === undefined) { + throw new BadRequestError("Taproot address requires a pubkey"); + } + const tapInternalKey = Buffer.from(pubkey, "hex"); + if (tapInternalKey.length !== 32) { + throw new BadRequestError("Taproot pubkey must be 32 bytes"); + } + psbt.addInput({ + hash, + index, + witnessUtxo: { + script: utils.taproot.getPaymentOutput(tapInternalKey, lookup.btcnetwork), + value: 0, + }, + tapInternalKey, + }); + break; + } + + case "bech32": { + psbt.addInput({ + hash, + index, + witnessUtxo: { + script: Buffer.from(vout.scriptPubKey.hex, "hex"), + value: 0, + }, + }); + break; + } + + default: { + psbt.addInput({ hash, index }); } - psbt.addInput({ - hash, - index, - witnessUtxo: { - script: utils.taproot.getPaymentOutput(tapInternalKey, btcNetwork), - value: 0, - }, - tapInternalKey, - }); - } else { - psbt.addInput({ hash, index }); } + // ### Output + // Add transaction output to the PSBT. The output is a empty OP_RETURN output + // with the maker address as the value. + psbt.addOutput({ script: payments.embed({ data: [Buffer.from(maker, "utf8")] }).output!, value: 0, diff --git a/src/Methods/Taproot/Bip84/GetBip84Account.ts b/src/Methods/Taproot/Bip84/GetBip84Account.ts index 86abba4..cb36348 100644 --- a/src/Methods/Taproot/Bip84/GetBip84Account.ts +++ b/src/Methods/Taproot/Bip84/GetBip84Account.ts @@ -10,12 +10,21 @@ export const getBip84Account = method({ network: validate.schema.network, mnemonic: string, account: number, + path: string.optional(), }), - handler: async ({ mnemonic, account, network }) => { + handler: async ({ network, mnemonic, account, path }) => { const masterNode = utils.taproot.getMasterNode(mnemonic, network); - const wallet = Wallet.fromBase58(utils.taproot.getBip84Account(masterNode, network, account).toBase58(), network); + + let wallet = Wallet.fromBase58(utils.taproot.getBip84Account(masterNode, network, account).toBase58(), network); + if (path !== undefined) { + wallet = Wallet.fromPrivateKey(wallet.privateKey!.toString("hex"), network).derive(path); + } + return { - key: wallet.privateKey?.toString("hex"), + privateKey: wallet.privateKey?.toString("hex"), + publicKey: wallet.publicKey.toString("hex"), + xPublicKey: wallet.internalPubkey.toString("hex"), + address: wallet.address, }; }, }); diff --git a/src/Methods/Taproot/Bip84/GetBip84Address.ts b/src/Methods/Taproot/Bip84/GetBip84Address.ts index 217199b..449f37a 100644 --- a/src/Methods/Taproot/Bip84/GetBip84Address.ts +++ b/src/Methods/Taproot/Bip84/GetBip84Address.ts @@ -1,4 +1,4 @@ -import { method } from "@valkyr/api"; +import { BadRequestError, method } from "@valkyr/api"; import Schema, { number, string } from "computed-types"; import { Wallet } from "../../../Libraries/Wallet"; @@ -7,12 +7,24 @@ import { validate } from "../../../Validators"; export const getBip84Address = method({ params: Schema({ network: validate.schema.network, + from: string, key: string, type: validate.schema.bip84.type, index: number, }), - handler: async ({ key, type, index, network }) => { - const wallet = Wallet.fromPrivateKey(key, network)[type](index); - return wallet.address; + handler: async ({ from, key, type, index, network }) => { + switch (from) { + case "privateKey": { + const wallet = Wallet.fromPrivateKey(key, network)[type](index); + return wallet.address; + } + case "publicKey": { + const wallet = Wallet.fromPublicKey(key, network)[type](index); + return wallet.address; + } + default: { + throw new BadRequestError("Unknown wallet parser type provided"); + } + } }, }); diff --git a/src/Methods/Taproot/SignPSBT.ts b/src/Methods/Taproot/SignPSBT.ts new file mode 100644 index 0000000..ecf2199 --- /dev/null +++ b/src/Methods/Taproot/SignPSBT.ts @@ -0,0 +1,24 @@ +import { method } from "@valkyr/api"; +import { Psbt } from "bitcoinjs-lib"; +import Schema, { string } from "computed-types"; + +import { Wallet } from "../../Libraries/Wallet"; +import { utils } from "../../Utilities"; +import { validate } from "../../Validators"; + +export const signPsbt = method({ + params: Schema({ + network: validate.schema.network, + psbt: string, + key: string, + }), + handler: async (params) => { + const wallet = Wallet.fromPrivateKey(params.key, params.network); + + const psbt = Psbt.fromBase64(params.psbt, { + network: utils.bitcoin.getBitcoinNetwork(params.network), + }).signAllInputs(wallet.signer); + + return { psbt: psbt.toBase64() }; + }, +}); diff --git a/src/Methods/Taproot/index.ts b/src/Methods/Taproot/index.ts index 8ad69db..73e4fb9 100644 --- a/src/Methods/Taproot/index.ts +++ b/src/Methods/Taproot/index.ts @@ -5,6 +5,7 @@ import { getBip84PrivateKey } from "./Bip84/GetBip84PrivateKey"; import { getBip84PublicKey } from "./Bip84/GetBip84PublicKey"; import { createTransaction } from "./CreateTransaction"; import { createWallet } from "./CreateWallet"; +import { signPsbt } from "./SignPSBT"; api.register("CreateWallet", createWallet); api.register("CreateTransaction", createTransaction); @@ -12,3 +13,4 @@ api.register("GetBip84Account", getBip84Account); api.register("GetBip84PublicKey", getBip84PublicKey); api.register("GetBip84PrivateKey", getBip84PrivateKey); api.register("GetBip84Address", getBip84Address); +api.register("SignPsbt", signPsbt); diff --git a/src/Utilities/Bitcoin.ts b/src/Utilities/Bitcoin.ts index 189fdd1..38019f0 100644 --- a/src/Utilities/Bitcoin.ts +++ b/src/Utilities/Bitcoin.ts @@ -9,13 +9,13 @@ const addressFormats = { mainnet: { p2pkh: /^[1][a-km-zA-HJ-NP-Z1-9]{25,34}$/, // legacy p2sh: /^[3][a-km-zA-HJ-NP-Z1-9]{25,34}$/, // segwit - bech32: /^(bc1[qp])[a-zA-HJ-NP-Z0-9]{14,74}$/, // bech32 + bech32: /^(bc1q)[a-zA-HJ-NP-Z0-9]{14,74}$/, // bech32 taproot: /^(bc1p)[a-zA-HJ-NP-Z0-9]{14,74}$/, // taproot }, other: { p2pkh: /^[mn][a-km-zA-HJ-NP-Z1-9]{25,34}$/, p2sh: /^[2][a-km-zA-HJ-NP-Z1-9]{25,34}$/, - bech32: /^(tb1[qp]|bcrt1[qp])[a-zA-HJ-NP-Z0-9]{14,74}$/, + bech32: /^(tb1q|bcrt1q)[a-zA-HJ-NP-Z0-9]{14,74}$/, taproot: /^(tb1p|bcrt1p)[a-zA-HJ-NP-Z0-9]{14,74}$/, }, } as const;