From 21bed2c1f3a1d708b16a11154d1ed19d14ed9e69 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 11 Jun 2024 20:07:04 +0300 Subject: [PATCH] add nft code & script --- contracts/imports/op-codes.fc | 24 ++++ contracts/imports/params.fc | 10 ++ contracts/imports/stdlib.fc | 4 +- contracts/nft_collection.fc | 215 ++++++++++++++++++++++++++++++ contracts/nft_item.fc | 156 ++++++++++++++++++++++ signer/deploy_collection.py | 64 +++++++++ wrappers/NftCollection.compile.ts | 6 + wrappers/NftItem.compile.ts | 6 + 8 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 contracts/imports/op-codes.fc create mode 100644 contracts/imports/params.fc create mode 100644 contracts/nft_collection.fc create mode 100644 contracts/nft_item.fc create mode 100644 signer/deploy_collection.py create mode 100644 wrappers/NftCollection.compile.ts create mode 100644 wrappers/NftItem.compile.ts diff --git a/contracts/imports/op-codes.fc b/contracts/imports/op-codes.fc new file mode 100644 index 0000000..948a977 --- /dev/null +++ b/contracts/imports/op-codes.fc @@ -0,0 +1,24 @@ +int op::transfer() asm "0x5fcc3d14 PUSHINT"; +int op::ownership_assigned() asm "0x05138d91 PUSHINT"; +int op::excesses() asm "0xd53276db PUSHINT"; +int op::get_static_data() asm "0x2fcb26a2 PUSHINT"; +int op::report_static_data() asm "0x8b771735 PUSHINT"; +int op::get_royalty_params() asm "0x693d3950 PUSHINT"; +int op::report_royalty_params() asm "0xa8cb00ad PUSHINT"; + +;; NFTEditable +int op::edit_content() asm "0x1a0b9d51 PUSHINT"; +int op::transfer_editorship() asm "0x1c04412a PUSHINT"; +int op::editorship_assigned() asm "0x511a4463 PUSHINT"; + +;; SBT +int op::request_owner() asm "0xd0c3bfea PUSHINT"; +int op::owner_info() asm "0x0dd607e3 PUSHINT"; + +int op::prove_ownership() asm "0x04ded148 PUSHINT"; +int op::ownership_proof() asm "0x0524c7ae PUSHINT"; +int op::ownership_proof_bounced() asm "0xc18e86d2 PUSHINT"; + +int op::destroy() asm "0x1f04537a PUSHINT"; +int op::revoke() asm "0x6f89f5e3 PUSHINT"; +int op::take_excess() asm "0xd136d3b3 PUSHINT"; diff --git a/contracts/imports/params.fc b/contracts/imports/params.fc new file mode 100644 index 0000000..d98ce72 --- /dev/null +++ b/contracts/imports/params.fc @@ -0,0 +1,10 @@ +int workchain() asm "0 PUSHINT"; + +() force_chain(slice addr) impure { + (int wc, _) = parse_std_addr(addr); + throw_unless(333, wc == workchain()); +} + +slice null_addr() asm "b{00} PUSHSLICE"; +int flag::regular() asm "0x10 PUSHINT"; +int flag::bounce() asm "0x8 PUSHINT"; diff --git a/contracts/imports/stdlib.fc b/contracts/imports/stdlib.fc index fa048f6..fba56df 100644 --- a/contracts/imports/stdlib.fc +++ b/contracts/imports/stdlib.fc @@ -621,5 +621,7 @@ int get_seed() impure asm "RANDSEED"; int equal_slice_bits(slice a, slice b) asm "SDEQ"; int equal_slices(slice a, slice b) asm "SDEQ"; +int builder_null?(builder b) asm "ISNULL"; + ;;; Concatenates two builders -builder store_builder(builder to, builder from) asm "STBR"; \ No newline at end of file +builder store_builder(builder to, builder from) asm "STBR"; diff --git a/contracts/nft_collection.fc b/contracts/nft_collection.fc new file mode 100644 index 0000000..cd54fa5 --- /dev/null +++ b/contracts/nft_collection.fc @@ -0,0 +1,215 @@ +;; NFT collection smart contract + +#include "imports/stdlib.fc"; +#include "imports/op-codes.fc"; +#include "imports/params.fc"; + +;; storage scheme +;; default#_ royalty_factor:uint16 royalty_base:uint16 royalty_address:MsgAddress = RoyaltyParams; +;; storage#_ owner_address:MsgAddress next_item_index:uint64 +;; ^[collection_content:^Cell common_content:^Cell] +;; nft_item_code:^Cell +;; royalty_params:^RoyaltyParams +;; = Storage; + +(slice, int, cell, cell, cell) load_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 + ); +} + +() save_data(slice owner_address, int next_item_index, cell content, cell nft_item_code, cell royalty_params) impure inline { + set_data(begin_cell() + .store_slice(owner_address) + .store_uint(next_item_index, 64) + .store_ref(content) + .store_ref(nft_item_code) + .store_ref(royalty_params) + .end_cell()); +} + +cell calculate_nft_item_state_init(int item_index, cell nft_item_code) { + cell data = begin_cell().store_uint(item_index, 64).store_slice(my_address()).end_cell(); + return begin_cell().store_uint(0, 2).store_dict(nft_item_code).store_dict(data).store_uint(0, 1).end_cell(); +} + +slice calculate_nft_item_address(int wc, cell state_init) { + return begin_cell().store_uint(4, 3) + .store_int(wc, 8) + .store_uint(cell_hash(state_init), 256) + .end_cell() + .begin_parse(); +} + +() deploy_nft_item(int item_index, cell nft_item_code, int amount, cell nft_content) impure { + cell state_init = calculate_nft_item_state_init(item_index, nft_item_code); + slice nft_address = calculate_nft_item_address(workchain(), state_init); + var msg = begin_cell() + .store_uint(0x18, 6) + .store_slice(nft_address) + .store_coins(amount) + .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) + .store_ref(state_init) + .store_ref(nft_content); + send_raw_message(msg.end_cell(), 1); ;; pay transfer fees separately, revert on errors +} + +(int, int) encode_number_to_text(int number) inline { + int len = 0; + int value = 0; + int mult = 1; + do { + (number, int res) = number.divmod(10); + value = value + (res + 48) * mult; + mult = mult * 256; + len = len + 1; + } until (number == 0); + return (len, value); +} + +() send_royalty_params(slice to_address, int query_id, slice data) impure inline { + var msg = begin_cell() + .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool packages:MsgAddress -> 011000 + .store_slice(to_address) + .store_coins(0) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_uint(op::report_royalty_params(), 32) + .store_uint(query_id, 64) + .store_slice(data); + send_raw_message(msg.end_cell(), 64); ;; carry all the remaining value of the inbound message +} + +() recv_internal(cell in_msg_full, slice in_msg_body) impure { + if (in_msg_body.slice_empty?()) { ;; ignore empty messages + return (); + } + slice cs = in_msg_full.begin_parse(); + int flags = cs~load_uint(4); + + if (flags & 1) { ;; ignore all bounced messages + return (); + } + slice sender_address = cs~load_msg_addr(); + + int op = in_msg_body~load_uint(32); + int query_id = in_msg_body~load_uint(64); + + var (owner_address, next_item_index, content, nft_item_code, royalty_params) = load_data(); + + if (op == op::get_royalty_params()) { + send_royalty_params(sender_address, query_id, royalty_params.begin_parse()); + return (); + } + + throw_unless(401, equal_slices(sender_address, owner_address)); + + + if (op == 1) { ;; deploy new nft + int item_index = in_msg_body~load_uint(64); + throw_unless(402, item_index <= next_item_index); + var is_last = item_index == next_item_index; + deploy_nft_item(item_index, nft_item_code, in_msg_body~load_coins(), in_msg_body~load_ref()); + if (is_last) { + next_item_index += 1; + save_data(owner_address, next_item_index, content, nft_item_code, royalty_params); + } + return (); + } + if (op == 2) { ;; batch deploy of new nfts + int counter = 0; + cell deploy_list = in_msg_body~load_ref(); + do { + var (item_index, item, f?) = deploy_list~udict::delete_get_min(64); + if (f?) { + counter += 1; + if (counter >= 250) { ;; Limit due to limits of action list size + throw(399); + } + + throw_unless(403 + counter, item_index <= next_item_index); + deploy_nft_item(item_index, nft_item_code, item~load_coins(), item~load_ref()); + if (item_index == next_item_index) { + next_item_index += 1; + } + } + } until ( ~ f?); + save_data(owner_address, next_item_index, content, nft_item_code, royalty_params); + return (); + } + if (op == 3) { ;; change owner + slice new_owner = in_msg_body~load_msg_addr(); + save_data(new_owner, next_item_index, content, nft_item_code, royalty_params); + return (); + } + + if (op == 0xCB33) { ;; mint nft throw next index + in_msg_body~skip_bits(64); + slice forward_content = in_msg_body~load_ref().begin_parse(); + (int t_len, int t_xt) = encode_number_to_text(next_item_index); + builder mint_content = begin_cell() + .store_ref( + begin_cell() + .store_uint(0x6e6674732f6d657461646174612f, 112) ;; nfts/metadata/ + .store_uint(t_xt, t_len * 8) + .store_uint(0x2e6a736f6e, 5 * 8) ;; .json + .end_cell() + ) + .store_slice(forward_content); + + deploy_nft_item(next_item_index, nft_item_code, in_msg_body~load_coins(), mint_content.end_cell()); + next_item_index += 1; + save_data(owner_address, next_item_index, content, nft_item_code, royalty_params); + return (); + } + + if (op == 0xCB38) { ;; upgrade contract + cell new_code = in_msg_body~load_maybe_ref(); + cell new_data = in_msg_body~load_maybe_ref(); + ifnot (new_code.null?()) { + set_code(new_code); + } + ifnot (new_data.null?()) { + set_data(new_data); + } + return (); + } + + throw(0xffff); +} + +;; Get methods + +(int, cell, slice) get_collection_data() method_id { + var (owner_address, next_item_index, content, _, _) = load_data(); + slice cs = content.begin_parse(); + return (next_item_index, cs~load_ref(), owner_address); +} + +slice get_nft_address_by_index(int index) method_id { + var (_, _, _, nft_item_code, _) = load_data(); + cell state_init = calculate_nft_item_state_init(index, nft_item_code); + return calculate_nft_item_address(0, state_init); +} + +(int, int, slice) royalty_params() method_id { + var (_, _, _, _, royalty) = load_data(); + slice rs = royalty.begin_parse(); + return (rs~load_uint(16), rs~load_uint(16), rs~load_msg_addr()); +} + +cell get_nft_content(int index, cell individual_nft_content) method_id { + var (_, _, content, _, _) = load_data(); + slice cs = content.begin_parse(); + cs~load_ref(); + slice common_content = cs~load_ref().begin_parse(); + return (begin_cell() + .store_uint(1, 8) ;; offchain tag + .store_slice(common_content) + .store_ref(individual_nft_content) + .end_cell()); +} diff --git a/contracts/nft_item.fc b/contracts/nft_item.fc new file mode 100644 index 0000000..57743a0 --- /dev/null +++ b/contracts/nft_item.fc @@ -0,0 +1,156 @@ +;; +;; TON NFT Item Smart Contract v2 +;; support ownership_assigned on minting nft +;; + +{- + + NOTE that this tokens can be transferred within the same workchain. + + This is suitable for most tokens, if you need tokens transferable between workchains there are two solutions: + + 1) use more expensive but universal function below to calculate message forward fee for arbitrary destination (see `misc/forward-fee-calc.cs`) + + 2) use token holder proxies in target workchain (that way even 'non-universal' token can be used from any workchain) + +-} + +#include "imports/stdlib.fc"; +#include "imports/op-codes.fc"; +#include "imports/params.fc"; + +int min_tons_for_storage() asm "50000000 PUSHINT"; ;; 0.05 TON + +;; +;; Storage +;; +;; uint64 index +;; MsgAddressInt collection_address +;; MsgAddressInt owner_address +;; cell content +;; + +(int, int, slice, slice, cell) load_data() { + slice ds = get_data().begin_parse(); + var (index, collection_address) = (ds~load_uint(64), ds~load_msg_addr()); + if (ds.slice_bits() > 0) { + return (-1, index, collection_address, ds~load_msg_addr(), ds~load_ref()); + } else { + return (0, index, collection_address, null(), null()); ;; nft not initialized yet + } +} + +() store_data(int index, slice collection_address, slice owner_address, cell content) impure { + set_data( + begin_cell() + .store_uint(index, 64) + .store_slice(collection_address) + .store_slice(owner_address) + .store_ref(content) + .end_cell() + ); +} + +() send_msg(slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline { + var msg = begin_cell() + .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000 + .store_slice(to_address) + .store_coins(amount) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_uint(op, 32) + .store_uint(query_id, 64); + + if (~ builder_null?(payload)) { + msg = msg.store_builder(payload); + } + + send_raw_message(msg.end_cell(), send_mode); +} + +() transfer_ownership(int my_balance, int index, slice collection_address, slice owner_address, cell content, slice sender_address, int query_id, slice in_msg_body, int fwd_fees) impure inline { + throw_unless(401, equal_slices(sender_address, owner_address)); + + slice new_owner_address = in_msg_body~load_msg_addr(); + force_chain(new_owner_address); + slice response_destination = in_msg_body~load_msg_addr(); + in_msg_body~load_int(1); ;; this nft don't use custom_payload + int forward_amount = in_msg_body~load_coins(); + + int rest_amount = my_balance - min_tons_for_storage(); + if (forward_amount) { + rest_amount -= (forward_amount + fwd_fees); + } + int need_response = response_destination.preload_uint(2) != 0; ;; if NOT addr_none: 00 + if (need_response) { + rest_amount -= fwd_fees; + } + + throw_unless(402, rest_amount >= 0); ;; base nft spends fixed amount of gas, will not check for response + + if (forward_amount) { + send_msg(new_owner_address, forward_amount, op::ownership_assigned(), query_id, begin_cell().store_slice(owner_address).store_slice(in_msg_body), 1); ;; paying fees, revert on errors + } + if (need_response) { + force_chain(response_destination); + send_msg(response_destination, rest_amount, op::excesses(), query_id, null(), 1); ;; paying fees, revert on errors + } + + store_data(index, collection_address, new_owner_address, content); +} + +() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { + if (in_msg_body.slice_empty?()) { ;; ignore empty messages + return (); + } + + slice cs = in_msg_full.begin_parse(); + int flags = cs~load_uint(4); + + if (flags & 1) { ;; ignore all bounced messages + return (); + } + slice sender_address = cs~load_msg_addr(); + + cs~load_msg_addr(); ;; skip dst + cs~load_coins(); ;; skip value + cs~skip_bits(1); ;; skip extracurrency collection + cs~load_coins(); ;; skip ihr_fee + int fwd_fee = muldiv(cs~load_coins(), 3, 2); ;; we use message fwd_fee for estimation of forward_payload costs + + + (int init?, int index, slice collection_address, slice owner_address, cell content) = load_data(); + if (~ init?) { + throw_unless(405, equal_slices(collection_address, sender_address)); + var new_owner_address = in_msg_body~load_msg_addr(); + store_data(index, collection_address, new_owner_address, in_msg_body~load_ref()); + if (in_msg_body.slice_data_empty?() == false) { + var forward_amount = in_msg_body~load_coins(); + if (forward_amount) { + send_msg(new_owner_address, forward_amount, op::ownership_assigned(), 0, begin_cell().store_slice(collection_address).store_slice(in_msg_body), 3); ;; paying fees, ignore errors + } + } + return (); + } + + int op = in_msg_body~load_uint(32); + int query_id = in_msg_body~load_uint(64); + + if (op == op::transfer()) { + transfer_ownership(my_balance, index, collection_address, owner_address, content, sender_address, query_id, in_msg_body, fwd_fee); + return (); + } + if (op == op::get_static_data()) { + send_msg(sender_address, 0, op::report_static_data(), query_id, begin_cell().store_uint(index, 256).store_slice(collection_address), 64); ;; carry all the remaining value of the inbound message + return (); + } + throw(0xffff); +} + +;; +;; GET Methods +;; + +(int, int, slice, slice, cell) get_nft_data() method_id { + (int init?, int index, slice collection_address, slice owner_address, cell content) = load_data(); + return (init?, index, collection_address, owner_address, content); +} diff --git a/signer/deploy_collection.py b/signer/deploy_collection.py new file mode 100644 index 0000000..1770105 --- /dev/null +++ b/signer/deploy_collection.py @@ -0,0 +1,64 @@ +from base64 import b64encode + +from tonsdk.contract.token.nft import NFTCollection, NFTItem +from tonsdk.contract.wallet import Wallets +import os +import asyncio + +from tonsdk.utils import Address +import httpx +import time + +WALLET_MNEMONIC = os.getenv('WALLET_MNEMONIC', 'lucky odor welcome cool climb cream sauce either piece embark ritual purpose bacon timber please nest paddle obscure student wheel police speak acquire cream').split(' ') +WALLET_VERSION = os.getenv('WALLET_VERSION', 'v3r2') + + +async def main(): + _, _, _, wallet = Wallets.from_mnemonics(WALLET_MNEMONIC, WALLET_VERSION) + print(f"Wallet address: {wallet.address.to_string(1, 1, 1)}") + + collection = NFTCollection( + code='b5ee9c724102160100029f000114ff00f4a413f4bcf2c80b01020162020f0202cd030a04e9d10638048adf000e8698180b8d848adf07d201800e98fe99ff6a2687d20699fea6a6a184108349e9ca829405d47141baf8280e8410854658056b84008646582a802e78b127d010a65b509e58fe59f80e78b64c0207d80701b28b9e382f970c892e000f18112e001718112e001f18112c1006599dd40405060700603502d33f5313bbf2e1925313ba01fa00d43028103459f0068e1201a44343c85005cf1613cb3fccccccc9ed54925f05e200a6357003d4308e378040f4966fa5208e2906a4208100fabe93f2c18fde81019321a05325bbf2f402fa00d43022544b30f00623ba9302a402de04926c21e2b3e6303250444313c85005cf1613cb3fccccccc9ed54002c323401fa40304144c85005cf1613cb3fccccccc9ed5402fe8e6d35028040d721d401d0237020718e14037aa90ca63024a812a003aa0702a421c0004430e6306c1282182e6a736f6e82606e6674732f6d657461646174612fc8cb6f03aa0213cf01cb27c9c8cc01cf1601fa003001c923544730f00601a44343c85005cf1613cb3fccccccc9ed54e010345f04328200cb38bae30230840f0809002cf404f40430216e91319301fb04e2206e913092ed54e20004f2f00201200b0e0201200c0d002d007232cffe0a33c5b25c083232c044fd003d0032c03260001b3e401d3232c084b281f2fff27420003d45af0047021f005778018c8cb0558cf165004fa0213cb6b12ccccc971fb008020120101502012011120043b8b5d31ed44d0fa40d33fd4d4d43010245f04d0d431d430d071c8cb0701cf16ccc980201201314002fb5dafda89a1f481a67fa9a9a860d883a1a61fa61ff480610002db4f47da89a1f481a67fa9a9a86028be09e008e003e00b00025bc82df6a2687d20699fea6a6a182de86a182c4699353e6', + owner_address=Address('EQC6VQVXeuRdLqFmbAdeJywe2pO7eyasZTWqVdHKo-vdgsBE'), + collection_content_uri='https://geton.farm/nfts/collection/meta.json', + nft_item_content_base_uri='https://geton.farm/nfts/collection/', + nft_item_code_hex='b5ee9c7241020e01000229000114ff00f4a413f4bcf2c80b01020162020d0202ce030a020120040903b90c8871c02497c0f83434c0c05c6c2497c0f83e903e900c7e800c5c75c87e800c7e800c1cea6d003c00812cf8c081b4c7f4cfe08417f30f45148c2ea3a24c840dd78c9004f6cf380c0d0d0d4d60840bf2c9a884aeb8c097c12103fcbc2005060800ca306c22345232c705f2e19501fa40d45423405235f00321c701c0008e4401fa00218e3a821005138d9170c85006cf1658cf161034413073708010c8cb055007cf165005fa0215cb6a12cb1fcb3f226eb39458cf17019132e201c901fb00925f04e2925f03e201f65135c705f2e191fa4021f001fa40d20031fa00820afaf0801ba121945315a0a1de22d70b01c300209206a19136e220c2fff2e192218e3e821005138d91c85009cf16500bcf16712449145446a0708010c8cb055007cf165005fa0215cb6a12cb1fcb3f226eb39458cf17019132e201c901fb00104794102a375be2070082028e3526f0018210d53276db103744006d71708010c8cb055007cf165005fa0215cb6a12cb1fcb3f226eb39458cf17019132e201c901fb0093303234e25502f00300727082108b77173505c8cbff5004cf1610248040708010c8cb055007cf165005fa0215cb6a12cb1fcb3f226eb39458cf17019132e201c901fb0000113e910c1c2ebcb853600201200b0c003b3b513434cffe900835d27080269fc07e90350c04090408f80c1c165b5b60001d00f232cfd633c58073c5b3327b55200009a11f9fe005df38d804', + royalty=0.2, + royalty_address=Address('UQDLahdbuqdPW7YoFP5_HxIcVrraziR6IBmeEYgruHCntCrb'), + ) + print(f"Collection address: {collection.address.to_string(1, 1, 1)}") + + wallet_seqno = httpx.get(f'https://tonapi.io/v2/blockchain/accounts/{wallet.address.to_string(1, 1, 1)}/methods/seqno').json() + print("Wallet seqno response:", wallet_seqno) + try: + assert not ('error' in wallet_seqno) + assert wallet_seqno['exit_code'] == 0 + wallet_seqno = int(wallet_seqno['stack'][0]['num'], 16) + except: + wallet_seqno = 0 + + print(f"Wallet seqno: {wallet_seqno}") + # time.sleep(4) + + query = wallet.create_transfer_message( + collection.address.to_string(0, 1, 1), + 2e7, + wallet_seqno, + state_init=collection.create_state_init()['state_init'] + ) + print( + httpx.post( + 'https://toncenter.com/api/v2/sendBoc', + json={ + 'boc': b64encode(query['message'].to_boc(False)).decode() + } + ).json() + ) + + +if __name__ == '__main__': + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() + + diff --git a/wrappers/NftCollection.compile.ts b/wrappers/NftCollection.compile.ts new file mode 100644 index 0000000..d99ca27 --- /dev/null +++ b/wrappers/NftCollection.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from '@ton/blueprint'; + +export const compile: CompilerConfig = { + lang: 'func', + targets: ['contracts/nft_collection.fc'], +}; diff --git a/wrappers/NftItem.compile.ts b/wrappers/NftItem.compile.ts new file mode 100644 index 0000000..ede8cfd --- /dev/null +++ b/wrappers/NftItem.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from '@ton/blueprint'; + +export const compile: CompilerConfig = { + lang: 'func', + targets: ['contracts/nft_item.fc'], +};