Skip to content

Commit

Permalink
[TON]: Move TON blockchain to Rust (#3957)
Browse files Browse the repository at this point in the history
* [TON]: Add TheOpenNetwork Rust blockchain skeleton

* [TON]: Add Cell, BoC encoding

* Add base64 config

* [TON]: Add `RawAddress` and `UserFriendlyAddress`

* [TON]: Add `CellBuilder`

* [TON]: Add TonAddress user-friendly by default

* [TON]: Derive wallet_v4r2 address

* [TON]: Add standard TON transfer message

* feat(ton): Implement TON's Transaction Signer

* Add standard transfer and transfer with state_init tests

* feat(ton): Add 'SigningOutput::hash' for better UX

* Add more transfer tests

* feat(ton): Add Jetton transfer

* feat(ton): Add testto check if TransactionCompiler is not supported

* feat(ton): Add AddressConverter module

* Add `TWTONAddressConverterToBoc`, TWTONAddressConverterFromBoc`, `TWTONAddressConverterToUserFriendly` FFIs

* feat(ton): Increase test coverage

* feat(ton): Add tests for `CellParser`

* feat(ton): Add fuzz test

* feat(ton): Add address deriving test

* feat(ton): Add address deriving test to `codegen-v2` tool

* feat(ton): Add support for custom payload signing

* feat(ton): Refactor TheOpenNetwork.proto slightly by moving payload to Transfer message

* feat(ton): Remove C++ implementation

* feat(ton): Make `CustomPayload` working with `state_init`

* [TON]: Fix Android, iOS tests

* [CI] Trigger CI

* [TON]: Fix rustfmt, clippy warnings

* [TON]: Fix WASM tests

* [CI]: Try to update kotlin to fix KMP sample

* [TON]: Fix critical bugs in BoC encoding

* Add Transfer with custom payload tests on Android and iOS
  • Loading branch information
satoshiotomakan authored Jul 26, 2024
1 parent 221584f commit f14ae4c
Show file tree
Hide file tree
Showing 126 changed files with 5,565 additions and 1,436 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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=="]]}'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions include/TrustWalletCore/TWTONAddressConverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -4555,7 +4555,7 @@
"coinId": 607,
"symbol": "TON",
"decimals": 9,
"blockchain": "The Open Network",
"blockchain": "TheOpenNetwork",
"derivation": [
{
"path": "m/44'/607'/0'"
Expand Down
72 changes: 64 additions & 8 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions rust/chains/tw_solana/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,7 +70,7 @@ impl SolanaCompiler {
) -> SigningResult<Proto::SigningOutput<'static>> {
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() {
Expand Down
10 changes: 5 additions & 5 deletions rust/chains/tw_solana/src/modules/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,8 +34,7 @@ impl SolanaTransaction {
recent_blockhash: &str,
private_keys: &[Data],
) -> SigningResult<Proto::SigningOutput<'static>> {
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)?;
Expand Down Expand Up @@ -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),
Expand Down
5 changes: 3 additions & 2 deletions rust/chains/tw_solana/src/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,7 +31,7 @@ impl SolanaSigner {
) -> SigningResult<Proto::SigningOutput<'static>> {
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);
Expand Down
Loading

0 comments on commit f14ae4c

Please sign in to comment.