From 6dc2c62b7340b186cdf1fc3abb7d8d1e096b101f Mon Sep 17 00:00:00 2001 From: 10gic Date: Fri, 22 Nov 2024 17:52:49 +0800 Subject: [PATCH] Export bech32 and bech32m functions (#4101) * Export bech32 and bech32m functions * Fix test cases * Merge bech32m functions into TWBech32 struct * Fix failed test cases * Fix failed swift test case --- .../trustwallet/core/app/utils/TestBech32.kt | 49 ++++++++++++ codegen-v2/manifest/TWBech32.yaml | 78 +++++++++++++++++++ include/TrustWalletCore/TWBase58.h | 4 +- include/TrustWalletCore/TWBech32.h | 47 +++++++++++ src/Bech32.cpp | 4 +- src/interface/TWBech32.cpp | 60 ++++++++++++++ swift/Tests/Bech32Tests.swift | 42 ++++++++++ tests/interface/TWBech32Tests.cpp | 49 ++++++++++++ wasm/tests/Bech32.test.ts | 47 +++++++++++ 9 files changed, 376 insertions(+), 4 deletions(-) create mode 100644 android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestBech32.kt create mode 100644 codegen-v2/manifest/TWBech32.yaml create mode 100644 include/TrustWalletCore/TWBech32.h create mode 100644 src/interface/TWBech32.cpp create mode 100644 swift/Tests/Bech32Tests.swift create mode 100644 tests/interface/TWBech32Tests.cpp create mode 100644 wasm/tests/Bech32.test.ts diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestBech32.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestBech32.kt new file mode 100644 index 00000000000..64b32728c5c --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestBech32.kt @@ -0,0 +1,49 @@ +package com.trustwallet.core.app.utils + +import org.junit.Assert.* +import org.junit.Test +import wallet.core.jni.Bech32 + +class TestBech32 { + init { + System.loadLibrary("TrustWalletCore"); + } + + @Test + fun testEncode() { + val data = Numeric.hexStringToByteArray("00443214c74254b635cf84653a56d7c675be77df") + assertEquals(Bech32.encode("abcdef", data), "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw") + } + + @Test + fun testDecode() { + val decoded = Bech32.decode("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw") + assertEquals(Numeric.toHexString(decoded), "0x00443214c74254b635cf84653a56d7c675be77df") + } + + @Test + fun testDecodeWrongChecksumVariant() { + // This is a Bech32m variant, not Bech32 variant. So it should fail using Bech32 decoder. + val decoded = Bech32.decode("abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx") + assertNull(decoded) + } + + @Test + fun testEncodeM() { + val data = Numeric.hexStringToByteArray("ffbbcdeb38bdab49ca307b9ac5a928398a418820") + assertEquals(Bech32.encodeM("abcdef", data), "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx") + } + + @Test + fun testDecodeM() { + val decoded = Bech32.decodeM("abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx") + assertEquals(Numeric.toHexString(decoded), "0xffbbcdeb38bdab49ca307b9ac5a928398a418820") + } + + @Test + fun testDecodeMWrongChecksumVariant() { + // This is a Bech32 variant, not Bech32m variant. So it should fail using Bech32M decoder. + val decoded = Bech32.decodeM("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw") + assertNull(decoded) + } +} diff --git a/codegen-v2/manifest/TWBech32.yaml b/codegen-v2/manifest/TWBech32.yaml new file mode 100644 index 00000000000..1d192712adb --- /dev/null +++ b/codegen-v2/manifest/TWBech32.yaml @@ -0,0 +1,78 @@ +name: TWBech32 +structs: +- name: TWBech32 + is_public: true + is_class: false +functions: +- name: TWBech32Encode + is_public: true + is_static: true + params: + - name: hrp + type: + variant: string + is_constant: true + is_nullable: false + is_pointer: true + - name: data + type: + variant: data + is_constant: true + is_nullable: false + is_pointer: true + return_type: + variant: string + is_constant: true + is_nullable: false + is_pointer: true +- name: TWBech32Decode + is_public: true + is_static: true + params: + - name: string + type: + variant: string + is_constant: true + is_nullable: false + is_pointer: true + return_type: + variant: data + is_constant: true + is_nullable: true + is_pointer: true +- name: TWBech32EncodeM + is_public: true + is_static: true + params: + - name: hrp + type: + variant: string + is_constant: true + is_nullable: false + is_pointer: true + - name: data + type: + variant: data + is_constant: true + is_nullable: false + is_pointer: true + return_type: + variant: string + is_constant: true + is_nullable: false + is_pointer: true +- name: TWBech32DecodeM + is_public: true + is_static: true + params: + - name: string + type: + variant: string + is_constant: true + is_nullable: false + is_pointer: true + return_type: + variant: data + is_constant: true + is_nullable: true + is_pointer: true diff --git a/include/TrustWalletCore/TWBase58.h b/include/TrustWalletCore/TWBase58.h index c59ddd2a2a0..a35ae178865 100644 --- a/include/TrustWalletCore/TWBase58.h +++ b/include/TrustWalletCore/TWBase58.h @@ -31,14 +31,14 @@ TWString *_Nonnull TWBase58EncodeNoCheck(TWData *_Nonnull data); /// Decodes a Base58 string, checking the checksum. Returns null if the string is not a valid Base58 string. /// /// \param string The Base58 string to decode. -/// \return the decoded data, empty if the string is not a valid Base58 string with checksum. +/// \return the decoded data, null if the string is not a valid Base58 string with checksum. TW_EXPORT_STATIC_METHOD TWData *_Nullable TWBase58Decode(TWString *_Nonnull string); /// Decodes a Base58 string, w/o checking the checksum. Returns null if the string is not a valid Base58 string. /// /// \param string The Base58 string to decode. -/// \return the decoded data, empty if the string is not a valid Base58 string without checksum. +/// \return the decoded data, null if the string is not a valid Base58 string without checksum. TW_EXPORT_STATIC_METHOD TWData *_Nullable TWBase58DecodeNoCheck(TWString *_Nonnull string); diff --git a/include/TrustWalletCore/TWBech32.h b/include/TrustWalletCore/TWBech32.h new file mode 100644 index 00000000000..53c0ab8973b --- /dev/null +++ b/include/TrustWalletCore/TWBech32.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWData.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// Bech32 encode / decode functions +TW_EXPORT_STRUCT +struct TWBech32; + +/// Encodes data as a Bech32 string. +/// +/// \param hrp The human-readable part. +/// \param data The data part. +/// \return the encoded Bech32 string. +TW_EXPORT_STATIC_METHOD +TWString *_Nonnull TWBech32Encode(TWString* _Nonnull hrp, TWData *_Nonnull data); + +/// Decodes a Bech32 string. Returns null if the string is not a valid Bech32 string. +/// +/// \param string The Bech32 string to decode. +/// \return the decoded data, null if the string is not a valid Bech32 string. Note that the human-readable part is not returned. +TW_EXPORT_STATIC_METHOD +TWData *_Nullable TWBech32Decode(TWString *_Nonnull string); + +/// Encodes data as a Bech32m string. +/// +/// \param hrp The human-readable part. +/// \param data The data part. +/// \return the encoded Bech32m string. +TW_EXPORT_STATIC_METHOD +TWString *_Nonnull TWBech32EncodeM(TWString* _Nonnull hrp, TWData *_Nonnull data); + +/// Decodes a Bech32m string. Returns null if the string is not a valid Bech32m string. +/// +/// \param string The Bech32m string to decode. +/// \return the decoded data, null if the string is not a valid Bech32m string. Note that the human-readable part is not returned. +TW_EXPORT_STATIC_METHOD +TWData *_Nullable TWBech32DecodeM(TWString *_Nonnull string); + +TW_EXTERN_C_END diff --git a/src/Bech32.cpp b/src/Bech32.cpp index d6bd309a1de..273c1f3f16c 100644 --- a/src/Bech32.cpp +++ b/src/Bech32.cpp @@ -103,7 +103,7 @@ Data create_checksum(const std::string& hrp, const Data& values, ChecksumVariant } // namespace -/** Encode a Bech32 string. */ +/** Encode a Bech32 string. Note that the values must each encode 5 bits, normally get from convertBits<8, 5, true> */ std::string encode(const std::string& hrp, const Data& values, ChecksumVariant variant) { Data checksum = create_checksum(hrp, values, variant); Data combined = values; @@ -116,7 +116,7 @@ std::string encode(const std::string& hrp, const Data& values, ChecksumVariant v return ret; } -/** Decode a Bech32 string. */ +/** Decode a Bech32 string. Note that the returned values are 5 bits each, you may want to use convertBits<5, 8, false> */ std::tuple decode(const std::string& str) { if (str.length() > 120 || str.length() < 2) { // too long or too short diff --git a/src/interface/TWBech32.cpp b/src/interface/TWBech32.cpp new file mode 100644 index 00000000000..fe184ead846 --- /dev/null +++ b/src/interface/TWBech32.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include + +#include "../Bech32.h" + +#include + +using namespace TW; + +static TWString *_Nonnull encodeGeneric(TWString* _Nonnull hrp, TWData *_Nonnull data, const Bech32::ChecksumVariant variant) { + const auto cppHrp = *static_cast(hrp); + const auto cppData = *static_cast(data); + Data enc; + if (!Bech32::convertBits<8, 5, true>(enc, cppData)) { + return TWStringCreateWithUTF8Bytes(""); + } + const auto result = Bech32::encode(cppHrp, enc, variant); + return TWStringCreateWithUTF8Bytes(result.c_str()); +} + +static TWData *_Nullable decodeGeneric(TWString *_Nonnull string, const Bech32::ChecksumVariant variant) { + const auto cppString = *static_cast(string); + const auto decoded = Bech32::decode(cppString); + + const auto data = std::get<1>(decoded); + if (data.empty()) { // Failed to decode + return nullptr; + } + + if (std::get<2>(decoded) != variant) { // Wrong ChecksumVariant + return nullptr; + } + + // Bech bits conversion + Data conv; + if (!Bech32::convertBits<5, 8, false>(conv, data)) { + return nullptr; + } + + return TWDataCreateWithBytes(conv.data(), conv.size()); +} + +TWString *_Nonnull TWBech32Encode(TWString* _Nonnull hrp, TWData *_Nonnull data) { + return encodeGeneric(hrp, data, Bech32::ChecksumVariant::Bech32); +} + +TWString *_Nonnull TWBech32EncodeM(TWString* _Nonnull hrp, TWData *_Nonnull data) { + return encodeGeneric(hrp, data, Bech32::ChecksumVariant::Bech32M); +} + +TWData *_Nullable TWBech32Decode(TWString *_Nonnull string) { + return decodeGeneric(string, Bech32::ChecksumVariant::Bech32); +} + +TWData *_Nullable TWBech32DecodeM(TWString *_Nonnull string) { + return decodeGeneric(string, Bech32::ChecksumVariant::Bech32M); +} diff --git a/swift/Tests/Bech32Tests.swift b/swift/Tests/Bech32Tests.swift new file mode 100644 index 00000000000..f09bd9507fa --- /dev/null +++ b/swift/Tests/Bech32Tests.swift @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +import XCTest +import WalletCore + +class Bech32Tests: XCTestCase { + func testEncode() { + let data = Data(hexString: "00443214c74254b635cf84653a56d7c675be77df")! + let encoded = Bech32.encode(hrp: "abcdef", data: data) + XCTAssertEqual(encoded, "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw") + } + + func testDecode() { + let decoded = Bech32.decode(string: "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw")! + XCTAssertEqual(decoded.hexString, "00443214c74254b635cf84653a56d7c675be77df") + } + + func testDecodeWrongChecksumVariant() { + // This is a Bech32m variant, not Bech32 variant. So it should fail using Bech32 decoder. + let decoded = Bech32.decode(string: "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx") + XCTAssertNil(decoded) + } + + func testEncodeM() { + let data = Data(hexString: "ffbbcdeb38bdab49ca307b9ac5a928398a418820")! + let encoded = Bech32.encodeM(hrp: "abcdef", data: data) + XCTAssertEqual(encoded, "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx") + } + + func testDecodeM() { + let decoded = Bech32.decodeM(string: "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx")! + XCTAssertEqual(decoded.hexString, "ffbbcdeb38bdab49ca307b9ac5a928398a418820") + } + + func testDecodeMWrongChecksumVariant() { + // This is a Bech32 variant, not Bech32m variant. So it should fail using Bech32M decoder. + let decoded = Bech32.decodeM(string: "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw") + XCTAssertNil(decoded) + } +} diff --git a/tests/interface/TWBech32Tests.cpp b/tests/interface/TWBech32Tests.cpp new file mode 100644 index 00000000000..00c1b3b6d98 --- /dev/null +++ b/tests/interface/TWBech32Tests.cpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TestUtilities.h" + +#include + +#include + +TEST(TWBech32, Encode) { + const auto hrp = STRING("abcdef"); + const auto data = DATA("00443214c74254b635cf84653a56d7c675be77df"); + const auto result = WRAPS(TWBech32Encode(hrp.get(), data.get())); + assertStringsEqual(result, "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"); +} + +TEST(TWBech32, Decode) { + const auto input = STRING("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"); + const auto result = WRAPD(TWBech32Decode(input.get())); + assertHexEqual(result, "00443214c74254b635cf84653a56d7c675be77df"); +} + +TEST(TWBech32, Decode_WrongChecksumVariant) { + // This is a Bech32m variant, not Bech32 variant. So it should fail using Bech32 decoder. + const auto input = STRING("abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx"); + const auto result = WRAPD(TWBech32Decode(input.get())); + ASSERT_EQ(result.get(), nullptr); +} + +TEST(TWBech32, EncodeM) { + const auto hrp = STRING("abcdef"); + const auto data = DATA("ffbbcdeb38bdab49ca307b9ac5a928398a418820"); + const auto result = WRAPS(TWBech32EncodeM(hrp.get(), data.get())); + assertStringsEqual(result, "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx"); +} + +TEST(TWBech32, DecodeM) { + const auto input = STRING("abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx"); + auto result = WRAPD(TWBech32DecodeM(input.get())); + assertHexEqual(result, "ffbbcdeb38bdab49ca307b9ac5a928398a418820"); +} + +TEST(TWBech32, DecodeM_WrongChecksumVariant) { + // This is a Bech32 variant, not Bech32m variant. So it should fail using Bech32M decoder. + const auto input = STRING("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"); + const auto result = WRAPD(TWBech32DecodeM(input.get())); + ASSERT_EQ(result.get(), nullptr); +} diff --git a/wasm/tests/Bech32.test.ts b/wasm/tests/Bech32.test.ts new file mode 100644 index 00000000000..f9ac76a0c9f --- /dev/null +++ b/wasm/tests/Bech32.test.ts @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +import { assert } from "chai"; +import { Buffer } from "buffer"; + +describe("Bech32", () => { + + it("test encode", () => { + const { Bech32 } = globalThis.core; + + const encoded = Bech32.encode("abcdef", Buffer.from("00443214c74254b635cf84653a56d7c675be77df", "hex")); + + assert.equal(encoded, "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"); + }); + + it("test decode", () => { + const { Bech32, HexCoding } = globalThis.core; + + const decoded = Bech32.decode("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"); + + assert.equal( + HexCoding.encode(decoded), + "0x00443214c74254b635cf84653a56d7c675be77df" + ); + }); + + it("test encodeM", () => { + const { Bech32 } = globalThis.core; + + const encoded = Bech32.encodeM("abcdef", Buffer.from("ffbbcdeb38bdab49ca307b9ac5a928398a418820", "hex")); + + assert.equal(encoded, "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx"); + }); + + it("test decodeM", () => { + const { Bech32, HexCoding } = globalThis.core; + + const decoded = Bech32.decodeM("abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx"); + + assert.equal( + HexCoding.encode(decoded), + "0xffbbcdeb38bdab49ca307b9ac5a928398a418820" + ); + }); +});