Skip to content

Commit 0ce0ec3

Browse files
committed
feat: Add multisig taproot example
1 parent 4e8de80 commit 0ce0ec3

File tree

1 file changed

+133
-0
lines changed

1 file changed

+133
-0
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package org.bitcoindevkit
2+
3+
fun main() {
4+
// Add your regtest URL here. Regtest environment must have esplora to run this.
5+
// See here for regtest podman setup https://github.com/thunderbiscuit/podman-regtest-infinity-pro
6+
val REGTEST_URL = "http://127.0.0.1:3002"
7+
8+
// An unspendable compressed public key to use as internal key for Taproot multisig
9+
// This could be any valid compressed public key that no one has the private key for or
10+
// full descriptor path including derivation.
11+
val UNSPENDABLE_KEY = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
12+
13+
// Create 3 wallets using existing helper (P2TR = Taproot)
14+
val aliceWallet = getNewWallet(ActiveWalletScriptType.P2TR, Network.REGTEST)
15+
val bobWallet = getNewWallet(ActiveWalletScriptType.P2TR, Network.REGTEST)
16+
val mattWallet = getNewWallet(ActiveWalletScriptType.P2TR, Network.REGTEST)
17+
18+
// Get the public descriptors of each wallet. Extract inner key expressions
19+
var alicePublicDescriptor = aliceWallet.publicDescriptor(KeychainKind.EXTERNAL)
20+
alicePublicDescriptor = alicePublicDescriptor.substring(alicePublicDescriptor.indexOf("(") + 1, alicePublicDescriptor.indexOf(")"))
21+
var bobPublicDescriptor = bobWallet.publicDescriptor(KeychainKind.EXTERNAL)
22+
bobPublicDescriptor = bobPublicDescriptor.substring(bobPublicDescriptor.indexOf("(") + 1, bobPublicDescriptor.indexOf(")"))
23+
var mattPublicDescriptor = mattWallet.publicDescriptor(KeychainKind.EXTERNAL)
24+
mattPublicDescriptor = mattPublicDescriptor.substring(mattPublicDescriptor.indexOf("(") + 1, mattPublicDescriptor.indexOf(")"))
25+
26+
// Get the change descriptors of each wallet (for change path)
27+
var aliceChangeDescriptor = aliceWallet.publicDescriptor(KeychainKind.INTERNAL)
28+
aliceChangeDescriptor = aliceChangeDescriptor.substring(aliceChangeDescriptor.indexOf("(") + 1, aliceChangeDescriptor.indexOf(")"))
29+
var bobChangeDescriptor = bobWallet.publicDescriptor(KeychainKind.INTERNAL)
30+
bobChangeDescriptor = bobChangeDescriptor.substring(bobChangeDescriptor.indexOf("(") + 1, bobChangeDescriptor.indexOf(")"))
31+
var mattChangeDescriptor = mattWallet.publicDescriptor(KeychainKind.INTERNAL)
32+
mattChangeDescriptor = mattChangeDescriptor.substring(mattChangeDescriptor.indexOf("(") + 1, mattChangeDescriptor.indexOf(")"))
33+
34+
// Define Taproot multisig descriptors. For Taproot, an internal key is required.
35+
// We use a random unspendable compressed public key as the internal key. This means
36+
// that the wallet can only be spent via the script path (no key-path spending).
37+
val externalDescriptor = Descriptor(
38+
descriptor = "tr($UNSPENDABLE_KEY,multi_a(2,$alicePublicDescriptor,$bobPublicDescriptor,$mattPublicDescriptor))",
39+
network = Network.REGTEST
40+
)
41+
val changeDescriptor = Descriptor(
42+
descriptor = "tr($UNSPENDABLE_KEY,multi_a(2,$aliceChangeDescriptor,$bobChangeDescriptor,$mattChangeDescriptor))",
43+
network = Network.REGTEST
44+
)
45+
46+
// Create multisig wallet (reuse shared persistence helper)
47+
val connection: Persister = Persister.newSqlite(generateUniquePersistenceFilePath())
48+
val multisigWallet = Wallet(externalDescriptor, changeDescriptor, Network.REGTEST, connection)
49+
50+
val changeAddress = multisigWallet.revealNextAddress(KeychainKind.INTERNAL)
51+
val address = multisigWallet.revealNextAddress(KeychainKind.EXTERNAL)
52+
println("Change address: ${changeAddress.address}")
53+
54+
// Fund this address. Alice, Bob and Matt own funds sent here
55+
println("receiving address: ${address.address}")
56+
57+
// Client: wait for funding then scan
58+
println("Waiting 40 seconds for funds to arrive here before scanning ${address.address} ...")
59+
Thread.sleep(40000)
60+
61+
val esploraClient = EsploraClient(REGTEST_URL)
62+
val fullScanRequest: FullScanRequest = multisigWallet.startFullScan().build()
63+
val update = esploraClient.fullScan(fullScanRequest, 10uL, 1uL)
64+
multisigWallet.applyUpdate(update)
65+
multisigWallet.persist(connection)
66+
println("Balance: ${multisigWallet.balance().total.toSat()}")
67+
68+
Thread.sleep(40000)
69+
70+
// Create PSBT
71+
val recipient = Address("bcrt1q645m0j78v9pajdfp0g0w6wacl4v8s7mvrwsjx5", Network.REGTEST)
72+
println("Balance: ${multisigWallet.balance().total.toSat()}")
73+
val psbt: Psbt = TxBuilder()
74+
.addRecipient(recipient.scriptPubkey(), Amount.fromSat(4420uL))
75+
.feeRate(FeeRate.fromSatPerVb(22uL))
76+
.finish(multisigWallet)
77+
78+
val psbtTransaction = psbt.extractTx()
79+
println(psbtTransaction.toString())
80+
println("Before any signature ${psbt.serialize()}")
81+
println("Before any signature. Json: ${psbt.jsonSerialize()}")
82+
83+
// Sign Psbt via script path; do not allow key-path signing so taptree fields are populated
84+
aliceWallet.sign(psbt, SignOptions(
85+
trustWitnessUtxo = false,
86+
assumeHeight = null,
87+
allowAllSighashes = false,
88+
tryFinalize = false,
89+
signWithTapInternalKey = false,
90+
allowGrinding = false
91+
))
92+
println("After signing with aliceWallet ${psbt.serialize()}")
93+
println("After signing with aliceWallet Json ${psbt.jsonSerialize()}")
94+
println("Transaction after signing with wallet1: ${psbt.extractTx().toString()}")
95+
96+
bobWallet.sign(psbt, SignOptions(
97+
trustWitnessUtxo = false,
98+
assumeHeight = null,
99+
allowAllSighashes = false,
100+
tryFinalize = false,
101+
signWithTapInternalKey = false,
102+
allowGrinding = false
103+
))
104+
println("After signing with bobWallet ${psbt.serialize()}")
105+
println("After signing with bobWallet Json ${psbt.jsonSerialize()}")
106+
println("Transaction after signing with bobWallet: ${psbt.extractTx().toString()}")
107+
108+
// Matt does not need to sign since it is a 2 of 3 multisig. Alice and Bob have already signed.
109+
mattWallet.sign(psbt, SignOptions(
110+
trustWitnessUtxo = false,
111+
assumeHeight = null,
112+
allowAllSighashes = false,
113+
tryFinalize = false,
114+
signWithTapInternalKey = false,
115+
allowGrinding = false
116+
))
117+
println("After signing with mattWallet ${psbt.serialize()}")
118+
println("After signing with mattWallet Json ${psbt.jsonSerialize()}")
119+
println("Transaction after signing with mattWallet: ${psbt.extractTx().toString()}")
120+
121+
multisigWallet.finalizePsbt(psbt)
122+
println("After finalize: ${psbt.serialize()}")
123+
println("After finalize Json: ${psbt.jsonSerialize()}")
124+
println("Transaction after finalize: ${psbt.extractTx().toString()}")
125+
126+
127+
val tx: Transaction = psbt.extractTx()
128+
println("Txid is: ${tx.computeTxid()}")
129+
130+
// Now you can broadcast the transaction
131+
esploraClient.broadcast(tx)
132+
println("Tx was broadcasted")
133+
}

0 commit comments

Comments
 (0)