diff --git a/carbonmark-api/package.json b/carbonmark-api/package.json index a4dd439606..ffb55f0079 100644 --- a/carbonmark-api/package.json +++ b/carbonmark-api/package.json @@ -1,6 +1,6 @@ { "name": "@klimadao/carbonmark-api", - "version": "6.1.0", + "version": "6.1.1", "description": "An API for exploring Carbonmark project data, prices and activity.", "main": "app.ts", "scripts": { diff --git a/carbonmark-api/src/app.constants.ts b/carbonmark-api/src/app.constants.ts index 820d6ff423..32aa849d7e 100644 --- a/carbonmark-api/src/app.constants.ts +++ b/carbonmark-api/src/app.constants.ts @@ -67,6 +67,15 @@ export const RPC_URLS = { polygonTestnetRpc: "https://rpc-mumbai.maticvigil.com", }; +export type RegistryKey = keyof typeof REGISTRIES; + +export type RegistryId = (typeof REGISTRIES)[keyof typeof REGISTRIES]["id"]; + +export const IS_REGISTRY_ID = (id: string): id is RegistryId => { + const REGISTRY_IDS = Object.values(REGISTRIES).map((r) => r.id); + return REGISTRY_IDS.includes(id); +}; + /** Definitions of available registries */ export const REGISTRIES = { Verra: { @@ -74,16 +83,19 @@ export const REGISTRIES = { title: "Verra", url: "https://registry.verra.org", api: "https://registry.verra.org/uiapi", + decimals: 18, }, GoldStandard: { id: "GS", title: "Gold Standard", url: "https://registry.goldstandard.org", + decimals: 18, }, ICR: { id: "ICR", title: "International Carbon Registry", url: "https://www.carbonregistry.com", + decimals: 0, }, }; diff --git a/carbonmark-api/src/routes/purchases/get.utils.ts b/carbonmark-api/src/routes/purchases/get.utils.ts index 1751759817..d666a0a6ec 100644 --- a/carbonmark-api/src/routes/purchases/get.utils.ts +++ b/carbonmark-api/src/routes/purchases/get.utils.ts @@ -1,5 +1,7 @@ import { utils } from "ethers"; import { GetPurchaseByIdQuery } from "src/.generated/types/marketplace.types"; +import { IS_REGISTRY_ID } from "../../../src/app.constants"; +import { formatAmountByRegistry } from "../../../src/utils/marketplace.utils"; import { Purchase } from "../../models/Purchase.model"; import { CreditId } from "../../utils/CreditId"; @@ -14,12 +16,15 @@ export const composePurchaseModel = ( ): Purchase => { const project = purchase.listing.project; // The digits after the registry identifier. e.g 1234 in VCS-1234 - const [, registryProjectId] = CreditId.splitProjectId(project.key); + const [registry, registryProjectId] = CreditId.splitProjectId(project.key); + + if (!IS_REGISTRY_ID(registry)) { + throw new Error(`Invalid registry id in composePurchaseModel: ${registry}`); + } + return { id: purchase.id, - amount: purchase.listing.project.key.startsWith("ICR") - ? purchase.amount - : utils.formatUnits(purchase.amount, 18), + amount: formatAmountByRegistry(registry, purchase.amount), price: utils.formatUnits(purchase.price, 6), listing: { id: purchase.listing.id, diff --git a/carbonmark-api/src/routes/users/[walletOrHandle]/get.ts b/carbonmark-api/src/routes/users/[walletOrHandle]/get.ts index 34fa4ffd58..19d579b51e 100644 --- a/carbonmark-api/src/routes/users/[walletOrHandle]/get.ts +++ b/carbonmark-api/src/routes/users/[walletOrHandle]/get.ts @@ -1,5 +1,6 @@ import { utils } from "ethers"; import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import { IS_REGISTRY_ID } from "../../../../src/app.constants"; import { Activity } from "../../../models/Activity.model"; import { User } from "../../../models/User.model"; import { getActiveListings } from "../../../utils/helpers/listings.utils"; @@ -8,7 +9,10 @@ import { getProfileByHandle, getUserProfilesByIds, } from "../../../utils/helpers/users.utils"; -import { formatListing } from "../../../utils/marketplace.utils"; +import { + formatAmountByRegistry, + formatListing, +} from "../../../utils/marketplace.utils"; import { Params, Querystring, schema } from "./get.schema"; import { getHoldingsByWallet, @@ -82,15 +86,23 @@ const handler = (fastify: FastifyInstance) => handle: UserProfilesMap.get(a.seller.id.toLowerCase())?.handle || null, }; + + const registry = a.project.key.split("-")[0]; + + if (!IS_REGISTRY_ID(registry)) { + throw new Error( + `Invalid registry id in getUserProfilesByIds: ${registry}` + ); + } + return { ...a, - amount: a.project.key.startsWith("ICR") - ? a.amount - : utils.formatUnits(a.amount || "0", 18), + amount: formatAmountByRegistry(registry, a.amount || "0"), price: utils.formatUnits(a.price || "0", 6), - previousAmount: a.project.key.startsWith("ICR") - ? a.previousAmount - : utils.formatUnits(a.previousAmount || "0", 18), + previousAmount: formatAmountByRegistry( + registry, + a.previousAmount || "0" + ), previousPrice: utils.formatUnits(a.previousPrice || "0", 6), buyer: buyer || null, seller: seller || null, diff --git a/carbonmark-api/src/utils/helpers/activities.utils.ts b/carbonmark-api/src/utils/helpers/activities.utils.ts index f3bfaaae6a..d3d2479026 100644 --- a/carbonmark-api/src/utils/helpers/activities.utils.ts +++ b/carbonmark-api/src/utils/helpers/activities.utils.ts @@ -1,10 +1,12 @@ import { utils } from "ethers"; import { FastifyInstance } from "fastify"; import { set, sortBy } from "lodash"; +import { IS_REGISTRY_ID } from "../../../src/app.constants"; import { ActivityType } from "../../.generated/types/marketplace.types"; import { Activity } from "../../models/Activity.model"; import { CreditId } from "../CreditId"; import { GQL_SDK } from "../gqlSdk"; +import { formatAmountByRegistry } from "../marketplace.utils"; import { getUserProfilesByIds } from "./users.utils"; type ActivitiesParams = { @@ -17,23 +19,28 @@ const mapUserToActivities = async ( activities: Activity[], fastify: FastifyInstance ): Promise => { - const formattedActivities = activities.map((activity) => ({ - ...activity, - price: activity.price ? utils.formatUnits(activity.price, 6) : null, - previousPrice: activity.previousPrice - ? utils.formatUnits(activity.previousPrice, 6) - : null, - amount: activity.amount - ? activity.project.key.startsWith("ICR") - ? activity.amount - : utils.formatUnits(activity.amount, 18) - : null, - previousAmount: activity.previousAmount - ? activity.project.key.startsWith("ICR") - ? activity.amount - : utils.formatUnits(activity.previousAmount, 18) - : null, - })); + const formattedActivities = activities.map((activity) => { + const registry = activity.project.key.split("-")[0]; + + if (!IS_REGISTRY_ID(registry)) { + throw new Error( + `Invalid registry id in mapUserToActivities: ${registry}` + ); + } + return { + ...activity, + price: activity.price ? utils.formatUnits(activity.price, 6) : null, + previousPrice: activity.previousPrice + ? utils.formatUnits(activity.previousPrice, 6) + : null, + amount: activity.amount + ? formatAmountByRegistry(registry, activity.amount) + : null, + previousAmount: activity.previousAmount + ? formatAmountByRegistry(registry, activity.previousAmount) + : null, + }; + }); const userIds = new Set(); formattedActivities.forEach((activity) => { diff --git a/carbonmark-api/src/utils/marketplace.utils.ts b/carbonmark-api/src/utils/marketplace.utils.ts index bfb08914a5..d2c338068d 100644 --- a/carbonmark-api/src/utils/marketplace.utils.ts +++ b/carbonmark-api/src/utils/marketplace.utils.ts @@ -1,5 +1,11 @@ import { utils } from "ethers"; +import { formatUnits } from "ethers/lib/utils"; import { compact } from "lodash/fp"; +import { + IS_REGISTRY_ID, + REGISTRIES, + RegistryId, +} from "../../src/app.constants"; import { GetProjectsQuery, Listing, @@ -32,27 +38,41 @@ export type GetProjectListing = NonNullable< GetProjectsQuery["projects"][number]["listings"] >[number]; +/** Format amounts or quantities by registry decimals */ +/** Currently all registries use 18 decimals except ICR, which uses 0 */ + +export const formatAmountByRegistry = ( + registryId: RegistryId, + quantity: string +) => { + const registry = Object.values(REGISTRIES).find((r) => r.id === registryId); + + if (!registry) { + throw new Error(`Registry with id ${registryId} not found.`); + } + + return formatUnits(quantity, registry.decimals); +}; + /** Formats a gql.marketplace listing to match Listing.model, and formats integers */ export const formatListing = (listing: GetProjectListing): ListingModel => { const registry = listing.project.key.split("-")[0]; + if (!IS_REGISTRY_ID(registry)) { + throw new Error(`Invalid registry id in formatListing: ${registry}`); + } + return { ...formatGraphTimestamps(listing), - leftToSell: - registry === "ICR" - ? listing.leftToSell - : utils.formatUnits(listing.leftToSell, 18), + leftToSell: formatAmountByRegistry(registry, listing.leftToSell), singleUnitPrice: utils.formatUnits(listing.singleUnitPrice, 6), - minFillAmount: - registry === "ICR" - ? listing.minFillAmount - : utils.formatUnits(listing.minFillAmount, 18), - totalAmountToSell: - registry === "ICR" - ? listing.totalAmountToSell - : utils.formatUnits(listing.totalAmountToSell, 18), + minFillAmount: formatAmountByRegistry(registry, listing.minFillAmount), + totalAmountToSell: formatAmountByRegistry( + registry, + listing.totalAmountToSell + ), expiration: Number(listing.expiration), project: { ...listing.project, diff --git a/carbonmark-api/test/fixtures/marketplace.ts b/carbonmark-api/test/fixtures/marketplace.ts index 0553ce41b0..7caf426abc 100644 --- a/carbonmark-api/test/fixtures/marketplace.ts +++ b/carbonmark-api/test/fixtures/marketplace.ts @@ -15,6 +15,20 @@ const listing = aListing({ leftToSell: "100000000000000000000", updatedAt: "1234", createdAt: "1234", + project: { + id: "VCS-191-2008", + key: "VCS-191", + vintage: "2008", + category: { id: "Renewable Energy" }, + country: { id: "United States" }, + methodology: "VM0006", + name: "Hydroelectric Fixture", + activities: [], + listings: [], + registry: "VCS", + updatedAt: "1234", + projectAddress: "0x1234", + }, }); const projectWithListing = aProject({ diff --git a/carbonmark-api/test/routes/projects/[id]/get.test.ts b/carbonmark-api/test/routes/projects/[id]/get.test.ts index fb0c16408e..e7224362f3 100644 --- a/carbonmark-api/test/routes/projects/[id]/get.test.ts +++ b/carbonmark-api/test/routes/projects/[id]/get.test.ts @@ -23,10 +23,6 @@ describe("GET /projects/:id", () => { } }); - afterEach(async () => { - nock.cleanAll(); - }); - test("Returns project from CMS without prices or listings", async () => { nock(SANITY_URLS.cms) .post("") @@ -99,10 +95,6 @@ describe("GET /projects/:id", () => { }, }); - nock(GRAPH_URLS["mumbai"].marketplace).post("").reply(200, { - data: {}, - }); - const response = await fastify.inject({ method: "GET", url: `${DEV_URL}/projects/VCS-191-2008?network=polygon`, diff --git a/carbonmark-api/test/routes/projects/get.test.mocks.ts b/carbonmark-api/test/routes/projects/get.test.mocks.ts index 2338e7f6fb..77d2c56b7c 100644 --- a/carbonmark-api/test/routes/projects/get.test.mocks.ts +++ b/carbonmark-api/test/routes/projects/get.test.mocks.ts @@ -152,12 +152,6 @@ export const mockMarketplaceProjects = (override?: Project[]) => { .reply(200, { data: { projects: override ?? [fixtures.marketplace.projectWithListing] }, }); - - nock(GRAPH_URLS["mumbai"].marketplace) - .post("") - .reply(200, { - data: { projects: [fixtures.marketplace.projectWithListing] }, - }); }; //Mocks all categories, countries and vintages export const mockMarketplaceArgs = () => {