Validation

Bitcoin addresses

Bitcoin addresses checked with their real checksums, Base58Check, Bech32 and Bech32m, and told apart: P2PKH, P2SH, SegWit v0, Taproot, the network and the output script. Addresses made from a public key or hash, and BIP-21 payment links read and written, amounts exact to the satoshi and unknown `req-` parameters refused.

Version 1.0.0 · 8 functions · 4 crates

#bitcoin#bech32#base58#bip21#address

Functions

What use bitcoin_address.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use btc_address, btc_is_valid from bitcoin_address (Quillo 0.6).

btc_address(address: str) → anyor an error

What an address is: { valid, type, network, encoding, witness_version, program, script_pubkey }, type one of p2pkh, p2sh, p2wpkh, p2wsh, p2tr, witness_unknown. Testnet and signet share their prefixes. An error value says what is wrong with it.

btc_is_valid(address: str, network: str) → any

Whether an address is valid for a network: { valid, reason }, for a form field. network is mainnet, testnet, regtest or "" for any.

btc_encode(kind: str, hash_hex: str, network: str) → stror an error

An address from a hash or key in hex. kind: p2pkh and p2sh take the 20-byte hash; p2wpkh 20 bytes; p2wsh and p2tr 32 (for p2tr, the output key, tweaked already).

btc_from_pubkey(pubkey_hex: str, network: str) → anyor an error

The addresses of a public key in hex: { p2pkh, p2sh_p2wpkh, p2wpkh, hash160 }. A SegWit one needs a compressed key (33 bytes); an uncompressed key gives p2pkh alone.

btc_sats(amount: str) → intor an error

Satoshis in an amount of bitcoin written as text: "0.0012" is 120000. An error value past 8 decimals or 21 million.

btc_amount(sats: int) → stror an error

Satoshis as an amount of bitcoin, in shortest text: 120000 is "0.0012".

btc_uri_parse(uri: str) → anyor an error

A BIP-21 bitcoin: URI read: { address, type, network, amount, sats, label, message, lightning, other }. A parameter starting req- that is not understood must be refused, so it is an error value, as is any amount not plain decimal.

btc_uri_build(address: str, params: any) → stror an error

A BIP-21 URI from an address and { amount (text) or sats, label, message, lightning, and any other parameter }. The address is checked, and spaces are written %20, as wallets expect.

Get it

$ quillo install bitcoin_address               # bitcoin_address.qwer into this folder$ quillo install bitcoin_address --example     # and its example, bitcoin_address.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/bitcoin_address.qwer$ curl -O https://quillolang.org/plugins/files/bitcoin_address.qwe

Then, in your program, all of it or only what you call:

main.qwe
use bitcoin_address.qweruse btc_address, btc_is_valid from bitcoin_address      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
sha2 = "0.10"ripemd = "0.1"hex = "0.4"serde_json = { version = "1", features = ["preserve_order"] }

Example

bitcoin_address.qwe
// Bitcoin addresses: each kind told apart with its network and script, a wrong one refused with// its reason, addresses from a public key, and BIP-21 payment links both ways. No node is asked.////   quillo run bitcoin_address.qwe//   then open http://localhost:8080/check?address=bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4, /kinds, /refused, /keys and /uriuse "bitcoin_address.qwer"route GET /check ? { address: str } {    let a = btc_address(address)    if is_err(a) { reply 422 a.error }    a}// The addresses BIP-173 and BIP-350 give, and the genesis block's.route GET /kinds = {    legacy: btc_address("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")?,    script: btc_address("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy")?.type,    segwit: btc_address("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4")?.script_pubkey,    witness_script: btc_address("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7")?,    taproot: btc_address("bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0")?,    testnet: btc_address("mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn")?.network,}// One character changed, then each thing the standards forbid.route GET /refused = {    checksum: btc_address("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNb").error,    alphabet: btc_address("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfN0").error,    bech32: btc_address("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5").error,    mixed: btc_address("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kV8F3T4").error,    wrong_constant: btc_address("bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd").error,    private_key: btc_is_valid("5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ", ""),    wrong_network: btc_is_valid("mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn", "mainnet"),}// The public key of private key 1, whose addresses are well known.let key = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"route GET /keys = {    addresses: btc_from_pubkey(key, "mainnet")?,    testnet: btc_from_pubkey(key, "testnet")?.p2wpkh,    made: btc_encode("p2tr", "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "mainnet")?,    short: btc_encode("p2wsh", "00ff", "mainnet").error,}route GET /uri {    let to = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"    let link = btc_uri_build(to, { amount: "0.0012", label: "Coffee & cake", message: "Order 1001" })?    {        link: link,        read: btc_uri_parse(link)?,        bip21: btc_uri_parse("bitcoin:1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2?amount=20.3&label=Luke-Jr")?,        sats: btc_sats("20.30000001")?,        back: btc_amount(120000)?,        required: btc_uri_parse("bitcoin:1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2?req-somethingyoudontunderstand=50").error,        exponent: btc_uri_parse("bitcoin:1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2?amount=1e3").error,        tiny: btc_sats("0.000000001").error,    }}
$ quillo run bitcoin_address.qwe$ curl 'localhost:8080/check?address=bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'$ curl 'localhost:8080/kinds'$ curl 'localhost:8080/refused'$ curl 'localhost:8080/keys'$ curl 'localhost:8080/uri'

Source

bitcoin_address.qwer
//! Bitcoin addresses checked and told apart: legacy P2PKH, P2SH, SegWit v0 and Taproot, by Base58Check, Bech32 and Bech32m, with their network and output script; addresses made from a public key or hash, and BIP-21 payment URIs read and written with amounts exact to the satoshi.//! version: 1.0.0//!//! [dependencies]//! sha2 = "0.10"//! ripemd = "0.1"//! hex = "0.4"//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Map, Value};use sha2::{Digest, Sha256};const B58: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";const B32: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";fn sha256d(b: &[u8]) -> [u8; 32] {    Sha256::digest(Sha256::digest(b)).into()}fn hash160(b: &[u8]) -> Vec<u8> {    ripemd::Ripemd160::digest(Sha256::digest(b)).to_vec()}fn base58_decode(s: &str) -> Result<Vec<u8>, String> {    let mut out: Vec<u8> = vec![];    for (i, c) in s.bytes().enumerate() {        let d = B58.iter().position(|x| *x == c).ok_or_else(|| format!("`{}` at position {} is not in Base58 (0, O, I and l are left out)", s[i..].chars().next().unwrap_or('?'), i + 1))? as u32;        let mut carry = d;        for b in out.iter_mut().rev() {            let v = *b as u32 * 58 + carry;            *b = v as u8;            carry = v >> 8;        }        while carry > 0 {            out.insert(0, carry as u8);            carry >>= 8;        }    }    let zeros = s.bytes().take_while(|c| *c == b'1').count();    let mut r = vec![0u8; zeros];    r.extend(out);    Ok(r)}fn base58_encode(b: &[u8]) -> String {    let mut digits: Vec<u8> = vec![];    for x in b {        let mut carry = *x as u32;        for d in digits.iter_mut() {            let v = ((*d as u32) << 8) + carry;            *d = (v % 58) as u8;            carry = v / 58;        }        while carry > 0 {            digits.push((carry % 58) as u8);            carry /= 58;        }    }    let zeros = b.iter().take_while(|x| **x == 0).count();    let mut s = "1".repeat(zeros);    s.extend(digits.iter().rev().map(|d| B58[*d as usize] as char));    s}fn base58check(version: u8, payload: &[u8]) -> String {    let mut v = vec![version];    v.extend_from_slice(payload);    let c = sha256d(&v);    v.extend_from_slice(&c[..4]);    base58_encode(&v)}fn polymod(vals: impl Iterator<Item = u8>) -> u32 {    let mut chk: u32 = 1;    for v in vals {        let top = chk >> 25;        chk = (chk & 0x1ffffff) << 5 ^ v as u32;        for (i, g) in [0x3b6a57b2u32, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3].iter().enumerate() {            if top >> i & 1 == 1 {                chk ^= g;            }        }    }    chk}fn hrp_expand(hrp: &str) -> Vec<u8> {    let mut v: Vec<u8> = hrp.bytes().map(|b| b >> 5).collect();    v.push(0);    v.extend(hrp.bytes().map(|b| b & 31));    v}const BECH32: u32 = 1;const BECH32M: u32 = 0x2bc830a3;// Gives the human part, the five-bit values without the checksum, and which constant it has.fn bech32_parts(s: &str) -> Result<(String, Vec<u8>, u32), String> {    if s.bytes().any(|b| b.is_ascii_lowercase()) && s.bytes().any(|b| b.is_ascii_uppercase()) {        return Err("mixes capitals and small letters, which Bech32 forbids".into());    }    if s.len() > 90 {        return Err(format!("is {} characters long; Bech32 allows 90", s.len()));    }    let low = s.to_ascii_lowercase();    let at = low.rfind('1').ok_or("has no `1` between its prefix and data")?;    if at == 0 || at + 7 > low.len() {        return Err("has too short a prefix or data part".into());    }    let hrp = &low[..at];    let mut data = vec![];    for (i, c) in low[at + 1..].bytes().enumerate() {        data.push(B32.iter().position(|x| *x == c).ok_or_else(|| format!("`{}` at position {} is not in Bech32 (1, b, i and o are left out)", c as char, at + 2 + i))? as u8);    }    let sum = polymod(hrp_expand(hrp).into_iter().chain(data.iter().copied()));    if sum != BECH32 && sum != BECH32M {        return Err("fails its checksum: a character is wrong, or two have changed places".into());    }    data.truncate(data.len() - 6);    Ok((hrp.to_string(), data, sum))}fn bech32_encode(hrp: &str, data: &[u8], constant: u32) -> String {    let sum = polymod(hrp_expand(hrp).into_iter().chain(data.iter().copied()).chain([0u8; 6])) ^ constant;    let mut s = format!("{}1", hrp);    s.extend(data.iter().map(|d| B32[*d as usize] as char));    s.extend((0..6).map(|i| B32[(sum >> (5 * (5 - i)) & 31) as usize] as char));    s}fn five_to_eight(d: &[u8]) -> Result<Vec<u8>, String> {    let (mut acc, mut bits, mut out) = (0u32, 0u32, vec![]);    for v in d {        acc = acc << 5 | *v as u32;        bits += 5;        if bits >= 8 {            bits -= 8;            out.push((acc >> bits & 255) as u8);        }    }    if bits >= 5 || acc & ((1 << bits) - 1) != 0 {        return Err("has bad padding in its witness program".into());    }    Ok(out)}fn eight_to_five(d: &[u8]) -> Vec<u8> {    let (mut acc, mut bits, mut out) = (0u32, 0u32, vec![]);    for v in d {        acc = acc << 8 | *v as u32;        bits += 8;        while bits >= 5 {            bits -= 5;            out.push((acc >> bits & 31) as u8);        }    }    if bits > 0 {        out.push((acc << (5 - bits) & 31) as u8);    }    out}struct Addr {    kind: &'static str,    network: &'static str,    encoding: &'static str,    witness: Option<u8>,    program: Vec<u8>,    script: Vec<u8>,}fn script_for(kind: &str, version: Option<u8>, p: &[u8]) -> Vec<u8> {    let mut s = vec![];    match (kind, version) {        ("p2pkh", _) => {            s.extend_from_slice(&[0x76, 0xa9, 0x14]);            s.extend_from_slice(p);            s.extend_from_slice(&[0x88, 0xac]);        }        ("p2sh", _) => {            s.extend_from_slice(&[0xa9, 0x14]);            s.extend_from_slice(p);            s.push(0x87);        }        (_, Some(v)) => {            s.push(if v == 0 { 0 } else { 0x50 + v });            s.push(p.len() as u8);            s.extend_from_slice(p);        }        _ => {}    }    s}fn read_address(input: &str) -> Result<Addr, String> {    let a = input.trim();    if a.is_empty() {        return Err("no address given".into());    }    let low = a.to_ascii_lowercase();    let segwit = (low.starts_with("bc1") || low.starts_with("tb1") || low.starts_with("bcrt1")) && a.len() > 8;    if !segwit {        let raw = base58_decode(a)?;        if raw.len() != 25 {            return Err(format!("is {} bytes long once decoded; a legacy address is 25", raw.len()));        }        let want = sha256d(&raw[..21]);        if want[..4] != raw[21..] {            return Err(format!("fails its Base58Check checksum: it ends in {} and should end in {}", hex::encode(&raw[21..]), hex::encode(&want[..4])));        }        let (kind, network) = match raw[0] {            0x00 => ("p2pkh", "mainnet"),            0x05 => ("p2sh", "mainnet"),            0x6f => ("p2pkh", "testnet"),            0xc4 => ("p2sh", "testnet"),            v => return Err(format!("has version byte {}, which is no Bitcoin address type (private keys start 128, Litecoin 48)", v)),        };        let p = raw[1..21].to_vec();        return Ok(Addr { kind, network, encoding: "base58check", witness: None, script: script_for(kind, None, &p), program: p });    }    let (hrp, data, sum) = bech32_parts(a)?;    let network = match hrp.as_str() {        "bc" => "mainnet",        "tb" => "testnet",        "bcrt" => "regtest",        h => return Err(format!("has prefix `{}`; Bitcoin's are bc, tb and bcrt", h)),    };    let v = *data.first().ok_or("has no witness version")?;    if v > 16 {        return Err(format!("has witness version {}; there are 0 to 16", v));    }    let p = five_to_eight(&data[1..])?;    if p.len() < 2 || p.len() > 40 {        return Err(format!("has a witness program of {} bytes; it is 2 to 40", p.len()));    }    if v == 0 && sum != BECH32 {        return Err("is witness version 0 with a Bech32m checksum; version 0 uses Bech32 (BIP-173)".into());    }    if v != 0 && sum != BECH32M {        return Err(format!("is witness version {} with a Bech32 checksum; version 1 and up use Bech32m (BIP-350)", v));    }    let kind = match (v, p.len()) {        (0, 20) => "p2wpkh",        (0, 32) => "p2wsh",        (0, n) => return Err(format!("is witness version 0 with a {}-byte program; it must be 20 or 32", n)),        (1, 32) => "p2tr",        _ => "witness_unknown",    };    Ok(Addr { kind, network, encoding: if sum == BECH32 { "bech32" } else { "bech32m" }, witness: Some(v), script: script_for(kind, Some(v), &p), program: p })}fn describe(a: &Addr, input: &str) -> Value {    json!({        "valid": true,        "address": input.trim(),        "type": a.kind,        "network": a.network,        "encoding": a.encoding,        "witness_version": a.witness,        "program": hex::encode(&a.program),        "script_pubkey": hex::encode(&a.script),    })}/// What an address is: { valid, type, network, encoding, witness_version, program, script_pubkey }, type one of p2pkh, p2sh, p2wpkh, p2wsh, p2tr, witness_unknown. Testnet and signet share their prefixes. An error value says what is wrong with it.pub fn btc_address(address: &str) -> Result<Value, String> {    let a = read_address(address).map_err(|e| format!("`{}` {}", address.trim(), e))?;    Ok(describe(&a, address))}/// Whether an address is valid for a network: { valid, reason }, for a form field. `network` is mainnet, testnet, regtest or "" for any.pub fn btc_is_valid(address: &str, network: &str) -> Value {    match read_address(address) {        Ok(a) if network.is_empty() || a.network == network => json!({ "valid": true, "reason": null, "type": a.kind }),        Ok(a) => json!({ "valid": false, "reason": format!("this is a {} address, not {}", a.network, network), "type": a.kind }),        Err(e) => json!({ "valid": false, "reason": format!("the address {}", e), "type": null }),    }}fn net_hrp(network: &str) -> Result<(&'static str, u8, u8), String> {    match network {        "mainnet" => Ok(("bc", 0x00, 0x05)),        "testnet" | "signet" => Ok(("tb", 0x6f, 0xc4)),        "regtest" => Ok(("bcrt", 0x6f, 0xc4)),        o => Err(format!("no network `{}`; there are mainnet, testnet, signet and regtest", o)),    }}fn segwit_address(hrp: &str, version: u8, program: &[u8]) -> String {    let mut d = vec![version];    d.extend(eight_to_five(program));    bech32_encode(hrp, &d, if version == 0 { BECH32 } else { BECH32M })}/// An address from a hash or key in hex. `kind`: p2pkh and p2sh take the 20-byte hash; p2wpkh 20 bytes; p2wsh and p2tr 32 (for p2tr, the output key, tweaked already).pub fn btc_encode(kind: &str, hash_hex: &str, network: &str) -> Result<String, String> {    let (hrp, pkh, sh) = net_hrp(network)?;    let h = hex::decode(hash_hex.trim()).map_err(|_| format!("`{}` is not hex", hash_hex))?;    let need = |n: usize| if h.len() == n { Ok(()) } else { Err(format!("a {} address takes {} bytes, this is {}", kind, n, h.len())) };    match kind {        "p2pkh" => need(20).map(|_| base58check(pkh, &h)),        "p2sh" => need(20).map(|_| base58check(sh, &h)),        "p2wpkh" => need(20).map(|_| segwit_address(hrp, 0, &h)),        "p2wsh" => need(32).map(|_| segwit_address(hrp, 0, &h)),        "p2tr" => need(32).map(|_| segwit_address(hrp, 1, &h)),        o => Err(format!("no address type `{}`; there are p2pkh, p2sh, p2wpkh, p2wsh and p2tr", o)),    }}/// The addresses of a public key in hex: { p2pkh, p2sh_p2wpkh, p2wpkh, hash160 }. A SegWit one needs a compressed key (33 bytes); an uncompressed key gives p2pkh alone.pub fn btc_from_pubkey(pubkey_hex: &str, network: &str) -> Result<Value, String> {    let (hrp, pkh, sh) = net_hrp(network)?;    let k = hex::decode(pubkey_hex.trim()).map_err(|_| format!("`{}` is not hex", pubkey_hex))?;    let ok = (k.len() == 33 && (k[0] == 2 || k[0] == 3)) || (k.len() == 65 && k[0] == 4);    if !ok {        return Err(format!("a public key is 33 bytes starting 02 or 03, or 65 starting 04; this is {} bytes", k.len()));    }    let h = hash160(&k);    if k.len() == 65 {        return Ok(json!({ "p2pkh": base58check(pkh, &h), "p2sh_p2wpkh": null, "p2wpkh": null, "hash160": hex::encode(&h) }));    }    let mut redeem = vec![0u8, 0x14];    redeem.extend_from_slice(&h);    Ok(json!({        "p2pkh": base58check(pkh, &h),        "p2sh_p2wpkh": base58check(sh, &hash160(&redeem)),        "p2wpkh": segwit_address(hrp, 0, &h),        "hash160": hex::encode(&h),    }))}// Amounts are text so that 0.1 stays 0.1.fn sats_of(btc: &str) -> Result<i64, String> {    let t = btc.trim();    let (w, f) = t.split_once('.').unwrap_or((t, ""));    if (w.is_empty() && f.is_empty()) || !w.bytes().all(|b| b.is_ascii_digit()) || !f.bytes().all(|b| b.is_ascii_digit()) {        return Err(format!("`{}` is not an amount of bitcoin: BIP-21 takes digits and a point, no exponent or sign", btc));    }    if f.len() > 8 {        return Err(format!("`{}` has more than 8 decimals, the smallest being a satoshi", btc));    }    let whole: i64 = if w.is_empty() { 0 } else { w.parse().map_err(|_| format!("`{}` is too large", btc))? };    let frac: i64 = format!("{:0<8}", f).parse().unwrap_or(0);    let total = whole.checked_mul(100_000_000).and_then(|x| x.checked_add(frac)).ok_or(format!("`{}` is too large", btc))?;    if total > 2_100_000_000_000_000 {        return Err(format!("`{}` is more than the 21 million bitcoin there will be", btc));    }    Ok(total)}fn btc_text(sats: i64) -> String {    let s = format!("{}.{:08}", sats / 100_000_000, sats % 100_000_000);    s.trim_end_matches('0').trim_end_matches('.').to_string()}/// Satoshis in an amount of bitcoin written as text: "0.0012" is 120000. An error value past 8 decimals or 21 million.pub fn btc_sats(amount: &str) -> Result<i64, String> {    sats_of(amount)}/// Satoshis as an amount of bitcoin, in shortest text: 120000 is "0.0012".pub fn btc_amount(sats: i64) -> Result<String, String> {    if sats < 0 {        return Err("an amount cannot be negative".into());    }    Ok(btc_text(sats))}fn pct_decode(s: &str) -> Result<String, String> {    let b = s.as_bytes();    let (mut out, mut i) = (vec![], 0);    while i < b.len() {        if b[i] == b'%' {            let h = s.get(i + 1..i + 3).and_then(|x| u8::from_str_radix(x, 16).ok()).ok_or(format!("`{}` has a broken %-escape", s))?;            out.push(h);            i += 3;        } else {            out.push(b[i]);            i += 1;        }    }    String::from_utf8(out).map_err(|_| format!("`{}` is not UTF-8 once decoded", s))}fn pct(s: &str) -> String {    s.bytes().map(|b| if b.is_ascii_alphanumeric() || b"-_.~".contains(&b) { (b as char).to_string() } else { format!("%{:02X}", b) }).collect()}/// A BIP-21 `bitcoin:` URI read: { address, type, network, amount, sats, label, message, lightning, other }. A parameter starting `req-` that is not understood must be refused, so it is an error value, as is any amount not plain decimal.pub fn btc_uri_parse(uri: &str) -> Result<Value, String> {    let u = uri.trim();    let (scheme, rest) = u.split_once(':').ok_or("a BIP-21 URI starts with `bitcoin:`")?;    if !scheme.eq_ignore_ascii_case("bitcoin") {        return Err(format!("a BIP-21 URI starts with `bitcoin:`, not `{}:`", scheme));    }    let rest = rest.strip_prefix("//").unwrap_or(rest);    let (addr, query) = rest.split_once('?').unwrap_or((rest, ""));    let mut o = Map::new();    let mut other = Map::new();    if addr.is_empty() {        // Allowed by BIP-21's later use, with a lightning= invoice carrying the payment.        o.insert("address".into(), Value::Null);        o.insert("type".into(), Value::Null);        o.insert("network".into(), Value::Null);    } else {        let a = read_address(addr).map_err(|e| format!("`{}` {}", addr, e))?;        o.insert("address".into(), json!(addr));        o.insert("type".into(), json!(a.kind));        o.insert("network".into(), json!(a.network));    }    for k in ["amount", "sats", "label", "message", "lightning"] {        o.insert(k.into(), Value::Null);    }    for pair in query.split('&').filter(|p| !p.is_empty()) {        let (k, v) = pair.split_once('=').unwrap_or((pair, ""));        let key = k.to_ascii_lowercase();        let v = pct_decode(v)?;        match key.as_str() {            "amount" => {                if o["amount"] != Value::Null {                    return Err("the URI has two amounts".into());                }                let sats = sats_of(&v)?;                o.insert("amount".into(), json!(btc_text(sats)));                o.insert("sats".into(), json!(sats));            }            "label" | "message" | "lightning" => {                o.insert(key, json!(v));            }            r if r.starts_with("req-") => return Err(format!("the URI requires `{}`, which is not understood, so it must not be paid", k)),            _ => {                other.insert(k.to_string(), json!(v));            }        }    }    if o["address"] == Value::Null && o["lightning"] == Value::Null {        return Err("the URI has neither an address nor a lightning invoice".into());    }    o.insert("other".into(), Value::Object(other));    Ok(Value::Object(o))}/// A BIP-21 URI from an address and { amount (text) or sats, label, message, lightning, and any other parameter }. The address is checked, and spaces are written %20, as wallets expect.pub fn btc_uri_build(address: &str, params: serde_json::Value) -> Result<String, String> {    read_address(address).map_err(|e| format!("`{}` {}", address, e))?;    let mut q = vec![];    let sats = match (params.get("sats").and_then(|v| v.as_i64()), params.get("amount")) {        (Some(s), _) => Some(s),        (None, Some(Value::String(a))) => Some(sats_of(a)?),        (None, Some(Value::Null)) | (None, None) => None,        (None, Some(_)) => return Err("give the amount as text, like \"0.0012\", or `sats` as an int: a float would round".into()),    };    if let Some(s) = sats {        q.push(format!("amount={}", btc_text(s)));    }    for (k, v) in params.as_object().into_iter().flatten() {        if k == "sats" || k == "amount" || v.is_null() {            continue;        }        let text = v.as_str().map(String::from).unwrap_or(v.to_string());        q.push(format!("{}={}", pct(k), pct(&text)));    }    Ok(if q.is_empty() { format!("bitcoin:{}", address) } else { format!("bitcoin:{}?{}", address, q.join("&")) })}
Thread
34Useful? Like it, or add to its thread below.

Thread 0 posts

On Bitcoin addresses: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.