diff --git a/bchain/coins/eth/contract.go b/bchain/coins/eth/contract.go index 8df7e7fe..676b6d35 100644 --- a/bchain/coins/eth/contract.go +++ b/bchain/coins/eth/contract.go @@ -2,6 +2,7 @@ package eth import ( "context" + "encoding/hex" "math/big" "strings" @@ -32,14 +33,20 @@ const contractDecimalsSignature = "0x313ce567" const contractBalanceOfSignature = "0x70a08231" func addressFromPaddedHex(s string) (string, error) { - var t big.Int - var ok bool + // Logs/topics and calldata often include a 0x prefix; strip it so length math and decoding are consistent. if has0xPrefix(s) { - _, ok = t.SetString(s[2:], 16) - } else { - _, ok = t.SetString(s, 16) + s = s[2:] } - if !ok { + if len(s) >= EthereumTypeAddressDescriptorLen*2 { + // Fast path: padded topics/data store the address in the last 20 bytes. + s = s[len(s)-EthereumTypeAddressDescriptorLen*2:] + if b, err := hex.DecodeString(s); err == nil && len(b) == EthereumTypeAddressDescriptorLen { + return ethcommon.BytesToAddress(b).String(), nil + } + } + // Fallback: handle odd formats by parsing the full value as a number. + var t big.Int + if _, ok := t.SetString(s, 16); !ok { return "", errors.New("Data is not a number") } a := ethcommon.BigToAddress(&t) diff --git a/bchain/coins/eth/contract_test.go b/bchain/coins/eth/contract_test.go index ca70c858..5b2e5a81 100644 --- a/bchain/coins/eth/contract_test.go +++ b/bchain/coins/eth/contract_test.go @@ -3,6 +3,8 @@ package eth import ( + "bytes" + "encoding/hex" "fmt" "math/big" "strings" @@ -12,6 +14,80 @@ import ( "github.com/trezor/blockbook/tests/dbtestdata" ) +func Test_addressFromPaddedHex(t *testing.T) { + tests := []struct { + name string + input string + wantHex string + wantErr bool + }{ + { + name: "address_no_padding_with_prefix", + input: "0x5dc6288b35e0807a3d6feb89b3a2ff4ab773168e", + wantHex: "5dc6288b35e0807a3d6feb89b3a2ff4ab773168e", + }, + { + name: "uppercase_prefix", + input: "0X0000000000000000000000005dc6288b35e0807a3d6feb89b3a2ff4ab773168e", + wantHex: "5dc6288b35e0807a3d6feb89b3a2ff4ab773168e", + }, + { + name: "padded", + input: "0x0000000000000000000000002aacf811ac1a60081ea39f7783c0d26c500871a8", + wantHex: "2aacf811ac1a60081ea39f7783c0d26c500871a8", + }, + { + name: "all_zero", + input: "0x0000000000000000000000000000000000000000000000000000000000000000", + wantHex: "0000000000000000000000000000000000000000", + }, + { + name: "unpadded", + input: "1a2b3c", + wantHex: "00000000000000000000000000000000001a2b3c", + }, + { + name: "invalid_fast_path", + input: "0x0000000000000000000000002aacf811ac1a60081ea39f7783c0d26c500871zz", + wantErr: true, + }, + { + name: "invalid", + input: "0xzz", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := addressFromPaddedHex(tt.input) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error for %q", tt.input) + } + return + } + if err != nil { + t.Fatalf("addressFromPaddedHex(%q) error %v", tt.input, err) + } + if len(got) < 2 || !strings.HasPrefix(got, "0x") { + t.Fatalf("addressFromPaddedHex(%q) returned %q", tt.input, got) + } + gotBytes, err := hex.DecodeString(got[2:]) + if err != nil { + t.Fatalf("decode got %q error %v", got, err) + } + wantBytes, err := hex.DecodeString(tt.wantHex) + if err != nil { + t.Fatalf("decode want %q error %v", tt.wantHex, err) + } + if !bytes.Equal(gotBytes, wantBytes) { + t.Fatalf("addressFromPaddedHex(%q) bytes %x want %x", tt.input, gotBytes, wantBytes) + } + }) + } +} + func Test_contractGetTransfersFromLog(t *testing.T) { tests := []struct { name string