Don't use for real money on the main network! Use on regtest or testnet only!
Developer-friendly Taproot-only descriptor wallet on the command line that works in conjunction with Bitcoin Core.
Create custom key or script spends. Debug and test applications with handmade transactions. Learn about Taproot, descriptors, Miniscript, timelocks, and other transaction internals.
- init
- Create empty state
- print
- Print current state
- key
- Schnorr key pair
- img
- SHA-256 (pre)image pair
- addr
- Temporary inbound address for creating UTXOs
- utxo
- UTXO (unspent transaction output)
- in
- Transaction input
- out
- Transaction output
- locktime
- Update locktime
- fee
- Update transaction fee
- spend
- Create transaction witness and print raw transaction hex to send via Bitcoin Core
- final
- Finalize transaction and save transaction outputs as UTXOs
$ cargo build
(Tested with version 24.0.1)
Download and verify the latest Bitcoin Core binary.
Or build from source using the official instructions.
Or install via nixpkgs. This will give you bitcoind
and bitcoin-cli
.
$ nix-shell -p bitcoind
Feel free to use the provided Bitcoin configuration.
$ mkdir ~/.bitcoin
$ cp bitcoin.conf ~/.bitcoin
Run bitcoind.
$ bitcoind
Create a wallet if you don't already have one.
$ bitcoin-cli createwallet <WALLETNAME>
Or load your existing wallet.
$ bitcoin-cli loadwallet <WALLETNAME>
Mine some blocks to fund your wallet. You must mine at least 101 blocks because the block subsidy is only valid after 100 blocks have passed!
$ bitcoin-cli getnewaddress "" "bech32m"
: <ADDRESS>
$ bitcoin-cli generatetoaddress 101 <ADDRESS>
Check if your wallet has funds ("balance"). If you followed the above steps, your wallet should have 50 bitcoin.
$ elements-cli getwalletinfo
tappy keeps track of its state using the file state.json
in the current directory. Secrets are stored on the hard drive! Generate the initial state like so:
$ tappy init
You can view the current state like so:
$ tappy print
tappy keeps a set of Schnorr key pairs. Generate fresh keys by calling tappy key gen
followed by the number of keys.
$ tappy key gen 5
By default, keys are disabled for spending. Enable a key pair by calling tappy key en
followed by the xpub.
$ tappy key en 1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f
Disable by tappy key dis
plus the xpub.
$ tappy key dis 1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f
tappy also keeps a set of SHA-256 (pre)image pairs. Generate a pair by calling tappy img gen
followed by the number of pairs.
$ tappy img gen 5
Enable (pre)image pairs for spending by calling tappy img en
followed by the image.
$ tappy img en d166f218267103b44f1102a3ef05e87a9911b9f7cc7f0887f91e198e6a7d3fc4
Disable by tappy img dis
plus the image.
$ tappy img dis d166f218267103b44f1102a3ef05e87a9911b9f7cc7f0887f91e198e6a7d3fc4
In tappy you create a Bitcoin transaction from utxos, inputs and outputs. This is represented in the current state. Inputs and outputs are specified by Taproot descriptors that use keys/images from the key/image store or combinations of them (and, or, thres, multi, ...).
tappy maintains a set of UTXOs (unspend transaction outputs) that it can spend. Transaction inputs reference some unique UTXO that they want to spend.
tappy starts off without any UTXOs, so we must manually add them. Coins must enter the system. We set a temporary inbound address to create a UTXO with a locking script of our choosing. Use tappy addr set
followed by a descriptor. Use the Policy to Miniscript compiler to generate valid Miniscript.
$ tappy addr set "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f)"
: Fund this address: bcrt1pwkjuv2laefk6wqnhmqqurxnuhsc8jmmyn4xa48l4v26z3q4z6gjs5wymts
Fund this address using bitcoin-cli. In this example we send 1 BTC = 100000000 sat. The RPC returns the transaction id.
$ bitcoin-cli sendtoaddress bcrt1pwkjuv2laefk6wqnhmqqurxnuhsc8jmmyn4xa48l4v26z3q4z6gjs5wymts 1
: 3e59661081cbdbfa69e68a9e679a88f3d9070e209aeb11ff424ea06c806a1e7a
Get the full transaction hex using bitcoin-cli.
$ bitcoin-cli getrawtransaction 3e59661081cbdbfa69e68a9e679a88f3d9070e209aeb11ff424ea06c806a1e7a
: <TX_HEX>
Append 1
for human-readable format.
$ bitcoin-cli getrawtransaction 3e59661081cbdbfa69e68a9e679a88f3d9070e209aeb11ff424ea06c806a1e7a 1
We recommend the use of hal for even better formatting. Among other things, hal displays values in satoshi, which is the required format for tappy utxos.
$ hal tx decode <TX_HEX>
Given this information, convert the inbound address into a UTXO by calling tappy addr utxo
followed by the transaction id, the output index (vout) and the value in satoshi.
$ tappy addr utxo 3e59661081cbdbfa69e68a9e679a88f3d9070e209aeb11ff424ea06c806a1e7a 0 100000000
Phew, manually typing all of this stuff was a lot of work. Fortunately, tappy can add UTXOs that result from your transactions almost automatically. See Finalizing below for more.
Add a new transaction input by calling tappy in
followed by the input index, new
and the utxo index.
$ tappy in 0 new 0
Call tappy utxo list
to list all UTXOs with their index.
$ tappy utxo list
Add a new transaction output by calling tappy out
followed by the output index, descriptor and value in satoshi.
$ tappy out 0 "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f)" 99999000
Omit the value to signify that all input funds minus the other outputs minus fees should go to this output. This works for at most one output.
$ tappy out 0 "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f)"
Set the fee to whatever seems reasonable. It should not be zero. During spending the feerate will be displayed, so if Bitcoin Core rejects your transaction you can bump the fee. Call tappy fee
followed by the value in satoshi.
$ tappy fee 1000
Transaction inputs with absolute timelocks (after(n)
) enforce the transaction locktime to be at least n
. A transaction is valid if the height of its containing block is strictly greater than its locktime.
Set the locktime by calling tappy locktime
followed by the block height. Locktime in unix time is not supported.
$ tappy locktime 785572
The locktime is disabled if all inputs have the default sequence. Timelock opcodes will fail and locktime will be ignored. Change the sequence of any input to a relative locktime (which may be zero) to enable locktime. Other ways to enable locktime are not supported.
$ tappy in 0 seq enable 0
While locktime applies to the whole transaction, sequence applies to a single input. Transaction inputs with relative timelocks (older(n)
) enforce the sequence of that input to be a relative locktime of at least n
. A transaction is valid if the height of its containing block is strictly greater than the height of the utxo block plus n
.
Set a relative locktime for an input by calling tappy in
followed by the input index, seq enable
and the relative block height. Relative locktime in unix time is not supported.
$ tappy in 0 seq enable 10
Disable relative locktime for an input by calling tappy in
followed by the input index and seq disable
.
$ tappy in 0 seq disable
With everything set, attempt to create a spending transaction by calling tappy spend
. Remember to enable the required keys/images, and pay attention to the inputs' timelocks. Which keys/images are enabled influences the possible spend paths. tappy will return a transaction hex.
$ tappy spend
: <TX_HEX>
Use bitcoin-cli to broadcast this transaction. You will receive a transaction id if it worked.
$ bitcoin-cli sendrawtransaction <TX_HEX>
: <TXID>
Make sure to save the UTXOs that you just created by broadcasting the spending transaction. Call tappy final
followed by the transaction id.
$ tappy final 3e59661081cbdbfa69e68a9e679a88f3d9070e209aeb11ff424ea06c806a1e7a
All transaction outputs are automatically converted and added to the UTXO set. The current transaction is cleared and a new transaction is created for the next spend. By default, the first output of the old transaction becomes the first input of the new transaction.
$ tappy in 0 new 0
$ tappy out 0 new "tr(9fb5213dd37f61c98629500a436ae8f390b03d37d3609af2f01d515d4e899800)"
$ tappy fee 1000
$ tappy spend
$ tappy final f4a5f64b1552803ee93db9f35d2faa67be82bd1d508c1e16045112aa6f77d468
$ tappy in 0 new 0
$ tappy out 0 new "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f,multi_a(2,816945ddf16d3a568644d5fe174dca7d68ed2931d3ee4edefbd96d09ae30ec2e,75910f6c72d67cd2530d17ecba4bce9058d003564887925d46201e18513d804a,9fb5213dd37f61c98629500a436ae8f390b03d37d3609af2f01d515d4e899800))"
$ tappy fee 1000
$ tappy spend
$ tappy final f4a5f64b1552803ee93db9f35d2faa67be82bd1d508c1e16045112aa6f77d468
$ tappy in 0 new 0
$ tappy out 0 new "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f,and_v(v:pk(+++816945ddf16d3a568644d5fe174dca7d68ed2931d3ee4edefbd96d09ae30ec2e),after(10)))"
$ tappy fee 1000
$ tappy spend
$ tappy final f4a5f64b1552803ee93db9f35d2faa67be82bd1d508c1e16045112aa6f77d468
$ tappy in 0 new 0
$ tappy out 0 new "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f,and_v(v:pk(+++816945ddf16d3a568644d5fe174dca7d68ed2931d3ee4edefbd96d09ae30ec2e),older(10)))"
$ tappy fee 1000
$ tappy spend
$ tappy final f4a5f64b1552803ee93db9f35d2faa67be82bd1d508c1e16045112aa6f77d468
$ tappy in 0 new 0
$ tappy in 1 new 1
$ tappy out 0 new "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f)" 50000000
$ tappy out 1 new "tr(9fb5213dd37f61c98629500a436ae8f390b03d37d3609af2f01d515d4e899800)"
$ tappy fee 1000
$ tappy spend
$ tappy final f4a5f64b1552803ee93db9f35d2faa67be82bd1d508c1e16045112aa6f77d468