Skip to content

Commit

Permalink
feat(iam): using moralis SDK for eth price and caching in memory for …
Browse files Browse the repository at this point in the history
…5 minutes
  • Loading branch information
lucianHymer committed Aug 29, 2023
1 parent 37a505c commit 605480f
Show file tree
Hide file tree
Showing 14 changed files with 1,514 additions and 199 deletions.
20 changes: 11 additions & 9 deletions database-client/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */

module.exports = {
// [...]
"preset": 'ts-jest',
"extensionsToTreatAsEsm": [".ts"],
"globals": {
"ts-jest": {
"useESM": true
}
}
}
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
useESM: true,
},
],
},
preset: "ts-jest",
extensionsToTreatAsEsm: [".ts"],
};
4 changes: 2 additions & 2 deletions database-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
"@glazed/types": "^0.2.0",
"@types/jest": "^27.4.1",
"@types/node": "^16.11.6",
"jest": "^27.5.1",
"ts-jest": "^27.1.4",
"jest": "^29.6.4",
"ts-jest": "^29.1.1",
"ts-node": "^10.8.0"
},
"resolutions": {
Expand Down
1 change: 1 addition & 0 deletions iam/.env-example.env
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ IAM_PORT=8003

RPC_URL=https://eth-mainnet.alchemyapi.io/v2/<API_KEY>
ALCHEMY_API_KEY=<API_KEY>
MORALIS_API_KEY=<API_KEY>

AMI_API_TOKEN='SECRET_GITCOIN_AMI_API_TOKEN'

Expand Down
1 change: 1 addition & 0 deletions iam/__tests__/additional_signer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ process.env.GITCOIN_VERIFIER_CHAIN_ID = "84531";
process.env.ALLO_SCORER_ID = "1";
process.env.SCORER_ENDPOINT = "http://127.0.0.1:8002";
process.env.SCORER_API_KEY = "abcd";
process.env.MORALIS_API_KEY = "abcd";
process.env.EAS_GITCOIN_STAMP_SCHEMA = "0x";

// ---- Test subject
Expand Down
63 changes: 63 additions & 0 deletions iam/__tests__/easFees.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { getEASFeeAmount } from "../src/utils/easFees";
import { utils } from "ethers";
import Moralis from "moralis";

jest.mock("moralis", () => ({
EvmApi: {
token: {
getTokenPrice: jest.fn().mockResolvedValue({
result: { usdPrice: 3000 },
}),
},
},
}));

describe("EthPriceLoader", () => {
beforeAll(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.advanceTimersByTime(1000 * 60 * 6); // Advance by 6 minutes
jest.clearAllMocks();
});

describe("getEASFeeAmount", () => {
it("should calculate the correct EAS Fee amount based on the current ETH price", async () => {
const usdFeeAmount = 2;
const result = await getEASFeeAmount(usdFeeAmount);

const expectedEthFeeAmount = usdFeeAmount / 3000;
const expectedBigNumberValue = utils.parseEther(expectedEthFeeAmount.toFixed(18));

expect(result).toEqual(expectedBigNumberValue);
});

it("should handle Moralis errors gracefully", async () => {
(Moralis.EvmApi.token.getTokenPrice as jest.Mock).mockRejectedValueOnce(new Error("Failed fetching price"));

await expect(getEASFeeAmount(2)).rejects.toThrow("Failed to get ETH price");
});
});

it("should call Moralis API only once if getEASFeeAmount is called multiple times in succession before cachePeriod is reached", async () => {
await getEASFeeAmount(2);
await getEASFeeAmount(3);
await getEASFeeAmount(4);

expect(Moralis.EvmApi.token.getTokenPrice).toHaveBeenCalledTimes(1);
});

it("should call Moralis API again if cachePeriod is exceeded", async () => {
// We're making the first call
await getEASFeeAmount(2);

// Fast-forwarding time to exceed the cache period of 5 minutes
jest.advanceTimersByTime(1000 * 60 * 6); // Advance by 6 minutes

// Making the second call after the cache period
await getEASFeeAmount(2);

expect(Moralis.EvmApi.token.getTokenPrice).toHaveBeenCalledTimes(2);
});
});
11 changes: 11 additions & 0 deletions iam/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ process.env.ATTESTATION_SIGNER_PRIVATE_KEY = "0x04d16281ff3bf268b29cdd684183f725
process.env.ALLO_SCORER_ID = "1";
process.env.SCORER_ENDPOINT = "http://127.0.0.1:8002";
process.env.SCORER_API_KEY = "abcd";
process.env.MORALIS_API_KEY = "abcd";
process.env.EAS_GITCOIN_STAMP_SCHEMA = "0x";

// ---- Test subject
Expand Down Expand Up @@ -69,6 +70,16 @@ jest.mock("@ethereum-attestation-service/eas-sdk", () => {
};
});

jest.mock("moralis", () => ({
EvmApi: {
token: {
getTokenPrice: jest.fn().mockResolvedValue({
result: { usdPrice: 3000 },
}),
},
},
}));

const chainIdHex = "0xa";

describe("POST /challenge", function () {
Expand Down
4 changes: 3 additions & 1 deletion iam/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"express": "4",
"google-auth-library": "^7.14.1",
"luxon": "^2.4.0",
"moralis": "^2.22.5",
"tslint": "^6.1.3",
"twitter-api-v2": "^1.15.1",
"typescript": "~4.6.3",
Expand All @@ -44,8 +45,9 @@
"@types/node-fetch": "latest",
"@types/supertest": "^2.0.12",
"@types/webpack-env": "^1.16.3",
"jest": "^27.5.1",
"jest": "^29.6.4",
"supertest": "^6.2.2",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1"
},
"resolutions": {
Expand Down
7 changes: 6 additions & 1 deletion iam/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dotenv.config();
// ---- Server
import express, { Request } from "express";
import { router as procedureRouter } from "@gitcoin/passport-platforms/dist/commonjs/procedure-router";
import { TypedDataDomain } from "@ethersproject/abstract-signer";

// ---- Production plugins
import cors from "cors";
Expand Down Expand Up @@ -75,6 +76,10 @@ if (!process.env.EAS_GITCOIN_STAMP_SCHEMA) {
configErrors.push("EAS_GITCOIN_STAMP_SCHEMA is required");
}

if (!process.env.MORALIS_API_KEY) {
configErrors.push("MORALIS_API_KEY is required");
}

if (configErrors.length > 0) {
configErrors.forEach((error) => console.error(error)); // eslint-disable-line no-console
throw new Error("Missing required configuration");
Expand All @@ -95,7 +100,7 @@ export const config: {

const attestationSignerWallet = new ethers.Wallet(process.env.ATTESTATION_SIGNER_PRIVATE_KEY);

export const getAttestationDomainSeparator = (chainIdHex: keyof typeof onchainInfo) => {
export const getAttestationDomainSeparator = (chainIdHex: keyof typeof onchainInfo): TypedDataDomain => {
const verifyingContract = onchainInfo[chainIdHex].GitcoinVerifier.address;
const chainId = parseInt(chainIdHex, 16).toString();
return {
Expand Down
17 changes: 14 additions & 3 deletions iam/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
// ---- Main App from index
import { app } from "./index";
import Moralis from "moralis";

// default port to listen on
const port = process.env.IAM_PORT || 80;

// start the Express server
app.listen(port, () => {
const startServer = async (): Promise<void> => {
await Moralis.start({
apiKey: process.env.MORALIS_API_KEY,
});

app.listen(port, () => {
// eslint-disable-next-line no-console
console.log(`server started at http://localhost:${port}`);
});
};

startServer().catch((error) => {
// eslint-disable-next-line no-console
console.log(`server started at http://localhost:${port}`);
console.error(error);
});
65 changes: 48 additions & 17 deletions iam/src/utils/easFees.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,52 @@
import axios from "axios";
import { utils } from "ethers";
import { BigNumber } from "@ethersproject/bignumber";
import Moralis from "moralis";

export async function getEASFeeAmount(usdAmount: number): Promise<BigNumber> {
const ethUSD: {
data: {
ethereum: {
usd: number;
};
};
} = await axios.get("https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=USD", {
headers: {
// TODO add API key
Accept: "application/json",
},
});

const ethAmount = usdAmount / ethUSD.data.ethereum.usd;
return utils.parseEther(ethAmount.toFixed(18));
const FIVE_MINUTES = 1000 * 60 * 5;
const WETH_CONTRACT = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";

class EthPriceLoader {
cachedPrice: number;
lastUpdated: number;
cachePeriod = FIVE_MINUTES;

constructor() {
this.cachedPrice = 0;
this.lastUpdated = 0;
}

async getPrice(): Promise<number> {
if (this.#needsUpdate()) {
this.cachedPrice = await this.#requestCurrentPrice();
this.lastUpdated = Date.now();
}
return this.cachedPrice;
}

#needsUpdate(): boolean {
return Date.now() - this.lastUpdated > this.cachePeriod;
}

async #requestCurrentPrice(): Promise<number> {
try {
const { result } = await Moralis.EvmApi.token.getTokenPrice({
chain: "0x1",
address: WETH_CONTRACT,
});

return result.usdPrice;
} catch (e) {
let message = "Failed to get ETH price";
if (e instanceof Error) message += `, ${e.name}: ${e.message}`;
throw new Error(message);
}
}
}

const ethPriceLoader = new EthPriceLoader();

export async function getEASFeeAmount(usdFeeAmount: number): Promise<BigNumber> {
const ethPrice = await ethPriceLoader.getPrice();
const ethFeeAmount = usdFeeAmount / ethPrice;
return utils.parseEther(ethFeeAmount.toFixed(18));
}
4 changes: 4 additions & 0 deletions infra/production/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,10 @@ const service = new awsx.ecs.FargateService("dpopp-iam", {
name: "TRUSTA_LABS_ACCESS_TOKEN",
valueFrom: `${IAM_SERVER_SSM_ARN}:TRUSTA_LABS_ACCESS_TOKEN::`,
},
{
name: "MORALIS_API_KEY",
valueFrom: `${IAM_SERVER_SSM_ARN}:MORALIS_API_KEY::`,
},
],
},
},
Expand Down
4 changes: 4 additions & 0 deletions infra/review/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,10 @@ const service = new awsx.ecs.FargateService("dpopp-iam", {
name: "TRUSTA_LABS_ACCESS_TOKEN",
valueFrom: `${IAM_SERVER_SSM_ARN}:TRUSTA_LABS_ACCESS_TOKEN::`,
},
{
name: "MORALIS_API_KEY",
valueFrom: `${IAM_SERVER_SSM_ARN}:MORALIS_API_KEY::`,
},
],
},
},
Expand Down
4 changes: 4 additions & 0 deletions infra/staging/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,10 @@ const service = new awsx.ecs.FargateService("dpopp-iam", {
name: "TRUSTA_LABS_ACCESS_TOKEN",
valueFrom: `${IAM_SERVER_SSM_ARN}:TRUSTA_LABS_ACCESS_TOKEN::`,
},
{
name: "MORALIS_API_KEY",
valueFrom: `${IAM_SERVER_SSM_ARN}:MORALIS_API_KEY::`,
},
],
},
},
Expand Down
Loading

0 comments on commit 605480f

Please sign in to comment.