├── .gitattributes ├── .github └── workflows │ └── go.yml ├── .gitignore ├── Makefile ├── README.md ├── address ├── address.go └── address_test.go ├── asset ├── id.go ├── id_test.go ├── image.go └── image_test.go ├── coin ├── coins.go ├── coins.yml ├── coins_test.go ├── gen.go ├── gen_test.go ├── models.go └── models_test.go ├── go.mod ├── go.sum ├── numbers ├── amount.go ├── amount_test.go ├── decimal.go ├── decimal_test.go ├── number.go └── number_test.go ├── slice ├── batch.go └── batch_test.go └── types ├── asset.go ├── chain.go ├── chain_test.go ├── chainid.go ├── collectibles.go ├── marshal.go ├── marshal_test.go ├── subscription.go ├── subscription_test.go ├── token.go ├── token_test.go ├── tx.go ├── tx_test.go ├── types.go └── types_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.json linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.19 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | 29 | - name: Test 30 | run: go test -v ./... 31 | 32 | golangci: 33 | name: Lint 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v3 37 | - name: golangci-lint 38 | uses: golangci/golangci-lint-action@v6 39 | with: 40 | version: v1.63 41 | only-new-issues: true 42 | args: --timeout=5m 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | .idea/ 18 | 19 | bin/* 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOBASE := $(shell pwd) 2 | GOBIN := $(GOBASE)/bin 3 | 4 | ## generate-coins converts coin/coins.yml file into golang model and helper functions 5 | generate-coins: 6 | @echo " > Generating coin file" 7 | GOBIN=$(GOBIN) go run -tags=coins coin/gen.go 8 | goimports -w coin/coins.go 9 | 10 | ## test executes all tests 11 | test: 12 | go test -v ./... 13 | 14 | ## golint: Run linter. 15 | lint: go-lint-install go-lint 16 | 17 | go-lint-install: 18 | ifeq (,$(wildcard test -f bin/golangci-lint)) 19 | @echo " > Installing golint" 20 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- v1.63.4 21 | endif 22 | 23 | go-lint: 24 | @echo " > Running golint" 25 | bin/golangci-lint run --timeout=2m 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-primitives 2 | ___ 3 | go-primitives is Go library that contains blockchain types and functions to work with them. 4 | It also supposed to contain extension functions for basic Go types. 5 | -------------------------------------------------------------------------------- /address/address.go: -------------------------------------------------------------------------------- 1 | package address 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "strings" 7 | 8 | "golang.org/x/crypto/sha3" 9 | 10 | "github.com/trustwallet/go-primitives/coin" 11 | ) 12 | 13 | var ErrInvalidInput = errors.New("invalid input") 14 | 15 | // Decode decodes a hex string with 0x prefix. 16 | func Remove0x(input string) string { 17 | if strings.HasPrefix(input, "0x") { 18 | return input[2:] 19 | } 20 | return input 21 | } 22 | 23 | // Hex returns an EIP55-compliant hex string representation of the address. 24 | func EIP55Checksum(unchecksummed string) (string, error) { 25 | v := []byte(Remove0x(strings.ToLower(unchecksummed))) 26 | 27 | _, err := hex.DecodeString(string(v)) 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | sha := sha3.NewLegacyKeccak256() 33 | _, err = sha.Write(v) 34 | if err != nil { 35 | return "", err 36 | } 37 | hash := sha.Sum(nil) 38 | 39 | result := v 40 | if (len(result)-1)/2 >= len(hash) { 41 | return "", ErrInvalidInput 42 | } 43 | 44 | for i := 0; i < len(result); i++ { 45 | hashByte := hash[i/2] 46 | if i%2 == 0 { 47 | hashByte = hashByte >> 4 48 | } else { 49 | hashByte &= 0xf 50 | } 51 | if result[i] > '9' && hashByte > 7 { 52 | result[i] -= 32 53 | } 54 | } 55 | val := string(result) 56 | return "0x" + val, nil 57 | } 58 | 59 | func ToEIP55ByCoinID(str string, coinID uint) (eip55Addr string, err error) { 60 | if !coin.IsEVM(coinID) { 61 | return str, nil 62 | } 63 | 64 | // special case for ronin addresses 65 | const roninPrefix, hexPrefix = "ronin:", "0x" 66 | if coinID == coin.RONIN && strings.HasPrefix(str, roninPrefix) { 67 | str = hexPrefix + str[len(roninPrefix):] 68 | defer func() { 69 | // remove 0x prefix, then add roninPrefix 70 | eip55Addr = roninPrefix + eip55Addr[len(hexPrefix):] 71 | }() 72 | } 73 | 74 | eip55Addr, err = EIP55Checksum(str) 75 | if err != nil { 76 | return "", err 77 | } 78 | 79 | return 80 | } 81 | -------------------------------------------------------------------------------- /address/address_test.go: -------------------------------------------------------------------------------- 1 | package address 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/trustwallet/go-primitives/coin" 9 | ) 10 | 11 | func TestEIP55Checksum(t *testing.T) { 12 | type args struct { 13 | unchecksummed string 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want string 19 | wantErr bool 20 | }{ 21 | {"test checksum number", args{"16345785d8a00000"}, "0x16345785D8A00000", false}, 22 | {"test checksum hex", args{"fffdefefed"}, "0xFfFDEfeFeD", false}, 23 | {"test checksum 3", args{"0x0000000000000000003731342d4f4e452d354639"}, "0x0000000000000000003731342d4f4E452d354639", false}, 24 | {"test checksum 4", args{"0000000000000000003731342d4f4e452d354639"}, "0x0000000000000000003731342d4f4E452d354639", false}, 25 | {"test checksum Ethereum address", args{"0x84a0d77c693adabe0ebc48f88b3ffff010577051"}, "0x84A0d77c693aDAbE0ebc48F88b3fFFF010577051", false}, 26 | {"test invalid address format", args{"https://bscscan.com/token/0x959229D94c9060552daea25AC17193bcA65D7884"}, "", true}, 27 | {"test large invalid address format", args{"91d3b8e6af67670fe4b54942bf893c0b594fdf271bb474bfdecd7a09848da48f81e0a87d616bb17df004eb0308bae26dabbfeabe9d1b49353d18bd49272c1ca8"}, "", true}, 28 | } 29 | for _, tt := range tests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | got, err := EIP55Checksum(tt.args.unchecksummed) 32 | if (err != nil) != tt.wantErr { 33 | t.Errorf("EIP55Checksum() error = %v, wantErr %v", err, tt.wantErr) 34 | return 35 | } 36 | if got != tt.want { 37 | t.Errorf("EIP55Checksum() got = %v, want %v", got, tt.want) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func TestRemove0x(t *testing.T) { 44 | tests := []struct { 45 | name string 46 | input string 47 | want string 48 | }{ 49 | {"remove 0x from addres", "0x158079ee67fce2f58472a96584a73c7ab9ac95c1", "158079ee67fce2f58472a96584a73c7ab9ac95c1"}, 50 | {"remove 0x from hash", "0x230798fe22abff459b004675bf827a4089326a296fa4165d0c2ad27688e03e0c", "230798fe22abff459b004675bf827a4089326a296fa4165d0c2ad27688e03e0c"}, 51 | {"remove 0x hex value", "0xfffdefefed", "fffdefefed"}, 52 | {"remove 0x hex number", "0x16345785d8a0000", "16345785d8a0000"}, 53 | {"remove hex without 0x", "trustwallet", "trustwallet"}, 54 | {"remove hex number without 0x", "16345785d8a0000", "16345785d8a0000"}, 55 | } 56 | for _, tt := range tests { 57 | t.Run(tt.name, func(t *testing.T) { 58 | if got := Remove0x(tt.input); got != tt.want { 59 | t.Errorf("Remove0x() = %v, want %v", got, tt.want) 60 | } 61 | }) 62 | } 63 | } 64 | 65 | func TestToEIP55ByCoinID(t *testing.T) { 66 | var ( 67 | addr1 = "0xea674fdde714fd979de3edf0f56aa9716b898ec8" 68 | addr1EIP55 = "0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8" 69 | wanAddrLowercase = "0xae96137e0e05681ed2f5d1af272c3ee512939d0f" 70 | wanAddrEIP55Checksum = "0xAe96137E0e05681eD2F5D1AF272C3ee512939D0F" 71 | wanAddrEIP55ChecksumWanchain = "0xAe96137E0e05681eD2F5D1AF272C3ee512939D0F" 72 | 73 | roninAddr = "ronin:ea674fdde714fd979de3edf0f56aa9716b898ec8" 74 | roninAddrEIP55 = "ronin:EA674fdDe714fd979de3EdF0F56AA9716B898ec8" 75 | 76 | tests = []struct { 77 | name, address, expectedAddress string 78 | coinID uint 79 | }{ 80 | {"Ethereum", addr1, addr1EIP55, coin.ETHEREUM}, 81 | {"Ethereum Classic", addr1, addr1EIP55, coin.CLASSIC}, 82 | {"POA", addr1, addr1EIP55, coin.POA}, 83 | {"Callisto", addr1, addr1EIP55, coin.CALLISTO}, 84 | {"Tomochain", addr1, addr1EIP55, coin.TOMOCHAIN}, 85 | {"Thunder", addr1, addr1EIP55, coin.THUNDERTOKEN}, 86 | {"Thunder", addr1, addr1EIP55, coin.THUNDERTOKEN}, 87 | {"GoChain", addr1, addr1EIP55, coin.GOCHAIN}, 88 | {"Wanchain 1", wanAddrLowercase, wanAddrEIP55ChecksumWanchain, coin.WANCHAIN}, 89 | {"Wanchain 2", wanAddrEIP55Checksum, wanAddrEIP55ChecksumWanchain, coin.WANCHAIN}, 90 | {"Non Ethereum like chain 1", "", "", coin.TRON}, 91 | {"Non Ethereum like chain 2", addr1, addr1, coin.BINANCE}, 92 | {"SmartChain", addr1, addr1EIP55, coin.SMARTCHAIN}, 93 | {"Ronin", roninAddr, roninAddrEIP55, coin.RONIN}, 94 | } 95 | ) 96 | 97 | t.Run("Test TestToEIP55ByCoinID", func(t *testing.T) { 98 | for _, tt := range tests { 99 | actual, _ := ToEIP55ByCoinID(tt.address, tt.coinID) 100 | assert.Equal(t, tt.expectedAddress, actual) 101 | } 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /asset/id.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | var ErrBadAssetID = errors.New("bad ID") 10 | 11 | type CoinType string 12 | 13 | const ( 14 | Coin CoinType = "coin" 15 | Token CoinType = "token" 16 | 17 | coinPrefix = 'c' 18 | tokenPrefix = 't' 19 | ) 20 | 21 | func ParseID(id string) (uint, string, error) { 22 | rawResult := strings.SplitN(id, "_", 2) 23 | resLen := len(rawResult) 24 | if resLen < 1 { 25 | return 0, "", ErrBadAssetID 26 | } 27 | 28 | coin, err := FindCoinID(rawResult) 29 | if err != nil { 30 | return 0, "", ErrBadAssetID 31 | } 32 | 33 | token := FindTokenID(rawResult) 34 | 35 | if token != "" { 36 | return coin, token, nil 37 | } 38 | 39 | return coin, "", nil 40 | } 41 | 42 | func BuildID(coin uint, token string) string { 43 | c := strconv.Itoa(int(coin)) 44 | if token != "" { 45 | return string(coinPrefix) + c + "_" + string(tokenPrefix) + token 46 | } 47 | return string(coinPrefix) + c 48 | } 49 | 50 | func FindCoinID(words []string) (uint, error) { 51 | for _, w := range words { 52 | if len(w) == 0 { 53 | return 0, errors.New("empty coin") 54 | } 55 | 56 | if w[0] == coinPrefix { 57 | rawCoin := removeFirstChar(w) 58 | coin, err := strconv.Atoi(rawCoin) 59 | if err != nil { 60 | return 0, errors.New("bad coin") 61 | } 62 | return uint(coin), nil 63 | } 64 | } 65 | return 0, errors.New("no coin") 66 | } 67 | 68 | func FindTokenID(words []string) string { 69 | for _, w := range words { 70 | if len(w) > 0 && w[0] == tokenPrefix { 71 | token := removeFirstChar(w) 72 | if len(token) > 0 { 73 | return token 74 | } 75 | return "" 76 | } 77 | } 78 | return "" 79 | } 80 | 81 | func removeFirstChar(input string) string { 82 | if len(input) <= 1 { 83 | return "" 84 | } 85 | return string([]rune(input)[1:]) 86 | } 87 | -------------------------------------------------------------------------------- /asset/id_test.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParseID(t *testing.T) { 11 | testStruct := []struct { 12 | name string 13 | givenID string 14 | wantedCoin uint 15 | wantedToken string 16 | wantedType CoinType 17 | wantedError error 18 | }{ 19 | { 20 | "c714_tTWT-8C2", 21 | "c714_tTWT-8C2", 22 | 714, 23 | "TWT-8C2", 24 | Token, 25 | nil, 26 | }, 27 | { 28 | "tTWT-8C2_c714", 29 | "tTWT-8C2_c714", 30 | 714, 31 | "TWT-8C2", 32 | Token, 33 | nil, 34 | }, 35 | { 36 | "c714", 37 | "c714", 38 | 714, 39 | "", 40 | Coin, 41 | nil, 42 | }, 43 | { 44 | "tTWT-8C2", 45 | "tTWT-8C2", 46 | 0, 47 | "", 48 | Coin, 49 | ErrBadAssetID, 50 | }, 51 | { 52 | "c714_TWT-8C2", 53 | "c714_TWT-8C2", 54 | 714, 55 | "", 56 | Coin, 57 | nil, 58 | }, 59 | { 60 | name: "c60_", 61 | givenID: "c60_", 62 | wantedCoin: 60, 63 | wantedToken: "", 64 | wantedType: Coin, 65 | wantedError: nil, 66 | }, 67 | { 68 | name: "c637_t0xe4ccb6d39136469f376242c31b34d10515c8eaaa38092f804db8e08a8f53c5b2::assets_v1::EchoCoin002", 69 | givenID: "c637_t0xe4ccb6d39136469f376242c31b34d10515c8eaaa38092f804db8e08a8f53c5b2::assets_v1::EchoCoin002", 70 | wantedCoin: 637, 71 | wantedToken: "0xe4ccb6d39136469f376242c31b34d10515c8eaaa38092f804db8e08a8f53c5b2::assets_v1::EchoCoin002", 72 | wantedType: Token, 73 | wantedError: nil, 74 | }, 75 | { 76 | name: "c637_t0xe4ccb6d39136469f376242c31b34d10515c8eaaa38092f804db8e08a8f53c5b2", 77 | givenID: "c637_t0xe4ccb6d39136469f376242c31b34d10515c8eaaa38092f804db8e08a8f53c5b2", 78 | wantedCoin: 637, 79 | wantedToken: "0xe4ccb6d39136469f376242c31b34d10515c8eaaa38092f804db8e08a8f53c5b2", 80 | wantedType: Token, 81 | wantedError: nil, 82 | }, 83 | } 84 | 85 | for _, tt := range testStruct { 86 | t.Run(tt.name, func(t *testing.T) { 87 | coin, token, err := ParseID(tt.givenID) 88 | assert.Equal(t, tt.wantedCoin, coin) 89 | assert.Equal(t, tt.wantedToken, token) 90 | assert.Equal(t, tt.wantedError, err) 91 | }) 92 | } 93 | } 94 | 95 | func TestBuildID(t *testing.T) { 96 | testStruct := []struct { 97 | wantedID string 98 | givenCoin uint 99 | givenToken string 100 | }{ 101 | {"c714_tTWT-8C2", 102 | 714, 103 | "TWT-8C2", 104 | }, 105 | {"c60", 106 | 60, 107 | "", 108 | }, 109 | {"c0", 110 | 0, 111 | "", 112 | }, 113 | {"c0_t:fnfjunwpiucU#*0! 02", 114 | 0, 115 | ":fnfjunwpiucU#*0! 02", 116 | }, 117 | } 118 | 119 | for _, tt := range testStruct { 120 | id := BuildID(tt.givenCoin, tt.givenToken) 121 | assert.Equal(t, tt.wantedID, id) 122 | } 123 | } 124 | 125 | func Test_removeFirstChar(t *testing.T) { 126 | tests := []struct { 127 | name string 128 | input string 129 | expected string 130 | }{ 131 | {"Normal case", "Bob", "ob"}, 132 | {"Empty String", "", ""}, 133 | {"One Char String Test", "A", ""}, 134 | {"Another normaal", "abcdef", "bcdef"}, 135 | } 136 | 137 | for _, tt := range tests { 138 | var got = removeFirstChar(tt.input) 139 | if got != tt.expected { 140 | t.Fatalf("Got %v, Expected %v.", got, tt.expected) 141 | } 142 | } 143 | } 144 | 145 | func Test_findCoinID(t *testing.T) { 146 | tests := []struct { 147 | name string 148 | words []string 149 | expected uint 150 | expectedErr error 151 | }{ 152 | {"Normal case", []string{"c100", "t60", "e30"}, 100, nil}, 153 | {"Empty coin", []string{"d100", "t60", "e30"}, 0, errors.New("no coin")}, 154 | {"Empty words", []string{}, 0, errors.New("no coin")}, 155 | {"Bad coin", []string{"cd100", "t60", "e30"}, 0, errors.New("bad coin")}, 156 | {"Bad coin #2", []string{"c", "t60", "e30"}, 0, errors.New("bad coin")}, 157 | } 158 | 159 | for _, tt := range tests { 160 | got, err := FindCoinID(tt.words) 161 | assert.Equal(t, tt.expected, got) 162 | assert.Equal(t, tt.expectedErr, err) 163 | } 164 | } 165 | 166 | func Test_findTokenID(t *testing.T) { 167 | tests := []struct { 168 | name string 169 | words []string 170 | expected string 171 | }{ 172 | {"Normal case", []string{"c100", "t60", "e30"}, "60"}, 173 | {"Empty token", []string{"d100", "a", "e30"}, ""}, 174 | {"Empty words", []string{}, ""}, 175 | {"Bad token", []string{"cd100", "t", "e30"}, ""}, 176 | } 177 | 178 | for _, tt := range tests { 179 | got := FindTokenID(tt.words) 180 | assert.Equal(t, tt.expected, got) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /asset/image.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/trustwallet/go-primitives/coin" 7 | ) 8 | 9 | func GetImageURL(endpoint, asset string) string { 10 | coinId, tokenId, err := ParseID(asset) 11 | if err != nil { 12 | return "" 13 | } 14 | if c, ok := coin.Coins[coinId]; ok { 15 | if len(tokenId) > 0 { 16 | return fmt.Sprintf("%s/blockchains/%s/assets/%s/logo.png", endpoint, c.Handle, tokenId) 17 | } 18 | return fmt.Sprintf("%s/blockchains/%s/info/logo.png", endpoint, c.Handle) 19 | } 20 | return "" 21 | } 22 | -------------------------------------------------------------------------------- /asset/image_test.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import "testing" 4 | 5 | func TestGetImageURL(t *testing.T) { 6 | type args struct { 7 | endpoint string 8 | asset string 9 | } 10 | tests := []struct { 11 | name string 12 | args args 13 | want string 14 | }{ 15 | { 16 | "Test coin", 17 | args{ 18 | endpoint: "https://assets.com", 19 | asset: "c60", 20 | }, 21 | "https://assets.com/blockchains/ethereum/info/logo.png", 22 | }, 23 | { 24 | "Test coin", 25 | args{ 26 | endpoint: "https://assets.com", 27 | asset: "c60_t123", 28 | }, 29 | "https://assets.com/blockchains/ethereum/assets/123/logo.png", 30 | }, 31 | { 32 | "Test invalid coin", 33 | args{ 34 | endpoint: "https://assets.com", 35 | asset: "c123", 36 | }, 37 | "", 38 | }, 39 | } 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | if got := GetImageURL(tt.args.endpoint, tt.args.asset); got != tt.want { 43 | t.Errorf("GetImageURL() = %v, want %v", got, tt.want) 44 | } 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /coin/coins.yml: -------------------------------------------------------------------------------- 1 | - id: 60 2 | symbol: ETH 3 | handle: ethereum 4 | name: Ethereum 5 | decimals: 18 6 | blockTime: 10000 7 | blockchain: Ethereum 8 | minConfirmations: 12 9 | 10 | 11 | - id: 61 12 | symbol: ETC 13 | handle: classic 14 | name: Ethereum Classic 15 | decimals: 18 16 | blockTime: 30000 17 | blockchain: Ethereum 18 | minConfirmations: 12 19 | 20 | 21 | - id: 74 22 | symbol: ICX 23 | handle: icon 24 | name: ICON 25 | decimals: 18 26 | blockTime: 10000 27 | blockchain: Icon 28 | 29 | 30 | - id: 118 31 | symbol: ATOM 32 | handle: cosmos 33 | name: Cosmos 34 | decimals: 6 35 | blockTime: 5000 36 | blockchain: Cosmos 37 | minConfirmations: 7 38 | 39 | 40 | - id: 144 41 | symbol: XRP 42 | handle: ripple 43 | name: Ripple 44 | decimals: 6 45 | blockTime: 5000 46 | blockchain: Ripple 47 | 48 | 49 | - id: 148 50 | symbol: XLM 51 | handle: stellar 52 | name: Stellar 53 | decimals: 7 54 | blockTime: 5000 55 | blockchain: Stellar 56 | 57 | 58 | - id: 178 59 | symbol: POA 60 | handle: poa 61 | name: Poa 62 | decimals: 18 63 | blockTime: 5000 64 | blockchain: Ethereum 65 | minConfirmations: 12 66 | 67 | 68 | - id: 195 69 | symbol: TRX 70 | handle: tron 71 | name: Tron 72 | decimals: 6 73 | blockTime: 10000 74 | blockchain: Tron 75 | 76 | 77 | - id: 235 78 | symbol: FIO 79 | handle: fio 80 | name: FIO 81 | decimals: 9 82 | blockTime: 5000 83 | blockchain: FIO 84 | 85 | 86 | - id: 242 87 | symbol: NIM 88 | handle: nimiq 89 | name: Nimiq 90 | decimals: 5 91 | blockTime: 60000 92 | blockchain: Nimiq 93 | 94 | 95 | - id: 304 96 | symbol: IOTX 97 | handle: iotex 98 | name: IoTeX 99 | decimals: 18 100 | blockTime: 10000 101 | blockchain: IoTeX 102 | 103 | 104 | - id: 10004689 105 | symbol: IOTX 106 | handle: iotexevm 107 | name: IoTeX Network Mainnet 108 | decimals: 18 109 | blockTime: 10000 110 | blockchain: Ethereum 111 | minConfirmations: 12 112 | 113 | 114 | - id: 313 115 | symbol: ZIL 116 | handle: zilliqa 117 | name: Zilliqa 118 | decimals: 12 119 | blockTime: 30000 120 | minConfirmations: 1 121 | blockchain: Zilliqa 122 | 123 | 124 | - id: 425 125 | symbol: AION 126 | handle: aion 127 | name: Aion 128 | decimals: 18 129 | blockTime: 10000 130 | blockchain: Aion 131 | 132 | 133 | - id: 457 134 | symbol: AE 135 | handle: aeternity 136 | name: Aeternity 137 | decimals: 18 138 | blockTime: 6000 139 | blockchain: Aeternity 140 | 141 | 142 | - id: 459 143 | symbol: KAVA 144 | handle: kava 145 | name: Kava 146 | decimals: 6 147 | blockTime: 5000 148 | blockchain: Cosmos 149 | minConfirmations: 7 150 | 151 | 152 | - id: 500 153 | symbol: THETA 154 | handle: theta 155 | name: Theta 156 | decimals: 18 157 | blockchain: Theta 158 | 159 | 160 | - id: 714 161 | symbol: BNB 162 | handle: binance 163 | name: BNB 164 | decimals: 8 165 | blockTime: 1000 166 | minConfirmations: 2 167 | blockchain: Binance 168 | 169 | 170 | - id: 818 171 | symbol: VET 172 | handle: vechain 173 | name: VeChain Token 174 | decimals: 18 175 | blockTime: 20000 176 | blockchain: Vechain 177 | 178 | 179 | - id: 820 180 | symbol: CLO 181 | handle: callisto 182 | name: Callisto 183 | decimals: 18 184 | blockTime: 10000 185 | blockchain: Ethereum 186 | minConfirmations: 12 187 | 188 | 189 | - id: 889 190 | symbol: TOMO 191 | handle: tomochain 192 | name: TOMO 193 | blockTime: 4000 194 | decimals: 18 195 | blockchain: Ethereum 196 | minConfirmations: 12 197 | 198 | 199 | - id: 1001 200 | symbol: TT 201 | handle: thundertoken 202 | name: ThunderCore 203 | decimals: 18 204 | blockTime: 10000 205 | blockchain: Ethereum 206 | minConfirmations: 36 207 | 208 | 209 | - id: 1024 210 | symbol: ONT 211 | handle: ontology 212 | name: Ontology 213 | decimals: 0 214 | blockTime: 10000 215 | blockchain: Ontology 216 | 217 | 218 | - id: 1729 219 | symbol: XTZ 220 | handle: tezos 221 | name: Tezos 222 | decimals: 6 223 | blockTime: 20000 224 | blockchain: Tezos 225 | 226 | 227 | - id: 2017 228 | symbol: KIN 229 | handle: kin 230 | name: Kin 231 | decimals: 5 232 | blockTime: 5000 233 | blockchain: Stellar 234 | 235 | 236 | - id: 2718 237 | symbol: NAS 238 | handle: nebulas 239 | name: Nebulas 240 | decimals: 18 241 | blockTime: 30000 242 | blockchain: Nebulas 243 | 244 | 245 | - id: 6060 246 | symbol: GO 247 | handle: gochain 248 | name: GoChain GO 249 | decimals: 18 250 | blockTime: 20000 251 | blockchain: Ethereum 252 | minConfirmations: 12 253 | 254 | 255 | - id: 5718350 256 | symbol: WAN 257 | handle: wanchain 258 | name: Wanchain 259 | decimals: 18 260 | blockTime: 30000 261 | blockchain: Ethereum 262 | minConfirmations: 12 263 | 264 | 265 | - id: 5741564 266 | symbol: WAVES 267 | handle: waves 268 | name: WAVES 269 | decimals: 8 270 | blockTime: 30000 271 | minConfirmations: 1 272 | blockchain: Waves 273 | 274 | 275 | - id: 0 276 | symbol: BTC 277 | handle: bitcoin 278 | name: Bitcoin 279 | decimals: 8 280 | blockTime: 600000 281 | blockchain: Bitcoin 282 | 283 | 284 | - id: 2 285 | symbol: LTC 286 | handle: litecoin 287 | name: Litecoin 288 | decimals: 8 289 | blockTime: 150000 290 | blockchain: Bitcoin 291 | 292 | 293 | - id: 3 294 | symbol: DOGE 295 | handle: doge 296 | name: Dogecoin 297 | decimals: 8 298 | blockTime: 60000 299 | blockchain: Bitcoin 300 | 301 | 302 | - id: 5 303 | symbol: DASH 304 | handle: dash 305 | name: Dash 306 | decimals: 8 307 | blockTime: 180000 308 | blockchain: Bitcoin 309 | 310 | 311 | - id: 14 312 | symbol: VIA 313 | handle: viacoin 314 | name: Viacoin 315 | decimals: 8 316 | blockTime: 15000 317 | blockchain: Bitcoin 318 | 319 | 320 | - id: 17 321 | symbol: GRS 322 | handle: groestlcoin 323 | name: Groestlcoin 324 | decimals: 8 325 | blockTime: 60000 326 | blockchain: Groestlcoin 327 | 328 | 329 | - id: 133 330 | symbol: ZEC 331 | handle: zcash 332 | name: Zcash 333 | decimals: 8 334 | blockTime: 150000 335 | blockchain: Zcash 336 | 337 | 338 | - id: 136 339 | symbol: FIRO 340 | handle: firo 341 | name: Firo 342 | decimals: 8 343 | blockTime: 300000 344 | blockchain: Bitcoin 345 | 346 | 347 | - id: 145 348 | symbol: BCH 349 | handle: bitcoincash 350 | name: Bitcoin Cash 351 | decimals: 8 352 | blockTime: 600000 353 | blockchain: Bitcoin 354 | 355 | 356 | - id: 175 357 | symbol: RVN 358 | handle: ravencoin 359 | name: Raven 360 | decimals: 8 361 | blockTime: 60000 362 | blockchain: Bitcoin 363 | 364 | 365 | - id: 2301 366 | symbol: QTUM 367 | handle: qtum 368 | name: Qtum 369 | decimals: 8 370 | blockTime: 60000 371 | blockchain: Bitcoin 372 | 373 | 374 | - id: 19167 375 | symbol: ZEL 376 | handle: zelcash 377 | name: Zelcash 378 | decimals: 8 379 | blockTime: 120000 380 | blockchain: Zcash 381 | 382 | 383 | - id: 42 384 | symbol: DCR 385 | handle: decred 386 | name: Decred 387 | decimals: 8 388 | blockTime: 300000 389 | blockchain: Decred 390 | 391 | 392 | - id: 283 393 | symbol: ALGO 394 | handle: algorand 395 | name: Algorand 396 | decimals: 6 397 | blockTime: 20000 398 | blockchain: Algorand 399 | 400 | 401 | - id: 165 402 | symbol: XNO 403 | handle: nano 404 | name: Nano 405 | decimals: 30 406 | blockchain: Nano 407 | 408 | 409 | - id: 20 410 | symbol: DGB 411 | handle: digibyte 412 | name: DigiByte 413 | decimals: 8 414 | blockTime: 15000 415 | blockchain: Bitcoin 416 | 417 | 418 | - id: 1023 419 | symbol: ONE 420 | handle: harmony 421 | name: Harmony 422 | decimals: 18 423 | blockTime: 5000 424 | blockchain: Harmony 425 | 426 | 427 | - id: 434 428 | symbol: KSM 429 | handle: kusama 430 | name: Kusama 431 | decimals: 12 432 | blockTime: 6000 433 | blockchain: Kusama 434 | 435 | 436 | - id: 354 437 | symbol: DOT 438 | handle: polkadot 439 | name: Polkadot 440 | decimals: 10 441 | blockTime: 6000 442 | blockchain: Polkadot 443 | 444 | 445 | - id: 501 446 | symbol: SOL 447 | handle: solana 448 | name: Solana 449 | decimals: 9 450 | blockTime: 500 451 | blockchain: Solana 452 | 453 | 454 | - id: 397 455 | symbol: NEAR 456 | handle: near 457 | name: NEAR 458 | decimals: 24 459 | blockTime: 2000 460 | blockchain: NEAR 461 | 462 | 463 | - id: 508 464 | symbol: eGLD 465 | handle: elrond 466 | name: Elrond 467 | decimals: 18 468 | blockTime: 6000 469 | blockchain: ElrondNetwork 470 | 471 | 472 | - id: 20000714 473 | symbol: BNB 474 | handle: smartchain 475 | name: 'Smart Chain' 476 | decimals: 18 477 | blockTime: 3000 478 | blockchain: Ethereum 479 | minConfirmations: 12 480 | 481 | 482 | - id: 461 483 | symbol: FIL 484 | handle: filecoin 485 | name: Filecoin 486 | decimals: 18 487 | blockTime: 3000 488 | blockchain: Filecoin 489 | 490 | 491 | - id: 474 492 | symbol: ROSE 493 | handle: oasis 494 | name: Oasis 495 | decimals: 9 496 | blockTime: 6000 497 | blockchain: OasisNetwork 498 | 499 | 500 | - id: 22 501 | symbol: MONA 502 | handle: monacoin 503 | name: Monacoin 504 | decimals: 8 505 | blockTime: 90000 506 | blockchain: Bitcoin 507 | 508 | 509 | - id: 156 510 | symbol: BTG 511 | handle: bitcoingold 512 | name: Bitcoin Gold 513 | decimals: 8 514 | blockTime: 600000 515 | blockchain: Bitcoin 516 | 517 | 518 | - id: 194 519 | symbol: EOS 520 | handle: eos 521 | name: EOS 522 | decimals: 4 523 | blockTime: 500 524 | blockchain: EOS 525 | 526 | 527 | - id: 330 528 | symbol: LUNC 529 | handle: terra 530 | name: Terra Classic 531 | decimals: 6 532 | blockchain: Cosmos 533 | minConfirmations: 7 534 | 535 | 536 | - id: 494 537 | symbol: BAND 538 | handle: band 539 | name: BandChain 540 | decimals: 6 541 | blockTime: 2000 542 | blockchain: Cosmos 543 | 544 | 545 | - id: 888 546 | symbol: NEO 547 | handle: neo 548 | name: NEO 549 | decimals: 8 550 | blockchain: NEO 551 | 552 | 553 | - id: 1815 554 | symbol: ADA 555 | handle: cardano 556 | name: Cardano 557 | decimals: 6 558 | blockchain: Cardano 559 | 560 | 561 | - id: 8964 562 | symbol: NULS 563 | handle: nuls 564 | name: NULS 565 | decimals: 8 566 | blockchain: NULS 567 | 568 | 569 | - id: 966 570 | symbol: POL 571 | handle: polygon 572 | name: POL (ex-MATIC) 573 | decimals: 18 574 | blockchain: Ethereum 575 | minConfirmations: 12 576 | 577 | 578 | - id: 931 579 | symbol: RUNE 580 | handle: thorchain 581 | name: THORChain 582 | decimals: 8 583 | blockchain: Thorchain 584 | 585 | 586 | - id: 10000070 587 | symbol: OETH 588 | handle: optimism 589 | name: Optimism Ethereum 590 | decimals: 18 591 | blockchain: Ethereum 592 | minConfirmations: 36 593 | 594 | 595 | - id: 10000100 596 | symbol: xDAI 597 | handle: xdai 598 | name: xDai 599 | decimals: 18 600 | blockchain: Ethereum 601 | minConfirmations: 12 602 | 603 | 604 | - id: 10009000 605 | symbol: AVAX 606 | handle: avalanchec 607 | name: Avalanche C-Chain 608 | decimals: 18 609 | blockchain: Ethereum 610 | minConfirmations: 36 611 | 612 | 613 | - id: 10000553 614 | symbol: HT 615 | handle: heco 616 | name: Huobi ECO Chain 617 | decimals: 18 618 | blockchain: Ethereum 619 | minConfirmations: 12 620 | deprecated: true 621 | 622 | 623 | - id: 10000250 624 | symbol: FTM 625 | handle: fantom 626 | name: Fantom 627 | decimals: 18 628 | blockchain: Ethereum 629 | minConfirmations: 12 630 | 631 | 632 | - id: 10042221 633 | symbol: ARETH 634 | handle: arbitrum 635 | name: Arbitrum 636 | decimals: 18 637 | blockchain: Ethereum 638 | minConfirmations: 36 639 | 640 | 641 | - id: 52752 642 | symbol: CELO 643 | handle: celo 644 | name: Celo 645 | decimals: 18 646 | blockchain: Ethereum 647 | minConfirmations: 12 648 | 649 | 650 | - id: 10002020 651 | symbol: RON 652 | handle: ronin 653 | name: Ronin 654 | decimals: 18 655 | blockchain: Ethereum 656 | minConfirmations: 12 657 | 658 | 659 | - id: 10000118 660 | symbol: OSMO 661 | handle: osmosis 662 | name: Osmosis 663 | decimals: 6 664 | blockchain: Cosmos 665 | minConfirmations: 7 666 | 667 | 668 | - id: 10000025 669 | symbol: CRO 670 | handle: cronos 671 | name: Cronos 672 | decimals: 18 673 | blockchain: Ethereum 674 | minConfirmations: 12 675 | 676 | 677 | - id: 10000321 678 | symbol: KCS 679 | handle: kcc 680 | name: KuCoin Community Chain 681 | decimals: 18 682 | blockchain: Ethereum 683 | minConfirmations: 12 684 | 685 | 686 | - id: 1323161554 687 | symbol: AURORAETH 688 | handle: aurora 689 | name: Aurora 690 | decimals: 18 691 | blockchain: Ethereum 692 | minConfirmations: 36 693 | 694 | 695 | - id: 10002222 696 | symbol: KAVA 697 | handle: kavaevm 698 | name: KavaEvm 699 | decimals: 18 700 | blockchain: Ethereum 701 | minConfirmations: 7 702 | 703 | 704 | - id: 18000 705 | symbol: MTR 706 | handle: meter 707 | name: Meter 708 | decimals: 18 709 | blockchain: Ethereum 710 | minConfirmations: 12 711 | 712 | 713 | - id: 10009001 714 | symbol: EVMOS 715 | handle: evmos 716 | name: Evmos 717 | decimals: 18 718 | blockchain: Ethereum 719 | minConfirmations: 12 720 | 721 | 722 | - id: 20009001 723 | symbol: EVMOS 724 | handle: nativeevmos 725 | name: NativeEvmos 726 | decimals: 18 727 | blockchain: Cosmos 728 | minConfirmations: 7 729 | 730 | 731 | - id: 996 732 | symbol: OKT 733 | handle: okc 734 | name: OKX Chain 735 | decimals: 18 736 | blockchain: Ethereum 737 | minConfirmations: 7 738 | 739 | 740 | - id: 394 741 | symbol: CRO 742 | handle: cryptoorg 743 | name: CryptoOrg 744 | decimals: 8 745 | blockchain: Cosmos 746 | minConfirmations: 7 747 | 748 | 749 | - id: 637 750 | symbol: APTOS 751 | handle: aptos 752 | name: Aptos 753 | decimals: 8 754 | blockchain: Aptos 755 | 756 | 757 | - id: 10001284 758 | symbol: GLMR 759 | handle: moonbeam 760 | name: Moonbeam 761 | decimals: 18 762 | blockchain: Ethereum 763 | minConfirmations: 7 764 | 765 | 766 | - id: 10008217 767 | symbol: KLAY 768 | handle: klaytn 769 | name: Kaia 770 | decimals: 18 771 | blockchain: Ethereum 772 | minConfirmations: 36 773 | 774 | 775 | - id: 10001088 776 | symbol: METIS 777 | handle: metis 778 | name: Metis 779 | decimals: 18 780 | blockchain: Ethereum 781 | minConfirmations: 36 782 | 783 | 784 | - id: 10001285 785 | symbol: MOVR 786 | handle: moonriver 787 | name: Moonriver 788 | decimals: 18 789 | blockchain: Ethereum 790 | minConfirmations: 2 791 | 792 | 793 | - id: 10000288 794 | symbol: BOBAETH 795 | handle: boba 796 | name: Boba 797 | decimals: 18 798 | blockchain: Ethereum 799 | minConfirmations: 1 800 | 801 | 802 | - id: 607 803 | symbol: TON 804 | handle: ton 805 | name: TON 806 | decimals: 9 807 | blockchain: The Open Network 808 | 809 | 810 | - id: 10001101 811 | symbol: ZKEVM 812 | handle: polygonzkevm 813 | name: Polygon zkEVM 814 | decimals: 18 815 | blockchain: Ethereum 816 | minConfirmations: 36 817 | 818 | 819 | - id: 10000324 820 | symbol: ZKSYNC 821 | handle: zksync 822 | name: Zksync 823 | decimals: 18 824 | blockchain: Ethereum 825 | minConfirmations: 36 826 | 827 | 828 | - id: 784 829 | symbol: SUI 830 | handle: sui 831 | name: Sui 832 | decimals: 9 833 | blockchain: Sui 834 | minConfirmations: 1 835 | 836 | 837 | - id: 40000118 838 | symbol: STRD 839 | handle: stride 840 | name: Stride 841 | decimals: 6 842 | blockchain: Cosmos 843 | minConfirmations: 7 844 | 845 | 846 | - id: 90000118 847 | symbol: NTRN 848 | handle: neutron 849 | name: Neutron 850 | decimals: 6 851 | blockchain: Cosmos 852 | minConfirmations: 10 853 | 854 | 855 | - id: 20000118 856 | symbol: STARS 857 | handle: stargaze 858 | name: Stargaze 859 | decimals: 6 860 | blockchain: Cosmos 861 | minConfirmations: 7 862 | 863 | 864 | - id: 10000060 865 | symbol: INJ 866 | handle: nativeinjective 867 | name: NativeInjective 868 | decimals: 18 869 | blockchain: Cosmos 870 | minConfirmations: 30 871 | 872 | 873 | - id: 1030 874 | symbol: CFX 875 | handle: cfxevm 876 | name: Conflux eSpace 877 | decimals: 18 878 | blockchain: Ethereum 879 | minConfirmations: 36 880 | 881 | 882 | - id: 787 883 | symbol: ACA 884 | handle: acala 885 | name: Acala 886 | decimals: 12 887 | blockchain: Polkadot 888 | 889 | 890 | - id: 10000787 891 | symbol: ACA 892 | handle: acalaevm 893 | name: Acala EVM 894 | decimals: 18 895 | blockchain: Ethereum 896 | minConfirmations: 2 897 | 898 | 899 | - id: 8453 900 | symbol: ETH 901 | handle: base 902 | name: Base 903 | decimals: 18 904 | blockchain: Ethereum 905 | minConfirmations: 12 906 | 907 | 908 | - id: 17000118 909 | symbol: AKT 910 | handle: akash 911 | name: Akash 912 | decimals: 6 913 | blockchain: Cosmos 914 | minConfirmations: 7 915 | 916 | 917 | - id: 564 918 | symbol: BLD 919 | handle: agoric 920 | name: Agoric 921 | decimals: 6 922 | blockchain: Cosmos 923 | minConfirmations: 7 924 | 925 | 926 | - id: 50000118 927 | symbol: AXL 928 | handle: axelar 929 | name: Axelar 930 | decimals: 6 931 | blockchain: Cosmos 932 | minConfirmations: 7 933 | 934 | 935 | - id: 30000118 936 | symbol: JUNO 937 | handle: juno 938 | name: Juno 939 | decimals: 6 940 | blockchain: Cosmos 941 | minConfirmations: 7 942 | 943 | 944 | - id: 19000118 945 | symbol: SEI 946 | handle: sei 947 | name: Sei 948 | decimals: 6 949 | blockchain: Cosmos 950 | 951 | 952 | - id: 245022934 953 | symbol: NEON 954 | handle: neon 955 | name: Neon 956 | decimals: 18 957 | blockchain: Ethereum 958 | minConfirmations: 1 959 | 960 | 961 | - id: 204 962 | symbol: BNB 963 | handle: opbnb 964 | name: OpBNB 965 | decimals: 18 966 | blockchain: Ethereum 967 | minConfirmations: 24 968 | 969 | 970 | - id: 59144 971 | symbol: ETH 972 | handle: linea 973 | name: Linea 974 | decimals: 18 975 | blockchain: Ethereum 976 | minConfirmations: 7 977 | 978 | 979 | - id: 5600 980 | symbol: gBNB 981 | handle: gbnb 982 | name: BNB Greenfield 983 | decimals: 18 984 | blockchain: Greenfield 985 | 986 | 987 | - id: 5000 988 | symbol: MNT 989 | handle: mantle 990 | name: Mantle 991 | decimals: 18 992 | blockchain: Ethereum 993 | 994 | 995 | - id: 169 996 | symbol: ETH 997 | handle: manta 998 | name: Manta Pacific 999 | decimals: 18 1000 | blockchain: Ethereum 1001 | 1002 | 1003 | - id: 10007000 1004 | symbol: ZETA 1005 | handle: zetachain 1006 | name: NativeZetaChain 1007 | decimals: 18 1008 | blockchain: Cosmos 1009 | 1010 | 1011 | - id: 20007000 1012 | symbol: ZETA 1013 | handle: zetaevm 1014 | name: Zeta EVM 1015 | decimals: 18 1016 | blockchain: Ethereum 1017 | 1018 | 1019 | - id: 4200 1020 | symbol: BTC 1021 | handle: merlin 1022 | name: Merlin 1023 | decimals: 18 1024 | blockchain: Ethereum 1025 | 1026 | 1027 | - id: 81457 1028 | symbol: ETH 1029 | handle: blast 1030 | name: Blast 1031 | decimals: 18 1032 | blockchain: Ethereum 1033 | 1034 | 1035 | - id: 534352 1036 | symbol: ETH 1037 | handle: scroll 1038 | name: Scroll 1039 | decimals: 18 1040 | blockchain: Ethereum 1041 | 1042 | 1043 | - id: 223 1044 | symbol: ICP 1045 | handle: internet_computer 1046 | name: Internet Computer 1047 | decimals: 8 1048 | blockchain: Internet Computer 1049 | 1050 | 1051 | - id: 6001 1052 | symbol: BB 1053 | handle: bouncebit 1054 | name: BounceBit 1055 | decimals: 18 1056 | blockchain: Ethereum 1057 | 1058 | 1059 | - id: 810180 1060 | symbol: ETH 1061 | handle: zklinknova 1062 | name: zkLink Nova 1063 | decimals: 18 1064 | blockchain: Ethereum 1065 | 1066 | 1067 | - id: 10000146 1068 | symbol: S 1069 | handle: sonic 1070 | name: Sonic 1071 | decimals: 18 1072 | blockchain: Ethereum 1073 | -------------------------------------------------------------------------------- /coin/coins_test.go: -------------------------------------------------------------------------------- 1 | package coin 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // TestCoinsBlockchain checks if all chains from coins.yml have blockchain field defined 11 | func TestCoinsBlockchain(t *testing.T) { 12 | for _, c := range Coins { 13 | assert.Falsef(t, c.Blockchain == "", fmt.Sprintf("chain: %s", c.Handle)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /coin/gen.go: -------------------------------------------------------------------------------- 1 | //go:build coins 2 | // +build coins 3 | 4 | package main 5 | 6 | import ( 7 | "html/template" 8 | "log" 9 | "os" 10 | "strings" 11 | "time" 12 | 13 | "gopkg.in/yaml.v2" 14 | ) 15 | 16 | const ( 17 | coinFile = "coin/coins.yml" 18 | filename = "coin/coins.go" 19 | templateFile = `// Code generated by go generate; DO NOT EDIT. 20 | // This file was generated by robots at 21 | // {{ .Timestamp }} 22 | // using data from coins.yml 23 | package coin 24 | 25 | import ( 26 | "fmt" 27 | ) 28 | 29 | const ( 30 | coinPrefix = "c" 31 | tokenPrefix = "t" 32 | ) 33 | 34 | // Coin is the native currency of a blockchain 35 | type Coin struct { 36 | ID uint 37 | Handle string 38 | Symbol string 39 | Name string 40 | Decimals uint 41 | BlockTime int 42 | MinConfirmations int64 43 | Blockchain string // Name of the Blockchain which core is used for this network 44 | } 45 | 46 | type AssetID string 47 | 48 | func (c *Coin) String() string { 49 | return fmt.Sprintf("[%s] %s (#%d)", c.Symbol, c.Name, c.ID) 50 | } 51 | 52 | func (c Coin) AssetID() AssetID { 53 | return AssetID(coinPrefix + fmt.Sprint(c.ID)) 54 | } 55 | 56 | func (c Coin) TokenAssetID(t string) AssetID { 57 | result := c.AssetID() 58 | if len(t) > 0 { 59 | result += AssetID("_" + tokenPrefix + t) 60 | } 61 | 62 | return result 63 | } 64 | 65 | const ( 66 | {{- range .Coins }} 67 | {{- if .Deprecated}} 68 | //Deprecated: {{ .Handle | ToUpper }} exists for historical compatibility and should not be used. 69 | {{- end}} 70 | {{ .Handle | ToUpper }} = {{ .ID }} 71 | {{- end }} 72 | ) 73 | 74 | var Coins = map[uint]Coin{ 75 | {{- range .Coins }} 76 | {{ .Handle | ToUpper }}: { 77 | ID: {{.ID}}, 78 | Handle: "{{.Handle}}", 79 | Symbol: "{{.Symbol}}", 80 | Name: "{{.Name}}", 81 | Decimals: {{.Decimals}}, 82 | BlockTime: {{.BlockTime}}, 83 | MinConfirmations: {{.MinConfirmations}}, 84 | Blockchain: "{{.Blockchain}}", 85 | }, 86 | {{- end }} 87 | } 88 | 89 | var Chains = map[string]Coin{ 90 | {{- range .Coins }} 91 | {{ .Handle | Capitalize }}().Handle: { 92 | ID: {{.ID}}, 93 | Handle: "{{.Handle}}", 94 | Symbol: "{{.Symbol}}", 95 | Name: "{{.Name}}", 96 | Decimals: {{.Decimals}}, 97 | BlockTime: {{.BlockTime}}, 98 | MinConfirmations: {{.MinConfirmations}}, 99 | Blockchain: "{{.Blockchain}}", 100 | }, 101 | {{- end }} 102 | } 103 | 104 | {{- range .Coins }} 105 | 106 | func {{ .Handle | Capitalize }}() Coin { 107 | return Coins[{{ .Handle | ToUpper }}] 108 | } 109 | {{- end }} 110 | 111 | ` 112 | ) 113 | 114 | type Coin struct { 115 | ID uint `yaml:"id"` 116 | Handle string `yaml:"handle"` 117 | Symbol string `yaml:"symbol"` 118 | Name string `yaml:"name"` 119 | Decimals uint `yaml:"decimals"` 120 | BlockTime int `yaml:"blockTime"` 121 | MinConfirmations int64 `yaml:"minConfirmations"` 122 | Blockchain string `yaml:"blockchain"` 123 | Deprecated bool `yaml:"deprecated"` 124 | } 125 | 126 | func main() { 127 | var coinList []Coin 128 | coin, err := os.Open(coinFile) 129 | dec := yaml.NewDecoder(coin) 130 | err = dec.Decode(&coinList) 131 | if err != nil { 132 | log.Panic(err) 133 | } 134 | 135 | f, err := os.Create(filename) 136 | if err != nil { 137 | log.Panic(err) 138 | } 139 | defer f.Close() 140 | 141 | funcMap := template.FuncMap{ 142 | "Capitalize": strings.Title, 143 | "ToUpper": strings.ToUpper, 144 | } 145 | 146 | coinsTemplate := template.Must(template.New("").Funcs(funcMap).Parse(templateFile)) 147 | err = coinsTemplate.Execute(f, map[string]interface{}{ 148 | "Timestamp": time.Now(), 149 | "Coins": coinList, 150 | }) 151 | if err != nil { 152 | log.Panic(err) 153 | } 154 | } 155 | 156 | func getValidParameter(env, variable string) string { 157 | e, ok := os.LookupEnv(env) 158 | if ok { 159 | return e 160 | } 161 | return variable 162 | } 163 | -------------------------------------------------------------------------------- /coin/gen_test.go: -------------------------------------------------------------------------------- 1 | package coin 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "regexp" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "golang.org/x/text/cases" 13 | "golang.org/x/text/language" 14 | "gopkg.in/yaml.v2" 15 | ) 16 | 17 | const ( 18 | coinFile = "coins.yml" 19 | filename = "coins.go" 20 | ) 21 | 22 | type TestCoin struct { 23 | ID uint `yaml:"id"` 24 | Handle string `yaml:"handle"` 25 | Symbol string `yaml:"symbol"` 26 | Name string `yaml:"name"` 27 | Decimals uint `yaml:"decimals"` 28 | BlockTime int `yaml:"blockTime"` 29 | MinConfirmations int64 `yaml:"minConfirmations"` 30 | SampleAddr string `yaml:"sampleAddress"` 31 | } 32 | 33 | func TestFilesExists(t *testing.T) { 34 | assert.True(t, assert.FileExists(t, coinFile)) 35 | assert.True(t, assert.FileExists(t, filename)) 36 | } 37 | 38 | func TestCoinFile(t *testing.T) { 39 | var coinList []TestCoin 40 | coin, err := os.Open(coinFile) 41 | if err != nil { 42 | t.Error(err) 43 | } 44 | dec := yaml.NewDecoder(coin) 45 | err = dec.Decode(&coinList) 46 | if err != nil { 47 | t.Error(err) 48 | } 49 | 50 | f, err := os.Open(filename) 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | defer func() { _ = f.Close() }() 55 | 56 | b, err := io.ReadAll(f) 57 | if err != nil { 58 | t.Error(err) 59 | } 60 | 61 | r := regexp.MustCompile(`[ |\t]+`) 62 | code := string(r.ReplaceAll(b, []byte(" "))) // replace multi spaces and tabs with one space 63 | 64 | for _, want := range coinList { 65 | got, ok := Coins[want.ID] 66 | assert.True(t, ok) 67 | assert.Equal(t, got.ID, want.ID) 68 | assert.Equal(t, got.Handle, want.Handle) 69 | assert.Equal(t, got.Symbol, want.Symbol) 70 | assert.Equal(t, got.Name, want.Name) 71 | assert.Equal(t, got.Decimals, want.Decimals) 72 | assert.Equal(t, got.BlockTime, want.BlockTime) 73 | assert.Equal(t, got.MinConfirmations, want.MinConfirmations) 74 | 75 | s := cases.Title(language.English).String(want.Handle) 76 | method := fmt.Sprintf("func %s() Coin", s) 77 | assert.True(t, strings.Contains(code, method), "Coin method not found") 78 | 79 | enum := fmt.Sprintf("%s = %d", strings.ToUpper(want.Handle), want.ID) 80 | assert.True(t, strings.Contains(code, enum), "Coin enum not found") 81 | 82 | } 83 | } 84 | 85 | func TestEthereum(t *testing.T) { 86 | 87 | c := Ethereum() 88 | 89 | assert.Equal(t, uint(60), c.ID) 90 | assert.Equal(t, "ethereum", c.Handle) 91 | assert.Equal(t, "ETH", c.Symbol) 92 | assert.Equal(t, "Ethereum", c.Name) 93 | assert.Equal(t, uint(18), c.Decimals) 94 | assert.Equal(t, 10000, c.BlockTime) 95 | assert.Equal(t, int64(12), c.MinConfirmations) 96 | } 97 | 98 | func TestBinance(t *testing.T) { 99 | 100 | c := Smartchain() 101 | 102 | assert.Equal(t, uint(20000714), c.ID) 103 | assert.Equal(t, "smartchain", c.Handle) 104 | assert.Equal(t, "BNB", c.Symbol) 105 | assert.Equal(t, "Smart Chain", c.Name) 106 | assert.Equal(t, uint(18), c.Decimals) 107 | assert.Equal(t, 3000, c.BlockTime) 108 | assert.Equal(t, int64(12), c.MinConfirmations) 109 | } 110 | 111 | func TestCosmos(t *testing.T) { 112 | 113 | c := Cosmos() 114 | 115 | assert.Equal(t, uint(118), c.ID) 116 | assert.Equal(t, "cosmos", c.Handle) 117 | assert.Equal(t, "ATOM", c.Symbol) 118 | assert.Equal(t, "Cosmos", c.Name) 119 | assert.Equal(t, uint(6), c.Decimals) 120 | assert.Equal(t, 5000, c.BlockTime) 121 | assert.Equal(t, int64(7), c.MinConfirmations) 122 | } 123 | 124 | func TestPublicVariables(t *testing.T) { 125 | want := []Coin{ 126 | { 127 | ID: 20000714, 128 | Handle: "smartchain", 129 | Symbol: "BNB", 130 | Name: "Smart Chain", 131 | Decimals: 18, 132 | BlockTime: 3000, 133 | MinConfirmations: 12, 134 | Blockchain: "Ethereum", 135 | }, 136 | { 137 | ID: 60, 138 | Handle: "ethereum", 139 | Symbol: "ETH", 140 | Name: "Ethereum", 141 | Decimals: 18, 142 | BlockTime: 10000, 143 | MinConfirmations: 12, 144 | Blockchain: "Ethereum", 145 | }, 146 | } 147 | 148 | for _, c := range want { 149 | coin := Coins[c.ID] 150 | assert.Equal(t, c, coin) 151 | 152 | chain := Chains[c.Handle] 153 | assert.Equal(t, c, chain) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /coin/models.go: -------------------------------------------------------------------------------- 1 | package coin 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | const BlockchainEthereum = "Ethereum" 10 | 11 | func GetCoinForId(id string) (Coin, error) { 12 | for _, c := range Coins { 13 | if c.Handle == id { 14 | return c, nil 15 | } 16 | } 17 | return Coin{}, errors.New("unknown id " + id) 18 | } 19 | 20 | func IsEVM(coinID uint) bool { 21 | return Coins[coinID].Blockchain == BlockchainEthereum 22 | } 23 | 24 | // nolint:cyclop 25 | func GetCoinExploreURL(c Coin, tokenID, tokenType string) (string, error) { 26 | switch c.ID { 27 | case ETHEREUM: 28 | return fmt.Sprintf("https://etherscan.io/token/%s", tokenID), nil 29 | case TRON: 30 | if _, err := strconv.ParseUint(tokenID, 10, 64); err == nil { 31 | return fmt.Sprintf("https://tronscan.io/#/token/%s", tokenID), nil 32 | } 33 | 34 | return fmt.Sprintf("https://tronscan.io/#/token20/%s", tokenID), nil 35 | case BINANCE: 36 | return fmt.Sprintf("https://explorer.binance.org/asset/%s", tokenID), nil 37 | case SMARTCHAIN: 38 | return fmt.Sprintf("https://bscscan.com/token/%s", tokenID), nil 39 | case EOS: 40 | return fmt.Sprintf("https://bloks.io/account/%s", tokenID), nil 41 | case NEO: 42 | return fmt.Sprintf("https://neo.tokenview.com/en/token/0x%s", tokenID), nil 43 | case NULS: 44 | return fmt.Sprintf("https://nulscan.io/token/info?contractAddress=%s", tokenID), nil 45 | case WANCHAIN: 46 | return fmt.Sprintf("https://www.wanscan.org/token/%s", tokenID), nil 47 | case SOLANA: 48 | return fmt.Sprintf("https://solscan.io/token/%s", tokenID), nil 49 | case TOMOCHAIN: 50 | return fmt.Sprintf("https://tomoscan.io/token/%s", tokenID), nil 51 | case KAVA: 52 | return "https://www.mintscan.io/kava", nil 53 | case ONTOLOGY: 54 | return "https://explorer.ont.io", nil 55 | case GOCHAIN: 56 | return fmt.Sprintf("https://explorer.gochain.io/addr/%s", tokenID), nil 57 | case THETA: 58 | return "https://explorer.thetatoken.org/", nil 59 | case THUNDERTOKEN: 60 | return fmt.Sprintf("https://viewblock.io/thundercore/address/%s", tokenID), nil 61 | case CLASSIC: 62 | return fmt.Sprintf("https://blockscout.com/etc/mainnet/tokens/%s", tokenID), nil 63 | case VECHAIN: 64 | return fmt.Sprintf("https://explore.vechain.org/accounts/%s", tokenID), nil 65 | case WAVES: 66 | return fmt.Sprintf("https://wavesexplorer.com/assets/%s", tokenID), nil 67 | case XDAI: 68 | return fmt.Sprintf("https://blockscout.com/xdai/mainnet/tokens/%s", tokenID), nil 69 | case POA: 70 | return fmt.Sprintf("https://blockscout.com/poa/core/tokens/%s", tokenID), nil 71 | case POLYGON: 72 | return fmt.Sprintf("https://polygonscan.com/token/%s", tokenID), nil 73 | case OPTIMISM: 74 | return fmt.Sprintf("https://optimistic.etherscan.io/token/%s", tokenID), nil 75 | case AVALANCHEC: 76 | return fmt.Sprintf("https://snowtrace.io/token/%s", tokenID), nil 77 | case ARBITRUM: 78 | return fmt.Sprintf("https://arbiscan.io/token/%s", tokenID), nil 79 | case FANTOM: 80 | return fmt.Sprintf("https://ftmscan.com/token/%s", tokenID), nil 81 | case TERRA: 82 | return fmt.Sprintf("https://finder.terra.money/mainnet/address/%s", tokenID), nil 83 | case RONIN: 84 | return fmt.Sprintf("https://explorer.roninchain.com/token/%s", tokenID), nil 85 | case CELO: 86 | return fmt.Sprintf("https://explorer.celo.org/mainnet/address/%s", tokenID), nil 87 | case ELROND: 88 | if tokenType == "ESDT" { 89 | return fmt.Sprintf("https://explorer.multiversx.com/tokens/%s", tokenID), nil 90 | } 91 | 92 | return fmt.Sprintf("https://explorer.multiversx.com/collections/%s", tokenID), nil 93 | case HECO: 94 | return fmt.Sprintf("https://hecoinfo.com/token/%s", tokenID), nil 95 | case OASIS: 96 | return fmt.Sprintf("https://explorer.oasis.updev.si/token/%s", tokenID), nil 97 | case CRONOS: 98 | return fmt.Sprintf("https://cronos.org/explorer/address/%s/token-transfers", tokenID), nil 99 | case STELLAR: 100 | return fmt.Sprintf("https://stellar.expert/explorer/public/asset/%s", tokenID), nil 101 | case KCC: 102 | return fmt.Sprintf("https://explorer.kcc.io/token/%s", tokenID), nil 103 | case AURORA: 104 | return fmt.Sprintf("https://aurorascan.dev/address/%s", tokenID), nil 105 | case ALGORAND: 106 | return fmt.Sprintf("https://algoexplorer.io/asset/%s", tokenID), nil 107 | case KAVAEVM: 108 | return fmt.Sprintf("https://explorer.kava.io/token/%s", tokenID), nil 109 | case METER: 110 | return fmt.Sprintf("https://scan.meter.io/address/%s", tokenID), nil 111 | case EVMOS: 112 | return fmt.Sprintf("https://evm.evmos.org/address/%s", tokenID), nil 113 | case OKC: 114 | return fmt.Sprintf("https://www.oklink.com/en/okc/address/%s", tokenID), nil 115 | case APTOS: 116 | switch tokenType { 117 | case "APTOSFA": 118 | return fmt.Sprintf("https://explorer.aptoslabs.com/fungible_asset/%s?network=mainnet", tokenID), nil 119 | default: 120 | return fmt.Sprintf("https://explorer.aptoslabs.com/coin/%s?network=mainnet", tokenID), nil 121 | } 122 | case MOONBEAM: 123 | return fmt.Sprintf("https://moonscan.io/token/%s", tokenID), nil 124 | case KLAYTN: 125 | return fmt.Sprintf("https://kaiascan.io/token/%s", tokenID), nil 126 | case METIS: 127 | return fmt.Sprintf("https://andromeda-explorer.metis.io/token/%s", tokenID), nil 128 | case MOONRIVER: 129 | return fmt.Sprintf("https://moonriver.moonscan.io/token/%s", tokenID), nil 130 | case BOBA: 131 | return fmt.Sprintf("https://bobascan.com/token/%s", tokenID), nil 132 | case TON: 133 | return fmt.Sprintf("https://tonscan.org/address/%s", tokenID), nil 134 | case POLYGONZKEVM: 135 | return fmt.Sprintf("https://explorer.public.zkevm-test.net/address/%s", tokenID), nil 136 | case ZKSYNC: 137 | return fmt.Sprintf("https://explorer.zksync.io/address/%s", tokenID), nil 138 | case SUI: 139 | return fmt.Sprintf("https://explorer.sui.io/address/%s", tokenID), nil 140 | case STRIDE: 141 | return fmt.Sprintf("https://www.mintscan.io/stride/account/%s", tokenID), nil 142 | case NEUTRON: 143 | return fmt.Sprintf("https://www.mintscan.io/neutron/account/%s", tokenID), nil 144 | case IOTEXEVM: 145 | return fmt.Sprintf("https://iotexscan.io/address/%s#transactions", tokenID), nil 146 | case CRYPTOORG: 147 | return fmt.Sprintf("https://crypto.org/explorer/account/%s", tokenID), nil 148 | case TEZOS: 149 | return fmt.Sprintf("https://tzstats.com/%s", tokenID), nil 150 | case CFXEVM: 151 | return fmt.Sprintf("https://evm.confluxscan.net/address/%s", tokenID), nil 152 | case ACALA: 153 | if tokenType == "custom_token" { 154 | return fmt.Sprintf("https://acala.subscan.io/custom_token?customTokenId=%s", tokenID), nil 155 | } 156 | return fmt.Sprintf("https://acala.subscan.io/system_token_detail?unique_id=%s", tokenID), nil 157 | case ACALAEVM: 158 | return fmt.Sprintf("https://blockscout.acala.network/token/%s", tokenID), nil 159 | case BASE: 160 | return fmt.Sprintf("https://basescan.org/token/%s", tokenID), nil 161 | case CARDANO: 162 | return fmt.Sprintf("https://cexplorer.io/asset/%s", tokenID), nil 163 | case NEON: 164 | return fmt.Sprintf("https://neonscan.org/token/%s", tokenID), nil 165 | case MANTLE: 166 | return fmt.Sprintf("https://explorer.mantle.xyz/address/%s", tokenID), nil 167 | case LINEA: 168 | return fmt.Sprintf("https://explorer.linea.build/token/%s", tokenID), nil 169 | case OPBNB: 170 | return fmt.Sprintf("https://opbnbscan.com/token/%s", tokenID), nil 171 | case MANTA: 172 | return fmt.Sprintf("https://pacific-explorer.manta.network/token/%s", tokenID), nil 173 | case ZETACHAIN: 174 | return fmt.Sprintf("https://explorer.zetachain.com/address/%s", tokenID), nil 175 | case ZETAEVM: 176 | return fmt.Sprintf("https://explorer.zetachain.com/address/%s", tokenID), nil 177 | case BITCOIN: 178 | return fmt.Sprintf("https://unisat.io/brc20/%s", tokenID), nil 179 | case BLAST: 180 | return fmt.Sprintf("https://blastscan.io/token/%s", tokenID), nil 181 | case SCROLL: 182 | return fmt.Sprintf("https://scrollscan.com/token/%s", tokenID), nil 183 | case ZKLINKNOVA: 184 | return fmt.Sprintf("https://explorer.zklink.io/address/%s", tokenID), nil 185 | case RIPPLE: 186 | return fmt.Sprintf("https://xrpscan.com/account/%s", tokenID), nil 187 | case SONIC: 188 | return fmt.Sprintf("https://sonicscan.org/token/%s", tokenID), nil 189 | } 190 | 191 | return "", errors.New("no explorer for coin: " + c.Handle) 192 | } 193 | -------------------------------------------------------------------------------- /coin/models_test.go: -------------------------------------------------------------------------------- 1 | package coin 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGetCoinForId(t *testing.T) { 12 | type args struct { 13 | id string 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want Coin 19 | wantErr bool 20 | }{ 21 | { 22 | "Test ethereum", 23 | args{ 24 | id: "ethereum", 25 | }, 26 | Ethereum(), 27 | false, 28 | }, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | got, err := GetCoinForId(tt.args.id) 33 | if (err != nil) != tt.wantErr { 34 | t.Errorf("GetCoinForId() error = %v, wantErr %v", err, tt.wantErr) 35 | return 36 | } 37 | if !reflect.DeepEqual(got, tt.want) { 38 | t.Errorf("GetCoinForId() got = %v, want %v", got, tt.want) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | func TestGetCoinExploreURL(t *testing.T) { 45 | type args struct { 46 | addr string 47 | tokenType string 48 | chain Coin 49 | } 50 | 51 | tests := []struct { 52 | name string 53 | args args 54 | want string 55 | wantErr bool 56 | }{ 57 | { 58 | name: "Test ethereum", 59 | args: args{ 60 | addr: "token", 61 | tokenType: "", 62 | chain: Ethereum(), 63 | }, 64 | want: "https://etherscan.io/token/token", 65 | wantErr: false, 66 | }, 67 | { 68 | name: "Test custom chain", 69 | args: args{ 70 | addr: "token", 71 | tokenType: "", 72 | chain: Coin{ 73 | ID: 1, // some id 74 | Name: "Custom Coin", 75 | }, 76 | }, 77 | want: "", 78 | wantErr: true, 79 | }, 80 | { 81 | name: "Test Tron TRC10", 82 | args: args{ 83 | addr: "10001", 84 | tokenType: "", 85 | chain: Tron(), 86 | }, 87 | want: "https://tronscan.io/#/token/10001", 88 | wantErr: false, 89 | }, 90 | { 91 | name: "Test Tron TRC20", 92 | args: args{ 93 | addr: "token", 94 | tokenType: "", 95 | chain: Tron(), 96 | }, 97 | want: "https://tronscan.io/#/token20/token", 98 | wantErr: false, 99 | }, 100 | { 101 | name: "Test Elrond ESDT", 102 | args: args{ 103 | addr: "EGLDUSDC-594e5e", 104 | tokenType: "ESDT", 105 | chain: Elrond(), 106 | }, 107 | want: "https://explorer.multiversx.com/tokens/EGLDUSDC-594e5e", 108 | wantErr: false, 109 | }, 110 | { 111 | name: "Test STELLAR", 112 | args: args{ 113 | addr: "yXLM-GARDNV3Q7YGT4AKSDF25LT32YSCCW4EV22Y2TV3I2PU2MMXJTEDL5T55", 114 | tokenType: "STELLAR", 115 | chain: Stellar(), 116 | }, 117 | want: "https://stellar.expert/explorer/public/asset/yXLM-GARDNV3Q7YGT4AKSDF25LT32YSCCW4EV22Y2TV3I2PU2MMXJTEDL5T55", 118 | wantErr: false, 119 | }, 120 | { 121 | name: "Test CRONOS", 122 | args: args{ 123 | addr: "0x145677FC4d9b8F19B5D56d1820c48e0443049a30", 124 | tokenType: "CRC20", 125 | chain: Cronos(), 126 | }, 127 | want: "https://cronos.org/explorer/address/0x145677FC4d9b8F19B5D56d1820c48e0443049a30/token-transfers", 128 | wantErr: false, 129 | }, 130 | { 131 | name: "Test Aurora", 132 | args: args{ 133 | addr: "0x7b37ABAe99A560Aec9497DBbe1741204bd439AC0", 134 | tokenType: "AURORA", 135 | chain: Aurora(), 136 | }, 137 | want: "https://aurorascan.dev/address/0x7b37ABAe99A560Aec9497DBbe1741204bd439AC0", 138 | wantErr: false, 139 | }, 140 | { 141 | name: "Test KuCoin", 142 | args: args{ 143 | addr: "0x2cA48b4eeA5A731c2B54e7C3944DBDB87c0CFB6F", 144 | tokenType: "KRC20", 145 | chain: Kcc(), 146 | }, 147 | want: "https://explorer.kcc.io/token/0x2cA48b4eeA5A731c2B54e7C3944DBDB87c0CFB6F", 148 | wantErr: false, 149 | }, 150 | { 151 | name: "Test Algorand", 152 | args: args{ 153 | addr: "test", 154 | tokenType: "ALGORAND", 155 | chain: Algorand(), 156 | }, 157 | want: "https://algoexplorer.io/asset/test", 158 | wantErr: false, 159 | }, 160 | { 161 | name: "Test KavaEvm", 162 | args: args{ 163 | addr: "test", 164 | tokenType: "KAVA", 165 | chain: Kavaevm(), 166 | }, 167 | want: "https://explorer.kava.io/token/test", 168 | wantErr: false, 169 | }, 170 | { 171 | name: "Test Meter", 172 | args: args{ 173 | addr: "test", 174 | tokenType: "METER", 175 | chain: Meter(), 176 | }, 177 | want: "https://scan.meter.io/address/test", 178 | wantErr: false, 179 | }, 180 | { 181 | name: "Test Evmos", 182 | args: args{ 183 | addr: "test", 184 | tokenType: "EVMOS_ERC20", 185 | chain: Evmos(), 186 | }, 187 | want: "https://evm.evmos.org/address/test", 188 | wantErr: false, 189 | }, 190 | { 191 | name: "Test Okc", 192 | args: args{ 193 | addr: "test", 194 | tokenType: "KIP20", 195 | chain: Okc(), 196 | }, 197 | want: "https://www.oklink.com/en/okc/address/test", 198 | wantErr: false, 199 | }, 200 | { 201 | name: "Test Moonbeam", 202 | args: args{ 203 | addr: "test", 204 | tokenType: "MOONBEAM", 205 | chain: Moonbeam(), 206 | }, 207 | want: "https://moonscan.io/token/test", 208 | wantErr: false, 209 | }, 210 | { 211 | name: "Test Klaytn", 212 | args: args{ 213 | addr: "test", 214 | tokenType: "KLAYTN", 215 | chain: Klaytn(), 216 | }, 217 | want: "https://kaiascan.io/token/test", 218 | wantErr: false, 219 | }, 220 | { 221 | name: "Test Metis", 222 | args: args{ 223 | addr: "test", 224 | tokenType: "METIS", 225 | chain: Metis(), 226 | }, 227 | want: "https://andromeda-explorer.metis.io/token/test", 228 | wantErr: false, 229 | }, 230 | { 231 | name: "Test Moonriver", 232 | args: args{ 233 | addr: "test", 234 | tokenType: "MOONRIVER", 235 | chain: Moonriver(), 236 | }, 237 | want: "https://moonriver.moonscan.io/token/test", 238 | wantErr: false, 239 | }, 240 | { 241 | name: "Test Boba", 242 | args: args{ 243 | addr: "test", 244 | tokenType: "BOBA", 245 | chain: Boba(), 246 | }, 247 | want: "https://bobascan.com/token/test", 248 | wantErr: false, 249 | }, 250 | { 251 | name: "Test Ton", 252 | args: args{ 253 | addr: "test", 254 | tokenType: "TON", 255 | chain: Ton(), 256 | }, 257 | want: "https://tonscan.org/address/test", 258 | wantErr: false, 259 | }, 260 | { 261 | name: "Test ZKEVM", 262 | args: args{ 263 | addr: "test", 264 | tokenType: "ZKEVM", 265 | chain: Polygonzkevm(), 266 | }, 267 | want: "https://explorer.public.zkevm-test.net/address/test", 268 | wantErr: false, 269 | }, 270 | { 271 | name: "Test ZKSync", 272 | args: args{ 273 | addr: "test", 274 | tokenType: "ZKSYNC", 275 | chain: Zksync(), 276 | }, 277 | want: "https://explorer.zksync.io/address/test", 278 | wantErr: false, 279 | }, 280 | { 281 | name: "Test Sui", 282 | args: args{ 283 | addr: "test", 284 | tokenType: "Sui", 285 | chain: Sui(), 286 | }, 287 | want: "https://explorer.sui.io/address/test", 288 | wantErr: false, 289 | }, 290 | { 291 | name: "Test Stride", 292 | args: args{ 293 | addr: "test", 294 | tokenType: "Stride", 295 | chain: Stride(), 296 | }, 297 | want: "https://www.mintscan.io/stride/account/test", 298 | wantErr: false, 299 | }, 300 | { 301 | name: "Test Neutron", 302 | args: args{ 303 | addr: "test", 304 | tokenType: "Neutron", 305 | chain: Neutron(), 306 | }, 307 | want: "https://www.mintscan.io/neutron/account/test", 308 | wantErr: false, 309 | }, 310 | { 311 | name: "Test IoTex EVM", 312 | args: args{ 313 | addr: "test", 314 | tokenType: "", 315 | chain: Iotexevm(), 316 | }, 317 | want: "https://iotexscan.io/address/test#transactions", 318 | wantErr: false, 319 | }, 320 | { 321 | name: "Test CFXEVM", 322 | args: args{ 323 | addr: "test", 324 | tokenType: "", 325 | chain: Cfxevm(), 326 | }, 327 | want: "https://evm.confluxscan.net/address/test", 328 | wantErr: false, 329 | }, 330 | { 331 | name: "Test Acala system token", 332 | args: args{ 333 | addr: "test", 334 | tokenType: "", 335 | chain: Acala(), 336 | }, 337 | want: "https://acala.subscan.io/system_token_detail?unique_id=test", 338 | wantErr: false, 339 | }, 340 | { 341 | name: "Test Acala custom token", 342 | args: args{ 343 | addr: "test", 344 | tokenType: "custom_token", 345 | chain: Acala(), 346 | }, 347 | want: "https://acala.subscan.io/custom_token?customTokenId=test", 348 | wantErr: false, 349 | }, 350 | { 351 | name: "Test BASE20 token", 352 | args: args{ 353 | addr: "0x48bcf9455ba97cc439a2efbcfdf8f1afe692139b", 354 | tokenType: "BASE20", 355 | chain: Base(), 356 | }, 357 | want: "https://basescan.org/token/0x48bcf9455ba97cc439a2efbcfdf8f1afe692139b", 358 | wantErr: false, 359 | }, 360 | { 361 | name: "Test NEON token", 362 | args: args{ 363 | addr: "0x5f38248f339bf4e84a2caf4e4c0552862dc9f82a", 364 | tokenType: "NEON", 365 | chain: Neon(), 366 | }, 367 | want: "https://neonscan.org/token/0x5f38248f339bf4e84a2caf4e4c0552862dc9f82a", 368 | wantErr: false, 369 | }, 370 | { 371 | name: "Test Native ZETA", 372 | args: args{ 373 | addr: "zeta14py36sx57ud82t9yrks9z6hdsrpn5x6kmxs0ne", 374 | tokenType: "ZETA", 375 | chain: Zetachain(), 376 | }, 377 | want: "https://explorer.zetachain.com/address/zeta14py36sx57ud82t9yrks9z6hdsrpn5x6kmxs0ne", 378 | wantErr: false, 379 | }, 380 | { 381 | name: "Test ZETA EVM", 382 | args: args{ 383 | addr: "0x890a1b6dc3ca666eacda1c453115494291c6bc6a", 384 | tokenType: "ZETA", 385 | chain: Zetaevm(), 386 | }, 387 | want: "https://explorer.zetachain.com/address/0x890a1b6dc3ca666eacda1c453115494291c6bc6a", 388 | wantErr: false, 389 | }, 390 | { 391 | name: "Test Celo", 392 | args: args{ 393 | addr: "0x639A647fbe20b6c8ac19E48E2de44ea792c62c5C", 394 | tokenType: "CELO", 395 | chain: Celo(), 396 | }, 397 | want: "https://explorer.celo.org/mainnet/address/0x639A647fbe20b6c8ac19E48E2de44ea792c62c5C", 398 | wantErr: false, 399 | }, 400 | { 401 | name: "Test Blast", 402 | args: args{ 403 | addr: "0x4300000000000000000000000000000000000004", 404 | tokenType: "BLAST", 405 | chain: Blast(), 406 | }, 407 | want: "https://blastscan.io/token/0x4300000000000000000000000000000000000004", 408 | wantErr: false, 409 | }, 410 | { 411 | name: "Test Scroll", 412 | args: args{ 413 | addr: "0xf55bec9cafdbe8730f096aa55dad6d22d44099df", 414 | tokenType: "SCROLL", 415 | chain: Scroll(), 416 | }, 417 | want: "https://scrollscan.com/token/0xf55bec9cafdbe8730f096aa55dad6d22d44099df", 418 | wantErr: false, 419 | }, 420 | { 421 | name: "Test zkLink Nova - Dai", 422 | args: args{ 423 | addr: "0xF573fA04A73d5AC442F3DEa8741317fEaA3cDeab", 424 | tokenType: "ZKLINKNOVA", 425 | chain: Zklinknova(), 426 | }, 427 | want: "https://explorer.zklink.io/address/0xF573fA04A73d5AC442F3DEa8741317fEaA3cDeab", 428 | wantErr: false, 429 | }, 430 | { 431 | name: "Test Aptos (legacy)", 432 | args: args{ 433 | addr: "0xacd014e8bdf395fa8497b6d585b164547a9d45269377bdf67c96c541b7fec9ed::coin::T", 434 | tokenType: "APTOS", 435 | chain: Aptos(), 436 | }, 437 | want: "https://explorer.aptoslabs.com/coin/0xacd014e8bdf395fa8497b6d585b164547a9d45269377bdf67c96c541b7fec9ed::coin::T?network=mainnet", 438 | wantErr: false, 439 | }, 440 | { 441 | name: "Test Aptos (fungible asset)", 442 | args: args{ 443 | addr: "0x357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b", 444 | tokenType: "APTOSFA", 445 | chain: Aptos(), 446 | }, 447 | want: "https://explorer.aptoslabs.com/fungible_asset/0x357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b?network=mainnet", 448 | wantErr: false, 449 | }, 450 | { 451 | name: "Test XRP", 452 | args: args{ 453 | addr: "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De", 454 | tokenType: "XRP", 455 | chain: Ripple(), 456 | }, 457 | want: "https://xrpscan.com/account/rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De", 458 | wantErr: false, 459 | }, 460 | { 461 | name: "Test Sonic", 462 | args: args{ 463 | addr: "0x29219dd400f2bf60e5a23d13be72b486d4038894", 464 | tokenType: "SONIC", 465 | chain: Sonic(), 466 | }, 467 | want: "https://sonicscan.org/token/0x29219dd400f2bf60e5a23d13be72b486d4038894", 468 | wantErr: false, 469 | }, 470 | } 471 | for _, tt := range tests { 472 | t.Run(tt.name, func(t *testing.T) { 473 | got, err := GetCoinExploreURL(tt.args.chain, tt.args.addr, tt.args.tokenType) 474 | if (err != nil) != tt.wantErr { 475 | t.Errorf("GetCoinForId() error = %v, wantErr %v", err, tt.wantErr) 476 | return 477 | } 478 | if !reflect.DeepEqual(got, tt.want) { 479 | t.Errorf("GetCoinForId() got = %v, want %v", got, tt.want) 480 | } 481 | }) 482 | } 483 | } 484 | 485 | var evmCoinsTestSet = map[uint]struct{}{ 486 | ETHEREUM: {}, 487 | CLASSIC: {}, 488 | POA: {}, 489 | CALLISTO: {}, 490 | WANCHAIN: {}, 491 | THUNDERTOKEN: {}, 492 | GOCHAIN: {}, 493 | TOMOCHAIN: {}, 494 | SMARTCHAIN: {}, 495 | POLYGON: {}, 496 | OPTIMISM: {}, 497 | XDAI: {}, 498 | AVALANCHEC: {}, 499 | FANTOM: {}, 500 | HECO: {}, 501 | RONIN: {}, 502 | CRONOS: {}, 503 | KCC: {}, 504 | AURORA: {}, 505 | ARBITRUM: {}, 506 | KAVAEVM: {}, 507 | METER: {}, 508 | EVMOS: {}, 509 | CELO: {}, 510 | OKC: {}, 511 | MOONBEAM: {}, 512 | KLAYTN: {}, 513 | METIS: {}, 514 | MOONRIVER: {}, 515 | BOBA: {}, 516 | POLYGONZKEVM: {}, 517 | ZKSYNC: {}, 518 | CFXEVM: {}, 519 | ACALAEVM: {}, 520 | BASE: {}, 521 | NEON: {}, 522 | IOTEXEVM: {}, 523 | OPBNB: {}, 524 | LINEA: {}, 525 | MANTLE: {}, 526 | MANTA: {}, 527 | ZETAEVM: {}, 528 | MERLIN: {}, 529 | BLAST: {}, 530 | SCROLL: {}, 531 | BOUNCEBIT: {}, 532 | ZKLINKNOVA: {}, 533 | SONIC: {}, 534 | } 535 | 536 | // TestEvmCoinsList This test will automatically fail when new EVM chain is added to coins.yml 537 | // To fix it, extend evmCoinsTestSet with that new chain 538 | func TestEvmCoinsList(t *testing.T) { 539 | for _, c := range Coins { 540 | _, ok := evmCoinsTestSet[c.ID] 541 | assert.Equalf(t, c.Blockchain == BlockchainEthereum, ok, fmt.Sprintf("chain: %s", c.Handle)) 542 | } 543 | } 544 | 545 | func TestIsEVM(t *testing.T) { 546 | for _, c := range Coins { 547 | _, ok := evmCoinsTestSet[c.ID] 548 | if ok { 549 | assert.Truef(t, IsEVM(c.ID), fmt.Sprintf("chain: %s", c.Handle)) 550 | } 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/trustwallet/go-primitives 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/deckarep/golang-set v1.7.1 7 | github.com/shopspring/decimal v1.2.0 8 | github.com/stretchr/testify v1.7.0 9 | golang.org/x/crypto v0.1.0 10 | golang.org/x/text v0.4.0 11 | gopkg.in/yaml.v2 v2.4.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.0 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | golang.org/x/sys v0.1.0 // indirect 18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ= 4 | github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 8 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 9 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 10 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 11 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 12 | golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= 13 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 14 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 15 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 16 | golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= 17 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 21 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 23 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | -------------------------------------------------------------------------------- /numbers/amount.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | ) 7 | 8 | func GetAmountValue(amount string) string { 9 | value := ParseAmount(amount) 10 | return strconv.FormatInt(value, 10) 11 | } 12 | 13 | func ParseAmount(amount string) int64 { 14 | value, err := strconv.ParseInt(amount, 10, 64) 15 | if err == nil { 16 | return value 17 | } 18 | return ToSatoshi(amount) 19 | } 20 | 21 | func ToSatoshi(amount string) int64 { 22 | value, err := strconv.ParseFloat(amount, 64) 23 | if err != nil { 24 | return 0 25 | } 26 | total := value * math.Pow10(8) 27 | return int64(total) 28 | } 29 | 30 | func AddAmount(left string, right string) (sum string) { 31 | amount1 := ParseAmount(left) 32 | amount2 := ParseAmount(right) 33 | return strconv.FormatInt(amount1+amount2, 10) 34 | } 35 | -------------------------------------------------------------------------------- /numbers/amount_test.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_addAmount(t *testing.T) { 8 | type args struct { 9 | left string 10 | right string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | wantSum string 16 | }{ 17 | {"test zero + float", args{left: "0", right: "0.33333"}, "33333000"}, 18 | {"test zero + int", args{left: "0", right: "333"}, "333"}, 19 | {"test zero + zero", args{left: "0", right: "0"}, "0"}, 20 | {"test int + float", args{left: "232", right: "0.222"}, "22200232"}, 21 | {"test int + int", args{left: "661", right: "12"}, "673"}, 22 | {"test int + zero", args{left: "131", right: "0"}, "131"}, 23 | {"test float + float", args{left: "0.4141", right: "0.11211"}, "52621000"}, 24 | {"test float + int", args{left: "3.111", right: "11"}, "311100011"}, 25 | {"test float + zero", args{left: "0.455", right: "0"}, "45500000"}, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | if gotSum := AddAmount(tt.args.left, tt.args.right); gotSum != tt.wantSum { 30 | t.Errorf("AddAmount() = %v, want %v", gotSum, tt.wantSum) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | func Test_ToSatoshi(t *testing.T) { 37 | tests := []struct { 38 | name string 39 | amount string 40 | want int64 41 | }{ 42 | {"test float", "0.33333", 33333000}, 43 | {"test int", "3333", 333300000000}, 44 | {"test zero", "0", 0}, 45 | {"test error", "trust", 0}, 46 | } 47 | for _, tt := range tests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | if got := ToSatoshi(tt.amount); got != tt.want { 50 | t.Errorf("ToSatoshi() = %v, want %v", got, tt.want) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func Test_getValue(t *testing.T) { 57 | tests := []struct { 58 | name string 59 | amount string 60 | want string 61 | }{ 62 | {"test float", "0.33333", "33333000"}, 63 | {"test int", "3333", "3333"}, 64 | {"test zero", "0", "0"}, 65 | {"test error", "trust", "0"}, 66 | } 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | if got := GetAmountValue(tt.amount); got != tt.want { 70 | t.Errorf("GetAmountValue() = %v, want %v", got, tt.want) 71 | } 72 | }) 73 | } 74 | } 75 | 76 | func Test_parseAmount(t *testing.T) { 77 | tests := []struct { 78 | name string 79 | amount string 80 | want int64 81 | }{ 82 | {"test float", "0.33333", 33333000}, 83 | {"test int", "3333", 3333}, 84 | {"test zero", "0", 0}, 85 | {"test error", "trust", 0}, 86 | } 87 | for _, tt := range tests { 88 | t.Run(tt.name, func(t *testing.T) { 89 | if got := ParseAmount(tt.amount); got != tt.want { 90 | t.Errorf("ParseAmount() = %v, want %v", got, tt.want) 91 | } 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /numbers/decimal.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "errors" 5 | "math/big" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | // DecimalToSatoshis removes the comma in a decimal string 11 | // "12.345" => "12345" 12 | // "0.0230" => "230" 13 | func DecimalToSatoshis(dec string) (string, error) { 14 | out := strings.TrimLeft(dec, " ") 15 | out = strings.TrimRight(out, " ") 16 | out = strings.Replace(out, ".", "", 1) 17 | // trim left 0's but keep last 18 | if l := len(out); l >= 2 { 19 | out = strings.TrimLeft(out[:l-1], "0") + out[l-1:l] 20 | } 21 | if len(out) == 0 { 22 | return "", errors.New("Invalid empty input: " + dec) 23 | } 24 | for _, c := range out { 25 | if !unicode.IsNumber(c) { 26 | return "", errors.New("not a number: " + dec) 27 | } 28 | } 29 | return out, nil 30 | } 31 | 32 | // DecimalExp calculates dec * 10^exp in decimal string representation 33 | func DecimalExp(dec string, exp int) string { 34 | // 0 * n = 0 35 | if dec == "0" { 36 | return "0" 37 | } 38 | // Get comma position 39 | i := strings.IndexRune(dec, '.') 40 | if i == -1 { 41 | // Virtual comma at the end of the string 42 | i = len(dec) 43 | } else { 44 | // Remove comma from underlying number 45 | dec = strings.Replace(dec, ".", "", 1) 46 | } 47 | // Shift comma by exponent 48 | i += exp 49 | // Remove leading zeros 50 | origSize := len(dec) 51 | dec = strings.TrimLeft(dec, "0") 52 | i -= origSize - len(dec) 53 | // Fix bounds 54 | if i <= 0 { 55 | zeros := "" 56 | for ; i < 0; i++ { 57 | zeros += "0" 58 | } 59 | return "0." + zeros + dec 60 | } else if i >= len(dec) { 61 | for i > len(dec) { 62 | dec += "0" 63 | } 64 | return dec 65 | } 66 | // No bound fix needed 67 | return dec[:i] + "." + dec[i:] 68 | } 69 | 70 | // HexToDecimal converts a hexadecimal integer to a base-10 integer 71 | // "0x1fbad5f2e25570000" => "36582000000000000000" 72 | func HexToDecimal(hex string) (string, error) { 73 | if len(hex) == 0 || hex == "0x" { 74 | return "0", nil 75 | } 76 | var i big.Int 77 | if _, ok := i.SetString(hex, 0); !ok { 78 | return "", errors.New("invalid hex: " + hex) 79 | } 80 | return i.String(), nil 81 | } 82 | 83 | // CutZeroFractional cuts off a decimal separator and zeros to the right. 84 | // Fails if the fractional part contains contains other digits than zeros. 85 | // - CutZeroFractional("123.00000") => ("123", true) 86 | // - CutZeroFractional("123.456") => ("", false) 87 | func CutZeroFractional(dec string) (integer string, ok bool) { 88 | // Get comma position 89 | comma := strings.IndexRune(dec, '.') 90 | if comma == -1 { 91 | return dec, true 92 | } 93 | 94 | for i := len(dec) - 1; i > comma; i-- { 95 | if dec[i] != '0' { 96 | return "", false 97 | } 98 | } 99 | 100 | if comma == 0 { 101 | return "0", true 102 | } else { 103 | return dec[:comma], true 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /numbers/decimal_test.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import "testing" 4 | 5 | func TestDecimalToSatoshis(t *testing.T) { 6 | assertSatEquals := func(expected string, input string) { 7 | actual, err := DecimalToSatoshis(input) 8 | if err != nil { 9 | t.Error(err) 10 | } 11 | if expected != actual { 12 | t.Errorf("expected %s, got %s, input %s", expected, actual, input) 13 | } 14 | } 15 | 16 | assertSatError := func(input string) { 17 | actual, err := DecimalToSatoshis(input) 18 | if err == nil { 19 | t.Errorf("Expected error but no error: got %s, input %s", actual, input) 20 | } 21 | } 22 | 23 | assertSatEquals("10", "1.0") 24 | assertSatEquals("1", "0.1") 25 | assertSatEquals("13602", "136.02") 26 | assertSatEquals("13602", "0136.02") 27 | assertSatEquals("1500000", "0.01500000") 28 | assertSatEquals("0", "0") 29 | assertSatEquals("2030", "0.002030") 30 | assertSatEquals("101010", "0101010") 31 | assertSatEquals("11001100", "0011001100") 32 | assertSatEquals("376", " 376") 33 | assertSatEquals("376", "376 ") 34 | 35 | assertSatError("12NotNumber34") 36 | assertSatError("12,34") 37 | assertSatError("") 38 | assertSatError(" ") 39 | assertSatError("37 6") 40 | assertSatError("37,6") 41 | } 42 | 43 | func TestDecimalExp(t *testing.T) { 44 | assertEquals := func(inputDec string, inputExp int, expected string) { 45 | actual := DecimalExp(inputDec, inputExp) 46 | if expected != actual { 47 | t.Errorf("expected: %s * (10^%d) = %s, got %s", 48 | inputDec, inputExp, expected, actual) 49 | } 50 | } 51 | 52 | // No-Op 53 | assertEquals("0", 300, "0") 54 | assertEquals("0", 8, "0") 55 | assertEquals("123", 0, "123") 56 | assertEquals("0.456", 0, "0.456") 57 | assertEquals("123.456", 0, "123.456") 58 | 59 | // In-Bounds, comma 60 | assertEquals("12.34", -1, "1.234") 61 | assertEquals("12.34", 1, "123.4") 62 | 63 | // 1 past bounds, comma 64 | assertEquals("12.34", -2, "0.1234") 65 | assertEquals("12.34", 2, "1234") 66 | 67 | // n past bounds, comma 68 | assertEquals("12.34", -4, "0.001234") 69 | assertEquals("12.34", 4, "123400") 70 | 71 | // Integer 72 | assertEquals("1234", -1, "123.4") 73 | assertEquals("1234", 1, "12340") 74 | 75 | // Denormalized 76 | assertEquals("0.1234", -1, "0.01234") 77 | assertEquals("0.1234", 1, "1.234") 78 | 79 | // Tiny 80 | assertEquals("0.001234", -1, "0.0001234") 81 | assertEquals("0.001234", 1, "0.01234") 82 | assertEquals("0.000375", 8, "37500") 83 | } 84 | 85 | func TestCutZeroFractional(t *testing.T) { 86 | assertEquals := func(inputDec string, expected string, expOk bool) { 87 | actual, ok := CutZeroFractional(inputDec) 88 | if expected != actual || ok != expOk { 89 | t.Errorf("expected: %s => (%s, %v), actual: (%s, %v)", 90 | inputDec, expected, expOk, actual, ok) 91 | } 92 | } 93 | 94 | // No comma 95 | assertEquals("", "", true) 96 | assertEquals("eee", "eee", true) 97 | 98 | // Length 1 99 | assertEquals(".", "0", true) 100 | assertEquals(".3", "", false) 101 | assertEquals(".0", "0", true) 102 | assertEquals("0.", "0", true) 103 | assertEquals("1.0", "1", true) 104 | assertEquals("1.1", "", false) 105 | assertEquals("1.0.0", "", false) 106 | 107 | // Arbitrary content left to comma 108 | assertEquals("eee.000", "eee", true) 109 | assertEquals("eee.001", "", false) 110 | assertEquals("eee.100", "", false) 111 | 112 | // Long strings 113 | assertEquals("163056848705309039018274728757999527956626319283048085297785610.238523", "", false) 114 | assertEquals("11434397695550368380599182733571088333799363173941798154.0000000000000", "11434397695550368380599182733571088333799363173941798154", true) 115 | } 116 | 117 | func TestHexToDecimal(t *testing.T) { 118 | tests := []struct { 119 | name string 120 | hex string 121 | want string 122 | wantErr bool 123 | }{ 124 | { 125 | name: "Empty value", 126 | hex: "", 127 | want: "0", 128 | wantErr: false, 129 | }, 130 | { 131 | name: "Empty value 2", 132 | hex: "0x", 133 | want: "0", 134 | wantErr: false, 135 | }, 136 | { 137 | name: "Empty value 3", 138 | hex: "0x0", 139 | want: "0", 140 | wantErr: false, 141 | }, 142 | { 143 | name: "Hex to decimal", 144 | hex: "0x1fbad5f2e25570000", 145 | want: "36582000000000000000", 146 | wantErr: false, 147 | }, 148 | } 149 | for _, tt := range tests { 150 | t.Run(tt.name, func(t *testing.T) { 151 | got, err := HexToDecimal(tt.hex) 152 | if (err != nil) != tt.wantErr { 153 | t.Errorf("HexToDecimal() error = %v, wantErr %v", err, tt.wantErr) 154 | return 155 | } 156 | if got != tt.want { 157 | t.Errorf("HexToDecimal() got = %v, want %v", got, tt.want) 158 | } 159 | }) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /numbers/number.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "math" 5 | "math/big" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/shopspring/decimal" 10 | ) 11 | 12 | func Min(x, y int) int { 13 | if x < y { 14 | return x 15 | } 16 | return y 17 | } 18 | 19 | func Max(x, y int64) int64 { 20 | if x > y { 21 | return x 22 | } 23 | return y 24 | } 25 | 26 | func Round(num float64) int { 27 | return int(num + math.Copysign(0.5, num)) 28 | } 29 | 30 | func Float64toPrecision(num float64, precision int) float64 { 31 | output := math.Pow(10, float64(precision)) 32 | return float64(Round(num*output)) / output 33 | } 34 | 35 | // 0.1010 => "0.101" 36 | func Float64toString(num float64) string { 37 | return strconv.FormatFloat(num, 'f', -1, 64) 38 | } 39 | 40 | // "0.00037500" => 0.000375, "non-string-number" => 0 41 | func StringNumberToFloat64(str string) (float64, error) { 42 | value, err := strconv.ParseFloat(str, 64) 43 | if err != nil { 44 | return 0, err 45 | } else { 46 | return value, nil 47 | } 48 | } 49 | 50 | func FromDecimal(dec string) string { 51 | v, err := DecimalToSatoshis(dec) 52 | if err != nil { 53 | return "0" 54 | } 55 | return v 56 | } 57 | 58 | func ToDecimal(value string, exp int) string { 59 | num, ok := new(big.Int).SetString(value, 10) 60 | if !ok { 61 | return "0" 62 | } 63 | denom := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(exp)), nil) 64 | rat := new(big.Rat).SetFrac(num, denom) 65 | f, err := decimal.NewFromString(rat.FloatString(10)) 66 | if err != nil { 67 | return "0" 68 | } 69 | return f.String() 70 | } 71 | 72 | func FromDecimalExp(dec string, exp int) string { 73 | return strings.Split(DecimalExp(dec, exp), ".")[0] 74 | } 75 | 76 | func SliceAtoi(sa []string) ([]int, error) { 77 | si := make([]int, 0, len(sa)) 78 | for _, a := range sa { 79 | i, err := strconv.Atoi(a) 80 | if err != nil { 81 | return si, err 82 | } 83 | si = append(si, i) 84 | } 85 | return si, nil 86 | } 87 | -------------------------------------------------------------------------------- /numbers/number_test.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMin(t *testing.T) { 10 | assert.Equal(t, Min(1, 5), 1) 11 | assert.Equal(t, Min(22, 5), 5) 12 | } 13 | 14 | func TestMax(t *testing.T) { 15 | assert.Equal(t, Max(1, 5), int64(5)) 16 | assert.Equal(t, Max(22, 5), int64(22)) 17 | } 18 | 19 | func TestToDecimal(t *testing.T) { 20 | assert.Equal(t, ToDecimal("0", 18), "0") 21 | assert.Equal(t, ToDecimal("100", 1), "10") 22 | assert.Equal(t, ToDecimal("123123", 3), "123.123") 23 | assert.Equal(t, ToDecimal("10012000000000000", 12), "10012") 24 | assert.Equal(t, ToDecimal("123456789012345678901", 18), "123.4567890123") 25 | assert.Equal(t, ToDecimal("4618", 6), "0.004618") 26 | assert.Equal(t, ToDecimal("218218", 8), "0.00218218") 27 | assert.Equal(t, ToDecimal("212880628", 9), "0.212880628") 28 | assert.Equal(t, ToDecimal("4634460765323682", 18), "0.0046344608") 29 | assert.Equal(t, ToDecimal("100000000000", 8), "1000") 30 | assert.Equal(t, ToDecimal("5000000000", 8), "50") 31 | } 32 | 33 | func TestFromDecimal(t *testing.T) { 34 | assert.Equal(t, FromDecimal("100.12"), "10012") 35 | } 36 | 37 | func TestToDecimalExp(t *testing.T) { 38 | assert.Equal(t, FromDecimalExp("10", 1), "100") 39 | assert.Equal(t, FromDecimalExp("100", 1), "1000") 40 | assert.Equal(t, FromDecimalExp("10012", 12), "10012000000000000") 41 | assert.Equal(t, FromDecimalExp("123.123", 3), "123123") 42 | //assert.Equal(t, FromDecimalExp("0.005170630816959669", 2), "") Need fix 43 | assert.Equal(t, FromDecimalExp("0.000180508184692364", 4), "1") 44 | assert.Equal(t, FromDecimalExp("0.004618071835862274", 6), "4618") 45 | assert.Equal(t, FromDecimalExp("0.00216013705800604", 8), "216013") 46 | assert.Equal(t, FromDecimalExp("0.002182187913804679", 8), "218218") 47 | assert.Equal(t, FromDecimalExp("0.21288062808828456", 9), "212880628") 48 | assert.Equal(t, FromDecimalExp("0.004634460765323682", 18), "4634460765323682") 49 | assert.Equal(t, FromDecimalExp("0.00000001", 8), "1") 50 | assert.Equal(t, FromDecimalExp("10.00000000", 8), "1000000000") 51 | } 52 | 53 | func TestFloat64toPrecision(t *testing.T) { 54 | assert.Equal(t, Float64toPrecision(3.643005, 4), 3.6430) 55 | assert.Equal(t, Float64toPrecision(9.8233168e-5, 4), 0.0001) 56 | assert.Equal(t, Float64toPrecision(0.8010, 4), 0.8010) 57 | assert.Equal(t, Float64toPrecision(26.5, 4), 26.5) 58 | assert.Equal(t, Float64toPrecision(3374, 4), 3374.0) 59 | } 60 | 61 | func TestFloat64toString(t *testing.T) { 62 | assert.Equal(t, Float64toString(0), "0") 63 | assert.Equal(t, Float64toString(0.0), "0") 64 | assert.Equal(t, Float64toString(0.1), "0.1") 65 | assert.Equal(t, Float64toString(0.1010), "0.101") 66 | assert.Equal(t, Float64toString(0.015), "0.015") 67 | assert.Equal(t, Float64toString(1), "1") 68 | assert.Equal(t, Float64toString(1.1), "1.1") 69 | assert.Equal(t, Float64toString(1.015), "1.015") 70 | assert.Equal(t, Float64toString(2800.00000000), "2800") 71 | assert.Equal(t, Float64toString(0.00037500), "0.000375") 72 | } 73 | 74 | func TestStringNumberToFloat64(t *testing.T) { 75 | var tests = []struct { 76 | stringNumber string 77 | expect float64 78 | ecpectErr bool 79 | }{ 80 | {"0.29970000", 0.2997, false}, 81 | {"0.00037500", 0.000375, false}, 82 | {"1.0", 1, false}, 83 | {"1", 1, false}, 84 | {"0", 0, false}, 85 | {"23.12", 23.120, true}, 86 | } 87 | 88 | for _, tt := range tests { 89 | t.Run("", func(t *testing.T) { 90 | actual, err := StringNumberToFloat64(tt.stringNumber) 91 | if tt.ecpectErr { 92 | assert.Nil(t, err) 93 | } else { 94 | assert.Equal(t, tt.expect, actual) 95 | } 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /slice/batch.go: -------------------------------------------------------------------------------- 1 | package slice 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | ) 7 | 8 | type Batch[T any] struct { 9 | values []T 10 | } 11 | 12 | func NewBatch[T any](values ...T) Batch[T] { 13 | return Batch[T]{ 14 | values: values, 15 | } 16 | } 17 | 18 | func (b Batch[T]) GetChunks(size int) [][]T { 19 | if size <= 0 { 20 | return nil 21 | } 22 | 23 | resultLength := (len(b.values) + size - 1) / size 24 | result := make([][]T, resultLength) 25 | lo, hi := 0, size 26 | for i := range result { 27 | if hi > len(b.values) { 28 | hi = len(b.values) 29 | } 30 | result[i] = b.values[lo:hi:hi] 31 | lo, hi = hi, hi+size 32 | } 33 | return result 34 | } 35 | 36 | func GetChunks(slice interface{}, size uint) ([][]interface{}, error) { 37 | interfaceSlice, err := GetInterfaceSlice(slice) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return GetInterfaceSliceBatch(interfaceSlice, size), nil 43 | } 44 | 45 | func GetInterfaceSlice(slice interface{}) ([]interface{}, error) { 46 | s := reflect.ValueOf(slice) 47 | if s.Kind() != reflect.Slice { 48 | return nil, errors.New("InterfaceSlice() given a non-slice type") 49 | } 50 | 51 | ret := make([]interface{}, s.Len()) 52 | 53 | for i := 0; i < s.Len(); i++ { 54 | ret[i] = s.Index(i).Interface() 55 | } 56 | 57 | return ret, nil 58 | } 59 | 60 | func GetInterfaceSliceBatch(values []interface{}, sizeUint uint) (chunks [][]interface{}) { 61 | size := int(sizeUint) 62 | resultLength := (len(values) + size - 1) / size 63 | result := make([][]interface{}, resultLength) 64 | lo, hi := 0, size 65 | for i := range result { 66 | if hi > len(values) { 67 | hi = len(values) 68 | } 69 | result[i] = values[lo:hi:hi] 70 | lo, hi = hi, hi+size 71 | } 72 | return result 73 | //shorter version (https://gist.github.com/mustafaturan/7a29e8251a7369645fb6c2965f8c2daf) 74 | //for int(sizeUint) < len(values) { 75 | // values, chunks = values[sizeUint:], append(chunks, values[0:sizeUint:sizeUint]) 76 | //} 77 | //return append(chunks, values) 78 | } 79 | -------------------------------------------------------------------------------- /slice/batch_test.go: -------------------------------------------------------------------------------- 1 | package slice 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestBatch_GetChunks(t *testing.T) { 9 | type testCase[T any] struct { 10 | name string 11 | b Batch[T] 12 | size int 13 | want [][]T 14 | } 15 | tests := []testCase[int]{ 16 | { 17 | name: "wrong input", 18 | b: NewBatch[int](1, 2, 3), 19 | size: 0, 20 | want: nil, 21 | }, 22 | { 23 | name: "single", 24 | b: NewBatch[int](1, 2, 3), 25 | size: 5, 26 | want: [][]int{ 27 | { 28 | 1, 2, 3, 29 | }, 30 | }, 31 | }, 32 | { 33 | name: "multiple", 34 | b: NewBatch[int](1, 2, 3), 35 | size: 2, 36 | want: [][]int{ 37 | { 38 | 1, 2, 39 | }, 40 | { 41 | 3, 42 | }, 43 | }, 44 | }, 45 | } 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | if got := tt.b.GetChunks(tt.size); !reflect.DeepEqual(got, tt.want) { 49 | t.Errorf("GetChunks() = %v, want %v", got, tt.want) 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /types/asset.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Asset struct { 4 | Id string `json:"asset"` 5 | Name string `json:"name"` 6 | Symbol string `json:"symbol"` 7 | Type TokenType `json:"type"` 8 | Decimals uint `json:"decimals"` 9 | } 10 | 11 | func GetAssetsIds(assets []Token) []string { 12 | assetIds := make([]string, 0) 13 | for _, asset := range assets { 14 | assetIds = append(assetIds, asset.AssetId()) 15 | } 16 | return assetIds 17 | } 18 | -------------------------------------------------------------------------------- /types/chain.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/trustwallet/go-primitives/coin" 7 | ) 8 | 9 | // nolint:cyclop 10 | func GetChainFromAssetType(assetType string) (coin.Coin, error) { 11 | switch TokenType(assetType) { 12 | case BRC20: 13 | return coin.Bitcoin(), nil 14 | case ERC20: 15 | return coin.Ethereum(), nil 16 | case BEP2, BEP8: 17 | return coin.Binance(), nil 18 | case BEP20: 19 | return coin.Smartchain(), nil 20 | case ETC20: 21 | return coin.Classic(), nil 22 | case TRC10, TRC20: 23 | return coin.Tron(), nil 24 | case WAN20: 25 | return coin.Wanchain(), nil 26 | case TT20: 27 | return coin.Thundertoken(), nil 28 | case SPL: 29 | return coin.Solana(), nil 30 | case EOS: 31 | return coin.Eos(), nil 32 | case GO20: 33 | return coin.Gochain(), nil 34 | case KAVA: 35 | return coin.Kava(), nil 36 | case NEP5: 37 | return coin.Neo(), nil 38 | case NRC20: 39 | return coin.Nuls(), nil 40 | case VET: 41 | return coin.Vechain(), nil 42 | case ONTOLOGY: 43 | return coin.Ontology(), nil 44 | case THETA: 45 | return coin.Theta(), nil 46 | case TOMO, TRC21: 47 | return coin.Tomochain(), nil 48 | case XDAI: 49 | return coin.Xdai(), nil 50 | case WAVES: 51 | return coin.Waves(), nil 52 | case POA, POA20: 53 | return coin.Poa(), nil 54 | case POLYGON: 55 | return coin.Polygon(), nil 56 | case OPTIMISM: 57 | return coin.Optimism(), nil 58 | case AVALANCHE: 59 | return coin.Avalanchec(), nil 60 | case ARBITRUM: 61 | return coin.Arbitrum(), nil 62 | case FANTOM: 63 | return coin.Fantom(), nil 64 | case TERRA, CW20: 65 | return coin.Terra(), nil 66 | case RONIN: 67 | return coin.Ronin(), nil 68 | case CELO: 69 | return coin.Celo(), nil 70 | case HRC20: 71 | return coin.Heco(), nil 72 | case CLO20: 73 | return coin.Callisto(), nil 74 | case ESDT: 75 | return coin.Elrond(), nil 76 | case OASIS: 77 | return coin.Oasis(), nil 78 | case CRC20: 79 | return coin.Cronos(), nil 80 | case STELLAR: 81 | return coin.Stellar(), nil 82 | case KRC20: 83 | return coin.Kcc(), nil 84 | case AURORA: 85 | return coin.Aurora(), nil 86 | case ALGORAND: 87 | return coin.Algorand(), nil 88 | case KAVAEVM: 89 | return coin.Kavaevm(), nil 90 | case METER: 91 | return coin.Meter(), nil 92 | case EVMOS_ERC20: 93 | return coin.Evmos(), nil 94 | case KIP20: 95 | return coin.Okc(), nil 96 | case APTOS, APTOSFA: 97 | return coin.Aptos(), nil 98 | case MOONBEAM: 99 | return coin.Moonbeam(), nil 100 | case KLAYTN: 101 | return coin.Klaytn(), nil 102 | case METIS: 103 | return coin.Metis(), nil 104 | case MOONRIVER: 105 | return coin.Moonriver(), nil 106 | case BOBA: 107 | return coin.Boba(), nil 108 | case JETTON: 109 | return coin.Ton(), nil 110 | case POLYGONZKEVM: 111 | return coin.Polygonzkevm(), nil 112 | case ZKSYNC: 113 | return coin.Zksync(), nil 114 | case SUI: 115 | return coin.Sui(), nil 116 | case STRIDE: 117 | return coin.Stride(), nil 118 | case NEUTRON: 119 | return coin.Neutron(), nil 120 | case FA2: 121 | return coin.Tezos(), nil 122 | case CONFLUX: 123 | return coin.Cfxevm(), nil 124 | case ACA: 125 | return coin.Acala(), nil 126 | case ACALAEVM: 127 | return coin.Acalaevm(), nil 128 | case BASE: 129 | return coin.Base(), nil 130 | case AKASH: 131 | return coin.Akash(), nil 132 | case AGORIC: 133 | return coin.Agoric(), nil 134 | case AXELAR: 135 | return coin.Axelar(), nil 136 | case JUNO: 137 | return coin.Juno(), nil 138 | case SEI: 139 | return coin.Sei(), nil 140 | case CARDANO: 141 | return coin.Cardano(), nil 142 | case NEON: 143 | return coin.Neon(), nil 144 | case OSMOSIS: 145 | return coin.Osmosis(), nil 146 | case NATIVEINJECTIVE: 147 | return coin.Nativeinjective(), nil 148 | case NATIVEEVMOS: 149 | return coin.Nativeevmos(), nil 150 | case CRYPTOORG: 151 | return coin.Cryptoorg(), nil 152 | case COSMOS: 153 | return coin.Cosmos(), nil 154 | case OPBNB: 155 | return coin.Opbnb(), nil 156 | case LINEA: 157 | return coin.Linea(), nil 158 | case STARGAZE: 159 | return coin.Stargaze(), nil 160 | case MANTLE: 161 | return coin.Mantle(), nil 162 | case MANTA: 163 | return coin.Manta(), nil 164 | case ZETACHAIN: 165 | return coin.Zetachain(), nil 166 | case ZETAEVM: 167 | return coin.Zetaevm(), nil 168 | case MERLIN: 169 | return coin.Merlin(), nil 170 | case BLAST: 171 | return coin.Blast(), nil 172 | case SCROLL: 173 | return coin.Scroll(), nil 174 | case ICP: 175 | return coin.Internet_computer(), nil 176 | case BOUNCEBIT: 177 | return coin.Bouncebit(), nil 178 | case ZKLINKNOVA: 179 | return coin.Zklinknova(), nil 180 | case XRP: 181 | return coin.Ripple(), nil 182 | case SONIC: 183 | return coin.Sonic(), nil 184 | 185 | } 186 | 187 | return coin.Coin{}, errors.New("unknown asset type: " + assetType) 188 | } 189 | -------------------------------------------------------------------------------- /types/chain_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/trustwallet/go-primitives/coin" 8 | ) 9 | 10 | func TestGetChainFromAssetType(t *testing.T) { 11 | type args struct { 12 | type_ string 13 | } 14 | 15 | tests := []struct { 16 | name string 17 | args args 18 | want coin.Coin 19 | wantErr bool 20 | }{ 21 | { 22 | name: "Test ERC20", 23 | args: args{ 24 | type_: "ERC20", 25 | }, 26 | want: coin.Ethereum(), 27 | wantErr: false, 28 | }, 29 | { 30 | name: "Test custom chain type", 31 | args: args{ 32 | type_: "UNKNOWN20", 33 | }, 34 | want: coin.Coin{}, 35 | wantErr: true, 36 | }, 37 | { 38 | name: "Test TRC20", 39 | args: args{ 40 | type_: "TRC20", 41 | }, 42 | want: coin.Tron(), 43 | wantErr: false, 44 | }, 45 | { 46 | name: "Test TRC10", 47 | args: args{ 48 | type_: "TRC10", 49 | }, 50 | want: coin.Tron(), 51 | wantErr: false, 52 | }, 53 | { 54 | name: "Test TRC10", 55 | args: args{ 56 | type_: "TRC10", 57 | }, 58 | want: coin.Tron(), 59 | wantErr: false, 60 | }, 61 | { 62 | name: "Test TOMO", 63 | args: args{ 64 | type_: "TOMO", 65 | }, 66 | want: coin.Tomochain(), 67 | wantErr: false, 68 | }, 69 | { 70 | name: "Test TRC21", 71 | args: args{ 72 | type_: "TRC21", 73 | }, 74 | want: coin.Tomochain(), 75 | wantErr: false, 76 | }, 77 | { 78 | name: "Test STELLAR", 79 | args: args{ 80 | type_: "STELLAR", 81 | }, 82 | want: coin.Stellar(), 83 | wantErr: false, 84 | }, 85 | { 86 | name: "Test Conflux eSpace", 87 | args: args{ 88 | type_: "CONFLUX", 89 | }, 90 | want: coin.Cfxevm(), 91 | wantErr: false, 92 | }, 93 | { 94 | name: "Test APTOS (legacy)", 95 | args: args{ 96 | type_: "APTOS", 97 | }, 98 | want: coin.Aptos(), 99 | wantErr: false, 100 | }, 101 | { 102 | name: "Test APTOSFA (fungible asset)", 103 | args: args{ 104 | type_: "APTOSFA", 105 | }, 106 | want: coin.Aptos(), 107 | wantErr: false, 108 | }, 109 | { 110 | name: "Test XRP", 111 | args: args{ 112 | type_: "XRP", 113 | }, 114 | want: coin.Ripple(), 115 | wantErr: false, 116 | }, 117 | } 118 | for _, tt := range tests { 119 | t.Run(tt.name, func(t *testing.T) { 120 | got, err := GetChainFromAssetType(tt.args.type_) 121 | if (err != nil) != tt.wantErr { 122 | t.Errorf("GetCoinForId() error = %v, wantErr %v", err, tt.wantErr) 123 | return 124 | } 125 | if !reflect.DeepEqual(got, tt.want) { 126 | t.Errorf("GetCoinForId() got = %v, want %v", got, tt.want) 127 | } 128 | }) 129 | } 130 | } 131 | 132 | func TestGetChainFromAssetTypeFullness(t *testing.T) { 133 | for _, tokenType := range GetTokenTypes() { 134 | if tokenType == ERC721 || tokenType == ERC1155 { 135 | continue 136 | } 137 | 138 | _, err := GetChainFromAssetType(string(tokenType)) 139 | if err != nil { 140 | t.Error(err) 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /types/chainid.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | const ( 4 | ChainIDEthereum = 1 5 | ChainIDOptimism = 10 6 | ChainIDSmartChain = 56 7 | ChainIDPolygon = 137 8 | ChainIDArbitrum = 42161 9 | ChainIDGnosis = 100 10 | ChainIDAvalanche = 43114 11 | ChainIDFantom = 250 12 | ChainIDMoonbeam = 1284 13 | ChainIDKlaytn = 8217 14 | ChainIDMetis = 1088 15 | ChainIDMoonriver = 1285 16 | ChainIDBoba = 288 17 | ChainIDTon = 607 18 | ChainIDZKEVM = 1101 19 | ChainIDZKSync = 324 20 | ChainIDIoTeXEVM = 4689 21 | ChainIDCFXEVM = 1030 22 | ) 23 | -------------------------------------------------------------------------------- /types/collectibles.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | var ( 4 | ExtensionMimetypes = map[string]string{ 5 | "png": "image/png", 6 | "jpg": "image/jpg", 7 | "jpeg": "image/jpg", 8 | "gif": "image/gif", 9 | } 10 | ) 11 | 12 | type ( 13 | Collection struct { 14 | Id string `json:"id"` 15 | Name string `json:"name"` 16 | ImageUrl string `json:"image_url"` 17 | Description string `json:"description"` 18 | ExternalLink string `json:"external_link"` 19 | Total int `json:"total"` 20 | Address string `json:"address"` 21 | Coin uint `json:"coin"` 22 | Type string `json:"-"` 23 | PreviewImageURL *CollectibleMedia `json:"preview_image_url,omitempty"` 24 | OriginalSourceURL CollectibleMedia `json:"original_source_url"` 25 | } 26 | 27 | CollectionPage []Collection 28 | 29 | CollectibleTransferFee struct { 30 | Asset string `json:"asset"` 31 | Amount string `json:"amount"` 32 | } 33 | 34 | CollectibleMedia struct { 35 | Mimetype string `json:"mimetype,omitempty"` 36 | URL string `json:"url"` 37 | } 38 | 39 | Collectible struct { 40 | ID string `json:"id"` 41 | CollectionID string `json:"collection_id"` 42 | TokenID string `json:"token_id"` 43 | ContractAddress string `json:"contract_address"` 44 | Category string `json:"category"` 45 | ImageUrl string `json:"image_url"` 46 | ExternalLink string `json:"external_link"` 47 | ProviderLink string `json:"provider_link"` 48 | Type string `json:"type"` 49 | Description string `json:"description"` 50 | Coin uint `json:"coin"` 51 | Name string `json:"name"` 52 | Version string `json:"nft_version"` 53 | TransferFee *CollectibleTransferFee `json:"transfer_fee,omitempty"` 54 | PreviewImageURL *CollectibleMedia `json:"preview_image_url,omitempty"` 55 | OriginalSourceURL CollectibleMedia `json:"original_source_url"` 56 | Properties []CollectibleProperty `json:"properties"` 57 | About string `json:"about"` 58 | Balance string `json:"balance"` 59 | } 60 | 61 | CollectibleProperty struct { 62 | Key string `json:"key"` 63 | Value string `json:"value"` 64 | Rarity float32 `json:"rarity,omitempty"` 65 | } 66 | 67 | CollectiblePage []Collectible 68 | ) 69 | -------------------------------------------------------------------------------- /types/marshal.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | // Tx, but with default JSON marshalling methods 10 | type wrappedTx Tx 11 | 12 | // UnmarshalJSON creates a transaction along with metadata from a JSON object. 13 | // Fails if the meta object can't be read. 14 | func (t *Tx) UnmarshalJSON(data []byte) error { 15 | // Wrap the Tx type to avoid infinite recursion 16 | var wrapped wrappedTx 17 | 18 | var raw json.RawMessage 19 | wrapped.Metadata = &raw 20 | if err := json.Unmarshal(data, &wrapped); err != nil { 21 | return err 22 | } 23 | 24 | *t = Tx(wrapped) 25 | 26 | switch t.Type { 27 | case TxTransfer, TxStakeDelegate, TxStakeUndelegate, TxStakeRedelegate, TxStakeClaimRewards, TxStakeCompound: 28 | t.Metadata = new(Transfer) 29 | case TxContractCall: 30 | t.Metadata = new(ContractCall) 31 | case TxSwap: 32 | t.Metadata = new(Swap) 33 | case TxTransferNFT: 34 | t.Metadata = new(TransferNFT) 35 | default: 36 | return fmt.Errorf("unsupported tx type: %s, hash: %s, metadata: %+v", t.Type, t.ID, t.Metadata) 37 | } 38 | 39 | err := json.Unmarshal(raw, t.Metadata) 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | } 45 | 46 | // MarshalJSON creates a JSON object from a transaction. 47 | func (t Tx) MarshalJSON() ([]byte, error) { 48 | isTypeOk := false 49 | for _, txType := range SupportedTypes { 50 | if t.Type == txType { 51 | isTypeOk = true 52 | break 53 | } 54 | } 55 | if !isTypeOk { 56 | return nil, fmt.Errorf("tx type is not supported: %v", t) 57 | } 58 | 59 | // validate metadata type 60 | switch t.Metadata.(type) { 61 | case *Transfer, *ContractCall, *Swap, *TransferNFT: 62 | break 63 | default: 64 | return nil, errors.New("unsupported tx metadata") 65 | } 66 | 67 | // Set status to completed by default 68 | if t.Status == "" { 69 | t.Status = StatusCompleted 70 | } 71 | 72 | // Wrap the Tx type to avoid infinite recursion 73 | return json.Marshal(wrappedTx(t)) 74 | } 75 | 76 | // Sort sorts the response by date, descending 77 | func (txs Txs) Len() int { return len(txs) } 78 | func (txs Txs) Less(i, j int) bool { return txs[i].CreatedAt > txs[j].CreatedAt } 79 | func (txs Txs) Swap(i, j int) { txs[i], txs[j] = txs[j], txs[i] } 80 | -------------------------------------------------------------------------------- /types/marshal_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestTxMarshalling(t *testing.T) { 11 | tests := []struct { 12 | Name string 13 | Type TransactionType 14 | Metadata interface{} 15 | marshalErr assert.ErrorAssertionFunc 16 | unmarshalErr assert.ErrorAssertionFunc 17 | }{ 18 | { 19 | Name: "transfer", 20 | Type: TxTransfer, 21 | Metadata: &Transfer{}, 22 | marshalErr: assert.NoError, 23 | unmarshalErr: assert.NoError, 24 | }, 25 | { 26 | Name: "contract_call", 27 | Type: TxContractCall, 28 | Metadata: &ContractCall{}, 29 | marshalErr: assert.NoError, 30 | unmarshalErr: assert.NoError, 31 | }, 32 | { 33 | Name: "swap", 34 | Type: TxSwap, 35 | Metadata: &Swap{}, 36 | marshalErr: assert.NoError, 37 | unmarshalErr: assert.NoError, 38 | }, 39 | { 40 | Name: "claim_rewards", 41 | Type: TxStakeClaimRewards, 42 | Metadata: &Transfer{}, 43 | marshalErr: assert.NoError, 44 | unmarshalErr: assert.NoError, 45 | }, 46 | { 47 | Name: "delegate", 48 | Type: TxStakeDelegate, 49 | Metadata: &Transfer{}, 50 | marshalErr: assert.NoError, 51 | unmarshalErr: assert.NoError, 52 | }, 53 | { 54 | Name: "undelegate", 55 | Type: TxStakeUndelegate, 56 | Metadata: &Transfer{}, 57 | marshalErr: assert.NoError, 58 | unmarshalErr: assert.NoError, 59 | }, 60 | { 61 | Name: "redelegate", 62 | Type: TxStakeRedelegate, 63 | Metadata: &Transfer{}, 64 | marshalErr: assert.NoError, 65 | unmarshalErr: assert.NoError, 66 | }, 67 | { 68 | Name: "compound", 69 | Type: TxStakeCompound, 70 | Metadata: &Transfer{}, 71 | marshalErr: assert.NoError, 72 | unmarshalErr: assert.NoError, 73 | }, 74 | { 75 | Name: "without_type", 76 | Metadata: &Transfer{}, 77 | marshalErr: assert.Error, 78 | unmarshalErr: assert.Error, 79 | }, 80 | { 81 | Name: "unsupported_type", 82 | Metadata: &Transfer{}, 83 | marshalErr: assert.Error, 84 | unmarshalErr: assert.Error, 85 | }, 86 | } 87 | 88 | for _, tc := range tests { 89 | t.Run(tc.Name, func(t *testing.T) { 90 | tx := Tx{ 91 | Type: tc.Type, 92 | Metadata: tc.Metadata, 93 | } 94 | 95 | data, err := json.Marshal(tx) 96 | tc.marshalErr(t, err) 97 | 98 | var receiver Tx 99 | err = json.Unmarshal(data, &receiver) 100 | tc.unmarshalErr(t, err) 101 | }) 102 | } 103 | } 104 | 105 | func TestTxsMarshalling(t *testing.T) { 106 | tests := []struct { 107 | Name string 108 | Type TransactionType 109 | Metadata interface{} 110 | marshalErr assert.ErrorAssertionFunc 111 | unmarshalErr assert.ErrorAssertionFunc 112 | expectNil bool 113 | }{ 114 | { 115 | Name: "transfer", 116 | Type: TxTransfer, 117 | Metadata: &Transfer{}, 118 | marshalErr: assert.NoError, 119 | unmarshalErr: assert.NoError, 120 | }, 121 | { 122 | Name: "contract_call", 123 | Type: TxContractCall, 124 | Metadata: &ContractCall{}, 125 | marshalErr: assert.NoError, 126 | unmarshalErr: assert.NoError, 127 | }, 128 | { 129 | Name: "claim_rewards", 130 | Type: TxStakeClaimRewards, 131 | Metadata: &Transfer{}, 132 | marshalErr: assert.NoError, 133 | unmarshalErr: assert.NoError, 134 | }, 135 | { 136 | Name: "delegate", 137 | Type: TxStakeDelegate, 138 | Metadata: &Transfer{}, 139 | marshalErr: assert.NoError, 140 | unmarshalErr: assert.NoError, 141 | }, 142 | { 143 | Name: "undelegate", 144 | Type: TxStakeUndelegate, 145 | Metadata: &Transfer{}, 146 | marshalErr: assert.NoError, 147 | unmarshalErr: assert.NoError, 148 | }, 149 | { 150 | Name: "redelegate", 151 | Type: TxStakeRedelegate, 152 | Metadata: &Transfer{}, 153 | marshalErr: assert.NoError, 154 | unmarshalErr: assert.NoError, 155 | }, 156 | { 157 | Name: "redelegate", 158 | Type: TxStakeRedelegate, 159 | Metadata: &Transfer{}, 160 | marshalErr: assert.NoError, 161 | unmarshalErr: assert.NoError, 162 | }, 163 | { 164 | Name: "without_type", 165 | Metadata: &Transfer{}, 166 | marshalErr: assert.Error, 167 | unmarshalErr: assert.Error, 168 | expectNil: true, 169 | }, 170 | { 171 | Name: "unsupported_type", 172 | Metadata: &Transfer{}, 173 | marshalErr: assert.Error, 174 | unmarshalErr: assert.Error, 175 | expectNil: true, 176 | }, 177 | } 178 | 179 | for _, tc := range tests { 180 | t.Run(tc.Name, func(t *testing.T) { 181 | txs := Txs{{ 182 | Type: tc.Type, 183 | Metadata: tc.Metadata, 184 | Status: StatusCompleted, 185 | }} 186 | 187 | data, err := json.Marshal(txs) 188 | tc.marshalErr(t, err) 189 | 190 | var receiver Txs 191 | err = json.Unmarshal(data, &receiver) 192 | tc.unmarshalErr(t, err) 193 | 194 | if tc.expectNil { 195 | assert.Equal(t, Txs(nil), receiver) 196 | } else { 197 | assert.Equal(t, txs[0], receiver[0]) 198 | } 199 | }) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /types/subscription.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type ( 8 | Subscriptions map[string][]string 9 | 10 | SubscriptionOperation string 11 | 12 | SubscriptionEvent struct { 13 | Subscriptions Subscriptions `json:"subscriptions"` 14 | Operation SubscriptionOperation `json:"operation"` 15 | } 16 | 17 | Subscription struct { 18 | Coin uint `json:"coin"` 19 | Address string `json:"address"` 20 | } 21 | 22 | TransactionNotification struct { 23 | Action TransactionType `json:"action"` 24 | Result Tx `json:"result"` 25 | } 26 | ) 27 | 28 | type Subscriber string 29 | 30 | const ( 31 | Notifications Subscriber = "notifications" 32 | AddSubscription SubscriptionOperation = "AddSubscription" 33 | DeleteSubscription SubscriptionOperation = "DeleteSubscription" 34 | ) 35 | 36 | func (v *Subscription) AddressID() string { 37 | return GetAddressID(strconv.Itoa(int(v.Coin)), v.Address) 38 | } 39 | 40 | func GetAddressID(coin, address string) string { 41 | return coin + "_" + address 42 | } 43 | 44 | func (e *SubscriptionEvent) ParseSubscriptions(s Subscriptions) []Subscription { 45 | subs := make([]Subscription, 0) 46 | for coinStr, perCoin := range s { 47 | coin, err := strconv.Atoi(coinStr) 48 | if err != nil { 49 | continue 50 | } 51 | for _, addr := range perCoin { 52 | subs = append(subs, Subscription{ 53 | Coin: uint(coin), 54 | Address: addr, 55 | }) 56 | } 57 | } 58 | return subs 59 | } 60 | -------------------------------------------------------------------------------- /types/subscription_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_parseSubscriptions(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | subscriptions SubscriptionEvent 14 | wantSubs []Subscription 15 | }{ 16 | { 17 | name: "guid with 1 coin", 18 | subscriptions: SubscriptionEvent{ 19 | Subscriptions: Subscriptions{ 20 | "0": {"xpub6BpYi6J1GZzfY3yY7DbhLLccF3efQa18nQngM3jaehgtNSoEgk6UtPULpC3oK5oA3trczY8Ld34LFw1USMPfGHwTEizdD5QyGcMyuh2UoBA", "xpub6CYwPfnPJLPquufPkb98coSb3mdy1CgaZrWUtYWGJTJ4VWZUbzH9HLGy7nHpP7DG4UdTkYYpirkTWQSP7pWHsrk24Nos5oYNHpfr4BgPVTL"}, 21 | }, 22 | }, 23 | wantSubs: []Subscription{ 24 | { 25 | Coin: 0, 26 | Address: "xpub6BpYi6J1GZzfY3yY7DbhLLccF3efQa18nQngM3jaehgtNSoEgk6UtPULpC3oK5oA3trczY8Ld34LFw1USMPfGHwTEizdD5QyGcMyuh2UoBA", 27 | }, 28 | { 29 | Coin: 0, 30 | Address: "xpub6CYwPfnPJLPquufPkb98coSb3mdy1CgaZrWUtYWGJTJ4VWZUbzH9HLGy7nHpP7DG4UdTkYYpirkTWQSP7pWHsrk24Nos5oYNHpfr4BgPVTL", 31 | }, 32 | }, 33 | }, 34 | { 35 | name: "guid with 2 coins", 36 | subscriptions: SubscriptionEvent{ 37 | Subscriptions: Subscriptions{ 38 | "2": {"zpub6rH4MwgyTmuexAX6HAraks5cKv5BbtmwdLirvnU5845ovUJb4abgjt9DtXK4ZEaToRrNj8dQznuLC6Nka4eMviGMinCVMUxKLpuyddcG9Vc"}, 39 | "0": {"xpub6BpYi6J1GZzfY3yY7DbhLLccF3efQa18nQngM3jaehgtNSoEgk6UtPULpC3oK5oA3trczY8Ld34LFw1USMPfGHwTEizdD5QyGcMyuh2UoBA", "xpub6CYwPfnPJLPquufPkb98coSb3mdy1CgaZrWUtYWGJTJ4VWZUbzH9HLGy7nHpP7DG4UdTkYYpirkTWQSP7pWHsrk24Nos5oYNHpfr4BgPVTL"}, 40 | }, 41 | }, 42 | wantSubs: []Subscription{ 43 | { 44 | Coin: 2, 45 | Address: "zpub6rH4MwgyTmuexAX6HAraks5cKv5BbtmwdLirvnU5845ovUJb4abgjt9DtXK4ZEaToRrNj8dQznuLC6Nka4eMviGMinCVMUxKLpuyddcG9Vc", 46 | }, 47 | { 48 | Coin: 0, 49 | Address: "xpub6BpYi6J1GZzfY3yY7DbhLLccF3efQa18nQngM3jaehgtNSoEgk6UtPULpC3oK5oA3trczY8Ld34LFw1USMPfGHwTEizdD5QyGcMyuh2UoBA", 50 | }, 51 | { 52 | Coin: 0, 53 | Address: "xpub6CYwPfnPJLPquufPkb98coSb3mdy1CgaZrWUtYWGJTJ4VWZUbzH9HLGy7nHpP7DG4UdTkYYpirkTWQSP7pWHsrk24Nos5oYNHpfr4BgPVTL", 54 | }, 55 | }, 56 | }, 57 | } 58 | for _, tt := range tests { 59 | t.Run(tt.name, func(t *testing.T) { 60 | gotSubs := tt.subscriptions.ParseSubscriptions(tt.subscriptions.Subscriptions) 61 | sort.Slice(gotSubs, func(i, j int) bool { 62 | return gotSubs[i].Coin > gotSubs[j].Coin 63 | }) 64 | sort.Slice(tt.wantSubs, func(i, j int) bool { 65 | return tt.wantSubs[i].Coin > tt.wantSubs[j].Coin 66 | }) 67 | assert.EqualValues(t, tt.wantSubs, gotSubs) 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /types/token.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/trustwallet/go-primitives/asset" 10 | "github.com/trustwallet/go-primitives/coin" 11 | ) 12 | 13 | var ErrUnknownTokenType = errors.New("unknown token type") 14 | var errTokenVersionNotImplemented = errors.New("token version not implemented") 15 | 16 | type ( 17 | TokenType string 18 | TokenVersion int 19 | 20 | // Token describes the non-native tokens. 21 | // Examples: ERC-20, TRC-20, BEP-2 22 | Token struct { 23 | Name string `json:"name"` 24 | Symbol string `json:"symbol"` 25 | Decimals uint `json:"decimals"` 26 | TokenID string `json:"token_id"` 27 | Coin uint `json:"coin"` 28 | Type TokenType `json:"type"` 29 | } 30 | ) 31 | 32 | const ( 33 | Coin TokenType = "coin" 34 | Gas TokenType = "gas" 35 | BRC20 TokenType = "BRC20" 36 | ERC20 TokenType = "ERC20" 37 | ERC721 TokenType = "ERC721" 38 | ERC1155 TokenType = "ERC1155" 39 | BEP2 TokenType = "BEP2" 40 | BEP8 TokenType = "BEP8" 41 | BEP20 TokenType = "BEP20" 42 | TRC10 TokenType = "TRC10" 43 | ETC20 TokenType = "ETC20" 44 | POA20 TokenType = "POA20" 45 | TRC20 TokenType = "TRC20" 46 | TRC21 TokenType = "TRC21" 47 | CLO20 TokenType = "CLO20" 48 | GO20 TokenType = "GO20" 49 | WAN20 TokenType = "WAN20" 50 | TT20 TokenType = "TT20" 51 | KAVA TokenType = "KAVA" 52 | COSMOS TokenType = "COSMOS" 53 | CRYPTOORG TokenType = "CRYPTOORG" 54 | NATIVEEVMOS TokenType = "NATIVEEVMOS" 55 | NATIVEINJECTIVE TokenType = "INJECTIVE" 56 | STARGAZE TokenType = "STARGAZE" 57 | NEUTRON TokenType = "NEUTRON" 58 | OSMOSIS TokenType = "OSMOSIS" 59 | SPL TokenType = "SPL" 60 | POLYGON TokenType = "POLYGON" 61 | OPTIMISM TokenType = "OPTIMISM" 62 | XDAI TokenType = "XDAI" 63 | AVALANCHE TokenType = "AVALANCHE" 64 | FANTOM TokenType = "FANTOM" 65 | HRC20 TokenType = "HRC20" 66 | ARBITRUM TokenType = "ARBITRUM" 67 | TERRA TokenType = "TERRA" 68 | RONIN TokenType = "RONIN" 69 | EOS TokenType = "EOS" 70 | NEP5 TokenType = "NEP5" 71 | NRC20 TokenType = "NRC20" 72 | VET TokenType = "VET" 73 | ONTOLOGY TokenType = "ONTOLOGY" 74 | THETA TokenType = "THETA" 75 | TOMO TokenType = "TOMO" 76 | WAVES TokenType = "WAVES" 77 | POA TokenType = "POA" 78 | CELO TokenType = "CELO" 79 | ESDT TokenType = "ESDT" 80 | CW20 TokenType = "CW20" 81 | OASIS TokenType = "OASIS" 82 | CRC20 TokenType = "CRC20" 83 | STELLAR TokenType = "STELLAR" 84 | KRC20 TokenType = "KRC20" 85 | AURORA TokenType = "AURORA" 86 | ALGORAND TokenType = "ASA" 87 | KAVAEVM TokenType = "KAVAEVM" 88 | METER TokenType = "METER" 89 | EVMOS_ERC20 TokenType = "EVMOS_ERC20" 90 | KIP20 TokenType = "KIP20" 91 | APTOS TokenType = "APTOS" 92 | APTOSFA TokenType = "APTOSFA" 93 | MOONBEAM TokenType = "MOONBEAM" 94 | KLAYTN TokenType = "KAIA" 95 | METIS TokenType = "METIS" 96 | MOONRIVER TokenType = "MOONRIVER" 97 | BOBA TokenType = "BOBA" 98 | JETTON TokenType = "JETTON" 99 | POLYGONZKEVM TokenType = "ZKEVM" 100 | ZKSYNC TokenType = "ZKSYNC" 101 | SUI TokenType = "SUI" 102 | STRIDE TokenType = "STRIDE" 103 | FA2 TokenType = "FA2" 104 | CONFLUX TokenType = "CONFLUX" 105 | ACA TokenType = "ACA" 106 | ACALAEVM TokenType = "ACALAEVM" 107 | BASE TokenType = "BASE" 108 | AKASH TokenType = "AKT" 109 | AGORIC TokenType = "BLD" 110 | AXELAR TokenType = "AXL" 111 | JUNO TokenType = "JUNO" 112 | SEI TokenType = "SEI" 113 | CARDANO TokenType = "CARDANO" 114 | NEON TokenType = "NEON" 115 | IOTEXEVM TokenType = "XRC20" 116 | OPBNB TokenType = "OPBNB" 117 | LINEA TokenType = "LINEA" 118 | MANTLE TokenType = "MANTLE" 119 | MANTA TokenType = "MANTA" 120 | ZETACHAIN TokenType = "ZETACHAIN" 121 | ZETAEVM TokenType = "ZETAEVM" 122 | MERLIN TokenType = "MERLIN" 123 | BLAST TokenType = "BLAST" 124 | SCROLL TokenType = "SCROLL" 125 | ICP TokenType = "ICP" 126 | BOUNCEBIT TokenType = "BOUNCEBIT" 127 | ZKLINKNOVA TokenType = "ZKLINKNOVA" 128 | XRP TokenType = "XRP" 129 | SONIC TokenType = "SONIC" 130 | ) 131 | 132 | const ( 133 | TokenVersionV0 TokenVersion = 0 134 | TokenVersionV1 TokenVersion = 1 135 | TokenVersionV3 TokenVersion = 3 136 | TokenVersionV4 TokenVersion = 4 137 | TokenVersionV5 TokenVersion = 5 138 | TokenVersionV6 TokenVersion = 6 139 | TokenVersionV7 TokenVersion = 7 140 | TokenVersionV8 TokenVersion = 8 141 | TokenVersionV9 TokenVersion = 9 142 | TokenVersionV10 TokenVersion = 10 143 | TokenVersionV11 TokenVersion = 11 144 | TokenVersionV12 TokenVersion = 12 145 | TokenVersionV13 TokenVersion = 13 146 | TokenVersionV14 TokenVersion = 14 147 | TokenVersionV15 TokenVersion = 15 148 | TokenVersionV16 TokenVersion = 16 149 | TokenVersionV17 TokenVersion = 17 150 | TokenVersionV18 TokenVersion = 18 151 | TokenVersionV19 TokenVersion = 19 152 | TokenVersionV20 TokenVersion = 20 153 | TokenVersionV21 TokenVersion = 21 154 | TokenVersionV22 TokenVersion = 22 155 | TokenVersionUndefined TokenVersion = -1 156 | ) 157 | 158 | func GetTokenTypes() []TokenType { 159 | return []TokenType{ 160 | BRC20, 161 | ERC20, 162 | ERC721, 163 | ERC1155, 164 | BEP2, 165 | BEP8, 166 | BEP20, 167 | TRC10, 168 | ETC20, 169 | POA20, 170 | TRC20, 171 | TRC21, 172 | CLO20, 173 | GO20, 174 | WAN20, 175 | TT20, 176 | CW20, 177 | COSMOS, 178 | KAVA, 179 | JUNO, 180 | AGORIC, 181 | AKASH, 182 | AXELAR, 183 | CRYPTOORG, 184 | NATIVEEVMOS, 185 | NATIVEINJECTIVE, 186 | OSMOSIS, 187 | STARGAZE, 188 | SPL, 189 | POLYGON, 190 | OPTIMISM, 191 | XDAI, 192 | AVALANCHE, 193 | FANTOM, 194 | HRC20, 195 | ARBITRUM, 196 | TERRA, 197 | RONIN, 198 | EOS, 199 | NEP5, 200 | NRC20, 201 | VET, 202 | ONTOLOGY, 203 | THETA, 204 | TOMO, 205 | WAVES, 206 | POA, 207 | CELO, 208 | ESDT, 209 | OASIS, 210 | CRC20, 211 | STELLAR, 212 | KRC20, 213 | AURORA, 214 | ALGORAND, 215 | KAVAEVM, 216 | METER, 217 | EVMOS_ERC20, 218 | KIP20, 219 | APTOS, 220 | APTOSFA, 221 | MOONBEAM, 222 | KLAYTN, 223 | METIS, 224 | MOONRIVER, 225 | BOBA, 226 | JETTON, 227 | POLYGONZKEVM, 228 | ZKSYNC, 229 | SUI, 230 | STRIDE, 231 | NEUTRON, 232 | CONFLUX, 233 | ACA, 234 | BASE, 235 | SEI, 236 | CARDANO, 237 | NEON, 238 | OPBNB, 239 | LINEA, 240 | ACALAEVM, 241 | MANTLE, 242 | MANTA, 243 | ZETACHAIN, 244 | ZETAEVM, 245 | MERLIN, 246 | BLAST, 247 | SCROLL, 248 | ICP, 249 | BOUNCEBIT, 250 | ZKLINKNOVA, 251 | XRP, 252 | SONIC, 253 | } 254 | } 255 | 256 | func GetTokenType(c uint, tokenID string) (string, bool) { 257 | if coin.IsEVM(c) { 258 | tokenType, err := GetEthereumTokenTypeByIndex(c) 259 | if err != nil { 260 | return "", false 261 | } 262 | return string(tokenType), true 263 | } 264 | 265 | switch c { 266 | case coin.BITCOIN: 267 | return string(BRC20), true 268 | case coin.TRON: 269 | _, err := strconv.Atoi(tokenID) 270 | if err != nil { 271 | return string(TRC20), true 272 | } 273 | return string(TRC10), true 274 | case coin.TERRA: 275 | idSize := len(tokenID) 276 | if idSize == 44 { 277 | return string(CW20), true 278 | } 279 | return string(TERRA), true 280 | case coin.BINANCE: 281 | return string(BEP2), true 282 | case coin.WAVES: 283 | return string(WAVES), true 284 | case coin.THETA: 285 | return string(THETA), true 286 | case coin.ONTOLOGY: 287 | return string(ONTOLOGY), true 288 | case coin.NULS: 289 | return string(NRC20), true 290 | case coin.VECHAIN: 291 | return string(VET), true 292 | case coin.NEO: 293 | return string(NEP5), true 294 | case coin.EOS: 295 | return string(EOS), true 296 | case coin.SOLANA: 297 | return string(SPL), true 298 | case coin.HECO, coin.HARMONY: 299 | return string(HRC20), true 300 | case coin.OASIS: 301 | return string(OASIS), true 302 | case coin.STELLAR: 303 | return string(STELLAR), true 304 | case coin.ALGORAND: 305 | return string(ALGORAND), true 306 | case coin.KAVA: 307 | return string(KAVA), true 308 | case coin.COSMOS: 309 | return string(COSMOS), true 310 | case coin.CRYPTOORG: 311 | return string(CRYPTOORG), true 312 | case coin.NATIVEEVMOS: 313 | return string(NATIVEEVMOS), true 314 | case coin.NATIVEINJECTIVE: 315 | return string(NATIVEINJECTIVE), true 316 | case coin.STARGAZE: 317 | return string(STARGAZE), true 318 | case coin.NEUTRON: 319 | return string(NEUTRON), true 320 | case coin.OSMOSIS: 321 | return string(OSMOSIS), true 322 | case coin.CELO: 323 | return string(CELO), true 324 | case coin.ELROND: 325 | return string(ESDT), true 326 | case coin.EVMOS: 327 | return string(EVMOS_ERC20), true 328 | case coin.OKC: 329 | return string(KIP20), true 330 | case coin.APTOS: 331 | // TODO: improve this 332 | if strings.Contains(tokenID, "::") { 333 | return string(APTOS), true 334 | } 335 | return string(APTOSFA), true 336 | case coin.TON: 337 | return string(JETTON), true 338 | case coin.SUI: 339 | return string(SUI), true 340 | case coin.STRIDE: 341 | return string(STRIDE), true 342 | case coin.CFXEVM: 343 | return string(CONFLUX), true 344 | case coin.ACALA: 345 | return string(ACA), true 346 | case coin.AKASH: 347 | return string(AKASH), true 348 | case coin.AGORIC: 349 | return string(AGORIC), true 350 | case coin.AXELAR: 351 | return string(AXELAR), true 352 | case coin.JUNO: 353 | return string(JUNO), true 354 | case coin.SEI: 355 | return string(SEI), true 356 | case coin.CARDANO: 357 | return string(CARDANO), true 358 | case coin.NEON: 359 | return string(NEON), true 360 | case coin.MANTLE: 361 | return string(MANTLE), true 362 | case coin.MANTA: 363 | return string(MANTA), true 364 | case coin.ZETACHAIN: 365 | return string(ZETACHAIN), true 366 | case coin.ZETAEVM: 367 | return string(ZETAEVM), true 368 | case coin.BLAST: 369 | return string(BLAST), true 370 | case coin.SCROLL: 371 | return string(SCROLL), true 372 | case coin.INTERNET_COMPUTER: 373 | return string(ICP), true 374 | case coin.BOUNCEBIT: 375 | return string(BOUNCEBIT), true 376 | case coin.ZKLINKNOVA: 377 | return string(ZKLINKNOVA), true 378 | case coin.RIPPLE: 379 | return string(XRP), true 380 | 381 | default: 382 | return "", false 383 | } 384 | } 385 | 386 | func GetTokenVersion(tokenType string) (TokenVersion, error) { 387 | parsedTokenType, err := ParseTokenTypeFromString(tokenType) 388 | if err != nil { 389 | return TokenVersionUndefined, err 390 | } 391 | switch parsedTokenType { 392 | case ERC20, 393 | BEP2, 394 | BEP20, 395 | BEP8, 396 | ETC20, 397 | POA20, 398 | CLO20, 399 | TRC10, 400 | TRC21, 401 | WAN20, 402 | GO20, 403 | TT20, 404 | WAVES, 405 | APTOS: 406 | return TokenVersionV0, nil 407 | case TRC20: 408 | return TokenVersionV1, nil 409 | case SPL, KAVA: 410 | return TokenVersionV3, nil 411 | case POLYGON: 412 | return TokenVersionV4, nil 413 | case AVALANCHE, ARBITRUM, FANTOM, HRC20, OPTIMISM, XDAI: 414 | return TokenVersionV5, nil 415 | case TERRA: 416 | return TokenVersionV6, nil 417 | case CELO, NRC20: 418 | return TokenVersionV7, nil 419 | case CW20: 420 | return TokenVersionV8, nil 421 | case ESDT, CRC20: 422 | return TokenVersionV9, nil 423 | case KRC20, STELLAR: 424 | return TokenVersionV10, nil 425 | case RONIN, AURORA: 426 | return TokenVersionV11, nil 427 | case JETTON, POLYGONZKEVM, ZKSYNC, SUI: 428 | return TokenVersionV12, nil 429 | case BASE, AKASH, AGORIC, AXELAR, JUNO, SEI, OPBNB: 430 | return TokenVersionV13, nil 431 | case KAVAEVM, BOBA, METIS, NEON, LINEA, ACA, ACALAEVM, CONFLUX, IOTEXEVM, KLAYTN, MOONRIVER, MOONBEAM, MANTLE, 432 | NATIVEINJECTIVE, MANTA, ZETACHAIN, ZETAEVM: 433 | return TokenVersionV14, nil 434 | case BRC20: 435 | return TokenVersionV16, nil 436 | case MERLIN, ICP: 437 | return TokenVersionV17, nil 438 | case BLAST, SCROLL: 439 | return TokenVersionV18, nil 440 | case BOUNCEBIT: 441 | return TokenVersionV19, nil 442 | case ZKLINKNOVA: 443 | return TokenVersionV20, nil 444 | case ERC721, ERC1155, EOS, NEP5, VET, ONTOLOGY, THETA, TOMO, POA, OASIS, ALGORAND, METER, EVMOS_ERC20, 445 | KIP20, STRIDE, NEUTRON, FA2, CARDANO, NATIVEEVMOS, CRYPTOORG, COSMOS, OSMOSIS, STARGAZE: 446 | return TokenVersionUndefined, nil 447 | case APTOSFA: 448 | return TokenVersionV21, nil 449 | case XRP, SONIC: 450 | return TokenVersionV22, nil 451 | default: 452 | // This should not happen, as it is guarded by TestGetTokenVersionImplementEverySupportedTokenTypes 453 | return TokenVersionUndefined, fmt.Errorf("tokenType %s: %w", parsedTokenType, errTokenVersionNotImplemented) 454 | } 455 | } 456 | 457 | func ParseTokenTypeFromString(t string) (TokenType, error) { 458 | for _, knownTokenType := range GetTokenTypes() { 459 | if string(knownTokenType) == t { 460 | return knownTokenType, nil 461 | } 462 | } 463 | return "", fmt.Errorf("%s: %w", t, ErrUnknownTokenType) 464 | } 465 | 466 | func GetEthereumTokenTypeByIndex(coinIndex uint) (TokenType, error) { 467 | var tokenType TokenType 468 | switch coinIndex { 469 | case coin.ETHEREUM: 470 | tokenType = ERC20 471 | case coin.CLASSIC: 472 | tokenType = ETC20 473 | case coin.POA: 474 | tokenType = POA20 475 | case coin.CALLISTO: 476 | tokenType = CLO20 477 | case coin.WANCHAIN: 478 | tokenType = WAN20 479 | case coin.THUNDERTOKEN: 480 | tokenType = TT20 481 | case coin.GOCHAIN: 482 | tokenType = GO20 483 | case coin.TOMOCHAIN: 484 | tokenType = TRC21 485 | case coin.SMARTCHAIN: 486 | tokenType = BEP20 487 | case coin.POLYGON: 488 | tokenType = POLYGON 489 | case coin.OPTIMISM: 490 | tokenType = OPTIMISM 491 | case coin.XDAI: 492 | tokenType = XDAI 493 | case coin.AVALANCHEC: 494 | tokenType = AVALANCHE 495 | case coin.FANTOM: 496 | tokenType = FANTOM 497 | case coin.HECO: 498 | tokenType = HRC20 499 | case coin.RONIN: 500 | tokenType = RONIN 501 | case coin.CELO: 502 | tokenType = CELO 503 | case coin.CRONOS: 504 | tokenType = CRC20 505 | case coin.KCC: 506 | tokenType = KRC20 507 | case coin.AURORA: 508 | tokenType = AURORA 509 | case coin.ARBITRUM: 510 | tokenType = ARBITRUM 511 | case coin.KAVAEVM: 512 | tokenType = KAVAEVM 513 | case coin.METER: 514 | tokenType = METER 515 | case coin.EVMOS: 516 | tokenType = EVMOS_ERC20 517 | case coin.OKC: 518 | tokenType = KIP20 519 | case coin.MOONBEAM: 520 | tokenType = MOONBEAM 521 | case coin.KLAYTN: 522 | tokenType = KLAYTN 523 | case coin.METIS: 524 | tokenType = METIS 525 | case coin.MOONRIVER: 526 | tokenType = MOONRIVER 527 | case coin.BOBA: 528 | tokenType = BOBA 529 | case coin.TON: 530 | tokenType = JETTON 531 | case coin.POLYGONZKEVM: 532 | tokenType = POLYGONZKEVM 533 | case coin.ZKSYNC: 534 | tokenType = ZKSYNC 535 | case coin.SUI: 536 | tokenType = SUI 537 | case coin.STRIDE: 538 | tokenType = STRIDE 539 | case coin.NEUTRON: 540 | tokenType = NEUTRON 541 | case coin.CFXEVM: 542 | tokenType = CONFLUX 543 | case coin.ACALAEVM: 544 | tokenType = ACALAEVM 545 | case coin.BASE: 546 | tokenType = BASE 547 | case coin.NEON: 548 | tokenType = NEON 549 | case coin.IOTEXEVM: 550 | tokenType = IOTEXEVM 551 | case coin.OPBNB: 552 | tokenType = OPBNB 553 | case coin.LINEA: 554 | tokenType = LINEA 555 | case coin.MANTLE: 556 | tokenType = MANTLE 557 | case coin.MANTA: 558 | tokenType = MANTA 559 | case coin.ZETAEVM: 560 | tokenType = ZETAEVM 561 | case coin.MERLIN: 562 | tokenType = MERLIN 563 | case coin.BLAST: 564 | tokenType = BLAST 565 | case coin.SCROLL: 566 | tokenType = SCROLL 567 | case coin.BOUNCEBIT: 568 | tokenType = BOUNCEBIT 569 | case coin.ZKLINKNOVA: 570 | tokenType = ZKLINKNOVA 571 | case coin.SONIC: 572 | tokenType = SONIC 573 | } 574 | 575 | if tokenType == "" { 576 | return "", fmt.Errorf("not evm coin %d", coinIndex) 577 | } 578 | 579 | return tokenType, nil 580 | } 581 | 582 | func (t Token) AssetId() string { 583 | return asset.BuildID(t.Coin, t.TokenID) 584 | } 585 | -------------------------------------------------------------------------------- /types/token_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/trustwallet/go-primitives/coin" 10 | ) 11 | 12 | func TestGetEthereumTokenTypeByIndex(t *testing.T) { 13 | type args struct { 14 | coinIndex uint 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want TokenType 20 | wantErr bool 21 | }{ 22 | { 23 | name: "Ethereum ERC20", 24 | args: args{coinIndex: coin.ETHEREUM}, 25 | want: ERC20, 26 | }, 27 | { 28 | name: "Ethereum Classic ETC20", 29 | args: args{coinIndex: coin.CLASSIC}, 30 | want: ETC20, 31 | }, 32 | { 33 | name: "Poa POA20", 34 | args: args{coinIndex: coin.POA}, 35 | want: POA20, 36 | }, 37 | { 38 | name: "Callisto CLO20", 39 | args: args{coinIndex: coin.CALLISTO}, 40 | want: CLO20, 41 | }, 42 | { 43 | name: "WanChain WAN20", 44 | args: args{coinIndex: coin.WANCHAIN}, 45 | want: WAN20, 46 | }, 47 | { 48 | name: "Thundertoken TT20", 49 | args: args{coinIndex: coin.THUNDERTOKEN}, 50 | want: TT20, 51 | }, 52 | { 53 | name: "GoChain GO20", 54 | args: args{coinIndex: coin.GOCHAIN}, 55 | want: GO20, 56 | }, 57 | { 58 | name: "TomoChain TRC21", 59 | args: args{coinIndex: coin.TOMOCHAIN}, 60 | want: TRC21, 61 | }, 62 | { 63 | name: "Smartchain BEP20", 64 | args: args{coinIndex: coin.SMARTCHAIN}, 65 | want: BEP20, 66 | }, 67 | { 68 | name: "Solana SPL", 69 | args: args{coinIndex: coin.SOLANA}, 70 | want: "", 71 | wantErr: true, 72 | }, 73 | { 74 | name: "Polygon POLYGON", 75 | args: args{coinIndex: coin.POLYGON}, 76 | want: POLYGON, 77 | }, 78 | { 79 | name: "Optimism ERC20", 80 | args: args{coinIndex: coin.OPTIMISM}, 81 | want: OPTIMISM, 82 | }, 83 | { 84 | name: "xDAI ERC20", 85 | args: args{coinIndex: coin.XDAI}, 86 | want: XDAI, 87 | }, 88 | { 89 | name: "Avalanche ERC20", 90 | args: args{coinIndex: coin.AVALANCHEC}, 91 | want: AVALANCHE, 92 | }, 93 | { 94 | name: "Fantom ERC20", 95 | args: args{coinIndex: coin.FANTOM}, 96 | want: FANTOM, 97 | }, 98 | { 99 | name: "Heco ERC20", 100 | args: args{coinIndex: coin.HECO}, 101 | want: HRC20, 102 | }, 103 | { 104 | name: "Ronin ERC20", 105 | args: args{coinIndex: coin.RONIN}, 106 | want: RONIN, 107 | }, 108 | { 109 | name: "Cronos CRC20", 110 | args: args{coinIndex: coin.CRONOS}, 111 | want: CRC20, 112 | }, 113 | { 114 | name: "KuCoin KRC20", 115 | args: args{coinIndex: coin.KCC}, 116 | want: KRC20, 117 | }, 118 | { 119 | name: "Aurora AURORA", 120 | args: args{coinIndex: coin.AURORA}, 121 | want: AURORA, 122 | }, 123 | { 124 | name: "Arbitrum ARBITRUM", 125 | args: args{coinIndex: coin.ARBITRUM}, 126 | want: ARBITRUM, 127 | }, 128 | { 129 | name: "Moonbeam MOONBEAM", 130 | args: args{coinIndex: coin.MOONBEAM}, 131 | want: MOONBEAM, 132 | }, 133 | { 134 | name: "Klaytn KLAYTN", 135 | args: args{coinIndex: coin.KLAYTN}, 136 | want: KLAYTN, 137 | }, 138 | { 139 | name: "Metis METIS", 140 | args: args{coinIndex: coin.METIS}, 141 | want: METIS, 142 | }, 143 | { 144 | name: "Moonriver MOONRIVER", 145 | args: args{coinIndex: coin.MOONRIVER}, 146 | want: MOONRIVER, 147 | }, 148 | { 149 | name: "Boba BOBA", 150 | args: args{coinIndex: coin.BOBA}, 151 | want: BOBA, 152 | }, 153 | { 154 | name: "Conflux eSpace", 155 | args: args{coinIndex: coin.CFXEVM}, 156 | want: CONFLUX, 157 | }, 158 | { 159 | name: "BounceBit BOUNCEBIT", 160 | args: args{coinIndex: coin.BOUNCEBIT}, 161 | want: BOUNCEBIT, 162 | }, 163 | } 164 | for _, tt := range tests { 165 | t.Run(tt.name, func(t *testing.T) { 166 | got, err := GetEthereumTokenTypeByIndex(tt.args.coinIndex) 167 | if tt.wantErr { 168 | assert.Error(t, err) 169 | } 170 | assert.Equal(t, tt.want, got) 171 | 172 | }) 173 | } 174 | } 175 | 176 | func TestGetTokenType(t *testing.T) { 177 | type args struct { 178 | coinIndex uint 179 | tokenID string 180 | } 181 | tests := []struct { 182 | name string 183 | args args 184 | want string 185 | wantBool bool 186 | }{ 187 | { 188 | name: "Ethereum", 189 | args: args{coin.ETHEREUM, ""}, 190 | want: string(ERC20), 191 | wantBool: true, 192 | }, 193 | { 194 | name: "Cronos", 195 | args: args{coin.CRONOS, ""}, 196 | want: string(CRC20), 197 | wantBool: true, 198 | }, 199 | { 200 | name: "KuCoin", 201 | args: args{coin.KCC, ""}, 202 | want: string(KRC20), 203 | wantBool: true, 204 | }, 205 | { 206 | name: "Aurora", 207 | args: args{coin.AURORA, ""}, 208 | want: string(AURORA), 209 | wantBool: true, 210 | }, 211 | { 212 | name: "Tron TRC20", 213 | args: args{coin.TRON, "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"}, 214 | want: string(TRC20), 215 | wantBool: true, 216 | }, 217 | { 218 | name: "Tron TRC10", 219 | args: args{coin.TRON, "1"}, 220 | want: string(TRC10), 221 | wantBool: true, 222 | }, 223 | { 224 | name: "Binance", 225 | args: args{coin.BINANCE, ""}, 226 | want: string(BEP2), 227 | wantBool: true, 228 | }, 229 | { 230 | name: "Waves", 231 | args: args{coin.WAVES, ""}, 232 | want: string(WAVES), 233 | wantBool: true, 234 | }, 235 | { 236 | name: "Theta", 237 | args: args{coin.THETA, ""}, 238 | want: string(THETA), 239 | wantBool: true, 240 | }, 241 | { 242 | name: "Ontology", 243 | args: args{coin.ONTOLOGY, ""}, 244 | want: string(ONTOLOGY), 245 | wantBool: true, 246 | }, 247 | { 248 | name: "Nuls", 249 | args: args{coin.NULS, ""}, 250 | want: string(NRC20), 251 | wantBool: true, 252 | }, 253 | { 254 | name: "Vechain", 255 | args: args{coin.VECHAIN, ""}, 256 | want: string(VET), 257 | wantBool: true, 258 | }, 259 | { 260 | name: "Vechain", 261 | args: args{coin.NEO, ""}, 262 | want: string(NEP5), 263 | wantBool: true, 264 | }, 265 | { 266 | name: "Eos", 267 | args: args{coin.EOS, ""}, 268 | want: string(EOS), 269 | wantBool: true, 270 | }, 271 | { 272 | name: "Solana", 273 | args: args{coin.SOLANA, ""}, 274 | want: string(SPL), 275 | wantBool: true, 276 | }, 277 | { 278 | name: "Bitcoin", 279 | args: args{coin.BITCOIN, ""}, 280 | want: string(BRC20), 281 | wantBool: true, 282 | }, 283 | { 284 | name: "TERRA CW20", 285 | args: args{coin.TERRA, "terra14z56l0fp2lsf86zy3hty2z47ezkhnthtr9yq76"}, 286 | want: string(CW20), 287 | wantBool: true, 288 | }, 289 | { 290 | name: "OASIS", 291 | args: args{coin.OASIS, ""}, 292 | want: string(OASIS), 293 | wantBool: true, 294 | }, 295 | { 296 | name: "Stellar", 297 | args: args{coin.STELLAR, ""}, 298 | want: string(STELLAR), 299 | wantBool: true, 300 | }, 301 | { 302 | name: "Algorand", 303 | args: args{coin.ALGORAND, ""}, 304 | want: string(ALGORAND), 305 | wantBool: true, 306 | }, 307 | { 308 | name: "Aptos", 309 | args: args{coin.APTOS, "0xf22bede237a07e121b56d91a491eb7bcdfd1f5907926a9e58338f964a01b17fa::asset::USDT"}, 310 | want: string(APTOS), 311 | wantBool: true, 312 | }, 313 | { 314 | name: "Aptos", 315 | args: args{coin.APTOS, "0x357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b"}, 316 | want: string(APTOSFA), 317 | wantBool: true, 318 | }, 319 | { 320 | name: "Jetton", 321 | args: args{coin.TON, ""}, 322 | want: string(JETTON), 323 | wantBool: true, 324 | }, 325 | { 326 | name: "zkEVM", 327 | args: args{coin.POLYGONZKEVM, ""}, 328 | want: string(POLYGONZKEVM), 329 | wantBool: true, 330 | }, 331 | { 332 | name: "zksync", 333 | args: args{coin.ZKSYNC, ""}, 334 | want: string(ZKSYNC), 335 | wantBool: true, 336 | }, 337 | { 338 | name: "Sui", 339 | args: args{coin.SUI, ""}, 340 | want: string(SUI), 341 | wantBool: true, 342 | }, 343 | { 344 | name: "Stride", 345 | args: args{coin.STRIDE, ""}, 346 | want: string(STRIDE), 347 | wantBool: true, 348 | }, 349 | { 350 | name: "Neutron", 351 | args: args{coin.NEUTRON, ""}, 352 | want: string(NEUTRON), 353 | wantBool: true, 354 | }, 355 | { 356 | name: "Conflux eSpace", 357 | args: args{coin.CFXEVM, ""}, 358 | want: string(CONFLUX), 359 | wantBool: true, 360 | }, 361 | { 362 | name: "Acala", 363 | args: args{coin.ACALA, ""}, 364 | want: string(ACA), 365 | wantBool: true, 366 | }, 367 | { 368 | name: "NativeZeta", 369 | args: args{coin.ZETACHAIN, ""}, 370 | want: string(ZETACHAIN), 371 | wantBool: true, 372 | }, 373 | { 374 | name: "ZetaEVM", 375 | args: args{coin.ZETAEVM, ""}, 376 | want: string(ZETAEVM), 377 | wantBool: true, 378 | }, 379 | { 380 | name: "Blast", 381 | args: args{coin.BLAST, ""}, 382 | want: string(BLAST), 383 | wantBool: true, 384 | }, 385 | { 386 | name: "Scroll", 387 | args: args{coin.SCROLL, ""}, 388 | want: string(SCROLL), 389 | wantBool: true, 390 | }, 391 | { 392 | name: "BounceBit", 393 | args: args{coin.BOUNCEBIT, ""}, 394 | want: string(BOUNCEBIT), 395 | wantBool: true, 396 | }, 397 | { 398 | name: "ZkLinkNova", 399 | args: args{coin.ZKLINKNOVA, ""}, 400 | want: string(ZKLINKNOVA), 401 | wantBool: true, 402 | }, 403 | { 404 | name: "Ripple", 405 | args: args{coin.RIPPLE, ""}, 406 | want: string(XRP), 407 | wantBool: true, 408 | }, 409 | { 410 | name: "Sonic", 411 | args: args{coin.SONIC, ""}, 412 | want: string(SONIC), 413 | wantBool: true, 414 | }, 415 | } 416 | 417 | for _, tt := range tests { 418 | t.Run(tt.name, func(t *testing.T) { 419 | got, gotBool := GetTokenType(tt.args.coinIndex, tt.args.tokenID) 420 | assert.Equal(t, tt.want, got) 421 | assert.Equal(t, tt.wantBool, gotBool) 422 | 423 | }) 424 | } 425 | } 426 | 427 | func TestGetTokenVersion(t *testing.T) { 428 | type args struct { 429 | t string 430 | } 431 | tests := []struct { 432 | name string 433 | args args 434 | wantVersion TokenVersion 435 | wantErr error 436 | }{ 437 | { 438 | "ERC20 token version", 439 | args{t: string(ERC20)}, 440 | TokenVersionV0, 441 | nil, 442 | }, 443 | { 444 | "SPL token version", 445 | args{t: string(SPL)}, 446 | TokenVersionV3, 447 | nil, 448 | }, 449 | { 450 | "Polygon token version", 451 | args{t: string(POLYGON)}, 452 | TokenVersionV4, 453 | nil, 454 | }, 455 | { 456 | "Fantom token version", 457 | args{t: string(FANTOM)}, 458 | TokenVersionV5, 459 | nil, 460 | }, 461 | { 462 | "Terra token version", 463 | args{t: "TERRA"}, 464 | TokenVersionV6, 465 | nil, 466 | }, 467 | { 468 | "CELO token version", 469 | args{t: "CELO"}, 470 | TokenVersionV7, 471 | nil, 472 | }, 473 | { 474 | "CW20 token version", 475 | args{t: "CW20"}, 476 | TokenVersionV8, 477 | nil, 478 | }, 479 | { 480 | "CRC20 token version", 481 | args{t: "CRC20"}, 482 | TokenVersionV9, 483 | nil, 484 | }, 485 | { 486 | "ESDT token version", 487 | args{t: "ESDT"}, 488 | TokenVersionV9, 489 | nil, 490 | }, 491 | { 492 | "KRC20 token version", 493 | args{t: "KRC20"}, 494 | TokenVersionV10, 495 | nil, 496 | }, 497 | { 498 | "RONIN token version", 499 | args{t: "RONIN"}, 500 | TokenVersionV11, 501 | nil, 502 | }, 503 | { 504 | "AURORA token version", 505 | args{t: "AURORA"}, 506 | TokenVersionV11, 507 | nil, 508 | }, 509 | { 510 | "OASIS token version", 511 | args{t: "OASIS"}, 512 | TokenVersionUndefined, 513 | nil, 514 | }, 515 | { 516 | "Random token version", 517 | args{t: "Random"}, 518 | TokenVersionUndefined, 519 | ErrUnknownTokenType, 520 | }, 521 | { 522 | "Meter token version", 523 | args{t: string(METER)}, 524 | TokenVersionUndefined, 525 | nil, 526 | }, 527 | { 528 | "Evmos token version", 529 | args{t: string(EVMOS_ERC20)}, 530 | TokenVersionUndefined, 531 | nil, 532 | }, 533 | { 534 | "Okc token version", 535 | args{t: string(KIP20)}, 536 | TokenVersionUndefined, 537 | nil, 538 | }, 539 | { 540 | "Moonbeam token version", 541 | args{t: string(MOONBEAM)}, 542 | TokenVersionV14, 543 | nil, 544 | }, 545 | { 546 | "Klaytn token version", 547 | args{t: string(KLAYTN)}, 548 | TokenVersionV14, 549 | nil, 550 | }, 551 | { 552 | "Metis token version", 553 | args{t: string(METIS)}, 554 | TokenVersionV14, 555 | nil, 556 | }, 557 | { 558 | "Moonriver token version", 559 | args{t: string(MOONRIVER)}, 560 | TokenVersionV14, 561 | nil, 562 | }, 563 | { 564 | "Boba token version", 565 | args{t: string(BOBA)}, 566 | TokenVersionV14, 567 | nil, 568 | }, 569 | { 570 | "Jetton token version", 571 | args{t: string(JETTON)}, 572 | TokenVersionV12, 573 | nil, 574 | }, 575 | { 576 | "Stride token version", 577 | args{t: string(STRIDE)}, 578 | TokenVersionUndefined, 579 | nil, 580 | }, 581 | { 582 | "Neutron token version", 583 | args{t: string(NEUTRON)}, 584 | TokenVersionUndefined, 585 | nil, 586 | }, 587 | { 588 | "Conflux eSpace token version", 589 | args{t: string(CONFLUX)}, 590 | TokenVersionV14, 591 | nil, 592 | }, 593 | { 594 | "Acala token version", 595 | args{t: string(ACA)}, 596 | TokenVersionV14, 597 | nil, 598 | }, 599 | { 600 | "NativeZeta token version", 601 | args{t: string(ZETACHAIN)}, 602 | TokenVersionV14, 603 | nil, 604 | }, 605 | { 606 | "ZetaEVM token version", 607 | args{t: string(ZETAEVM)}, 608 | TokenVersionV14, 609 | nil, 610 | }, 611 | { 612 | "BRC20 token version", 613 | args{t: string(BRC20)}, 614 | TokenVersionV16, 615 | nil, 616 | }, 617 | { 618 | "MERLIN token version", 619 | args{t: string(MERLIN)}, 620 | TokenVersionV17, 621 | nil, 622 | }, 623 | { 624 | "BLAST token version", 625 | args{t: string(BLAST)}, 626 | TokenVersionV18, 627 | nil, 628 | }, 629 | { 630 | "SCROLL token version", 631 | args{t: string(SCROLL)}, 632 | TokenVersionV18, 633 | nil, 634 | }, 635 | { 636 | "BOUNCEBIT token version", 637 | args{t: string(BOUNCEBIT)}, 638 | TokenVersionV19, 639 | nil, 640 | }, 641 | { 642 | "ZKLINKNOVA token version", 643 | args{t: string(ZKLINKNOVA)}, 644 | TokenVersionV20, 645 | nil, 646 | }, 647 | { 648 | "SONIC token version", 649 | args{t: string(SONIC)}, 650 | TokenVersionV22, 651 | nil, 652 | }, 653 | } 654 | for _, tt := range tests { 655 | t.Run(tt.name, func(t *testing.T) { 656 | got, gotErr := GetTokenVersion(tt.args.t) 657 | assert.Equal(t, tt.wantVersion, got) 658 | assert.True(t, errors.Is(gotErr, tt.wantErr)) 659 | }) 660 | } 661 | } 662 | 663 | // TestGetTokenVersionImplementEverySupportedTokenTypes makes sure every supported token type has a version. 664 | // This also makes sure when we add new token type, we remember to add a version for it 665 | func TestGetTokenVersionImplementEverySupportedTokenTypes(t *testing.T) { 666 | for _, tokenType := range GetTokenTypes() { 667 | _, err := GetTokenVersion(string(tokenType)) 668 | assert.NoError(t, err) 669 | } 670 | } 671 | 672 | // TestGetCheckTokenTypes makes sure that for every supported token type there is corresponding entry in: 673 | // - GetChainFromAssetType function, that returns coin by asset type 674 | // - GetTokenType function, that return token type by coin 675 | func TestGetCheckTokenTypes(t *testing.T) { 676 | for _, tokenType := range GetTokenTypes() { 677 | if tokenType == ERC721 || tokenType == ERC1155 { 678 | continue 679 | } 680 | 681 | c, err := GetChainFromAssetType(string(tokenType)) 682 | assert.NoErrorf(t, err, "Missing chain for token type") 683 | 684 | result, ok := GetTokenType(c.ID, "") 685 | assert.Truef(t, ok, "Missing token type for coin %d", c.ID) 686 | 687 | assert.Truef(t, len(result) > 0, "Empty token type for coin %d", c.ID) 688 | } 689 | } 690 | -------------------------------------------------------------------------------- /types/tx.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | 10 | mapset "github.com/deckarep/golang-set" 11 | 12 | "github.com/trustwallet/go-primitives/asset" 13 | "github.com/trustwallet/go-primitives/coin" 14 | ) 15 | 16 | const ( 17 | StatusCompleted Status = "completed" 18 | StatusPending Status = "pending" 19 | StatusError Status = "error" 20 | 21 | DirectionOutgoing Direction = "outgoing" 22 | DirectionIncoming Direction = "incoming" 23 | DirectionSelf Direction = "yourself" 24 | 25 | TxTransfer TransactionType = "transfer" 26 | TxSwap TransactionType = "swap" 27 | TxContractCall TransactionType = "contract_call" 28 | TxStakeClaimRewards TransactionType = "stake_claim_rewards" 29 | TxStakeDelegate TransactionType = "stake_delegate" 30 | TxStakeUndelegate TransactionType = "stake_undelegate" 31 | TxStakeRedelegate TransactionType = "stake_redelegate" 32 | TxStakeCompound TransactionType = "stake_compound" 33 | TxTransferNFT TransactionType = "transfer_nft" 34 | ) 35 | 36 | var SupportedTypes = []TransactionType{ 37 | TxTransfer, TxSwap, TxContractCall, TxStakeClaimRewards, TxStakeDelegate, TxStakeUndelegate, TxStakeRedelegate, 38 | TxStakeCompound, 39 | TxTransferNFT, 40 | } 41 | 42 | // Transaction fields 43 | type ( 44 | Direction string 45 | Status string 46 | TransactionType string 47 | KeyType string 48 | KeyTitle string 49 | 50 | // Amount is a positive decimal integer string. 51 | // It is written in the smallest possible unit (e.g. Wei, Satoshis) 52 | Amount string 53 | ) 54 | 55 | // Data objects 56 | type ( 57 | Block struct { 58 | Number int64 `json:"number"` 59 | Txs []Tx `json:"txs"` 60 | } 61 | 62 | TxPage struct { 63 | Total int `json:"total"` 64 | Docs []Tx `json:"docs"` 65 | } 66 | 67 | // Tx describes an on-chain transaction generically 68 | Tx struct { 69 | // Unique identifier 70 | ID string `json:"id"` 71 | 72 | // Address of the transaction sender 73 | From string `json:"from"` 74 | 75 | // Address of the transaction recipient 76 | To string `json:"to"` 77 | 78 | // Unix timestamp of the block the transaction was included in 79 | BlockCreatedAt int64 `json:"block_created_at"` 80 | 81 | // Height of the block the transaction was included in 82 | Block uint64 `json:"block"` 83 | 84 | // Status of the transaction e.g: "completed", "pending", "error" 85 | Status Status `json:"status"` 86 | 87 | // Empty if the transaction "completed" or "pending", else error explaining why the transaction failed (optional) 88 | Error string `json:"error,omitempty"` 89 | 90 | // Transaction nonce or sequence 91 | Sequence uint64 `json:"sequence"` 92 | 93 | // Type of metadata 94 | Type TransactionType `json:"type"` 95 | 96 | // Transaction Direction 97 | Direction Direction `json:"direction,omitempty"` 98 | 99 | Inputs []TxOutput `json:"inputs,omitempty"` 100 | Outputs []TxOutput `json:"outputs,omitempty"` 101 | 102 | Tokens []Asset `json:"tokens,omitempty"` 103 | 104 | Memo string `json:"memo,omitempty"` 105 | 106 | Fee Fee `json:"fee"` 107 | 108 | // Metadata data object 109 | Metadata interface{} `json:"metadata"` 110 | 111 | // Create At indicates transactions creation time in database, Unix 112 | CreatedAt int64 `json:"created_at"` 113 | } 114 | 115 | // Every transaction consumes some Fee 116 | Fee struct { 117 | Asset coin.AssetID `json:"asset"` 118 | Value Amount `json:"value"` 119 | } 120 | 121 | // UTXO transactions consist of a set of inputs and a set of outputs 122 | // both represented by TxOutput model 123 | TxOutput struct { 124 | Address string `json:"address"` 125 | Value Amount `json:"value"` 126 | Asset coin.AssetID `json:"asset,omitempty"` 127 | } 128 | 129 | // Transfer describes the transfer of currency 130 | Transfer struct { 131 | Asset coin.AssetID `json:"asset"` 132 | Value Amount `json:"value"` 133 | } 134 | 135 | // TransferNFT describes the transfer of the NFT token 136 | TransferNFT struct { 137 | Asset coin.AssetID `json:"asset"` 138 | Collection string `json:"collection"` 139 | CollectibleID string `json:"collectible_id"` 140 | CollectionSymbol string `json:"collection_symbol"` 141 | Value Amount `json:"value"` 142 | } 143 | 144 | Swap struct { 145 | From Transfer `json:"from"` 146 | To Transfer `json:"to"` 147 | } 148 | 149 | // ContractCall describes a 150 | ContractCall struct { 151 | Asset coin.AssetID `json:"asset"` 152 | Value Amount `json:"value"` 153 | Input string `json:"input"` 154 | } 155 | 156 | Txs []Tx 157 | 158 | AssetHolder interface { 159 | GetAsset() coin.AssetID 160 | } 161 | 162 | Validator interface { 163 | Validate() error 164 | } 165 | ) 166 | 167 | var ( 168 | EmptyTxPage = TxPage{Total: 0, Docs: Txs{}} 169 | ) 170 | 171 | func NewTxPage(txs Txs) TxPage { 172 | if txs == nil { 173 | txs = Txs{} 174 | } 175 | return TxPage{ 176 | Total: len(txs), 177 | Docs: txs, 178 | } 179 | } 180 | 181 | func (txs Txs) FilterUniqueID() Txs { 182 | keys := make(map[string]bool) 183 | list := make(Txs, 0) 184 | for _, entry := range txs { 185 | if _, value := keys[entry.ID]; !value { 186 | keys[entry.ID] = true 187 | list = append(list, entry) 188 | } 189 | } 190 | return list 191 | } 192 | 193 | func (txs Txs) CleanMemos() { 194 | for i := range txs { 195 | txs[i].Memo = cleanMemo(txs[i].Memo) 196 | } 197 | } 198 | 199 | func (txs Txs) SortByBlockCreationTime() Txs { 200 | sort.Slice(txs, func(i, j int) bool { 201 | return txs[i].BlockCreatedAt > txs[j].BlockCreatedAt 202 | }) 203 | return txs 204 | } 205 | 206 | func (txs Txs) FilterTransactionsByType(types []TransactionType) Txs { 207 | result := make(Txs, 0) 208 | for _, tx := range txs { 209 | for _, t := range types { 210 | if tx.Type == t { 211 | result = append(result, tx) 212 | } 213 | } 214 | } 215 | 216 | return result 217 | } 218 | 219 | func (t *Transfer) GetAsset() coin.AssetID { 220 | return t.Asset 221 | } 222 | 223 | func (t *Transfer) Validate() error { 224 | if t.Value == "" { 225 | return fmt.Errorf("emtpy transfer value") 226 | } 227 | 228 | if t.Asset == "" { 229 | return fmt.Errorf("empty transfer asset") 230 | } 231 | 232 | return nil 233 | } 234 | 235 | func (cc *ContractCall) GetAsset() coin.AssetID { 236 | return cc.Asset 237 | } 238 | 239 | func (cc *ContractCall) Validate() error { 240 | if cc.Value == "" { 241 | return fmt.Errorf("empty contract call value") 242 | } 243 | 244 | if cc.Asset == "" { 245 | return fmt.Errorf("empty contract call asset") 246 | } 247 | 248 | return nil 249 | } 250 | 251 | func (cc *TransferNFT) GetAsset() coin.AssetID { 252 | return cc.Asset 253 | } 254 | 255 | func (cc *TransferNFT) Validate() error { 256 | if cc.CollectibleID == "" { 257 | return fmt.Errorf("empty transfer NFT collectible ID value") 258 | } 259 | 260 | if cc.Asset == "" { 261 | return fmt.Errorf("empty transfer NFT asset") 262 | } 263 | 264 | return nil 265 | } 266 | 267 | func (s *Swap) GetAsset() coin.AssetID { 268 | return coin.AssetID(strings.Split(string(s.From.Asset), "_")[0]) 269 | } 270 | 271 | func cleanMemo(memo string) string { 272 | if len(memo) == 0 { 273 | return "" 274 | } 275 | 276 | _, err := strconv.ParseFloat(memo, 64) 277 | if err != nil { 278 | return "" 279 | } 280 | 281 | return memo 282 | } 283 | 284 | func (t *Tx) GetAddresses() []string { 285 | addresses := make([]string, 0) 286 | switch t.Type { 287 | case TxTransfer, TxTransferNFT: 288 | if len(t.Inputs) > 0 || len(t.Outputs) > 0 { 289 | uniqueAddresses := make(map[string]struct{}) 290 | for _, input := range t.Inputs { 291 | uniqueAddresses[input.Address] = struct{}{} 292 | } 293 | 294 | for _, output := range t.Outputs { 295 | uniqueAddresses[output.Address] = struct{}{} 296 | } 297 | 298 | for address := range uniqueAddresses { 299 | addresses = append(addresses, address) 300 | } 301 | 302 | return addresses 303 | } 304 | 305 | return append(addresses, t.From, t.To) 306 | case TxContractCall: 307 | uniqueAddresses := map[string]struct{}{ 308 | t.From: {}, 309 | t.To: {}, 310 | } 311 | for _, input := range t.Inputs { 312 | uniqueAddresses[input.Address] = struct{}{} 313 | } 314 | 315 | for _, output := range t.Outputs { 316 | uniqueAddresses[output.Address] = struct{}{} 317 | } 318 | 319 | for address := range uniqueAddresses { 320 | addresses = append(addresses, address) 321 | } 322 | 323 | return addresses 324 | case TxSwap: 325 | return append(addresses, t.From, t.To) 326 | case TxStakeDelegate, TxStakeRedelegate, TxStakeUndelegate, TxStakeClaimRewards, TxStakeCompound: 327 | return append(addresses, t.From) 328 | default: 329 | return addresses 330 | } 331 | } 332 | 333 | func (t *Tx) GetSubscriptionAddresses() ([]string, error) { 334 | coin, _, err := asset.ParseID(string(t.Metadata.(AssetHolder).GetAsset())) 335 | if err != nil { 336 | return nil, err 337 | } 338 | 339 | addresses := t.GetAddresses() 340 | result := make([]string, len(addresses)) 341 | for i, a := range addresses { 342 | result[i] = fmt.Sprintf("%d_%s", coin, a) 343 | } 344 | 345 | return result, nil 346 | } 347 | 348 | func (t *Tx) GetDirection(address string) Direction { 349 | if len(t.Direction) > 0 { 350 | return t.Direction 351 | } 352 | 353 | if len(t.Inputs) > 0 && len(t.Outputs) > 0 { 354 | addressSet := mapset.NewSet(address) 355 | return InferDirection(t, addressSet) 356 | } 357 | 358 | return t.determineTransactionDirection(address, t.From, t.To) 359 | } 360 | 361 | func (t *Tx) GetAssetID() *coin.AssetID { 362 | if t.Metadata == nil { 363 | return nil 364 | } 365 | 366 | assetHolder, ok := t.Metadata.(AssetHolder) 367 | if !ok { 368 | return nil 369 | } 370 | 371 | assetID := assetHolder.GetAsset() 372 | return &assetID 373 | } 374 | 375 | func (t *Tx) determineTransactionDirection(address, from, to string) Direction { 376 | if t.Type == TxStakeUndelegate || t.Type == TxStakeClaimRewards { 377 | return DirectionIncoming 378 | } 379 | 380 | if address == to { 381 | if from == to { 382 | return DirectionSelf 383 | } 384 | return DirectionIncoming 385 | } 386 | return DirectionOutgoing 387 | } 388 | 389 | func (t *Tx) IsUTXO() bool { 390 | return t.Type == TxTransfer && len(t.Outputs) > 0 391 | } 392 | 393 | func (t *Tx) IsEVM() (bool, error) { 394 | if t.Metadata == nil { 395 | return false, nil 396 | } 397 | 398 | assetHolder, ok := t.Metadata.(AssetHolder) 399 | if !ok { 400 | return false, nil 401 | } 402 | 403 | coinID, _, err := asset.ParseID(string(assetHolder.GetAsset())) 404 | if err != nil { 405 | return false, err 406 | } 407 | 408 | return coin.IsEVM(coinID), nil 409 | } 410 | 411 | func (t *Tx) GetUTXOValueFor(address string) (Amount, error) { 412 | isTransferOut := false 413 | isSelf := true 414 | 415 | totalInputValue := big.NewInt(0) 416 | addressInputValue := big.NewInt(0) 417 | for _, input := range t.Inputs { 418 | value, ok := big.NewInt(0).SetString(string(input.Value), 10) 419 | if !ok { 420 | return "0", fmt.Errorf("invalid input value for address %s: %s", input.Address, input.Value) 421 | } 422 | 423 | totalInputValue = totalInputValue.Add(totalInputValue, value) 424 | 425 | if input.Address == address { 426 | addressInputValue = value 427 | isTransferOut = true 428 | } 429 | } 430 | 431 | addressOutputValue := big.NewInt(0) 432 | totalOutputValue := big.NewInt(0) 433 | for _, output := range t.Outputs { 434 | value, ok := big.NewInt(0).SetString(string(output.Value), 10) 435 | if !ok { 436 | return "0", fmt.Errorf("invalid output value for address %s: %s", output.Address, output.Value) 437 | } 438 | totalOutputValue = totalOutputValue.Add(totalOutputValue, value) 439 | if output.Address == address { 440 | addressOutputValue = addressOutputValue.Add(addressOutputValue, value) 441 | } else { 442 | isSelf = false 443 | } 444 | } 445 | 446 | var result *big.Int 447 | if isTransferOut && !isSelf { 448 | if addressInputValue.Cmp(addressOutputValue) < 0 { 449 | result = big.NewInt(0) // address received more than sent, although it's an outgoing tx 450 | } else { 451 | //addressInputValue - (totalInputValue-totalOutputValue)/uint64(len(t.Inputs)) - addressOutputValue 452 | totalTransferred := totalInputValue.Sub(totalInputValue, totalOutputValue) 453 | avgSent := totalTransferred.Div(totalTransferred, big.NewInt(int64(len(t.Inputs)))) 454 | output := addressInputValue.Sub(addressInputValue, addressOutputValue) 455 | 456 | // for utxo there is no way to define the exact amount sent 457 | // because there is many senders and many recipients 458 | result = output.Sub(output, avgSent) 459 | } 460 | } else { 461 | result = addressOutputValue 462 | } 463 | 464 | return Amount(fmt.Sprintf("%d", result)), nil 465 | } 466 | 467 | func InferDirection(tx *Tx, addressSet mapset.Set) Direction { 468 | inputSet := mapset.NewSet() 469 | for _, address := range tx.Inputs { 470 | inputSet.Add(address.Address) 471 | } 472 | outputSet := mapset.NewSet() 473 | for _, address := range tx.Outputs { 474 | outputSet.Add(address.Address) 475 | } 476 | intersect := addressSet.Intersect(inputSet) 477 | if intersect.Cardinality() == 0 { 478 | return DirectionIncoming 479 | } 480 | if outputSet.IsProperSubset(addressSet) || outputSet.Equal(inputSet) { 481 | return DirectionSelf 482 | } 483 | return DirectionOutgoing 484 | } 485 | 486 | func IsTxTypeAmong(txType TransactionType, types []TransactionType) bool { 487 | result := false 488 | for _, t := range types { 489 | if txType == t { 490 | result = true 491 | break 492 | } 493 | } 494 | 495 | return result 496 | } 497 | -------------------------------------------------------------------------------- /types/tx_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestTxs_CleanMemos(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | tx Tx 14 | expectedMemo string 15 | }{ 16 | { 17 | name: "transfer_ok", 18 | tx: Tx{Memo: "1"}, 19 | expectedMemo: "1", 20 | }, 21 | { 22 | name: "transfer_empty_string", 23 | tx: Tx{Metadata: &Transfer{}}, 24 | expectedMemo: "", 25 | }, 26 | { 27 | name: "transfer_non_number", 28 | tx: Tx{Memo: "non_number"}, 29 | expectedMemo: "", 30 | }, 31 | { 32 | name: "delegation_ok", 33 | tx: Tx{Memo: "1"}, 34 | expectedMemo: "1", 35 | }, 36 | { 37 | name: "delegation_empty_string", 38 | tx: Tx{Metadata: &Transfer{}}, 39 | expectedMemo: "", 40 | }, 41 | { 42 | name: "delegation_non_number", 43 | tx: Tx{Memo: "non_number"}, 44 | expectedMemo: "", 45 | }, 46 | { 47 | name: "redelegation_ok", 48 | tx: Tx{Memo: "1"}, 49 | expectedMemo: "1", 50 | }, 51 | { 52 | name: "redelegation_empty_string", 53 | tx: Tx{Metadata: &Transfer{}}, 54 | expectedMemo: "", 55 | }, 56 | { 57 | name: "redelegation_non_number", 58 | tx: Tx{Memo: "non_number"}, 59 | expectedMemo: "", 60 | }, 61 | { 62 | name: "claim_rewards_ok", 63 | tx: Tx{Memo: "1"}, 64 | expectedMemo: "1", 65 | }, 66 | { 67 | name: "claim_rewards_empty_string", 68 | tx: Tx{Metadata: &Transfer{}}, 69 | expectedMemo: "", 70 | }, 71 | { 72 | name: "claim_rewards_non_number", 73 | tx: Tx{Memo: "non_number"}, 74 | expectedMemo: "", 75 | }, 76 | { 77 | name: "any_action_ok", 78 | tx: Tx{Memo: "1"}, 79 | expectedMemo: "1", 80 | }, 81 | } 82 | 83 | for _, tc := range tests { 84 | t.Run(tc.name, func(t *testing.T) { 85 | txs := Txs{tc.tx} 86 | txs.CleanMemos() 87 | 88 | assert.Equal(t, tc.expectedMemo, txs[0].Memo) 89 | }) 90 | } 91 | } 92 | 93 | func TestCleanMemo(t *testing.T) { 94 | tests := []struct { 95 | name string 96 | value string 97 | expected string 98 | }{ 99 | { 100 | name: "empty_value", 101 | value: "", 102 | expected: "", 103 | }, 104 | { 105 | name: "string_value", 106 | value: "test", 107 | expected: "", 108 | }, 109 | { 110 | name: "good_number_value", 111 | value: "1", 112 | expected: "1", 113 | }, 114 | } 115 | 116 | for _, tc := range tests { 117 | t.Run(tc.name, func(t *testing.T) { 118 | result := cleanMemo(tc.value) 119 | assert.Equal(t, tc.expected, result) 120 | }) 121 | } 122 | } 123 | 124 | func TestTx_GetAddresses(t *testing.T) { 125 | tests := []struct { 126 | name string 127 | tx Tx 128 | expected []string 129 | }{ 130 | { 131 | name: "transfer", 132 | tx: Tx{ 133 | Type: TxTransfer, 134 | From: "from", 135 | To: "to", 136 | Metadata: &Transfer{}, 137 | }, 138 | expected: []string{"from", "to"}, 139 | }, 140 | { 141 | name: "transfer_nft", 142 | tx: Tx{ 143 | Type: TxTransferNFT, 144 | From: "from", 145 | To: "to", 146 | Metadata: &TransferNFT{}, 147 | }, 148 | expected: []string{"from", "to"}, 149 | }, 150 | { 151 | name: "delegation", 152 | tx: Tx{ 153 | Type: TxStakeDelegate, 154 | From: "from", 155 | To: "to", 156 | Metadata: &Transfer{}, 157 | }, 158 | expected: []string{"from"}, 159 | }, 160 | { 161 | name: "undelegation", 162 | tx: Tx{ 163 | Type: TxStakeUndelegate, 164 | From: "from", 165 | To: "to", 166 | Metadata: &Transfer{}, 167 | }, 168 | expected: []string{"from"}, 169 | }, 170 | { 171 | name: "claim_rewards", 172 | tx: Tx{ 173 | Type: TxStakeClaimRewards, 174 | From: "from", 175 | To: "to", 176 | Metadata: &Transfer{}, 177 | }, 178 | expected: []string{"from"}, 179 | }, 180 | { 181 | name: "contract_call", 182 | tx: Tx{ 183 | Type: TxContractCall, 184 | From: "from", 185 | To: "to", 186 | Inputs: []TxOutput{ 187 | { 188 | Address: "from1", 189 | }, 190 | { 191 | Address: "from", 192 | }, 193 | }, 194 | Outputs: []TxOutput{ 195 | { 196 | Address: "to", 197 | }, 198 | { 199 | Address: "to1", 200 | }, 201 | }, 202 | Metadata: &ContractCall{}, 203 | }, 204 | expected: []string{"from", "to", "from1", "to1"}, 205 | }, 206 | { 207 | name: "swap", 208 | tx: Tx{ 209 | Type: TxSwap, 210 | From: "from", 211 | To: "from", 212 | Metadata: &ContractCall{}, 213 | }, 214 | expected: []string{"from", "from"}, 215 | }, 216 | { 217 | name: "any_action", 218 | tx: Tx{ 219 | Type: TxTransfer, 220 | From: "from", 221 | To: "to", 222 | Metadata: &Transfer{}, 223 | }, 224 | expected: []string{"from", "to"}, 225 | }, 226 | { 227 | name: "redelegation", 228 | tx: Tx{ 229 | Type: TxStakeRedelegate, 230 | From: "from", 231 | To: "to_validator", 232 | Metadata: &Transfer{}, 233 | }, 234 | expected: []string{"from"}, 235 | }, 236 | { 237 | name: "undefined", 238 | tx: Tx{ 239 | From: "from", 240 | To: "to", 241 | }, 242 | expected: []string{}, 243 | }, 244 | { 245 | name: "utxo", 246 | tx: Tx{ 247 | Type: TxTransfer, 248 | From: "from_utxo", 249 | To: "from_utxo", 250 | Inputs: []TxOutput{{Address: "from_utxo"}}, 251 | Outputs: []TxOutput{{Address: "from_utxo"}, {Address: "to_utxo"}}, 252 | Metadata: &Transfer{}, 253 | }, 254 | expected: []string{"from_utxo", "to_utxo"}, 255 | }, 256 | { 257 | name: "stake_compound", 258 | tx: Tx{ 259 | Type: TxStakeCompound, 260 | From: "from", 261 | To: "to", 262 | Metadata: &Transfer{}, 263 | }, 264 | expected: []string{"from"}, 265 | }, 266 | } 267 | 268 | for _, tc := range tests { 269 | t.Run(tc.name, func(t *testing.T) { 270 | result := tc.tx.GetAddresses() 271 | sort.Strings(tc.expected) 272 | sort.Strings(result) 273 | assert.EqualValues(t, tc.expected, result) 274 | }) 275 | } 276 | 277 | // make sure all supported types have a test 278 | supportedTypesMap := map[TransactionType]struct{}{} 279 | for _, supportedType := range SupportedTypes { 280 | supportedTypesMap[supportedType] = struct{}{} 281 | } 282 | 283 | testedTypesMap := map[TransactionType]struct{}{} 284 | for _, tc := range tests { 285 | if _, supported := supportedTypesMap[tc.tx.Type]; supported { 286 | testedTypesMap[tc.tx.Type] = struct{}{} 287 | } 288 | } 289 | assert.Equal(t, len(supportedTypesMap), len(testedTypesMap)) 290 | } 291 | 292 | func TestTx_GetDirection(t *testing.T) { 293 | tests := []struct { 294 | name string 295 | tx Tx 296 | address string 297 | expected Direction 298 | }{ 299 | { 300 | name: "direction_defined_outgoing", 301 | tx: Tx{ 302 | Direction: DirectionOutgoing, 303 | }, 304 | expected: DirectionOutgoing, 305 | }, 306 | { 307 | name: "direction_defined_incoming", 308 | tx: Tx{ 309 | Direction: DirectionIncoming, 310 | }, 311 | expected: DirectionIncoming, 312 | }, 313 | { 314 | name: "utxo_outgoing", 315 | tx: Tx{ 316 | Inputs: []TxOutput{ 317 | { 318 | Address: "sender", 319 | }, 320 | }, 321 | Outputs: []TxOutput{ 322 | { 323 | Address: "receiver", 324 | }, 325 | }, 326 | }, 327 | address: "sender", 328 | expected: DirectionOutgoing, 329 | }, 330 | { 331 | name: "utxo_incoming", 332 | tx: Tx{ 333 | Inputs: []TxOutput{ 334 | { 335 | Address: "sender", 336 | }, 337 | }, 338 | Outputs: []TxOutput{ 339 | { 340 | Address: "receiver", 341 | }, 342 | }, 343 | }, 344 | address: "receiver", 345 | expected: DirectionIncoming, 346 | }, 347 | { 348 | name: "utxo_self", 349 | tx: Tx{ 350 | Inputs: []TxOutput{ 351 | { 352 | Address: "sender", 353 | }, 354 | }, 355 | Outputs: []TxOutput{ 356 | { 357 | Address: "sender", 358 | }, 359 | }, 360 | }, 361 | address: "sender", 362 | expected: DirectionSelf, 363 | }, 364 | { 365 | name: "outgoing", 366 | tx: Tx{ 367 | From: "sender", 368 | To: "receiver", 369 | }, 370 | address: "sender", 371 | expected: DirectionOutgoing, 372 | }, 373 | { 374 | name: "incoming", 375 | tx: Tx{ 376 | From: "sender", 377 | To: "receiver", 378 | }, 379 | address: "receiver", 380 | expected: DirectionIncoming, 381 | }, 382 | { 383 | name: "self", 384 | tx: Tx{ 385 | From: "sender", 386 | To: "sender", 387 | }, 388 | address: "sender", 389 | expected: DirectionSelf, 390 | }, 391 | { 392 | name: "stake_delegate", 393 | tx: Tx{ 394 | From: "sender", 395 | To: "sender", 396 | }, 397 | address: "sender", 398 | expected: DirectionSelf, 399 | }, 400 | { 401 | name: "self", 402 | tx: Tx{ 403 | From: "sender", 404 | To: "sender", 405 | }, 406 | address: "sender", 407 | expected: DirectionSelf, 408 | }, 409 | { 410 | name: "stake_undelegate", 411 | tx: Tx{ 412 | From: "delegator", 413 | To: "owner", 414 | Type: TxStakeUndelegate, 415 | }, 416 | address: "owner", 417 | expected: DirectionIncoming, 418 | }, 419 | { 420 | name: "stake_redelegate", 421 | tx: Tx{ 422 | From: "delegator1", 423 | To: "delegator2", 424 | Type: TxStakeRedelegate, 425 | }, 426 | address: "owner", 427 | expected: DirectionOutgoing, 428 | }, 429 | { 430 | name: "stake_delegate", 431 | tx: Tx{ 432 | From: "owner", 433 | To: "delegator", 434 | Type: TxStakeDelegate, 435 | }, 436 | address: "owner", 437 | expected: DirectionOutgoing, 438 | }, 439 | { 440 | name: "stake_claim_rewards", 441 | tx: Tx{ 442 | From: "delegator", 443 | To: "sender", 444 | Type: TxStakeClaimRewards, 445 | }, 446 | address: "sender", 447 | expected: DirectionIncoming, 448 | }, 449 | } 450 | 451 | for _, tc := range tests { 452 | t.Run(tc.name, func(t *testing.T) { 453 | result := tc.tx.GetDirection(tc.address) 454 | assert.Equal(t, tc.expected, result) 455 | }) 456 | } 457 | 458 | } 459 | 460 | func TestUTXOValueByAddress(t *testing.T) { 461 | tests := []struct { 462 | name string 463 | tx Tx 464 | address string 465 | expected Amount 466 | expectedErrAssertion assert.ErrorAssertionFunc 467 | }{ 468 | { 469 | name: "transfer_self", 470 | tx: Tx{ 471 | Inputs: []TxOutput{{ 472 | Address: "addr", 473 | Value: "1000", 474 | }}, 475 | Outputs: []TxOutput{ 476 | { 477 | Address: "addr", 478 | Value: "900", 479 | }, 480 | { 481 | Address: "addr", 482 | Value: "100", 483 | }, 484 | }, 485 | }, 486 | address: "addr", 487 | expected: "1000", 488 | expectedErrAssertion: assert.NoError, 489 | }, 490 | { 491 | name: "transfer_self_multi", 492 | tx: Tx{ 493 | Inputs: []TxOutput{ 494 | { 495 | Address: "bc1qrfr44n2j4czd5c9txwlnw0yj2h82x9566fglqj", 496 | Value: "10772", 497 | }, 498 | { 499 | Address: "bc1qf9xslrccq3hnwa8dyd9gnjcuxlyz45v5dku5t9", 500 | Value: "12257", 501 | }}, 502 | Outputs: []TxOutput{ 503 | { 504 | Address: "bc1qrfr44n2j4czd5c9txwlnw0yj2h82x9566fglqj", 505 | Value: "14663", 506 | }, 507 | }, 508 | }, 509 | address: "bc1qrfr44n2j4czd5c9txwlnw0yj2h82x9566fglqj", 510 | expected: "14663", 511 | expectedErrAssertion: assert.NoError, 512 | }, 513 | { 514 | name: "transfer_in", 515 | tx: Tx{ 516 | Outputs: []TxOutput{{ 517 | Address: "addr", 518 | Value: "1000", 519 | }}, 520 | }, 521 | address: "addr", 522 | expected: "1000", 523 | expectedErrAssertion: assert.NoError, 524 | }, 525 | { 526 | name: "transfer_out_with_utxo", 527 | tx: Tx{ 528 | Inputs: []TxOutput{{ 529 | Address: "addr", 530 | Value: "1000", 531 | }}, 532 | Outputs: []TxOutput{ 533 | { 534 | Address: "addr", 535 | Value: "100", 536 | }, 537 | { 538 | Address: "addr1", 539 | Value: "800", 540 | }, 541 | }, 542 | }, 543 | address: "addr", 544 | expected: "800", 545 | expectedErrAssertion: assert.NoError, 546 | }, 547 | { 548 | name: "uint64 overflow", 549 | tx: Tx{ 550 | Outputs: []TxOutput{{ 551 | Address: "addr", 552 | Value: "58446744073709551620", 553 | }}, 554 | }, 555 | address: "addr", 556 | expected: "58446744073709551620", 557 | expectedErrAssertion: assert.NoError, 558 | }, 559 | } 560 | 561 | for _, tc := range tests { 562 | t.Run(tc.name, func(t *testing.T) { 563 | result, err := tc.tx.GetUTXOValueFor(tc.address) 564 | tc.expectedErrAssertion(t, err) 565 | 566 | assert.Equal(t, tc.expected, result) 567 | }) 568 | } 569 | } 570 | -------------------------------------------------------------------------------- /types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/big" 7 | "strings" 8 | ) 9 | 10 | type HexNumber big.Int 11 | 12 | func (i HexNumber) MarshalJSON() ([]byte, error) { 13 | hexNumber := fmt.Sprintf("\"0x%x\"", (*big.Int)(&i)) 14 | 15 | return []byte(hexNumber), nil 16 | } 17 | 18 | func (i *HexNumber) UnmarshalJSON(data []byte) error { 19 | var resultStr string 20 | err := json.Unmarshal(data, &resultStr) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | var value *big.Int 26 | if resultStr == "0x" { 27 | value = new(big.Int) 28 | } else { 29 | hex := strings.Replace(resultStr, "0x", "", 1) 30 | 31 | var ok bool 32 | value, ok = new(big.Int).SetString(hex, 16) 33 | if !ok { 34 | return fmt.Errorf("could not parse hex value %v", resultStr) 35 | } 36 | } 37 | 38 | *i = HexNumber(*value) 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /types/types_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "math/big" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestHex_UnmarshalAndMarshalJSON(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | input []byte 15 | result string 16 | isError bool 17 | }{ 18 | { 19 | name: "value greater than 2^64 -1", 20 | input: []byte(`{"value":"0x850a9af493d065b6c"}`), 21 | result: "153386322112866048876", 22 | isError: false, 23 | }, 24 | { 25 | name: "value less than 2^64 -1", 26 | input: []byte(`{"value":"0x746a528800"}`), 27 | result: "500000000000", 28 | isError: false, 29 | }, 30 | { 31 | name: "error case: not hex (string)", 32 | input: []byte(`{"value":"error_value"}`), 33 | result: "", 34 | isError: true, 35 | }, 36 | { 37 | name: "error case: not hex (octal)", 38 | input: []byte(`{"value":"20502515364447501455554"}`), 39 | result: "", 40 | isError: true, 41 | }, 42 | } 43 | 44 | for _, tc := range tests { 45 | t.Run(tc.name, func(t *testing.T) { 46 | type req struct { 47 | Value *HexNumber `json:"value"` 48 | } 49 | 50 | var v req 51 | 52 | err := json.Unmarshal(tc.input, &v) 53 | if tc.isError { 54 | return 55 | } 56 | assert.NoError(t, err) 57 | 58 | output := (*big.Int)(v.Value) 59 | assert.Equal(t, tc.result, output.String()) 60 | 61 | bytes, err := json.Marshal(v) 62 | assert.NoError(t, err) 63 | assert.Equal(t, tc.input, bytes) 64 | }) 65 | } 66 | } 67 | --------------------------------------------------------------------------------