@ -10,11 +10,27 @@
int min_tons_for_storage() asm "1000000000 PUSHINT"; ;; 1 TON
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_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_end_duration = 3600; ;; 1 hour = 60 * 60; in testnet 1 min
const auction_prolongation = 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
;; MsgAddressInt max_bid_address
;; Coins max_bid_amount
;; Coins max_bid_amount
;; int auction_end_time
;; 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) {
cell pack_auction(slice max_bid_address, int max_bid_amount, int auction_end_time) {
return begin_cell()
return begin_cell()
.store_slice(max_bid_address)
.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();
.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() {
(cell, cell, int, int, slice, slice, cell, cell, int) load_data() {
slice ds = get_data().begin_parse();
slice ds = get_data().begin_parse();
cell content = ds~load_ref(); ;; content
cell code = ds~load_ref(); ;; code
cell code = ds~load_ref(); ;; code
var (index, collection_address) = (ds~load_uint(256), ds~load_msg_addr());
var (index, collection_address) = (ds~load_uint(256), ds~load_msg_addr());
if (ds.slice_bits() > 0) {
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 {
} 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 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();
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) {
slice calculate_nft_item_address(int wc, cell state_init) {
return begin_cell()
return begin_cell()
.store_uint(4, 3)
.store_uint(4, 3)
@ -75,14 +90,15 @@ slice calculate_nft_item_address(int wc, cell state_init) {
.begin_parse();
.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(
set_data(
begin_cell()
begin_cell()
.store_ref(collection_content)
.store_ref(nft_item_code)
.store_ref(nft_item_code)
.store_uint(index, 256)
.store_uint(index, 256)
.store_slice(collection_address)
.store_slice(collection_address)
.store_slice(owner_address)
.store_slice(owner_address)
.store_ref(content)
.store_ref(domain)
.store_ref(domain)
.store_dict(auction)
.store_dict(auction)
.store_uint(last_fill_up_time, 64)
.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);
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();
slice new_owner_address = in_msg_body~load_msg_addr();
force_chain(new_owner_address);
force_chain(new_owner_address);
slice response_destination = in_msg_body~load_msg_addr();
slice response_destination = in_msg_body~load_msg_addr();
in_msg_body~load_int(1); ;; this nft don't use custom_payload
in_msg_body~load_int(1); ;; this nft don't use custom_payload
int forward_amount = in_msg_body~load_coins();
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();
int rest_amount = my_balance - min_tons_for_storage();
if (forward_amount) {
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);
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
int need_response = response_destination.preload_uint(2) != 0; ;; if NOT addr_none: 00
if (need_response) {
if (need_response) {
;; The response is needed, so we consider the commission for the outbound message
rest_amount -= fwd_fees;
rest_amount -= fwd_fees;
}
}
throw_unless(402, rest_amount >= 0); ;; base nft spends fixed amount of gas, will not check for response
throw_unless(402, rest_amount >= 0); ;; base nft spends fixed amount of gas, will not check for response
if (forward_amount) {
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) {
if (need_response) {
force_chain(response_destination);
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
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());
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 {
() recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure {
if (in_msg_body.slice_empty?()) { ;; bounce back empty messages
if (in_msg_body.slice_empty?()) { ;; bounce back empty messages
throw(0xffff);
throw(0xffff);
@ -148,8 +197,16 @@ slice calculate_nft_item_address(int wc, cell state_init) {
return ();
return ();
}
}
slice sender_address = cs~load_msg_addr();
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();
(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?) {
if (~ init?) {
;; The initialization message has to come from the collection
throw_unless(405, equal_slices(collection_address, sender_address));
throw_unless(405, equal_slices(collection_address, sender_address));
slice from_address = in_msg_body~load_msg_addr();
slice from_address = in_msg_body~load_msg_addr();
cell domain = in_msg_body~load_ref();
cell domain = in_msg_body~load_ref();
@ -161,13 +218,16 @@ slice calculate_nft_item_address(int wc, cell state_init) {
if (months > 12) {
if (months > 12) {
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 duration = auction_start_duration - (auction_start_duration - auction_end_duration) * months / 12;
int auction_end_time = now() + duration;
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());
store_data(content, item_code, index, collection_address, zero_address(), domain, pack_auction(from_address, msg_value, auction_end_time), now());
return ();
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();
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
send_msg(from_address, 0, 0, cur_lt(), null(), 64); ;; carry all the remaining value of the inbound message
return ();
return ();