diff --git a/VERSION b/VERSION index e11e94c5d..ccc64186e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.111.4 +0.111.5 diff --git a/abi-spec/core_test.go b/abi-spec/core_test.go index 15a1e5448..f891a4570 100644 --- a/abi-spec/core_test.go +++ b/abi-spec/core_test.go @@ -74,7 +74,7 @@ func TestEncode(t *testing.T) { require.NoError(t, err) require.Equal(t, "0xb3de648bffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", result) - //TODO following case would fail cause go-ethereum does not support type uint + //TODO following case would fail cause go-ethereum does not support type uint, should we fix this? //result, err = Encode("g(uint[][],string[])", `[[[1,2],[3]],["one","two","three"]]`) //require.NoError(t, err) //require.Equal(t, "0xad6a3446000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000036f6e650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000374776f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000057468726565000000000000000000000000000000000000000000000000000000", result) diff --git a/abi-spec/utf8.go b/abi-spec/utf8.go new file mode 100644 index 000000000..eafc88533 --- /dev/null +++ b/abi-spec/utf8.go @@ -0,0 +1,255 @@ +package abispec + +import ( + "fmt" + "unicode/utf8" + + "github.com/ethereum/go-ethereum/common/hexutil" +) + +func stringToRunes(str string) []rune { + var runes []rune + bytes := []byte(str) + for len(bytes) > 0 { + r, size := utf8.DecodeRune(bytes) + if r == utf8.RuneError { + for i := 0; i < size; i++ { + runes = append(runes, rune(bytes[i])) + } + } else { + runes = append(runes, r) + } + bytes = bytes[size:] + } + return runes +} + +// Taken from https://mths.be/punycode +func ucs2decode(str string) []rune { + var runes = stringToRunes(str) + var output []rune + var counter = 0 + var length = len(runes) + var value rune + var extra rune + for counter < length { + value = runes[counter] + counter++ + if value >= 0xD800 && value <= 0xDBFF && counter < length { + // high surrogate, and there is a next character + extra = runes[counter] + counter++ + if (extra & 0xFC00) == 0xDC00 { // low surrogate + output = append(output, ((value&0x3FF)<<10)+(extra&0x3FF)+0x10000) + } else { + // unmatched surrogate; only append this code unit, in case the next + // code unit is the high surrogate of a surrogate pair + output = append(output, value) + counter-- + } + } else { + output = append(output, value) + } + } + return output +} + +// Taken from https://mths.be/punycode +func ucs2encode(array []rune) []byte { + var length = len(array) + var index = 0 + var value rune + var output []byte + for index < length { + value = array[index] + if value > 0xFFFF { + value -= 0x10000 + codePoint := rune(uint32(value)>>10&0x3FF | 0xD800) + output = appendBytes(output, stringFromCharCode(codePoint)) + value = 0xDC00 | value&0x3FF + } + output = appendBytes(output, stringFromCharCode(value)) + index++ + } + return output +} + +func appendBytes(dest []byte, bytes []byte) []byte { + for i := 0; i < len(bytes); i++ { + dest = append(dest, bytes[i]) + } + return dest +} + +func checkScalarValue(codePoint rune) error { + if codePoint >= 0xD800 && codePoint <= 0xDFFF { + return fmt.Errorf("lone surrogate U+%s is not a scalar value", hexutil.EncodeUint64(uint64(codePoint))) + } + return nil +} + +func stringFromCharCode(codePoint rune) []byte { + var buf = make([]byte, 4) + n := utf8.EncodeRune(buf, codePoint) + return buf[0:n] +} + +func createByte(codePoint rune, shift uint32) []byte { + return stringFromCharCode(((codePoint >> shift) & 0x3F) | 0x80) +} + +func encodeCodePoint(codePoint rune) ([]byte, error) { + if (uint32(codePoint) & uint32(0xFFFFFF80)) == 0 { // 1-byte sequence + return stringFromCharCode(codePoint), nil + } + var symbol []byte + if uint32(codePoint)&0xFFFFF800 == 0 { // 2-byte sequence + symbol = stringFromCharCode(((codePoint >> 6) & 0x1F) | 0xC0) + } else if (uint32(codePoint) & 0xFFFF0000) == 0 { // 3-byte sequence + err := checkScalarValue(codePoint) + if err != nil { + return nil, err + } + symbol = stringFromCharCode(((codePoint >> 12) & 0x0F) | 0xE0) + symbol = appendBytes(symbol, createByte(codePoint, 6)) + } else if (uint32(codePoint) & 0xFFE00000) == 0 { // 4-byte sequence + symbol = stringFromCharCode(((codePoint >> 18) & 0x07) | 0xF0) + symbol = appendBytes(symbol, createByte(codePoint, 12)) + symbol = appendBytes(symbol, createByte(codePoint, 6)) + } + symbol = appendBytes(symbol, stringFromCharCode((codePoint&0x3F)|0x80)) + return symbol, nil +} + +// implementation referenced from https://github.com/mathiasbynens/utf8.js/blob/2ce09544b62f2a274dbcd249473c0986e3660849/utf8.js +func Utf8encode(str string) (string, error) { + var codePoints = ucs2decode(str) + var length = len(codePoints) + var index = 0 + var codePoint rune + var bytes []byte + for index < length { + codePoint = codePoints[index] + cps, err := encodeCodePoint(codePoint) + if err != nil { + return "", err + } + bytes = appendBytes(bytes, cps) + index++ + } + return string(bytes), nil +} + +func readContinuationByte(byteArray []rune, byteCount int, pByteIndex *int) (rune, error) { + if *pByteIndex >= byteCount { + return utf8.RuneError, fmt.Errorf("invalid byte index") + } + + var continuationByte = byteArray[*pByteIndex] & 0xFF + *pByteIndex = *pByteIndex + 1 + + if (continuationByte & 0xC0) == 0x80 { + return continuationByte & 0x3F, nil + } + + // If we end up here, it’s not a continuation byte + return utf8.RuneError, fmt.Errorf("invalid continuation byte") +} + +func decodeSymbol(byteArray []rune, byteCount int, pByteIndex *int) (rune, bool, error) { + var byte1 rune + var codePoint rune + + if *pByteIndex > byteCount { + return utf8.RuneError, false, fmt.Errorf("invalid byte index") + } + + if *pByteIndex == byteCount { + return utf8.RuneError, false, nil + } + + // Read first byte + byte1 = byteArray[*pByteIndex] & 0xFF + *pByteIndex = *pByteIndex + 1 + + // 1-byte sequence (no continuation bytes) + if (byte1 & 0x80) == 0 { + return byte1, true, nil + } + + // 2-byte sequence + if (byte1 & 0xE0) == 0xC0 { + byte2, err := readContinuationByte(byteArray, byteCount, pByteIndex) + if err != nil { + return utf8.RuneError, false, err + } + codePoint = ((byte1 & 0x1F) << 6) | byte2 + if codePoint >= 0x80 { + return codePoint, true, nil + } + return utf8.RuneError, false, fmt.Errorf("invalid continuation byte") + } + + // 3-byte sequence (may include unpaired surrogates) + if (byte1 & 0xF0) == 0xE0 { + byte2, err := readContinuationByte(byteArray, byteCount, pByteIndex) + if err != nil { + return utf8.RuneError, false, err + } + byte3, err := readContinuationByte(byteArray, byteCount, pByteIndex) + if err != nil { + return utf8.RuneError, false, err + } + codePoint = ((byte1 & 0x0F) << 12) | (byte2 << 6) | byte3 + if codePoint >= 0x0800 { + err := checkScalarValue(codePoint) + if err != nil { + return utf8.RuneError, false, err + } + return codePoint, true, nil + } + return utf8.RuneError, false, fmt.Errorf("invalid continuation byte") + } + + // 4-byte sequence + if (byte1 & 0xF8) == 0xF0 { + byte2, err := readContinuationByte(byteArray, byteCount, pByteIndex) + if err != nil { + return utf8.RuneError, false, err + } + byte3, err := readContinuationByte(byteArray, byteCount, pByteIndex) + if err != nil { + return utf8.RuneError, false, err + } + byte4, err := readContinuationByte(byteArray, byteCount, pByteIndex) + if err != nil { + return utf8.RuneError, false, err + } + codePoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0C) | + (byte3 << 0x06) | byte4 + if codePoint >= 0x010000 && codePoint <= 0x10FFFF { + return codePoint, true, nil + } + } + + return utf8.RuneError, false, fmt.Errorf("invalid UTF-8 detected") +} + +// implementation referenced from https://github.com/mathiasbynens/utf8.js/blob/2ce09544b62f2a274dbcd249473c0986e3660849/utf8.js +func Utf8decode(str string) ([]byte, error) { + byteArray := ucs2decode(str) + byteCount := len(byteArray) + byteIndex := 0 + var codePoints []rune + for true { + codePoint, goOn, err := decodeSymbol(byteArray, byteCount, &byteIndex) + if err != nil { + return nil, err + } + if !goOn { + break + } + codePoints = append(codePoints, codePoint) + } + return ucs2encode(codePoints), nil +} diff --git a/abi-spec/utils.go b/abi-spec/utils.go index b609f24e9..6be505c44 100644 --- a/abi-spec/utils.go +++ b/abi-spec/utils.go @@ -3,8 +3,23 @@ package abispec import ( "fmt" "math/big" + "regexp" + "strings" + "unicode" + + "github.com/ethereum/go-ethereum/common" + "github.com/status-im/status-go/eth-node/crypto" ) +var unicodeZeroPattern = regexp.MustCompile("^(?:\u0000)*") +var hexZeroPattern = regexp.MustCompile("^(?:00)*") +var hexStringPattern = regexp.MustCompile("(?i)^[0-9a-f]+$") +var hexPrefixPattern = regexp.MustCompile("(?i)^0x") + +var addressBasicPattern = regexp.MustCompile("(?i)^(0x)?[0-9a-f]{40}$") +var addressLowerCasePattern = regexp.MustCompile("^(0x|0X)?[0-9a-f]{40}$") +var addressUpperCasePattern = regexp.MustCompile("^(0x|0X)?[0-9A-F]{40}$") + func HexToNumber(hex string) string { num, success := big.NewInt(0).SetString(hex, 16) if success { @@ -20,3 +35,158 @@ func NumberToHex(numString string) string { } return "" } + +func Sha3(str string) string { + bytes := crypto.Keccak256([]byte(str)) + return common.Bytes2Hex(bytes) +} + +func reverse(str string) string { + bytes := []byte(str) + var out []byte + for i := len(bytes) - 1; i >= 0; i-- { + out = append(out, bytes[i]) + } + return string(out) +} + +// remove \u0000 padding from either side +func removeUnicodeZeroPadding(str string) string { + found := unicodeZeroPattern.FindString(str) + str = strings.Replace(str, found, "", 1) + str = reverse(str) + found = unicodeZeroPattern.FindString(str) + str = strings.Replace(str, found, "", 1) + return reverse(str) +} + +// remove 00 padding from either side +func removeHexZeroPadding(str string) string { + found := hexZeroPattern.FindString(str) + str = strings.Replace(str, found, "", 1) + str = reverse(str) + found = hexZeroPattern.FindString(str) + str = strings.Replace(str, found, "", 1) + return reverse(str) +} + +// implementation referenced from https://github.com/ChainSafe/web3.js/blob/edcd215bf657a4bba62fabaafd08e6e70040976e/packages/web3-utils/src/utils.js#L165 +func Utf8ToHex(str string) (string, error) { + str, err := Utf8encode(str) + if err != nil { + return "", err + } + str = removeUnicodeZeroPadding(str) + + var hex = "" + for _, r := range str { + n := fmt.Sprintf("%x", r) + if len(n) < 2 { + hex += "0" + n + } else { + hex += n + } + } + return "0x" + hex, nil +} + +// implementation referenced from https://github.com/ChainSafe/web3.js/blob/edcd215bf657a4bba62fabaafd08e6e70040976e/packages/web3-utils/src/utils.js#L193 +func HexToUtf8(hexString string) (string, error) { + hexString = removeHexPrefix(hexString) + if !hexStringPattern.MatchString(hexString) { + return "", fmt.Errorf("the parameter '%s' must be a valid HEX string", hexString) + } + if len(hexString)%2 != 0 { + return "", fmt.Errorf("the parameter '%s' must have a even number of digits", hexString) + } + + hexString = removeHexZeroPadding(hexString) + + n := big.NewInt(0) + var bytes []byte + for i := 0; i < len(hexString); i += 2 { + hex := hexString[i : i+2] + n, success := n.SetString(hex, 16) + if !success { + return "", fmt.Errorf("invalid hex value %s", hex) + } + r := rune(n.Int64()) + bs := stringFromCharCode(r) + bytes = appendBytes(bytes, bs) + } + + bytes, err := Utf8decode(string(bytes)) + if err != nil { + return "", err + } + return string(bytes), nil +} + +func removeHexPrefix(str string) string { + found := hexPrefixPattern.FindString(str) + return strings.Replace(str, found, "", 1) +} + +// implementation referenced from https://github.com/ChainSafe/web3.js/blob/edcd215bf657a4bba62fabaafd08e6e70040976e/packages/web3-utils/src/utils.js#L107 +func CheckAddressChecksum(address string) (bool, error) { + address = removeHexPrefix(address) + addressHash := Sha3(strings.ToLower(address)) + + n := big.NewInt(0) + for i := 0; i < 40; i++ { + // the nth letter should be uppercase if the nth digit of casemap is 1 + n, success := n.SetString(addressHash[i:i+1], 16) + if !success { + return false, fmt.Errorf("failed to convert hex value '%s' to int", addressHash[i:i+1]) + } + v := n.Int64() + + if (v > 7 && uint8(unicode.ToUpper(rune(address[i]))) != address[i]) || (v <= 7 && uint8(unicode.ToLower(rune(address[i]))) != address[i]) { + return false, nil + } + } + return true, nil +} + +// implementation referenced from https://github.com/ChainSafe/web3.js/blob/edcd215bf657a4bba62fabaafd08e6e70040976e/packages/web3-utils/src/utils.js#L85 +func IsAddress(address string) (bool, error) { + // check if it has the basic requirements of an address + if !addressBasicPattern.MatchString(address) { + return false, nil + } else if addressLowerCasePattern.MatchString(address) || addressUpperCasePattern.MatchString(address) { + return true, nil + } else { + return CheckAddressChecksum(address) + } +} + +// implementation referenced from https://github.com/ChainSafe/web3.js/blob/2022b17d52d31ce95559d18d5530d18c83eb4d1c/packages/web3-utils/src/index.js#L283 +func ToChecksumAddress(address string) (string, error) { + if strings.Trim(address, "") == "" { + return "", nil + } + if !addressBasicPattern.MatchString(address) { + return "", fmt.Errorf("given address '%s' is not a valid Ethereum address", address) + } + + address = strings.ToLower(address) + address = removeHexPrefix(address) + addressHash := Sha3(address) + + var checksumAddress = []rune("0x") + var n = big.NewInt(0) + for i := 0; i < len(address); i++ { + // If ith character is 9 to f then make it uppercase + n, success := n.SetString(addressHash[i:i+1], 16) + if !success { + return "", fmt.Errorf("failed to convert hex value '%s' to int", addressHash[i:i+1]) + } + if n.Int64() > 7 { + upper := unicode.ToUpper(rune(address[i])) + checksumAddress = append(checksumAddress, upper) + } else { + checksumAddress = append(checksumAddress, rune(address[i])) + } + } + return string(checksumAddress), nil +} diff --git a/abi-spec/utils_test.go b/abi-spec/utils_test.go index 05c4d4e04..2f6225059 100644 --- a/abi-spec/utils_test.go +++ b/abi-spec/utils_test.go @@ -33,3 +33,75 @@ func TestHexToNumber(t *testing.T) { func TestNumberToHex(t *testing.T) { require.Equal(t, "20000000000002", NumberToHex("9007199254740994")) } + +func TestSha3(t *testing.T) { + require.Equal(t, "48bed44d1bcd124a28c27f343a817e5f5243190d3c52bf347daf876de1dbbf77", Sha3("abcd")) +} + +func TestHexToUtf8(t *testing.T) { + str, err := HexToUtf8("0x49206861766520313030e282ac") + require.NoError(t, err) + require.Equal(t, "I have 100€", str) + + str, err = HexToUtf8("0xe4bda0e5a5bd") + require.NoError(t, err) + require.Equal(t, "你好", str) + + _, err = HexToUtf8("0xfffefd") + require.ErrorContains(t, err, "invalid UTF-8 detected") +} + +func TestUtf8ToHex(t *testing.T) { + str, err := Utf8ToHex("\xff") + require.NoError(t, err) + require.Equal(t, "0xc3bf", str) + + str, err = Utf8ToHex("你好") + require.NoError(t, err) + require.Equal(t, "0xe4bda0e5a5bd", str) +} + +const ( + address1 = "0x0eD343df16A5327aC13B689072804A2705a26F47" + address2 = "0x0eD343df16A5327aC13B689072804A2705a26F47a" + address3 = "0x0eD343df16A5327aC13B689072804A2705a26f47" +) + +func TestIsAddress(t *testing.T) { + valid, err := IsAddress(address1) + require.NoError(t, err) + require.True(t, valid) + + valid, err = IsAddress(address2) + require.NoError(t, err) + require.False(t, valid) + + valid, err = IsAddress(address3) + require.NoError(t, err) + require.False(t, valid) +} + +func TestCheckAddressChecksum(t *testing.T) { + valid, err := CheckAddressChecksum(address1) + require.NoError(t, err) + require.True(t, valid) + + valid, err = CheckAddressChecksum(address2) + require.NoError(t, err) + require.False(t, valid) + + valid, err = CheckAddressChecksum(address3) + require.NoError(t, err) + require.False(t, valid) +} + +func TestToChecksumAddress(t *testing.T) { + checksumAddress, err := ToChecksumAddress(address3) + require.NoError(t, err) + require.Equal(t, address1, checksumAddress) + + address := "0x0ed343df16A5327aC13B689072804A2705A26f47" + checksumAddress, err = ToChecksumAddress(address) + require.NoError(t, err) + require.Equal(t, address1, checksumAddress) +} diff --git a/mobile/status.go b/mobile/status.go index fc4b25294..1ea345220 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -986,3 +986,49 @@ func HexToNumber(hex string) string { func NumberToHex(numString string) string { return abi_spec.NumberToHex(numString) } + +func Sha3(str string) string { + return "0x" + abi_spec.Sha3(str) +} + +func Utf8ToHex(str string) string { + hexString, err := abi_spec.Utf8ToHex(str) + if err != nil { + log.Error("failed to convert utf8 to hex", "str", str, "error", err) + } + return hexString +} + +func HexToUtf8(hexString string) string { + str, err := abi_spec.HexToUtf8(hexString) + if err != nil { + log.Error("failed to convert hex to utf8", "hexString", hexString, "error", err) + } + return str +} + +func CheckAddressChecksum(address string) string { + valid, err := abi_spec.CheckAddressChecksum(address) + if err != nil { + log.Error("failed to invoke check address checksum", "address", address, "error", err) + } + result, _ := json.Marshal(valid) + return string(result) +} + +func IsAddress(address string) string { + valid, err := abi_spec.IsAddress(address) + if err != nil { + log.Error("failed to invoke IsAddress", "address", address, "error", err) + } + result, _ := json.Marshal(valid) + return string(result) +} + +func ToChecksumAddress(address string) string { + address, err := abi_spec.ToChecksumAddress(address) + if err != nil { + log.Error("failed to convert to checksum address", "address", address, "error", err) + } + return address +}