From 67b1c296d033a5d5fe982d2fa3def61c4b75f42e Mon Sep 17 00:00:00 2001 From: Vadim Volodin Date: Fri, 28 Jul 2023 11:57:18 +0400 Subject: [PATCH] Add The Open Network token (jetton) transfer functionality according to TEP-74 (#3331) --- .../TestTheOpenNetworkSigner.kt | 35 +++++++++++ src/Everscale/CommonTON/CellBuilder.cpp | 9 +++ src/Everscale/CommonTON/CellBuilder.h | 2 + src/TheOpenNetwork/Payloads.cpp | 42 +++++++++++++ src/TheOpenNetwork/Payloads.h | 19 ++++++ src/TheOpenNetwork/Signer.cpp | 48 ++++++++++++++- src/TheOpenNetwork/Signer.h | 3 + src/TheOpenNetwork/wallet/Wallet.cpp | 40 +++++++++---- src/TheOpenNetwork/wallet/Wallet.h | 14 ++++- src/proto/TheOpenNetwork.proto | 21 +++++++ .../Blockchains/TheOpenNetworkTests.swift | 35 +++++++++++ tests/chains/TheOpenNetwork/SignerTests.cpp | 59 +++++++++++++++++++ wasm/tests/Blockchain/TheOpenNetwork.test.ts | 41 +++++++++++++ 13 files changed, 352 insertions(+), 16 deletions(-) create mode 100644 src/TheOpenNetwork/Payloads.cpp create mode 100644 src/TheOpenNetwork/Payloads.h 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 f229ac373f3..887c3516493 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 @@ -48,4 +48,39 @@ class TestTheOpenNetworkSigner { assertEquals(output.encoded, expectedString) } + + @Test + fun TheOpenNetworkJettonTransferSigning() { + val privateKey = PrivateKey("c054900a527538c1b4325688a421c0469b171c29f23a62da216e90b0df2412ee".toHexByteArray()) + + val transferData = 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) + + val jettonTransfer = TheOpenNetwork.JettonTransfer.newBuilder() + .setTransfer(transferData) + .setJettonAmount(500 * 1000 * 1000) + .setToOwner("EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8") + .setResponseAddress("EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk") + .setForwardAmount(1) + .build() + + val input = TheOpenNetwork.SigningInput.newBuilder() + .setJettonTransfer(jettonTransfer) + .setPrivateKey(ByteString.copyFrom(privateKey.data())) + .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=" + + assertEquals(output.encoded, expectedString) + } } diff --git a/src/Everscale/CommonTON/CellBuilder.cpp b/src/Everscale/CommonTON/CellBuilder.cpp index afba8e4ba5a..e9b047280b9 100644 --- a/src/Everscale/CommonTON/CellBuilder.cpp +++ b/src/Everscale/CommonTON/CellBuilder.cpp @@ -225,6 +225,15 @@ void CellBuilder::appendWithDoubleShifting(const Data& appendedData, uint16_t bi } } +void CellBuilder::appendAddress(const AddressData& addressData) { + Data rawData(addressData.hash.begin(), addressData.hash.end()); + Data prefix{0x80}; + appendRaw(prefix, 2); + appendBitZero(); + appendI8(addressData.workchainId); + appendRaw(rawData, 256); +} + uint8_t CellBuilder::clzU128(const uint128_t& u) { auto hi = static_cast(u >> 64); auto lo = static_cast(u); diff --git a/src/Everscale/CommonTON/CellBuilder.h b/src/Everscale/CommonTON/CellBuilder.h index 0944222c925..36efae364d9 100644 --- a/src/Everscale/CommonTON/CellBuilder.h +++ b/src/Everscale/CommonTON/CellBuilder.h @@ -12,6 +12,7 @@ #include "Cell.h" #include "CellSlice.h" +#include "RawAddress.h" namespace TW::CommonTON { @@ -40,6 +41,7 @@ class CellBuilder { void appendReferenceCell(Cell::Ref child); void appendBuilder(const CellBuilder& builder); void appendCellSlice(const CellSlice& other); + void appendAddress(const AddressData& addressData); Cell::Ref intoCell(); diff --git a/src/TheOpenNetwork/Payloads.cpp b/src/TheOpenNetwork/Payloads.cpp new file mode 100644 index 00000000000..dcd3151b803 --- /dev/null +++ b/src/TheOpenNetwork/Payloads.cpp @@ -0,0 +1,42 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#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 new file mode 100644 index 00000000000..6f2b0274c14 --- /dev/null +++ b/src/TheOpenNetwork/Payloads.h @@ -0,0 +1,19 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#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 index 96e69cb872e..d7f18f1c98f 100644 --- a/src/TheOpenNetwork/Signer.cpp +++ b/src/TheOpenNetwork/Signer.cpp @@ -9,10 +9,11 @@ #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, @@ -29,6 +30,33 @@ Data Signer::createTransferMessage(std::shared_ptr wallet, const Private 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); @@ -56,6 +84,24 @@ Proto::SigningOutput Signer::sign(const Proto::SigningInput &input) noexcept { } 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; } diff --git a/src/TheOpenNetwork/Signer.h b/src/TheOpenNetwork/Signer.h index 78b76f57790..e18a49df046 100644 --- a/src/TheOpenNetwork/Signer.h +++ b/src/TheOpenNetwork/Signer.h @@ -23,6 +23,9 @@ class Signer { /// 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; }; diff --git a/src/TheOpenNetwork/wallet/Wallet.cpp b/src/TheOpenNetwork/wallet/Wallet.cpp index 5ff61790275..717b9d49b91 100644 --- a/src/TheOpenNetwork/wallet/Wallet.cpp +++ b/src/TheOpenNetwork/wallet/Wallet.cpp @@ -35,8 +35,8 @@ Cell::Ref Wallet::createSigningMessage( uint64_t amount, uint32_t sequence_number, uint8_t mode, - uint32_t expireAt, - const std::string& comment + const Cell::Ref& queryPayload, + uint32_t expireAt ) const { CellBuilder builder; this->writeSigningPayload(builder, sequence_number, expireAt); @@ -46,13 +46,7 @@ Cell::Ref Wallet::createSigningMessage( const auto header = std::make_shared(true, dest.isBounceable, dest.addressData, amount); TheOpenNetwork::Message internalMessage = TheOpenNetwork::Message(MessageData(header)); - CellBuilder bodyBuilder; - if (!comment.empty()) { - const auto& data = Data(comment.begin(), comment.end()); - bodyBuilder.appendU32(0); - bodyBuilder.appendRaw(data, static_cast(data.size()) * 8); - } - internalMessage.setBody(bodyBuilder.intoCell()); + internalMessage.setBody(queryPayload); builder.appendReferenceCell(internalMessage.intoCell()); } @@ -60,14 +54,14 @@ Cell::Ref Wallet::createSigningMessage( return builder.intoCell(); } -Cell::Ref Wallet::createTransferMessage( +Cell::Ref Wallet::createQueryMessage( const PrivateKey& privateKey, const Address& dest, uint64_t amount, uint32_t sequence_number, uint8_t mode, - uint32_t expireAt, - const std::string& comment + const Cell::Ref& queryPayload, + uint32_t expireAt ) const { const auto transferMessageHeader = std::make_shared(this->getAddress().addressData); Message transferMessage = Message(MessageData(transferMessageHeader)); @@ -78,7 +72,7 @@ Cell::Ref Wallet::createTransferMessage( { // Set body of transfer message CellBuilder bodyBuilder; - const Cell::Ref signingMessage = this->createSigningMessage(dest, amount, sequence_number, mode, expireAt, comment); + 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); @@ -91,4 +85,24 @@ Cell::Ref Wallet::createTransferMessage( 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 index 58bc9ee254b..c18d1088031 100644 --- a/src/TheOpenNetwork/wallet/Wallet.h +++ b/src/TheOpenNetwork/wallet/Wallet.h @@ -40,6 +40,16 @@ class Wallet { 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; @@ -50,8 +60,8 @@ class Wallet { uint64_t amount, uint32_t sequence_number, uint8_t mode, - uint32_t expireAt = 0, - const std::string& comment = "" + const Cell::Ref& payload, + uint32_t expireAt = 0 ) const; [[nodiscard]] CommonTON::StateInit createStateInit() const; }; diff --git a/src/proto/TheOpenNetwork.proto b/src/proto/TheOpenNetwork.proto index 73017fdb941..490a0c5f351 100644 --- a/src/proto/TheOpenNetwork.proto +++ b/src/proto/TheOpenNetwork.proto @@ -55,6 +55,26 @@ message Transfer { bool bounceable = 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; + + // 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; + + // Address of the new owner of the jettons. + string to_owner = 4; + + // 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; + + // Amount in nanotons to forward to recipient. Basically minimum amount - 1 nanoton should be used + uint64 forward_amount = 6; +} + message SigningInput { // The secret private key used for signing (32 bytes). bytes private_key = 1; @@ -62,6 +82,7 @@ message SigningInput { // The payload transfer oneof action_oneof { Transfer transfer = 2; + JettonTransfer jetton_transfer = 3; } } diff --git a/swift/Tests/Blockchains/TheOpenNetworkTests.swift b/swift/Tests/Blockchains/TheOpenNetworkTests.swift index 7508efab6c9..cda63c430fa 100644 --- a/swift/Tests/Blockchains/TheOpenNetworkTests.swift +++ b/swift/Tests/Blockchains/TheOpenNetworkTests.swift @@ -60,4 +60,39 @@ class TheOpenNetworkTests: XCTestCase { XCTAssertEqual(output.encoded, expectedString) } + + func testJettonTransferSign() { + let privateKeyData = Data(hexString: "c054900a527538c1b4325688a421c0469b171c29f23a62da216e90b0df2412ee")! + + let transferData = 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 + } + + let jettonTransfer = TheOpenNetworkJettonTransfer.with { + $0.transfer = transferData + $0.jettonAmount = 500 * 1000 * 1000 + $0.toOwner = "EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8" + $0.responseAddress = "EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk" + $0.forwardAmount = 1 + } + + let input = TheOpenNetworkSigningInput.with { + $0.jettonTransfer = jettonTransfer + $0.privateKey = privateKeyData + } + + 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=" + + XCTAssertEqual(output.encoded, expectedString) + } } diff --git a/tests/chains/TheOpenNetwork/SignerTests.cpp b/tests/chains/TheOpenNetwork/SignerTests.cpp index 44c49f9724a..655297436a2 100644 --- a/tests/chains/TheOpenNetwork/SignerTests.cpp +++ b/tests/chains/TheOpenNetwork/SignerTests.cpp @@ -170,4 +170,63 @@ TEST(TheOpenNetworkSigner, InvalidWalletVersion) { 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.tonscan.org/tx/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.tonscan.org/tx/Er_oT5R3QK7D-qVPBKUGkJAOOq6ayVls-mgEphpI9Ck= + // comment can be seen here: 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/wasm/tests/Blockchain/TheOpenNetwork.test.ts b/wasm/tests/Blockchain/TheOpenNetwork.test.ts index ffbec7723d9..46e5ffbdbf9 100644 --- a/wasm/tests/Blockchain/TheOpenNetwork.test.ts +++ b/wasm/tests/Blockchain/TheOpenNetwork.test.ts @@ -96,6 +96,47 @@ describe("TheOpenNetwork", () => { assert.equal(output.encoded, expectedString) }); + + it("test jetton transfer TheOpenNetwork", () => { + const { PrivateKey, HexCoding, CoinType, AnySigner } = globalThis.core; + + let privateKeyData = HexCoding.decode("c054900a527538c1b4325688a421c0469b171c29f23a62da216e90b0df2412ee"); + + let transferData = 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, + }); + + 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, + privateKey: PrivateKey.createWithData(privateKeyData).data(), + }); + + const encoded = TW.TheOpenNetwork.Proto.SigningInput.encode(input).finish(); + let outputData = AnySigner.sign(encoded, CoinType.ton); + 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="; + + assert.equal(output.encoded, expectedString) + }); + + });