Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BTC P2SH Examples #70

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions btc-p2sh/nodejs/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
LIT_CAPACITY_CREDIT_TOKEN_ID=
PKP_PUBLIC_KEY_1=
PKP_PUBLIC_KEY_2=
ETHEREUM_PRIVATE_KEY=
LIT_NETWORK=
BTC_DESTINATION_ADDRESS=
4 changes: 4 additions & 0 deletions btc-p2sh/nodejs/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://json.schemastore.org/mocharc.json",
"require": "tsx"
}
20 changes: 20 additions & 0 deletions btc-p2sh/nodejs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Bitcoin (P2SH) Transactions with Lit Protocol

These examples demonstrate how to use Lit Protocol PKPs (Programmable Key Pairs) to execute Bitcoin (P2SH) transactions.

## Prerequisites
Before compiling and trying this example out, there are a few things you'll need to prepare.
1. You will need to mint a PKP. The fastest way to do this is through the [Lit Explorer](https://explorer.litprotocol.com/). Please make sure you mint the PKP on the same network you run the example on.
2. An Ethereum wallet. Please make sure that this wallet was used to mint the PKP, and therefore has ownership of it.
3. The Bitcoin address derived from the PKP(s) must have a UTXO. Without a UTXO, the PKP will be unable to send any Bitcoin and this example will fail. Visit the [docs](https://developer.litprotocol.com/user-wallets/pkps/bitcoin/overview) for more information.
4. Choose one of the four P2SH examples to run. This can be done at the bottom of the `executeBtcSigning` function in `src/index.ts`. Just uncomment the function you want to run.

## Running the Example

Once the prerequisites are met, you can run the code by running the following commands:

Install dependencies:
```yarn```

Run the code:
```yarn test```
36 changes: 36 additions & 0 deletions btc-p2sh/nodejs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "btc-p2sh-example",
"version": "0.1.0",
"description": "Examples of P2SH transactions with Lit Protocol PKPs",
"type": "module",
"scripts": {
"test": "npx @dotenvx/dotenvx run -- mocha test/**/*.spec.ts"
},
"dependencies": {
"@lit-protocol/constants": "^6.6.0",
"@lit-protocol/lit-node-client": "^6.6.0",
"@mempool/mempool.js": "^2.3.0",
"@simplewebauthn/browser": "^10.0.0",
"@types/elliptic": "^6.4.18",
"@types/express": "^4.17.21",
"@types/node": "^22.5.5",
"bip66": "^2.0.0",
"bitcoinjs-lib": "^7.0.0-rc.0",
"bn.js": "^5.2.1",
"ecpair": "^3.0.0-rc.0",
"elliptic": "^6.5.7",
"node-fetch": "^3.3.2",
"tiny-secp256k1": "^2.2.3"
},
"devDependencies": {
"@types/chai": "^4.3.16",
"@types/chai-json-schema": "^1.4.10",
"@types/mocha": "^10.0.6",
"chai": "^5.1.1",
"chai-json-schema": "^1.5.1",
"mocha": "^10.4.0",
"tsc": "^2.0.4",
"tsx": "^4.12.0",
"typescript": "^5.5.3"
}
}
133 changes: 133 additions & 0 deletions btc-p2sh/nodejs/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { LitNodeClient } from "@lit-protocol/lit-node-client";
import { LitNetwork, LIT_RPC, } from "@lit-protocol/constants";
import {
createSiweMessageWithRecaps,
generateAuthSig,
LitAbility,
LitActionResource,
LitPKPResource,
} from "@lit-protocol/auth-helpers";
import { LitContracts } from "@lit-protocol/contracts-sdk";
import { LIT_NETWORKS_KEYS } from "@lit-protocol/types";
import * as ethers from "ethers";

import { getEnv } from "./utils/utils";
import { singleSig } from "./p2sh/single-sig";
import { multiSig } from "./p2sh/multi-sig";
import { oneOfOneMultiSig } from "./p2sh/1of1-multi-sig";
import { collaborativeMultiSig } from "./p2sh/collaborative";

const PKP_PUBLIC_KEY_1 = getEnv("PKP_PUBLIC_KEY_1");
const PKP_PUBLIC_KEY_2 = getEnv("PKP_PUBLIC_KEY_2");
const ETHEREUM_PRIVATE_KEY = getEnv("ETHEREUM_PRIVATE_KEY");
const BTC_DESTINATION_ADDRESS = getEnv("BTC_DESTINATION_ADDRESS");
const LIT_NETWORK = process.env["LIT_NETWORK"] as LIT_NETWORKS_KEYS || LitNetwork.Datil;
const LIT_CAPACITY_CREDIT_TOKEN_ID = process.env["LIT_CAPACITY_CREDIT_TOKEN_ID"];

const ethersWallet = new ethers.Wallet(
ETHEREUM_PRIVATE_KEY,
new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE)
);

const address = ethersWallet.address;

export const executeBtcSigning = async () => {
let litNodeClient: LitNodeClient;

try {
litNodeClient = new LitNodeClient({
litNetwork: LIT_NETWORK,
debug: false,
});
await litNodeClient.connect();

const litContracts = new LitContracts({
signer: ethersWallet,
network: LIT_NETWORK,
debug: false,
});
await litContracts.connect();

let capacityTokenId = LIT_CAPACITY_CREDIT_TOKEN_ID;
if (!capacityTokenId) {
console.log("🔄 No Capacity Credit provided, minting a new one...");
const mintResult = await litContracts.mintCapacityCreditsNFT({
requestsPerKilosecond: 10,
daysUntilUTCMidnightExpiration: 1,
});
capacityTokenId = mintResult.capacityTokenIdStr;
console.log(`✅ Minted new Capacity Credit with ID: ${capacityTokenId}`);
} else {
console.log(`ℹ️ Using provided Capacity Credit with ID: ${LIT_CAPACITY_CREDIT_TOKEN_ID}`);
}

console.log("🔄 Creating capacityDelegationAuthSig...");
const { capacityDelegationAuthSig } =
await litNodeClient.createCapacityDelegationAuthSig({
dAppOwnerWallet: ethersWallet,
capacityTokenId,
delegateeAddresses: [address],
uses: "1",
});
console.log("✅ Capacity Delegation Auth Sig created");

console.log("🔄 Getting Session Signatures...");
const sessionSigs = await litNodeClient.getSessionSigs({
chain: "ethereum",
expiration: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(), // 24 hours
capabilityAuthSigs: [capacityDelegationAuthSig],
resourceAbilityRequests: [
{
resource: new LitPKPResource("*"),
ability: LitAbility.PKPSigning,
},
{
resource: new LitActionResource("*"),
ability: LitAbility.LitActionExecution,
},
],
authNeededCallback: async ({
resourceAbilityRequests,
expiration,
uri,
}) => {
const toSign = await createSiweMessageWithRecaps({
uri: uri!,
expiration: expiration!,
resources: resourceAbilityRequests!,
walletAddress: address,
nonce: await litNodeClient!.getLatestBlockhash(),
litNodeClient,
});

return await generateAuthSig({
signer: ethersWallet,
toSign,
});
},
});
console.log("✅ Got Session Signatures");

console.log("🔄 Executing Bitcoin Transaction...");
return await singleSig(litNodeClient, sessionSigs, PKP_PUBLIC_KEY_1, BTC_DESTINATION_ADDRESS);

/**
* Single Sig P2SH:
return await singleSig(litNodeClient, sessionSigs, PKP_PUBLIC_KEY_1, BTC_DESTINATION_ADDRESS);

* Multi Sig P2SH:
return await multiSig(litNodeClient, sessionSigs, PKP_PUBLIC_KEY_1, PKP_PUBLIC_KEY_2, BTC_DESTINATION_ADDRESS);

* 1of1 Multi Sig P2SH:
return await oneOfOneMultiSig(litNodeClient, sessionSigs, PKP_PUBLIC_KEY_1, PKP_PUBLIC_KEY_2, BTC_DESTINATION_ADDRESS);

* Collaborative Multi Sig P2SH:
return await collaborativeMultiSig(litNodeClient, sessionSigs, PKP_PUBLIC_KEY_1, PKP_PUBLIC_KEY_2, BTC_DESTINATION_ADDRESS);
*/

} catch (error) {
console.error(error);
} finally {
litNodeClient!.disconnect();
}
};
94 changes: 94 additions & 0 deletions btc-p2sh/nodejs/src/p2sh/1of1-multi-sig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as bitcoin from "bitcoinjs-lib";
import * as ecc from "tiny-secp256k1";
import mempoolJS from "@mempool/mempool.js";
import { LitNodeClient } from "@lit-protocol/lit-node-client";

import { convertSignature, broadcastTransaction } from "../utils/utils";
import { litActionCode } from "../utils/litAction";

bitcoin.initEccLib(ecc);

export async function oneOfOneMultiSig(litNodeClient: LitNodeClient, sessionSigs: any, pkpPublicKey1: string, pkpPublicKey2: string, destinationAddress: string) {
const network = bitcoin.networks.bitcoin;
const pubKeyBuffer_1 = Buffer.from(pkpPublicKey1, "hex");
const pubKeyBuffer_2 = Buffer.from(pkpPublicKey2, "hex");

const redeemScript = bitcoin.script.compile([
bitcoin.opcodes.OP_1,
pubKeyBuffer_1,
pubKeyBuffer_2,
bitcoin.opcodes.OP_2,
bitcoin.opcodes.OP_CHECKMULTISIG,
]);

const p2shPayment = bitcoin.payments.p2sh({
redeem: { output: redeemScript },
network: network,
});

const { bitcoin: { addresses, transactions } } = mempoolJS({
hostname: "mempool.space",
network: "mainnet",
});

const addressUtxos = await addresses.getAddressTxsUtxo({
address: p2shPayment.address!,
});
console.log("P2SH Address:", p2shPayment.address);

if (addressUtxos.length === 0) {
console.log("No UTXOs found for address:", p2shPayment.address);
return;
}

const utxo = addressUtxos[0];
const psbt = new bitcoin.Psbt({ network });
const utxoRawTx = await transactions.getTxHex({ txid: utxo.txid });

psbt.addInput({
hash: utxo.txid,
index: utxo.vout,
nonWitnessUtxo: Buffer.from(utxoRawTx, "hex"),
redeemScript: redeemScript,
});

const fee = 1000;
const amountToSend = utxo.value - fee;

psbt.addOutput({
address: destinationAddress,
value: BigInt(amountToSend),
});

//@ts-ignore
const tx = psbt.__CACHE.__TX.clone();
const sighash = tx.hashForSignature(
0,
redeemScript,
bitcoin.Transaction.SIGHASH_ALL
);

const litActionResponse = await litNodeClient.executeJs({
code: litActionCode,
sessionSigs,
jsParams: {
publicKey: pkpPublicKey1,
toSign: Buffer.from(sighash, "hex"),
},
});

const signatureWithHashType = await convertSignature(
litActionResponse.signatures.btcSignature
);

psbt.updateInput(0, {
finalScriptSig: bitcoin.script.compile([
bitcoin.opcodes.OP_0,
signatureWithHashType,
redeemScript,
]),
});

const txHex = psbt.extractTransaction().toHex();
return await broadcastTransaction(txHex);
}
Loading