Skip to content

Commit

Permalink
Add metrics method and bitcoin query wrappers (#523)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamspofford-dfinity committed Mar 2, 2024
1 parent 66246e9 commit 9abd75b
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 7 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased

* Timestamps are now being checked in `Agent::verify` and `Agent::verify_for_subnet`. If you were using it with old certificates, increase the expiry timeout to continue to verify them.
* Added ECDSA and Bitcoin functions to `MgmtMethod`. There are no new wrappers in `ManagementCanister` because only canisters can call these functions.
* Added node metrics, ECDSA, and Bitcoin functions to `MgmtMethod`. Most do not have wrappers in `ManagementCanister` because only canisters can call these functions.
* Added `FetchCanisterLogs` function to `MgmtMethod` and a corresponding wrapper to `ManagementCanister`.

## [0.33.0] - 2024-02-08
Expand Down
129 changes: 127 additions & 2 deletions ic-utils/src/interfaces/management_canister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,18 @@ pub enum MgmtMethod {
SignWithEcdsa,
/// There is no corresponding agent function as only canisters can call it.
BitcoinGetBalance,
/// There is no corresponding agent function as only canisters can call it.
/// See [`ManagementCanister::bitcoin_get_balance_query`].
BitcoinGetBalanceQuery,
/// There is no corresponding agent function as only canisters can call it.
BitcoinGetUtxos,
/// There is no corresponding agent function as only canisters can call it.
/// See [`ManagementCanister::bitcoin_get_utxos_query`].
BitcoinGetUtxosQuery,
/// There is no corresponding agent function as only canisters can call it.
BitcoinSendTransaction,
/// There is no corresponding agent function as only canisters can call it.
BitcoinGetCurrentFeePercentiles,
/// There is no corresponding agent function as only canisters can call it.
NodeMetricsHistory,
}

impl<'agent> ManagementCanister<'agent> {
Expand Down Expand Up @@ -221,6 +223,78 @@ pub struct FetchCanisterLogsResponse {
/// A SHA-256 hash of a WASM chunk.
pub type ChunkHash = [u8; 32];

/// The Bitcoin network that a Bitcoin transaction is placed on.
#[derive(Clone, Copy, Debug, CandidType, Deserialize, PartialEq, Eq)]
pub enum BitcoinNetwork {
/// The BTC network.
#[serde(rename = "mainnet")]
Mainnet,
/// The TESTBTC network.
#[serde(rename = "testnet")]
Testnet,
}

impl BitcoinNetwork {
/// Gets the [effective canister ID](crate::call::SyncCallBuilder::with_effective_canister_id)
// to make `bitcoin_*` calls to.
pub fn effective_canister_id(&self) -> Principal {
const BITCOIN_MAINNET_CANISTER: Principal =
Principal::from_slice(b"\x00\x00\x00\x00\x01\xa0\x00\x04\x01\x01"); // ghsi2-tqaaa-aaaan-aaaca-cai
const BITCOIN_TESTNET_CANISTER: Principal =
Principal::from_slice(b"\x00\x00\x00\x00\x01\xa0\x00\x01\x01\x01"); // g4xu7-jiaaa-aaaan-aaaaq-cai

match self {
Self::Mainnet => BITCOIN_MAINNET_CANISTER,
Self::Testnet => BITCOIN_TESTNET_CANISTER,
}
}
}

/// Defines how to filter results from [`bitcoin_get_utxos_query`](ManagementCanister::bitcoin_get_utxos_query).
#[derive(Debug, Clone, CandidType, Deserialize)]
pub enum UtxosFilter {
/// Filter by the minimum number of UTXO confirmations. Most applications should set this to 6.
#[serde(rename = "min_confirmations")]
MinConfirmations(u32),
/// When paginating results, use this page. Provided by [`GetUtxosResponse.next_page`](GetUtxosResponse).
#[serde(rename = "page")]
Page(#[serde(with = "serde_bytes")] Vec<u8>),
}

/// Unique output descriptor of a Bitcoin transaction.
#[derive(Debug, Clone, CandidType, Deserialize)]
pub struct UtxoOutpoint {
/// The ID of the transaction. Not necessarily unique on its own.
pub txid: Vec<u8>,
/// The index of the outpoint within the transaction.
pub vout: u32,
}

/// A Bitcoin [`UTXO`](https://en.wikipedia.org/wiki/Unspent_transaction_output), produced by a transaction.
#[derive(Debug, Clone, CandidType, Deserialize)]
pub struct Utxo {
/// The transaction outpoint that produced this UTXO.
pub outpoint: UtxoOutpoint,
/// The BTC quantity, in satoshis.
pub value: u64,
/// The block index this transaction was placed at.
pub height: u32,
}

/// Response type for the `bitcoin_get_utxos_query` function.
#[derive(Debug, Clone, CandidType, Deserialize)]
pub struct GetUtxosResponse {
/// A list of UTXOs available for the specified address.
pub utxos: Vec<Utxo>,
/// The hash of the tip.
pub tip_block_hash: Vec<u8>,
/// The block index of the tip of the chain known to the IC.
pub tip_height: u32,
/// If `Some`, then `utxos` does not contain the entire results of the query.
/// Call `bitcoin_get_utxos_query` again using `UtxosFilter::Page` for the next page of results.
pub next_page: Option<Vec<u8>>,
}

impl<'agent> ManagementCanister<'agent> {
/// Get the status of a canister.
pub fn canister_status(
Expand Down Expand Up @@ -467,4 +541,55 @@ impl<'agent> ManagementCanister<'agent> {
.with_effective_canister_id(*canister_id)
.build()
}

/// Gets the BTC balance (in satoshis) of a particular Bitcoin address, filtering by number of confirmations.
/// Most applications should require 6 confirmations.
pub fn bitcoin_get_balance_query(
&self,
address: &str,
network: BitcoinNetwork,
min_confirmations: Option<u32>,
) -> impl 'agent + SyncCall<(u64,)> {
#[derive(CandidType)]
struct In<'a> {
address: &'a str,
network: BitcoinNetwork,
min_confirmations: Option<u32>,
}
self.query(MgmtMethod::BitcoinGetBalanceQuery.as_ref())
.with_arg(In {
address,
network,
min_confirmations,
})
.with_effective_canister_id(network.effective_canister_id())
.build()
}

/// Fetch the list of [UTXOs](https://en.wikipedia.org/wiki/Unspent_transaction_output) for a Bitcoin address,
/// filtering by number of confirmations. Most applications should require 6 confirmations.
///
/// This method is paginated. If not all the results can be returned, then `next_page` will be set to `Some`,
/// and its value can be passed to this method to get the next page.
pub fn bitcoin_get_utxos_query(
&self,
address: &str,
network: BitcoinNetwork,
filter: Option<UtxosFilter>,
) -> impl 'agent + SyncCall<(GetUtxosResponse,)> {
#[derive(CandidType)]
struct In<'a> {
address: &'a str,
network: BitcoinNetwork,
filter: Option<UtxosFilter>,
}
self.query(MgmtMethod::BitcoinGetUtxosQuery.as_ref())
.with_arg(In {
address,
network,
filter,
})
.with_effective_canister_id(network.effective_canister_id())
.build()
}
}
16 changes: 12 additions & 4 deletions icx/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use ic_agent::{
};
use ic_utils::interfaces::management_canister::{
builders::{CanisterInstall, CanisterSettings},
MgmtMethod,
BitcoinNetwork, MgmtMethod,
};
use ring::signature::Ed25519KeyPair;
use std::{
Expand Down Expand Up @@ -323,14 +323,22 @@ pub fn get_effective_canister_id(
.context("Argument is not valid for InstallChunkedCode")?;
Ok(in_args.target_canister)
}
MgmtMethod::BitcoinGetBalanceQuery | MgmtMethod::BitcoinGetUtxosQuery => {
#[derive(CandidType, Deserialize)]
struct In {
network: BitcoinNetwork,
}
let in_args = Decode!(arg_value, In)
.with_context(|| format!("Argument is not valid for {method_name}"))?;
Ok(in_args.network.effective_canister_id())
}
MgmtMethod::BitcoinGetBalance
| MgmtMethod::BitcoinGetBalanceQuery
| MgmtMethod::BitcoinGetUtxos
| MgmtMethod::BitcoinGetUtxosQuery
| MgmtMethod::BitcoinSendTransaction
| MgmtMethod::BitcoinGetCurrentFeePercentiles
| MgmtMethod::EcdsaPublicKey
| MgmtMethod::SignWithEcdsa => {
| MgmtMethod::SignWithEcdsa
| MgmtMethod::NodeMetricsHistory => {
bail!("Management canister method {method_name} can only be run from canisters");
}
}
Expand Down

0 comments on commit 9abd75b

Please sign in to comment.