Bitcoin address classification
Classify addresses and derive the script hash an Electrum-style server indexes by.
module wallet
import nanyx/encoding
-- Classifying bitcoin addresses and deriving the script hash that an
-- Electrum-style server indexes them by.
export type AddressType =
| #p2pkh
| #p2sh
| #p2wpkh
| #p2tr
| #p2pkhTestnet
| #p2shTestnet
| #p2wpkhTestnet
| #p2trTestnet
| #unknown
export def addressType: string -> AddressType = { address ->
if | address \startsWith("bc1q") -> #p2wpkh
| address \startsWith("bc1p") -> #p2tr
| address \startsWith("1") -> #p2pkh
| address \startsWith("3") -> #p2sh
| address \startsWith("tb1q") -> #p2wpkhTestnet
| address \startsWith("tb1p") -> #p2trTestnet
| address \startsWith("m") -> #p2pkhTestnet
| address \startsWith("2") -> #p2shTestnet
| else -> #unknown
}
export def AddressType.isTestnet: AddressType -> bool = {
| #p2pkhTestnet | #p2shTestnet | #p2wpkhTestnet | #p2trTestnet -> true
| _ -> false
}
-- A pay-to-public-key-hash output script wraps the 20-byte hash of the key
export def addressToP2PKH: string -> #some(string) | #error(#invalidAddress) = { address -> proc {
def decoded = try Base58Check.decode(address) \Result.mapError { _ -> #invalidAddress }
def hash160 = decoded \Hex.encode \drop(2) -- drop the version byte
return "76a914{hash160}88ac"
}}
-- sha256 of the output script, byte-reversed
export def scriptHash: Binary -> string = { script ->
script \Sha256.hash \Binary.reverse \Hex.encode
}
type ScriptInfo = (output: string, scriptHash: string, address: string)
export def detectAddressTypeToScriptHash
: string -> #some(ScriptInfo) | #error(#unrecognizedAddress | #invalidAddress)
= { address ->
match addressType(address)
| #unknown -> #error(#unrecognizedAddress)
| #p2pkh | #p2pkhTestnet -> proc {
def output = try addressToP2PKH(address)
return ScriptInfo(output = output, scriptHash = scriptHash(Hex.decode(output)), address = address)
}
| _ -> proc {
def output = try Bech32.toOutputScript(address) \Result.mapError { _ -> #invalidAddress }
return ScriptInfo(output = Hex.encode(output), scriptHash = scriptHash(output), address = address)
}
}
-- Replace the "primary" and "funding" aliases with real addresses from the wallet
type WalletInfo = (primary: string, funding: string, imported: map(string, string))
export def resolveAlias: (WalletInfo, string) -> string = { wallet, alias ->
if | alias == "primary" -> wallet.primary
| alias == "funding" -> wallet.funding
| else ->
def #some(address) = wallet.imported(alias) except #notFound -> #some(alias)
address
}
This program lives in the repository as examples/wallet.nyx.