-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add price service backed by CoinGecko
Add script to generate a JSON index of Asset ID to CoinGecko ID. Add chain libs at the core to govern the supported chains in the application. Add EVM utility to check and get address without regard of the given string format.
- Loading branch information
1 parent
ad672a4
commit 6f515cf
Showing
22 changed files
with
6,686 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 2 additions & 1 deletion
3
apps/orchestration/src/policy-engine/persistence/schema/transaction-request.schema.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 3 additions & 0 deletions
3
apps/orchestration/src/price/core/exception/price.exception.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { ApplicationException } from '@app/orchestration/shared/exception/application.exception' | ||
|
||
export class PriceException extends ApplicationException {} |
89 changes: 89 additions & 0 deletions
89
apps/orchestration/src/price/core/service/__test__/unit/price.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { ASSET_ID_MAINNET_USDC, FIAT_ID_USD } from '@app/orchestration/orchestration.constant' | ||
import { PriceException } from '@app/orchestration/price/core/exception/price.exception' | ||
import { PriceService } from '@app/orchestration/price/core/service/price.service' | ||
import { CoinGeckoClient } from '@app/orchestration/price/http/client/coin-gecko/coin-gecko.client' | ||
import { ETHEREUM, POLYGON } from '@app/orchestration/shared/core/lib/chains.lib' | ||
import { getAssetId } from '@narval/authz-shared' | ||
import { Test, TestingModule } from '@nestjs/testing' | ||
import { mock } from 'jest-mock-extended' | ||
|
||
describe(PriceService.name, () => { | ||
let module: TestingModule | ||
let service: PriceService | ||
let coinGeckoClientMock: CoinGeckoClient | ||
|
||
const SIMPLE_PRICE = { | ||
ethereum: { | ||
usd: 2313.8968819430966 | ||
}, | ||
'matic-network': { | ||
usd: 0.8123732992684908 | ||
}, | ||
'usd-coin': { | ||
usd: 1.000709110429112 | ||
} | ||
} | ||
|
||
beforeEach(async () => { | ||
coinGeckoClientMock = mock<CoinGeckoClient>() | ||
|
||
jest.spyOn(coinGeckoClientMock, 'getSimplePrice').mockResolvedValue(SIMPLE_PRICE) | ||
|
||
module = await Test.createTestingModule({ | ||
providers: [ | ||
PriceService, | ||
{ | ||
provide: CoinGeckoClient, | ||
useValue: coinGeckoClientMock | ||
} | ||
] | ||
}).compile() | ||
|
||
service = module.get<PriceService>(PriceService) | ||
}) | ||
|
||
describe('getPrices', () => { | ||
it('converts asset id to coingecko id', async () => { | ||
await service.getPrices({ | ||
from: [ETHEREUM.coin.id, POLYGON.coin.id, ASSET_ID_MAINNET_USDC], | ||
to: [FIAT_ID_USD] | ||
}) | ||
|
||
expect(coinGeckoClientMock.getSimplePrice).toHaveBeenCalledWith({ | ||
data: { | ||
ids: ['ethereum', 'matic-network', 'usd-coin'], | ||
vs_currencies: ['usd'], | ||
precision: 18 | ||
} | ||
}) | ||
}) | ||
|
||
it('responds with prices', async () => { | ||
const prices = await service.getPrices({ | ||
from: [ETHEREUM.coin.id, POLYGON.coin.id, ASSET_ID_MAINNET_USDC], | ||
to: [FIAT_ID_USD] | ||
}) | ||
|
||
expect(prices).toEqual({ | ||
[ETHEREUM.coin.id]: { | ||
[FIAT_ID_USD]: SIMPLE_PRICE.ethereum.usd | ||
}, | ||
[POLYGON.coin.id]: { | ||
[FIAT_ID_USD]: SIMPLE_PRICE['matic-network'].usd | ||
}, | ||
[ASSET_ID_MAINNET_USDC]: { | ||
[FIAT_ID_USD]: SIMPLE_PRICE['usd-coin'].usd | ||
} | ||
}) | ||
}) | ||
|
||
it('throws PriceException when given asset id does not exist on coin gecko index', () => { | ||
expect(() => | ||
service.getPrices({ | ||
from: [getAssetId('eip155:00000/erc20:0x0000000000000000000000000000000000000000')], | ||
to: [FIAT_ID_USD] | ||
}) | ||
).rejects.toThrow(PriceException) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { PriceException } from '@app/orchestration/price/core/exception/price.exception' | ||
import { CoinGeckoClient } from '@app/orchestration/price/http/client/coin-gecko/coin-gecko.client' | ||
import { SimplePrice } from '@app/orchestration/price/http/client/coin-gecko/coin-gecko.type' | ||
import CoinGeckoAssetIdIndex from '@app/orchestration/price/resource/coin-gecko-asset-id-index.json' | ||
import { CHAINS } from '@app/orchestration/shared/core/lib/chains.lib' | ||
import { FiatId, Price } from '@app/orchestration/shared/core/type/price.type' | ||
import { AssetId, getAssetId, isCoin, parseAsset } from '@narval/authz-shared' | ||
import { HttpStatus, Injectable } from '@nestjs/common' | ||
import { compact } from 'lodash/fp' | ||
|
||
type GetPricesOption = { | ||
from: AssetId[] | ||
to: FiatId[] | ||
} | ||
|
||
@Injectable() | ||
export class PriceService { | ||
constructor(private coinGeckoClient: CoinGeckoClient) {} | ||
|
||
async getPrices(options: GetPricesOption): Promise<Price> { | ||
const from = options.from.map(this.getCoinGeckoId) | ||
|
||
if (from.some((id) => id === null)) { | ||
throw new PriceException({ | ||
message: "Couldn't determine the source ID for the given asset ID", | ||
suggestedHttpStatusCode: HttpStatus.BAD_REQUEST, | ||
context: { options, from } | ||
}) | ||
} | ||
|
||
const prices = await this.coinGeckoClient.getSimplePrice({ | ||
data: { | ||
ids: compact(from), | ||
vs_currencies: options.to.map(this.getCoinGeckoCurrencyId), | ||
precision: 18 | ||
} | ||
}) | ||
|
||
return this.buildPrice(prices) | ||
} | ||
|
||
private buildPrice(prices: SimplePrice): Price { | ||
return Object.keys(prices).reduce((acc, coinId) => { | ||
const assetId = this.getAssetId(coinId) | ||
|
||
if (assetId) { | ||
return { | ||
...acc, | ||
[assetId]: Object.keys(prices[coinId]).reduce((result, fiat) => { | ||
return { | ||
...result, | ||
[`fiat:${fiat}`]: prices[coinId].usd | ||
} | ||
}, {}) | ||
} | ||
} | ||
|
||
return acc | ||
}, {}) | ||
} | ||
|
||
private getAssetId(coinId: string): AssetId | null { | ||
const chain = Object.values(CHAINS).find((chain) => chain.coinGecko.coinId === coinId) | ||
|
||
if (chain) { | ||
return chain.coin.id | ||
} | ||
|
||
for (const key in CoinGeckoAssetIdIndex) { | ||
if (CoinGeckoAssetIdIndex[key as keyof typeof CoinGeckoAssetIdIndex] === coinId) { | ||
return getAssetId(key) | ||
} | ||
} | ||
|
||
return null | ||
} | ||
|
||
private getCoinGeckoId(assetId: AssetId): string | null { | ||
const asset = parseAsset(assetId) | ||
const chain = CHAINS[asset.chainId] | ||
|
||
if (!chain) { | ||
return null | ||
} | ||
|
||
if (isCoin(asset)) { | ||
return chain.coinGecko.coinId | ||
} | ||
|
||
return CoinGeckoAssetIdIndex[assetId as keyof typeof CoinGeckoAssetIdIndex] || null | ||
} | ||
|
||
private getCoinGeckoCurrencyId(fiat: FiatId): string { | ||
return fiat.replace('fiat:', '') | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.