From ee0dbb30c0796c7f7f131179404c31c6504e8c89 Mon Sep 17 00:00:00 2001 From: Abhishek Uniyal <56363630+uniyalabhishek@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:41:38 +0530 Subject: [PATCH] feat: format known addresses --- cmd/account/show/allowances.go | 8 +- cmd/account/show/delegations.go | 12 ++- cmd/account/show/show.go | 32 +++++- cmd/common/helpers.go | 184 +++++++++++++++++++++++++++++++- cmd/common/helpers_test.go | 123 +++++++++++++++++++++ cmd/common/json.go | 6 +- cmd/common/wallet.go | 14 ++- cmd/contract.go | 8 +- cmd/network/governance/list.go | 4 +- cmd/network/governance/show.go | 19 ++-- cmd/network/show.go | 6 +- cmd/paratime/show.go | 7 +- cmd/rofl/deploy.go | 5 +- cmd/rofl/list.go | 12 ++- cmd/rofl/machine/mgmt.go | 26 +++-- cmd/rofl/machine/show.go | 8 +- cmd/rofl/mgmt.go | 4 +- cmd/rofl/provider/list.go | 15 ++- cmd/rofl/provider/show.go | 12 ++- cmd/wallet/list.go | 7 +- cmd/wallet/remote_signer.go | 2 +- config/addressbook.go | 5 +- config/wallet.go | 60 +++++++++++ 23 files changed, 522 insertions(+), 57 deletions(-) create mode 100644 cmd/common/helpers_test.go diff --git a/cmd/account/show/allowances.go b/cmd/account/show/allowances.go index 5369a0b2..b8c58472 100644 --- a/cmd/account/show/allowances.go +++ b/cmd/account/show/allowances.go @@ -7,6 +7,8 @@ import ( staking "github.com/oasisprotocol/oasis-core/go/staking/api" + "github.com/oasisprotocol/cli/cmd/common" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" @@ -60,8 +62,12 @@ func prettyPrintAllowanceDescriptions( // element so we can align all values. lenLongest := lenLongestString(beneficiaryFieldName, amountFieldName) + // Precompute address formatting context for efficiency (network-aware for paratime names). + addrCtx := common.GenAddressFormatContextForNetwork(network) + for _, desc := range allowDescriptions { - fmt.Fprintf(w, "%s - %-*s %s", prefix, lenLongest, beneficiaryFieldName, desc.beneficiary) + prettyAddr := common.PrettyAddressWith(addrCtx, desc.beneficiary.String()) + fmt.Fprintf(w, "%s - %-*s %s", prefix, lenLongest, beneficiaryFieldName, prettyAddr) if desc.self { fmt.Fprintf(w, " (self)") } diff --git a/cmd/account/show/delegations.go b/cmd/account/show/delegations.go index 14d84ec2..dc8649e6 100644 --- a/cmd/account/show/delegations.go +++ b/cmd/account/show/delegations.go @@ -89,6 +89,12 @@ func prettyPrintDelegationDescriptions( fmt.Fprintf(w, "%sDelegations:\n", prefix) + // Guard against empty slice to prevent panic when accessing delDescriptions[0]. + if len(delDescriptions) == 0 { + fmt.Fprintf(w, "%s \n", prefix) + return + } + sort.Sort(byEndTimeAmountAddress(delDescriptions)) // Get the length of name of the longest field to display for each @@ -104,8 +110,12 @@ func prettyPrintDelegationDescriptions( lenLongest = lenLongestString(addressFieldName, amountFieldName, endTimeFieldName) } + // Precompute address formatting context for efficiency (network-aware for paratime names). + addrCtx := common.GenAddressFormatContextForNetwork(network) + for _, desc := range delDescriptions { - fmt.Fprintf(w, "%s - %-*s %s", prefix, lenLongest, addressFieldName, desc.address) + prettyAddr := common.PrettyAddressWith(addrCtx, desc.address.String()) + fmt.Fprintf(w, "%s - %-*s %s", prefix, lenLongest, addressFieldName, prettyAddr) if desc.self { fmt.Fprintf(w, " (self)") } diff --git a/cmd/account/show/show.go b/cmd/account/show/show.go index 79caa4cf..5908900a 100644 --- a/cmd/account/show/show.go +++ b/cmd/account/show/show.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" flag "github.com/spf13/pflag" + "golang.org/x/term" consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" roothash "github.com/oasisprotocol/oasis-core/go/roothash/api" @@ -38,13 +39,19 @@ var ( // Determine which address to show. If an explicit argument was given, use that // otherwise use the default account. var targetAddress string + var walletNameForEth string switch { case len(args) >= 1: // Explicit argument given. targetAddress = args[0] + if _, ok := cfg.Wallet.All[targetAddress]; ok { + // Wallet account selected by name. + walletNameForEth = targetAddress + } case npa.Account != nil: // Default account is selected. targetAddress = npa.Account.Address + walletNameForEth = npa.AccountName default: // No address given and no wallet configured. cobra.CheckErr("no address given and no wallet configured") @@ -58,10 +65,33 @@ var ( nativeAddr, ethAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, targetAddress) cobra.CheckErr(err) - if name := common.FindAccountName(nativeAddr.String()); name != "" { + name := common.FindAccountNameForNetwork(npa.Network, nativeAddr.String()) + if name != "" { fmt.Printf("Name: %s\n", name) } + // If eth address is not available, try to get it from wallet config (no unlock required). + if ethAddr == nil { + for _, walletCfg := range cfg.Wallet.All { + if walletCfg.Address == nativeAddr.String() { + ethAddr = walletCfg.GetEthAddress() + break + } + } + } + + // If eth address is still not available and the user selected a wallet account, + // load it (may require passphrase) to derive and persist eth_address metadata. + if ethAddr == nil && walletNameForEth != "" { + if walletCfg, ok := cfg.Wallet.All[walletNameForEth]; ok && walletCfg.SupportsEthAddress() { + // Avoid prompting in non-interactive contexts (e.g. piping output). + if term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) { + acc := common.LoadAccount(cfg, walletNameForEth) + ethAddr = acc.EthAddress() + } + } + } + height, err := common.GetActualHeight( ctx, c.Consensus().Core(), diff --git a/cmd/common/helpers.go b/cmd/common/helpers.go index 88b09439..23d949c0 100644 --- a/cmd/common/helpers.go +++ b/cmd/common/helpers.go @@ -4,11 +4,16 @@ import ( "fmt" "os" + ethCommon "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" + staking "github.com/oasisprotocol/oasis-core/go/staking/api" + configSdk "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/testing" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" + buildRoflProvider "github.com/oasisprotocol/cli/build/rofl/provider" "github.com/oasisprotocol/cli/config" ) @@ -42,18 +47,51 @@ func CheckForceErr(err interface{}) { } // GenAccountNames generates a map of all addresses -> account name for pretty printing. +// Priority order (later entries overwrite earlier): test accounts < addressbook < wallet. +// This ensures wallet identity takes precedence over addressbook labels for the same address. func GenAccountNames() types.AccountNames { + return GenAccountNamesForNetwork(nil) +} + +// GenAccountNamesForNetwork generates a map of all addresses -> account name for pretty printing, +// including network-specific entries (paratimes, ROFL providers) when a network is provided. +// Priority order (later entries overwrite earlier): test accounts < network entries < addressbook < wallet. +func GenAccountNamesForNetwork(net *configSdk.Network) types.AccountNames { an := types.AccountNames{} - for name, acc := range config.Global().Wallet.All { - an[acc.GetAddress().String()] = name + + // Test accounts have lowest priority. + for name, acc := range testing.TestAccounts { + an[acc.Address.String()] = fmt.Sprintf("test:%s", name) } + // Network-derived entries (paratimes, ROFL providers) have second-lowest priority. + if net != nil { + // Include ParaTime runtime addresses as paratime:. + for ptName, pt := range net.ParaTimes.All { + rtAddr := types.NewAddressFromConsensus(staking.NewRuntimeAddress(pt.Namespace())) + an[rtAddr.String()] = fmt.Sprintf("paratime:%s", ptName) + } + + // Include ROFL default provider addresses as rofl:provider:. + for ptName, pt := range net.ParaTimes.All { + if svc, ok := buildRoflProvider.DefaultRoflServices[pt.ID]; ok { + if svc.Provider != "" { + if a, _, err := helpers.ResolveEthOrOasisAddress(svc.Provider); err == nil && a != nil { + an[a.String()] = fmt.Sprintf("rofl:provider:%s", ptName) + } + } + } + } + } + + // Addressbook entries have medium priority. for name, acc := range config.Global().AddressBook.All { an[acc.GetAddress().String()] = name } - for name, acc := range testing.TestAccounts { - an[acc.Address.String()] = fmt.Sprintf("test:%s", name) + // Wallet entries have highest priority. + for name, acc := range config.Global().Wallet.All { + an[acc.GetAddress().String()] = name } return an @@ -64,3 +102,141 @@ func FindAccountName(address string) string { an := GenAccountNames() return an[address] } + +// FindAccountNameForNetwork finds account's name in the context of a specific network. +func FindAccountNameForNetwork(net *configSdk.Network, address string) string { + an := GenAccountNamesForNetwork(net) + return an[address] +} + +// AddressFormatContext contains precomputed maps for address formatting. +type AddressFormatContext struct { + // Names maps native address string to account name. + Names types.AccountNames + // Eth maps native address string to Ethereum hex address string (if known). + Eth map[string]string +} + +// GenAccountEthMap generates a map of native address string -> eth hex address (if known). +func GenAccountEthMap() map[string]string { + return GenAccountEthMapForNetwork(nil) +} + +// GenAccountEthMapForNetwork generates a map of native address string -> eth hex address (if known). +func GenAccountEthMapForNetwork(_ *configSdk.Network) map[string]string { + eth := make(map[string]string) + + // From test accounts. + for _, acc := range testing.TestAccounts { + if acc.EthAddress != nil { + eth[acc.Address.String()] = acc.EthAddress.Hex() + } + } + + // From address book entries (higher priority than test accounts). + for _, acc := range config.Global().AddressBook.All { + if ethAddr := acc.GetEthAddress(); ethAddr != nil { + eth[acc.GetAddress().String()] = ethAddr.Hex() + } + } + + // From wallet entries (highest priority), when an Ethereum address is available in config. + for _, acc := range config.Global().Wallet.All { + if ethAddr := acc.GetEthAddress(); ethAddr != nil { + eth[acc.GetAddress().String()] = ethAddr.Hex() + } + } + + return eth +} + +// GenAddressFormatContext builds both name and eth address maps for formatting. +func GenAddressFormatContext() AddressFormatContext { + return GenAddressFormatContextForNetwork(nil) +} + +// GenAddressFormatContextForNetwork builds both name and eth address maps for formatting, +// including network-specific entries when a network is provided. +func GenAddressFormatContextForNetwork(net *configSdk.Network) AddressFormatContext { + return AddressFormatContext{ + Names: GenAccountNamesForNetwork(net), + Eth: GenAccountEthMapForNetwork(net), + } +} + +// PrettyAddressWith formats an address using a precomputed context. +// If the address is known (in wallet, addressbook, or test accounts), it returns "name (address)". +// For secp256k1 accounts with a known Ethereum address, the Ethereum hex format is preferred in parentheses. +// If the address is unknown, it returns the original address string unchanged. +func PrettyAddressWith(ctx AddressFormatContext, addr string) string { + // Try to parse the address to get canonical native form. + nativeAddr, ethFromInput, err := helpers.ResolveEthOrOasisAddress(addr) + if err != nil || nativeAddr == nil { + // Cannot parse, return unchanged. + return addr + } + + nativeStr := nativeAddr.String() + + // Look up the name. + name := ctx.Names[nativeStr] + if name == "" { + // Unknown address, return the original input. + return addr + } + + // Determine which address to show in parentheses. + // Prefer Ethereum address if available (from input or from known eth addresses). + var parenAddr string + if ethFromInput != nil { + parenAddr = ethFromInput.Hex() + } else if ethHex := ctx.Eth[nativeStr]; ethHex != "" { + parenAddr = ethHex + } else { + parenAddr = nativeStr + } + + // Guard against redundant "name (name)" output. + if name == parenAddr { + return parenAddr + } + + return fmt.Sprintf("%s (%s)", name, parenAddr) +} + +func preferredAddressString(nativeAddr *types.Address, ethAddr *ethCommon.Address) string { + if ethAddr != nil { + return ethAddr.Hex() + } + if nativeAddr == nil { + return "" + } + return nativeAddr.String() +} + +// PrettyResolvedAddressWith formats a resolved address tuple (native, eth) using a precomputed context. +// +// If ethAddr is non-nil, it is preferred to preserve the "user provided eth" behavior even when +// the native-to-eth mapping is not available in the context. +func PrettyResolvedAddressWith(ctx AddressFormatContext, nativeAddr *types.Address, ethAddr *ethCommon.Address) string { + addrStr := preferredAddressString(nativeAddr, ethAddr) + if addrStr == "" { + return "" + } + return PrettyAddressWith(ctx, addrStr) +} + +// PrettyAddress formats an address for display without network context. +// If the address is known (in wallet, addressbook, or test accounts), it returns "name (address)". +// For secp256k1 accounts with a known Ethereum address, the Ethereum hex format is preferred in parentheses. +// If the address is unknown, it returns the original address string unchanged. +func PrettyAddress(addr string) string { + return PrettyAddressWith(GenAddressFormatContext(), addr) +} + +// PrettyAddressForNetwork formats an address for display with network context. +// This includes network-derived names like paratime addresses and ROFL providers. +func PrettyAddressForNetwork(net *configSdk.Network, addr types.Address) string { + ctx := GenAddressFormatContextForNetwork(net) + return PrettyAddressWith(ctx, addr.String()) +} diff --git a/cmd/common/helpers_test.go b/cmd/common/helpers_test.go new file mode 100644 index 00000000..b88b30f3 --- /dev/null +++ b/cmd/common/helpers_test.go @@ -0,0 +1,123 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" +) + +func TestPrettyAddressWith(t *testing.T) { + require := require.New(t) + + nativeAddr, ethAddr, err := helpers.ResolveEthOrOasisAddress("0x60a6321eA71d37102Dbf923AAe2E08d005C4e403") + require.NoError(err) + require.NotNil(nativeAddr) + require.NotNil(ethAddr) + + t.Run("eth preferred when known", func(t *testing.T) { + ctx := AddressFormatContext{ + Names: types.AccountNames{ + nativeAddr.String(): "my", + }, + Eth: map[string]string{ + nativeAddr.String(): ethAddr.Hex(), + }, + } + + require.Equal("my ("+ethAddr.Hex()+")", PrettyAddressWith(ctx, nativeAddr.String())) + require.Equal("my ("+ethAddr.Hex()+")", PrettyAddressWith(ctx, ethAddr.Hex())) + }) + + t.Run("native fallback when eth unknown", func(t *testing.T) { + ctx := AddressFormatContext{ + Names: types.AccountNames{ + nativeAddr.String(): "my", + }, + Eth: map[string]string{}, + } + + require.Equal("my ("+nativeAddr.String()+")", PrettyAddressWith(ctx, nativeAddr.String())) + // If the user explicitly provided an Ethereum address, prefer it even if not in ctx.Eth. + require.Equal("my ("+ethAddr.Hex()+")", PrettyAddressWith(ctx, ethAddr.Hex())) + }) + + t.Run("unknown returns unchanged", func(t *testing.T) { + ctx := AddressFormatContext{ + Names: types.AccountNames{}, + Eth: map[string]string{}, + } + + require.Equal(nativeAddr.String(), PrettyAddressWith(ctx, nativeAddr.String())) + require.Equal(ethAddr.Hex(), PrettyAddressWith(ctx, ethAddr.Hex())) + }) + + t.Run("unparseable returns unchanged", func(t *testing.T) { + ctx := AddressFormatContext{ + Names: types.AccountNames{ + nativeAddr.String(): "my", + }, + Eth: map[string]string{ + nativeAddr.String(): ethAddr.Hex(), + }, + } + + require.Equal("not-an-address", PrettyAddressWith(ctx, "not-an-address")) + }) +} + +func TestPrettyResolvedAddressWith(t *testing.T) { + require := require.New(t) + + nativeAddr, ethAddr, err := helpers.ResolveEthOrOasisAddress("0x60a6321eA71d37102Dbf923AAe2E08d005C4e403") + require.NoError(err) + require.NotNil(nativeAddr) + require.NotNil(ethAddr) + + t.Run("eth preferred when provided", func(t *testing.T) { + ctx := AddressFormatContext{ + Names: types.AccountNames{ + nativeAddr.String(): "my", + }, + Eth: map[string]string{}, + } + + require.Equal("my ("+ethAddr.Hex()+")", PrettyResolvedAddressWith(ctx, nativeAddr, ethAddr)) + }) + + t.Run("eth preferred when mapped", func(t *testing.T) { + ctx := AddressFormatContext{ + Names: types.AccountNames{ + nativeAddr.String(): "my", + }, + Eth: map[string]string{ + nativeAddr.String(): ethAddr.Hex(), + }, + } + + require.Equal("my ("+ethAddr.Hex()+")", PrettyResolvedAddressWith(ctx, nativeAddr, nil)) + }) + + t.Run("native fallback when eth unknown", func(t *testing.T) { + ctx := AddressFormatContext{ + Names: types.AccountNames{ + nativeAddr.String(): "my", + }, + Eth: map[string]string{}, + } + + require.Equal("my ("+nativeAddr.String()+")", PrettyResolvedAddressWith(ctx, nativeAddr, nil)) + }) + + t.Run("unknown returns preferred input", func(t *testing.T) { + ctx := AddressFormatContext{ + Names: types.AccountNames{}, + Eth: map[string]string{}, + } + + require.Equal(nativeAddr.String(), PrettyResolvedAddressWith(ctx, nativeAddr, nil)) + require.Equal(ethAddr.Hex(), PrettyResolvedAddressWith(ctx, nativeAddr, ethAddr)) + }) +} diff --git a/cmd/common/json.go b/cmd/common/json.go index e3436aa7..cf1d98a5 100644 --- a/cmd/common/json.go +++ b/cmd/common/json.go @@ -172,7 +172,11 @@ func PrettyPrint(npa *NPASelection, prefix string, blob interface{}) string { ctx = context.WithValue(ctx, config.ContextKeyParaTimeCfg, npa.ParaTime) } ctx = context.WithValue(ctx, signature.ContextKeySigContext, &sigCtx) - ctx = context.WithValue(ctx, types.ContextKeyAccountNames, GenAccountNames()) + + // Provide network-aware names and native->ETH mapping for address formatting. + addrCtx := GenAddressFormatContextForNetwork(npa.Network) + ctx = context.WithValue(ctx, types.ContextKeyAccountNames, addrCtx.Names) + ctx = context.WithValue(ctx, types.ContextKeyAccountEthMap, addrCtx.Eth) // Set up chain context for signature verification during pretty-printing. coreSignature.UnsafeResetChainContext() diff --git a/cmd/common/wallet.go b/cmd/common/wallet.go index 54f3b9db..448e11d7 100644 --- a/cmd/common/wallet.go +++ b/cmd/common/wallet.go @@ -130,6 +130,17 @@ func LoadAccount(cfg *config.Config, name string) wallet.Account { acc, err := cfg.Wallet.Load(name, passphrase) cobra.CheckErr(err) + // Persist Ethereum address metadata (if available) so other commands can prefer it without + // needing to unlock/load the account again. This is best-effort and should never fail the command. + if accCfg, ok := cfg.Wallet.All[name]; ok && accCfg.EthAddress == "" { + if ethAddr := acc.EthAddress(); ethAddr != nil { + accCfg.EthAddress = ethAddr.Hex() + if err := cfg.Save(); err != nil { + Warnf("Warning: failed to persist eth_address for wallet account '%s': %v", name, err) + } + } + } + return acc } @@ -200,8 +211,7 @@ func ResolveLocalAccountOrAddress(net *configSdk.Network, address string) (*type // Check if address is the account name in the wallet. if acc, ok := config.Global().Wallet.All[address]; ok { addr := acc.GetAddress() - // TODO: Implement acc.GetEthAddress() - return &addr, nil, nil + return &addr, acc.GetEthAddress(), nil } // Check if address is the name of an address book entry. diff --git a/cmd/contract.go b/cmd/contract.go index 7961e90f..dea33844 100644 --- a/cmd/contract.go +++ b/cmd/contract.go @@ -59,9 +59,11 @@ var ( inst, err := conn.Runtime(npa.ParaTime).Contracts.Instance(ctx, client.RoundLatest, contracts.InstanceID(instanceID)) cobra.CheckErr(err) + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) + fmt.Printf("ID: %d\n", inst.ID) fmt.Printf("Code ID: %d\n", inst.CodeID) - fmt.Printf("Creator: %s\n", inst.Creator) + fmt.Printf("Creator: %s\n", common.PrettyAddressWith(addrCtx, inst.Creator.String())) fmt.Printf("Upgrades policy: %s\n", formatPolicy(&inst.UpgradesPolicy)) }, } @@ -87,10 +89,12 @@ var ( code, err := conn.Runtime(npa.ParaTime).Contracts.Code(ctx, client.RoundLatest, contracts.CodeID(codeID)) cobra.CheckErr(err) + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) + fmt.Printf("ID: %d\n", code.ID) fmt.Printf("Hash: %s\n", code.Hash) fmt.Printf("ABI: %s (sv: %d)\n", code.ABI, code.ABISubVersion) - fmt.Printf("Uploader: %s\n", code.Uploader) + fmt.Printf("Uploader: %s\n", common.PrettyAddressWith(addrCtx, code.Uploader.String())) fmt.Printf("Instantiate policy: %s\n", formatPolicy(&code.InstantiatePolicy)) }, } diff --git a/cmd/network/governance/list.go b/cmd/network/governance/list.go index 0a114fc6..3e282748 100644 --- a/cmd/network/governance/list.go +++ b/cmd/network/governance/list.go @@ -27,6 +27,8 @@ var govListCmd = &cobra.Command{ conn, err := connection.Connect(ctx, npa.Network) cobra.CheckErr(err) + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) + table := table.New() table.Header("ID", "Kind", "Submitter", "Created At", "Closes At", "State") @@ -52,7 +54,7 @@ var govListCmd = &cobra.Command{ output = append(output, []string{ fmt.Sprintf("%d", proposal.ID), kind, - proposal.Submitter.String(), + common.PrettyAddressWith(addrCtx, proposal.Submitter.String()), fmt.Sprintf("%d", proposal.CreatedAt), fmt.Sprintf("%d", proposal.ClosesAt), proposal.State.String(), diff --git a/cmd/network/governance/show.go b/cmd/network/governance/show.go index 894848a6..96cdb0b9 100644 --- a/cmd/network/governance/show.go +++ b/cmd/network/governance/show.go @@ -54,6 +54,7 @@ var ( Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) // Determine the proposal ID to query. proposalID, err := strconv.ParseUint(args[0], 10, 64) @@ -276,7 +277,7 @@ var ( fmt.Printf("Network: %s\n", npa.PrettyPrintNetwork()) fmt.Printf("Proposal ID: %d\n", proposalID) fmt.Printf("Status: %s\n", proposal.State) - fmt.Printf("Submitted By: %s\n", proposal.Submitter) + fmt.Printf("Submitted By: %s\n", common.PrettyAddressWith(addrCtx, proposal.Submitter.String())) fmt.Printf("Created At: epoch %d\n", proposal.CreatedAt) switch proposal.State { @@ -391,24 +392,26 @@ var ( fmt.Println("=== VALIDATORS VOTED ===") votersList := entitiesByDescendingStake(validatorVoters) for i, val := range votersList { + valAddr := common.PrettyAddressWith(addrCtx, val.Address.String()) name := getName(val.Address) stakePercentage := new(big.Float).SetInt(val.Stake.Clone().ToBigInt()) stakePercentage = stakePercentage.Mul(stakePercentage, new(big.Float).SetInt64(100)) stakePercentage = stakePercentage.Quo(stakePercentage, new(big.Float).SetInt(totalVotingStake.ToBigInt())) if hasCorrectVotingPower { - fmt.Printf(" %d. %s,%s,%s (%.2f%%): %s\n", i+1, val.Address, name, val.Stake, stakePercentage, validatorVotes[val.Address]) + fmt.Printf(" %d. %s,%s,%s (%.2f%%): %s\n", i+1, valAddr, name, val.Stake, stakePercentage, validatorVotes[val.Address]) } else { - fmt.Printf(" %d. %s,%s: %s\n", i+1, val.Address, name, validatorVotes[val.Address]) + fmt.Printf(" %d. %s,%s: %s\n", i+1, valAddr, name, validatorVotes[val.Address]) } // Display delegators that voted differently. for voter, override := range validatorVoteOverrides[val.Address] { + voterAddr := common.PrettyAddressWith(addrCtx, voter.String()) voterName := getName(voter) if hasCorrectVotingPower { - fmt.Printf(" - %s,%s,%s (%.2f%%) -> %s\n", voter, voterName, override.shares, override.sharePercent, override.vote) + fmt.Printf(" - %s,%s,%s (%.2f%%) -> %s\n", voterAddr, voterName, override.shares, override.sharePercent, override.vote) } else { - fmt.Printf(" - %s,%s -> %s\n", voter, voterName, override.vote) + fmt.Printf(" - %s,%s -> %s\n", voterAddr, voterName, override.vote) } } } @@ -418,16 +421,18 @@ var ( fmt.Println("=== VALIDATORS NOT VOTED ===") nonVotersList := entitiesByDescendingStake(validatorNonVoters) for i, val := range nonVotersList { + valAddr := common.PrettyAddressWith(addrCtx, val.Address.String()) name := getName(val.Address) stakePercentage := new(big.Float).SetInt(val.Stake.Clone().ToBigInt()) stakePercentage = stakePercentage.Mul(stakePercentage, new(big.Float).SetInt64(100)) stakePercentage = stakePercentage.Quo(stakePercentage, new(big.Float).SetInt(totalVotingStake.ToBigInt())) - fmt.Printf(" %d. %s,%s,%s (%.2f%%)", i+1, val.Address, name, val.Stake, stakePercentage) + fmt.Printf(" %d. %s,%s,%s (%.2f%%)", i+1, valAddr, name, val.Stake, stakePercentage) fmt.Println() // Display delegators that voted differently. for voter, override := range validatorVoteOverrides[val.Address] { + voterAddr := common.PrettyAddressWith(addrCtx, voter.String()) voterName := getName(voter) - fmt.Printf(" - %s,%s,%s (%.2f%%) -> %s", voter, voterName, override.shares, override.sharePercent, override.vote) + fmt.Printf(" - %s,%s,%s (%.2f%%) -> %s", voterAddr, voterName, override.shares, override.sharePercent, override.vote) fmt.Println() } } diff --git a/cmd/network/show.go b/cmd/network/show.go index 09d2288f..f1601684 100644 --- a/cmd/network/show.go +++ b/cmd/network/show.go @@ -49,10 +49,12 @@ func prettyPrintEntityNodes(ctx context.Context, npa *common.NPASelection, staki return err } + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) + fmt.Printf("=== ENTITY ===\n") entityAddr := staking.NewAddress(entity.ID) - fmt.Printf("Entity Address: %s\n", entityAddr.String()) + fmt.Printf("Entity Address: %s\n", common.PrettyAddressWith(addrCtx, entityAddr.String())) fmt.Printf("Entity ID: %s\n", entity.ID.String()) @@ -86,7 +88,7 @@ func prettyPrintEntityNodes(ctx context.Context, npa *common.NPASelection, staki fmt.Printf("=== NODES ===\n") for i, node := range entity.Nodes { nodeAddr := staking.NewAddress(node) - fmt.Printf("Node Address: %s\n", nodeAddr.String()) + fmt.Printf("Node Address: %s\n", common.PrettyAddressWith(addrCtx, nodeAddr.String())) fmt.Printf("Node ID: %s\n", node.String()) idQuery2 := ®istry.IDQuery{ Height: height, diff --git a/cmd/paratime/show.go b/cmd/paratime/show.go index b2d7f6a8..864489b9 100644 --- a/cmd/paratime/show.go +++ b/cmd/paratime/show.go @@ -201,7 +201,12 @@ var ( fmt.Printf("Chain ID: %s\n", ethTx.ChainId()) fmt.Printf("Nonce: %d\n", ethTx.Nonce()) fmt.Printf("Type: %d\n", ethTx.Type()) - fmt.Printf("To: %s\n", ethTx.To()) + toStr := "" + if to := ethTx.To(); to != nil { + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) + toStr = common.PrettyAddressWith(addrCtx, to.Hex()) + } + fmt.Printf("To: %s\n", toStr) fmt.Printf("Value: %s\n", ethTx.Value()) fmt.Printf("Gas limit: %d\n", ethTx.Gas()) fmt.Printf("Gas price: %s\n", ethTx.GasPrice()) diff --git a/cmd/rofl/deploy.go b/cmd/rofl/deploy.go index 0ce37124..25a1a452 100644 --- a/cmd/rofl/deploy.go +++ b/cmd/rofl/deploy.go @@ -122,12 +122,13 @@ var ( } // Resolve provider address. - providerAddr, _, err := common.ResolveLocalAccountOrAddress(npa.Network, machine.Provider) + providerAddr, providerEthAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, machine.Provider) if err != nil { cobra.CheckErr(fmt.Sprintf("Invalid provider address: %s", err)) } - fmt.Printf("Using provider: %s (%s)\n", machine.Provider, providerAddr) + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) + fmt.Printf("Using provider: %s\n", common.PrettyResolvedAddressWith(addrCtx, providerAddr, providerEthAddr)) if roflCommon.ShowOffers { // Display all offers supported by the provider. diff --git a/cmd/rofl/list.go b/cmd/rofl/list.go index a8348d3b..868ba0f5 100644 --- a/cmd/rofl/list.go +++ b/cmd/rofl/list.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/client" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/rofl" @@ -58,7 +59,7 @@ Use --format json for machine-readable output.`, return } - outputAppsText(apps) + outputAppsText(npa.Network, apps) }, } @@ -75,10 +76,13 @@ func outputAppsJSON(apps []*rofl.AppConfig) { } // outputAppsText returns apps in human-readable table format. -func outputAppsText(apps []*rofl.AppConfig) { +func outputAppsText(network *config.Network, apps []*rofl.AppConfig) { tbl := table.New() tbl.Header("App ID", "Name", "Version", "Admin") + // Precompute address formatting context for efficiency (network-aware for paratime/ROFL names). + addrCtx := common.GenAddressFormatContextForNetwork(network) + rows := make([][]string, 0, len(apps)) for _, app := range apps { // Extract name from metadata. @@ -87,10 +91,10 @@ func outputAppsText(apps []*rofl.AppConfig) { // Extract version from metadata. version := app.Metadata["net.oasis.rofl.version"] - // Format admin address. + // Format admin address with pretty formatting. var admin string if app.Admin != nil { - admin = app.Admin.String() + admin = common.PrettyAddressWith(addrCtx, app.Admin.String()) } rows = append(rows, []string{ diff --git a/cmd/rofl/machine/mgmt.go b/cmd/rofl/machine/mgmt.go index dcf0c851..d5d83b7a 100644 --- a/cmd/rofl/machine/mgmt.go +++ b/cmd/rofl/machine/mgmt.go @@ -74,10 +74,11 @@ var ( machine, machineName, machineID := resolveMachine(args, deployment) // Resolve provider address. - providerAddr, _, err := common.ResolveLocalAccountOrAddress(npa.Network, machine.Provider) + providerAddr, providerEthAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, machine.Provider) if err != nil { cobra.CheckErr(fmt.Sprintf("Invalid provider address: %s", err)) } + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) // When not in offline mode, connect to the given network endpoint. ctx := context.Background() @@ -87,7 +88,7 @@ var ( cobra.CheckErr(err) } - fmt.Printf("Using provider: %s (%s)\n", machine.Provider, providerAddr) + fmt.Printf("Using provider: %s\n", common.PrettyResolvedAddressWith(addrCtx, providerAddr, providerEthAddr)) fmt.Printf("Canceling machine: %s [%s]\n", machineName, machine.ID) common.Warn("WARNING: Canceling a machine will permanently destroy it including any persistent storage!") roflCommon.PrintRentRefundWarning() @@ -144,13 +145,14 @@ var ( machine, machineName, machineID := resolveMachine(args, deployment) // Resolve provider address. - providerAddr, _, err := common.ResolveLocalAccountOrAddress(npa.Network, machine.Provider) + providerAddr, providerEthAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, machine.Provider) if err != nil { cobra.CheckErr(fmt.Sprintf("Invalid provider address: %s", err)) } + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) // Resolve new admin address. - newAdminAddr, _, err := common.ResolveLocalAccountOrAddress(npa.Network, newAdminAddress) + newAdminAddr, newAdminEthAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, newAdminAddress) if err != nil { cobra.CheckErr(fmt.Sprintf("Invalid admin address: %s", err)) } @@ -163,7 +165,7 @@ var ( cobra.CheckErr(err) } - fmt.Printf("Provider: %s (%s)\n", machine.Provider, providerAddr) + fmt.Printf("Provider: %s\n", common.PrettyResolvedAddressWith(addrCtx, providerAddr, providerEthAddr)) fmt.Printf("Machine: %s [%s]\n", machineName, machine.ID) // Resolve old admin in online mode. @@ -171,10 +173,10 @@ var ( insDsc, err := conn.Runtime(npa.ParaTime).ROFLMarket.Instance(ctx, client.RoundLatest, *providerAddr, machineID) cobra.CheckErr(err) - fmt.Printf("Old admin: %s\n", insDsc.Admin) + fmt.Printf("Old admin: %s\n", common.PrettyAddressWith(addrCtx, insDsc.Admin.String())) } - fmt.Printf("New admin: %s\n", newAdminAddr) + fmt.Printf("New admin: %s\n", common.PrettyResolvedAddressWith(addrCtx, newAdminAddr, newAdminEthAddr)) // Prepare transaction. tx := roflmarket.NewInstanceChangeAdmin(nil, &roflmarket.InstanceChangeAdmin{ @@ -216,10 +218,11 @@ var ( machine, machineName, machineID := resolveMachine(args, deployment) // Resolve provider address. - providerAddr, _, err := common.ResolveLocalAccountOrAddress(npa.Network, machine.Provider) + providerAddr, providerEthAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, machine.Provider) if err != nil { cobra.CheckErr(fmt.Sprintf("invalid provider address: %s", err)) } + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) // Parse machine payment term. if roflCommon.Term == "" { @@ -254,7 +257,7 @@ var ( } } - fmt.Printf("Using provider: %s (%s)\n", machine.Provider, providerAddr) + fmt.Printf("Using provider: %s\n", common.PrettyResolvedAddressWith(addrCtx, providerAddr, providerEthAddr)) fmt.Printf("Top-up machine: %s [%s]\n", machineName, machine.ID) if txCfg.Offline { fmt.Printf("Top-up term: %d x %s\n", roflCommon.TermCount, roflCommon.Term) @@ -343,10 +346,11 @@ func queueCommand(cliArgs []string, method string, args any, msgAfter string) { machine, machineName, machineID := resolveMachine(cliArgs, deployment) // Resolve provider address. - providerAddr, _, err := common.ResolveLocalAccountOrAddress(npa.Network, machine.Provider) + providerAddr, providerEthAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, machine.Provider) if err != nil { cobra.CheckErr(fmt.Sprintf("Invalid provider address: %s", err)) } + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) // When not in offline mode, connect to the given network endpoint. ctx := context.Background() @@ -356,7 +360,7 @@ func queueCommand(cliArgs []string, method string, args any, msgAfter string) { cobra.CheckErr(err) } - fmt.Printf("Using provider: %s (%s)\n", machine.Provider, providerAddr) + fmt.Printf("Using provider: %s\n", common.PrettyResolvedAddressWith(addrCtx, providerAddr, providerEthAddr)) fmt.Printf("Machine: %s [%s]\n", machineName, machine.ID) fmt.Printf("Command: %s\n", method) fmt.Printf("Args:\n") diff --git a/cmd/rofl/machine/show.go b/cmd/rofl/machine/show.go index 635fdb94..b1bc9574 100644 --- a/cmd/rofl/machine/show.go +++ b/cmd/rofl/machine/show.go @@ -59,6 +59,8 @@ var showCmd = &cobra.Command{ cobra.CheckErr(fmt.Sprintf("Invalid provider address: %s", err)) } + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) + insDsc, err := conn.Runtime(npa.ParaTime).ROFLMarket.Instance(ctx, client.RoundLatest, *providerAddr, machineID) if err != nil { // The "instance not found" error originates from Rust code, so we can't compare it nicely here. @@ -91,7 +93,7 @@ var showCmd = &cobra.Command{ expired := !time.Now().Before(paidUntil) fmt.Printf("Name: %s\n", machineName) - fmt.Printf("Provider: %s\n", insDsc.Provider) + fmt.Printf("Provider: %s\n", common.PrettyAddressWith(addrCtx, insDsc.Provider.String())) fmt.Printf("ID: %s\n", insDsc.ID) fmt.Printf("Offer: %s\n", insDsc.Offer) fmt.Printf("Status: %s", insDsc.Status) @@ -99,8 +101,8 @@ var showCmd = &cobra.Command{ fmt.Printf(" (EXPIRED)") } fmt.Println() - fmt.Printf("Creator: %s\n", insDsc.Creator) - fmt.Printf("Admin: %s\n", insDsc.Admin) + fmt.Printf("Creator: %s\n", common.PrettyAddressWith(addrCtx, insDsc.Creator.String())) + fmt.Printf("Admin: %s\n", common.PrettyAddressWith(addrCtx, insDsc.Admin.String())) switch insDsc.NodeID { case nil: fmt.Printf("Node ID: \n") diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index c6e5fe83..28359ed3 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -506,13 +506,15 @@ var ( return } + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) + fmt.Printf("App ID: %s\n", appCfg.ID) fmt.Printf("Admin: ") switch appCfg.Admin { case nil: fmt.Printf("none\n") default: - fmt.Printf("%s\n", *appCfg.Admin) + fmt.Printf("%s\n", common.PrettyAddressWith(addrCtx, appCfg.Admin.String())) } stakedAmnt := helpers.FormatParaTimeDenomination(npa.ParaTime, appCfg.Stake) fmt.Printf("Staked amount: %s\n", stakedAmnt) diff --git a/cmd/rofl/provider/list.go b/cmd/rofl/provider/list.go index a565439f..6fe42152 100644 --- a/cmd/rofl/provider/list.go +++ b/cmd/rofl/provider/list.go @@ -101,6 +101,9 @@ func outputText(ctx context.Context, npa *common.NPASelection, conn connection.C table := table.New() table.Header("Provider Address", "Scheduler App", "Nodes", "Offers", "Instances") + // Precompute address formatting context for efficiency (network-aware for paratime/ROFL names). + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) + rows := make([][]string, 0, len(providers)) for _, provider := range providers { // Format node count. @@ -112,7 +115,7 @@ func outputText(ctx context.Context, npa *common.NPASelection, conn connection.C } rows = append(rows, []string{ - provider.Address.String(), + common.PrettyAddressWith(addrCtx, provider.Address.String()), provider.SchedulerApp.String(), nodesList, fmt.Sprintf("%d", provider.OffersCount), @@ -127,20 +130,22 @@ func outputText(ctx context.Context, npa *common.NPASelection, conn connection.C if roflCommon.ShowOffers { fmt.Println() for _, provider := range providers { - showProviderOffersExpanded(ctx, npa, conn, provider) + showProviderOffersExpanded(ctx, npa, conn, provider, addrCtx) } } } // showProviderOffersExpanded returns all offers for a given provider with expanded display. -func showProviderOffersExpanded(ctx context.Context, npa *common.NPASelection, conn connection.Connection, provider *roflmarket.Provider) { +func showProviderOffersExpanded(ctx context.Context, npa *common.NPASelection, conn connection.Connection, provider *roflmarket.Provider, addrCtx common.AddressFormatContext) { offers, err := conn.Runtime(npa.ParaTime).ROFLMarket.Offers(ctx, client.RoundLatest, provider.Address) if err != nil { cobra.CheckErr(fmt.Errorf("failed to query offers for provider %s: %w", provider.Address, err)) } + prettyAddr := common.PrettyAddressWith(addrCtx, provider.Address.String()) + if len(offers) == 0 { - fmt.Printf("Provider %s: No offers\n", provider.Address) + fmt.Printf("Provider %s: No offers\n", prettyAddr) return } @@ -154,7 +159,7 @@ func showProviderOffersExpanded(ctx context.Context, npa *common.NPASelection, c return false }) - fmt.Printf("Provider %s (%d offers):\n", provider.Address, len(offers)) + fmt.Printf("Provider %s (%d offers):\n", prettyAddr, len(offers)) for _, offer := range offers { ShowOfferSummary(npa, offer) } diff --git a/cmd/rofl/provider/show.go b/cmd/rofl/provider/show.go index 435de21d..cf7644af 100644 --- a/cmd/rofl/provider/show.go +++ b/cmd/rofl/provider/show.go @@ -85,20 +85,24 @@ func outputProviderJSON(provider *roflmarket.Provider, offers []*roflmarket.Offe // outputProviderText outputs provider details in human-readable format. func outputProviderText(npa *common.NPASelection, provider *roflmarket.Provider, offers []*roflmarket.Offer) { - fmt.Printf("Provider: %s\n", provider.Address) + // Precompute address formatting context for efficiency (network-aware for paratime/ROFL names). + addrCtx := common.GenAddressFormatContextForNetwork(npa.Network) + + fmt.Printf("Provider: %s\n", common.PrettyAddressWith(addrCtx, provider.Address.String())) fmt.Println() // Basic information. fmt.Println("=== Basic Information ===") fmt.Printf("Scheduler App: %s\n", provider.SchedulerApp) - // Payment address. + // Payment address with pretty formatting. var paymentAddr string switch { case provider.PaymentAddress.Native != nil: - paymentAddr = provider.PaymentAddress.Native.String() + paymentAddr = common.PrettyAddressWith(addrCtx, provider.PaymentAddress.Native.String()) case provider.PaymentAddress.Eth != nil: - paymentAddr = fmt.Sprintf("0x%x", provider.PaymentAddress.Eth[:]) + ethAddr := fmt.Sprintf("0x%x", provider.PaymentAddress.Eth[:]) + paymentAddr = common.PrettyAddressWith(addrCtx, ethAddr) default: paymentAddr = "" } diff --git a/cmd/wallet/list.go b/cmd/wallet/list.go index 60eb3d9d..6b3a65a7 100644 --- a/cmd/wallet/list.go +++ b/cmd/wallet/list.go @@ -25,10 +25,15 @@ var listCmd = &cobra.Command{ if cfg.Wallet.Default == name { name += common.DefaultMarker } + + addrStr := acc.Address + if ethAddr := acc.GetEthAddress(); ethAddr != nil { + addrStr = ethAddr.Hex() + } output = append(output, []string{ name, acc.PrettyKind(), - acc.Address, + addrStr, }) } diff --git a/cmd/wallet/remote_signer.go b/cmd/wallet/remote_signer.go index 5405fcd6..d6ab586c 100644 --- a/cmd/wallet/remote_signer.go +++ b/cmd/wallet/remote_signer.go @@ -58,7 +58,7 @@ var remoteSignerCmd = &cobra.Command{ err = srv.Start() cobra.CheckErr(err) - fmt.Printf("Address: %s\n", acc.Address()) + fmt.Printf("Address: %s\n", common.PrettyAddress(acc.Address().String())) fmt.Printf("Node Args:\n --signer.backend=remote \\\n --signer.remote.address=unix:%s\n", socketPath) fmt.Printf("\n*** REMOTE SIGNER READY ***\n") diff --git a/config/addressbook.go b/config/addressbook.go index ff8dff8d..fa50934b 100644 --- a/config/addressbook.go +++ b/config/addressbook.go @@ -4,7 +4,6 @@ import ( "fmt" ethCommon "github.com/ethereum/go-ethereum/common" - "github.com/spf13/cobra" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers" @@ -155,7 +154,9 @@ func (a *AddressBookEntry) GetAddress() types.Address { func (a *AddressBookEntry) GetEthAddress() *ethCommon.Address { if a.EthAddress != "" { _, ethAddr, err := helpers.ResolveEthOrOasisAddress(a.EthAddress) - cobra.CheckErr(err) + if err != nil { + return nil + } return ethAddr } diff --git a/config/wallet.go b/config/wallet.go index 511f6fad..73d12cbf 100644 --- a/config/wallet.go +++ b/config/wallet.go @@ -3,7 +3,10 @@ package config import ( "fmt" + ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" "github.com/oasisprotocol/cli/wallet" @@ -80,6 +83,11 @@ func (w *Wallet) Create(name string, passphrase string, nw *Account) error { } nw.Address = string(address) + // Store Ethereum address (if any) so we don't need to unlock/load the account later just to show it. + if ethAddr := acc.EthAddress(); ethAddr != nil { + nw.EthAddress = ethAddr.Hex() + } + if w.All == nil { w.All = make(map[string]*Account) } @@ -219,6 +227,11 @@ func (w *Wallet) Import(name string, passphrase string, nw *Account, src *wallet } nw.Address = string(address) + // Store Ethereum address (if any) so we don't need to unlock/load the account later just to show it. + if ethAddr := acc.EthAddress(); ethAddr != nil { + nw.EthAddress = ethAddr.Hex() + } + if w.All == nil { w.All = make(map[string]*Account) } @@ -252,6 +265,7 @@ type Account struct { Description string `mapstructure:"description"` Kind string `mapstructure:"kind"` Address string `mapstructure:"address"` + EthAddress string `mapstructure:"eth_address"` // Config contains kind-specific configuration for this wallet. Config map[string]interface{} `mapstructure:",remain"` @@ -270,6 +284,20 @@ func (a *Account) Validate() error { return fmt.Errorf("malformed address '%s': %w", a.Address, err) } + // Check that Ethereum address is valid and matches native address, if set. + if a.EthAddress != "" { + nativeAddr, ethAddr, err := helpers.ResolveEthOrOasisAddress(a.EthAddress) + if err != nil { + return fmt.Errorf("malformed eth address '%s': %w", a.EthAddress, err) + } + if nativeAddr == nil || ethAddr == nil { + return fmt.Errorf("eth address '%s' was not recognized as valid eth address", a.EthAddress) + } + if nativeAddr.String() != a.Address { + return fmt.Errorf("eth address '%s' (converted to '%s') mismatches stored address '%s'", a.EthAddress, nativeAddr.String(), a.Address) + } + } + // Check the algorithm is not empty. if _, ok := a.Config["algorithm"]; !ok { return fmt.Errorf("algorithm field not defined") @@ -287,6 +315,38 @@ func (a *Account) GetAddress() types.Address { return address } +// GetEthAddress returns the Ethereum address object, if set. +func (a *Account) GetEthAddress() *ethCommon.Address { + if a.EthAddress == "" { + return nil + } + + _, ethAddr, err := helpers.ResolveEthOrOasisAddress(a.EthAddress) + if err != nil { + return nil + } + return ethAddr +} + +// SupportsEthAddress returns true iff the account can have a corresponding Ethereum address. +func (a *Account) SupportsEthAddress() bool { + rawAlg, ok := a.Config["algorithm"] + if !ok { + return false + } + alg, ok := rawAlg.(string) + if !ok { + return false + } + + switch alg { + case wallet.AlgorithmSecp256k1Bip44, wallet.AlgorithmSecp256k1Raw: + return true + default: + return false + } +} + // SetConfigFromFlags populates the kind-specific configuration from CLI flags. func (a *Account) SetConfigFromFlags() error { af, err := wallet.Load(a.Kind)