So at chapter 15.2 of the FCC course we build a NFT marketplace and the front end has a file called
AddEvent.js available here https://github.com/PatrickAlphaC/nextjs-nft-marketplace-moralis-fcc/blob/main/addEvents.js
AddEvent.js:
const Moralis = require("moralis/node")
require("dotenv").config()
const contractAddresses = require("./constants/networkMapping.json")
let chainId = process.env.chainId || 31337
let moralisChainId = chainId == "31337" ? "1337" : chainId
const contractAddressArray = contractAddresses[chainId]["NftMarketplace"]
const contractAddress = contractAddressArray[contractAddressArray.length - 1]
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL
const appId = process.env.NEXT_PUBLIC_APP_ID
const masterKey = process.env.masterKey
async function main() {
await Moralis.start({ serverUrl, appId, masterKey })
console.log(`Working with contrat address ${contractAddress}`)
let itemListedOptions = {
// Moralis understands a local chain is 1337
chainId: moralisChainId,
sync_historical: true,
topic: "ItemListed(address,address,uint256,uint256)",
address: contractAddress,
abi: {
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "seller",
type: "address",
},
{
indexed: true,
internalType: "address",
name: "nftAddress",
type: "address",
},
{
indexed: true,
internalType: "uint256",
name: "tokenId",
type: "uint256",
},
{
indexed: false,
internalType: "uint256",
name: "price",
type: "uint256",
},
],
name: "ItemListed",
type: "event",
},
tableName: "ItemListed",
}
let itemBoughtOptions = {
chainId: moralisChainId,
address: contractAddress,
sync_historical: true,
topic: "ItemBought(address,address,uint256,uint256)",
abi: {
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "buyer",
type: "address",
},
{
indexed: true,
internalType: "address",
name: "nftAddress",
type: "address",
},
{
indexed: true,
internalType: "uint256",
name: "tokenId",
type: "uint256",
},
{
indexed: false,
internalType: "uint256",
name: "price",
type: "uint256",
},
],
name: "ItemBought",
type: "event",
},
tableName: "ItemBought",
}
let itemCanceledOptions = {
chainId: moralisChainId,
address: contractAddress,
topic: "ItemCanceled(address,address,uint256)",
sync_historical: true,
abi: {
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "seller",
type: "address",
},
{
indexed: true,
internalType: "address",
name: "nftAddress",
type: "address",
},
{
indexed: true,
internalType: "uint256",
name: "tokenId",
type: "uint256",
},
],
name: "ItemCanceled",
type: "event",
},
tableName: "ItemCanceled",
}
const listedResponse = await Moralis.Cloud.run("watchContractEvent", itemListedOptions, {
useMasterKey: true,
})
const boughtResponse = await Moralis.Cloud.run("watchContractEvent", itemBoughtOptions, {
useMasterKey: true,
})
const canceledResponse = await Moralis.Cloud.run("watchContractEvent", itemCanceledOptions, {
useMasterKey: true,
})
if (listedResponse.success && canceledResponse.success && boughtResponse.success) {
console.log("Success! Database Updated with watching events")
} else {
console.log("Something went wrong...")
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error)
process.exit(1)
})
I tried to update it as follows to consider the Goerli testate (chainId = 5) network instead of dev chain since Moralis v2 self hosted server has no native options to connect to dev chain at this stage. I also replaced morales/node and by parse/node:
// const Moralis = require("moralis-v1/node")
const Parse = require("parse/node")
require("dotenv").config()
const contractAddresses = require("./constants/networkMapping.json")
// let chainId = process.env.chainId || 31337
// let moralisChainId = chainId == "31337" ? "1337" : chainId
// temp fix for Moralis v2:
// let chainId = process.env.chainId || 5
// let moralisChainId = chainId == "5" ? "1337" : chainId
let moralisChainId = 5
const contractAddress = contractAddresses[chainId]["NftMarketplace"][0]
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL
const appId = process.env.NEXT_PUBLIC_APP_ID
const masterKey = process.env.masterKey
async function main() {
// await Moralis.start({ serverUrl, appId, masterKey })
await Parse.start({ serverUrl, appId, masterKey }) // masterKey needed?
console.log(`Working with contract address ${contractAddress}`)
let ItemListedOptions = {
// Mortalis understands a local chain is 1337
chainId: moralisChainId,
address: contractAddress,
sync_historical: true,
topic: "ItemListed(address, address, uint256, uint256)",
abi: {
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "seller",
type: "address",
},
{
indexed: true,
internalType: "address",
name: "nftAddress",
type: "address",
},
{
indexed: true,
internalType: "uint256",
name: "tokenId",
type: "uint256",
},
{
indexed: false,
internalType: "uint256",
name: "price",
type: "uint256",
},
],
name: "ItemListed",
type: "event",
},
tableName: "ItemListed",
}
let ItemBoughtOptions = {
chainId: moralisChainId,
address: contractAddress,
topic: "ItemBought(address, address, uint256, uint256)",
sync_historical: true,
abi: {
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "buyer",
type: "address",
},
{
indexed: true,
internalType: "address",
name: "nftAddress",
type: "address",
},
{
indexed: true,
internalType: "uint256",
name: "tokenId",
type: "uint256",
},
{
indexed: false,
internalType: "uint256",
name: "price",
type: "uint256",
},
],
name: "ItemBought",
type: "event",
},
tableName: "ItemBought",
}
let ItemCanceledOptions = {
chainId: moralisChainId,
address: contractAddress,
topic: "ItemCanceled(address, address, uint256)",
sync_historical: true,
abi: {
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "seller",
type: "address",
},
{
indexed: true,
internalType: "address",
name: "nftAddress",
type: "address",
},
{
indexed: true,
internalType: "uint256",
name: "tokenId",
type: "uint256",
},
],
name: "ItemCanceled",
type: "event",
},
tableName: "ItemCanceled",
}
// const listedResponse = await Moralis.Cloud.run("watchContractEvent", ItemListedOptions, {
// useMasterKey: true,
// })
// const boughtResponse = await Moralis.Cloud.run("watchContractEvent", ItemBoughtOptions, {
// useMasterKey: true,
// })
// const canceledResponse = await Moralis.Cloud.run("watchContractEvent", ItemCanceledOptions, {
// useMasterKey: true,
// })
// temp fix for Moralis v2:
const listedResponse = await Parse.Cloud.run("watchContractEvent", ItemListedOptions, {
useMasterKey: true,
})
const boughtResponse = await Parse.Cloud.run("watchContractEvent", ItemBoughtOptions, {
useMasterKey: true,
})
const canceledResponse = await Parse.Cloud.run("watchContractEvent", ItemCanceledOptions, {
useMasterKey: true,
})
if (listedResponse.success && canceledResponse.success && boughtResponse.success) {
console.log("Success! Database Updated with watching event")
} else {
console.log("Something went wrong...")
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error)
process.exit(1)
})
Then regarding the cloud functions, the course offer the updateActiveItems.js file in the front end as well, as per the following link
https://github.com/PatrickAlphaC/nextjs-nft-marketplace-moralis-fcc/blob/main/cloudFunctions/updateActiveItems.js and code below:
updateActiveItems.js
Moralis.Cloud.afterSave("ItemListed", async (request) => {
const confirmed = request.object.get("confirmed")
const logger = Moralis.Cloud.getLogger()
logger.info("Looking for confirmed TX...")
if (confirmed) {
logger.info("Found item!")
const ActiveItem = Moralis.Object.extend("ActiveItem")
// In case of listing update, search for already listed ActiveItem and delete
const query = new Moralis.Query(ActiveItem)
query.equalTo("nftAddress", request.object.get("nftAddress"))
query.equalTo("tokenId", request.object.get("tokenId"))
query.equalTo("marketplaceAddress", request.object.get("address"))
query.equalTo("seller", request.object.get("seller"))
logger.info(`Marketplace | Query: ${query}`)
const alreadyListedItem = await query.first()
console.log(`alreadyListedItem ${JSON.stringify(alreadyListedItem)}`)
if (alreadyListedItem) {
logger.info(`Deleting ${alreadyListedItem.id}`)
await alreadyListedItem.destroy()
logger.info(
`Deleted item with tokenId ${request.object.get(
"tokenId"
)} at address ${request.object.get(
"address"
)} since the listing is being updated. `
)
}
// Add new ActiveItem
const activeItem = new ActiveItem()
activeItem.set("marketplaceAddress", request.object.get("address"))
activeItem.set("nftAddress", request.object.get("nftAddress"))
activeItem.set("price", request.object.get("price"))
activeItem.set("tokenId", request.object.get("tokenId"))
activeItem.set("seller", request.object.get("seller"))
logger.info(
`Adding Address: ${request.object.get("address")} TokenId: ${request.object.get(
"tokenId"
)}`
)
logger.info("Saving...")
await activeItem.save()
}
})
Moralis.Cloud.afterSave("ItemCanceled", async (request) => {
const confirmed = request.object.get("confirmed")
const logger = Moralis.Cloud.getLogger()
logger.info(`Marketplace | Object: ${request.object}`)
if (confirmed) {
const ActiveItem = Moralis.Object.extend("ActiveItem")
const query = new Moralis.Query(ActiveItem)
query.equalTo("marketplaceAddress", request.object.get("address"))
query.equalTo("nftAddress", request.object.get("nftAddress"))
query.equalTo("tokenId", request.object.get("tokenId"))
logger.info(`Marketplace | Query: ${query}`)
const canceledItem = await query.first()
logger.info(`Marketplace | CanceledItem: ${JSON.stringify(canceledItem)}`)
if (canceledItem) {
logger.info(`Deleting ${canceledItem.id}`)
await canceledItem.destroy()
logger.info(
`Deleted item with tokenId ${request.object.get(
"tokenId"
)} at address ${request.object.get("address")} since it was canceled. `
)
} else {
logger.info(
`No item canceled with address: ${request.object.get(
"address"
)} and tokenId: ${request.object.get("tokenId")} found.`
)
}
}
})
Moralis.Cloud.afterSave("ItemBought", async (request) => {
const confirmed = request.object.get("confirmed")
const logger = Moralis.Cloud.getLogger()
logger.info(`Marketplace | Object: ${request.object}`)
if (confirmed) {
const ActiveItem = Moralis.Object.extend("ActiveItem")
const query = new Moralis.Query(ActiveItem)
query.equalTo("marketplaceAddress", request.object.get("address"))
query.equalTo("nftAddress", request.object.get("nftAddress"))
query.equalTo("tokenId", request.object.get("tokenId"))
logger.info(`Marketplace | Query: ${query}`)
const boughtItem = await query.first()
logger.info(`Marketplace | boughtItem: ${JSON.stringify(boughtItem)}`)
if (boughtItem) {
logger.info(`Deleting boughtItem ${boughtItem.id}`)
await boughtItem.destroy()
logger.info(
`Deleted item with tokenId ${request.object.get(
"tokenId"
)} at address ${request.object.get(
"address"
)} from ActiveItem table since it was bought.`
)
} else {
logger.info(
`No item bought with address: ${request.object.get(
"address"
)} and tokenId: ${request.object.get("tokenId")} found`
)
}
}
})
The course offered an option to push the cloud functions to Moralis v1 with the following terminal command:
moralis-admin-cli watch-cloud-folder XXXXXXXXXXXXXXX --moralisSubdomain XXXXXXXXXXXX.grandmoralis.com --autoSave 1 --moralisCloudfolder ./cloudFunctions
But this canât be done anymore with Moralis v2 so for the self hosted parse server of Moralis v2, I have moved these cloud functions to the Parse server repo available there: https://github.com/MoralisWeb3/Moralis-JS-SDK/tree/main/demos/parse-server-migration
I modified the cloud functions by basically replacing Moralis Parse in the following file: parse-server-migration/src/cloud/main.ts and as shown below:
main.ts
declare const Parse: any;
import './generated/evmApi';
import './generated/solApi';
import { requestMessage } from '../auth/authService';
const ethers = require('ethers');
Parse.Cloud.define('requestMessage', async ({ params }: any) => {
const { address, chain, networkType } = params;
const message = await requestMessage({
address,
chain,
networkType,
});
return { message };
});
Parse.Cloud.define('getPluginSpecs', () => {
// Not implemented, only excists to remove client-side errors when using the moralis-v1 package
return [];
});
Parse.Cloud.define('getServerTime', () => {
// Not implemented, only excists to remove client-side errors when using the moralis-v1 package
return null;
});
Parse.Cloud.afterSave('ItemListed', async (request: any) => {
// Every event gets triggered twice, once on unconfirmed, again on confirmed
const confirmed = request.object.get('confirmed');
const logger = Parse.Cloud.getLogger();
logger.info('Looking for confirmed TX...');
if (confirmed) {
logger.info('Found item');
const ActiveItem = Parse.Object.extend('ActiveItem');
const query = new Parse.Query(ActiveItem);
query.equalTo('nftAddress', request.object.get('nftAddress'));
query.equalTo('tokenId', request.object.get('tokenId'));
query.equalTo('marketplaceAddress', request.object.get('address'));
query.equalTo('seller', request.object.get('seller'));
const alreadyListedItem = await query.first();
if (alreadyListedItem) {
logger.info(`Deleting already listed ${request.object.get('objectId')}`);
await alreadyListedItem.destroy();
logger.info(
`Deleted item with tokenId ${request.object.get('tokenId')} at address ${request.object.get(
'address',
)} since it's already been listed`,
);
}
const activeItem = new ActiveItem();
activeItem.set('marketplaceAddress', request.object.get('address'));
activeItem.set('nftAddress', request.object.get('nftAddress'));
activeItem.set('price', request.object.get('price'));
activeItem.set('tokenId', request.object.get('tokenId'));
activeItem.set('seller', request.object.get('seller'));
logger.info(`Adding address: ${request.object.get('address')}. TokenId: ${request.object.get('tokenId')}`);
logger.info('Saving');
await activeItem.save();
}
});
Parse.Cloud.afterSave('ItemCanceled', async (request: any) => {
const confirmed = request.object.get('confirmed');
const logger = Parse.Cloud.getLogger();
logger.info(`Marketplace | Object: ${request.object}`);
if (confirmed) {
const ActiveItem = Parse.Object.extend('ActiveItem');
const query = new Parse.Query(ActiveItem);
query.equalTo('marketplaceAddress', request.object.get('address'));
query.equalTo('nftAddress', request.object.get('nftAddress'));
query.equalTo('tokenId', request.object.get('tokenId'));
logger.info(`Marketplace | Query: ${query}`);
const canceledItem = await query.first();
logger.info(`Marketplace | CanceledItem: ${canceledItem}`);
if (canceledItem) {
logger.info(
`Deleting ${request.object.get('tokenId')} at address: ${request.object.get('address')} since it was canceled`,
);
await canceledItem.destroy();
} else {
logger.info(
`No item found with address ${request.object.get('address')} and tokenId: ${request.object.get('tokenId')}`,
);
}
}
});
Parse.Cloud.afterSave('ItemBought', async (request: any) => {
const confirmed = request.object.get('confirmed');
const logger = Parse.Cloud.getLogger();
logger.info(`Marketplace | Object: ${request.object}`);
if (confirmed) {
const ActiveItem = Parse.Object.extend('ActiveItem');
const query = new Parse.Query(ActiveItem);
query.equalTo('marketplaceAddress', request.object.get('address'));
query.equalTo('nftAddress', request.object.get('nftAddress'));
query.equalTo('tokenId', request.object.get('tokenId'));
logger.info(`Marketplace | Query: ${query}`);
const boughtItem = await query.first();
logger.info(`Marketplace | BoughtItem: ${boughtItem}`);
if (boughtItem) {
logger.info(
`Deleting ${request.object.get('objectId')} at address: ${request.object.get('address')} since it was bought`,
);
await boughtItem.destroy();
} else {
logger.info(
`No item found with address ${request.object.get('address')} and tokenId: ${request.object.get('tokenId')}`,
);
}
}
});
All I could get in my MongoDB collection was the smart contracts event logs in parse.NftmarketplaceEventsLogs but I am not getting the desired new tables rows about âItemListedâ, âActiveItemâ, âItemCancelledâ, and âItemBoughtâ.
Someone created a cloud function called âwatchContractEventâ as per this forum thread Self-hosted Moralis server: local devchain & cloud functions and as shown below. I also tried to add it in my parse server repo in main.ts (parse-server-migration/src/cloud/main.ts) but no success to get the new DB table rows.
Parse.Cloud.define('watchContractEvent', async ({ params, user, ip }: any) => {
let provider: any;
if (params['chainId'] == 1337) {
provider = new ethers.providers.WebSocketProvider('http://127.0.0.1:8545/');
} else {
provider = new ethers.providers.WebSocketProvider(process.env.GOERLI_RPC_WEBS!);
}
const contractAddress = params['address'];
const contract = new ethers.Contract(contractAddress, [params['abi']], provider);
if (params['tableName'] == 'ItemBought') {
contract.on('ItemBought', (buyer: any, nftAddress: any, tokenId: any, price: any, event: any) => {
const ItemBought = Parse.Object.extend('ItemBought');
const itemBought = new ItemBought();
itemBought.set('buyer', buyer);
itemBought.set('nftAddress', nftAddress);
itemBought.set('tokenId', tokenId);
itemBought.set('price', ethers.utils.formatUnits(price, 6));
itemBought.save();
});
return { success: true };
}
if (params['tableName'] == 'ItemListed') {
contract.on('ItemListed', (seller: any, nftAddress: any, tokenId: any, price: any, event: any) => {
const ItemListed = Parse.Object.extend('ItemListed');
const itemListed = new ItemListed();
itemListed.set('seller', seller);
itemListed.set('nftAddress', nftAddress);
itemListed.set('tokenId', tokenId);
itemListed.set('price', ethers.utils.formatUnits(price, 6));
itemListed.save();
});
return { success: true };
}
if (params['tableName'] == 'ItemCanceled') {
contract.on('ItemCanceled', (seller: any, nftAddress: any, tokenId: any, event: any) => {
const ItemCanceled = Parse.Object.extend('ItemCanceled');
const itemCanceled = new ItemCanceled();
itemCanceled.set('seller', seller);
itemCanceled.set('nftAddress', nftAddress);
itemCanceled.set('tokenId', tokenId);
itemCanceled.save();
});
return { success: true };
}
return { success: false };
});