mirror of
https://github.com/luxfi/bank.git
synced 2026-10-30 17:02:07 +00:00
Modules declare go 1.27.1 and the build images follow. go fix applied the modernizers; omitzero is not applied, since it changes what the JSON says. Co-authored-by: Hanzo Dev <[email protected]>
171 lines
6.3 KiB
Go
171 lines
6.3 KiB
Go
package bank
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// ChainBackend — what the chain itself can answer: which chain this is, what it
|
|
// carries, whether a destination is well formed, and what an address holds.
|
|
// Every one of those is a read, and a read needs no key, which is why none of it
|
|
// takes a principal. Who holds the keys and who signs is Custodian's, next door.
|
|
//
|
|
// Three implementations, chosen by chain(): evmChain against a real EVM,
|
|
// simChain for the sandbox, and offChain for a chain that was configured and
|
|
// cannot be reached.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
type ChainBackend interface {
|
|
// Network is the chain identifier the wallet reports (lux-testnet / lux-mainnet).
|
|
Network() string
|
|
// Assets is every asset this chain carries, mapped to its token contract.
|
|
// The chain's own coin has no contract, so its entry is empty — which is
|
|
// how the wallet view tells a native balance from a token balance.
|
|
Assets() map[string]string
|
|
// Valid reports whether a destination is well formed for an asset here.
|
|
Valid(asset, addr string) bool
|
|
// Balance is the asset's balance at an address.
|
|
Balance(addr, asset string) (Minor, error)
|
|
}
|
|
|
|
// chain resolves the active backend: the real EVM when one is configured, the
|
|
// simulation when none is. The simulation stays the default deliberately — the
|
|
// sandbox demo has to run with nothing configured at all.
|
|
//
|
|
// The third case is the one that matters. A bank configured for a chain it
|
|
// cannot reach must not fall back to the simulation, because the simulation
|
|
// answers a send with a receipt for a transfer that never happened. It refuses.
|
|
func chain() ChainBackend {
|
|
if c := evm(); c != nil {
|
|
return c
|
|
}
|
|
if ChainConfigured() {
|
|
return offChain{}
|
|
}
|
|
return simChain{}
|
|
}
|
|
|
|
// offChain stands in for a chain the bank was told to use and cannot reach.
|
|
// Every operation fails; nothing is invented.
|
|
type offChain struct{}
|
|
|
|
var errChainDown = errors.New("the configured chain is unreachable")
|
|
|
|
func (offChain) Network() string { return networkName() }
|
|
func (offChain) Assets() map[string]string { return map[string]string{} }
|
|
func (offChain) Valid(_, addr string) bool { return validEVMAddress(addr) }
|
|
func (offChain) Balance(string, string) (Minor, error) { return 0, errChainDown }
|
|
|
|
// simChain simulates the testnet: it names the network, lists the assets, and
|
|
// has nothing to read a balance from. Its stand-in addresses and receipts belong
|
|
// to simCustodian, because inventing those is custody, not chain.
|
|
type simChain struct{}
|
|
|
|
func (simChain) Network() string { return networkName() }
|
|
|
|
// simAddress is the simulation's display address for an account in an asset. The
|
|
// simulation models each asset as its own chain, so BTC is bech32 and everything
|
|
// else is 0x — a shape a real EVM does not have, and one more reason the
|
|
// simulation is reachable only with no chain configured at all.
|
|
func simAddress(index, asset string) string {
|
|
if strings.ToUpper(asset) == "BTC" {
|
|
return simBTCAddress(index)
|
|
}
|
|
return evmAddress(index, asset)
|
|
}
|
|
|
|
// Assets: the simulation models each asset as its own chain, so none of them is
|
|
// a token contract on a shared one.
|
|
func (simChain) Assets() map[string]string {
|
|
out := make(map[string]string, len(SupportedCrypto))
|
|
for _, asset := range SupportedCrypto {
|
|
out[asset] = ""
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (simChain) Valid(asset, addr string) bool { return validAddress(asset, addr) }
|
|
|
|
// Balance: the simulation has no chain to read, so the ledger is the truth.
|
|
func (simChain) Balance(string, string) (Minor, error) {
|
|
return 0, errors.New("no chain configured")
|
|
}
|
|
|
|
// txHashFor returns a random display tx hash in the shape of the asset's chain:
|
|
// Bitcoin hashes are 64 bare hex chars; EVM-family hashes are 0x + 64 hex.
|
|
func txHashFor(asset string) string {
|
|
if strings.ToUpper(asset) == "BTC" {
|
|
var b [32]byte
|
|
rand.Read(b[:])
|
|
return hex.EncodeToString(b[:])
|
|
}
|
|
return simTxHash()
|
|
}
|
|
|
|
// simTxHash returns a random 0x + 64-hex display hash for an EVM transaction.
|
|
func simTxHash() string {
|
|
var b [32]byte
|
|
rand.Read(b[:])
|
|
return "0x" + hex.EncodeToString(b[:])
|
|
}
|
|
|
|
// evmAddress derives a deterministic, display-only EVM (0x) address from a seed
|
|
// and asset. Nobody holds the key to it — it is a stable sandbox stand-in,
|
|
// distinct per asset, and simCustodian.Holds answers false so it can never
|
|
// replace an address somebody can actually spend from.
|
|
func evmAddress(seed, asset string) string {
|
|
sum := sha256.Sum256([]byte("lux-evm:" + strings.ToUpper(asset) + ":" + seed))
|
|
return "0x" + hex.EncodeToString(sum[12:32])
|
|
}
|
|
|
|
// simBTCAddress derives a deterministic, display-only native-SegWit v0 (bc1q)
|
|
// address whose bech32 checksum verifies, so a BTC deposit address the sandbox
|
|
// shows also passes validAddress and never reads as invalid to the sender. The
|
|
// 20-byte witness program is derived from the seed (not a spendable key) — a
|
|
// sandbox stand-in, valid in shape and checksum but not backed by a real key.
|
|
func simBTCAddress(seed string) string {
|
|
sum := sha256.Sum256([]byte("lux-btc:" + seed))
|
|
return bech32Encode("bc", 0, sum[:20])
|
|
}
|
|
|
|
// bech32Encode builds a BIP-173 SegWit address: hrp + '1' + witver + the
|
|
// 8→5-bit-repacked program + a 6-symbol checksum.
|
|
func bech32Encode(hrp string, witver byte, program []byte) string {
|
|
data := append([]int{int(witver)}, convertBits(program, 8, 5, true)...)
|
|
// Checksum: polymod over hrp-expanded data + 6 zero symbols, XOR 1.
|
|
polymod := bech32Polymod(hrp, append(append([]int{}, data...), 0, 0, 0, 0, 0, 0)) ^ 1
|
|
for i := range 6 {
|
|
data = append(data, int((polymod>>uint(5*(5-i)))&31))
|
|
}
|
|
var b strings.Builder
|
|
b.WriteString(hrp)
|
|
b.WriteByte('1')
|
|
for _, d := range data {
|
|
b.WriteByte(bech32Charset[d])
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// convertBits repacks a byte stream from `from`-bit to `to`-bit groups.
|
|
func convertBits(data []byte, from, to uint, pad bool) []int {
|
|
acc, bits := 0, uint(0)
|
|
maxv := (1 << to) - 1
|
|
out := make([]int, 0, len(data)*int(from)/int(to)+1)
|
|
for _, b := range data {
|
|
acc = (acc << from) | int(b)
|
|
bits += from
|
|
for bits >= to {
|
|
bits -= to
|
|
out = append(out, (acc>>bits)&maxv)
|
|
}
|
|
}
|
|
if pad && bits > 0 {
|
|
out = append(out, (acc<<(to-bits))&maxv)
|
|
}
|
|
return out
|
|
}
|