Streams api for events with self hosted server

Hi There,
I followed the Self Hosted Moralis Server Walkthrough available at https://www.youtube.com/watch?v=l2qTyc-V9cM.
I now need to stream api for events for my self hosted server.
I believe the only tutorial available on this at this stage is available on this thread: Using event-syncs in Moralis SDK v2 & Parse Server.

I have followed it and modified the parse server files from the Self Hosted Moralis Server Walkthrough tutorial as shown with the code below:

My questions are:

  1. What webhook url should I use for my Moralis stream to listens to a smart contract event
  2. I am getting the below error in relation to the index.ts file. Should I put my event name instead and will it be recognized by Moralis?

Property ‘eventName’ does not exist on type ‘{ filter: { transaction_hash: any; log_index: any; }; update: { transaction_hash: any; log_index: any; }; }’.ts(2339)

  1. When can we expect a thorough tutorial on all this?

Thank you

Index.ts

import Moralis from 'moralis';
import express from 'express';
import cors from 'cors';
import config from './config';
import { parseServer } from './parseServer';
// @ts-ignore
import ParseServer from 'parse-server';
import http from 'http';
import { streamsSync } from '@moralisweb3/parse-server';

import { verifySignature, realtimeUpsertParams } from './helpers/utils';

export const app = express();

Moralis.start({
  apiKey: config.MORALIS_API_KEY,
});

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use(cors());

app.use(
  streamsSync(parseServer, {
    apiKey: config.MORALIS_API_KEY,
    webhookUrl: '/streams',
  }),
);

app.use(`/server`, parseServer.app);

const httpServer = http.createServer(app);
httpServer.listen(config.PORT, () => {
  // eslint-disable-next-line no-console
  console.log(`Moralis Server is running on port ${config.PORT}.`);
});
// This will enable the Live Query real-time server
ParseServer.createLiveQueryServer(httpServer);

////// Added for Moralis streams ///////

app.post('/webhooks/test', async (req: any, res: any) => {
  console.log(req.body);
  try {
    verifySignature(req, config.MORALIS_API_KEY);
    const { data, tagName, eventName }: any = parseEventData(req);
    console.log(data);
    await parseUpdate(`SFS_${eventName}`, data);
  } catch (e) {
    console.log(e);
  }
  res.send('ok');
});

export async function parseUpdate(tableName: string, object: any) {
  // Check if object exists in db

  const query = new ParseServer.Query(tableName); 
  query.equalTo('transaction_hash', object.transaction_hash);
  const result = await query.first({ useMasterKey: true });
  if (result) {
    // Loop through object's keys
    for (const key in object) {
      result.set(key, object[key]);
    }
    return result?.save(null, { useMasterKey: true });
  } else {
    // Create new object
    const newObject = new ParseServer.Object(tableName); 
    for (const key in object) {
      newObject.set(key, object[key]);
    }
    return newObject.save(null, { useMasterKey: true });
  }
}

export function parseEventData(req: any) {
  try {
    const updates: any = {};
    for (const log of req.body.logs) {
      const abi = req.body.abis[log.streamId];
      if (abi) {
        const { filter, update, eventName } = realtimeUpsertParams(abi, log, req.body.confirmed, req.body.block);
        return { data: update, tagName: log.tag, eventName };
      }
    }
  } catch (e: any) {
    console.log(e);
  }
}

and added the below file:
utils.ts

const Web3 = require('web3');
const web3 = new Web3();
// import Moralis from 'moralis';

const ethers = require('ethers');

export const verifySignature = (req: any, secret: any) => {
  const ProvidedSignature = req.headers['x-signature'];
  if (!ProvidedSignature) throw new Error('Signature not provided');
  const GeneratedSignature = web3.utils.sha3(JSON.stringify(req.body) + secret);
  if (GeneratedSignature !== ProvidedSignature) throw new Error('Invalid Signature');
};

function realtimeUpsertTxParams(trxData: any, confirmed: any, block: any) {
  const filter = {
    transaction_hash: trxData.transactionHash,
    transaction_index: trxData.transactionIndex,
  };

  const update = {
    ...trxData,
    confirmed,
    block_number: block.number,
  };
  return { filter, update };
}

export function realtimeUpsertParams(abi: any, trxData: any, confirmed: any, block: any) {
  const block_number = block.number;
  const address = trxData.address.toLowerCase();
  const transaction_hash = trxData.transactionHash.toLowerCase();
  const log_index = trxData.logIndex;
  const topics = [trxData.topic0, trxData.topic1, trxData.topic2, trxData.topic3];
  const data = trxData.data;

  const filter = {
    transaction_hash,
    log_index,
  };

  const rest: { [index: string]: string } = {
    address,
    block_number,
  };

  rest.confirmed = confirmed;

  if (abi) {
    const update = {
      ...filter,
      ...decodeWithEthers(
        abi,
        data,
        topics.filter((t) => t !== null),
      ),
      ...rest,
    };
    return { filter, update };
  }

  const update = {
    ...filter,
    ...rest,
    data,
    topic0: topics[0],
    topic1: topics[1],
    topic2: topics[2],
    topic3: topics[3],
  };

  return { filter, update };
}

function decodeWithEthers(abi: any, data: any, topics: any) {
  try {
    const iface = new ethers.utils.Interface(abi);
    const { args } = iface.parseLog({ data, topics });
    const event = iface.getEvent(topics[0]);
    const decoded: { [index: string]: string } = {};
    event.inputs.forEach((input: { [index: string]: string }, index: any) => {
      if (input.type === 'uint256') {
        /*decoded[`${input.name}_decimal`] = {
                __type: "NumberDecimal",
                value: parseInt(ethers.BigNumber.from(args[index]._hex).toString())
            };*/
        decoded[input.name] = ethers.BigNumber.from(args[index]._hex).toString();
        return;
      }
      if (input.type === 'bytes') {
        decoded[input.name] = args[index].hash;
        return;
      }
      if (input.type === 'address') {
        decoded[input.name] = args[index].toLowerCase();
        return;
      }
      decoded[input.name] = args[index];
    });
    return decoded;
  } catch (error) {
    return {};
  }
}

module.exports = {
  verifySignature,
  realtimeUpsertParams,
  realtimeUpsertTxParams,
  decodeWithEthers,
};

There is a plugin now for using streams api with a parse server

Thank you.
I have followed the tutorials and when I create a stream in Moralis I use the following webhook url:

http://localhost:1337/server/streams-webhook
But when I click on Create Streams, I get the following error:
Could not POST to http://localhost:1337/server/streams-webhook. Please check your webhook URL.

you should use ngrok as a proxy if you want to use a webhook on local host, with a proxy that web hook url can be accessed from the internet, that local host url can not be accessed from the internet directly

1 Like

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

1 Like

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.
1 Like

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 };
});
2 Likes

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