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

Sats and Coins

Sapio uses integer satoshis for transaction values. At a JSON boundary, the interface must also make the denomination clear: 10 could otherwise mean 10 satoshis or 10 bitcoin.

Units and serialization

These types serve different purposes:

TypeRepresentation and JSON behavior
u64An unsigned integer; the interface must specify its unit.
i64A signed integer; the interface must specify its unit.
bitcoin::AmountUnsigned integer satoshis. With Bitcoin’s serde feature, its default JSON representation is an integer number of satoshis.
bitcoin::SignedAmountSigned integer satoshis. Use an explicit bitcoin::amount::serde adapter for JSON fields.
sapio_base::amount::CoinAmountA tagged input: {"Sats": 1000} or {"Btc": 0.00001}.

CoinAmount belongs to Sapio, not Bitcoin. Convert it to Amount before using it in a transaction. The Btc variant uses Amount::from_btc to check the conversion, including range and fractional-satoshi precision; Sats preserves the supplied integer exactly.

#![allow(unused)]
fn main() {
use bitcoin::Amount;
use sapio_base::amount::CoinAmount;

let amount = Amount::try_from(CoinAmount::Sats(1000)).unwrap();
assert_eq!(amount.to_sat(), 1000);
assert_eq!(serde_json::to_string(&amount).unwrap(), "1000");
}

A type’s integer range is not a contract budget or Bitcoin’s monetary limit. Validate amounts against the funds available and the rules of the interface.

Binary floating point cannot represent most decimal bitcoin fractions exactly. Prefer integer satoshis for arithmetic and JSON interfaces you control. In JavaScript, integers up to 2^53 - 1 are exact, which covers Bitcoin’s maximum supply expressed in satoshis, but does not cover every u64 value. JSON itself does not require consumers to use floating point.

Choosing a different wire format

Amount already works in a Vec<Amount> without a custom serializer. An explicit wrapper is useful when an external interface requires another format, such as a floating-point number of bitcoin. Its schema must describe that chosen format too:

#![allow(unused)]
fn main() {
use bitcoin::Amount;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// Serialize this interface's amounts as a number of bitcoin.
#[derive(
    Serialize, Deserialize, JsonSchema, Clone, Copy, Debug, Ord, PartialOrd, PartialEq, Eq,
)]
#[serde(transparent)]
struct AmountF64(
    #[schemars(with = "f64")]
    #[serde(with = "bitcoin::amount::serde::as_btc")]
    Amount,
);

impl From<Amount> for AmountF64 {
    fn from(amount: Amount) -> Self {
        Self(amount)
    }
}

impl From<AmountF64> for Amount {
    fn from(amount: AmountF64) -> Self {
        amount.0
    }
}
}

Ordinary Amount fields derive an integer-satoshi schema directly through Sapio’s sapio-jsonschema fork and its bitcoin032 feature. Explicit Serde adapters still need matching schema annotations: as_sat uses u64, while as_btc uses f64. Keep the serializer, schema and interface documentation consistent when choosing a denomination.

Checked arithmetic

Amount arithmetic operators can panic on overflow or underflow. Use checked_add, checked_sub and the other checked operations when values come from callers, and propagate an error when a calculation cannot be represented. Keep calculations in integer satoshis even when the wire format uses bitcoin.