Skip to content

Commit

Permalink
🚧 (smpl): wip psbt creation
Browse files Browse the repository at this point in the history
  • Loading branch information
jdabbech-ledger committed Jan 6, 2025
1 parent 62cd518 commit c922a05
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 3 deletions.
5 changes: 5 additions & 0 deletions apps/sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@
"@ledgerhq/device-transport-kit-web-hid": "workspace:*",
"@ledgerhq/react-ui": "^0.17.0",
"@playwright/test": "^1.49.0",
"@scure/base": "^1.2.1",
"@scure/bip32": "^1.6.1",
"@sentry/nextjs": "^8.42.0",
"bitcoinjs-lib": "^6.1.6",
"bs58check": "^4.0.0",
"ethers": "6.13.4",
"micro-btc-signer": "^0.4.2",
"next": "14.2.15",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
7 changes: 4 additions & 3 deletions apps/sample/src/components/SignerBtcView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import {

import { DeviceActionsList } from "@/components/DeviceActionsView/DeviceActionsList";
import { type DeviceActionProps } from "@/components/DeviceActionsView/DeviceActionTester";
import psbtBase64 from "@/components/SignerBtcView/psbt";
import { useDmk } from "@/providers/DeviceManagementKitProvider";

// Native segwit
const DEFAULT_DERIVATION_PATH = "84'/0'/0'";
const DEFAULT_DERIVATION_PATH = "86'/0'/0'";

export const SignerBtcView: React.FC<{ sessionId: string }> = ({
sessionId,
Expand Down Expand Up @@ -47,7 +48,7 @@ export const SignerBtcView: React.FC<{ sessionId: string }> = ({
});
},
initialValues: {
derivationPath: "84'/0'/0'",
derivationPath: DEFAULT_DERIVATION_PATH,
checkOnDevice: false,
},
deviceModelId,
Expand Down Expand Up @@ -103,7 +104,7 @@ export const SignerBtcView: React.FC<{ sessionId: string }> = ({
},
initialValues: {
derivationPath: DEFAULT_DERIVATION_PATH,
psbt: "cHNidP8BAFUCAAAAAVEiws3mgj5VdUF1uSycV6Co4ayDw44Xh/06H/M0jpUTAQAAAAD9////AXhBDwAAAAAAGXapFBPX1YFmlGw+wCKTQGbYwNER0btBiKwaBB0AAAEA+QIAAAAAAQHsIw5TCVJWBSokKCcO7ASYlEsQ9vHFePQxwj0AmLSuWgEAAAAXFgAUKBU5gg4t6XOuQbpgBLQxySHE2G3+////AnJydQAAAAAAF6kUyLkGrymMcOYDoow+/C+uGearKA+HQEIPAAAAAAAZdqkUy65bUM+Tnm9TG4prer14j+FLApeIrAJHMEQCIDfstCSDYar9T4wR5wXw+npfvc1ZUXL81WQ/OxG+/11AAiACDG0yb2w31jzsra9OszX67ffETgX17x0raBQLAjvRPQEhA9rIL8Cs/Pw2NI1KSKRvAc6nfyuezj+MO0yZ0LCy+ZXShPIcACIGAu6GCCB+IQKEJvaedkR9fj1eB3BJ9eaDwxNsIxR2KkcYGPWswv0sAACAAQAAgAAAAIAAAAAAAAAAAAAA",
psbt: psbtBase64,
},
deviceModelId,
} satisfies DeviceActionProps<
Expand Down
117 changes: 117 additions & 0 deletions apps/sample/src/components/SignerBtcView/psbt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { base64, hex } from "@scure/base";
import { HDKey } from "@scure/bip32";
import * as bitcoin from "bitcoinjs-lib";
import * as btc from "micro-btc-signer";

const ACCT_XPUB =
"xpub6ChVjsbs6gVVb7urXG5fzPegohAKvhDwdnC1cr9PLUJcYWWVv1VUy4vdUrstiUsL9CNyXicxmogj6zsSExDsQKKZuMpApZMr1f3YMZB72VD";
const MASTER_FINGERPRINT_HEX = "ee4cc60f";

function createTaprootPSBT(utxos, recipientOutput, changeOutput) {
try {
const accountKey = HDKey.fromExtendedKey(ACCT_XPUB);
console.log("ACC KEY", accountKey.publicKey);

const childKeyRecipient = accountKey.derive(recipientOutput.path);
const compressedPubKeyRecipient = childKeyRecipient.publicKey;
const xonlyPubKeyRecipient = compressedPubKeyRecipient.slice(1);

const childKeyChange = accountKey.derive(changeOutput.path);
const compressedPubKeyChange = childKeyChange.publicKey;
const xonlyPubKeyChange = compressedPubKeyChange.slice(1);

console.log("Recipient x-only:", hex.encode(xonlyPubKeyRecipient));
console.log("Change x-only: ", hex.encode(xonlyPubKeyChange));

const tx = new btc.Transaction();

const p2trRecipient = btc.p2tr(
xonlyPubKeyRecipient,
undefined,
bitcoin.networks.bitcoin,
);
const p2trChange = btc.p2tr(
xonlyPubKeyRecipient,
undefined,
bitcoin.networks.bitcoin,
);

for (const utxo of utxos) {
tx.addInput({
txid: utxo.tx_hash,
index: utxo.tx_output_n,
redeemScript: p2trRecipient.redeemScript,
witnessUtxo: {
script: p2trRecipient.script,
amount: BigInt(utxo.value),
},
tapInternalKey: accountKey.publicKey!.slice(1),
bip32_derivation: [
[
hex.encode(compressedPubKeyRecipient),
{
master_fingerprint: hex.decode(MASTER_FINGERPRINT_HEX),
path: "m/86'/0'/0'/" + recipientOutput.path.slice(2),
pubkey: compressedPubKeyRecipient,
},
],
],
});
}

tx.addOutput({
script: p2trRecipient.script,
amount: BigInt(recipientOutput.amount),
tapInternalKey: xonlyPubKeyRecipient,
});

tx.addOutput({
script: p2trChange.script,
amount: BigInt(changeOutput.amount),
tapInternalKey: xonlyPubKeyChange,
bip32_derivation: [
[
hex.encode(compressedPubKeyChange),
{
master_fingerprint: hex.decode(MASTER_FINGERPRINT_HEX),
path: "86'/0'/0'/" + changeOutput.path.slice(2),
pubkey: compressedPubKeyChange,
},
],
],
});

const psbt = tx.toPSBT(0);
const psbtBase64 = base64.encode(psbt);
console.log("PSBT (Base64):", psbtBase64);

console.log(
JSON.stringify(bitcoin.Psbt.fromBase64(psbtBase64).data, null, 2),
);

return psbtBase64;
} catch (err) {
console.error("Error creating Taproot PSBT:", err);
throw err;
}
}

const utxos = [
{
tx_hash: "db87829c15a5e3100c41563bad05d65c129f637f91705628afb16a2b33c50d27",
tx_output_n: 1,
value: 300000,
},
];

const recipientOutput = {
path: "m/0/7",
amount: 200000,
};

const changeOutput = {
path: "m/1/0",
amount: 100000,
};

export default createTaprootPSBT(utxos, recipientOutput, changeOutput);
87 changes: 87 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c922a05

Please sign in to comment.