From ae8ac285af46b76de5371bf473958f58a67b6aba Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Wed, 7 Dec 2022 23:30:25 +0300 Subject: [PATCH 1/2] Jettons toncli tests added --- jetton_tests/.gitignore | 2 + jetton_tests/README.md | 59 ++ jetton_tests/fift/.gitkeep | 0 jetton_tests/fift/burn_jettons.fif | 8 + jetton_tests/fift/mint_jettons.fif | 33 ++ jetton_tests/fift/minter_data.fif | 28 + jetton_tests/fift/send_jettons.fif | 27 + jetton_tests/fift/wallet_data.fif | 30 + jetton_tests/project.yaml | 38 ++ jetton_tests/tests/.gitkeep | 0 jetton_tests/tests/minter-tests-int.func | 96 ++++ jetton_tests/tests/minter-tests.func | 204 +++++++ jetton_tests/tests/utils/constants.func | 7 + jetton_tests/tests/utils/helpers.func | 2 + jetton_tests/tests/utils/op-codes.fc | 9 + jetton_tests/tests/wallet-tests-int.func | 118 ++++ jetton_tests/tests/wallet-tests.func | 666 +++++++++++++++++++++++ 17 files changed, 1327 insertions(+) create mode 100644 jetton_tests/.gitignore create mode 100644 jetton_tests/README.md create mode 100644 jetton_tests/fift/.gitkeep create mode 100644 jetton_tests/fift/burn_jettons.fif create mode 100644 jetton_tests/fift/mint_jettons.fif create mode 100644 jetton_tests/fift/minter_data.fif create mode 100644 jetton_tests/fift/send_jettons.fif create mode 100644 jetton_tests/fift/wallet_data.fif create mode 100644 jetton_tests/project.yaml create mode 100644 jetton_tests/tests/.gitkeep create mode 100644 jetton_tests/tests/minter-tests-int.func create mode 100644 jetton_tests/tests/minter-tests.func create mode 100644 jetton_tests/tests/utils/constants.func create mode 100644 jetton_tests/tests/utils/helpers.func create mode 100644 jetton_tests/tests/utils/op-codes.fc create mode 100644 jetton_tests/tests/wallet-tests-int.func create mode 100644 jetton_tests/tests/wallet-tests.func diff --git a/jetton_tests/.gitignore b/jetton_tests/.gitignore new file mode 100644 index 0000000..2053e73 --- /dev/null +++ b/jetton_tests/.gitignore @@ -0,0 +1,2 @@ +*.pk +build diff --git a/jetton_tests/README.md b/jetton_tests/README.md new file mode 100644 index 0000000..b87aa56 --- /dev/null +++ b/jetton_tests/README.md @@ -0,0 +1,59 @@ +# Jetton Minter example project + +This project allows you to: + +1. Build basic jetton minter contract +2. Aims to *hopefully* test any nftcollection contract for compliance with [Jetton standerd](https://github.com/ton-blockchain/TIPs/issues/74) +3. Deploy minter contract via `toncli deploy` +4. Manually deploy jetton wallet via minting tokens +5. Manually send to other jetton wallets +6. Manyally burn coins on your wallet + +## Building + + `toncli start jetton_minter` + `toncli build` + +## Testing + + Same here `toncli run_test` + If you encounter **error 6** during *run_tests* + make shure that your binaries are built according to:[this manual](https://github.com/disintar/toncli/blob/master/docs/advanced/func_tests_new.md) + +## Deploying minter contract + + This project consists of two sub-projects **jetton_minter** and **jetton_wallet** + You can see that in the *project.yml* + **BOTH** of those have to be built. + First type:`toncli build` + However it makes sense to deploy only *jetton_minter*. + Prior to deployment you need to check out *fift/minter_data.fif* + and change all mock configuration values to your own liking. + To deploy run:`toncli deploy -n testnet jetton_minter`. + +## Minting jettons + + To mint coins to your wallet + you will have to: + ++ Configure *fift/mint_jettons.fif* script with your own values: +[Take a look](https://github.com/ton-blockchain/TIPs/issues/74) + ++ Make yourself familiar with process of sending [internal messages](https://github.com/disintar/toncli/blob/master/docs/advanced/send_fift_internal.md) + +`toncli send -n testnet -a 0.035 -c jetton_minter --body fift/mint_jettons.fif` + +## Sending jettons + + To send coins to someone elses jetton wallet + you will have to: + ++ Setup values in *fift/send_jettons.fif* ++ Run:`toncli send -n testnet -a 0.1 --address < your jetton wallet addr> --body fift/send_jettons.fif` + +## Burning jettons + + To burn jettons + ++ Setup values in *fift/burn_jettons.fif* ++ Run `toncli send -n testnet -a 0.1 --address < your jetton wallet addr > --body fift/burn_jettons.fif` diff --git a/jetton_tests/fift/.gitkeep b/jetton_tests/fift/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/jetton_tests/fift/burn_jettons.fif b/jetton_tests/fift/burn_jettons.fif new file mode 100644 index 0000000..bfd6c35 --- /dev/null +++ b/jetton_tests/fift/burn_jettons.fif @@ -0,0 +1,8 @@ +"TonUtil.fif" include + + diff --git a/jetton_tests/fift/mint_jettons.fif b/jetton_tests/fift/mint_jettons.fif new file mode 100644 index 0000000..6bbcdf2 --- /dev/null +++ b/jetton_tests/fift/mint_jettons.fif @@ -0,0 +1,33 @@ +"TonUtil.fif" include + +"EQDlT07NpSh0uj-aSBkF2TRxOqR2nw0ErOQsA6TYakr1-FxP" constant mint_address +0x178d4519 constant internal_transfer +1000000000 10 * constant mint_amount +30000000 constant forward_amount + +mint_address +$>smca 0= abort"Specify valid mint addr" +drop // Drop flags + +2constant mint_raw // worchain and addr into single constant + +// Here goes master message +// internal_transfer + + + + diff --git a/jetton_tests/fift/minter_data.fif b/jetton_tests/fift/minter_data.fif new file mode 100644 index 0000000..5382599 --- /dev/null +++ b/jetton_tests/fift/minter_data.fif @@ -0,0 +1,28 @@ +"TonUtil.fif" include +"Asm.fif" include + + +"EQDlT07NpSh0uj-aSBkF2TRxOqR2nw0ErOQsA6TYakr1-FxP" constant owner_address // Specify your own +"https://raw.githubusercontent.com/Trinketer22/token-contract/main/ft/web-example/test_jetton.json" constant jetton_meta // Specify your own +"build/jetton_wallet.fif" constant wallet_code_path +1000000000 100 * constant jetton_supply // Starting jetton supply + +B B, +b> + + +owner_address +$>smca 0= abort"Specify valid admin addr" +drop // Drop flags + + diff --git a/jetton_tests/fift/send_jettons.fif b/jetton_tests/fift/send_jettons.fif new file mode 100644 index 0000000..9574f61 --- /dev/null +++ b/jetton_tests/fift/send_jettons.fif @@ -0,0 +1,27 @@ +"TonUtil.fif" include + +"EQDRebAnF1pvH1YsKNp7mtpsz+CLs6WxaffUojt1ijyrazkg" constant receiver_address +1000000000 constant send_amount +20000000 constant forward_amount +12345 constant query_id +0xf8a7ea5 constant op_transfer + +receiver_address +$>smca 0= abort"Specify valid send addr" +drop // Drop flags + +2constant send_addr // worchain and addr into single constant + +// Here goes master message +// internal_transfer + + diff --git a/jetton_tests/fift/wallet_data.fif b/jetton_tests/fift/wallet_data.fif new file mode 100644 index 0000000..64abbf5 --- /dev/null +++ b/jetton_tests/fift/wallet_data.fif @@ -0,0 +1,30 @@ +"TonUtil.fif" include +"Asm.fif" include + +1000000000 10 * constant initial_balance // Wallet balance +"build/jetton_wallet.fif" constant wallet_code + +/* + This file is primerely for tests to run + Main way of deploying jetton wallet is via jetton minter "mint" operation. + Sending jettons from already deployed wallet is also an option + Thus random 256 bit value will be used ans and address for collection and owner + + We're going to use newkeypair as a way to generate two random 256 bit values + Do not do that in real app +*/ + +newkeypair + +256 B>u@ +swap +256 B>u@ + + diff --git a/jetton_tests/project.yaml b/jetton_tests/project.yaml new file mode 100644 index 0000000..65bfcd9 --- /dev/null +++ b/jetton_tests/project.yaml @@ -0,0 +1,38 @@ +jetton_minter: + data: fift/minter_data.fif + func: + - ../ft/op-codes.fc + - ../ft/params.fc + - ../ft/jetton-utils.fc + - ../ft/discovery-params.fc + - tests/utils/helpers.func + - ../ft/jetton-minter.fc + tests: + - tests/minter-tests.func + - tests/minter-tests-int.func + +jetton_discoverable: + data: fift/minter_data.fif + func: + - ../ft/op-codes.fc + - ../ft/params.fc + - tests/utils/helpers.func + - ../ft/jetton-utils.fc + - ../ft/discovery-params.fc + - ../ft/jetton-minter-discoverable.fc + tests: + - tests/minter-tests.func + - tests/minter-tests-int.func + + +jetton_wallet: + data: fift/wallet_data.fif + func: + - ../ft/op-codes.fc + - ../ft/params.fc + - ../ft/jetton-utils.fc + - tests/utils/helpers.func + - ../ft/jetton-wallet.fc + tests: + - tests/wallet-tests.func + - tests/wallet-tests-int.func diff --git a/jetton_tests/tests/.gitkeep b/jetton_tests/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/jetton_tests/tests/minter-tests-int.func b/jetton_tests/tests/minter-tests-int.func new file mode 100644 index 0000000..e957726 --- /dev/null +++ b/jetton_tests/tests/minter-tests-int.func @@ -0,0 +1,96 @@ +#pragma version >=0.2.0; + +#include "utils/constants.func"; + + +int __test_get_jetton_data_mint() { + + var( gas_before, stack ) = invoke_method( get_jetton_data, [] ); + + int total_supply = stack.first(); + int mintable? = stack.second(); + slice admin = stack.third(); + cell content = stack.fourth(); + cell code = stack.at( 4 ); + + + ;; Can't test non mintable contract with mint message. + if( ~ mintable? ) { + return gas_before; + } + + int query_id = rand(1337) + 1; + int forward_ton = one_unit / 10; + int mint_amount = ( rand( 100 ) + 1 ) * one_unit / 10; + slice rand_addr = generate_internal_address_with_custom_data(0, 0, random()); + slice mint_dst = generate_internal_address_with_custom_data(0, 0, random()); + slice mint_from = generate_empty_address(); + cell mint_payload = generate_jetton_internal_transfer_request( query_id, mint_amount, mint_from, rand_addr, forward_ton, null(), false ).end_cell(); + builder mint_body = generate_internal_message_body( op_mint, query_id ).store_slice( mint_dst ).store_grams( forward_ton ).store_ref( mint_payload ); + ;; Testing mint with non-admin address + cell msg = generate_internal_message_custom( 0, 0, 0, mint_body, admin, null(), 0 ); + + var( gas_mint, _ ) = invoke_method( recv_internal, [ one_unit, one_unit, msg, mint_body.end_cell().begin_parse() ] ); + var( gas_after, stack ) = invoke_method( get_jetton_data, [] ); + ;; Total supply should increase by our mint_amount + throw_unless( 500, total_supply + mint_amount == stack.first() ); + + ;; Rest should not change + + throw_unless( 501, mintable? == stack.second() ); + throw_unless( 502, equal_slices( admin, stack.third() ) ); + throw_unless( 503, equal_slices( content.begin_parse(), stack.fourth().begin_parse() ) ); + throw_unless( 504, equal_slices( code.begin_parse(), stack.at( 4 ).begin_parse() ) ); + + + return gas_before + gas_mint + gas_after; +} + +int __test_get_jetton_data_burn() { + + var( gas_before, stack ) = invoke_method( get_jetton_data, [] ); + + int total_supply = stack.first(); + int mintable? = stack.second(); + slice admin = stack.third(); + cell content = stack.fourth(); + cell code = stack.at( 4 ); + + ;;Nothing to burn + if( total_supply == 0 ) { + return gas_before; + } + + {- + If jetton is not mintabe, does it mean that it's necesserely + non-burnable too? IDK. + I'd say doesn't and burnable? flag required. + So we would ignore mintable? in this test. + -} + + int query_id = rand(1337) + 1; + int forward_ton = one_unit / 10; + int burn_amount = ( rand( 100 ) + 1 ) * one_unit / 10; + + slice jetton_addr = my_address(); ;;In testing mode my_address() for test code equals my_address() for contract code + slice sender = generate_internal_address_with_custom_data( 0, 0, random() ); + slice src = calculate_user_jetton_wallet_address( sender, jetton_addr, code ); + var msg_body = generate_jetton_burn_notification( query_id, burn_amount, sender, admin ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, src, null(), 0); + + ( int gas_burn, _ ) = invoke_method( recv_internal, [one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] ); + + ( int gas_after, stack ) = invoke_method( get_jetton_data, [] ); + + + throw_unless( 500, total_supply - burn_amount == stack.first() ); + + ;; Rest should not change + + throw_unless( 501, mintable? == stack.second() ); + throw_unless( 502, equal_slices( admin, stack.third() ) ); + throw_unless( 503, equal_slices( content.begin_parse(), stack.fourth().begin_parse() ) ); + throw_unless( 504, equal_slices( code.begin_parse(), stack.at( 4 ).begin_parse() ) ); + + return gas_before + gas_burn + gas_after; +} diff --git a/jetton_tests/tests/minter-tests.func b/jetton_tests/tests/minter-tests.func new file mode 100644 index 0000000..a08651c --- /dev/null +++ b/jetton_tests/tests/minter-tests.func @@ -0,0 +1,204 @@ +#pragma version >=0.2.0; + +#include "utils/constants.func"; +#include "utils/op-codes.fc"; + +(int, slice, cell, cell) load_test_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 + ); +} + +_ verify_excess_jetton( int query_id, slice resp_dst, int msg_value, int forward_fee, int forward_amount, cell msg ) impure inline { + {- + TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody; + Excess message should be sent to resp_dst with all of the msg_value - fees taken to process + We verify that: + 1) message is sent to resp_dst + 2) attached amount is at least msg_value - forward_fee * 2 + 3) op matches excess op + 4) query_id matches request query_id + -} + + tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) ); + + ;;Check dst_addr to be equal to resp_dst + throw_unless( 701, equal_slices( resp_dst, parsed_msg.at( 4 ) ) ); + + int total_sent = parsed_msg.at( 5 ); + int should_sent = msg_value - forward_amount - forward_fee * 2; + + throw_unless( 702, total_sent >= should_sent ); + + slice msg_body = parsed_msg.at( 8 ); + + throw_unless( 703, op_excesses == msg_body~load_uint( 32 ) ); + + throw_unless( 704, query_id == msg_body~load_uint( 64 ) ); +} + +_ validate_TIP_64( slice content_data ) impure inline { + + int content_layout = content_data~load_uint( 8 ); + + ;; Check for allowed content_layout + throw_unless( 305, ( content_layout == 1 ) | ( content_layout == 0 ) ); + + if( content_layout == 1 ){ + + ;; Check that off-chain URI contains at least one ASCII char + throw_unless( 306, token_snake_len( content_data ) > 8 ); + } else { + ;; On-chain is stored as dict + ;; Has to be non-empty + throw_if( 306, content_data.preload_dict().dict_empty?() ); + + ;; Perhaps could go further and test for Optional dict keys but none of those are required so i'll leave it be + ;; For now + } +} + +int __test_burn_notification() { + {- + burn_notification query_id:uint64 amount:(VarUInteger 16) + sender:MsgAddress response_destination:MsgAddress + = InternalMsgBody; + + On receiving burn notification jetton master should + 1) Decrease total supply by burn_amount. + 2) Send excess message to the response_destination. + + -} + var ( total_supply, admin_address, content, code ) = load_test_data(); + + int query_id = rand( 1337 ) + 1; + int burn_amount = total_supply / 10; + ;; Making sure that source address would be valid jetton address + + slice sender = generate_internal_address_with_custom_data( 0, 0, random() ); + slice src = calculate_user_jetton_wallet_address( sender, my_address(), code ); + var msg_body = generate_jetton_burn_notification( query_id, burn_amount, sender, admin_address ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, src, null(), 0); + + ( int gas_used, _ ) = invoke_method( recv_internal, [one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] ); + + ;; Expect single message + tuple actions = parse_c5(); + throw_unless( 600, actions.tuple_length() == 1 ); + + ( int action_type, cell sent_msg, int mode ) = actions.at(0).untriple(); + throw_unless( 601, action_type == 0 ); + throw_unless( 602, ( mode >= 64 ) & ( mode <= 66 ) ); + + ;; it's mode 64 so msg_value - fees return guaranteed + ;; Still have to verify excess structure + + verify_excess_jetton( query_id, admin_address, 0, 0, 0, sent_msg ); + + var( new_supply, _, _, _ ) = load_test_data(); + + throw_unless( 603, total_supply - burn_amount == new_supply ); + + + return gas_used; +} + +int __test_mint() { + + ;;Non-standardized but widely used message + var ( total_supply, admin_address, content, code ) = load_test_data(); + + int query_id = rand(1337) + 1; + int forward_ton = one_unit / 10; + int mint_amount = ( rand( 100 ) + 1 ) * one_unit / 10; + slice rand_addr = generate_internal_address_with_custom_data(0, 0, random()); + slice mint_dst = generate_internal_address_with_custom_data(0, 0, random()); + slice mint_from = generate_empty_address(); + + {- + In current implementation sends any forward payload provided. + However for jettons to be successfully creditated on wallet balance + it should have internal_transfer message format. + -} + + cell mint_payload = generate_jetton_internal_transfer_request( query_id, mint_amount, mint_from, rand_addr, forward_ton, null(), false ).end_cell(); + builder mint_body = generate_internal_message_body( op_mint, query_id ).store_slice( mint_dst ).store_grams( forward_ton ).store_ref( mint_payload ); + ;; Testing mint with non-admin address + cell msg = generate_internal_message_custom( 0, 0, 0, mint_body, rand_addr, null(), 0 ); + + int gas_failed = invoke_method_expect_fail( recv_internal, [ one_unit, one_unit, msg, mint_body.end_cell().begin_parse() ] ); + assert_no_actions(); + + ;;Now changing source address to admin + msg = generate_internal_message_custom( 0, 0, 0, mint_body, admin_address, null(), 0 ); + + var( gas_success, _ ) = invoke_method( recv_internal, [ one_unit, one_unit, msg, mint_body.end_cell().begin_parse() ] ); + + ;; Expect single message + tuple actions = parse_c5(); + throw_unless( 600, actions.tuple_length() == 1 ); + + ( int action_type, cell sent_msg, int mode ) = actions.at(0).untriple(); + throw_unless( 601, action_type == 0 ); + + tuple parsed_msg = unsafe_tuple( parse_internal_message( sent_msg ) ); + + {- + What do we know about how mint message should look like in general? + 1) Wallet has to be deployed thus StateInit has to be present + 2) Our forward payload should be the message body + That's what we're going to check for. + Rest is very contract specific. + -} + + throw_if( 602, parsed_msg.at( 7 ).null?() ); + throw_unless( 603, equal_slices( mint_payload.begin_parse(), parsed_msg.at( 8 ) ) ); + + ;; Also total supply should increase by mint_amount + + var( new_supply, _, _, _ ) = load_test_data(); + throw_unless( 604, total_supply + mint_amount == new_supply ); + + return gas_failed + gas_success; + +} + +;; Get methods tests start here + +int __test_get_jetton_data() { + + int expect_mintable? = true; ;;Determines if test expects mintable contract + + var ( total_supply, admin_address, content, code ) = load_test_data(); + + var ( gas_used, stack ) = invoke_method( get_jetton_data, [] ); + + throw_unless( 500, stack.tuple_length() == 5 ); + + throw_unless( 501, total_supply == stack.first() ); + throw_unless( 502, expect_mintable? & stack.second() ); + throw_unless( 503, equal_slices( admin_address, stack.third() ) ); + + slice res_cs = stack.fourth().begin_parse(); ;; Content slice + + throw_unless( 504, equal_slices( content.begin_parse(), res_cs ) ); + validate_TIP_64( res_cs ); + throw_unless( 505, equal_slices( code.begin_parse(), stack.at( 4 ).begin_parse() ) ); + + return gas_used; +} + +int __test_get_wallet_address() { + + var ( gas_used, stack ) = invoke_method( get_wallet_address, [ my_address() ] ); + + throw_unless( 700, stack.tuple_length() == 1 ); + + parse_std_addr( stack.first() ); ;;I guess that's what else we can check + + return gas_used; +} diff --git a/jetton_tests/tests/utils/constants.func b/jetton_tests/tests/utils/constants.func new file mode 100644 index 0000000..28221d7 --- /dev/null +++ b/jetton_tests/tests/utils/constants.func @@ -0,0 +1,7 @@ +const int one_unit = 1000000000; ;; 10^9 + +;; These are not standardized values each Jetton can have it's own fee guidelines +const int jetton_min_storage = 10000000; ;;0.01 TON +const int jetton_gas_fee = 10000000; ;;0.01 TON + + diff --git a/jetton_tests/tests/utils/helpers.func b/jetton_tests/tests/utils/helpers.func new file mode 100644 index 0000000..8cf2360 --- /dev/null +++ b/jetton_tests/tests/utils/helpers.func @@ -0,0 +1,2 @@ +int equal_slices (slice a, slice b) asm "SDEQ"; +int tuple_length( tuple t ) asm "TLEN"; diff --git a/jetton_tests/tests/utils/op-codes.fc b/jetton_tests/tests/utils/op-codes.fc new file mode 100644 index 0000000..2b6ff80 --- /dev/null +++ b/jetton_tests/tests/utils/op-codes.fc @@ -0,0 +1,9 @@ +const int op_transfer = 0xf8a7ea5; +const int op_transfer_notification = 0x7362d09c; +const int op_internal_transfer = 0x178d4519; +const int op_excesses = 0xd53276db; +const int op_burn = 0x595f07bc; +const int op_burn_notification = 0x7bdd97de; + +;; Minter +const int op_mint = 21; diff --git a/jetton_tests/tests/wallet-tests-int.func b/jetton_tests/tests/wallet-tests-int.func new file mode 100644 index 0000000..1e509c7 --- /dev/null +++ b/jetton_tests/tests/wallet-tests-int.func @@ -0,0 +1,118 @@ +#pragma version >=0.2.0; + +#include "utils/constants.func"; + + +int __test_get_wallet_data_transfer() { + + var ( _, dst, resp_dst, query_id ) = setup_req_fields(); + var ( gas_before, stack ) = invoke_method( get_wallet_data, [] ); + + int balance = stack.first(); + slice owner = stack.second(); + slice master = stack.third(); + cell code = stack.fourth(); + + ;; Nothing to send + + if( balance <= 0 ) { + return gas_before; ;;throw( 100 ) ? + } + + int transfer_amount = ( rand( 10 ) + 1 ) * balance / 10; ;; From 0.1 * balance to 1 * balance + int forward_amount = one_unit / 10; + int forward_fee = one_unit / 100; + + + builder msg_body = generate_jetton_transfer_request( query_id, transfer_amount, dst,resp_dst, null(), forward_amount, null(), false ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), forward_fee ); + + var ( gas_transfer, _ ) = invoke_method( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] ); + var ( gas_after, stack ) = invoke_method( get_wallet_data, [] ); + + int new_balance = stack.first(); + + throw_unless( 500, balance - transfer_amount == stack.first() ); + + ;; Rest should not change + throw_unless( 501, equal_slices( owner, stack.second() ) ); + throw_unless( 502, equal_slices( master, stack.third() ) ); + throw_unless( 503, equal_slices( code.begin_parse(), stack.fourth().begin_parse() ) ); + + + return gas_before + gas_transfer + gas_after; +} + +int __test_get_wallet_internal_transfer() { + + {- + When incoming transfer happens + Balance should increase accordingly + -} + + var ( _, dst, resp_dst, query_id ) = setup_req_fields(); + var ( gas_before, stack ) = invoke_method( get_wallet_data, [] ); + + int balance = stack.first(); + slice owner = stack.second(); + slice master = stack.third(); + cell code = stack.fourth(); + + + int transfer_amount = ( rand( 10 ) + 1 ) * one_unit / 10; ;;From 0.1 to 1 unit + int forward_ton = one_unit / 10; + int msg_value = one_unit; + + var msg_body = generate_jetton_internal_transfer_request( query_id, transfer_amount, master, dst, forward_ton, null(), false ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, master, null(), 0 ); + + var ( gas_transfer, _ ) = invoke_method( recv_internal, [ msg_value, msg_value, msg, msg_body.end_cell().begin_parse() ] ); + var ( gas_after, stack ) = invoke_method( get_wallet_data, [] ); + + throw_unless( 500, balance + transfer_amount == stack.first() ); + ;; Rest should not change + throw_unless( 501, equal_slices( owner, stack.second() ) ); + throw_unless( 502, equal_slices( master, stack.third() ) ); + throw_unless( 503, equal_slices( code.begin_parse(), stack.fourth().begin_parse() ) ); + + return gas_before + gas_transfer + gas_after; +} + +int __test_get_wallet_data_burn() { + + var ( _, dst, resp_dst, query_id ) = setup_req_fields(); + var ( gas_before, stack ) = invoke_method( get_wallet_data, [] ); + + int balance = stack.first(); + slice owner = stack.second(); + slice master = stack.third(); + cell code = stack.fourth(); + + + ;; Nothing to burn + + if( balance <= 0 ) { + return gas_before; ;;throw( 100 ) ? + } + + int burn_amount = ( rand( 10 ) + 1 ) * balance / 10; ;; From 0.1 * balance to 1 * balance + int forward_amount = one_unit / 10; + int forward_fee = one_unit / 100; + var msg_body = generate_jetton_burn_request( query_id, burn_amount, resp_dst, null() ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 ); + + var ( gas_burn, _ ) = invoke_method( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] ); + + var ( gas_after, stack ) = invoke_method( get_wallet_data, [] ); + + throw_unless( 500, balance - burn_amount == stack.first() ); + + ;; Rest should not change + throw_unless( 501, equal_slices( owner, stack.second() ) ); + throw_unless( 502, equal_slices( master, stack.third() ) ); + throw_unless( 503, equal_slices( code.begin_parse(), stack.fourth().begin_parse() ) ); + + + + return gas_before + gas_burn; +} diff --git a/jetton_tests/tests/wallet-tests.func b/jetton_tests/tests/wallet-tests.func new file mode 100644 index 0000000..17fce59 --- /dev/null +++ b/jetton_tests/tests/wallet-tests.func @@ -0,0 +1,666 @@ +#pragma version >=0.2.0; + +#include "utils/constants.func"; +#include "utils/op-codes.fc"; + + +( int, slice, slice, cell ) load_test_data() inline { + + slice ds = get_data().begin_parse(); + return (ds~load_coins(), ds~load_msg_addr(), ds~load_msg_addr(), ds~load_ref()); +} + +( slice ) get_owner() inline { + + slice ds = get_data().begin_parse(); + ds~load_coins(); + + return ds~load_msg_addr(); +} + +( slice ) get_master() inline { + slice ds = get_data().begin_parse(); + ds~load_coins(); + ds~load_msg_addr(); + return ds~load_msg_addr(); +} + +( int ) get_test_balance() inline { + slice ds = get_data().begin_parse(); + + return ds~load_coins(); +} + +( cell,() ) replace_msg_source( cell msg, slice new_addr ) inline { + slice old_msg = msg.begin_parse(); + builder new_msg = begin_cell().store_slice( old_msg~load_bits( 4 ) ); + old_msg~load_msg_addr(); ;;Skip source addr + new_msg = new_msg.store_slice( new_addr ).store_slice( old_msg ); ;;Store new addr and the rest of the orig msg + + return ( new_msg.end_cell(),() ); + +} + +slice gen_non_owner( slice old_owner ) inline { + slice new_owner = generate_internal_address_with_custom_data( 0, 0, random() ); + + ;; In theory we can win a bingo and get same 256bit integer from RNG + while( equal_slices( new_owner, old_owner ) ) { + new_owner = generate_internal_address_with_custom_data( 0, 0, random() ); + } + + return new_owner; +} + +( slice, slice, slice, int ) setup_req_fields() { + slice owner = get_owner(); + slice dst = generate_internal_address_with_custom_data( 0, 0, random() ); + slice resp_dst = generate_internal_address_with_custom_data( 0, 0, random() ); + + return ( owner, dst, resp_dst, rand( 1337 ) + 1 ); +} + +cell get_state_init_field( tuple state_init, int idx ) inline { + cell res = null(); + ;; Next flag index + int next_idx = 0; + + do { + int flag = state_init.at( next_idx ); + + next_idx = flag ? next_idx + 2 : next_idx + 1; + idx -= 1; + + if( idx == 0 & flag ) { + res = state_init.at( next_idx - 1 ); + } + + } until( ~ res.null?() | idx <= 0 ) + + return res; +} + +_ verify_transfer_notification( cell msg, int jetton_amount, int query_id, slice dst, slice resp_dst, slice sender, int forward_amount, cell payload ) impure inline { + {- + transfer_notification#7362d09c query_id:uint64 amount:(VarUInteger 16) + sender:MsgAddress forward_payload:(Either Cell ^Cell) + = InternalMsgBody; + + Transfer notification message is sent when forward_amount > 0 from receiver to sender wallet + 1) query_id should be equal to the query_id of sender request + 2) jetton_amount should be equal to the amount of transfered jettons + 3) sender should be equal to the sender address + 4) forward_payload ( if any ) should be equal to the payload of sender request + -} + + + tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) ); + + {- + Why it's dst and ton resp_dst? + I don't fully get it, but by standard: + if forward_amount > 0 ensure that receiver's jetton-wallet send message to destination address with forward_amount nanotons attached + Probably has something to do with separating excess and forward amount value + -} + throw_unless( 801, equal_slices( dst, parsed_msg.at( 4 ) ) ); + + ;;forward_amount tons should be attached + throw_unless( 802, forward_amount == parsed_msg.at( 5 ) ); + + slice msg_body = parsed_msg.at( 8 ); + + throw_unless( 803, op_transfer_notification == msg_body~load_uint( 32 ) ); + + throw_unless( 804, query_id == msg_body~load_uint( 64 ) ); + + throw_unless( 805, jetton_amount == msg_body~load_coins() ); + + throw_unless( 806, equal_slices( sender, msg_body~load_msg_addr() ) ); + + if( ~ null?( payload ) ) { + if( msg_body~load_uint( 1 ) ) { + throw_unless( 807, equal_slices( payload.begin_parse(), msg_body~load_ref().begin_parse() ) ); + } + else { + throw_unless( 807, equal_slices( payload.begin_parse(), msg_body ) ); + } + } +} + +_ verify_burn_notification( cell msg, int query_id, int burn_amount, slice sender, slice resp_dst ) impure inline { + {- + burn_notification query_id:uint64 amount:(VarUInteger 16) + sender:MsgAddress response_destination:MsgAddress + = InternalMsgBody; + + Burn notification is sent to master jetton( minter ) contract on successfull burn + + 1) Message dst addr should be equal to master jetton addr + 2) query_id should be equal to request query_id + 3) amount should be equal to request burn_amount + 4) response_destination should be equal to request resp_dst + + -} + + tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) ); + + ;; Burn notification should be sent to master + throw_unless( 901, equal_slices( get_master(), parsed_msg.at( 4 ) ) ); + + slice msg_body = parsed_msg.at( 8 ); + + throw_unless( 902, op_burn_notification == msg_body~load_uint( 32 ) ); + + throw_unless( 903, query_id == msg_body~load_uint( 64 ) ); + + throw_unless( 903, burn_amount == msg_body~load_coins() ); + + throw_unless( 904, equal_slices( sender, msg_body~load_msg_addr() ) ); + + throw_unless( 905, equal_slices( resp_dst, msg_body~load_msg_addr() ) ); +} + +_ verify_excess_jetton( int query_id, slice resp_dst, int msg_value, int forward_fee, int forward_amount, cell msg ) impure inline { + {- + TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody; + Excess message should be sent to resp_dst with all of the msg_value - fees taken to process + We verify that: + 1) message is sent to resp_dst + 2) attached amount is at least msg_value - forward_fee * 2 + 3) op matches excess op + 4) query_id matches request query_id + -} + + tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) ); + + ;;Check dst_addr to be equal to resp_dst + throw_unless( 701, equal_slices( resp_dst, parsed_msg.at( 4 ) ) ); + + int total_sent = parsed_msg.at( 5 ); + int should_sent = msg_value - forward_amount - forward_fee * 2; + + throw_unless( 702, total_sent >= should_sent ); + + slice msg_body = parsed_msg.at( 8 ); + + throw_unless( 703, op_excesses == msg_body~load_uint( 32 ) ); + + throw_unless( 704, query_id == msg_body~load_uint( 64 ) ); +} + +int transfer_test_msg_value( int msg_value, int forward_fee, int forward_amount, int expect_fail? ) impure inline { + {- + Transfer should be rejected if wallet has less or equal to + forward_amount + number of forwarded messages ( 1 if no forward_amount 2 otherwise ) * fwd_fee + jetton_gas_fee + jetton_min_storage + on the balance. + Tricky part is that forward and response part happens on the receiver wallet. + So jetton_gas_fee should be taken twice by sender and the receiver + one min_storage on the receiver part + Thus IMHO at least one jetton_gas_fee should be accounted from sender balance and not msg_value. + However author if this contract comparse all of that vs msg_value that would be passed to the receiver + https://github.com/ton-blockchain/token-contract/blob/main/ft/jetton-wallet.fc#L80 + -} + + var ( owner, dst, resp_dst, query_id ) = setup_req_fields(); + + int gas_used = 0; + int transfer_amount = get_test_balance(); + builder msg_body = generate_jetton_transfer_request( query_id, transfer_amount, dst,resp_dst, null(), forward_amount, null(), false ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), forward_fee ); + + if( expect_fail? ) { + ;; Should fail with no actions + gas_used = invoke_method_expect_fail( recv_internal, [ one_unit, msg_value, msg, msg_body.end_cell().begin_parse() ] ); + assert_no_actions(); + } + else { + + var ( gas_used, _ ) = invoke_method( recv_internal, [one_unit, msg_value + jetton_min_storage, msg, msg_body.end_cell().begin_parse() ] ); + } + + return gas_used; +} + +{- + Testing wallet transfer query handling + + transfer#0f8a7ea5 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; + +-} + +int __test_transfer_not_owner() { + + var ( owner, dst, resp_dst, query_id ) = setup_req_fields(); + + slice non_owner = gen_non_owner( owner ); + int query_id = rand( 1337 ) + 1; + builder msg_body = generate_jetton_transfer_request( query_id, one_unit, dst,resp_dst, null(), 0, null(), false ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, non_owner, null(), 0 ); + + ;; Should fail with no actions + int gas_err = invoke_method_expect_fail( recv_internal, [ one_unit * 10, one_unit, msg, msg_body.end_cell().begin_parse() ] ); + assert_no_actions(); + + ;;Now verify that owner addr triggered fail by changing source to owner addr + + msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 ); + + var ( gas_success, _ ) = invoke_method( recv_internal, [ one_unit * 10, one_unit, msg, msg_body.end_cell().begin_parse() ] ); + + return gas_err + gas_success; +} + +int __test_transfer_no_jettons() { + + var ( owner, dst, resp_dst, query_id ) = setup_req_fields(); + + int balance = get_test_balance(); + int transfer_amount = balance * 10; ;; Going to ask for way more jettons + + + builder msg_body = generate_jetton_transfer_request( query_id, transfer_amount, dst,resp_dst, null(), 0, null(), false ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 ); + + ;; Should fail with no actions + int gas_err = invoke_method_expect_fail( recv_internal, [ one_unit * 10, one_unit, msg, msg_body.end_cell().begin_parse() ] ); + assert_no_actions(); + + ;; Now let's verify that balance triggers fail by requesting transfer equal to balance amount + + msg_body = generate_jetton_transfer_request( query_id, balance, dst,resp_dst, null(), 0, null(), false ); + + msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 ); + + var ( gas_success, _ ) = invoke_method( recv_internal, [ one_unit * 10, one_unit, msg, msg_body.end_cell().begin_parse() ] ); + + return gas_err + gas_success; +} + +int __test_transfer_storage_fee() { + int transfer_fee = jetton_gas_fee * 2; + int forward_fee = one_unit / 10; + int forward_amount = one_unit / 4; + int excesses = 1; + int msg_expected = forward_amount > 0 ? 2 : 1; + int msg_value = transfer_fee + muldiv(forward_fee, 3, 2) * msg_expected + forward_amount + excesses; ;;Account for everythin except storage fee + int gas_used = 0; + + ;;Expect fail + gas_used += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true ); + + ;; Verifying error trigger + gas_used += transfer_test_msg_value( msg_value + jetton_min_storage, forward_fee, forward_amount, false ) ; + + return gas_used; +} + + +int __test_transfer_jetton_gas_fee() { + int transfer_fee = jetton_gas_fee * 2; + int forward_fee = one_unit / 10; + int forward_amount = one_unit / 4; + int excesses = 1; + int msg_expected = forward_amount > 0 ? 2 : 1; + int msg_value = muldiv(forward_fee, 3, 2) * msg_expected + forward_amount + excesses; ;;Account for everythin except transfer gas fee + int gas_used = 0; + + ;;Expect fail + gas_used += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true ); + + ;; Verifying error trigger + gas_used += transfer_test_msg_value( msg_value + transfer_fee, forward_fee, forward_amount, false ) ; + + return gas_used; +} + +int __test_transfer_forward_fee_once() { + int transfer_fee = jetton_gas_fee * 2; + int forward_fee = one_unit / 10; + int forward_amount = 0; ;;one_unit / 4; + int excesses = 1; + int msg_expected = forward_amount > 0 ? 2 : 1; + int msg_value = transfer_fee + forward_amount + excesses; ;;Account for everythin except forward_fee; + int gas_used = 0; + + ;;Expect fail + gas_used += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true ); + + ;; Verifying error trigger + gas_used += transfer_test_msg_value( msg_value + muldiv(forward_fee, 3, 2), forward_fee, forward_amount, false ) ; + + return gas_used; +} + +int __test_transfer_forward_fee_twice() { + int transfer_fee = jetton_gas_fee * 2; + int forward_fee = one_unit / 10; + int forward_amount = one_unit / 4; + int excesses = 1; + int msg_expected = forward_amount > 0 ? 2 : 1; + int msg_value = transfer_fee + muldiv(forward_fee, 3, 2) + forward_amount + excesses; ;;Account for everythin except that two forward fees instead of one + int gas_used = 0; + + ;;Expect fail + gas_used += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true ); + + ;; Verifying error trigger + gas_used += transfer_test_msg_value( msg_value + muldiv(forward_fee, 3, 2), forward_fee, forward_amount, false ) ; + + return gas_used; +} + +int __test_transfer_forward_amount() { + int transfer_fee = jetton_gas_fee * 2; + int forward_fee = one_unit / 10; + int forward_amount = one_unit / 4; + int excesses = 1; + int msg_expected = forward_amount > 0 ? 2 : 1; + int msg_value = transfer_fee + muldiv(forward_fee, 3, 2) * msg_expected + excesses; ;;Account for everythin except forward_amount + int gas_used = 0; + + ;;Expect fail + gas_used += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true ); + + ;; Verifying error trigger + gas_used += transfer_test_msg_value( msg_value + forward_amount, forward_fee, forward_amount, false ) ; + + return gas_used; +} + +int __test_transfer_excess() { + int transfer_fee = jetton_gas_fee * 2; + int forward_fee = one_unit / 10; + int forward_amount = one_unit / 4; + int excesses = 1; + int msg_expected = forward_amount > 0 ? 2 : 1; + int msg_value = transfer_fee + muldiv(forward_fee, 3, 2) * msg_expected + forward_amount; ;;Account for everythin except excess ( nothing to return ) + int gas_used = 0; + + ;;Expect fail + gas_used += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true ); + + ;; Verifying error trigger + gas_used += transfer_test_msg_value( msg_value + excesses, forward_fee, forward_amount, false ) ; + + return gas_used; +} + +int verify_successfull_transfer( int transfer_amount, int forward_amount, cell fwd_payload ) impure inline { + + var ( owner, dst, resp_dst, query_id ) = setup_req_fields(); + + int forward_fee = one_unit / 10; + int msg_value = one_unit * 2; + int prev_balance = get_test_balance(); + int expect_messages = forward_amount > 0 ? 2 : 1; + + builder msg_body = generate_jetton_transfer_request( query_id, transfer_amount, dst,resp_dst, null(), forward_amount, fwd_payload, false ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), forward_fee ); + + ;; Should fail with no actions + var ( gas_send, _ ) = invoke_method( recv_internal, [ one_unit, msg_value, msg, msg_body.end_cell().begin_parse() ] ); + + ;; Here we should get a message sent. + + tuple actions = parse_c5(); + + throw_unless( 500, actions.tuple_length() == 1 ); + + ( int action_type, cell sent_msg, int mode ) = actions.at(0).untriple(); + + throw_unless( 501, action_type == 0 ); + ;; And we should verify that wallet balance decreased + + throw_unless( 502, prev_balance > get_test_balance() ); + + {- + Now here comes the tricky part. + We should verify that receiver wallet would send certain messages back to dst and resp_dst + At a first glace receiver wallet is deployed from this contract code so we can just relay those messages + to the recv_internal of this contract. + However, data in c4 is different from one deployed contract to another. + So let's try to pick data from StateInit and set it to this contract c4 to make sure that it's on the same page + with hypothetical receiver. + -} + + cell old_data = get_data(); ;; Backup old c4 data + + + tuple parsed_msg = unsafe_tuple( parse_internal_message( sent_msg ) ); + tuple state_init = parsed_msg.at( 7 ); + throw_if( 503, null?( state_init ) ); + + ;;Fourth element of StateInit is data cell + + cell sent_data = state_init.get_state_init_field( 4 ); + throw_if( 504, null?( sent_data ) ); + + set_data( sent_data ); ;;Now we got same persistent state the receiver would in real world + slice sent_body = parsed_msg.at( 8 ); ;;Getting parsed msg body + + {- + Now we're going to have issues with the source address of the message because in tesing mode it is not going to pass + calculate_user_jetton_wallet_address check + So let's switch it up with the jetton master address + Expect successfull execution + and up to two messages. + 1) if forward_amount > 0 also transfer notification + + transfer_notification#7362d09c query_id:uint64 amount:(VarUInteger 16) + sender:MsgAddress forward_payload:(Either Cell ^Cell) + = InternalMsgBody; + + 2) excesses#d53276db query_id:uint64 = InternalMsgBody; + -} + + sent_msg~replace_msg_source( get_master() ); + ;;Let's say balance was empty before + var ( gas_replay, _ ) = invoke_method( recv_internal, [ msg_value - forward_fee, msg_value - forward_fee, sent_msg, sent_body ] ); + actions = parse_c5(); + throw_unless( 505, actions.tuple_length() == expect_messages ); + + if( forward_amount > 0 ) { + + ( action_type, cell notify_msg, mode ) = actions.at(0).untriple(); + throw_unless( 501, action_type == 0 ); + + verify_transfer_notification( notify_msg, transfer_amount, query_id, dst, resp_dst, owner, forward_amount, fwd_payload ); + } + + ( action_type, cell excess_msg, mode ) = actions.at( expect_messages - 1 ).untriple(); + + throw_unless( 501, action_type == 0 ); + + verify_excess_jetton( query_id, resp_dst, msg_value - forward_fee, forward_fee, forward_amount, excess_msg ); + + return gas_send + gas_replay; +} + +int __test_transfer_success_no_fwd() { + int gas_used = verify_successfull_transfer( one_unit, 0, null() ); ;;Test without forward amount first + + return gas_used; +} + +int __test_transfer_success_with_fwd() { + + cell payload = begin_cell().store_slice("Hop hey").end_cell(); + ;; 0.25 forward amount and some payload + int gas_used = verify_successfull_transfer( one_unit, one_unit / 4, payload ); ;;Test with forward amount + return gas_used; +} + +{- + Testing burn request + + burn#595f07bc query_id:uint64 amount:(VarUInteger 16) + response_destination:MsgAddress custom_payload:(Maybe ^Cell) + = InternalMsgBody; + +-} + +int burn_test_msg_value( int msg_value, int forward_fee, int expect_fail? ) impure inline { + ;; Testing msg_value related cases + var ( owner, dst, resp_dst, query_id ) = setup_req_fields(); + int gas_used = 0; + var msg_body = generate_jetton_burn_request( query_id, get_test_balance() / 10, resp_dst, null() ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), forward_fee ); + slice ms = msg_body.end_cell().begin_parse(); + + if( expect_fail? ) { + gas_used = invoke_method_expect_fail( recv_internal, [ msg_value, msg_value, msg, ms ] ); + } + else { + (gas_used, _ ) = invoke_method( recv_internal, [ msg_value, msg_value, msg, ms ] ); + } + + return gas_used; +} + +int __test_burn_not_owner() { + + var ( owner, dst, resp_dst, query_id ) = setup_req_fields(); + + slice non_owner = gen_non_owner( owner ); + var msg_body = generate_jetton_burn_request( query_id, one_unit * 5, resp_dst, null() ); ;;Burn 5 jetton units + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, non_owner, null(), 0 ); + int msg_value = one_unit; + + ;; Should fail with no actions + int gas_err = invoke_method_expect_fail( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] ); + assert_no_actions(); + + ;; changing msg source addres to owner to verify error trigger + msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 ); + + var ( gas_success, _ ) = invoke_method( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] ); + + return gas_err + gas_success; +} + +int __test_burn_too_many() { + ;; Testing case where user burns more jettons than available on balance + var ( owner, dst, resp_dst, query_id ) = setup_req_fields(); + + int balance = get_test_balance(); + var msg_body = generate_jetton_burn_request( query_id, balance + 1, resp_dst, null() ); ;;Burn balance + 1 jettons + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 ); + + ;; Should fail with no actions + int gas_err = invoke_method_expect_fail( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] ); + assert_no_actions(); + + ;; Now transfer exactly balance to verify error trigger + msg_body = generate_jetton_burn_request( query_id, balance, resp_dst, null() ); + + var ( gas_success, _ ) = invoke_method( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] ); + + return gas_err + gas_success; +} + +int __test_burn_no_gas() { + int burn_fee = jetton_gas_fee * 2; + int forward_fee = one_unit / 10; + int excess = 1; + int msg_value = muldiv(forward_fee, 3, 2) + excess; ;;Only forward fee and access no gas fee + int gas_used = burn_test_msg_value( msg_value, forward_fee, true ); + assert_no_actions(); + ;; verifying error trigger + gas_used += burn_test_msg_value( msg_value + burn_fee, forward_fee, false ); + + return gas_used; + +} + +int __test_burn_no_forward_fee() { + int burn_fee = jetton_gas_fee * 2; + int forward_fee = one_unit / 10; + int excess = 1; + int msg_value = burn_fee + excess; ;;Only burn_fee and excess no forward_fee + int gas_used = burn_test_msg_value( msg_value, forward_fee, true ); + assert_no_actions(); + ;; verifying error trigger + gas_used += burn_test_msg_value( msg_value + muldiv(forward_fee, 3, 2), forward_fee, false ); + + return gas_used; +} + +int __test_burn_no_excess() { + int burn_fee = jetton_gas_fee * 2; + int forward_fee = one_unit / 10; + int excess = 1; + int msg_value = burn_fee + muldiv(forward_fee, 3, 2); ;;Only burn_fee + forward_fee nothing to return back + int gas_used = burn_test_msg_value( msg_value, forward_fee, true ); + assert_no_actions(); + ;; verifying error trigger + gas_used += burn_test_msg_value( msg_value + excess, forward_fee, false ); + + return gas_used; +} + +int __test_burn_successfull() { + var ( owner, dst, resp_dst, query_id ) = setup_req_fields(); + + int balance = get_test_balance(); + int msg_value = one_unit; + int forward_fee = one_unit / 10; + int forward_amount = one_unit / 4; + int burn_amount = ( rand( 10 ) + 1 ) * balance / 10; + + var msg_body = generate_jetton_burn_request( query_id, burn_amount, resp_dst, null() ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 ); + var ( gas_used, _ ) = invoke_method( recv_internal, [ msg_value, msg_value, msg, msg_body.end_cell().begin_parse() ] ); + + ;; Balance should decrease by burn amount + throw_unless( 502, balance - burn_amount == get_test_balance() ); + + ;; We expect excess message to be sent to resp_dst + tuple actions = parse_c5(); + + throw_unless( 505, actions.tuple_length() == 1 ); + + ( int action_type, cell burn_note, int mode ) = actions.first().untriple(); + throw_unless( 501, action_type == 0 ); + + verify_burn_notification( burn_note, query_id, burn_amount, owner, resp_dst ); + + return gas_used; + +} + +int __test_internal_transfer() { + + var ( owner, dst, resp_dst, query_id ) = setup_req_fields(); + + slice from = get_master(); + int balance = get_test_balance(); + int forward_ton = one_unit / 10; + int msg_value = one_unit; + int transfer_amount = ( rand( 10 ) + 1 ) * one_unit / 10; ;;From 0.1 to 1 unit + + + var msg_body = generate_jetton_internal_transfer_request( query_id, transfer_amount, from, dst, forward_ton, null(), false ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, from, null(), 0 ); + var ( gas_used, _ ) = invoke_method( recv_internal, [ msg_value, msg_value, msg, msg_body.end_cell().begin_parse() ] ); + + throw_unless( 500, balance + transfer_amount == get_test_balance() ); + + return gas_used; +} + +;; Get methods testing starts here + +int __test_get_wallet_data() { + var ( balance, owner, master, code ) = load_test_data(); + var ( gas_used, stack ) = invoke_method( get_wallet_data, [] ); + + throw_unless( 300, stack.tuple_length() == 4 ); + throw_unless( 301, balance == stack.first() ); + throw_unless( 302, equal_slices( owner, stack.second() ) ); + throw_unless( 303, equal_slices( master, stack.third() ) ); + throw_unless( 304, equal_slices( code.begin_parse(), stack.fourth().begin_parse() ) ); + + return gas_used; +} From 1c789c33682a568cbbc4be4aba76ace0560d11f9 Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Wed, 7 Dec 2022 23:34:36 +0300 Subject: [PATCH 2/2] Nft toncli tests added --- nft_tests/.gitignore | 2 + nft_tests/README.md | 55 ++ nft_tests/fift/.gitkeep | 0 nft_tests/fift/collection-data.fif | 68 +++ nft_tests/fift/deploy.fif | 27 + nft_tests/fift/nft-data.fif | 39 ++ nft_tests/fift/parse-data-nft-collection.fif | 60 ++ nft_tests/fift/parse-data-nft-single.fif | 22 + nft_tests/func/utils/helpers.func | 222 +++++++ nft_tests/func/utils/op-codes.func | 12 + nft_tests/func/utils/params.func | 6 + nft_tests/project.yaml | 21 + nft_tests/tests/.gitkeep | 0 nft_tests/tests/collection-tests-int.func | 94 +++ nft_tests/tests/collection-tests.func | 273 +++++++++ nft_tests/tests/nft-tests-int.func | 24 + nft_tests/tests/nft-tests.func | 584 +++++++++++++++++++ nft_tests/tests/utils/collection-data.func | 38 ++ nft_tests/tests/utils/constants.func | 2 + 19 files changed, 1549 insertions(+) create mode 100644 nft_tests/.gitignore create mode 100644 nft_tests/README.md create mode 100644 nft_tests/fift/.gitkeep create mode 100644 nft_tests/fift/collection-data.fif create mode 100644 nft_tests/fift/deploy.fif create mode 100644 nft_tests/fift/nft-data.fif create mode 100644 nft_tests/fift/parse-data-nft-collection.fif create mode 100644 nft_tests/fift/parse-data-nft-single.fif create mode 100644 nft_tests/func/utils/helpers.func create mode 100644 nft_tests/func/utils/op-codes.func create mode 100644 nft_tests/func/utils/params.func create mode 100644 nft_tests/project.yaml create mode 100644 nft_tests/tests/.gitkeep create mode 100644 nft_tests/tests/collection-tests-int.func create mode 100644 nft_tests/tests/collection-tests.func create mode 100644 nft_tests/tests/nft-tests-int.func create mode 100644 nft_tests/tests/nft-tests.func create mode 100644 nft_tests/tests/utils/collection-data.func create mode 100644 nft_tests/tests/utils/constants.func diff --git a/nft_tests/.gitignore b/nft_tests/.gitignore new file mode 100644 index 0000000..2053e73 --- /dev/null +++ b/nft_tests/.gitignore @@ -0,0 +1,2 @@ +*.pk +build diff --git a/nft_tests/README.md b/nft_tests/README.md new file mode 100644 index 0000000..27efa70 --- /dev/null +++ b/nft_tests/README.md @@ -0,0 +1,55 @@ +# NFT Collection example project + +This project allows you to: + +1. Build basic nft collection contract +2. Aims to *hopefully* test any nft collection contract for compliance with [NFT Standard](https://github.com/ton-blockchain/TIPs/issues/62) +3. Deploy collection contract via `toncli deploy nft_collection` +4. Manually deploy NFT item to the collection look (Deploying individual items) + +## Building + + Just run `toncli build` + Depending on your fift/func build you may want + to uncomment some *func/helpers* + +## Testing + + Build project and then: `toncli run_test` + + ⚠ If you see `6` error code on all tests - you need to update your binary [more information here](https://github.com/disintar/toncli/issues/72) + +## Deploying collection contract + + This project consists of two subprojects **nft_item** and **nft_collection** + You can see that in the *project.yml* + **BOTH** of those have to be built. + However, it makes sense to deploy only *nft_collection*. + Prior to deployment you need to check out *fift/collection-data.fif* + and change all mock configuration values like collection_content, + owner_address Etc. + To deploy run:`toncli deploy -n testnet nft_collection`. + +## Deploying individual items + + To deploy your own NFT item to the already deployed collection + you will need: + ++ Configure *fift/deploy.fif* script with your own values: +[Take a look](https://github.com/ton-blockchain/TIPs/issues/64) + ++ Make yourself familiar with process of sending [internal messages](https://github.com/disintar/toncli/blob/master/docs/advanced/send_fift_internal.md) + +`toncli send -n testnet -a 0.05 -c nft_collection --body fift/deploy.fif` +Every next item deployment you should make sure to +change item index in the *fift/deploy.fif* file ( Yes. Manually for now ). + +## Parse nft content + +Parse nft for collection (will work only if collection-data is same with on-chain): + +`toncli get get_nft_data -a "NFT_ADDRESS" --fift ./fift/parse-data-nft-collection.fif` + +Parse nft for single: + +`toncli get get_nft_data -a "NFT_ADDRESS" --fift ./fift/parse-data-nft-single.fif` diff --git a/nft_tests/fift/.gitkeep b/nft_tests/fift/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/nft_tests/fift/collection-data.fif b/nft_tests/fift/collection-data.fif new file mode 100644 index 0000000..8037470 --- /dev/null +++ b/nft_tests/fift/collection-data.fif @@ -0,0 +1,68 @@ +"TonUtil.fif" include +"Color.fif" include +"Asm.fif" include + +// toncli creates file in fift-libs +// where store deploy wallet address +// you can use this address to control nft or nft collection +"OwnerAddr.fif" include + +// "EQDlT07NpSh0uj-aSBkF2TRxOqR2nw0ErOQsA6TYakr1-FxP" instead of owner_address <- you can use address in any form +// or use address of toncli deploy wallet: +owner_address constant collection_owner_address + +// Change to your own +"EQB8EqvPWlUq6g3pZPW9AQU4xLDDh65MxfFT_UStljYuCy_X" constant royalty_address + +// 5 / 100 = 5% :) +5 constant royalty_numerator +100 constant royalty_denominator + +// Specify your own collection base uri +"https://raw.githubusercontent.com/Trinketer22/token-contract/main/nft/web-example/" constant collection_base + +// Specify your own collection metadata uri +"https://raw.githubusercontent.com/Trinketer22/token-contract/main/nft/web-example/my_collection.json" constant collection_json + + +// Path to builded NFT smart contract +"build/nft.fif" constant nft_path + +// Parse and save addresses + +royalty_address $>smca 0= abort"Specify valid royalty addr" drop +2constant royalty_raw + +collection_owner_address $>smca 0= abort"Specify valid owner addr" drop +2constant owner_raw + +^reset ."ðŸ‘― Owner Address: " owner_address ^green type cr +^reset ."ðŸ‘ŧ Royalty Address: " royalty_address ^green type cr +^reset + +nft_path include constant nft_code + +// just little helper +{ B B, b> } : offchain-token-data + + +// This is final c4 of smart contract +// It will be loaded in nft-collection.func in 11 line + +B B, b> ref, + b> ref, // content cell + + nft_code ref, // nft code + + ref, // royalty cell +b> diff --git a/nft_tests/fift/deploy.fif b/nft_tests/fift/deploy.fif new file mode 100644 index 0000000..bdde92f --- /dev/null +++ b/nft_tests/fift/deploy.fif @@ -0,0 +1,27 @@ +"TonUtil.fif" include + +50000000 =: storage_grams + +// "EQDlT07NpSh0uj-aSBkF2TRxOqR2nw0ErOQsA6TYakr1-FxP" +"TotallyNotValid" constant nft_owner_address + +."Deploying with nft owner address:" nft_owner_address type cr +nft_owner_address $>smca 0= abort"Specify valid owner address" +drop // Dropping address flags + + ref, // nft content +b> constant nft_payload + + + storage_grams Gram, // Storing forward_amount for nft deployment + // Initialization message body consists of owner_address and ref to the nft_content cell + nft_payload ref, // Storing content ref +b> + + + diff --git a/nft_tests/fift/nft-data.fif b/nft_tests/fift/nft-data.fif new file mode 100644 index 0000000..1280606 --- /dev/null +++ b/nft_tests/fift/nft-data.fif @@ -0,0 +1,39 @@ +"TonUtil.fif" include +"Asm.fif" include + +"kQDzy14OkWffhTvu289ftEY0xFZCqNU1cUbpiv8rZvQviL5F" constant collection_address // Specify your own +"EQDlT07NpSh0uj-aSBkF2TRxOqR2nw0ErOQsA6TYakr1-FxP" constant owner_address // Specify your own +"my_nft.json" constant nft_json // Your nft metadata relative path to collection_base + + + +collection_address +$>smca 0= abort"Specify valid collection address" +drop + +2constant coll_raw + +."Collection address:" collection_address type cr + +owner_address +$>smca 0= abort"Specify valid owner address" +drop + +2constant owner_raw + +."Owner address:" owner_address type cr + + + +B B, +b> + + diff --git a/nft_tests/fift/parse-data-nft-collection.fif b/nft_tests/fift/parse-data-nft-collection.fif new file mode 100644 index 0000000..db804a5 --- /dev/null +++ b/nft_tests/fift/parse-data-nft-collection.fif @@ -0,0 +1,60 @@ +"TonUtil.fif" include +"Color.fif" include + +// In stack: init?, index, collection address, owner_address, body +4 roll // In stack: index, collection address, owner_address, body, init? +^reset ."👋 NFT is inited: " ^magenta (dump) type cr + +3 roll // In stack: collection address, owner_address, body, index +constant index +^reset ."ðŸ‘ŋ NFT index: " ^green index (dump) type cr + + +^reset 2 roll addr@ ."ðŸĪ— Collection address: " ^yellow print-addr cr // In stack: owner_address, body, collection address SLICE +constant nft-body +^reset ."ðŸĪŊ NFT Body: " nft-body $ (dump) type cr + + +// Parse data with collection + +"../build/nft_collection.fif" include constant collection-code +"./collection-data.fif" include constant collection-data + +0x076ef1ea // magic +0 // actions +0 // msgs_sent +0 // unixtime +1 // block_lt +0 // trans_lt +239 // randseed +1000000000 null pair // balance_remaining + $ ^cyan (dump) type cr + +.s +^reset ."😏 NFT individual content: " ref@ $ ^green (dump) type cr + +^reset ."ðŸū NFT data: " $ ^cyan (dump) type cr diff --git a/nft_tests/fift/parse-data-nft-single.fif b/nft_tests/fift/parse-data-nft-single.fif new file mode 100644 index 0000000..cf0bbba --- /dev/null +++ b/nft_tests/fift/parse-data-nft-single.fif @@ -0,0 +1,22 @@ +"TonUtil.fif" include +"Color.fif" include + +// In stack: init?, index, collection address, owner_address, body +4 roll // In stack: index, collection address, owner_address, body, init? +^reset ."👋 NFT is inited: " ^magenta (dump) type cr + +3 roll // In stack: collection address, owner_address, body, index +constant index +^reset ."ðŸ‘ŋ NFT index: " ^green index (dump) type cr + + +^reset 2 roll addr@ ."ðŸĪ— Collection address: " ^yellow print-addr cr // In stack: owner_address, body, collection address SLICE +constant nft-body +^reset ."ðŸĪŊ NFT Body: " nft-body $ (dump) type cr + diff --git a/nft_tests/func/utils/helpers.func b/nft_tests/func/utils/helpers.func new file mode 100644 index 0000000..2dd37bc --- /dev/null +++ b/nft_tests/func/utils/helpers.func @@ -0,0 +1,222 @@ +;; Standard library for funC +;; + +;;forall X -> tuple cons(X head, tuple tail) asm "CONS"; +;;forall X -> (X, tuple) uncons(tuple list) asm "UNCONS"; +;;forall X -> (tuple, X) list_next(tuple list) asm( -> 1 0) "UNCONS"; +;;forall X -> X car(tuple list) asm "CAR"; +;;tuple cdr(tuple list) asm "CDR"; +;;tuple empty_tuple() asm "NIL"; +;;forall X -> tuple tpush(tuple t, X value) asm "TPUSH"; +;;forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH"; +;;forall X -> [X] single(X x) asm "SINGLE"; +;;forall X -> X unsingle([X] t) asm "UNSINGLE"; +;;forall X, Y -> [X, Y] pair(X x, Y y) asm "PAIR"; +;;forall X, Y -> (X, Y) unpair([X, Y] t) asm "UNPAIR"; +;;forall X, Y, Z -> [X, Y, Z] triple(X x, Y y, Z z) asm "TRIPLE"; +;;forall X, Y, Z -> (X, Y, Z) untriple([X, Y, Z] t) asm "UNTRIPLE"; +;;forall X, Y, Z, W -> [X, Y, Z, W] tuple4(X x, Y y, Z z, W w) asm "4 TUPLE"; +;;forall X, Y, Z, W -> (X, Y, Z, W) untuple4([X, Y, Z, W] t) asm "4 UNTUPLE"; +;;forall X -> X first(tuple t) asm "FIRST"; +;;forall X -> X second(tuple t) asm "SECOND"; +;;forall X -> X third(tuple t) asm "THIRD"; +;;forall X -> X fourth(tuple t) asm "3 INDEX"; +;;forall X, Y -> X pair_first([X, Y] p) asm "FIRST"; +;;forall X, Y -> Y pair_second([X, Y] p) asm "SECOND"; +;;forall X, Y, Z -> X triple_first([X, Y, Z] p) asm "FIRST"; +;;forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND"; +;;forall X, Y, Z -> Z triple_third([X, Y, Z] p) asm "THIRD"; +;;forall X -> X null() asm "PUSHNULL"; +;;forall X -> (X, ()) ~impure_touch(X x) impure asm "NOP"; +;; +;;int now() asm "NOW"; +;;slice my_address() asm "MYADDR"; +;;[int, cell] get_balance() asm "BALANCE"; +;;int cur_lt() asm "LTIME"; +;;int block_lt() asm "BLOCKLT"; +;; +;;int cell_hash(cell c) asm "HASHCU"; +;;int slice_hash(slice s) asm "HASHSU"; +;;int string_hash(slice s) asm "SHA256U"; +;; +;;int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU"; +;;int check_data_signature(slice data, slice signature, int public_key) asm "CHKSIGNS"; +;; +;;(int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE"; +;;(int, int, int) slice_compute_data_size(slice s, int max_cells) impure asm "SDATASIZE"; +;;(int, int, int, int) compute_data_size?(cell c, int max_cells) asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; +;;(int, int, int, int) slice_compute_data_size?(cell c, int max_cells) asm "SDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; +;; +;;;; () throw_if(int excno, int cond) impure asm "THROWARGIF"; +;; +;;() dump_stack() impure asm "DUMPSTK"; +;; +;;cell get_data() asm "c4 PUSH"; +;;() set_data(cell c) impure asm "c4 POP"; +;;cont get_c3() impure asm "c3 PUSH"; +;;() set_c3(cont c) impure asm "c3 POP"; +;;cont bless(slice s) impure asm "BLESS"; +;; +;;() accept_message() impure asm "ACCEPT"; +;;() set_gas_limit(int limit) impure asm "SETGASLIMIT"; +;;() commit() impure asm "COMMIT"; +;;() buy_gas(int gram) impure asm "BUYGAS"; +;; +;;int min(int x, int y) asm "MIN"; +;;int max(int x, int y) asm "MAX"; +;;(int, int) minmax(int x, int y) asm "MINMAX"; +;;int abs(int x) asm "ABS"; +;; +;;slice begin_parse(cell c) asm "CTOS"; +;;() end_parse(slice s) impure asm "ENDS"; +;;(slice, cell) load_ref(slice s) asm( -> 1 0) "LDREF"; +;;cell preload_ref(slice s) asm "PLDREF"; +;;;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX"; +;;;; (slice, int) ~load_uint(slice s, int len) asm( -> 1 0) "LDUX"; +;;;; int preload_int(slice s, int len) asm "PLDIX"; +;;;; int preload_uint(slice s, int len) asm "PLDUX"; +;;;; (slice, slice) load_bits(slice s, int len) asm(s len -> 1 0) "LDSLICEX"; +;;;; slice preload_bits(slice s, int len) asm "PLDSLICEX"; +;;(slice, int) load_grams(slice s) asm( -> 1 0) "LDGRAMS"; +;;slice skip_bits(slice s, int len) asm "SDSKIPFIRST"; +;;(slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST"; +;;slice first_bits(slice s, int len) asm "SDCUTFIRST"; +;;slice skip_last_bits(slice s, int len) asm "SDSKIPLAST"; +;;(slice, ()) ~skip_last_bits(slice s, int len) asm "SDSKIPLAST"; +;;slice slice_last(slice s, int len) asm "SDCUTLAST"; +;;(slice, cell) load_dict(slice s) asm( -> 1 0) "LDDICT"; +;;cell preload_dict(slice s) asm "PLDDICT"; +;;slice skip_dict(slice s) asm "SKIPDICT"; +;; +;;(slice, cell) load_maybe_ref(slice s) asm( -> 1 0) "LDOPTREF"; +;;cell preload_maybe_ref(slice s) asm "PLDOPTREF"; +;;builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF"; +;; +;;int cell_depth(cell c) asm "CDEPTH"; +;; +;;int slice_refs(slice s) asm "SREFS"; +;;int slice_bits(slice s) asm "SBITS"; +;;(int, int) slice_bits_refs(slice s) asm "SBITREFS"; +;;int slice_empty?(slice s) asm "SEMPTY"; +;;int slice_data_empty?(slice s) asm "SDEMPTY"; +;;int slice_refs_empty?(slice s) asm "SREMPTY"; +;;int slice_depth(slice s) asm "SDEPTH"; +;; +;;int builder_refs(builder b) asm "BREFS"; +;;int builder_bits(builder b) asm "BBITS"; +;;int builder_depth(builder b) asm "BDEPTH"; +;; +;;builder begin_cell() asm "NEWC"; +;;cell end_cell(builder b) asm "ENDC"; +;;builder store_ref(builder b, cell c) asm(c b) "STREF"; +;;;; builder store_uint(builder b, int x, int len) asm(x b len) "STUX"; +;;;; builder store_int(builder b, int x, int len) asm(x b len) "STIX"; +;;builder store_slice(builder b, slice s) asm "STSLICER"; +;;builder store_grams(builder b, int x) asm "STGRAMS"; +;;builder store_dict(builder b, cell c) asm(c b) "STDICT"; +;; +;;(slice, slice) load_msg_addr(slice s) asm( -> 1 0) "LDMSGADDR"; +;;tuple parse_addr(slice s) asm "PARSEMSGADDR"; +;;(int, int) parse_std_addr(slice s) asm "REWRITESTDADDR"; +;;(int, slice) parse_var_addr(slice s) asm "REWRITEVARADDR"; +;; +;;cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; +;;(cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; +;;cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; +;;(cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; +;;cell idict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETOPTREF"; +;;(cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF"; +;;(cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF"; +;;(cell, cell) idict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETGETOPTREF"; +;;(cell, cell) udict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETGETOPTREF"; +;;(cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL"; +;;(cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL"; +;;(slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT"; +;;(slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT"; +;;(cell, slice, int) idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; +;;(cell, slice, int) udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; +;;(cell, (slice, int)) ~idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; +;;(cell, (slice, int)) ~udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; +;;cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; +;;(cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; +;;cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; +;;(cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; +;;cell dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; +;;(cell, ()) ~dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; +;;(cell, int) udict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUADD"; +;;(cell, int) udict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUREPLACE"; +;;(cell, int) idict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIADD"; +;;(cell, int) idict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIREPLACE"; +;;cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; +;;(cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; +;;cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; +;;(cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; +;;cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; +;;(cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; +;;(cell, int) udict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUADDB"; +;;(cell, int) udict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUREPLACEB"; +;;(cell, int) idict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIADDB"; +;;(cell, int) idict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIREPLACEB"; +;;(cell, int, slice, int) udict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; +;;(cell, (int, slice, int)) ~udict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; +;;(cell, int, slice, int) idict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; +;;(cell, (int, slice, int)) ~idict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; +;;(cell, slice, slice, int) dict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; +;;(cell, (slice, slice, int)) ~dict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; +;;(cell, int, slice, int) udict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; +;;(cell, (int, slice, int)) ~udict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; +;;(cell, int, slice, int) idict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; +;;(cell, (int, slice, int)) ~idict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; +;;(cell, slice, slice, int) dict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; +;;(cell, (slice, slice, int)) ~dict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; +;;(int, slice, int) udict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMIN" "NULLSWAPIFNOT2"; +;;(int, slice, int) udict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAX" "NULLSWAPIFNOT2"; +;;(int, cell, int) udict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMINREF" "NULLSWAPIFNOT2"; +;;(int, cell, int) udict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAXREF" "NULLSWAPIFNOT2"; +;;(int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2"; +;;(int, slice, int) idict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAX" "NULLSWAPIFNOT2"; +;;(int, cell, int) idict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMINREF" "NULLSWAPIFNOT2"; +;;(int, cell, int) idict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAXREF" "NULLSWAPIFNOT2"; +;;(int, slice, int) udict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXT" "NULLSWAPIFNOT2"; +;;(int, slice, int) udict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXTEQ" "NULLSWAPIFNOT2"; +;;(int, slice, int) udict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREV" "NULLSWAPIFNOT2"; +;;(int, slice, int) udict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREVEQ" "NULLSWAPIFNOT2"; +;;(int, slice, int) idict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXT" "NULLSWAPIFNOT2"; +;;(int, slice, int) idict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXTEQ" "NULLSWAPIFNOT2"; +;;(int, slice, int) idict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREV" "NULLSWAPIFNOT2"; +;;(int, slice, int) idict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREVEQ" "NULLSWAPIFNOT2"; +;;cell new_dict() asm "NEWDICT"; +;;int dict_empty?(cell c) asm "DICTEMPTY"; +;; +;;(slice, slice, slice, int) pfxdict_get?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTGETQ" "NULLSWAPIFNOT2"; +;;(cell, int) pfxdict_set?(cell dict, int key_len, slice key, slice value) asm(value key dict key_len) "PFXDICTSET"; +;;(cell, int) pfxdict_delete?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTDEL"; + +;;cell config_param(int x) asm "CONFIGOPTPARAM"; +;;int cell_null?(cell c) asm "ISNULL"; +;; +;;() raw_reserve(int amount, int mode) impure asm "RAWRESERVE"; +;;() raw_reserve_extra(int amount, cell extra_amount, int mode) impure asm "RAWRESERVEX"; +;;() send_raw_message(cell msg, int mode) impure asm "SENDRAWMSG"; +;;() set_code(cell new_code) impure asm "SETCODE"; +;; +;;int random() impure asm "RANDU256"; +;;int rand(int range) impure asm "RAND"; +;;int get_seed() impure asm "RANDSEED"; +;;int set_seed() impure asm "SETRAND"; +;;() randomize(int x) impure asm "ADDRAND"; +;;() randomize_lt() impure asm "LTIME" "ADDRAND"; +;; +;;builder store_coins(builder b, int x) asm "STVARUINT16"; +;;(slice, int) load_coins(slice s) asm( -> 1 0) "LDVARUINT16"; + +int equal_slices (slice a, slice b) asm "SDEQ"; +int builder_null?(builder b) asm "ISNULL"; +int tuple_length( tuple t ) asm "TLEN"; +forall X -> int is_null(X x) asm "ISNULL"; +forall X -> int is_int(X x) asm "<{ TRY:<{ 0 PUSHINT ADD DROP -1 PUSHINT }>CATCH<{ 2DROP 0 PUSHINT }> }>CONT 1 1 CALLXARGS"; +forall X -> int is_cell(X x) asm "<{ TRY:<{ CTOS DROP -1 PUSHINT }>CATCH<{ 2DROP 0 PUSHINT }> }>CONT 1 1 CALLXARGS"; +forall X -> int is_slice(X x) asm "<{ TRY:<{ SBITS DROP -1 PUSHINT }>CATCH<{ 2DROP 0 PUSHINT }> }>CONT 1 1 CALLXARGS"; +forall X -> int is_tuple(X x) asm "ISTUPLE"; +;;builder store_builder(builder to, builder from) asm "STBR"; + diff --git a/nft_tests/func/utils/op-codes.func b/nft_tests/func/utils/op-codes.func new file mode 100644 index 0000000..b3f8be8 --- /dev/null +++ b/nft_tests/func/utils/op-codes.func @@ -0,0 +1,12 @@ +const int op_transfer = 0x5fcc3d14; +const int op_ownership_assigned = 0x05138d91; +const int op_excesses = 0xd53276db; +const int op_get_static_data = 0x2fcb26a2; +const int op_report_static_data = 0x8b771735; +const int op_get_royalty_params = 0x693d3950; +const int op_report_royalty_params = 0xa8cb00ad; + +;; NFTEditable +const int op_edit_content = 0x1a0b9d51; +const int op_transfer_editorship = 0x1c04412a; +const int op_editorship_assigned = 0x511a4463; diff --git a/nft_tests/func/utils/params.func b/nft_tests/func/utils/params.func new file mode 100644 index 0000000..eaaaa27 --- /dev/null +++ b/nft_tests/func/utils/params.func @@ -0,0 +1,6 @@ +const int workchain = 0; + +() force_chain(slice addr) impure { + (int wc, _) = parse_std_addr(addr); + throw_unless(333, wc == workchain); +} diff --git a/nft_tests/project.yaml b/nft_tests/project.yaml new file mode 100644 index 0000000..16f8d1f --- /dev/null +++ b/nft_tests/project.yaml @@ -0,0 +1,21 @@ +nft_collection: + data: fift/collection-data.fif + func: + - func/utils/helpers.func + - ../nft//params.fc + - ../nft//op-codes.fc + - ../nft//nft-collection.fc + tests: + - tests/collection-tests.func + - tests/collection-tests-int.func + +nft: + data: fift/nft-data.fif + func: + - func/utils/helpers.func + - ../nft/params.fc + - ../nft/op-codes.fc + - ../nft/nft-item.fc + tests: + - tests/nft-tests.func + - tests/nft-tests-int.func diff --git a/nft_tests/tests/.gitkeep b/nft_tests/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/nft_tests/tests/collection-tests-int.func b/nft_tests/tests/collection-tests-int.func new file mode 100644 index 0000000..c6727c7 --- /dev/null +++ b/nft_tests/tests/collection-tests-int.func @@ -0,0 +1,94 @@ +#pragma version >=0.2.0; + +#include "utils/collection-data.func"; +#include "utils/constants.func"; + + +int __test_get_collection_data_deploy() { + var ( gas_before, stack ) = invoke_method( get_collection_data, [] ); + + int idx_next = stack.first(); + int forward_amount = one_ton / 10; + int query_id = rand( 1337 ) + 1; + cell coll_content = stack.second(); + slice owner = stack.third(); + + cell nft_content = begin_cell().store_slice("my_nft.json").end_cell(); + cell nft_init = begin_cell().store_slice(owner).store_ref(nft_content).end_cell(); + + builder msg_body = generate_nft_deploy_request( idx_next, nft_init, query_id, forward_amount); + cell msg = generate_internal_message_custom(0, 0, 0, msg_body, owner, null(), 0); + + var ( gas_deploy, _ ) = invoke_method(recv_internal, [one_ton, one_ton, msg, msg_body.end_cell().begin_parse()]); + var ( gas_after, stack ) = invoke_method( get_collection_data, [] ); + ;;Index should increase by one + throw_unless( 500, stack.first() == idx_next + 1 ); + + ;; Nothing should change here + throw_unless( 501, equal_slices( coll_content.begin_parse(), stack.second().begin_parse() ) ); + throw_unless( 502, equal_slices( owner, stack.third() ) ); + + return gas_before + gas_after + gas_deploy; +} + +int __test_get_collection_data_change_owner() { + + var ( gas_before, stack ) = invoke_method( get_collection_data, [] ); + + int query_id = rand( 1337 ) + 1; + int change_owner = 3; ;;WARNING contract dependent value. + slice owner = stack.third(); + slice new_owner = generate_internal_address_with_custom_data(0, 0, random()); + builder msg_body = generate_internal_message_body( change_owner, query_id ).store_slice( new_owner ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 ); + + var ( gas_change, _ ) = invoke_method( recv_internal, [one_ton, one_ton, msg, msg_body.end_cell().begin_parse()] ); + var ( gas_after, stack ) = invoke_method( get_collection_data, [] ); + + throw_unless( 500, equal_slices( new_owner, stack.third() ) ); + + return gas_before + gas_change + gas_after; +} + +int __test_get_nft_address_by_index() { + + slice owner = get_owner(); + + ;; To test that nft address is correct + ;; we have to deploy one first and capture it's address + ;; Index:0 + + cell nft_content = begin_cell().store_slice("my_nft.json").end_cell(); + cell nft_init = begin_cell().store_slice(owner).store_ref(nft_content).end_cell(); + + int query_id = rand(1337) + 1; + int forward_amount = one_ton / 10; + int deploy_idx = 0; + + builder msg_body = generate_nft_deploy_request(deploy_idx, nft_init, query_id, forward_amount); + cell msg = generate_internal_message_custom(0, 0, 0, msg_body, owner, null(), 0); + + (int gas_used, _) = invoke_method(recv_internal, [one_ton, 0, msg, msg_body.end_cell().begin_parse()]); + ;; Should successfully deploy and produce singe deployment message + + tuple actions = parse_c5(); + throw_unless(500, actions.tuple_length() == 1); + + (int action_type, cell body, int mode) = actions.at(0).untriple(); + throw_unless(501, action_type == 0); + + tuple parsed_msg = unsafe_tuple(parse_internal_message(body)); + + ;;Picking nft address from deployment message + slice nft_address = parsed_msg.at(4); + + var (gas_used2, stack) = invoke_method(get_nft_address_by_index, [deploy_idx]); + + throw_unless(502, stack.tuple_length() == 1); + + throw_unless(503, equal_slices(nft_address, stack.first())); + + + return gas_used; +} + diff --git a/nft_tests/tests/collection-tests.func b/nft_tests/tests/collection-tests.func new file mode 100644 index 0000000..c947272 --- /dev/null +++ b/nft_tests/tests/collection-tests.func @@ -0,0 +1,273 @@ +#pragma version >=0.2.0; + +#include "utils/collection-data.func"; +#include "utils/constants.func"; +#include "../func/utils/op-codes.func"; + + +;; Pretty unpleasent tuple to work with. +;; Flags are mixed up with data that is not always inserted + +cell get_state_init_field(tuple state_init, int idx) inline { + cell res = null(); + ;; Next flag index + int next_idx = 0; + + do { + int flag = state_init.at(next_idx); + + next_idx = flag ? next_idx + 2 : next_idx + 1; + idx -= 1; + + if(idx == 0 & flag) { + res = state_init.at(next_idx - 1); + } + + } until (~ res.null?() | idx <= 0) + + return res; +} + +_ validate_TIP_64(slice content_data) impure inline { + + int content_layout = content_data~load_uint(8); + + ;; Check for allowed content_layout + throw_unless(305, (content_layout == 1) | (content_layout == 0)); + + if(content_layout == 1) { + + ;; Check that off-chain URI contains at least one ASCII char + throw_unless(306, token_snake_len(content_data) > 8); + } else { + ;; On-chain is stored as dict + ;; Has to be non-empty + throw_if(306, content_data.preload_dict().dict_empty?()); + + ;; Perhaps could go further and test for Optional dict keys but none of those are required so i'll leave it be + ;; For now + } +} + +int __test_change_owner() { + + {- + This method is not defined in TIP-62. + Still this is de-facto standard. + One could argue that this one should + be moved elsewhere. + -} + + var (owner_address, prev_item_index, content, nft_item_code, royalty_params) = load_test_data(); ;; this data will load from c4 + + int change_owner = 3; ;;Change owner op could be different + int query_id = rand(1337) + 1; + slice rand_addr = generate_internal_address_with_custom_data(0, 0, random()); + ;;Setting this random address as new collection owner. + builder msg_body = generate_internal_message_body( change_owner, query_id ).store_slice( rand_addr ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, rand_addr, null(), 0 ); + + ;; Should fail from non-owner address + int gas_failed = invoke_method_expect_fail( recv_internal, [one_ton, one_ton, msg, msg_body.end_cell().begin_parse()] ); + + ;;With correct owner it should succeed + msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner_address, null(), 0 ); + var ( gas_success, _ ) = invoke_method( recv_internal, [one_ton, 0, msg, msg_body.end_cell().begin_parse()] ); + + throw_unless( 500, equal_slices( rand_addr, get_owner() ) ); + + + return gas_success + gas_failed; +} + +int __test_deploy_item() { + {- + Collection deployment behaviour is not really defined in TIP-62 + Besides get methods. + Still have to test that it at least barely works right? + Will test: + + 1) Will test just that it won't allow deployment from non-owner + 2) Successfull deployment increases index and sends apropriate msgs + -} + + var (owner_address, prev_item_index, content, nft_item_code, royalty_params) = load_test_data(); ;; this data will load from c4 + + slice rand_addr = generate_internal_address_with_custom_data(0, 0, random()); + + {- + This is not the actual content format required for successfull deployment. + Contrant will accept it and i think it's something to fix + cell nft_content = begin_cell().store_uint( 1, 8 ).store_slice("my_nft.json").end_cell(); + -} + + cell nft_content = begin_cell().store_slice("my_nft.json").end_cell(); + ;; That's what actually should be sent for successfull deployment + cell nft_init = begin_cell().store_slice(owner_address).store_ref(nft_content).end_cell(); + int query_id = rand(1337) + 1; + int forward_amount = one_ton / 10; + builder msg_body = generate_nft_deploy_request(0, nft_init, query_id, forward_amount); + cell msg = generate_internal_message_custom(0, 0, 0, msg_body, rand_addr, null(), 0); + ;; Should not allow deploy from non-owner address + int gas_used = invoke_method_expect_fail(recv_internal, [one_ton, 0, msg, msg_body.end_cell().begin_parse()]); + assert_no_actions(); + ;; Verify that non-owner address was error trigger + ;; Expect success + + cell msg = generate_internal_message_custom(0, 0, 0, msg_body, owner_address, null(), 0); + (int gas_success, _) = invoke_method(recv_internal, [one_ton, 0, msg, msg_body.end_cell().begin_parse()]); + + tuple actions = parse_c5(); + + throw_unless(500, actions.tuple_length() == 1); + + (int action_type, cell body, int mode) = actions.at(0).untriple(); + + throw_unless(501, action_type == 0); + + tuple parsed_msg = unsafe_tuple(parse_internal_message(body)); + + throw_unless(502, forward_amount == parsed_msg.at(5)); + + tuple state_init = parsed_msg.at(7); + + ;; Deployment message has to have state_init + int state_len = state_init.tuple_length(); + + throw_unless(503, state_len > 0); + + ;; Fourth flag is data segment of state_init + + cell init_data = state_init.get_state_init_field(4); + + ;; Data segment has to be present in ntf StateInit + + throw_if(504, init_data.null?()); + + ;; Content in message body should be equal to what we sent + + throw_unless(505, equal_slices(nft_init.begin_parse(), parsed_msg.at(8))); + + ;; Nft index has to increase + + throw_unless(506, get_nft_index() > prev_item_index); + + return gas_used + gas_success; +} + +int __test_royalty_msg () { + + ( int numirator, int denominator, slice dst ) = get_test_royalty(); ;;Could be load_test_royalty + + int query_id = rand( 1337 ) + 1; + slice src_addr = generate_internal_address_with_custom_data( 0, 0, random() ); + builder msg_body = generate_get_royalty_params( query_id ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, src_addr, null(), 0 ); + ( int gas_used, _ ) = invoke_method( recv_internal, [ one_ton, one_ton, msg, msg_body.end_cell().begin_parse() ] ); + tuple actions = parse_c5(); + + throw_unless( 500, actions.tuple_length() == 1 ); + + ( int action_type, cell body, int mode ) = actions.first().untriple(); + throw_unless( 501, action_type == 0 ); + throw_unless( 502, mode == 64 ); + + + tuple parsed_msg = unsafe_tuple( parse_internal_message(body) ); + + ;;Message is sent back + throw_unless( 503, equal_slices( src_addr, parsed_msg.at( 4 ) ) ); + + slice msg_body = parsed_msg.at( 8 ); + throw_unless( 504, op_report_royalty_params == msg_body~load_uint( 32 ) ); + throw_unless( 505, query_id == msg_body~load_uint( 64 ) ); + throw_unless( 506, numirator == msg_body~load_uint( 16 ) ); + throw_unless( 507, denominator == msg_body~load_uint( 16 ) ); + throw_unless( 508, equal_slices( dst, msg_body~load_msg_addr() ) ); + + + return gas_used; +} + +;; Get methods test cases + +int __test_get_collection_data() { + + var (owner_address, next_item_index, content, nft_item_code, royalty_params) = load_test_data(); + + var (gas_used, stack) = invoke_method(get_collection_data, []); + ;; Should return 3 values + + throw_unless(600, stack.tuple_length() == 3); + + ;; Index, content and owner address should equal to c4 + + int res_idx = stack.first(); + throw_unless(601, next_item_index == res_idx); + + ;; First ref of a content is returned + ;; Collection content + + cell res_content = stack.second(); + slice cont_slice = content.begin_parse(); + cell coll_content = cont_slice~load_ref(); + + throw_unless(602, equal_slices(coll_content.begin_parse(), res_content.begin_parse())); + + slice res_owner = stack.third(); + throw_unless(603, equal_slices(owner_address, res_owner)); + + ;; Let's check that CollectionContent is compliant to TIP-64 + + validate_TIP_64(coll_content.begin_parse()); + + + return gas_used; +} + + +int __test_get_nft_content() { + + ;;Is there anything to be integrationally tested here? + ;;Probably not + + var (owner_address, next_item_index, content, nft_item_code, royalty_params) = load_test_data(); + slice coll_slice = content.begin_parse(); + cell nft_content = begin_cell().store_slice("my_nft.json").end_cell(); + + (_, cell collection_comm) = (coll_slice~load_ref(), coll_slice~load_ref()); + cell concat_content = snake_concat_tagged(1, collection_comm, nft_content); + + var (gas_used, stack) = invoke_method(get_nft_content, [0, nft_content]); + + cell res = stack.at(0); + slice res_slice = res.begin_parse(); + + throw_unless(601, snake_equal?(concat_content.begin_parse(), res_slice)); + + ;; Has to comply with TIP-64 + validate_TIP_64(res_slice); + + return gas_used; + +} + +{- + get_nft_address_by_index requires deployed nft item. + Thus it has been moved to integrational tests. +-} + +int __test_royalty_params() { + + var ( gas_used, stack ) = invoke_method( royalty_params, [] ); + + throw_unless( 700, stack.tuple_length() == 3 ); + throw_unless( 701, is_int( stack.first() ) ); + throw_unless( 702, is_int( stack.second() ) ); + throw_unless( 703, is_slice( stack.third() ) ); + + parse_std_addr( stack.third() ); + + return gas_used; +} + diff --git a/nft_tests/tests/nft-tests-int.func b/nft_tests/tests/nft-tests-int.func new file mode 100644 index 0000000..28684e0 --- /dev/null +++ b/nft_tests/tests/nft-tests-int.func @@ -0,0 +1,24 @@ +#pragma version >=0.2.0; + +#include "utils/constants.func"; + +int __test_get_nft_data_transfer() { + ;; (int init?, int index, slice collection_address, slice owner_address, cell individual_content) + + var ( _ , new_owner, resp_dst ) = setup_transfer_addresses( false ); + var ( gas_before, stack ) = invoke_method( get_nft_data, [] ); + + slice owner = stack.fourth(); + int query_id = rand( 1337 ) + 1; + builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, query_id, null(), 0, null(), 0); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 ); + + var ( gas_transfer, _ ) = invoke_method( recv_internal, [ one_ton, one_ton, msg, msg_body.end_cell().begin_parse() ] ); + var ( gas_after, stack ) = invoke_method( get_nft_data, [] ); + + throw_unless( 500, equal_slices( new_owner, stack.fourth() ) ); + + + return gas_before + gas_transfer + gas_after; +} + diff --git a/nft_tests/tests/nft-tests.func b/nft_tests/tests/nft-tests.func new file mode 100644 index 0000000..07ac943 --- /dev/null +++ b/nft_tests/tests/nft-tests.func @@ -0,0 +1,584 @@ +#pragma version >=0.2.0; + +#include "utils/constants.func"; +#include "../func/utils/op-codes.func"; + +slice get_owner_addr() inline { + slice ds = get_data().begin_parse().skip_bits( 64 ); + ds~load_msg_addr(); + return ds~load_msg_addr(); +} + +slice gen_new_owner( slice old_owner ) inline { + slice new_owner = generate_internal_address_with_custom_data( 0, 0, random() ); + + ;; In theory we can win a bingo and get same 256b integer from RNG + while( equal_slices( new_owner, old_owner ) ){ + new_owner = generate_internal_address(); + } + + return new_owner; +} + +( int, slice, slice, cell ) load_test_data() inline { + slice ds = get_data().begin_parse(); + int idx = ds~load_uint( 64 ); + slice coll_addr = ds~load_msg_addr(); + slice owner = ds~load_msg_addr(); + + ;;We need all addresses on Workchain 0 otherwise force_chain will throw 333 + + if (coll_addr.preload_uint(2) != 0){ ;; if not addr_none + coll_addr.force_chain(); + } + + if (owner.preload_uint(2) != 0){ + owner.force_chain(); + } + + return ( idx, coll_addr, owner, ds~load_ref() ); +} + +( slice, slice, slice ) setup_transfer_addresses( int has_dst? ) inline { + slice owner_addr = get_owner_addr(); + + ;;We need all addresses on Workchain 0 otherwise force_chain will throw 333 + + slice new_owner = gen_new_owner( owner_addr ); + + slice resp_dst = has_dst? ? generate_internal_address_with_custom_data( 0, 0, random() ) : generate_empty_address(); + + return ( owner_addr, new_owner, resp_dst ); + +} + +_ verify_ownership_assigned( int query_id, slice owner, slice new_owner, int forward_amount, slice forward_payload, cell msg ) impure inline { + {- + TL-B schema: ownership_assigned#05138d91 query_id:uint64 prev_owner:MsgAddress + forward_payload:(Either Cell ^Cell) = InternalMsgBody; + query_id should be equal with request's query_id. + We verify that: + 1) message is sent to the new_owner address + 2) attached amount matches forward_amount + 3) op matches ownership_assigned op + 4) query_id matches query_id sent + 5) forward_payload matches payload set + -} + + tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) ); + + throw_unless(601, equal_slices( new_owner, parsed_msg.at( 4 ) ) ); + ;; Check that forward_amount matches + throw_unless(602, forward_amount == parsed_msg.at( 5 ) ); + + slice msg_body = parsed_msg.at( 8 ); + + throw_unless( 603, op_ownership_assigned == msg_body~load_uint( 32 ) ); + + throw_unless( 604, query_id == msg_body~load_uint( 64 ) ); + + ;;Checking for previous owner addr + throw_unless( 605, equal_slices( owner, msg_body~load_msg_addr() ) ); + + if( ~ null?( forward_payload ) ){ + if( msg_body~load_uint( 1 ) ){ + cell msg_payload = msg_body~load_ref(); + throw_unless( 606, equal_slices( forward_payload, msg_payload.begin_parse() ) ); + } + else { + throw_unless( 606, equal_slices( forward_payload, msg_body ) ); + } + } +} + +_ verify_excess_sent( int query_id, slice resp_dst, int balance, int forward_fee, int forward_amount, cell msg ) impure inline { + {- + TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody; + Excess message should be sent to resp_dst with all of the msg_value - fees taken to process + We verify that: + 1) message is sent to resp_dst + 2) attached amount is balance - fees taken - forward_amount + 3) op matches excess op + 4) query_id matches request query_id + -} + + tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) ); + + ;;Check dst_addr to be equal to resp_dst + throw_unless( 701, equal_slices( resp_dst, parsed_msg.at( 4 ) ) ); + + int total_sent = parsed_msg.at( 5 ); + int should_sent = balance - min_storage - forward_fee; + + if( forward_amount > 0 ) { + should_sent -= forward_amount + forward_fee; + } + + throw_unless( 702, should_sent == total_sent ); + + slice msg_body = parsed_msg.at( 8 ); + + throw_unless( 703, op_excesses == msg_body~load_uint( 32 ) ); + + throw_unless( 704, query_id == msg_body~load_uint( 64 ) ); + +} + +;;Transfer ownership reject cases + +int __test_transfer_not_owner(){ + + var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true ); + + ;;Trying to execute transfer ownership from non-owner addr + + builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton / 10, null(), 0 ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, new_owner, null(), 0 ); + + ;;Should fail + int gas_used = invoke_method_expect_fail( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] ); + + {- + These are for testing this specific contract exit_code values invoke_method_expect_fail is more general way + var ( exit_code, gas_used, _ ) = invoke_method_full( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] ); + throw_unless( exit_code, exit_code == 401 ); + Should not generate any actions + -} + + assert_no_actions(); + + ;;Now let's verify that owner_addr triggered fail by changin source addr to actual owner_addr + ;;Expect contract to return success + + msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), 0 ); + + ( int gas_success , _ ) = invoke_method( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] ); + + return gas_used + gas_success; + +} + +int __test_transfer_forward_amount_too_large() { + + var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true ); + + {- + NFT should reject transfer if balance lower than forward_amount + message forward fee + minimal storage fee + Sending message with forward_amount of 1 TON and balance 0.1 TON + Now using legit owner address + TON balance:0.1 forward_amount:1 TON fwd_fee:0 verifying that forward_amount is taken into account + Should fail with no actions + -} + + builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton, null(), 0 ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), 0 ); + int gas_used = invoke_method_expect_fail( recv_internal, [ one_ton / 10, 0, msg, msg_body.end_cell().begin_parse() ] ); + + ;;(int exit_code, _, _ ) = invoke_method_full( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] ); + ;;throw_unless( exit_code, exit_code == 402 ); + + assert_no_actions(); + + ;;Now verify that balance was the error trigger by increasing contract balance to 10 TONs + ;;Expect success + + ( int gas_success, _ ) = invoke_method( recv_internal, [ one_ton * 10, 0, msg, msg_body.end_cell().begin_parse() ] ); + + + return gas_used + gas_success; +} + +int __test_transfer_storage_fee() { + + var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true ); + + {- + Now let's try forward_amount exactly equal to balance and fwd_fee 0 + 1 TON Balance forward_amount:1 TON fwd_fee:0 verifying that minimal storage comes into play + Should fail with no actions + -} + + builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton, null(), 0 ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), 0 ); + + int gas_used = invoke_method_expect_fail( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] ); + + assert_no_actions(); + + ;; Let's verify that storage fee was an error trigger by increasing balance by min_storage + ;; Expect success + + ( int gas_success, _ ) = invoke_method( recv_internal, [ one_ton + min_storage, 0, msg, msg_body.end_cell().begin_parse() ] ); + + + return gas_used + gas_success; +} + +int __test_transfer_forward_fee_single() { + {- + If transfer is successfull NFT supposed to send up to 2 messages + 1)To the owner_address with forward_amount of coins + 2)To the response_addr with forward_payload if response_addr is not addr_none + Each of those messages costs fwd_fee + Let' test the first case only by setting resp_dst to addr_none + -} + + var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( false ); + + {- + Now we test if contract takes forward fee into account by adding forward fee to the incoming message + Contract balance would be 1TON + storage_fee and fwd_fee would be 0.01 TON + Should fail with no actions + -} + + int forward_fee = one_ton / 100; + int balance = one_ton + min_storage; + + builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton, null(), 0 ); + + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee); + + int gas_used = invoke_method_expect_fail( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] ); + assert_no_actions(); + + ;; Let's verify that forward fee was an error trigger by increasing balance by that fee + ;; Expect success + + ( int gas_success, _ ) = invoke_method( recv_internal, [ balance + muldiv(forward_fee, 3, 2), 0, msg, msg_body.end_cell().begin_parse() ] ); + + + return gas_used; +} + +int __test_transfer_forward_fee_double() { + + {- + If transfer is successfull NFT supposed to send up to 2 messages + 1)To the owner_address with forward_amount of coins + 2)To the response_addr with forward_payload if response_addr is not addr_none + Each of those messages costs fwd_fee + In this case we test scenario where both messages required to be sent but balance has funs only for single message + To do so resp_dst has be a valid address not equal to addr_none + -} + + var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true ); + + {- + Now we test if contract takes forward fee into account by adding forward fee to the incoming message + Contract balance would be 1TON + storage_fee and fwd_fee would be 0.01 TON + Should fail with no actions + -} + + int forward_fee = one_ton / 100; + int balance = one_ton + min_storage + muldiv(forward_fee, 3, 2); + + builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton, null(), 0 ); + + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee); + + int gas_used = invoke_method_expect_fail( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] ); + assert_no_actions(); + + ;; Let's verify that double forward fee was an error trigger by increasing balance by another forward_fee + ;; Expect success + + ( int gas_success, _ ) = invoke_method( recv_internal, [ balance + muldiv(forward_fee, 3, 2), 0, msg, msg_body.end_cell().begin_parse() ] ); + + + return gas_used; +} + +int __test_success_no_forward_no_reponse() { + + {- + forward_amount:0 resp_dst:addr_none + On successfull execution only address change should occur. + Expect no messages to be sent. + -} + + var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( false ); + + builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), 0, null(), 0 ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), 0 ); + + (int gas_used, _ ) = invoke_method( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] ); + + ;; Owner address has to be changed to the new_owner + throw_unless( 100, equal_slices( get_owner_addr(), new_owner ) ); + + ;; resp_dst is addr_none and no forward_amount, so no message should be generated + assert_no_actions(); + + return gas_used; + +} + +;; Transfer ownership success cases + +int __test_transfer_success_forward_no_response() { + + {- + forward_amount:1TON resp_dst: addr_none balance:10TON + forward_payload:"Hop hey!" + On successfull execution expect: + 1) Address change to new owner + 2) Single ownership_assigned message is sent to new_owner addr with forward_amount attached + TL-B schema: ownership_assigned#05138d91 query_id:uint64 prev_owner:MsgAddress + forward_payload:(Either Cell ^Cell) = InternalMsgBody; + query_id should be equal with request's query_id. + + forward_payload should be equal with request's forward_payload. + + prev_owner is address of the previous owner of this NFT item. + -} + + var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( false ); + + int forward_fee = one_ton / 100; + int forward_amount = one_ton; + int balance = one_ton * 10; + int query_id = rand( 1337 ) + 1; + builder forward_payload = begin_cell().store_slice("Hop hey!"); + + + builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, query_id, null(), forward_amount, forward_payload.end_cell(), 0); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee ); + + (int gas_used, _ ) = invoke_method( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] ); + + ;; Owner address has to be changed to the new_owner + throw_unless( 100, equal_slices( get_owner_addr(), new_owner ) ); + + tuple actions = parse_c5(); ;; test-libs/c5_parse_helpers.func + + int actions_count = actions.tuple_length(); + + ;; Only one message should be sent + throw_unless( 500 + actions_count, actions_count == 1 ); + + ( int action_type, cell body, int mode ) = actions.at(0).untriple(); + + ;; Action has to be action_send_msg + throw_unless( 600, action_type == 0); + + verify_ownership_assigned( query_id, owner_addr, new_owner, forward_amount, forward_payload.end_cell().begin_parse(), body ); + + + return gas_used; +} + +int __test_transfer_success_forward_with_response() { + + {- + forward_amount:1TON resp_dst: addr_none balance:10TON + forward_payload:"Hop hey!" + On successfull execution expect: + 1) Address change to new owner + 2) Single ownership_assigned message is sent to new_owner addr with forward_amount attached + TL-B schema: ownership_assigned#05138d91 query_id:uint64 prev_owner:MsgAddress + forward_payload:(Either Cell ^Cell) = InternalMsgBody; + query_id should be equal with request's query_id. + + forward_payload should be equal with request's forward_payload. + + prev_owner is address of the previous owner of this NFT item. + + 3) Single excesses message to resp_dst + TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody; + + For second message to be sent resp_dst should be valid address not equal to addr_none + -} + + var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true ); + + int forward_fee = one_ton / 100; + int forward_amount = one_ton; + int balance = one_ton * 10; + int query_id = rand( 1338 ) + 1; + builder forward_payload = begin_cell().store_slice("Hop hey!"); + + + builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, query_id, null(), forward_amount, forward_payload.end_cell(), 0); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee ); + + ;;Attach 5 TONs to message value to test excess + (int gas_used, _ ) = invoke_method( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] ); + + ;; Owner address has to be changed to the new_owner + throw_unless( 100, equal_slices( get_owner_addr(), new_owner ) ); + + tuple actions = parse_c5(); ;; test-libs/c5_parse_helpers.func + + int actions_count = actions.tuple_length(); + + ;; Two messages should be sent + throw_unless( 500 + actions_count, actions_count == 2 ); + + ( int action_type, cell body, int mode ) = actions.at(0).untriple(); + + ;; Action has to be action_send_msg + throw_unless( 600, action_type == 0); + + verify_ownership_assigned( query_id, owner_addr, new_owner, forward_amount, forward_payload.end_cell().begin_parse(), body ); + + ( action_type, body, mode ) = actions.at( 1 ).untriple(); + + throw_unless( 700, action_type == 0); + + verify_excess_sent( query_id, resp_dst, balance, muldiv(forward_fee, 3, 2), forward_amount, body ); + + return gas_used; +} + + +int __test_transfer_success_response_only() { + + {- + forward_amount:0 TON resp_dst: valid address balance:10TON + forward_payload:"Hop hey!" + On successfull execution expect: + 1) Address change to new owner + 2) Single excesses message to resp_dst + TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody; + + For second message to be sent resp_dst should be valid address not equal to addr_none + -} + + var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true ); + + int forward_fee = one_ton / 100; + int forward_amount = 0; + int balance = one_ton * 10; + int query_id = rand( 1337 ) + 1; + builder forward_payload = begin_cell().store_slice("Hop hey!"); + + + builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, query_id, null(), forward_amount, forward_payload.end_cell(), 0); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee ); + + (int gas_used, _ ) = invoke_method( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] ); + + ;; Owner address has to be changed to the new_owner + throw_unless( 100, equal_slices( get_owner_addr(), new_owner ) ); + + tuple actions = parse_c5(); ;; test-libs/c5_parse_helpers.func + + int actions_count = actions.tuple_length(); + + ;; Only one message should be sent + throw_unless( 500 + actions_count, actions_count == 1 ); + + ( int action_type, cell body, int mode ) = actions.at(0).untriple(); + + ;; Action has to be action_send_msg + throw_unless( 600, action_type == 0); + + verify_excess_sent( query_id, resp_dst, balance, muldiv(forward_fee, 3, 2), forward_amount, body ); + + return gas_used; +} + +;;get_static_data test cases + +int __test_get_static_data() { + {- + Sends get_static_data request. + Expect success. + On success execution expect: + 1) Single report_static_data message received + 2) Message send mode is 64 + 3) query_id matches query_id in request + 4) Message has 256 bit index value + 5) Message has valid collection_address matching c4 collection_addr + -} + + int query_id = rand( 1337 ) + 1; + + var ( idx, coll_addr, owner, _ ) = load_test_data(); + builder msg_body = generate_nft_get_static_data_request( query_id ); + cell msg = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 ); + + ( int gas_used, _ ) = invoke_method( recv_internal,[ one_ton * 10, 0, msg, msg_body.end_cell().begin_parse() ] ); + + tuple actions = parse_c5(); + + throw_unless( 800, actions.tuple_length() == 1 ); + + ( int action_type, cell body, int mode ) = actions.at(0).untriple(); + + throw_unless( 801, action_type == 0 ); + + throw_unless( 802, mode == 64 ); + + tuple parsed_msg = unsafe_tuple( parse_internal_message( body ) ); + slice ds = parsed_msg.at( 8 ); + + ( int op, int resp_query_id ) = ( ds~load_uint( 32 ), ds~load_uint( 64 ) ); + + throw_unless( 803, op_report_static_data == op ); + + throw_unless( 804, query_id == resp_query_id ); + + throw_unless( 805, idx == ds~load_uint( 256 ) ); + + throw_unless( 806, equal_slices( coll_addr, ds~load_msg_addr() ) ); + + return gas_used; +} + +;; Get methods test + +int __test_get_nft_data() { + ;; (int init?, int index, slice collection_address, slice owner_address, cell individual_content) + + var ( idx, coll_addr, owner, content ) = load_test_data(); + + ( int gas_used, tuple stack ) = invoke_method( get_nft_data, [] ); + + ;;Check that argument count match signature + + throw_unless( 300, stack.tuple_length() == 5 ); + int init? = stack.first(); + int res_idx = stack.second(); + slice res_collection = stack.third(); + slice res_owner = stack.fourth(); + cell res_content = stack.at( 4 ); + + ;; Check that initialized NFT can't have empty owner_address + ;; And the other way around + throw_unless( 301, ( init? ^ res_owner.null?() ) ); + + ;; Check that contract correctly loads data + throw_unless( 302, equal_slices( owner, res_owner ) ); + throw_unless( 303, equal_slices( coll_addr, res_collection ) ); + + slice content_data = res_content.begin_parse(); + + throw_unless( 304, equal_slices( content.begin_parse(), content_data ) ); + + ;; Only collection-less NFT have to comply with TIP-64 + ;; Such NFT items have addr_none as collection_address + if( equal_slices( res_owner, generate_empty_address() ) ){ + + int content_layout = content_data~load_uint( 8 ); + + ;; Check for allowed content_layout + throw_unless( 305, ( content_layout == 1 ) | ( content_layout == 0 ) ); + + + if( content_layout == 1 ){ + + ;; Check that off-chain URI contains at least one ASCII char + throw_unless( 306, token_snake_len( content_data ) > 8 ); + } else { + ;; On-chain is stored as dict + ;; Has to be non-empty + throw_if( 306, content_data.preload_dict().dict_empty?() ); + + ;; Perhaps could go further and test for Optional dict keys but none of those is required so i'll leave it be + } + } + + + return gas_used; +} diff --git a/nft_tests/tests/utils/collection-data.func b/nft_tests/tests/utils/collection-data.func new file mode 100644 index 0000000..e1bdf33 --- /dev/null +++ b/nft_tests/tests/utils/collection-data.func @@ -0,0 +1,38 @@ +(slice, int, cell, cell, cell) load_test_data() inline { + var ds = get_data().begin_parse(); + return ( ds~load_msg_addr(), ;; owner_address + ds~load_uint(64), ;; next_item_index + ds~load_ref(), ;; content + ds~load_ref(), ;; nft_item_code + ds~load_ref() ;; royalty_params + ); +} + +slice get_owner() { + (slice owner, _, _, _, _) = load_test_data(); + return owner; +} + +cell get_content() inline { + (_, _, cell content, _, _) = load_test_data(); + return content; +} + +int get_nft_index() inline { + (_, int idx, _, _, _) = load_test_data(); + + return idx; +} + +( int, int, slice ) load_test_royalty() { + ( _, _, _, _, cell royalty ) = load_test_data(); + slice ds = royalty.begin_parse(); + + return ( ds~load_uint( 16 ), ds~load_uint( 16 ), ds~load_msg_addr() ); +} + +( int, int, slice ) get_test_royalty() { + ( _, tuple stack ) = invoke_method( royalty_params, [] ); + + return ( stack.first(), stack.second(), stack.third() ); +} diff --git a/nft_tests/tests/utils/constants.func b/nft_tests/tests/utils/constants.func new file mode 100644 index 0000000..42f8c31 --- /dev/null +++ b/nft_tests/tests/utils/constants.func @@ -0,0 +1,2 @@ +const int one_ton = 1000000000; ;;10^9 +const int min_storage = 50000000; ;; 0.05 TON minimal storage fee