From 4fc7dbc26fae2f6bcd1b7628c918f01caf80a652 Mon Sep 17 00:00:00 2001 From: p_sparacino <72105234+psparacino@users.noreply.github.com> Date: Mon, 19 Feb 2024 01:26:16 -0500 Subject: [PATCH] Format retirement amounts by registry (#2237) * format for ICR amounts * api: updated query and types * api: fix type error * api: increment api version * api: update new fixtures and tests for new query * api: remove logs * api: adjust fixtures, add Moss as registry * api: use CreditId class for formatting * api: account for Moss project id) * api: fix missing imports * api: increment api version --- .../.generated/types/digitalCarbon.types.ts | 20 ++++++++++---- carbonmark-api/src/app.constants.ts | 6 +++++ .../src/graphql/digitalCarbon.fragments.gql | 2 ++ carbonmark-api/src/graphql/digitalCarbon.gql | 14 +++++++--- .../routes/retirements/[id]/provenance/get.ts | 26 +++++++++++++----- .../src/utils/helpers/records.utils.ts | 27 ++++++++++++++----- .../src/utils/helpers/retirements.utils.ts | 11 ++++++-- carbonmark-api/src/utils/marketplace.utils.ts | 2 ++ carbonmark-api/test/fixtures/digitalCarbon.ts | 18 +++++++++++-- .../[id]/provenance/get.test.mocks.ts | 4 +-- .../retirements/[id]/provenance/get.test.ts | 4 ++- 11 files changed, 106 insertions(+), 28 deletions(-) diff --git a/carbonmark-api/src/.generated/types/digitalCarbon.types.ts b/carbonmark-api/src/.generated/types/digitalCarbon.types.ts index 270491b8ae..428244c217 100644 --- a/carbonmark-api/src/.generated/types/digitalCarbon.types.ts +++ b/carbonmark-api/src/.generated/types/digitalCarbon.types.ts @@ -4323,7 +4323,7 @@ export enum _SubgraphErrorPolicy_ { deny = 'deny' } -export type ProvenanceRecordFragmentFragment = { __typename?: 'ProvenanceRecord', id: any, transactionType: ProvenanceType, registrySerialNumbers: Array, token: any, sender: any, receiver: any, originalAmount: string, remainingAmount: string, createdAt: string, updatedAt: string }; +export type ProvenanceRecordFragmentFragment = { __typename?: 'ProvenanceRecord', id: any, transactionType: ProvenanceType, registrySerialNumbers: Array, token: any, tokenId: string | null, sender: any, receiver: any, originalAmount: string, remainingAmount: string, createdAt: string, updatedAt: string, transactionHash: any }; export type RetireFragmentFragment = { __typename?: 'Retire', id: any, bridgeID: string | null, hash: any, amount: string, beneficiaryName: string, retirementMessage: string, retiringName: string, timestamp: string, pool: { __typename?: 'CarbonPool', id: any } | null, beneficiaryAddress: { __typename?: 'Account', id: any }, retiringAddress: { __typename?: 'Account', id: any } }; @@ -4394,7 +4394,7 @@ export type GetProvenanceRecordsByHashQueryVariables = Exact<{ }>; -export type GetProvenanceRecordsByHashQuery = { __typename?: 'Query', provenanceRecords: Array<{ __typename?: 'ProvenanceRecord', id: any, transactionType: ProvenanceType, registrySerialNumbers: Array, token: any, sender: any, receiver: any, originalAmount: string, remainingAmount: string, createdAt: string, updatedAt: string, priorRecords: Array<{ __typename?: 'ProvenanceRecord', id: any, transactionType: ProvenanceType, registrySerialNumbers: Array, token: any, sender: any, receiver: any, originalAmount: string, remainingAmount: string, createdAt: string, updatedAt: string }> }> }; +export type GetProvenanceRecordsByHashQuery = { __typename?: 'Query', retires: Array<{ __typename?: 'Retire', credit: { __typename?: 'CarbonCredit', project: { __typename?: 'CarbonProject', id: string, registry: Registry } }, provenance: { __typename?: 'ProvenanceRecord', id: any, transactionType: ProvenanceType, registrySerialNumbers: Array, token: any, tokenId: string | null, sender: any, receiver: any, originalAmount: string, remainingAmount: string, createdAt: string, updatedAt: string, transactionHash: any, priorRecords: Array<{ __typename?: 'ProvenanceRecord', id: any, transactionType: ProvenanceType, registrySerialNumbers: Array, token: any, tokenId: string | null, sender: any, receiver: any, originalAmount: string, remainingAmount: string, createdAt: string, updatedAt: string, transactionHash: any }> } | null }> }; export const ProvenanceRecordFragmentFragmentDoc = gql` fragment ProvenanceRecordFragment on ProvenanceRecord { @@ -4402,12 +4402,14 @@ export const ProvenanceRecordFragmentFragmentDoc = gql` transactionType registrySerialNumbers token + tokenId sender receiver originalAmount remainingAmount createdAt updatedAt + transactionHash } `; export const RetireFragmentFragmentDoc = gql` @@ -4578,10 +4580,18 @@ export const AllRetirementsDocument = gql` ${CarbonCreditFragmentFragmentDoc}`; export const GetProvenanceRecordsByHashDocument = gql` query getProvenanceRecordsByHash($hash: Bytes!) { - provenanceRecords(where: {transactionHash: $hash}) { - ...ProvenanceRecordFragment - priorRecords(orderBy: createdAt, orderDirection: desc) { + retires(where: {provenance_: {transactionHash: $hash}}) { + credit { + project { + id + registry + } + } + provenance { ...ProvenanceRecordFragment + priorRecords(orderBy: createdAt, orderDirection: desc) { + ...ProvenanceRecordFragment + } } } } diff --git a/carbonmark-api/src/app.constants.ts b/carbonmark-api/src/app.constants.ts index ae2cfaaf3a..80a7841fcd 100644 --- a/carbonmark-api/src/app.constants.ts +++ b/carbonmark-api/src/app.constants.ts @@ -97,6 +97,12 @@ export const REGISTRIES = { url: "https://www.carbonregistry.com", decimals: 0, }, + Moss: { + id: "MOSS", + title: "MOSS", + url: "https://moss.earth", + decimals: 18, + }, }; export const ICR_API = ( diff --git a/carbonmark-api/src/graphql/digitalCarbon.fragments.gql b/carbonmark-api/src/graphql/digitalCarbon.fragments.gql index 1a503d3f26..4628f4b07c 100644 --- a/carbonmark-api/src/graphql/digitalCarbon.fragments.gql +++ b/carbonmark-api/src/graphql/digitalCarbon.fragments.gql @@ -3,12 +3,14 @@ fragment ProvenanceRecordFragment on ProvenanceRecord { transactionType registrySerialNumbers token + tokenId sender receiver originalAmount remainingAmount createdAt updatedAt + transactionHash } fragment RetireFragment on Retire { diff --git a/carbonmark-api/src/graphql/digitalCarbon.gql b/carbonmark-api/src/graphql/digitalCarbon.gql index 3185ce79e0..32fa8ca1ce 100644 --- a/carbonmark-api/src/graphql/digitalCarbon.gql +++ b/carbonmark-api/src/graphql/digitalCarbon.gql @@ -95,10 +95,18 @@ query allRetirements($account_id: String) { } query getProvenanceRecordsByHash($hash: Bytes!) { - provenanceRecords(where: { transactionHash: $hash }) { - ...ProvenanceRecordFragment - priorRecords(orderBy: createdAt, orderDirection: desc) { + retires(where: { provenance_: { transactionHash: $hash } }) { + credit { + project { + id + registry + } + } + provenance { ...ProvenanceRecordFragment + priorRecords(orderBy: createdAt, orderDirection: desc) { + ...ProvenanceRecordFragment + } } } } diff --git a/carbonmark-api/src/routes/retirements/[id]/provenance/get.ts b/carbonmark-api/src/routes/retirements/[id]/provenance/get.ts index 826b71be30..d66504bed0 100644 --- a/carbonmark-api/src/routes/retirements/[id]/provenance/get.ts +++ b/carbonmark-api/src/routes/retirements/[id]/provenance/get.ts @@ -1,4 +1,5 @@ import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import { CreditId } from "../../../../../src/utils/CreditId"; import { gql_sdk } from "../../../../utils/gqlSdk"; import { formatRecord } from "../../../../utils/helpers/records.utils"; import { Params, Querystring } from "../get.schema"; @@ -15,19 +16,30 @@ const handler = () => ) { const sdk = gql_sdk(request.query.network); - // Finds the retirement using polygon-bridged-carbon. We can bypass this when we will have the trasaction hash in the Retire model of polygon-digital-carbon - const retirementRecord = ( + const retirementRecord = await sdk.digital_carbon.getProvenanceRecordsByHash({ hash: request.params.id, - }) - ).provenanceRecords.at(0); + }); + if (retirementRecord.retires.length === 0 || !retirementRecord.retires[0]) { + return reply.notFound(); + } - if (retirementRecord == null) { + if (!retirementRecord.retires[0].provenance) { return reply.notFound(); } + + const [registry] = CreditId.splitProjectId( + retirementRecord.retires[0].credit.project.id + ); + + const lastRecord = { ...retirementRecord.retires[0].provenance }; + + const priorRecords = + retirementRecord.retires[0].provenance?.priorRecords ?? []; + const records = [ - formatRecord(retirementRecord), - ...retirementRecord.priorRecords.map(formatRecord), + formatRecord(lastRecord, registry), + ...priorRecords.map((record) => formatRecord(record, registry)), ]; return reply.send(JSON.stringify(records)); }; diff --git a/carbonmark-api/src/utils/helpers/records.utils.ts b/carbonmark-api/src/utils/helpers/records.utils.ts index d749961e6e..de39a2d907 100644 --- a/carbonmark-api/src/utils/helpers/records.utils.ts +++ b/carbonmark-api/src/utils/helpers/records.utils.ts @@ -1,7 +1,8 @@ -import { formatUnits } from "ethers-v6"; import { pick } from "lodash"; +import { RegistryId } from "src/app.constants"; import { ProvenanceRecord } from "src/models/ProvenanceRecord.model"; import { GetProvenanceRecordsByHashQuery } from "../../.generated/types/digitalCarbon.types"; +import { formatAmountByRegistry } from "../marketplace.utils"; /** * Format a Record coming from a GQL query into a standardized API response fragment @@ -9,11 +10,21 @@ import { GetProvenanceRecordsByHashQuery } from "../../.generated/types/digitalC * @returns */ +type Provenance = NonNullable< + GetProvenanceRecordsByHashQuery["retires"][0]["provenance"] +>; +type PriorRecord = Provenance["priorRecords"][number]; + +type RecordType = Provenance | PriorRecord; + export function formatRecord( - record: - | GetProvenanceRecordsByHashQuery["provenanceRecords"][0] - | GetProvenanceRecordsByHashQuery["provenanceRecords"][0]["priorRecords"][0] + record: RecordType, + registry: RegistryId ): ProvenanceRecord { + if (!record) { + throw new Error("Record is undefined or null"); + } + return { ...pick(record, [ "id", @@ -25,7 +36,11 @@ export function formatRecord( ]), createdAt: Number(record.createdAt), updatedAt: Number(record.updatedAt), - originalAmount: Number(formatUnits(record.originalAmount, 18)), - remainingAmount: Number(formatUnits(record.remainingAmount, 18)), + originalAmount: Number( + formatAmountByRegistry(registry, record.originalAmount) + ), + remainingAmount: Number( + formatAmountByRegistry(registry, record.remainingAmount) + ), }; } diff --git a/carbonmark-api/src/utils/helpers/retirements.utils.ts b/carbonmark-api/src/utils/helpers/retirements.utils.ts index ee96c16959..66ea0e8c1c 100644 --- a/carbonmark-api/src/utils/helpers/retirements.utils.ts +++ b/carbonmark-api/src/utils/helpers/retirements.utils.ts @@ -1,7 +1,8 @@ -import { formatUnits } from "ethers-v6"; import { pick } from "lodash"; import { GetRetirementByHashQuery } from "../../.generated/types/digitalCarbon.types"; import { Retirement } from "../../models/Retirement.model"; +import { CreditId } from "../CreditId"; +import { formatAmountByRegistry } from "../marketplace.utils"; import { formatCarbonCredit } from "./carbonCredits.utils"; import { MOSS_POOL } from "./utils.constants"; @@ -14,6 +15,12 @@ import { MOSS_POOL } from "./utils.constants"; export function formatRetirement( retirement: GetRetirementByHashQuery["retires"][0] ): Retirement { + let registry; + + retirement.credit.project.id === "Moss" + ? (registry = "MOSS") + : ([registry] = CreditId.splitProjectId(retirement.credit.project.id)); + return { ...pick(retirement, [ "id", @@ -26,7 +33,7 @@ export function formatRetirement( beneficiaryAddress: retirement.beneficiaryAddress.id, retiringAddress: retirement.retiringAddress.id, timestamp: Number(retirement.timestamp), - amount: Number(formatUnits(retirement.amount || 0, 18)), + amount: Number(formatAmountByRegistry(registry, retirement.amount)), hasProvenanceDetails: retirement.pool?.id == MOSS_POOL, credit: retirement.credit ? formatCarbonCredit(retirement.credit) diff --git a/carbonmark-api/src/utils/marketplace.utils.ts b/carbonmark-api/src/utils/marketplace.utils.ts index d92ed0fa15..c249df8662 100644 --- a/carbonmark-api/src/utils/marketplace.utils.ts +++ b/carbonmark-api/src/utils/marketplace.utils.ts @@ -44,6 +44,8 @@ export const formatAmountByRegistry = ( registryId: RegistryId, quantity: string ) => { + registryId ?? (registryId = "MOSS"); + const registry = Object.values(REGISTRIES).find((r) => r.id === registryId); if (!registry) { diff --git a/carbonmark-api/test/fixtures/digitalCarbon.ts b/carbonmark-api/test/fixtures/digitalCarbon.ts index a3cda1d2bc..91eb646812 100644 --- a/carbonmark-api/test/fixtures/digitalCarbon.ts +++ b/carbonmark-api/test/fixtures/digitalCarbon.ts @@ -111,11 +111,25 @@ const provenanceRecordWithoutPriorRecords = aProvenanceRecord({ updatedAt: "1701095377", priorRecords: [], }); -const provenanceRecord = aProvenanceRecord({ +const fullProvenanceRecord = aProvenanceRecord({ ...provenanceRecordWithoutPriorRecords, priorRecords: [provenanceRecordWithoutPriorRecords], }); +const retireWithProvenance = [ + { + credit: { + project: { + id: "VCS-191", + registry: "VCS", + }, + }, + provenance: { + ...fullProvenanceRecord, + }, + }, +]; + const empty_countries = { data: { carbonProjects: [], @@ -128,7 +142,7 @@ const fixtures = { poolBalance, digitalCarbonProject, retirement, - provenanceRecord, + retireWithProvenance, }; export default fixtures; diff --git a/carbonmark-api/test/routes/retirements/[id]/provenance/get.test.mocks.ts b/carbonmark-api/test/routes/retirements/[id]/provenance/get.test.mocks.ts index 3b69ee51e6..b707ac44d1 100644 --- a/carbonmark-api/test/routes/retirements/[id]/provenance/get.test.mocks.ts +++ b/carbonmark-api/test/routes/retirements/[id]/provenance/get.test.mocks.ts @@ -2,7 +2,7 @@ import nock from "nock"; import { ProvenanceRecord } from "src/models/ProvenanceRecord.model"; import { GRAPH_URLS } from "../../../../../src/app.constants"; import { fixtures } from "../../../../fixtures"; -const mockProvenanceRecord = fixtures.digitalCarbon.provenanceRecord; +const retires = fixtures.digitalCarbon.retireWithProvenance; export const mockDigitalCarbonProvenanceRecords = ( override?: ProvenanceRecord[] @@ -10,5 +10,5 @@ export const mockDigitalCarbonProvenanceRecords = ( nock(GRAPH_URLS["polygon"].digitalCarbon) .post("", (body) => body.query.includes("getProvenanceRecordsByHash")) .reply(200, { - data: { provenanceRecords: override ?? [mockProvenanceRecord] }, + data: { retires: override ?? retires }, }); diff --git a/carbonmark-api/test/routes/retirements/[id]/provenance/get.test.ts b/carbonmark-api/test/routes/retirements/[id]/provenance/get.test.ts index 0809bc9a3b..04ace37c68 100644 --- a/carbonmark-api/test/routes/retirements/[id]/provenance/get.test.ts +++ b/carbonmark-api/test/routes/retirements/[id]/provenance/get.test.ts @@ -5,7 +5,8 @@ import { build } from "../../../../helper"; import { DEV_URL } from "../../../../test.constants"; import { mockDigitalCarbonProvenanceRecords } from "./get.test.mocks"; -const mockProvenanceRecord = fixtures.digitalCarbon.provenanceRecord; +const mockProvenanceRecord = + fixtures.digitalCarbon.retireWithProvenance[0].provenance; describe("GET /retirements/:id/provenance", () => { let fastify: FastifyInstance; @@ -29,6 +30,7 @@ describe("GET /retirements/:id/provenance", () => { url: `${DEV_URL}/retirements/0xa049a8354af988a4285eadc5c540590d26d95bca1c6a85c873e32a5c280e7509/provenance`, }); const record = await response.json(); + const transformedRecord = { ...pick(mockProvenanceRecord, [ "id",