Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Add missing services #13

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
"@types/chai": "^4.3.16",
"@types/mocha": "^10.0.7",
"@types/sinon": "^17.0.3",
"bignumber.js": "^9.1.2",
"cache-manager": "^5.2.4",
"chai": "^4.3.7",
"eslint": "^8.21.0",
"mocha": "^10.2.0",
Expand Down
1 change: 1 addition & 0 deletions src/evmIndexer/evmIndexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export async function processDeposits(
accountID: d.sender,
deposit: deposit,
fee: new Fee(d.fee),
usdValue: d.usdValue,
});

if (!deposits.has(d.id)) {
Expand Down
1 change: 1 addition & 0 deletions src/evmIndexer/evmTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type DecodedDepositLog = {
amount: string;
senderStatus?: string;
fee: FeeData;
usdValue?: number;
};

export type DecodedProposalExecutionLog = {
Expand Down
28 changes: 23 additions & 5 deletions src/evmIndexer/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import * as FeeHandlerRouter from "../../abi/FeeHandlerRouter.json";
import * as bridge from "../../abi/bridge";
import type { Domain } from "../../config";
import type { Log } from "../../evmProcessor";
import type CoinMarketCapService from "../../services/coinmarketcap/coinmarketcap.service";
import type { OfacComplianceService } from "../../services/ofac";
import { generateTransferID } from "../../utils";
import { logger } from "../../utils/logger";
import type {
Expand All @@ -39,6 +41,8 @@ export async function parseDeposit(
toDomain: Domain,
provider: Provider,
substrateRpcUrlConfig: Map<number, ApiPromise>,
coinMarketCapService: CoinMarketCapService,
ofacComplianceService: OfacComplianceService,
): Promise<DecodedDepositLog> {
const event = bridge.events.Deposit.decode(log);
const resource = fromDomain.resources.find(
Expand All @@ -54,6 +58,22 @@ export async function parseDeposit(

const transaction = assertNotNull(log.transaction, "Missing transaction");

const amount = decodeAmountsOrTokenId(
event.data,
resourceDecimals,
resourceType,
) as string;

const amountInUSD = await coinMarketCapService.getValueInUSD(
amount,
resource.symbol,
resourceType,
);

const senderStatus = await ofacComplianceService.checkSanctionedAddress(
transaction.from,
);

return {
id: generateTransferID(
event.depositNonce.toString(),
Expand All @@ -77,11 +97,9 @@ export async function parseDeposit(
depositData: event.data,
handlerResponse: event.handlerResponse,
transferType: resourceType,
amount: decodeAmountsOrTokenId(
event.data,
resourceDecimals,
resourceType,
) as string,
amount: amount,
senderStatus: senderStatus,
usdValue: amountInUSD,
fee: await getFee(event, fromDomain, provider),
};
}
Expand Down
6 changes: 6 additions & 0 deletions src/evmProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
parseFailedHandlerExecution,
parseProposalExecution,
} from "./evmIndexer/utils";
import type CoinMarketCapService from "./services/coinmarketcap/coinmarketcap.service";
import type { OfacComplianceService } from "./services/ofac";

let processor: EvmBatchProcessor;

Expand All @@ -47,6 +49,8 @@ export function startEvmProcessing(
thisDomain: Domain,
provider: Provider,
substrateRpcUrlConfig: Map<number, ApiPromise>,
coinMarketCapService: CoinMarketCapService,
ofacComplianceService: OfacComplianceService,
): void {
processor = getEvmProcessor(processorConfig);

Expand Down Expand Up @@ -78,6 +82,8 @@ export function startEvmProcessing(
toDomain,
provider,
substrateRpcUrlConfig,
coinMarketCapService,
ofacComplianceService,
),
);
} else if (log.topics[0] === bridge.events.ProposalExecution.topic) {
Expand Down
20 changes: 20 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ The Licensed Work is (c) 2024 Sygma
SPDX-License-Identifier: LGPL-3.0-only
*/
import { Network } from "@buildwithsygma/sygma-sdk-core";
import { caching } from "cache-manager";
import { ethers } from "ethers";

import {
Expand All @@ -12,6 +13,8 @@ import {
getSsmDomainConfig,
} from "./config";
import { startEvmProcessing } from "./evmProcessor";
import CoinMarketCapService from "./services/coinmarketcap/coinmarketcap.service";
import { OfacComplianceService } from "./services/ofac";
import { logger } from "./utils/logger";

async function startProcessing(): Promise<void> {
Expand All @@ -28,6 +31,21 @@ async function startProcessing(): Promise<void> {
);
}

const ttlInMins = Number(process.env.CACHE_TTL_IN_MINS) || 5;
const memoryCache = await caching("memory", {
ttl: ttlInMins * 1000,
});
const coinMarketCapServiceInstance = new CoinMarketCapService(
process.env.COINMARKETCAP_API_KEY || "",
process.env.COINMARKETCAP_API_URL || "",
memoryCache,
);

const ofacComplianceService = new OfacComplianceService(
process.env.CHAIN_ANALYSIS_URL || "",
process.env.CHAIN_ANALYSIS_API_KEY || "",
);

switch (domainConfig.domainType) {
case Network.EVM: {
const provider = new ethers.JsonRpcProvider(domainConfig.rpcURL);
Expand All @@ -43,6 +61,8 @@ async function startProcessing(): Promise<void> {
thisDomain,
provider,
substrateRpcUrlConfig,
coinMarketCapServiceInstance,
ofacComplianceService,
);
break;
}
Expand Down
102 changes: 102 additions & 0 deletions src/services/coinmarketcap/coinmarketcap.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
The Licensed Work is (c) 2024 Sygma
SPDX-License-Identifier: LGPL-3.0-only
*/

import path from "path";

import { BigNumber } from "bignumber.js";
import type { MemoryCache } from "cache-manager";

import { DepositType } from "../../evmIndexer/evmTypes";
import { fetchRetry } from "../../utils";
import { logger } from "../../utils/logger";

export type CoinMaketCapResponse = {
id: number;
symbol: string;
name: string;
amount: number;
last_updated: string;
quote: {
USD: {
price: BigNumber;
last_updated: string;
};
};
};

class CoinMarketCapService {
private coinMarketCapApiKey: string;
private coinMarketCapUrl: string;
private memoryCache: MemoryCache;

constructor(
coinMarketCapKey: string,
coinMarketCapApiUrl: string,
memoryCache: MemoryCache,
) {
this.coinMarketCapApiKey = coinMarketCapKey;
this.coinMarketCapUrl = coinMarketCapApiUrl;
this.memoryCache = memoryCache;
}

private async getValueConvertion(
amount: string,
tokenSymbol: string,
): Promise<CoinMaketCapResponse["quote"]["USD"]["price"]> {
const tokenValue: BigNumber | undefined =
await this.memoryCache.get(tokenSymbol);
if (tokenValue) {
return BigNumber(amount).times(tokenValue);
}

const url = path.join(
this.coinMarketCapUrl,
`/v2/tools/price-conversion?amount=1&symbol=${tokenSymbol}&convert=USD`,
);
logger.debug(`Calling CoinMarketCap service with URL: ${url}`);
try {
const response = await fetchRetry(url, {
method: "GET",
headers: {
"X-CMC_PRO_API_KEY": this.coinMarketCapApiKey,
},
});

const {
data: [res],
} = (await response.json()) as { data: CoinMaketCapResponse[] };
await this.memoryCache.set(tokenSymbol, res.quote.USD.price);
return BigNumber(amount).times(BigNumber(res.quote.USD.price));
} catch (err) {
if (err instanceof Error) {
logger.error(err.message);
}
throw new Error("Error getting value from CoinMarketCap");
}
}

public async getValueInUSD(
amount: string,
tokenSymbol: string,
resourceType: DepositType,
): Promise<number | undefined> {
if (resourceType !== DepositType.FUNGIBLE) {
return undefined;
} else {
try {
const convertedValue = await this.getValueConvertion(
amount,
tokenSymbol,
);
return convertedValue.toNumber();
} catch (error) {
logger.error((error as Error).message);
return 0;
}
}
}
}

export default CoinMarketCapService;
56 changes: 56 additions & 0 deletions src/services/ofac/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
The Licensed Work is (c) 2024 Sygma
SPDX-License-Identifier: LGPL-3.0-only
*/
import url from "url";

import { logger } from "../../utils/logger";

type ChainAnalysisIdentification = {
category: string;
name: string;
description: string;
url: string;
};

type ChainAnalysisResponse = {
identifications: Array<ChainAnalysisIdentification> | [];
};

enum AddressStatus {
OFAC = "ofac",
}

export class OfacComplianceService {
private chainAnalysisUrl: string;
private chainAnalysisApiKey: string;

constructor(chainAnalysisUrl: string, chainAnalisysApiKey: string) {
this.chainAnalysisUrl = chainAnalysisUrl;
this.chainAnalysisApiKey = chainAnalisysApiKey;
}

public async checkSanctionedAddress(address: string): Promise<string> {
try {
const urlToUse = url.resolve(this.chainAnalysisUrl, address);

const response = await fetch(urlToUse, {
headers: {
"X-API-Key": `${this.chainAnalysisApiKey}`,
Accept: "application/json",
},
});
const data = (await response.json()) as ChainAnalysisResponse;

if (response.status !== 200) {
throw new Error(
`Chain Analysis API returned status ${response.status}`,
);
}
return data.identifications.length ? AddressStatus.OFAC : "";
} catch (error) {
logger.error(`Checking address failed: ${(error as Error).message}`);
return "";
}
}
}
31 changes: 31 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,34 @@ export function generateTransferID(
): string {
return depositNonce + "-" + fromDomainID + "-" + toDomainID;
}

export async function fetchRetry(
input: RequestInfo | URL,
init?: RequestInit | undefined,
retryCount = parseInt(process.env.RETRY_COUNT || "3"),
backoff = parseInt(process.env.BACKOFF || "500"),
): Promise<Response> {
let statusCode = 0;
while (retryCount > 0) {
try {
const res = await fetch(input, init);
if (res.status != 200) {
statusCode = res.status;
throw new Error();
}
return res;
} catch {
await sleep(backoff);
backoff *= 2;
} finally {
retryCount -= 1;
}
}
throw new Error(
`Error while fetching URL: ${String(input)}. Status code: ${statusCode}`,
);
}

export async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
24 changes: 24 additions & 0 deletions tests/unit/fetchRetry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
The Licensed Work is (c) 2024 Sygma
SPDX-License-Identifier: LGPL-3.0-only
*/
import { expect } from "chai"
import { fetchRetry } from "../../src/utils/"

describe("Failed requests retry testing", function () {
it("Should successfully fetch", async () => {
const res = await fetchRetry("https://example.org", {}, 1, 100)
expect(res.status).to.be.deep.equal(200)
})

it("Should fail because of invalid request", async () => {
try {
await fetchRetry("https://invalid-url", {}, 1, 100)
} catch (err) {
expect(err).to.be.not.null
if (err instanceof Error) {
expect(err.message).to.be.equal("Error while fetching URL: https://invalid-url. Status code: 0")
}
}
})
})
Loading
Loading