diff --git a/cmd/account/show/allowances.go b/cmd/account/show/allowances.go index 5369a0b2..92bd9a30 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. + addrCtx := common.GenAddressFormatContext() + 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 7186ace3..9a164d32 100644 --- a/cmd/account/show/delegations.go +++ b/cmd/account/show/delegations.go @@ -16,6 +16,8 @@ import ( "github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/consensusaccounts" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" + + "github.com/oasisprotocol/cli/cmd/common" ) const amountFieldName = "Amount:" @@ -87,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 @@ -102,8 +110,12 @@ func prettyPrintDelegationDescriptions( lenLongest = lenLongestString(addressFieldName, amountFieldName, endTimeFieldName) } + // Precompute address formatting context for efficiency. + addrCtx := common.GenAddressFormatContext() + 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 f04f361f..619fff20 100644 --- a/cmd/account/show/show.go +++ b/cmd/account/show/show.go @@ -13,6 +13,7 @@ import ( "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" "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" @@ -64,13 +65,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") @@ -83,9 +90,36 @@ var ( nativeAddr, ethAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, targetAddress) cobra.CheckErr(err) - out.EthereumAddress = ethAddr out.NativeAddress = nativeAddr - out.Name = common.FindAccountName(nativeAddr.String()) + addrCtx := common.GenAddressFormatContext() + out.Name = addrCtx.Names[nativeAddr.String()] + + // If eth address is not available, try to get it from locally-known mappings + // (wallet/addressbook/test accounts). No unlock required. + if ethAddr == nil { + if ethHex := addrCtx.Eth[nativeAddr.String()]; ethHex != "" && ethCommon.IsHexAddress(ethHex) { + eth := ethCommon.HexToAddress(ethHex) + ethAddr = ð + } + } + + // 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. + // + // NOTE: We only do this for an explicitly selected wallet account to avoid surprising + // interactive prompts when a user provides an arbitrary address. + 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() + } + } + } + + // Ensure output reflects the final resolved/derived Ethereum address (if any). + out.EthereumAddress = ethAddr height, err := common.GetActualHeight( ctx, diff --git a/cmd/common/helpers.go b/cmd/common/helpers.go index 88b09439..14b5ed41 100644 --- a/cmd/common/helpers.go +++ b/cmd/common/helpers.go @@ -3,12 +3,17 @@ package common import ( "fmt" "os" + "sort" + ethCommon "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" + staking "github.com/oasisprotocol/oasis-core/go/staking/api" + "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" ) @@ -41,19 +46,66 @@ func CheckForceErr(err interface{}) { cobra.CheckErr(errMsg) } -// GenAccountNames generates a map of all addresses -> account name for pretty printing. +// GenAccountNames generates a map of all known native addresses -> account name for pretty printing. +// It includes test accounts, configured networks (paratimes/ROFL defaults), addressbook and wallet. +// +// Priority order (later entries overwrite earlier): +// test accounts < network entries < addressbook < wallet. func GenAccountNames() 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. + cfg := config.Global() + netNames := make([]string, 0, len(cfg.Networks.All)) + for name := range cfg.Networks.All { + netNames = append(netNames, name) } + sort.Strings(netNames) + for _, netName := range netNames { + net := cfg.Networks.All[netName] + if net == nil { + continue + } - for name, acc := range config.Global().AddressBook.All { + // Include ParaTime runtime addresses as paratime:. + ptNames := make([]string, 0, len(net.ParaTimes.All)) + for ptName := range net.ParaTimes.All { + ptNames = append(ptNames, ptName) + } + sort.Strings(ptNames) + for _, ptName := range ptNames { + pt := net.ParaTimes.All[ptName] + if pt == nil { + continue + } + + rtAddr := types.NewAddressFromConsensus(staking.NewRuntimeAddress(pt.Namespace())) + an[rtAddr.String()] = fmt.Sprintf("paratime:%s", ptName) + + // Include ROFL default provider addresses as rofl:provider:. + 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 cfg.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 cfg.Wallet.All { + an[acc.GetAddress().String()] = name } return an @@ -64,3 +116,118 @@ func FindAccountName(address string) string { an := GenAccountNames() 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. + // This is optional metadata coming from wallet/addressbook/test accounts (and never derived from chain state). + Eth map[string]string +} + +// GenAccountEthMap generates a map of native address string -> eth hex address (if known). +func GenAccountEthMap() 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 AddressFormatContext{ + Names: GenAccountNames(), + Eth: GenAccountEthMap(), + } +} + +// 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) +} 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..5108453c 100644 --- a/cmd/common/json.go +++ b/cmd/common/json.go @@ -152,6 +152,12 @@ func JSONMarshalUniversalValue(v interface{}) []byte { // For types implementing consensusPretty.PrettyPrinter, it uses the custom pretty printer. // For other types, it does basic JSON indentation and cleanup of common delimiters. func PrettyPrint(npa *NPASelection, prefix string, blob interface{}) string { + return PrettyPrintWithTxDetails(npa, prefix, blob, nil) +} + +// PrettyPrintWithTxDetails is like PrettyPrint but also includes transaction-specific details in the +// signature context (if the blob is a transaction pretty-printer). +func PrettyPrintWithTxDetails(npa *NPASelection, prefix string, blob interface{}, txDetails *signature.TxDetails) string { ret := "" switch rtx := blob.(type) { case consensusPretty.PrettyPrinter: @@ -164,6 +170,7 @@ func PrettyPrint(npa *NPASelection, prefix string, blob interface{}) string { RuntimeID: ns, ChainContext: npa.Network.ChainContext, Base: types.SignatureContextBase, + TxDetails: txDetails, } ctx := context.Background() ctx = context.WithValue(ctx, consensusPretty.ContextKeyTokenSymbol, npa.Network.Denomination.Symbol) @@ -172,7 +179,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 locally-known names and native->ETH mapping for address formatting. + addrCtx := GenAddressFormatContext() + 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/json_test.go b/cmd/common/json_test.go new file mode 100644 index 00000000..216d6ece --- /dev/null +++ b/cmd/common/json_test.go @@ -0,0 +1,56 @@ +package common + +import ( + "strings" + "testing" + + ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/oasisprotocol/oasis-core/go/common/quantity" + + sdkConfig "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" + sdkSignature "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/accounts" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" +) + +func TestPrettyPrintWithTxDetails_PreservesUnnamedEthTo(t *testing.T) { + require := require.New(t) + + pt := &sdkConfig.ParaTime{ + ID: strings.Repeat("0", 64), + Denominations: map[string]*sdkConfig.DenominationInfo{ + sdkConfig.NativeDenominationKey: { + Symbol: "TEST", + Decimals: 18, + }, + }, + } + + npa := &NPASelection{ + NetworkName: "testnet", + Network: &sdkConfig.Network{ + ChainContext: "test-chain-context", + Denomination: sdkConfig.DenominationInfo{ + Symbol: "TEST", + Decimals: 9, + }, + }, + ParaTimeName: "test-paratime", + ParaTime: pt, + } + + ethAddr := ethCommon.HexToAddress("0x1111111111111111111111111111111111111111") + to := types.NewAddressFromEth(ethAddr.Bytes()) + amt := types.NewBaseUnits(*quantity.NewFromUint64(0), types.NativeDenomination) + tx := accounts.NewTransferTx(nil, &accounts.Transfer{ + To: to, + Amount: amt, + }) + + out := PrettyPrintWithTxDetails(npa, "", tx, &sdkSignature.TxDetails{OrigTo: ðAddr}) + + require.Contains(out, "To: "+ethAddr.Hex()) + require.NotContains(out, "To: "+to.String()) +} diff --git a/cmd/common/transaction.go b/cmd/common/transaction.go index 15112103..040b66da 100644 --- a/cmd/common/transaction.go +++ b/cmd/common/transaction.go @@ -188,7 +188,7 @@ func SignConsensusTransaction( return tx, nil } - PrintTransactionBeforeSigning(npa, tx) + PrintTransactionBeforeSigning(npa, tx, nil) // Sign the transaction. // NOTE: We build our own domain separation context here as we need to support multiple chain @@ -345,7 +345,7 @@ func SignParaTimeTransaction( return tx, meta, nil } - PrintTransactionBeforeSigning(npa, tx) + PrintTransactionBeforeSigning(npa, tx, txDetails) // Sign the transaction. ts := tx.PrepareForSigning() @@ -363,9 +363,15 @@ func SignParaTimeTransaction( // PrintTransactionRaw prints the transaction which can be either signed or unsigned. func PrintTransactionRaw(npa *NPASelection, tx interface{}) { + PrintTransactionRawWithTxDetails(npa, tx, nil) +} + +// PrintTransactionRawWithTxDetails prints the transaction which can be either signed or unsigned, +// and also provides transaction-specific details to the pretty-printer when applicable. +func PrintTransactionRawWithTxDetails(npa *NPASelection, tx interface{}, txDetails *signature.TxDetails) { switch tx.(type) { case consensusPretty.PrettyPrinter: - fmt.Print(PrettyPrint(npa, "", tx)) + fmt.Print(PrettyPrintWithTxDetails(npa, "", tx, txDetails)) default: fmt.Printf("[unsupported transaction type: %T]\n", tx) } @@ -374,7 +380,13 @@ func PrintTransactionRaw(npa *NPASelection, tx interface{}) { // PrintTransaction prints the transaction which can be either signed or unsigned together with // information about the selected network/ParaTime. func PrintTransaction(npa *NPASelection, tx interface{}) { - PrintTransactionRaw(npa, tx) + PrintTransactionWithTxDetails(npa, tx, nil) +} + +// PrintTransactionWithTxDetails is like PrintTransaction but also provides transaction-specific +// details to the pretty-printer when applicable. +func PrintTransactionWithTxDetails(npa *NPASelection, tx interface{}, txDetails *signature.TxDetails) { + PrintTransactionRawWithTxDetails(npa, tx, txDetails) fmt.Println() fmt.Printf("Network: %s", npa.PrettyPrintNetwork()) @@ -392,10 +404,10 @@ func PrintTransaction(npa *NPASelection, tx interface{}) { } // PrintTransactionBeforeSigning prints the transaction and asks the user for confirmation. -func PrintTransactionBeforeSigning(npa *NPASelection, tx interface{}) { +func PrintTransactionBeforeSigning(npa *NPASelection, tx interface{}, txDetails *signature.TxDetails) { fmt.Printf("You are about to sign the following transaction:\n") - PrintTransaction(npa, tx) + PrintTransactionWithTxDetails(npa, tx, txDetails) fmt.Printf("Account: %s", npa.AccountName) if len(npa.Account.Description) > 0 { 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..547bf0b3 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.GenAddressFormatContext() + 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.GenAddressFormatContext() + 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..7fb7e410 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.GenAddressFormatContext() + 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..d357d9e7 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.GenAddressFormatContext() // 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..97aeb7cf 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.GenAddressFormatContext() + 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..01228532 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.GenAddressFormatContext() + 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..d17ecaf2 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.GenAddressFormatContext() + 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..5be9937b 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. + addrCtx := common.GenAddressFormatContext() + 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..7d9ed9a5 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.GenAddressFormatContext() // 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.GenAddressFormatContext() // 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.GenAddressFormatContext() // 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.GenAddressFormatContext() // 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 65c3ed01..0a16d306 100644 --- a/cmd/rofl/machine/show.go +++ b/cmd/rofl/machine/show.go @@ -60,6 +60,8 @@ var showCmd = &cobra.Command{ cobra.CheckErr(fmt.Sprintf("Invalid provider address: %s", err)) } + addrCtx := common.GenAddressFormatContext() + 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. @@ -103,7 +105,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) @@ -111,8 +113,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..d295e5b7 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -506,13 +506,15 @@ var ( return } + addrCtx := common.GenAddressFormatContext() + 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..23580574 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. + addrCtx := common.GenAddressFormatContext() + 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..d5bae5b8 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. + addrCtx := common.GenAddressFormatContext() + + 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..bdef2b1d 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" @@ -127,11 +126,11 @@ func (a *AddressBookEntry) Validate() error { } if a.EthAddress != "" { - nativeAddr, _, err := helpers.ResolveEthOrOasisAddress(a.EthAddress) + nativeAddr, ethAddr, err := helpers.ResolveEthOrOasisAddress(a.EthAddress) if err != nil { return fmt.Errorf("malformed address '%s': %w", a.EthAddress, err) } - if nativeAddr == nil { + 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 { @@ -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/config.go b/config/config.go index 60972f0a..1fba4af0 100644 --- a/config/config.go +++ b/config/config.go @@ -73,7 +73,13 @@ func encode(in interface{}) (interface{}, error) { const tagName = "mapstructure" v := reflect.ValueOf(in) + if !v.IsValid() { + return nil, nil + } if v.Kind() == reflect.Ptr { + if v.IsNil() { + return nil, nil + } v = v.Elem() } @@ -100,6 +106,11 @@ func encode(in interface{}) (interface{}, error) { } } + // Implement omitempty similarly to encoding/json: omit zero values when requested. + if attributes["omitempty"] && isEmptyValue(v.Field(i)) { + continue + } + // Encode value. value, err := encode(v.Field(i).Interface()) if err != nil { @@ -150,6 +161,25 @@ func encode(in interface{}) (interface{}, error) { } } +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + default: + return false + } +} + // Save saves the configuration structure to viper. func (cfg *Config) Save() error { if err := cfg.Validate(); err != nil { diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 00000000..9cdd425a --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,38 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEncodeOmitEmptyMapstructure(t *testing.T) { + require := require.New(t) + + t.Run("omitempty omits empty string field", func(t *testing.T) { + entry := AddressBookEntry{ + Description: "d", + Address: "oasis1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqd39y6h", + EthAddress: "", + } + + enc, err := encode(entry) + require.NoError(err) + m, ok := enc.(map[string]interface{}) + require.True(ok) + require.NotContains(m, "eth_address") + }) + + t.Run("omitempty includes non-empty string field", func(t *testing.T) { + entry := AddressBookEntry{ + Address: "oasis1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqd39y6h", + EthAddress: "0x60a6321ea71d37102dbf923aae2e08d005c4e403", + } + + enc, err := encode(entry) + require.NoError(err) + m, ok := enc.(map[string]interface{}) + require.True(ok) + require.Equal(entry.EthAddress, m["eth_address"]) + }) +} diff --git a/config/wallet.go b/config/wallet.go index 511f6fad..e346984e 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,omitempty"` // 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)