From 83fab5443e1feb1a26702b8c6013a98ae05482d9 Mon Sep 17 00:00:00 2001 From: leoloco Date: Wed, 3 Jan 2024 19:52:17 +0100 Subject: [PATCH] Adding marketplace contracts --- package-lock.json | 6 + .../marketplace/__tests__/as-pect.d.ts | 1 + .../contracts/marketplace/nft_contract.ts | 217 ++++++++++ .../marketplace/nft_contract_external.ts | 63 +++ .../contracts/marketplace/nft_marketplace.ts | 382 ++++++++++++++++++ 5 files changed, 669 insertions(+) create mode 100644 package-lock.json create mode 100644 smart-contracts/assembly/contracts/marketplace/__tests__/as-pect.d.ts create mode 100644 smart-contracts/assembly/contracts/marketplace/nft_contract.ts create mode 100644 smart-contracts/assembly/contracts/marketplace/nft_contract_external.ts create mode 100644 smart-contracts/assembly/contracts/marketplace/nft_marketplace.ts diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..2c0878d8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "massa-standards", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/smart-contracts/assembly/contracts/marketplace/__tests__/as-pect.d.ts b/smart-contracts/assembly/contracts/marketplace/__tests__/as-pect.d.ts new file mode 100644 index 00000000..6101ea23 --- /dev/null +++ b/smart-contracts/assembly/contracts/marketplace/__tests__/as-pect.d.ts @@ -0,0 +1 @@ +/// diff --git a/smart-contracts/assembly/contracts/marketplace/nft_contract.ts b/smart-contracts/assembly/contracts/marketplace/nft_contract.ts new file mode 100644 index 00000000..cd0cd9c3 --- /dev/null +++ b/smart-contracts/assembly/contracts/marketplace/nft_contract.ts @@ -0,0 +1,217 @@ +import { + Args, + bytesToU256, + stringToBytes, + u256ToBytes, +} from '@massalabs/as-types'; +import { + Address, + Context, + Storage, + generateEvent, + transferCoins, +} from '@massalabs/massa-as-sdk'; + +import { u256 } from 'as-bignum'; +import { + nameKey, + symbolKey, + totalSupplyKey, + baseURIKey, + ownerKey, + counterKey, + initCounter, + ownerTokenKey, + nft1_transferFrom, +} from '../NFT'; +import { + _currentSupply, + _increment, + _updateBalanceOf, + _onlyOwner, +} from '../NFT/NFT-internals'; + +export const mintPriceKey = 'mintPrice'; +export const mintHistoryKey = 'mintHistory_'; + +// Override to add mint price +export function constructor(binaryArgs: StaticArray): void { + // This line is important. It ensures that this function can't be called in the future. + // If you remove this check, someone could call your constructor function and reset your smart contract. + assert(Context.isDeployingContract()); + + const args = new Args(binaryArgs); + const name = args.nextString().expect('name argument is missing or invalid'); + const symbol = args + .nextString() + .expect('symbol argument is missing or invalid'); + const totalSupply = args + .nextU256() + .expect('totalSupply argument is missing or invalid'); + const baseURI = args + .nextString() + .expect('baseURI argument is missing or invalid'); + const mintPrice = args + .nextU64() + .expect('mintPrice argument is missing or invalid'); + + Storage.set(nameKey, name); + Storage.set(symbolKey, symbol); + Storage.set(totalSupplyKey, u256ToBytes(totalSupply)); + Storage.set(baseURIKey, baseURI); + Storage.set(ownerKey, Context.caller().toString()); + Storage.set(counterKey, u256ToBytes(initCounter)); + Storage.set(mintPriceKey, mintPrice.toString()); +} + +// Used to get the owner of the NFT from the Args instead of the caller +export function delegated_constructor(binaryArgs: StaticArray): void { + // This line is important. It ensures that this function can't be called in the future. + // If you remove this check, someone could call your constructor function and reset your smart contract. + assert(Context.isDeployingContract()); + + const args = new Args(binaryArgs); + const name = args.nextString().expect('name argument is missing or invalid'); + const symbol = args + .nextString() + .expect('symbol argument is missing or invalid'); + const totalSupply = args + .nextU256() + .expect('totalSupply argument is missing or invalid'); + const baseURI = args + .nextString() + .expect('baseURI argument is missing or invalid'); + const mintPrice = args + .nextU64() + .expect('mintPrice argument is missing or invalid'); + const owner = args + .nextString() + .expect('owner argument is missing or invalid'); + + Storage.set(nameKey, name); + Storage.set(symbolKey, symbol); + Storage.set(totalSupplyKey, u256ToBytes(totalSupply)); + Storage.set(baseURIKey, baseURI); + Storage.set(ownerKey, owner); + Storage.set(counterKey, u256ToBytes(initCounter)); + Storage.set(mintPriceKey, mintPrice.toString()); +} + +// Override NFT tokenURI function to target BaseURI +export function nft1_tokenURI(binaryArgs: StaticArray): StaticArray { + const args = new Args(binaryArgs); + const tokenId = args + .nextU256() + .expect('token id argument is missing or invalid') + .toString(); + return stringToBytes(Storage.get(baseURIKey)); +} + +// Override NFT mint function to add mint price +export function nft1_mint(_args: StaticArray): void { + assert( + bytesToU256(Storage.get(totalSupplyKey)) > _currentSupply(), + 'Max supply reached', + ); + + let price = U64.parseInt(Storage.get(mintPriceKey)); + assert( + Context.transferredCoins() >= price, + 'Not enough sent coins to mint this NFT', + ); + let owner = Storage.get(ownerKey); + transferCoins(new Address(owner), price); + + const args = new Args(_args); + + const mintAddress = args + .nextString() + .expect('mintAddress argument is missing or invalid'); + + // TODO: Check Address validity + _increment(); + + const tokenToMint = _currentSupply().toString(); + + const key = ownerTokenKey + tokenToMint; + + Storage.set(key, mintAddress); + + _updateBalanceOf(mintAddress, true); + + generateEvent('[ICO_QUEST] MINT: ' + Context.caller().toString()); + + Storage.set( + mintHistoryKey + tokenToMint, + mintAddress + + '_' + + tokenToMint + + '_' + + price.toString() + + '_' + + Context.timestamp().toString(), + ); +} + +export function nft1_mintPrice(binaryArgs: StaticArray): StaticArray { + return stringToBytes(Storage.get(mintPriceKey)); +} + +export function nft1_setMintPrice(binaryArgs: StaticArray): void { + assert(_onlyOwner(), 'The caller is not the owner of the contract'); + + const args = new Args(binaryArgs); + const mintPrice = args + .nextU64() + .expect('mintPrice argument is missing or invalid'); + + Storage.set(mintPriceKey, mintPrice.toString()); +} + +export function nft1_mintHistory( + _: StaticArray = new StaticArray(0), +): StaticArray { + const currentToken: u256 = _currentSupply() - new u256(1); + const global_history: string[] = []; + for (let i: u256 = new u256(0); i < currentToken; i++) { + let history = Storage.get(mintHistoryKey + i.toString()); + if (history != null) { + continue; + } + global_history.push(history); + } + return new Args().add>(global_history).serialize(); +} + +export function multiMint(_args: StaticArray): void { + const args = new Args(_args); + const nbToMint = args + .nextU256() + .expect('nbToMint argument is missing or invalid'); + + const mintAddress = args + .nextString() + .expect('mintAddress argument is missing or invalid'); + + for (let i: u256 = new u256(0); i < nbToMint; i++) { + let args = new Args(); + args.add(mintAddress); + nft1_mint(args.serialize()); + } +} + +export function multiTransfer(binaryArgs: StaticArray): void { + const args = new Args(binaryArgs); + const toAddress = args + .nextString() + .expect('toAddress argument is missing or invalid'); + const tokenIds = args + .nextFixedSizeArray() + .expect('tokenIds argument is missing or invalid'); + for (let i: i32 = 0; i < tokenIds.length; i++) { + let args = new Args(); + args.add(toAddress); + args.add(tokenIds[i]); + nft1_transferFrom(args.serialize()); + } +} diff --git a/smart-contracts/assembly/contracts/marketplace/nft_contract_external.ts b/smart-contracts/assembly/contracts/marketplace/nft_contract_external.ts new file mode 100644 index 00000000..1c182c98 --- /dev/null +++ b/smart-contracts/assembly/contracts/marketplace/nft_contract_external.ts @@ -0,0 +1,63 @@ +import { Args, stringToBytes, u256ToBytes } from '@massalabs/as-types'; +import { Context, Storage } from '@massalabs/massa-as-sdk'; +import { u256 } from 'as-bignum'; +import { + nameKey, + symbolKey, + totalSupplyKey, + baseURIKey, + ownerKey, + counterKey, + initCounter, + nft1_mint, +} from '../NFT'; + +export * from '../NFT/NFT'; + +// Override to add mint price +export function constructor(binaryArgs: StaticArray): void { + // This line is important. It ensures that this function can't be called in the future. + // If you remove this check, someone could call your constructor function and reset your smart contract. + assert(Context.isDeployingContract()); + + const args = new Args(binaryArgs); + const name = args.nextString().expect('name argument is missing or invalid'); + const symbol = args + .nextString() + .expect('symbol argument is missing or invalid'); + const totalSupply = args + .nextU256() + .expect('totalSupply argument is missing or invalid'); + const baseURI = args + .nextString() + .expect('baseURI argument is missing or invalid'); + const mintPrice = args + .nextU64() + .expect('mintPrice argument is missing or invalid'); + + Storage.set(nameKey, name); + Storage.set(symbolKey, symbol); + Storage.set(totalSupplyKey, u256ToBytes(totalSupply)); + Storage.set(baseURIKey, baseURI); + Storage.set(ownerKey, Context.caller().toString()); + Storage.set(counterKey, u256ToBytes(initCounter)); + + // GIVE 2 NFT TO THE CONTRACT's OWNER + let nbToMint = new u256(2); + let mintAddress = Context.caller().toString(); + for (let i: u256 = new u256(0); i < nbToMint; i++) { + let args = new Args(); + args.add(mintAddress); + nft1_mint(args.serialize()); + } +} + +// Override NFT tokenURI function to target BaseURI +export function nft1_tokenURI(binaryArgs: StaticArray): StaticArray { + const args = new Args(binaryArgs); + const tokenId = args + .nextU256() + .expect('token id argument is missing or invalid') + .toString(); + return stringToBytes(Storage.get(baseURIKey)); +} diff --git a/smart-contracts/assembly/contracts/marketplace/nft_marketplace.ts b/smart-contracts/assembly/contracts/marketplace/nft_marketplace.ts new file mode 100644 index 00000000..fbb3fb6e --- /dev/null +++ b/smart-contracts/assembly/contracts/marketplace/nft_marketplace.ts @@ -0,0 +1,382 @@ +// The entry file of your WebAssembly module. +import { + Args, + u64ToBytes, + bytesToString, + stringToBytes, +} from '@massalabs/as-types'; +import { + Address, + Context, + Storage, + call, + createSC, + generateEvent, + transferCoins, +} from '@massalabs/massa-as-sdk'; + +const ONE_UNIT = 10 ** 9; +const ONE_TENTH = 10 ** 8; + +export const nftArrayKey = stringToBytes('nfts'); +export const nftUserArrayKey = stringToBytes('nfts_users'); +export const nftContractCodeKey = stringToBytes('nft_contract_code'); + +export const ownerKey = 'nft_owner_key'; + +export const userProfileKey = 'userProfile_'; + +export const marketplaceFeeKey = 'marketplaceFee_'; // In 1/1000th. A fee of 10 is 1%. +export const marketplaceOwnerKey = 'marketplaceOwner_'; + +export const sellOfferKey = 'sellOffer_'; +export const saleHistoryKey = 'saleHistory_'; + +/** + * This function is meant to be called only one time: when the contract is deployed. + * + * @param args - The arguments to the constructor containing the message to be logged + */ +export function constructor(binaryArgs: StaticArray): void { + // This line is important. It ensures that this function can't be called in the future. + // If you remove this check, someone could call your constructor function and reset your smart contract. + if (!Context.isDeployingContract()) { + return; + } + const args = new Args(binaryArgs); + + let nft_contract_code = args + .nextFixedSizeArray() + .expect('nft_contract_code argument is missing or invalid'); + const marketplaceOwner = args + .nextString() + .expect('marketplaceOwner argument is missing or invalid'); + const marketplaceFee = args + .nextU64() + .expect('marketplaceFee argument is missing or invalid'); + + Storage.set(nftContractCodeKey, StaticArray.fromArray(nft_contract_code)); + Storage.set(nftArrayKey, new Args().add>([]).serialize()); + Storage.set(marketplaceOwnerKey, marketplaceOwner); + Storage.set(marketplaceFeeKey, bytesToString(u64ToBytes(marketplaceFee))); +} + +/* +ADMIN FEATURES +*/ +export function setMarkeplaceFee(binaryArgs: StaticArray): void { + const args = new Args(binaryArgs); + const callerAddress = Context.caller(); + let marketplaceOwner = new Address(Storage.get(marketplaceOwnerKey)); + assert( + callerAddress.toString() == marketplaceOwner.toString(), + 'Only marketplace owner can set the fee', + ); + + const marketplaceFee = args + .nextU64() + .expect('marketplaceFee argument is missing or invalid'); + + Storage.set(marketplaceFeeKey, bytesToString(u64ToBytes(marketplaceFee))); +} + +export function addCollections(binaryArgs: StaticArray): void { + const args = new Args(binaryArgs); + const callerAddress = Context.caller(); + let marketplaceOwner = new Address(Storage.get(marketplaceOwnerKey)); + assert( + callerAddress.toString() == marketplaceOwner.toString(), + 'Only marketplace owner can manually add collections', + ); + + let nfts = new Args(Storage.get(nftArrayKey)).nextStringArray().unwrap(); + + let addresses = args.nextStringArray().unwrap(); + for (let i = 0; i < addresses.length; i++) { + if (nfts.indexOf(addresses[i].toString()) == -1) { + nfts.push(addresses[i].toString()); + } + } + + Storage.set(nftArrayKey, new Args().add>(nfts).serialize()); +} + +export function delCollections(binaryArgs: StaticArray): void { + const args = new Args(binaryArgs); + const callerAddress = Context.caller(); + let marketplaceOwner = new Address(Storage.get(marketplaceOwnerKey)); + assert( + callerAddress.toString() == marketplaceOwner.toString(), + 'Only marketplace owner can manually delete collections', + ); + let nfts = new Args(Storage.get(nftArrayKey)).nextStringArray().unwrap(); + + let addresses = args.nextStringArray().unwrap(); + for (let i = 0; i < addresses.length; i++) { + const index = nfts.indexOf(addresses[i]); + if (index > -1) { + nfts.splice(index, 1); + } + } + + Storage.set(nftArrayKey, new Args().add>(nfts).serialize()); +} + +/* +USER FEATURES +*/ + +export function addUserCollections(binaryArgs: StaticArray): void { + const args = new Args(binaryArgs); + const addr = args.nextString().unwrap(); + const callerAddress = Context.caller(); + const marketplaceOwner = new Address(Storage.get(marketplaceOwnerKey)); + assert( + addr == marketplaceOwner.toString() || addr == callerAddress.toString(), + 'Only marketplace owner and target user can manually add user collections', + ); + + let key = stringToBytes( + bytesToString(nftUserArrayKey) + callerAddress.toString(), + ); + + if (!Storage.has(key)) { + Storage.set(key, new Args().add>([]).serialize()); + } + + let nfts_users = new Args(Storage.get(key)) + .nextStringArray() + .expect('nftUserArray not set for callerAddress'); + + const addresses = args.nextStringArray().unwrap(); + for (let i = 0; i < addresses.length; i++) { + if (nfts_users.indexOf(addresses[i].toString()) == -1) { + nfts_users.push(addresses[i].toString()); + } + } + + if (addresses.length > 0) { + generateEvent('[ICO_QUEST] LINK: ' + Context.caller().toString()); + } + + Storage.set(key, new Args().add>(nfts_users).serialize()); +} + +export function delUserCollections(binaryArgs: StaticArray): void { + const args = new Args(binaryArgs); + const addr = args.nextString().unwrap(); + const callerAddress = Context.caller(); + const marketplaceOwner = new Address(Storage.get(marketplaceOwnerKey)); + assert( + addr == marketplaceOwner.toString() || addr == callerAddress.toString(), + 'Only marketplace owner and target user can manually delete user collections', + ); + + let key = stringToBytes( + bytesToString(nftUserArrayKey) + callerAddress.toString(), + ); + + if (!Storage.has(key)) { + return; + } + + let nfts_users = new Args(Storage.get(key)).nextStringArray().unwrap(); + + let addresses = args.nextStringArray().unwrap(); + + for (let i = 0; i < addresses.length; i++) { + const index = nfts_users.indexOf(addresses[i]); + if (index > -1) { + nfts_users.splice(index, 1); + } + } + + Storage.set(key, new Args().add>(nfts_users).serialize()); +} + +export function create_nft(args: StaticArray): void { + let nft_contract_code = Storage.get(stringToBytes('nft_contract_code')); + let addr = createSC(nft_contract_code); + call(addr, 'delegated_constructor', new Args(args), 1 * ONE_UNIT); + let nfts = new Args(Storage.get(nftArrayKey)).nextStringArray().unwrap(); + nfts.push(addr.toString()); + Storage.set(nftArrayKey, new Args().add>(nfts).serialize()); + generateEvent(`NFT created at ${addr.toString()}`); + generateEvent(`CREATE: ${Context.caller().toString()}`); +} + +export function register_nft(args: StaticArray): void { + let nft_addr = new Args(args).nextString().unwrap(); + let nfts = new Args(Storage.get(nftArrayKey)) + .nextFixedSizeArray() + .unwrap(); + nfts.push(nft_addr); + Storage.set(nftArrayKey, new Args().add(nfts).serialize()); +} + +export function sell_offer(argsSerialized: StaticArray): void { + let args = new Args(argsSerialized); + let nft_collection_addr = args.nextString().unwrap(); + let nft_token_id = args.nextU256().unwrap(); + let price = args.nextU64().unwrap(); + let expireIn = args.nextU64().unwrap(); + let expirationTime = Context.timestamp() + expireIn; + + let key = sellOfferKey + nft_collection_addr + '_' + nft_token_id.toString(); + assert( + !Storage.has(key), + `Sell offer already exist for ${nft_collection_addr}/${nft_token_id.toString()}`, + ); + + // verify that the nft exists (or is from user collection) + let nfts = new Args(Storage.get(nftArrayKey)).nextStringArray().unwrap(); + + let user_key = stringToBytes( + bytesToString(nftUserArrayKey) + Context.caller().toString(), + ); + if (Storage.has(user_key)) { + const user_nfts = new Args(Storage.get(user_key)) + .nextStringArray() + .unwrap(); + assert( + nfts.indexOf(nft_collection_addr) > -1 || + user_nfts.indexOf(nft_collection_addr) > -1, + `NFT collection ${nft_collection_addr} is not registered`, + ); + } else { + assert( + nfts.indexOf(nft_collection_addr) > -1, + `NFT collection ${nft_collection_addr} is not registered`, + ); + } + + // verify that the caller is the NFT's owner + let owner = bytesToString( + call( + new Address(nft_collection_addr), + 'nft1_ownerOf', + new Args().add(nft_token_id), + ONE_TENTH, + ), + ); + assert( + owner == Context.caller().toString(), + `You are not the owner of ${nft_collection_addr}/${nft_token_id.toString()}`, + ); + Storage.set( + key, + Context.caller().toString() + + '_' + + price.toString() + + '_' + + expirationTime.toString(), + ); + generateEvent( + `Sell offer created for ${nft_collection_addr}/${nft_token_id.toString()} + for ${price.toString()}, expires at ${expirationTime.toString()}`, + ); + generateEvent('[ICO_QUEST] SELL: ' + Context.caller().toString()); +} + +export function remove_sell_offer(argsSerialized: StaticArray): void { + let args = new Args(argsSerialized); + let nft_collection_addr = args.nextString().unwrap(); + let nft_token_id = args.nextU256().unwrap(); + let key = sellOfferKey + nft_collection_addr + '_' + nft_token_id.toString(); + assert( + Storage.has(key), + `Sell offer doesn't exist for ${nft_collection_addr}/${nft_token_id.toString()}`, + ); + + // verify that the nft exists (or is from user collection) + let nfts = new Args(Storage.get(nftArrayKey)).nextStringArray().unwrap(); + + let user_key = stringToBytes( + bytesToString(nftUserArrayKey) + Context.caller().toString(), + ); + if (Storage.has(user_key)) { + const user_nfts = new Args(Storage.get(user_key)) + .nextStringArray() + .unwrap(); + assert( + nfts.indexOf(nft_collection_addr) > -1 || + user_nfts.indexOf(nft_collection_addr) > -1, + `NFT collection ${nft_collection_addr} is not registered`, + ); + } else { + assert( + nfts.indexOf(nft_collection_addr) > -1, + `NFT collection ${nft_collection_addr} is not registered`, + ); + } + + // verify that the caller is the NFT's owner + let owner = new Args( + call( + new Address(nft_collection_addr), + 'nft1_ownerOf', + new Args().add(nft_token_id), + ONE_TENTH, + ), + ) + .nextString() + .unwrap(); + assert( + owner == Context.caller().toString(), + `You are not the owner of ${nft_collection_addr}/${nft_token_id.toString()}`, + ); + + Storage.del(key); + generateEvent( + `Sell offer removed for ${nft_collection_addr}/${nft_token_id.toString()}`, + ); + generateEvent(`REMOVE_SELL: ${Context.caller().toString()}`); +} + +export function buy_nft(argsSerialized: StaticArray): void { + let args = new Args(argsSerialized); + let nft_collection_addr = args.nextString().unwrap(); + let nft_collection_addr_as_addr = new Address(nft_collection_addr); + let ContextCallerStr = Context.caller().toString(); + + let nft_token_id = args.nextU256().unwrap(); + let key = sellOfferKey + nft_collection_addr + '_' + nft_token_id.toString(); + + let storageGet = Storage.get(key); + let storageGetSplit = storageGet.split('_'); + let price = U64.parseInt(storageGetSplit[1]); + let expirationTime = U64.parseInt(storageGetSplit[2]); + assert(Context.timestamp() <= expirationTime, `Sell offer has expired`); + assert( + Context.transferredCoins() >= price, + 'Not enough sent coins to buy this NFT', + ); + + let owner = bytesToString( + call( + nft_collection_addr_as_addr, + 'nft1_ownerOf', + new Args().add(nft_token_id), + ONE_TENTH, + ), + ); + call( + nft_collection_addr_as_addr, + 'nft1_transferFrom', + new Args().add(owner).add(Context.caller().toString()).add(nft_token_id), + 1 * ONE_UNIT, + ); + transferCoins(new Address(owner), price); + Storage.del(key); + let historyKey = + saleHistoryKey + nft_collection_addr + '_' + nft_token_id.toString(); + Storage.set( + historyKey, + ContextCallerStr + + '_' + + price.toString() + + '_' + + Context.timestamp().toString(), + ); + generateEvent('[ICO_QUEST] BUY: ' + ContextCallerStr); +}