{
"data": {
"potlock.near": {
"widget": {
"utils": {
"": "const IPFS_BASE_URL = \"https://ipfs.near.social/ipfs/\";\nconst nearToUsd = useCache(\n () =>\n asyncFetch(\"https://api.coingecko.com/api/v3/simple/price?ids=near&vs_currencies=usd\").then(\n (res) => {\n if (res.ok) {\n return res.body.near.usd;\n }\n }\n ),\n \"nearToUsd\"\n);\n\nconst formatWithCommas = (amount) => {\n // Convert to a number and use toLocaleString to add commas\n return Number(amount).toLocaleString(undefined, {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n });\n};\n\nreturn {\n formatDate: (timestamp) => {\n const months = [\n \"Jan\",\n \"Feb\",\n \"Mar\",\n \"Apr\",\n \"May\",\n \"Jun\",\n \"Jul\",\n \"Aug\",\n \"Sep\",\n \"Oct\",\n \"Nov\",\n \"Dec\",\n ];\n const date = new Date(timestamp);\n\n const year = date.getFullYear();\n const month = months[date.getMonth()];\n const day = date.getDate();\n let hour = date.getHours();\n const minute = date.getMinutes();\n const ampm = hour >= 12 ? \"pm\" : \"am\";\n\n // Convert hour to 12-hour format\n hour = hour % 12;\n hour = hour ? hour : 12; // the hour '0' should be '12'\n\n // Minutes should be two digits\n const minuteFormatted = minute < 10 ? \"0\" + minute : minute;\n\n return `${month} ${day}, ${year} ${hour}:${minuteFormatted}${ampm}`;\n },\n daysAgo: (timestamp) => {\n const now = new Date();\n const pastDate = new Date(timestamp);\n const differenceInTime = now - pastDate;\n\n // Convert time difference from milliseconds to days\n const differenceInDays = Math.floor(differenceInTime / (1000 * 3600 * 24));\n return differenceInDays === 0\n ? \"< 1 day ago\"\n : `${differenceInDays} ${differenceInDays === 1 ? \"day\" : \"days\"} ago`;\n },\n daysUntil: (timestamp) => {\n const now = new Date();\n const futureDate = new Date(timestamp);\n const differenceInTime = futureDate - now;\n\n // Convert time difference from milliseconds to days\n const differenceInDays = Math.ceil(differenceInTime / (1000 * 3600 * 24));\n\n return `${differenceInDays} ${differenceInDays === 1 ? \"day\" : \"days\"}`;\n },\n getTagsFromSocialProfileData: (profileData) => {\n // first try to get tags from plCategories, then category (deprecated/old format), then default to empty array\n if (!profileData) return [];\n const DEPRECATED_CATEGORY_MAPPINGS = {\n \"social-impact\": \"Social Impact\",\n \"non-profit\": \"NonProfit\",\n climate: \"Climate\",\n \"public-good\": \"Public Good\",\n \"de-sci\": \"DeSci\",\n \"open-source\": \"Open Source\",\n community: \"Community\",\n education: \"Education\",\n };\n const tags = profileData.plCategories\n ? JSON.parse(profileData.plCategories)\n : profileData.category\n ? [profileData.category.text ?? DEPRECATED_CATEGORY_MAPPINGS[profileData.category] ?? \"\"]\n : [];\n return tags;\n },\n getTeamMembersFromSocialProfileData: (profileData) => {\n if (!profileData) return [];\n const team = profileData.plTeam\n ? JSON.parse(profileData.plTeam)\n : profileData.team\n ? Object.entries(profileData.team)\n .filter(([_, v]) => v !== null)\n .map(([k, _]) => k)\n : [];\n return team;\n },\n doesUserHaveDaoFunctionCallProposalPermissions: (policy) => {\n // TODO: break this out (NB: duplicated in Project.CreateForm)\n const userRoles = policy.roles.filter((role) => {\n if (role.kind === \"Everyone\") return true;\n return role.kind.Group && role.kind.Group.includes(context.accountId);\n });\n const kind = \"call\";\n const action = \"AddProposal\";\n // Check if the user is allowed to perform the action\n const allowed = userRoles.some(({ permissions }) => {\n return (\n permissions.includes(`${kind}:${action}`) ||\n permissions.includes(`${kind}:*`) ||\n permissions.includes(`*:${action}`) ||\n permissions.includes(\"*:*\")\n );\n });\n return allowed;\n },\n basisPointsToPercent: (basisPoints) => {\n return basisPoints / 100;\n },\n ipfsUrlFromCid: (cid) => {\n return `${IPFS_BASE_URL}${cid}`;\n },\n validateNearAddress: (address) => {\n const NEAR_ACCOUNT_ID_REGEX = /^(?=.{2,64}$)(?!.*\\.\\.)(?!.*-$)(?!.*_$)[a-z\\d._-]+$/i;\n let isValid = NEAR_ACCOUNT_ID_REGEX.test(address);\n // Additional \".near\" check for IDs less than 64 characters\n if (address.length < 64 && !address.endsWith(\".near\")) {\n isValid = false;\n }\n return isValid;\n },\n validateEVMAddress: (address) => {\n // Check if the address is defined and the length is correct (42 characters, including '0x')\n if (!address || address.length !== 42) {\n return false;\n }\n // Check if the address starts with '0x' and contains only valid hexadecimal characters after '0x'\n const re = /^0x[a-fA-F0-9]{40}$/;\n return re.test(address);\n },\n validateGithubRepoUrl: (url) => {\n // Regular expression to match the GitHub repository URL pattern\n // This regex checks for optional \"www.\", a required \"github.com/\", and then captures the username and repo name segments\n const githubRepoUrlPattern =\n /^(https?:\\/\\/)?(www\\.)?github\\.com\\/[a-zA-Z0-9-]+\\/[a-zA-Z0-9_.-]+\\/?$/;\n return githubRepoUrlPattern.test(url);\n },\n nearToUsd,\n yoctosToNear: (amountYoctos, abbreviate) => {\n return (\n formatWithCommas(new Big(amountYoctos).div(1e24).toFixed(2)) + (abbreviate ? \"N\" : \" NEAR\")\n );\n },\n yoctosToUsd: (amount) => {\n return nearToUsd\n ? \"~$\" + formatWithCommas(new Big(amount).mul(nearToUsd).div(1e24).toFixed(2))\n : null;\n },\n nearToUsdWithFallback: (amountNear, abbreviate) => {\n return nearToUsd\n ? \"~$\" + formatWithCommas((amountNear * nearToUsd).toFixed(2))\n : formatWithCommas(amountNear) + (abbreviate ? \"N\" : \" NEAR\");\n },\n yoctosToUsdWithFallback: (amountYoctos, abbreviate) => {\n return nearToUsd\n ? \"~$\" + formatWithCommas(new Big(amountYoctos).mul(nearToUsd).div(1e24).toFixed(2))\n : formatWithCommas(new Big(amountYoctos).div(1e24).toFixed(2)) + (abbreviate ? \"N\" : \" NEAR\");\n },\n calculatePayouts: (allPotDonations, totalMatchingPool) => {\n console.log(\"totalMatchingPool: \", totalMatchingPool);\n // first, flatten the list of donations into a list of contributions\n // const projectContributions = convertDonationsToProjectContributions(allPotDonations);\n const projectContributions = [];\n for (const d of allPotDonations) {\n const amount = new Big(d.total_amount);\n const val = [d.project_id, d.donor_id, amount];\n projectContributions.push(val);\n }\n // take the flattened list of contributions and aggregate\n // the amounts contributed by each user to each project.\n // create a dictionary where each key is a projectId and its value\n // is another dictionary of userIds and their aggregated contribution amounts.\n const contributions = {};\n for (const [proj, user, amount] of projectContributions) {\n if (!contributions[proj]) {\n contributions[proj] = {};\n }\n contributions[proj][user] = Big(contributions[proj][user] || 0).plus(amount);\n }\n console.log(\"contributions: \", contributions);\n // calculate the total overlapping contribution amounts between pairs of users for each project.\n // create a nested dictionary where the outer keys are userIds and the inner keys are also userIds,\n // and the inner values are the total overlap between these two users' contributions.\n // type PairTotals = { [key: UserId]: { [key: UserId]: YoctoBig } };\n const pairTotals = {};\n for (const contribz of Object.values(contributions)) {\n for (const [k1, v1] of Object.entries(contribz)) {\n if (!pairTotals[k1]) {\n pairTotals[k1] = {};\n }\n for (const [k2, v2] of Object.entries(contribz)) {\n if (!pairTotals[k1][k2]) {\n pairTotals[k1][k2] = Big(0);\n }\n pairTotals[k1][k2] = pairTotals[k1][k2].plus(v1.times(v2).sqrt());\n }\n }\n }\n // Compute the CLR (Contribution Matching) amount for each project\n // using the aggregated contributions, the total overlaps between user pairs,\n // a threshold value, and the total pot available for matching.\n // Then, calculate the matching amount for each project using the quadratic formula\n // and returns a list of objects containing the projectId, the number of contributions,\n // the total contributed amount, and the matching amount.\n const threshold = Big(\"25000000000000000000000000\"); // this value can be adjusted to tweak the matching algorithm\n const totalPot = Big(totalMatchingPool);\n let bigtot = Big(0);\n const totals = [];\n for (const [proj, contribz] of Object.entries(contributions)) {\n let tot = Big(0);\n let _num = 0;\n let _sum = Big(0);\n for (const [k1, v1] of Object.entries(contribz)) {\n _num += 1;\n _sum = _sum.plus(v1);\n for (const [k2, v2] of Object.entries(contribz)) {\n if (k2 > k1) {\n // not entirely sure of this \"if\" statement's purpose as the values being compared are account IDs. Originally taken from Gitcoin's CLR logic (see link at top of file)\n const sqrt = v1.times(v2).sqrt();\n tot = tot.plus(sqrt.div(pairTotals[k1][k2].div(threshold)));\n }\n }\n }\n bigtot = bigtot.plus(tot);\n totals.push({\n id: proj,\n number_contributions: _num,\n contribution_amount_str: _sum.toFixed(0),\n matching_amount_str: tot.toFixed(0),\n });\n }\n console.log(\"totals before: \", totals);\n // if we reach saturation, we need to normalize\n if (bigtot.gte(totalPot)) {\n console.log(\"NORMALIZING\");\n for (const t of totals) {\n t.matching_amount_str = Big(t.matching_amount_str).div(bigtot).times(totalPot).toFixed(0);\n }\n }\n\n let totalAllocatedBeforeRounding = Big(0); // Initialize the accumulator as a Big object\n\n for (const t of totals) {\n const currentMatchingAmount = Big(t.matching_amount_str);\n totalAllocatedBeforeRounding = totalAllocatedBeforeRounding.plus(currentMatchingAmount);\n }\n\n let residual = totalPot.minus(totalAllocatedBeforeRounding);\n console.log(\"first round residual: \", residual.toFixed(0));\n\n // Check if there is a residual due to rounding\n // if (residual.abs().gt(Big(\"0\"))) {\n if (residual.toString() !== \"0\") {\n // Fairly distribute residual (proportionally to initial allocation)\n // NB: there may still be a residual after this step\n for (let i = 0; i < totals.length; i++) {\n let proportion = Big(totals[i].matching_amount_str).div(totalAllocatedBeforeRounding);\n let additionalAllocation = proportion.times(residual);\n // Update the matching amount with the additional allocation\n totals[i].matching_amount_str = Big(totals[i].matching_amount_str)\n .plus(additionalAllocation)\n .toFixed(0);\n }\n\n console.log(\"CALCULATING TOTALS AFTER RESIDUAL DISTRIBUTION\");\n totalAllocatedBeforeRounding = Big(0); // Initialize the accumulator as a Big object\n for (const t of totals) {\n const currentMatchingAmount = Big(t.matching_amount_str);\n totalAllocatedBeforeRounding = totalAllocatedBeforeRounding.plus(currentMatchingAmount);\n }\n residual = totalPot.minus(totalAllocatedBeforeRounding);\n console.log(\"second round residual: \", residual.toFixed(0));\n\n // if (residual.abs().gt(Big(\"0\"))) {\n // console.log(\"MAKING FINAL ADJUSTMENT\");\n // // Step 1: Sort 'totals' in descending order based on 'matching_amount_str'\n // totals.sort((a, b) => Big(b.matching_amount_str).minus(Big(a.matching_amount_str)));\n\n // // Step 2: Allocate the residual\n // let residualToAllocate = Big(residual);\n // for (let i = 0; i < totals.length && residualToAllocate.gt(Big(0)); i++) {\n // let allocationIncrement = Big(1); // Smallest possible increment\n // if (residualToAllocate.lt(allocationIncrement)) {\n // allocationIncrement = residualToAllocate; // If the remaining residual is less than the increment, adjust it\n // }\n // totals[i].matching_amount_str = Big(totals[i].matching_amount_str)\n // .plus(allocationIncrement)\n // .toFixed(0);\n // residualToAllocate = residualToAllocate.minus(allocationIncrement);\n // }\n // // Ensure the loop above runs until 'residualToAllocate' is 0 or sufficiently small to be considered fully allocated\n\n // // Recalculate 'totalAllocatedBeforeRounding' to verify the final allocation matches the total matching pool\n // totalAllocatedBeforeRounding = Big(0);\n // for (const t of totals) {\n // const currentMatchingAmount = Big(t.matching_amount_str);\n // totalAllocatedBeforeRounding = totalAllocatedBeforeRounding.plus(currentMatchingAmount);\n // }\n // residual = totalPot.minus(totalAllocatedBeforeRounding);\n // console.log(\"FINAL residual: \", residual.toFixed(0));\n // }\n\n // if (residual.abs().gt(Big(0))) {\n if (residual.toString() !== \"0\") {\n // Directly adjust the matching amount of one project to correct the residual\n // Find a project to adjust. Prefer adjusting projects with larger allocations to minimize impact\n totals.sort((a, b) => Big(b.matching_amount_str).minus(Big(a.matching_amount_str)));\n if (residual.gt(Big(0))) {\n // If residual is positive, increment the largest allocation\n totals[0].matching_amount_str = Big(totals[0].matching_amount_str)\n .plus(residual)\n .toFixed(0);\n } else {\n // If residual is negative, decrement the largest allocation\n // Ensure the allocation is large enough to be decremented\n for (let i = 0; i < totals.length; i++) {\n if (Big(totals[i].matching_amount_str).gt(Big(abs(residual)))) {\n totals[i].matching_amount_str = Big(totals[i].matching_amount_str)\n .plus(residual)\n .toFixed(0); // Residual is negative here\n break;\n }\n }\n }\n\n // Verify that the adjustment has corrected the residual\n totalAllocatedBeforeRounding = Big(0);\n for (const t of totals) {\n const currentMatchingAmount = Big(t.matching_amount_str);\n totalAllocatedBeforeRounding = totalAllocatedBeforeRounding.plus(currentMatchingAmount);\n }\n residual = totalPot.minus(totalAllocatedBeforeRounding);\n console.log(\"Residual after final adjustment: \", residual.toFixed(0));\n }\n }\n\n const payouts = totals.reduce((acc, t) => {\n acc[t.id] = {\n totalAmount: t.contribution_amount_str,\n matchingAmount: t.matching_amount_str,\n donorCount: t.number_contributions,\n };\n return acc;\n }, {});\n return payouts;\n },\n};\n"
}
}
}
}
}