Guide

Working with Addresses

Turn a public key into an address. Pick a format. Check one you were given.

From public key to address

js
import { useBlockchain } from "@agntn/keys";
import Bitcoin from "@agntn/keys/blockchains/bitcoin";

const chain = useBlockchain(new Bitcoin());
const keys = chain.generateKeys();
const address = chain.getAddress(keys.keys.public);

console.log(address); // '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'

Every chain does the same thing here: hash the public key, then encode the hash the way the chain likes. What differs is the hash, the encoding, and whether there's a checksum.

Address types

The second argument picks a format on chains that have more than one.

js
chain.getAddress(publicKey); // legacy, starts with 1
chain.getAddress(publicKey, "segwit"); // bc1q...
chain.getAddress(publicKey, "taproot"); // bc1p...

Bitcoin and Litecoin have five formats, Bitcoin Gold four, Cardano three. Sui and the XRP Ledger use it to name the curve. Everything else has one format and throws if you ask for another. Leave the argument out.

Validating

js
chain.validateAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"); // true
chain.validateAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNb"); // false, checksum

Validation checks the format and the checksum when the format has one. It doesn't touch the network. A valid address with no history is still valid.

validateAddress is optional on the BlockchainImplementation type so that structural implementations can skip it, but every class built on AbstractBlockchain has one. Every built in chain does.

How each chain builds its address

ChainRecipe
BitcoinRIPEMD160(SHA256(pubkey)), then base58check for legacy and p2sh, bech32 for segwit, bech32m with a BIP341 tweak for taproot
Bitcoin CashBitcoin's P2PKH hash behind a version byte, then CashAddr with the bitcoincash prefix and a 40-bit checksum
Bitcoin Goldthe same hash and encodings, version 0x26 for legacy (G), 0x17 for p2sh (A), bech32 with btg, no taproot
Bitcoin SVBitcoin's legacy recipe, version 0x00, base58check, so it starts with 1
Litecointhe same hash and encodings, version 0x30 for legacy (L), 0x32 for p2sh (M), bech32 with ltc
DashBitcoin's legacy recipe, version 0x4c, base58check, so it starts with X
DecredRIPEMD160(BLAKE256(pubkey)) behind the two byte prefix 07 3f, base58check with a double BLAKE-256 checksum, so it starts with Ds
DogecoinBitcoin's legacy recipe, version 0x1e, base58check, so it starts with D
ZcashBitcoin's legacy recipe behind the two byte version 1c b8, base58check, so it starts with t1
eCashBitcoin Cash's recipe under the ecash prefix, so the same key hash starts with ecash:q
Ethereum, Baselast 20 bytes of Keccak-256(uncompressed pubkey without 04), 0x prefix, EIP-55 mixed case checksum
TRONsame 20 bytes as Ethereum, version byte 0x41, base58check, so it starts with T
Solanathe ed25519 public key itself, base58
Stellarversion byte 0x30, the ed25519 public key, a CRC16-XModem checksum, all in base32, so it starts with G
AptosSHA3-256(pubkey + 0x00), 0x prefix
SuiBlake2b-256(flag + pubkey), flag 0x00 for ed25519 and 0x01 for secp256k1, 0x prefix
CardanoBlake2b-224(pubkey) behind a header byte, bech32 with addr or stake
XRP LedgerBitcoin's RIPEMD160(SHA256(pubkey)) behind version 0x00, base58check in XRPL's alphabet, so it starts with r; an ed25519 key gets hashed with its ED byte
NEARthe ed25519 public key itself, lowercase hex, no prefix; named accounts like alice.near come back invalid
CosmosBitcoin's RIPEMD160(SHA256(compressed pubkey)), bech32 under the chain's prefix, cosmos1... on the Hub and osmo1... on Osmosis
Polkadotthe ed25519 public key itself behind the network prefix, two bytes of blake2b-512 as checksum, base58; 1... on Polkadot, a capital letter on Kusama
Moneroa network byte, the public spend key, the public view key and four bytes of Keccak-256, block base58, 95 characters or 106 with a payment ID; 4... on mainnet, 8... for a subaddress

The chain pages have the code for each, plus the testnet variants where they exist.

A quick tour

js
import Ethereum from "@agntn/keys/blockchains/ethereum";
import Solana from "@agntn/keys/blockchains/solana";
import Sui from "@agntn/keys/blockchains/sui";

const ethereumChain = useBlockchain(new Ethereum());
ethereumChain.getAddress(ethereumChain.getKeyPublic(privateKey)); // '0x...' with checksum

const solanaChain = useBlockchain(new Solana());
solanaChain.getAddress(solanaChain.getKeyPublic(privateKey)); // base58, no hashing at all

const suiChain = useBlockchain(new Sui());
const secpKey = suiChain.getKeyPublic(privateKey, { scheme: "secp256k1" });
suiChain.getAddress(secpKey, "secp256k1"); // '0x...' from the secp256k1 flag

The Sui line is the one people get wrong: a secp256k1 public key hashed with the ed25519 flag produces a perfectly valid looking address that no key can spend from. Keep the scheme on both calls.