diff --git a/indexer.ts b/indexer.ts new file mode 100644 index 0000000..ced3cc1 --- /dev/null +++ b/indexer.ts @@ -0,0 +1,814 @@ +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 created_at = new Date( + Number(BigInt(block.header().timestampNanosec) / BigInt(1000000)) + ); + + 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 potObject = { + id: receiverId, + pot_factory_id: predecessorId, + deployer_id: signerId, + 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: "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), + 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 ? 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: "0", + total_matching_pool_usd: data.total_matching_pool_usd, + 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: 0, + cooldown_period_ms: null, + 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: created_at, + type: "Deploy_Pot", + action_result: JSON.stringify(potObject), + 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 factory = { + id: receiverId, + owner_id: data.owner, + 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, + 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 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: data.admins[admin] }, ["id"], []); + let list_admin = { + list_id: data.id, + admin_id: admin, + }; + await context.db.ListAdmin.insert(list_admin); + } + } + + let regv = { + id: data.id, + owner_id: data.owner, + 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 + }; + + + 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 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(insert_data); + let activity = { + signer_id: signerId, + receiver_id: receiverId, + timestamp: insert_data[0].submitted_at, + type: "Register_Batch", + action_result: JSON.stringify(reg_data), + 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: created_at, + }; + + await context.db.ListRegistration.update( + { registrant_id: data.project_id }, + regUpdate + ); + } + + async function handlePotApplication(args, receiverId, signerId, receiptId) { + let data = JSON.parse(args); + 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: appl_data.project_id, + message: appl_data.message, + submitted_at: appl_data.submitted_at, + status: appl_data.status, + tx_hash: receiptId, + }; + await context.db.PotApplication.insert(application); + + let activity = { + signer_id: signerId, + receiver_id: receiverId, + timestamp: application.submitted_at, + type: "Submit_Application", + 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? + }; + + await context.db.Activity.insert(activity); + } + + async function handleApplicationStatusChange( + args, + receiverId, + signerId, + receiptId + ) { + let data = JSON.parse(args); + console.log("pot application update 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, + tx_hash: receiptId + }; + 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: 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) { + 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"] || "near", + tx_hash: receiptId, + }; + await context.db.PotPayout.insert(potPayout); + } + } + + 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 || created_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("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, + 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: JSON.stringify(payoutChallenge), + 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 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: created_at, + type: "Remove_List_Admin", + tx_hash: receiptId, + }; + + await context.db.Activity.insert(activity); + } + + 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}`; + } + + // helper function to format + function formatToNear(yoctoAmount) { + const nearAmount = yoctoAmount / 10 ** 24; + return nearAmount; + } + + async function handleNewDonations( + args, + receiverId, + signerId, + actionName, + receiptId + ) { + let donation_data; + if ((actionName == "direct")) { + console.log("calling donate on projects..."); + 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); + + 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 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 + } + + let total_amount = donation_data.total_amount; + 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 || "near", + 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 != "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, + 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_received_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 == "direct" + ? "Donate_Direct" + : donation.matching_pool + ? "Donate_Pot_Matching_Pool" + : "Donate_Pot_Public", + action_result: JSON.stringify(donation), + 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 + ) + : 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, + "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)); + await handleNewDonations( + args, + action.receiverId, + action.signerId, + "pot", + action.receiptId + ); + break; + // this handles project/list registration + case "register_batch": + 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 payout....:", + 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; + case "owner_remove_admins": + console.log( + "attempting to remove admins....:", + JSON.parse(args) + ); + await handleListAdminRemoval( + args, + action.receiverId, + action.signerId, + 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 new file mode 100644 index 0000000..715030b --- /dev/null +++ b/schema.sql @@ -0,0 +1,320 @@ +CREATE TABLE + account ( + id VARCHAR PRIMARY KEY, + 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 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', + 'Rejected', + '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 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 INT PRIMARY KEY, + registrant_id VARCHAR NOT NULL, + registered_by 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 NOT NULL, + 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 JSONB 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, + name TEXT NOT NULL, + description TEXT NOT NULL, + max_approved_applicants INT NOT 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, + min_matching_pool_donation_amount VARCHAR NOT 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, + 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, + cooldown_period_ms INT NOT NULL, + FOREIGN KEY (account_id) REFERENCES account (id), + all_paid_out BOOLEAN NOT 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), + 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, + status ENUM('Pending', 'Approved', 'Rejected', 'InReview') NOT NULL, + submitted_at TIMESTAMP NOT NULL, + last_updated_at TIMESTAMP, + tx_hash VARCHAR NOT NULL, + 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 NOT NULL, + 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_paid_usd DECIMAL(10, 2), + ft_id VARCHAR NOT NULL NOT NULL, + paid_at TIMESTAMP, + tx_hash VARCHAR NOT NULL, + FOREIGN KEY (recipient_id) REFERENCES account (id), + FOREIGN KEY (ft_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, + 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 NOT NULL, + FOREIGN KEY (challenge_id) REFERENCES pot_payout_challenge (id), + FOREIGN KEY (admin_id) REFERENCES account (id) + ); + +-- Table donation +CREATE TABLE + donation ( + 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 NOT NULL, + 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 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 (ft_id) REFERENCES account (id), + FOREIGN KEY (referrer_id) REFERENCES account (id), + FOREIGN KEY (chef_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 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', + '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) + ); + +CREATE TABLE token_historical_data ( + token_id VARCHAR PRIMARY KEY, + last_updated TIMESTAMP NOT NULL, + historical_price VARCHAR NOT NULL +); + +-- 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 + +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); + +-- 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); + +