Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sdk: add Atoi function for ChainID (parsing ChainID from string representations of integers) #4239

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions sdk/vaa/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"io"
"math/big"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -262,6 +263,33 @@ func (c ChainID) String() string {
}
}

// Atoi converts from a string representation of an integer into a valid ChainID.
func Atoi(s string) (chainId ChainID, err error) {
u16, err := strconv.ParseUint(s, 10, 16)
if err != nil {
return
}

fmt.Printf("Parsed %s to %d\n", s, u16)
// TODO: using slice.Contains would be nicer here, but it requires that users of the SDK are on Go version >= 1.21
// This is not the case for e.g. Wormchain, so this function can't be used here until SDK users upgrade their Go versions
found := false
// NOTE: This function does not contain ChainIDUnset, therefore this function returns an error if the argument is 0.
for _, id := range GetAllNetworkIDs() {
if ChainID(u16) == ChainID(id) {
found = true
break
}
}
if !found {
err = fmt.Errorf("value %s is not a valid chainId", s)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth doing a special check for id == 0 and returning ChainIDUnset?

Copy link
Contributor Author

@johnsaigle johnsaigle Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about your thoughts on that actually. I originally had the function set up to do just that, but then since I saw that GetAllNetworkIDs() seemed to specifically omit ChainIDUnset, I figured I'd follow what the existing code was doing. Maybe it's not a problem if they have different behaviours though?

Copy link
Contributor

@evan-gray evan-gray Jan 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends on the use case. chain ID 0 / unset is used for governance. so if you're using this to parse something that comes from the payload of real VAAs, then it's possible (though I'm not sure why you would have that as a string). If this is more for splitting a VAA ID or something, there should never be a VAA emitted with chain ID 0 as far as I am aware, which is similarly why 0 is not a "Network ID" for GetAllNetworkIDs. I guess I'm not sure of the use case that led to wishing for this 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up - what is the case where you have a chain ID (for some reason) but should be told it is invalid? Any uint16 is technically valid as a chain ID, so anything that passes the first line shouldn't necessarily trigger an error. Otherwise the caller would need to update their code any time there is a new chain added, which quickly becomes a nightmare. Maybe that's why I would lean on the side of continuing to not have this function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @evan-gray thanks for the review and for the questions. Essentially my motivation here is to get more use out of the ChainID type and wanted a way to go from a string to a valid, registered ChainID within Wormhole. Based on your questions I'm realizing that I was thinking of this code as a "wormhole SDK" rather than a "vaa SDK" and that's perhaps stretching the purpose of the code.

I think you're right that any uint16 is a valid ChainID in a sense (even if it doesn't correspond to any actual instantiated chain); for that reason Atoi is not a good name here because that sort of generic conversion should accept any uint16, I feel.

Would you be open to including this function if renamed/redocumented to something like "ValidChainIDFromString"? I would personally find this function useful but if in your view this is outside of the scope of this file then I'll defer to your judgement and close the PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My primary hesitation is that it’s a good way to cause a bug where the written code cannot withstand new chain deployments without being upgraded. This is a consistent issue with other tooling. It’s less of an issue in the guardian code (which dictates new chains, at least today) than it is for code elsewhere. Semantically, a chain number is no less “valid” just because a given version of the code doesn’t know what named chain it corresponds to yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps “known” would be a better adjective.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about KnownNetworkIDFromString()? That way we surface that it's using the NetworkID list under the hood and it becomes more clear that 0/ChainIDUnset is not a valid use of the function. In this way also it should have the same maintenance requirements/constraints that GetAllNetworkIDs has.

return
}
chainId = ChainID(u16)
return
}

// ChainIDFromString converts from a chain's full name (e.g. "solana") to its corresponding ChainID.
func ChainIDFromString(s string) (ChainID, error) {
s = strings.ToLower(s)

Expand Down
76 changes: 76 additions & 0 deletions sdk/vaa/structs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1111,3 +1111,79 @@ func TestUnmarshalBody(t *testing.T) {
})
}
}

func TestAtoi(t *testing.T) {

happy := []struct {
name string
input string
expected ChainID
}{
{
name: "simple int 1",
input: "1",
expected: ChainIDSolana,
},
{
name: "simple int 2",
input: "3104",
expected: ChainIDWormchain,
},
}
for _, tt := range happy {
// Avoid "loop variable capture".
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

actual, err := Atoi(tt.input)
require.Equal(t, tt.expected, actual)
require.NoError(t, err)
})
}

sad := []struct {
name string
input string
}{
{
name: "zero is not a valid ChainID",
input: "0",
},
{
name: "negative value",
input: "-1",
},
{
name: "NaN",
input: "garbage",
},
{
name: "overflow",
input: "65536",
},
{
name: "not a real chain",
input: "12345",
},
{
name: "empty string",
input: "",
},
{
name: "no hex inputs",
input: "0x10",
},
}
for _, tt := range sad {
// Avoid "loop variable capture".
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

actual, err := Atoi(tt.input)
require.Equal(t, ChainIDUnset, actual)
require.Error(t, err)
})
}
}
Loading