Skip to content

Commit ee0dbb3

Browse files
feat: format known addresses
1 parent 28cfc20 commit ee0dbb3

23 files changed

Lines changed: 522 additions & 57 deletions

File tree

cmd/account/show/allowances.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77

88
staking "github.com/oasisprotocol/oasis-core/go/staking/api"
99

10+
"github.com/oasisprotocol/cli/cmd/common"
11+
1012
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/config"
1113
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers"
1214
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/types"
@@ -60,8 +62,12 @@ func prettyPrintAllowanceDescriptions(
6062
// element so we can align all values.
6163
lenLongest := lenLongestString(beneficiaryFieldName, amountFieldName)
6264

65+
// Precompute address formatting context for efficiency (network-aware for paratime names).
66+
addrCtx := common.GenAddressFormatContextForNetwork(network)
67+
6368
for _, desc := range allowDescriptions {
64-
fmt.Fprintf(w, "%s - %-*s %s", prefix, lenLongest, beneficiaryFieldName, desc.beneficiary)
69+
prettyAddr := common.PrettyAddressWith(addrCtx, desc.beneficiary.String())
70+
fmt.Fprintf(w, "%s - %-*s %s", prefix, lenLongest, beneficiaryFieldName, prettyAddr)
6571
if desc.self {
6672
fmt.Fprintf(w, " (self)")
6773
}

cmd/account/show/delegations.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ func prettyPrintDelegationDescriptions(
8989

9090
fmt.Fprintf(w, "%sDelegations:\n", prefix)
9191

92+
// Guard against empty slice to prevent panic when accessing delDescriptions[0].
93+
if len(delDescriptions) == 0 {
94+
fmt.Fprintf(w, "%s <none>\n", prefix)
95+
return
96+
}
97+
9298
sort.Sort(byEndTimeAmountAddress(delDescriptions))
9399

94100
// Get the length of name of the longest field to display for each
@@ -104,8 +110,12 @@ func prettyPrintDelegationDescriptions(
104110
lenLongest = lenLongestString(addressFieldName, amountFieldName, endTimeFieldName)
105111
}
106112

113+
// Precompute address formatting context for efficiency (network-aware for paratime names).
114+
addrCtx := common.GenAddressFormatContextForNetwork(network)
115+
107116
for _, desc := range delDescriptions {
108-
fmt.Fprintf(w, "%s - %-*s %s", prefix, lenLongest, addressFieldName, desc.address)
117+
prettyAddr := common.PrettyAddressWith(addrCtx, desc.address.String())
118+
fmt.Fprintf(w, "%s - %-*s %s", prefix, lenLongest, addressFieldName, prettyAddr)
109119
if desc.self {
110120
fmt.Fprintf(w, " (self)")
111121
}

cmd/account/show/show.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/spf13/cobra"
1010
flag "github.com/spf13/pflag"
11+
"golang.org/x/term"
1112

1213
consensus "github.com/oasisprotocol/oasis-core/go/consensus/api"
1314
roothash "github.com/oasisprotocol/oasis-core/go/roothash/api"
@@ -38,13 +39,19 @@ var (
3839
// Determine which address to show. If an explicit argument was given, use that
3940
// otherwise use the default account.
4041
var targetAddress string
42+
var walletNameForEth string
4143
switch {
4244
case len(args) >= 1:
4345
// Explicit argument given.
4446
targetAddress = args[0]
47+
if _, ok := cfg.Wallet.All[targetAddress]; ok {
48+
// Wallet account selected by name.
49+
walletNameForEth = targetAddress
50+
}
4551
case npa.Account != nil:
4652
// Default account is selected.
4753
targetAddress = npa.Account.Address
54+
walletNameForEth = npa.AccountName
4855
default:
4956
// No address given and no wallet configured.
5057
cobra.CheckErr("no address given and no wallet configured")
@@ -58,10 +65,33 @@ var (
5865
nativeAddr, ethAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, targetAddress)
5966
cobra.CheckErr(err)
6067

61-
if name := common.FindAccountName(nativeAddr.String()); name != "" {
68+
name := common.FindAccountNameForNetwork(npa.Network, nativeAddr.String())
69+
if name != "" {
6270
fmt.Printf("Name: %s\n", name)
6371
}
6472

73+
// If eth address is not available, try to get it from wallet config (no unlock required).
74+
if ethAddr == nil {
75+
for _, walletCfg := range cfg.Wallet.All {
76+
if walletCfg.Address == nativeAddr.String() {
77+
ethAddr = walletCfg.GetEthAddress()
78+
break
79+
}
80+
}
81+
}
82+
83+
// If eth address is still not available and the user selected a wallet account,
84+
// load it (may require passphrase) to derive and persist eth_address metadata.
85+
if ethAddr == nil && walletNameForEth != "" {
86+
if walletCfg, ok := cfg.Wallet.All[walletNameForEth]; ok && walletCfg.SupportsEthAddress() {
87+
// Avoid prompting in non-interactive contexts (e.g. piping output).
88+
if term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) {
89+
acc := common.LoadAccount(cfg, walletNameForEth)
90+
ethAddr = acc.EthAddress()
91+
}
92+
}
93+
}
94+
6595
height, err := common.GetActualHeight(
6696
ctx,
6797
c.Consensus().Core(),

cmd/common/helpers.go

Lines changed: 180 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ import (
44
"fmt"
55
"os"
66

7+
ethCommon "github.com/ethereum/go-ethereum/common"
78
"github.com/spf13/cobra"
89

10+
staking "github.com/oasisprotocol/oasis-core/go/staking/api"
11+
configSdk "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config"
12+
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers"
913
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/testing"
1014
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/types"
1115

16+
buildRoflProvider "github.com/oasisprotocol/cli/build/rofl/provider"
1217
"github.com/oasisprotocol/cli/config"
1318
)
1419

@@ -42,18 +47,51 @@ func CheckForceErr(err interface{}) {
4247
}
4348

4449
// GenAccountNames generates a map of all addresses -> account name for pretty printing.
50+
// Priority order (later entries overwrite earlier): test accounts < addressbook < wallet.
51+
// This ensures wallet identity takes precedence over addressbook labels for the same address.
4552
func GenAccountNames() types.AccountNames {
53+
return GenAccountNamesForNetwork(nil)
54+
}
55+
56+
// GenAccountNamesForNetwork generates a map of all addresses -> account name for pretty printing,
57+
// including network-specific entries (paratimes, ROFL providers) when a network is provided.
58+
// Priority order (later entries overwrite earlier): test accounts < network entries < addressbook < wallet.
59+
func GenAccountNamesForNetwork(net *configSdk.Network) types.AccountNames {
4660
an := types.AccountNames{}
47-
for name, acc := range config.Global().Wallet.All {
48-
an[acc.GetAddress().String()] = name
61+
62+
// Test accounts have lowest priority.
63+
for name, acc := range testing.TestAccounts {
64+
an[acc.Address.String()] = fmt.Sprintf("test:%s", name)
4965
}
5066

67+
// Network-derived entries (paratimes, ROFL providers) have second-lowest priority.
68+
if net != nil {
69+
// Include ParaTime runtime addresses as paratime:<name>.
70+
for ptName, pt := range net.ParaTimes.All {
71+
rtAddr := types.NewAddressFromConsensus(staking.NewRuntimeAddress(pt.Namespace()))
72+
an[rtAddr.String()] = fmt.Sprintf("paratime:%s", ptName)
73+
}
74+
75+
// Include ROFL default provider addresses as rofl:provider:<paratime>.
76+
for ptName, pt := range net.ParaTimes.All {
77+
if svc, ok := buildRoflProvider.DefaultRoflServices[pt.ID]; ok {
78+
if svc.Provider != "" {
79+
if a, _, err := helpers.ResolveEthOrOasisAddress(svc.Provider); err == nil && a != nil {
80+
an[a.String()] = fmt.Sprintf("rofl:provider:%s", ptName)
81+
}
82+
}
83+
}
84+
}
85+
}
86+
87+
// Addressbook entries have medium priority.
5188
for name, acc := range config.Global().AddressBook.All {
5289
an[acc.GetAddress().String()] = name
5390
}
5491

55-
for name, acc := range testing.TestAccounts {
56-
an[acc.Address.String()] = fmt.Sprintf("test:%s", name)
92+
// Wallet entries have highest priority.
93+
for name, acc := range config.Global().Wallet.All {
94+
an[acc.GetAddress().String()] = name
5795
}
5896

5997
return an
@@ -64,3 +102,141 @@ func FindAccountName(address string) string {
64102
an := GenAccountNames()
65103
return an[address]
66104
}
105+
106+
// FindAccountNameForNetwork finds account's name in the context of a specific network.
107+
func FindAccountNameForNetwork(net *configSdk.Network, address string) string {
108+
an := GenAccountNamesForNetwork(net)
109+
return an[address]
110+
}
111+
112+
// AddressFormatContext contains precomputed maps for address formatting.
113+
type AddressFormatContext struct {
114+
// Names maps native address string to account name.
115+
Names types.AccountNames
116+
// Eth maps native address string to Ethereum hex address string (if known).
117+
Eth map[string]string
118+
}
119+
120+
// GenAccountEthMap generates a map of native address string -> eth hex address (if known).
121+
func GenAccountEthMap() map[string]string {
122+
return GenAccountEthMapForNetwork(nil)
123+
}
124+
125+
// GenAccountEthMapForNetwork generates a map of native address string -> eth hex address (if known).
126+
func GenAccountEthMapForNetwork(_ *configSdk.Network) map[string]string {
127+
eth := make(map[string]string)
128+
129+
// From test accounts.
130+
for _, acc := range testing.TestAccounts {
131+
if acc.EthAddress != nil {
132+
eth[acc.Address.String()] = acc.EthAddress.Hex()
133+
}
134+
}
135+
136+
// From address book entries (higher priority than test accounts).
137+
for _, acc := range config.Global().AddressBook.All {
138+
if ethAddr := acc.GetEthAddress(); ethAddr != nil {
139+
eth[acc.GetAddress().String()] = ethAddr.Hex()
140+
}
141+
}
142+
143+
// From wallet entries (highest priority), when an Ethereum address is available in config.
144+
for _, acc := range config.Global().Wallet.All {
145+
if ethAddr := acc.GetEthAddress(); ethAddr != nil {
146+
eth[acc.GetAddress().String()] = ethAddr.Hex()
147+
}
148+
}
149+
150+
return eth
151+
}
152+
153+
// GenAddressFormatContext builds both name and eth address maps for formatting.
154+
func GenAddressFormatContext() AddressFormatContext {
155+
return GenAddressFormatContextForNetwork(nil)
156+
}
157+
158+
// GenAddressFormatContextForNetwork builds both name and eth address maps for formatting,
159+
// including network-specific entries when a network is provided.
160+
func GenAddressFormatContextForNetwork(net *configSdk.Network) AddressFormatContext {
161+
return AddressFormatContext{
162+
Names: GenAccountNamesForNetwork(net),
163+
Eth: GenAccountEthMapForNetwork(net),
164+
}
165+
}
166+
167+
// PrettyAddressWith formats an address using a precomputed context.
168+
// If the address is known (in wallet, addressbook, or test accounts), it returns "name (address)".
169+
// For secp256k1 accounts with a known Ethereum address, the Ethereum hex format is preferred in parentheses.
170+
// If the address is unknown, it returns the original address string unchanged.
171+
func PrettyAddressWith(ctx AddressFormatContext, addr string) string {
172+
// Try to parse the address to get canonical native form.
173+
nativeAddr, ethFromInput, err := helpers.ResolveEthOrOasisAddress(addr)
174+
if err != nil || nativeAddr == nil {
175+
// Cannot parse, return unchanged.
176+
return addr
177+
}
178+
179+
nativeStr := nativeAddr.String()
180+
181+
// Look up the name.
182+
name := ctx.Names[nativeStr]
183+
if name == "" {
184+
// Unknown address, return the original input.
185+
return addr
186+
}
187+
188+
// Determine which address to show in parentheses.
189+
// Prefer Ethereum address if available (from input or from known eth addresses).
190+
var parenAddr string
191+
if ethFromInput != nil {
192+
parenAddr = ethFromInput.Hex()
193+
} else if ethHex := ctx.Eth[nativeStr]; ethHex != "" {
194+
parenAddr = ethHex
195+
} else {
196+
parenAddr = nativeStr
197+
}
198+
199+
// Guard against redundant "name (name)" output.
200+
if name == parenAddr {
201+
return parenAddr
202+
}
203+
204+
return fmt.Sprintf("%s (%s)", name, parenAddr)
205+
}
206+
207+
func preferredAddressString(nativeAddr *types.Address, ethAddr *ethCommon.Address) string {
208+
if ethAddr != nil {
209+
return ethAddr.Hex()
210+
}
211+
if nativeAddr == nil {
212+
return ""
213+
}
214+
return nativeAddr.String()
215+
}
216+
217+
// PrettyResolvedAddressWith formats a resolved address tuple (native, eth) using a precomputed context.
218+
//
219+
// If ethAddr is non-nil, it is preferred to preserve the "user provided eth" behavior even when
220+
// the native-to-eth mapping is not available in the context.
221+
func PrettyResolvedAddressWith(ctx AddressFormatContext, nativeAddr *types.Address, ethAddr *ethCommon.Address) string {
222+
addrStr := preferredAddressString(nativeAddr, ethAddr)
223+
if addrStr == "" {
224+
return ""
225+
}
226+
return PrettyAddressWith(ctx, addrStr)
227+
}
228+
229+
// PrettyAddress formats an address for display without network context.
230+
// If the address is known (in wallet, addressbook, or test accounts), it returns "name (address)".
231+
// For secp256k1 accounts with a known Ethereum address, the Ethereum hex format is preferred in parentheses.
232+
// If the address is unknown, it returns the original address string unchanged.
233+
func PrettyAddress(addr string) string {
234+
return PrettyAddressWith(GenAddressFormatContext(), addr)
235+
}
236+
237+
// PrettyAddressForNetwork formats an address for display with network context.
238+
// This includes network-derived names like paratime addresses and ROFL providers.
239+
func PrettyAddressForNetwork(net *configSdk.Network, addr types.Address) string {
240+
ctx := GenAddressFormatContextForNetwork(net)
241+
return PrettyAddressWith(ctx, addr.String())
242+
}

0 commit comments

Comments
 (0)