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 2 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
43 changes: 38 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,37 @@ export async function parseDeposit(

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

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

let amountInUSD: number | undefined;
if (resourceType !== DepositType.FUNGIBLE) {
amountInUSD = undefined;
} else {
try {
amountInUSD = await coinMarketCapService.getValueInUSD(
mj52951 marked this conversation as resolved.
Show resolved Hide resolved
amount,
resource.symbol,
);
} catch (error) {
logger.error((error as Error).message);
amountInUSD = 0;
}
}

let senderStatus: string;
try {
senderStatus = (await ofacComplianceService.checkSanctionedAddress(
mj52951 marked this conversation as resolved.
Show resolved Hide resolved
transaction.from,
)) as string;
} catch (e) {
logger.error(`Checking address failed: ${(e as Error).message}`);
senderStatus = "";
}

return {
id: generateTransferID(
event.depositNonce.toString(),
Expand All @@ -77,11 +112,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
24 changes: 24 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,25 @@ async function startProcessing(): Promise<void> {
);
}

const coinMarketCapAPIKey = process.env.COINMARKETCAP_API_KEY || "";
mj52951 marked this conversation as resolved.
Show resolved Hide resolved
const coinMarketCapUrl = process.env.COINMARKETCAP_API_URL || "";
const ttlInMins = Number(process.env.CACHE_TTL_IN_MINS) || 5;
const memoryCache = await caching("memory", {
ttl: ttlInMins * 1000,
});
const coinMarketCapServiceInstance = new CoinMarketCapService(
coinMarketCapAPIKey,
coinMarketCapUrl,
memoryCache,
);

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

switch (domainConfig.domainType) {
case Network.EVM: {
const provider = new ethers.JsonRpcProvider(domainConfig.rpcURL);
Expand All @@ -43,6 +65,8 @@ async function startProcessing(): Promise<void> {
thisDomain,
provider,
substrateRpcUrlConfig,
coinMarketCapServiceInstance,
ofacComplianceService,
);
break;
}
Expand Down
88 changes: 88 additions & 0 deletions src/services/coinmarketcap/coinmarketcap.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
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 { 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,
): Promise<number> {
const convertedValue = await this.getValueConvertion(amount, tokenSymbol);
return convertedValue.toNumber();
}
}

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

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 | Error> {
mj52951 marked this conversation as resolved.
Show resolved Hide resolved
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 : "";
}
}
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