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
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 errorWhat 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) → anyWhether 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 errorAn 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 errorThe 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 errorSatoshis 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 errorSatoshis as an amount of bitcoin, in shortest text: 120000 is "0.0012".
btc_uri_parse(uri: str) → anyor an errorA 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 errorA 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/bitcoin_address.qwer$ curl -O https://quillolang.org/plugins/files/bitcoin_address.qweThen, in your program, all of it or only what you call:
use bitcoin_address.qweruse btc_address, btc_is_valid from bitcoin_address // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
sha2 = "0.10"ripemd = "0.1"hex = "0.4"serde_json = { version = "1", features = ["preserve_order"] }Example
// 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 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("&")) })}Related
Thread 0 posts
On Bitcoin addresses: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.