Skip to content

Commit 04aa56b

Browse files
author
Guru
committed
btc rpc changes
1 parent 63760ec commit 04aa56b

File tree

5 files changed

+374
-26
lines changed

5 files changed

+374
-26
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { secp256k1 } from "@tkey/common-types";
2+
import { Web3AuthMPCCoreKit } from "@web3auth/mpc-core-kit";
3+
import { networks, SignerAsync } from "bitcoinjs-lib";
4+
import * as bitcoinjs from "bitcoinjs-lib";
5+
import ECPairFactory from "ecpair";
6+
7+
import ecc from "@bitcoinerlab/secp256k1";
8+
import BN from "bn.js";
9+
10+
const ECPair = ECPairFactory(ecc);
11+
12+
export function createBitcoinJsSigner(props: { coreKitInstance: Web3AuthMPCCoreKit; network: networks.Network }): SignerAsync {
13+
// props.coreKitInstance.setTssWalletIndex(1);
14+
// console.log("acc index: 1", props.coreKitInstance?.getPubKeyPoint() && props.coreKitInstance.getPubKeyPoint().toSEC1(secp256k1, true).toString("hex"));
15+
// props.coreKitInstance.setTssWalletIndex(0);
16+
// console.log("acc index: 0", props.coreKitInstance?.getPubKeyPoint() && props.coreKitInstance.getPubKeyPoint().toSEC1(secp256k1, true).toString("hex"));
17+
18+
return {
19+
sign: async (msg: Buffer) => {
20+
let sig = await props.coreKitInstance.sign(msg);
21+
return sig;
22+
},
23+
24+
publicKey: props.coreKitInstance?.getPubKeyPoint() && props.coreKitInstance.getPubKeyPoint().toSEC1(secp256k1, true),
25+
network: props.network,
26+
};
27+
}
28+
29+
30+
export function createBitcoinJsSignerBip340(props: { coreKitInstance: Web3AuthMPCCoreKit; network: networks.Network; }): SignerAsync {
31+
const bufPubKey = props.coreKitInstance.getPubKeyPoint().toSEC1(secp256k1, true);
32+
const xOnlyPubKey = bufPubKey.subarray(1, 33);
33+
const keyPair = ECPair.fromPublicKey(bufPubKey);
34+
const tweak = bitcoinjs.crypto.taggedHash("TapTweak", xOnlyPubKey);
35+
const tweakedChildNode = keyPair.tweak(tweak);
36+
const pk = tweakedChildNode.publicKey;
37+
38+
// const pk = props.coreKitInstance.getPubKeyPoint().toSEC1(secp256k1, true);
39+
return {
40+
sign: async (msg: Buffer) => {
41+
let sig = await props.coreKitInstance.sign(msg);
42+
return sig;
43+
},
44+
signSchnorr: async (msg: Buffer) => {
45+
const keyTweak = new BN(tweak);
46+
let sig = await props.coreKitInstance.sign(msg, { keyTweak });
47+
return sig;
48+
},
49+
publicKey: pk,
50+
network: props.network,
51+
};
52+
}
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import { useEffect, useState } from "react";
2+
import { networks, Psbt, payments, SignerAsync } from "bitcoinjs-lib";
3+
import ECPairFactory from "ecpair";
4+
import ecc from "@bitcoinerlab/secp256k1";
5+
import * as bitcoinjs from "bitcoinjs-lib";
6+
import axios from "axios";
7+
import { useCoreKit } from "./useCoreKit";
8+
import { createBitcoinJsSigner, createBitcoinJsSignerBip340 } from "./BitcoinSigner";
9+
import { COREKIT_STATUS } from "@web3auth/mpc-core-kit";
10+
11+
bitcoinjs.initEccLib(ecc);
12+
const ECPair = ECPairFactory(ecc);
13+
14+
type AddressType = "Taproot" | "Segwit" | "PSBT";
15+
16+
interface Utxo {
17+
txid: string;
18+
vout: number;
19+
value: number;
20+
status: {
21+
confirmed: boolean;
22+
};
23+
}
24+
25+
const useBtcRPC = () => {
26+
const [signer, setSigner] = useState<SignerAsync | null>(null);
27+
const [btcAddress, setBtcAddress] = useState<string>("");
28+
const [btcBalance, setBtcBalance] = useState<string>("0");
29+
const { coreKitInstance } = useCoreKit();
30+
const bitcoinNetwork = networks.testnet;
31+
32+
useEffect(() => {
33+
if (coreKitInstance && coreKitInstance.status === COREKIT_STATUS.LOGGED_IN) {
34+
const localSigner: SignerAsync = createBitcoinJsSigner({
35+
coreKitInstance,
36+
network: bitcoinNetwork,
37+
});
38+
setSigner(localSigner);
39+
}
40+
}, [coreKitInstance]);
41+
42+
useEffect(() => {
43+
getBtcAccount();
44+
getBtcBalance();
45+
}, [signer]);
46+
47+
const getBtcAccount = async (type: AddressType = "Taproot"): Promise<string> => {
48+
try {
49+
if (!signer) throw new Error("Signer is not initialized");
50+
const address = getAddress(signer, type);
51+
if (!address) throw new Error("Failed to generate address");
52+
setBtcAddress(address);
53+
return address;
54+
} catch (err) {
55+
return (err as Error).message;
56+
}
57+
};
58+
59+
const getBtcBalance = async (type: AddressType = "Taproot"): Promise<string> => {
60+
try {
61+
if (!signer) throw new Error("Signer is not initialized");
62+
const address = getAddress(signer, type);
63+
if (!address) throw new Error("Failed to generate address");
64+
const utxos = await fetchUtxos(address);
65+
const balance = utxos.reduce((acc, utxo) => acc + utxo.value, 0);
66+
setBtcBalance(balance.toString());
67+
return balance.toString();
68+
} catch (err) {
69+
return (err as Error).message;
70+
}
71+
};
72+
73+
const sendTransactionBtc = async (
74+
toAddress: string,
75+
amountInSatoshis: number,
76+
type: AddressType = "Taproot",
77+
broadcast = false
78+
): Promise<string> => {
79+
if (!signer) return "Signer not initialized";
80+
try {
81+
const address = getAddress(signer, type);
82+
if (!address) throw new Error("Failed to generate address");
83+
const utxos = await fetchUtxos(address);
84+
if (!utxos.length) throw new Error("No UTXOs found for this address");
85+
86+
const utxo = utxos[0];
87+
const fee = await estimateFee();
88+
const sendAmount = amountInSatoshis || utxo.value - fee;
89+
const xOnlyPubKey = signer.publicKey.subarray(1, 33);
90+
91+
92+
const postTransaction = utxo.value - sendAmount - fee;
93+
if (postTransaction <= 0) {
94+
throw new Error("Insufficient UTXO value to cover amount + fee");
95+
}
96+
97+
const psbt = new Psbt({ network: bitcoinNetwork });
98+
99+
if (type === "PSBT") {
100+
const txHex = await fetchTransactionHex(utxo.txid);
101+
psbt.addInput({
102+
hash: utxo.txid,
103+
index: utxo.vout,
104+
nonWitnessUtxo: Buffer.from(txHex, "hex"),
105+
});
106+
} else {
107+
const accountOutput = getPaymentOutput(signer, type);
108+
psbt.addInput({
109+
hash: utxo.txid,
110+
index: utxo.vout,
111+
witnessUtxo: {
112+
script: accountOutput,
113+
value: utxo.value,
114+
},
115+
tapInternalKey: xOnlyPubKey,
116+
// ...(type === "Taproot" ? { tapInternalKey: signer.publicKey.subarray(1, 33) } : {}),
117+
});
118+
}
119+
console.log("psbt.txInputs[0]", psbt.data.inputs);
120+
121+
psbt.addOutput({
122+
address: toAddress,
123+
value: +sendAmount,
124+
});
125+
console.log("signing tx");
126+
127+
if (type === "Taproot") {
128+
const signerBip340 = createBitcoinJsSignerBip340({
129+
coreKitInstance,
130+
network: bitcoinNetwork,
131+
});
132+
await psbt.signInputAsync(0, signerBip340);
133+
} else {
134+
await psbt.signAllInputsAsync(signer);
135+
const isValid = psbt.validateSignaturesOfInput(0, btcValidator);
136+
if (!isValid) throw new Error("Signature validation failed");
137+
}
138+
139+
const isValid = psbt.validateSignaturesOfInput(0, btcValidator);
140+
if (!isValid) throw new Error("Taproot signature validation failed");
141+
142+
const signedTxHex = psbt.finalizeAllInputs().extractTransaction().toHex();
143+
console.log("signed tx", signedTxHex, "Copy the above into https://blockstream.info/testnet/tx/push");
144+
if (broadcast) {
145+
const txid = await broadcastTx(signedTxHex);
146+
return `Transaction broadcasted. TXID: ${txid}`;
147+
}
148+
return signedTxHex;
149+
} catch (error) {
150+
return (error as Error).message;
151+
}
152+
};
153+
154+
/**
155+
* Sign an arbitrary message using a Taproot (BIP340) or other signer.
156+
*/
157+
const signMessageBtc = async (message: string, type: AddressType = "Taproot"): Promise<string> => {
158+
try {
159+
// const balance = await getBtcBalance();
160+
// console.log({ balance });
161+
if (!signer) throw new Error("Signer is not initialized");
162+
const msg = Buffer.from(message, "utf-8");
163+
164+
if (type === "Taproot") {
165+
// Use BIP340 signSchnorr for Taproot
166+
const signerBip340 = createBitcoinJsSignerBip340({
167+
coreKitInstance,
168+
network: bitcoinNetwork,
169+
});
170+
if (!signerBip340.signSchnorr) throw new Error("signSchnorr is not defined");
171+
const signature = await signerBip340.signSchnorr(msg);
172+
return signature.toString("hex");
173+
} else {
174+
// Fall back to standard signing
175+
const signature = await signer.sign(msg);
176+
return signature.toString("hex");
177+
}
178+
} catch (error) {
179+
return (error as Error).message;
180+
}
181+
};
182+
183+
const getAddress = (signerObj: SignerAsync, type: AddressType): string | undefined => {
184+
const paymentOutput = getPaymentOutput(signerObj, type);
185+
const decodePay = payments.p2tr({ output: paymentOutput, network: bitcoinNetwork }).address; // defaulting to p2tr decode
186+
187+
if (type === "PSBT") {
188+
return payments.p2pkh({ pubkey: signerObj.publicKey, network: bitcoinNetwork }).address;
189+
}
190+
if (type === "Segwit") {
191+
return payments.p2wpkh({ pubkey: signerObj.publicKey, network: bitcoinNetwork }).address;
192+
}
193+
return decodePay;
194+
};
195+
196+
const getPaymentOutput = (signerObj: SignerAsync, type: AddressType): Buffer => {
197+
const bufPubKey = signerObj.publicKey;
198+
const xOnlyPubKey = bufPubKey.subarray(1, 33);
199+
const keyPair = ECPair.fromPublicKey(bufPubKey, { network: bitcoinNetwork });
200+
const tweak = bitcoinjs.crypto.taggedHash("TapTweak", xOnlyPubKey);
201+
const tweakedChildNode = keyPair.tweak(tweak);
202+
203+
if (type === "PSBT") {
204+
return payments.p2pkh({ pubkey: bufPubKey, network: bitcoinNetwork }).output!;
205+
}
206+
if (type === "Segwit") {
207+
return payments.p2wpkh({ pubkey: bufPubKey, network: bitcoinNetwork }).output!;
208+
}
209+
if (type === "Taproot") {
210+
return payments.p2tr({ pubkey: Buffer.from(tweakedChildNode.publicKey.subarray(1, 33)), network: bitcoinNetwork }).output!;
211+
}
212+
return payments.p2tr({
213+
pubkey: Buffer.from(tweakedChildNode.publicKey.subarray(1, 33)),
214+
network: bitcoinNetwork,
215+
}).output!;
216+
};
217+
218+
const fetchUtxos = async (address: string): Promise<Utxo[]> => {
219+
const url = `https://blockstream.info/testnet/api/address/${address}/utxo`;
220+
const response = await axios.get(url);
221+
return response.data.filter((utxo: Utxo) => utxo.status.confirmed);
222+
};
223+
224+
const fetchTransactionHex = async (txId: string): Promise<string> => {
225+
const response = await fetch(`https://blockstream.info/testnet/api/tx/${txId}/hex`);
226+
if (!response.ok) {
227+
throw new Error(`Failed to fetch transaction hex for ${txId}`);
228+
}
229+
return await response.text();
230+
};
231+
232+
const estimateFee = async (): Promise<number> => {
233+
const feeResponse = await axios.get("https://blockstream.info/testnet/api/fee-estimates");
234+
const maxFee = Math.max(...Object.values(feeResponse.data as Record<string, number>));
235+
return Math.ceil(maxFee * 1.2);
236+
};
237+
238+
const btcValidator = (pubkey: Buffer, msghash: Buffer, signature: Buffer): boolean => {
239+
return ecc.verifySchnorr(Uint8Array.from(msghash), Uint8Array.from(pubkey), Uint8Array.from(signature));
240+
};
241+
242+
const broadcastTx = async (signedTx: string): Promise<string> => {
243+
const response = await axios.post(`https://blockstream.info/testnet/api/tx`, signedTx);
244+
return response.data;
245+
};
246+
247+
return {
248+
btcAddress,
249+
btcBalance,
250+
getBtcAccount,
251+
getBtcBalance,
252+
sendTransactionBtc,
253+
signMessageBtc,
254+
};
255+
};
256+
257+
export default useBtcRPC;

0 commit comments

Comments
 (0)