Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Creating Dogecoin Transactions

Unspent transaction outputs (UTXOs) are used as inputs to build Dogecoin transactions. Every Dogecoin transaction spends one or more UTXOs and in return creates new UTXOs. A UTXO exists until it is used as input in a transaction. In order to create a Dogecoin transaction, you need to:

  1. Get the available UTXOs corresponding to a Dogecoin address controlled by your canister using the dogecoin_get_utxos API endpoint.

  2. Calculate an appropriate transaction fee using the dogecoin_get_current_fee_percentiles API endpoint.

  3. Select a subset of the available UTXOs to spend that covers the transaction amount and fee.

  4. Create a transaction that spends the selected UTXOs and creates new UTXOs. You will need at least one for the recipient and, in most cases, one to collect the change.

A UTXO has the following structure:

// Unspent transaction output (UTXO).
pub struct Utxo {
    /// See [Outpoint].
    pub outpoint: Outpoint,
    /// Value in the units of koinu.
    pub value: Koinu,
    /// Height in the blockchain.
    pub height: u32,
}

/// Identifier of [Utxo].
pub struct Outpoint {
    /// Transaction Identifier.
    pub txid: Vec<u8>,
    /// The output index in the transaction.
    pub vout: u32,
}

Get available UTXOs

To get the available UTXOs for a Dogecoin address, use the dogecoin_get_utxos API endpoint. The following example demonstrates how to retrieve UTXOs for a given Dogecoin P2PKH address.

use crate::{dogecoin_get_utxos, DOGE_CONTEXT};
use ic_cdk::{
    bitcoin_canister::{GetUtxosRequest, GetUtxosResponse},
    update,
};

/// Returns the UTXOs of the given Dogecoin address.
#[update]
pub async fn get_utxos(address: String) -> GetUtxosResponse {
    let ctx = DOGE_CONTEXT.with(|ctx| ctx.get());

    dogecoin_get_utxos(&GetUtxosRequest {
        address,
        network: ctx.network.into(),
        filter: None,
    })
    .await
    .unwrap()
}

View the source on GitHub: get_utxo.rs

pub async fn dogecoin_get_utxos(arg: &GetUtxosRequest) -> CallResult<GetUtxosResponse> {
    let canister_id = get_dogecoin_canister_id(&into_dogecoin_network(arg.network));
    // same cycles cost as for the Bitcoin canister
    let cycles = bitcoin_canister::cost_get_utxos(arg);
    Ok(Call::bounded_wait(canister_id, "dogecoin_get_utxos")
        .with_arg(arg)
        .with_cycles(cycles)
        .await?
        .candid()?)
}

View the source on GitHub: lib.rs

Calculate transaction fee per byte

The transaction fee of a Dogecoin transaction is calculated based on the size of the transaction in bytes. An appropriate fee per byte can be determined by looking at the fees of recent transactions on the Dogecoin mainnet. The following snippet shows how to estimate the fee per byte for a transaction using the dogecoin_get_current_fee_percentiles API endpoint and choosing the 50th percentile.

use crate::{dogecoin_get_fee_percentiles, MillikoinuPerByte, DOGE_CONTEXT};
use ic_cdk::{bitcoin_canister::GetCurrentFeePercentilesRequest, update};

/// Returns the 100 fee percentiles measured in millikoinu/byte.
/// Percentiles are computed from the last 10,000 transactions (if available).
#[update]
pub async fn get_current_fee_percentiles() -> Vec<MillikoinuPerByte> {
    let ctx = DOGE_CONTEXT.with(|ctx| ctx.get());

    dogecoin_get_fee_percentiles(&GetCurrentFeePercentilesRequest {
        network: ctx.network.into(),
    })
    .await
    .unwrap()
}

View the source on GitHub: get_current_fee_percentiles.rs

pub async fn dogecoin_get_fee_percentiles(
    arg: &GetCurrentFeePercentilesRequest,
) -> CallResult<Vec<MillikoinuPerByte>> {
    let canister_id = get_dogecoin_canister_id(&into_dogecoin_network(arg.network));
    // same cycles cost as for the Bitcoin canister
    let cycles = bitcoin_canister::cost_get_current_fee_percentiles(arg);
    Ok(
        Call::bounded_wait(canister_id, "dogecoin_get_current_fee_percentiles")
            .with_arg(arg)
            .with_cycles(cycles)
            .await?
            .candid()?,
    )
}

View the source on GitHub: lib.rs

Build the transaction

Now the transaction can be built. Since the fee of a transaction is based on its size, the transaction has to be built iteratively and signed with a mock signer that adds the respective size of the signature. Each selected UTXO is used as an input for the transaction and requires a signature.

The following snippet shows a simplified version of how to build a transaction that will be signed by a P2PKH address:

// Builds a transaction to send the given `amount` of koinu to the
// destination address.
pub async fn build_transaction(
    ctx: &DogecoinContext,
    own_public_key: &PublicKey,
    own_address: &Address,
    own_utxos: &[Utxo],
    primary_output: &PrimaryOutput,
    fee_per_byte: MillisatoshiPerByte,
) -> Transaction {
    // 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 amount = match primary_output {
        PrimaryOutput::Address(_, amt) => *amt, // grab the amount
        PrimaryOutput::OpReturn(_) => trap("expected an address output, got OP_RETURN"),
    };

    let mut fee = 0;
    loop {
        let utxos_to_spend = select_utxos_greedy(own_utxos, amount, fee).unwrap();
        let transaction =
            build_transaction_with_fee(utxos_to_spend, own_address, primary_output, fee).unwrap();

        // 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 = sign_transaction(
            ctx,
            own_public_key,
            own_address,
            transaction.clone(),
            vec![], // mock derivation path
            mock_sign_with_ecdsa,
        )
        .await;

        let tx_vsize = signed_transaction.vsize() as u64;

        if (tx_vsize * fee_per_byte) / 1000 == fee {
            return transaction;
        } else {
            fee = (tx_vsize * fee_per_byte) / 1000;
        }
    }
}

View the source on GitHub: p2pkh.rs