diff --git a/contracts/imports/op-codes.fc b/contracts/imports/op-codes.fc index c2ceb3f..f0685a5 100644 --- a/contracts/imports/op-codes.fc +++ b/contracts/imports/op-codes.fc @@ -1,5 +1,9 @@ int op::transfer() asm "0x5fcc3d14 PUSHINT"; + +;; The operator for message to the owner after transfer int op::ownership_assigned() asm "0x05138d91 PUSHINT"; + +;; The op for the response message to the owner after transfer int op::excesses() asm "0xd53276db PUSHINT"; int op::get_static_data() asm "0x2fcb26a2 PUSHINT"; int op::report_static_data() asm "0x8b771735 PUSHINT"; diff --git a/contracts/main.fc b/contracts/main.fc index 69fa784..37d3b45 100644 --- a/contracts/main.fc +++ b/contracts/main.fc @@ -10,11 +10,27 @@ int min_tons_for_storage() asm "1000000000 PUSHINT"; ;; 1 TON +;; =============== storage ============================= +;; +;; Storage +;; +;; cell nft_item_code +;; uint256 index --- The index of this item in the collection +;; MsgAddressInt collection_address +;; MsgAddressInt owner_address +;; cell content --- The key-value map with the content both as item and a collection) +;; cell domain --- e.g contains "alice" (without ending \0) for "alice.ton" domain +;; cell auction --- auction info: (address max_bid_address, coins max_bid, uint32 end_time) +;; int last_fill_up_time + +;; todo: Move auction_start_time, auction_start_duration, auction_end_duration, auction_prolongation, price multiplicator to the auction data (also, make it updateable) +;; todo: Also, add functionality to buy a domain instantly (without auction) - and the corresponding settings const auction_start_duration = 604800; ;; 1 week = 60 * 60 * 24 * 7; in testnet 5 min const auction_end_duration = 3600; ;; 1 hour = 60 * 60; in testnet 1 min const auction_prolongation = 3600; ;; 1 hour = 60 * 60; in testnet 1 min +;; Parse the auction data ;; MsgAddressInt max_bid_address ;; Coins max_bid_amount ;; int auction_end_time @@ -27,6 +43,7 @@ const auction_prolongation = 3600; ;; 1 hour = 60 * 60; in testnet 1 min } } +;; Serialize the auction data cell pack_auction(slice max_bid_address, int max_bid_amount, int auction_end_time) { return begin_cell() .store_slice(max_bid_address) @@ -35,37 +52,35 @@ cell pack_auction(slice max_bid_address, int max_bid_amount, int auction_end_tim .end_cell(); } -;; =============== storage ============================= -;; -;; Storage -;; -;; content:^Cell -;; nft_item_code:^Cell -;; uint256 index -;; MsgAddressInt collection_address -;; MsgAddressInt owner_address -;; cell domain - e.g contains "alice" (without ending \0) for "alice.ton" domain -;; cell auction - auction info -;; int last_fill_up_time - +;; cell content, cell nft_item_code, uint256 index, address collection_address, +;; address owner_address, cell domain, cell auction, int last_fill_up_time (cell, cell, int, int, slice, slice, cell, cell, int) load_data() { slice ds = get_data().begin_parse(); - cell content = ds~load_ref(); ;; content cell code = ds~load_ref(); ;; code var (index, collection_address) = (ds~load_uint(256), ds~load_msg_addr()); if (ds.slice_bits() > 0) { - return (content, code, -1, index, collection_address, ds~load_msg_addr(), ds~load_ref(), ds~load_dict(), ds~load_uint(64)); + slice owner = ds~load_msg_addr(); + cell content = ds~load_ref(); ;; content + return (content, code, -1, index, collection_address, owner, ds~load_ref(), ds~load_dict(), ds~load_uint(64)); } else { - return (content, code, 0, index, collection_address, null(), null(), null(), 0); ;; nft not initialized yet + return (null(), code, 0, index, collection_address, null(), null(), null(), 0); ;; nft not initialized yet } } +;; The initial state of a new NFT item with a given index and code +;; Includes the code, the index, and the collection address - everything else should be initialized by the item itself cell calculate_nft_item_state_init(int item_index, cell nft_item_code) { - cell data = begin_cell().store_uint(item_index, 256).store_slice(my_address()).end_cell(); + cell data = begin_cell() + .store_ref(nft_item_code) + .store_uint(item_index, 256) + .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(); } +;; As noted in https://ton.org/tblkch.pdf part 1.7.3, the nft address should be calculated using the hash of the state +;; This function generates the address for the new NFT slice calculate_nft_item_address(int wc, cell state_init) { return begin_cell() .store_uint(4, 3) @@ -75,14 +90,15 @@ slice calculate_nft_item_address(int wc, cell state_init) { .begin_parse(); } -() store_data(cell collection_content, cell nft_item_code, int index, slice collection_address, slice owner_address, cell domain, cell auction, int last_fill_up_time) impure { +;; Serialize the data using the storage schema +() store_data(cell content, cell nft_item_code, int index, slice collection_address, slice owner_address, cell domain, cell auction, int last_fill_up_time) impure { set_data( begin_cell() - .store_ref(collection_content) .store_ref(nft_item_code) .store_uint(index, 256) .store_slice(collection_address) .store_slice(owner_address) + .store_ref(content) .store_ref(domain) .store_dict(auction) .store_uint(last_fill_up_time, 64) @@ -106,35 +122,68 @@ slice calculate_nft_item_address(int wc, cell state_init) { send_raw_message(msg.end_cell(), send_mode); } -() transfer_ownership(int my_balance, cell collection_content, cell nft_item_code, int index, slice collection_address, slice owner_address, slice sender_address, int query_id, slice in_msg_body, int fwd_fees, cell domain, cell auction) impure inline { +;; Transfer the ownership of the item +;; in_msg_body schema: +;; address new_owner +;; address response_destination (can be 0 if no response needed) +;; coins to forward + coins to pay the fees +() transfer_ownership(int my_balance, cell collection_content, cell nft_item_code, int index, + slice collection_address, slice owner_address, slice sender_address, int query_id, slice in_msg_body, + int fwd_fees, cell domain, cell auction) impure inline { 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(); + ;; The balance needs to be kept above `min_tons_for_storage()` (1 TON) int rest_amount = my_balance - min_tons_for_storage(); if (forward_amount) { + ;; The forward_amount has been already added to the balance, + ;; but we will have to give it back, so we need to subtract it as well as the message commission rest_amount -= (forward_amount + fwd_fees); } + ;; If the address starts with 00, it means that the response is not needed int need_response = response_destination.preload_uint(2) != 0; ;; if NOT addr_none: 00 if (need_response) { + ;; The response is needed, so we consider the commission for the outbound message 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 + 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 the response, which includes the balance of the NFT item except for 1 TON + ;; Basically, it means that the person transfers the NFT to someone else, but keeps the balance send_msg(response_destination, rest_amount, op::excesses(), query_id, null(), 1); ;; paying fees, revert on errors } + ;; Update the owner address and the fill-up time store_data(collection_content, nft_item_code, index, collection_address, new_owner_address, domain, auction, now()); } +;; Create a new item +() deploy_nft_item(int item_index, cell nft_item_code, 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(0) + .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(), 64); ;; carry all the remaining value of the inbound message, fee deducted from amount +} + () recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure { if (in_msg_body.slice_empty?()) { ;; bounce back empty messages throw(0xffff); @@ -148,8 +197,16 @@ slice calculate_nft_item_address(int wc, cell state_init) { return (); } slice sender_address = cs~load_msg_addr(); + ;; Un-serialize all the data (cell content, cell item_code, int init?, int index, slice collection_address, slice owner_address, cell domain, cell auction, int last_fill_up_time) = load_data(); + ;; This means that we are initializing the new contract + ;; The initialization message has this structure: + ;; address initial_owner_address (for an auction) + ;; slice domain (the domain for the nft) + ;; todo: for instant deploys of subdomains or buying them, this structure should be changed + ;; todo: we should also be able to pass additional content here if (~ init?) { + ;; The initialization message has to come from the collection throw_unless(405, equal_slices(collection_address, sender_address)); slice from_address = in_msg_body~load_msg_addr(); cell domain = in_msg_body~load_ref(); @@ -161,13 +218,16 @@ slice calculate_nft_item_address(int wc, cell state_init) { if (months > 12) { months = 12; } + ;; The auction duration becomes shorter over time int duration = auction_start_duration - (auction_start_duration - auction_end_duration) * months / 12; int auction_end_time = now() + duration; + ;; We initialize the contract with the auction and content store_data(content, item_code, index, collection_address, zero_address(), domain, pack_auction(from_address, msg_value, auction_end_time), now()); return (); } - if (init? & equal_slices(collection_address, sender_address)) { ;; todo: add comments + ;; If the item is initialized and the message comes from the collection + if (init? & equal_slices(collection_address, sender_address)) { ;; todo: add comments here slice from_address = in_msg_body~load_msg_addr(); send_msg(from_address, 0, 0, cur_lt(), null(), 64); ;; carry all the remaining value of the inbound message return ();