Thank you! Should I use ngrok to redirect the parse server url http://localhost:1337 or the front end url http://localhost:3000?
My assumption is that is for the parse server url
You will specify only the port initially for ngrok proxy
If the Moralis team could release a video on Youtube explaining the following itâd be great:
- Self Hosted Moralis Server as per https://www.youtube.com/watch?v=l2qTyc-V9cM
- Combined with Streams API using ngrok
- Combined with cloud functions to populate the MongoDB with Smart contracts event info from the Streams API.
Iâm also facing this issue. How did you solve this?
What is the code youâre using related to eventName
?
You can use the parsedLogs function to parse data.
There is now a tutorial video to âSet Up Self-Hosted Server and Listen to Blockchain Events in Realtimeâ
It works well to listen to smart contract events and get them into the self hosted MongoDB, however I am still unable to add cloud functions and replicate what âwatchContractEventâ was doing on Moralis-v1.
There are some elements of answers in this thread Self-hosted Moralis server: local devchain & cloud functions (e.g. someone made a âwatchContractEventâ cloud function to be able to advance the chapter 15.2 of the FCC course https://youtu.be/gyMwXuJrbJQ which uses Moralis v1) but we would require a full Moralis tutorial on cloud functions to manipulate API streams data into the self hosted the DB, create new tables etcâŚ
with what part you have issues?
what you mean with not being able to add cloud functions? you were not able to add a simple cloud function?
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 };
});
I forgot to mention that any help to resolve the above questions and make the code work for Moralis V2 would be greatly appreciated as many of us seem to be stuck with this.
Note that FCC course has 1.1M views on Youtube so itâd be very beneficial for the community and for Moralis to resolve this.
what is the main issue that you are having at this moment?
I am not able to get the new âItemListedâ, âActiveItemâ, âItemCancelledâ, and âItemBoughtâ tables in the MongoDB.
what is the difference between the table where it worked and the other tables?
There are new tables summarizing the NFTs activeItem, itemBought, itemCanceled, and itemListed.
I have attached snips from the course of what we were supposed to see in Moralis v1.
what information you have here in this table?
The only information I am currently getting in the self hosted MongoDB is coming from the Moralis web hook and it gives me the smart contract event log (parse.NftmarketplaceEventsLogs) for a NFT item I listed on the marketplace (see snip below).
But for example I am not getting the custom âitemListedâ table which is supposed to be generated with the cloud functions.
my expectation is that similar to how the data for this table was added, you can add it for any other event in a separate table, after you receive the event, you check its topic, and based on the topic or address, you use the abi to extract the data, and then you insert the data in a table in mongo db
No the Moralis stream is already listening to all the contract events (see snip below).
I am talking about the new tables activeItem, itemBought, itemCanceled, and itemListed that are
generated from the cloud functions listed here.
that I have moved into the parse server file:
Note that these customized tables typically set key fields like buyer, nftAddress, tokenId, price etc⌠as shown below:
[quote=âTristan3000, post:13, topic:20758â]
itemBought.set('buyer', buyer);
itemBought.set('nftAddress', nftAddress);
itemBought.set('tokenId', tokenId);
itemBought.set('price', ethers.utils.formatUnits(price, 6));
itemBought.save();
[/quote]
So you are using streams api here?
In this case you use the abi to decode the event I order to extract the fields and then you insert the data in mongondb.
At what step you are with those events from streams api?
There is also be a plugin for parse server and streams api.
streams API will work only with a chain that is supported, it will not work with a local devchain (just to clarify it)
here is more info about the plugin for parse server