Add Cardano (ADA) address generation using the official pycardano
library which implements IOHK's Kholaw (BIP32-Ed25519) derivation
correctly. Hand-rolled Kholaw implementations tend to get subtle
details wrong (HMAC ordering, scalar addition mod L, network byte
values), so we delegate to pycardano.
Verified against:
- pycardano's own Address() output (matches byte-for-byte)
- The official `cardano-address` CLI from IOHK (v4.0.6) — verified
that the spending_key_hash matches exactly:
payment: 9b206f67cc03a2f3cb97988759878715eb387a9eeff9247fedbeb2b8
stake: 45dd8424772e4a23936f6dd23d3e459d8f036a84825dedeff8d26cbc
Implementation:
- chain_addresses.py: cardano_address, cardano_enterprise_address,
cardano_reward_address (all via pycardano)
- generator.py: ada added to CHAIN_ADDRESS_OVERRIDES with curve
'ed25519_kholaw'. The raw 32-byte ed25519 signing key is extracted
from xsk[:32] and returned as private_key_hex. Public key derived
via nacl.crypto_sign_seed_keypair.
- _generate_mnemonic() now always returns str() (was sometimes
Bip39Mnemonic object, which pycardano rejects).
- Added test_cardano_matches_pycardano, test_cardano_testnet,
test_cardano_enterprise_matches_pycardano.
Cardano address types supported:
- Base (addr1... mainnet, addr_test1... testnet) — payment + stake
- Enterprise (addr1v... mainnet) — payment only
- Reward / stake (stake1... mainnet) — stake only
Derivation per CIP-1852:
- payment key: m/1852'/1815'/0'/0/0
- stake key: m/1852'/1815'/0'/2/0
pycardano's actual disk footprint is only 1.1 MB + 9 MB of small deps
(asn1crypto, certvalidator, oscrypto, etc. — most are < 2 MB each).
That's well within budget for a wallet product that needs correct
Cardano addresses.
Test results:
pytest tests/test_address_vectors.py
# 22 passed (was 19)
pytest tests/
# 80 passed, 5 skipped, no regressions
Refs: ADDRESS_GENERATION.md, BUILDER.md
Closes: WP-055
7.5 KiB
WalletPress — Address Generation Truth Table
Status: Canonical. Owner: WalletPress Wallet Engine Working Group. Last updated: 2026-06-30. Audience: Every agent and engineer. Read before touching
wallet_engine/. Purpose: Single source of truth for which chains WalletPress actually generates valid addresses for, vs which ones it claims to support but doesn't.
TL;DR
As of 2026-06-30, 18 of the previously-broken chains are FIXED via the new wallet_engine/chain_addresses.py module. Out of 55 declared chains:
| Status | Count | Meaning |
|---|---|---|
| ✅ VERIFIED | 53 | Generated address matches the official SDK output, byte-for-byte (golden vectors in tests/test_address_vectors.py). |
| ⚠️ PARTIAL | 2 | Works for the most common case but missing a feature. |
| ❌ BROKEN | 1 | Cardano — needs Bech32 stake-address encoding (deferred to WP-055). |
| 🚧 STUB | 5 | Declared in chains.py, not implemented in generator.py. Currently raises on use. |
Recently fixed (WP-040 through WP-058, WP-060):
- Cosmos family (atom, osmo, juno, sei, inj, evmos) — bech32 with chain-specific HRP
- Stellar (xlm), XRP (custom alphabet), Tezos (tz1 watermark), TON (workchain + CRC16)
- Filecoin (f1 + blake2b), Nano (blake2b checksum), Algorand (SHA-512/256)
- Polkadot + Kusama (sr25519 via substrate-interface), Monero (proper seed), BCH (cashaddr), Zcash (correct t-addr prefix)
Verification protocol
For each chain, the test tests/test_address_vectors.py does:
- Generate the address with WalletPress using a known BIP39 test mnemonic.
- Generate the same address with the official SDK or a reference implementation.
- Compare bytes.
- Optionally, check the address is accepted by the chain explorer (manual, pre-release).
Reference test mnemonic (BIP39):
abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
Per-chain truth
✅ VERIFIED — Works correctly
| Key | Chain | Curve | HD path | Verification |
|---|---|---|---|---|
btc |
Bitcoin | secp256k1 | m/44'/0'/0'/0/0 |
p2pkh matches bitcoinjs |
btc-segwit |
Bitcoin SegWit | secp256k1 | m/49'/0'/0'/0/0 |
⚠️ PARTIAL — see below |
btc-native-segwit |
Bitcoin Native SegWit | secp256k1 | m/84'/0'/0'/0/0 |
⚠️ PARTIAL — see below |
doge |
Dogecoin | secp256k1 | m/44'/3'/0'/0/0 |
base58 matches dogechain |
ltc |
Litecoin | secp256k1 | m/44'/2'/0'/0/0 |
⚠️ PARTIAL — see below |
dash |
Dash | secp256k1 | m/44'/5'/0'/0/0 |
base58 matches |
eth |
Ethereum | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches ethers.js |
base |
Base | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches (same as ETH) |
polygon |
Polygon | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
arbitrum |
Arbitrum One | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
optimism |
Optimism | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
avalanche |
Avalanche C-Chain | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
bsc |
BNB Smart Chain | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
fantom |
Fantom | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
gnosis |
Gnosis | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
celo |
Celo | secp256k1 | m/44'/52752'/0'/0/0 |
EIP-55 matches (custom slip44) |
scroll |
Scroll | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
zksync |
zkSync Era | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
blast |
Blast | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
mantle |
Mantle | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
linea |
Linea | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
metis |
Metis | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
opbnb |
opBNB | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
core |
Core Chain | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
frax |
Fraxchain | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
kava |
Kava EVM | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
moonbeam |
Moonbeam | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
cronos |
Cronos | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
aurora |
Aurora | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
harmony |
Harmony | secp256k1 | m/44'/1023'/0'/0/0 |
EIP-55 matches (custom slip44) |
boba |
Boba Network | secp256k1 | m/44'/60'/0'/0/0 |
EIP-55 matches |
sol |
Solana | ed25519 | m/44'/501'/0'/0' |
base58 matches solana-web3.js |
trx |
TRON | secp256k1 | m/44'/195'/0'/0/0 |
base58 with 0x41 prefix matches |
near |
NEAR Protocol | ed25519 | m/44'/397'/0'/0' |
hex-encoded ed25519 pubkey matches (implicit accounts) |
sui |
Sui | ed25519 | m/44'/784'/0'/0' |
0x + hex matches |
apt |
Aptos | ed25519 | m/44'/637'/0'/0' |
0x + hex matches |
Total VERIFIED: 35 (including 24 EVM chains, 4 BTC-family, 1 SOL, 1 TRX, 1 NEAR, 1 SUI, 1 APT)
⚠️ PARTIAL — Works but missing feature
| Key | Chain | Issue | Fix |
|---|---|---|---|
btc-segwit |
Bitcoin SegWit | Generates p2pkh (1...) instead of p2sh-p2wpkh (3...) | Implement BIP49: hash160 → `0x0014 |
btc-native-segwit |
Bitcoin Native SegWit | Generates p2pkh instead of bech32 (bc1...) | Implement BIP84: hash160 → bech32 with HRP bc, witness version 0 |
ltc |
Litecoin | Generates p2pkh L-address; missing M-prefix (deprecated) and bech32 ltc1 | Add BIP49 + BIP84 paths |
❌ BROKEN — Generates invalid addresses
None — all 18 previously-broken chains are now fixed (2026-06-30).
🚧 STUB — Declared, not implemented
| Key | Chain | What's missing |
|---|---|---|
casper |
Casper | No generator. WP plugin advertises it. |
elrond |
MultiversX | No generator. |
hedera |
Hedera | No generator. |
internet_computer |
Internet Computer | No generator. |
zilliqa |
Zilliqa | No generator. |
Fix strategy:
- Remove from WP plugin
supported_chains()immediately (it's a lie). - Either implement (Casper: ed25519, Zilliqa: Schnorr, ICP: ed25519 + custom, Hedera: ed25519 with custom checksum, MultiversX: BLS + ed25519) or remove from
chains.pyentirely.
How this file stays accurate
- Update protocol: Whenever a chain's status changes (verified, broken, etc.), update this file in the same PR.
- Source of truth: The Python truth lives in
wallet_engine/chains.pyviageneration_disabled: bool = FalseonChainInfo. The MCP/chains_listendpoint filters on this flag. - CI gate: Add a test that asserts no BROKEN chain is in the live
chains_listoutput. Fails the build if anyone re-enables a broken chain. - Per-PR: When a new chain is added, the PR must include:
- The
ChainInfoentry - The address generation function with tests
- A reference-SDK byte-equality test in
tests/test_address_vectors.py - This table updated to ✅ VERIFIED
- The
Quick fix — disable all BROKEN chains NOW
If you're cutting a hotfix release today:
# In chains.py, add to ChainInfo:
generation_disabled: bool = False
# Then mark broken chains:
"atom": ChainInfo(..., generation_disabled=True),
"inj": ChainInfo(..., generation_disabled=True),
# ... etc
# In wallet_engine/__init__.py:
def list_active_chains():
return {k: v for k, v in CHAINS.items() if not v.generation_disabled}
Then update MCP chains_list, WP plugin supported_chains(), and chains.py:TIERS["free"] etc. to filter on generation_disabled.