diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..2c0878d
--- /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 0000000..6101ea2
--- /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 0000000..cd0cd9c
--- /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 0000000..1c182c9
--- /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 0000000..fbb3fb6
--- /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);
+}