From b5298c04723fe49a163049757cb35b968a4a419a Mon Sep 17 00:00:00 2001 From: Thena Seer Date: Fri, 11 Feb 2022 15:45:39 -0800 Subject: [PATCH] Get CSV grain integration production-ready (#3320) --- .../grainIntegration-csv/distributionToCsv.js | 23 +++++++++- .../payoutDistributionToCsv.test.js | 15 +++--- .../src/api/instance/localInstance.js | 2 +- packages/sourcecred/src/api/main/grain.js | 2 +- packages/sourcecred/src/cli/grain.js | 8 ++-- packages/sourcecred/src/core/credGrainView.js | 46 ++++++++++++++----- .../src/ui/components/AccountOverview.js | 38 +++++++++++---- 7 files changed, 102 insertions(+), 32 deletions(-) diff --git a/packages/grainIntegration-csv/distributionToCsv.js b/packages/grainIntegration-csv/distributionToCsv.js index bdf495fc0..2f123baed 100644 --- a/packages/grainIntegration-csv/distributionToCsv.js +++ b/packages/grainIntegration-csv/distributionToCsv.js @@ -1,5 +1,10 @@ // @flow +const HARDCODED_DECIMAL_PRECISION = 18; +const ZEROS = Array.from(Array(HARDCODED_DECIMAL_PRECISION + 1)) + .map(() => "0") + .join(""); + /*:: type PayoutDistributions = $ReadOnlyArray<[string, string]>;*/ /*:: type IntegrationConfig = { currency: {tokenAddress?: string}, @@ -14,7 +19,23 @@ module.exports = function payoutDistributionToCsv( prefix = "erc20," + (config.currency.tokenAddress || "") + ","; let csvString = ""; for (const [payoutAddress, amount] of payoutDistributions) { - csvString += prefix + `${payoutAddress},${amount}\n`; + const amountWithZerosPrefix = ZEROS + amount; + const beforeDecimal = amountWithZerosPrefix.slice( + 0, + amountWithZerosPrefix.length - HARDCODED_DECIMAL_PRECISION + ); + const afterDecimal = amountWithZerosPrefix.slice( + amountWithZerosPrefix.length - HARDCODED_DECIMAL_PRECISION + ); + let formattedAmount = (beforeDecimal + "." + afterDecimal).replace( + /^0+|0+$/g, + "" + ); + if (formattedAmount.startsWith(".")) + formattedAmount = "0" + formattedAmount; + if (formattedAmount.endsWith(".")) formattedAmount = formattedAmount + "0"; + + csvString += prefix + `${payoutAddress},${formattedAmount}\n`; } return csvString; }; diff --git a/packages/grainIntegration-csv/payoutDistributionToCsv.test.js b/packages/grainIntegration-csv/payoutDistributionToCsv.test.js index 69d612bfe..55811534e 100644 --- a/packages/grainIntegration-csv/payoutDistributionToCsv.test.js +++ b/packages/grainIntegration-csv/payoutDistributionToCsv.test.js @@ -3,36 +3,39 @@ const payoutDistributionToCsv = require("./distributionToCsv"); describe("payoutDistributionToCsv", () => { it("serializes entries into a csv when gnosis config is absent", () => { const mockDistributions = [ - ["abc", "123"], + ["abc", "99123456789012345678"], ["def", "456"], + ["lol", "1000000000000000000"], ]; const result = payoutDistributionToCsv(mockDistributions, { currency: { tokenAddress: "0x1234" }, integration: undefined, }); - expect(result).toBe(`abc,123\ndef,456\n`); + expect(result).toBe(`abc,99.123456789012345678\ndef,0.000000000000000456\nlol,1.0\n`); }); it("serializes entries into a csv with gnosis prefix", () => { const mockDistributions = [ - ["abc", "123"], + ["abc", "99123456789012345678"], ["def", "456"], + ["lol", "1000000000000000000"], ]; const result = payoutDistributionToCsv(mockDistributions, { currency: { tokenAddress: "0x1234" }, integration: { gnosis: true }, }); - expect(result).toBe(`erc20,0x1234,abc,123\nerc20,0x1234,def,456\n`); + expect(result).toBe(`erc20,0x1234,abc,99.123456789012345678\nerc20,0x1234,def,0.000000000000000456\nerc20,0x1234,lol,1.0\n`); }); it("serializes entries into a csv with gnosis prefix when the tokenAddress is absent", () => { const mockDistributions = [ - ["abc", "123"], + ["abc", "99123456789012345678"], ["def", "456"], + ["lol", "1000000000000000000"], ]; const result = payoutDistributionToCsv(mockDistributions, { currency: {}, integration: { gnosis: true }, }); - expect(result).toBe(`erc20,,abc,123\nerc20,,def,456\n`); + expect(result).toBe(`erc20,,abc,99.123456789012345678\nerc20,,def,0.000000000000000456\nerc20,,lol,1.0\n`); }); it("returns an empty string when receiving an empty array", () => { const result = payoutDistributionToCsv([], { diff --git a/packages/sourcecred/src/api/instance/localInstance.js b/packages/sourcecred/src/api/instance/localInstance.js index a7a419b6c..a7db69ba7 100644 --- a/packages/sourcecred/src/api/instance/localInstance.js +++ b/packages/sourcecred/src/api/instance/localInstance.js @@ -227,7 +227,7 @@ export class LocalInstance extends ReadInstance implements Instance { const configName = config.grainConfig.integration?.name; if (!configName) return; const configUpdate = result.configUpdate; - if (Object.keys(configUpdate).length > 0) { + if (configUpdate && Object.keys(configUpdate).length > 0) { const grainConfigPath = pathJoin(...GRAIN_PATH); const currentConfig = await loadJson( this._storage, diff --git a/packages/sourcecred/src/api/main/grain.js b/packages/sourcecred/src/api/main/grain.js index 282af716f..4f9ce5b9f 100644 --- a/packages/sourcecred/src/api/main/grain.js +++ b/packages/sourcecred/src/api/main/grain.js @@ -51,7 +51,7 @@ export async function grain(input: GrainInput): Promise { const distributions = applyDistributions( input.grainConfig, - input.credGrainView, + input.credGrainView.withNewLedger(configuredLedger), configuredLedger, +Date.now(), input.allowMultipleDistributionsPerInterval || false diff --git a/packages/sourcecred/src/cli/grain.js b/packages/sourcecred/src/cli/grain.js index f8832d080..fcf0af18d 100644 --- a/packages/sourcecred/src/cli/grain.js +++ b/packages/sourcecred/src/cli/grain.js @@ -53,9 +53,11 @@ const grainCommand: Command = async (args, std) => { distributions ); - for (const result of results) { - await instance.writeGrainIntegrationOutput(result); - await instance.updateGrainIntegrationConfig(result, grainInput); + if (!simulation) { + for (const result of results) { + await instance.writeGrainIntegrationOutput(result); + await instance.updateGrainIntegrationConfig(result, grainInput); + } } let totalDistributed = G.ZERO; diff --git a/packages/sourcecred/src/core/credGrainView.js b/packages/sourcecred/src/core/credGrainView.js index 20b3def5e..207afbcb8 100644 --- a/packages/sourcecred/src/core/credGrainView.js +++ b/packages/sourcecred/src/core/credGrainView.js @@ -273,6 +273,29 @@ export class CredGrainView { return new CredGrainView(json.participants, json.intervals); } + withNewLedger(ledger: Ledger): CredGrainView { + const participants = ledger.accounts().map((account) => { + const grainEarnedPerInterval = CredGrainView._calculateGrainEarnedPerInterval( + account, + this._intervals + ); + const participant = this._participants.find( + (p) => p.identity.id === account.identity.id + ); + if (!participant) + throw new Error( + `CredGrainView.withNewLedger: new ledger has identity ID [${account.identity.id}] that is not in already in the CredGrainView.` + ); + return { + ...participant, + active: account.active, + grainEarned: account.paid, + grainEarnedPerInterval, + }; + }); + return new CredGrainView(participants, this._intervals); + } + /** Creates a CredGrainView using the output of the CredRank API. */ @@ -360,7 +383,7 @@ Creates a CredGrainView using the output of the CredEquate API. Math.max(...ledgerCredTimestamps, Date.now()) ); const participantsMap = new Map(); - ledger.accounts().forEach((account) => { + const participantPrototypes = ledger.accounts().map((account) => { const grainEarnedPerInterval = this._calculateGrainEarnedPerInterval( account, intervals @@ -375,6 +398,7 @@ Creates a CredGrainView using the output of the CredEquate API. for (const alias of account.identity.aliases) { participantsMap.set(alias.address, participant); } + return participant; }); for (const scoredContribution of scoredContributions) { @@ -389,17 +413,15 @@ Creates a CredGrainView using the output of the CredEquate API. } } - const participants = Array.from(new Set(participantsMap.values())).map( - (p) => { - return { - ...p, - cred: p.credPerInterval.reduce((a, b, index) => { - if (!b) p.credPerInterval[index] = 0; - return a + (b ?? 0); - }, 0), - }; - } - ); + const participants = participantPrototypes.map((p) => { + return { + ...p, + cred: p.credPerInterval.reduce((a, b, index) => { + if (!b) p.credPerInterval[index] = 0; + return a + (b ?? 0); + }, 0), + }; + }); return new CredGrainView(participants, intervals); } diff --git a/packages/sourcecred/src/ui/components/AccountOverview.js b/packages/sourcecred/src/ui/components/AccountOverview.js index 8839f0fac..350f0937f 100644 --- a/packages/sourcecred/src/ui/components/AccountOverview.js +++ b/packages/sourcecred/src/ui/components/AccountOverview.js @@ -85,16 +85,26 @@ export const AccountOverview = ({ [] ); - const sortingOptions = [ACTIVE_SORT, BALANCE_SORT, EARNED_SORT]; + const sortingOptions = useMemo( + () => + ledger.accounting().enabled + ? [ACTIVE_SORT, BALANCE_SORT, EARNED_SORT] + : [ACTIVE_SORT, EARNED_SORT], + [ledger] + ); + const initialSort = useMemo( + () => (ledger.accounting().enabled ? BALANCE_SORT : EARNED_SORT), + [ledger] + ); const tsAccounts = useTableState( {data: accounts}, { initialRowsPerPage: PAGINATION_OPTIONS[0], initialSort: { - sortName: BALANCE_SORT.name, + sortName: initialSort.name, sortOrder: SortOrders.DESC, - sortFn: BALANCE_SORT.fn, + sortFn: initialSort.fn, }, } ); @@ -163,7 +173,12 @@ export const AccountOverview = ({ {tsAccounts.currentPage.map((a) => - AccountRow(a, currencySuffix, decimalsToDisplay) + AccountRow( + a, + currencySuffix, + decimalsToDisplay, + ledger.accounting().enabled + ) )} @@ -192,15 +207,22 @@ export const AccountOverview = ({ ); }; -const AccountRow = (account: Account, suffix: string, decimals: number) => ( +const AccountRow = ( + account: Account, + suffix: string, + decimals: number, + accountingEnabled: boolean +) => ( {account.active ? "✅" : "🛑"} - - {G.format(account.balance, decimals, suffix)} - + {accountingEnabled && ( + + {G.format(account.balance, decimals, suffix)} + + )} {G.format(account.paid, decimals, suffix)}