diff --git a/compilables/ClaimHelper.compile.ts b/compilables/ClaimHelper.compile.ts new file mode 100644 index 0000000..42b9c4f --- /dev/null +++ b/compilables/ClaimHelper.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from '@ton/blueprint'; + +export const compile: CompilerConfig = { + lang: 'tact', + target: 'contracts/rewards/claim-helper.tact', +}; diff --git a/compilables/JettonMaster.compile.ts b/compilables/JettonMaster.compile.ts new file mode 100644 index 0000000..a7c4b39 --- /dev/null +++ b/compilables/JettonMaster.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from '@ton/blueprint'; + +export const compile: CompilerConfig = { + lang: 'func', + targets: ['contracts/jettons/standard/jetton_master.fc'], +}; diff --git a/compilables/JettonVault.compile.ts b/compilables/JettonVault.compile.ts new file mode 100644 index 0000000..3e08957 --- /dev/null +++ b/compilables/JettonVault.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from '@ton/blueprint'; + +export const compile: CompilerConfig = { + lang: 'tact', + target: 'contracts/rewards/jetton-vault.tact', +}; diff --git a/compilables/JettonWallet.compile.ts b/compilables/JettonWallet.compile.ts new file mode 100644 index 0000000..73a74c0 --- /dev/null +++ b/compilables/JettonWallet.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from '@ton/blueprint'; + +export const compile: CompilerConfig = { + lang: 'func', + targets: ['contracts/jettons/standard/jetton_wallet.fc'], +}; \ No newline at end of file diff --git a/compilables/RewardJettonMaster.compile.ts b/compilables/RewardJettonMaster.compile.ts new file mode 100644 index 0000000..84f87c4 --- /dev/null +++ b/compilables/RewardJettonMaster.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from '@ton/blueprint'; + +export const compile: CompilerConfig = { + lang: 'func', + targets: ['contracts/jettons/reward/reward_jetton_master.fc'], +}; diff --git a/contracts/jetton/messages.tact b/contracts/jetton/messages.tact index 0b9e000..8b7c174 100644 --- a/contracts/jetton/messages.tact +++ b/contracts/jetton/messages.tact @@ -21,6 +21,14 @@ message(0x7362d09c) TokenNotification { from: Address; forward_payload: Slice as remaining; } +// "op::transfer_notification"c (FunC) +message(0x4fb8dedc) TransferNotification { + queryId: Int as uint64; + amount: Int as coins; + from: Address; + forward_payload: Slice as remaining; +} + message(0x595f07bc) TokenBurn { queryId: Int as uint64; amount: Int as coins; diff --git a/contracts/jettons/common/jetton_utils.fc b/contracts/jettons/common/jetton_utils.fc new file mode 100644 index 0000000..69ebc5c --- /dev/null +++ b/contracts/jettons/common/jetton_utils.fc @@ -0,0 +1,39 @@ +#include "./params.fc"; +#include "./op_codes.fc"; + +cell pack_jetton_wallet_data(int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { + return begin_cell() + .store_coins(balance) + .store_slice(owner_address) + .store_slice(jetton_master_address) + .store_ref(jetton_wallet_code) + .end_cell(); +} + +;; get StateInit +cell calculate_jetton_wallet_state_init(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { + return begin_cell() + .store_uint(0, 2) + .store_dict(jetton_wallet_code) ;; code + .store_dict(pack_jetton_wallet_data(0, owner_address, jetton_master_address, jetton_wallet_code)) ;; data + .store_uint(0, 1) + .end_cell(); +} + +slice calculate_jetton_wallet_address(cell state_init) inline { + return begin_cell().store_uint(4, 3) + .store_int(workchain(), 8) + .store_uint(cell_hash(state_init), 256) + .end_cell() + .begin_parse(); +} + +slice calculate_user_jetton_wallet_address(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { + return calculate_jetton_wallet_address( + calculate_jetton_wallet_state_init( + owner_address, + jetton_master_address, + jetton_wallet_code + )); +} + diff --git a/contracts/jettons/common/op_codes.fc b/contracts/jettons/common/op_codes.fc new file mode 100644 index 0000000..5aee6f0 --- /dev/null +++ b/contracts/jettons/common/op_codes.fc @@ -0,0 +1,17 @@ +const op::transfer = "op::transfer"c; +const op::transfer_notification = "op::transfer_notification"c; +const op::internal_transfer = "op::internal_transfer"c; +const op::excesses = "op::excesses"c; +const op::burn = "op::burn"c; +const op::burn_notification = "op::burn_notification"c; + +;; Minter +const op::receiveInit = "op::receiveInit"c; +const op::mint = "op::mint"c; +const op::mint = "op::mint"c; + +const op::redeemMessage = "op::redeemMessage"c; + +;; Discovery params +const op::provide_wallet_address = "op::provide_wallet_address"c; +const op::take_wallet_address = "op::take_wallet_address"c; diff --git a/contracts/jettons/common/params.fc b/contracts/jettons/common/params.fc new file mode 100644 index 0000000..f561ea4 --- /dev/null +++ b/contracts/jettons/common/params.fc @@ -0,0 +1,12 @@ +int workchain() asm "0 PUSHINT"; + +() force_chain(slice addr) impure { + (int wc, _) = parse_std_addr(addr); + throw_unless(333, wc == workchain()); +} + +int is_resolvable?(slice addr) inline { + (int wc, _) = parse_std_addr(addr); + + return wc == workchain(); +} \ No newline at end of file diff --git a/contracts/jettons/reward/reward_jetton_master.fc b/contracts/jettons/reward/reward_jetton_master.fc new file mode 100644 index 0000000..f3f93b4 --- /dev/null +++ b/contracts/jettons/reward/reward_jetton_master.fc @@ -0,0 +1,198 @@ +#include "../../imports/stdlib.fc"; +#include "../common/jetton_utils.fc"; +#include "../common/params.fc"; +#include "../common/op_codes.fc"; + +const op::batch_mint = "op::batch_mint"c; + +;; Jettons discoverable smart contract + +(int, slice, cell, cell ) load_data() inline { + slice ds = get_data().begin_parse(); + return ( + ds~load_coins(), ;; total_supply + ds~load_msg_addr(), ;; admin_address + ds~load_ref(), ;; content + ds~load_ref() ;; jetton_wallet_code + ); +} + +() save_data(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) impure inline { + begin_cell() + .store_coins(total_supply) + .store_slice(admin_address) + .store_ref(content) + .store_ref(jetton_wallet_code) + .end_cell() + .set_data(); +} + +() mint_tokens(slice to_address, cell jetton_wallet_code, int amount, cell master_msg) impure { + cell state_init = calculate_jetton_wallet_state_init(to_address, my_address(), jetton_wallet_code); + slice to_wallet_address = calculate_jetton_wallet_address(state_init); + + var msg = begin_cell() + .store_uint(0x18, 6) ;; bounceable + .store_slice(to_wallet_address) + .store_coins(amount) + .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) + .store_ref(state_init) + .store_ref(master_msg); + send_raw_message(msg.end_cell(), 1); ;; pay transfer fees separately, revert on errors +} + +() send_text_message( slice to_addr, int value, int mode, builder content) impure { + var body = begin_cell() + .store_uint(0, 32) + .store_builder(content) + .end_cell(); + + var msg = begin_cell() + .store_uint(0x10, 6) ;; nobounce + .store_slice(to_addr) + .store_coins(value) + .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_ref(body) + .end_cell(); + + send_raw_message(msg, mode); +} + + +;; =========================== Main Entry Points =========================== +() recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure { + if (in_msg_body.slice_empty?()) { ;; ignore empty messages + return (); + } + + slice cs = in_msg_full.begin_parse(); + int flags = cs~load_uint(4); + if (flags & 1) { ;; ignore all bounced messages + return (); + } + + slice sender_address = cs~load_msg_addr(); + cs~load_msg_addr(); ;; skip dst + cs~load_coins(); ;; skip value + cs~skip_bits(1); ;; skip extracurrency collection + cs~load_coins(); ;; skip ihr_fee + int fwd_fee = muldiv(cs~load_coins(), 3, 2); + ;; we use message fwd_fee for estimation of forward_payload costs + + ;; check in_msg_body message + int op = in_msg_body~load_uint(32); + int query_id = in_msg_body~load_uint(64); + + (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); + + if (op == op::receiveInit) { ;; 0x0000015 + slice to_address = in_msg_body~load_msg_addr(); + builder msg_body = begin_cell().store_uint(0, 32).store_slice("Hello world!"); + send_text_message(to_address, 0, 64, msg_body); + return (); + } + + if (op == op::mint) { + throw_unless(73, equal_slices(sender_address, admin_address)); ;; only admin can mint - Wrapper Contract + + slice to_address = in_msg_body~load_msg_addr(); + int amount = in_msg_body~load_coins(); + cell master_msg = in_msg_body~load_ref(); ;; load a reference message + + slice master_msg_cs = master_msg.begin_parse(); + master_msg_cs~skip_bits(32 + 64); ;; op + query_id + int jetton_amount = master_msg_cs~load_coins(); + + mint_tokens(to_address, jetton_wallet_code, amount, master_msg); + save_data(total_supply + jetton_amount, admin_address, content, jetton_wallet_code); + return (); + } + if (op == op::batch_mint) { + ;; TODO: implement batch mint + ;; throw_unless(73, equal_slices(sender_address, admin_address)); + ;; cell master_msg = in_msg_body~load_ref(); ;; load a reference message + ;; slice master_msg_cs = master_msg.begin_parse(); + ;; master_msg_cs~skip_bits(32 + 64); ;; op + query_id + ;; int dict_size = master_msg_cs~load_uint(16); + ;; (slice s, cell dictionary_cell)= master_msg_cs~load_dict(); + ;; int added_jettons = 0; + ;; (int key, slice val, int flag) = dictionary_cell.udict_get_min?(32); + ;; while (flag) { + ;; slice to_addr = begin_cell().store_uint(key, 267).end_cell().begin_parse(); + ;; int val_as_int = val~load_coins(); + ;; mint_tokens(to_addr, jetton_wallet_code, val_as_int, master_msg); + ;; added_jettons += val_as_int; + ;; (key, val, flag) = dictionary_cell.udict_get_next?(32, key); + ;; } + ;; save_data(total_supply + added_jettons, admin_address, content, jetton_wallet_code); + ;; return (); + } + + if (op == op::burn_notification) { + int jetton_amount = in_msg_body~load_coins(); + slice from_address = in_msg_body~load_msg_addr(); + throw_unless(74, + equal_slices(calculate_user_jetton_wallet_address(from_address, my_address(), jetton_wallet_code), sender_address) + ); + save_data(total_supply - jetton_amount, admin_address, content, jetton_wallet_code); + + slice response_address = in_msg_body~load_msg_addr(); + if (response_address.preload_uint(2) != 0) { + var msg = begin_cell() + .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000 + .store_slice(response_address) + .store_coins(88) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + ;; ------ Op Code & Message body -------- + .store_uint(op::excesses, 32) + .store_uint(query_id, 64); + send_raw_message(msg.end_cell(), 1); + + var returnWrapper_msg = begin_cell() + .store_uint(0x10, 6) + .store_slice(admin_address) + .store_coins(0) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + ;; ------ Op Code & Message body -------- + .store_uint(3852451109, 32) ;; 0xe59fbd25 + .store_uint(query_id, 64) + .store_coins(jetton_amount) + .store_slice(response_address); + send_raw_message(returnWrapper_msg.end_cell(), 64); + } + return (); + } + + if (op == 3) { ;; change admin + throw_unless(73, equal_slices(sender_address, admin_address)); + slice new_admin_address = in_msg_body~load_msg_addr(); + save_data(total_supply, new_admin_address, content, jetton_wallet_code); + return (); + } + + if (op == 4) { ;; change content, delete this for immutable tokens + throw_unless(73, equal_slices(sender_address, admin_address)); + save_data(total_supply, admin_address, in_msg_body~load_ref(), jetton_wallet_code); + return (); + } + + throw(0xffff); +} + + +;; ------ Get Method ------ +(int, int, slice, cell, cell) get_jetton_data() method_id { + (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); + return ( + total_supply, + -1, + admin_address, + content, + jetton_wallet_code + ); +} + +slice get_wallet_address(slice owner_address) method_id { + (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); + return calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code); +} diff --git a/contracts/jettons/standard/jetton_master.fc b/contracts/jettons/standard/jetton_master.fc new file mode 100644 index 0000000..4e51e42 --- /dev/null +++ b/contracts/jettons/standard/jetton_master.fc @@ -0,0 +1,208 @@ +#include "../../imports/stdlib.fc"; +#include "../common/jetton_utils.fc"; +#include "../common/op_codes.fc"; + +;; Jettons discoverable smart contract + +;; 6905(computational_gas_price) * 1000(cur_gas_price) = 6905000 +;; ceil(6905000) = 10000000 ~= 0.01 TON +int provide_address_gas_consumption() asm "10000000 PUSHINT"; + + +(int, slice, cell, cell ) load_data() inline { + slice ds = get_data().begin_parse(); + return ( + ds~load_coins(), ;; total_supply + ds~load_msg_addr(), ;; admin_address + ds~load_ref(), ;; content + ds~load_ref() ;; jetton_wallet_code + ); +} + +() save_data(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) impure inline { + begin_cell() + .store_coins(total_supply) + .store_slice(admin_address) + .store_ref(content) + .store_ref(jetton_wallet_code) + .end_cell() + .set_data(); +} + +() mint_tokens(slice to_address, cell jetton_wallet_code, int amount, cell master_msg) impure { + cell state_init = calculate_jetton_wallet_state_init(to_address, my_address(), jetton_wallet_code); + slice to_wallet_address = calculate_jetton_wallet_address(state_init); + + var msg = begin_cell() + .store_uint(0x18, 6) ;; bounceable + .store_slice(to_wallet_address) + .store_coins(amount) + .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) + .store_ref(state_init) + .store_ref(master_msg); + send_raw_message(msg.end_cell(), 1); ;; pay transfer fees separately, revert on errors +} + +() send_text_message( slice to_addr, int value, int mode, builder content) impure { + var body = begin_cell() + .store_uint(0, 32) + .store_builder(content) + .end_cell(); + + var msg = begin_cell() + .store_uint(0x10, 6) ;; nobounce + .store_slice(to_addr) + .store_coins(value) + .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_ref(body) + .end_cell(); + + send_raw_message(msg, mode); +} + + +;; =========================== Main Entry Points =========================== +() recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure { + if (in_msg_body.slice_empty?()) { ;; ignore empty messages + return (); + } + + slice cs = in_msg_full.begin_parse(); + int flags = cs~load_uint(4); + if (flags & 1) { ;; ignore all bounced messages + return (); + } + + slice sender_address = cs~load_msg_addr(); + cs~load_msg_addr(); ;; skip dst + cs~load_coins(); ;; skip value + cs~skip_bits(1); ;; skip extracurrency collection + cs~load_coins(); ;; skip ihr_fee + int fwd_fee = muldiv(cs~load_coins(), 3, 2); + ;; we use message fwd_fee for estimation of forward_payload costs + + ;; check in_msg_body message + int op = in_msg_body~load_uint(32); + int query_id = in_msg_body~load_uint(64); + + (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); + + if (op == op::receiveInit) { ;; 0x0000015 + slice to_address = in_msg_body~load_msg_addr(); + builder msg_body = begin_cell().store_uint(0, 32).store_slice("Hello world!"); + send_text_message(to_address, 0, 64, msg_body); + return (); + } + + if (op == op::mint) { + throw_unless(73, equal_slices(sender_address, admin_address)); ;; only admin can mint - Wrapper Contract + + slice to_address = in_msg_body~load_msg_addr(); + int amount = in_msg_body~load_coins(); + cell master_msg = in_msg_body~load_ref(); ;; load a reference message + + slice master_msg_cs = master_msg.begin_parse(); + master_msg_cs~skip_bits(32 + 64); ;; op + query_id + int jetton_amount = master_msg_cs~load_coins(); + + mint_tokens(to_address, jetton_wallet_code, amount, master_msg); + save_data(total_supply + jetton_amount, admin_address, content, jetton_wallet_code); + return (); + } + + if (op == op::burn_notification) { + int jetton_amount = in_msg_body~load_coins(); + slice from_address = in_msg_body~load_msg_addr(); + throw_unless(74, + equal_slices(calculate_user_jetton_wallet_address(from_address, my_address(), jetton_wallet_code), sender_address) + ); + save_data(total_supply - jetton_amount, admin_address, content, jetton_wallet_code); + + slice response_address = in_msg_body~load_msg_addr(); + if (response_address.preload_uint(2) != 0) { + var msg = begin_cell() + .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000 + .store_slice(response_address) + .store_coins(88) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + ;; ------ Op Code & Message body -------- + .store_uint(op::excesses, 32) + .store_uint(query_id, 64); + send_raw_message(msg.end_cell(), 1); + + var returnWrapper_msg = begin_cell() + .store_uint(0x10, 6) + .store_slice(admin_address) + .store_coins(0) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + ;; ------ Op Code & Message body -------- + .store_uint(3852451109, 32) ;; 0xe59fbd25 + .store_uint(query_id, 64) + .store_coins(jetton_amount) + .store_slice(response_address); + send_raw_message(returnWrapper_msg.end_cell(), 64); + } + return (); + } + + if (op == op::provide_wallet_address) { + throw_unless(75, msg_value > fwd_fee + provide_address_gas_consumption()); + + slice owner_address = in_msg_body~load_msg_addr(); + int include_address? = in_msg_body~load_uint(1); + + cell included_address = include_address? + ? begin_cell().store_slice(owner_address).end_cell() + : null(); + + var msg = begin_cell() + .store_uint(0x18, 6) + .store_slice(sender_address) + .store_coins(0) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + ;; ------ Op Code & Message body -------- + .store_uint(op::take_wallet_address, 32) + .store_uint(query_id, 64); + + if (is_resolvable?(owner_address)) { + msg = msg.store_slice(calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code)); + } else { + msg = msg.store_uint(0, 2); ;; addr_none + } + send_raw_message(msg.store_maybe_ref(included_address).end_cell(), 64); + return (); + } + + if (op == 3) { ;; change admin + throw_unless(73, equal_slices(sender_address, admin_address)); + slice new_admin_address = in_msg_body~load_msg_addr(); + save_data(total_supply, new_admin_address, content, jetton_wallet_code); + return (); + } + + if (op == 4) { ;; change content, delete this for immutable tokens + throw_unless(73, equal_slices(sender_address, admin_address)); + save_data(total_supply, admin_address, in_msg_body~load_ref(), jetton_wallet_code); + return (); + } + + throw(0xffff); +} + + +;; ------ Get Method ------ +(int, int, slice, cell, cell) get_jetton_data() method_id { + (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); + return ( + total_supply, + -1, + admin_address, + content, + jetton_wallet_code + ); +} + +slice get_wallet_address(slice owner_address) method_id { + (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); + return calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code); +} diff --git a/contracts/jettons/standard/jetton_wallet.fc b/contracts/jettons/standard/jetton_wallet.fc new file mode 100644 index 0000000..3cb036d --- /dev/null +++ b/contracts/jettons/standard/jetton_wallet.fc @@ -0,0 +1,284 @@ +#include "../../imports/stdlib.fc"; +#include "../common/jetton_utils.fc"; +#include "../common/op_codes.fc"; + +;; Jetton Wallet Smart Contract + +{- + +NOTE that this tokens can be transferred within the same workchain. + +This is suitable for most tokens, if you need tokens transferable between workchains there are two solutions: + +1) use more expensive but universal function to calculate message forward fee for arbitrary destination (see `misc/forward-fee-calc.cs`) +2) use token holder proxies in target workchain (that way even 'non-universal' token can be used from any workchain) + +-} + +int min_tons_for_storage() asm "10000000 PUSHINT"; ;; 0.01 TON +;; Note that 2 * gas_consumptions is expected to be able to cover fees on both wallets (sender and receiver) +;; and also constant fees on inter-wallet interaction, in particular fwd fee on state_init transfer +;; that means that you need to reconsider this fee when: +;; a) jetton logic become more gas-heavy +;; b) jetton-wallet code (sent with inter-wallet message) become larger or smaller +;; c) global fee changes / different workchain +int gas_consumption() asm "15000000 PUSHINT"; ;; 0.015 TON + +{- + Storage + storage#_ balance:Coins owner_address:MsgAddressInt jetton_master_address:MsgAddressInt jetton_wallet_code:^Cell = Storage; +-} + +(int, slice, slice, cell) load_data() inline { + slice ds = get_data().begin_parse(); + return ( + ds~load_coins(), ;; balance + ds~load_msg_addr(), ;; owner_address + ds~load_msg_addr(), ;; jetton_master_address + ds~load_ref() ;; jetton_wallet_code + ); +} + +() save_data(int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) impure inline { + set_data( + pack_jetton_wallet_data( + balance, + owner_address, + jetton_master_address, + jetton_wallet_code + ) + ); +} + +{- + transfer query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress + response_destination:MsgAddress custom_payload:(Maybe ^Cell) + forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) + = InternalMsgBody; + + internal_transfer query_id:uint64 jetton_amount:(VarUInteger 16) from:MsgAddress + response_address:MsgAddress + forward_ton_amount:(VarUInteger 16) + forward_payload:(Either Cell ^Cell) + = InternalMsgBody; +-} + +() send_tokens(slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure { + + int query_id = in_msg_body~load_uint(64); + int jetton_amount = in_msg_body~load_coins(); + slice to_owner_address = in_msg_body~load_msg_addr(); + force_chain(to_owner_address); + + (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); + balance -= jetton_amount; + + throw_unless(705, equal_slices(owner_address, sender_address)); + throw_unless(706, balance >= 0); + + cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, jetton_wallet_code); + slice to_wallet_address = calculate_jetton_wallet_address(state_init); + + slice response_address = in_msg_body~load_msg_addr(); + cell custom_payload = in_msg_body~load_dict(); + int forward_ton_amount = in_msg_body~load_coins(); + + throw_unless(708, slice_bits(in_msg_body) >= 1); + slice either_forward_payload = in_msg_body; + + var msg = begin_cell() + .store_uint(0x18, 6) + .store_slice(to_wallet_address) + .store_coins(0) + .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) + .store_ref(state_init); + + var msg_body = begin_cell() + .store_uint(op::internal_transfer, 32) + .store_uint(query_id, 64) + .store_coins(jetton_amount) + .store_slice(owner_address) ;; from + .store_slice(response_address) ;; response_address + .store_coins(forward_ton_amount) ;; forward_ton_amount + .store_slice(either_forward_payload) ;; forward_payload + .end_cell(); + + msg = msg.store_ref(msg_body); + + int fwd_count = forward_ton_amount ? 2 : 1; + throw_unless(709, msg_value > + forward_ton_amount + + ;; 3 messages: wal1->wal2, wal2->owner, wal2->response + ;; but last one is optional (it is ok if it fails) + fwd_count * fwd_fee + + (2 * gas_consumption() + min_tons_for_storage())); + ;; universal message send fee calculation may be activated here + ;; by using this instead of fwd_fee + ;; msg_fwd_fee(to_wallet, msg_body, state_init, 15) + + send_raw_message(msg.end_cell(), 64); ;; revert on errors + save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); +} + +{- + internal_transfer query_id:uint64 amount:(VarUInteger 16) from:MsgAddress + response_address:MsgAddress + forward_ton_amount:(VarUInteger 16) + forward_payload:(Either Cell ^Cell) + = InternalMsgBody; +-} + +() receive_tokens(slice in_msg_body, slice sender_address, int my_ton_balance, int fwd_fee, int msg_value) impure { + ;; NOTE we can not allow fails in action phase since in that case there will be + ;; no bounce. Thus check and throw in computation phase. + (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); + int query_id = in_msg_body~load_uint(64); + + int jetton_amount = in_msg_body~load_coins(); + balance += jetton_amount; + + slice from_address = in_msg_body~load_msg_addr(); + + slice response_address = in_msg_body~load_msg_addr(); + throw_unless(707, + equal_slices(jetton_master_address, sender_address) + | + equal_slices(calculate_user_jetton_wallet_address(from_address, jetton_master_address, jetton_wallet_code), sender_address) + ); + + int forward_ton_amount = in_msg_body~load_coins(); + + int ton_balance_before_msg = my_ton_balance - msg_value; + int storage_fee = min_tons_for_storage() - min(ton_balance_before_msg, min_tons_for_storage()); + msg_value -= (storage_fee + gas_consumption()); + + if(forward_ton_amount) { + msg_value -= (forward_ton_amount + fwd_fee); + + slice either_forward_payload = in_msg_body; + + var msg_body = begin_cell() + .store_uint(op::transfer_notification, 32) + .store_uint(query_id, 64) + .store_coins(jetton_amount) + .store_slice(from_address) + .store_slice(either_forward_payload) + .end_cell(); + + var msg = begin_cell() + .store_uint(0x10, 6) ;; we should not bounce here cause receiver can have uninitialized contract + .store_slice(owner_address) + .store_coins(forward_ton_amount) + .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_ref(msg_body); + + send_raw_message(msg.end_cell(), 1); + } + + if ((response_address.preload_uint(2) != 0) & (msg_value > 0)) { + + var msg = begin_cell() + .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000 + .store_slice(response_address) + .store_coins(msg_value) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_uint(op::excesses, 32) + .store_uint(query_id, 64); + + send_raw_message(msg.end_cell(), 2); + } + + save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); +} + + +() burn_tokens(slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure { + ;; NOTE we can not allow fails in action phase since in that case there will be + ;; no bounce. Thus check and throw in computation phase. + (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); + int query_id = in_msg_body~load_uint(64); + int jetton_amount = in_msg_body~load_coins(); + slice response_address = in_msg_body~load_msg_addr(); + ;; ignore custom payload + ;; slice custom_payload = in_msg_body~load_dict(); + balance -= jetton_amount; + throw_unless(705, equal_slices(owner_address, sender_address)); + throw_unless(706, balance >= 0); + throw_unless(707, msg_value > fwd_fee + 2 * gas_consumption()); + + var msg_body = begin_cell() + .store_uint(op::burn_notification, 32) + .store_uint(query_id, 64) + .store_coins(jetton_amount) + .store_slice(owner_address) + .store_slice(response_address) + .end_cell(); + + var msg = begin_cell() + .store_uint(0x18, 6) + .store_slice(jetton_master_address) + .store_coins(0) + .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_ref(msg_body); + + send_raw_message(msg.end_cell(), 64); + + save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); +} + + +() on_bounce(slice in_msg_body) impure { + in_msg_body~skip_bits(32); ;; 0xFFFFFFFF + (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); + int op = in_msg_body~load_uint(32); + throw_unless(709, (op == op::internal_transfer) | (op == op::burn_notification)); + int query_id = in_msg_body~load_uint(64); + int jetton_amount = in_msg_body~load_coins(); + balance += jetton_amount; + save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); +} + + +() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { + if (in_msg_body.slice_empty?()) { ;; ignore empty messages + return (); + } + + slice cs = in_msg_full.begin_parse(); + int flags = cs~load_uint(4); + if (flags & 1) { + on_bounce(in_msg_body); + return (); + } + + slice sender_address = cs~load_msg_addr(); + cs~load_msg_addr(); ;; skip dst + cs~load_coins(); ;; skip value + cs~skip_bits(1); ;; skip extracurrency collection + cs~load_coins(); ;; skip ihr_fee + + int fwd_fee = muldiv(cs~load_coins(), 3, 2); ;; we use message fwd_fee for estimation of forward_payload costs + int op = in_msg_body~load_uint(32); + + if (op == op::transfer) { ;; outgoing transfer + send_tokens(in_msg_body, sender_address, msg_value, fwd_fee); + return (); + } + + if (op == op::internal_transfer) { ;; incoming transfer + receive_tokens(in_msg_body, sender_address, my_balance, fwd_fee, msg_value); + return (); + } + + if (op == op::burn) { ;; burn + burn_tokens(in_msg_body, sender_address, msg_value, fwd_fee); + return (); + } + + throw(0xffff); +} + + +(int, slice, slice, cell) get_wallet_data() method_id { + return load_data(); +} diff --git a/contracts/pool.tact b/contracts/pool.tact index ba69b1a..f0e958b 100644 --- a/contracts/pool.tact +++ b/contracts/pool.tact @@ -2,10 +2,6 @@ import "@stdlib/deploy"; import "@stdlib/ownable"; import "@stdlib/stoppable"; import "./user-account"; -import "./jetton/assetToken/atoken"; -import "./jetton/debtToken/dtoken"; -import "./jetton/assetToken/atoken-wallet"; -import "./jetton/debtToken/dtoken-wallet"; import "./types/struct"; import "./types/message"; import "./libraries/logic/reserve-logic"; @@ -44,7 +40,7 @@ contract Pool with Deployable, Ownable, Resumable, PoolView, PoolConfigurator, P // baseTokenAddress -> reserveConfiguration reservesConfiguration: map; reservesInterestRateStrategy: map; - + // oracle provider address oracleProvider: Address?; // ACL admins @@ -1131,10 +1127,10 @@ contract Pool with Deployable, Ownable, Resumable, PoolView, PoolConfigurator, P user: ownerAddress, msg: msg }; - + self.updatePositionMsg.set(queryId, updatePositionBounce); self.queryId += 1; - + // TODO: more gas check send(SendParameters{ to: userAccountAddress, diff --git a/contracts/rewards/claim-helper.tact b/contracts/rewards/claim-helper.tact new file mode 100644 index 0000000..6135014 --- /dev/null +++ b/contracts/rewards/claim-helper.tact @@ -0,0 +1,78 @@ +import "@stdlib/ownable"; +import "@stdlib/deploy"; +import "@stdlib/stoppable"; +import "../jetton/messages"; +import "../types/message"; +import "./message"; + +contract ClaimHelper with Ownable, Deployable, Resumable { + const MIN_TONS_FOR_STORAGE: Int = ton("0.03"); + const TOKEN_CLAIM_GAS_CONSUMPTION: Int = ton("0.05"); + const PROCESS_NOTIFICATION_GAS_CONSUMPTION: Int = ton("0.05"); + owner: Address; + stopped: Bool; + jettonMasterAddess: Address; + jettonWalletAddess: Address; + vaultAddress: Address; + + init(jettonMasterAddess: Address, vaultAddress: Address){ + self.jettonMasterAddess = jettonMasterAddess; + self.vaultAddress = vaultAddress; + self.stopped = true; + // should be reset after deployment + self.jettonWalletAddess = myAddress(); + self.owner = sender(); + } + + receive(msg: SetJettonWalletAddress) { + self.requireOwner(); + self.jettonWalletAddess = msg.newAddress; + // could also be reset by calling 'Resume' + self.stopped = false; + } + + receive(msg: TransferNotification){ + let ctx: Context = context(); + require(ctx.sender == self.jettonWalletAddess, "Invalid sender"); + let fwdFee: Int = ctx.readForwardFee() * 5; + let storageFee: Int = self.MIN_TONS_FOR_STORAGE; + let computationAndActionFee: Int = self.TOKEN_CLAIM_GAS_CONSUMPTION + self.PROCESS_NOTIFICATION_GAS_CONSUMPTION; + let totalFee: Int = (fwdFee + storageFee) + computationAndActionFee; + require(ctx.value > totalFee, "Insufficient fee"); + + // let forwardPayload: Slice = msg.forward_payload; + // let opCode: Int = forwardPayload.loadUint(32); + // "ClaimReward"c + // if (opCode == 0x7994ff68) { + let claimAmount: Int = msg.amount; + require(claimAmount > 0, "claim zero amount"); + let selfConsumption: Int = self.PROCESS_NOTIFICATION_GAS_CONSUMPTION + ctx.readForwardFee(); + let remainingMsgValue: Int = self.remainingValue(ctx.value, selfConsumption); + send(SendParameters{ + to: self.vaultAddress, + value: remainingMsgValue, + bounce: true, + mode: SendPayGasSeparately, + body: ClaimTargetJetton{ + jettonAddress: self.jettonMasterAddess, + owner: msg.from, + amount: claimAmount + }.toCell() + } + ); + // } + return; + } + + fun remainingValue(value: Int, selfConsumption: Int): Int { + let msgValue: Int = value; + let tonBalanceBeforeMsg: Int = myBalance() - msgValue; + let storageFee: Int = self.MIN_TONS_FOR_STORAGE - min(tonBalanceBeforeMsg, self.MIN_TONS_FOR_STORAGE); + msgValue = msgValue - (storageFee + selfConsumption); + return msgValue; + } + + // fun getJettonWalletInit(address: Address): StateInit { + // FunC implemented jetton wallet could not invoke initOf() func + // } +} \ No newline at end of file diff --git a/contracts/rewards/jetton-vault.tact b/contracts/rewards/jetton-vault.tact new file mode 100644 index 0000000..ee70759 --- /dev/null +++ b/contracts/rewards/jetton-vault.tact @@ -0,0 +1,140 @@ +import "@stdlib/ownable"; +import "@stdlib/deploy"; +import "@stdlib/stoppable"; +import "../jetton/messages"; +import "../types/message"; +import "./message"; +import "./struct.tact"; + +contract JettonVault with Ownable, Deployable, Resumable { + const MIN_TONS_FOR_STORAGE: Int = ton("0.03"); + const TOKEN_TRANSFER_GAS_CONSUMPTION: Int = ton("0.05"); + const TOKEN_CLAIM_GAS_CONSUMPTION: Int = ton("0.05"); + const PROCESS_NOTIFICATION_GAS_CONSUMPTION: Int = ton("0.05"); + const INITIATE_TIME_VESTING_GAS_CONSUMPTION: Int = ton("0.04"); + owner: Address; + stopped: Bool; + jettonWalletAddess: Address; + claimableConfigurations: map; + claimableConfigurationLength: Int = 0; + queryId: Int = 0; + init(){ + self.owner = sender(); + self.stopped = true; + // should be reset after deployment + self.jettonWalletAddess = myAddress(); + } + + receive(msg: SetJettonWalletAddress) { + self.requireOwner(); + self.jettonWalletAddess = msg.newAddress; + // could also be reset by calling 'Resume' + self.stopped = false; + } + + receive(msg: ConfigureClaimableConfiguration){ + self.requireOwner(); + let existedClaimableAddress: ClaimableConfiguration? = self.claimableConfigurations.get(msg.originJettonAddress); + // require(existedClaimableAddress == null, "Claimable Jetton already configured"); + self.claimableConfigurations.set(msg.originJettonAddress, ClaimableConfiguration { + jettonWalletAddress: msg.jettonWalletAddress, + targetBeneficiary: msg.targetBeneficiary, + claimType: msg.claimType, + claimHelper: msg.claimHelper, + }); + self.claimableConfigurationLength += 1; + self.reply("ClaimableConfiguration Added".asComment()); + } + + receive(msg: DropJettonMapping){ + self.requireOwner(); + let existedClaimableConfiguration: ClaimableConfiguration? = self.claimableConfigurations.get(msg.originJettonAddress); + require(existedClaimableConfiguration != null, "Claimable Jetton not configured"); + self.claimableConfigurations.del(msg.originJettonAddress); + self.claimableConfigurationLength -= 1; + self.reply("ClaimableConfiguration Dropped".asComment()); + } + + receive(msg: ClaimTargetJetton){ + let ctx: Context = context(); + let config: ClaimableConfiguration? = self.claimableConfigurations.get(msg.jettonAddress); + require(config != null, "Invalid jetton address"); + let jettonWalletAddress: Address = config!!.jettonWalletAddress; + require(ctx.sender == config!!.claimHelper, "Invalid sender"); + + let fwdFee: Int = ctx.readForwardFee() * 5; + let storageFee: Int = self.MIN_TONS_FOR_STORAGE; + let computationAndActionFee: Int = self.TOKEN_CLAIM_GAS_CONSUMPTION; + let totalFee: Int = (fwdFee + storageFee) + computationAndActionFee; + require(ctx.value > totalFee, "Insufficient fee"); + + let selfConsumption: Int = self.PROCESS_NOTIFICATION_GAS_CONSUMPTION + ctx.readForwardFee(); + let remainingMsgValue: Int = self.remainingValue(ctx.value, selfConsumption); + + let claimType: Int = config!!.claimType; + if (claimType == 0) { + self.sendJettonTransferViaVault(jettonWalletAddress, msg.owner, msg.amount); + } + if (claimType == 1) { + self.initiateTimeVestingViaVault(jettonWalletAddress, config!!.targetBeneficiary, msg.owner, msg.amount); + } + } + + fun sendJettonTransferViaVault(jettonWalletAddress: Address, toAddress: Address, amount: Int) { + let tokenTransferMsg: TokenTransfer = TokenTransfer{ + queryId: self.queryId, + amount: amount, + destination: toAddress, + response_destination: toAddress, + custom_payload: null, + forward_ton_amount: 0, + forward_payload: emptySlice() + }; + self.queryId += 1; + + send(SendParameters{ + to: jettonWalletAddress, + value: 0, + bounce: true, + mode: SendRemainingValue, + body: tokenTransferMsg.toCell() + }); + } + + fun initiateTimeVestingViaVault(jettonWalletAddress: Address, toAddress: Address, owner: Address, amount: Int) { + let tokenTransferMsg: TokenTransfer = TokenTransfer{ + queryId: self.queryId, + amount: amount, + destination: toAddress, + response_destination: owner, + custom_payload: null, + forward_ton_amount: self.INITIATE_TIME_VESTING_GAS_CONSUMPTION, + forward_payload: beginCell() + .storeUint(0x63ed65e, 32) // opcode: AddLock + .storeAddress(owner) + .endCell() + .asSlice() + }; + self.queryId += 1; + + send(SendParameters{ + to: jettonWalletAddress, + value: 0, + bounce: true, + mode: SendRemainingValue, + body: tokenTransferMsg.toCell() + }); + } + + get fun allClaimableJettonMapping(): map { + return self.claimableConfigurations; + } + + fun remainingValue(value: Int, selfConsumption: Int): Int { + let msgValue: Int = value; + let tonBalanceBeforeMsg: Int = myBalance() - msgValue; + let storageFee: Int = self.MIN_TONS_FOR_STORAGE - min(tonBalanceBeforeMsg, self.MIN_TONS_FOR_STORAGE); + msgValue = msgValue - (storageFee + selfConsumption); + return msgValue; + } +} \ No newline at end of file diff --git a/contracts/rewards/message.tact b/contracts/rewards/message.tact new file mode 100644 index 0000000..951d9dd --- /dev/null +++ b/contracts/rewards/message.tact @@ -0,0 +1,21 @@ +message(0x2daf1323) ClaimTargetJetton { + jettonAddress: Address; + owner: Address; + amount: Int as coins; +} + +message(0x8aed76c1) ConfigureClaimableConfiguration { + originJettonAddress: Address; + jettonWalletAddress: Address; + targetBeneficiary: Address; + claimType: Int; + claimHelper: Address; +} + +message(0x5891a820) DropJettonMapping { + originJettonAddress: Address; +} + +message(0x4b6be393) SetJettonWalletAddress { + newAddress: Address +} \ No newline at end of file diff --git a/contracts/rewards/struct.tact b/contracts/rewards/struct.tact new file mode 100644 index 0000000..31bc6c3 --- /dev/null +++ b/contracts/rewards/struct.tact @@ -0,0 +1,8 @@ +struct ClaimableConfiguration { + claimHelper: Address; + jettonWalletAddress: Address; + targetBeneficiary: Address; + claimType: Int; + // - 0: Claim via transfer + // - 1; Claim via time vesting +} \ No newline at end of file diff --git a/scripts/deployPool.ts b/scripts/deployPool.ts index 5da306f..c1a18a0 100644 --- a/scripts/deployPool.ts +++ b/scripts/deployPool.ts @@ -1,40 +1,54 @@ -import { toNano } from '@ton/core'; +import { OpenedContract, Sender, toNano } from '@ton/core'; import { Pool } from '../wrappers/Pool'; import { NetworkProvider, sleep } from '@ton/blueprint'; import { ACL } from '../helpers/constant'; -import { waitNextSeqno } from './utils'; import { waitForTx } from '../helpers/address'; +async function sendWithRetry(pool: OpenedContract, sender: Sender, value: bigint, message: any, retries = 10, delay = 5000) { + for (let i = 0; i < retries; i++) { + try { + await pool.send(sender, { value }, message); + return; + } catch (error: any) { + if (error.response && error.response.status === 429) { + console.log(`429 error encountered. Retrying in ${delay}ms...`); + console.log(error.response.data); + await sleep(delay); + } else { + throw error; + } + } + } + throw new Error('Max retries reached'); +} + export async function run(provider: NetworkProvider) { - console.log('Deploying pool...'); const pool = provider.open(await Pool.fromInit()); - await sleep(2000); + console.log(`[${provider.network()}] Deploying pool`) - await pool.send( + await sendWithRetry( + pool, provider.sender(), - { - value: toNano('0.05'), - }, + toNano('0.05'), { $$type: 'Deploy', queryId: 0n, - }, + } ); await sleep(2000); // latest: EQDlTidB1AZqnPwrtgYoai88pgr_rA1ATzB0pKke2cuQR2rI await provider.waitForDeploy(pool.address); console.log(`Deployed at ${pool.address.toString()}`); - await pool.send( + await sendWithRetry( + pool, provider.sender(), - { - value: toNano('0.1'), - }, + toNano('0.1'), { $$type: 'GrantRole', role: ACL.ASSET_LISTING_ADMIN_ROLE, admin: provider.sender().address!!, - }, + } ); await waitForTx(provider, pool.address); console.log( diff --git a/tests/ClaimRewards-FunCJetton.spec.ts b/tests/ClaimRewards-FunCJetton.spec.ts new file mode 100644 index 0000000..63c6e4d --- /dev/null +++ b/tests/ClaimRewards-FunCJetton.spec.ts @@ -0,0 +1,788 @@ +import { Blockchain, printTransactionFees, SandboxContract, TreasuryContract } from '@ton/sandbox'; +import { beginCell, Cell, toNano, Address } from '@ton/core'; +import '@ton/test-utils'; +import { SampleJetton } from '../build/SampleJetton/tact_SampleJetton'; +import { buildOnchainMetadata } from '../scripts/utils'; +import { JettonDefaultWallet } from '../build/SampleJetton/tact_JettonDefaultWallet'; +import { ClaimHelper } from '../wrappers/ClaimHelper'; +import { JettonVault } from '../wrappers/JettonVault'; +import { RewardJettonMaster } from '../wrappers/RewardJettonMaster'; +import { compile } from '@ton/blueprint'; +import { JettonWallet } from '../wrappers/JettonWallet'; +import { sumTransactionsFee } from '../jest.setup'; +import { MockTay } from '../wrappers/MockTay'; +import { TimeVestingMaster } from '../wrappers/TimeVestingMaster'; +import { TimeVesting } from '../wrappers/TimeVesting'; + +describe('ClaimRewards', () => { + let blockchain: Blockchain; + let deployer: SandboxContract; + let timeVestingMaster: SandboxContract; + let tay: SandboxContract; + let usdt: SandboxContract; + + // FunC implemented jetton contract + let rewardJettonMasterCode: Cell; + let rewardJettonWalletCode: Cell; + let usdtRewardJettonMaster: SandboxContract; + let usdtRewardJettonWallet: SandboxContract; + let tayRewardJettonMaster: SandboxContract; + let tayRewardJettonWallet: SandboxContract; + + let jettonVault: SandboxContract; + let usdtClaimHelper: SandboxContract; + let tayClaimHelper: SandboxContract; + let constructJettonWalletConfig: any; + let constructRewardJettonMasterConfig: any; + + let mintMockTay: any; + + beforeEach(async () => { + blockchain = await Blockchain.create(); + deployer = await blockchain.treasury('deployer'); + const jettonParams = { + name: 'USDT-Jetton', + description: 'Sample USDT Jetton for testing purposes', + decimals: '6', + image: 'https://ipfs.io/ipfs/bafybeicn7i3soqdgr7dwnrwytgq4zxy7a5jpkizrvhm5mv6bgjd32wm3q4/welcome-to-IPFS.jpg', + symbol: 'USDT', + }; + const usdtRewardJettonParams = { + ...jettonParams, + name: 'USDT-Reward-Jetton', + symbol: "T-USDT", + } + const tayJettonParams = { + ...jettonParams, + name: 'TonLayer Token', + description: 'TonLayer Token', + symbol: "TAY", + decimal: "9", + } + const tayRewardJettonParams = { + ...jettonParams, + name: 'TAY-Reward-Jetton', + description: 'TonLayer Reward Token', + symbol: "T-TAY", + decimal: "9", + } + + // It's the largest value I can use for max_supply in the tests + let max_supply = (1n << 120n) - 1n; + // let max_supply = toNano(1000000n); // ๐Ÿ”ด Set the specific total supply in nano + let content = buildOnchainMetadata(jettonParams); + let tayContent = buildOnchainMetadata(tayJettonParams); + let usdtRewardJettonContent = buildOnchainMetadata(usdtRewardJettonParams); + let tayRewardJettonContent = buildOnchainMetadata(tayRewardJettonParams); + + usdt = blockchain.openContract(await SampleJetton.fromInit(deployer.address, content, max_supply)); + tay = blockchain.openContract(await MockTay.fromInit(deployer.address, tayContent, max_supply)); + timeVestingMaster = blockchain.openContract(await TimeVestingMaster.fromInit()); + + rewardJettonWalletCode = await compile('JettonWallet'); + rewardJettonMasterCode = await compile('RewardJettonMaster'); + + constructRewardJettonMasterConfig = (ownerAddress: Address, contentData: any) => + blockchain.openContract(RewardJettonMaster.createFromConfig( + { + admin: ownerAddress, + content: contentData, + walletCode: rewardJettonWalletCode, + }, + rewardJettonMasterCode + )); + + constructJettonWalletConfig = (ownerAddress: Address, minterAddress: Address) => + blockchain.openContract( + JettonWallet.createFromConfig( + { + owner: ownerAddress, + minter: minterAddress, + walletCode: rewardJettonWalletCode, + }, + rewardJettonWalletCode + )); + + usdtRewardJettonMaster = constructRewardJettonMasterConfig(deployer.address, usdtRewardJettonContent); + usdtRewardJettonWallet = constructJettonWalletConfig(deployer.address, usdtRewardJettonMaster.address) + tayRewardJettonMaster = constructRewardJettonMasterConfig(deployer.address, tayRewardJettonContent); + tayRewardJettonWallet = constructJettonWalletConfig(deployer.address, tayRewardJettonMaster.address) + + jettonVault = blockchain.openContract(await JettonVault.fromInit()); + const usdtDeployResult = await usdt.send( + deployer.getSender(), + { + value: toNano('0.05'), + }, + { + $$type: 'Deploy', + queryId: 0n, + }, + ); + expect(usdtDeployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: usdt.address, + deploy: true, + success: true, + }); + + const tayDeployResult = await tay.send( + deployer.getSender(), + { + value: toNano('0.05'), + }, + { + $$type: 'Deploy', + queryId: 0n, + }, + ); + expect(tayDeployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: tay.address, + deploy: true, + success: true, + }); + await timeVestingMaster.send( + deployer.getSender(), + { + value: toNano('0.05'), + }, + { + $$type: 'Deploy', + queryId: 0n, + }, + ); + const timeVestingMasterTayWallet = await tay.getGetWalletAddress(timeVestingMaster.address); + await timeVestingMaster.send( + deployer.getSender(), + { + value: toNano('0.5'), + }, + { + $$type: 'SetTayWallet', + tayWallet: timeVestingMasterTayWallet, + }, + ); + mintMockTay = async (jetton: SandboxContract, receiver: Address, amount: bigint) => { + await jetton.send( + deployer.getSender(), + { + value: toNano('0.05'), + }, + { + $$type: 'Mint', + queryId: 0n, + amount, + receiver, + }, + ); + }; + + const deployUsdtRewardJetton = await usdtRewardJettonMaster.sendDeploy( + deployer.getSender(), + toNano('0.05'), + ); + expect(deployUsdtRewardJetton.transactions).toHaveTransaction({ + from: deployer.address, + to: usdtRewardJettonMaster.address, + deploy: true, + success: true, + }) + const deployTayRewardJetton = await tayRewardJettonMaster.sendDeploy( + deployer.getSender(), + toNano('0.05'), + ); + expect(deployTayRewardJetton.transactions).toHaveTransaction({ + from: deployer.address, + to: tayRewardJettonMaster.address, + deploy: true, + success: true, + }) + + const deployJettonVault = await jettonVault.send( + deployer.getSender(), + { + value: toNano('0.05'), + }, + { + $$type: 'Deploy', + queryId: 0n, + }, + ); + expect(deployJettonVault.transactions).toHaveTransaction({ + from: deployer.address, + to: jettonVault.address, + deploy: true, + success: true, + }) + const jettonVaultUsdtWalletAddress = await usdt.getGetWalletAddress(jettonVault.address); + await jettonVault.send( + deployer.getSender(), + { + value: toNano("0.05"), + }, + { + + $$type: 'SetJettonWalletAddress', + newAddress: jettonVaultUsdtWalletAddress, + }, + ) + + const jettonVaultTayWalletAddress = await tay.getGetWalletAddress(jettonVault.address); + await jettonVault.send( + deployer.getSender(), + { + value: toNano("0.05"), + }, + { + + $$type: 'SetJettonWalletAddress', + newAddress: jettonVaultTayWalletAddress, + }, + ) + + usdtClaimHelper = blockchain.openContract(await ClaimHelper.fromInit(usdtRewardJettonMaster.address, jettonVault.address)); + const deployUsdtClaimHelper = await usdtClaimHelper.send( + deployer.getSender(), + { + value: toNano('0.05'), + }, + { + $$type: 'Deploy', + queryId: 0n, + }, + ) + expect(deployUsdtClaimHelper.transactions).toHaveTransaction({ + from: deployer.address, + to: usdtClaimHelper.address, + deploy: true, + success: true, + }) + tayClaimHelper = blockchain.openContract(await ClaimHelper.fromInit(tayRewardJettonMaster.address, jettonVault.address)); + await tayClaimHelper.send( + deployer.getSender(), + { + value: toNano('0.05'), + }, + { + $$type: 'Deploy', + queryId: 0n, + }, + ) + + const usdtClaimHelperUsdtRewardJettonWalletAddress = await usdtRewardJettonMaster.getWalletAddress(usdtClaimHelper.address); + await usdtClaimHelper.send( + deployer.getSender(), + { + value: toNano("0.05"), + }, + { + + $$type: 'SetJettonWalletAddress', + newAddress: usdtClaimHelperUsdtRewardJettonWalletAddress, + }, + ) + const tayClaimHelperTayRewardJettonWalletAddress = await tayRewardJettonMaster.getWalletAddress(tayClaimHelper.address); + await tayClaimHelper.send( + deployer.getSender(), + { + value: toNano("0.05"), + }, + { + + $$type: 'SetJettonWalletAddress', + newAddress: tayClaimHelperTayRewardJettonWalletAddress, + }, + ) + + const setUsdtClaimable = await jettonVault.send( + deployer.getSender(), + { + value: toNano('0.05'), + }, + { + $$type: 'ConfigureClaimableConfiguration', + originJettonAddress: usdtRewardJettonMaster.address, + jettonWalletAddress: jettonVaultUsdtWalletAddress, + targetBeneficiary: jettonVaultUsdtWalletAddress, + claimType: 0n, + claimHelper: usdtClaimHelper.address, + }, + ); + expect(setUsdtClaimable.transactions).toHaveTransaction({ + from: deployer.address, + to: jettonVault.address, + success: true, + }); + + const setTayClaimable = await jettonVault.send( + deployer.getSender(), + { + value: toNano('0.05'), + }, + { + $$type: 'ConfigureClaimableConfiguration', + originJettonAddress: tayRewardJettonMaster.address, + jettonWalletAddress: jettonVaultTayWalletAddress, + targetBeneficiary: timeVestingMaster.address, + claimType: 1n, + claimHelper: tayClaimHelper.address, + }, + ); + expect(setTayClaimable.transactions).toHaveTransaction({ + from: deployer.address, + to: jettonVault.address, + success: true, + }); + + // const mapping = await jettonVault.getAllClaimableJettonMapping(); + // console.log(mapping); + }); + + describe('claim USDT & TAY rewards', () => { + it('should claim USDT successfully', async () => { + await usdt.send( + deployer.getSender(), + { + value: toNano('0.05'), + }, + { + $$type: 'Mint', + queryId: 0n, + amount: toNano("1"), + receiver: jettonVault.address, + }, + ); + + const sender = deployer.getSender(); + const senderAddress = sender.address; + const mintAmount = toNano("1") + const mintUsdtRewardJettonResult = await usdtRewardJettonMaster.sendMint( + sender, + senderAddress, + mintAmount, + toNano('0.05'), + toNano('0.06'), + ); + expect(mintUsdtRewardJettonResult.transactions).toHaveTransaction({ + from: undefined, + oldStatus: "active", + endStatus: "active", + success: true, + }); + + const jettonVaultUsdtRewardJettonWalletAddress = await usdtRewardJettonMaster.getWalletAddress(jettonVault.address); + const senderUsdtRewardJettonWalletAddress = await usdtRewardJettonMaster.getWalletAddress(senderAddress); + const usdtClaimHelperUsdtRewardJettonWalletAddress = await usdtRewardJettonMaster.getWalletAddress(usdtClaimHelper.address); + const jettonVaultUsdtWalletAddress = await usdt.getGetWalletAddress(jettonVault.address); + const senderUsdtWalletAddress = await usdt.getGetWalletAddress(senderAddress); + const usdtClaimHelperWalletAddress = await usdt.getGetWalletAddress(usdtClaimHelper.address); + + const jettonVaultUsdtRewardJettonWallet = constructJettonWalletConfig(jettonVault.address, usdtRewardJettonMaster.address) + const usdtClaimHelperUsdtRewardJettonWallet = constructJettonWalletConfig(usdtClaimHelper.address, usdtRewardJettonMaster.address) + const senderUsdtRewardJettonWallet = constructJettonWalletConfig(senderAddress, usdtRewardJettonMaster.address) + + const jettonVaultUsdtWallet = blockchain.openContract(JettonDefaultWallet.fromAddress(jettonVaultUsdtWalletAddress)); + const senderUsdtWallet = blockchain.openContract(JettonDefaultWallet.fromAddress(senderUsdtWalletAddress)); + const usdtClaimHelperWallet = blockchain.openContract(JettonDefaultWallet.fromAddress(usdtClaimHelperWalletAddress)); + + const senderUsdtRewardJettonWalletBalanceBefore = await senderUsdtRewardJettonWallet.getJettonBalance(); + const usdtClaimHelperUsdtRewardJettonWalletBalanceBefore = await usdtClaimHelperUsdtRewardJettonWallet.getJettonBalance(); + const jettonVaultUsdtRewardJettonWalletBalanceBefore = await jettonVaultUsdtRewardJettonWallet.getJettonBalance(); + // let senderUsdtWalletBalanceBefore = 0, usdtClaimHelperWalletBalanceBefore = 0, jettonVaultUsdtWalletBalanceBefore = 0; + // const senderUsdtWalletBalanceBefore = (await senderUsdtWallet.getGetWalletData()).balance; + // const usdtClaimHelperWalletBalanceBefore = (await usdtClaimHelperWallet.getGetWalletData()).balance; + const jettonVaultUsdtWalletBalanceBefore = (await jettonVaultUsdtWallet.getGetWalletData()).balance; + + const claimAmount = toNano("0.3"); + const claimUsdtRewardResult = await usdtRewardJettonWallet.sendTransfer( + sender, + toNano('0.35'), + toNano('0.35'), + usdtClaimHelper.address, + claimAmount, + beginCell().storeUint(0x7994ff68, 32).endCell(), // opcode: ClaimReward + ); + + // console.log({ + // senderAddress: deployer.getSender().address.toString(), + // usdt: usdt.address.toString(), + // usdtRewardJettonMaster: usdtRewardJettonMaster.address.toString(), + // usdtClaimHelper: usdtClaimHelper.address.toString(), + // jettonVault: jettonVault.address.toString(), + // jettonVaultUsdtRewardJettonWalletAddress: jettonVaultUsdtRewardJettonWalletAddress.toString(), + // senderUsdtRewardJettonWalletAddress: senderUsdtRewardJettonWalletAddress.toString(), + // usdtClaimHelperUsdtRewardJettonWalletAddress: usdtClaimHelperUsdtRewardJettonWallet.address.toString(), + // jettonVaultUsdtWalletAddress: jettonVaultUsdtWalletAddress.toString(), + // senderUsdtWalletAddress: senderUsdtWalletAddress.toString(), + // usdtClaimHelperWalletAddress: usdtClaimHelperWalletAddress.toString(), + // }) + // external message + expect(claimUsdtRewardResult.transactions).toHaveTransaction({ + from: undefined, + to: senderAddress, + outMessagesCount: 1, + success: true, + }); + // op::transfer message + expect(claimUsdtRewardResult.transactions).toHaveTransaction({ + from: senderAddress, + to: senderUsdtRewardJettonWalletAddress, + outMessagesCount: 1, + success: true, + }); + // op::internal_transfer message + expect(claimUsdtRewardResult.transactions).toHaveTransaction({ + from: senderUsdtRewardJettonWalletAddress, + to: usdtClaimHelperUsdtRewardJettonWalletAddress, + outMessagesCount: 2, + oldStatus: "uninitialized", + endStatus: "active", + success: true, + }); + // op::transfer_notification message + expect(claimUsdtRewardResult.transactions).toHaveTransaction({ + from: usdtClaimHelperUsdtRewardJettonWalletAddress, + to: usdtClaimHelper.address, + outMessagesCount: 1, + success: true, + }); + // op::excesses message + expect(claimUsdtRewardResult.transactions).toHaveTransaction({ + from: usdtClaimHelperUsdtRewardJettonWalletAddress, + to: senderAddress, + outMessagesCount: 0, + success: true, + }) + // ClaimReward message + expect(claimUsdtRewardResult.transactions).toHaveTransaction({ + from: usdtClaimHelper.address, + to: jettonVault.address, + outMessagesCount: 1, + success: true, + }) + // TokenTransfer message + expect(claimUsdtRewardResult.transactions).toHaveTransaction({ + from: jettonVault.address, + to: jettonVaultUsdtWalletAddress, + outMessagesCount: 1, + success: true, + }) + // op::internal_transfer message + expect(claimUsdtRewardResult.transactions).toHaveTransaction({ + from: jettonVaultUsdtWalletAddress, + to: senderUsdtWalletAddress, + oldStatus: "uninitialized", + endStatus: "active", + outMessagesCount: 1, + success: true, + }) + // op::transfer_notification message + expect(claimUsdtRewardResult.transactions).toHaveTransaction({ + from: senderUsdtWalletAddress, + to: senderAddress, + outMessagesCount: 0, + success: true, + }) + + const jettonVaultUsdtRewardJettonWalletBalance = await jettonVaultUsdtRewardJettonWallet.getJettonBalance(); + const usdtClaimHelperUsdtRewardJettonWalletBalance = await usdtClaimHelperUsdtRewardJettonWallet.getJettonBalance(); + const senderUsdtRewardJettonWalletBalance = await senderUsdtRewardJettonWallet.getJettonBalance(); + + const senderUsdtWalletBalance = (await senderUsdtWallet.getGetWalletData()).balance; + // const usdtClaimHelperWalletBalance = (await usdtClaimHelperWallet.getGetWalletData()).balance; + const jettonVaultUsdtWalletBalance = (await jettonVaultUsdtWallet.getGetWalletData()).balance; + + expect(usdtClaimHelperUsdtRewardJettonWalletBalance - usdtClaimHelperUsdtRewardJettonWalletBalanceBefore).toEqual(claimAmount); + expect(jettonVaultUsdtRewardJettonWalletBalance - jettonVaultUsdtRewardJettonWalletBalanceBefore).toEqual(toNano("0")); + expect(senderUsdtRewardJettonWalletBalanceBefore - senderUsdtRewardJettonWalletBalance).toEqual(claimAmount); + + expect(jettonVaultUsdtWalletBalanceBefore - jettonVaultUsdtWalletBalance).toEqual(claimAmount); + expect(senderUsdtWalletBalance).toEqual(claimAmount); + + console.table([ + { + "JettonVault-T-USDT": jettonVaultUsdtRewardJettonWalletBalanceBefore, + "ClaimHelper-T-USDT": usdtClaimHelperUsdtRewardJettonWalletBalanceBefore, + "Sender-T-USDT": senderUsdtRewardJettonWalletBalanceBefore, + "JettonVault-USDT": jettonVaultUsdtWalletBalanceBefore, + "ClaimHelper-USDT": 0, + "Sender-USDT": 0, + }, + { + "JettonVault-T-USDT": jettonVaultUsdtRewardJettonWalletBalance, + "ClaimHelper-T-USDT": usdtClaimHelperUsdtRewardJettonWalletBalance, + "Sender-T-USDT": senderUsdtRewardJettonWalletBalance, + "JettonVault-USDT": jettonVaultUsdtWalletBalance, + "ClaimHelper-USDT": 0, + "Sender-USDT": senderUsdtWalletBalance, + } + ]) + // โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + // โ”‚ (index) โ”‚ JettonVault-T-USDT โ”‚ ClaimHelper-T-USDT โ”‚ Sender-T-USDT โ”‚ JettonVault-USDT โ”‚ ClaimHelper-USDT โ”‚ Sender-USDT โ”‚ + // โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + // โ”‚ 0 โ”‚ 0n โ”‚ 0n โ”‚ 1000000000n โ”‚ 1000000000n โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 1 โ”‚ 0n โ”‚ 300000000n โ”‚ 700000000n โ”‚ 700000000n โ”‚ 0 โ”‚ 300000000n โ”‚ + // โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + // printTransactionFees(claimUsdtRewardResult.transactions) + // โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + // โ”‚ (index) โ”‚ op โ”‚ valueIn โ”‚ valueOut โ”‚ totalFees โ”‚ inForwardFee โ”‚ outForwardFee โ”‚ outActions โ”‚ computeFee โ”‚ exitCode โ”‚ actionCode โ”‚ + // โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + // โ”‚ 0 โ”‚ 'N/A' โ”‚ 'N/A' โ”‚ '0.7 TON' โ”‚ '0.002061 TON' โ”‚ 'N/A' โ”‚ '0.000775 TON' โ”‚ 1 โ”‚ '0.000775 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 1 โ”‚ '0x3ee943f1' โ”‚ '0.7 TON' โ”‚ '0.69228 TON' โ”‚ '0.004911 TON' โ”‚ '0.000517 TON' โ”‚ '0.004214 TON' โ”‚ 1 โ”‚ '0.003506 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 2 โ”‚ '0xce30d1dc' โ”‚ '0.69228 TON' โ”‚ '0.662666 TON' โ”‚ '0.004357 TON' โ”‚ '0.00281 TON' โ”‚ '0.001053 TON' โ”‚ 2 โ”‚ '0.004006 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 3 โ”‚ '0x4fb8dedc' โ”‚ '0.35 TON' โ”‚ '0.299348 TON' โ”‚ '0.006515 TON' โ”‚ '0.000436 TON' โ”‚ '0.000681 TON' โ”‚ 1 โ”‚ '0.006288 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 4 โ”‚ '0x7d7aec1d' โ”‚ '0.312666 TON' โ”‚ '0 TON' โ”‚ '0.000124 TON' โ”‚ '0.000267 TON' โ”‚ 'N/A' โ”‚ 0 โ”‚ '0.000124 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 5 โ”‚ '0x2daf1323' โ”‚ '0.299348 TON' โ”‚ '0.05 TON' โ”‚ '0.007443 TON' โ”‚ '0.000454 TON' โ”‚ '0.000709 TON' โ”‚ 1 โ”‚ '0.007206 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 6 โ”‚ '0xf8a7ea5' โ”‚ '0.05 TON' โ”‚ '0.033059 TON' โ”‚ '0.011073 TON' โ”‚ '0.000473 TON' โ”‚ '0.008804 TON' โ”‚ 1 โ”‚ '0.008138 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 7 โ”‚ '0x178d4519' โ”‚ '0.033059 TON' โ”‚ '0.003777 TON' โ”‚ '0.007272 TON' โ”‚ '0.00587 TON' โ”‚ '0.000479 TON' โ”‚ 1 โ”‚ '0.007112 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 8 โ”‚ '0xd53276db' โ”‚ '0.003777 TON' โ”‚ '0 TON' โ”‚ '0.000124 TON' โ”‚ '0.000319 TON' โ”‚ 'N/A' โ”‚ 0 โ”‚ '0.000124 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + const totalTransactionFee = sumTransactionsFee(claimUsdtRewardResult.transactions); + expect(totalTransactionFee).toBeLessThanOrEqual(0.045076311); // real: 0.044076311 + }); + + it('should claim TAY successfully', async () => { + await mintMockTay(tay, jettonVault.address, toNano("1")); + const sender = deployer.getSender(); + const senderAddress = sender.address; + + const mintAmount = toNano("1") + const mintTayRewardJettonResult = await tayRewardJettonMaster.sendMint( + sender, + senderAddress, + mintAmount, + toNano('0.05'), + toNano('0.06'), + ); + expect(mintTayRewardJettonResult.transactions).toHaveTransaction({ + from: await tayRewardJettonMaster.getWalletAddress(senderAddress), + to: senderAddress, + oldStatus: "active", + endStatus: "active", + success: true, + }); + + const jettonVaultTayRewardJettonWalletAddress = await tayRewardJettonMaster.getWalletAddress(jettonVault.address); + const senderTayRewardJettonWalletAddress = await tayRewardJettonMaster.getWalletAddress(senderAddress); + const tayClaimHelperTayRewardJettonWalletAddress = await tayRewardJettonMaster.getWalletAddress(tayClaimHelper.address); + const jettonVaultTayWalletAddress = await tay.getGetWalletAddress(jettonVault.address); + const senderTayWalletAddress = await tay.getGetWalletAddress(senderAddress); + const tayClaimHelperWalletAddress = await tay.getGetWalletAddress(tayClaimHelper.address); + const timeVestingMasterTayWalletAddress = await tay.getGetWalletAddress(timeVestingMaster.address); + + const jettonVaultTayRewardJettonWallet = constructJettonWalletConfig(jettonVault.address, tayRewardJettonMaster.address) + const tayClaimHelperTayRewardJettonWallet = constructJettonWalletConfig(tayClaimHelper.address, tayRewardJettonMaster.address) + const senderTayRewardJettonWallet = constructJettonWalletConfig(senderAddress, tayRewardJettonMaster.address) + const timeVestingRewardJettonWallet = constructJettonWalletConfig(timeVestingMaster.address, tayRewardJettonMaster.address) + const timeVestingMasterTayWallet = blockchain.openContract(JettonDefaultWallet.fromAddress(timeVestingMasterTayWalletAddress)); + + const jettonVaultTayWallet = blockchain.openContract(JettonDefaultWallet.fromAddress(jettonVaultTayWalletAddress)); + // const senderTayWallet = blockchain.openContract(JettonDefaultWallet.fromAddress(senderTayWalletAddress)); + // const tayClaimHelperWallet = blockchain.openContract(JettonDefaultWallet.fromAddress(tayClaimHelperWalletAddress)); + + const senderTayRewardJettonWalletBalanceBefore = await senderTayRewardJettonWallet.getJettonBalance(); + const tayClaimHelperTayRewardJettonWalletBalanceBefore = await tayClaimHelperTayRewardJettonWallet.getJettonBalance(); + const jettonVaultTayRewardJettonWalletBalanceBefore = await jettonVaultTayRewardJettonWallet.getJettonBalance(); + + // const timeVestingMasterTayWalletBalanceBefore = (await timeVestingMasterTayWallet.getGetWalletData()).balance; + const jettonVaultTayWalletBalanceBefore = (await jettonVaultTayWallet.getGetWalletData()).balance; + + const claimAmount = toNano("0.3"); + // MessageFlow + // 1. (external) undefined => senderAddress + // 2. (op::transfer) senderAddress => senderTayRewardJettonWalletAddress + // 3. (op::internal_transfer) senderTayRewardJettonWalletAddress => tayClaimHelperTayRewardJettonWalletAddress + // 4. (op::transfer_notification) tayClaimHelperTayRewardJettonWalletAddress => tayClaimHelper + // 5. (op::excesses) tayClaimHelperTayRewardJettonWalletAddress => senderAddress + // 6. (ClaimReward) tayClaimHelper => jettonVault + // 7. (TokenTransfer) jettonVault => jettonVaultTayWalletAddress + // 8. (InternalTransfer) jettonVaultTayWalletAddress => timeVestingMasterTayWallet + // 9. (TokenNotification) timeVestingMasterTayWallet => timeVestingMaster + // 10.(Excesses) timeVestingMasterTayWallet => senderAddress + // 11.(AddLock) timeVestingMaster => senderTimeVestingMasterWalletAddress + // 12.(SelfReply) senderTimeVestingMasterWalletAddress => senderAddress + const claimTayRewardResult = await tayRewardJettonWallet.sendTransfer( + sender, + toNano('0.2'), + toNano('0.3'), + tayClaimHelper.address, + claimAmount, + beginCell().storeUint(0x7994ff68, 32).endCell(), // opcode: ClaimReward + ); + + const senderTimeVestingMasterWalletAddress = await timeVestingMaster.getUserTimeVestingAddress(deployer.address); + // console.log({ + // senderAddress: deployer.getSender().address.toString(), + // tay: tay.address.toString(), + // tayRewardJettonMaster: tayRewardJettonMaster.address.toString(), + // tayClaimHelper: tayClaimHelper.address.toString(), + // jettonVault: jettonVault.address.toString(), + // jettonVaultTayRewardJettonWalletAddress: jettonVaultTayRewardJettonWalletAddress.toString(), + // senderTayRewardJettonWalletAddress: senderTayRewardJettonWalletAddress.toString(), + // tayClaimHelperTayRewardJettonWalletAddress: tayClaimHelperTayRewardJettonWallet.address.toString(), + // jettonVaultTayWalletAddress: jettonVaultTayWalletAddress.toString(), + // senderTayWalletAddress: senderTayWalletAddress.toString(), + // tayClaimHelperWalletAddress: tayClaimHelperWalletAddress.toString(), + // timeVestingMaster: timeVestingMaster.address.toString(), + // senderTimeVestingMasterWalletAddress: senderTimeVestingMasterWalletAddress.toString(), + // timeVestingMasterTayWalletAddress: timeVestingMasterTayWalletAddress.toString(), + // }) + + // 1. external message + expect(claimTayRewardResult.transactions).toHaveTransaction({ + from: undefined, + to: senderAddress, + outMessagesCount: 1, + success: true, + }); + // 2. op::transfer message + expect(claimTayRewardResult.transactions).toHaveTransaction({ + from: senderAddress, + to: senderTayRewardJettonWalletAddress, + outMessagesCount: 1, + success: true, + }); + // 3. op::internal_transfer message + expect(claimTayRewardResult.transactions).toHaveTransaction({ + from: senderTayRewardJettonWalletAddress, + to: tayClaimHelperTayRewardJettonWalletAddress, + outMessagesCount: 2, + oldStatus: "uninitialized", + endStatus: "active", + success: true, + }); + // 4. op::transfer_notification message + expect(claimTayRewardResult.transactions).toHaveTransaction({ + from: tayClaimHelperTayRewardJettonWalletAddress, + to: tayClaimHelper.address, + outMessagesCount: 1, + success: true, + }); + // 5. op::excesses message + expect(claimTayRewardResult.transactions).toHaveTransaction({ + from: tayClaimHelperTayRewardJettonWalletAddress, + to: senderAddress, + outMessagesCount: 0, + success: true, + }) + // 6. ClaimReward message + expect(claimTayRewardResult.transactions).toHaveTransaction({ + from: tayClaimHelper.address, + to: jettonVault.address, + outMessagesCount: 1, + success: true, + }) + // 7. TokenTransfer message + expect(claimTayRewardResult.transactions).toHaveTransaction({ + from: jettonVault.address, + to: jettonVaultTayWalletAddress, + outMessagesCount: 1, + success: true, + }) + // 8. InternalTransfer message + expect(claimTayRewardResult.transactions).toHaveTransaction({ + from: jettonVaultTayWalletAddress, + to: timeVestingMasterTayWalletAddress, + oldStatus: "uninitialized", + endStatus: "active", + outMessagesCount: 2, + success: true, + }) + // 9. TokenNotification message + expect(claimTayRewardResult.transactions).toHaveTransaction({ + from: timeVestingMasterTayWalletAddress, + to: timeVestingMaster.address, + success: true, + }); + // 10. Excesses message + expect(claimTayRewardResult.transactions).toHaveTransaction({ + from: timeVestingMasterTayWalletAddress, + to: senderAddress, + success: true, + }); + // 11. AddLock message + expect(claimTayRewardResult.transactions).toHaveTransaction({ + from: timeVestingMaster.address, + to: senderTimeVestingMasterWalletAddress, + success: true, + }); + // 12. SelfReply message + expect(claimTayRewardResult.transactions).toHaveTransaction({ + from: senderTimeVestingMasterWalletAddress, + to: senderAddress, + success: true, + }); + + const deployerTimeVesting = blockchain.openContract( + TimeVesting.fromAddress(await timeVestingMaster.getUserTimeVestingAddress(senderAddress)), + ); + const lockedTAY = await deployerTimeVesting.getTimeVestingData(); + console.log(lockedTAY); + + // const jettonVaultTayRewardJettonWalletBalance = await jettonVaultTayRewardJettonWallet.getJettonBalance(); + const tayClaimHelperTayRewardJettonWalletBalance = await tayClaimHelperTayRewardJettonWallet.getJettonBalance(); + const senderTayRewardJettonWalletBalance = await senderTayRewardJettonWallet.getJettonBalance(); + + // const senderTayWalletBalance = (await senderTayWallet.getGetWalletData()).balance; + // const tayClaimHelperWalletBalance = (await tayClaimHelperWallet.getGetWalletData()).balance; + const jettonVaultTayWalletBalance = (await jettonVaultTayWallet.getGetWalletData()).balance; + // const timeVestingMasterTayWalletBalance = (await tay.getGetWalletAddress(timeVestingMasterTayWallet)) + const timeVestingMasterTayWalletBalance = (await timeVestingMasterTayWallet.getGetWalletData()).balance + // (await tay.getGetWalletData(timeVestingMasterTayWallet)).balance; + + expect(tayClaimHelperTayRewardJettonWalletBalance - tayClaimHelperTayRewardJettonWalletBalanceBefore).toEqual(claimAmount); + expect(jettonVaultTayWalletBalanceBefore - jettonVaultTayWalletBalance).toEqual(claimAmount); + expect(senderTayRewardJettonWalletBalanceBefore - senderTayRewardJettonWalletBalance).toEqual(claimAmount); + expect(timeVestingMasterTayWalletBalance).toEqual(claimAmount); + + console.table([ + { + "JettonVault-T-TAY": jettonVaultTayRewardJettonWalletBalanceBefore, + "ClaimHelper-T-TAY": tayClaimHelperTayRewardJettonWalletBalanceBefore, + "Sender-T-TAY": senderTayRewardJettonWalletBalanceBefore, + "JettonVault-TAY": jettonVaultTayWalletBalanceBefore, + "ClaimHelper-TAY": 0, + "Sender-TAY": 0, + "TimeVesting-TAY": 0, + }, + { + "JettonVault-T-TAY": 0, + "ClaimHelper-T-TAY": tayClaimHelperTayRewardJettonWalletBalance, + "Sender-T-TAY": senderTayRewardJettonWalletBalance, + "JettonVault-TAY": jettonVaultTayWalletBalance, + "ClaimHelper-TAY": 0, + "Sender-TAY": 0, + "TimeVesting-TAY": timeVestingMasterTayWalletBalance, + } + ]) + // โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + // โ”‚ (index) โ”‚ JettonVault-T-TAY โ”‚ ClaimHelper-T-TAY โ”‚ Sender-T-TAY โ”‚ JettonVault-TAY โ”‚ ClaimHelper-TAY โ”‚ Sender-TAY โ”‚ TimeVesting-TAY โ”‚ + // โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + // โ”‚ 0 โ”‚ 0n โ”‚ 0n โ”‚ 1000000000n โ”‚ 1000000000n โ”‚ 0 โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 1 โ”‚ 0 โ”‚ 300000000n โ”‚ 700000000n โ”‚ 700000000n โ”‚ 0 โ”‚ 0 โ”‚ 300000000n โ”‚ + // โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + // printTransactionFees(claimTayRewardResult.transactions) + // โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + // โ”‚ (index) โ”‚ op โ”‚ valueIn โ”‚ valueOut โ”‚ totalFees โ”‚ inForwardFee โ”‚ outForwardFee โ”‚ outActions โ”‚ computeFee โ”‚ exitCode โ”‚ actionCode โ”‚ + // โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + // โ”‚ 0 โ”‚ 'N/A' โ”‚ 'N/A' โ”‚ '0.5 TON' โ”‚ '0.002061 TON' โ”‚ 'N/A' โ”‚ '0.000775 TON' โ”‚ 1 โ”‚ '0.000775 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 1 โ”‚ '0x3ee943f1' โ”‚ '0.5 TON' โ”‚ '0.49228 TON' โ”‚ '0.004911 TON' โ”‚ '0.000517 TON' โ”‚ '0.004214 TON' โ”‚ 1 โ”‚ '0.003506 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 2 โ”‚ '0xce30d1dc' โ”‚ '0.49228 TON' โ”‚ '0.462666 TON' โ”‚ '0.004357 TON' โ”‚ '0.00281 TON' โ”‚ '0.001053 TON' โ”‚ 2 โ”‚ '0.004006 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 3 โ”‚ '0x4fb8dedc' โ”‚ '0.3 TON' โ”‚ '0.249348 TON' โ”‚ '0.006334 TON' โ”‚ '0.000436 TON' โ”‚ '0.000681 TON' โ”‚ 1 โ”‚ '0.006107 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 4 โ”‚ '0x7d7aec1d' โ”‚ '0.162666 TON' โ”‚ '0 TON' โ”‚ '0.000124 TON' โ”‚ '0.000267 TON' โ”‚ 'N/A' โ”‚ 0 โ”‚ '0.000124 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 5 โ”‚ '0x2daf1323' โ”‚ '0.249348 TON' โ”‚ '0.240457 TON' โ”‚ '0.00833 TON' โ”‚ '0.000454 TON' โ”‚ '0.000841 TON' โ”‚ 1 โ”‚ '0.00805 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 6 โ”‚ '0xf8a7ea5' โ”‚ '0.240457 TON' โ”‚ '0.223441 TON' โ”‚ '0.011098 TON' โ”‚ '0.000561 TON' โ”‚ '0.008879 TON' โ”‚ 1 โ”‚ '0.008138 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 7 โ”‚ '0x178d4519' โ”‚ '0.223441 TON' โ”‚ '0.194084 TON' โ”‚ '0.009068 TON' โ”‚ '0.005919 TON' โ”‚ '0.001198 TON' โ”‚ 2 โ”‚ '0.008668 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 8 โ”‚ '0x7362d09c' โ”‚ '0.04 TON' โ”‚ '0.02627 TON' โ”‚ '0.009667 TON' โ”‚ '0.00048 TON' โ”‚ '0.006097 TON' โ”‚ 1 โ”‚ '0.007634 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 9 โ”‚ '0xd53276db' โ”‚ '0.154084 TON' โ”‚ '0 TON' โ”‚ '0.000124 TON' โ”‚ '0.000319 TON' โ”‚ 'N/A' โ”‚ 0 โ”‚ '0.000124 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 10 โ”‚ '0xf0e97869' โ”‚ '0.02627 TON' โ”‚ '0.001106 TON' โ”‚ '0.004819 TON' โ”‚ '0.004065 TON' โ”‚ '0.000517 TON' โ”‚ 2 โ”‚ '0.004647 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ”‚ 11 โ”‚ '0x0' โ”‚ '0.001106 TON' โ”‚ '0 TON' โ”‚ '0.000124 TON' โ”‚ '0.000345 TON' โ”‚ 'N/A' โ”‚ 0 โ”‚ '0.000124 TON' โ”‚ 0 โ”‚ 0 โ”‚ + // โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + const totalTransactionFee = sumTransactionsFee(claimTayRewardResult.transactions); + expect(totalTransactionFee).toBeLessThanOrEqual(0.062); // real: 0.06101040500000001 + }); + }); +}); diff --git a/tests/Pool.Borrow.spec.ts b/tests/Pool.Borrow.spec.ts index 8d242a7..7a8c8d8 100644 --- a/tests/Pool.Borrow.spec.ts +++ b/tests/Pool.Borrow.spec.ts @@ -1,4 +1,4 @@ -import { Blockchain, printTransactionFees, SandboxContract, TreasuryContract } from '@ton/sandbox'; +import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'; import { Address, beginCell, Dictionary, fromNano, toNano } from '@ton/core'; import { Pool, storeBorrowToken, storeBorrowTon } from '../wrappers/Pool'; import '@ton/test-utils'; @@ -143,7 +143,7 @@ describe('Pool', () => { }); const totalTransactionFee = sumTransactionsFee(result.transactions); - expect(totalTransactionFee).toBeLessThanOrEqual(0.11); // real: 0.10973918099999999 + expect(totalTransactionFee).toBeLessThanOrEqual(0.1105); // real: 0.11042544299999998 const userAccountContract = blockchain.openContract(userAccountAddress); const accountData = await userAccountContract.getAccount(); diff --git a/tests/Pool.spec.ts b/tests/Pool.spec.ts index 2e0e8b1..081f56a 100644 --- a/tests/Pool.spec.ts +++ b/tests/Pool.spec.ts @@ -111,9 +111,9 @@ describe('Pool', () => { success: true, }); - // TODO: the aToken calculated from Atoken.fromInit and pool.getCalculateATokenAddress is different!!! why? - // const aToken = blockchain.openContract(await AToken.fromInit(pool.address, contents.aTokenContent, reserveAddress)) + const aTokenCalculated = blockchain.openContract(await AToken.fromInit(pool.address, contents.aTokenContent, reserveAddress)) const aToken = blockchain.openContract(AToken.fromAddress(reserveConfiguration.aTokenAddress)); + expect(aTokenCalculated.address.toString()).toEqual(aToken.address.toString()); expect((await aToken.getOwner()).toString()).toEqual(pool.address.toString()); expect((await aToken.getGetPoolData()).pool.toString()).toEqual(pool.address.toString()); expect((await aToken.getGetPoolData()).asset.toString()).toEqual(reserveAddress.toString()); diff --git a/tests/RewardJetton.spec.ts b/tests/RewardJetton.spec.ts new file mode 100644 index 0000000..254bfe2 --- /dev/null +++ b/tests/RewardJetton.spec.ts @@ -0,0 +1,283 @@ +import '@ton/test-utils'; +import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'; +import { Cell, toNano } from '@ton/core'; +import { buildOnchainMetadata } from '../scripts/utils'; +import { JettonWallet } from '../wrappers/JettonWallet'; +import { RewardJettonMaster } from '../wrappers/RewardJettonMaster'; +import { compile } from '@ton/blueprint'; + +describe('RewardJetton', () => { + let blockchain: Blockchain; + let deployer: SandboxContract; + let sampleRewardJettonMaster: SandboxContract; + let sampleJettonWallet: SandboxContract; + + beforeEach(async () => { + blockchain = await Blockchain.create(); + deployer = await blockchain.treasury('deployer'); + const jettonParams = { + name: 'SampleJetton', + description: 'Sample Jetton for testing purposes', + decimals: '9', + image: 'https://ipfs.io/ipfs/bafybeicn7i3soqdgr7dwnrwytgq4zxy7a5jpkizrvhm5mv6bgjd32wm3q4/welcome-to-IPFS.jpg', + symbol: 'SAM', + }; + + // It's the largest value I can use for max_supply in the tests + let content = buildOnchainMetadata(jettonParams); + + const walletCode = await compile('JettonWallet'); + const masterCode = await compile('RewardJettonMaster'); + sampleRewardJettonMaster = blockchain.openContract(RewardJettonMaster.createFromConfig( + { + admin: deployer.address, + content, + walletCode, + }, + masterCode + )); + sampleJettonWallet = blockchain.openContract(JettonWallet.createFromConfig( + { + owner: deployer.address, + minter: sampleRewardJettonMaster.address, + walletCode, + }, + walletCode + )); + + const deployResult = await sampleRewardJettonMaster.sendDeploy( + deployer.getSender(), + toNano('0.05'), + ); + + expect(deployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: sampleRewardJettonMaster.address, + deploy: true, + success: true, + }); + }); + + describe("should run", () => { + it("should deploy jetton master and jetton wallet successfully", async () => { + const jettonData = await sampleRewardJettonMaster.getJettonData(); + expect(jettonData.totalSupply).toEqual(0n); + expect(jettonData.mintable).toEqual(true); + expect(jettonData.adminAddress.toString()).toEqual(deployer.getSender().address.toString()); + expect(jettonData.content).toEqual(jettonData.content); + + const calculatedWalletAddress = await sampleRewardJettonMaster.getWalletAddress(deployer.address); + expect(calculatedWalletAddress.toString()).toEqual(sampleJettonWallet.address.toString()); + }) + }) + + // NOTE: discovery is disabled in RewardJettonMaster + // describe('discover', () => { + // it('should discover wallet address successfully', async () => { + // const receiverAddress = (await blockchain.createWallets(1))[0].address; + + // // should be failed due to insufficient funds + // const result = await sampleRewardJettonMaster.sendDiscovery( + // deployer.getSender(), + // receiverAddress, + // true, + // toNano('0.01'), + // ); + // expect(result.transactions).toHaveTransaction({ + // to: sampleRewardJettonMaster.address, + // inMessageBounced: false, + // inMessageBounceable: true, + // success: false, + // exitCode: Errors.insufficient_discovery_fee, + // }); + // expect(result.transactions).toHaveTransaction({ + // from: sampleRewardJettonMaster.address, + // inMessageBounced: true, + // success: true, + // }); + + // // should be successful now due to sufficient funds + // const result2 = await sampleRewardJettonMaster.sendDiscovery( + // deployer.getSender(), + // receiverAddress, + // true, + // toNano('0.1'), + // ); + + // // provide_wallet_address message + // expect(result2.transactions).toHaveTransaction({ + // to: sampleRewardJettonMaster.address, + // success: true, + // }) + // // take_wallet_address message + // expect(result2.transactions).toHaveTransaction({ + // from: sampleRewardJettonMaster.address, + // success: true, + // }); + // }); + // }); + + describe('mint', () => { + it('should mint token successfully (mint -> internal_transfer -> transfer_notification)', async () => { + const receiverAddress = (await blockchain.createWallets(1))[0].address; + const result = await sampleRewardJettonMaster.sendMint( + deployer.getSender(), + receiverAddress, + toNano('0.05'), + toNano('0.05'), + toNano('0.06'), + ); + const receiverWalletAddress = await sampleRewardJettonMaster.getWalletAddress(receiverAddress); + const balance = await blockchain.openContract( + JettonWallet.createFromAddress(receiverWalletAddress), + ).getJettonBalance(); + expect(balance).toEqual(toNano('0.05')); + + // mint message + expect(result.transactions).toHaveTransaction({ + from: undefined, + oldStatus: "active", + endStatus: "active", + success: true, + }); + + // internal transfer message + expect(result.transactions).toHaveTransaction({ + from: sampleRewardJettonMaster.address, + to: receiverWalletAddress, + oldStatus: "uninitialized", + endStatus: "active", + inMessageBounceable: true, + success: true, + }); + + // transfer_notification Message + expect(result.transactions).toHaveTransaction({ + from: receiverWalletAddress, + to: receiverAddress, + oldStatus: "active", + endStatus: "active", + inMessageBounceable: false, + success: true, + }); + }); + + // TODO: Implement mint_batch in FunC contract + // it('should mint batch of tokens successfully', async () => { + // blockchain.verbosity = { + // print: true, + // blockchainLogs: true, + // vmLogs: 'vm_logs_full', + // debugLogs: true, + // } + // const wallets = await blockchain.createWallets(1); + // const records = Dictionary.empty(Dictionary.Keys.Address(), Dictionary.Values.BigUint(256)); + // wallets.forEach(wallet => records.set(wallet.address, toNano('0.66'))); + + // const result = await sampleRewardJettonMaster.sendMintBatch( + // deployer.getSender(), + // records, + // ); + + // const supply = await sampleRewardJettonMaster.getJettonData(); + // expect(supply.totalSupply).toEqual(toNano('0.1')); + + // expect(result.transactions).toHaveTransaction({ + // from: sampleRewardJettonMaster.address, + // success: false, + // }); + // }) + }) + + + describe('transfer', () => { + it("should transfer token successfully", async () => { + const sender = deployer.getSender(); + const senderAddress = sender.address; + const amount = toNano("1") + const result = await sampleRewardJettonMaster.sendMint( + sender, + senderAddress, + amount, + toNano('0.05'), + toNano('0.06'), + ); + + const senderWalletAddress = await sampleRewardJettonMaster.getWalletAddress(senderAddress); + const balance = await blockchain.openContract(JettonWallet.createFromAddress(senderWalletAddress),).getJettonBalance(); + expect(balance).toEqual(toNano('1')); + + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: sampleRewardJettonMaster.address, + success: true, + }); + + const receiverAddress = (await blockchain.createWallets(1))[0].address; + const receiverWalletAddress = await sampleRewardJettonMaster.getWalletAddress(receiverAddress); + const receiverWallet = blockchain.openContract(JettonWallet.createFromAddress(receiverWalletAddress)) + const transferAmount = toNano("0.3") + const transferResult = await sampleJettonWallet.sendTransfer( + sender, + toNano('0.05'), + toNano('0.05'), + receiverAddress, + transferAmount, + Cell.EMPTY, + ); + + // console.log({ + // senderAddress: senderAddress.toString(), + // receiverAddress: receiverAddress.toString(), + // senderWalletAddress: senderWalletAddress.toString(), + // receiverWalletAddress: receiverWalletAddress.toString(), + // sampleRewardJettonMaster: sampleRewardJettonMaster.address.toString(), + // }) + + // external message + expect(transferResult.transactions).toHaveTransaction({ + from: undefined, + to: senderAddress, + oldStatus: "active", + endStatus: "active", + outMessagesCount: 1, + success: true, + }); + + // op::transfer message + expect(transferResult.transactions).toHaveTransaction({ + from: senderAddress, + to: senderWalletAddress, + outMessagesCount: 1, + success: true, + }); + + // op::transfer message + expect(transferResult.transactions).toHaveTransaction({ + from: senderWalletAddress, + to: receiverWalletAddress, + outMessagesCount: 2, // op::transfer_notification & op::excesses + oldStatus: "uninitialized", + endStatus: "active", + success: true, + }); + + // op::transfer_notification message + expect(transferResult.transactions).toHaveTransaction({ + from: receiverWalletAddress, + to: receiverAddress, + success: true, + }); + + // op::excesses message + expect(transferResult.transactions).toHaveTransaction({ + from: receiverWalletAddress, + to: senderAddress, + success: true, + }); + + const newWalletData = await receiverWallet.getJettonBalance(); + expect(newWalletData).toEqual(transferAmount); + }); + }) +}); diff --git a/tests/SampleJetton.spec.ts b/tests/SampleJetton.spec.ts index 95a5b72..e940839 100644 --- a/tests/SampleJetton.spec.ts +++ b/tests/SampleJetton.spec.ts @@ -1,5 +1,5 @@ import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'; -import { toNano } from '@ton/core'; +import { Cell, toNano } from '@ton/core'; import '@ton/test-utils'; import { SampleJetton } from '../build/SampleJetton/tact_SampleJetton'; import { buildOnchainMetadata } from '../scripts/utils'; @@ -92,4 +92,67 @@ describe('SampleJetton', () => { expect(walletData.balance).toEqual(1000000000n); }); }); + + // TODO: Fix Exit Code `5` (Integer out of expected range.) + // describe('transfer', () => { + // it("should transfer token successfully", async () => { + // const sender = (await blockchain.createWallets(1))[0]; + // const senderAddress = sender.address; + // const amount = toNano("1") + // const result = await sampleJetton.send( + // deployer.getSender(), + // { + // value: toNano('0.05'), + // }, + // { + // $$type: 'Mint', + // queryId: 0n, + // amount: amount, + // receiver: senderAddress, + // }, + // ); + // expect(result.transactions).toHaveTransaction({ + // from: deployer.address, + // to: sampleJetton.address, + // success: true, + // }); + + // const senderSampleJettonWalletAddress = await sampleJetton.getGetWalletAddress(senderAddress); + // const senderSampleJettonWallet = blockchain.openContract( + // JettonDefaultWallet.fromAddress(senderSampleJettonWalletAddress), + // ); + // const walletData = await senderSampleJettonWallet.getGetWalletData(); + // expect(walletData.balance).toEqual(amount); + + // const receiver = (await blockchain.createWallets(2))[1]; + // const receiverAddress = receiver.address; + // const transferAmount = toNano("0.3") + // const transferResult = await senderSampleJettonWallet.send( + // sender.getSender(), + // { + // value: toNano('0.05'), + // }, + // { + // $$type: 'TokenTransfer', + // queryId: 0n, + // amount: transferAmount, + // destination: receiverAddress, + // response_destination: receiverAddress, + // custom_payload: null, + // forward_ton_amount: toNano('0.15'), + // forward_payload: Cell.EMPTY, + // }, + // ); + + // expect(transferResult.transactions).toHaveTransaction({ + // from: senderSampleJettonWalletAddress, + // to: senderAddress, + // success: true, + // }); + + // const newWalletData = await senderSampleJettonWallet.getGetWalletData(); + // expect(newWalletData.balance).toEqual(amount - transferAmount - BigInt(1)); + + // }); + // }) }); diff --git a/wrappers/ClaimHelper.ts b/wrappers/ClaimHelper.ts new file mode 100644 index 0000000..bb82c9a --- /dev/null +++ b/wrappers/ClaimHelper.ts @@ -0,0 +1 @@ +export * from '../build/ClaimHelper/tact_ClaimHelper'; diff --git a/wrappers/JettonConstants.ts b/wrappers/JettonConstants.ts new file mode 100644 index 0000000..86751da --- /dev/null +++ b/wrappers/JettonConstants.ts @@ -0,0 +1,30 @@ +export abstract class Op { + static transfer = 0x3ee943f1; + static transfer_notification = 0x0626b4be; + static internal_transfer = 0xce30d1dc; + static excesses = 0x7d7aec1d; + static burn = 0xbae7fba1; + static burn_notification = 0x894844ca; + + static mint = 0xecad15c4; + static mint_batch = 0x4fb31204; + static change_admin = 3; + static change_content = 4; + + static provide_wallet_address = 0xe450e86a; + static take_wallet_address = 0x3331a011; +} + +export abstract class Errors { + static invalid_op = 709; + static not_admin = 73; + static unouthorized_burn = 74; + static insufficient_discovery_fee = 75; + static wrong_op = 0xffff; + static not_owner = 705; + static not_enough_ton = 709; + static not_enough_gas = 707; + static not_valid_wallet = 707; + static wrong_workchain = 333; + static balance_error = 706; +} \ No newline at end of file diff --git a/wrappers/JettonMaster.ts b/wrappers/JettonMaster.ts new file mode 100644 index 0000000..7125071 --- /dev/null +++ b/wrappers/JettonMaster.ts @@ -0,0 +1,244 @@ +import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode, toNano, internal as internal_relaxed, storeMessageRelaxed, Dictionary } from '@ton/core'; + +import { Op } from './JettonConstants'; + +export type JettonMasterContent = { + type: 0 | 1, + uri: string +}; + +export type JettonMasterConfig = { admin: Address; content: Cell; walletCode: Cell }; + +export function jettonMasterConfigToCell(config: JettonMasterConfig): Cell { + return beginCell() + .storeCoins(0) + .storeAddress(config.admin) + .storeRef(config.content) + .storeRef(config.walletCode) + .endCell(); +} + +export function jettonContentToCell(content: JettonMasterContent) { + return beginCell() + .storeUint(content.type, 8) + .storeStringTail(content.uri) //Snake logic under the hood + .endCell(); +} + +export class JettonMaster implements Contract { + constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) { } + + static createFromAddress(address: Address) { + return new JettonMaster(address); + } + + static createFromConfig(config: JettonMasterConfig, code: Cell, workchain = 0) { + const data = jettonMasterConfigToCell(config); + const init = { code, data }; + return new JettonMaster(contractAddress(workchain, init), init); + } + + async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { + await provider.internal(via, { + value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell().endCell(), + }); + } + + protected static jettonInternalTransfer(jetton_amount: bigint, + forward_ton_amount: bigint, + response_addr?: Address, + query_id: number | bigint = 0) { + return beginCell() + .storeUint(Op.internal_transfer, 32) + .storeUint(query_id, 64) + .storeCoins(jetton_amount) + .storeAddress(null) + .storeAddress(response_addr) + .storeCoins(forward_ton_amount) + .storeBit(false) + .endCell(); + + } + + // if (op == op::mint) { + // throw_unless(73, equal_slices(sender_address, admin_address)); ;; only admin can mint - Wrapper Contract + + // slice to_address = in_msg_body~load_msg_addr(); + // int amount = in_msg_body~load_coins(); + // cell master_msg = in_msg_body~load_ref(); ;; load a reference message + + // slice master_msg_cs = master_msg.begin_parse(); + // master_msg_cs~skip_bits(32 + 64); ;; op + query_id + // int jetton_amount = master_msg_cs~load_coins(); + + // mint_tokens(to_address, jetton_wallet_code, amount, master_msg); + // save_data(total_supply + jetton_amount, admin_address, content, jetton_wallet_code); + // return (); + // } + static mintMessage(from: Address, to: Address, jetton_amount: bigint, forward_ton_amount: bigint, total_ton_amount: bigint, query_id: number | bigint = 0) { + const mintMsg = beginCell().storeUint(Op.internal_transfer, 32) + .storeUint(0, 64) + .storeCoins(jetton_amount) + .storeAddress(null) + .storeAddress(from) // Response addr + .storeCoins(forward_ton_amount) + .storeMaybeRef(null) + .endCell(); + + return beginCell().storeUint(Op.mint, 32).storeUint(query_id, 64) // op, queryId + .storeAddress(to) + .storeCoins(total_ton_amount) + .storeCoins(jetton_amount) + .storeRef(mintMsg) + .endCell(); + } + async sendMint(provider: ContractProvider, via: Sender, to: Address, jetton_amount: bigint, forward_ton_amount: bigint, total_ton_amount: bigint) { + if (total_ton_amount <= forward_ton_amount) { + throw new Error("Total ton amount should be > forward amount"); + } + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: JettonMaster.mintMessage(this.address, to, jetton_amount, forward_ton_amount, total_ton_amount), + value: total_ton_amount + toNano('0.015'), + }); + } + + // if (op == op::batch_mint) { + // throw_unless(73, equal_slices(sender_address, admin_address)); ;; ๅชๆœ‰็ฎก็†ๅ‘˜ๅฏไปฅๆ‰น้‡ mint + + // cell batch_data = in_msg_body~load_ref(); + // int mint_count = in_msg_body~load_uint(16); + + // int i = 0; + // while (i < mint_count) { + // slice batch_slice = batch_data.begin_parse(); + + // while (~ batch_slice.slice_empty?()) { + // slice to_address = batch_slice~load_msg_addr(); + // int amount = batch_slice~load_coins(); + // cell master_msg = batch_slice~load_ref(); + + // slice master_msg_cs = master_msg.begin_parse(); + // master_msg_cs~skip_bits(32 + 64); ;; op + query_id + // int jetton_amount = master_msg_cs~load_coins(); + + // mint_tokens(to_address, jetton_wallet_code, amount, master_msg); + // total_supply += jetton_amount; + + // i += 1; + // if (i >= mint_count) { + // break; + // } + // } + + // if (batch_slice.slice_refs_empty?()) { + // break; + // } + // batch_data = batch_slice~load_ref(); + // } + + // save_data(total_supply, admin_address, content, jetton_wallet_code); + // return (); + // } + + static mintBatchMessage( + batch_data: Dictionary, + query_id: number | bigint = 0 + ) { + return beginCell().storeUint(Op.mint_batch, 32).storeUint(query_id, 64) // op, queryId + .storeDict(batch_data, Dictionary.Keys.Address(), Dictionary.Values.BigInt(257)) + .storeUint(batch_data.size, 16) + .endCell(); + } + + async sendMintBatch( + provider: ContractProvider, + via: Sender, + batch_data: Dictionary + ) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: JettonMaster.mintBatchMessage(batch_data), + value: toNano('0.015') * BigInt(batch_data.size), + }); + } + + + /* provide_wallet_address#e450e86a query_id:uint64 owner_address:MsgAddress include_address:Bool = InternalMsgBody; + */ + static discoveryMessage(owner: Address, include_address: boolean) { + return beginCell().storeUint(Op.provide_wallet_address, 32).storeUint(0, 64) // op, queryId + .storeAddress(owner).storeBit(include_address) + .endCell(); + } + + async sendDiscovery(provider: ContractProvider, via: Sender, owner: Address, include_address: boolean, value: bigint = toNano('0.1')) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: JettonMaster.discoveryMessage(owner, include_address), + value: value, + }); + } + + static changeAdminMessage(newOwner: Address) { + return beginCell().storeUint(Op.change_admin, 32).storeUint(0, 64) // op, queryId + .storeAddress(newOwner) + .endCell(); + } + + async sendChangeAdmin(provider: ContractProvider, via: Sender, newOwner: Address) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: JettonMaster.changeAdminMessage(newOwner), + value: toNano("0.05"), + }); + } + static changeContentMessage(content: Cell) { + return beginCell().storeUint(Op.change_content, 32).storeUint(0, 64) // op, queryId + .storeRef(content) + .endCell(); + } + + async sendChangeContent(provider: ContractProvider, via: Sender, content: Cell) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: JettonMaster.changeContentMessage(content), + value: toNano("0.05"), + }); + } + async getWalletAddress(provider: ContractProvider, owner: Address): Promise
{ + const res = await provider.get('get_wallet_address', [{ type: 'slice', cell: beginCell().storeAddress(owner).endCell() }]) + return res.stack.readAddress() + } + + async getJettonData(provider: ContractProvider) { + let res = await provider.get('get_jetton_data', []); + let totalSupply = res.stack.readBigNumber(); + let mintable = res.stack.readBoolean(); + let adminAddress = res.stack.readAddress(); + let content = res.stack.readCell(); + let walletCode = res.stack.readCell(); + return { + totalSupply, + mintable, + adminAddress, + content, + walletCode + }; + } + + async getTotalSupply(provider: ContractProvider) { + let res = await this.getJettonData(provider); + return res.totalSupply; + } + async getAdminAddress(provider: ContractProvider) { + let res = await this.getJettonData(provider); + return res.adminAddress; + } + async getContent(provider: ContractProvider) { + let res = await this.getJettonData(provider); + return res.content; + } +} \ No newline at end of file diff --git a/wrappers/JettonVault.ts b/wrappers/JettonVault.ts new file mode 100644 index 0000000..af182c8 --- /dev/null +++ b/wrappers/JettonVault.ts @@ -0,0 +1 @@ +export * from '../build/JettonVault/tact_JettonVault'; diff --git a/wrappers/JettonWallet.ts b/wrappers/JettonWallet.ts new file mode 100644 index 0000000..0d9db2f --- /dev/null +++ b/wrappers/JettonWallet.ts @@ -0,0 +1,73 @@ +import { Address, beginCell, Builder, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode } from '@ton/core'; + +export type JettonWalletConfig = { + owner: Address; + minter: Address; + walletCode: Cell; +}; + +export function jettonWalletConfigToCell(config: JettonWalletConfig): Cell { + return beginCell() + .storeCoins(0) + .storeAddress(config.owner) + .storeAddress(config.minter) + .storeRef(config.walletCode) + .endCell(); +} + +export class JettonWallet implements Contract { + constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) { } + + static createFromAddress(address: Address) { + return new JettonWallet(address); + } + + static createFromConfig(config: JettonWalletConfig, code: Cell, workchain = 0) { + const data = jettonWalletConfigToCell(config); + const init = { code, data }; + return new JettonWallet(contractAddress(workchain, init), init); + } + + async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { + await provider.internal(via, { + value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell().endCell(), + }); + } + + async sendTransfer( + provider: ContractProvider, + via: Sender, + value: bigint, + forwardValue: bigint, + recipient: Address, + amount: bigint, + forwardPayload: Cell + ) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell() + .storeUint(0x3ee943f1, 32) + .storeUint(0, 64) + .storeCoins(amount) + .storeAddress(recipient) + .storeAddress(via.address) + .storeUint(0, 1) + .storeCoins(forwardValue) + .storeUint(1, 1) + .storeRef(forwardPayload) + .endCell(), + value: value + forwardValue, + }); + } + + async getJettonBalance(provider: ContractProvider) { + let state = await provider.getState(); + if (state.state.type !== 'active') { + return 0n; + } + let res = await provider.get('get_wallet_data', []); + return res.stack.readBigNumber(); + } +} diff --git a/wrappers/RewardJettonMaster.ts b/wrappers/RewardJettonMaster.ts new file mode 100644 index 0000000..a2c919b --- /dev/null +++ b/wrappers/RewardJettonMaster.ts @@ -0,0 +1,190 @@ +import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode, toNano, internal as internal_relaxed, storeMessageRelaxed, Dictionary } from '@ton/core'; + +import { Op } from './JettonConstants'; + +export type RewardJettonMasterContent = { + type: 0 | 1, + uri: string +}; + +export type RewardJettonMasterConfig = { admin: Address; content: Cell; walletCode: Cell }; + +export function RewardJettonMasterConfigToCell(config: RewardJettonMasterConfig): Cell { + return beginCell() + .storeCoins(0) + .storeAddress(config.admin) + .storeRef(config.content) + .storeRef(config.walletCode) + .endCell(); +} + +export function jettonContentToCell(content: RewardJettonMasterContent) { + return beginCell() + .storeUint(content.type, 8) + .storeStringTail(content.uri) //Snake logic under the hood + .endCell(); +} + +export class RewardJettonMaster implements Contract { + constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) { } + + static createFromAddress(address: Address) { + return new RewardJettonMaster(address); + } + + static createFromConfig(config: RewardJettonMasterConfig, code: Cell, workchain = 0) { + const data = RewardJettonMasterConfigToCell(config); + const init = { code, data }; + return new RewardJettonMaster(contractAddress(workchain, init), init); + } + + async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { + await provider.internal(via, { + value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell().endCell(), + }); + } + + protected static jettonInternalTransfer(jetton_amount: bigint, + forward_ton_amount: bigint, + response_addr?: Address, + query_id: number | bigint = 0) { + return beginCell() + .storeUint(Op.internal_transfer, 32) + .storeUint(query_id, 64) + .storeCoins(jetton_amount) + .storeAddress(null) + .storeAddress(response_addr) + .storeCoins(forward_ton_amount) + .storeBit(false) + .endCell(); + } + + static mintMessage(from: Address, to: Address, jetton_amount: bigint, forward_ton_amount: bigint, total_ton_amount: bigint, query_id: number | bigint = 0) { + const mintMsg = beginCell().storeUint(Op.internal_transfer, 32) + .storeUint(0, 64) + .storeCoins(jetton_amount) + .storeAddress(null) + .storeAddress(from) // Response addr + .storeCoins(forward_ton_amount) + .storeMaybeRef(null) + .endCell(); + + return beginCell().storeUint(Op.mint, 32).storeUint(query_id, 64) // op, queryId + .storeAddress(to) + .storeCoins(total_ton_amount) + .storeCoins(jetton_amount) + .storeRef(mintMsg) + .endCell(); + } + async sendMint(provider: ContractProvider, via: Sender, to: Address, jetton_amount: bigint, forward_ton_amount: bigint, total_ton_amount: bigint) { + if (total_ton_amount <= forward_ton_amount) { + throw new Error("Total ton amount should be > forward amount"); + } + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: RewardJettonMaster.mintMessage(this.address, to, jetton_amount, forward_ton_amount, total_ton_amount), + value: total_ton_amount + toNano('0.015'), + }); + } + + static mintBatchMessage( + batch_data: Dictionary, + query_id: number | bigint = 0 + ) { + return beginCell().storeUint(Op.mint_batch, 32).storeUint(query_id, 64) // op, queryId + .storeUint(batch_data.size, 16) + .storeDict(batch_data, Dictionary.Keys.Address(), Dictionary.Values.BigInt(257)) + .endCell(); + } + + async sendMintBatch( + provider: ContractProvider, + via: Sender, + batch_data: Dictionary + ) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: RewardJettonMaster.mintBatchMessage(batch_data), + value: toNano('0.015') * BigInt(batch_data.size), + }); + } + + + /* provide_wallet_address#e450e86a query_id:uint64 owner_address:MsgAddress include_address:Bool = InternalMsgBody; + */ + static discoveryMessage(owner: Address, include_address: boolean) { + return beginCell().storeUint(Op.provide_wallet_address, 32).storeUint(0, 64) // op, queryId + .storeAddress(owner).storeBit(include_address) + .endCell(); + } + + async sendDiscovery(provider: ContractProvider, via: Sender, owner: Address, include_address: boolean, value: bigint = toNano('0.1')) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: RewardJettonMaster.discoveryMessage(owner, include_address), + value: value, + }); + } + + static changeAdminMessage(newOwner: Address) { + return beginCell().storeUint(Op.change_admin, 32).storeUint(0, 64) // op, queryId + .storeAddress(newOwner) + .endCell(); + } + + async sendChangeAdmin(provider: ContractProvider, via: Sender, newOwner: Address) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: RewardJettonMaster.changeAdminMessage(newOwner), + value: toNano("0.05"), + }); + } + static changeContentMessage(content: Cell) { + return beginCell().storeUint(Op.change_content, 32).storeUint(0, 64) // op, queryId + .storeRef(content) + .endCell(); + } + + async sendChangeContent(provider: ContractProvider, via: Sender, content: Cell) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: RewardJettonMaster.changeContentMessage(content), + value: toNano("0.05"), + }); + } + async getWalletAddress(provider: ContractProvider, owner: Address): Promise
{ + const res = await provider.get('get_wallet_address', [{ type: 'slice', cell: beginCell().storeAddress(owner).endCell() }]) + return res.stack.readAddress() + } + + async getJettonData(provider: ContractProvider) { + let res = await provider.get('get_jetton_data', []); + let totalSupply = res.stack.readBigNumber(); + let mintable = res.stack.readBoolean(); + let adminAddress = res.stack.readAddress(); + let content = res.stack.readCell(); + let walletCode = res.stack.readCell(); + return { + totalSupply, + mintable, + adminAddress, + content, + walletCode + }; + } + + async getTotalSupply(provider: ContractProvider) { + let res = await this.getJettonData(provider); + return res.totalSupply; + } + async getAdminAddress(provider: ContractProvider) { + let res = await this.getJettonData(provider); + return res.adminAddress; + } + async getContent(provider: ContractProvider) { + let res = await this.getJettonData(provider); + return res.content; + } +} \ No newline at end of file