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 f0ae94056e6..f37d4c7e4e9 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 @@ -66,7 +66,7 @@ class TestTheOpenNetworkAddress { fun testGenerateJettonAddress() { val mainAddress = "UQBjKqthWBE6GEcqb_epTRFrQ1niS6Z1Z1MHMwR-mnAYRoYr" val mainAddressBoc = TONAddressConverter.toBoc(mainAddress) - assertEquals(mainAddressBoc, "te6ccgICAAEAAQAAACQAAABDgAxlVWwrAidDCOVN/vUpoi1oazxJdM6s6mDmYI/TTgMI0A==") + assertEquals(mainAddressBoc, "te6cckEBAQEAJAAAQ4AMZVVsKwInQwjlTf71KaItaGs8SXTOrOpg5mCP004DCNAptHQU") // 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=="]]}' diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkSigner.kt index 4374bba398c..6fb0edc455c 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkSigner.kt @@ -28,21 +28,21 @@ class TestTheOpenNetworkSigner { .setWalletVersion(TheOpenNetwork.WalletVersion.WALLET_V4_R2) .setDest("EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q") .setAmount(10) - .setSequenceNumber(6) .setMode(TheOpenNetwork.SendMode.PAY_FEES_SEPARATELY_VALUE or TheOpenNetwork.SendMode.IGNORE_ACTION_PHASE_ERRORS_VALUE) - .setExpireAt(1671132440) .setBounceable(true) .build() val input = TheOpenNetwork.SigningInput.newBuilder() - .setTransfer(transfer) .setPrivateKey(ByteString.copyFrom(privateKey.data())) + .addMessages(transfer) + .setSequenceNumber(6) + .setExpireAt(1671132440) .build() val output = AnySigner.sign(input, CoinType.TON, SigningOutput.parser()) // tx: https://tonscan.org/tx/3Z4tHpXNLyprecgu5aTQHWtY7dpHXEoo11MAX61Xyg0= - val expectedString = "te6ccgICAAQAAQAAALAAAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGcEUPkil2aZ4s8KKparSep/OKHMC8vuXafFbW2HGp/9AcTRv0J5T4dwyW1G0JpHw+g5Ov6QI3Xo0O9RFr3KidICimpoxdjm3UYAAAABgADAAIBYmIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmIUAAAAAAAAAAAAAAAAAEAAwAA" + val expectedString = "te6cckEBBAEArQABRYgBsLf6vJOEq42xW0AoyWX0K+uBMUcXFDLFqmkDg6k1Io4MAQGcEUPkil2aZ4s8KKparSep/OKHMC8vuXafFbW2HGp/9AcTRv0J5T4dwyW1G0JpHw+g5Ov6QI3Xo0O9RFr3KidICimpoxdjm3UYAAAABgADAgFiYgAzffHi4B365BPJfIJk/F+URKU1UekJ6g4QK02ypVb22YhQAAAAAAAAAAAAAAAAAQMAAA08Nzs=" assertEquals(output.encoded, expectedString) } @@ -51,33 +51,77 @@ class TestTheOpenNetworkSigner { fun TheOpenNetworkJettonTransferSigning() { val privateKey = PrivateKey("c054900a527538c1b4325688a421c0469b171c29f23a62da216e90b0df2412ee".toHexByteArray()) - val transferData = TheOpenNetwork.Transfer.newBuilder() + val jettonTransfer = TheOpenNetwork.JettonTransfer.newBuilder() + .setJettonAmount(500 * 1000 * 1000) + .setToOwner("EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8") + .setResponseAddress("EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk") + .setForwardAmount(1) + .build() + + val transfer = TheOpenNetwork.Transfer.newBuilder() .setWalletVersion(TheOpenNetwork.WalletVersion.WALLET_V4_R2) .setDest("EQBiaD8PO1NwfbxSkwbcNT9rXDjqhiIvXWymNO-edV0H5lja") .setAmount(100 * 1000 * 1000) - .setSequenceNumber(1) .setMode(TheOpenNetwork.SendMode.PAY_FEES_SEPARATELY_VALUE or TheOpenNetwork.SendMode.IGNORE_ACTION_PHASE_ERRORS_VALUE) - .setExpireAt(1787693046) .setComment("test comment") .setBounceable(true) + .setJettonTransfer(jettonTransfer) - val jettonTransfer = TheOpenNetwork.JettonTransfer.newBuilder() - .setTransfer(transferData) - .setJettonAmount(500 * 1000 * 1000) - .setToOwner("EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8") - .setResponseAddress("EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk") - .setForwardAmount(1) + val input = TheOpenNetwork.SigningInput.newBuilder() + .setPrivateKey(ByteString.copyFrom(privateKey.data())) + .addMessages(transfer) + .setSequenceNumber(1) + .setExpireAt(1787693046) + .build() + + val output = AnySigner.sign(input, CoinType.TON, SigningOutput.parser()) + + // tx: https://testnet.tonscan.org/tx/Er_oT5R3QK7D-qVPBKUGkJAOOq6ayVls-mgEphpI9Ck= + val expectedString = "te6cckECBAEAARUAAUWIALRRBlXIE21P9b6OpAqeFhqw+IMh1Jac2CjUU/IsfEsqDAEBnGiFlaLItV573gJqBvctP5j3jVKlLuxmO+pnW0QGlXjXgzjw5YeTNwRG9upJHOl6GA3pFetKNojqGzfkxku+owUpqaMXao4H9gAAAAEAAwIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEDAMoPin6lAAAAAAAAAABB3NZQCAALgZZzC14dAz6ZxChX5pn6bIJ3WNipSJrCELO7Ex0TOQAWiiDKuQJtqf630dSBU8LDVh8QZDqS05sFGop+RY+JZUICAAAAAHRlc3QgY29tbWVudG/bd5c=" + + assertEquals(output.encoded, expectedString) + } + + @Test + fun TheOpenNetworkTransferCustomPayload() { + val privateKey = PrivateKey("5525e673087587bc0efd7ab09920ef7d3c1bf6b854a661430244ca59ab19e9d1".toHexByteArray()) + + // Doge chatbot contract payload to be deployed. + // Docs: https://docs.ton.org/develop/dapps/ton-connect/transactions#smart-contract-deployment + val dogeChatbotStateInit = "te6cckEBBAEAUwACATQBAgEU/wD0pBP0vPLICwMAEAAAAZDrkbgQAGrTMAGCCGlJILmRMODQ0wMx+kAwi0ZG9nZYcCCAGMjLBVAEzxaARfoCE8tqEssfAc8WyXP7AO4ioYU=" + // Doge chatbot's address after the contract is deployed. + val dogeChatbotDeployingAddress = "0:3042cd5480da232d5ac1d9cbe324e3c9eb58f167599f6b7c20c6e638aeed0335" + + // The comment has nothing to do with Doge chatbot. + // It's just used to attach the following ASCII comment to the transaction: + // "This transaction deploys Doge Chatbot contract" + val commentPayload = "te6cckEBAQEANAAAZAAAAABUaGlzIHRyYW5zYWN0aW9uIGRlcGxveXMgRG9nZSBDaGF0Ym90IGNvbnRyYWN0v84vSg==" + + val customPayload = TheOpenNetwork.CustomPayload.newBuilder() + .setStateInit(dogeChatbotStateInit) + .setPayload(commentPayload) .build() + val transfer = TheOpenNetwork.Transfer.newBuilder() + .setWalletVersion(TheOpenNetwork.WalletVersion.WALLET_V4_R2) + .setDest(dogeChatbotDeployingAddress) + // 0.069 TON + .setAmount(69_000_000) + .setMode(TheOpenNetwork.SendMode.PAY_FEES_SEPARATELY_VALUE or TheOpenNetwork.SendMode.IGNORE_ACTION_PHASE_ERRORS_VALUE) + .setBounceable(false) + .setCustomPayload(customPayload) + val input = TheOpenNetwork.SigningInput.newBuilder() - .setJettonTransfer(jettonTransfer) .setPrivateKey(ByteString.copyFrom(privateKey.data())) + .addMessages(transfer) + .setSequenceNumber(4) + .setExpireAt(1721939714) .build() val output = AnySigner.sign(input, CoinType.TON, SigningOutput.parser()) - // tx: https://testnet.tonscan.org/tx/Er_oT5R3QK7D-qVPBKUGkJAOOq6ayVls-mgEphpI9Ck= - val expectedString = "te6ccgICAAQAAQAAARgAAAFFiAC0UQZVyBNtT/W+jqQKnhYasPiDIdSWnNgo1FPyLHxLKgwAAQGcaIWVosi1XnveAmoG9y0/mPeNUqUu7GY76mdbRAaVeNeDOPDlh5M3BEb26kkc6XoYDekV60o2iOobN+TGS76jBSmpoxdqjgf2AAAAAQADAAIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEAAwDKD4p+pQAAAAAAAAAAQdzWUAgAC4GWcwteHQM+mcQoV+aZ+myCd1jYqUiawhCzuxMdEzkAFoogyrkCban+t9HUgVPCw1YfEGQ6ktObBRqKfkWPiWVCAgAAAAB0ZXN0IGNvbW1lbnQ=" + // Successfully broadcasted: https://tonviewer.com/transaction/f4b7ed2247b1adf54f33dd2fd99216fbd61beefb281542d0b330ccea9b8d0338 + val expectedString = "te6cckECCAEAATcAAUWIAfq4NsPLegfou/MPhtHE9YuzV3gnI/q6jm3MRJh2PtpaDAEBnPbyCSsWrOZpEjb7ZFxz5yYi+an6M6Lnq7rI7TFWdDS76LEtGBrVVrhMGziwxuy6LCVtsMBikI7RPVQ89FCIAAYpqaMXZqK3AgAAAAQAAwICaUIAGCFmqkBtEZatYOzl8ZJx5PWseLOsz7W+EGNzHFd2gZqgIObaAAAAAAAAAAAAAAAAAAPAAwQCATQFBgBkAAAAAFRoaXMgdHJhbnNhY3Rpb24gZGVwbG95cyBEb2dlIENoYXRib3QgY29udHJhY3QBFP8A9KQT9LzyyAsHABAAAAGQ65G4EABq0zABgghpSSC5kTDg0NMDMfpAMItGRvZ2WHAggBjIywVQBM8WgEX6AhPLahLLHwHPFslz+wAa2r/S" assertEquals(output.encoded, expectedString) } diff --git a/codegen-v2/src/codegen/rust/templates/integration_tests/address_tests.rs b/codegen-v2/src/codegen/rust/templates/integration_tests/address_tests.rs index 55c23efc6ee..1d417a77ed4 100644 --- a/codegen-v2/src/codegen/rust/templates/integration_tests/address_tests.rs +++ b/codegen-v2/src/codegen/rust/templates/integration_tests/address_tests.rs @@ -3,10 +3,16 @@ // Copyright © 2017 Trust Wallet. use tw_any_coin::test_utils::address_utils::{ - test_address_get_data, test_address_invalid, test_address_normalization, test_address_valid, + test_address_derive, test_address_get_data, test_address_invalid, test_address_normalization, + test_address_valid, }; use tw_coin_registry::coin_type::CoinType; +#[test] +fn test_{COIN_ID}_address_derive() { + test_address_derive(CoinType::{COIN_TYPE}, "PRIVATE_KEY", "EXPECTED ADDRESS"); +} + #[test] fn test_{COIN_ID}_address_normalization() { test_address_normalization(CoinType::{COIN_TYPE}, "DENORMALIZED", "EXPECTED"); diff --git a/include/TrustWalletCore/TWTONAddressConverter.h b/include/TrustWalletCore/TWTONAddressConverter.h index 142c98a4cb7..39bb4dfed7e 100644 --- a/include/TrustWalletCore/TWTONAddressConverter.h +++ b/include/TrustWalletCore/TWTONAddressConverter.h @@ -36,6 +36,7 @@ TWString *_Nullable TWTONAddressConverterFromBoc(TWString *_Nonnull boc); /// \param address raw or user-friendly address to be converted. /// \param bounceable whether the result address should be bounceable. /// \param testnet whether the result address should be testnet. +/// \return user-friendly address str. TW_EXPORT_STATIC_METHOD TWString *_Nullable TWTONAddressConverterToUserFriendly(TWString *_Nonnull address, bool bounceable, bool testnet); diff --git a/registry.json b/registry.json index 216fa4115b8..ab83bc849f1 100644 --- a/registry.json +++ b/registry.json @@ -4518,7 +4518,7 @@ "coinId": 607, "symbol": "TON", "decimals": 9, - "blockchain": "The Open Network", + "blockchain": "TheOpenNetwork", "derivation": [ { "path": "m/44'/607'/0'" diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 0eb28d98085..d73705dbb8a 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -220,6 +220,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "bitstream-io" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcde5f311c85b8ca30c2e4198d4326bc342c76541590106f5fa4a50946ea499" + [[package]] name = "bitvec" version = "0.20.4" @@ -405,6 +411,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crunchy" version = "0.2.2" @@ -898,11 +919,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -918,11 +938,10 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] @@ -951,9 +970,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -1639,6 +1658,7 @@ dependencies = [ "tw_misc", "tw_number", "tw_proto", + "tw_ton_sdk", "tw_utxo", ] @@ -1768,6 +1788,7 @@ dependencies = [ "tw_solana", "tw_sui", "tw_thorchain", + "tw_ton", ] [[package]] @@ -2048,6 +2069,40 @@ dependencies = [ "tw_proto", ] +[[package]] +name = "tw_ton" +version = "0.1.0" +dependencies = [ + "lazy_static", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_number", + "tw_proto", + "tw_ton_sdk", +] + +[[package]] +name = "tw_ton_sdk" +version = "0.1.0" +dependencies = [ + "bitreader", + "bitstream-io", + "crc", + "lazy_static", + "num-bigint", + "serde", + "serde_json", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_number", +] + [[package]] name = "tw_utxo" version = "0.1.0" @@ -2146,6 +2201,7 @@ dependencies = [ "tw_number", "tw_proto", "tw_solana", + "tw_ton", "uuid", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 2b0bfa58765..6aed21641b8 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -4,14 +4,16 @@ members = [ "chains/tw_binance", "chains/tw_cosmos", "chains/tw_ethereum", - "chains/tw_internet_computer", "chains/tw_greenfield", + "chains/tw_internet_computer", "chains/tw_native_evmos", "chains/tw_native_injective", "chains/tw_ronin", "chains/tw_solana", "chains/tw_sui", "chains/tw_thorchain", + "chains/tw_ton", + "frameworks/tw_ton_sdk", "frameworks/tw_utxo", "tw_any_coin", "tw_base58_address", diff --git a/rust/chains/tw_solana/src/compiler.rs b/rust/chains/tw_solana/src/compiler.rs index f3341bbd907..818f04a8ce9 100644 --- a/rust/chains/tw_solana/src/compiler.rs +++ b/rust/chains/tw_solana/src/compiler.rs @@ -13,7 +13,8 @@ use tw_coin_entry::coin_context::CoinContext; use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; use tw_coin_entry::error::prelude::*; use tw_coin_entry::signing_output_error; -use tw_encoding::{base58, base64}; +use tw_encoding::base58; +use tw_encoding::base64::{self, STANDARD}; use tw_keypair::ed25519; use tw_keypair::traits::VerifyingKeyTrait; use tw_proto::Solana::Proto; @@ -69,7 +70,7 @@ impl SolanaCompiler { ) -> SigningResult> { let encode = move |data| match input.tx_encoding { Proto::Encoding::Base58 => base58::encode(data, SOLANA_ALPHABET), - Proto::Encoding::Base64 => base64::encode(data, false), + Proto::Encoding::Base64 => base64::encode(data, STANDARD), }; if signatures.len() != public_keys.len() { diff --git a/rust/chains/tw_solana/src/modules/utils.rs b/rust/chains/tw_solana/src/modules/utils.rs index cc9a1ebf4ec..85b662784b8 100644 --- a/rust/chains/tw_solana/src/modules/utils.rs +++ b/rust/chains/tw_solana/src/modules/utils.rs @@ -10,7 +10,8 @@ use crate::SOLANA_ALPHABET; use std::borrow::Cow; use tw_coin_entry::error::prelude::*; use tw_coin_entry::signing_output_error; -use tw_encoding::{base58, base64}; +use tw_encoding::base58; +use tw_encoding::base64::{self, STANDARD}; use tw_hash::H256; use tw_keypair::{ed25519, KeyPairResult}; use tw_memory::Data; @@ -33,8 +34,7 @@ impl SolanaTransaction { recent_blockhash: &str, private_keys: &[Data], ) -> SigningResult> { - let is_url = false; - let tx_bytes = base64::decode(encoded_tx, is_url)?; + let tx_bytes = base64::decode(encoded_tx, STANDARD)?; let tx_to_sign: VersionedTransaction = bincode::deserialize(&tx_bytes).map_err(|_| SigningErrorType::Error_input_parse)?; @@ -63,10 +63,10 @@ impl SolanaTransaction { TxSigner::sign_versioned(msg_to_sign, &private_keys, &external_signatures)? }; - let unsigned_encoded = base64::encode(&unsigned_encoded, is_url); + let unsigned_encoded = base64::encode(&unsigned_encoded, STANDARD); let signed_encoded = bincode::serialize(&signed_tx).tw_err(|_| SigningErrorType::Error_internal)?; - let signed_encoded = base64::encode(&signed_encoded, is_url); + let signed_encoded = base64::encode(&signed_encoded, STANDARD); Ok(Proto::SigningOutput { encoded: Cow::from(signed_encoded), diff --git a/rust/chains/tw_solana/src/signer.rs b/rust/chains/tw_solana/src/signer.rs index 4d3db06c69a..caa552aa4da 100644 --- a/rust/chains/tw_solana/src/signer.rs +++ b/rust/chains/tw_solana/src/signer.rs @@ -10,7 +10,8 @@ use std::borrow::Cow; use tw_coin_entry::coin_context::CoinContext; use tw_coin_entry::error::prelude::*; use tw_coin_entry::signing_output_error; -use tw_encoding::{base58, base64}; +use tw_encoding::base58; +use tw_encoding::base64::{self, STANDARD}; use tw_proto::Solana::Proto; pub struct SolanaSigner; @@ -30,7 +31,7 @@ impl SolanaSigner { ) -> SigningResult> { let encode = move |data| match input.tx_encoding { Proto::Encoding::Base58 => base58::encode(data, SOLANA_ALPHABET), - Proto::Encoding::Base64 => base64::encode(data, false), + Proto::Encoding::Base64 => base64::encode(data, STANDARD), }; let builder = MessageBuilder::new(input); diff --git a/rust/chains/tw_solana/src/transaction/mod.rs b/rust/chains/tw_solana/src/transaction/mod.rs index b9875f15c74..a37cdc75963 100644 --- a/rust/chains/tw_solana/src/transaction/mod.rs +++ b/rust/chains/tw_solana/src/transaction/mod.rs @@ -76,8 +76,9 @@ mod tests { use crate::transaction::v0::MessageAddressTableLookup; use crate::transaction::versioned::{VersionedMessage, VersionedTransaction}; use crate::SOLANA_ALPHABET; + use tw_encoding::base58; + use tw_encoding::base64::{self, STANDARD}; use tw_encoding::hex::ToHex; - use tw_encoding::{base58, base64}; use tw_hash::H256; use tw_memory::Data; @@ -92,7 +93,7 @@ mod tests { #[test] fn test_rango_transaction_ser_de() { - let serialized = base64::decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHEIoR5xuWyrvjIW4xU7CWlPOfyFAiy8B295hGo6tNjBmRCgUkQaFYTleMcAX2p74eBXQZd1dwDyQZAPJfSv2KGc5kcFLJj5qd2BVMaSNGVPfVBm74GbLwUq5/U1Ccdqc2gokZQxRDpMq7aeToP3nRaWIP4RXMxN+LJetccXMPq/QumgOqt7kkqk07cyPCKgYoQ4fQtOqqZn5sEqjWHYj3CDS5ha48uggePWu090s1ff4yoCjAvULeZ+cqYFn+Adk5Teyfw71W3u/F6VTnLQEPW96gJr5Kcm3bGi08n224JyF++PTko52VL0CIM2xtl0WkvNslD6Wawxr7yd9HYllN4Lz8lFwXilWGgyJdOq1qqBuZbE49glHeCO/sJHNnIHC0BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+Fm0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6OL4d+g9rsaIj0Orta57MRu3jDSWCJf85ae4LBbiD/GXvOojZjsHekJrpRUuPggLJr943hDVD5UareeEucjCvaoHCgAFAsBcFQAKAAkDBBcBAAAAAAANBgAGACMJDAEBCQIABgwCAAAAAMqaOwAAAAAMAQYBEQs1DA8ABgEFAiMhCwsOCx0MDxoBGQcYBAgDJBscDB4PBwUQEhEfFR8UFwcFISITHw8MDCAfFgstwSCbM0HWnIEAAwAAABEBZAABCh0BAyZHAQMAypo7AAAAAJaWFAYAAAAAMgAADAMGAAABCQPZoILFk7gfE2y5bt3AC+g/4OwNzdiHKBhIbdeYvYFEjQPKyMkExMUkx0R25UNa/g5KsG0vfUwdUJ8e8HecK/Jkd3qm9XefBOB0BaD1+J+dBJz09vfyGuRYZH09HfdE/kL8v6Ql+H03+tO+9lMmmVg8O1c6gAN6eX0Cbn4=", false).unwrap(); + let serialized = base64::decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHEIoR5xuWyrvjIW4xU7CWlPOfyFAiy8B295hGo6tNjBmRCgUkQaFYTleMcAX2p74eBXQZd1dwDyQZAPJfSv2KGc5kcFLJj5qd2BVMaSNGVPfVBm74GbLwUq5/U1Ccdqc2gokZQxRDpMq7aeToP3nRaWIP4RXMxN+LJetccXMPq/QumgOqt7kkqk07cyPCKgYoQ4fQtOqqZn5sEqjWHYj3CDS5ha48uggePWu090s1ff4yoCjAvULeZ+cqYFn+Adk5Teyfw71W3u/F6VTnLQEPW96gJr5Kcm3bGi08n224JyF++PTko52VL0CIM2xtl0WkvNslD6Wawxr7yd9HYllN4Lz8lFwXilWGgyJdOq1qqBuZbE49glHeCO/sJHNnIHC0BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+Fm0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6OL4d+g9rsaIj0Orta57MRu3jDSWCJf85ae4LBbiD/GXvOojZjsHekJrpRUuPggLJr943hDVD5UareeEucjCvaoHCgAFAsBcFQAKAAkDBBcBAAAAAAANBgAGACMJDAEBCQIABgwCAAAAAMqaOwAAAAAMAQYBEQs1DA8ABgEFAiMhCwsOCx0MDxoBGQcYBAgDJBscDB4PBwUQEhEfFR8UFwcFISITHw8MDCAfFgstwSCbM0HWnIEAAwAAABEBZAABCh0BAyZHAQMAypo7AAAAAJaWFAYAAAAAMgAADAMGAAABCQPZoILFk7gfE2y5bt3AC+g/4OwNzdiHKBhIbdeYvYFEjQPKyMkExMUkx0R25UNa/g5KsG0vfUwdUJ8e8HecK/Jkd3qm9XefBOB0BaD1+J+dBJz09vfyGuRYZH09HfdE/kL8v6Ql+H03+tO+9lMmmVg8O1c6gAN6eX0Cbn4=", STANDARD).unwrap(); let actual: VersionedTransaction = bincode::deserialize(&serialized).unwrap(); let expected = VersionedTransaction { diff --git a/rust/chains/tw_sui/src/compiler.rs b/rust/chains/tw_sui/src/compiler.rs index 90b95ec7a6b..1da7236a8f3 100644 --- a/rust/chains/tw_sui/src/compiler.rs +++ b/rust/chains/tw_sui/src/compiler.rs @@ -11,7 +11,7 @@ use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; use tw_coin_entry::common::compile_input::SingleSignaturePubkey; use tw_coin_entry::error::prelude::*; use tw_coin_entry::signing_output_error; -use tw_encoding::base64; +use tw_encoding::base64::{self, STANDARD}; use tw_keypair::ed25519; use tw_proto::Sui::Proto; use tw_proto::TxCompiler::Proto as CompilerProto; @@ -88,8 +88,7 @@ impl SuiCompiler { let signature_info = SuiSignatureInfo::ed25519(&signature, &public_key); - let is_url = false; - let unsigned_tx = base64::encode(&unsigned_tx_data, is_url); + let unsigned_tx = base64::encode(&unsigned_tx_data, STANDARD); Ok(Proto::SigningOutput { unsigned_tx: Cow::from(unsigned_tx), signature: Cow::from(signature_info.to_base64()), diff --git a/rust/chains/tw_sui/src/modules/tx_builder.rs b/rust/chains/tw_sui/src/modules/tx_builder.rs index 44ce73e43d6..301f9f3cde4 100644 --- a/rust/chains/tw_sui/src/modules/tx_builder.rs +++ b/rust/chains/tw_sui/src/modules/tx_builder.rs @@ -9,7 +9,7 @@ use crate::transaction::transaction_data::TransactionData; use std::borrow::Cow; use std::str::FromStr; use tw_coin_entry::error::prelude::*; -use tw_encoding::base64; +use tw_encoding::base64::{self, STANDARD}; use tw_keypair::ed25519; use tw_keypair::traits::KeyPairTrait; use tw_memory::Data; @@ -59,8 +59,7 @@ impl<'a> TWTransactionBuilder<'a> { } fn sign_direct_from_proto(&self, sign_direct: &Proto::SignDirect<'_>) -> SigningResult { - let url = false; - base64::decode(&sign_direct.unsigned_tx_msg, url) + base64::decode(&sign_direct.unsigned_tx_msg, STANDARD) .tw_err(|_| SigningErrorType::Error_input_parse) .context("Error parsing Raw Unsigned TX message as base64") } diff --git a/rust/chains/tw_sui/src/signature.rs b/rust/chains/tw_sui/src/signature.rs index 4a35bf808d0..e03aacad05f 100644 --- a/rust/chains/tw_sui/src/signature.rs +++ b/rust/chains/tw_sui/src/signature.rs @@ -2,7 +2,7 @@ // // Copyright © 2017 Trust Wallet. -use tw_encoding::base64; +use tw_encoding::base64::{self, STANDARD}; use tw_hash::{H256, H512}; use tw_keypair::ed25519; use tw_memory::Data; @@ -40,7 +40,6 @@ impl SuiSignatureInfo { } pub fn to_base64(&self) -> String { - let is_url = false; - base64::encode(&self.to_vec(), is_url) + base64::encode(&self.to_vec(), STANDARD) } } diff --git a/rust/chains/tw_sui/src/signer.rs b/rust/chains/tw_sui/src/signer.rs index 46546e6e356..4cf4df45514 100644 --- a/rust/chains/tw_sui/src/signer.rs +++ b/rust/chains/tw_sui/src/signer.rs @@ -8,7 +8,7 @@ use std::borrow::Cow; use tw_coin_entry::coin_context::CoinContext; use tw_coin_entry::error::prelude::*; use tw_coin_entry::signing_output_error; -use tw_encoding::base64; +use tw_encoding::base64::{self, STANDARD}; use tw_proto::Sui::Proto; pub struct SuiSigner; @@ -35,8 +35,7 @@ impl SuiSigner { TWTransaction::SignDirect(tx_data) => TxSigner::sign_direct(tx_data, &signer_key)?, }; - let is_url = false; - let unsigned_tx = base64::encode(&preimage.unsigned_tx_data, is_url); + let unsigned_tx = base64::encode(&preimage.unsigned_tx_data, STANDARD); Ok(Proto::SigningOutput { unsigned_tx: Cow::from(unsigned_tx), signature: Cow::from(signature.to_base64()), diff --git a/rust/chains/tw_sui/tests/decode_transaction.rs b/rust/chains/tw_sui/tests/decode_transaction.rs index 746516874b1..4b2c8c4a370 100644 --- a/rust/chains/tw_sui/tests/decode_transaction.rs +++ b/rust/chains/tw_sui/tests/decode_transaction.rs @@ -3,8 +3,9 @@ // Copyright © 2017 Trust Wallet. use std::str::FromStr; +use tw_encoding::base64::{self, STANDARD}; +use tw_encoding::bcs; use tw_encoding::hex::DecodeHex; -use tw_encoding::{base64, bcs}; use tw_sui::address::SuiAddress; use tw_sui::transaction::command::{Argument, Command}; use tw_sui::transaction::programmable_transaction::ProgrammableTransaction; @@ -59,8 +60,7 @@ fn test_decode_transfer_tx() { }; let data = TransactionData::V1(v1); - let is_url = false; - let bytes = base64::encode(&bcs::encode(&data).unwrap(), is_url); + let bytes = base64::encode(&bcs::encode(&data).unwrap(), STANDARD); // Successfully broadcasted https://explorer.sui.io/txblock/HkPo6rYPyDY53x1MBszvSZVZyixVN7CHvCJGX381czAh?network=devnet assert_eq!(bytes, "AAACAAgQJwAAAAAAAAAgJZ/4B0q0Jcu0ifI24Y4I8D8aeFa998eih3vWT3OLUBUCAgABAQAAAQEDAAAAAAEBANV1rX8Y6UhGKlz2mPVk7zlKdSpx/sYkk6+KBVwBLA1QAQbywsjB2JZN8QGdZhbpcFcZvrq9kx2idVy5SM635olk7AIAAAAAAAAgYEVuxmf1zRBGdoDr+VDtMpIFF12s2Ua7I2ru1XyGF8/Vda1/GOlIRipc9pj1ZO85SnUqcf7GJJOvigVcASwNUAEAAAAAAAAA0AcAAAAAAAAA"); } diff --git a/rust/chains/tw_ton/Cargo.toml b/rust/chains/tw_ton/Cargo.toml new file mode 100644 index 00000000000..10bfc3d0297 --- /dev/null +++ b/rust/chains/tw_ton/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tw_ton" +version = "0.1.0" +edition = "2021" + +[dependencies] +lazy_static = "1.4.0" +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_number = { path = "../../tw_number" } +tw_proto = { path = "../../tw_proto" } +tw_ton_sdk = { path = "../../frameworks/tw_ton_sdk" } diff --git a/rust/chains/tw_ton/fuzz/.gitignore b/rust/chains/tw_ton/fuzz/.gitignore new file mode 100644 index 00000000000..5c404b9583f --- /dev/null +++ b/rust/chains/tw_ton/fuzz/.gitignore @@ -0,0 +1,5 @@ +target +corpus +artifacts +coverage +Cargo.lock diff --git a/rust/chains/tw_ton/fuzz/Cargo.toml b/rust/chains/tw_ton/fuzz/Cargo.toml new file mode 100644 index 00000000000..671fc399a02 --- /dev/null +++ b/rust/chains/tw_ton/fuzz/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "tw_ton-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +tw_any_coin = { path = "../../../tw_any_coin", features = ["test-utils"] } +tw_coin_registry = { path = "../../../tw_coin_registry" } +tw_proto = { path = "../../../tw_proto", features = ["fuzz"] } + +[dependencies.tw_ton] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "sign" +path = "fuzz_targets/sign.rs" +test = false +doc = false diff --git a/rust/chains/tw_ton/fuzz/fuzz_targets/sign.rs b/rust/chains/tw_ton/fuzz/fuzz_targets/sign.rs new file mode 100644 index 00000000000..4c2a190be6a --- /dev/null +++ b/rust/chains/tw_ton/fuzz/fuzz_targets/sign.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use tw_any_coin::test_utils::sign_utils::AnySignerHelper; +use tw_coin_registry::coin_type::CoinType; +use tw_proto::TheOpenNetwork::Proto; + +fuzz_target!(|input: Proto::SigningInput<'_>| { + let mut signer = AnySignerHelper::::default(); + let _ = signer.sign(CoinType::TON, input); +}); diff --git a/rust/chains/tw_ton/resources/wallet/wallet_v4r2.code b/rust/chains/tw_ton/resources/wallet/wallet_v4r2.code new file mode 100644 index 00000000000..e1d04cde08a --- /dev/null +++ b/rust/chains/tw_ton/resources/wallet/wallet_v4r2.code @@ -0,0 +1 @@ +te6cckECFAEAAtQAART/APSkE/S88sgLAQIBIAIDAgFIBAUE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8QERITAubQAdDTAyFxsJJfBOAi10nBIJJfBOAC0x8hghBwbHVnvSKCEGRzdHK9sJJfBeAD+kAwIPpEAcjKB8v/ydDtRNCBAUDXIfQEMFyBAQj0Cm+hMbOSXwfgBdM/yCWCEHBsdWe6kjgw4w0DghBkc3RyupJfBuMNBgcCASAICQB4AfoA9AQw+CdvIjBQCqEhvvLgUIIQcGx1Z4MesXCAGFAEywUmzxZY+gIZ9ADLaRfLH1Jgyz8gyYBA+wAGAIpQBIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UAXKwjiOCEGRzdHKDHrFwgBhQBcsFUAPPFiP6AhPLassfyz/JgED7AJJfA+ICASAKCwBZvSQrb2omhAgKBrkPoCGEcNQICEekk30pkQzmkD6f+YN4EoAbeBAUiYcVnzGEAgFYDA0AEbjJftRNDXCx+AA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIA4PABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AAG7SB/oA1NQi+QAFyMoHFcv/ydB3dIAYyMsFywIizxZQBfoCFMtrEszMyXP7AMhAFIEBCPRR8qcCAHCBAQjXGPoA0z/IVCBHgQEI9FHyp4IQbm90ZXB0gBjIywXLAlAGzxZQBPoCFMtqEssfyz/Jc/sAAgBsgQEI1xj6ANM/MFIkgQEI9Fnyp4IQZHN0cnB0gBjIywXLAlAFzxZQA/oCE8tqyx8Syz/Jc/sAAAr0AMntVGliJeU= \ No newline at end of file diff --git a/rust/chains/tw_ton/src/address.rs b/rust/chains/tw_ton/src/address.rs new file mode 100644 index 00000000000..3175a4cab0b --- /dev/null +++ b/rust/chains/tw_ton/src/address.rs @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::resources::{BASE_WORKCHAIN, MASTER_WORKCHAIN}; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_hash::H256; +use tw_memory::Data; +use tw_ton_sdk::address::address_data::AddressData; +use tw_ton_sdk::address::raw_address::RawAddress; +use tw_ton_sdk::address::user_friendly_address::UserFriendlyAddress; + +pub const DEFAULT_BOUNCEABLE: bool = false; +pub const DEFAULT_TESTNET: bool = false; + +/// User-friendly, base64 URL-safe **by default** encoded TON address. +/// Please note it also supports raw (hex) and user-friendly base64 standard representations as well. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TonAddress(UserFriendlyAddress); + +impl TonAddress { + pub const NULL: TonAddress = TonAddress::null(); + + pub const fn null() -> TonAddress { + TonAddress(UserFriendlyAddress::with_flags( + AddressData::null(), + DEFAULT_BOUNCEABLE, + DEFAULT_TESTNET, + )) + } + + pub fn new(workchain: i32, hash_part: H256) -> Self { + let data = AddressData::new(workchain, hash_part); + TonAddress(UserFriendlyAddress::with_flags( + data, + DEFAULT_BOUNCEABLE, + DEFAULT_TESTNET, + )) + } + + pub fn from_hex_str(s: &str) -> AddressResult { + let raw_address = RawAddress::from_str(s)?; + let user_friendly_address = UserFriendlyAddress::with_flags( + raw_address.into_data(), + DEFAULT_BOUNCEABLE, + DEFAULT_TESTNET, + ); + Ok(TonAddress(user_friendly_address)) + } + + #[inline] + pub fn from_base64_url(s: &str) -> AddressResult { + UserFriendlyAddress::from_base64_url(s).map(TonAddress) + } + + #[inline] + pub fn from_base64_std(s: &str) -> AddressResult { + UserFriendlyAddress::from_base64_std(s).map(TonAddress) + } + + #[inline] + pub fn with_address_data(data: AddressData) -> TonAddress { + TonAddress(UserFriendlyAddress::with_flags( + data, + DEFAULT_BOUNCEABLE, + DEFAULT_TESTNET, + )) + } + + /// Normalizes the TON address according to the best wallet practice: + /// https://docs.ton.org/learn/overviews/addresses#bounceable-vs-non-bounceable-addresses + /// + /// Returns error if the address workchain is unexpected. + #[inline] + pub fn normalize(self) -> AddressResult { + let workchain = self.0.as_ref().workchain; + if workchain != MASTER_WORKCHAIN && workchain != BASE_WORKCHAIN { + return Err(AddressError::UnexpectedAddressPrefix); + } + + Ok(self + .set_bounceable(DEFAULT_BOUNCEABLE) + .set_testnet(DEFAULT_TESTNET)) + } + + #[inline] + pub fn set_bounceable(self, bounceable: bool) -> Self { + TonAddress(self.0.set_bounceable(bounceable)) + } + + #[inline] + pub fn set_testnet(self, testnet: bool) -> Self { + TonAddress(self.0.set_testnet(testnet)) + } + + #[inline] + pub fn bounceable(&self) -> bool { + self.0.bounceable() + } +} + +impl AsRef for TonAddress { + fn as_ref(&self) -> &AddressData { + self.0.as_ref() + } +} + +impl CoinAddress for TonAddress { + #[inline] + fn data(&self) -> Data { + self.0.as_ref().hash_part.to_vec() + } +} + +impl FromStr for TonAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + if s.len() != 48 { + return TonAddress::from_hex_str(s); + } + + // Some form of base64 address, check which one + if s.contains('-') || s.contains('_') { + TonAddress::from_base64_url(s) + } else { + TonAddress::from_base64_std(s) + } + } +} + +impl fmt::Display for TonAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.to_base64_url()) + } +} diff --git a/rust/chains/tw_ton/src/compiler.rs b/rust/chains/tw_ton/src/compiler.rs new file mode 100644 index 00000000000..060e6879acd --- /dev/null +++ b/rust/chains/tw_ton/src/compiler.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_proto::TheOpenNetwork::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct TheOpenNetworkCompiler; + +impl TheOpenNetworkCompiler { + #[inline] + pub fn preimage_hashes( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> CompilerProto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e)) + } + + fn preimage_hashes_impl( + _coin: &dyn CoinContext, + _input: Proto::SigningInput<'_>, + ) -> SigningResult> { + SigningError::err(SigningErrorType::Error_not_supported) + .context("Transaction pre-image hashing is not supported for TON blockchain yet") + } + + #[inline] + pub fn compile( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + Self::compile_impl(coin, input, signatures, public_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn compile_impl( + _coin: &dyn CoinContext, + _input: Proto::SigningInput<'_>, + _signatures: Vec, + _public_keys: Vec, + ) -> SigningResult> { + SigningError::err(SigningErrorType::Error_not_supported) + .context("Transaction compiling is not supported for TON blockchain yet") + } +} diff --git a/rust/chains/tw_ton/src/entry.rs b/rust/chains/tw_ton/src/entry.rs new file mode 100644 index 00000000000..a0bbf9cd165 --- /dev/null +++ b/rust/chains/tw_ton/src/entry.rs @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use crate::compiler::TheOpenNetworkCompiler; +use crate::signer::TheOpenNetworkSigner; +use crate::wallet::TonWallet; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_coin_entry::prefix::NoPrefix; +use tw_keypair::tw::PublicKey; +use tw_proto::TheOpenNetwork::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct TheOpenNetworkEntry; + +impl CoinEntry for TheOpenNetworkEntry { + type AddressPrefix = NoPrefix; + type Address = TonAddress; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + // TODO consider checking whether the transaction is on testnet. + TonAddress::from_str(address).and_then(TonAddress::normalize) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + TonAddress::from_str(address).and_then(TonAddress::normalize) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + let ed25519_pubkey = public_key + .to_ed25519() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + TonWallet::std_with_public_key(ed25519_pubkey.clone()) + .map(|wallet| wallet.address().clone()) + .map_err(|_| AddressError::Internal) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + TheOpenNetworkSigner::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + TheOpenNetworkCompiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + TheOpenNetworkCompiler::compile(coin, input, signatures, public_keys) + } +} diff --git a/rust/chains/tw_ton/src/lib.rs b/rust/chains/tw_ton/src/lib.rs new file mode 100644 index 00000000000..7e35d3bc165 --- /dev/null +++ b/rust/chains/tw_ton/src/lib.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod compiler; +pub mod entry; +pub mod message; +pub mod modules; +pub mod resources; +pub mod signer; +pub mod signing_request; +pub mod transaction; +pub mod wallet; diff --git a/rust/chains/tw_ton/src/message/external_message.rs b/rust/chains/tw_ton/src/message/external_message.rs new file mode 100644 index 00000000000..874c45b8469 --- /dev/null +++ b/rust/chains/tw_ton/src/message/external_message.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::message::internal_message::InternalMessage; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::CellResult; + +pub struct ExternalMessage { + pub wallet_id: i32, + pub expire_at: u32, + pub seqno: u32, + /// Whether the wallet version supports OP codes, + /// eg https://github.com/ton-blockchain/wallet-contract/blob/4111fd9e3313ec17d99ca9b5b1656445b5b49d8f/func/wallet-v4-code.fc#L94 + pub has_op: bool, + pub internal_messages: Vec, +} + +impl ExternalMessage { + pub fn build(&self) -> CellResult { + let mut builder = CellBuilder::new(); + builder + .store_i32(32, self.wallet_id)? + .store_u32(32, self.expire_at)? + .store_u32(32, self.seqno)?; + if self.has_op { + builder.store_u8(8, 0)?; + } + for internal_message in self.internal_messages.iter() { + builder.store_u8(8, internal_message.mode)?; + builder.store_reference(&internal_message.message)?; + } + builder.build() + } +} diff --git a/rust/chains/tw_ton/src/message/internal_message/mod.rs b/rust/chains/tw_ton/src/message/internal_message/mod.rs new file mode 100644 index 00000000000..87516a8f068 --- /dev/null +++ b/rust/chains/tw_ton/src/message/internal_message/mod.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_ton_sdk::cell::{Cell, CellArc}; + +pub mod transfer; + +pub struct InternalMessage { + /// https://docs.ton.org/develop/smart-contracts/messages#message-modes + pub mode: u8, + pub message: CellArc, +} + +impl InternalMessage { + pub fn new(mode: u8, message: Cell) -> InternalMessage { + InternalMessage { + mode, + message: message.into_arc(), + } + } +} diff --git a/rust/chains/tw_ton/src/message/internal_message/transfer.rs b/rust/chains/tw_ton/src/message/internal_message/transfer.rs new file mode 100644 index 00000000000..3da99cc0f09 --- /dev/null +++ b/rust/chains/tw_ton/src/message/internal_message/transfer.rs @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use tw_number::U256; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::{Cell, CellArc}; +use tw_ton_sdk::error::CellResult; + +const BIT_0: bool = false; +const IHR_DISABLED: bool = true; +const BOUNCED: bool = false; +const CURRENCY_COLLECTIONS: bool = false; +const CREATED_LT: u64 = 0; +const CREATED_AT: u32 = 0; +const IHR_FEES: U256 = U256::ZERO; +const FWD_FEES: U256 = U256::ZERO; + +/// Standard Internal message - transfer TON. +pub struct TransferInternalMessage { + pub dest: TonAddress, + pub value: U256, + /// Deploy a smart contract different from the sender's wallet contract. + /// For example, to deploy a chatbot Doge: https://github.com/LaDoger/doge.fc + /// + /// Note consider using [`ExternalMessage::state_init`] to deploy the sender's wallet contract. + pub state_init: Option, + pub data: Option, +} + +impl TransferInternalMessage { + pub fn new(dest: TonAddress, value: U256) -> Self { + TransferInternalMessage { + dest, + value, + state_init: None, + data: None, + } + } + + pub fn with_data(&mut self, data: CellArc) -> &mut Self { + self.data = Some(data); + self + } + + pub fn with_state_init(&mut self, state_init: CellArc) -> &mut Self { + self.state_init = Some(state_init); + self + } + + pub fn build(&self) -> CellResult { + let mut builder = CellBuilder::new(); + builder.store_bit(BIT_0)?; // bit0 + builder.store_bit(IHR_DISABLED)?; // ihr_disabled + builder.store_bit(self.dest.bounceable())?; // bounce + builder.store_bit(BOUNCED)?; // bounced + builder.store_address(&TonAddress::NULL)?; // src_addr + builder.store_address(&self.dest)?; // dest_addr + builder.store_coins(&self.value)?; // value + builder.store_bit(CURRENCY_COLLECTIONS)?; // currency_collections + builder.store_coins(&IHR_FEES)?; // ihr_fees + builder.store_coins(&FWD_FEES)?; // fwd_fees + builder.store_u64(64, CREATED_LT)?; // created_lt + builder.store_u32(32, CREATED_AT)?; // created_at + + // (Maybe (Either StateInit ^StateInit)) + builder.store_bit(self.state_init.is_some())?; // state_init? + if let Some(state_init) = self.state_init.as_ref() { + builder.store_bit(true)?; // store state_init as a reference, not inline + builder.store_reference(state_init)?; + } + + // (Either X ^X) = Message X + builder.store_bit(self.data.is_some())?; // data? + if let Some(data) = self.data.as_ref() { + builder.store_reference(data)?; + } + + builder.build() + } +} diff --git a/rust/chains/tw_ton/src/message/mod.rs b/rust/chains/tw_ton/src/message/mod.rs new file mode 100644 index 00000000000..1f8c298d45c --- /dev/null +++ b/rust/chains/tw_ton/src/message/mod.rs @@ -0,0 +1,4 @@ +pub mod external_message; +pub mod internal_message; +pub mod payload; +pub mod signed_message; diff --git a/rust/chains/tw_ton/src/message/payload/comment.rs b/rust/chains/tw_ton/src/message/payload/comment.rs new file mode 100644 index 00000000000..64176ae1d15 --- /dev/null +++ b/rust/chains/tw_ton/src/message/payload/comment.rs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::CellResult; + +/// Transaction payload the consists of an arbitrary comment only. +pub struct CommentPayload { + comment: String, +} + +impl CommentPayload { + pub fn new(comment: String) -> Self { + CommentPayload { comment } + } + + pub fn build(&self) -> CellResult { + let mut builder = CellBuilder::new(); + builder.store_u32(32, 0)?.store_string(&self.comment)?; + builder.build() + } +} diff --git a/rust/chains/tw_ton/src/message/payload/empty.rs b/rust/chains/tw_ton/src/message/payload/empty.rs new file mode 100644 index 00000000000..724cad0592e --- /dev/null +++ b/rust/chains/tw_ton/src/message/payload/empty.rs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::CellResult; + +/// Empty transaction payload. +pub struct EmptyPayload; + +impl EmptyPayload { + pub fn build(&self) -> CellResult { + CellBuilder::new().build() + } +} diff --git a/rust/chains/tw_ton/src/message/payload/jetton_transfer.rs b/rust/chains/tw_ton/src/message/payload/jetton_transfer.rs new file mode 100644 index 00000000000..b6e687551ae --- /dev/null +++ b/rust/chains/tw_ton/src/message/payload/jetton_transfer.rs @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use tw_coin_entry::error::prelude::ResultContext; +use tw_number::U256; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::{Cell, CellArc}; +use tw_ton_sdk::error::{CellError, CellErrorType, CellResult}; + +pub const JETTON_TRANSFER: u32 = 0x0f8a7ea5; + +/// Jetton transfer message payload with an optional comment. +#[derive(Debug)] +pub struct JettonTransferPayload { + /// Arbitrary request number. + query_id: u64, + /// Amount of transferred jettons in elementary units. + jetton_amount: U256, + /// Address of the new owner of the jettons. + destination: TonAddress, + /// Address where to send a response with confirmation of a successful transfer and the rest of the incoming message Toncoins. + response_destination: TonAddress, + /// Optional custom data (which is used by either sender or receiver jetton wallet for inner logic). + /// At WalletCore, we do not use `custom_payload` at the moment. + #[allow(dead_code)] + custom_payload: Option, + /// Amount of nanotons to be sent to the destination address. + forward_ton_amount: U256, + /// Optional custom data that should be sent to the destination address. + /// At WalletCore, we do not use `forward_payload` at the moment. + #[allow(dead_code)] + forward_payload: Option, + /// Optional transfer comment. + comment: Option, +} + +impl JettonTransferPayload { + pub fn new(destination: TonAddress, jetton_amount: U256) -> Self { + JettonTransferPayload { + query_id: 0, + jetton_amount, + destination, + response_destination: TonAddress::null(), + custom_payload: None, + forward_ton_amount: U256::zero(), + forward_payload: None, + comment: None, + } + } + + pub fn with_query_id(&mut self, query_id: u64) -> &mut Self { + self.query_id = query_id; + self + } + + pub fn with_response_destination(&mut self, response_destination: TonAddress) -> &mut Self { + self.response_destination = response_destination; + self + } + + pub fn with_comment(&mut self, comment: String) -> &mut Self { + self.comment = Some(comment); + self + } + + pub fn with_forward_ton_amount(&mut self, forward_ton_amount: U256) -> &mut Self { + self.forward_ton_amount = forward_ton_amount; + self + } + + pub fn build(&self) -> CellResult { + if self.forward_ton_amount.is_zero() && self.forward_payload.is_some() { + return CellError::err(CellErrorType::CellBuilderError) + .context("Forward_ton_amount must be positive when specifying forward_payload"); + } + + let mut message = CellBuilder::new(); + message.store_u32(32, JETTON_TRANSFER)?; + message.store_u64(64, self.query_id)?; + message.store_coins(&self.jetton_amount)?; + message.store_address(&self.destination)?; + message.store_address(&self.response_destination)?; + + if let Some(ref cp) = self.custom_payload { + message.store_bit(true)?; + message.store_reference(cp)?; + } else { + message.store_bit(false)?; + } + + message.store_coins(&self.forward_ton_amount)?; + + if let Some(ref fp) = self.forward_payload { + message.store_bit(true)?; + message.store_reference(fp)?; + } else { + message.store_bit(false)?; + } + + if let Some(ref comment) = self.comment { + message.store_u32(32, 0)?; + message.store_string(comment)?; + } + + message.build() + } +} diff --git a/rust/chains/tw_ton/src/message/payload/mod.rs b/rust/chains/tw_ton/src/message/payload/mod.rs new file mode 100644 index 00000000000..8b50715cd12 --- /dev/null +++ b/rust/chains/tw_ton/src/message/payload/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod comment; +pub mod empty; +pub mod jetton_transfer; diff --git a/rust/chains/tw_ton/src/message/signed_message.rs b/rust/chains/tw_ton/src/message/signed_message.rs new file mode 100644 index 00000000000..3791d8d20f6 --- /dev/null +++ b/rust/chains/tw_ton/src/message/signed_message.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_hash::H512; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::*; + +pub struct SignedMessage { + pub signature: H512, + pub external_message: Cell, +} + +impl SignedMessage { + pub fn build(&self) -> CellResult { + let mut body_builder = CellBuilder::new(); + body_builder.store_slice(self.signature.as_slice())?; + body_builder.store_cell(&self.external_message)?; + body_builder.build() + } +} diff --git a/rust/chains/tw_ton/src/modules/address_converter.rs b/rust/chains/tw_ton/src/modules/address_converter.rs new file mode 100644 index 00000000000..733524140af --- /dev/null +++ b/rust/chains/tw_ton/src/modules/address_converter.rs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use tw_coin_entry::error::prelude::ResultContext; +use tw_ton_sdk::boc::BagOfCells; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::CellResult; + +const HAS_CRC32: bool = true; + +pub struct AddressConverter; + +impl AddressConverter { + /// Converts a TON user address into a single root Cell. + pub fn convert_to_cell(addr: &TonAddress) -> CellResult { + let mut builder = CellBuilder::new(); + builder + .store_address(addr) + .context("Error storing given address to CellBuilder")?; + builder.build() + } + + /// 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 + pub fn convert_to_boc(addr: &TonAddress) -> CellResult { + Self::convert_to_cell(addr).map(BagOfCells::from_root) + } + + /// 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 + pub fn convert_to_boc_base64(addr: &TonAddress) -> CellResult { + Self::convert_to_boc(addr).and_then(|boc| boc.to_base64(HAS_CRC32)) + } + + /// Parses a TON address from a single root Cell. + pub fn parse_from_cell(cell: &Cell) -> CellResult { + cell.parse_fully(|parser| parser.load_address()) + .map(TonAddress::with_address_data) + } + + /// 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 + pub fn parse_from_boc(boc: &BagOfCells) -> CellResult { + boc.single_root() + .and_then(|cell| Self::parse_from_cell(cell)) + } + + /// 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 + pub fn parse_from_boc_base64(boc: &str) -> CellResult { + BagOfCells::parse_base64(boc).and_then(|boc| Self::parse_from_boc(&boc)) + } +} diff --git a/rust/chains/tw_ton/src/modules/mod.rs b/rust/chains/tw_ton/src/modules/mod.rs new file mode 100644 index 00000000000..3ed4c72f589 --- /dev/null +++ b/rust/chains/tw_ton/src/modules/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address_converter; diff --git a/rust/chains/tw_ton/src/resources.rs b/rust/chains/tw_ton/src/resources.rs new file mode 100644 index 00000000000..01f4e26a646 --- /dev/null +++ b/rust/chains/tw_ton/src/resources.rs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use lazy_static::lazy_static; +use tw_ton_sdk::boc::BagOfCells; + +pub const DEFAULT_WALLET_ID: i32 = 0x29a9a317; +/// https://docs.ton.org/develop/howto/step-by-step#1-smart-contract-addresses +pub const BASE_WORKCHAIN: i32 = 0; +pub const MASTER_WORKCHAIN: i32 = -1; + +lazy_static! { + pub static ref WALLET_V4R2_CODE: BagOfCells = { + let code = include_str!("../resources/wallet/wallet_v4r2.code"); + BagOfCells::parse_base64(code).expect("Cannot decode wallet_v4r2.code") + }; +} diff --git a/rust/chains/tw_ton/src/signer.rs b/rust/chains/tw_ton/src/signer.rs new file mode 100644 index 00000000000..3a1417b7088 --- /dev/null +++ b/rust/chains/tw_ton/src/signer.rs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::signing_request::builder::SigningRequestBuilder; +use crate::signing_request::cell_creator::ExternalMessageCreator; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_proto::TheOpenNetwork::Proto; +use tw_ton_sdk::boc::BagOfCells; +use tw_ton_sdk::error::cell_to_signing_error; + +const HAS_CRC32: bool = true; + +pub struct TheOpenNetworkSigner; + +impl TheOpenNetworkSigner { + pub fn sign( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> Proto::SigningOutput<'static> { + Self::sign_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn sign_impl( + _coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let signing_request = SigningRequestBuilder::build(&input)?; + + let external_message = + ExternalMessageCreator::create_external_message_to_sign(&signing_request) + .map_err(cell_to_signing_error)?; + + // Whether to add 'StateInit' reference. + let state_init = signing_request.seqno == 0; + let signed_tx = signing_request + .wallet + .sign_transaction(external_message, state_init) + .context("Error signing/wrapping an external message")? + .build() + .context("Error generating signed message cell") + .map_err(cell_to_signing_error)?; + + let signed_tx_hash = signed_tx.cell_hash(); + let signed_tx_encoded = BagOfCells::from_root(signed_tx) + .to_base64(HAS_CRC32) + .context("Error serializing signed transaction as BoC") + .map_err(cell_to_signing_error)?; + + Ok(Proto::SigningOutput { + encoded: signed_tx_encoded.into(), + hash: signed_tx_hash.to_vec().into(), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_ton/src/signing_request/builder.rs b/rust/chains/tw_ton/src/signing_request/builder.rs new file mode 100644 index 00000000000..0b180435bb6 --- /dev/null +++ b/rust/chains/tw_ton/src/signing_request/builder.rs @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use crate::signing_request::{ + JettonTransferRequest, SigningRequest, TransferCustomRequest, TransferPayload, TransferRequest, +}; +use crate::wallet::wallet_v4::WalletV4; +use crate::wallet::TonWallet; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_keypair::ed25519::sha512::{KeyPair, PublicKey}; +use tw_number::U256; +use tw_proto::TheOpenNetwork::Proto; +use tw_ton_sdk::error::cell_to_signing_error; +use Proto::mod_Transfer::OneOfpayload as PayloadType; + +const STATE_INIT_EXPIRE_AT: u32 = 0xffffffff; + +pub struct SigningRequestBuilder; + +impl SigningRequestBuilder { + pub fn build(input: &Proto::SigningInput) -> SigningResult { + let wallet = Self::wallet(input)?; + + let messages = input + .messages + .iter() + .map(Self::transfer_request) + .collect::>>()?; + + let expire_at = if input.sequence_number == 0 { + STATE_INIT_EXPIRE_AT + } else if input.expire_at == 0 { + return SigningError::err(SigningErrorType::Error_invalid_params) + .context("'expire_at' must be set"); + } else { + input.expire_at + }; + + Ok(SigningRequest { + wallet, + messages, + expire_at, + seqno: input.sequence_number, + }) + } + + /// Currently, V4R2 wallet supported only. + fn wallet(input: &Proto::SigningInput) -> SigningResult> { + if !input.private_key.is_empty() { + let key_pair = KeyPair::try_from(input.private_key.as_ref()) + .into_tw() + .context("Invalid private key")?; + return TonWallet::std_with_key_pair(&key_pair).map_err(cell_to_signing_error); + } + + let public_key = PublicKey::try_from(input.public_key.as_ref()) + .into_tw() + .context("Expected either 'private_key' or 'public_key' to be set")?; + TonWallet::std_with_public_key(public_key).map_err(cell_to_signing_error) + } + + fn transfer_request(input: &Proto::Transfer) -> SigningResult { + if input.wallet_version != Proto::WalletVersion::WALLET_V4_R2 { + return SigningError::err(SigningErrorType::Error_not_supported) + .context("'WALLET_V4_R2' version is supported only"); + } + + let dest = TonAddress::from_str(input.dest.as_ref()) + .into_tw() + .context("Invalid 'dest' address")? + // Set the 'bounceable' flag explicitly as specified in the Protobuf. + .set_bounceable(input.bounceable); + + let comment = if input.comment.is_empty() { + None + } else { + Some(input.comment.to_string()) + }; + + let mode = u8::try_from(input.mode) + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("'mode' must fit uint8")?; + + let payload = match input.payload { + PayloadType::jetton_transfer(ref jetton) => { + Some(Self::jetton_transfer_request(jetton)?) + }, + PayloadType::custom_payload(ref custom) => Some(Self::custom_request(custom)?), + PayloadType::None => None, + }; + + Ok(TransferRequest { + dest, + ton_amount: U256::from(input.amount), + mode, + comment, + payload, + }) + } + + fn jetton_transfer_request(input: &Proto::JettonTransfer) -> SigningResult { + let dest = TonAddress::from_str(input.to_owner.as_ref()) + .into_tw() + .context("Invalid 'dest' address")?; + + let response_address = TonAddress::from_str(input.response_address.as_ref()) + .into_tw() + .context("Invalid 'response_address' address")?; + + let jetton_payload = JettonTransferRequest { + query_id: input.query_id, + jetton_amount: U256::from(input.jetton_amount), + dest, + response_address, + forward_ton_amount: U256::from(input.forward_amount), + }; + + Ok(TransferPayload::JettonTransfer(jetton_payload)) + } + + fn custom_request(input: &Proto::CustomPayload) -> SigningResult { + let state_init = if input.state_init.is_empty() { + None + } else { + Some(input.state_init.to_string()) + }; + + let payload = if input.payload.is_empty() { + None + } else { + Some(input.payload.to_string()) + }; + + Ok(TransferPayload::Custom(TransferCustomRequest { + state_init, + payload, + })) + } +} diff --git a/rust/chains/tw_ton/src/signing_request/cell_creator.rs b/rust/chains/tw_ton/src/signing_request/cell_creator.rs new file mode 100644 index 00000000000..f2295848d90 --- /dev/null +++ b/rust/chains/tw_ton/src/signing_request/cell_creator.rs @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::message::internal_message::transfer::TransferInternalMessage; +use crate::message::internal_message::InternalMessage; +use crate::message::payload::comment::CommentPayload; +use crate::message::payload::empty::EmptyPayload; +use crate::message::payload::jetton_transfer::JettonTransferPayload; +use crate::signing_request::{ + JettonTransferRequest, SigningRequest, TransferCustomRequest, TransferPayload, TransferRequest, +}; +use std::sync::Arc; +use tw_coin_entry::error::prelude::ResultContext; +use tw_ton_sdk::boc::BagOfCells; +use tw_ton_sdk::cell::{Cell, CellArc}; +use tw_ton_sdk::error::{CellError, CellErrorType, CellResult}; + +pub struct InternalMessageCreator; + +impl InternalMessageCreator { + pub fn create_internal_message( + transfer_request: &TransferRequest, + ) -> CellResult { + let mut transfer_message = TransferInternalMessage::new( + transfer_request.dest.clone(), + transfer_request.ton_amount, + ); + + // Store a custom contract StateInit Cell if it's provided. + if let Some(state_init) = Self::maybe_custom_state_init(transfer_request)? { + transfer_message.with_state_init(state_init); + } + // In WalletCore, we always store the transfer data even if it's an empty Cell. + transfer_message.with_data(Self::transfer_payload(transfer_request)?); + + let transfer_message_cell = transfer_message + .build() + .context("Error generating 'Transfer' internal message cell")?; + + Ok(InternalMessage::new( + transfer_request.mode, + transfer_message_cell, + )) + } + + fn transfer_payload(transfer_request: &TransferRequest) -> CellResult { + match transfer_request.payload { + Some(TransferPayload::JettonTransfer(ref jetton)) => { + Self::jetton_transfer_payload(jetton, transfer_request.comment.clone()) + }, + Some(TransferPayload::Custom(ref custom)) => Self::custom_payload(custom), + // Otherwise, this is an ordinary TON transfer with an optional comment. + None => Self::maybe_comment_payload(transfer_request.comment.clone()), + } + } + + fn maybe_comment_payload(comment: Option) -> CellResult { + match comment { + Some(comment) => CommentPayload::new(comment) + .build() + .context("Error generating Transfer's comment payload"), + None => EmptyPayload + .build() + .context("Error generating Transfer's empty payload"), + } + .map(Cell::into_arc) + } + + fn jetton_transfer_payload( + jetton: &JettonTransferRequest, + comment: Option, + ) -> CellResult { + let mut payload = JettonTransferPayload::new(jetton.dest.clone(), jetton.jetton_amount); + payload + .with_query_id(jetton.query_id) + .with_response_destination(jetton.response_address.clone()) + .with_forward_ton_amount(jetton.forward_ton_amount); + + if let Some(comment) = comment { + payload.with_comment(comment); + } + + payload + .build() + .map(Cell::into_arc) + .context("Error generating Jetton Transfer payload") + } + + fn custom_payload(custom: &TransferCustomRequest) -> CellResult { + match custom.payload { + Some(ref payload) => BagOfCells::parse_base64(payload) + .context("Error parsing custom Transfer payload")? + .single_root() + .map(Arc::clone) + .context("Custom Transfer payload must contain only one single root"), + // Create an empty Cell payload. + None => EmptyPayload + .build() + .map(Cell::into_arc) + .context("Error generating Transfer's empty payload"), + } + } + + fn maybe_custom_state_init(request: &TransferRequest) -> CellResult> { + let Some(TransferPayload::Custom(ref custom)) = request.payload else { + return Ok(None); + }; + + let Some(ref state_init) = custom.state_init else { + return Ok(None); + }; + + let state_init_cell = BagOfCells::parse_base64(state_init) + .context("Error parsing Transfer stateInit")? + .single_root() + .map(Arc::clone) + .context("stateInit must contain only one single root")?; + Ok(Some(state_init_cell)) + } +} + +pub struct ExternalMessageCreator; + +impl ExternalMessageCreator { + pub fn create_external_message_to_sign(request: &SigningRequest) -> CellResult { + if request.messages.is_empty() { + return CellError::err(CellErrorType::CellBuilderError) + .context("There must be at least one Transfer message"); + } + + let internal_messages = request + .messages + .iter() + .map(InternalMessageCreator::create_internal_message) + .collect::>>()?; + + request + .wallet + .create_external_body(request.expire_at, request.seqno, internal_messages) + .context("Error generating an external message cell") + } +} diff --git a/rust/chains/tw_ton/src/signing_request/mod.rs b/rust/chains/tw_ton/src/signing_request/mod.rs new file mode 100644 index 00000000000..ec3b3109cfb --- /dev/null +++ b/rust/chains/tw_ton/src/signing_request/mod.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use crate::wallet::wallet_v4::WalletV4; +use crate::wallet::TonWallet; +use tw_number::U256; + +pub mod builder; +pub mod cell_creator; + +pub enum TransferPayload { + /// Jetton Transfer message payload. + JettonTransfer(JettonTransferRequest), + /// Custom Transfer message payload. + Custom(TransferCustomRequest), +} + +pub struct TransferRequest { + /// TON recipient address. + /// Also determines whether the transaction is bounceable or not. + pub dest: TonAddress, + /// Amount to send in nanotons. + pub ton_amount: U256, + /// Send mode. + /// https://ton.org/docs/develop/func/stdlib#send_raw_message + pub mode: u8, + /// Transfer comment message. + pub comment: Option, + /// Transfer payload. + pub payload: Option, +} + +pub struct JettonTransferRequest { + /// Arbitrary request number. + pub query_id: u64, + /// Amount of transferred jettons in elementary integer units. + /// The real value transferred is `jetton_amount` multiplied by ten to the power of token decimal precision. + pub jetton_amount: U256, + /// Address of the new owner of the jettons. + pub dest: TonAddress, + /// Address where to send a response with confirmation of a successful transfer and the rest of the incoming message Toncoins. + /// Usually the sender should get back their toncoins. + pub response_address: TonAddress, + /// Amount in nanotons to forward to recipient. Basically minimum amount - 1 nanoton should be used. + pub forward_ton_amount: U256, +} + +pub struct TransferCustomRequest { + /// (string base64, optional): raw one-cell BoC encoded in Base64. + /// Can be used to deploy a smart contract. + pub state_init: Option, + /// (string base64, optional): raw one-cell BoC encoded in Base64. + pub payload: Option, +} + +pub struct SigningRequest { + /// Wallet initialized with the user's key-pair or public key. + pub wallet: TonWallet, + pub messages: Vec, + /// External message counter. + /// https://ton.org/docs/develop/smart-contracts/guidelines/external-messages + pub seqno: u32, + /// Expiration UNIX timestamp. + pub expire_at: u32, +} diff --git a/rust/chains/tw_ton/src/transaction.rs b/rust/chains/tw_ton/src/transaction.rs new file mode 100644 index 00000000000..37dd1f6fdce --- /dev/null +++ b/rust/chains/tw_ton/src/transaction.rs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use crate::message::signed_message::SignedMessage; +use tw_number::U256; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::*; +use tw_ton_sdk::message::state_init::StateInit; + +/// See an example at https://docs.ton.org/develop/smart-contracts/tutorials/wallet#contract-deployment-via-wallet +pub const INCOMING_EXTERNAL_TRANSACTION: u8 = 0b10; + +pub struct SignedTransaction { + pub src_address: TonAddress, + pub dest_address: TonAddress, + pub import_fee: U256, + /// Created via `StateInit`. + pub state_init: Option, + pub signed_body: SignedMessage, +} + +impl SignedTransaction { + pub fn build(&self) -> CellResult { + let mut wrap_builder = CellBuilder::new(); + wrap_builder + .store_u8(2, INCOMING_EXTERNAL_TRANSACTION)? // incoming external transaction + .store_address(&self.src_address)? // src + .store_address(&self.dest_address)? // dest + .store_coins(&self.import_fee)?; // import fee + + if let Some(ref state_init) = self.state_init { + wrap_builder.store_bit(true)?; // state init present + wrap_builder.store_bit(true)?; // state init in ref + wrap_builder.store_child(state_init.to_cell()?)?; // state init + } else { + wrap_builder.store_bit(false)?; // state init absent + } + + wrap_builder.store_bit(true)?; // signed_body is always defined + wrap_builder.store_child(self.signed_body.build()?)?; // Signed body + + wrap_builder.build() + } +} diff --git a/rust/chains/tw_ton/src/wallet/mod.rs b/rust/chains/tw_ton/src/wallet/mod.rs new file mode 100644 index 00000000000..e6257528e76 --- /dev/null +++ b/rust/chains/tw_ton/src/wallet/mod.rs @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use crate::message::external_message::ExternalMessage; +use crate::message::internal_message::InternalMessage; +use crate::message::signed_message::SignedMessage; +use crate::resources::{BASE_WORKCHAIN, DEFAULT_WALLET_ID}; +use crate::transaction::SignedTransaction; +use tw_coin_entry::error::prelude::*; +use tw_hash::H256; +use tw_keypair::ed25519::sha512::{KeyPair, PrivateKey, PublicKey}; +use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait}; +use tw_number::U256; +use tw_ton_sdk::cell::{Cell, CellArc}; +use tw_ton_sdk::error::{cell_to_signing_error, CellResult}; +use tw_ton_sdk::message::state_init::StateInit; + +pub mod wallet_v4; + +pub trait WalletVersion { + /// Returns data that is (will be) stored in the wallet smart contract + /// when it is first deployed to the blockchain. + fn initial_data(&self, wallet_id: i32, public_key: H256) -> CellResult; + + /// Returns this wallet specific version contract code as a Cell. + fn code(&self) -> CellResult; + + /// Whether the wallet version supports OP codes. + /// For example, plugin OP codes: https://github.com/ton-blockchain/wallet-contract/blob/main/func/wallet-v4-code.fc#L102 + fn has_op(&self) -> bool; +} + +pub struct TonWallet { + public_key: PublicKey, + private_key: Option, + version: Version, + /// TON address derived from the [`TonWallet::public_key`]. + address: TonAddress, + wallet_id: i32, +} + +impl TonWallet { + /// Creates a standard TON wallet from the given public key. + /// Please note when created with public key only, wallet cannot be used to sign messages. + pub fn std_with_public_key(public_key: PublicKey) -> CellResult { + Self::with_public_key( + BASE_WORKCHAIN, + wallet_v4::WalletV4::r2()?, + public_key, + DEFAULT_WALLET_ID, + ) + } + + /// Creates a standard TON wallet from the given key-pair. + pub fn std_with_key_pair(key_pair: &KeyPair) -> CellResult { + Self::with_key_pair( + BASE_WORKCHAIN, + wallet_v4::WalletV4::r2()?, + key_pair, + DEFAULT_WALLET_ID, + ) + } +} + +impl TonWallet { + /// Creates a TON wallet from the given public key. + /// Please note when created with public key only, wallet cannot be used to sign messages. + pub fn with_public_key( + workchain: i32, + version: Version, + public_key: PublicKey, + wallet_id: i32, + ) -> CellResult { + Self::new(workchain, version, public_key, None, wallet_id) + } + + /// Creates a TON wallet from the given key-pair. + pub fn with_key_pair( + workchain: i32, + version: Version, + key_pair: &KeyPair, + wallet_id: i32, + ) -> CellResult { + let public = key_pair.public().clone(); + let private = key_pair.private().clone(); + Self::new(workchain, version, public, Some(private), wallet_id) + } + + pub fn address(&self) -> &TonAddress { + &self.address + } + + pub fn state_init(&self) -> CellResult { + Self::state_init_impl(&self.version, &self.public_key, self.wallet_id) + } + + pub fn create_external_body( + &self, + expire_at: u32, + seqno: u32, + internal_messages: Vec, + ) -> CellResult { + ExternalMessage { + wallet_id: self.wallet_id, + expire_at, + seqno, + has_op: self.version.has_op(), + internal_messages, + } + .build() + } + + pub fn sign_external_message(&self, external_message: Cell) -> SigningResult { + let message_hash = external_message.cell_hash(); + let sig = self + .private_key + .as_ref() + .or_tw_err(SigningErrorType::Error_internal) + .context( + "'TonWallet' should be initialized with a key-pair to be able to sign a message", + )? + .sign(message_hash.to_vec())?; + Ok(SignedMessage { + signature: sig.to_bytes(), + external_message, + }) + } + + pub fn sign_transaction( + &self, + external_message: Cell, + state_init: bool, + ) -> SigningResult { + let state_init = if state_init { + let state_init = self.state_init().map_err(cell_to_signing_error)?; + Some(state_init) + } else { + None + }; + + let signed_body = self.sign_external_message(external_message)?; + Ok(SignedTransaction { + src_address: TonAddress::null(), + // The wallet contract address. + dest_address: self.address.clone(), + import_fee: U256::zero(), + state_init, + signed_body, + }) + } + + /// Private function to create the TonWallet with the given public and optional private keys. + /// Do not make it public as the function caller can provide unrelated keys. + fn new( + workchain: i32, + version: Version, + public_key: PublicKey, + private_key: Option, + wallet_id: i32, + ) -> CellResult { + let state_init_hash = + Self::state_init_impl(&version, &public_key, wallet_id)?.create_account_id()?; + let address = TonAddress::new(workchain, state_init_hash); + Ok(TonWallet { + public_key, + private_key, + version, + address, + wallet_id, + }) + } + + fn state_init_impl( + version: &Version, + public_key: &PublicKey, + wallet_id: i32, + ) -> CellResult { + let public_key_bytes = public_key.to_bytes(); + + let initial_data = version + .initial_data(wallet_id, public_key_bytes)? + .into_arc(); + let code = version.code()?; + Ok(StateInit::default().set_code(code).set_data(initial_data)) + } +} diff --git a/rust/chains/tw_ton/src/wallet/wallet_v4.rs b/rust/chains/tw_ton/src/wallet/wallet_v4.rs new file mode 100644 index 00000000000..9beae8846ec --- /dev/null +++ b/rust/chains/tw_ton/src/wallet/wallet_v4.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::resources::WALLET_V4R2_CODE; +use crate::wallet::WalletVersion; +use std::sync::Arc; +use tw_hash::H256; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::{Cell, CellArc}; +use tw_ton_sdk::error::CellResult; + +/// We support WalletV4R2 version only. +/// Consider adding a new `WalletVR` if needed. +enum Revision { + R2, +} + +pub struct WalletV4 { + revision: Revision, +} + +impl WalletV4 { + pub fn r2() -> CellResult { + Ok(WalletV4 { + revision: Revision::R2, + }) + } +} + +impl WalletVersion for WalletV4 { + fn initial_data(&self, wallet_id: i32, public_key: H256) -> CellResult { + let seqno = 0; + + let mut builder = CellBuilder::new(); + builder + .store_u32(32, seqno)? + .store_i32(32, wallet_id)? + .store_slice(public_key.as_slice())? + // empty plugin dict + .store_bit(false)?; + builder.build() + } + + fn code(&self) -> CellResult { + match self.revision { + Revision::R2 => WALLET_V4R2_CODE.single_root().map(Arc::clone), + } + } + + fn has_op(&self) -> bool { + matches!(self.revision, Revision::R2) + } +} diff --git a/rust/frameworks/tw_ton_sdk/Cargo.toml b/rust/frameworks/tw_ton_sdk/Cargo.toml new file mode 100644 index 00000000000..c8544bef84b --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tw_ton_sdk" +version = "0.1.0" +edition = "2021" + +[dependencies] +bitreader = "0.3.8" +bitstream-io = "2.5.0" +crc = "3" +lazy_static = "1.4.0" +num-bigint = "0.4" +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_keypair = { path = "../../tw_keypair" } +tw_hash = { path = "../../tw_hash" } +tw_memory = { path = "../../tw_memory" } +tw_number = { path = "../../tw_number" } + +[dev-dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/rust/frameworks/tw_ton_sdk/fuzz/.gitignore b/rust/frameworks/tw_ton_sdk/fuzz/.gitignore new file mode 100644 index 00000000000..5c404b9583f --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/fuzz/.gitignore @@ -0,0 +1,5 @@ +target +corpus +artifacts +coverage +Cargo.lock diff --git a/rust/frameworks/tw_ton_sdk/fuzz/Cargo.toml b/rust/frameworks/tw_ton_sdk/fuzz/Cargo.toml new file mode 100644 index 00000000000..2536b143a70 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/fuzz/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tw_ton_sdk-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.tw_ton_sdk] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "boc_encode" +path = "fuzz_targets/boc_encode.rs" +test = false +doc = false diff --git a/rust/frameworks/tw_ton_sdk/fuzz/fuzz_targets/boc_encode.rs b/rust/frameworks/tw_ton_sdk/fuzz/fuzz_targets/boc_encode.rs new file mode 100644 index 00000000000..fd178943d78 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/fuzz/fuzz_targets/boc_encode.rs @@ -0,0 +1,8 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use tw_ton_sdk::boc::BagOfCells; + +fuzz_target!(|data: &[u8]| { + let _ = BagOfCells::parse(data); +}); diff --git a/rust/frameworks/tw_ton_sdk/src/address/address_data.rs b/rust/frameworks/tw_ton_sdk/src/address/address_data.rs new file mode 100644 index 00000000000..73784543467 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/address/address_data.rs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_hash::H256; + +const WORKCHAIN_MASK: i32 = 0xff; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AddressData { + pub workchain: i32, + pub hash_part: H256, +} + +impl AddressData { + pub const NULL: AddressData = AddressData::null(); + + pub const fn null() -> AddressData { + AddressData { + workchain: 0, + hash_part: H256::new(), + } + } + + pub fn new(workchain: i32, hash_part: H256) -> AddressData { + AddressData { + workchain, + hash_part, + } + } + + pub fn workchain_byte(&self) -> u8 { + (self.workchain & WORKCHAIN_MASK) as u8 + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/address/mod.rs b/rust/frameworks/tw_ton_sdk/src/address/mod.rs new file mode 100644 index 00000000000..0a9b80e7d60 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/address/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address_data; +pub mod raw_address; +pub mod user_friendly_address; diff --git a/rust/frameworks/tw_ton_sdk/src/address/raw_address.rs b/rust/frameworks/tw_ton_sdk/src/address/raw_address.rs new file mode 100644 index 00000000000..37c18357c4d --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/address/raw_address.rs @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::address_data::AddressData; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::error::prelude::AddressError; +use tw_encoding::hex; +use tw_hash::H256; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawAddress(AddressData); + +impl RawAddress { + #[inline] + pub fn into_data(self) -> AddressData { + self.0 + } +} + +impl AsRef for RawAddress { + #[inline] + fn as_ref(&self) -> &AddressData { + &self.0 + } +} + +impl From for RawAddress { + #[inline] + fn from(value: AddressData) -> Self { + RawAddress(value) + } +} + +impl FromStr for RawAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + let mut it = s.split(':'); + + let workchain = it + .next() + .ok_or(AddressError::MissingPrefix)? + .parse::() + .map_err(|_| AddressError::InvalidInput)?; + + let hash_hex = it.next().ok_or(AddressError::InvalidInput)?; + let decoded_hash_part = hex::decode(hash_hex).map_err(|_| AddressError::FromHexError)?; + + let hash_part = + H256::try_from(decoded_hash_part.as_slice()).map_err(|_| AddressError::InvalidInput)?; + + // Expected only 2 parts of the hex-encoded address. + if it.next().is_some() { + return Err(AddressError::InvalidInput); + } + + Ok(RawAddress(AddressData::new(workchain, hash_part))) + } +} + +impl fmt::Display for RawAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let prefixed = false; + write!( + f, + "{}:{}", + self.0.workchain, + hex::encode(self.0.hash_part, prefixed) + ) + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/address/user_friendly_address.rs b/rust/frameworks/tw_ton_sdk/src/address/user_friendly_address.rs new file mode 100644 index 00000000000..b1723e108d1 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/address/user_friendly_address.rs @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::address_data::AddressData; +use crate::crc::CRC_16_XMODEM; +use tw_coin_entry::error::prelude::{AddressError, AddressResult}; +use tw_encoding::base64; +use tw_encoding::base64::{NO_PAD, URL_NO_PAD}; +use tw_hash::{H256, H288}; + +const BASE64_ADDRESS_LEN: usize = 48; +const CHECKSUM_MASK: u16 = 0xff; + +const BOUNCEABLE: u8 = 0x11; +const NON_BOUNCEABLE: u8 = 0x51; +const BOUNCEABLE_TESTNET: u8 = 0x91; +const NON_BOUNCEABLE_TESTNET: u8 = 0xD1; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UserFriendlyAddress { + data: AddressData, + bounceable: bool, + testnet: bool, +} + +impl UserFriendlyAddress { + #[inline] + pub const fn with_flags(data: AddressData, bounceable: bool, testnet: bool) -> Self { + UserFriendlyAddress { + data, + bounceable, + testnet, + } + } + + #[inline] + pub fn into_data(self) -> AddressData { + self.data + } + + #[inline] + pub fn bounceable(&self) -> bool { + self.bounceable + } + + #[inline] + pub fn testnet(&self) -> bool { + self.testnet + } + + #[inline] + pub fn set_bounceable(self, bounceable: bool) -> Self { + UserFriendlyAddress { bounceable, ..self } + } + + #[inline] + pub fn set_testnet(self, testnet: bool) -> Self { + UserFriendlyAddress { testnet, ..self } + } + + /// Parses url-safe base64 representation of an address + /// + /// # Returns + /// the address, non-bounceable flag, non-production flag. + pub fn from_base64_url(s: &str) -> AddressResult { + Self::from_base64_with_config(s, URL_NO_PAD) + } + + /// Parses standard base64 representation of an address + /// + /// # Returns + /// the address, bounceable flag, testnet flag. + pub fn from_base64_std(s: &str) -> AddressResult { + Self::from_base64_with_config(s, NO_PAD) + } + + /// Parses base64 representation of an address with encoding config. + /// + /// # Returns + /// the address, non-bounceable flag, non-production flag. + fn from_base64_with_config(s: &str, config: base64::Config) -> AddressResult { + if s.len() != BASE64_ADDRESS_LEN { + return Err(AddressError::InvalidInput); + } + let bytes = base64::decode(s, config).map_err(|_| AddressError::FromBase64Error)?; + // Address length has been checked already. + let slice = H288::try_from(bytes.as_slice()).map_err(|_| AddressError::Internal)?; + Self::from_base64_bytes(slice) + } + + /// Parses decoded base64 representation of an address + /// + /// # Returns + /// the address, bounceable flag, testnet flag. + fn from_base64_bytes(bytes: H288) -> AddressResult { + let (bounceable, testnet) = match bytes[0] { + BOUNCEABLE => (true, false), + NON_BOUNCEABLE => (false, false), + BOUNCEABLE_TESTNET => (true, true), + NON_BOUNCEABLE_TESTNET => (false, true), + _ => return Err(AddressError::InvalidInput), + }; + + let workchain = bytes[1] as i8 as i32; + + let calc_crc = CRC_16_XMODEM.checksum(&bytes[0..34]); + let addr_crc = ((bytes[34] as u16) << 8) | bytes[35] as u16; + if calc_crc != addr_crc { + return Err(AddressError::InvalidChecksum); + } + + let hash_part = H256::try_from(&bytes[2..34]).expect("Expected exactly 32 bytes"); + let data = AddressData::new(workchain, hash_part); + Ok(UserFriendlyAddress { + data, + bounceable, + testnet, + }) + } + + pub fn to_base64_url(&self) -> String { + self.to_base64_with_config(URL_NO_PAD) + } + + pub fn to_base64_std(&self) -> String { + self.to_base64_with_config(NO_PAD) + } + + fn to_base64_with_config(&self, config: base64::Config) -> String { + let bytes = self.to_base64_bytes(); + base64::encode(bytes.as_slice(), config) + } + + fn to_base64_bytes(&self) -> H288 { + let mut bytes = H288::default(); + let tag: u8 = match (self.bounceable, self.testnet) { + (false, false) => NON_BOUNCEABLE, + (true, false) => BOUNCEABLE, + (false, true) => NON_BOUNCEABLE_TESTNET, + (true, true) => BOUNCEABLE_TESTNET, + }; + bytes[0] = tag; + bytes[1] = self.data.workchain_byte(); + bytes[2..34].clone_from_slice(self.data.hash_part.as_slice()); + let crc = CRC_16_XMODEM.checksum(&bytes[0..34]); + bytes[34] = ((crc >> 8) & CHECKSUM_MASK) as u8; + bytes[35] = (crc & CHECKSUM_MASK) as u8; + + bytes + } +} + +impl AsRef for UserFriendlyAddress { + #[inline] + fn as_ref(&self) -> &AddressData { + &self.data + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/boc/binary_reader.rs b/rust/frameworks/tw_ton_sdk/src/boc/binary_reader.rs new file mode 100644 index 00000000000..5ae28069164 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/boc/binary_reader.rs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::error::{CellErrorType, CellResult}; +use bitstream_io::{BigEndian, ByteRead, ByteReader}; +use std::io::Cursor; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; + +pub struct BinaryReader<'a> { + reader: ByteReader, BigEndian>, +} + +#[allow(dead_code)] +impl<'a> BinaryReader<'a> { + pub fn new(data: &'a [u8]) -> Self { + let cursor = Cursor::new(data); + BinaryReader { + reader: ByteReader::new(cursor), + } + } + + pub fn read_u8(&mut self) -> CellResult { + self.reader + .read::() + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError) + } + + pub fn read_u32(&mut self) -> CellResult { + self.reader + .read::() + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError) + } + + pub fn read_bytes(&mut self, buf: &mut [u8]) -> CellResult<()> { + self.reader + .read_bytes(buf) + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError) + } + + pub fn read_to_vec(&mut self, num_bytes: usize) -> CellResult { + self.reader + .read_to_vec(num_bytes) + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError) + } + + pub fn read_var_size(&mut self, num_bytes: usize) -> CellResult { + let mut bytes = vec![0; num_bytes]; + self.read_bytes(&mut bytes)?; + + let mut result = 0; + for &byte in &bytes { + result <<= 8; + result |= usize::from(byte); + } + Ok(result) + } + + pub fn skip(&mut self, num_bytes: u32) -> CellResult<()> { + self.reader + .skip(num_bytes) + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError) + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/boc/binary_writer.rs b/rust/frameworks/tw_ton_sdk/src/boc/binary_writer.rs new file mode 100644 index 00000000000..688d71f4e18 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/boc/binary_writer.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::error::{CellErrorType, CellResult}; +use bitstream_io::{BigEndian, BitWrite, BitWriter, Numeric}; +use tw_coin_entry::error::prelude::{MapTWError, OrTWError, ResultContext}; +use tw_memory::Data; + +pub struct BinaryWriter { + writer: BitWriter, +} + +impl BinaryWriter { + pub fn with_capacity(capacity: usize) -> BinaryWriter { + BinaryWriter { + writer: BitWriter::new(Vec::with_capacity(capacity)), + } + } + + pub fn write_bit(&mut self, bit: bool) -> CellResult<&mut Self> { + self.writer + .write_bit(bit) + .tw_err(|_| CellErrorType::BagOfCellsSerializationError)?; + Ok(self) + } + + pub fn write(&mut self, bits: u32, val: V) -> CellResult<&mut Self> + where + V: Numeric, + { + self.writer + .write(bits, val) + .tw_err(|_| CellErrorType::BagOfCellsSerializationError)?; + Ok(self) + } + + pub fn write_bytes(&mut self, bytes: &[u8]) -> CellResult<&mut Self> { + self.writer + .write_bytes(bytes) + .tw_err(|_| CellErrorType::BagOfCellsSerializationError)?; + Ok(self) + } + + /// TODO the function doesn't count `bit_len / 8` count of bytes. + /// Original code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell.rs#L507-L526 + pub(crate) fn write_bits(&mut self, data: &[u8], bit_len: usize) -> CellResult<()> { + let data_len = data.len(); + let rest_bits = bit_len % 8; + let full_bytes = rest_bits == 0; + + if full_bytes { + self.write_bytes(data)?; + } else { + self.write_bytes(&data[..data_len - 1])?; + let last_byte = data[data_len - 1]; + let l = last_byte | 1 << (8 - rest_bits - 1); + self.write(8, l)?; + } + + Ok(()) + } + + pub fn bytes_if_aligned(&mut self) -> CellResult<&[u8]> { + self.writer + .writer() + .map(|vec| vec.as_slice()) + .or_tw_err(CellErrorType::BagOfCellsSerializationError) + .context("Stream is not byte-aligned") + } + + /// Pads the stream with 0 bits until it is aligned at a whole byte. + /// Does nothing if the stream is already aligned. + /// Returns the number of trailing zero bits required to align the Cell. + pub fn align(&mut self) -> CellResult { + let mut trailing_zeros = 0; + while !self.writer.byte_aligned() { + self.write_bit(false)?; + trailing_zeros += 1; + } + Ok(trailing_zeros) + } + + pub fn finish(mut self) -> CellResult { + self.bytes_if_aligned().map(|slice| slice.to_vec()) + } +} + +impl Default for BinaryWriter { + fn default() -> Self { + BinaryWriter { + writer: BitWriter::new(Vec::default()), + } + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/boc/boc_to_raw_boc.rs b/rust/frameworks/tw_ton_sdk/src/boc/boc_to_raw_boc.rs new file mode 100644 index 00000000000..efcbddd4001 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/boc/boc_to_raw_boc.rs @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/raw_boc_from_boc.rs + +use crate::boc::raw::{RawBagOfCells, RawCell}; +use crate::boc::BagOfCells; +use crate::cell::{Cell, CellArc}; +use crate::error::{CellErrorType, CellResult}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::sync::Arc; +use tw_coin_entry::error::prelude::{OrTWError, ResultContext}; +use tw_hash::H256; + +type IndexedCellRef = RefCell; + +#[derive(Debug, Clone)] +struct IndexedCell { + index: usize, + cell: CellArc, +} + +pub(crate) fn convert_to_raw_boc(boc: &BagOfCells) -> CellResult { + let cells_by_hash = build_and_verify_index(&boc.roots); + + // Sort indexed cells by their index value. + let mut index_slice: Vec<_> = cells_by_hash.values().collect(); + index_slice.sort_unstable_by(|a, b| a.borrow().index.cmp(&b.borrow().index)); + + // Remove gaps in indices. + index_slice + .iter() + .enumerate() + .for_each(|(real_index, indexed_cell)| indexed_cell.borrow_mut().index = real_index); + + let cells_iter = index_slice + .into_iter() + .map(|indexed_cell| indexed_cell.borrow().cell.clone()); + let raw_cells = raw_cells_from_cells(cells_iter, &cells_by_hash)?; + let root_indices = root_indices(&boc.roots, &cells_by_hash)?; + + Ok(RawBagOfCells { + cells: raw_cells, + roots: root_indices, + }) +} + +fn build_and_verify_index(roots: &[CellArc]) -> HashMap { + let mut current_cells: Vec<_> = roots.iter().map(Arc::clone).collect(); + let mut new_hash_index = 0; + let mut cells_by_hash = HashMap::new(); + + // Process cells to build the initial index. + while !current_cells.is_empty() { + let mut next_cells = Vec::with_capacity(current_cells.len() * 4); + for cell in current_cells.iter() { + let hash = cell.cell_hash(); + + if cells_by_hash.contains_key(&hash) { + continue; // Skip if already indexed. + } + + cells_by_hash.insert( + hash, + RefCell::new(IndexedCell { + cell: Arc::clone(cell), + index: new_hash_index, + }), + ); + + new_hash_index += 1; + next_cells.extend(cell.references().iter().map(Arc::clone)); // Add referenced cells for the next iteration. + } + + current_cells = next_cells; + } + + // Ensure indices are in the correct order based on cell references. + let mut verify_order = true; + while verify_order { + verify_order = false; + + for index_cell in cells_by_hash.values() { + for reference in index_cell.borrow().cell.references().iter() { + let ref_hash = reference.cell_hash(); + if let Some(id_ref) = cells_by_hash.get(&ref_hash) { + if id_ref.borrow().index < index_cell.borrow().index { + id_ref.borrow_mut().index = new_hash_index; + new_hash_index += 1; + verify_order = true; // Reverify if an index was updated. + } + } + } + } + } + + cells_by_hash +} + +fn root_indices( + roots: &[CellArc], + cells_dict: &HashMap, +) -> CellResult> { + roots + .iter() + .map(|root_cell| root_cell.cell_hash()) + .map(|root_cell_hash| { + cells_dict + .get(&root_cell_hash) + .map(|index_record| index_record.borrow().index) + .or_tw_err(CellErrorType::BagOfCellsSerializationError) + .with_context(|| { + format!( + "Couldn't find cell with hash {root_cell_hash} while searching for roots" + ) + }) + }) + .collect() +} + +fn raw_cells_from_cells( + cells: impl Iterator, + cells_by_hash: &HashMap, +) -> CellResult> { + cells + .map(|cell| raw_cell_from_cell(&cell, cells_by_hash)) + .collect() +} + +fn raw_cell_from_cell( + cell: &Cell, + cells_by_hash: &HashMap, +) -> CellResult { + raw_cell_reference_indices(cell, cells_by_hash).map(|reference_indices| { + RawCell::new( + cell.data().to_vec(), + cell.bit_len(), + reference_indices, + cell.get_level_mask(), + cell.is_exotic(), + ) + }) +} + +fn raw_cell_reference_indices( + cell: &Cell, + cells_by_hash: &HashMap, +) -> CellResult> { + cell.references() + .iter() + .map(|cell| { + cells_by_hash + .get(&cell.cell_hash()) + .or_tw_err(CellErrorType::BagOfCellsSerializationError) + .with_context(|| { + format!( + "Couldn't find cell with hash {:?} while searching for references", + cell.cell_hash() + ) + }) + .map(|cell| cell.borrow().index) + }) + .collect() +} diff --git a/rust/frameworks/tw_ton_sdk/src/boc/mod.rs b/rust/frameworks/tw_ton_sdk/src/boc/mod.rs new file mode 100644 index 00000000000..b0d4a9f2be1 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/boc/mod.rs @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/bag_of_cells.rs + +use crate::boc::raw::RawBagOfCells; +use crate::cell::{Cell, CellArc}; +use crate::error::{CellError, CellErrorType, CellResult}; +use std::sync::Arc; +use tw_coin_entry::error::prelude::*; +use tw_encoding::base64::{self, STANDARD}; +use tw_memory::Data; + +pub mod binary_reader; +pub mod binary_writer; +pub mod boc_to_raw_boc; +pub mod raw; + +use boc_to_raw_boc::convert_to_raw_boc; + +#[derive(PartialEq, Eq, Debug, Clone, Hash)] +pub struct BagOfCells { + pub roots: Vec, +} + +impl BagOfCells { + pub fn from_root(root: Cell) -> BagOfCells { + BagOfCells { + roots: vec![root.into_arc()], + } + } + + pub fn single_root(&self) -> CellResult<&CellArc> { + let root_count = self.roots.len(); + if root_count == 1 { + Ok(&self.roots[0]) + } else { + CellError::err(CellErrorType::CellParserError) + .context(format!("Single root expected, got {root_count}")) + } + } + + pub fn parse(serial: &[u8]) -> CellResult { + let raw = RawBagOfCells::parse(serial)?; + let num_cells = raw.cells.len(); + let mut cells: Vec = Vec::with_capacity(num_cells); + + for (cell_index, raw_cell) in raw.cells.into_iter().enumerate().rev() { + let mut references = Vec::with_capacity(raw_cell.references.len()); + for ref_index in &raw_cell.references { + if *ref_index <= cell_index { + return CellError::err(CellErrorType::BagOfCellsDeserializationError) + .context("References to previous cells are not supported"); + } + let cell_ref_idx = (num_cells - 1) + .checked_sub(*ref_index) + .or_tw_err(CellErrorType::BagOfCellsDeserializationError) + .context("Cell references to an out-of-bound cell")?; + references.push(cells[cell_ref_idx].clone()); + } + + let cell = Cell::new( + raw_cell.data, + raw_cell.bit_len, + references, + raw_cell.is_exotic, + ) + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError)?; + cells.push(cell.into_arc()); + } + + if num_cells < raw.roots.len() { + return CellError::err(CellErrorType::BagOfCellsDeserializationError) + .context("BagOfCells contains more roots than cells"); + } + let roots = raw + .roots + .into_iter() + .map(|r| { + let cell_idx = (num_cells - 1) + .checked_sub(r) + .or_tw_err(CellErrorType::BagOfCellsDeserializationError) + .context("Root index doesn't correspond to a Cell")?; + Ok(Arc::clone(&cells[cell_idx])) + }) + .collect::>>()?; + + Ok(BagOfCells { roots }) + } + + pub fn parse_base64(base64: &str) -> CellResult { + let bin = base64::decode(base64, STANDARD) + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError) + .context("Expected base64 encoded BagOfCells")?; + Self::parse(&bin) + } + + pub fn serialize(&self, has_crc32: bool) -> CellResult { + let raw = convert_to_raw_boc(self)?; + raw.serialize(has_crc32) + } + + pub fn to_base64(&self, has_crc32: bool) -> CellResult { + let encoded = self.serialize(has_crc32)?; + Ok(base64::encode(&encoded, STANDARD)) + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/boc/raw.rs b/rust/frameworks/tw_ton_sdk/src/boc/raw.rs new file mode 100644 index 00000000000..3b02a02d415 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/boc/raw.rs @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/raw.rs + +use crate::boc::binary_reader::BinaryReader; +use crate::boc::binary_writer::BinaryWriter; +use crate::cell::level_mask::LevelMask; +use crate::crc::CRC_32_ISCSI; +use crate::error::{CellError, CellErrorType, CellResult}; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; + +const GENERIC_BOC_MAGIC: u32 = 0xb5ee9c72; +/// The max number of cells in a BoC. +const MAX_CELLS: usize = 4096; + +/// Raw representation of Cell. +/// +/// References are stored as indices in BagOfCells. +#[derive(PartialEq, Eq, Debug, Clone, Hash)] +pub(crate) struct RawCell { + pub(crate) data: Data, + pub(crate) bit_len: usize, + pub(crate) references: Vec, + pub(crate) is_exotic: bool, + level_mask: u32, +} + +impl RawCell { + pub(crate) fn new( + data: Vec, + bit_len: usize, + references: Vec, + level_mask: u32, + is_exotic: bool, + ) -> Self { + Self { + data, + bit_len, + references, + level_mask: level_mask & 7, + is_exotic, + } + } +} + +/// Raw representation of BagOfCells. +/// +/// `cells` must be topologically sorted. +#[derive(PartialEq, Eq, Debug, Clone, Hash)] +pub(crate) struct RawBagOfCells { + pub(crate) cells: Vec, + pub(crate) roots: Vec, +} + +impl RawBagOfCells { + pub(crate) fn parse(serial: &[u8]) -> CellResult { + let mut reader = BinaryReader::new(serial); + + // serialized_boc#b5ee9c72 + let magic = reader.read_u32()?; + + let (has_idx, has_crc32c, _has_cache_bits, size) = match magic { + GENERIC_BOC_MAGIC => { + // has_idx:(## 1) has_crc32c:(## 1) has_cache_bits:(## 1) flags:(## 2) { flags = 0 } + let header = reader.read_u8()?; + let has_idx = (header >> 7) & 1 == 1; + let has_crc32c = (header >> 6) & 1 == 1; + let has_cache_bits = (header >> 5) & 1 == 1; + // size:(## 3) { size <= 4 } + let size = header & 0b0000_0111; + + (has_idx, has_crc32c, has_cache_bits, size) + }, + magic => { + return CellError::err(CellErrorType::BagOfCellsDeserializationError) + .context(format!("Unsupported cell magic number: {:#}", magic)); + }, + }; + // off_bytes:(## 8) { off_bytes <= 8 } + let off_bytes = reader.read_u8()?; + //cells:(##(size * 8)) + let cells = reader.read_var_size(size as usize)?; + if cells > MAX_CELLS { + return CellError::err(CellErrorType::BagOfCellsDeserializationError).context(format!( + "Max number of cells is '{MAX_CELLS}', but given '{cells}' Cells" + )); + } + + // roots:(##(size * 8)) { roots >= 1 } + let roots = reader.read_var_size(size as usize)?; + if roots > MAX_CELLS { + return CellError::err(CellErrorType::BagOfCellsDeserializationError).context(format!( + "Max number of cells is '{MAX_CELLS}', but given '{roots}' root Cells" + )); + } + + // absent:(##(size * 8)) { roots + absent <= cells } + let _absent = reader.read_var_size(size as usize)?; + // tot_cells_size:(##(off_bytes * 8)) + let _tot_cells_size = reader.read_var_size(off_bytes as usize)?; + // root_list:(roots * ##(size * 8)) + let mut root_list = vec![]; + for _ in 0..roots { + root_list.push(reader.read_var_size(size as usize)?) + } + // index:has_idx?(cells * ##(off_bytes * 8)) + let mut index = vec![]; + if has_idx { + for _ in 0..cells { + index.push(reader.read_var_size(off_bytes as usize)?) + } + } + // cell_data:(tot_cells_size * [ uint8 ]) + let mut cell_vec = Vec::with_capacity(cells); + + for _ in 0..cells { + let cell = read_cell(&mut reader, size)?; + cell_vec.push(cell); + } + // crc32c:has_crc32c?uint32 + let _crc32c = if has_crc32c { reader.read_u32()? } else { 0 }; + // TODO: Check crc32 + + Ok(RawBagOfCells { + cells: cell_vec, + roots: root_list, + }) + } + + pub(crate) fn serialize(&self, has_crc32: bool) -> CellResult { + // Based on https://github.com/toncenter/tonweb/blob/c2d5d0fc23d2aec55a0412940ce6e580344a288c/src/boc/Cell.js#L198 + + let root_count = self.roots.len(); + let num_ref_bits = 32 - (self.cells.len() as u32).leading_zeros(); + let num_ref_bytes = (num_ref_bits + 7) / 8; + let has_idx = false; + + let mut full_size = 0u32; + + for cell in &self.cells { + full_size += raw_cell_size(cell, num_ref_bytes); + } + + let num_offset_bits = 32 - full_size.leading_zeros(); + let num_offset_bytes = (num_offset_bits + 7) / 8; + + let total_size = 4 + // magic + 1 + // flags and s_bytes + 1 + // offset_bytes + 3 * num_ref_bytes + // cells_num, roots, complete + num_offset_bytes + // full_size + num_ref_bytes + // root_idx + (if has_idx { self.cells.len() as u32 * num_offset_bytes } else { 0 }) + + full_size + + (if has_crc32 { 4 } else { 0 }); + + let mut writer = BinaryWriter::with_capacity(total_size as usize); + + writer.write(32, GENERIC_BOC_MAGIC)?; + + //write flags byte + let has_cache_bits = false; + let flags: u8 = 0; + writer.write_bit(has_idx)?; + writer.write_bit(has_crc32)?; + writer.write_bit(has_cache_bits)?; + writer.write(2, flags)?; + writer.write(3, num_ref_bytes)?; + writer.write(8, num_offset_bytes)?; + writer.write(8 * num_ref_bytes, self.cells.len() as u32)?; + writer.write(8 * num_ref_bytes, root_count as u32)?; + writer.write(8 * num_ref_bytes, 0)?; // Complete BOCs only + writer.write(8 * num_offset_bytes, full_size)?; + for &root in &self.roots { + writer.write(8 * num_ref_bytes, root as u32)?; + } + + for cell in &self.cells { + write_raw_cell(&mut writer, cell, num_ref_bytes)?; + } + + if has_crc32 { + let bytes = writer.bytes_if_aligned()?; + let cs = CRC_32_ISCSI.checksum(bytes); + writer.write_bytes(cs.to_le_bytes().as_slice())?; + } + writer.align()?; + writer.finish() + } +} + +fn read_cell(reader: &mut BinaryReader, size: u8) -> CellResult { + let d1 = reader.read_u8()?; + let d2 = reader.read_u8()?; + + let ref_num = d1 & 0b111; + let is_exotic = (d1 & 0b1000) != 0; + let has_hashes = (d1 & 0b10000) != 0; + let level_mask = (d1 >> 5) as u32; + let data_size = ((d2 >> 1) + (d2 & 1)).into(); + let full_bytes = (d2 & 0x01) == 0; + + if has_hashes { + let hash_count = LevelMask::new(level_mask).hash_count(); + let skip_size = hash_count * (32 + 2); + + // TODO: check depth and hashes + reader.skip(skip_size as u32)?; + } + + let mut data = reader.read_to_vec(data_size)?; + + let data_len = data.len(); + let padding_len = if data_len > 0 && !full_bytes { + // Fix last byte, + // see https://github.com/toncenter/tonweb/blob/c2d5d0fc23d2aec55a0412940ce6e580344a288c/src/boc/BitString.js#L302 + let num_zeros = data[data_len - 1].trailing_zeros(); + if num_zeros >= 8 { + return CellError::err(CellErrorType::BagOfCellsDeserializationError) + .context("Last byte of binary must not be zero if full_byte flag is not set"); + } + data[data_len - 1] &= !(1 << num_zeros); + num_zeros + 1 + } else { + 0 + }; + let bit_len = data.len() * 8 - padding_len as usize; + let mut references: Vec = Vec::new(); + for _ in 0..ref_num { + references.push(reader.read_var_size(size as usize)?); + } + let cell = RawCell::new(data, bit_len, references, level_mask, is_exotic); + Ok(cell) +} + +fn raw_cell_size(cell: &RawCell, ref_size_bytes: u32) -> u32 { + let data_len = (cell.bit_len + 7) / 8; + 2 + data_len as u32 + cell.references.len() as u32 * ref_size_bytes +} + +fn write_raw_cell( + writer: &mut BinaryWriter, + cell: &RawCell, + ref_size_bytes: u32, +) -> CellResult<()> { + let level = cell.level_mask; + let is_exotic = cell.is_exotic as u32; + let num_refs = cell.references.len() as u32; + let d1 = num_refs + is_exotic * 8 + level * 32; + + let padding_bits = cell.bit_len % 8; + let full_bytes = padding_bits == 0; + let data = cell.data.as_slice(); + let data_len_bytes = (cell.bit_len + 7) / 8; + // data_len_bytes <= 128 by spec, but d2 must be u8 by spec as well + let d2 = (data_len_bytes * 2 - if full_bytes { 0 } else { 1 }) as u8; //subtract 1 if the last byte is not full + + writer.write(8, d1)?; + writer.write(8, d2)?; + if !full_bytes { + writer.write_bytes(&data[..data_len_bytes - 1])?; + let last_byte = data[data_len_bytes - 1]; + let l = last_byte | 1 << (8 - padding_bits - 1); + writer.write(8, l)?; + } else { + writer.write_bytes(data)?; + } + + for r in cell.references.as_slice() { + writer.write(8 * ref_size_bytes, *r as u32)?; + } + + Ok(()) +} diff --git a/rust/frameworks/tw_ton_sdk/src/cell/cell_builder.rs b/rust/frameworks/tw_ton_sdk/src/cell/cell_builder.rs new file mode 100644 index 00000000000..99d394ca169 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/cell/cell_builder.rs @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/builder.rs + +use crate::address::address_data::AddressData; +use crate::boc::binary_writer::BinaryWriter; +use crate::cell::cell_parser::CellParser; +use crate::cell::{Cell, CellArc}; +use crate::error::{CellError, CellErrorType, CellResult}; +use bitstream_io::Numeric; +use std::sync::Arc; +use tw_coin_entry::error::prelude::{MapTWError, ResultContext}; +use tw_number::U256; + +const MAX_CELL_BITS: usize = 1023; +const MAX_CELL_REFERENCES: usize = 4; + +#[derive(Default)] +pub struct CellBuilder { + bit_writer: BinaryWriter, + references: Vec, + is_cell_exotic: bool, +} + +impl CellBuilder { + pub fn new() -> CellBuilder { + CellBuilder::default() + } + + pub fn store_bit(&mut self, val: bool) -> CellResult<&mut Self> { + self.bit_writer.write_bit(val)?; + Ok(self) + } + + pub fn store_byte(&mut self, val: u8) -> CellResult<&mut Self> { + self.store_u8(8, val) + } + + pub fn store_u8(&mut self, bit_len: usize, val: u8) -> CellResult<&mut Self> { + self.store_numeric(bit_len, val) + } + + pub fn store_u32(&mut self, bit_len: usize, val: u32) -> CellResult<&mut Self> { + self.store_numeric(bit_len, val) + } + + pub fn store_i32(&mut self, bit_len: usize, val: i32) -> CellResult<&mut Self> { + self.store_numeric(bit_len, val) + } + + pub fn store_u64(&mut self, bit_len: usize, val: u64) -> CellResult<&mut Self> { + self.store_numeric(bit_len, val) + } + + pub fn store_uint(&mut self, bit_len: usize, val: &U256) -> CellResult<&mut Self> { + if val.bits() > bit_len { + return CellError::err(CellErrorType::CellBuilderError).context(format!( + "Value {val} doesn't fit in {bit_len} bits (takes {} bits)", + val.bits() + )); + } + // example: bit_len=13, val=5. 5 = 00000101, we must store 0000000000101 + // leading_zeros_bits = 10 + // leading_zeros_bytes = 10 / 8 = 1 + let leading_zero_bits = bit_len - val.bits(); + let leading_zeros_bytes = leading_zero_bits / 8; + for _ in 0..leading_zeros_bytes { + self.store_byte(0)?; + } + // we must align high byte of val to specified bit_len, 00101 in our case + let extra_zeros = leading_zero_bits % 8; + for _ in 0..extra_zeros { + self.store_bit(false)?; + } + // and then store val's high byte in minimum number of bits + let val_bytes = val.to_big_endian_compact(); + let high_bits_cnt = { + let cnt = val.bits() % 8; + if cnt == 0 { + 8 + } else { + cnt + } + }; + let high_byte = val_bytes[0]; + for i in 0..high_bits_cnt { + self.store_bit(high_byte & (1 << (high_bits_cnt - i - 1)) != 0)?; + } + // store the rest of val + for byte in val_bytes.iter().skip(1) { + self.store_byte(*byte)?; + } + Ok(self) + } + + pub fn store_slice(&mut self, slice: &[u8]) -> CellResult<&mut Self> { + for val in slice { + self.store_byte(*val)?; + } + Ok(self) + } + + pub fn store_string(&mut self, val: &str) -> CellResult<&mut Self> { + self.store_slice(val.as_bytes()) + } + + pub fn store_coins(&mut self, val: &U256) -> CellResult<&mut Self> { + if val.is_zero() { + self.store_u8(4, 0) + } else { + let num_bytes = (val.bits() + 7) / 8; + self.store_u8(4, num_bytes as u8)?; + self.store_uint(num_bytes * 8, val) + } + } + + /// Stores address without optimizing hole address. + pub fn store_raw_address(&mut self, val: A) -> CellResult<&mut Self> + where + A: AsRef, + { + let val = val.as_ref(); + + self.store_u8(2, 0b10_u8)?; + self.store_bit(false)?; + self.store_u8(8, val.workchain_byte())?; + self.store_slice(val.hash_part.as_slice())?; + Ok(self) + } + + /// Stores address optimizing hole address two to bits + pub fn store_address(&mut self, val: A) -> CellResult<&mut Self> + where + A: AsRef, + { + if val.as_ref() == &AddressData::NULL { + self.store_u8(2, 0)?; + } else { + self.store_raw_address(val)?; + } + Ok(self) + } + + /// Adds reference to an existing `Cell`. + /// + /// The reference is passed as `ArcCell` so it might be references from other cells. + pub fn store_reference(&mut self, cell: &CellArc) -> CellResult<&mut Self> { + let ref_count = self.references.len() + 1; + if ref_count > MAX_CELL_REFERENCES { + return CellError::err(CellErrorType::CellBuilderError).context(format!( + "Cell must contain at most 4 references, got {ref_count}" + )); + } + self.references.push(Arc::clone(cell)); + Ok(self) + } + + pub fn store_references(&mut self, refs: &[CellArc]) -> CellResult<&mut Self> { + for r in refs { + self.store_reference(r)?; + } + Ok(self) + } + + /// Adds a reference to a newly constructed `Cell`. + /// + /// The cell is wrapped it the `Arc`. + pub fn store_child(&mut self, cell: Cell) -> CellResult<&mut Self> { + self.store_reference(&cell.into_arc()) + } + + pub fn store_remaining_bits(&mut self, parser: &mut CellParser) -> CellResult<&mut Self> { + let num_full_bytes = parser.remaining_bits() / 8; + let bytes = parser.load_bytes(num_full_bytes)?; + self.store_slice(bytes.as_slice())?; + let num_bits = parser.remaining_bits() % 8; + let tail = parser.load_u8(num_bits)?; + self.store_u8(num_bits, tail)?; + Ok(self) + } + + pub fn store_cell_data(&mut self, cell: &Cell) -> CellResult<&mut Self> { + let mut parser = cell.parser(); + self.store_remaining_bits(&mut parser)?; + Ok(self) + } + + pub fn store_cell(&mut self, cell: &Cell) -> CellResult<&mut Self> { + self.store_cell_data(cell)?; + self.store_references(cell.references.as_slice())?; + Ok(self) + } + + pub fn build(mut self) -> CellResult { + let trailing_zeros = self.bit_writer.align()?; + + let vec = self + .bit_writer + .finish() + .tw_err(|_| CellErrorType::InternalError) + .context("Stream must be byte-aligned already")?; + + let bit_len = vec.len() * 8 - trailing_zeros; + if bit_len > MAX_CELL_BITS { + return CellError::err(CellErrorType::CellBuilderError).context(format!( + "Cell must contain at most {MAX_CELL_BITS} bits, got {bit_len}", + )); + } + let ref_count = self.references.len(); + if ref_count > MAX_CELL_REFERENCES { + return CellError::err(CellErrorType::CellBuilderError).context(format!( + "Cell must contain at most 4 references, got {ref_count}", + )); + } + + Cell::new(vec, bit_len, self.references.clone(), self.is_cell_exotic) + } + + fn store_numeric(&mut self, bit_len: usize, val: V) -> CellResult<&mut Self> { + self.bit_writer.write(bit_len as u32, val)?; + Ok(self) + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/cell/cell_parser.rs b/rust/frameworks/tw_ton_sdk/src/cell/cell_parser.rs new file mode 100644 index 00000000000..e7143ded0a3 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/cell/cell_parser.rs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/parser.rs + +use crate::address::address_data::AddressData; +use crate::error::{CellError, CellErrorType, CellResult}; +use bitreader::BitReader; +use num_bigint::BigUint; +use tw_coin_entry::error::prelude::{MapTWError, ResultContext}; +use tw_hash::H256; +use tw_memory::Data; +use tw_number::U256; + +pub struct CellParser<'a> { + bit_reader: BitReader<'a>, +} + +impl<'a> CellParser<'a> { + pub fn new(data: &'a [u8], bit_len: usize) -> Self { + CellParser { + bit_reader: BitReader::new(data).relative_reader_atmost(bit_len as u64), + } + } + + pub fn remaining_bits(&self) -> usize { + self.bit_reader.remaining() as usize + } + + pub fn load_bit(&mut self) -> CellResult { + self.bit_reader + .read_bool() + .tw_err(|_| CellErrorType::CellParserError) + } + + pub fn load_u8(&mut self, bit_len: usize) -> CellResult { + self.bit_reader + .read_u8(bit_len as u8) + .tw_err(|_| CellErrorType::CellParserError) + } + + pub fn load_u32(&mut self, bit_len: usize) -> CellResult { + self.bit_reader + .read_u32(bit_len as u8) + .tw_err(|_| CellErrorType::CellParserError) + } + + pub fn load_u64(&mut self, bit_len: usize) -> CellResult { + self.bit_reader + .read_u64(bit_len as u8) + .tw_err(|_| CellErrorType::CellParserError) + } + + pub fn load_uint(&mut self, bit_len: usize) -> CellResult { + let num_words = (bit_len + 31) / 32; + let high_word_bits = if bit_len % 32 == 0 { 32 } else { bit_len % 32 }; + let mut words: Vec = vec![0; num_words]; + let high_word = self.load_u32(high_word_bits)?; + words[num_words - 1] = high_word; + for i in (0..num_words - 1).rev() { + let word = self.load_u32(32)?; + words[i] = word; + } + let big_uint = BigUint::new(words); + let uint = U256::from_big_endian_slice(&big_uint.to_bytes_be()) + .tw_err(|_| CellErrorType::CellParserError) + .context("Expected up to 32 bytes of uint")?; + Ok(uint) + } + + pub fn load_slice(&mut self, slice: &mut [u8]) -> CellResult<()> { + self.bit_reader + .read_u8_slice(slice) + .tw_err(|_| CellErrorType::CellParserError) + } + + pub fn load_bytes(&mut self, num_bytes: usize) -> CellResult { + let mut res = vec![0; num_bytes]; + self.load_slice(res.as_mut_slice())?; + Ok(res) + } + + pub fn load_string(&mut self, num_bytes: usize) -> CellResult { + let bytes = self.load_bytes(num_bytes)?; + String::from_utf8(bytes).tw_err(|_| CellErrorType::CellParserError) + } + + pub fn load_coins(&mut self) -> CellResult { + let num_bytes = self.load_u8(4)?; + if num_bytes == 0 { + Ok(U256::zero()) + } else { + self.load_uint((num_bytes * 8) as usize) + } + } + + pub fn load_address(&mut self) -> CellResult { + let tp = self.load_u8(2)?; + match tp { + 0 => Ok(AddressData::null()), + 2 => { + let _res1 = self.load_u8(1)?; + let wc = self.load_u8(8)?; + let mut hash_part = H256::default(); + self.load_slice(hash_part.as_mut_slice())?; + Ok(AddressData::new(wc as i32, hash_part)) + }, + _ => CellError::err(CellErrorType::CellParserError) + .context(format!("Invalid address type: {tp}")), + } + } + + pub fn ensure_empty(&self) -> CellResult<()> { + let remaining = self.remaining_bits(); + if remaining == 0 { + Ok(()) + } else { + CellError::err(CellErrorType::CellParserError) + .context(format!("{remaining} unread bits left")) + } + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/cell/cell_type.rs b/rust/frameworks/tw_ton_sdk/src/cell/cell_type.rs new file mode 100644 index 00000000000..3effee51a1d --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/cell/cell_type.rs @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/cell_type.rs + +use crate::cell::level_mask::LevelMask; +use crate::cell::{Cell, CellArc}; +use crate::error::{CellError, CellErrorType, CellResult}; +use bitstream_io::{BigEndian, ByteRead, ByteReader}; +use std::io::Cursor; +use tw_coin_entry::error::prelude::*; +use tw_hash::H256; + +struct Pruned { + hash: H256, + depth: u16, +} + +pub(crate) struct HashesAndDepths { + pub hashes: [H256; 4], + pub depths: [u16; 4], +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum CellType { + Ordinary, + PrunedBranch, + Library, + MerkleProof, + MerkleUpdate, +} + +impl CellType { + pub(crate) fn determine_exotic_cell_type(data: &[u8]) -> CellResult { + let Some(type_byte) = data.first() else { + return CellError::err(CellErrorType::InvalidExoticCell) + .context("Not enough data to determine exotic cell type"); + }; + + let cell_type = match type_byte { + 1 => CellType::PrunedBranch, + 2 => CellType::Library, + 3 => CellType::MerkleProof, + 4 => CellType::MerkleUpdate, + cell_type => { + return CellError::err(CellErrorType::InvalidExoticCell).context(format!( + "Invalid first byte in exotic cell data: {cell_type}" + )); + }, + }; + Ok(cell_type) + } + + pub(crate) fn validate( + &self, + _data: &[u8], + _bit_len: usize, + _references: impl AsRef<[CellArc]>, + ) -> CellResult<()> { + // TODO consider implementing data validation according to the cell type. + // match self { + // CellType::Ordinary => Ok(()), + // CellType::PrunedBranch => self.validate_exotic_pruned(data, bit_len, references), + // CellType::Library => self.validate_library(bit_len), + // CellType::MerkleProof => self.validate_merkle_proof(data, bit_len, references), + // CellType::MerkleUpdate => self.validate_merkle_update(data, bit_len, references), + // } + Ok(()) + } + + pub(crate) fn level_mask( + &self, + cell_data: &[u8], + cell_data_bit_len: usize, + references: &[CellArc], + ) -> CellResult { + let ensure_ref_at_least = |at_least_count: usize| { + if references.len() < at_least_count { + return CellError::err(CellErrorType::CellParserError) + .context("Invalid number of Cell references to get level_mask"); + } + Ok(()) + }; + + let result = match self { + CellType::Ordinary => references + .iter() + .fold(LevelMask::new(0), |level_mask, reference| { + level_mask.apply_or(reference.level_mask) + }), + CellType::PrunedBranch => self.pruned_level_mask(cell_data, cell_data_bit_len)?, + CellType::Library => LevelMask::new(0), + CellType::MerkleProof => { + ensure_ref_at_least(1)?; + references[0].level_mask.shift_right() + }, + CellType::MerkleUpdate => { + ensure_ref_at_least(2)?; + references[0] + .level_mask + .apply_or(references[1].level_mask) + .shift_right() + }, + }; + + Ok(result) + } + + pub(crate) fn child_depth(&self, child: &Cell, level: u8) -> u16 { + if matches!(self, CellType::MerkleProof | CellType::MerkleUpdate) { + child.get_depth(level + 1) + } else { + child.get_depth(level) + } + } + + pub(crate) fn resolve_hashes_and_depths( + &self, + hashes: Vec, + depths: Vec, + data: &[u8], + bit_len: usize, + level_mask: LevelMask, + ) -> CellResult { + let mut resolved_hashes = [H256::default(); 4]; + let mut resolved_depths = [0; 4]; + + for i in 0..4 { + let hash_index = level_mask.apply(i).hash_index(); + + let (hash, depth) = if self == &CellType::PrunedBranch { + let this_hash_index = level_mask.hash_index(); + if hash_index != this_hash_index { + let pruned = self.pruned(data, bit_len, level_mask)?; + (pruned[hash_index].hash, pruned[hash_index].depth) + } else { + (hashes[0], depths[0]) + } + } else { + (hashes[hash_index], depths[hash_index]) + }; + + resolved_hashes[i as usize] = hash; + resolved_depths[i as usize] = depth; + } + + Ok(HashesAndDepths { + hashes: resolved_hashes, + depths: resolved_depths, + }) + } + + fn pruned_level_mask(&self, data: &[u8], bit_len: usize) -> CellResult { + if data.len() < 5 { + return CellError::err(CellErrorType::InvalidExoticCell).context(format!( + "Pruned Branch cell date can't be shorter than 5 bytes, got {}", + data.len() + )); + } + + let level_mask = if self.is_config_proof(bit_len) { + LevelMask::new(1) + } else { + let mask_byte = data[1]; + LevelMask::new(mask_byte as u32) + }; + + Ok(level_mask) + } + + fn pruned( + &self, + data: &[u8], + bit_len: usize, + level_mask: LevelMask, + ) -> CellResult> { + type RawCellHash = [u8; H256::LEN]; + + let current_index = if self.is_config_proof(bit_len) { 1 } else { 2 }; + + let cursor = Cursor::new(&data[current_index..]); + let mut reader = ByteReader::endian(cursor, BigEndian); + + let level = level_mask.level() as usize; + let hashes = (0..level) + .map(|_| reader.read::().map(H256::from)) + .collect::, _>>() + .tw_err(|_| CellErrorType::CellBuilderError)?; + let depths = (0..level) + .map(|_| reader.read::()) + .collect::, _>>() + .tw_err(|_| CellErrorType::CellBuilderError)?; + + let result = hashes + .into_iter() + .zip(depths) + .map(|(hash, depth)| Pruned { hash, depth }) + .collect(); + + Ok(result) + } + + /// Special case for config proof + /// This test proof is generated in the moment of voting for a slashing + /// it seems that tools generate it incorrectly and therefore doesn't have mask in it + /// so we need to hardcode it equal to 1 in this case + fn is_config_proof(&self, bit_len: usize) -> bool { + self == &CellType::PrunedBranch && bit_len == 280 + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/cell/level_mask.rs b/rust/frameworks/tw_ton_sdk/src/cell/level_mask.rs new file mode 100644 index 00000000000..18075a49368 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/cell/level_mask.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/level_mask.rs + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct LevelMask { + mask: u32, +} + +impl LevelMask { + pub fn new(new_mask: u32) -> Self { + Self { mask: new_mask } + } + + pub fn mask(&self) -> u32 { + self.mask + } + + pub fn level(&self) -> u8 { + 32 - self.mask.leading_zeros() as u8 + } + + pub fn hash_index(&self) -> usize { + self.mask.count_ones() as usize + } + + pub fn hash_count(&self) -> usize { + self.hash_index() + 1 + } + + pub fn apply(&self, level: u8) -> Self { + LevelMask { + mask: self.mask & ((1u32 << level) - 1), + } + } + + pub fn apply_or(&self, other: Self) -> Self { + LevelMask { + mask: self.mask | other.mask, + } + } + + pub fn shift_right(&self) -> Self { + LevelMask { + mask: self.mask >> 1, + } + } + + pub fn is_significant(&self, level: u8) -> bool { + level == 0 || ((self.mask >> (level - 1)) % 2 != 0) + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/cell/mod.rs b/rust/frameworks/tw_ton_sdk/src/cell/mod.rs new file mode 100644 index 00000000000..c6edaf7adfb --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/cell/mod.rs @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell.rs + +use crate::boc::binary_writer::BinaryWriter; +use crate::cell::cell_parser::CellParser; +use std::fmt; +use std::sync::Arc; +use tw_coin_entry::error::prelude::{MapTWError, OrTWError, ResultContext}; +use tw_encoding::base64::{self, URL_NO_PAD}; +use tw_encoding::hex::ToHex; +use tw_hash::sha2::sha256; +use tw_hash::H256; +use tw_memory::Data; + +pub mod cell_builder; +pub mod cell_parser; +pub mod cell_type; +pub mod level_mask; + +use crate::cell::cell_type::{CellType, HashesAndDepths}; +use crate::cell::level_mask::LevelMask; +use crate::error::{CellError, CellErrorType, CellResult}; + +const MAX_LEVEL: u8 = 3; + +pub type CellArc = Arc; + +#[derive(PartialEq, Eq, Clone, Hash)] +pub struct Cell { + data: Data, + bit_len: usize, + references: Vec, + cell_type: CellType, + level_mask: LevelMask, + hashes: [H256; 4], + depths: [u16; 4], +} + +impl Cell { + pub fn new( + data: Data, + bit_len: usize, + references: Vec, + is_exotic: bool, + ) -> CellResult { + let cell_type = if is_exotic { + CellType::determine_exotic_cell_type(&data)? + } else { + CellType::Ordinary + }; + + cell_type.validate(&data, bit_len, &references)?; + let level_mask = cell_type.level_mask(&data, bit_len, &references)?; + let HashesAndDepths { hashes, depths } = + calculate_hashes_and_depths(cell_type, &data, bit_len, &references, level_mask)?; + + let result = Self { + data, + bit_len, + references, + level_mask, + cell_type, + hashes, + depths, + }; + + Ok(result) + } + + pub fn into_arc(self) -> CellArc { + Arc::new(self) + } + + pub fn data(&self) -> &[u8] { + self.data.as_slice() + } + + pub fn bit_len(&self) -> usize { + self.bit_len + } + + pub fn references(&self) -> &[CellArc] { + &self.references + } + + pub(crate) fn get_level_mask(&self) -> u32 { + self.level_mask.mask() + } + + pub fn is_exotic(&self) -> bool { + self.cell_type != CellType::Ordinary + } + + pub fn cell_hash(&self) -> H256 { + self.get_hash(MAX_LEVEL) + } + + pub fn cell_hash_base64(&self) -> String { + base64::encode(self.cell_hash().as_slice(), URL_NO_PAD) + } + + pub fn get_hash(&self, level: u8) -> H256 { + self.hashes[level.min(3) as usize] + } + + pub fn get_depth(&self, level: u8) -> u16 { + self.depths[level.min(3) as usize] + } + + pub fn parser(&self) -> CellParser { + CellParser::new(&self.data, self.bit_len) + } + + pub fn parse_fully(&self, parse: F) -> Result + where + F: FnOnce(&mut CellParser) -> CellResult, + { + let mut reader = self.parser(); + let res = parse(&mut reader); + reader.ensure_empty()?; + res + } + + fn fmt_debug(&self, f: &mut fmt::Formatter<'_>, depth: usize) -> fmt::Result { + for _ in 0..depth { + write!(f, "\t")?; + } + // Append Cell IDX. + if depth == 0 { + write!(f, "Cell(root)")?; + } else { + write!(f, "-> Cell(ref {depth})")?; + } + + let maybe_exotic_type = if matches!(self.cell_type, CellType::Ordinary) { + "".to_string() + } else { + format!("exotic={:?} ", self.cell_type) + }; + + write!( + f, + " {{ data={}, bit_len={} {maybe_exotic_type}}}", + self.data.to_hex(), + self.bit_len, + )?; + + writeln!(f)?; + let ref_depth = depth + 1; + for reference in self.references() { + reference.fmt_debug(f, ref_depth)?; + } + Ok(()) + } +} + +impl fmt::Debug for Cell { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let root_depth = 0; + self.fmt_debug(f, root_depth) + } +} + +fn get_repr_for_data( + (original_data, original_data_bit_len): (&[u8], usize), + (data, data_bit_len): (&[u8], usize), + refs: &[CellArc], + level_mask: LevelMask, + level: u8, + cell_type: CellType, +) -> CellResult { + // Allocate + let data_len = data.len(); + // descriptors + data + (hash + depth) * refs_count + let buffer_len = 2 + data_len + (32 + 2) * refs.len(); + + let mut writer = BinaryWriter::with_capacity(buffer_len); + let d1 = get_refs_descriptor(cell_type, refs, level_mask.apply(level).mask())?; + let d2 = get_bits_descriptor(original_data, original_data_bit_len)?; + + // Write descriptors + writer.write(8, d1)?; + writer.write(8, d2)?; + // Write main data + writer.write_bits(data, data_bit_len)?; + // Write ref data + write_ref_depths(&mut writer, refs, cell_type, level)?; + write_ref_hashes(&mut writer, refs, cell_type, level)?; + + writer.bytes_if_aligned().map(|b| b.to_vec()) +} + +/// This function replicates unknown logic of resolving cell data +/// https://github.com/ton-blockchain/ton/blob/24dc184a2ea67f9c47042b4104bbb4d82289fac1/crypto/vm/cells/DataCell.cpp#L214 +fn calculate_hashes_and_depths( + cell_type: CellType, + data: &[u8], + bit_len: usize, + references: &[CellArc], + level_mask: LevelMask, +) -> CellResult { + let hash_count = if cell_type == CellType::PrunedBranch { + 1 + } else { + level_mask.hash_count() + }; + + let total_hash_count = level_mask.hash_count(); + let hash_i_offset = total_hash_count - hash_count; + + let mut depths: Vec = Vec::with_capacity(hash_count); + let mut hashes: Vec = Vec::with_capacity(hash_count); + + // Iterate through significant levels + for (hash_i, level_i) in (0..=level_mask.level()) + .filter(|&i| level_mask.is_significant(i)) + .enumerate() + { + if hash_i < hash_i_offset { + continue; + } + + let (current_data, current_bit_len) = if hash_i == hash_i_offset { + (data, bit_len) + } else { + let previous_hash = hashes + .get(hash_i - hash_i_offset - 1) + .or_tw_err(CellErrorType::InternalError) + .context("Can't get right hash")?; + (previous_hash.as_slice(), 256) + }; + + // Calculate Depth + let depth = if references.is_empty() { + 0 + } else { + let max_ref_depth = references.iter().fold(0, |max_depth, reference| { + let child_depth = cell_type.child_depth(reference, level_i); + max_depth.max(child_depth) + }); + + max_ref_depth + .checked_add(1) + .or_tw_err(CellErrorType::CellParserError) + .with_context(|| format!("max_ref_depth is too large: {max_ref_depth}"))? + }; + + // Calculate Hash + let repr = get_repr_for_data( + (data, bit_len), + (current_data, current_bit_len), + references, + level_mask, + level_i, + cell_type, + )?; + let hash = sha256(&repr); + let hash = H256::try_from(hash.as_slice()).expect("Expected 32 bytes hash"); + + depths.push(depth); + hashes.push(hash); + } + + cell_type.resolve_hashes_and_depths(hashes, depths, data, bit_len, level_mask) +} + +/// `references.len() as u8 + 8 * cell_type_var + level_mask as u8 * 32` +fn get_refs_descriptor( + cell_type: CellType, + references: &[CellArc], + level_mask: u32, +) -> CellResult { + let cell_type_var = (cell_type != CellType::Ordinary) as u8; + let references_len: u8 = references + .len() + .try_into() + .tw_err(|_| CellErrorType::CellParserError) + .with_context(|| format!("Got too much Cell references: {}", references.len()))?; + let level_mask_u8: u8 = level_mask + .try_into() + .tw_err(|_| CellErrorType::CellParserError) + .with_context(|| format!("Cell level_mask is too large: {level_mask}"))?; + + level_mask_u8 + .checked_mul(32) + .and_then(|v| v.checked_add(8 * cell_type_var)) + .and_then(|v| v.checked_add(references_len)) + .or_tw_err(CellErrorType::CellParserError) + .context("!get_refs_descriptor") +} + +fn get_bits_descriptor(data: &[u8], bit_len: usize) -> CellResult { + let rest_bits = bit_len % 8; + let full_bytes = rest_bits == 0; + + let double_len = data + .len() + .try_into() + .ok() + .and_then(|len: u8| len.checked_mul(2)) + .or_tw_err(CellErrorType::CellParserError) + .context("!get_bits_descriptor()")?; + + let inverted_full_bytes = !full_bytes as u8; + + // subtract 1 if the last byte is not full + double_len + .checked_sub(inverted_full_bytes) + .or_tw_err(CellErrorType::CellParserError) + .context("!get_bits_descriptor()") +} + +fn write_ref_depths( + writer: &mut BinaryWriter, + refs: &[CellArc], + parent_cell_type: CellType, + level: u8, +) -> CellResult<()> { + for reference in refs { + let child_depth = if matches!( + parent_cell_type, + CellType::MerkleProof | CellType::MerkleUpdate + ) { + reference.get_depth(level + 1) + } else { + reference.get_depth(level) + }; + + writer.write(8, child_depth / 256)?; + writer.write(8, child_depth % 256)?; + } + + Ok(()) +} + +fn write_ref_hashes( + writer: &mut BinaryWriter, + refs: &[CellArc], + parent_cell_type: CellType, + level: u8, +) -> CellResult<()> { + for reference in refs { + let child_hash = if matches!( + parent_cell_type, + CellType::MerkleProof | CellType::MerkleUpdate + ) { + reference.get_hash(level + 1) + } else { + reference.get_hash(level) + }; + + writer.write_bytes(child_hash.as_slice())?; + } + + Ok(()) +} diff --git a/rust/frameworks/tw_ton_sdk/src/crc.rs b/rust/frameworks/tw_ton_sdk/src/crc.rs new file mode 100644 index 00000000000..fcd860b6b0e --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/crc.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crc::Crc; +use lazy_static::lazy_static; + +lazy_static! { + pub static ref CRC_32_ISCSI: Crc = Crc::::new(&crc::CRC_32_ISCSI); + pub static ref CRC_16_XMODEM: Crc = Crc::::new(&crc::CRC_16_XMODEM); +} diff --git a/rust/frameworks/tw_ton_sdk/src/error.rs b/rust/frameworks/tw_ton_sdk/src/error.rs new file mode 100644 index 00000000000..0994ba2a614 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/error.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::error::prelude::*; + +pub type CellResult = Result; +pub type CellError = TWError; + +#[derive(Debug)] +pub enum CellErrorType { + BagOfCellsDeserializationError, + BagOfCellsSerializationError, + CellBuilderError, + CellParserError, + InvalidAddressType, + InvalidExoticCell, + NonEmptyReader, + InternalError, +} + +pub fn cell_to_signing_error(cell_err: CellError) -> SigningError { + cell_err.map_err(|cell_ty| match cell_ty { + CellErrorType::BagOfCellsDeserializationError + | CellErrorType::CellParserError + | CellErrorType::InvalidExoticCell + | CellErrorType::NonEmptyReader => SigningErrorType::Error_input_parse, + CellErrorType::BagOfCellsSerializationError | CellErrorType::CellBuilderError => { + SigningErrorType::Error_internal + }, + CellErrorType::InvalidAddressType => SigningErrorType::Error_invalid_address, + CellErrorType::InternalError => SigningErrorType::Error_internal, + }) +} diff --git a/rust/frameworks/tw_ton_sdk/src/lib.rs b/rust/frameworks/tw_ton_sdk/src/lib.rs new file mode 100644 index 00000000000..7224847dd6b --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/lib.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod boc; +pub mod cell; +pub mod crc; +pub mod error; +pub mod message; diff --git a/rust/frameworks/tw_ton_sdk/src/message/mod.rs b/rust/frameworks/tw_ton_sdk/src/message/mod.rs new file mode 100644 index 00000000000..2558269f38c --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/message/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod state_init; diff --git a/rust/frameworks/tw_ton_sdk/src/message/state_init.rs b/rust/frameworks/tw_ton_sdk/src/message/state_init.rs new file mode 100644 index 00000000000..a0d8a9d1630 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/message/state_init.rs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::cell::cell_builder::CellBuilder; +use crate::cell::{Cell, CellArc}; +use crate::error::CellResult; +use tw_hash::H256; + +#[derive(Default)] +pub struct StateInit { + code: Option, + data: Option, +} + +impl StateInit { + pub fn set_code(mut self, code: CellArc) -> Self { + self.code = Some(code); + self + } + + pub fn set_data(mut self, data: CellArc) -> Self { + self.data = Some(data); + self + } + + pub fn create_account_id(&self) -> CellResult { + Ok(self.to_cell()?.cell_hash()) + } + + pub fn to_cell(&self) -> CellResult { + let split_depth = false; + let tick_tock = false; + let library = false; + + let mut builder = CellBuilder::new(); + builder + .store_bit(split_depth)? + .store_bit(tick_tock)? + .store_bit(self.code.is_some())? + .store_bit(self.data.is_some())? + .store_bit(library)?; + if let Some(ref code) = self.code { + builder.store_reference(code)?; + } + if let Some(ref data) = self.data { + builder.store_reference(data)?; + } + builder.build() + } +} diff --git a/rust/frameworks/tw_ton_sdk/tests/address.rs b/rust/frameworks/tw_ton_sdk/tests/address.rs new file mode 100644 index 00000000000..8e84bf301b8 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/tests/address.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::str::FromStr; +use tw_hash::H256; +use tw_ton_sdk::address::address_data::AddressData; +use tw_ton_sdk::address::raw_address::RawAddress; +use tw_ton_sdk::address::user_friendly_address::UserFriendlyAddress; + +const WORKCHAIN: i32 = 0; +const ADDRESS_BYTES: &str = "e4d954ef9f4e1250a26b5bbad76a1cdd17cfd08babad6f4c23e372270aef6f76"; + +const RAW_ADDRESS: &str = "0:e4d954ef9f4e1250a26b5bbad76a1cdd17cfd08babad6f4c23e372270aef6f76"; +const BOUNCEABLE_URL_ADDRESS: &str = "EQDk2VTvn04SUKJrW7rXahzdF8_Qi6utb0wj43InCu9vdjrR"; +const BOUNCEABLE_ADDRESS: &str = "EQDk2VTvn04SUKJrW7rXahzdF8/Qi6utb0wj43InCu9vdjrR"; + +fn addr_data() -> AddressData { + let bytes = H256::from_str(ADDRESS_BYTES).unwrap(); + AddressData::new(WORKCHAIN, bytes) +} + +#[test] +fn test_raw_address_from_to_string() { + let actual = RawAddress::from_str(RAW_ADDRESS).unwrap(); + let expected = RawAddress::from(addr_data()); + assert_eq!(actual, expected); + let actual_encoded = actual.to_string(); + assert_eq!(actual_encoded, RAW_ADDRESS); +} + +#[test] +fn test_user_friendly_address_from_to_url_string() { + let actual = UserFriendlyAddress::from_base64_url(BOUNCEABLE_URL_ADDRESS).unwrap(); + let expected = UserFriendlyAddress::with_flags(addr_data(), true, false); + assert_eq!(actual, expected); + let actual_encoded = actual.to_base64_url(); + assert_eq!(actual_encoded, BOUNCEABLE_URL_ADDRESS); +} + +#[test] +fn test_user_friendly_address_from_to_std_string() { + let actual = UserFriendlyAddress::from_base64_std(BOUNCEABLE_ADDRESS).unwrap(); + let expected = UserFriendlyAddress::with_flags(addr_data(), true, false); + assert_eq!(actual, expected); + let actual_encoded = actual.to_base64_std(); + assert_eq!(actual_encoded, BOUNCEABLE_ADDRESS); +} diff --git a/rust/frameworks/tw_ton_sdk/tests/boc_encode.rs b/rust/frameworks/tw_ton_sdk/tests/boc_encode.rs new file mode 100644 index 00000000000..6c4a9dcf2ae --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/tests/boc_encode.rs @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use serde::Deserialize; +use serde_json::{json, Value as Json}; +use tw_encoding::hex::{DecodeHex, ToHex}; +use tw_ton_sdk::boc::BagOfCells; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; + +#[derive(Deserialize)] +struct ExpectedCell { + data: String, + bit_len: usize, + references: Vec, +} + +struct TestCase { + input_encoded: &'static str, + expected: Json, + expected_hash: &'static str, + /// Expected encoded can be different from the original [`TestCase::input_encoded`] + /// since same Cell can be BoC encoded differently. + expected_encoded: &'static str, +} + +/// Takes a JSON object that will be deserialized as `ExpectedCell`. +/// It's done to allow the function caller to use `json!` macro for readability. +#[track_caller] +fn test_boc_encode_decode(input: TestCase) { + let boc_decoded = BagOfCells::parse_base64(input.input_encoded).unwrap(); + let root_cell = boc_decoded + .single_root() + .expect("Expected single root Cell"); + + let expected: ExpectedCell = serde_json::from_value(input.expected) + .expect("Error deserializing `ExpectedCell` from JSON"); + assert_eq_cell(root_cell, expected); + + let actual_hash = root_cell.cell_hash_base64(); + assert_eq!(actual_hash, input.expected_hash); + + // Wrap the Cell to the BoC again. + let boc_encoded = BagOfCells::from_root(root_cell.as_ref().clone()) + .to_base64(true) + .unwrap(); + assert_eq!(boc_encoded, input.expected_encoded); +} + +#[track_caller] +fn assert_eq_cell(cell: &Cell, expected: ExpectedCell) { + assert_eq!(cell.data().to_hex(), expected.data, "Invalid Cell.data"); + assert_eq!(cell.bit_len(), expected.bit_len, "Invalid Cell.bit_len"); + for (cell_ref, expected_ref) in cell.references().iter().zip(expected.references) { + assert_eq_cell(cell_ref, expected_ref); + } +} + +#[test] +fn test_boc_encode_jetton_transfer_tx() { + let expected = json!({ + "data": "8800b4510655c8136d4ff5be8ea40a9e161ab0f88321d4969cd828d453f22c7c4b2a08", + "bit_len": 277, + "references": [{ + "data": "688595a2c8b55e7bde026a06f72d3f98f78d52a52eec663bea675b44069578d78338f0e58793370446f6ea491ce97a180de915eb4a3688ea1b37e4c64bbea30529a9a3176a8e07f6000000010003", + "bit_len": 624, + "references": [{ + "data": "620031341f879da9b83ede2949836e1a9fb5ae1c75431117aeb6531a77cf3aae83f3202faf080000000000000000000000000001", + "bit_len": 416, + "references": [{ + "data": "0f8a7ea5000000000000000041dcd65008000b8196730b5e1d033e99c42857e699fa6c827758d8a9489ac210b3bb131d133900168a20cab9026da9feb7d1d48153c2c3561f10643a92d39b051a8a7e458f89654202000000007465737420636f6d6d656e74", + "bit_len": 808, + "references": [] + }] + }] + }] + }); + + // The same Cell can be BoC encoded differently. + // Try to decode the original encoded transaction: https://testnet.tonviewer.com/transaction/12bfe84f947740aec3faa54f04a50690900e3aae9ac9596cfa6804a61a48f429 + test_boc_encode_decode(TestCase { + input_encoded: "te6ccgICAAQAAQAAARgAAAFFiAC0UQZVyBNtT/W+jqQKnhYasPiDIdSWnNgo1FPyLHxLKgwAAQGcaIWVosi1XnveAmoG9y0/mPeNUqUu7GY76mdbRAaVeNeDOPDlh5M3BEb26kkc6XoYDekV60o2iOobN+TGS76jBSmpoxdqjgf2AAAAAQADAAIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEAAwDKD4p+pQAAAAAAAAAAQdzWUAgAC4GWcwteHQM+mcQoV+aZ+myCd1jYqUiawhCzuxMdEzkAFoogyrkCban+t9HUgVPCw1YfEGQ6ktObBRqKfkWPiWVCAgAAAAB0ZXN0IGNvbW1lbnQ=", + expected: expected.clone(), + expected_hash: "yYwgXI3TfZpqtdthYvW503zvoGfeJKdlFUpet6NZ8i8", + expected_encoded: "te6cckECBAEAARUAAUWIALRRBlXIE21P9b6OpAqeFhqw+IMh1Jac2CjUU/IsfEsqDAEBnGiFlaLItV573gJqBvctP5j3jVKlLuxmO+pnW0QGlXjXgzjw5YeTNwRG9upJHOl6GA3pFetKNojqGzfkxku+owUpqaMXao4H9gAAAAEAAwIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEDAMoPin6lAAAAAAAAAABB3NZQCAALgZZzC14dAz6ZxChX5pn6bIJ3WNipSJrCELO7Ex0TOQAWiiDKuQJtqf630dSBU8LDVh8QZDqS05sFGop+RY+JZUICAAAAAHRlc3QgY29tbWVudG/bd5c=", + }); + + // Try to decode with same encoded data. + test_boc_encode_decode(TestCase { + input_encoded: "te6cckECBAEAARUAAUWIALRRBlXIE21P9b6OpAqeFhqw+IMh1Jac2CjUU/IsfEsqDAEBnGiFlaLItV573gJqBvctP5j3jVKlLuxmO+pnW0QGlXjXgzjw5YeTNwRG9upJHOl6GA3pFetKNojqGzfkxku+owUpqaMXao4H9gAAAAEAAwIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEDAMoPin6lAAAAAAAAAABB3NZQCAALgZZzC14dAz6ZxChX5pn6bIJ3WNipSJrCELO7Ex0TOQAWiiDKuQJtqf630dSBU8LDVh8QZDqS05sFGop+RY+JZUICAAAAAHRlc3QgY29tbWVudG/bd5c=", + expected, + expected_hash: "yYwgXI3TfZpqtdthYvW503zvoGfeJKdlFUpet6NZ8i8", + expected_encoded: "te6cckECBAEAARUAAUWIALRRBlXIE21P9b6OpAqeFhqw+IMh1Jac2CjUU/IsfEsqDAEBnGiFlaLItV573gJqBvctP5j3jVKlLuxmO+pnW0QGlXjXgzjw5YeTNwRG9upJHOl6GA3pFetKNojqGzfkxku+owUpqaMXao4H9gAAAAEAAwIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEDAMoPin6lAAAAAAAAAABB3NZQCAALgZZzC14dAz6ZxChX5pn6bIJ3WNipSJrCELO7Ex0TOQAWiiDKuQJtqf630dSBU8LDVh8QZDqS05sFGop+RY+JZUICAAAAAHRlc3QgY29tbWVudG/bd5c=", + }); +} + +#[test] +fn test_wallet_code_hashes() { + let wallet_v3r1_code = BagOfCells::parse_base64("te6cckEBAQEAYgAAwP8AIN0gggFMl7qXMO1E0NcLH+Ck8mCDCNcYINMf0x/TH/gjE7vyY+1E0NMf0x/T/9FRMrryoVFEuvKiBPkBVBBV+RDyo/gAkyDXSpbTB9QC+wDo0QGkyMsfyx/L/8ntVD++buA=").unwrap(); + assert_eq!( + wallet_v3r1_code.single_root().unwrap().cell_hash_base64(), + "thBBpYp5gLlG6PueGY48kE0keZ_6NldOpCUcQaVm9YE" + ); + + let wallet_v3r2_code = BagOfCells::parse_base64("te6cckEBAQEAcQAA3v8AIN0gggFMl7ohggEznLqxn3Gw7UTQ0x/THzHXC//jBOCk8mCDCNcYINMf0x/TH/gjE7vyY+1E0NMf0x/T/9FRMrryoVFEuvKiBPkBVBBV+RDyo/gAkyDXSpbTB9QC+wDo0QGkyMsfyx/L/8ntVBC9ba0=").unwrap(); + assert_eq!( + wallet_v3r2_code.single_root().unwrap().cell_hash_base64(), + "hNr6RJ-Ypph3ibojI1gHK8D3bcRSQAKl0JGLmnXS1Zk" + ); + + let wallet_v4r2_code = BagOfCells::parse_base64("te6cckECFAEAAtQAART/APSkE/S88sgLAQIBIAIDAgFIBAUE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8QERITAubQAdDTAyFxsJJfBOAi10nBIJJfBOAC0x8hghBwbHVnvSKCEGRzdHK9sJJfBeAD+kAwIPpEAcjKB8v/ydDtRNCBAUDXIfQEMFyBAQj0Cm+hMbOSXwfgBdM/yCWCEHBsdWe6kjgw4w0DghBkc3RyupJfBuMNBgcCASAICQB4AfoA9AQw+CdvIjBQCqEhvvLgUIIQcGx1Z4MesXCAGFAEywUmzxZY+gIZ9ADLaRfLH1Jgyz8gyYBA+wAGAIpQBIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UAXKwjiOCEGRzdHKDHrFwgBhQBcsFUAPPFiP6AhPLassfyz/JgED7AJJfA+ICASAKCwBZvSQrb2omhAgKBrkPoCGEcNQICEekk30pkQzmkD6f+YN4EoAbeBAUiYcVnzGEAgFYDA0AEbjJftRNDXCx+AA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIA4PABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AAG7SB/oA1NQi+QAFyMoHFcv/ydB3dIAYyMsFywIizxZQBfoCFMtrEszMyXP7AMhAFIEBCPRR8qcCAHCBAQjXGPoA0z/IVCBHgQEI9FHyp4IQbm90ZXB0gBjIywXLAlAGzxZQBPoCFMtqEssfyz/Jc/sAAgBsgQEI1xj6ANM/MFIkgQEI9Fnyp4IQZHN0cnB0gBjIywXLAlAFzxZQA/oCE8tqyx8Syz/Jc/sAAAr0AMntVGliJeU=").unwrap(); + assert_eq!( + wallet_v4r2_code.single_root().unwrap().cell_hash_base64(), + "_rX_aCDi_w2Ug-fg1iyBfYRniftK5YDIeIZtlZ2r1cA" + ); +} + +#[test] +fn test_boc_encode_cell_builder() { + let leaf = { + let mut builder = CellBuilder::new(); + builder.store_byte(10).unwrap(); + builder.build().unwrap() + }; + let inter = { + let mut builder = CellBuilder::new(); + builder.store_byte(20).unwrap().store_child(leaf).unwrap(); + builder.build().unwrap() + }; + let root = { + let mut builder = CellBuilder::new(); + builder.store_byte(30).unwrap().store_child(inter).unwrap(); + builder.build().unwrap() + }; + + let boc = BagOfCells::from_root(root); + assert_eq!( + boc.to_base64(true).unwrap(), + "te6cckEBAwEACwABAh4BAQIUAgACCjHga8U=" + ); +} + +fn typical_boc_test(boc_base64: &str, expected_hash: &str) { + let boc = BagOfCells::parse_base64(boc_base64).unwrap(); + let root_cell = boc.single_root().unwrap(); + let hash = root_cell.cell_hash(); + assert_eq!(hash.to_string(), expected_hash); + + let boc_base64_again = boc.to_base64(false).unwrap(); + assert_eq!(boc_base64_again, boc_base64); +} + +#[test] +fn test_boc_encode_pruned_block() { + let boc = "te6ccgEBBAEArwAJRgPIr248LcbQSSCsDD5Rb27WLhRGYiTEGG+uChgAAXoNHAAIASJxwAtrH/x8t+GjDO5/X/f1fk4Rw3oYx+9S1gRE8vya04qzwiyFkEMdYglgAAAaNN8fbBluIJfFw9NAAgMoSAEB/rX/aCDi/w2Ug+fg1iyBfYRniftK5YDIeIZtlZ2r1cAAByhIAQEg0z54hgTX/ohMEnHs6qluCydagWgxQoxSyLwK8qfAOQAA"; + let hash = "a6f4b8afa43a9ee61f6d89050d665d164c94c5eca658ddb6c2ab34b4118ab34c"; + typical_boc_test(boc, hash); +} + +/// Checks whether BoC encoding doesn't panic because of invalid input. +#[test] +fn test_boc_decode_invalid() { + #[track_caller] + fn test_invalid(input_hex: &str) { + let input = input_hex.decode_hex().unwrap(); + BagOfCells::parse(&input).unwrap_err(); + } + + test_invalid("b5ee9c725e0000030000000000000000000000000000000000005e"); + + // Errors in `BagOfCells::parse()`. + test_invalid("b5ee9c72c9000001000000000000100000000000000000ff20d1fffe20000052180000001926"); + test_invalid("b5ee9c7201000001000056600000000c000c0cff5e0000005eb5ee9c72ca0c0c0c0c0c0c00"); + test_invalid("b5ee9c72ca0000010000560c0c130c0c0c0c0c0c0c0c000c0c0c5e5e0c0c00b5ee0c5e5e"); + + // Errors in `cell::get_bit_descriptor()`. + test_invalid("b5ee9c72ca0000010000560c0c130c0c0c0c0c0c0c0c000c0c0c5e5e0c0c00b5ee0c5e5e"); + test_invalid("b5ee9c72ca0000230000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000c000c0cffffffffffff0000000000000000000000000000000000000000000600080c"); + + // Error in `cell::get_refs_descriptor()`. + test_invalid("b5ee9c72d1000c0c0c0c20260cba5e0900002a2600000000000000090909090909090909090909090909090909090909091f1f1f1f090909090909090909090971ee31310909090909090909090200000900090909090901680909090909090909090909090909090909090909090000000000000000000000000c88f3"); + // Errors in `CellType::level_mask()`. + test_invalid("b5ee9c72ca0000180000250125000000000000000b0b0b0b0b0b0404040404040404030404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040408080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808040404040c04040404040404040404040404040404040404040404040404040404040404040404040404270404040404040404040404040404040404040400005204040404040404040404000404040404040404040404040404040403fb04040404040404040404040404040404040404040404040400002501250b4b0b0800ca00250c00000c000c100c0c0c26"); + // Error in `cell::calculate_hashes_and_depths()`. + test_invalid("b5ee9c72d1000a000000000000000008860101ff041cffff000100000000000010081c01000000000000000000000000000000000000b5ee00000000ff9c72d1000a0000000000000000000000ac0000000006060606060606060606060606000008d60104ff031cff530000002e0000080000000000000000b0504f4f4ab0b0b0b0b0b0b0b0b00f00b00500000f0000000000030053a900002f00000000000000feffffffff0000000000009ce4ee6100000000000000000000000000000886fc00ff041cffff00000000000063000000000000eeee9c72069c720606060000060600"); +} diff --git a/rust/frameworks/tw_ton_sdk/tests/cell.rs b/rust/frameworks/tw_ton_sdk/tests/cell.rs new file mode 100644 index 00000000000..0baf948a31b --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/tests/cell.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_ton_sdk::boc::BagOfCells; + +#[test] +fn test_cell_format() { + let boc = BagOfCells::parse_base64( + "te6ccgEBAgEALQABDv8AiNDtHtgBCEIC5wowbAAnJ5YkP1ac4Mko6kz8nxtlxbAGbjghWfXoDfU=", + ) + .unwrap(); + let cell = boc.single_root().unwrap(); + + let actual_fmt = format!("{cell:?}"); + let expected = +"Cell(root) { data=ff0088d0ed1ed8, bit_len=56 } + -> Cell(ref 1) { data=02e70a306c00272796243f569ce0c928ea4cfc9f1b65c5b0066e382159f5e80df5, bit_len=264 exotic=Library } +"; + assert_eq!(actual_fmt, expected); +} diff --git a/rust/frameworks/tw_ton_sdk/tests/cell_parser.rs b/rust/frameworks/tw_ton_sdk/tests/cell_parser.rs new file mode 100644 index 00000000000..445e3842a3d --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/tests/cell_parser.rs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_number::U256; +use tw_ton_sdk::address::address_data::AddressData; +use tw_ton_sdk::address::user_friendly_address::UserFriendlyAddress; +use tw_ton_sdk::boc::BagOfCells; + +/// In this test we parse a TON internal transfer message encoded as BoC. +#[test] +fn test_cell_parse_internal_transfer_message() { + let boc = BagOfCells::parse_base64( + "te6cckEBAQEAOgAAcGIARUMTww0u7LZO2ecEA9iRbS9qOcmmc6tFLaWOGw5dlFdEAAAAAAAAAAAAAAAAAAAAAAAAAAAAfRXk0w==", + ) + .unwrap(); + let cell = boc.single_root().unwrap(); + let mut parser = cell.parser(); + + let bit_0 = parser.load_bit().unwrap(); + let ihr_disabled = parser.load_bit().unwrap(); + let bounce = parser.load_bit().unwrap(); + let bounced = parser.load_bit().unwrap(); + let src_addr = parser.load_address().unwrap(); + let dest_addr = parser.load_address().unwrap(); + let value = parser.load_coins().unwrap(); + let currency_collections = parser.load_bit().unwrap(); + let ihr_fees = parser.load_coins().unwrap(); + let fwd_fees = parser.load_coins().unwrap(); + let created_lt = parser.load_u64(64).unwrap(); + let created_at = parser.load_u32(32).unwrap(); + let contains_state_init = parser.load_bit().unwrap(); + let contains_data = parser.load_bit().unwrap(); + + parser.ensure_empty().expect("Must be read fully"); + + let expected_dest_addr = + UserFriendlyAddress::from_base64_url("EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorkdl") + .unwrap(); + let expected_value = U256::from(9_223_372_036_854_775_808_u64); + + assert_eq!(bit_0, false); + assert_eq!(ihr_disabled, true); + assert_eq!(bounce, true); + assert_eq!(bounced, false); + assert_eq!(src_addr, AddressData::NULL); + assert_eq!(dest_addr, expected_dest_addr.into_data()); + assert_eq!(value, expected_value); + assert_eq!(currency_collections, false); + assert_eq!(ihr_fees, U256::ZERO); + assert_eq!(fwd_fees, U256::ZERO); + assert_eq!(created_lt, 0); + assert_eq!(created_at, 0); + assert_eq!(contains_state_init, false); + assert_eq!(contains_data, false); +} diff --git a/rust/tw_any_coin/Cargo.toml b/rust/tw_any_coin/Cargo.toml index e64c9506620..a0eb245c67e 100644 --- a/rust/tw_any_coin/Cargo.toml +++ b/rust/tw_any_coin/Cargo.toml @@ -25,9 +25,10 @@ test-utils = [ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tw_any_coin = { path = "./", features = ["test-utils"] } -tw_utxo = { path = "../frameworks/tw_utxo" } tw_cosmos_sdk = { path = "../tw_cosmos_sdk", features = ["test-utils"] } tw_keypair = { path = "../tw_keypair", features = ["test-utils"] } tw_memory = { path = "../tw_memory", features = ["test-utils"] } tw_misc = { path = "../tw_misc", features = ["test-utils"] } tw_number = { path = "../tw_number" } +tw_ton_sdk = { path = "../frameworks/tw_ton_sdk" } +tw_utxo = { path = "../frameworks/tw_utxo" } diff --git a/rust/tw_any_coin/src/test_utils/address_utils.rs b/rust/tw_any_coin/src/test_utils/address_utils.rs index 5775f6551de..528dce6092a 100644 --- a/rust/tw_any_coin/src/test_utils/address_utils.rs +++ b/rust/tw_any_coin/src/test_utils/address_utils.rs @@ -4,11 +4,14 @@ use crate::ffi::tw_any_address::{ tw_any_address_create_base58_with_public_key, tw_any_address_create_bech32_with_public_key, - tw_any_address_create_with_string, tw_any_address_data, tw_any_address_delete, - tw_any_address_description, tw_any_address_is_valid, tw_any_address_is_valid_base58, - tw_any_address_is_valid_bech32, TWAnyAddress, + tw_any_address_create_with_public_key_derivation, tw_any_address_create_with_string, + tw_any_address_data, tw_any_address_delete, tw_any_address_description, + tw_any_address_is_valid, tw_any_address_is_valid_base58, tw_any_address_is_valid_bech32, + TWAnyAddress, }; use tw_coin_registry::coin_type::CoinType; +use tw_coin_registry::registry::get_coin_item; +use tw_coin_registry::tw_derivation::TWDerivation; use tw_encoding::hex::{DecodeHex, ToHex}; use tw_keypair::ffi::privkey::tw_private_key_get_public_key_by_type; use tw_keypair::test_utils::tw_private_key_helper::TWPrivateKeyHelper; @@ -26,6 +29,26 @@ impl WithDestructor for TWAnyAddress { } } +pub fn test_address_derive(coin: CoinType, private_key: &str, address: &str) { + let coin_item = get_coin_item(coin).unwrap(); + + let private_key = TWPrivateKeyHelper::with_hex(private_key); + let public_key = TWPublicKeyHelper::wrap(unsafe { + tw_private_key_get_public_key_by_type(private_key.ptr(), coin_item.public_key_type as u32) + }); + + let any_address = TWAnyAddressHelper::wrap(unsafe { + tw_any_address_create_with_public_key_derivation( + public_key.ptr(), + coin as u32, + TWDerivation::Default as u32, + ) + }); + + let actual = TWStringHelper::wrap(unsafe { tw_any_address_description(any_address.ptr()) }); + assert_eq!(actual.to_string().unwrap(), address); +} + pub fn test_address_normalization(coin: CoinType, denormalized: &str, normalized: &str) { let expected = normalized; let denormalized = TWStringHelper::create(denormalized); @@ -40,13 +63,21 @@ pub fn test_address_normalization(coin: CoinType, denormalized: &str, normalized } pub fn test_address_valid(coin: CoinType, address: &str) { - let address = TWStringHelper::create(address); - assert!(unsafe { tw_any_address_is_valid(address.ptr(), coin as u32) }); + let addr = TWStringHelper::create(address); + assert!( + unsafe { tw_any_address_is_valid(addr.ptr(), coin as u32) }, + "'{}' expected to be valid", + address + ); } pub fn test_address_invalid(coin: CoinType, address: &str) { - let address = TWStringHelper::create(address); - assert!(!unsafe { tw_any_address_is_valid(address.ptr(), coin as u32) }); + let addr = TWStringHelper::create(address); + assert!( + !unsafe { tw_any_address_is_valid(addr.ptr(), coin as u32) }, + "'{}' expected to be invalid", + address + ); } pub fn test_address_get_data(coin: CoinType, address: &str, data_hex: &str) { diff --git a/rust/tw_any_coin/tests/chains/mod.rs b/rust/tw_any_coin/tests/chains/mod.rs index f98011f9492..0f587a6d26a 100644 --- a/rust/tw_any_coin/tests/chains/mod.rs +++ b/rust/tw_any_coin/tests/chains/mod.rs @@ -17,4 +17,5 @@ mod solana; mod sui; mod tbinance; mod thorchain; +mod ton; mod zetachain; diff --git a/rust/tw_any_coin/tests/chains/solana/solana_transaction.rs b/rust/tw_any_coin/tests/chains/solana/solana_transaction.rs index df10f8dc8c4..27b0ab7e91e 100644 --- a/rust/tw_any_coin/tests/chains/solana/solana_transaction.rs +++ b/rust/tw_any_coin/tests/chains/solana/solana_transaction.rs @@ -6,8 +6,8 @@ use std::borrow::Cow; use tw_any_coin::test_utils::sign_utils::{AnySignerHelper, CompilerHelper, PreImageHelper}; use tw_any_coin::test_utils::transaction_decode_utils::TransactionDecoderHelper; use tw_coin_registry::coin_type::CoinType; -use tw_encoding::base58::Alphabet; -use tw_encoding::{base58, base64}; +use tw_encoding::base58::{self, Alphabet}; +use tw_encoding::base64::{self, STANDARD}; use tw_proto::Common::Proto::SigningError; use tw_proto::Solana::Proto; use tw_proto::Solana::Proto::mod_RawMessage as raw_message; @@ -55,7 +55,7 @@ const ENCODED_UNSIGNED_MSG: &str = "AgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9f #[test] fn test_solana_decode_transaction() { - let encoded_tx = base64::decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHEIoR5xuWyrvjIW4xU7CWlPOfyFAiy8B295hGo6tNjBmRCgUkQaFYTleMcAX2p74eBXQZd1dwDyQZAPJfSv2KGc5kcFLJj5qd2BVMaSNGVPfVBm74GbLwUq5/U1Ccdqc2gokZQxRDpMq7aeToP3nRaWIP4RXMxN+LJetccXMPq/QumgOqt7kkqk07cyPCKgYoQ4fQtOqqZn5sEqjWHYj3CDS5ha48uggePWu090s1ff4yoCjAvULeZ+cqYFn+Adk5Teyfw71W3u/F6VTnLQEPW96gJr5Kcm3bGi08n224JyF++PTko52VL0CIM2xtl0WkvNslD6Wawxr7yd9HYllN4Lz8lFwXilWGgyJdOq1qqBuZbE49glHeCO/sJHNnIHC0BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+Fm0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6OL4d+g9rsaIj0Orta57MRu3jDSWCJf85ae4LBbiD/GXvOojZjsHekJrpRUuPggLJr943hDVD5UareeEucjCvaoHCgAFAsBcFQAKAAkDBBcBAAAAAAANBgAGACMJDAEBCQIABgwCAAAAAMqaOwAAAAAMAQYBEQs1DA8ABgEFAiMhCwsOCx0MDxoBGQcYBAgDJBscDB4PBwUQEhEfFR8UFwcFISITHw8MDCAfFgstwSCbM0HWnIEAAwAAABEBZAABCh0BAyZHAQMAypo7AAAAAJaWFAYAAAAAMgAADAMGAAABCQPZoILFk7gfE2y5bt3AC+g/4OwNzdiHKBhIbdeYvYFEjQPKyMkExMUkx0R25UNa/g5KsG0vfUwdUJ8e8HecK/Jkd3qm9XefBOB0BaD1+J+dBJz09vfyGuRYZH09HfdE/kL8v6Ql+H03+tO+9lMmmVg8O1c6gAN6eX0Cbn4=", false).unwrap(); + let encoded_tx = base64::decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHEIoR5xuWyrvjIW4xU7CWlPOfyFAiy8B295hGo6tNjBmRCgUkQaFYTleMcAX2p74eBXQZd1dwDyQZAPJfSv2KGc5kcFLJj5qd2BVMaSNGVPfVBm74GbLwUq5/U1Ccdqc2gokZQxRDpMq7aeToP3nRaWIP4RXMxN+LJetccXMPq/QumgOqt7kkqk07cyPCKgYoQ4fQtOqqZn5sEqjWHYj3CDS5ha48uggePWu090s1ff4yoCjAvULeZ+cqYFn+Adk5Teyfw71W3u/F6VTnLQEPW96gJr5Kcm3bGi08n224JyF++PTko52VL0CIM2xtl0WkvNslD6Wawxr7yd9HYllN4Lz8lFwXilWGgyJdOq1qqBuZbE49glHeCO/sJHNnIHC0BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+Fm0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6OL4d+g9rsaIj0Orta57MRu3jDSWCJf85ae4LBbiD/GXvOojZjsHekJrpRUuPggLJr943hDVD5UareeEucjCvaoHCgAFAsBcFQAKAAkDBBcBAAAAAAANBgAGACMJDAEBCQIABgwCAAAAAMqaOwAAAAAMAQYBEQs1DA8ABgEFAiMhCwsOCx0MDxoBGQcYBAgDJBscDB4PBwUQEhEfFR8UFwcFISITHw8MDCAfFgstwSCbM0HWnIEAAwAAABEBZAABCh0BAyZHAQMAypo7AAAAAJaWFAYAAAAAMgAADAMGAAABCQPZoILFk7gfE2y5bt3AC+g/4OwNzdiHKBhIbdeYvYFEjQPKyMkExMUkx0R25UNa/g5KsG0vfUwdUJ8e8HecK/Jkd3qm9XefBOB0BaD1+J+dBJz09vfyGuRYZH09HfdE/kL8v6Ql+H03+tO+9lMmmVg8O1c6gAN6eX0Cbn4=", STANDARD).unwrap(); // Step 1: Decode the transaction. @@ -162,7 +162,7 @@ fn test_solana_decode_transaction() { #[test] fn test_solana_decode_transaction_update_blockhash_and_sign_with_external_fee_payer() { // https://explorer.solana.com/tx/3KbvREZUat76wgWMtnJfWbJL74Vzh4U2eabVJa3Z3bb2fPtW8AREP5pbmRwUrxZCESbTomWpL41PeKDcPGbojsej?cluster=devnet - let encoded_tx = base64::decode(PREV_ENCODED_TX, false).unwrap(); + let encoded_tx = base64::decode(PREV_ENCODED_TX, STANDARD).unwrap(); // Step 1: Decode the transaction. let mut decoder = TransactionDecoderHelper::::default(); @@ -210,7 +210,7 @@ fn test_solana_decode_transaction_update_blockhash_and_sign_with_external_fee_pa #[test] fn test_solana_decode_transaction_update_blockhash_and_sign_with_external_fee_payer_signature() { // https://explorer.solana.com/tx/3KbvREZUat76wgWMtnJfWbJL74Vzh4U2eabVJa3Z3bb2fPtW8AREP5pbmRwUrxZCESbTomWpL41PeKDcPGbojsej?cluster=devnet - let encoded_tx = base64::decode(PREV_ENCODED_TX, false).unwrap(); + let encoded_tx = base64::decode(PREV_ENCODED_TX, STANDARD).unwrap(); // Step 1: Decode the transaction. let mut decoder = TransactionDecoderHelper::::default(); @@ -257,7 +257,7 @@ fn test_solana_decode_transaction_update_blockhash_and_sign_with_external_fee_pa #[test] fn test_solana_decode_transaction_update_blockhash_and_sign_no_matching_pubkey() { - let encoded_tx = base64::decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHEIoR5xuWyrvjIW4xU7CWlPOfyFAiy8B295hGo6tNjBmRCgUkQaFYTleMcAX2p74eBXQZd1dwDyQZAPJfSv2KGc5kcFLJj5qd2BVMaSNGVPfVBm74GbLwUq5/U1Ccdqc2gokZQxRDpMq7aeToP3nRaWIP4RXMxN+LJetccXMPq/QumgOqt7kkqk07cyPCKgYoQ4fQtOqqZn5sEqjWHYj3CDS5ha48uggePWu090s1ff4yoCjAvULeZ+cqYFn+Adk5Teyfw71W3u/F6VTnLQEPW96gJr5Kcm3bGi08n224JyF++PTko52VL0CIM2xtl0WkvNslD6Wawxr7yd9HYllN4Lz8lFwXilWGgyJdOq1qqBuZbE49glHeCO/sJHNnIHC0BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+Fm0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6OL4d+g9rsaIj0Orta57MRu3jDSWCJf85ae4LBbiD/GXvOojZjsHekJrpRUuPggLJr943hDVD5UareeEucjCvaoHCgAFAsBcFQAKAAkDBBcBAAAAAAANBgAGACMJDAEBCQIABgwCAAAAAMqaOwAAAAAMAQYBEQs1DA8ABgEFAiMhCwsOCx0MDxoBGQcYBAgDJBscDB4PBwUQEhEfFR8UFwcFISITHw8MDCAfFgstwSCbM0HWnIEAAwAAABEBZAABCh0BAyZHAQMAypo7AAAAAJaWFAYAAAAAMgAADAMGAAABCQPZoILFk7gfE2y5bt3AC+g/4OwNzdiHKBhIbdeYvYFEjQPKyMkExMUkx0R25UNa/g5KsG0vfUwdUJ8e8HecK/Jkd3qm9XefBOB0BaD1+J+dBJz09vfyGuRYZH09HfdE/kL8v6Ql+H03+tO+9lMmmVg8O1c6gAN6eX0Cbn4=", false).unwrap(); + let encoded_tx = base64::decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHEIoR5xuWyrvjIW4xU7CWlPOfyFAiy8B295hGo6tNjBmRCgUkQaFYTleMcAX2p74eBXQZd1dwDyQZAPJfSv2KGc5kcFLJj5qd2BVMaSNGVPfVBm74GbLwUq5/U1Ccdqc2gokZQxRDpMq7aeToP3nRaWIP4RXMxN+LJetccXMPq/QumgOqt7kkqk07cyPCKgYoQ4fQtOqqZn5sEqjWHYj3CDS5ha48uggePWu090s1ff4yoCjAvULeZ+cqYFn+Adk5Teyfw71W3u/F6VTnLQEPW96gJr5Kcm3bGi08n224JyF++PTko52VL0CIM2xtl0WkvNslD6Wawxr7yd9HYllN4Lz8lFwXilWGgyJdOq1qqBuZbE49glHeCO/sJHNnIHC0BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+Fm0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6OL4d+g9rsaIj0Orta57MRu3jDSWCJf85ae4LBbiD/GXvOojZjsHekJrpRUuPggLJr943hDVD5UareeEucjCvaoHCgAFAsBcFQAKAAkDBBcBAAAAAAANBgAGACMJDAEBCQIABgwCAAAAAMqaOwAAAAAMAQYBEQs1DA8ABgEFAiMhCwsOCx0MDxoBGQcYBAgDJBscDB4PBwUQEhEfFR8UFwcFISITHw8MDCAfFgstwSCbM0HWnIEAAwAAABEBZAABCh0BAyZHAQMAypo7AAAAAJaWFAYAAAAAMgAADAMGAAABCQPZoILFk7gfE2y5bt3AC+g/4OwNzdiHKBhIbdeYvYFEjQPKyMkExMUkx0R25UNa/g5KsG0vfUwdUJ8e8HecK/Jkd3qm9XefBOB0BaD1+J+dBJz09vfyGuRYZH09HfdE/kL8v6Ql+H03+tO+9lMmmVg8O1c6gAN6eX0Cbn4=", STANDARD).unwrap(); // Step 1: Decode the transaction. let mut decoder = TransactionDecoderHelper::::default(); @@ -287,7 +287,7 @@ fn test_solana_decode_transaction_update_blockhash_and_sign_no_matching_pubkey() #[test] fn test_solana_decode_transaction_update_blockhash_preimage_hash_and_compile() { // https://explorer.solana.com/tx/3KbvREZUat76wgWMtnJfWbJL74Vzh4U2eabVJa3Z3bb2fPtW8AREP5pbmRwUrxZCESbTomWpL41PeKDcPGbojsej?cluster=devnet - let encoded_tx = base64::decode(PREV_ENCODED_TX, false).unwrap(); + let encoded_tx = base64::decode(PREV_ENCODED_TX, STANDARD).unwrap(); // Step 1: Decode the transaction. let mut decoder = TransactionDecoderHelper::::default(); @@ -311,7 +311,7 @@ fn test_solana_decode_transaction_update_blockhash_preimage_hash_and_compile() { let preimage_output = preimager.pre_image_hashes(CoinType::Solana, &input); assert_eq!(preimage_output.error, SigningError::OK); - let expected_unsigned_msg = base64::decode(ENCODED_UNSIGNED_MSG, false).unwrap(); + let expected_unsigned_msg = base64::decode(ENCODED_UNSIGNED_MSG, STANDARD).unwrap(); assert_eq!(preimage_output.data, expected_unsigned_msg); // Step 4. Compile the updated transaction with signatures. diff --git a/rust/tw_any_coin/tests/chains/ton/cell_example.rs b/rust/tw_any_coin/tests/chains/ton/cell_example.rs new file mode 100644 index 00000000000..55c2248c939 --- /dev/null +++ b/rust/tw_any_coin/tests/chains/ton/cell_example.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::sync::Arc; +use tw_ton_sdk::address::address_data::AddressData; +use tw_ton_sdk::address::raw_address::RawAddress; +use tw_ton_sdk::boc::BagOfCells; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::message::state_init::StateInit; + +pub fn comment_cell(comment: &str) -> String { + let mut builder = CellBuilder::new(); + builder + .store_u32(32, 0) + .unwrap() + .store_string(comment) + .unwrap(); + let cell = builder.build().unwrap(); + BagOfCells::from_root(cell).to_base64(true).unwrap() +} + +/// Doge chatbot example +/// https://docs.ton.org/develop/dapps/ton-connect/transactions#smart-contract-deployment +pub fn doge_chatbot_state_init(current_timestamp: u32) -> String { + let current_timestamp_ms = current_timestamp as u64 * 1000; + + let doge_chatbot_boc = BagOfCells::parse_base64("te6cckEBAgEARAABFP8A9KQT9LzyyAsBAGrTMAGCCGlJILmRMODQ0wMx+kAwi0ZG9nZYcCCAGMjLBVAEzxaARfoCE8tqEssfAc8WyXP7AN4uuM8=").unwrap(); + let doge_chatbot_cell = doge_chatbot_boc.single_root().unwrap(); + + let mut data_builder = CellBuilder::new(); + // Current timestamp makes this Doge chatbot unique across other bots with the same contract code. + data_builder.store_u64(64, current_timestamp_ms).unwrap(); + let data = data_builder.build().unwrap(); + + let state_init_cell = StateInit::default() + .set_code(Arc::clone(doge_chatbot_cell)) + .set_data(data.into_arc()) + .to_cell() + .unwrap(); + + BagOfCells::from_root(state_init_cell) + .to_base64(true) + .unwrap() +} + +pub fn contract_address_from_state_init(state_init: &str) -> String { + let state_init_boc = BagOfCells::parse_base64(state_init).unwrap(); + let state_init_cell = state_init_boc.single_root().unwrap(); + let state_init_hash = state_init_cell.cell_hash(); + RawAddress::from(AddressData::new(0, state_init_hash)).to_string() +} diff --git a/rust/tw_any_coin/tests/chains/ton/mod.rs b/rust/tw_any_coin/tests/chains/ton/mod.rs new file mode 100644 index 00000000000..1a5d122cbdf --- /dev/null +++ b/rust/tw_any_coin/tests/chains/ton/mod.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +mod cell_example; +mod ton_address; +mod ton_compile; +mod ton_sign; diff --git a/rust/tw_any_coin/tests/chains/ton/ton_address.rs b/rust/tw_any_coin/tests/chains/ton/ton_address.rs new file mode 100644 index 00000000000..b6a388f499f --- /dev/null +++ b/rust/tw_any_coin/tests/chains/ton/ton_address.rs @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::test_utils::address_utils::{ + test_address_derive, test_address_get_data, test_address_invalid, test_address_normalization, + test_address_valid, +}; +use tw_coin_registry::coin_type::CoinType; + +const WALLET_1_DATA: &str = "8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae"; +const WALLET_1_RAW: &str = "0:8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae"; +const WALLET_1_BOUNCEABLE: &str = "EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorkdl"; +const WALLET_1_NON_BOUNCEABLE: &str = "UQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorhqg"; +const WALLET_1_BOUNCEABLE_TESTNET: &str = "kQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorvzv"; +const WALLET_1_NON_BOUNCEABLE_TESTNET: &str = "0QCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorqEq"; + +#[test] +fn test_ton_address_derive() { + test_address_derive( + CoinType::TON, + "5849481021e305dfdf9f0eaf87e07f15efec3fde8d8ed639c9fcf0bc351d998b", + "UQAACKJfEIfI5vkht_w3NYk8k-OU5Xl_jq9XNmmkcPaUO-tB", + ); + test_address_derive( + CoinType::TON, + "4a22d994755145e4a4ce7263bdb3a8a70e449c1fccdd299f80df862bd4dcb930", + "UQA3fRa_AHKBo1Lu8QF5xm_fiCLi197NfoPbeta0VJyvKa76", + ); + test_address_derive( + CoinType::TON, + "287d4c0fcc445173fe211e4ade3518c75cff7a9dd79070f54395058cdd53e485", + "UQCbJ0QdlDmC0ofyCtH15PHBzrj9sMLoLjNjkgTYaxR8gn9z", + ); + test_address_derive( + CoinType::TON, + "136e464280c4222a99ac34d5077a2edf11e4468a076741e495d9f31ca7939a1f", + "UQAliC8yJh-Ru2uwEWgEaEV9LHEs0-c1blYr_XZe7CpEwm9D", + ); + test_address_derive( + CoinType::TON, + "f15bb09a2cf37f6e6b6515be4000cdf271338c56fb1ec81848f2f1407b3a4003", + "UQCeAQaIFwwjmcJkYfqGiyHo2ag7qUMMfsUi28HLWmtpA7zF", + ); + test_address_derive( + CoinType::TON, + "532005268411b3b4ac85b080c8a3bc4a52600be75f758013302745ac05ac18f0", + "UQDD0YS5pQSe3fgHEKd-D7qTieRxmSknlQQW6fb7IFu7ky9T", + ); + test_address_derive( + CoinType::TON, + "15e5a13ec259bb4515105ba0a84ee93eaa9f56f6fdf73bf6179d1ed80b6a399c", + "UQAx8JmUT4p14RUAu9gpXqmTzkQz4e3GZz8VQjqWXFDxG6-S", + ); + test_address_derive( + CoinType::TON, + "9b503ff85debe95093acf0f9b057607a0a5be91cb47e2e6ec342d7825c7fafbc", + "UQD91HEk-TJVublA57dgkSwgrRORj48ubEIfjEPZIjQ08oZl", + ); + test_address_derive( + CoinType::TON, + "97075969876382280ff7598738b3fd2c1748f9a549dd6f5d6aa5694c21deddce", + "UQBrL2lNG3ThmYbf9gaA_-tsPfdrcGy27LP0M-qg-1TpG_wR", + ); + test_address_derive( + CoinType::TON, + "57d86027989f8ec649cce3be862d68564d471395c5694918b0348e17c7ef6ffe", + "UQDUJLg8MYZPT2C-2n-ulyNSkBsdDaUzqEq71dBx3l6fhnuL", + ); + test_address_derive( + CoinType::TON, + "b1ad2bff14fc018493c32c37cded62892ec507471e34a25da0b5b5f05e131751", + "UQD079e5CETiOrR_iS0atJukl7ixS6EYbtaWRSGykFAtwRhL", + ); + test_address_derive( + CoinType::TON, + "16ad201c59ecd7ace74e1677160106923a3d2ee11c495be5c3b88ba6f7ef3d17", + "UQDuU7NOk_eGzP5CLs_Zeg0LBpySAGy02qqGv0cO_zX5WN1n", + ); + test_address_derive( + CoinType::TON, + "3d935b7a8c24e7dc55ef7c0c890806cee3af1174a62165d4d2fb64ccf2e2260b", + "UQALRogl66QrJIb3KbStOd-ZadA6Ye2g23ME6JAMU1HOXRG2", + ); + test_address_derive( + CoinType::TON, + "68f3a87d12514854774300b8a4c449616e208336f1b609c96fcd8b1a87d4e064", + "UQALrF1c2UeoCybOsSeAdUmip4yhcCtrUAhKZ-9bxv6_okVx", + ); + test_address_derive( + CoinType::TON, + "fbfbc640c4cd4649161a935562217f1caecf6e7f3a2818921f9ee336741a48cb", + "UQAzCS7JoSiOi1BdH4nFkuvUwbBjxUzPx1AhQKwiwXAv27Xs", + ); +} + +#[test] +fn test_ton_address_normalization() { + test_address_normalization(CoinType::TON, WALLET_1_RAW, WALLET_1_NON_BOUNCEABLE); + test_address_normalization(CoinType::TON, WALLET_1_BOUNCEABLE, WALLET_1_NON_BOUNCEABLE); + test_address_normalization( + CoinType::TON, + WALLET_1_NON_BOUNCEABLE, + WALLET_1_NON_BOUNCEABLE, + ); + test_address_normalization( + CoinType::TON, + WALLET_1_BOUNCEABLE_TESTNET, + WALLET_1_NON_BOUNCEABLE, + ); + test_address_normalization( + CoinType::TON, + WALLET_1_NON_BOUNCEABLE_TESTNET, + WALLET_1_NON_BOUNCEABLE, + ); +} + +#[test] +fn test_ton_address_is_valid() { + // Raw, hex-encoded + // For now, we allow to specify master-chain addresses. + test_address_valid( + CoinType::TON, + "-1:8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae", + ); + + // User-friendly, url-safe + test_address_valid( + CoinType::TON, + "EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorkdl", + ); + test_address_valid( + CoinType::TON, + "EQBGqFmKe3oY8PChYN9g92ZEV2ybkzVB-hCiesQRn5mFnrNv", + ); + test_address_valid( + CoinType::TON, + "Ef8JfFPRpHBV_tZpCurvxMJW69nt2js3SuGEWojGnOpCVPRe", + ); + test_address_valid( + CoinType::TON, + "Ef_drj6m7jcME0fWTA-OwFC-6F0Le2SuOUQ6ibRc3Vz8HL8H", + ); + + // User-friendly + test_address_valid( + CoinType::TON, + "EQAN6Dr3vziti1Kp9D3aEFqJX4bBVfCaV57Z+9jwKTBXICv8", + ); + test_address_valid( + CoinType::TON, + "EQCmGW+z+UL00FmnhWaMvJq/i86YY5GlJP3uJW19KC5Tzq4C", + ); +} + +#[test] +fn test_ton_address_invalid() { + test_address_invalid(CoinType::TON, "random string"); + + // Invalid size + test_address_invalid( + CoinType::TON, + "EQIcIZpPoMnWXd8FbC1KaLtcyIgVUlwsbFK_3P6f5uf_YyzoE", + ); + test_address_invalid( + CoinType::TON, + "EQIcIZpPoMnWXd8FbC1KaLtcyIgVUlwsbFK_3P6f5uf_YyE", + ); + + // Invalid size after decode + test_address_invalid( + CoinType::TON, + "EQIcIZpPoMnWXd8FbC1KaLtcyIgVUlwsbFK_3P6f5uf_Yyw=", + ); + + // Invalid workchain + test_address_invalid( + CoinType::TON, + "1:0ccd5119f27f7fe4614476c34f7e5e93c7ae098e577cf2012f8b8043165cb809", + ); + test_address_invalid( + CoinType::TON, + "EQEMzVEZ8n9_5GFEdsNPfl6Tx64Jjld88gEvi4BDFly4CSyl", + ); + test_address_invalid( + CoinType::TON, + "-2:e0e98cfcf743292298ad9e379a3c2e6401797b9cbfc0fe98b4e14fd0ce07ecdf", + ); + test_address_invalid( + CoinType::TON, + "Ef7g6Yz890MpIpitnjeaPC5kAXl7nL_A_pi04U_Qzgfs3-Cj", + ); + + // Invalid tag + test_address_invalid( + CoinType::TON, + "MwCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorsn8", + ); // 0x33 + test_address_invalid( + CoinType::TON, + "swCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsornJ2", + ); // 0x80 + 0x33 + test_address_invalid( + CoinType::TON, + "EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsormVH", + ); // crc[a, b] = crc[b, a] + test_address_invalid( + CoinType::TON, + "EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorpcF", + ); // crc=0x9705 +} + +#[test] +fn test_ton_address_get_data() { + test_address_get_data(CoinType::TON, WALLET_1_NON_BOUNCEABLE, WALLET_1_DATA); +} diff --git a/rust/tw_any_coin/tests/chains/ton/ton_compile.rs b/rust/tw_any_coin/tests/chains/ton/ton_compile.rs new file mode 100644 index 00000000000..43fa1e9daf6 --- /dev/null +++ b/rust/tw_any_coin/tests/chains/ton/ton_compile.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::test_utils::sign_utils::{CompilerHelper, PreImageHelper}; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::DecodeHex; +use tw_proto::Common::Proto::SigningError; +use tw_proto::TheOpenNetwork::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +#[test] +fn test_ton_compile_not_supported() { + let input = Proto::SigningInput::default(); + + // Step 2: Obtain preimage hash + let mut pre_imager = PreImageHelper::::default(); + let preimage_output = pre_imager.pre_image_hashes(CoinType::TON, &input); + + assert_eq!(preimage_output.error, SigningError::Error_not_supported); + + // Step 3: Compile transaction info + let signature_bytes = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".decode_hex().unwrap(); + let public_key = "0000000000000000000000000000000000000000000000000000000000000000" + .decode_hex() + .unwrap(); + let mut compiler = CompilerHelper::::default(); + let output = compiler.compile( + CoinType::TON, + &input, + vec![signature_bytes], + vec![public_key], + ); + + assert_eq!(output.error, SigningError::Error_not_supported); +} diff --git a/rust/tw_any_coin/tests/chains/ton/ton_sign.rs b/rust/tw_any_coin/tests/chains/ton/ton_sign.rs new file mode 100644 index 00000000000..a34b9b2cdbc --- /dev/null +++ b/rust/tw_any_coin/tests/chains/ton/ton_sign.rs @@ -0,0 +1,477 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::chains::ton::cell_example::{ + comment_cell, contract_address_from_state_init, doge_chatbot_state_init, +}; +use tw_any_coin::test_utils::sign_utils::AnySignerHelper; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::{DecodeHex, ToHex}; +use tw_proto::Common::Proto::SigningError; +use tw_proto::TheOpenNetwork::Proto; +use tw_proto::TheOpenNetwork::Proto::mod_Transfer::OneOfpayload as PayloadType; + +/// The same Cell can be BoC encoded differently. +/// Use this function to compare inner Cells closing eyes on the encoding. +#[track_caller] +fn assert_eq_boc(left: &str, right: &str) { + use tw_ton_sdk::boc::BagOfCells; + + let left_boc = BagOfCells::parse_base64(left).unwrap(); + let right_boc = BagOfCells::parse_base64(right).unwrap(); + + assert_eq!(left_boc, right_boc); +} + +#[test] +fn test_ton_sign_transfer_and_deploy() { + let private_key = "63474e5fe9511f1526a50567ce142befc343e71a49b865ac3908f58667319cb8"; + + let transfer = Proto::Transfer { + wallet_version: Proto::WalletVersion::WALLET_V4_R2, + dest: "EQDYW_1eScJVxtitoBRksvoV9cCYo4uKGWLVNIHB1JqRR3n0".into(), + amount: 10, + mode: Proto::SendMode::PAY_FEES_SEPARATELY as u32 + | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS as u32, + bounceable: true, + ..Proto::Transfer::default() + }; + + let input = Proto::SigningInput { + private_key: private_key.decode_hex().unwrap().into(), + messages: vec![transfer], + expire_at: 1671135440, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::TON, input); + + assert_eq!(output.error, SigningError::OK, "{}", output.error_message); + // Successfully broadcasted: https://tonviewer.com/transaction/6ZzWOFKZt_m3kZjbwfbATwLaVwmUOdDp0xjhuY7PO3k= + assert_eq_boc(&output.encoded, "te6ccgICABoAAQAAA8sAAAJFiADN98eLgHfrkE8l8gmT8X5REpTVR6QnqDhArTbKlVvbZh4ABAABAZznxvGBhoRXhPogxNY8QmHlihJWxg5t6KptqcAIZlVks1r+Z+r1avCWNCeqeLC/oaiVN4mDx/E1+Zhi33G25rcIKamjF/////8AAAAAAAMAAgFiYgBsLf6vJOEq42xW0AoyWX0K+uBMUcXFDLFqmkDg6k1Io4hQAAAAAAAAAAAAAAAAAQADAAACATQABgAFAFEAAAAAKamjF/Qsd/kxvqIOxdAVBzEna7suKGCUdmEkWyMZ74Ez7o1BQAEU/wD0pBP0vPLICwAHAgEgAA0ACAT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/wAMAAsACgAJAAr0AMntVABsgQEI1xj6ANM/MFIkgQEI9Fnyp4IQZHN0cnB0gBjIywXLAlAFzxZQA/oCE8tqyx8Syz/Jc/sAAHCBAQjXGPoA0z/IVCBHgQEI9FHyp4IQbm90ZXB0gBjIywXLAlAGzxZQBPoCFMtqEssfyz/Jc/sAAgBu0gf6ANTUIvkABcjKBxXL/8nQd3SAGMjLBcsCIs8WUAX6AhTLaxLMzMlz+wDIQBSBAQj0UfKnAgIBSAAXAA4CASAAEAAPAFm9JCtvaiaECAoGuQ+gIYRw1AgIR6STfSmRDOaQPp/5g3gSgBt4EBSJhxWfMYQCASAAEgARABG4yX7UTQ1wsfgCAVgAFgATAgEgABUAFAAZrx32omhAEGuQ64WPwAAZrc52omhAIGuQ64X/wAA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYALm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDQAZABgAilAEgQEI9Fkw7UTQgQFA1yDIAc8W9ADJ7VQBcrCOI4IQZHN0coMesXCAGFAFywVQA88WI/oCE8tqyx/LP8mAQPsAkl8D4gB4AfoA9AQw+CdvIjBQCqEhvvLgUIIQcGx1Z4MesXCAGFAEywUmzxZY+gIZ9ADLaRfLH1Jgyz8gyYBA+wAG"); + assert_eq!( + output.hash.to_hex(), + "b3d9462c13a8c67e19b62002447839c386de51415ace3ff6473b1e6294299819" + ); +} + +#[test] +fn test_ton_sign_transfer_and_deploy_4b1d9f() { + // UQApdOSfRTScoB9bMNGeo3bn-DGpQD8gywA27UaZVvAQsrHg + let private_key = "e97620499dfee0107c0cd7f0ecb2afb3323d385b3a82320a5e3fa1fbdca6e722"; + + let transfer = Proto::Transfer { + wallet_version: Proto::WalletVersion::WALLET_V4_R2, + dest: "UQA6whN_oU5h9jPljnlDSWRYQNDPkLaUqqaEWULNB_Zoykuu".into(), + // 0.0001 TON + amount: 100_000, + mode: Proto::SendMode::PAY_FEES_SEPARATELY as u32 + | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS as u32, + bounceable: true, + ..Proto::Transfer::default() + }; + + let input = Proto::SigningInput { + private_key: private_key.decode_hex().unwrap().into(), + messages: vec![transfer], + expire_at: 1721892371, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::TON, input); + + assert_eq!(output.error, SigningError::OK, "{}", output.error_message); + // Successfully broadcasted: https://tonviewer.com/transaction/4b1d9f09856af70ea1058b557a87c9ba2abb0bca2029e0cbbe8c659d5dae4ce1 + assert_eq_boc(&output.encoded, "te6cckECGgEAA7QAAkWIAFLpyT6KaTlAPrZhoz1G7c/wY1KAfkGWAG3ajTKt4CFkHgECAgE0AwQBnPhb8QZRko0VcLtSmZsGLICPVXj7+QmBkrCrCL1R1bdHYWibC86XsAcA2dgem8QSl8xxsChuCd2oVG1FoMVUUgEpqaMX/////wAAAAAAAwUBFP8A9KQT9LzyyAsGAFEAAAAAKamjFyoNBiYB1ihabNBrDwYVwXUbvpGlthGturFziw2Dl/PLQAFmYgAdYQm/0Kcw+xnyxzyhpLIsIGhnyFtKVVNCLKFmg/s0ZRgMNQAAAAAAAAAAAAAAAAABBwIBIAgJAAACAUgKCwT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/wwNDg8C5tAB0NMDIXGwkl8E4CLXScEgkl8E4ALTHyGCEHBsdWe9IoIQZHN0cr2wkl8F4AP6QDAg+kQByMoHy//J0O1E0IEBQNch9AQwXIEBCPQKb6Exs5JfB+AF0z/IJYIQcGx1Z7qSODDjDQOCEGRzdHK6kl8G4w0QEQIBIBITAG7SB/oA1NQi+QAFyMoHFcv/ydB3dIAYyMsFywIizxZQBfoCFMtrEszMyXP7AMhAFIEBCPRR8qcCAHCBAQjXGPoA0z/IVCBHgQEI9FHyp4IQbm90ZXB0gBjIywXLAlAGzxZQBPoCFMtqEssfyz/Jc/sAAgBsgQEI1xj6ANM/MFIkgQEI9Fnyp4IQZHN0cnB0gBjIywXLAlAFzxZQA/oCE8tqyx8Syz/Jc/sAAAr0AMntVAB4AfoA9AQw+CdvIjBQCqEhvvLgUIIQcGx1Z4MesXCAGFAEywUmzxZY+gIZ9ADLaRfLH1Jgyz8gyYBA+wAGAIpQBIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UAXKwjiOCEGRzdHKDHrFwgBhQBcsFUAPPFiP6AhPLassfyz/JgED7AJJfA+ICASAUFQBZvSQrb2omhAgKBrkPoCGEcNQICEekk30pkQzmkD6f+YN4EoAbeBAUiYcVnzGEAgFYFhcAEbjJftRNDXCx+AA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIBgZABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AaTiNAg=="); + assert_eq!( + output.hash.to_hex(), + "4b1d9f09856af70ea1058b557a87c9ba2abb0bca2029e0cbbe8c659d5dae4ce1" + ); +} + +#[test] +fn test_ton_sign_transfer_ordinary() { + let private_key = "c38f49de2fb13223a9e7d37d5d0ffbdd89a5eb7c8b0ee4d1c299f2cefe7dc4a0"; + + let transfer = Proto::Transfer { + wallet_version: Proto::WalletVersion::WALLET_V4_R2, + dest: "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q".into(), + amount: 10, + mode: Proto::SendMode::PAY_FEES_SEPARATELY as u32 + | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS as u32, + bounceable: true, + ..Proto::Transfer::default() + }; + + let input = Proto::SigningInput { + private_key: private_key.decode_hex().unwrap().into(), + messages: vec![transfer], + sequence_number: 6, + expire_at: 1671132440, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::TON, input); + + assert_eq!(output.error, SigningError::OK, "{}", output.error_message); + // Successfully broadcasted: https://tonviewer.com/transaction/3Z4tHpXNLyprecgu5aTQHWtY7dpHXEoo11MAX61Xyg0= + assert_eq_boc(&output.encoded, "te6ccgICAAQAAQAAALAAAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGcEUPkil2aZ4s8KKparSep/OKHMC8vuXafFbW2HGp/9AcTRv0J5T4dwyW1G0JpHw+g5Ov6QI3Xo0O9RFr3KidICimpoxdjm3UYAAAABgADAAIBYmIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmIUAAAAAAAAAAAAAAAAAEAAwAA"); + assert_eq!( + output.hash.to_hex(), + "3908cf8b570c1d3d261c62620c9f368db11f6e821a07614cff64de2e7319f81b" + ); +} + +#[test] +fn test_ton_sign_transfer_all_balance() { + let private_key = "c38f49de2fb13223a9e7d37d5d0ffbdd89a5eb7c8b0ee4d1c299f2cefe7dc4a0"; + + let transfer = Proto::Transfer { + wallet_version: Proto::WalletVersion::WALLET_V4_R2, + dest: "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q".into(), + amount: 0, + mode: Proto::SendMode::ATTACH_ALL_CONTRACT_BALANCE as u32 + | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS as u32, + bounceable: true, + ..Proto::Transfer::default() + }; + + let input = Proto::SigningInput { + private_key: private_key.decode_hex().unwrap().into(), + messages: vec![transfer], + sequence_number: 7, + expire_at: 1681102222, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::TON, input); + + assert_eq!(output.error, SigningError::OK, "{}", output.error_message); + // Successfully broadcasted: https://tonviewer.com/transaction/cVcXgI9DWNWlN2iyTsteaWJckTswVqWZnRVvX5krXeA= + assert_eq_boc(&output.encoded, "te6ccgICAAQAAQAAAK8AAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGc58rMUQc/u78bg+Wtt8ETkyM0udf7S+F7wWk7lnPib2KChnBx9dZ7a/zLzhfLq+W9LjLZZfx995J17+0sbkvGCympoxdkM5WOAAAABwCCAAIBYGIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmAAAAAAAAAAAAAAAAAAQADAAA="); + assert_eq!( + output.hash.to_hex(), + "d5c5980c9083f697a7f114426effbbafac6d5c88554297d290eb65c8def3008e" + ); +} + +#[test] +fn test_ton_sign_transfer_all_balance_non_bounceable() { + let private_key = "c38f49de2fb13223a9e7d37d5d0ffbdd89a5eb7c8b0ee4d1c299f2cefe7dc4a0"; + + let transfer = Proto::Transfer { + wallet_version: Proto::WalletVersion::WALLET_V4_R2, + dest: "UQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts4DV".into(), + amount: 0, + mode: Proto::SendMode::ATTACH_ALL_CONTRACT_BALANCE as u32 + | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS as u32, + bounceable: false, + ..Proto::Transfer::default() + }; + + let input = Proto::SigningInput { + private_key: private_key.decode_hex().unwrap().into(), + messages: vec![transfer], + sequence_number: 8, + expire_at: 1681102222, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::TON, input); + + assert_eq!(output.error, SigningError::OK, "{}", output.error_message); + // Successfully broadcasted: https://tonviewer.com/transaction/0sJkPKu6u6uObVRuSWGd_bVGiyy5lJuzEKDqSXifQEA= + assert_eq_boc(&output.encoded, "te6ccgICAAQAAQAAAK8AAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGcRQQvxdU1u4QoE2Pas0AsZQMc9lea3+wtSvaC6QfLUlyJ9oISMCFnaErpyFHelDhPu4iuZqhkoLwjkR1VYhFSCimpoxdkM5WOAAAACACCAAIBYEIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmAAAAAAAAAAAAAAAAAAQADAAA="); + assert_eq!( + output.hash.to_hex(), + "e9c816780fa8e578bae309c2e098db8eb16aa25545b3ad2b61bb711ec9562795" + ); +} + +#[test] +fn test_ton_sign_transfer_with_ascii_comment() { + let private_key = "c38f49de2fb13223a9e7d37d5d0ffbdd89a5eb7c8b0ee4d1c299f2cefe7dc4a0"; + + let transfer = Proto::Transfer { + wallet_version: Proto::WalletVersion::WALLET_V4_R2, + dest: "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q".into(), + amount: 10, + mode: Proto::SendMode::PAY_FEES_SEPARATELY as u32 + | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS as u32, + bounceable: true, + comment: "test comment".into(), + ..Proto::Transfer::default() + }; + + let input = Proto::SigningInput { + private_key: private_key.decode_hex().unwrap().into(), + messages: vec![transfer], + sequence_number: 10, + expire_at: 1681102222, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::TON, input); + + assert_eq!(output.error, SigningError::OK, "{}", output.error_message); + // Successfully broadcasted: https://tonviewer.com/transaction/9wjD-VrgEDpa0D9u1g03KSD7kvTNsxRocR7LEdQtCNQ= + assert_eq_boc(&output.encoded, "te6ccgICAAQAAQAAAMAAAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGcY4XlvKqu7spxyjL6vyBSKjbskDgqkHhqBsdTe900RGrzExtpvwc04j94v8HOczEWSMCXjTXk0z+CVUXSL54qCimpoxdkM5WOAAAACgADAAIBYmIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmIUAAAAAAAAAAAAAAAAAEAAwAgAAAAAHRlc3QgY29tbWVudA=="); + assert_eq!( + output.hash.to_hex(), + "a8c6943d5587f590c43fcdb0e894046f1965c615e19bcaf0c8407e9ccb74518d" + ); +} + +#[test] +fn test_ton_sign_transfer_with_utf8_comment() { + let private_key = "c38f49de2fb13223a9e7d37d5d0ffbdd89a5eb7c8b0ee4d1c299f2cefe7dc4a0"; + + let transfer = Proto::Transfer { + wallet_version: Proto::WalletVersion::WALLET_V4_R2, + dest: "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q".into(), + amount: 10, + mode: Proto::SendMode::PAY_FEES_SEPARATELY as u32 + | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS as u32, + bounceable: true, + comment: "тестовый комментарий".into(), + ..Proto::Transfer::default() + }; + + let input = Proto::SigningInput { + private_key: private_key.decode_hex().unwrap().into(), + messages: vec![transfer], + sequence_number: 11, + expire_at: 1681102222, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::TON, input); + + assert_eq!(output.error, SigningError::OK, "{}", output.error_message); + // Successfully broadcasted: https://tonviewer.com/transaction/9wjD-VrgEDpa0D9u1g03KSD7kvTNsxRocR7LEdQtCNQ= + assert_eq_boc(&output.encoded, "te6ccgICAAQAAQAAANsAAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGchoDa7EdGQuPuehHy3+0X9WNVEvYxdBtaEWn15oYUX8PEKyzztYy94Xq0T2XdhVvj2H7PTSQ+D/Ny1IBRCxk0BimpoxdkM5WOAAAACwADAAIBYmIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmIUAAAAAAAAAAAAAAAAAEAAwBWAAAAANGC0LXRgdGC0L7QstGL0Lkg0LrQvtC80LzQtdC90YLQsNGA0LjQuQ=="); + assert_eq!( + output.hash.to_hex(), + "1091dfae81583d3972825633592c24eab0d3d74c91f60fda9d4afe7535103633" + ); +} + +#[test] +fn test_ton_sign_transfer_invalid_wallet_version() { + let private_key = "63474e5fe9511f1526a50567ce142befc343e71a49b865ac3908f58667319cb8"; + + let transfer = Proto::Transfer { + wallet_version: Proto::WalletVersion::WALLET_V3_R2, + dest: "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q".into(), + amount: 10, + mode: Proto::SendMode::PAY_FEES_SEPARATELY as u32 + | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS as u32, + bounceable: true, + ..Proto::Transfer::default() + }; + + let input = Proto::SigningInput { + private_key: private_key.decode_hex().unwrap().into(), + messages: vec![transfer], + expire_at: 1671135440, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::TON, input); + + assert_eq!(output.error, SigningError::Error_not_supported); +} + +#[test] +fn test_ton_sign_transfer_jettons() { + let private_key = "c054900a527538c1b4325688a421c0469b171c29f23a62da216e90b0df2412ee"; + + let jetton_transfer = Proto::JettonTransfer { + query_id: 69, + // Transfer 1 testtwt (decimal precision is 9). + jetton_amount: 1000 * 1000 * 1000, + to_owner: "EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8".into(), + // Send unused toncoins back to sender. + response_address: "EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk".into(), + forward_amount: 1, + }; + + let transfer = Proto::Transfer { + wallet_version: Proto::WalletVersion::WALLET_V4_R2, + dest: "EQBiaD8PO1NwfbxSkwbcNT9rXDjqhiIvXWymNO-edV0H5lja".into(), + amount: 100 * 1000 * 1000, + mode: Proto::SendMode::PAY_FEES_SEPARATELY as u32 + | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS as u32, + bounceable: true, + payload: PayloadType::jetton_transfer(jetton_transfer), + ..Proto::Transfer::default() + }; + + let input = Proto::SigningInput { + private_key: private_key.decode_hex().unwrap().into(), + messages: vec![transfer], + sequence_number: 0, + expire_at: 1787693046, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::TON, input); + + assert_eq!(output.error, SigningError::OK, "{}", output.error_message); + // Successfully broadcasted: https://testnet.tonviewer.com/transaction/2HOPGAXhez3v6sdfj-5p8mPHX4S4T0CgxVbm0E2swxE= + assert_eq_boc(&output.encoded, "te6ccgICABoAAQAABCMAAAJFiAC0UQZVyBNtT/W+jqQKnhYasPiDIdSWnNgo1FPyLHxLKh4ABAABAZz3iNHD1z2mxbtpFAtmbVevYMnB4yHPkF3WAsL3KHcrqCw0SWezOg4lVz1zzSReeFDx98ByAqY9+eR5VF3xyugAKamjF/////8AAAAAAAMAAgFoYgAxNB+Hnam4Pt4pSYNuGp+1rhx1QxEXrrZTGnfPOq6D8yAvrwgAAAAAAAAAAAAAAAAAAQADAKoPin6lAAAAAAAAAEVDuaygCAALgZZzC14dAz6ZxChX5pn6bIJ3WNipSJrCELO7Ex0TOQAWiiDKuQJtqf630dSBU8LDVh8QZDqS05sFGop+RY+JZUICAgE0AAYABQBRAAAAACmpoxfOamBhePRNnx/pqQViBzW0dDCy/+1WLV1VhgbVTL6i30ABFP8A9KQT9LzyyAsABwIBIAANAAgE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8ADAALAAoACQAK9ADJ7VQAbIEBCNcY+gDTPzBSJIEBCPRZ8qeCEGRzdHJwdIAYyMsFywJQBc8WUAP6AhPLassfEss/yXP7AABwgQEI1xj6ANM/yFQgR4EBCPRR8qeCEG5vdGVwdIAYyMsFywJQBs8WUAT6AhTLahLLH8s/yXP7AAIAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJc/sAyEAUgQEI9FHypwICAUgAFwAOAgEgABAADwBZvSQrb2omhAgKBrkPoCGEcNQICEekk30pkQzmkD6f+YN4EoAbeBAUiYcVnzGEAgEgABIAEQARuMl+1E0NcLH4AgFYABYAEwIBIAAVABQAGa8d9qJoQBBrkOuFj8AAGa3OdqJoQCBrkOuF/8AAPbKd+1E0IEBQNch9AQwAsjKB8v/ydABgQEI9ApvoTGAC5tAB0NMDIXGwkl8E4CLXScEgkl8E4ALTHyGCEHBsdWe9IoIQZHN0cr2wkl8F4AP6QDAg+kQByMoHy//J0O1E0IEBQNch9AQwXIEBCPQKb6Exs5JfB+AF0z/IJYIQcGx1Z7qSODDjDQOCEGRzdHK6kl8G4w0AGQAYAIpQBIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UAXKwjiOCEGRzdHKDHrFwgBhQBcsFUAPPFiP6AhPLassfyz/JgED7AJJfA+IAeAH6APQEMPgnbyIwUAqhIb7y4FCCEHBsdWeDHrFwgBhQBMsFJs8WWPoCGfQAy2kXyx9SYMs/IMmAQPsABg=="); + assert_eq!( + output.hash.to_hex(), + "3e4dac37acdc99ca670b3747ab2730e818727d9d25c80d3987abe501356d0da0" + ); +} + +#[test] +fn test_ton_sign_transfer_jettons_with_comment() { + let private_key = "c054900a527538c1b4325688a421c0469b171c29f23a62da216e90b0df2412ee"; + + let jetton_transfer = Proto::JettonTransfer { + query_id: 0, + // Transfer 0.5 testtwt (decimal precision is 9). + jetton_amount: 500 * 1000 * 1000, + to_owner: "EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8".into(), + // Send unused toncoins back to sender. + response_address: "EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk".into(), + forward_amount: 1, + }; + + let transfer = Proto::Transfer { + wallet_version: Proto::WalletVersion::WALLET_V4_R2, + dest: "EQBiaD8PO1NwfbxSkwbcNT9rXDjqhiIvXWymNO-edV0H5lja".into(), + amount: 100 * 1000 * 1000, + mode: Proto::SendMode::PAY_FEES_SEPARATELY as u32 + | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS as u32, + bounceable: true, + comment: "test comment".into(), + payload: PayloadType::jetton_transfer(jetton_transfer), + ..Proto::Transfer::default() + }; + + let input = Proto::SigningInput { + private_key: private_key.decode_hex().unwrap().into(), + messages: vec![transfer], + sequence_number: 1, + expire_at: 1787693046, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::TON, input); + + assert_eq!(output.error, SigningError::OK, "{}", output.error_message); + // Successfully broadcasted: https://testnet.tonviewer.com/transaction/12bfe84f947740aec3faa54f04a50690900e3aae9ac9596cfa6804a61a48f429 + assert_eq_boc(&output.encoded, "te6ccgICAAQAAQAAARgAAAFFiAC0UQZVyBNtT/W+jqQKnhYasPiDIdSWnNgo1FPyLHxLKgwAAQGcaIWVosi1XnveAmoG9y0/mPeNUqUu7GY76mdbRAaVeNeDOPDlh5M3BEb26kkc6XoYDekV60o2iOobN+TGS76jBSmpoxdqjgf2AAAAAQADAAIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEAAwDKD4p+pQAAAAAAAAAAQdzWUAgAC4GWcwteHQM+mcQoV+aZ+myCd1jYqUiawhCzuxMdEzkAFoogyrkCban+t9HUgVPCw1YfEGQ6ktObBRqKfkWPiWVCAgAAAAB0ZXN0IGNvbW1lbnQ="); + assert_eq!( + output.hash.to_hex(), + "c98c205c8dd37d9a6ab5db6162f5b9d37cefa067de24a765154a5eb7a359f22f" + ); +} + +#[test] +fn test_ton_sign_transfer_custom_payload() { + // UQApdOSfRTScoB9bMNGeo3bn-DGpQD8gywA27UaZVvAQsrHg + let private_key = "e97620499dfee0107c0cd7f0ecb2afb3323d385b3a82320a5e3fa1fbdca6e722"; + + let transfer = Proto::Transfer { + wallet_version: Proto::WalletVersion::WALLET_V4_R2, + dest: "UQA6whN_oU5h9jPljnlDSWRYQNDPkLaUqqaEWULNB_Zoykuu".into(), + // 0.00025 TON + amount: 250_000, + mode: Proto::SendMode::PAY_FEES_SEPARATELY as u32 + | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS as u32, + bounceable: true, + payload: PayloadType::custom_payload(Proto::CustomPayload { + state_init: "".into(), + payload: comment_cell("Hi there sir").into(), + }), + ..Proto::Transfer::default() + }; + + let input = Proto::SigningInput { + private_key: private_key.decode_hex().unwrap().into(), + expire_at: 1721906219, + messages: vec![transfer], + sequence_number: 2, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::TON, input); + + assert_eq!(output.error, SigningError::OK, "{}", output.error_message); + // Successfully broadcasted: https://tonviewer.com/transaction/4b1d9f09856af70ea1058b557a87c9ba2abb0bca2029e0cbbe8c659d5dae4ce1 + assert_eq_boc(&output.encoded, "te6cckEBBAEAvwABRYgAUunJPoppOUA+tmGjPUbtz/BjUoB+QZYAbdqNMq3gIWQMAQGc981GQ9a8Yr4m2YeIeuuNIWlzdHliyW6MRq3RDs5kgvXJP+iNhdZU7o79DJnm/OKuzWI5FbiNy3SF0fGGBObDDCmpoxdmojQrAAAAAgADAgFmYgAdYQm/0Kcw+xnyxzyhpLIsIGhnyFtKVVNCLKFmg/s0ZRgehIAAAAAAAAAAAAAAAAABAwAgAAAAAEhpIHRoZXJlIHNpcoAlI8E="); + assert_eq!( + output.hash.to_hex(), + "4ca4442921d0737d518b4863f8eafc2eb26824e9362ebaa9bc59373950b7fa86" + ); +} + +#[test] +fn test_ton_sign_transfer_custom_payload_with_state_init() { + // UQD9XBth5b0D9F35h8No4nrF2au8E5H9XUc25iJMOx9tLfy3 + let private_key = "5525e673087587bc0efd7ab09920ef7d3c1bf6b854a661430244ca59ab19e9d1"; + + let current_timestamp = 1721939114; + let expire_at = current_timestamp + 60 * 10; + + let doge_state_init = doge_chatbot_state_init(current_timestamp); + let doge_contract_address = contract_address_from_state_init(&doge_state_init); + assert_eq!( + doge_state_init, + "te6cckEBBAEAUwACATQBAgEU/wD0pBP0vPLICwMAEAAAAZDrkbgQAGrTMAGCCGlJILmRMODQ0wMx+kAwi0ZG9nZYcCCAGMjLBVAEzxaARfoCE8tqEssfAc8WyXP7AO4ioYU=" + ); + assert_eq!( + doge_contract_address, + "0:3042cd5480da232d5ac1d9cbe324e3c9eb58f167599f6b7c20c6e638aeed0335", + ); + + let transfer = Proto::Transfer { + wallet_version: Proto::WalletVersion::WALLET_V4_R2, + dest: doge_contract_address.into(), + // 0.069 TON + amount: 69_000_000, + mode: Proto::SendMode::PAY_FEES_SEPARATELY as u32 + | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS as u32, + bounceable: false, + payload: PayloadType::custom_payload(Proto::CustomPayload { + state_init: doge_state_init.into(), + payload: comment_cell("This transaction deploys Doge Chatbot contract").into(), + }), + ..Proto::Transfer::default() + }; + + let input = Proto::SigningInput { + private_key: private_key.decode_hex().unwrap().into(), + expire_at, + messages: vec![transfer], + sequence_number: 4, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::TON, input); + + assert_eq!(output.error, SigningError::OK, "{}", output.error_message); + // Successfully broadcasted: https://tonviewer.com/transaction/f4b7ed2247b1adf54f33dd2fd99216fbd61beefb281542d0b330ccea9b8d0338 + assert_eq_boc(&output.encoded, "te6cckECCAEAATcAAUWIAfq4NsPLegfou/MPhtHE9YuzV3gnI/q6jm3MRJh2PtpaDAEBnPbyCSsWrOZpEjb7ZFxz5yYi+an6M6Lnq7rI7TFWdDS76LEtGBrVVrhMGziwxuy6LCVtsMBikI7RPVQ89FCIAAYpqaMXZqK3AgAAAAQAAwICaUIAGCFmqkBtEZatYOzl8ZJx5PWseLOsz7W+EGNzHFd2gZqgIObaAAAAAAAAAAAAAAAAAAPAAwQCATQFBgBkAAAAAFRoaXMgdHJhbnNhY3Rpb24gZGVwbG95cyBEb2dlIENoYXRib3QgY29udHJhY3QBFP8A9KQT9LzyyAsHABAAAAGQ65G4EABq0zABgghpSSC5kTDg0NMDMfpAMItGRvZ2WHAggBjIywVQBM8WgEX6AhPLahLLHwHPFslz+wAa2r/S"); + assert_eq!( + output.hash.to_hex(), + "724735eea1ab0663aff42af421fb90f7f0c0f7ca3dda424d97c1daf1a21036ad" + ); +} diff --git a/rust/tw_any_coin/tests/coin_address_derivation_test.rs b/rust/tw_any_coin/tests/coin_address_derivation_test.rs index a757ed9122a..a1e69245dac 100644 --- a/rust/tw_any_coin/tests/coin_address_derivation_test.rs +++ b/rust/tw_any_coin/tests/coin_address_derivation_test.rs @@ -154,6 +154,7 @@ fn test_coin_address_derivation() { CoinType::Dydx => "dydx1ten42eesehw0ktddcp0fws7d3ycsqez3kaamq3", CoinType::Solana => "5sn9QYhDaq61jLXJ8Li5BKqGL4DDMJQvU1rdN8XgVuwC", CoinType::Sui => "0x01a5c6c1b74cec4fbd12b3e17252b83448136065afcdf24954dc3a9c26df4905", + CoinType::TON => "UQCj3jAU_Ec2kXdAqweKt4rYjiwTNwiCfaUnIDHGh7wTwx_G", // end_of_coin_address_derivation_tests_marker_do_not_modify _ => panic!("{:?} must be covered", coin), }; diff --git a/rust/tw_coin_entry/src/error/address_error.rs b/rust/tw_coin_entry/src/error/address_error.rs index 8a08830da72..d49f307dada 100644 --- a/rust/tw_coin_entry/src/error/address_error.rs +++ b/rust/tw_coin_entry/src/error/address_error.rs @@ -11,6 +11,7 @@ pub enum AddressError { MissingPrefix, FromHexError, FromBase58Error, + FromBase64Error, FromBech32Error, PublicKeyTypeMismatch, UnexpectedAddressPrefix, diff --git a/rust/tw_coin_registry/Cargo.toml b/rust/tw_coin_registry/Cargo.toml index dfbbc4b7b0d..6cc8641f84f 100644 --- a/rust/tw_coin_registry/Cargo.toml +++ b/rust/tw_coin_registry/Cargo.toml @@ -28,6 +28,7 @@ tw_ronin = { path = "../chains/tw_ronin" } tw_solana = { path = "../chains/tw_solana" } tw_sui = { path = "../chains/tw_sui" } tw_thorchain = { path = "../chains/tw_thorchain" } +tw_ton = { path = "../chains/tw_ton" } [build-dependencies] itertools = "0.10.5" diff --git a/rust/tw_coin_registry/src/blockchain_type.rs b/rust/tw_coin_registry/src/blockchain_type.rs index 3aff7a6d250..a91e4172813 100644 --- a/rust/tw_coin_registry/src/blockchain_type.rs +++ b/rust/tw_coin_registry/src/blockchain_type.rs @@ -21,6 +21,7 @@ pub enum BlockchainType { Ronin, Solana, Sui, + TheOpenNetwork, Thorchain, // end_of_blockchain_type - USED TO GENERATE CODE #[serde(other)] diff --git a/rust/tw_coin_registry/src/dispatcher.rs b/rust/tw_coin_registry/src/dispatcher.rs index 5a335fa5c3b..bda3fdba931 100644 --- a/rust/tw_coin_registry/src/dispatcher.rs +++ b/rust/tw_coin_registry/src/dispatcher.rs @@ -22,6 +22,7 @@ use tw_ronin::entry::RoninEntry; use tw_solana::entry::SolanaEntry; use tw_sui::entry::SuiEntry; use tw_thorchain::entry::ThorchainEntry; +use tw_ton::entry::TheOpenNetworkEntry; pub type CoinEntryExtStaticRef = &'static dyn CoinEntryExt; pub type EvmEntryExtStaticRef = &'static dyn EvmEntryExt; @@ -39,6 +40,7 @@ const NATIVE_INJECTIVE: NativeInjectiveEntry = NativeInjectiveEntry; const RONIN: RoninEntry = RoninEntry; const SOLANA: SolanaEntry = SolanaEntry; const SUI: SuiEntry = SuiEntry; +const THE_OPEN_NETWORK: TheOpenNetworkEntry = TheOpenNetworkEntry; const THORCHAIN: ThorchainEntry = ThorchainEntry; // end_of_blockchain_entries - USED TO GENERATE CODE @@ -57,6 +59,7 @@ pub fn blockchain_dispatcher(blockchain: BlockchainType) -> RegistryResult Ok(&RONIN), BlockchainType::Solana => Ok(&SOLANA), BlockchainType::Sui => Ok(&SUI), + BlockchainType::TheOpenNetwork => Ok(&THE_OPEN_NETWORK), BlockchainType::Thorchain => Ok(&THORCHAIN), // end_of_blockchain_dispatcher - USED TO GENERATE CODE BlockchainType::Unsupported => Err(RegistryError::Unsupported), diff --git a/rust/tw_cosmos_sdk/tests/sign_terra_wasm.rs b/rust/tw_cosmos_sdk/tests/sign_terra_wasm.rs index 734c953194d..9ce7d2ed39a 100644 --- a/rust/tw_cosmos_sdk/tests/sign_terra_wasm.rs +++ b/rust/tw_cosmos_sdk/tests/sign_terra_wasm.rs @@ -9,7 +9,7 @@ use tw_cosmos_sdk::context::StandardCosmosContext; use tw_cosmos_sdk::modules::tx_builder::TxBuilder; use tw_cosmos_sdk::test_utils::proto_utils::{make_amount, make_fee, make_message}; use tw_cosmos_sdk::test_utils::sign_utils::{test_sign_json, test_sign_protobuf, TestInput}; -use tw_encoding::base64; +use tw_encoding::base64::{self, STANDARD}; use tw_encoding::hex::DecodeHex; use tw_keypair::tw::PublicKeyType; use tw_number::U256; @@ -191,7 +191,7 @@ fn test_terra_wasm_send() { .with_public_key_type(PublicKeyType::Secp256k1) .with_hrp("terra"); - let encoded_msg = base64::encode(r#"{"some_message":{}}"#.as_bytes(), false); + let encoded_msg = base64::encode(r#"{"some_message":{}}"#.as_bytes(), STANDARD); assert_eq!(encoded_msg, "eyJzb21lX21lc3NhZ2UiOnt9fQ=="); let send = Proto::mod_Message::WasmTerraExecuteContractSend { diff --git a/rust/tw_cosmos_sdk/tests/sign_wasm_contract.rs b/rust/tw_cosmos_sdk/tests/sign_wasm_contract.rs index e2ce673c929..b80f2b8100c 100644 --- a/rust/tw_cosmos_sdk/tests/sign_wasm_contract.rs +++ b/rust/tw_cosmos_sdk/tests/sign_wasm_contract.rs @@ -9,7 +9,7 @@ use tw_cosmos_sdk::context::StandardCosmosContext; use tw_cosmos_sdk::modules::tx_builder::TxBuilder; use tw_cosmos_sdk::test_utils::proto_utils::{make_amount, make_fee, make_message}; use tw_cosmos_sdk::test_utils::sign_utils::{test_sign_json, test_sign_protobuf, TestInput}; -use tw_encoding::base64; +use tw_encoding::base64::{self, STANDARD}; use tw_encoding::hex::DecodeHex; use tw_keypair::tw::PublicKeyType; use tw_number::U256; @@ -176,7 +176,7 @@ fn test_wasm_execute_send() { .with_public_key_type(PublicKeyType::Secp256k1) .with_hrp("terra"); - let encoded_msg = base64::encode(r#"{"some_message":{}}"#.as_bytes(), false); + let encoded_msg = base64::encode(r#"{"some_message":{}}"#.as_bytes(), STANDARD); let send = Proto::mod_Message::WasmExecuteContractSend { sender_address: "terra18wukp84dq227wu4mgh0jm6n9nlnj6rs82pp9wf".into(), contract_address: "terra14z56l0fp2lsf86zy3hty2z47ezkhnthtr9yq76".into(), diff --git a/rust/tw_encoding/fuzz/fuzz_targets/base_encode.rs b/rust/tw_encoding/fuzz/fuzz_targets/base_encode.rs index aba1f7d1356..6819fe6a852 100644 --- a/rust/tw_encoding/fuzz/fuzz_targets/base_encode.rs +++ b/rust/tw_encoding/fuzz/fuzz_targets/base_encode.rs @@ -14,5 +14,11 @@ struct BaseEncodeInput<'a> { fuzz_target!(|input: BaseEncodeInput<'_>| { base32::encode(input.data, None, input.padding).ok(); base58::encode(input.data, input.alphabet_base58.into()); - base64::encode(input.data, input.is_url); + base64::encode( + input.data, + base64::Config { + url: input.is_url, + padding: input.padding, + }, + ); }); diff --git a/rust/tw_encoding/src/base64.rs b/rust/tw_encoding/src/base64.rs index 827613b036b..78723360a66 100644 --- a/rust/tw_encoding/src/base64.rs +++ b/rust/tw_encoding/src/base64.rs @@ -7,21 +7,54 @@ use serde::de::Error as DeError; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tw_memory::Data; -pub fn encode(data: &[u8], is_url: bool) -> String { - if is_url { - data_encoding::BASE64URL.encode(data) - } else { - data_encoding::BASE64.encode(data) - } +pub const STANDARD: Config = Config { + url: false, + pad: true, +}; + +pub const NO_PAD: Config = Config { + url: false, + pad: true, +}; + +pub const URL_SAFE: Config = Config { + url: true, + pad: true, +}; + +pub const URL_NO_PAD: Config = Config { + url: true, + pad: false, +}; + +#[derive(Clone, Copy)] +pub struct Config { + /// Whether URL safe. + pub url: bool, + /// Whether '=' padded. + pub pad: bool, } -pub fn decode(data: &str, is_url: bool) -> EncodingResult> { - if is_url { - data_encoding::BASE64URL.decode(data.as_bytes()) - } else { - data_encoding::BASE64.decode(data.as_bytes()) +impl Config { + fn as_encoding(&self) -> data_encoding::Encoding { + match (self.url, self.pad) { + (false, false) => data_encoding::BASE64_NOPAD, + (false, true) => data_encoding::BASE64, + (true, false) => data_encoding::BASE64URL_NOPAD, + (true, true) => data_encoding::BASE64URL, + } } - .map_err(|_| EncodingError::InvalidInput) +} + +pub fn encode(data: &[u8], config: Config) -> String { + config.as_encoding().encode(data) +} + +pub fn decode(data: &str, config: Config) -> EncodingResult> { + config + .as_encoding() + .decode(data.as_bytes()) + .map_err(|_| EncodingError::InvalidInput) } #[derive(Clone, Debug)] @@ -32,8 +65,7 @@ impl Serialize for Base64Encoded { where S: Serializer, { - let is_url = false; - serializer.serialize_str(&encode(&self.0, is_url)) + serializer.serialize_str(&encode(&self.0, STANDARD)) } } @@ -43,8 +75,7 @@ impl<'de> Deserialize<'de> for Base64Encoded { D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; - let is_url = false; - decode(&s, is_url) + decode(&s, STANDARD) .map(Base64Encoded) .map_err(|e| DeError::custom(format!("{e:?}"))) } diff --git a/rust/tw_encoding/src/ffi.rs b/rust/tw_encoding/src/ffi.rs index 7f9458ef1ac..0f58c984ec4 100644 --- a/rust/tw_encoding/src/ffi.rs +++ b/rust/tw_encoding/src/ffi.rs @@ -154,7 +154,11 @@ pub unsafe extern "C" fn decode_base58( #[no_mangle] pub unsafe extern "C" fn encode_base64(data: *const u8, len: usize, is_url: bool) -> *mut c_char { let data = std::slice::from_raw_parts(data, len); - let encoded = base64::encode(data, is_url); + let config = base64::Config { + url: is_url, + pad: true, + }; + let encoded = base64::encode(data, config); CString::new(encoded).unwrap().into_raw() } @@ -171,7 +175,11 @@ pub unsafe extern "C" fn decode_base64(data: *const c_char, is_url: bool) -> CBy Ok(input) => input, Err(_) => return CByteArrayResult::error(CEncodingCode::InvalidInput), }; - base64::decode(str_slice, is_url) + let config = base64::Config { + url: is_url, + pad: true, + }; + base64::decode(str_slice, config) .map(CByteArray::from) .map_err(CEncodingCode::from) .into() diff --git a/rust/tw_hash/src/hash_array.rs b/rust/tw_hash/src/hash_array.rs index 31946c8d989..e1676410954 100644 --- a/rust/tw_hash/src/hash_array.rs +++ b/rust/tw_hash/src/hash_array.rs @@ -14,6 +14,7 @@ pub type H32 = Hash<4>; pub type H160 = Hash<20>; pub type H256 = Hash<32>; pub type H264 = Hash<33>; +pub type H288 = Hash<36>; pub type H512 = Hash<64>; pub type H520 = Hash<65>; diff --git a/rust/tw_hash/src/lib.rs b/rust/tw_hash/src/lib.rs index 0e83ce463b6..1fb102ddbb4 100644 --- a/rust/tw_hash/src/lib.rs +++ b/rust/tw_hash/src/lib.rs @@ -17,7 +17,9 @@ pub mod sha3; mod hash_array; mod hash_wrapper; -pub use hash_array::{as_byte_sequence, as_bytes, concat, Hash, H160, H256, H264, H32, H512, H520}; +pub use hash_array::{ + as_byte_sequence, as_bytes, concat, Hash, H160, H256, H264, H288, H32, H512, H520, +}; use tw_encoding::hex::FromHexError; diff --git a/rust/tw_keypair/src/ed25519/private.rs b/rust/tw_keypair/src/ed25519/private.rs index 5df5c59b5a0..984392393a6 100644 --- a/rust/tw_keypair/src/ed25519/private.rs +++ b/rust/tw_keypair/src/ed25519/private.rs @@ -15,7 +15,7 @@ use tw_misc::traits::ToBytesZeroizing; use zeroize::{ZeroizeOnDrop, Zeroizing}; /// Represents an `ed25519` private key. -#[derive(ZeroizeOnDrop)] +#[derive(Clone, ZeroizeOnDrop)] pub struct PrivateKey { secret: H256, /// An expanded secret key obtained from [`PrivateKey::secret`]. diff --git a/rust/tw_keypair/src/ed25519/secret.rs b/rust/tw_keypair/src/ed25519/secret.rs index c1419e9fe92..895f788b15d 100644 --- a/rust/tw_keypair/src/ed25519/secret.rs +++ b/rust/tw_keypair/src/ed25519/secret.rs @@ -17,7 +17,7 @@ use zeroize::ZeroizeOnDrop; /// with 512-bits output to digest a `PrivateKey`. /// /// Represents [ed25519_extsk](https://github.com/trustwallet/wallet-core/blob/423f0e34725f69c0a9d535e1a32534c99682edea/trezor-crypto/crypto/ed25519-donna/ed25519.c#L23-L32). -#[derive(ZeroizeOnDrop)] +#[derive(Clone, ZeroizeOnDrop)] pub(crate) struct ExpandedSecretKey { /// 32 byte scalar. Represents `extsk[0..32]`. pub key: Scalar, diff --git a/rust/tw_number/src/u256.rs b/rust/tw_number/src/u256.rs index a4285f739b4..1b2d2df4c17 100644 --- a/rust/tw_number/src/u256.rs +++ b/rust/tw_number/src/u256.rs @@ -35,10 +35,11 @@ impl U256 { pub const BYTES: usize = U256::WORDS_COUNT * 8; pub const BITS: usize = 256; pub const MAX: U256 = U256(primitive_types::U256::MAX); + pub const ZERO: U256 = U256::zero(); #[inline] - pub fn zero() -> U256 { - U256::default() + pub const fn zero() -> U256 { + U256(primitive_types::U256::zero()) } #[inline] diff --git a/rust/wallet_core_rs/Cargo.toml b/rust/wallet_core_rs/Cargo.toml index 3fb32b403a2..f75f4859576 100644 --- a/rust/wallet_core_rs/Cargo.toml +++ b/rust/wallet_core_rs/Cargo.toml @@ -14,6 +14,7 @@ default = [ "ethereum", "keypair", "solana", + "ton", "utils", ] any-coin = ["tw_any_coin"] @@ -21,6 +22,7 @@ bitcoin = ["tw_bitcoin"] ethereum = ["tw_ethereum", "tw_coin_registry"] keypair = ["tw_keypair"] solana = ["tw_solana"] +ton = ["tw_ton"] utils = [ "tw_encoding", "tw_hash", @@ -44,6 +46,7 @@ tw_number = { path = "../tw_number", optional = true } tw_misc = { path = "../tw_misc" } tw_proto = { path = "../tw_proto", optional = true } tw_solana = { path = "../chains/tw_solana", optional = true } +tw_ton = { path = "../chains/tw_ton", optional = true } uuid = { version = "1.7", features = ["v4"], optional = true } [dev-dependencies] diff --git a/rust/wallet_core_rs/src/ffi/mod.rs b/rust/wallet_core_rs/src/ffi/mod.rs index 8b0bc21ca38..7b77f1ed28a 100644 --- a/rust/wallet_core_rs/src/ffi/mod.rs +++ b/rust/wallet_core_rs/src/ffi/mod.rs @@ -8,5 +8,7 @@ pub mod bitcoin; pub mod ethereum; #[cfg(feature = "solana")] pub mod solana; +#[cfg(feature = "ton")] +pub mod ton; #[cfg(feature = "utils")] pub mod utils; diff --git a/rust/wallet_core_rs/src/ffi/ton/address_converter.rs b/rust/wallet_core_rs/src/ffi/ton/address_converter.rs new file mode 100644 index 00000000000..f16ee89ac69 --- /dev/null +++ b/rust/wallet_core_rs/src/ffi/ton/address_converter.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#![allow(clippy::missing_safety_doc)] + +use std::str::FromStr; +use tw_memory::ffi::tw_string::TWString; +use tw_memory::ffi::RawPtrTrait; +use tw_misc::try_or_else; +use tw_ton::address::TonAddress; +use tw_ton::modules::address_converter::AddressConverter; + +/// 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. +#[no_mangle] +pub unsafe extern "C" fn tw_ton_address_converter_to_boc( + address: *const TWString, +) -> *mut TWString { + let address = try_or_else!(TWString::from_ptr_as_ref(address), std::ptr::null_mut); + let address_str = try_or_else!(address.as_str(), std::ptr::null_mut); + let address_ton = try_or_else!(TonAddress::from_str(address_str), std::ptr::null_mut); + + let boc_encoded = try_or_else!( + AddressConverter::convert_to_boc_base64(&address_ton), + std::ptr::null_mut + ); + + TWString::from(boc_encoded).into_ptr() +} + +/// 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. +#[no_mangle] +pub unsafe extern "C" fn tw_ton_address_converter_from_boc(boc: *const TWString) -> *mut TWString { + let boc = try_or_else!(TWString::from_ptr_as_ref(boc), std::ptr::null_mut); + let boc_str = try_or_else!(boc.as_str(), std::ptr::null_mut); + + let address_ton = try_or_else!( + AddressConverter::parse_from_boc_base64(boc_str), + std::ptr::null_mut + ); + + TWString::from(address_ton.to_string()).into_ptr() +} + +/// Converts any TON address format to user friendly with the given parameters. +/// +/// \param address raw or user-friendly address to be converted. +/// \param bounceable whether the result address should be bounceable. +/// \param testnet whether the result address should be testnet. +/// \return user-friendly address str. +#[no_mangle] +pub unsafe extern "C" fn tw_ton_address_converter_to_user_friendly( + address: *const TWString, + bounceable: bool, + testnet: bool, +) -> *mut TWString { + let address = try_or_else!(TWString::from_ptr_as_ref(address), std::ptr::null_mut); + let address_str = try_or_else!(address.as_str(), std::ptr::null_mut); + let address_ton = try_or_else!(TonAddress::from_str(address_str), std::ptr::null_mut) + .set_bounceable(bounceable) + .set_testnet(testnet); + + TWString::from(address_ton.to_string()).into_ptr() +} diff --git a/rust/wallet_core_rs/src/ffi/ton/mod.rs b/rust/wallet_core_rs/src/ffi/ton/mod.rs new file mode 100644 index 00000000000..3ed4c72f589 --- /dev/null +++ b/rust/wallet_core_rs/src/ffi/ton/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address_converter; diff --git a/rust/wallet_core_rs/tests/ton_address_converter.rs b/rust/wallet_core_rs/tests/ton_address_converter.rs new file mode 100644 index 00000000000..203da896306 --- /dev/null +++ b/rust/wallet_core_rs/tests/ton_address_converter.rs @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_memory::test_utils::tw_string_helper::TWStringHelper; +use wallet_core_rs::ffi::ton::address_converter::{ + tw_ton_address_converter_from_boc, tw_ton_address_converter_to_boc, + tw_ton_address_converter_to_user_friendly, +}; + +struct GetJettonAddressInput { + main_address: &'static str, + expected_main_address_encoded: &'static str, + jetton_address_boc_encoded: &'static str, + expected_jetton_address: &'static str, +} + +fn test_address_converter_get_jetton_address(input: GetJettonAddressInput) { + let main_address = TWStringHelper::create(input.main_address); + + let main_address_boc_encoded_ptr = + unsafe { tw_ton_address_converter_to_boc(main_address.ptr()) }; + let main_address_boc_encoded = TWStringHelper::wrap(main_address_boc_encoded_ptr) + .to_string() + .unwrap(); + assert_eq!( + main_address_boc_encoded, + input.expected_main_address_encoded + ); + + // curl --location 'https://toncenter.com/api/v2/runGetMethod' --header 'Content-Type: application/json' --data \ + // '{"address":"JETTON_CONTRACT_ADDRESS","method":"get_wallet_address","method":"get_wallet_address","stack":[["tvm.Slice","YOUR_MAIN_ADDRESS"]]}' + + // `get_wallet_address` response: + let jetton_address_boc_encoded = TWStringHelper::create(input.jetton_address_boc_encoded); + let jetton_address_ptr = + unsafe { tw_ton_address_converter_from_boc(jetton_address_boc_encoded.ptr()) }; + let jetton_address_str = TWStringHelper::wrap(jetton_address_ptr) + .to_string() + .unwrap(); + assert_eq!(jetton_address_str, input.expected_jetton_address); +} + +#[test] +fn test_address_converter_get_jetton_notcoin_address() { + test_address_converter_get_jetton_address(GetJettonAddressInput { + main_address: "UQBjKqthWBE6GEcqb_epTRFrQ1niS6Z1Z1MHMwR-mnAYRoYr", + expected_main_address_encoded: + "te6cckEBAQEAJAAAQ4AMZVVsKwInQwjlTf71KaItaGs8SXTOrOpg5mCP004DCNAptHQU", + // 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","te6cckEBAQEAJAAAQ4AMZVVsKwInQwjlTf71KaItaGs8SXTOrOpg5mCP004DCNAptHQU"]]}' + jetton_address_boc_encoded: + "te6cckEBAQEAJAAAQ4AFvT5rqwxcbKfITqnkwL+go4Zi9bulRHAtLt4cjjFdK7B8L+Cq", + expected_jetton_address: "UQAt6fNdWGLjZT5CdU8mBf0FHDMXrd0qI4FpdvDkcYrpXV5H", + }); +} + +#[test] +fn test_address_converter_get_jetton_usdt_address() { + test_address_converter_get_jetton_address(GetJettonAddressInput { + main_address: "UQBjKqthWBE6GEcqb_epTRFrQ1niS6Z1Z1MHMwR-mnAYRoYr", + expected_main_address_encoded: + "te6cckEBAQEAJAAAQ4AMZVVsKwInQwjlTf71KaItaGs8SXTOrOpg5mCP004DCNAptHQU", + // 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","te6cckEBAQEAJAAAQ4AMZVVsKwInQwjlTf71KaItaGs8SXTOrOpg5mCP004DCNAptHQU"]]}' + jetton_address_boc_encoded: + "te6cckEBAQEAJAAAQ4Aed71FEI46jdFXghsGUIG2GIR8wpbQaLzrKNj7BtHOEHBSO5Mf", + expected_jetton_address: "UQDzveoohHHUboq8ENgyhA2wxCPmFLaDRedZRsfYNo5wg4TL", + }); +} + +#[test] +fn test_address_converter_get_jetton_ston_address() { + test_address_converter_get_jetton_address(GetJettonAddressInput { + main_address: "EQATQPeCwtMzQ9u54nTjUNcK4n_0VRSxPOOROLf_IE0OU3XK", + expected_main_address_encoded: + "te6cckEBAQEAJAAAQ4ACaB7wWFpmaHt3PE6cahrhXE/+iqKWJ5xyJxb/5AmhynDu6Ygj", + // 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","te6cckEBAQEAJAAAQ4ACaB7wWFpmaHt3PE6cahrhXE/+iqKWJ5xyJxb/5AmhynDu6Ygj"]]}' + jetton_address_boc_encoded: + "te6cckEBAQEAJAAAQ4ALPu0dyA1gHd3r7J1rxlvhXSvT5y3rokMDMiCQ86TsUJDnt69H", + expected_jetton_address: "UQBZ92juQGsA7u9fZOteMt8K6V6fOW9dEhgZkQSHnSdihHPH", + }); +} + +#[test] +fn test_address_converter_from_boc_null_address() { + // `get_wallet_address` response: + let null_address_boc_encoded = TWStringHelper::create("te6cckEBAQEAAwAAASCUQYZV"); + let jetton_address_ptr = + unsafe { tw_ton_address_converter_from_boc(null_address_boc_encoded.ptr()) }; + assert_eq!( + TWStringHelper::wrap(jetton_address_ptr) + .to_string() + .unwrap(), + "UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJKZ" + ); +} + +#[test] +fn test_address_converter_from_boc_error() { + #[track_caller] + fn test_impl(addr_boc_encoded: &str) { + let addr_boc_encoded_ptr = TWStringHelper::create(addr_boc_encoded); + let jetton_address_ptr = + unsafe { tw_ton_address_converter_from_boc(addr_boc_encoded_ptr.ptr()) }; + assert!( + jetton_address_ptr.is_null(), + "'{}' BoC encoded expected to be invalid", + addr_boc_encoded + ); + } + + // No type bit. + test_impl("te6cckEBAQEAAwAAAcCO6ba2"); + // No res1 and workchain bits. + test_impl("te6cckEBAQEAAwAAAaDsenDX"); + // Incomplete hash (31 bytes instead of 32). + test_impl("te6cckEBAQEAIwAAQYAgQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAUGJnJWk="); + // Expected 267 bits, found 268. + test_impl("te6cckEBAQEAJAAAQ4AgQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEgGG0Gq"); +} + +#[test] +fn test_address_converter_to_user_friendly() { + #[track_caller] + fn test_impl(addr: &str, bounceable: bool, testnet: bool, expected: &str) { + let addr_ptr = TWStringHelper::create(addr); + let actual_ptr = unsafe { + tw_ton_address_converter_to_user_friendly(addr_ptr.ptr(), bounceable, testnet) + }; + let actual_str = TWStringHelper::wrap(actual_ptr).to_string().unwrap(); + assert_eq!(actual_str, expected); + } + + let raw_address = "0:8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae"; + let bounceable = "EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorkdl"; + let non_bounceable = "UQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorhqg"; + let bounceable_testnet = "kQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorvzv"; + let non_bounceable_testnet = "0QCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorqEq"; + + // Raw to user friendly. + test_impl(raw_address, true, false, bounceable); + test_impl(raw_address, false, false, non_bounceable); + test_impl(raw_address, true, true, bounceable_testnet); + test_impl(raw_address, false, true, non_bounceable_testnet); + + // Bounceable to non-bounceable. + test_impl(bounceable, false, false, non_bounceable); + + // Non-bounceable to bounceable. + test_impl(non_bounceable, true, false, bounceable); + + // Non-bounceable to non-bounceable. + test_impl(non_bounceable, false, false, non_bounceable); +} + +#[test] +fn test_address_converter_to_user_friendly_error() { + #[track_caller] + fn test_impl(addr: &str, bounceable: bool, testnet: bool) { + let addr_ptr = TWStringHelper::create(addr); + let actual_ptr = unsafe { + tw_ton_address_converter_to_user_friendly(addr_ptr.ptr(), bounceable, testnet) + }; + assert!(actual_ptr.is_null()); + } + + // No "0:" prefix. + test_impl( + "8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae", + true, + false, + ); + + // Too short. + test_impl( + "EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsor", + false, + false, + ); +} diff --git a/samples/kmp/androidApp/build.gradle.kts b/samples/kmp/androidApp/build.gradle.kts index 24d6a52c14b..44b6129bc02 100644 --- a/samples/kmp/androidApp/build.gradle.kts +++ b/samples/kmp/androidApp/build.gradle.kts @@ -17,7 +17,7 @@ android { compose = true } composeOptions { - kotlinCompilerExtensionVersion = "1.4.0" + kotlinCompilerExtensionVersion = "1.5.12" } packagingOptions { resources { diff --git a/samples/kmp/build.gradle.kts b/samples/kmp/build.gradle.kts index 003bc3a5e90..ded20aa5f71 100644 --- a/samples/kmp/build.gradle.kts +++ b/samples/kmp/build.gradle.kts @@ -2,8 +2,8 @@ plugins { //trick: for the same plugin versions in all sub-modules id("com.android.application").version("7.4.1").apply(false) id("com.android.library").version("7.4.1").apply(false) - kotlin("android").version("1.8.0").apply(false) - kotlin("multiplatform").version("1.8.0").apply(false) + kotlin("android").version("1.9.23").apply(false) + kotlin("multiplatform").version("1.9.23").apply(false) } tasks.register("clean", Delete::class) { diff --git a/src/TheOpenNetwork/Address.cpp b/src/TheOpenNetwork/Address.cpp deleted file mode 100644 index eb25f25aa2d..00000000000 --- a/src/TheOpenNetwork/Address.cpp +++ /dev/null @@ -1,153 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Address.h" - -#include "Base64.h" -#include "Crc.h" -#include "Everscale/CommonTON/CellBuilder.h" - -#include "WorkchainType.h" - -namespace TW::TheOpenNetwork { - -using AddressImpl = TW::CommonTON::RawAddress; - -static inline Data decodeUserFriendlyAddress(const std::string& string) { - Data decoded; - if (string.find('-') != std::string::npos || string.find('_') != std::string::npos) { - decoded = Base64::decodeBase64Url(string); - } else { - decoded = Base64::decode(string); - } - return decoded; -} - -bool Address::isValid(const std::string& string) noexcept { - if (AddressImpl::isValid(string)) { - return true; - } - - if (string.size() != b64UserFriendlyAddressLen) { - return false; - } - - if (!Base64::isBase64orBase64Url(string)) { - return false; - } - Data decoded = decodeUserFriendlyAddress(string); - - if (decoded.size() != userFriendlyAddressLen) { - return false; - } - - byte tag = decoded[0]; - if (tag & AddressTag::TEST_ONLY) { - tag ^= AddressTag::TEST_ONLY; - } - - if (tag != AddressTag::BOUNCEABLE && tag != AddressTag::NON_BOUNCEABLE) { - return false; - } - - int8_t workchainId = decoded[1]; - if (workchainId != WorkchainType::Basechain && workchainId != WorkchainType::Masterchain) { - return false; - } - - Data data(decoded.begin(), decoded.end() - 2); - Data givenCrc(decoded.end() - 2, decoded.end()); - - const uint16_t crc16 = Crc::crc16(data.data(), (uint32_t) data.size()); - const byte b1 = (crc16 >> 8) & 0xff; - const byte b2 = crc16 & 0xff; - if (b1 != givenCrc[0] || b2 != givenCrc[1]) { - return false; - } - - return true; -} - -Address::Address(const std::string& string, std::optional bounceable) { - if (!Address::isValid(string)) { - throw std::invalid_argument("Invalid address string"); - } - - if (string.find(':') != std::string::npos) { - addressData = AddressImpl::splitAddress(string); - } else { - isUserFriendly = true; - - Data decoded = decodeUserFriendlyAddress(string); - addressData.workchainId = decoded[1]; - - if (!bounceable.has_value()) { - byte tag = decoded[0]; - if (tag & AddressTag::TEST_ONLY) { - isTestOnly = true; - tag ^= AddressTag::TEST_ONLY; - } - isBounceable = (tag == AddressTag::BOUNCEABLE); - } else { - isBounceable = *bounceable; - } - - std::copy(decoded.begin() + 2, decoded.end() - 2, addressData.hash.begin()); - } -} - -std::string Address::string() const { - return this->string(isUserFriendly, isBounceable, isTestOnly); -} - -std::string Address::string(bool userFriendly, bool bounceable, bool testOnly) const { - if (!userFriendly) { - return AddressImpl::to_string(addressData); - } - - Data data; - Data hashData(addressData.hash.begin(), addressData.hash.end()); - - byte tag = bounceable ? AddressTag::BOUNCEABLE : AddressTag::NON_BOUNCEABLE; - if (testOnly) { - tag |= AddressTag::TEST_ONLY; - } - - append(data, tag); - append(data, addressData.workchainId); - append(data, hashData); - - const uint16_t crc16 = Crc::crc16(data.data(), (uint32_t) data.size()); - append(data, (crc16 >> 8) & 0xff); - append(data, crc16 & 0xff); - - 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 deleted file mode 100644 index 15b85bbd00a..00000000000 --- a/src/TheOpenNetwork/Address.h +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "Data.h" -#include "PublicKey.h" - -#include "Everscale/CommonTON/RawAddress.h" -#include - -#include - -namespace TW::TheOpenNetwork { - -enum AddressTag : uint8_t { - BOUNCEABLE = 0x11, - NON_BOUNCEABLE = 0x51, - TEST_ONLY = 0x80, -}; - -using AddressData = CommonTON::AddressData; - -class Address { -public: - AddressData addressData; - - /// User-friendly address lens - static const size_t b64UserFriendlyAddressLen = 48; - static const size_t userFriendlyAddressLen = 36; - - /// Determines whether the address is user-friendly - bool isUserFriendly = false; - - /// Determines whether the address is bounceable - bool isBounceable = false; - - /// Determines whether the address is for tests only - bool isTestOnly = false; - - /// Determines whether a string makes a valid address. - [[nodiscard]] static bool isValid(const std::string& string) noexcept; - - /// Initializes an address with a string representation. - explicit Address(const std::string& string, std::optional bounceable = std::nullopt); - - /// Initializes an address with its parts - explicit Address( - int8_t workchainId, std::array hash, - bool userFriendly = true, bool bounceable = false, bool testOnly = false - ) : addressData(workchainId, hash), - isUserFriendly(userFriendly), - isBounceable(bounceable), - isTestOnly(testOnly) {} - - /// 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/TheOpenNetwork/Entry.cpp b/src/TheOpenNetwork/Entry.cpp deleted file mode 100644 index 0cf95eb5ec6..00000000000 --- a/src/TheOpenNetwork/Entry.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Entry.h" - -#include "Address.h" -#include "Signer.h" - -#include "TheOpenNetwork/wallet/WalletV4R2.h" -#include "WorkchainType.h" - -namespace TW::TheOpenNetwork { - -bool Entry::validateAddress([[maybe_unused]] TWCoinType coin, [[maybe_unused]] const std::string& address, [[maybe_unused]] const PrefixVariant& addressPrefix) const { - return Address::isValid(address); -} - -std::string Entry::normalizeAddress([[maybe_unused]] TWCoinType coin, const std::string& address) const { - return Address(address).string(true, false, false); -} - -std::string Entry::deriveAddress([[maybe_unused]] TWCoinType coin, const PublicKey& publicKey, [[maybe_unused]] TWDerivation derivation, [[maybe_unused]] const PrefixVariant& addressPrefix) const { - return WalletV4R2(publicKey, WorkchainType::Basechain).getAddress().string(); -} - -void Entry::sign([[maybe_unused]] TWCoinType coin, const TW::Data& dataIn, TW::Data& dataOut) const { - signTemplate(dataIn, dataOut); -} - -} // namespace TW::TheOpenNetwork diff --git a/src/TheOpenNetwork/Entry.h b/src/TheOpenNetwork/Entry.h index cdcf623681d..ad187a83b6a 100644 --- a/src/TheOpenNetwork/Entry.h +++ b/src/TheOpenNetwork/Entry.h @@ -4,16 +4,11 @@ #pragma once -#include "CoinEntry.h" +#include "rust/RustCoinEntry.h" namespace TW::TheOpenNetwork { -class Entry final : public CoinEntry { -public: - bool validateAddress(TWCoinType coin, const std::string& address, const PrefixVariant& addressPrefix) const; - std::string normalizeAddress(TWCoinType coin, const std::string& address) const; - std::string deriveAddress(TWCoinType coin, const PublicKey& publicKey, TWDerivation derivation, const PrefixVariant& addressPrefix) const; - void sign(TWCoinType coin, const Data& dataIn, Data& dataOut) const; +class Entry final : public Rust::RustCoinEntry { }; } // namespace TW::TheOpenNetwork diff --git a/src/TheOpenNetwork/Message.cpp b/src/TheOpenNetwork/Message.cpp deleted file mode 100644 index 05321503dc4..00000000000 --- a/src/TheOpenNetwork/Message.cpp +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Message.h" - -namespace TW::TheOpenNetwork { - -Cell::Ref Message::intoCell() const { - CellBuilder builder; - - // info:CommonMsgInfo - _messageData.header->writeTo(builder); - - // init:(Maybe (Either StateInit ^StateInit)) - if (_messageData.init.has_value()) { - builder.appendBitOne(); // Maybe - - builder.appendBitOne(); // Either as ^X - builder.appendReferenceCell(_messageData.init.value().writeTo().intoCell()); - } else { - builder.appendBitZero(); - } - - // body:(Either X ^X) - if (_messageData.body.has_value()) { - builder.appendBitOne(); // Either as ^X - builder.appendReferenceCell(_messageData.body.value()); - } else { - builder.appendBitZero(); - } - - return builder.intoCell(); -} - -} // namespace TW::TheOpenNetwork diff --git a/src/TheOpenNetwork/Message.h b/src/TheOpenNetwork/Message.h deleted file mode 100644 index cdbba14e416..00000000000 --- a/src/TheOpenNetwork/Message.h +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "Everscale/CommonTON/Messages.h" - -using namespace TW::CommonTON; - -namespace TW::TheOpenNetwork { - -class Message { -private: - MessageData _messageData; - -public: - explicit Message(MessageData messageData) : _messageData(std::move(messageData)) {} - - [[nodiscard]] Cell::Ref intoCell() const; - - inline void setBody(const Cell::Ref& body) { _messageData.body = body; } - inline void setStateInit(const StateInit& stateInit) { _messageData.init = stateInit; } -}; - -} // namespace TW::TheOpenNetwork diff --git a/src/TheOpenNetwork/Payloads.cpp b/src/TheOpenNetwork/Payloads.cpp deleted file mode 100644 index 460de3fa919..00000000000 --- a/src/TheOpenNetwork/Payloads.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Payloads.h" - - -namespace TW::TheOpenNetwork { - -const uint64_t jettonTransferOperation = 0xf8a7ea5; - -using TW::CommonTON::Cell; -using TW::CommonTON::CellBuilder; - -Cell::Ref jettonTransferPayload( - const Address& responseAddress, - const Address& toOwner, - uint64_t jettonAmount, - uint64_t forwardAmount, - const std::string& comment, - uint64_t query_id -) { - CellBuilder bodyBuilder; - bodyBuilder.appendU32(jettonTransferOperation); - bodyBuilder.appendU64(query_id); - bodyBuilder.appendU128(jettonAmount); - bodyBuilder.appendAddress(toOwner.addressData); - bodyBuilder.appendAddress(responseAddress.addressData); - bodyBuilder.appendBitZero(); // null custom_payload - bodyBuilder.appendU128(forwardAmount); - bodyBuilder.appendBitZero(); // forward_payload in this slice, not separate cell - if (!comment.empty()) { - const auto& data = Data(comment.begin(), comment.end()); - bodyBuilder.appendU32(0); - bodyBuilder.appendRaw(data, static_cast(data.size()) * 8); - } - return bodyBuilder.intoCell(); -} - -} // namespace TW::TheOpenNetwork \ No newline at end of file diff --git a/src/TheOpenNetwork/Payloads.h b/src/TheOpenNetwork/Payloads.h deleted file mode 100644 index e65319efac0..00000000000 --- a/src/TheOpenNetwork/Payloads.h +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Everscale/CommonTON/CellBuilder.h" - -#include "TheOpenNetwork/Address.h" - -namespace TW::TheOpenNetwork { - TW::CommonTON::Cell::Ref jettonTransferPayload( - const Address& responseAddress, - const Address& toOwner, - uint64_t jettonAmount, - uint64_t forwardAmount, - const std::string& comment, - uint64_t query_id); -} // namespace TW::TheOpenNetwork \ No newline at end of file diff --git a/src/TheOpenNetwork/Signer.cpp b/src/TheOpenNetwork/Signer.cpp deleted file mode 100644 index 2e946bf9904..00000000000 --- a/src/TheOpenNetwork/Signer.cpp +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Signer.h" - -#include "Base64.h" - -#include "TheOpenNetwork/wallet/WalletV4R2.h" -#include "TheOpenNetwork/Payloads.h" -#include "WorkchainType.h" - -namespace TW::TheOpenNetwork { - -Data Signer::createTransferMessage(std::shared_ptr wallet, const PrivateKey& privateKey, const Proto::Transfer& transfer) { - const auto msg = wallet->createTransferMessage( - privateKey, - Address(transfer.dest(), transfer.bounceable()), - transfer.amount(), - transfer.sequence_number(), - static_cast(transfer.mode()), - transfer.expire_at(), - transfer.comment() - ); - - Data result{}; - msg->serialize(result); - return result; -} - -Data Signer::createJettonTransferMessage(std::shared_ptr wallet, const PrivateKey& privateKey, const Proto::JettonTransfer& jettonTransfer) { - const Proto::Transfer& transferData = jettonTransfer.transfer(); - - const auto payload = jettonTransferPayload( - Address(jettonTransfer.response_address()), - Address(jettonTransfer.to_owner()), - jettonTransfer.jetton_amount(), - jettonTransfer.forward_amount(), - transferData.comment(), - jettonTransfer.query_id() - ); - - const auto msg = wallet->createQueryMessage( - privateKey, - Address(transferData.dest(), transferData.bounceable()), - transferData.amount(), - transferData.sequence_number(), - static_cast(transferData.mode()), - payload, - transferData.expire_at() - ); - - Data result{}; - msg->serialize(result); - return result; -} - -Proto::SigningOutput Signer::sign(const Proto::SigningInput &input) noexcept { - const auto& privateKey = PrivateKey(input.private_key()); - const auto& publicKey = privateKey.getPublicKey(TWPublicKeyTypeED25519); - - auto protoOutput = Proto::SigningOutput(); - - switch (input.action_oneof_case()) { - case Proto::SigningInput::ActionOneofCase::kTransfer: { - const auto& transfer = input.transfer(); - - try { - switch (transfer.wallet_version()) { - case Proto::WalletVersion::WALLET_V4_R2: { - const int8_t workchainId = WorkchainType::Basechain; - auto wallet = std::make_shared(publicKey, workchainId); - const auto& transferMessage = Signer::createTransferMessage(wallet, privateKey, transfer); - protoOutput.set_encoded(TW::Base64::encode(transferMessage)); - break; - } - default: - protoOutput.set_error(Common::Proto::Error_invalid_params); - protoOutput.set_error_message("Unsupported wallet version"); - break; - } - } catch (...) { } - break; - } - case Proto::SigningInput::ActionOneofCase::kJettonTransfer: { - const auto& jettonTransfer = input.jetton_transfer(); - try { - switch (jettonTransfer.transfer().wallet_version()) { - case Proto::WalletVersion::WALLET_V4_R2: { - const int8_t workchainId = WorkchainType::Basechain; - auto wallet = std::make_shared(publicKey, workchainId); - const auto& transferMessage = Signer::createJettonTransferMessage(wallet, privateKey, jettonTransfer); - protoOutput.set_encoded(TW::Base64::encode(transferMessage)); - break; - } - default: - protoOutput.set_error(Common::Proto::Error_invalid_params); - protoOutput.set_error_message("Unsupported wallet version"); - break; - } - } catch (...) { } - } - default: - break; - } - return protoOutput; -} - -} // namespace TW::TheOpenNetwork diff --git a/src/TheOpenNetwork/Signer.h b/src/TheOpenNetwork/Signer.h deleted file mode 100644 index 656bb2ed7aa..00000000000 --- a/src/TheOpenNetwork/Signer.h +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "Data.h" -#include "PrivateKey.h" -#include "wallet/Wallet.h" - -#include "proto/TheOpenNetwork.pb.h" - -namespace TW::TheOpenNetwork { - -/// Helper class that performs TheOpenNetwork transaction signing. -class Signer { -public: - /// Hide default constructor - Signer() = delete; - - /// Creates a signed transfer message - static Data createTransferMessage(std::shared_ptr wallet, const PrivateKey& privateKey, const Proto::Transfer& transfer); - - /// Creates a signed jetton transfer message - static Data createJettonTransferMessage(std::shared_ptr wallet, const PrivateKey& privateKey, const Proto::JettonTransfer& transfer); - - /// Signs a Proto::SigningInput transaction - static Proto::SigningOutput sign(const Proto::SigningInput& input) noexcept; -}; - -} // namespace TW::TheOpenNetwork diff --git a/src/TheOpenNetwork/WorkchainType.h b/src/TheOpenNetwork/WorkchainType.h deleted file mode 100644 index 3947f93f21a..00000000000 --- a/src/TheOpenNetwork/WorkchainType.h +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "Everscale/CommonTON/WorkchainType.h" - -namespace TW::TheOpenNetwork { - using WorkchainType = CommonTON::WorkchainType; -} // namespace TW::TheOpenNetwork diff --git a/src/TheOpenNetwork/wallet/Wallet.cpp b/src/TheOpenNetwork/wallet/Wallet.cpp deleted file mode 100644 index 409f6aba187..00000000000 --- a/src/TheOpenNetwork/wallet/Wallet.cpp +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Wallet.h" - -#include "HexCoding.h" - -namespace TW::TheOpenNetwork { - -static const uint32_t standard_wallet_id = 698983191; - -Wallet::Wallet(PublicKey publicKey, int8_t workchainId, Data walletCode) - : publicKey(std::move(publicKey)) - , workchainId(workchainId) - , walletCode(std::move(walletCode)) - , walletId(standard_wallet_id + workchainId) { -} - -Address Wallet::getAddress() const { - const auto stateInit = this->createStateInit(); - return Address(workchainId, stateInit.writeTo().intoCell()->hash); -} - -CommonTON::StateInit Wallet::createStateInit() const { - Cell::Ref code = Cell::deserialize(walletCode.data(), walletCode.size()); - Cell::Ref data = this->createDataCell(); - return StateInit{code, data}; -} - -Cell::Ref Wallet::createSigningMessage( - const Address& dest, - uint64_t amount, - uint32_t sequence_number, - uint8_t mode, - const Cell::Ref& queryPayload, - uint32_t expireAt -) const { - CellBuilder builder; - this->writeSigningPayload(builder, sequence_number, expireAt); - builder.appendU8(mode); - - { // Add internal message as a reference cell - const auto header = std::make_shared(true, dest.isBounceable, dest.addressData, amount); - TheOpenNetwork::Message internalMessage = TheOpenNetwork::Message(MessageData(header)); - - internalMessage.setBody(queryPayload); - - builder.appendReferenceCell(internalMessage.intoCell()); - } - - return builder.intoCell(); -} - -Cell::Ref Wallet::createQueryMessage( - const PrivateKey& privateKey, - const Address& dest, - uint64_t amount, - uint32_t sequence_number, - uint8_t mode, - const Cell::Ref& queryPayload, - uint32_t expireAt -) const { - const auto transferMessageHeader = std::make_shared(this->getAddress().addressData); - Message transferMessage = Message(MessageData(transferMessageHeader)); - if (sequence_number == 0) { - const auto stateInit = this->createStateInit(); - transferMessage.setStateInit(stateInit); - } - - { // Set body of transfer message - CellBuilder bodyBuilder; - const Cell::Ref signingMessage = this->createSigningMessage(dest, amount, sequence_number, mode, queryPayload, expireAt); - Data data(signingMessage->hash.begin(), signingMessage->hash.end()); - const auto signature = privateKey.sign(data, TWCurveED25519); - - bodyBuilder.appendRaw(signature, static_cast(signature.size()) * 8); - bodyBuilder.appendCellSlice(CellSlice(signingMessage.get())); - - transferMessage.setBody(bodyBuilder.intoCell()); - } - - return transferMessage.intoCell(); -} - - -Cell::Ref Wallet::createTransferMessage( - const PrivateKey& privateKey, - const Address& dest, - uint64_t amount, - uint32_t sequence_number, - uint8_t mode, - uint32_t expireAt, - const std::string& comment -) const { - CellBuilder bodyBuilder; - if (!comment.empty()) { - const auto& data = Data(comment.begin(), comment.end()); - bodyBuilder.appendU32(0); - bodyBuilder.appendRaw(data, static_cast(data.size()) * 8); - } - return createQueryMessage(privateKey, dest, amount, sequence_number, mode, bodyBuilder.intoCell(), expireAt); -} - - -} // namespace TW::TheOpenNetwork diff --git a/src/TheOpenNetwork/wallet/Wallet.h b/src/TheOpenNetwork/wallet/Wallet.h deleted file mode 100644 index 356d2be1d4e..00000000000 --- a/src/TheOpenNetwork/wallet/Wallet.h +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "PublicKey.h" -#include "PrivateKey.h" - -#include "TheOpenNetwork/Address.h" -#include "TheOpenNetwork/Message.h" - -#include "Everscale/CommonTON/Messages.h" - -namespace TW::TheOpenNetwork { - -class Wallet { -protected: - PublicKey publicKey; - int8_t workchainId; - - // Explore standard codes: https://github.com/toncenter/tonweb/blob/master/src/contract/wallet/WalletSources.md - const Data walletCode; - const uint32_t walletId; - -public: - explicit Wallet(PublicKey publicKey, int8_t workchainId, Data walletCode); - virtual ~Wallet() noexcept = default; - - [[nodiscard]] Address getAddress() const; - [[nodiscard]] Cell::Ref createTransferMessage( - const PrivateKey& privateKey, - const Address& dest, - uint64_t amount, - uint32_t sequence_number, - uint8_t mode, - uint32_t expireAt = 0, - const std::string& comment = "" - ) const; - - [[nodiscard]] Cell::Ref createQueryMessage( - const PrivateKey& privateKey, - const Address& dest, - uint64_t amount, - uint32_t sequence_number, - uint8_t mode, - const Cell::Ref& payload, - uint32_t expireAt = 0 - ) const; - -protected: - [[nodiscard]] virtual Cell::Ref createDataCell() const = 0; - virtual void writeSigningPayload(CellBuilder& builder, uint32_t sequence_number = 0, uint32_t expireAt = 0) const = 0; - -private: - [[nodiscard]] Cell::Ref createSigningMessage( - const Address& dest, - uint64_t amount, - uint32_t sequence_number, - uint8_t mode, - const Cell::Ref& payload, - uint32_t expireAt = 0 - ) const; - [[nodiscard]] CommonTON::StateInit createStateInit() const; -}; - -} // namespace TW::TheOpenNetwork diff --git a/src/TheOpenNetwork/wallet/WalletV4R2.cpp b/src/TheOpenNetwork/wallet/WalletV4R2.cpp deleted file mode 100644 index bb96f5a2823..00000000000 --- a/src/TheOpenNetwork/wallet/WalletV4R2.cpp +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "WalletV4R2.h" - -#include "HexCoding.h" - -#include - -namespace TW::TheOpenNetwork { - -// WalletV4R2 contract func https://github.com/ton-blockchain/wallet-contract/blob/4111fd9e3313ec17d99ca9b5b1656445b5b49d8f/func/wallet-v4-code.fc -// Contract codes https://github.com/toncenter/tonweb/blob/master/src/contract/wallet/WalletSources.md -const Data WalletV4R2::code = parse_hex("B5EE9C72410214010002D4000114FF00F4A413F4BCF2C80B010201200203020148040504F8F28308D71820D31FD31FD31F02F823BBF264ED44D0D31FD31FD3FFF404D15143BAF2A15151BAF2A205F901541064F910F2A3F80024A4C8CB1F5240CB1F5230CBFF5210F400C9ED54F80F01D30721C0009F6C519320D74A96D307D402FB00E830E021C001E30021C002E30001C0039130E30D03A4C8CB1F12CB1FCBFF1011121302E6D001D0D3032171B0925F04E022D749C120925F04E002D31F218210706C7567BD22821064737472BDB0925F05E003FA403020FA4401C8CA07CBFFC9D0ED44D0810140D721F404305C810108F40A6FA131B3925F07E005D33FC8258210706C7567BA923830E30D03821064737472BA925F06E30D06070201200809007801FA00F40430F8276F2230500AA121BEF2E0508210706C7567831EB17080185004CB0526CF1658FA0219F400CB6917CB1F5260CB3F20C98040FB0006008A5004810108F45930ED44D0810140D720C801CF16F400C9ED540172B08E23821064737472831EB17080185005CB055003CF1623FA0213CB6ACB1FCB3FC98040FB00925F03E20201200A0B0059BD242B6F6A2684080A06B90FA0218470D4080847A4937D29910CE6903E9FF9837812801B7810148987159F31840201580C0D0011B8C97ED44D0D70B1F8003DB29DFB513420405035C87D010C00B23281F2FFF274006040423D029BE84C600201200E0F0019ADCE76A26840206B90EB85FFC00019AF1DF6A26840106B90EB858FC0006ED207FA00D4D422F90005C8CA0715CBFFC9D077748018C8CB05CB0222CF165005FA0214CB6B12CCCCC973FB00C84014810108F451F2A7020070810108D718FA00D33FC8542047810108F451F2A782106E6F746570748018C8CB05CB025006CF165004FA0214CB6A12CB1FCB3FC973FB0002006C810108D718FA00D33F305224810108F459F2A782106473747270748018C8CB05CB025005CF165003FA0213CB6ACB1F12CB3FC973FB00000AF400C9ED54696225E5"); - -WalletV4R2::WalletV4R2(PublicKey publicKey, int8_t workchainId) - : Wallet( - std::move(publicKey), - workchainId, - WalletV4R2::code - ) { -} - -Cell::Ref WalletV4R2::createDataCell() const { - CellBuilder builder; - - builder.appendU32(0); // sequence_number - builder.appendU32(walletId); - builder.appendRaw(publicKey.bytes, 256); - builder.appendBitZero(); // no plugins - - return builder.intoCell(); -} - -void WalletV4R2::writeSigningPayload(CellBuilder& builder, uint32_t sequence_number, uint32_t expireAt) const { - builder.appendU32(walletId); - if (sequence_number == 0) { - builder.appendU32(0xffffffff); - } else { - if (expireAt == 0) { - expireAt = (uint32_t) duration_cast( - std::chrono::system_clock::now().time_since_epoch() - ).count() + 60; // TON v4 wallet requires uint32 for now - } - builder.appendU32(expireAt); - } - builder.appendU32(sequence_number); - builder.appendU8(0); -} - -} // namespace TW::TheOpenNetwork diff --git a/src/TheOpenNetwork/wallet/WalletV4R2.h b/src/TheOpenNetwork/wallet/WalletV4R2.h deleted file mode 100644 index e77384a9eac..00000000000 --- a/src/TheOpenNetwork/wallet/WalletV4R2.h +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "Wallet.h" - -namespace TW::TheOpenNetwork { - -class WalletV4R2 : public Wallet { -public: - explicit WalletV4R2(PublicKey publicKey, int8_t workchainId); - - static const Data code; - -private: - [[nodiscard]] Cell::Ref createDataCell() const override; - void writeSigningPayload(CellBuilder& builder, uint32_t sequence_number = 0, uint32_t expireAt = 0) const override; -}; - -} // namespace TW::TheOpenNetwork diff --git a/src/interface/TWTONAddressConverter.cpp b/src/interface/TWTONAddressConverter.cpp index c9d2a26695f..4113448ad24 100644 --- a/src/interface/TWTONAddressConverter.cpp +++ b/src/interface/TWTONAddressConverter.cpp @@ -5,52 +5,42 @@ #include #include "Base64.h" -#include "TheOpenNetwork/Address.h" +#include "rust/Wrapper.h" using namespace TW; TWString *_Nullable TWTONAddressConverterToBoc(TWString *_Nonnull address) { auto& addressString = *reinterpret_cast(address); - try { - const TheOpenNetwork::Address addressTon(addressString); - auto bocEncoded = addressTon.toBoc(); - return TWStringCreateWithUTF8Bytes(bocEncoded.c_str()); - } catch (...) { + const Rust::TWStringWrapper addressRustStr = addressString; + const Rust::TWStringWrapper bocRustStr = Rust::tw_ton_address_converter_to_boc(addressRustStr.get()); + if (!bocRustStr) { return nullptr; } + + return TWStringCreateWithUTF8Bytes(bocRustStr.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 (...) { + const Rust::TWStringWrapper bocRustStr = bocEncoded; + const Rust::TWStringWrapper addressRustStr = Rust::tw_ton_address_converter_from_boc(bocRustStr.get()); + if (!addressRustStr) { return nullptr; } + + return TWStringCreateWithUTF8Bytes(addressRustStr.c_str()); } TWString *_Nullable TWTONAddressConverterToUserFriendly(TWString *_Nonnull address, bool bounceable, bool testnet) { auto& addressString = *reinterpret_cast(address); - try { - const TheOpenNetwork::Address addressTon(addressString); - - auto userFriendly = true; - const auto addressFormatted = addressTon.string(userFriendly, bounceable, testnet); - - return TWStringCreateWithUTF8Bytes(addressFormatted.c_str()); - } catch (...) { + const Rust::TWStringWrapper addressRustStr = addressString; + const Rust::TWStringWrapper userFriendlyRustStr = Rust::tw_ton_address_converter_to_user_friendly(addressRustStr.get(), bounceable, testnet); + if (!userFriendlyRustStr) { return nullptr; } + + return TWStringCreateWithUTF8Bytes(userFriendlyRustStr.c_str()); } diff --git a/src/proto/TheOpenNetwork.proto b/src/proto/TheOpenNetwork.proto index 1547554301b..cc2424b9a75 100644 --- a/src/proto/TheOpenNetwork.proto +++ b/src/proto/TheOpenNetwork.proto @@ -34,54 +34,69 @@ message Transfer { // Amount to send in nanotons uint64 amount = 3; - // Message counter (optional, 0 by default used for the first deploy) - // This field is required, because we need to protect the smart contract against "replay attacks" - // Learn more: https://ton.org/docs/develop/smart-contracts/guidelines/external-messages - uint32 sequence_number = 4; - // Send mode (optional, 0 by default) // Learn more: https://ton.org/docs/develop/func/stdlib#send_raw_message - uint32 mode = 5; - - // Expiration UNIX timestamp (optional, now() + 60 by default) - uint32 expire_at = 6; + uint32 mode = 4; // Transfer comment message (optional, empty by default) - string comment = 7; + // Ignored if `custom_payload` is specified + string comment = 5; // If the address is bounceable - bool bounceable = 8; + bool bounceable = 6; + + // One of the Transfer message payloads (optional). + oneof payload { + // Jetton transfer payload. + JettonTransfer jetton_transfer = 7; + // TON transfer with custom stateInit and payload (contract call). + CustomPayload custom_payload = 8; + } } message JettonTransfer { - // Dest in Transfer means contract address of sender's jetton wallet. - Transfer transfer = 1; - - // Arbitrary request number. Deafult is 0. Optional field. - uint64 query_id = 2; + // Arbitrary request number. Default is 0. Optional field. + uint64 query_id = 1; // Amount of transferred jettons in elementary integer units. The real value transferred is jetton_amount multiplied by ten to the power of token decimal precision - uint64 jetton_amount = 3; + uint64 jetton_amount = 2; // Address of the new owner of the jettons. - string to_owner = 4; + string to_owner = 3; // Address where to send a response with confirmation of a successful transfer and the rest of the incoming message Toncoins. Usually the sender should get back their toncoins. - string response_address = 5; + string response_address = 4; // Amount in nanotons to forward to recipient. Basically minimum amount - 1 nanoton should be used - uint64 forward_amount = 6; + uint64 forward_amount = 5; +} + +message CustomPayload { + // (string base64, optional): raw one-cell BoC encoded in Base64. + // Can be used to deploy a smart contract. + string state_init = 1; + + // (string base64, optional): raw one-cell BoC encoded in Base64. + string payload = 2; } message SigningInput { // The secret private key used for signing (32 bytes). bytes private_key = 1; - // The payload transfer - oneof action_oneof { - Transfer transfer = 2; - JettonTransfer jetton_transfer = 3; - } + // Public key of the signer (32 bytes). Used when transaction is going to be signed externally. + bytes public_key = 2; + + // Up to 4 internal messages. + repeated Transfer messages = 3; + + // Message counter (optional, 0 by default used for the first deploy) + // This field is required, because we need to protect the smart contract against "replay attacks" + // Learn more: https://ton.org/docs/develop/smart-contracts/guidelines/external-messages + uint32 sequence_number = 4; + + // Expiration UNIX timestamp (optional, now() + 60 by default) + uint32 expire_at = 5; } // Transaction signing output. @@ -89,9 +104,12 @@ message SigningOutput { // Signed and base64 encoded BOC message string encoded = 1; + // Transaction Cell hash + bytes hash = 2; + // error code, 0 is ok, other codes will be treated as errors - Common.Proto.SigningError error = 2; + Common.Proto.SigningError error = 3; // error code description - string error_message = 3; + string error_message = 4; } diff --git a/swift/Tests/Blockchains/TheOpenNetworkTests.swift b/swift/Tests/Blockchains/TheOpenNetworkTests.swift index e8a5faf5dd6..10ef0f7725e 100644 --- a/swift/Tests/Blockchains/TheOpenNetworkTests.swift +++ b/swift/Tests/Blockchains/TheOpenNetworkTests.swift @@ -48,7 +48,7 @@ class TheOpenNetworkTests: XCTestCase { func testGenerateJettonAddress() { let mainAddress = "UQBjKqthWBE6GEcqb_epTRFrQ1niS6Z1Z1MHMwR-mnAYRoYr" let mainAddressBoc = TONAddressConverter.toBoc(address: mainAddress) - XCTAssertEqual(mainAddressBoc, "te6ccgICAAEAAQAAACQAAABDgAxlVWwrAidDCOVN/vUpoi1oazxJdM6s6mDmYI/TTgMI0A==") + XCTAssertEqual(mainAddressBoc, "te6cckEBAQEAJAAAQ4AMZVVsKwInQwjlTf71KaItaGs8SXTOrOpg5mCP004DCNAptHQU") // 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=="]]}' @@ -66,56 +66,100 @@ class TheOpenNetworkTests: XCTestCase { $0.walletVersion = TheOpenNetworkWalletVersion.walletV4R2 $0.dest = "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q" $0.amount = 10 - $0.sequenceNumber = 6 $0.mode = UInt32(TheOpenNetworkSendMode.payFeesSeparately.rawValue | TheOpenNetworkSendMode.ignoreActionPhaseErrors.rawValue) - $0.expireAt = 1671132440 $0.bounceable = true } let input = TheOpenNetworkSigningInput.with { - $0.transfer = transfer + $0.messages = [transfer] $0.privateKey = privateKeyData + $0.sequenceNumber = 6 + $0.expireAt = 1671132440 } let output: TheOpenNetworkSigningOutput = AnySigner.sign(input: input, coin: .ton) // tx: https://tonscan.org/tx/3Z4tHpXNLyprecgu5aTQHWtY7dpHXEoo11MAX61Xyg0= - let expectedString = "te6ccgICAAQAAQAAALAAAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGcEUPkil2aZ4s8KKparSep/OKHMC8vuXafFbW2HGp/9AcTRv0J5T4dwyW1G0JpHw+g5Ov6QI3Xo0O9RFr3KidICimpoxdjm3UYAAAABgADAAIBYmIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmIUAAAAAAAAAAAAAAAAAEAAwAA" + let expectedString = "te6cckEBBAEArQABRYgBsLf6vJOEq42xW0AoyWX0K+uBMUcXFDLFqmkDg6k1Io4MAQGcEUPkil2aZ4s8KKparSep/OKHMC8vuXafFbW2HGp/9AcTRv0J5T4dwyW1G0JpHw+g5Ov6QI3Xo0O9RFr3KidICimpoxdjm3UYAAAABgADAgFiYgAzffHi4B365BPJfIJk/F+URKU1UekJ6g4QK02ypVb22YhQAAAAAAAAAAAAAAAAAQMAAA08Nzs=" XCTAssertEqual(output.encoded, expectedString) } func testJettonTransferSign() { let privateKeyData = Data(hexString: "c054900a527538c1b4325688a421c0469b171c29f23a62da216e90b0df2412ee")! - - let transferData = TheOpenNetworkTransfer.with { + + let jettonTransfer = TheOpenNetworkJettonTransfer.with { + $0.jettonAmount = 500 * 1000 * 1000 + $0.toOwner = "EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8" + $0.responseAddress = "EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk" + $0.forwardAmount = 1 + } + + let transfer = TheOpenNetworkTransfer.with { $0.walletVersion = TheOpenNetworkWalletVersion.walletV4R2 $0.dest = "EQBiaD8PO1NwfbxSkwbcNT9rXDjqhiIvXWymNO-edV0H5lja" $0.amount = 100 * 1000 * 1000 - $0.sequenceNumber = 1 $0.mode = UInt32(TheOpenNetworkSendMode.payFeesSeparately.rawValue | TheOpenNetworkSendMode.ignoreActionPhaseErrors.rawValue) - $0.expireAt = 1787693046 $0.comment = "test comment" $0.bounceable = true + $0.jettonTransfer = jettonTransfer + } + + let input = TheOpenNetworkSigningInput.with { + $0.messages = [transfer] + $0.privateKey = privateKeyData + $0.sequenceNumber = 1 + $0.expireAt = 1787693046 } + + let output: TheOpenNetworkSigningOutput = AnySigner.sign(input: input, coin: .ton) + + // tx: https://testnet.tonscan.org/tx/Er_oT5R3QK7D-qVPBKUGkJAOOq6ayVls-mgEphpI9Ck= + let expectedString = "te6cckECBAEAARUAAUWIALRRBlXIE21P9b6OpAqeFhqw+IMh1Jac2CjUU/IsfEsqDAEBnGiFlaLItV573gJqBvctP5j3jVKlLuxmO+pnW0QGlXjXgzjw5YeTNwRG9upJHOl6GA3pFetKNojqGzfkxku+owUpqaMXao4H9gAAAAEAAwIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEDAMoPin6lAAAAAAAAAABB3NZQCAALgZZzC14dAz6ZxChX5pn6bIJ3WNipSJrCELO7Ex0TOQAWiiDKuQJtqf630dSBU8LDVh8QZDqS05sFGop+RY+JZUICAAAAAHRlc3QgY29tbWVudG/bd5c=" + + XCTAssertEqual(output.encoded, expectedString) + } + + func testTransferCustomPayloadSign() { + let privateKeyData = Data(hexString: "5525e673087587bc0efd7ab09920ef7d3c1bf6b854a661430244ca59ab19e9d1")! - let jettonTransfer = TheOpenNetworkJettonTransfer.with { - $0.transfer = transferData - $0.jettonAmount = 500 * 1000 * 1000 - $0.toOwner = "EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8" - $0.responseAddress = "EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk" - $0.forwardAmount = 1 + // Doge chatbot contract payload to be deployed. + // Docs: https://docs.ton.org/develop/dapps/ton-connect/transactions#smart-contract-deployment + let dogeChatbotStateInit = "te6cckEBBAEAUwACATQBAgEU/wD0pBP0vPLICwMAEAAAAZDrkbgQAGrTMAGCCGlJILmRMODQ0wMx+kAwi0ZG9nZYcCCAGMjLBVAEzxaARfoCE8tqEssfAc8WyXP7AO4ioYU=" + // Doge chatbot's address after the contract is deployed. + let dogeChatbotDeployingAddress = "0:3042cd5480da232d5ac1d9cbe324e3c9eb58f167599f6b7c20c6e638aeed0335" + + // The comment has nothing to do with Doge chatbot. + // It's just used to attach the following ASCII comment to the transaction: + // "This transaction deploys Doge Chatbot contract" + let commentPayload = "te6cckEBAQEANAAAZAAAAABUaGlzIHRyYW5zYWN0aW9uIGRlcGxveXMgRG9nZSBDaGF0Ym90IGNvbnRyYWN0v84vSg==" + + let customPayload = TheOpenNetworkCustomPayload.with { + $0.stateInit = dogeChatbotStateInit + $0.payload = commentPayload + } + + let transfer = TheOpenNetworkTransfer.with { + $0.walletVersion = TheOpenNetworkWalletVersion.walletV4R2 + $0.dest = dogeChatbotDeployingAddress + // 0.069 TON + $0.amount = 69_000_000 + $0.mode = UInt32(TheOpenNetworkSendMode.payFeesSeparately.rawValue | TheOpenNetworkSendMode.ignoreActionPhaseErrors.rawValue) + $0.bounceable = false + $0.customPayload = customPayload } let input = TheOpenNetworkSigningInput.with { - $0.jettonTransfer = jettonTransfer + $0.messages = [transfer] $0.privateKey = privateKeyData + $0.sequenceNumber = 4 + $0.expireAt = 1721939714 } let output: TheOpenNetworkSigningOutput = AnySigner.sign(input: input, coin: .ton) - // tx: https://testnet.tonscan.org/tx/Er_oT5R3QK7D-qVPBKUGkJAOOq6ayVls-mgEphpI9Ck= - let expectedString = "te6ccgICAAQAAQAAARgAAAFFiAC0UQZVyBNtT/W+jqQKnhYasPiDIdSWnNgo1FPyLHxLKgwAAQGcaIWVosi1XnveAmoG9y0/mPeNUqUu7GY76mdbRAaVeNeDOPDlh5M3BEb26kkc6XoYDekV60o2iOobN+TGS76jBSmpoxdqjgf2AAAAAQADAAIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEAAwDKD4p+pQAAAAAAAAAAQdzWUAgAC4GWcwteHQM+mcQoV+aZ+myCd1jYqUiawhCzuxMdEzkAFoogyrkCban+t9HUgVPCw1YfEGQ6ktObBRqKfkWPiWVCAgAAAAB0ZXN0IGNvbW1lbnQ=" + // Successfully broadcasted: https://tonviewer.com/transaction/f4b7ed2247b1adf54f33dd2fd99216fbd61beefb281542d0b330ccea9b8d0338 + let expectedString = "te6cckECCAEAATcAAUWIAfq4NsPLegfou/MPhtHE9YuzV3gnI/q6jm3MRJh2PtpaDAEBnPbyCSsWrOZpEjb7ZFxz5yYi+an6M6Lnq7rI7TFWdDS76LEtGBrVVrhMGziwxuy6LCVtsMBikI7RPVQ89FCIAAYpqaMXZqK3AgAAAAQAAwICaUIAGCFmqkBtEZatYOzl8ZJx5PWseLOsz7W+EGNzHFd2gZqgIObaAAAAAAAAAAAAAAAAAAPAAwQCATQFBgBkAAAAAFRoaXMgdHJhbnNhY3Rpb24gZGVwbG95cyBEb2dlIENoYXRib3QgY29udHJhY3QBFP8A9KQT9LzyyAsHABAAAAGQ65G4EABq0zABgghpSSC5kTDg0NMDMfpAMItGRvZ2WHAggBjIywVQBM8WgEX6AhPLahLLHwHPFslz+wAa2r/S" XCTAssertEqual(output.encoded, expectedString) } diff --git a/tests/chains/TheOpenNetwork/AddressTests.cpp b/tests/chains/TheOpenNetwork/AddressTests.cpp deleted file mode 100644 index a92e3a0e4cd..00000000000 --- a/tests/chains/TheOpenNetwork/AddressTests.cpp +++ /dev/null @@ -1,237 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#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 { - -TEST(TheOpenNetworkAddress, Valid) { - ASSERT_TRUE(Address::isValid("-1:8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae")); - ASSERT_TRUE(Address::isValid("0:8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae")); - - // user-friendly, b64urlsafe - ASSERT_TRUE(Address::isValid("EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorkdl")); - ASSERT_TRUE(Address::isValid("EQBGqFmKe3oY8PChYN9g92ZEV2ybkzVB-hCiesQRn5mFnrNv")); - ASSERT_TRUE(Address::isValid("Ef8JfFPRpHBV_tZpCurvxMJW69nt2js3SuGEWojGnOpCVPRe")); - ASSERT_TRUE(Address::isValid("Ef_drj6m7jcME0fWTA-OwFC-6F0Le2SuOUQ6ibRc3Vz8HL8H")); - - // user-friendly, b64 - ASSERT_TRUE(Address::isValid("EQAN6Dr3vziti1Kp9D3aEFqJX4bBVfCaV57Z+9jwKTBXICv8")); - ASSERT_TRUE(Address::isValid("EQCmGW+z+UL00FmnhWaMvJq/i86YY5GlJP3uJW19KC5Tzq4C")); -} - -TEST(TheOpenNetworkAddress, Invalid) { - ASSERT_FALSE(Address::isValid("random string")); - - // invalid size - ASSERT_FALSE(Address::isValid("EQIcIZpPoMnWXd8FbC1KaLtcyIgVUlwsbFK_3P6f5uf_YyzoE")); - ASSERT_FALSE(Address::isValid("EQIcIZpPoMnWXd8FbC1KaLtcyIgVUlwsbFK_3P6f5uf_YyE")); - - // invalid size after decode - ASSERT_FALSE(Address::isValid("EQIcIZpPoMnWXd8FbC1KaLtcyIgVUlwsbFK_3P6f5uf_Yyw=")); - - // invalid workchain - ASSERT_FALSE(Address::isValid("1:0ccd5119f27f7fe4614476c34f7e5e93c7ae098e577cf2012f8b8043165cb809")); - ASSERT_FALSE(Address::isValid("EQEMzVEZ8n9_5GFEdsNPfl6Tx64Jjld88gEvi4BDFly4CSyl")); - ASSERT_FALSE(Address::isValid("-2:e0e98cfcf743292298ad9e379a3c2e6401797b9cbfc0fe98b4e14fd0ce07ecdf")); - ASSERT_FALSE(Address::isValid("Ef7g6Yz890MpIpitnjeaPC5kAXl7nL_A_pi04U_Qzgfs3-Cj")); - - // invalid tag - ASSERT_FALSE(Address::isValid("MwCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorsn8")); // 0x33 - ASSERT_FALSE(Address::isValid("swCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsornJ2")); // 0x80 + 0x33 - - // invalid crc - ASSERT_FALSE(Address::isValid("EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsormVH")); // crc[a, b] = crc[b, a] - ASSERT_FALSE(Address::isValid("EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorpcF")); // crc=0x9705 -} - -TEST(TheOpenNetworkAddress, FromString) { - auto raw_address = Address("0:8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae"); - ASSERT_EQ(raw_address.string(), "0:8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae"); - ASSERT_FALSE(raw_address.isUserFriendly); - - auto raw_address_uppercase = Address("0:8A8627861A5DD96C9DB3CE0807B122DA5ED473934CE7568A5B4B1C361CBB28AE"); - ASSERT_EQ(raw_address_uppercase.string(), "0:8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae"); - ASSERT_FALSE(raw_address_uppercase.isUserFriendly); - - // 0x11 - auto bounceable_address = Address("EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorkdl"); - ASSERT_EQ(bounceable_address.string(), "EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorkdl"); - ASSERT_TRUE(bounceable_address.isUserFriendly); - ASSERT_TRUE(bounceable_address.isBounceable); - ASSERT_FALSE(bounceable_address.isTestOnly); - - // 0x51 - auto non_bounceable_address = Address("UQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorhqg"); - ASSERT_EQ(non_bounceable_address.string(), "UQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorhqg"); - ASSERT_TRUE(non_bounceable_address.isUserFriendly); - ASSERT_FALSE(non_bounceable_address.isBounceable); - ASSERT_FALSE(non_bounceable_address.isTestOnly); - - // 0x11 | 0x80 - auto test_bounceable_address = Address("kQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorvzv"); - ASSERT_EQ(test_bounceable_address.string(), "kQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorvzv"); - ASSERT_TRUE(test_bounceable_address.isUserFriendly); - ASSERT_TRUE(test_bounceable_address.isBounceable); - ASSERT_TRUE(test_bounceable_address.isTestOnly); - - // 0x51 | 0x80 - auto test_non_bounceable_address = Address("0QCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorqEq"); - ASSERT_EQ(test_non_bounceable_address.string(), "0QCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorqEq"); - ASSERT_TRUE(test_non_bounceable_address.isUserFriendly); - ASSERT_FALSE(test_non_bounceable_address.isBounceable); - ASSERT_TRUE(test_non_bounceable_address.isTestOnly); -} - -TEST(TheOpenNetworkAddress, FromPrivateKeyV4R2) { - const auto privateKey = PrivateKey(parse_hex("ff3ceb81a22c726e9d61d3f336fc783de5d60020972ca3abc27b99e3cf573a88")); - const auto publicKey = privateKey.getPublicKey(TWPublicKeyTypeED25519); - - WalletV4R2 wallet(publicKey, WorkchainType::Basechain); - const auto address = wallet.getAddress(); - - ASSERT_EQ(address.string(), "UQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorhqg"); -} - -TEST(TheOpenNetworkAddress, FromPublicKeyV4R2) { - const auto publicKey = PublicKey(parse_hex("c2036a1ca901059e1d1ab38cd7a7a4709b5e8f9d85b387f0514d7adae70b6afe"), TWPublicKeyTypeED25519); - - WalletV4R2 wallet(publicKey, WorkchainType::Basechain); - const auto address = wallet.getAddress(); - - ASSERT_EQ(address.string(), "UQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorhqg"); -} - -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); -} - -TEST(TheOpenNetworkAddress, ToUserFriendly) { - auto rawAddress = "0:8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae"; - auto bounceable = "EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorkdl"; - auto nonBounceable = "UQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorhqg"; - auto bounceableTestnet = "kQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorvzv"; - auto nonBounceableTestnet = "0QCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorqEq"; - - // Raw to user friendly. - assertStringsEqual( - WRAPS(TWTONAddressConverterToUserFriendly(STRING(rawAddress).get(), true, false)), - bounceable - ); - assertStringsEqual( - WRAPS(TWTONAddressConverterToUserFriendly(STRING(rawAddress).get(), false, false)), - nonBounceable - ); - assertStringsEqual( - WRAPS(TWTONAddressConverterToUserFriendly(STRING(rawAddress).get(), true, true)), - bounceableTestnet - ); - assertStringsEqual( - WRAPS(TWTONAddressConverterToUserFriendly(STRING(rawAddress).get(), false, true)), - nonBounceableTestnet - ); - - // Bounceable to non-bounceable. - assertStringsEqual( - WRAPS(TWTONAddressConverterToUserFriendly(STRING(bounceable).get(), false, false)), - nonBounceable - ); - - // Non-bounceable to bounceable. - assertStringsEqual( - WRAPS(TWTONAddressConverterToUserFriendly(STRING(nonBounceable).get(), true, false)), - bounceable - ); - - // Non-bounceable to non-bounceable. - assertStringsEqual( - WRAPS(TWTONAddressConverterToUserFriendly(STRING(nonBounceable).get(), false, false)), - nonBounceable - ); -} - -TEST(TheOpenNetworkAddress, ToUserFriendlyError) { - // No "0:" prefix. - auto invalid1 = STRING("8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae"); - ASSERT_EQ(TWTONAddressConverterToUserFriendly(invalid1.get(), true, false), nullptr); - - // Too short. - auto invalid2 = STRING("EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsor"); - ASSERT_EQ(TWTONAddressConverterToUserFriendly(invalid1.get(), false, false), nullptr); -} - -} // namespace TW::TheOpenNetwork::tests diff --git a/tests/chains/TheOpenNetwork/SignerTests.cpp b/tests/chains/TheOpenNetwork/SignerTests.cpp deleted file mode 100644 index 976affa59ed..00000000000 --- a/tests/chains/TheOpenNetwork/SignerTests.cpp +++ /dev/null @@ -1,229 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "HexCoding.h" - -#include "TheOpenNetwork/Signer.h" -#include "Everscale/CommonTON/Cell.h" - -#include - -namespace TW::TheOpenNetwork::tests { - -TEST(TheOpenNetworkSigner, TransferAndDeploy) { - auto input = Proto::SigningInput(); - - auto& transfer = *input.mutable_transfer(); - transfer.set_wallet_version(Proto::WALLET_V4_R2); - transfer.set_dest("EQDYW_1eScJVxtitoBRksvoV9cCYo4uKGWLVNIHB1JqRR3n0"); - transfer.set_amount(10); - transfer.set_mode(Proto::SendMode::PAY_FEES_SEPARATELY | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS); - transfer.set_expire_at(1671135440); - transfer.set_bounceable(true); - - const auto privateKey = parse_hex("63474e5fe9511f1526a50567ce142befc343e71a49b865ac3908f58667319cb8"); - input.set_private_key(privateKey.data(), privateKey.size()); - - auto output = Signer::sign(input); - - ASSERT_EQ(hex(CommonTON::Cell::fromBase64(output.encoded())->hash), "b3d9462c13a8c67e19b62002447839c386de51415ace3ff6473b1e6294299819"); - - // tx: https://tonviewer.com/transaction/6ZzWOFKZt_m3kZjbwfbATwLaVwmUOdDp0xjhuY7PO3k= - ASSERT_EQ(output.encoded(), "te6ccgICABoAAQAAA8sAAAJFiADN98eLgHfrkE8l8gmT8X5REpTVR6QnqDhArTbKlVvbZh4ABAABAZznxvGBhoRXhPogxNY8QmHlihJWxg5t6KptqcAIZlVks1r+Z+r1avCWNCeqeLC/oaiVN4mDx/E1+Zhi33G25rcIKamjF/////8AAAAAAAMAAgFiYgBsLf6vJOEq42xW0AoyWX0K+uBMUcXFDLFqmkDg6k1Io4hQAAAAAAAAAAAAAAAAAQADAAACATQABgAFAFEAAAAAKamjF/Qsd/kxvqIOxdAVBzEna7suKGCUdmEkWyMZ74Ez7o1BQAEU/wD0pBP0vPLICwAHAgEgAA0ACAT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/wAMAAsACgAJAAr0AMntVABsgQEI1xj6ANM/MFIkgQEI9Fnyp4IQZHN0cnB0gBjIywXLAlAFzxZQA/oCE8tqyx8Syz/Jc/sAAHCBAQjXGPoA0z/IVCBHgQEI9FHyp4IQbm90ZXB0gBjIywXLAlAGzxZQBPoCFMtqEssfyz/Jc/sAAgBu0gf6ANTUIvkABcjKBxXL/8nQd3SAGMjLBcsCIs8WUAX6AhTLaxLMzMlz+wDIQBSBAQj0UfKnAgIBSAAXAA4CASAAEAAPAFm9JCtvaiaECAoGuQ+gIYRw1AgIR6STfSmRDOaQPp/5g3gSgBt4EBSJhxWfMYQCASAAEgARABG4yX7UTQ1wsfgCAVgAFgATAgEgABUAFAAZrx32omhAEGuQ64WPwAAZrc52omhAIGuQ64X/wAA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYALm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDQAZABgAilAEgQEI9Fkw7UTQgQFA1yDIAc8W9ADJ7VQBcrCOI4IQZHN0coMesXCAGFAFywVQA88WI/oCE8tqyx/LP8mAQPsAkl8D4gB4AfoA9AQw+CdvIjBQCqEhvvLgUIIQcGx1Z4MesXCAGFAEywUmzxZY+gIZ9ADLaRfLH1Jgyz8gyYBA+wAG"); -} - -TEST(TheOpenNetworkSigner, TransferOrdinary) { - auto input = Proto::SigningInput(); - - auto& transfer = *input.mutable_transfer(); - transfer.set_wallet_version(Proto::WALLET_V4_R2); - transfer.set_dest("EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q"); - transfer.set_amount(10); - transfer.set_sequence_number(6); - transfer.set_mode(Proto::SendMode::PAY_FEES_SEPARATELY | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS); - transfer.set_expire_at(1671132440); - transfer.set_bounceable(true); - - const auto privateKey = parse_hex("c38f49de2fb13223a9e7d37d5d0ffbdd89a5eb7c8b0ee4d1c299f2cefe7dc4a0"); - input.set_private_key(privateKey.data(), privateKey.size()); - - auto output = Signer::sign(input); - - ASSERT_EQ(hex(CommonTON::Cell::fromBase64(output.encoded())->hash), "3908cf8b570c1d3d261c62620c9f368db11f6e821a07614cff64de2e7319f81b"); - - // tx: https://tonviewer.com/transaction/3Z4tHpXNLyprecgu5aTQHWtY7dpHXEoo11MAX61Xyg0= - ASSERT_EQ(output.encoded(), "te6ccgICAAQAAQAAALAAAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGcEUPkil2aZ4s8KKparSep/OKHMC8vuXafFbW2HGp/9AcTRv0J5T4dwyW1G0JpHw+g5Ov6QI3Xo0O9RFr3KidICimpoxdjm3UYAAAABgADAAIBYmIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmIUAAAAAAAAAAAAAAAAAEAAwAA"); -} - -TEST(TheOpenNetworkSigner, TransferAllBalance) { - auto input = Proto::SigningInput(); - - auto& transfer = *input.mutable_transfer(); - transfer.set_wallet_version(Proto::WALLET_V4_R2); - transfer.set_dest("EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q"); - transfer.set_amount(0); - transfer.set_sequence_number(7); - transfer.set_mode(Proto::SendMode::ATTACH_ALL_CONTRACT_BALANCE | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS); - transfer.set_expire_at(1681102222); - transfer.set_bounceable(true); - - const auto privateKey = parse_hex("c38f49de2fb13223a9e7d37d5d0ffbdd89a5eb7c8b0ee4d1c299f2cefe7dc4a0"); - input.set_private_key(privateKey.data(), privateKey.size()); - - auto output = Signer::sign(input); - - ASSERT_EQ(hex(CommonTON::Cell::fromBase64(output.encoded())->hash), "d5c5980c9083f697a7f114426effbbafac6d5c88554297d290eb65c8def3008e"); - - // tx: https://tonviewer.com/transaction/cVcXgI9DWNWlN2iyTsteaWJckTswVqWZnRVvX5krXeA= - ASSERT_EQ(output.encoded(), "te6ccgICAAQAAQAAAK8AAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGc58rMUQc/u78bg+Wtt8ETkyM0udf7S+F7wWk7lnPib2KChnBx9dZ7a/zLzhfLq+W9LjLZZfx995J17+0sbkvGCympoxdkM5WOAAAABwCCAAIBYGIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmAAAAAAAAAAAAAAAAAAQADAAA="); -} - -TEST(TheOpenNetworkSigner, TransferAllBalanceNonBounceable) { - auto input = Proto::SigningInput(); - - auto& transfer = *input.mutable_transfer(); - transfer.set_wallet_version(Proto::WALLET_V4_R2); - transfer.set_dest("UQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts4DV"); - transfer.set_amount(0); - transfer.set_sequence_number(8); - transfer.set_mode(Proto::SendMode::ATTACH_ALL_CONTRACT_BALANCE | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS); - transfer.set_expire_at(1681102222); - transfer.set_bounceable(false); - - const auto privateKey = parse_hex("c38f49de2fb13223a9e7d37d5d0ffbdd89a5eb7c8b0ee4d1c299f2cefe7dc4a0"); - input.set_private_key(privateKey.data(), privateKey.size()); - - auto output = Signer::sign(input); - - ASSERT_EQ(hex(CommonTON::Cell::fromBase64(output.encoded())->hash), "e9c816780fa8e578bae309c2e098db8eb16aa25545b3ad2b61bb711ec9562795"); - - // tx: https://tonviewer.com/transaction/0sJkPKu6u6uObVRuSWGd_bVGiyy5lJuzEKDqSXifQEA= - ASSERT_EQ(output.encoded(), "te6ccgICAAQAAQAAAK8AAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGcRQQvxdU1u4QoE2Pas0AsZQMc9lea3+wtSvaC6QfLUlyJ9oISMCFnaErpyFHelDhPu4iuZqhkoLwjkR1VYhFSCimpoxdkM5WOAAAACACCAAIBYEIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmAAAAAAAAAAAAAAAAAAQADAAA="); -} - -TEST(TheOpenNetworkSigner, TransferWithASCIIComment) { - auto input = Proto::SigningInput(); - - auto& transfer = *input.mutable_transfer(); - transfer.set_wallet_version(Proto::WALLET_V4_R2); - transfer.set_dest("EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q"); - transfer.set_amount(10); - transfer.set_sequence_number(10); - transfer.set_mode(Proto::SendMode::PAY_FEES_SEPARATELY | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS); - transfer.set_expire_at(1681102222); - transfer.set_comment("test comment"); - transfer.set_bounceable(true); - - const auto privateKey = parse_hex("c38f49de2fb13223a9e7d37d5d0ffbdd89a5eb7c8b0ee4d1c299f2cefe7dc4a0"); - input.set_private_key(privateKey.data(), privateKey.size()); - - auto output = Signer::sign(input); - - ASSERT_EQ(hex(CommonTON::Cell::fromBase64(output.encoded())->hash), "a8c6943d5587f590c43fcdb0e894046f1965c615e19bcaf0c8407e9ccb74518d"); - - // tx: https://tonviewer.com/transaction/9wjD-VrgEDpa0D9u1g03KSD7kvTNsxRocR7LEdQtCNQ= - ASSERT_EQ(output.encoded(), "te6ccgICAAQAAQAAAMAAAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGcY4XlvKqu7spxyjL6vyBSKjbskDgqkHhqBsdTe900RGrzExtpvwc04j94v8HOczEWSMCXjTXk0z+CVUXSL54qCimpoxdkM5WOAAAACgADAAIBYmIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmIUAAAAAAAAAAAAAAAAAEAAwAgAAAAAHRlc3QgY29tbWVudA=="); -} - -TEST(TheOpenNetworkSigner, TransferWithUTF8Comment) { - auto input = Proto::SigningInput(); - - auto& transfer = *input.mutable_transfer(); - transfer.set_wallet_version(Proto::WALLET_V4_R2); - transfer.set_dest("EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q"); - transfer.set_amount(10); - transfer.set_sequence_number(11); - transfer.set_mode(Proto::SendMode::PAY_FEES_SEPARATELY | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS); - transfer.set_expire_at(1681102222); - transfer.set_comment("тестовый комментарий"); - transfer.set_bounceable(true); - - const auto privateKey = parse_hex("c38f49de2fb13223a9e7d37d5d0ffbdd89a5eb7c8b0ee4d1c299f2cefe7dc4a0"); - input.set_private_key(privateKey.data(), privateKey.size()); - - auto output = Signer::sign(input); - - ASSERT_EQ(hex(CommonTON::Cell::fromBase64(output.encoded())->hash), "1091dfae81583d3972825633592c24eab0d3d74c91f60fda9d4afe7535103633"); - - // tx: https://tonviewer.com/transaction/VOTt8HW6eRuWHmuM_P3aC-Dy4TMu4cCRePoTAiDfcoQ= - ASSERT_EQ(output.encoded(), "te6ccgICAAQAAQAAANsAAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGchoDa7EdGQuPuehHy3+0X9WNVEvYxdBtaEWn15oYUX8PEKyzztYy94Xq0T2XdhVvj2H7PTSQ+D/Ny1IBRCxk0BimpoxdkM5WOAAAACwADAAIBYmIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmIUAAAAAAAAAAAAAAAAAEAAwBWAAAAANGC0LXRgdGC0L7QstGL0Lkg0LrQvtC80LzQtdC90YLQsNGA0LjQuQ=="); -} - -TEST(TheOpenNetworkSigner, InvalidWalletVersion) { - auto input = Proto::SigningInput(); - - auto& transfer = *input.mutable_transfer(); - transfer.set_wallet_version(Proto::WALLET_V3_R2); - transfer.set_dest("EQDYW_1eScJVxtitoBRksvoV9cCYo4uKGWLVNIHB1JqRR3n0"); - transfer.set_amount(10); - transfer.set_mode(Proto::SendMode::PAY_FEES_SEPARATELY | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS); - transfer.set_expire_at(1671135440); - transfer.set_bounceable(true); - - const auto privateKey = parse_hex("63474e5fe9511f1526a50567ce142befc343e71a49b865ac3908f58667319cb8"); - input.set_private_key(privateKey.data(), privateKey.size()); - - auto output = Signer::sign(input); - ASSERT_EQ(output.error(), 22); -} - -TEST(TheOpenNetworkSigner, JettonTransfer) { - auto input = Proto::SigningInput(); - auto& jettonTransfer = *input.mutable_jetton_transfer(); - auto& transferData = *jettonTransfer.mutable_transfer(); - - transferData.set_wallet_version(Proto::WALLET_V4_R2); - transferData.set_dest("EQBiaD8PO1NwfbxSkwbcNT9rXDjqhiIvXWymNO-edV0H5lja"); - transferData.set_amount(100 * 1000 * 1000); - transferData.set_mode(Proto::SendMode::PAY_FEES_SEPARATELY | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS); - transferData.set_expire_at(1787693046); - transferData.set_bounceable(true); - jettonTransfer.set_response_address("EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk"); // send unused toncoins back to sender - jettonTransfer.set_to_owner("EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8"); - jettonTransfer.set_query_id(69); - jettonTransfer.set_forward_amount(1); - jettonTransfer.set_jetton_amount(1000 * 1000 * 1000); // transfer 1 testtwt (decimal precision is 9) - - const auto privateKey = parse_hex("c054900a527538c1b4325688a421c0469b171c29f23a62da216e90b0df2412ee"); - input.set_private_key(privateKey.data(), privateKey.size()); - - auto output = Signer::sign(input); - - ASSERT_EQ(hex(CommonTON::Cell::fromBase64(output.encoded())->hash), "3e4dac37acdc99ca670b3747ab2730e818727d9d25c80d3987abe501356d0da0"); - - // tx: https://testnet.tonviewer.com/transaction/2HOPGAXhez3v6sdfj-5p8mPHX4S4T0CgxVbm0E2swxE= - ASSERT_EQ(output.encoded(), "te6ccgICABoAAQAABCMAAAJFiAC0UQZVyBNtT/W+jqQKnhYasPiDIdSWnNgo1FPyLHxLKh4ABAABAZz3iNHD1z2mxbtpFAtmbVevYMnB4yHPkF3WAsL3KHcrqCw0SWezOg4lVz1zzSReeFDx98ByAqY9+eR5VF3xyugAKamjF/////8AAAAAAAMAAgFoYgAxNB+Hnam4Pt4pSYNuGp+1rhx1QxEXrrZTGnfPOq6D8yAvrwgAAAAAAAAAAAAAAAAAAQADAKoPin6lAAAAAAAAAEVDuaygCAALgZZzC14dAz6ZxChX5pn6bIJ3WNipSJrCELO7Ex0TOQAWiiDKuQJtqf630dSBU8LDVh8QZDqS05sFGop+RY+JZUICAgE0AAYABQBRAAAAACmpoxfOamBhePRNnx/pqQViBzW0dDCy/+1WLV1VhgbVTL6i30ABFP8A9KQT9LzyyAsABwIBIAANAAgE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8ADAALAAoACQAK9ADJ7VQAbIEBCNcY+gDTPzBSJIEBCPRZ8qeCEGRzdHJwdIAYyMsFywJQBc8WUAP6AhPLassfEss/yXP7AABwgQEI1xj6ANM/yFQgR4EBCPRR8qeCEG5vdGVwdIAYyMsFywJQBs8WUAT6AhTLahLLH8s/yXP7AAIAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJc/sAyEAUgQEI9FHypwICAUgAFwAOAgEgABAADwBZvSQrb2omhAgKBrkPoCGEcNQICEekk30pkQzmkD6f+YN4EoAbeBAUiYcVnzGEAgEgABIAEQARuMl+1E0NcLH4AgFYABYAEwIBIAAVABQAGa8d9qJoQBBrkOuFj8AAGa3OdqJoQCBrkOuF/8AAPbKd+1E0IEBQNch9AQwAsjKB8v/ydABgQEI9ApvoTGAC5tAB0NMDIXGwkl8E4CLXScEgkl8E4ALTHyGCEHBsdWe9IoIQZHN0cr2wkl8F4AP6QDAg+kQByMoHy//J0O1E0IEBQNch9AQwXIEBCPQKb6Exs5JfB+AF0z/IJYIQcGx1Z7qSODDjDQOCEGRzdHK6kl8G4w0AGQAYAIpQBIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UAXKwjiOCEGRzdHKDHrFwgBhQBcsFUAPPFiP6AhPLassfyz/JgED7AJJfA+IAeAH6APQEMPgnbyIwUAqhIb7y4FCCEHBsdWeDHrFwgBhQBMsFJs8WWPoCGfQAy2kXyx9SYMs/IMmAQPsABg=="); -} - -TEST(TheOpenNetworkSigner, JettonTransferComment) { - auto input = Proto::SigningInput(); - auto& jettonTransfer = *input.mutable_jetton_transfer(); - auto& transferData = *jettonTransfer.mutable_transfer(); - - transferData.set_wallet_version(Proto::WALLET_V4_R2); - transferData.set_dest("EQBiaD8PO1NwfbxSkwbcNT9rXDjqhiIvXWymNO-edV0H5lja"); - transferData.set_amount(100 * 1000 * 1000); - transferData.set_sequence_number(1); - transferData.set_mode(Proto::SendMode::PAY_FEES_SEPARATELY | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS); - transferData.set_expire_at(1787693046); - transferData.set_comment("test comment"); - transferData.set_bounceable(true); - jettonTransfer.set_jetton_amount(500 * 1000 * 1000); // transfer 0.5 testtwt (decimal precision is 9) - jettonTransfer.set_to_owner("EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8"); - jettonTransfer.set_response_address("EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk"); // send unused toncoins back to sender - jettonTransfer.set_forward_amount(1); - - - const auto privateKey = parse_hex("c054900a527538c1b4325688a421c0469b171c29f23a62da216e90b0df2412ee"); - input.set_private_key(privateKey.data(), privateKey.size()); - - auto output = Signer::sign(input); - - ASSERT_EQ(hex(CommonTON::Cell::fromBase64(output.encoded())->hash), "c98c205c8dd37d9a6ab5db6162f5b9d37cefa067de24a765154a5eb7a359f22f"); - - // tx: https://testnet.tonviewer.com/transaction/12bfe84f947740aec3faa54f04a50690900e3aae9ac9596cfa6804a61a48f429 - ASSERT_EQ(output.encoded(), "te6ccgICAAQAAQAAARgAAAFFiAC0UQZVyBNtT/W+jqQKnhYasPiDIdSWnNgo1FPyLHxLKgwAAQGcaIWVosi1XnveAmoG9y0/mPeNUqUu7GY76mdbRAaVeNeDOPDlh5M3BEb26kkc6XoYDekV60o2iOobN+TGS76jBSmpoxdqjgf2AAAAAQADAAIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEAAwDKD4p+pQAAAAAAAAAAQdzWUAgAC4GWcwteHQM+mcQoV+aZ+myCd1jYqUiawhCzuxMdEzkAFoogyrkCban+t9HUgVPCw1YfEGQ6ktObBRqKfkWPiWVCAgAAAAB0ZXN0IGNvbW1lbnQ="); -} - -} // namespace TW::TheOpenNetwork::tests diff --git a/tests/chains/TheOpenNetwork/TWAnyAddressTests.cpp b/tests/chains/TheOpenNetwork/TWAnyAddressTests.cpp index 35f4dbd16fd..6a7fc5a7a5d 100644 --- a/tests/chains/TheOpenNetwork/TWAnyAddressTests.cpp +++ b/tests/chains/TheOpenNetwork/TWAnyAddressTests.cpp @@ -28,4 +28,17 @@ TEST(TWTheOpenNetwork, Address) { assertStringsEqual(addressStr, "UQDYW_1eScJVxtitoBRksvoV9cCYo4uKGWLVNIHB1JqRRyQx"); } +TEST(TWTheOpenNetwork, AddressValidate) { + auto string = STRING("EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorkdl"); + + ASSERT_TRUE(TWAnyAddressIsValid(string.get(), TWCoinTypeTON)); + auto addr = WRAP(TWAnyAddress, TWAnyAddressCreateWithString(string.get(), TWCoinTypeTON)); + + auto keyHash = WRAPD(TWAnyAddressData(addr.get())); + assertHexEqual(keyHash, "8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae"); + + auto normalized = WRAPS(TWAnyAddressDescription(addr.get())); + assertStringsEqual(normalized, "UQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorhqg"); +} + } // namespace TW::TheOpenNetwork::tests diff --git a/tests/chains/TheOpenNetwork/TWAnySignerTests.cpp b/tests/chains/TheOpenNetwork/TWAnySignerTests.cpp index 2a2bbe740be..467461bf1b8 100644 --- a/tests/chains/TheOpenNetwork/TWAnySignerTests.cpp +++ b/tests/chains/TheOpenNetwork/TWAnySignerTests.cpp @@ -15,21 +15,25 @@ namespace TW::TheOpenNetwork::tests { TEST(TWAnySignerTheOpenNetwork, SingMessageToTransferAndDeployWallet) { Proto::SigningInput input; - auto& transfer = *input.mutable_transfer(); + auto& transfer = *input.add_messages(); transfer.set_wallet_version(Proto::WALLET_V4_R2); transfer.set_dest("EQDYW_1eScJVxtitoBRksvoV9cCYo4uKGWLVNIHB1JqRR3n0"); transfer.set_amount(10); transfer.set_mode(Proto::SendMode::PAY_FEES_SEPARATELY | Proto::SendMode::IGNORE_ACTION_PHASE_ERRORS); - transfer.set_expire_at(1671135440); transfer.set_bounceable(true); const auto privateKey = parse_hex("63474e5fe9511f1526a50567ce142befc343e71a49b865ac3908f58667319cb8"); input.set_private_key(privateKey.data(), privateKey.size()); + input.set_expire_at(1671135440); + Proto::SigningOutput output; ANY_SIGN(input, TWCoinTypeTON); - ASSERT_EQ(output.encoded(), "te6ccgICABoAAQAAA8sAAAJFiADN98eLgHfrkE8l8gmT8X5REpTVR6QnqDhArTbKlVvbZh4ABAABAZznxvGBhoRXhPogxNY8QmHlihJWxg5t6KptqcAIZlVks1r+Z+r1avCWNCeqeLC/oaiVN4mDx/E1+Zhi33G25rcIKamjF/////8AAAAAAAMAAgFiYgBsLf6vJOEq42xW0AoyWX0K+uBMUcXFDLFqmkDg6k1Io4hQAAAAAAAAAAAAAAAAAQADAAACATQABgAFAFEAAAAAKamjF/Qsd/kxvqIOxdAVBzEna7suKGCUdmEkWyMZ74Ez7o1BQAEU/wD0pBP0vPLICwAHAgEgAA0ACAT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/wAMAAsACgAJAAr0AMntVABsgQEI1xj6ANM/MFIkgQEI9Fnyp4IQZHN0cnB0gBjIywXLAlAFzxZQA/oCE8tqyx8Syz/Jc/sAAHCBAQjXGPoA0z/IVCBHgQEI9FHyp4IQbm90ZXB0gBjIywXLAlAGzxZQBPoCFMtqEssfyz/Jc/sAAgBu0gf6ANTUIvkABcjKBxXL/8nQd3SAGMjLBcsCIs8WUAX6AhTLaxLMzMlz+wDIQBSBAQj0UfKnAgIBSAAXAA4CASAAEAAPAFm9JCtvaiaECAoGuQ+gIYRw1AgIR6STfSmRDOaQPp/5g3gSgBt4EBSJhxWfMYQCASAAEgARABG4yX7UTQ1wsfgCAVgAFgATAgEgABUAFAAZrx32omhAEGuQ64WPwAAZrc52omhAIGuQ64X/wAA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYALm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDQAZABgAilAEgQEI9Fkw7UTQgQFA1yDIAc8W9ADJ7VQBcrCOI4IQZHN0coMesXCAGFAFywVQA88WI/oCE8tqyx/LP8mAQPsAkl8D4gB4AfoA9AQw+CdvIjBQCqEhvvLgUIIQcGx1Z4MesXCAGFAEywUmzxZY+gIZ9ADLaRfLH1Jgyz8gyYBA+wAG"); + // The same Cell can be BoC encoded differently. + // This encoded BoC equals to: + // te6ccgICABoAAQAAA8sAAAJFiADN98eLgHfrkE8l8gmT8X5REpTVR6QnqDhArTbKlVvbZh4ABAABAZznxvGBhoRXhPogxNY8QmHlihJWxg5t6KptqcAIZlVks1r+Z+r1avCWNCeqeLC/oaiVN4mDx/E1+Zhi33G25rcIKamjF/////8AAAAAAAMAAgFiYgBsLf6vJOEq42xW0AoyWX0K+uBMUcXFDLFqmkDg6k1Io4hQAAAAAAAAAAAAAAAAAQADAAACATQABgAFAFEAAAAAKamjF/Qsd/kxvqIOxdAVBzEna7suKGCUdmEkWyMZ74Ez7o1BQAEU/wD0pBP0vPLICwAHAgEgAA0ACAT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/wAMAAsACgAJAAr0AMntVABsgQEI1xj6ANM/MFIkgQEI9Fnyp4IQZHN0cnB0gBjIywXLAlAFzxZQA/oCE8tqyx8Syz/Jc/sAAHCBAQjXGPoA0z/IVCBHgQEI9FHyp4IQbm90ZXB0gBjIywXLAlAGzxZQBPoCFMtqEssfyz/Jc/sAAgBu0gf6ANTUIvkABcjKBxXL/8nQd3SAGMjLBcsCIs8WUAX6AhTLaxLMzMlz+wDIQBSBAQj0UfKnAgIBSAAXAA4CASAAEAAPAFm9JCtvaiaECAoGuQ+gIYRw1AgIR6STfSmRDOaQPp/5g3gSgBt4EBSJhxWfMYQCASAAEgARABG4yX7UTQ1wsfgCAVgAFgATAgEgABUAFAAZrx32omhAEGuQ64WPwAAZrc52omhAIGuQ64X/wAA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYALm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDQAZABgAilAEgQEI9Fkw7UTQgQFA1yDIAc8W9ADJ7VQBcrCOI4IQZHN0coMesXCAGFAFywVQA88WI/oCE8tqyx/LP8mAQPsAkl8D4gB4AfoA9AQw+CdvIjBQCqEhvvLgUIIQcGx1Z4MesXCAGFAEywUmzxZY+gIZ9ADLaRfLH1Jgyz8gyYBA+wAG + ASSERT_EQ(output.encoded(), "te6cckECGgEAA7IAAkWIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmHgECAgE0AwQBnOfG8YGGhFeE+iDE1jxCYeWKElbGDm3oqm2pwAhmVWSzWv5n6vVq8JY0J6p4sL+hqJU3iYPH8TX5mGLfcbbmtwgpqaMX/////wAAAAAAAwUBFP8A9KQT9LzyyAsGAFEAAAAAKamjF/Qsd/kxvqIOxdAVBzEna7suKGCUdmEkWyMZ74Ez7o1BQAFiYgBsLf6vJOEq42xW0AoyWX0K+uBMUcXFDLFqmkDg6k1Io4hQAAAAAAAAAAAAAAAAAQcCASAICQAAAgFICgsE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8MDQ4PAubQAdDTAyFxsJJfBOAi10nBIJJfBOAC0x8hghBwbHVnvSKCEGRzdHK9sJJfBeAD+kAwIPpEAcjKB8v/ydDtRNCBAUDXIfQEMFyBAQj0Cm+hMbOSXwfgBdM/yCWCEHBsdWe6kjgw4w0DghBkc3RyupJfBuMNEBECASASEwBu0gf6ANTUIvkABcjKBxXL/8nQd3SAGMjLBcsCIs8WUAX6AhTLaxLMzMlz+wDIQBSBAQj0UfKnAgBwgQEI1xj6ANM/yFQgR4EBCPRR8qeCEG5vdGVwdIAYyMsFywJQBs8WUAT6AhTLahLLH8s/yXP7AAIAbIEBCNcY+gDTPzBSJIEBCPRZ8qeCEGRzdHJwdIAYyMsFywJQBc8WUAP6AhPLassfEss/yXP7AAAK9ADJ7VQAeAH6APQEMPgnbyIwUAqhIb7y4FCCEHBsdWeDHrFwgBhQBMsFJs8WWPoCGfQAy2kXyx9SYMs/IMmAQPsABgCKUASBAQj0WTDtRNCBAUDXIMgBzxb0AMntVAFysI4jghBkc3Rygx6xcIAYUAXLBVADzxYj+gITy2rLH8s/yYBA+wCSXwPiAgEgFBUAWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAIBWBYXABG4yX7UTQ1wsfgAPbKd+1E0IEBQNch9AQwAsjKB8v/ydABgQEI9ApvoTGACASAYGQAZrc52omhAIGuQ64X/wAAZrx32omhAEGuQ64WPwJiaP4Q="); } } // namespace TW::TheOpenNetwork::tests diff --git a/tests/chains/TheOpenNetwork/TWTONAddressConverterTests.cpp b/tests/chains/TheOpenNetwork/TWTONAddressConverterTests.cpp new file mode 100644 index 00000000000..01113313ce8 --- /dev/null +++ b/tests/chains/TheOpenNetwork/TWTONAddressConverterTests.cpp @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TestUtilities.h" + +#include "TrustWalletCore/TWTONAddressConverter.h" + +namespace TW::TheOpenNetwork::tests { + +TEST(TWTONAddressConverter, GetJettonNotcoinAddress) { + auto mainAddress = STRING("UQBjKqthWBE6GEcqb_epTRFrQ1niS6Z1Z1MHMwR-mnAYRoYr"); + auto addressBocEncoded = WRAPS(TWTONAddressConverterToBoc(mainAddress.get())); + assertStringsEqual(addressBocEncoded, "te6cckEBAQEAJAAAQ4AMZVVsKwInQwjlTf71KaItaGs8SXTOrOpg5mCP004DCNAptHQU"); + + // 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(TWTONAddressConverter, GetJettonUSDTAddress) { + auto mainAddress = STRING("UQBjKqthWBE6GEcqb_epTRFrQ1niS6Z1Z1MHMwR-mnAYRoYr"); + auto addressBocEncoded = WRAPS(TWTONAddressConverterToBoc(mainAddress.get())); + assertStringsEqual(addressBocEncoded, "te6cckEBAQEAJAAAQ4AMZVVsKwInQwjlTf71KaItaGs8SXTOrOpg5mCP004DCNAptHQU"); + + // 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(TWTONAddressConverter, GetJettonStonAddress) { + auto mainAddress = STRING("EQATQPeCwtMzQ9u54nTjUNcK4n_0VRSxPOOROLf_IE0OU3XK"); + auto addressBocEncoded = WRAPS(TWTONAddressConverterToBoc(mainAddress.get())); + assertStringsEqual(addressBocEncoded, "te6cckEBAQEAJAAAQ4ACaB7wWFpmaHt3PE6cahrhXE/+iqKWJ5xyJxb/5AmhynDu6Ygj"); + + // 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(TWTONAddressConverter, FromBocNullAddress) { + auto jettonAddressBocEncoded = STRING("te6cckEBAQEAAwAAASCUQYZV"); + auto jettonAddress = WRAPS(TWTONAddressConverterFromBoc(jettonAddressBocEncoded.get())); + assertStringsEqual(jettonAddress, "UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJKZ"); +} + +TEST(TWTONAddressConverter, 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); +} + +TEST(TWTONAddressConverter, ToUserFriendly) { + auto rawAddress = "0:8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae"; + auto bounceable = "EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorkdl"; + auto nonBounceable = "UQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorhqg"; + auto bounceableTestnet = "kQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorvzv"; + auto nonBounceableTestnet = "0QCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorqEq"; + + // Raw to user friendly. + assertStringsEqual( + WRAPS(TWTONAddressConverterToUserFriendly(STRING(rawAddress).get(), true, false)), + bounceable + ); + assertStringsEqual( + WRAPS(TWTONAddressConverterToUserFriendly(STRING(rawAddress).get(), false, false)), + nonBounceable + ); + assertStringsEqual( + WRAPS(TWTONAddressConverterToUserFriendly(STRING(rawAddress).get(), true, true)), + bounceableTestnet + ); + assertStringsEqual( + WRAPS(TWTONAddressConverterToUserFriendly(STRING(rawAddress).get(), false, true)), + nonBounceableTestnet + ); + + // Bounceable to non-bounceable. + assertStringsEqual( + WRAPS(TWTONAddressConverterToUserFriendly(STRING(bounceable).get(), false, false)), + nonBounceable + ); + + // Non-bounceable to bounceable. + assertStringsEqual( + WRAPS(TWTONAddressConverterToUserFriendly(STRING(nonBounceable).get(), true, false)), + bounceable + ); + + // Non-bounceable to non-bounceable. + assertStringsEqual( + WRAPS(TWTONAddressConverterToUserFriendly(STRING(nonBounceable).get(), false, false)), + nonBounceable + ); +} + +TEST(TWTONAddressConverter, ToUserFriendlyError) { + // No "0:" prefix. + auto invalid1 = STRING("8a8627861a5dd96c9db3ce0807b122da5ed473934ce7568a5b4b1c361cbb28ae"); + ASSERT_EQ(TWTONAddressConverterToUserFriendly(invalid1.get(), true, false), nullptr); + + // Too short. + auto invalid2 = STRING("EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsor"); + ASSERT_EQ(TWTONAddressConverterToUserFriendly(invalid1.get(), false, false), nullptr); +} + +} // namespace TW::TheOpenNetwork::tests diff --git a/wasm/tests/Blockchain/TheOpenNetwork.test.ts b/wasm/tests/Blockchain/TheOpenNetwork.test.ts index d7cda0c4a22..6da95ff578c 100644 --- a/wasm/tests/Blockchain/TheOpenNetwork.test.ts +++ b/wasm/tests/Blockchain/TheOpenNetwork.test.ts @@ -14,7 +14,7 @@ describe("TheOpenNetwork", () => { let data = HexCoding.decode("63474e5fe9511f1526a50567ce142befc343e71a49b865ac3908f58667319cb8"); let privateKey = PrivateKey.createWithData(data); - assert.isTrue(PrivateKey.isValid(data, Curve.ed25519)); + assert.isTrue(PrivateKey.isValid(data, Curve.ed25519)); let publicKey = privateKey.getPublicKeyEd25519(); let address = AnyAddress.createWithPublicKey(publicKey, CoinType.ton) @@ -74,15 +74,15 @@ describe("TheOpenNetwork", () => { walletVersion: TW.TheOpenNetwork.Proto.WalletVersion.WALLET_V4_R2, dest: "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q", amount: new Long(10), - sequenceNumber: 6, mode: (TW.TheOpenNetwork.Proto.SendMode.PAY_FEES_SEPARATELY | TW.TheOpenNetwork.Proto.SendMode.IGNORE_ACTION_PHASE_ERRORS), - expireAt: 1671132440, - bounceable: true + bounceable: true, }); let input = TW.TheOpenNetwork.Proto.SigningInput.create({ - transfer: transfer, + messages: [transfer], privateKey: PrivateKey.createWithData(privateKeyData).data(), + sequenceNumber: 6, + expireAt: 1671132440, }); const encoded = TW.TheOpenNetwork.Proto.SigningInput.encode(input).finish(); @@ -90,7 +90,7 @@ describe("TheOpenNetwork", () => { let output = TW.TheOpenNetwork.Proto.SigningOutput.decode(outputData); // tx: https://tonscan.org/tx/3Z4tHpXNLyprecgu5aTQHWtY7dpHXEoo11MAX61Xyg0= - let expectedString = "te6ccgICAAQAAQAAALAAAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGcEUPkil2aZ4s8KKparSep/OKHMC8vuXafFbW2HGp/9AcTRv0J5T4dwyW1G0JpHw+g5Ov6QI3Xo0O9RFr3KidICimpoxdjm3UYAAAABgADAAIBYmIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmIUAAAAAAAAAAAAAAAAAEAAwAA"; + let expectedString = "te6cckEBBAEArQABRYgBsLf6vJOEq42xW0AoyWX0K+uBMUcXFDLFqmkDg6k1Io4MAQGcEUPkil2aZ4s8KKparSep/OKHMC8vuXafFbW2HGp/9AcTRv0J5T4dwyW1G0JpHw+g5Ov6QI3Xo0O9RFr3KidICimpoxdjm3UYAAAABgADAgFiYgAzffHi4B365BPJfIJk/F+URKU1UekJ6g4QK02ypVb22YhQAAAAAAAAAAAAAAAAAQMAAA08Nzs="; assert.equal(output.encoded, expectedString) }); @@ -100,28 +100,28 @@ describe("TheOpenNetwork", () => { let privateKeyData = HexCoding.decode("c054900a527538c1b4325688a421c0469b171c29f23a62da216e90b0df2412ee"); - let transferData = TW.TheOpenNetwork.Proto.Transfer.create({ + let jettonTransfer = TW.TheOpenNetwork.Proto.JettonTransfer.create({ + jettonAmount: new Long(500 * 1000 * 1000), + toOwner: "EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8", + responseAddress: "EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk", + forwardAmount: new Long(1) + }); + + let transfer = TW.TheOpenNetwork.Proto.Transfer.create({ walletVersion: TW.TheOpenNetwork.Proto.WalletVersion.WALLET_V4_R2, dest: "EQBiaD8PO1NwfbxSkwbcNT9rXDjqhiIvXWymNO-edV0H5lja", amount: new Long(100 * 1000 * 1000), - sequenceNumber: 1, mode: (TW.TheOpenNetwork.Proto.SendMode.PAY_FEES_SEPARATELY | TW.TheOpenNetwork.Proto.SendMode.IGNORE_ACTION_PHASE_ERRORS), - expireAt: 1787693046, comment: "test comment", bounceable: true, + jettonTransfer: jettonTransfer, }); - let jettonTransfer = TW.TheOpenNetwork.Proto.JettonTransfer.create({ - transfer: transferData, - jettonAmount: new Long(500 * 1000 * 1000), - toOwner: "EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8", - responseAddress: "EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk", - forwardAmount: new Long(1) - }) - let input = TW.TheOpenNetwork.Proto.SigningInput.create({ - jettonTransfer: jettonTransfer, + messages: [transfer], privateKey: PrivateKey.createWithData(privateKeyData).data(), + sequenceNumber: 1, + expireAt: 1787693046, }); const encoded = TW.TheOpenNetwork.Proto.SigningInput.encode(input).finish(); @@ -129,7 +129,7 @@ describe("TheOpenNetwork", () => { let output = TW.TheOpenNetwork.Proto.SigningOutput.decode(outputData); // tx: https://testnet.tonscan.org/tx/Er_oT5R3QK7D-qVPBKUGkJAOOq6ayVls-mgEphpI9Ck= - let expectedString = "te6ccgICAAQAAQAAARgAAAFFiAC0UQZVyBNtT/W+jqQKnhYasPiDIdSWnNgo1FPyLHxLKgwAAQGcaIWVosi1XnveAmoG9y0/mPeNUqUu7GY76mdbRAaVeNeDOPDlh5M3BEb26kkc6XoYDekV60o2iOobN+TGS76jBSmpoxdqjgf2AAAAAQADAAIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEAAwDKD4p+pQAAAAAAAAAAQdzWUAgAC4GWcwteHQM+mcQoV+aZ+myCd1jYqUiawhCzuxMdEzkAFoogyrkCban+t9HUgVPCw1YfEGQ6ktObBRqKfkWPiWVCAgAAAAB0ZXN0IGNvbW1lbnQ="; + let expectedString = "te6cckECBAEAARUAAUWIALRRBlXIE21P9b6OpAqeFhqw+IMh1Jac2CjUU/IsfEsqDAEBnGiFlaLItV573gJqBvctP5j3jVKlLuxmO+pnW0QGlXjXgzjw5YeTNwRG9upJHOl6GA3pFetKNojqGzfkxku+owUpqaMXao4H9gAAAAEAAwIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEDAMoPin6lAAAAAAAAAABB3NZQCAALgZZzC14dAz6ZxChX5pn6bIJ3WNipSJrCELO7Ex0TOQAWiiDKuQJtqf630dSBU8LDVh8QZDqS05sFGop+RY+JZUICAAAAAHRlc3QgY29tbWVudG/bd5c="; assert.equal(output.encoded, expectedString) });