-
Notifications
You must be signed in to change notification settings - Fork 3
/
BitcoinWallet.mo
233 lines (200 loc) · 9.24 KB
/
BitcoinWallet.mo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
//! A demo of a very bare-bones Bitcoin "wallet".
//!
//! The wallet here showcases how Bitcoin addresses can be be computed
//! and how Bitcoin transactions can be signed. It is missing several
//! pieces that any production-grade wallet would have, including:
//!
//! * Support for address types that aren't P2PKH.
//! * Caching spent UTXOs so that they are not reused in future transactions.
//! * Option to set the fee.
import Debug "mo:base/Debug";
import Array "mo:base/Array";
import Nat8 "mo:base/Nat8";
import Nat32 "mo:base/Nat32";
import Nat64 "mo:base/Nat64";
import Iter "mo:base/Iter";
import Blob "mo:base/Blob";
import EcdsaTypes "./motoko-bitcoin/src/ecdsa/Types";
import P2pkh "./motoko-bitcoin/src/bitcoin/P2pkh";
import Bitcoin "./motoko-bitcoin/src/bitcoin/Bitcoin";
import Address "./motoko-bitcoin/src/bitcoin/Address";
import Transaction "./motoko-bitcoin/src/bitcoin/Transaction";
import Script "./motoko-bitcoin/src/bitcoin/Script";
import Publickey "./motoko-bitcoin/src/ecdsa/Publickey";
import Der "./motoko-bitcoin/src/ecdsa/Der";
import Affine "./motoko-bitcoin/src/ec/Affine";
import Types "./types";
import EcdsaApi "EcdsaApi";
import BitcoinApi "BitcoinApi";
import Utils "./utils";
module {
type Network = Types.Network;
type BitcoinAddress = Types.BitcoinAddress;
type Satoshi = Types.Satoshi;
type Utxo = Types.Utxo;
type MillisatoshiPerByte = Types.MillisatoshiPerByte;
let CURVE = Types.CURVE;
type PublicKey = EcdsaTypes.PublicKey;
type Transaction = Transaction.Transaction;
type Script = Script.Script;
type SighashType = Nat32;
let SIGHASH_ALL : SighashType = 0x01;
/// Returns the P2PKH address of this canister at the given derivation path.
public func get_p2pkh_address(network : Network, key_name : Text, derivation_path : [[Nat8]]) : async BitcoinAddress {
// Fetch the public key of the given derivation path.
let public_key = await EcdsaApi.ecdsa_public_key(key_name, Array.map(derivation_path, Blob.fromArray));
// Compute the address.
public_key_to_p2pkh_address(network, Blob.toArray(public_key))
};
/// Sends a transaction to the network that transfers the given amount to the
/// given destination, where the source of the funds is the canister itself
/// at the given derivation path.
public func send(network : Network, derivation_path : [[Nat8]], key_name : Text, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] {
// Get fee percentiles from previous transactions to estimate our own fee.
let fee_percentiles = await BitcoinApi.get_current_fee_percentiles(network);
let fee_per_byte : MillisatoshiPerByte = if(fee_percentiles.size() == 0) {
// There are no fee percentiles. This case can only happen on a regtest
// network where there are no non-coinbase transactions. In this case,
// we use a default of 1000 millisatoshis/byte (i.e. 2 satoshi/byte)
2000
} else {
// Choose the 50th percentile for sending fees.
fee_percentiles[49]
};
// Fetch our public key, P2PKH address, and UTXOs.
let own_public_key = Blob.toArray(await EcdsaApi.ecdsa_public_key(key_name, Array.map(derivation_path, Blob.fromArray)));
let own_address = public_key_to_p2pkh_address(network, own_public_key);
Debug.print("Fetching UTXOs...");
let own_utxos = (await BitcoinApi.get_utxos(network, own_address)).utxos;
// Build the transaction that sends `amount` to the destination address.
let tx_bytes = await build_transaction(own_public_key, own_address, own_utxos, dst_address, amount, fee_per_byte);
let transaction =
Utils.get_ok(Transaction.fromBytes(Iter.fromArray(tx_bytes)));
Debug.print("Transaction to sign: " # debug_show(tx_bytes));
// Sign the transaction.
let signed_transaction_bytes = await sign_transaction(own_public_key, own_address, transaction, key_name, Array.map(derivation_path, Blob.fromArray), EcdsaApi.sign_with_ecdsa);
let signed_transaction = Utils.get_ok(Transaction.fromBytes(Iter.fromArray(signed_transaction_bytes)));
Debug.print("Signed transaction: " # debug_show(signed_transaction_bytes));
Debug.print("Sending transaction...");
await BitcoinApi.send_transaction(network, signed_transaction_bytes);
Debug.print("Done");
signed_transaction.id()
};
// Builds a transaction to send the given `amount` of satoshis to the
// destination address.
public func build_transaction(
own_public_key : [Nat8],
own_address : BitcoinAddress,
own_utxos : [Utxo],
dst_address : BitcoinAddress,
amount : Satoshi,
fee_per_byte : MillisatoshiPerByte,
) : async [Nat8] {
// We have a chicken-and-egg problem where we need to know the length
// of the transaction in order to compute its proper fee, but we need
// to know the proper fee in order to figure out the inputs needed for
// the transaction.
//
// We solve this problem iteratively. We start with a fee of zero, build
// and sign a transaction, see what its size is, and then update the fee,
// rebuild the transaction, until the fee is set to the correct amount.
let fee_per_byte_nat = Nat64.toNat(fee_per_byte);
Debug.print("Building transaction...");
var total_fee : Nat = 0;
loop {
let transaction = switch(Bitcoin.buildTransaction(2, own_utxos, [(#p2pkh dst_address, (amount - Nat64.fromNat(total_fee)))], #p2pkh own_address, Nat64.fromNat(total_fee))) {
case (#ok value){ value };
case (#err error) {
Debug.print(error);
Debug.trap(error);
};
};
// Sign the transaction. In this case, we only care about the size
// of the signed transaction, so we use a mock signer here for efficiency.
let signed_transaction_bytes = await sign_transaction(
own_public_key,
own_address,
transaction,
"", // mock key name
[], // mock derivation path
mock_signer,
);
let signed_tx_bytes_len : Nat = signed_transaction_bytes.size();
if((signed_tx_bytes_len * fee_per_byte_nat) / 1000 == total_fee) {
Debug.print("Transaction built with fee " # debug_show(total_fee));
return transaction.toBytes();
} else {
total_fee := (signed_tx_bytes_len * fee_per_byte_nat) / 1000;
}
}
};
type SignFun = (Text, [Blob], Blob) -> async Blob;
// Sign a bitcoin transaction.
//
// IMPORTANT: This method is for demonstration purposes only and it only
// supports signing transactions if:
//
// 1. All the inputs are referencing outpoints that are owned by `own_address`.
// 2. `own_address` is a P2PKH address.
public func sign_transaction(
own_public_key : [Nat8],
own_address : BitcoinAddress,
transaction : Transaction,
key_name : Text,
derivation_path : [Blob],
signer : SignFun,
) : async [Nat8] {
// Obtain the scriptPubKey of the source address which is also the
// scriptPubKey of the Tx output being spent.
switch (Address.scriptPubKey(#p2pkh own_address)) {
case (#ok scriptPubKey) {
let scriptSigs = Array.init<Script>(transaction.txInputs.size(), []);
// Obtain scriptSigs for each Tx input.
for (i in Iter.range(0, transaction.txInputs.size() - 1)) {
let sighash = transaction.createSignatureHash(
scriptPubKey, Nat32.fromIntWrap(i), SIGHASH_ALL);
let signature_sec = await signer(key_name, derivation_path, Blob.fromArray(sighash));
let signature_der = Blob.toArray(Der.encodeSignature(signature_sec));
// Append the sighash type.
let encodedSignatureWithSighashType = Array.tabulate<Nat8>(
signature_der.size() + 1, func (n) {
if (n < signature_der.size()) {
signature_der[n]
} else {
Nat8.fromNat(Nat32.toNat(SIGHASH_ALL))
};
});
// Create Script Sig which looks like:
// ScriptSig = <Signature> <Public Key>.
let script = [
#data encodedSignatureWithSighashType,
#data own_public_key
];
scriptSigs[i] := script;
};
// Assign ScriptSigs to their associated TxInputs.
for (i in Iter.range(0, scriptSigs.size() - 1)) {
transaction.txInputs[i].script := scriptSigs[i];
};
};
// Verify that our own address is P2PKH.
case (#err msg)
Debug.trap("This example supports signing p2pkh addresses only.");
};
transaction.toBytes()
};
// Converts a public key to a P2PKH address.
func public_key_to_p2pkh_address(network : Network, public_key_bytes : [Nat8]) : BitcoinAddress {
let public_key = public_key_bytes_to_public_key(public_key_bytes);
// Compute the P2PKH address from our public key.
P2pkh.deriveAddress(network, Publickey.toSec1(public_key, true))
};
// A mock for rubber-stamping ECDSA signatures.
func mock_signer(_key_name : Text, _derivation_path : [Blob], _message_hash : Blob) : async Blob {
Blob.fromArray(Array.freeze(Array.init<Nat8>(64, 255)))
};
func public_key_bytes_to_public_key(public_key_bytes : [Nat8]) : PublicKey {
let point = Utils.unwrap(Affine.fromBytes(public_key_bytes, CURVE));
Utils.get_ok(Publickey.decode(#point point))
};
}