diff --git a/.changelog/6453.feature.md b/.changelog/6453.feature.md new file mode 100644 index 00000000000..387628534ff --- /dev/null +++ b/.changelog/6453.feature.md @@ -0,0 +1 @@ +go/staking/api: Format known addresses in staking transaction PrettyPrint diff --git a/go/staking/api/api.go b/go/staking/api/api.go index ee95c75fc28..daabf1d0731 100644 --- a/go/staking/api/api.go +++ b/go/staking/api/api.go @@ -426,7 +426,7 @@ type Transfer struct { // PrettyPrint writes a pretty-printed representation of Transfer to the given // writer. func (t Transfer) PrettyPrint(ctx context.Context, prefix string, w io.Writer) { - fmt.Fprintf(w, "%sTo: %s\n", prefix, t.To) + fmt.Fprintf(w, "%sTo: %s\n", prefix, FormatAddress(ctx, t.To)) fmt.Fprintf(w, "%sAmount: ", prefix) token.PrettyPrintAmount(ctx, t.Amount, w) @@ -477,7 +477,7 @@ type Escrow struct { // PrettyPrint writes a pretty-printed representation of Escrow to the given // writer. func (e Escrow) PrettyPrint(ctx context.Context, prefix string, w io.Writer) { - fmt.Fprintf(w, "%sTo: %s\n", prefix, e.Account) + fmt.Fprintf(w, "%sTo: %s\n", prefix, FormatAddress(ctx, e.Account)) fmt.Fprintf(w, "%sAmount: ", prefix) token.PrettyPrintAmount(ctx, e.Amount, w) @@ -503,8 +503,8 @@ type ReclaimEscrow struct { // PrettyPrint writes a pretty-printed representation of ReclaimEscrow to the // given writer. -func (re ReclaimEscrow) PrettyPrint(_ context.Context, prefix string, w io.Writer) { - fmt.Fprintf(w, "%sFrom: %s\n", prefix, re.Account) +func (re ReclaimEscrow) PrettyPrint(ctx context.Context, prefix string, w io.Writer) { + fmt.Fprintf(w, "%sFrom: %s\n", prefix, FormatAddress(ctx, re.Account)) fmt.Fprintf(w, "%sShares: %s\n", prefix, re.Shares) } @@ -552,7 +552,7 @@ type Allow struct { // PrettyPrint writes a pretty-printed representation of Allow to the given writer. func (aw Allow) PrettyPrint(ctx context.Context, prefix string, w io.Writer) { - fmt.Fprintf(w, "%sBeneficiary: %s\n", prefix, aw.Beneficiary) + fmt.Fprintf(w, "%sBeneficiary: %s\n", prefix, FormatAddress(ctx, aw.Beneficiary)) sign := "+" if aw.Negative { @@ -582,7 +582,7 @@ type Withdraw struct { // PrettyPrint writes a pretty-printed representation of Withdraw to the given writer. func (wt Withdraw) PrettyPrint(ctx context.Context, prefix string, w io.Writer) { - fmt.Fprintf(w, "%sFrom: %s\n", prefix, wt.From) + fmt.Fprintf(w, "%sFrom: %s\n", prefix, FormatAddress(ctx, wt.From)) fmt.Fprintf(w, "%sAmount: ", prefix) token.PrettyPrintAmount(ctx, wt.Amount, w) diff --git a/go/staking/api/prettyprint.go b/go/staking/api/prettyprint.go index 4cadbdbb8a5..8b989e3941c 100644 --- a/go/staking/api/prettyprint.go +++ b/go/staking/api/prettyprint.go @@ -9,6 +9,43 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/quantity" ) +type contextKey string + +// ContextKeyAccountNames is the key to retrieve native (Bech32) account names from context. +var ContextKeyAccountNames = contextKey("staking/account-names") + +// AccountNames maps native (Bech32) addresses to user-defined account names for pretty printing. +type AccountNames map[string]string + +// FormatAddress is like FormatAddressWith but reads names from ctx. +func FormatAddress(ctx context.Context, addr Address) string { + var names AccountNames + if v, ok := ctx.Value(ContextKeyAccountNames).(AccountNames); ok { + names = v + } + + return FormatAddressWith(names, addr) +} + +// FormatAddressWith formats a staking address for display. +// +// Output cases: +// - Named address: "name (oasis1...)" +// - Unknown address: "oasis1..." +func FormatAddressWith(names AccountNames, addr Address) string { + native := addr.String() + if names == nil { + return native + } + + name := names[native] + if name == "" { + return native + } + + return fmt.Sprintf("%s (%s)", name, native) +} + // PrettyPrintCommissionRatePercentage returns the string representing the // commission rate (bound) in percentage for the given commission rate (bound) // numerator. diff --git a/go/staking/api/prettyprint_test.go b/go/staking/api/prettyprint_test.go index 73e867b4bd8..e5302f38f68 100644 --- a/go/staking/api/prettyprint_test.go +++ b/go/staking/api/prettyprint_test.go @@ -1,6 +1,7 @@ package api import ( + "bytes" "context" "testing" @@ -10,6 +11,14 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/quantity" ) +func mustAddress(t *testing.T, raw string) Address { + t.Helper() + + var addr Address + require.NoError(t, addr.UnmarshalText([]byte(raw))) + return addr +} + func TestPrettyPrintCommissionRatePercentage(t *testing.T) { require := require.New(t) @@ -54,3 +63,119 @@ func TestPrettyPrintCommissionScheduleIndexInfixes(t *testing.T) { require.Equal(t.expectedEmptyInfix, emptyInfix, "obtained empty infix didn't match expected value") } } + +func TestFormatAddressWith(t *testing.T) { + require := require.New(t) + + addr := mustAddress(t, "oasis1qrydpazemvuwtnp3efm7vmfvg3tde044qg6cxwzx") + native := addr.String() + + for _, tc := range []struct { + name string + names AccountNames + expected string + }{ + { + name: "nil names", + names: nil, + expected: native, + }, + { + name: "empty names", + names: AccountNames{}, + expected: native, + }, + { + name: "unknown name", + names: AccountNames{"oasis1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpkfh7w": "ignored"}, + expected: native, + }, + { + name: "named address", + names: AccountNames{native: "test:bob"}, + expected: "test:bob (" + native + ")", + }, + } { + t.Run(tc.name, func(t *testing.T) { + require.Equal(tc.expected, FormatAddressWith(tc.names, addr)) + }) + } +} + +func TestFormatAddress(t *testing.T) { + require := require.New(t) + + addr := mustAddress(t, "oasis1qrydpazemvuwtnp3efm7vmfvg3tde044qg6cxwzx") + native := addr.String() + + t.Run("without context names", func(t *testing.T) { + require.Equal(native, FormatAddress(context.Background(), addr)) + }) + + t.Run("with context names", func(t *testing.T) { + ctx := context.WithValue(context.Background(), ContextKeyAccountNames, AccountNames{ + native: "test:bob", + }) + require.Equal("test:bob ("+native+")", FormatAddress(ctx, addr)) + }) +} + +func TestStakingTxPrettyPrintUsesNamedAddresses(t *testing.T) { + require := require.New(t) + + addr := mustAddress(t, "oasis1qrydpazemvuwtnp3efm7vmfvg3tde044qg6cxwzx") + native := addr.String() + amt := *quantity.NewFromUint64(1) + + ctx := context.WithValue(context.Background(), ContextKeyAccountNames, AccountNames{ + native: "test:bob", + }) + + for _, tc := range []struct { + name string + pretty func(context.Context, *bytes.Buffer) + expected string + }{ + { + name: "transfer to", + pretty: func(ctx context.Context, buf *bytes.Buffer) { + Transfer{To: addr, Amount: amt}.PrettyPrint(ctx, "", buf) + }, + expected: "To: test:bob (" + native + ")", + }, + { + name: "escrow account", + pretty: func(ctx context.Context, buf *bytes.Buffer) { + Escrow{Account: addr, Amount: amt}.PrettyPrint(ctx, "", buf) + }, + expected: "To: test:bob (" + native + ")", + }, + { + name: "reclaim escrow from", + pretty: func(ctx context.Context, buf *bytes.Buffer) { + ReclaimEscrow{Account: addr, Shares: amt}.PrettyPrint(ctx, "", buf) + }, + expected: "From: test:bob (" + native + ")", + }, + { + name: "allow beneficiary", + pretty: func(ctx context.Context, buf *bytes.Buffer) { + Allow{Beneficiary: addr, AmountChange: amt}.PrettyPrint(ctx, "", buf) + }, + expected: "Beneficiary: test:bob (" + native + ")", + }, + { + name: "withdraw from", + pretty: func(ctx context.Context, buf *bytes.Buffer) { + Withdraw{From: addr, Amount: amt}.PrettyPrint(ctx, "", buf) + }, + expected: "From: test:bob (" + native + ")", + }, + } { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + tc.pretty(ctx, &buf) + require.Contains(buf.String(), tc.expected) + }) + } +}