Skip to content

Commit

Permalink
feat(vtransfer): port some address-hooks.js functions to Go
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed Dec 8, 2024
1 parent c3904ac commit 5aa7df0
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 73 deletions.
89 changes: 62 additions & 27 deletions golang/cosmos/x/vtransfer/types/baseaddr.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package types

import (
"fmt"
"net/url"
"strings"

"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/types/bech32"

transfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types"
clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types"
Expand All @@ -18,48 +18,83 @@ type AddressRole string
const (
RoleSender AddressRole = "Sender"
RoleReceiver AddressRole = "Receiver"
)

func trimSlashPrefix(s string) string {
return strings.TrimPrefix(s, "/")
}
AddressHookHumanReadableSuffix = "-hook"
BaseAddressLengthBytes = 2
)

// ExtractBaseAddress extracts the base address from a parameterized address.
// It removes all subpath and query components from addr.
func ExtractBaseAddress(addr string) (string, error) {
parsed, err := url.Parse(addr)
baseAddr, _, err := SplitHookedAddress(addr)
if err != nil {
return "", err
}
return baseAddr, nil
}

// SplitHookedAddress splits a hooked address into its base address and hook data.
// For the JS implementation, look at address-hooks.js.
func SplitHookedAddress(addr string) (string, []byte, error) {
outerPrefix, bz, err := bech32.DecodeAndConvert(addr)
if err != nil {
return "", []byte{}, err
}

// Specify the fields and values we expect. Unspecified fields will only
// match if they are zero values in order to be robust against extensions to
// the url.URL struct.
//
// Remove leading slashes from the path fields so that only parsed relative
// paths match the expected test.
expected := url.URL{
Path: trimSlashPrefix(parsed.Path),
RawPath: trimSlashPrefix(parsed.RawPath),
RawQuery: parsed.RawQuery,
Fragment: parsed.Fragment,
RawFragment: parsed.RawFragment,
innerPrefix := strings.TrimSuffix(outerPrefix, AddressHookHumanReadableSuffix)
if len(outerPrefix) == len(innerPrefix) {
// Return an unhooked address.
return addr, []byte{}, nil
}

// Skip over parsing control flags.
ForceQuery: parsed.ForceQuery,
OmitHost: parsed.OmitHost,
if len(bz) < BaseAddressLengthBytes {
return "", []byte{}, fmt.Errorf("hooked address must have at least %d bytes", BaseAddressLengthBytes)
}

if *parsed != expected {
return "", fmt.Errorf("address must be relative path with optional query and fragment, got %s", addr)
b := 0
for i := BaseAddressLengthBytes - 1; i >= 0; i -= 1 {
by := bz[len(bz)-1-i]
b <<= 8
b |= int(by)
}

baseAddr, _, _ := strings.Cut(expected.Path, "/")
if baseAddr == "" {
return "", fmt.Errorf("base address cannot be empty")
if b > len(bz)-BaseAddressLengthBytes {
return "", []byte{}, fmt.Errorf("base address length 0x%x is longer than specimen length 0x%x", b, len(bz)-BaseAddressLengthBytes)
}

return baseAddr, nil
baseAddressBuf := bz[0:b]
baseAddress, err := bech32.ConvertAndEncode(innerPrefix, baseAddressBuf)
if err != nil {
return "", []byte{}, err
}

return baseAddress, bz[b:], nil
}

// JoinHookedAddress joins a base bech32 address with hook data to create a
// hooked bech32 address.
// For the JS implementation, look at address-hooks.js.
func JoinHookedAddress(baseAddr string, hookData []byte) (string, error) {
innerPrefix, bz, err := bech32.DecodeAndConvert(baseAddr)
if err != nil {
return "", err
}

outerPrefix := innerPrefix + AddressHookHumanReadableSuffix
if len(outerPrefix) == len(innerPrefix) {
// Return an unhooked address.
return baseAddr, nil
}

b := len(bz)
if b > 0xffff {
return "", fmt.Errorf("base address length 0x%x is longer than the maximum 0x%x", b, 1<<(8*BaseAddressLengthBytes-1)+1)
}

bz = append(bz, hookData...)
bz = append(bz, byte(b>>8), byte(b))

return bech32.ConvertAndEncode(outerPrefix, bz)
}

// extractBaseTransferData returns the base address from the transferData.Sender
Expand Down
79 changes: 33 additions & 46 deletions golang/cosmos/x/vtransfer/types/baseaddr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,12 @@ func TestExtractBaseAddress(t *testing.T) {
name string
addr string
}{
{"agoric address", "agoric1abcdefghiteaneas"},
{"cosmos address", "cosmos1abcdeffiharceuht"},
{"hex address", "0xabcdef198189818c93839ibia"},
}

prefixes := []struct {
prefix string
baseIsWrong bool
isErr bool
}{
{"", false, false},
{"/", false, true},
{"orch:/", false, true},
{"unexpected", true, false},
{"norch:/", false, true},
{"orch:", false, true},
{"norch:", false, true},
{"\x01", false, true},
{"agoric address", "agoric1qqp0e5ys"},
{"cosmos address", "cosmos1qqxuevtt"},
}

suffixes := []struct {
suffix string
hookStr string
baseIsWrong bool
isErr bool
}{
Expand All @@ -50,31 +34,29 @@ func TestExtractBaseAddress(t *testing.T) {
{"/sub/account", false, false},
{"?query=something&k=v&k2=v2", false, false},
{"?query=something&k=v&k2=v2#fragment", false, false},
{"unexpected", true, false},
{"\x01", false, true},
{"unexpected", false, false},
{"\x01", false, false},
}

for _, b := range bases {
b := b
for _, p := range prefixes {
p := p
for _, s := range suffixes {
s := s
t.Run(b.name+" "+p.prefix+" "+s.suffix, func(t *testing.T) {
addr := p.prefix + b.addr + s.suffix
addr, err := types.ExtractBaseAddress(addr)
if p.isErr || s.isErr {
require.Error(t, err)
for _, s := range suffixes {
s := s
t.Run(b.name+" "+s.hookStr, func(t *testing.T) {
addrHook, err := types.JoinHookedAddress(b.addr, []byte(s.hookStr))
require.NoError(t, err)
addr, err := types.ExtractBaseAddress(addrHook)
if s.isErr {
require.Error(t, err)
} else {
require.NoError(t, err)
if s.baseIsWrong {
require.NotEqual(t, b.addr, addr)
} else {
require.NoError(t, err)
if p.baseIsWrong || s.baseIsWrong {
require.NotEqual(t, b.addr, addr)
} else {
require.Equal(t, b.addr, addr)
}
require.Equal(t, b.addr, addr)
}
})
}
}
})
}
}
}
Expand All @@ -86,32 +68,37 @@ func TestExtractBaseAddressFromPacket(t *testing.T) {
channeltypes.RegisterInterfaces(ir)
clienttypes.RegisterInterfaces(ir)

cosmosHook, err := types.JoinHookedAddress("cosmos1qqxuevtt", []byte("?foo=bar&baz=bot#fragment"))
require.NoError(t, err)
agoricHook, err := types.JoinHookedAddress("agoric1qqp0e5ys", []byte("?bingo=again"))
require.NoError(t, err)

cases := []struct {
name string
addrs map[types.AddressRole]struct{ addr, baseAddr string }
}{
{"sender has params",
map[types.AddressRole]struct{ addr, baseAddr string }{
types.RoleSender: {"cosmos1abcdeffiharceuht?foo=bar&baz=bot#fragment", "cosmos1abcdeffiharceuht"},
types.RoleReceiver: {"agoric1abcdefghiteaneas", "agoric1abcdefghiteaneas"},
types.RoleSender: {cosmosHook, "cosmos1qqxuevtt"},
types.RoleReceiver: {"agoric1qqp0e5ys", "agoric1qqp0e5ys"},
},
},
{"receiver has params",
map[types.AddressRole]struct{ addr, baseAddr string }{
types.RoleSender: {"cosmos1abcdeffiharceuht", "cosmos1abcdeffiharceuht"},
types.RoleReceiver: {"agoric1abcdefghiteaneas?bingo=again", "agoric1abcdefghiteaneas"},
types.RoleSender: {"cosmos1qqxuevtt", "cosmos1qqxuevtt"},
types.RoleReceiver: {agoricHook, "agoric1qqp0e5ys"},
},
},
{"both are base",
map[types.AddressRole]struct{ addr, baseAddr string }{
types.RoleSender: {"cosmos1abcdeffiharceuht", "cosmos1abcdeffiharceuht"},
types.RoleReceiver: {"agoric1abcdefghiteaneas", "agoric1abcdefghiteaneas"},
types.RoleSender: {"cosmos1qqxuevtt", "cosmos1qqxuevtt"},
types.RoleReceiver: {"agoric1qqp0e5ys", "agoric1qqp0e5ys"},
},
},
{"both have params",
map[types.AddressRole]struct{ addr, baseAddr string }{
types.RoleSender: {"agoric1abcdefghiteaneas?bingo=again", "agoric1abcdefghiteaneas"},
types.RoleReceiver: {"cosmos1abcdeffiharceuht?foo=bar&baz=bot#fragment", "cosmos1abcdeffiharceuht"},
types.RoleSender: {agoricHook, "agoric1qqp0e5ys"},
types.RoleReceiver: {cosmosHook, "cosmos1qqxuevtt"},
},
},
}
Expand Down

0 comments on commit 5aa7df0

Please sign in to comment.