Compare commits

..

1 Commits

Author SHA1 Message Date
user 21bed2c1f3 add nft code & script 2024-06-11 20:07:04 +03:00
8 changed files with 484 additions and 1 deletions

View File

@ -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";

View File

@ -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";

View File

@ -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";

215
contracts/nft_collection.fc Normal file
View File

@ -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());
}

156
contracts/nft_item.fc Normal file
View File

@ -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);
}

View File

@ -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()

View File

@ -0,0 +1,6 @@
import { CompilerConfig } from '@ton/blueprint';
export const compile: CompilerConfig = {
lang: 'func',
targets: ['contracts/nft_collection.fc'],
};

View File

@ -0,0 +1,6 @@
import { CompilerConfig } from '@ton/blueprint';
export const compile: CompilerConfig = {
lang: 'func',
targets: ['contracts/nft_item.fc'],
};