Skip to content

Commit

Permalink
Add The Open Network token (jetton) transfer functionality according …
Browse files Browse the repository at this point in the history
…to TEP-74 (#3331)
  • Loading branch information
PolyProgrammist committed Jul 28, 2023
1 parent 2ab331d commit 67b1c29
Show file tree
Hide file tree
Showing 13 changed files with 352 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
9 changes: 9 additions & 0 deletions src/Everscale/CommonTON/CellBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint64_t>(u >> 64);
auto lo = static_cast<uint64_t>(u);
Expand Down
2 changes: 2 additions & 0 deletions src/Everscale/CommonTON/CellBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include "Cell.h"
#include "CellSlice.h"
#include "RawAddress.h"

namespace TW::CommonTON {

Expand Down Expand Up @@ -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();

Expand Down
42 changes: 42 additions & 0 deletions src/TheOpenNetwork/Payloads.cpp
Original file line number Diff line number Diff line change
@@ -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<uint16_t>(data.size()) * 8);
}
return bodyBuilder.intoCell();
}

} // namespace TW::TheOpenNetwork
19 changes: 19 additions & 0 deletions src/TheOpenNetwork/Payloads.h
Original file line number Diff line number Diff line change
@@ -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
48 changes: 47 additions & 1 deletion src/TheOpenNetwork/Signer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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> wallet, const PrivateKey& privateKey, const Proto::Transfer& transfer) {
const auto msg = wallet->createTransferMessage(
privateKey,
Expand All @@ -29,6 +30,33 @@ Data Signer::createTransferMessage(std::shared_ptr<Wallet> wallet, const Private
return result;
}

Data Signer::createJettonTransferMessage(std::shared_ptr<Wallet> 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<uint8_t>(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);
Expand Down Expand Up @@ -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<WalletV4R2>(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;
}
Expand Down
3 changes: 3 additions & 0 deletions src/TheOpenNetwork/Signer.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class Signer {
/// Creates a signed transfer message
static Data createTransferMessage(std::shared_ptr<Wallet> wallet, const PrivateKey& privateKey, const Proto::Transfer& transfer);

/// Creates a signed jetton transfer message
static Data createJettonTransferMessage(std::shared_ptr<Wallet> wallet, const PrivateKey& privateKey, const Proto::JettonTransfer& transfer);

/// Signs a Proto::SigningInput transaction
static Proto::SigningOutput sign(const Proto::SigningInput& input) noexcept;
};
Expand Down
40 changes: 27 additions & 13 deletions src/TheOpenNetwork/wallet/Wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -46,28 +46,22 @@ Cell::Ref Wallet::createSigningMessage(
const auto header = std::make_shared<CommonTON::InternalMessageHeader>(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<uint16_t>(data.size()) * 8);
}
internalMessage.setBody(bodyBuilder.intoCell());
internalMessage.setBody(queryPayload);

builder.appendReferenceCell(internalMessage.intoCell());
}

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<CommonTON::ExternalInboundMessageHeader>(this->getAddress().addressData);
Message transferMessage = Message(MessageData(transferMessageHeader));
Expand All @@ -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);

Expand All @@ -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<uint16_t>(data.size()) * 8);
}
return createQueryMessage(privateKey, dest, amount, sequence_number, mode, bodyBuilder.intoCell(), expireAt);
}


} // namespace TW::TheOpenNetwork
14 changes: 12 additions & 2 deletions src/TheOpenNetwork/wallet/Wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
};
Expand Down
21 changes: 21 additions & 0 deletions src/proto/TheOpenNetwork.proto
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,34 @@ 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;

// The payload transfer
oneof action_oneof {
Transfer transfer = 2;
JettonTransfer jetton_transfer = 3;
}
}

Expand Down
35 changes: 35 additions & 0 deletions swift/Tests/Blockchains/TheOpenNetworkTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading

0 comments on commit 67b1c29

Please sign in to comment.