We're going to use chantools as a signer, so we don't have to deal with an actual hardware wallet
for testing in regtest.
The output of this step will just be the xpub of a P2TR account of the seed and the seed's master key fingerprint.
For this example we use the BIP39 seed "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon cactus"
which is equivalent to the master root key tprv8ZgxMBicQKsPdKXUBQMuH7B5NAfjBmTdsT2hitzzLWB3BZJURFLTqfdJPr15WkH8huiTrr6k5aCwLoWDxLxhqi8waU7WcrZ9f6kGyJGP2BL.
#!/bin/bash
ROOTKEY="tprv8ZgxMBicQKsPdKXUBQMuH7B5NAfjBmTdsT2hitzzLWB3BZJURFLTqfdJPr15WkH8huiTrr6k5aCwLoWDxLxhqi8waU7WcrZ9f6kGyJGP2BL"
# This should probably be different on regtest, but lnd only uses network ID 0.
DERIVATIONPATH="m/86'/0'/0'"
XPUB=$(chantools --regtest derivekey --path "$DERIVATIONPATH" --rootkey $ROOTKEY | grep xpub | cut -d':' -f2 | xargs)
# We use chantools as a signer which doesn't need a specific fingerprint to identify the seed.
# So we just use a dummy value of the correct format.
FINGERPRINT=00112233
echo xpub: $XPUB
echo master key fingerprint: $FINGERPRINTWe now import the xpub into lnd. This can be the same lnd that tapd is connected to, or any other instance.
Then we derive an address from that account and show its public key.
#!/bin/bash
LNCLI=reg_alice
ACCOUNT=group-keys
# Import xpub into lnd wallet.
$LNCLI wallet accounts import --address_type p2tr --master_key_fingerprint $FINGERPRINT $XPUB $ACCOUNT
# Derive new p2tr address.
ADDRESS=$($LNCLI newaddress p2tr --account $ACCOUNT | jq -r .address)
echo address: $ADDRESS
# Show all addresses of imported account.
$LNCLI wallet addresses list --account_name $ACCOUNTWhich outputs:
{
"account": {
"name": "group-keys",
"address_type": "TAPROOT_PUBKEY",
"extended_public_key": "tpubDD4ho2vw7ckz1kn1xMNpxTwMvRKWPAqmqo4oppTcW2x96nEw1sJXvWUsTovwwcvZ5mdb98eN8b5E4drMDWmzL1hRzqXfr4BnPwNQZCuw9tv",
"master_key_fingerprint": "00112233",
"derivation_path": "m/86'/0'/0'",
"external_key_count": 0,
"internal_key_count": 0,
"watch_only": true
},
"dry_run_external_addrs": [],
"dry_run_internal_addrs": []
}
address: bcrt1p0svqqvsaxtu5te5nxpq8qa9xg89rrenfg4xxvkhlqcu7hhrwzl9qjjnytg
{
"account_with_addresses": [
{
"name": "group-keys",
"address_type": "TAPROOT_PUBKEY",
"derivation_path": "m/86'/0'/0'",
"addresses": [
{
"address": "bcrt1p0svqqvsaxtu5te5nxpq8qa9xg89rrenfg4xxvkhlqcu7hhrwzl9qjjnytg",
"is_internal": false,
"balance": "0",
"derivation_path": "m/86'/0'/1'/0/0",
"public_key": "02eaf1e2c9eb14d96b36a5804c758324170ad41eea98ebb9fd5b57b2e4a3365c70"
}
]
}
]
}The above now also shows us our group public key we're going to want to use.
Note that the derivation path shown for the address is wrong (should be "m/86'/0'/0'/0/0"), due to a bug in lnd.
This is only wrong in the output of the specific RPC response but should be correct in the PSBTs later on.
To be able to create a template transaction easily, we just send some funds to the address. Don't send too much but also not just above dust. A couple of thousand satoshis should be fine.
#!/bin/bash
LNCLI=reg_alice
ACCOUNT=group-keys
BITCOINCLI=reg_bitcoin
MINE="regtest mine 1"
# Send 2k satoshis to generated address.
$BITCOINCLI sendtoaddress $ADDRESS 0.00002
# Mine a block (or wait for one to be mined on non-regtest)
$MINE
# Show account balance.
$LNCLI walletbalance --account $ACCOUNT
# Extract the outpoint of the now confirmed UTXO.
OUTPOINT=$($LNCLI listunspent | jq -r ".utxos[] | select(.address | contains(\"$ADDRESS\")) | .outpoint")
echo outpoint: $OUTPOINTOutput:
{
"total_balance": "2000",
"confirmed_balance": "2000",
"unconfirmed_balance": "0",
"locked_balance": "0",
"reserved_balance_anchor_chan": "20000",
"account_balance": {
"group-keys": {
"confirmed_balance": "2000",
"unconfirmed_balance": "0"
}
}
}
outpoint: 94abfc1b03c3f8556a3ad386bece690b4461bb219796ac8ef0939dd1ad8046e6:1
We now create a template from attempting to spend those 2k sats again. But don't worry, we're not actually going to spend them. First we lease/lock the UTXO to signal we're going to use it. Then we create a spend PSBT for that input.
#!/bin/bash
LNCLI=reg_alice
ACCOUNT=group-keys
BITCOINCLI=reg_bitcoin
# The lock ID identifies the application locking the UTXO, we just use
# 32 random bytes here as it doesn't really matter.
LOCKID=$(head -c 32 /dev/random|xxd -p -c32)
# Lease the output (prerequisite for PSBT funding).
$LNCLI wallet leaseoutput --outpoint $OUTPOINT --lockid $LOCKID --expiry 3600
# Create a PSBT that spends the UTXO into the same address again.
# Specifying the change output index 0 will make sure this creates a
# one-input-one-output transaction by adding any left-over balance after
# fee estimation to the given single output.
PSBT=$($LNCLI wallet psbt fundtemplate --account $ACCOUNT --inputs="[\"$OUTPOINT\"]" --outputs="[\"$ADDRESS:123\"]" --change_type p2tr --change_output_index 0 --sat_per_vbyte 1 | jq -r .psbt)
echo psbt: $PSBT
# Print the decoded PSBT.
$BITCOINCLI decodepsbt $PSBTOutput:
{
"expiration": "1732126195"
}
psbt: cHNidP8BAF4CAAAAAeZGgK3RnZPwjqyWlyG7YUQLac6+htM6alX4wwMb/KuUAQAAAAAAAAAAAWEHAAAAAAAAIlEgfBgAMh0y+UXmkzBAcHSmQcox5mlFTGZa/wY569xuF8oAAAAAAAEBK9AHAAAAAAAAIlEgfBgAMh0y+UXmkzBAcHSmQcox5mlFTGZa/wY569xuF8oiBgLq8eLJ6xTZazalgEx1gyQXCtQe6pjruf1bV7LkozZccBgzIhEAVgAAgAAAAIAAAACAAAAAAAAAAAAhFurx4snrFNlrNqWATHWDJBcK1B7qmOu5/VtXsuSjNlxwGQAzIhEAVgAAgAAAAIAAAACAAAAAAAAAAAAAAA==
{
"tx": {
"txid": "fa72d1361d87802a10bc7bbfdfc59de47c47b3f5126d69ce02f5044c41257bfc",
"hash": "fa72d1361d87802a10bc7bbfdfc59de47c47b3f5126d69ce02f5044c41257bfc",
"version": 2,
"size": 94,
"vsize": 94,
"weight": 376,
"locktime": 0,
"vin": [
{
"txid": "94abfc1b03c3f8556a3ad386bece690b4461bb219796ac8ef0939dd1ad8046e6",
"vout": 1,
"scriptSig": {
"asm": "",
"hex": ""
},
"sequence": 0
}
],
"vout": [
{
"value": 0.00001889,
"n": 0,
"scriptPubKey": {
"asm": "1 7c1800321d32f945e69330407074a641ca31e669454c665aff0639ebdc6e17ca",
"desc": "rawtr(7c1800321d32f945e69330407074a641ca31e669454c665aff0639ebdc6e17ca)#hf49xftv",
"hex": "51207c1800321d32f945e69330407074a641ca31e669454c665aff0639ebdc6e17ca",
"address": "bcrt1p0svqqvsaxtu5te5nxpq8qa9xg89rrenfg4xxvkhlqcu7hhrwzl9qjjnytg",
"type": "witness_v1_taproot"
}
}
]
},
"global_xpubs": [
],
"psbt_version": 0,
"proprietary": [
],
"unknown": {
},
"inputs": [
{
"witness_utxo": {
"amount": 0.00002000,
"scriptPubKey": {
"asm": "1 7c1800321d32f945e69330407074a641ca31e669454c665aff0639ebdc6e17ca",
"desc": "rawtr(7c1800321d32f945e69330407074a641ca31e669454c665aff0639ebdc6e17ca)#hf49xftv",
"hex": "51207c1800321d32f945e69330407074a641ca31e669454c665aff0639ebdc6e17ca",
"address": "bcrt1p0svqqvsaxtu5te5nxpq8qa9xg89rrenfg4xxvkhlqcu7hhrwzl9qjjnytg",
"type": "witness_v1_taproot"
}
},
"bip32_derivs": [
{
"pubkey": "02eaf1e2c9eb14d96b36a5804c758324170ad41eea98ebb9fd5b57b2e4a3365c70",
"master_fingerprint": "33221100",
"path": "m/86h/0h/0h/0/0"
}
],
"taproot_bip32_derivs": [
{
"pubkey": "eaf1e2c9eb14d96b36a5804c758324170ad41eea98ebb9fd5b57b2e4a3365c70",
"master_fingerprint": "33221100",
"path": "m/86h/0h/0h/0/0",
"leaf_hashes": [
]
}
]
}
],
"outputs": [
{
}
],
"fee": 0.00000111
}#!/bin/bash
# Variables
LNCLI=reg_alice
ACCOUNT=group-keys
BITCOINCLI=reg_bitcoin
MINE="regtest mine 1"
# Import xpub into lnd wallet.
$LNCLI wallet accounts import --address_type p2tr --master_key_fingerprint $FINGERPRINT $XPUB $ACCOUNT
# Derive new p2tr address.
ADDRESS=$($LNCLI newaddress p2tr --account $ACCOUNT | jq -r .address)
echo address: $ADDRESS
# Show all addresses of imported account.
$LNCLI wallet addresses list --account_name $ACCOUNT
# Send 2k satoshis to generated address.
$BITCOINCLI sendtoaddress $ADDRESS 0.00002
# Mine a block (or wait for one to be mined on non-regtest)
$MINE
# Show account balance.
$LNCLI walletbalance --account $ACCOUNT
# Extract the outpoint of the now confirmed UTXO.
OUTPOINT=$($LNCLI listunspent | jq -r ".utxos[] | select(.address | contains(\"$ADDRESS\")) | .outpoint")
echo outpoint: $OUTPOINT
# The lock ID identifies the application locking the UTXO, we just use
# 32 random bytes here as it doesn't really matter.
LOCKID=$(head -c 32 /dev/random|xxd -p -c32)
# Lease the output (prerequisite for PSBT funding).
$LNCLI wallet leaseoutput --outpoint $OUTPOINT --lockid $LOCKID --expiry 3600
# Create a PSBT that spends the UTXO into the same address again.
# Specifying the change output index 0 will make sure this creates a
# one-input-one-output transaction by adding any left-over balance after
# fee estimation to the given single output.
PSBT=$($LNCLI wallet psbt fundtemplate --account $ACCOUNT --inputs="[\"$OUTPOINT\"]" --outputs="[\"$ADDRESS:123\"]" --change_type p2tr --change_output_index 0 --sat_per_vbyte 1 | jq -r .psbt)
echo psbt: $PSBT
# Print the decoded PSBT.
$BITCOINCLI decodepsbt $PSBT