From e1fcb8235c8b2903798dd3f4aba2dbee4faebfab Mon Sep 17 00:00:00 2001 From: Prometheus Date: Thu, 21 Mar 2024 11:25:40 +0100 Subject: [PATCH 1/3] initial indexer code --- indexer.ts | 694 +++++++++++++++++++++++++++++++++++++++++++++++++++++ schema.sql | 287 ++++++++++++++++++++++ 2 files changed, 981 insertions(+) create mode 100644 indexer.ts create mode 100644 schema.sql diff --git a/indexer.ts b/indexer.ts new file mode 100644 index 0000000..d4a4f39 --- /dev/null +++ b/indexer.ts @@ -0,0 +1,694 @@ +import { Block } from "@near-lake/primitives"; +/** + * Note: We only support javascript at the moment. We will support Rust, Typescript in a further release. + */ + +/** + * getBlock(block, context) applies your custom logic to a Block on Near and commits the data to a database. + * context is a global variable that contains helper methods. + * context.db is a subfield which contains helper methods to interact with your database. + * + * Learn more about indexers here: https://docs.near.org/concepts/advanced/indexers + * + * @param {block} Block - A Near Protocol Block + */ + +async function getBlock(block: Block) { + const BASE_ACCOUNT_ID = "potlock.near"; // potlock base id to match all actions + + // this function helps match factories, deployed in the manner `version.ptofactory....` where version can be v1,v2,vn + function matchVersionPattern(text) { + const pattern = /^v\d+\.potfactory\.potlock\.near$/; + return pattern.test(text); + } + + // filter receipts in this block to make sure we're inly indexing successful actions + const receiptStatusMap = block + .receipts() + .filter( + (receipt) => + receipt.receiverId.endsWith(BASE_ACCOUNT_ID) && + (receipt.status.hasOwnProperty("SuccessValue") || + receipt.status.hasOwnProperty("SuccessReceiptId")) + ) + .map((receipt) => receipt.receiptId); + + console.log("let us see...", receiptStatusMap); + + // filter actions in this block whose receipts can be found in the list of successful receipts + // const matchingTx = block.transactions.filter( + // (tx) => tx.receiverId.endsWith(BASE_ACCOUNT_ID) && tx.status + // ); + // console.log(matchingTx); + + + // filter actions in this block whose receipts can be found in the list of successful receipts + const matchingActions = block + .actions() + .filter( + (action) => + action.receiverId.endsWith(BASE_ACCOUNT_ID) && + receiptStatusMap.includes(action.receiptId) + ); + + console.log("macthing actions rambo=====", matchingActions); + + // if there are no actions matching our conditions, then we have nothing to index in this block, unto the next. + if (!matchingActions.length) { + console.log("nothin concerns us here"); + return; + } + + const functionCallTracked = [ + "CREATE_ACCOUNT", + "deploy_pot", + "deploy_pot_callback", + "register", + "apply", + "assert_can_apply_callback", + "handle_apply", + "challenge_payouts", + "chef_set_payouts", + ]; + + async function handleNewPot( + args, + receiverId, + signerId, + predecessorId, + receiptId + ) { + let data = JSON.parse(args); + console.log("new pot data::", { ...data }, receiverId, signerId); + await context.db.Account.upsert({ id: data.owner }, ["id"], []); + await context.db.Account.upsert({ id: signerId }, ["id"], []); + + if (data.chef) { + await context.db.Account.upsert({ id: data.chef }, ["id"], []); + } + + if (data.admins) { + for (const admin in data.admins) { + await context.db.Account.upsert({ id: admin }, ["id"], []); + let pot_admin = { + pot_id: receiverId, + admin_id: admin, + }; + await context.db.PotAdmin.insert(pot_admin); + } + } + + const deploy_time = new Date( + Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) + ); + + const potObject = { + id: receiverId, + pot_factory_id: predecessorId, + deployer_id: signerId, + deployed_at: deploy_time, + source_metadata: JSON.stringify(data.source_metadata), + owner_id: data.owner, + chef_id: data.chef, + name: data.pot_name, + description: data.pot_description, + max_approved_applicants: data.max_projects, + base_currency: null, + application_start: new Date(data.application_start_ms), + application_end: new Date(data.application_end_ms), + matching_round_start: new Date(data.public_round_start_ms), + matching_round_end: new Date(data.public_round_end_ms), + registry_provider: data.registry_provider, + min_matching_pool_donation_amount: data.min_matching_pool_donation_amount, + sybil_wrapper_provider: data.sybil_wrapper_provider, + custom_sybil_checks: data.custom_sybil_checks, + custom_min_threshold_score: data.custom_min_threshold_score, + referral_fee_matching_pool_basis_points: + data.referral_fee_matching_pool_basis_points, + referral_fee_public_round_basis_points: + data.referral_fee_public_round_basis_points, + chef_fee_basis_points: data.chef_fee_basis_points, + total_matching_pool: data.total_matching_pool || "0", + total_matching_pool_usd: data.total_matching_pool_usd, + matching_pool_balance: data.matching_pool_balance || "0", + matching_pool_donations_count: data.matching_pool_donations_count || 0, + total_public_donations: data.total_public_donations || "0", + total_public_donations_usd: data.total_public_donations_usd, + public_donations_count: data.public_donations_count || 0, + cooldown_end: data.cooldown_end, + all_paid_out: false, + protocol_config_provider: data.protocol_config_provider, + }; + + await context.db.Pot.insert(potObject); + + let activity = { + signer_id: signerId, + receiver_id: receiverId, + timestamp: deploy_time, + type: "Deploy_Pot", + action_result: potObject.id, + tx_hash: receiptId, + }; + + await context.db.Activity.insert(activity); + } + + async function handleNewFactory(args, receiverId, signerId, receiptId) { + let data = JSON.parse(args); + console.log("new factory data::", { ...data }, receiverId); + // try saving concerned accounts + await context.db.Account.upsert({ id: data.owner }, ["id"], []); + await context.db.Account.upsert( + { id: data.protocol_fee_recipient_account }, + ["id"], + [] + ); + if (data.admins) { + for (const admin in data.admins) { + await context.db.Account.upsert({ id: admin }, ["id"], []); + let factory_admin = { + pot_factory_id: receiverId, + admin_id: admin, + }; + await context.db.PotFactoryAdmin.insert(factory_admin); + } + } + + if (data.whitelisted_deployers) { + for (const deployer in data.whitelisted_deployers) { + await context.db.Account.upsert({ id: deployer }, ["id"], []); + let factory_deployer = { + pot_factory_id: receiverId, + whitelisted_deployer_id: deployer, + }; + await context.db.PotFactoryWhitelistedDeployer.insert(factory_deployer); + } + } + + const deploy_time = new Date( + Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) + ); + const factory = { + id: receiverId, + owner_id: data.owner, + deployed_at: deploy_time, + source_metadata: JSON.stringify(data.source_metadata), + protocol_fee_basis_points: data.protocol_fee_basis_points, + protocol_fee_recipient_account: data.protocol_fee_recipient_account, + require_whitelist: data.require_whitelist, + }; + console.log("factory..", factory); + await context.db.PotFactory.insert(factory); + } + + // function tracks registry contracts, where projects are registered + async function handleRegistry(args, receiverId, signerId, receiptId) { + let data = JSON.parse(args); + console.log("new Registry data::", { ...data }, receiverId); + + if (data.admins) { + for (const admin in data.admins) { + await context.db.Account.upsert({ id: admin }, ["id"], []); + let list_admin = { + list_id: receiverId, + admin_id: admin, + }; + await context.db.ListAdmin.insert(list_admin); + } + } + + let regv = { + id: receiverId, + owner_id: data.owner, + default_registration_status: "Approved", // the first registry contract has approved as default, and the later changed through the admin set function call, which we also listen to, so it should self correct. + name: receiverId.split(".")[0], + tx_hash: receiptId, + }; + + + await context.db.Account.upsert({ id: data.owner }, ["id"], []); + + await context.db.List.insert(regv); + } + + // function tracks register function calls on the registry conract + async function handleNewProject(args, receiverId, signerId, receiptId) { + let data = JSON.parse(args); + console.log("new Project data::", { ...data }, receiverId); + let registry = (await context.db.List.select({ id: receiverId }))[0]; // fetch the registry so as to get default + let reg = { + registrant_id: data._project_id || signerId, + status: registry.default_registration_status, + submitted_at: new Date( + Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) + ), + }; + if (data._project_id) { + await context.db.Account.upsert({ id: data._project_id }, ["id"], []); + } + + await context.db.Account.upsert({ id: signerId }, ["id"], []); + + await context.db.ListRegistration.insert(reg); + let activity = { + signer_id: signerId, + receiver_id: receiverId, + timestamp: reg.submitted_at, + type: "Register", + action_result: reg.registrant_id, + tx_hash: receiptId, + }; + + await context.db.Activity.insert(activity); + } + + async function handleProjectRegistrationUpdate( + args, + receiverId, + signerId, + receiptId + ) { + let data = JSON.parse(args); + console.log("new Project data::", { ...data }, receiverId); + let regUpdate = { + status: data.status, + admin_notes: data.review_notes, + updated_at: new Date( + Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) + ), + }; + + await context.db.ListRegistration.update( + { registrant_id: data.project_id }, + regUpdate + ); + } + + async function handlePotApplication(args, receiverId, signerId, receiptId) { + let data = JSON.parse(args); + console.log("new factory data::", { ...data }, receiverId); + await context.db.Account.upsert({ id: data.project_id }, ["id"], []); + let application = { + pot_id: receiverId, + applicant_id: data.project_id, + message: data.message, + submitted_at: data.submitted_at, + current_status: data.status, + tx_hash: receiptId, + }; + const appl = await context.db.PotApplication.insert(application); + + let activity = { + signer_id: signerId, + receiver_id: receiverId, + timestamp: application.submitted_at, + type: "Submit_Application", + action_result: appl[0].id.toString(), // result points to the pot application created. + tx_hash: receiptId, // should we have receipt on both action and activity? + }; + + await context.db.Activity.insert(activity); + } + + async function handleApplicationStatusChange( + args, + receiverId, + signerId, + receiptId + ) { + let data = JSON.parse(args); + console.log("new factory data::", { ...data }, receiverId); + + let receipt = block + .receipts() + .filter((receipt) => receipt.receiptId == receiptId)[0]; + let update_data = JSON.parse(atob(receipt.status["SuccessValue"])); + let appl = ( + await context.db.PotApplication.select({ applicant_id: data.project_id }) + )[0]; + + let applicationReview = { + application_id: appl.id, + reviewer_id: signerId, + notes: update_data.notes, + status: update_data.status, + reviewed_at: update_data.updated_at, + }; + let applicationUpdate = { + current_status: update_data.status, + last_updated_at: update_data.updated_at, + }; + + await context.db.PotApplicationReview.insert(applicationReview); + await context.db.PotApplication.update({ id: appl.id }, applicationUpdate); + } + + async function handleDefaultListStatusChange( + args, + receiverId, + signerId, + receiptId + ) { + let data = JSON.parse(args); + console.log("update project data::", { ...data }, receiverId); + + let listUpdate = { + default_registration_status: data.status, + }; + + await context.db.List.update({ id: receiverId }, listUpdate); + } + + async function handleSettingPayout(args, receiverId, signerId, receiptId) { + let data = JSON.parse(args); + console.log("set payout data::", { ...data }, receiverId); + let payouts = data.payouts; + for (const payout in payouts) { + // general question: should we register projects as accounts? + let potPayout = { + recipient_id: payout["project_id"], + amount: payout["amount"], + ft_id: payout["ft_id"] || null, + tx_hash: receiptId, + }; + await context.db.PotPayout.insert(potPayout); + } + } + + async function handlePayout(args, receiverId, signerId, receiptId) { + let data = JSON.parse(args); + console.log("fulfill payout data::", { ...data }, receiverId); + let payout = { + recipient_id: data.project_id, + amount: data.amount, + paid_at: data.paid_at, + tx_hash: receiptId, + }; + await context.db.PotPayout.update({ recipient_id: data.project_id }, payout); + } + + async function handlePayoutChallenge(args, receiverId, signerId, receiptId) { + let data = JSON.parse(args); + console.log("set payout data::", { ...data }, receiverId); + let payoutChallenge = { + challenger_id: signerId, + pot_id: receiverId, + created_at: new Date( + Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) // convert to ms then date + ), + message: data.reason, + tx_hash: receiptId, + }; + await context.db.PotPayoutChallenge.insert(payoutChallenge); + } + + const GECKO_URL = "https://api.coingecko.com/api/v3"; + function formatDate(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + + return `${day}-${month}-${year}`; + } + + function formatToNear(yoctoAmount) { + const nearAmount = yoctoAmount / 10 ** 24; + return nearAmount; + } + + async function handleNewDonations( + args, + receiverId, + signerId, + actionName, + receiptId + ) { + let matching_pool = JSON.parse(args).matching_pool + if ((actionName == "donate" && receiverId != "donate.potlock.near") || matching_pool) { + console.log("calling donate on projects..."); + return; + } + console.log("action and recv", actionName, receiverId); + let receipt = block + .receipts() + .filter((receipt) => receipt.receiptId == receiptId)[0]; + let donation_data = JSON.parse(atob(receipt.status["SuccessValue"])); + console.log("result arg...:", donation_data); + let donated_at = new Date(donation_data.donated_at || donation_data.donated_at_ms); + await context.db.Account.upsert({ id: donation_data.donor_id }, ["id"], []); + let endpoint = `${GECKO_URL}/coins/${ + donation_data.ft_id || "near" + }/history?date=${formatDate(donated_at)}&localization=false`; + let response = await fetch(endpoint); + let data = await response.json(); + let unit_price = data.market_data?.current_price.usd; + + let total_amount = donation_data.total_amount; + let net_amount = ( + BigInt(donation_data.total_amount) - BigInt(donation_data.protocol_fee) + ).toString(); + + let totalnearAMount = formatToNear(total_amount); + let netnearAMount = formatToNear(net_amount); + let total_amount_usd = unit_price * totalnearAMount + let net_amount_usd = unit_price * netnearAMount + + console.log("feram..", unit_price, totalnearAMount, total_amount, net_amount, total_amount_usd, net_amount_usd) + + let donation = { + donor_id: donation_data.donor_id, + total_amount, + total_amount_usd, + net_amount_usd, + net_amount, + ft_id: donation_data.ft_id || null, + message: donation_data.message, + donated_at, + matching_pool: donation_data.matching_pool || false, + recipient_id: donation_data.project_id || donation_data.recipient_id, + protocol_fee: donation_data.protocol_fee, + referrer_id: donation_data.referrer_id, + referrer_fee: donation_data.referrer_fee, + tx_hash: receiptId, + }; + await context.db.Donation.insert(donation); + + if (actionName != "donate") { + let pot = (await context.db.Pot.select({ id: receiverId}))[0] + donation["pot_id"] = pot.id + let potUpdate = { + total_public_donations_usd: pot.total_public_donations_usd || 0 + total_amount_usd, + total_public_donations: pot.total_public_donations || 0 + total_amount, + } + if (donation_data.matching_pool) { + potUpdate["total_matching_pool_usd"] = pot.total_matching_pool_usd || 0 + total_amount_usd + potUpdate["total_matching_pool"] = pot.total_matching_pool || 0 + total_amount + potUpdate["matching_pool_donations_count"] = pot.matching_pool_donations_count || 0 + 1 + let accountUpdate = { + + } + } else { + potUpdate["public_donations_count"] = pot.public_donations_count || 0 + 1 + } + await context.db.Pot.update({id: pot.id}, potUpdate) + } + + let recipient = donation_data.project_id || donation_data.recipient_id + + if (recipient) { + let acct = (await context.db.Account.select({ id: recipient }))[0] + console.log("selected acct", acct) + let acctUpdate = { + total_donations_usd: acct.total_donations_usd || 0 + total_amount_usd, + donors_count: acct.donors_count || 0 + 1 + } + await context.db.Account.update({ id: recipient }, acctUpdate) + } + + let activity = { + signer_id: signerId, + receiver_id: receiverId, + timestamp: donation.donated_at, + type: + actionName == "donate" + ? "Donate_Direct" + : donation.matching_pool + ? "Donate_Pot_Matching_Pool" + : "Donate_Pot_Public", + action_result: recipient, + tx_hash: receiptId, + }; + + await context.db.Activity.insert(activity); + } + + // map through the successful actions and swittch on the methodName called for each, then call the designated handlerFunction. + await Promise.all( + matchingActions.flatMap((action) => { + action.operations.map(async (operation) => { + console.log("see the contents here...:,==", operation["FunctionCall"]); + let call = operation["FunctionCall"]; + if (call) { + const args = atob(call.args); // decode function call argument + switch (call.methodName) { + case "new": + // new can be called on a couple of things, if the recever id matches factory pattern, then it's a new factory, else if it matches the registry account id, then it's a registry contract initialization, else, it's a new pot initialization. + matchVersionPattern(action.receiverId) + ? await handleNewFactory( + args, + action.receiverId, + action.signerId, + action.receiptId + ) + : action.receiverId == "registry.potlock.near" // initializing registry + ? await handleRegistry( + args, + action.receiverId, + action.signerId, + action.receiptId + ) + : await handleNewPot( + args, + action.receiverId, + action.signerId, + action.predecessorId, + action.receiptId + ); + break; + // this is the callback after user applies to a certain pot, it can either be this or handle_apply + case "assert_can_apply_callback": + console.log("application case:", JSON.parse(args)); + await handlePotApplication( + args, + action.receiverId, + action.signerId, + action.receiptId + ); + break; + case "handle_apply": + console.log("application case 2:", JSON.parse(args)); + break; + + // if function call is donate, call the handle new donations function + case "donate": + console.log("donatons to project incoming:", JSON.parse(args)); + await handleNewDonations( + args, + action.receiverId, + action.signerId, + "donate", + action.receiptId + ); + break; + + // this is a form of donation where user calls donate on a pot + case "handle_protocol_fee_callback": + console.log("donations to pool incoming:", JSON.parse(args)); + await handleNewDonations( + args, + action.receiverId, + action.signerId, + "handle_protocol_fee_callback", + action.receiptId + ); + break; + // this handles project/list registration + case "register": + console.log("registrations incoming:", JSON.parse(args)); + await handleNewProject( + args, + action.receiverId, + action.signerId, + action.receiptId + ); + break; + + // chefs approve/decline ... etc a projects application to a pot + case "chef_set_application_status": + console.log( + "application status change incoming:", + JSON.parse(args) + ); + await handleApplicationStatusChange( + args, + action.receiverId, + action.signerId, + action.receiptId + ); + break; + + // registries can have default status for projects + case "admin_set_default_project_status": + console.log( + "registry default status setting incoming:", + JSON.parse(args) + ); + await handleDefaultListStatusChange( + args, + action.receiverId, + action.signerId, + action.receiptId + ); + break; + + // admins can set a project's status + case "admin_set_project_status": + console.log( + "project registration status update incoming:", + JSON.parse(args) + ); + await handleProjectRegistrationUpdate( + args, + action.receiverId, + action.signerId, + action.receiptId + ); + break; + + // fires when chef set payouts + case "chef_set_payouts": + console.log( + "setting payot....:", + JSON.parse(args) + ); + await handleSettingPayout( + args, + action.receiverId, + action.signerId, + action.receiptId + ); + break; + + // fires when there is a payout challenge + case "challenge_payouts": + console.log( + "challenge payout:", + JSON.parse(args) + ); + await handlePayoutChallenge( + args, + action.receiverId, + action.signerId, + action.receiptId + ); + break; + // fires when fulfilling payouts + case "transfer_payout_callback": + console.log( + "fulfilling payouts.....", + JSON.parse(args) + ); + await handlePayout( + args, + action.receiverId, + action.signerId, + action.receiptId + ); + break; + } + } + }); + }) + ); +} diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..f38c1ed --- /dev/null +++ b/schema.sql @@ -0,0 +1,287 @@ +CREATE TABLE + account ( + id VARCHAR PRIMARY KEY, + total_donations_usd DECIMAL(10, 2), + total_matching_pool_allocations_usd DECIMAL(10, 2), + donors_count INT + ); + +CREATE TABLE + list ( + id VARCHAR PRIMARY KEY, + owner_id VARCHAR NOT NULL, + name VARCHAR NOT NULL, + description TEXT, + default_registration_status ENUM( + 'Pending', + 'Approved', + 'Rejected', + 'Graylisted', + 'Blacklisted' + ) NOT NULL, + FOREIGN KEY (owner_id) REFERENCES account (id) + ); + +CREATE TABLE + list_admin ( + list_id INT NOT NULL, + admin_id VARCHAR NOT NULL, + PRIMARY KEY (list_id, admin_id), + FOREIGN KEY (list_id) REFERENCES list (id), + FOREIGN KEY (admin_id) REFERENCES account (id) + ); + +CREATE TABLE + list_registration ( + id SERIAL PRIMARY KEY, + registrant_id VARCHAR NOT NULL, + status ENUM( + 'Pending', + 'Approved', + 'Rejected', + 'Graylisted', + 'Blacklisted' + ) NOT NULL, + submitted_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP, + registrant_notes TEXT, + admin_notes TEXT, + tx_hash VARCHAR, + FOREIGN KEY (registrant_id) REFERENCES account (id) + ); + +CREATE TABLE + pot_factory ( + id VARCHAR PRIMARY KEY, + owner_id VARCHAR NOT NULL, + deployed_at TIMESTAMP NOT NULL, + source_metadata VARCHAR NOT NULL, + protocol_fee_basis_points INT NOT NULL, + protocol_fee_recipient_account VARCHAR NOT NULL, + require_whitelist BOOLEAN NOT NULL, + FOREIGN KEY (owner_id) REFERENCES account (id), + FOREIGN KEY (protocol_fee_recipient_account) REFERENCES account (id) + ); + +CREATE TABLE + pot_factory_admin ( + pot_factory_id INT NOT NULL, + admin_id VARCHAR NOT NULL, + PRIMARY KEY (pot_factory_id, admin_id), + FOREIGN KEY (pot_factory_id) REFERENCES pot_factory (id), + FOREIGN KEY (admin_id) REFERENCES account (id) + ); + +CREATE TABLE + pot_factory_whitelisted_deployer ( + pot_factory_id INT NOT NULL, + whitelisted_deployer_id VARCHAR NOT NULL, + PRIMARY KEY (pot_factory_id, whitelisted_deployer_id), + FOREIGN KEY (pot_factory_id) REFERENCES pot_factory (id), + FOREIGN KEY (whitelisted_deployer_id) REFERENCES account (id) + ); + +CREATE TABLE + pot ( + id VARCHAR PRIMARY KEY, + pot_factory_id INT NOT NULL, + deployer_id VARCHAR NOT NULL, + deployed_at TIMESTAMP NOT NULL, + source_metadata VARCHAR NOT NULL, + owner_id VARCHAR NOT NULL, + chef_id VARCHAR NULL, + name TEXT NOT NULL, + description TEXT NOT NULL, + max_approved_applicants INT NOT NULL, + base_currency VARCHAR NULL, + application_start TIMESTAMP NOT NULL, + application_end TIMESTAMP NOT NULL, + matching_round_start TIMESTAMP NOT NULL, + matching_round_end TIMESTAMP NOT NULL, + registry_provider VARCHAR NULL, + min_matching_pool_donation_amount VARCHAR NOT NULL, + sybil_wrapper_provider VARCHAR NULL, + custom_sybil_checks VARCHAR NULL, + custom_min_threshold_score INT NULL, + referral_fee_matching_pool_basis_points INT NOT NULL, + referral_fee_public_round_basis_points INT NOT NULL, + chef_fee_basis_points INT NOT NULL, + total_matching_pool VARCHAR NOT NULL, + total_matching_pool_usd DECIMAL(10, 2) NULL, + matching_pool_balance VARCHAR NOT NULL, + matching_pool_donations_count INT NOT NULL, + total_public_donations VARCHAR NOT NULL, + total_public_donations_usd DECIMAL(10, 2) NULL, + public_donations_count INT NOT NULL, + cooldown_end TIMESTAMP NULL, + all_paid_out BOOLEAN NOT NULL, + protocol_config_provider VARCHAR NULL, + FOREIGN KEY (pot_factory_id) REFERENCES pot_factory (id), + FOREIGN KEY (deployer_id) REFERENCES account (id), + FOREIGN KEY (owner_id) REFERENCES account (id), + FOREIGN KEY (chef_id) REFERENCES account (id), + FOREIGN KEY (base_currency) REFERENCES account (id) + ); + +-- Table pot_application +CREATE TABLE + pot_application ( + id SERIAL PRIMARY KEY, + pot_id INT NOT NULL, + applicant_id VARCHAR NOT NULL, + message TEXT, + current_status ENUM('Pending', 'Approved', 'Rejected', 'InReview') NOT NULL, + submitted_at TIMESTAMP NOT NULL, + last_updated_at TIMESTAMP, + tx_hash VARCHAR, + FOREIGN KEY (pot_id) REFERENCES pot (id), + FOREIGN KEY (applicant_id) REFERENCES account (id) + ); + +-- Table pot_application_review +CREATE TABLE + pot_application_review ( + id SERIAL PRIMARY KEY, + application_id INT NOT NULL, + reviewer_id VARCHAR NOT NULL, + notes TEXT, + status ENUM('Pending', 'Approved', 'Rejected', 'InReview') NOT NULL, + reviewed_at TIMESTAMP NOT NULL, + tx_hash VARCHAR, + FOREIGN KEY (application_id) REFERENCES pot_application (id), + FOREIGN KEY (reviewer_id) REFERENCES account (id) + ); + +-- Table pot_payout +CREATE TABLE + pot_payout ( + id SERIAL PRIMARY KEY, + recipient_id VARCHAR NOT NULL, + amount VARCHAR NOT NULL, + amount_usd DECIMAL(10, 2), + ft_id VARCHAR NOT NULL, + paid_at TIMESTAMP, + tx_hash VARCHAR, + FOREIGN KEY (recipient_id) REFERENCES account (id) + ); + +-- Table pot_payout_challenge +CREATE TABLE + pot_payout_challenge ( + id SERIAL PRIMARY KEY, + challenger_id VARCHAR NOT NULL, + pot_id INT NOT NULL, + created_at TIMESTAMP NOT NULL, + message TEXT NOT NULL, + tx_hash VARCHAR, + FOREIGN KEY (challenger_id) REFERENCES account (id), + FOREIGN KEY (pot_id) REFERENCES pot (id) + ); + +-- Table pot_payout_challenge_admin_response +CREATE TABLE + pot_payout_challenge_admin_response ( + id SERIAL PRIMARY KEY, + challenge_id INT NOT NULL, + admin_id VARCHAR NOT NULL, + created_at TIMESTAMP NOT NULL, + message TEXT, + resolved BOOL NOT NULL, + tx_hash VARCHAR, + FOREIGN KEY (challenge_id) REFERENCES pot_payout_challenge (id), + FOREIGN KEY (admin_id) REFERENCES account (id) + ); + +-- Table donation +CREATE TABLE + donation ( + id SERIAL PRIMARY KEY, + donor_id VARCHAR NOT NULL, + total_amount VARCHAR NOT NULL, + total_amount_usd DECIMAL(10, 2), + net_amount VARCHAR NOT NULL, + net_amount_usd DECIMAL(10, 2), + ft_id VARCHAR, + pot_id INT, + matching_pool BOOLEAN NOT NULL, + message TEXT, + donated_at TIMESTAMP NOT NULL, + recipient_id VARCHAR, + protocol_fee VARCHAR NOT NULL, + protocol_fee_usd DECIMAL(10, 2), + referrer_id VARCHAR, + referrer_fee VARCHAR, + referrer_fee_usd DECIMAL(10, 2), + chef_id VARCHAR, + chef_fee VARCHAR, + chef_fee_usd DECIMAL(10, 2), + tx_hash VARCHAR, + FOREIGN KEY (donor_id) REFERENCES account (id), + FOREIGN KEY (pot_id) REFERENCES pot (id), + FOREIGN KEY (recipient_id) REFERENCES account (id) + ); + +-- Table activity +CREATE TABLE + activity ( + id SERIAL PRIMARY KEY, + signer_id VARCHAR NOT NULL, + receiver_id VARCHAR NOT NULL, + timestamp TIMESTAMP NOT NULL, + action_result VARCHAR, + tx_hash VARCHAR, + type + ENUM( + 'Donate_Direct', + 'Donate_Pot_Public', + 'Donate_Pot_Matching_Pool', + 'Register', + 'Deploy_Pot', + 'Process_Payouts', + 'Challenge_Payout', + 'Submit_Application', + 'Update_Pot_Config', + 'Add_List_Admin', + 'Remove_List_Admin' + ) NOT NULL + ); + +CREATE TABLE + pot_admin ( + pot_id INT NOT NULL, + admin_id VARCHAR NOT NULL, + PRIMARY KEY (pot_id, admin_id), + FOREIGN KEY (pot_id) REFERENCES pot (id), + FOREIGN KEY (admin_id) REFERENCES account (id) + ); + +-- pot index + +-- CREATE INDEX "deploy_time_idx" ON pot (deployed_at); +CREATE INDEX idx_pot_application_start ON pot (application_start); +CREATE INDEX idx_pot_application_end ON pot (application_end); +CREATE INDEX "idx_pot_deployer_id" ON pot (deployer_id); + +-- pot application index + +CREATE INDEX idx_pot_application_pot_id ON pot_application (pot_id); +CREATE INDEX idx_pot_application_applicant_id ON pot_application (applicant_id); +CREATE INDEX idx_pot_application_submitted_at ON pot_application (submitted_at); + + +-- payout index +CREATE INDEX idx_pot_payout_recipient_id ON pot_payout (recipient_id); + +-- donation index +CREATE INDEX idx_donation_donor_id ON donation (donor_id); +CREATE INDEX idx_donation_pot_id ON donation (pot_id); +CREATE INDEX idx_donation_donated_at ON donation (donated_at); + +-- activity index +CREATE INDEX idx_activity_timestamp ON activity (timestamp); + +-- CREATE INDEX idx_pot_payout_recipient_id ON pot_payout (recipient_id); +-- CREATE INDEX idx_pot_payout_ft_id ON pot_payout (ft_id); +CREATE INDEX idx_pot_payout_paid_at ON pot_payout (paid_at); + + From 9a2c268db687a60edb8a7d51e68bc01f2abe6eba Mon Sep 17 00:00:00 2001 From: Prometheus Date: Thu, 21 Mar 2024 12:41:40 +0100 Subject: [PATCH 2/3] remove admins --- indexer.ts | 67 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/indexer.ts b/indexer.ts index d4a4f39..1d89b0d 100644 --- a/indexer.ts +++ b/indexer.ts @@ -15,7 +15,6 @@ import { Block } from "@near-lake/primitives"; async function getBlock(block: Block) { const BASE_ACCOUNT_ID = "potlock.near"; // potlock base id to match all actions - // this function helps match factories, deployed in the manner `version.ptofactory....` where version can be v1,v2,vn function matchVersionPattern(text) { const pattern = /^v\d+\.potfactory\.potlock\.near$/; @@ -390,17 +389,52 @@ async function getBlock(block: Block) { async function handlePayoutChallenge(args, receiverId, signerId, receiptId) { let data = JSON.parse(args); - console.log("set payout data::", { ...data }, receiverId); + console.log("challenging payout..: ", { ...data }, receiverId); + let created_at = new Date( + Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) // convert to ms then date + ) let payoutChallenge = { challenger_id: signerId, pot_id: receiverId, - created_at: new Date( - Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) // convert to ms then date - ), + created_at, message: data.reason, tx_hash: receiptId, }; await context.db.PotPayoutChallenge.insert(payoutChallenge); + + let activity = { + signer_id: signerId, + receiver_id: receiverId, + timestamp: created_at, + type: "Challenge_Payout", + action_result: receiverId, + tx_hash: receiptId, + }; + + await context.db.Activity.insert(activity); + } + + async function handleListAdminRemoval(args, receiverId, signerId, receiptId) { + let data = JSON.parse(args); + console.log("removing admin...: ", { ...data }, receiverId); + let ts = new Date( + Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) // convert to ms then date + ) + let list = await context.db.List.select({id: receiverId}) + + for (const acct in data.admins) { + await context.db.ListAdmin.delete({ list_id: list[0].id, admin_id: acct}) + } + + let activity = { + signer_id: signerId, + receiver_id: receiverId, + timestamp: ts, + type: "Remove_List_Admin", + tx_hash: receiptId, + }; + + await context.db.Activity.insert(activity); } const GECKO_URL = "https://api.coingecko.com/api/v3"; @@ -412,6 +446,7 @@ async function getBlock(block: Block) { return `${day}-${month}-${year}`; } + // helper function to format function formatToNear(yoctoAmount) { const nearAmount = yoctoAmount / 10 ** 24; return nearAmount; @@ -443,6 +478,11 @@ async function getBlock(block: Block) { let response = await fetch(endpoint); let data = await response.json(); let unit_price = data.market_data?.current_price.usd; + if (!unit_price) { + console.log("api rate limi?::", data) + unit_price = parseFloat(localStorage.getItem("last_price")) + } + localStorage.setItem("last_price", unit_price.toString()) // store last price incase subsequent price fetch fails, does it support local storage? let total_amount = donation_data.total_amount; let net_amount = ( @@ -453,9 +493,6 @@ async function getBlock(block: Block) { let netnearAMount = formatToNear(net_amount); let total_amount_usd = unit_price * totalnearAMount let net_amount_usd = unit_price * netnearAMount - - console.log("feram..", unit_price, totalnearAMount, total_amount, net_amount, total_amount_usd, net_amount_usd) - let donation = { donor_id: donation_data.donor_id, total_amount, @@ -474,7 +511,7 @@ async function getBlock(block: Block) { }; await context.db.Donation.insert(donation); - if (actionName != "donate") { + if (actionName != "donate") { // update pot data such as public donations and matchingpool let pot = (await context.db.Pot.select({ id: receiverId}))[0] donation["pot_id"] = pot.id let potUpdate = { @@ -686,6 +723,18 @@ async function getBlock(block: Block) { action.receiptId ); break; + case "owner_remove_admins": + console.log( + "setting payot....:", + JSON.parse(args) + ); + await handleListAdminRemoval( + args, + action.receiverId, + action.signerId, + action.receiptId + ); + break; } } }); From 54d726cd5c305c02014e44ceb6a0a64105d8451e Mon Sep 17 00:00:00 2001 From: Prometheus Date: Wed, 27 Mar 2024 15:30:48 +0100 Subject: [PATCH 3/3] updates --- indexer.ts | 325 ++++++++++++++++++++++++++++++++--------------------- schema.sql | 99 ++++++++++------ 2 files changed, 264 insertions(+), 160 deletions(-) diff --git a/indexer.ts b/indexer.ts index 1d89b0d..ced3cc1 100644 --- a/indexer.ts +++ b/indexer.ts @@ -58,6 +58,10 @@ async function getBlock(block: Block) { return; } + const created_at = new Date( + Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) + ); + const functionCallTracked = [ "CREATE_ACCOUNT", "deploy_pot", @@ -97,22 +101,18 @@ async function getBlock(block: Block) { } } - const deploy_time = new Date( - Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) - ); - const potObject = { id: receiverId, pot_factory_id: predecessorId, deployer_id: signerId, - deployed_at: deploy_time, + deployed_at: created_at, source_metadata: JSON.stringify(data.source_metadata), owner_id: data.owner, chef_id: data.chef, name: data.pot_name, description: data.pot_description, max_approved_applicants: data.max_projects, - base_currency: null, + base_currency: "near", application_start: new Date(data.application_start_ms), application_end: new Date(data.application_end_ms), matching_round_start: new Date(data.public_round_start_ms), @@ -120,21 +120,21 @@ async function getBlock(block: Block) { registry_provider: data.registry_provider, min_matching_pool_donation_amount: data.min_matching_pool_donation_amount, sybil_wrapper_provider: data.sybil_wrapper_provider, - custom_sybil_checks: data.custom_sybil_checks, + custom_sybil_checks: data.custom_sybil_checks ? JSON.stringify(data.custom_sybil_checks) : null, custom_min_threshold_score: data.custom_min_threshold_score, referral_fee_matching_pool_basis_points: data.referral_fee_matching_pool_basis_points, referral_fee_public_round_basis_points: data.referral_fee_public_round_basis_points, chef_fee_basis_points: data.chef_fee_basis_points, - total_matching_pool: data.total_matching_pool || "0", + total_matching_pool: "0", total_matching_pool_usd: data.total_matching_pool_usd, - matching_pool_balance: data.matching_pool_balance || "0", - matching_pool_donations_count: data.matching_pool_donations_count || 0, - total_public_donations: data.total_public_donations || "0", + matching_pool_balance: "0", + matching_pool_donations_count: 0, + total_public_donations: "0", total_public_donations_usd: data.total_public_donations_usd, - public_donations_count: data.public_donations_count || 0, - cooldown_end: data.cooldown_end, + public_donations_count: 0, + cooldown_period_ms: null, all_paid_out: false, protocol_config_provider: data.protocol_config_provider, }; @@ -144,9 +144,9 @@ async function getBlock(block: Block) { let activity = { signer_id: signerId, receiver_id: receiverId, - timestamp: deploy_time, + timestamp: created_at, type: "Deploy_Pot", - action_result: potObject.id, + action_result: JSON.stringify(potObject), tx_hash: receiptId, }; @@ -184,14 +184,10 @@ async function getBlock(block: Block) { await context.db.PotFactoryWhitelistedDeployer.insert(factory_deployer); } } - - const deploy_time = new Date( - Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) - ); const factory = { id: receiverId, owner_id: data.owner, - deployed_at: deploy_time, + deployed_at: created_at, source_metadata: JSON.stringify(data.source_metadata), protocol_fee_basis_points: data.protocol_fee_basis_points, protocol_fee_recipient_account: data.protocol_fee_recipient_account, @@ -203,14 +199,17 @@ async function getBlock(block: Block) { // function tracks registry contracts, where projects are registered async function handleRegistry(args, receiverId, signerId, receiptId) { - let data = JSON.parse(args); + let receipt = block + .receipts() + .filter((receipt) => receipt.receiptId == receiptId)[0]; + let data = JSON.parse(atob(receipt.status["SuccessValue"])); console.log("new Registry data::", { ...data }, receiverId); if (data.admins) { for (const admin in data.admins) { - await context.db.Account.upsert({ id: admin }, ["id"], []); + await context.db.Account.upsert({ id: data.admins[admin] }, ["id"], []); let list_admin = { - list_id: receiverId, + list_id: data.id, admin_id: admin, }; await context.db.ListAdmin.insert(list_admin); @@ -218,11 +217,16 @@ async function getBlock(block: Block) { } let regv = { - id: receiverId, + id: data.id, owner_id: data.owner, - default_registration_status: "Approved", // the first registry contract has approved as default, and the later changed through the admin set function call, which we also listen to, so it should self correct. - name: receiverId.split(".")[0], + default_registration_status: data.default_registration_status, // the first registry contract has approved as default, and the later changed through the admin set function call, which we also listen to, so it should self correct. + name: data.name, + description: data.description, + cover_image_url: data.cover_image_url, + admin_only_registrations: data.admin_only_registrations, tx_hash: receiptId, + created_at: data.created_at, + updated_at: data.updated_at }; @@ -235,27 +239,37 @@ async function getBlock(block: Block) { async function handleNewProject(args, receiverId, signerId, receiptId) { let data = JSON.parse(args); console.log("new Project data::", { ...data }, receiverId); - let registry = (await context.db.List.select({ id: receiverId }))[0]; // fetch the registry so as to get default - let reg = { - registrant_id: data._project_id || signerId, - status: registry.default_registration_status, - submitted_at: new Date( - Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) - ), - }; - if (data._project_id) { - await context.db.Account.upsert({ id: data._project_id }, ["id"], []); - } - + let receipt = block + .receipts() + .filter((receipt) => receipt.receiptId == receiptId)[0]; + let reg_data = JSON.parse(atob(receipt.status["SuccessValue"])); + // let array_data = Array.isArray(reg_data) ? reg_data : [reg_data] + let project_list = [] + let insert_data = reg_data.map((dt) => { + project_list.push({id: dt.registrant_id}); + return { + id: dt.id, + registrant_id: dt.registrant_id, + list_id: dt.list_id, + status: dt.status, + submitted_at: dt.submitted_ms, + updated_at: dt.updated_ms, + registered_by: dt.registered_by, + admin_notes: dt.admin_notes, + registrant_notes: dt.registrant_notes, + tx_hash: receiptId + }; + }) + await context.db.Account.upsert(project_list, ["id"], []); await context.db.Account.upsert({ id: signerId }, ["id"], []); - await context.db.ListRegistration.insert(reg); + await context.db.ListRegistration.insert(insert_data); let activity = { signer_id: signerId, receiver_id: receiverId, - timestamp: reg.submitted_at, - type: "Register", - action_result: reg.registrant_id, + timestamp: insert_data[0].submitted_at, + type: "Register_Batch", + action_result: JSON.stringify(reg_data), tx_hash: receiptId, }; @@ -273,9 +287,7 @@ async function getBlock(block: Block) { let regUpdate = { status: data.status, admin_notes: data.review_notes, - updated_at: new Date( - Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) - ), + updated_at: created_at, }; await context.db.ListRegistration.update( @@ -286,24 +298,28 @@ async function getBlock(block: Block) { async function handlePotApplication(args, receiverId, signerId, receiptId) { let data = JSON.parse(args); - console.log("new factory data::", { ...data }, receiverId); + let receipt = block + .receipts() + .filter((receipt) => receipt.receiptId == receiptId)[0]; + let appl_data = JSON.parse(atob(receipt.status["SuccessValue"])); + console.log("new pot application data::", { ...data }, receiverId); await context.db.Account.upsert({ id: data.project_id }, ["id"], []); let application = { pot_id: receiverId, - applicant_id: data.project_id, - message: data.message, - submitted_at: data.submitted_at, - current_status: data.status, + applicant_id: appl_data.project_id, + message: appl_data.message, + submitted_at: appl_data.submitted_at, + status: appl_data.status, tx_hash: receiptId, }; - const appl = await context.db.PotApplication.insert(application); + await context.db.PotApplication.insert(application); let activity = { signer_id: signerId, receiver_id: receiverId, timestamp: application.submitted_at, type: "Submit_Application", - action_result: appl[0].id.toString(), // result points to the pot application created. + action_result: JSON.stringify(appl_data), // result points to the pot application created. tx_hash: receiptId, // should we have receipt on both action and activity? }; @@ -317,7 +333,7 @@ async function getBlock(block: Block) { receiptId ) { let data = JSON.parse(args); - console.log("new factory data::", { ...data }, receiverId); + console.log("pot application update data::", { ...data }, receiverId); let receipt = block .receipts() @@ -333,6 +349,7 @@ async function getBlock(block: Block) { notes: update_data.notes, status: update_data.status, reviewed_at: update_data.updated_at, + tx_hash: receiptId }; let applicationUpdate = { current_status: update_data.status, @@ -356,7 +373,35 @@ async function getBlock(block: Block) { default_registration_status: data.status, }; - await context.db.List.update({ id: receiverId }, listUpdate); + await context.db.List.update({ id: data.list_id || receiverId }, listUpdate); + } + + async function handleUpVoting( + args, + receiverId, + signerId, + receiptId + ) { + let data = JSON.parse(args); + console.log("upvote list::", { ...data }, receiverId); + + let listUpVote = { + list_id: data.list_id, + account_id: signerId, + created_at + }; + + let activity = { + signer_id: signerId, + receiver_id: receiverId, + timestamp: created_at, + type: "Upvote", + action_result: JSON.stringify(data), + tx_hash: receiptId, + }; + + await context.db.List.update({ id: data.list_id || receiverId }, listUpVote); + await context.db.Activity.insert(activity); } async function handleSettingPayout(args, receiverId, signerId, receiptId) { @@ -368,7 +413,7 @@ async function getBlock(block: Block) { let potPayout = { recipient_id: payout["project_id"], amount: payout["amount"], - ft_id: payout["ft_id"] || null, + ft_id: payout["ft_id"] || "near", tx_hash: receiptId, }; await context.db.PotPayout.insert(potPayout); @@ -377,11 +422,12 @@ async function getBlock(block: Block) { async function handlePayout(args, receiverId, signerId, receiptId) { let data = JSON.parse(args); + data = data.payout console.log("fulfill payout data::", { ...data }, receiverId); let payout = { recipient_id: data.project_id, amount: data.amount, - paid_at: data.paid_at, + paid_at: data.paid_at || created_at, tx_hash: receiptId, }; await context.db.PotPayout.update({ recipient_id: data.project_id }, payout); @@ -407,7 +453,7 @@ async function getBlock(block: Block) { receiver_id: receiverId, timestamp: created_at, type: "Challenge_Payout", - action_result: receiverId, + action_result: JSON.stringify(payoutChallenge), tx_hash: receiptId, }; @@ -417,19 +463,16 @@ async function getBlock(block: Block) { async function handleListAdminRemoval(args, receiverId, signerId, receiptId) { let data = JSON.parse(args); console.log("removing admin...: ", { ...data }, receiverId); - let ts = new Date( - Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) // convert to ms then date - ) - let list = await context.db.List.select({id: receiverId}) + let list = await context.db.List.select({ id: receiverId }) for (const acct in data.admins) { - await context.db.ListAdmin.delete({ list_id: list[0].id, admin_id: acct}) + await context.db.ListAdmin.delete({ list_id: list[0].id, admin_id: acct }) } let activity = { signer_id: signerId, receiver_id: receiverId, - timestamp: ts, + timestamp: created_at, type: "Remove_List_Admin", tx_hash: receiptId, }; @@ -459,47 +502,64 @@ async function getBlock(block: Block) { actionName, receiptId ) { - let matching_pool = JSON.parse(args).matching_pool - if ((actionName == "donate" && receiverId != "donate.potlock.near") || matching_pool) { + let donation_data; + if ((actionName == "direct")) { console.log("calling donate on projects..."); - return; + let event = block.events().filter((evt) => evt.relatedReceiptId == receiptId) + if (!event.length) { return } + const event_data = event[0].rawEvent.standard + console.log("pioper..", event_data) + donation_data = JSON.parse(event_data).data[0].donation + console.log("DODONDAWA..", donation_data) + } else { + let receipt = block + .receipts() + .filter((receipt) => receipt.receiptId == receiptId)[0]; + donation_data = JSON.parse(atob(receipt.status["SuccessValue"])); } console.log("action and recv", actionName, receiverId); - let receipt = block - .receipts() - .filter((receipt) => receipt.receiptId == receiptId)[0]; - let donation_data = JSON.parse(atob(receipt.status["SuccessValue"])); + console.log("result arg...:", donation_data); let donated_at = new Date(donation_data.donated_at || donation_data.donated_at_ms); await context.db.Account.upsert({ id: donation_data.donor_id }, ["id"], []); - let endpoint = `${GECKO_URL}/coins/${ - donation_data.ft_id || "near" - }/history?date=${formatDate(donated_at)}&localization=false`; - let response = await fetch(endpoint); - let data = await response.json(); - let unit_price = data.market_data?.current_price.usd; - if (!unit_price) { - console.log("api rate limi?::", data) - unit_price = parseFloat(localStorage.getItem("last_price")) + let endpoint = `${GECKO_URL}/coins/${donation_data.ft_id || "near" + }/history?date=${formatDate(donated_at)}&localization=false`; + let unit_price; + try { + let response = await fetch(endpoint); + let data = await response.json(); + unit_price = data.market_data?.current_price.usd; + let hist_data = { + token_id: donation_data.ft_id || "near", + last_updated: created_at, + historical_price: unit_price + } + await context.db.TokenHistoricalData.upsert(hist_data, ["token_id"], ["historical_price"]) + } catch (err) { + console.log("api rate limi?::", err) + let historica_price = await context.db.TokenHistoricalData.select({ token_id: donation_data.ft_id || "near" }) + unit_price = historica_price[0].historical_price } - localStorage.setItem("last_price", unit_price.toString()) // store last price incase subsequent price fetch fails, does it support local storage? let total_amount = donation_data.total_amount; - let net_amount = ( - BigInt(donation_data.total_amount) - BigInt(donation_data.protocol_fee) - ).toString(); - - let totalnearAMount = formatToNear(total_amount); - let netnearAMount = formatToNear(net_amount); - let total_amount_usd = unit_price * totalnearAMount - let net_amount_usd = unit_price * netnearAMount + let net_amount = donation_data.net_amount + if (donation_data.referrer_fee) { + net_amount = ( + BigInt(net_amount) - BigInt(donation_data.referrer_fee) + ).toString(); + } + + let totalnearAmount = formatToNear(total_amount); + let netnearAmount = formatToNear(net_amount); + let total_amount_usd = unit_price * totalnearAmount + let net_amount_usd = unit_price * netnearAmount let donation = { donor_id: donation_data.donor_id, total_amount, total_amount_usd, net_amount_usd, net_amount, - ft_id: donation_data.ft_id || null, + ft_id: donation_data.ft_id || "near", message: donation_data.message, donated_at, matching_pool: donation_data.matching_pool || false, @@ -511,8 +571,8 @@ async function getBlock(block: Block) { }; await context.db.Donation.insert(donation); - if (actionName != "donate") { // update pot data such as public donations and matchingpool - let pot = (await context.db.Pot.select({ id: receiverId}))[0] + if (actionName != "direct") { // update pot data such as public donations and matchingpool + let pot = (await context.db.Pot.select({ id: receiverId }))[0] donation["pot_id"] = pot.id let potUpdate = { total_public_donations_usd: pot.total_public_donations_usd || 0 + total_amount_usd, @@ -528,7 +588,7 @@ async function getBlock(block: Block) { } else { potUpdate["public_donations_count"] = pot.public_donations_count || 0 + 1 } - await context.db.Pot.update({id: pot.id}, potUpdate) + await context.db.Pot.update({ id: pot.id }, potUpdate) } let recipient = donation_data.project_id || donation_data.recipient_id @@ -537,7 +597,7 @@ async function getBlock(block: Block) { let acct = (await context.db.Account.select({ id: recipient }))[0] console.log("selected acct", acct) let acctUpdate = { - total_donations_usd: acct.total_donations_usd || 0 + total_amount_usd, + total_donations_usd: acct.total_donations_received_usd || 0 + total_amount_usd, donors_count: acct.donors_count || 0 + 1 } await context.db.Account.update({ id: recipient }, acctUpdate) @@ -548,12 +608,12 @@ async function getBlock(block: Block) { receiver_id: receiverId, timestamp: donation.donated_at, type: - actionName == "donate" + actionName == "direct" ? "Donate_Direct" : donation.matching_pool - ? "Donate_Pot_Matching_Pool" + ? "Donate_Pot_Matching_Pool" : "Donate_Pot_Public", - action_result: recipient, + action_result: JSON.stringify(donation), tx_hash: receiptId, }; @@ -570,28 +630,21 @@ async function getBlock(block: Block) { const args = atob(call.args); // decode function call argument switch (call.methodName) { case "new": - // new can be called on a couple of things, if the recever id matches factory pattern, then it's a new factory, else if it matches the registry account id, then it's a registry contract initialization, else, it's a new pot initialization. + // new can be called on a couple of things, if the recever id matches factory pattern, then it's a new factory, else if it matches the registry account id, then it's a registry contract initialization, else, it's a new pot initialization. matchVersionPattern(action.receiverId) ? await handleNewFactory( - args, - action.receiverId, - action.signerId, - action.receiptId - ) - : action.receiverId == "registry.potlock.near" // initializing registry - ? await handleRegistry( - args, - action.receiverId, - action.signerId, - action.receiptId - ) + args, + action.receiverId, + action.signerId, + action.receiptId + ) : await handleNewPot( - args, - action.receiverId, - action.signerId, - action.predecessorId, - action.receiptId - ); + args, + action.receiverId, + action.signerId, + action.predecessorId, + action.receiptId + ); break; // this is the callback after user applies to a certain pot, it can either be this or handle_apply case "assert_can_apply_callback": @@ -606,7 +659,7 @@ async function getBlock(block: Block) { case "handle_apply": console.log("application case 2:", JSON.parse(args)); break; - + // if function call is donate, call the handle new donations function case "donate": console.log("donatons to project incoming:", JSON.parse(args)); @@ -614,11 +667,11 @@ async function getBlock(block: Block) { args, action.receiverId, action.signerId, - "donate", + "direct", action.receiptId ); break; - + // this is a form of donation where user calls donate on a pot case "handle_protocol_fee_callback": console.log("donations to pool incoming:", JSON.parse(args)); @@ -626,12 +679,12 @@ async function getBlock(block: Block) { args, action.receiverId, action.signerId, - "handle_protocol_fee_callback", + "pot", action.receiptId ); break; // this handles project/list registration - case "register": + case "register_batch": console.log("registrations incoming:", JSON.parse(args)); await handleNewProject( args, @@ -640,7 +693,7 @@ async function getBlock(block: Block) { action.receiptId ); break; - + // chefs approve/decline ... etc a projects application to a pot case "chef_set_application_status": console.log( @@ -654,7 +707,7 @@ async function getBlock(block: Block) { action.receiptId ); break; - + // registries can have default status for projects case "admin_set_default_project_status": console.log( @@ -668,7 +721,7 @@ async function getBlock(block: Block) { action.receiptId ); break; - + // admins can set a project's status case "admin_set_project_status": console.log( @@ -682,11 +735,11 @@ async function getBlock(block: Block) { action.receiptId ); break; - + // fires when chef set payouts case "chef_set_payouts": console.log( - "setting payot....:", + "setting payout....:", JSON.parse(args) ); await handleSettingPayout( @@ -696,7 +749,7 @@ async function getBlock(block: Block) { action.receiptId ); break; - + // fires when there is a payout challenge case "challenge_payouts": console.log( @@ -725,7 +778,7 @@ async function getBlock(block: Block) { break; case "owner_remove_admins": console.log( - "setting payot....:", + "attempting to remove admins....:", JSON.parse(args) ); await handleListAdminRemoval( @@ -735,6 +788,24 @@ async function getBlock(block: Block) { action.receiptId ); break; + case "create_list": + console.log("creating list...", JSON.parse(args)) + await handleRegistry( + args, + action.receiverId, + action.signerId, + action.receiptId + ); + break; + case "upvote": + console.log("up voting...", JSON.parse(args)) + await handleUpVoting( + args, + action.receiverId, + action.signerId, + action.receiptId + ) + } } }); diff --git a/schema.sql b/schema.sql index f38c1ed..715030b 100644 --- a/schema.sql +++ b/schema.sql @@ -1,17 +1,20 @@ CREATE TABLE account ( id VARCHAR PRIMARY KEY, - total_donations_usd DECIMAL(10, 2), + total_donations_received_usd DECIMAL(10, 2), + total_donated_usd DECIMAL(10, 2), total_matching_pool_allocations_usd DECIMAL(10, 2), donors_count INT ); CREATE TABLE list ( - id VARCHAR PRIMARY KEY, + id BIGINT PRIMARY KEY, owner_id VARCHAR NOT NULL, name VARCHAR NOT NULL, description TEXT, + cover_image_url VARCHAR, + admin_only_registrations BOOLEAN NOT NULL DEFAULT FALSE, default_registration_status ENUM( 'Pending', 'Approved', @@ -19,22 +22,35 @@ CREATE TABLE 'Graylisted', 'Blacklisted' ) NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP, FOREIGN KEY (owner_id) REFERENCES account (id) ); CREATE TABLE list_admin ( - list_id INT NOT NULL, + list_id BIGINT NOT NULL, admin_id VARCHAR NOT NULL, PRIMARY KEY (list_id, admin_id), FOREIGN KEY (list_id) REFERENCES list (id), FOREIGN KEY (admin_id) REFERENCES account (id) ); +CREATE TABLE + list_upvotes ( + list_id BIGINT NOT NULL, + account_id VARCHAR NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + PRIMARY KEY (list_id, account_id), + FOREIGN KEY (list_id) REFERENCES list (id), + FOREIGN KEY (account_id) REFERENCES account (id) + ); + CREATE TABLE list_registration ( - id SERIAL PRIMARY KEY, + id INT PRIMARY KEY, registrant_id VARCHAR NOT NULL, + registered_by VARCHAR NOT NULL, status ENUM( 'Pending', 'Approved', @@ -46,7 +62,7 @@ CREATE TABLE updated_at TIMESTAMP, registrant_notes TEXT, admin_notes TEXT, - tx_hash VARCHAR, + tx_hash VARCHAR NOT NULL, FOREIGN KEY (registrant_id) REFERENCES account (id) ); @@ -55,7 +71,7 @@ CREATE TABLE id VARCHAR PRIMARY KEY, owner_id VARCHAR NOT NULL, deployed_at TIMESTAMP NOT NULL, - source_metadata VARCHAR NOT NULL, + source_metadata JSONB NOT NULL, protocol_fee_basis_points INT NOT NULL, protocol_fee_recipient_account VARCHAR NOT NULL, require_whitelist BOOLEAN NOT NULL, @@ -89,19 +105,19 @@ CREATE TABLE deployed_at TIMESTAMP NOT NULL, source_metadata VARCHAR NOT NULL, owner_id VARCHAR NOT NULL, - chef_id VARCHAR NULL, + chef_id VARCHAR, name TEXT NOT NULL, description TEXT NOT NULL, max_approved_applicants INT NOT NULL, - base_currency VARCHAR NULL, + base_currency VARCHAR, application_start TIMESTAMP NOT NULL, application_end TIMESTAMP NOT NULL, matching_round_start TIMESTAMP NOT NULL, matching_round_end TIMESTAMP NOT NULL, - registry_provider VARCHAR NULL, + registry_provider VARCHAR, min_matching_pool_donation_amount VARCHAR NOT NULL, - sybil_wrapper_provider VARCHAR NULL, - custom_sybil_checks VARCHAR NULL, + sybil_wrapper_provider VARCHAR, + custom_sybil_checks VARCHAR, custom_min_threshold_score INT NULL, referral_fee_matching_pool_basis_points INT NOT NULL, referral_fee_public_round_basis_points INT NOT NULL, @@ -113,9 +129,11 @@ CREATE TABLE total_public_donations VARCHAR NOT NULL, total_public_donations_usd DECIMAL(10, 2) NULL, public_donations_count INT NOT NULL, - cooldown_end TIMESTAMP NULL, + cooldown_end TIMESTAMP, + cooldown_period_ms INT NOT NULL, + FOREIGN KEY (account_id) REFERENCES account (id), all_paid_out BOOLEAN NOT NULL, - protocol_config_provider VARCHAR NULL, + protocol_config_provider VARCHAR, FOREIGN KEY (pot_factory_id) REFERENCES pot_factory (id), FOREIGN KEY (deployer_id) REFERENCES account (id), FOREIGN KEY (owner_id) REFERENCES account (id), @@ -130,10 +148,10 @@ CREATE TABLE pot_id INT NOT NULL, applicant_id VARCHAR NOT NULL, message TEXT, - current_status ENUM('Pending', 'Approved', 'Rejected', 'InReview') NOT NULL, + status ENUM('Pending', 'Approved', 'Rejected', 'InReview') NOT NULL, submitted_at TIMESTAMP NOT NULL, last_updated_at TIMESTAMP, - tx_hash VARCHAR, + tx_hash VARCHAR NOT NULL, FOREIGN KEY (pot_id) REFERENCES pot (id), FOREIGN KEY (applicant_id) REFERENCES account (id) ); @@ -147,7 +165,7 @@ CREATE TABLE notes TEXT, status ENUM('Pending', 'Approved', 'Rejected', 'InReview') NOT NULL, reviewed_at TIMESTAMP NOT NULL, - tx_hash VARCHAR, + tx_hash VARCHAR NOT NULL, FOREIGN KEY (application_id) REFERENCES pot_application (id), FOREIGN KEY (reviewer_id) REFERENCES account (id) ); @@ -158,11 +176,12 @@ CREATE TABLE id SERIAL PRIMARY KEY, recipient_id VARCHAR NOT NULL, amount VARCHAR NOT NULL, - amount_usd DECIMAL(10, 2), - ft_id VARCHAR NOT NULL, + amount_paid_usd DECIMAL(10, 2), + ft_id VARCHAR NOT NULL NOT NULL, paid_at TIMESTAMP, - tx_hash VARCHAR, - FOREIGN KEY (recipient_id) REFERENCES account (id) + tx_hash VARCHAR NOT NULL, + FOREIGN KEY (recipient_id) REFERENCES account (id), + FOREIGN KEY (ft_id) REFERENCES account (id) ); -- Table pot_payout_challenge @@ -173,7 +192,6 @@ CREATE TABLE pot_id INT NOT NULL, created_at TIMESTAMP NOT NULL, message TEXT NOT NULL, - tx_hash VARCHAR, FOREIGN KEY (challenger_id) REFERENCES account (id), FOREIGN KEY (pot_id) REFERENCES pot (id) ); @@ -187,7 +205,7 @@ CREATE TABLE created_at TIMESTAMP NOT NULL, message TEXT, resolved BOOL NOT NULL, - tx_hash VARCHAR, + tx_hash VARCHAR NOT NULL, FOREIGN KEY (challenge_id) REFERENCES pot_payout_challenge (id), FOREIGN KEY (admin_id) REFERENCES account (id) ); @@ -195,13 +213,13 @@ CREATE TABLE -- Table donation CREATE TABLE donation ( - id SERIAL PRIMARY KEY, + id INT PRIMARY KEY, donor_id VARCHAR NOT NULL, total_amount VARCHAR NOT NULL, total_amount_usd DECIMAL(10, 2), net_amount VARCHAR NOT NULL, net_amount_usd DECIMAL(10, 2), - ft_id VARCHAR, + ft_id VARCHAR NOT NULL, pot_id INT, matching_pool BOOLEAN NOT NULL, message TEXT, @@ -215,10 +233,13 @@ CREATE TABLE chef_id VARCHAR, chef_fee VARCHAR, chef_fee_usd DECIMAL(10, 2), - tx_hash VARCHAR, + tx_hash VARCHAR NOT NULL, FOREIGN KEY (donor_id) REFERENCES account (id), FOREIGN KEY (pot_id) REFERENCES pot (id), - FOREIGN KEY (recipient_id) REFERENCES account (id) + FOREIGN KEY (recipient_id) REFERENCES account (id), + FOREIGN KEY (ft_id) REFERENCES account (id), + FOREIGN KEY (referrer_id) REFERENCES account (id), + FOREIGN KEY (chef_id) REFERENCES account (id) ); -- Table activity @@ -228,14 +249,15 @@ CREATE TABLE signer_id VARCHAR NOT NULL, receiver_id VARCHAR NOT NULL, timestamp TIMESTAMP NOT NULL, - action_result VARCHAR, - tx_hash VARCHAR, + action_result JSONB, + tx_hash VARCHAR NOT NULL, type ENUM( 'Donate_Direct', 'Donate_Pot_Public', 'Donate_Pot_Matching_Pool', 'Register', + 'Register_Batch', 'Deploy_Pot', 'Process_Payouts', 'Challenge_Payout', @@ -255,11 +277,21 @@ CREATE TABLE FOREIGN KEY (admin_id) REFERENCES account (id) ); --- pot index +CREATE TABLE token_historical_data ( + token_id VARCHAR PRIMARY KEY, + last_updated TIMESTAMP NOT NULL, + historical_price VARCHAR NOT NULL +); --- CREATE INDEX "deploy_time_idx" ON pot (deployed_at); -CREATE INDEX idx_pot_application_start ON pot (application_start); -CREATE INDEX idx_pot_application_end ON pot (application_end); +-- account index +CREATE INDEX idx_acct_donations_donors ON account (total_donations_received_usd, total_matching_pool_allocations_usd, total_donated_usd, donors_count); +-- list index +CREATE INDEX idx_list_stamps ON list (created_at, updated_at); + +CREATE INDEX idx_list_id_status ON list_registration(list_id, status); + +-- pot index +CREATE INDEX "deploy_time_idx" ON pot (deployed_at); CREATE INDEX "idx_pot_deployer_id" ON pot (deployer_id); -- pot application index @@ -267,7 +299,8 @@ CREATE INDEX "idx_pot_deployer_id" ON pot (deployer_id); CREATE INDEX idx_pot_application_pot_id ON pot_application (pot_id); CREATE INDEX idx_pot_application_applicant_id ON pot_application (applicant_id); CREATE INDEX idx_pot_application_submitted_at ON pot_application (submitted_at); - +CREATE INDEX idx_application_period ON pot(application_start, application_end); +CREATE INDEX idx_matching_period ON pot(matching_round_start, matching_round_end); -- payout index CREATE INDEX idx_pot_payout_recipient_id ON pot_payout (recipient_id);