Skip to content

Commit d0a2549

Browse files
feat: format known addresses
fix(account show): propagate derived eth address refactor: unify address formatting context generation feat: add PrettyPrintWithTxDetails function and related tests refactor: replace PrettyAddressWith with PrettyAddress for address formatting
1 parent bcd0401 commit d0a2549

32 files changed

Lines changed: 514 additions & 67 deletions

cmd/account/show/allowances.go

Lines changed: 4 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"
@@ -61,7 +63,8 @@ func prettyPrintAllowanceDescriptions(
6163
lenLongest := lenLongestString(beneficiaryFieldName, amountFieldName)
6264

6365
for _, desc := range allowDescriptions {
64-
fmt.Fprintf(w, "%s - %-*s %s", prefix, lenLongest, beneficiaryFieldName, desc.beneficiary)
66+
prettyAddr := common.PrettyAddress(desc.beneficiary.String())
67+
fmt.Fprintf(w, "%s - %-*s %s", prefix, lenLongest, beneficiaryFieldName, prettyAddr)
6568
if desc.self {
6669
fmt.Fprintf(w, " (self)")
6770
}

cmd/account/show/delegations.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers"
1717
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/consensusaccounts"
1818
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/types"
19+
20+
"github.com/oasisprotocol/cli/cmd/common"
1921
)
2022

2123
const amountFieldName = "Amount:"
@@ -87,6 +89,12 @@ func prettyPrintDelegationDescriptions(
8789

8890
fmt.Fprintf(w, "%sDelegations:\n", prefix)
8991

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+
9098
sort.Sort(byEndTimeAmountAddress(delDescriptions))
9199

92100
// Get the length of name of the longest field to display for each
@@ -103,7 +111,8 @@ func prettyPrintDelegationDescriptions(
103111
}
104112

105113
for _, desc := range delDescriptions {
106-
fmt.Fprintf(w, "%s - %-*s %s", prefix, lenLongest, addressFieldName, desc.address)
114+
prettyAddr := common.PrettyAddress(desc.address.String())
115+
fmt.Fprintf(w, "%s - %-*s %s", prefix, lenLongest, addressFieldName, prettyAddr)
107116
if desc.self {
108117
fmt.Fprintf(w, " (self)")
109118
}

cmd/account/show/show.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,20 @@ var (
8383

8484
nativeAddr, ethAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, targetAddress)
8585
cobra.CheckErr(err)
86-
out.EthereumAddress = ethAddr
8786
out.NativeAddress = nativeAddr
88-
out.Name = common.FindAccountName(nativeAddr.String())
87+
addrCtx := common.GenAddressFormatContext()
88+
out.Name = addrCtx.Names[nativeAddr.String()]
89+
90+
// If eth address is not available, try to get it from locally-known mappings
91+
// (wallet/addressbook/test accounts). No unlock required.
92+
if ethAddr == nil {
93+
if ethHex := addrCtx.Eth[nativeAddr.String()]; ethHex != "" && ethCommon.IsHexAddress(ethHex) {
94+
eth := ethCommon.HexToAddress(ethHex)
95+
ethAddr = &eth
96+
}
97+
}
98+
99+
out.EthereumAddress = ethAddr
89100

90101
height, err := common.GetActualHeight(
91102
ctx,

cmd/common/helpers.go

Lines changed: 136 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ package common
33
import (
44
"fmt"
55
"os"
6+
"sort"
67

78
"github.com/spf13/cobra"
89

10+
staking "github.com/oasisprotocol/oasis-core/go/staking/api"
11+
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers"
912
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/testing"
1013
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/types"
1114

15+
buildRoflProvider "github.com/oasisprotocol/cli/build/rofl/provider"
1216
"github.com/oasisprotocol/cli/config"
1317
)
1418

@@ -41,19 +45,66 @@ func CheckForceErr(err interface{}) {
4145
cobra.CheckErr(errMsg)
4246
}
4347

44-
// GenAccountNames generates a map of all addresses -> account name for pretty printing.
48+
// GenAccountNames generates a map of all known native addresses -> account name for pretty printing.
49+
// It includes test accounts, configured networks (paratimes/ROFL defaults), addressbook and wallet.
50+
//
51+
// Priority order (later entries overwrite earlier):
52+
// test accounts < network entries < addressbook < wallet.
4553
func GenAccountNames() types.AccountNames {
4654
an := types.AccountNames{}
47-
for name, acc := range config.Global().Wallet.All {
48-
an[acc.GetAddress().String()] = name
55+
56+
// Test accounts have lowest priority.
57+
for name, acc := range testing.TestAccounts {
58+
an[acc.Address.String()] = fmt.Sprintf("test:%s", name)
59+
}
60+
61+
// Network-derived entries (paratimes, ROFL providers) have second-lowest priority.
62+
cfg := config.Global()
63+
netNames := make([]string, 0, len(cfg.Networks.All))
64+
for name := range cfg.Networks.All {
65+
netNames = append(netNames, name)
66+
}
67+
sort.Strings(netNames)
68+
for _, netName := range netNames {
69+
net := cfg.Networks.All[netName]
70+
if net == nil {
71+
continue
72+
}
73+
74+
// Include ParaTime runtime addresses as paratime:<name>.
75+
ptNames := make([]string, 0, len(net.ParaTimes.All))
76+
for ptName := range net.ParaTimes.All {
77+
ptNames = append(ptNames, ptName)
78+
}
79+
sort.Strings(ptNames)
80+
for _, ptName := range ptNames {
81+
pt := net.ParaTimes.All[ptName]
82+
if pt == nil {
83+
continue
84+
}
85+
86+
rtAddr := types.NewAddressFromConsensus(staking.NewRuntimeAddress(pt.Namespace()))
87+
an[rtAddr.String()] = fmt.Sprintf("paratime:%s", ptName)
88+
89+
// Include ROFL default provider addresses as rofl:provider:<paratime>.
90+
if svc, ok := buildRoflProvider.DefaultRoflServices[pt.ID]; ok {
91+
if svc.Provider != "" {
92+
if a, _, err := helpers.ResolveEthOrOasisAddress(svc.Provider); err == nil && a != nil {
93+
an[a.String()] = fmt.Sprintf("rofl:provider:%s", ptName)
94+
}
95+
}
96+
}
97+
}
4998
}
5099

51-
for name, acc := range config.Global().AddressBook.All {
100+
// Addressbook entries have medium priority.
101+
for name, acc := range cfg.AddressBook.All {
52102
an[acc.GetAddress().String()] = name
53103
}
54104

55-
for name, acc := range testing.TestAccounts {
56-
an[acc.Address.String()] = fmt.Sprintf("test:%s", name)
105+
// Wallet entries have highest priority.
106+
for name, acc := range cfg.Wallet.All {
107+
an[acc.GetAddress().String()] = name
57108
}
58109

59110
return an
@@ -64,3 +115,82 @@ func FindAccountName(address string) string {
64115
an := GenAccountNames()
65116
return an[address]
66117
}
118+
119+
// AddressFormatContext contains precomputed maps for address formatting.
120+
type AddressFormatContext struct {
121+
// Names maps native address string to account name.
122+
Names types.AccountNames
123+
// Eth maps native address string to Ethereum hex address string, if known.
124+
Eth map[string]string
125+
}
126+
127+
// GenAccountEthMap generates a map of native address string -> eth hex address (if known).
128+
// Priority order matches GenAccountNames: test accounts < addressbook < wallet.
129+
func GenAccountEthMap() map[string]string {
130+
eth := make(map[string]string)
131+
132+
for _, acc := range testing.TestAccounts {
133+
if acc.EthAddress != nil {
134+
eth[acc.Address.String()] = acc.EthAddress.Hex()
135+
}
136+
}
137+
138+
for _, acc := range config.Global().AddressBook.All {
139+
if ethAddr := acc.GetEthAddress(); ethAddr != nil {
140+
eth[acc.GetAddress().String()] = ethAddr.Hex()
141+
}
142+
}
143+
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 AddressFormatContext{
156+
Names: GenAccountNames(),
157+
Eth: GenAccountEthMap(),
158+
}
159+
}
160+
161+
// PrettyAddressWith formats a string address for display using a precomputed context.
162+
// Known addresses return "name (preferred_addr)", unknown addresses return the input unchanged.
163+
func PrettyAddressWith(ctx AddressFormatContext, addr string) string {
164+
nativeAddr, ethFromInput, err := helpers.ResolveEthOrOasisAddress(addr)
165+
if err != nil || nativeAddr == nil {
166+
return addr
167+
}
168+
169+
nativeStr := nativeAddr.String()
170+
171+
name := ctx.Names[nativeStr]
172+
if name == "" {
173+
return addr
174+
}
175+
176+
// Prefer eth address in parentheses when available.
177+
var parenAddr string
178+
if ethFromInput != nil {
179+
parenAddr = ethFromInput.Hex()
180+
} else if ethHex := ctx.Eth[nativeStr]; ethHex != "" {
181+
parenAddr = ethHex
182+
} else {
183+
parenAddr = nativeStr
184+
}
185+
186+
if name == parenAddr {
187+
return parenAddr
188+
}
189+
190+
return fmt.Sprintf("%s (%s)", name, parenAddr)
191+
}
192+
193+
// PrettyAddress is like PrettyAddressWith but builds a fresh context on each call.
194+
func PrettyAddress(addr string) string {
195+
return PrettyAddressWith(GenAddressFormatContext(), addr)
196+
}

cmd/common/helpers_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package common
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers"
9+
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/types"
10+
)
11+
12+
func TestPrettyAddressWith(t *testing.T) {
13+
require := require.New(t)
14+
15+
nativeAddr, ethAddr, err := helpers.ResolveEthOrOasisAddress("0x60a6321eA71d37102Dbf923AAe2E08d005C4e403")
16+
require.NoError(err)
17+
require.NotNil(nativeAddr)
18+
require.NotNil(ethAddr)
19+
20+
t.Run("eth preferred when known", func(t *testing.T) {
21+
ctx := AddressFormatContext{
22+
Names: types.AccountNames{
23+
nativeAddr.String(): "my",
24+
},
25+
Eth: map[string]string{
26+
nativeAddr.String(): ethAddr.Hex(),
27+
},
28+
}
29+
30+
require.Equal("my ("+ethAddr.Hex()+")", PrettyAddressWith(ctx, nativeAddr.String()))
31+
require.Equal("my ("+ethAddr.Hex()+")", PrettyAddressWith(ctx, ethAddr.Hex()))
32+
})
33+
34+
t.Run("native fallback when eth unknown", func(t *testing.T) {
35+
ctx := AddressFormatContext{
36+
Names: types.AccountNames{
37+
nativeAddr.String(): "my",
38+
},
39+
Eth: map[string]string{},
40+
}
41+
42+
require.Equal("my ("+nativeAddr.String()+")", PrettyAddressWith(ctx, nativeAddr.String()))
43+
// If the user explicitly provided an Ethereum address, prefer it even if not in ctx.Eth.
44+
require.Equal("my ("+ethAddr.Hex()+")", PrettyAddressWith(ctx, ethAddr.Hex()))
45+
})
46+
47+
t.Run("unknown returns unchanged", func(t *testing.T) {
48+
ctx := AddressFormatContext{
49+
Names: types.AccountNames{},
50+
Eth: map[string]string{},
51+
}
52+
53+
require.Equal(nativeAddr.String(), PrettyAddressWith(ctx, nativeAddr.String()))
54+
require.Equal(ethAddr.Hex(), PrettyAddressWith(ctx, ethAddr.Hex()))
55+
})
56+
57+
t.Run("unparseable returns unchanged", func(t *testing.T) {
58+
ctx := AddressFormatContext{
59+
Names: types.AccountNames{
60+
nativeAddr.String(): "my",
61+
},
62+
Eth: map[string]string{
63+
nativeAddr.String(): ethAddr.Hex(),
64+
},
65+
}
66+
67+
require.Equal("not-an-address", PrettyAddressWith(ctx, "not-an-address"))
68+
})
69+
}

cmd/common/json.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ func JSONMarshalUniversalValue(v interface{}) []byte {
152152
// For types implementing consensusPretty.PrettyPrinter, it uses the custom pretty printer.
153153
// For other types, it does basic JSON indentation and cleanup of common delimiters.
154154
func PrettyPrint(npa *NPASelection, prefix string, blob interface{}) string {
155+
return PrettyPrintWithTxDetails(npa, prefix, blob, nil)
156+
}
157+
158+
// PrettyPrintWithTxDetails is like PrettyPrint but passes txDetails to the signature context.
159+
func PrettyPrintWithTxDetails(npa *NPASelection, prefix string, blob interface{}, txDetails *signature.TxDetails) string {
155160
ret := ""
156161
switch rtx := blob.(type) {
157162
case consensusPretty.PrettyPrinter:
@@ -164,6 +169,7 @@ func PrettyPrint(npa *NPASelection, prefix string, blob interface{}) string {
164169
RuntimeID: ns,
165170
ChainContext: npa.Network.ChainContext,
166171
Base: types.SignatureContextBase,
172+
TxDetails: txDetails,
167173
}
168174
ctx := context.Background()
169175
ctx = context.WithValue(ctx, consensusPretty.ContextKeyTokenSymbol, npa.Network.Denomination.Symbol)
@@ -172,7 +178,19 @@ func PrettyPrint(npa *NPASelection, prefix string, blob interface{}) string {
172178
ctx = context.WithValue(ctx, config.ContextKeyParaTimeCfg, npa.ParaTime)
173179
}
174180
ctx = context.WithValue(ctx, signature.ContextKeySigContext, &sigCtx)
175-
ctx = context.WithValue(ctx, types.ContextKeyAccountNames, GenAccountNames())
181+
182+
// Provide locally-known names and native->ETH mapping for address formatting.
183+
addrCtx := GenAddressFormatContext()
184+
185+
// Inject the original Ethereum "To" address into the eth map so that
186+
// FormatNamedAddressWith can prefer it over the native representation.
187+
if txDetails != nil && txDetails.OrigTo != nil {
188+
native := types.NewAddressFromEth(txDetails.OrigTo.Bytes()).String()
189+
addrCtx.Eth[native] = txDetails.OrigTo.Hex()
190+
}
191+
192+
ctx = context.WithValue(ctx, types.ContextKeyAccountNames, addrCtx.Names)
193+
ctx = context.WithValue(ctx, types.ContextKeyAccountEthMap, addrCtx.Eth)
176194

177195
// Set up chain context for signature verification during pretty-printing.
178196
coreSignature.UnsafeResetChainContext()

0 commit comments

Comments
 (0)