Skip to content

Ethereum (ETH) Wallet Architecture

This document is the authoritative reference for the ETH module's wallet architecture within go-crypto-wallet's Clean Architecture + 3-wallet security model (Watch / Keygen / Sign).

It covers:

  • Wallet role assignment: which wallets are required for ETH and why
  • Clean Architecture boundary map: use case, port, and infrastructure boundaries per wallet
  • Use case assignments: which wallet owns which use cases
  • Key differences from BTC: account model, EIP-1559, Safe multisig

Related documents:


1. Wallet Roles for ETH

ETH supports two transaction flows with different wallet requirements:

WalletEnvironmentSingle-sig EOASafe MultisigResponsibilities
WatchOnlineRequiredRequiredCreate/propose transactions, broadcast/submit, monitor confirmations
KeygenOffline (air-gapped)RequiredRequiredGenerate HD keys, manage keystore, sign (EOA single-sig or Safe owner 1)
SignOffline (air-gapped)Not requiredRequiredSign as Safe owner 2…n (offline EIP-712 signing)

Single-sig EOA (keygen sign --file): Only Watch + Keygen are needed. Keygen holds the EOA private key and signs the raw transaction directly.

Safe Multisig (keygen/sign sign signature --signer-address): All three wallets participate. Each owner signs the EIP-712 safeTxHash independently offline. Watch submits execTransaction once the threshold is met.


2. Use Case Assignments per Wallet

Watch Wallet

Single-sig EOA

Use CaseResponsibility
CreateTransactionBuild unsigned EIP-1559 (or legacy) transaction; write to JSON file; save record to DB
SendTransactionRead signed JSON file; broadcast via eth_sendRawTransaction
MonitorTransactionPoll receipt; track confirmation count; update status in DB

Safe Multisig

Use CaseResponsibility
CreateETHMultisigTransactionCall getTransactionHash on Safe; write unsigned proposal JSON file
SendETHMultisigTransactionRead fully-signed JSON file; call execTransaction on Safe contract
ETHSafeInfoRead Safe contract state: owners, threshold, nonce, version

Keygen Wallet

Key Management (shared)

Use CaseResponsibility
GenerateSeedGenerate BIP-39 mnemonic; store encrypted seed
GenerateHDWalletDerive BIP-44 keys at m/44'/60'/account'/0/i; store in DB
ImportPrivateKeyImport ECDSA key into local keystore (scrypt-encrypted)
ExportAddressExport public addresses to file for Watch Wallet import

Single-sig signing

Use CaseResponsibility
SignTransactionRead unsigned JSON file; derive child key from xpriv; sign with LatestSignerForChainID; write signed JSON file

Safe multisig signing

Use CaseResponsibility
SignMultisigTransactionRead proposal JSON file; recompute safeTxHash offline via EIP-712; append owner 1 signature; write new JSON file

Sign Wallet

Safe multisig signing (ETH only)

Use CaseResponsibility
SignMultisigTransactionRead partially-signed JSON file; recompute safeTxHash offline via EIP-712; append owner N signature; write new JSON file

The Sign Wallet is not used for ETH single-sig EOA. It is only active in the Safe multisig flow.


3. Architecture Boundary Map

Single-sig EOA

Safe Multisig


4. Port Interface Responsibilities

Single-sig EOA Ports

PortMethodsUsed By
ChainConfigProviderCoinTypeCode(), GetChainConf()CreateTx, KeygenSignTx
TxCreatorCreateRawTransaction(), CreateRawTransactionEIP1559(), SupportsEIP1559()CreateTx
GasEstimatorGasPrice(), EstimateGas(), SuggestGasTipCap()CreateTx
TxSignerSignOnRawTransaction(rawTx, privKey, chainID)KeygenSignTx
TxSenderSendSignedRawTransaction()SendTx
TxMonitorGetTransactionReceipt(), GetConfirmation()MonitorTx
BalanceCheckerGetTotalBalance(), BalanceAt()MonitorTx
AddressValidatorValidateAddr()CreateTx

The existing Ethereum struct (infrastructure) satisfies all small interfaces implicitly via Go's structural typing.

Safe Multisig Ports (application/ports/api/eth/)

PortMethodsUsed By
SafeNonceReaderGetSafeNonce(ctx, safeAddress)CreateETHMultisigTransaction
SafeTxHashComputerGetSafeTxHash(ctx, safe, to, value, data, op, nonce)CreateETHMultisigTransaction
SafeExecutorExecTransaction(ctx, file, gasPayerKey)SendETHMultisigTransaction
SafeInfoReaderGetSafeInfo(ctx, safeAddress)ETHSafeInfo

File Port (application/ports/file/)

PortMethodsUsed By
MultisigFileRepositorierReadETHMultisigJSONFile(), WriteETHMultisigJSONFile(), CreateMultisigFilePath()Create, Sign, Send multisig use cases

5. Signing Detail

5a. KeygenSignTx: Offline Single-sig Signing

The SignTransaction use case in the Keygen wallet has no network dependency.

KeygenSignTxUseCase.Sign(filePath)

    ├── Read unsigned JSON file (TransactionFileRepo)
    │     Fields: chain_id, nonce, from, to, value, gas, fee fields, raw_tx_hex

    ├── Load accountXpriv from DB (AccountKeyRepository)

    ├── Derive child private key
    │     hdkeychain: m/44'/60'/account'/0/addressIndex
    │     → *btcec.PrivateKey → *ecdsa.PrivateKey

    ├── Decode raw_tx_hex → types.Transaction (MarshalBinary format)

    ├── Sign transaction (offline, no RPC)
    │     signer := types.LatestSignerForChainID(chainID)
    │     signedTx, err := types.SignTx(tx, signer, privKey)

    ├── Verify sender address
    │     sender := types.Sender(signer, signedTx)
    │     assert sender == from

    └── Write signed JSON file (TransactionFileRepo)
          Adds: signed_tx_hex field

Key design decisions:

DecisionRationale
Use LatestSignerForChainID not NewLondonSignerForward-compatible: automatically selects correct signer for any tx type
Accept *ecdsa.PrivateKey directly in TxSigner portEnables true offline signing without node or keystore RPC dependency
Use MarshalBinary / UnmarshalBinary not RLPCorrect EIP-2718 typed transaction envelope; RLP breaks for Type 2
Verify sender after signingDetects key derivation index mismatch before writing file

5b. SignMultisigTransaction: Offline EIP-712 Signing (Safe)

The SignMultisigTransaction use case is shared by both Keygen and Sign wallets. It has no network dependency.

SignMultisigTransactionUseCase.Sign(filePath, signerAddress)

    ├── Read proposal JSON file (MultisigFileRepositorier)
    │     Fields: safe_address, to, value, data, operation, gas fields,
    │             nonce, chain_id, safe_tx_hash, threshold, signatures

    ├── Recompute safeTxHash offline (EIP-712)
    │     domainSeparator = keccak256(domainTypeHash || chainID || safeAddress)
    │     safeTxStructHash = keccak256(safeTxTypeHash || ABI-encode(tx fields))
    │     safeTxHash = keccak256("\x19\x01" || domainSeparator || safeTxStructHash)

    ├── Verify: recomputed hash == file.safe_tx_hash → abort if mismatch

    ├── Guard: signerAddress not already in file.signatures

    ├── Load accountXpriv from cold DB (ETHAccountKeyRepositorier)

    ├── Derive child private key (same BIP-44 derivation as single-sig)

    ├── Sign the hash (offline, no RPC)
    │     sig, _ := crypto.Sign(safeTxHash[:], privKey)
    │     sig[64] += 27  // adjust V to Safe's {27, 28} convention

    ├── Append SignEntry{signerAddress, "0x" + hex(sig)} to file.signatures

    ├── If len(signatures) >= threshold → set tx_type = "signed"

    └── Write new JSON file with incremented counter
          {actionType}_multisig_{uuid}_{signCount}.json

For the EIP-712 domain and struct type hashes, see internal/application/usecase/keygen/eth/safe_tx_hash.go.


6. Comparison with BTC Pattern

AspectBTC Keygen SignETH Single-sigETH Safe Multisig
Transaction formatPSBT (BIP-174)EIP-2718 JSONETHMultisigTransactionFile JSON
Signing librarybtcd txscriptgo-ethereum types.SignTxgo-ethereum crypto.Sign
Key derivationxpriv → child WIFxpriv → child ECDSAxpriv → child ECDSA (same)
SignerECDSA/Schnorr (Taproot)ECDSA with chain IDECDSA + EIP-712 domain
Multiple inputsDerives multiple WIFs for multiple UTXOsSingle key (account model)Single key per owner
Sign Wallet used?Yes (additional PSBT sigs)NoYes (owner N signature)
Completeness checkUnsignedCount == 0Signature present in SignedTxHexlen(signatures) >= threshold
Broadcastsendrawtransactioneth_sendRawTransactionexecTransaction (Safe ABI)

7. Directory Layout

internal/
├── domain/
│   ├── ethereum/
│   │   ├── types.go                    # RawTx, BlockInfo, ResponseGetTransaction
│   │   └── eth_detail_tx.go            # ETHDetailTx entity
│   └── transaction/
│       └── types.go                    # ActionType, TxTypeSigned/Unsigned (shared)

├── application/
│   ├── dto/eth/
│   │   └── multisig_transaction_file.go  # ETHMultisigTransactionFile, SignEntry, validation
│   │
│   ├── ports/api/eth/
│   │   ├── interface.go                # Monolithic Ethereumer (legacy, preserved)
│   │   ├── interfaces_small.go         # ISP-compliant small interfaces (EOA single-sig)
│   │   └── interfaces_safe.go          # Safe multisig ports: SafeNonceReader, SafeTxHashComputer, SafeExecutor, SafeInfoReader
│   │
│   ├── ports/file/
│   │   └── multisig_file.go            # MultisigFileRepositorier interface
│   │
│   └── usecase/
│       ├── watch/eth/
│       │   ├── create_transaction.go
│       │   ├── send_transaction.go
│       │   ├── monitor_transaction.go
│       │   ├── create_multisig_transaction.go   # Safe: propose → write _0.json
│       │   ├── send_multisig_transaction.go     # Safe: execTransaction on-chain
│       │   └── safe_info.go                     # Safe: read owners/threshold/nonce
│       └── keygen/eth/
│           ├── sign_transaction.go              # EOA single-sig (Keygen only)
│           ├── sign_multisig_transaction.go     # Safe: EIP-712 sign (Keygen + Sign)
│           ├── safe_tx_hash.go                  # EIP-712 offline hash computation
│           ├── import_private_key.go
│           └── generate_hdwallet.go (shared)

└── infrastructure/
    ├── api/eth/
    │   ├── eth/
    │   │   ├── ethereum.go             # Ethereum struct (implements all small interfaces)
    │   │   ├── transaction.go          # CreateRawTransaction, CreateRawTransactionEIP1559
    │   │   └── key.go                  # SignOnRawTransaction, GetPrivKey
    │   ├── ethtx/
    │   │   └── ethtx.go                # MarshalBinary / UnmarshalBinary helpers
    │   └── safe_client.go              # SafeClient: implements all Safe ports

    ├── storage/file/transaction/
    │   └── multisig.go                 # MultisigFileRepositorier implementation

    └── interface-adapters/wallet/eth/
        ├── keygen.go                   # ETHKeygen: wires all Keygen use cases incl. SignTx + SignMultisigTx
        ├── sign.go                     # ETHSign: wires SignMultisigTransaction
        └── watch.go                    # ETHWatch: wires all Watch use cases incl. multisig

Document Version: 1.1 Last Updated: 2026-03-08 Maintainer: go-crypto-wallet team