// Package wallet derives TRON (TRC20) receiving addresses from a BIP32 account // extended public key (xpub) — watch-only, no private keys involved. The // pangolin-pay watcher uses AddressFromAccountXpub to assign a unique receiving // address per order (m/44'/195'/0'/0/i). Private-key material (seed.go) is for // OFFLINE use only (sweep signing / vector generation), never on the hot service. package wallet import ( "crypto/sha256" "fmt" "github.com/btcsuite/btcd/btcutil/base58" "github.com/btcsuite/btcd/btcutil/hdkeychain" "golang.org/x/crypto/sha3" ) // tronAddrPrefix is the TRON mainnet address version byte (0x41). It is prepended // to the 20-byte address body before Base58Check encoding, yielding the familiar // "T..." addresses. const tronAddrPrefix = 0x41 // AddressFromAccountXpub derives the TRON address at m/…// from an // account-level extended public key (e.g. the xpub of m/44'/195'/0'). It is // watch-only: an xpub can derive child addresses/public keys but never private // keys, so this is safe to run on an internet-facing service. // // change is 0 for the external (receiving) chain; index is the per-order address // index. Both are non-hardened, which is exactly why the account-level xpub can // derive them. func AddressFromAccountXpub(xpub string, change, index uint32) (string, error) { acct, err := hdkeychain.NewKeyFromString(xpub) if err != nil { return "", fmt.Errorf("wallet: parse xpub: %w", err) } if acct.IsPrivate() { return "", fmt.Errorf("wallet: expected an xpub (public extended key), got a private one") } chainKey, err := acct.Derive(change) if err != nil { return "", fmt.Errorf("wallet: derive change %d: %w", change, err) } addrKey, err := chainKey.Derive(index) if err != nil { return "", fmt.Errorf("wallet: derive index %d: %w", index, err) } pub, err := addrKey.ECPubKey() if err != nil { return "", fmt.Errorf("wallet: ec pubkey: %w", err) } return PubKeyToTronAddress(pub.SerializeUncompressed()), nil } // PubKeyToTronAddress converts a 65-byte uncompressed secp256k1 public key // (0x04 || X || Y) to a TRON Base58Check address: // // body = 0x41 || keccak256(X||Y)[12:] // last 20 bytes of the Keccak hash // address = Base58( body || dsha256(body)[:4] ) // // Note: TRON/Ethereum use *legacy* Keccak-256 (not the finalized SHA3-256). func PubKeyToTronAddress(uncompressed []byte) string { h := sha3.NewLegacyKeccak256() h.Write(uncompressed[1:]) // drop the 0x04 prefix; hash the 64-byte X||Y sum := h.Sum(nil) body := append([]byte{tronAddrPrefix}, sum[12:]...) // 0x41 + last 20 bytes return base58CheckEncode(body) } // base58CheckEncode appends a 4-byte double-SHA256 checksum and Base58-encodes. // (TRON's version byte 0x41 is already inside input, so this is a plain // checksum-append, not btcutil's version-byte CheckEncode.) func base58CheckEncode(input []byte) string { first := sha256.Sum256(input) second := sha256.Sum256(first[:]) full := make([]byte, 0, len(input)+4) full = append(full, input...) full = append(full, second[:4]...) return base58.Encode(full) }