Skip to content

Commit

Permalink
Merge pull request #616 from dfinity/tl/polish_basic_bitcoin
Browse files Browse the repository at this point in the history
Polishing the basic Bitcoin example
  • Loading branch information
sesi200 authored Sep 25, 2023
2 parents a77af17 + 31585e2 commit 89f44ad
Show file tree
Hide file tree
Showing 10 changed files with 51 additions and 40 deletions.
8 changes: 5 additions & 3 deletions motoko/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
type satoshi = nat64;

type millisatoshi_per_byte = nat64;
type millisatoshi_per_vbyte = nat64;

type bitcoin_address = text;

type transaction_id = text;

type block_hash = blob;

type network = variant {
Expand Down Expand Up @@ -37,10 +39,10 @@ service : (network) -> {

"get_utxos": (bitcoin_address) -> (get_utxos_response);

"get_current_fee_percentiles": () -> (vec millisatoshi_per_byte);
"get_current_fee_percentiles": () -> (vec millisatoshi_per_vbyte);

"send": (record {
destination_address: bitcoin_address;
amount_in_satoshi: satoshi;
}) -> (text);
}) -> (transaction_id);
}
12 changes: 5 additions & 7 deletions motoko/basic_bitcoin/src/basic_bitcoin/src/BitcoinApi.mo
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module {
type Network = Types.Network;
type BitcoinAddress = Types.BitcoinAddress;
type GetUtxosResponse = Types.GetUtxosResponse;
type MillisatoshiPerByte = Types.MillisatoshiPerByte;
type MillisatoshiPerVByte = Types.MillisatoshiPerVByte;
type GetBalanceRequest = Types.GetBalanceRequest;
type GetUtxosRequest = Types.GetUtxosRequest;
type GetCurrentFeePercentilesRequest = Types.GetCurrentFeePercentilesRequest;
Expand All @@ -25,7 +25,7 @@ module {
type ManagementCanisterActor = actor {
bitcoin_get_balance : GetBalanceRequest -> async Satoshi;
bitcoin_get_utxos : GetUtxosRequest -> async GetUtxosResponse;
bitcoin_get_current_fee_percentiles : GetCurrentFeePercentilesRequest -> async [MillisatoshiPerByte];
bitcoin_get_current_fee_percentiles : GetCurrentFeePercentilesRequest -> async [MillisatoshiPerVByte];
bitcoin_send_transaction : SendTransactionRequest -> async ();
};

Expand All @@ -46,9 +46,7 @@ module {

/// Returns the UTXOs of the given Bitcoin address.
///
/// NOTE: Pagination is ignored in this example. If an address has many thousands
/// of UTXOs, then subsequent calls to `bitcoin_get_utxos` are required.
///
/// NOTE: Relies on the `bitcoin_get_utxos` endpoint.
/// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_get_utxos
public func get_utxos(network : Network, address : BitcoinAddress) : async GetUtxosResponse {
ExperimentalCycles.add(GET_UTXOS_COST_CYCLES);
Expand All @@ -59,12 +57,12 @@ module {
})
};

/// Returns the 100 fee percentiles measured in millisatoshi/byte.
/// Returns the 100 fee percentiles measured in millisatoshi/vbyte.
/// Percentiles are computed from the last 10,000 transactions (if available).
///
/// Relies on the `bitcoin_get_current_fee_percentiles` endpoint.
/// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_get_current_fee_percentiles
public func get_current_fee_percentiles(network : Network) : async [MillisatoshiPerByte] {
public func get_current_fee_percentiles(network : Network) : async [MillisatoshiPerVByte] {
ExperimentalCycles.add(GET_CURRENT_FEE_PERCENTILES_COST_CYCLES);
await management_canister_actor.bitcoin_get_current_fee_percentiles({
network;
Expand Down
21 changes: 12 additions & 9 deletions motoko/basic_bitcoin/src/basic_bitcoin/src/BitcoinWallet.mo
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ module {
type BitcoinAddress = Types.BitcoinAddress;
type Satoshi = Types.Satoshi;
type Utxo = Types.Utxo;
type MillisatoshiPerByte = Types.MillisatoshiPerByte;
type MillisatoshiPerVByte = Types.MillisatoshiPerVByte;
let CURVE = Types.CURVE;
type PublicKey = EcdsaTypes.PublicKey;
type Transaction = Transaction.Transaction;
Expand All @@ -61,25 +61,28 @@ module {
// 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) {
let fee_per_vbyte : MillisatoshiPerVByte = 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)
// we use a default of 1000 millisatoshis/vbyte (i.e. 2 satoshi/byte)
2000
} else {
// Choose the 50th percentile for sending fees.
fee_percentiles[49]
fee_percentiles[50]
};

// 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...");
// Note that pagination may have to be used to get all UTXOs for the given address.
// For the sake of simplicity, it is assumed here that the `utxo` field in the response
// contains all 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 tx_bytes = await build_transaction(own_public_key, own_address, own_utxos, dst_address, amount, fee_per_vbyte);
let transaction =
Utils.get_ok(Transaction.fromBytes(Iter.fromArray(tx_bytes)));

Expand Down Expand Up @@ -107,7 +110,7 @@ public func build_transaction(
own_utxos : [Utxo],
dst_address : BitcoinAddress,
amount : Satoshi,
fee_per_byte : MillisatoshiPerByte,
fee_per_vbyte : MillisatoshiPerVByte,
) : 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
Expand All @@ -117,7 +120,7 @@ public func build_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);
let fee_per_vbyte_nat = Nat64.toNat(fee_per_vbyte);
Debug.print("Building transaction...");
var total_fee : Nat = 0;
loop {
Expand All @@ -137,11 +140,11 @@ public func build_transaction(

let signed_tx_bytes_len : Nat = signed_transaction_bytes.size();

if((signed_tx_bytes_len * fee_per_byte_nat) / 1000 == total_fee) {
if((signed_tx_bytes_len * fee_per_vbyte_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;
total_fee := (signed_tx_bytes_len * fee_per_vbyte_nat) / 1000;
}
}
};
Expand Down
6 changes: 5 additions & 1 deletion motoko/basic_bitcoin/src/basic_bitcoin/src/EcdsaApi.mo
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ module {
type ECDSAPublicKeyReply = Types.ECDSAPublicKeyReply;
type SignWithECDSA = Types.SignWithECDSA;
type SignWithECDSAReply = Types.SignWithECDSAReply;
type Cycles = Types.Cycles;

/// Actor definition to handle interactions with the ECDSA canister.
type EcdsaCanisterActor = actor {
ecdsa_public_key : ECDSAPublicKey -> async ECDSAPublicKeyReply;
sign_with_ecdsa : SignWithECDSA -> async SignWithECDSAReply;
};

// The fee for the `sign_with_ecdsa` endpoint using the test key.
let SIGN_WITH_ECDSA_COST_CYCLES : Cycles = 10_000_000_000;

let ecdsa_canister_actor : EcdsaCanisterActor = actor("aaaaa-aa");

/// Returns the ECDSA public key of this canister at the given derivation path.
Expand All @@ -33,7 +37,7 @@ module {
};

public func sign_with_ecdsa(key_name : Text, derivation_path : [Blob], message_hash : Blob) : async Blob {
ExperimentalCycles.add(10_000_000_000);
ExperimentalCycles.add(SIGN_WITH_ECDSA_COST_CYCLES);
let res = await ecdsa_canister_actor.sign_with_ecdsa({
message_hash;
derivation_path;
Expand Down
6 changes: 3 additions & 3 deletions motoko/basic_bitcoin/src/basic_bitcoin/src/Main.mo
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Utils "Utils";

actor class BasicBitcoin(_network : Types.Network) {
type GetUtxosResponse = Types.GetUtxosResponse;
type MillisatoshiPerByte = Types.MillisatoshiPerByte;
type MillisatoshiPerVByte = Types.MillisatoshiPerVByte;
type SendRequest = Types.SendRequest;
type Network = Types.Network;
type BitcoinAddress = Types.BitcoinAddress;
Expand Down Expand Up @@ -41,9 +41,9 @@ actor class BasicBitcoin(_network : Types.Network) {
await BitcoinApi.get_utxos(NETWORK, address)
};

/// Returns the 100 fee percentiles measured in millisatoshi/byte.
/// Returns the 100 fee percentiles measured in millisatoshi/vbyte.
/// Percentiles are computed from the last 10,000 transactions (if available).
public func get_current_fee_percentiles() : async [MillisatoshiPerByte] {
public func get_current_fee_percentiles() : async [MillisatoshiPerVByte] {
await BitcoinApi.get_current_fee_percentiles(NETWORK)
};

Expand Down
2 changes: 1 addition & 1 deletion motoko/basic_bitcoin/src/basic_bitcoin/src/Types.mo
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ module Types {
};

public type Satoshi = Nat64;
public type MillisatoshiPerByte = Nat64;
public type MillisatoshiPerVByte = Nat64;
public type Cycles = Nat;
public type BitcoinAddress = Text;
public type BlockHash = [Nat8];
Expand Down
8 changes: 5 additions & 3 deletions rust/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
type satoshi = nat64;

type millisatoshi_per_byte = nat64;
type millisatoshi_per_vbyte = nat64;

type bitcoin_address = text;

type transaction_id = text;

type block_hash = blob;

type network = variant {
Expand Down Expand Up @@ -37,10 +39,10 @@ service : (network) -> {

"get_utxos": (bitcoin_address) -> (get_utxos_response);

"get_current_fee_percentiles": () -> (vec millisatoshi_per_byte);
"get_current_fee_percentiles": () -> (vec millisatoshi_per_vbyte);

"send": (record {
destination_address: bitcoin_address;
amount_in_satoshi: satoshi;
}) -> (text);
}) -> (transaction_id);
}
4 changes: 1 addition & 3 deletions rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ pub async fn get_balance(network: BitcoinNetwork, address: String) -> u64 {

/// Returns the UTXOs of the given bitcoin address.
///
/// NOTE: Pagination is ignored in this example. If an address has many thousands
/// of UTXOs, then subsequent calls to `bitcoin_get_utxos` are required.
///
/// NOTE: Relies on the `bitcoin_get_utxos` endpoint.
/// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_get_utxos
pub async fn get_utxos(network: BitcoinNetwork, address: String) -> GetUtxosResponse {
let utxos_res: Result<(GetUtxosResponse,), _> = call_with_payment(
Expand Down
5 changes: 4 additions & 1 deletion rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub async fn send(
2000
} else {
// Choose the 50th percentile for sending fees.
fee_percentiles[49]
fee_percentiles[50]
};

// Fetch our public key, P2PKH address, and UTXOs.
Expand All @@ -63,6 +63,9 @@ pub async fn send(
let own_address = public_key_to_p2pkh_address(network, &own_public_key);

print("Fetching UTXOs...");
// Note that pagination may have to be used to get all UTXOs for the given address.
// For the sake of simplicity, it is assumed here that the `utxo` field in the response
// contains all UTXOs.
let own_utxos = bitcoin_api::get_utxos(network, own_address.clone())
.await
.utxos;
Expand Down
19 changes: 10 additions & 9 deletions rust/basic_bitcoin/src/basic_bitcoin/src/ecdsa_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ use crate::types::*;
use candid::Principal;
use ic_cdk::{api::call::call_with_payment, call};

// The fee for the `sign_with_ecdsa` endpoint using the test key.
const SIGN_WITH_ECDSA_COST_CYCLES: u64 = 10_000_000_000;

/// Returns the ECDSA public key of this canister at the given derivation path.
pub async fn ecdsa_public_key(key_name: String, derivation_path: Vec<Vec<u8>>) -> Vec<u8> {
// Retrieve the public key of this canister at the given derivation path
// from the ECDSA API.
let res: (ECDSAPublicKeyReply,) = call(
let res: Result<(ECDSAPublicKeyReply,), _> = call(
Principal::management_canister(),
"ecdsa_public_key",
(ECDSAPublicKey {
Expand All @@ -18,18 +21,17 @@ pub async fn ecdsa_public_key(key_name: String, derivation_path: Vec<Vec<u8>>) -
},
},),
)
.await
.unwrap();
.await;

res.0.public_key
res.unwrap().0.public_key
}

pub async fn sign_with_ecdsa(
key_name: String,
derivation_path: Vec<Vec<u8>>,
message_hash: Vec<u8>,
) -> Vec<u8> {
let res: (SignWithECDSAReply,) = call_with_payment(
let res: Result<(SignWithECDSAReply,), _> = call_with_payment(
Principal::management_canister(),
"sign_with_ecdsa",
(SignWithECDSA {
Expand All @@ -40,10 +42,9 @@ pub async fn sign_with_ecdsa(
name: key_name,
},
},),
10_000_000_000,
SIGN_WITH_ECDSA_COST_CYCLES,
)
.await
.unwrap();
.await;

res.0.signature
res.unwrap().0.signature
}

0 comments on commit 89f44ad

Please sign in to comment.