Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] Fix for jetton burn without custom payload and without response_destination (stTON case) #116

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions tondb-scanner/src/EventProcessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,7 @@ class ActionDetector: public td::actor::Actor {
return response_destination.move_as_error_prefix("Failed to unpack burn response destination: ");
}
burn.response_destination = response_destination.move_as_ok();
if (!burn_record.custom_payload.write().fetch_maybe_ref(burn.custom_payload)) {
return td::Status::Error("Failed to fetch custom payload");
}
burn_record.custom_payload_cell.write().prefetch_maybe_ref(burn.custom_payload);

return burn;
}
Expand Down
34 changes: 24 additions & 10 deletions tondb-scanner/src/InterfaceDetectors.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -552,8 +552,18 @@ class JettonWalletDetector: public InterfaceDetector<JettonWalletData> {
void parse_burn_impl(JettonWalletData contract_data, schema::Transaction transaction, td::Ref<vm::CellSlice> cs, td::Promise<JettonBurn> promise) {
tokens::gen::InternalMsgBody::Record_burn burn_record;
if (!tlb::csr_unpack_inexact(cs, burn_record)) {
promise.set_error(td::Status::Error(ErrorCode::EVENT_PARSING_ERROR, "Failed to unpack burn"));
return;
// handle stTON burn message format
// burn#595f07bc query_id:uint64 jetton_amount:Coins = InternalMsgBody;
if (contract_data.jetton == "0:CD872FA7C5816052ACDF5332260443FAEC9AACC8C21CCA4D92E7F47034D11892") {
cs.write().skip_first(32); // opcode
if (!cs.write().fetch_ulong_bool(64, burn_record.query_id) || !tokens::gen::t_VarUInteger_16.fetch_to(cs.write(), burn_record.amount)) {
promise.set_error(td::Status::Error(ErrorCode::EVENT_PARSING_ERROR, "Failed to unpack stTON burn"));
return;
}
} else {
promise.set_error(td::Status::Error(ErrorCode::EVENT_PARSING_ERROR, "Failed to unpack burn"));
return;
}
}

JettonBurn burn;
Expand All @@ -580,15 +590,19 @@ class JettonWalletDetector: public InterfaceDetector<JettonWalletData> {
promise.set_error(td::Status::Error(ErrorCode::EVENT_PARSING_ERROR, "Failed to unpack burn amount"));
return;
}
auto response_destination = convert::to_raw_address(burn_record.response_destination);
if (response_destination.is_error()) {
promise.set_error(response_destination.move_as_error());
return;
if (burn_record.response_destination.not_null()) {
auto response_destination = convert::to_raw_address(burn_record.response_destination);
if (response_destination.is_error()) {
promise.set_error(response_destination.move_as_error());
return;
}
burn.response_destination = response_destination.move_as_ok();
} else {
burn.response_destination = "addr_none";
}
burn.response_destination = response_destination.move_as_ok();
if (!burn_record.custom_payload.write().fetch_maybe_ref(burn.custom_payload)) {
promise.set_error(td::Status::Error(ErrorCode::EVENT_PARSING_ERROR, "Failed to fetch custom payload"));
return;
// since some messages don't have maybe ref cell at all we can ignore error here
if (burn_record.custom_payload_cell.not_null()) {
burn_record.custom_payload_cell.write().fetch_maybe_ref(burn.custom_payload);
}

promise.set_value(std::move(burn));
Expand Down
4 changes: 3 additions & 1 deletion tondb-scanner/src/tlb/tokens.tlb
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ transfer_notification#7362d09c query_id:uint64 amount:(VarUInteger 16)
excesses#d53276db query_id:uint64 = InternalMsgBody;

burn#595f07bc query_id:uint64 amount:(VarUInteger 16)
response_destination:MsgAddress custom_payload:(Maybe ^Cell)
response_destination:MsgAddress custom_payload_cell:Cell
= InternalMsgBody;
// Note that in TEP-74 custom_payload is (Maybe ^Cell), but reference implementation allows to ignore it
// So we use Cell instead of Maybe ^Cell and parse Maybe in the code

transfer_nft#5fcc3d14 query_id:uint64 new_owner:MsgAddress response_destination:MsgAddress
custom_payload:(Maybe ^Cell) forward_amount:(VarUInteger 16)
Expand Down
115 changes: 115 additions & 0 deletions tondb-scanner/test/tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,121 @@ TEST(TonDbScanner, JettonWalletDetector_parse_burn) {
scheduler.stop();
}

TEST(TonDbScanner, JettonWalletDetector_parse_burn_without_custom_payload) {
/**
* According to the TEP-74 (https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md) burn message should have the following layout:
* burn#595f07bc query_id:uint64 amount:(VarUInteger 16)
* response_destination:MsgAddress custom_payload:(Maybe ^Cell)
* = InternalMsgBody;
* In fact, the Maybe flag could be omitted at all and the referrence implementation ignores it (https://github.com/ton-blockchain/token-contract/blob/main/ft/jetton-wallet.fc#L170-L171)
* So parser should be able to handle such cases since it is handled on the contract side.
*/
td::actor::Scheduler scheduler({1});
auto watcher = td::create_shared_destructor([] { td::actor::SchedulerContext::get()->stop(); });

// message payload for tx Z2AuVEPZgtkFeLtneWg4i39xiOkEIDD9Lrhvz66amZU=
auto message_payload = vm::load_cell_slice_ref(vm::std_boc_deserialize(td::base64_decode(
td::Slice("te6cckEBAQEAFAAAI1lfB7wAAAAAAAAAAFGSVNOAAqKoE5Y=")).move_as_ok()).move_as_ok());

auto transaction = schema::Transaction();
transaction.account = block::StdAddress(std::string("EQCvlBsnlt-qAsiKtb9cb0IwFraA0bdTmj4K6bKbNQNjy1Cc")); // jetton wallet
transaction.in_msg = std::make_optional(schema::Message());
transaction.in_msg->source = "0:893B73E987E771F0E28326962C9373E4A02D2C90553D5E219383758159AAA7A6"; // owner

td::actor::ActorId<InsertManagerInterface> insert_manager;
td::actor::ActorOwn<JettonWalletDetector> jetton_wallet_detector;
td::actor::ActorOwn<InterfaceManager> interface_manager;
td::actor::ActorOwn<JettonMasterDetector> jetton_master_detector;

// prepare jetton metadata
std::unordered_map<std::string, JettonWalletData> cache;
JettonWalletData jetton_master;
jetton_master.jetton = "0:2DCBB7322C88C5ED4F8AD15806FE0724B1300AB482E46C908D8CF62567CE5C09";
cache.emplace(std::string("0:AF941B2796DFAA02C88AB5BF5C6F423016B680D1B7539A3E0AE9B29B350363CB"), jetton_master);

scheduler.run_in_context([&] {
interface_manager = td::actor::create_actor<InterfaceManager>("interface_manager", insert_manager);
jetton_master_detector = td::actor::create_actor<JettonMasterDetector>("jetton_master_detector", interface_manager.get(), insert_manager);

jetton_wallet_detector = td::actor::create_actor<JettonWalletDetector>("jetton_wallet_detector",
jetton_master_detector.get(), interface_manager.get(), insert_manager, cache);

auto P = td::PromiseCreator::lambda([&transaction, &jetton_master](td::Result<JettonBurn> R) {
CHECK(R.is_ok());
auto burn = R.move_as_ok();
ASSERT_EQ(transaction.in_msg->source.value(), burn.owner);
ASSERT_EQ(convert::to_raw_address(transaction.account), burn.jetton_wallet);
ASSERT_EQ(jetton_master.jetton, burn.jetton_master);
ASSERT_EQ(0, burn.query_id);
ASSERT_TRUE(burn.custom_payload.is_null());
CHECK(td::BigIntG<257>(108000000000) == **burn.amount.get());
});
td::actor::send_closure(jetton_wallet_detector, &JettonWalletDetector::parse_burn, transaction, message_payload, std::move(P));
watcher.reset();
});

scheduler.run(10);
scheduler.stop();
}

TEST(TonDbScanner, JettonWalletDetector_parse_burn_without_response_destination) {
/**
* The same situation as in the previous test - burn message doesn't have some required fields.
* stTON (bemo) burn message doesn't require response_destination field at all and
* in the documentation (https://docs.bemo.fi/developers/unstake) one can find a TL-B schema of the burn message:
* burn#595f07bc query_id:uint64 jetton_amount:Coins = InternalMsgBody;
*
* Since stTON is quite popular, it is worth to add it as an exception to the rule.
*/
td::actor::Scheduler scheduler({1});
auto watcher = td::create_shared_destructor([] { td::actor::SchedulerContext::get()->stop(); });

// message payload for tx bea8befb0d6a928fa00e4ec4e6035b760ec12d4b339dd6cb2d53077fbf99c87f
auto message_payload = vm::load_cell_slice_ref(vm::std_boc_deserialize(td::base64_decode(
td::Slice("te6ccgEBAQEAFAAAI1lfB7wAAAAAAAAAAFU2CBF7qA==")).move_as_ok()).move_as_ok());

auto transaction = schema::Transaction();
transaction.account = block::StdAddress(std::string("EQCbVZsB2jOe0DGc0DSUA-gxqzfuAjBI7mCvIxuYmWkzBNl-")); // jetton wallet
transaction.in_msg = std::make_optional(schema::Message());
transaction.in_msg->source = "0:E27383455DD29192D213773AD731540C1D83CE03BB9C9D59A13A9800335EAC22"; // owner

td::actor::ActorId<InsertManagerInterface> insert_manager;
td::actor::ActorOwn<JettonWalletDetector> jetton_wallet_detector;
td::actor::ActorOwn<InterfaceManager> interface_manager;
td::actor::ActorOwn<JettonMasterDetector> jetton_master_detector;

// prepare jetton metadata
std::unordered_map<std::string, JettonWalletData> cache;
JettonWalletData jetton_master;
jetton_master.jetton = "0:CD872FA7C5816052ACDF5332260443FAEC9AACC8C21CCA4D92E7F47034D11892";
cache.emplace(std::string("0:9B559B01DA339ED0319CD0349403E831AB37EE023048EE60AF231B9899693304"), jetton_master);

scheduler.run_in_context([&] {
interface_manager = td::actor::create_actor<InterfaceManager>("interface_manager", insert_manager);
jetton_master_detector = td::actor::create_actor<JettonMasterDetector>("jetton_master_detector", interface_manager.get(), insert_manager);

jetton_wallet_detector = td::actor::create_actor<JettonWalletDetector>("jetton_wallet_detector",
jetton_master_detector.get(), interface_manager.get(), insert_manager, cache);

auto P = td::PromiseCreator::lambda([&transaction, &jetton_master](td::Result<JettonBurn> R) {
CHECK(R.is_ok());
auto burn = R.move_as_ok();
ASSERT_EQ(transaction.in_msg->source.value(), burn.owner);
ASSERT_EQ(convert::to_raw_address(transaction.account), burn.jetton_wallet);
ASSERT_EQ(jetton_master.jetton, burn.jetton_master);
ASSERT_EQ(0, burn.query_id);
ASSERT_TRUE(burn.custom_payload.is_null());
ASSERT_EQ(std::string("addr_none"), burn.response_destination);
CHECK(td::BigIntG<257>(358101358522) == **burn.amount.get());
});
td::actor::send_closure(jetton_wallet_detector, &JettonWalletDetector::parse_burn, transaction, message_payload, std::move(P));
watcher.reset();
});

scheduler.run(10);
scheduler.stop();
}


TEST(TonDbScanner, JettonWalletDetector) {
td::actor::Scheduler scheduler({1});
Expand Down