Skip to content

Commit

Permalink
[api] Standardize quantity/amount handling (#2189)
Browse files Browse the repository at this point in the history
* api: registry decimals added and formatByRegistry util

* api: standarize amount handling by registry

* api: increased formatAmountByRegistry maintainability

* api: logging clarity i.e. no duplicate logs

* api: added project to listing mock

* api: incremenet api version
  • Loading branch information
psparacino authored Feb 6, 2024
1 parent 81d4024 commit 5ce535f
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 55 deletions.
2 changes: 1 addition & 1 deletion carbonmark-api/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
12 changes: 12 additions & 0 deletions carbonmark-api/src/app.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,35 @@ 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: {
id: "VCS",
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,
},
};

Expand Down
13 changes: 9 additions & 4 deletions carbonmark-api/src/routes/purchases/get.utils.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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,
Expand Down
26 changes: 19 additions & 7 deletions carbonmark-api/src/routes/users/[walletOrHandle]/get.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
41 changes: 24 additions & 17 deletions carbonmark-api/src/utils/helpers/activities.utils.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -17,23 +19,28 @@ const mapUserToActivities = async (
activities: Activity[],
fastify: FastifyInstance
): Promise<Activity[]> => {
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<string>();
formattedActivities.forEach((activity) => {
Expand Down
44 changes: 32 additions & 12 deletions carbonmark-api/src/utils/marketplace.utils.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions carbonmark-api/test/fixtures/marketplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
8 changes: 0 additions & 8 deletions carbonmark-api/test/routes/projects/[id]/get.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
Expand Down Expand Up @@ -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`,
Expand Down
6 changes: 0 additions & 6 deletions carbonmark-api/test/routes/projects/get.test.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down

0 comments on commit 5ce535f

Please sign in to comment.