From feafc275fc8affc992f1d9677e04c3b81fc2fe3d Mon Sep 17 00:00:00 2001 From: satoshiotomakan <127754187+satoshiotomakan@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:51:24 +0200 Subject: [PATCH] [TON]: Add functions to help to generate Jetton user address (#3922) * [TON]: Add functions to help to generate Jetton user address * [TON]: Rename `TONAddress` module to `TONAddressConverter` * Add ios, android tests * [CI] Trigger CI * [TON]: Fix Android test --- .../TestTheOpenNetworkAddress.kt | 15 ++ .../TrustWalletCore/TWTONAddressConverter.h | 34 ++++ rust/Cargo.lock | 10 ++ rust/tw_memory/src/ffi/c_result.rs | 7 + rust/wallet_core_rs/Cargo.toml | 2 + .../src/ffi/utils/bit_reader_ffi.rs | 161 ++++++++++++++++++ rust/wallet_core_rs/src/ffi/utils/mod.rs | 1 + rust/wallet_core_rs/tests/bit_reader.rs | 69 ++++++++ src/Everscale/CommonTON/BitReader.cpp | 39 +++++ src/Everscale/CommonTON/BitReader.h | 31 ++++ src/Everscale/CommonTON/Cell.cpp | 48 ++++++ src/Everscale/CommonTON/Cell.h | 4 + src/TheOpenNetwork/Address.cpp | 26 +++ src/TheOpenNetwork/Address.h | 6 + src/interface/TWTONAddressConverter.cpp | 40 +++++ src/rust/Wrapper.h | 15 ++ .../Blockchains/TheOpenNetworkTests.swift | 14 ++ tests/chains/TheOpenNetwork/AddressTests.cpp | 69 ++++++++ 18 files changed, 591 insertions(+) create mode 100644 include/TrustWalletCore/TWTONAddressConverter.h create mode 100644 rust/wallet_core_rs/src/ffi/utils/bit_reader_ffi.rs create mode 100644 rust/wallet_core_rs/tests/bit_reader.rs create mode 100644 src/Everscale/CommonTON/BitReader.cpp create mode 100644 src/Everscale/CommonTON/BitReader.h create mode 100644 src/interface/TWTONAddressConverter.cpp diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkAddress.kt index c96c5ebb21a..66433c2e0a8 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkAddress.kt @@ -45,4 +45,19 @@ class TestTheOpenNetworkAddress { val address = AnyAddress(addressString, CoinType.TON) assertEquals(address.description(), "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q") } + + @Test + fun testGenerateJettonAddress() { + val mainAddress = "UQBjKqthWBE6GEcqb_epTRFrQ1niS6Z1Z1MHMwR-mnAYRoYr" + val mainAddressBoc = TONAddressConverter.toBoc(mainAddress) + assertEquals(mainAddressBoc, "te6ccgICAAEAAQAAACQAAABDgAxlVWwrAidDCOVN/vUpoi1oazxJdM6s6mDmYI/TTgMI0A==") + + // curl --location 'https://toncenter.com/api/v2/runGetMethod' --header 'Content-Type: application/json' --data \ + // '{"address":"EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT","method":"get_wallet_address","method":"get_wallet_address","stack":[["tvm.Slice","te6ccgICAAEAAQAAACQAAABDgAxlVWwrAidDCOVN/vUpoi1oazxJdM6s6mDmYI/TTgMI0A=="]]}' + + // Parse the `get_wallet_address` RPC response. + val jettonAddressBocEncoded = "te6cckEBAQEAJAAAQ4AFvT5rqwxcbKfITqnkwL+go4Zi9bulRHAtLt4cjjFdK7B8L+Cq" + val jettonAddress = TONAddressConverter.fromBoc(jettonAddressBocEncoded) + assertEquals(jettonAddress, "UQAt6fNdWGLjZT5CdU8mBf0FHDMXrd0qI4FpdvDkcYrpXV5H") + } } diff --git a/include/TrustWalletCore/TWTONAddressConverter.h b/include/TrustWalletCore/TWTONAddressConverter.h new file mode 100644 index 00000000000..55dfcaff58a --- /dev/null +++ b/include/TrustWalletCore/TWTONAddressConverter.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// TON address operations. +TW_EXPORT_CLASS +struct TWTONAddressConverter; + +/// Converts a TON user address into a Bag of Cells (BoC) with a single root Cell. +/// The function is mostly used to request a Jetton user address via `get_wallet_address` RPC. +/// https://docs.ton.org/develop/dapps/asset-processing/jettons#retrieving-jetton-wallet-addresses-for-a-given-user +/// +/// \param address Address to be converted into a Bag Of Cells (BoC). +/// \return Pointer to a base64 encoded Bag Of Cells (BoC). Null if invalid address provided. +TW_EXPORT_STATIC_METHOD +TWString *_Nullable TWTONAddressConverterToBoc(TWString *_Nonnull address); + +/// Parses a TON address from a Bag of Cells (BoC) with a single root Cell. +/// The function is mostly used to parse a Jetton user address received on `get_wallet_address` RPC. +/// https://docs.ton.org/develop/dapps/asset-processing/jettons#retrieving-jetton-wallet-addresses-for-a-given-user +/// +/// \param boc Base64 encoded Bag Of Cells (BoC). +/// \return Pointer to a Jetton address. +TW_EXPORT_STATIC_METHOD +TWString *_Nullable TWTONAddressConverterFromBoc(TWString *_Nonnull boc); + +TW_EXTERN_C_END diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 38a72095cd9..73378992bdb 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -211,6 +211,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitreader" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd859c9d97f7c468252795b35aeccc412bdbb1e90ee6969c4fa6328272eaeff" +dependencies = [ + "cfg-if", +] + [[package]] name = "bitvec" version = "0.20.4" @@ -2107,6 +2116,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "wallet-core-rs" version = "0.1.0" dependencies = [ + "bitreader", "serde_json", "tw_any_coin", "tw_bitcoin", diff --git a/rust/tw_memory/src/ffi/c_result.rs b/rust/tw_memory/src/ffi/c_result.rs index fe6ed25bc32..a1db2322139 100644 --- a/rust/tw_memory/src/ffi/c_result.rs +++ b/rust/tw_memory/src/ffi/c_result.rs @@ -95,6 +95,12 @@ pub struct CBoolResult { pub result: bool, } +#[repr(C)] +pub struct CUInt8Result { + pub code: i32, + pub result: u8, +} + #[repr(C)] pub struct CUInt64Result { pub code: i32, @@ -104,4 +110,5 @@ pub struct CUInt64Result { impl_c_result!(CStrResult, *const c_char, core::ptr::null()); impl_c_result!(CStrMutResult, *mut c_char, core::ptr::null_mut()); impl_c_result!(CBoolResult, bool, false); +impl_c_result!(CUInt8Result, u8, 0); impl_c_result!(CUInt64Result, u64, 0); diff --git a/rust/wallet_core_rs/Cargo.toml b/rust/wallet_core_rs/Cargo.toml index eb148ecab18..3fb32b403a2 100644 --- a/rust/wallet_core_rs/Cargo.toml +++ b/rust/wallet_core_rs/Cargo.toml @@ -31,6 +31,7 @@ utils = [ ] [dependencies] +bitreader = "0.3.8" tw_any_coin = { path = "../tw_any_coin", optional = true } tw_bitcoin = { path = "../tw_bitcoin", optional = true } tw_coin_registry = { path = "../tw_coin_registry", optional = true } @@ -49,5 +50,6 @@ uuid = { version = "1.7", features = ["v4"], optional = true } serde_json = "1.0" tw_any_coin = { path = "../tw_any_coin", features = ["test-utils"] } tw_coin_entry = { path = "../tw_coin_entry", features = ["test-utils"] } +tw_encoding = { path = "../tw_encoding" } tw_memory = { path = "../tw_memory", features = ["test-utils"] } tw_number = { path = "../tw_number", features = ["helpers"] } diff --git a/rust/wallet_core_rs/src/ffi/utils/bit_reader_ffi.rs b/rust/wallet_core_rs/src/ffi/utils/bit_reader_ffi.rs new file mode 100644 index 00000000000..e9fcc925635 --- /dev/null +++ b/rust/wallet_core_rs/src/ffi/utils/bit_reader_ffi.rs @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#![allow(clippy::missing_safety_doc)] + +use bitreader::{BitReader, BitReaderError}; +use tw_memory::ffi::c_byte_array::{CByteArray, CByteArrayResult}; +use tw_memory::ffi::c_result::{CUInt8Result, ErrorCode}; +use tw_memory::ffi::tw_data::TWData; +use tw_memory::ffi::RawPtrTrait; +use tw_memory::Data; +use tw_misc::try_or_else; + +#[derive(Debug, Eq, PartialEq)] +#[repr(C)] +pub enum CBitReaderCode { + Ok = 0, + /// Requested more bits than there are left in the byte slice at the current position. + NotEnoughData = 1, + /// Requested more bits than the returned variable can hold, for example more than 8 bits when + /// reading into a u8. + TooManyBitsForType = 2, + InalidInput = 3, +} + +impl From for CBitReaderCode { + fn from(value: BitReaderError) -> Self { + match value { + BitReaderError::NotEnoughData { .. } => CBitReaderCode::NotEnoughData, + BitReaderError::TooManyBitsForType { .. } => CBitReaderCode::TooManyBitsForType, + } + } +} + +impl From for ErrorCode { + fn from(error: CBitReaderCode) -> Self { + error as ErrorCode + } +} + +/// BitReader reads data from a big-endian byte slice at the granularity of a single bit. +#[derive(Debug)] +pub struct TWBitReader { + buffer: Data, + bit_position: u64, + bit_len: u64, +} + +impl TWBitReader { + pub fn with_relative_bit_len(buffer: Data, bit_len: u64) -> TWBitReader { + TWBitReader { + buffer, + bit_position: 0, + bit_len, + } + } + + /// Read at most 8 bits into a u8. + pub fn read_u8(&mut self, bit_count: u8) -> Result { + let mut reader = self.make_reader()?; + let res = reader.read_u8(bit_count)?; + // Update the bit position in case of success read. + self.bit_position += bit_count as u64; + Ok(res) + } + + // Reads an entire slice of `byte_count` bytes. If there aren't enough bits remaining + // after the internal cursor's current position, returns none. + pub fn read_u8_slice(&mut self, byte_count: usize) -> Result { + let mut reader = self.make_reader()?; + + let mut res = vec![0_u8; byte_count]; + reader.read_u8_slice(&mut res)?; + + // Update the bit position in case of success read. + self.bit_position += byte_count as u64 * 8; + Ok(res) + } + + pub fn is_finished(&self) -> bool { + self.bit_len == self.bit_position + } + + fn make_reader(&self) -> Result, CBitReaderCode> { + let mut reader = BitReader::new(&self.buffer).relative_reader_atmost(self.bit_len); + reader.skip(self.bit_position)?; + Ok(reader) + } +} + +impl RawPtrTrait for TWBitReader {} + +/// Constructs a new `TWBitReader` from a big-endian byte slice +/// that will not allow reading more than `bit_len` bits. It must be deleted at the end. +/// +/// \param data big-endian byte slice to be read. +/// \param bit_len length this reader is allowed to read from the slice. +/// \return nullable pointer to a `TWBitReader` instance. +#[no_mangle] +pub unsafe extern "C" fn tw_bit_reader_create( + data: *const TWData, + bit_len: u64, +) -> *mut TWBitReader { + let data = try_or_else!(TWData::from_ptr_as_ref(data), std::ptr::null_mut); + TWBitReader::with_relative_bit_len(data.to_vec(), bit_len).into_ptr() +} + +/// Deletes a `TWBitReader` and frees the memory. +/// \param reader a `TWBitReader` pointer. +#[no_mangle] +pub unsafe extern "C" fn tw_bit_reader_delete(reader: *mut TWBitReader) { + // Take the ownership back to rust and drop the owner. + let _ = TWBitReader::from_ptr(reader); +} + +/// Read at most 8 bits into a u8. +/// +/// \param reader a `TWBitReader` pointer. +/// \param bit_count number of bits to read. Expected from 1 to 8. +/// \return u8 or error. +#[no_mangle] +pub unsafe extern "C" fn tw_bit_reader_read_u8( + reader: *mut TWBitReader, + bit_count: u8, +) -> CUInt8Result { + let tw_reader = try_or_else!( + TWBitReader::from_ptr_as_mut(reader), + || CUInt8Result::error(CBitReaderCode::InalidInput) + ); + tw_reader.read_u8(bit_count).into() +} + +/// Reads an entire slice of `byteCount` bytes. If there aren't enough bits remaining +/// after the internal cursor's current position, returns null. +/// +/// \param reader a `TWBitReader` pointer. +/// \param byte_count number of bytes to read. +/// \return byte array or error. +#[no_mangle] +pub unsafe extern "C" fn tw_bit_reader_read_u8_slice( + reader: *mut TWBitReader, + byte_count: usize, +) -> CByteArrayResult { + let tw_reader = try_or_else!(TWBitReader::from_ptr_as_mut(reader), || { + CByteArrayResult::error(CBitReaderCode::InalidInput) + }); + tw_reader + .read_u8_slice(byte_count) + .map(CByteArray::from) + .into() +} + +/// Checks whether all bits were read. +/// +/// \param reader a `TWBitReader` pointer. +/// \return whether all bits were read. +#[no_mangle] +pub unsafe extern "C" fn tw_bit_reader_finished(reader: *const TWBitReader) -> bool { + try_or_else!(TWBitReader::from_ptr_as_ref(reader), || true).is_finished() +} diff --git a/rust/wallet_core_rs/src/ffi/utils/mod.rs b/rust/wallet_core_rs/src/ffi/utils/mod.rs index f233b194cb3..a6b8ac45aec 100644 --- a/rust/wallet_core_rs/src/ffi/utils/mod.rs +++ b/rust/wallet_core_rs/src/ffi/utils/mod.rs @@ -2,4 +2,5 @@ // // Copyright © 2017 Trust Wallet. +pub mod bit_reader_ffi; pub mod uuid_ffi; diff --git a/rust/wallet_core_rs/tests/bit_reader.rs b/rust/wallet_core_rs/tests/bit_reader.rs new file mode 100644 index 00000000000..ceb7d5fc5ba --- /dev/null +++ b/rust/wallet_core_rs/tests/bit_reader.rs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_encoding::hex::{DecodeHex, ToHex}; +use tw_memory::test_utils::tw_data_helper::TWDataHelper; +use wallet_core_rs::ffi::utils::bit_reader_ffi::{ + tw_bit_reader_create, tw_bit_reader_finished, tw_bit_reader_read_u8, + tw_bit_reader_read_u8_slice, CBitReaderCode, +}; + +#[test] +fn test_tw_bit_reader_success() { + let ton_address_cell = "8005bd3e6bab0c5c6ca7c84ea9e4c0bfa0a38662f5bba544702d2ede1c8e315d2ba0" + .decode_hex() + .unwrap(); + let ton_address_cell = TWDataHelper::create(ton_address_cell); + + let reader = unsafe { tw_bit_reader_create(ton_address_cell.ptr(), 267) }; + assert!(!reader.is_null()); + + let tp = unsafe { tw_bit_reader_read_u8(reader, 2) }; + assert_eq!(tp.into_result(), Ok(2)); + + let res1 = unsafe { tw_bit_reader_read_u8(reader, 1) }; + assert_eq!(res1.into_result(), Ok(0)); + + let wc = unsafe { tw_bit_reader_read_u8(reader, 8) }; + assert_eq!(wc.into_result(), Ok(0)); + + assert!(!unsafe { tw_bit_reader_finished(reader) }); + + let hash_part = unsafe { tw_bit_reader_read_u8_slice(reader, 32).unwrap().into_vec() }; + assert_eq!( + hash_part.to_hex(), + "2de9f35d5862e3653e42754f2605fd051c3317addd2a23816976f0e4718ae95d" + ); + + assert!(unsafe { tw_bit_reader_finished(reader) }); +} + +#[test] +fn test_tw_bit_reader_error() { + let bytes_len = 2; + // Less than two bytes. + let bits_len = 15; + + let data = TWDataHelper::create(vec![1; bytes_len]); + + let reader = unsafe { tw_bit_reader_create(data.ptr(), bits_len as u64) }; + assert!(!reader.is_null()); + + // Cannot read u8 from 9 bits. + let res = unsafe { tw_bit_reader_read_u8(reader, 9) }; + assert_eq!( + res.into_result().unwrap_err(), + CBitReaderCode::TooManyBitsForType as i32 + ); + + // Read a dummy u8. + let _ = unsafe { tw_bit_reader_read_u8_slice(reader, 8) }; + + // Cannot read 8 bits as there are 7 bits left only. + let res = unsafe { tw_bit_reader_read_u8_slice(reader, 8) }; + assert_eq!( + res.into_result().unwrap_err(), + CBitReaderCode::NotEnoughData as i32 + ); +} diff --git a/src/Everscale/CommonTON/BitReader.cpp b/src/Everscale/CommonTON/BitReader.cpp new file mode 100644 index 00000000000..44220144801 --- /dev/null +++ b/src/Everscale/CommonTON/BitReader.cpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "BitReader.h" + +namespace TW::CommonTON { + +std::optional BitReader::createExact(const TW::Data &buffer, uint64_t bitLen) { + Rust::TWDataWrapper twData(buffer); + auto *readerPtr = Rust::tw_bit_reader_create(twData.get(), bitLen); + if (!readerPtr) { + return std::nullopt; + } + + return BitReader(std::shared_ptr(readerPtr, Rust::tw_bit_reader_delete)); +} + +std::optional BitReader::readU8(uint8_t bitCount) { + Rust::CUInt8ResultWrapper res = Rust::tw_bit_reader_read_u8(reader.get(), bitCount); + if (res.isErr()) { + return std::nullopt; + } + return res.unwrap().value; +} + +std::optional BitReader::readU8Slice(uint64_t byteCount) { + Rust::CByteArrayResultWrapper res = Rust::tw_bit_reader_read_u8_slice(reader.get(), byteCount); + if (res.isErr()) { + return std::nullopt; + } + return res.unwrap().data; +} + +bool BitReader::finished() const { + return Rust::tw_bit_reader_finished(reader.get()); +} + +} // namespace TW::CommonTON diff --git a/src/Everscale/CommonTON/BitReader.h b/src/Everscale/CommonTON/BitReader.h new file mode 100644 index 00000000000..51177f25b09 --- /dev/null +++ b/src/Everscale/CommonTON/BitReader.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "rust/Wrapper.h" + +namespace TW::CommonTON { + +class BitReader { +public: + // Tries to create a bit reader with exact `bitLen` number of bits. + static std::optional createExact(const Data& buffer, uint64_t bitLen); + + // Read at most 8 bits into a u8. + std::optional readU8(uint8_t bitCount); + + // Reads an entire slice of `byteCount` bytes. If there aren't enough bits remaining + // after the internal cursor's current position, returns none. + std::optional readU8Slice(uint64_t byteCount); + + bool finished() const; + +private: + explicit BitReader(std::shared_ptr reader): reader(std::move(reader)) {} + + std::shared_ptr reader; +}; + +} // namespace TW::CommonTON diff --git a/src/Everscale/CommonTON/Cell.cpp b/src/Everscale/CommonTON/Cell.cpp index 8329c299713..b3be8774405 100644 --- a/src/Everscale/CommonTON/Cell.cpp +++ b/src/Everscale/CommonTON/Cell.cpp @@ -12,6 +12,7 @@ #include "Base64.h" #include "BinaryCoding.h" +#include "BitReader.h" using namespace TW; @@ -390,4 +391,51 @@ void Cell::finalize() { finalized = true; } +std::optional Cell::parseAddress() const { + auto reader = BitReader::createExact(data, static_cast(bitLen)); + if (!reader) { + return std::nullopt; + } + + auto tp = reader->readU8(2); + if (!tp) { + return std::nullopt; + } + + if (tp.value() == 0) { + // Hole address (default). Check if the Cell does not contain more bits. + if (!reader->finished()) { + return std::nullopt; + } + return AddressData(); + } + + // We expect type=0 or type=2 addresses only. + if (tp.value() != 2) { + return std::nullopt; + } + + // Ignore res1 value. + reader->readU8(1); + + auto workchain = reader->readU8(8); + if (!workchain) { + return std::nullopt; + } + + auto hashPart = reader->readU8Slice(AddressData::size); + if (!hashPart) { + return std::nullopt; + } + + if (!reader->finished()) { + return std::nullopt; + } + + std::array parsedHash {}; + std::copy(begin(hashPart.value()), end(hashPart.value()), begin(parsedHash)); + + return AddressData(static_cast(workchain.value()), parsedHash); +} + } // namespace TW::CommonTON diff --git a/src/Everscale/CommonTON/Cell.h b/src/Everscale/CommonTON/Cell.h index aaa03d67a9d..2341fd778db 100644 --- a/src/Everscale/CommonTON/Cell.h +++ b/src/Everscale/CommonTON/Cell.h @@ -10,6 +10,7 @@ #include "Data.h" #include "Hash.h" +#include "RawAddress.h" namespace TW::CommonTON { @@ -57,6 +58,9 @@ class Cell { [[nodiscard]] inline size_t serializedSize(uint8_t refSize) const noexcept { return 2 + (bitLen + 7) / 8 + refCount * refSize; } + + // Tries to parse an address from the Cell. + std::optional parseAddress() const; }; } // namespace TW::CommonTON diff --git a/src/TheOpenNetwork/Address.cpp b/src/TheOpenNetwork/Address.cpp index d8f0a194455..eb25f25aa2d 100644 --- a/src/TheOpenNetwork/Address.cpp +++ b/src/TheOpenNetwork/Address.cpp @@ -6,6 +6,7 @@ #include "Base64.h" #include "Crc.h" +#include "Everscale/CommonTON/CellBuilder.h" #include "WorkchainType.h" @@ -124,4 +125,29 @@ std::string Address::string(bool userFriendly, bool bounceable, bool testOnly) return Base64::encodeBase64Url(data); } +std::string Address::toBoc() const { + CommonTON::CellBuilder cellBuilder; + cellBuilder.appendAddress(addressData); + const auto cell = cellBuilder.intoCell(); + + Data bocData; + cell->serialize(bocData); + + return Base64::encode(bocData); +} + +std::optional
Address::fromBoc(const std::string& bocEncoded) { + const auto cell = CommonTON::Cell::fromBase64(bocEncoded); + if (!cell) { + return std::nullopt; + } + + const auto addressData = cell->parseAddress(); + if (!addressData) { + return std::nullopt; + } + + return std::make_optional
(addressData->workchainId, addressData->hash); +} + } // namespace TW::TheOpenNetwork diff --git a/src/TheOpenNetwork/Address.h b/src/TheOpenNetwork/Address.h index f830de83877..fe938a8f7b5 100644 --- a/src/TheOpenNetwork/Address.h +++ b/src/TheOpenNetwork/Address.h @@ -57,6 +57,12 @@ class Address { /// Returns a string representation of the address. [[nodiscard]] std::string string() const; [[nodiscard]] std::string string(bool userFriendly, bool bounceable = true, bool testOnly = false) const; + + // Converts a TON user address into a Bag of Cells (BoC) with a single root Cell. + [[nodiscard]] std::string toBoc() const; + + // Parses a TON address from a Bag of Cells (BoC) with a single root Cell. + [[nodiscard]] static std::optional
fromBoc(const std::string& bocEncoded); }; } // namespace TW::TheOpenNetwork diff --git a/src/interface/TWTONAddressConverter.cpp b/src/interface/TWTONAddressConverter.cpp new file mode 100644 index 00000000000..8da3613f8b0 --- /dev/null +++ b/src/interface/TWTONAddressConverter.cpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include + +#include "Base64.h" +#include "TheOpenNetwork/Address.h" + +using namespace TW; + +TWString *_Nullable TWTONAddressConverterToBoc(TWString *_Nonnull address) { + auto& addressString = *reinterpret_cast(address); + if (!TheOpenNetwork::Address::isValid(addressString)) { + return nullptr; + } + + const TheOpenNetwork::Address addressTon(addressString); + auto bocEncoded = addressTon.toBoc(); + return TWStringCreateWithUTF8Bytes(bocEncoded.c_str()); +} + +TWString *_Nullable TWTONAddressConverterFromBoc(TWString *_Nonnull boc) { + auto& bocEncoded = *reinterpret_cast(boc); + + try { + auto address = TheOpenNetwork::Address::fromBoc(bocEncoded); + if (!address) { + return nullptr; + } + + auto userFriendly = true; + auto bounceable = false; + auto addressStr = address->string(userFriendly, bounceable); + + return TWStringCreateWithUTF8Bytes(addressStr.c_str()); + } catch (...) { + return nullptr; + } +} diff --git a/src/rust/Wrapper.h b/src/rust/Wrapper.h index 6c730e99782..bb3feb3dddf 100644 --- a/src/rust/Wrapper.h +++ b/src/rust/Wrapper.h @@ -153,6 +153,20 @@ struct CStringWrapper { std::string str; }; +struct CUInt8Wrapper { + /// Implicit move constructor. + CUInt8Wrapper(uint8_t c_u8) { + *this = c_u8; + } + + CUInt8Wrapper& operator=(uint8_t c_u8) { + value = c_u8; + return *this; + } + + uint8_t value; +}; + struct CUInt64Wrapper { /// Implicit move constructor. CUInt64Wrapper(uint64_t c_u64) { @@ -214,6 +228,7 @@ class CResult { }; using CByteArrayResultWrapper = CResult; +using CUInt8ResultWrapper = CResult; using CUInt64ResultWrapper = CResult; } // namespace TW::Rust diff --git a/swift/Tests/Blockchains/TheOpenNetworkTests.swift b/swift/Tests/Blockchains/TheOpenNetworkTests.swift index d0265404742..2a64fc9204b 100644 --- a/swift/Tests/Blockchains/TheOpenNetworkTests.swift +++ b/swift/Tests/Blockchains/TheOpenNetworkTests.swift @@ -33,6 +33,20 @@ class TheOpenNetworkTests: XCTestCase { XCTAssertEqual(address!.description, "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q") } + func testGenerateJettonAddress() { + let mainAddress = "UQBjKqthWBE6GEcqb_epTRFrQ1niS6Z1Z1MHMwR-mnAYRoYr" + let mainAddressBoc = TONAddressConverter.toBoc(address: mainAddress) + XCTAssertEqual(mainAddressBoc, "te6ccgICAAEAAQAAACQAAABDgAxlVWwrAidDCOVN/vUpoi1oazxJdM6s6mDmYI/TTgMI0A==") + + // curl --location 'https://toncenter.com/api/v2/runGetMethod' --header 'Content-Type: application/json' --data \ + // '{"address":"EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT","method":"get_wallet_address","method":"get_wallet_address","stack":[["tvm.Slice","te6ccgICAAEAAQAAACQAAABDgAxlVWwrAidDCOVN/vUpoi1oazxJdM6s6mDmYI/TTgMI0A=="]]}' + + // Parse the `get_wallet_address` RPC response. + let jettonAddressBocEncoded = "te6cckEBAQEAJAAAQ4AFvT5rqwxcbKfITqnkwL+go4Zi9bulRHAtLt4cjjFdK7B8L+Cq" + let jettonAddress = TONAddressConverter.fromBoc(boc: jettonAddressBocEncoded) + XCTAssertEqual(jettonAddress, "UQAt6fNdWGLjZT5CdU8mBf0FHDMXrd0qI4FpdvDkcYrpXV5H") + } + func testSign() { let privateKeyData = Data(hexString: "c38f49de2fb13223a9e7d37d5d0ffbdd89a5eb7c8b0ee4d1c299f2cefe7dc4a0")! diff --git a/tests/chains/TheOpenNetwork/AddressTests.cpp b/tests/chains/TheOpenNetwork/AddressTests.cpp index 297fe113531..a46d2d53fd6 100644 --- a/tests/chains/TheOpenNetwork/AddressTests.cpp +++ b/tests/chains/TheOpenNetwork/AddressTests.cpp @@ -5,11 +5,14 @@ #include "HexCoding.h" #include "PublicKey.h" #include "PrivateKey.h" +#include "TestUtilities.h" #include "TheOpenNetwork/Address.h" #include "TheOpenNetwork/wallet/WalletV4R2.h" #include "TheOpenNetwork/WorkchainType.h" +#include "TrustWalletCore/TWTONAddressConverter.h" + #include namespace TW::TheOpenNetwork::tests { @@ -111,4 +114,70 @@ TEST(TheOpenNetworkAddress, FromPublicKeyV4R2) { ASSERT_EQ(address.string(), "EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorkdl"); } +TEST(TheOpenNetworkAddress, GetJettonNotcoinAddress) { + auto mainAddress = STRING("UQBjKqthWBE6GEcqb_epTRFrQ1niS6Z1Z1MHMwR-mnAYRoYr"); + auto addressBocEncoded = WRAPS(TWTONAddressConverterToBoc(mainAddress.get())); + assertStringsEqual(addressBocEncoded, "te6ccgICAAEAAQAAACQAAABDgAxlVWwrAidDCOVN/vUpoi1oazxJdM6s6mDmYI/TTgMI0A=="); + + // curl --location 'https://toncenter.com/api/v2/runGetMethod' --header 'Content-Type: application/json' --data \ + // '{"address":"EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT","method":"get_wallet_address","method":"get_wallet_address","stack":[["tvm.Slice","te6ccgICAAEAAQAAACQAAABDgAxlVWwrAidDCOVN/vUpoi1oazxJdM6s6mDmYI/TTgMI0A=="]]}' + + // `get_wallet_address` response: + auto jettonAddressBocEncoded = STRING("te6cckEBAQEAJAAAQ4AFvT5rqwxcbKfITqnkwL+go4Zi9bulRHAtLt4cjjFdK7B8L+Cq"); + auto jettonAddress = WRAPS(TWTONAddressConverterFromBoc(jettonAddressBocEncoded.get())); + assertStringsEqual(jettonAddress, "UQAt6fNdWGLjZT5CdU8mBf0FHDMXrd0qI4FpdvDkcYrpXV5H"); +} + +TEST(TheOpenNetworkAddress, GetJettonUSDTAddress) { + auto mainAddress = STRING("UQBjKqthWBE6GEcqb_epTRFrQ1niS6Z1Z1MHMwR-mnAYRoYr"); + auto addressBocEncoded = WRAPS(TWTONAddressConverterToBoc(mainAddress.get())); + assertStringsEqual(addressBocEncoded, "te6ccgICAAEAAQAAACQAAABDgAxlVWwrAidDCOVN/vUpoi1oazxJdM6s6mDmYI/TTgMI0A=="); + + // curl --location 'https://toncenter.com/api/v2/runGetMethod' --header 'Content-Type: application/json' --data \ + // '{"address":"EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs","method":"get_wallet_address","method":"get_wallet_address","stack":[["tvm.Slice","te6ccgICAAEAAQAAACQAAABDgAxlVWwrAidDCOVN/vUpoi1oazxJdM6s6mDmYI/TTgMI0A=="]]}' + + // `get_wallet_address` response: + auto jettonAddressBocEncoded = STRING("te6cckEBAQEAJAAAQ4Aed71FEI46jdFXghsGUIG2GIR8wpbQaLzrKNj7BtHOEHBSO5Mf"); + auto jettonAddress = WRAPS(TWTONAddressConverterFromBoc(jettonAddressBocEncoded.get())); + assertStringsEqual(jettonAddress, "UQDzveoohHHUboq8ENgyhA2wxCPmFLaDRedZRsfYNo5wg4TL"); +} + +TEST(TheOpenNetworkAddress, GetJettonStonAddress) { + auto mainAddress = STRING("EQATQPeCwtMzQ9u54nTjUNcK4n_0VRSxPOOROLf_IE0OU3XK"); + auto addressBocEncoded = WRAPS(TWTONAddressConverterToBoc(mainAddress.get())); + assertStringsEqual(addressBocEncoded, "te6ccgICAAEAAQAAACQAAABDgAJoHvBYWmZoe3c8TpxqGuFcT/6KopYnnHInFv/kCaHKcA=="); + + // curl --location 'https://toncenter.com/api/v2/runGetMethod' --header 'Content-Type: application/json' --data \ + // '{"address":"EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO","method":"get_wallet_address","method":"get_wallet_address","stack":[["tvm.Slice","te6ccgICAAEAAQAAACQAAABDgAxlVWwrAidDCOVN/vUpoi1oazxJdM6s6mDmYI/TTgMI0A=="]]}' + + // `get_wallet_address` response: + auto jettonAddressBocEncoded = STRING("te6cckEBAQEAJAAAQ4ALPu0dyA1gHd3r7J1rxlvhXSvT5y3rokMDMiCQ86TsUJDnt69H"); + auto jettonAddress = WRAPS(TWTONAddressConverterFromBoc(jettonAddressBocEncoded.get())); + assertStringsEqual(jettonAddress, "UQBZ92juQGsA7u9fZOteMt8K6V6fOW9dEhgZkQSHnSdihHPH"); +} + +TEST(TheOpenNetworkAddress, FromBocNullAddress) { + auto jettonAddressBocEncoded = STRING("te6cckEBAQEAAwAAASCUQYZV"); + auto jettonAddress = WRAPS(TWTONAddressConverterFromBoc(jettonAddressBocEncoded.get())); + assertStringsEqual(jettonAddress, "UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJKZ"); +} + +TEST(TheOpenNetworkAddress, FromBocError) { + // No type bit. + auto boc1 = STRING("te6cckEBAQEAAwAAAcCO6ba2"); + ASSERT_EQ(TWTONAddressConverterFromBoc(boc1.get()), nullptr); + + // No res1 and workchain bits. + auto boc2 = STRING("te6cckEBAQEAAwAAAaDsenDX"); + ASSERT_EQ(TWTONAddressConverterFromBoc(boc2.get()), nullptr); + + // Incomplete hash (31 bytes instead of 32). + auto boc3 = STRING("te6cckEBAQEAIwAAQYAgQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAUGJnJWk="); + ASSERT_EQ(TWTONAddressConverterFromBoc(boc3.get()), nullptr); + + // Expected 267 bits, found 268. + auto boc4 = STRING("te6cckEBAQEAJAAAQ4AgQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEgGG0Gq"); + ASSERT_EQ(TWTONAddressConverterFromBoc(boc4.get()), nullptr); +} + } // namespace TW::TheOpenNetwork::tests