diff --git a/examples/example.ts b/examples/example.ts index fe80136..7f915e3 100644 --- a/examples/example.ts +++ b/examples/example.ts @@ -54,13 +54,13 @@ async function main(): Promise { address ); - const blockfrostAdapter = new BlockfrostAdapter({ - networkId: NetworkId.TESTNET, - blockFrost: new BlockFrostAPI({ + const blockfrostAdapter = new BlockfrostAdapter( + NetworkId.TESTNET, + new BlockFrostAPI({ projectId: blockfrostProjectId, network: "preprod", - }), - }); + }) + ); const utxos = await lucid.utxosAt(address); diff --git a/src/adapter.ts b/src/adapter.ts index 3b97280..cb3fbde 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -1,11 +1,21 @@ import { BlockFrostAPI, BlockfrostServerError, + Responses, } from "@blockfrost/blockfrost-js"; import { PaginationOptions } from "@blockfrost/blockfrost-js/lib/types"; import invariant from "@minswap/tiny-invariant"; +import * as Prisma from "@prisma/client"; import Big from "big.js"; +import JSONBig from "json-bigint"; +import { + C, + fromHex, + SLOT_CONFIG_NETWORK, + slotToBeginUnixTime, +} from "lucid-cardano"; +import { PostgresRepositoryReader } from "./syncer/repository/postgres-reposiotry"; import { Asset } from "./types/asset"; import { DexV1Constant, @@ -13,7 +23,7 @@ import { StableswapConstant, } from "./types/constants"; import { FactoryV2 } from "./types/factory"; -import { NetworkId } from "./types/network"; +import { NetworkEnvironment, NetworkId } from "./types/network"; import { PoolV1, PoolV2, StablePool } from "./types/pool"; import { checkValidPoolOutput, @@ -21,19 +31,23 @@ import { normalizeAssets, } from "./types/pool.internal"; import { StringUtils } from "./types/string"; -import { TxHistory } from "./types/tx.internal"; +import { TxHistory, TxIn, Value } from "./types/tx.internal"; import { getScriptHashFromAddress } from "./utils/address-utils.internal"; +import { networkEnvToLucidNetwork } from "./utils/network.internal"; -export type BlockfrostAdapterOptions = { - networkId: NetworkId; - blockFrost: BlockFrostAPI; +export type GetPoolInTxParams = { + txHash: string; +}; + +export type GetPoolByIdParams = { + id: string; }; export type GetPoolsParams = Omit & { page: number; }; -export type GetPoolByIdParams = { +export type GetPoolHistoryParams = PaginationOptions & { id: string; }; @@ -49,43 +63,163 @@ export type GetV2PoolPriceParams = { decimalsB?: number; }; -export type GetPoolHistoryParams = PaginationOptions & { - id: string; -}; +interface Adapter { + getAssetDecimals(asset: string): Promise; -export type GetPoolInTxParams = { - txHash: string; -}; + getDatumByDatumHash(datumHash: string): Promise; -export type GetStablePoolInTxParams = { - networkId: NetworkId; - txHash: string; -}; - -export class BlockfrostAdapter { - private readonly api: BlockFrostAPI; - private readonly networkId: NetworkId; + /** + * Get pool state in a transaction. + * @param {Object} params - The parameters. + * @param {string} params.txHash - The transaction hash containing pool output. One of the way to acquire is by calling getPoolHistory. + * @returns {PoolV1.State} - Returns the pool state or null if the transaction doesn't contain pool. + */ + getV1PoolInTx({ txHash }: GetPoolInTxParams): Promise; - constructor({ networkId, blockFrost }: BlockfrostAdapterOptions) { - this.networkId = networkId; - this.api = blockFrost; - } + /** + * Get a specific pool by its ID. + * @param {Object} params - The parameters. + * @param {string} params.pool - The pool ID. This is the asset name of a pool's NFT and LP tokens. It can also be acquired by calling pool.id. + * @returns {PoolV1.State | null} - Returns the pool or null if not found. + */ + getV1PoolById({ id }: GetPoolByIdParams): Promise; /** * @returns The latest pools or empty array if current page is after last page */ + getV1Pools(params: GetPoolsParams): Promise; + + getV1PoolHistory(params: GetPoolHistoryParams): Promise; + + /** + * Get pool price. + * @param {Object} params - The parameters to calculate pool price. + * @param {string} params.pool - The pool we want to get price. + * @param {string} [params.decimalsA] - The decimals of assetA in pool, if undefined then query from Blockfrost. + * @param {string} [params.decimalsB] - The decimals of assetB in pool, if undefined then query from Blockfrost. + * @returns {[string, string]} - Returns a pair of asset A/B price and B/A price, adjusted to decimals. + */ + getV1PoolPrice(params: GetPoolPriceParams): Promise<[Big, Big]>; + + getAllV2Pools(): Promise<{ pools: PoolV2.State[]; errors: unknown[] }>; + + getV2Pools( + params: GetPoolsParams + ): Promise<{ pools: PoolV2.State[]; errors: unknown[] }>; + + getV2PoolByPair(assetA: Asset, assetB: Asset): Promise; + + getV2PoolByLp(lpAsset: Asset): Promise; + + /** + * Get pool price. + * @param {Object} params - The parameters to calculate pool price. + * @param {string} params.pool - The pool we want to get price. + * @param {string} [params.decimalsA] - The decimals of assetA in pool, if undefined then query from Blockfrost. + * @param {string} [params.decimalsB] - The decimals of assetB in pool, if undefined then query from Blockfrost. + * @returns {[string, string]} - Returns a pair of asset A/B price and B/A price, adjusted to decimals. + */ + getV2PoolPrice(params: GetV2PoolPriceParams): Promise<[Big, Big]>; + + getAllFactoriesV2(): Promise<{ + factories: FactoryV2.State[]; + errors: unknown[]; + }>; + + getFactoryV2ByPair( + assetA: Asset, + assetB: Asset + ): Promise; + + getAllStablePools(): Promise<{ + pools: StablePool.State[]; + errors: unknown[]; + }>; + + getStablePoolByLpAsset(lpAsset: Asset): Promise; + + getStablePoolByNFT(nft: Asset): Promise; +} + +export class BlockfrostAdapter implements Adapter { + protected readonly networkId: NetworkId; + private readonly blockFrostApi: BlockFrostAPI; + + constructor(networkId: NetworkId, blockFrostApi: BlockFrostAPI) { + this.networkId = networkId; + this.blockFrostApi = blockFrostApi; + } + + public async getAssetDecimals(asset: string): Promise { + if (asset === "lovelace") { + return 6; + } + try { + const assetAInfo = await this.blockFrostApi.assetsById(asset); + return assetAInfo.metadata?.decimals ?? 0; + } catch (err) { + if (err instanceof BlockfrostServerError && err.status_code === 404) { + return 0; + } + throw err; + } + } + + public async getDatumByDatumHash(datumHash: string): Promise { + const scriptsDatum = await this.blockFrostApi.scriptsDatumCbor(datumHash); + return scriptsDatum.cbor; + } + + public async getV1PoolInTx({ + txHash, + }: GetPoolInTxParams): Promise { + const poolTx = await this.blockFrostApi.txsUtxos(txHash); + const poolUtxo = poolTx.outputs.find( + (o) => + getScriptHashFromAddress(o.address) === DexV1Constant.POOL_SCRIPT_HASH + ); + if (!poolUtxo) { + return null; + } + + checkValidPoolOutput(poolUtxo.address, poolUtxo.amount, poolUtxo.data_hash); + invariant( + poolUtxo.data_hash, + `expect pool to have datum hash, got ${poolUtxo.data_hash}` + ); + + const txIn: TxIn = { txHash: txHash, index: poolUtxo.output_index }; + return new PoolV1.State( + poolUtxo.address, + txIn, + poolUtxo.amount, + poolUtxo.data_hash + ); + } + + public async getV1PoolById({ + id, + }: GetPoolByIdParams): Promise { + const nft = `${DexV1Constant.POOL_NFT_POLICY_ID}${id}`; + const nftTxs = await this.blockFrostApi.assetsTransactions(nft, { + count: 1, + page: 1, + order: "desc", + }); + if (nftTxs.length === 0) { + return null; + } + return this.getV1PoolInTx({ txHash: nftTxs[0].tx_hash }); + } + public async getV1Pools({ page, count = 100, order = "asc", }: GetPoolsParams): Promise { - const utxos = await this.api.addressesUtxos( + const utxos = await this.blockFrostApi.addressesUtxos( DexV1Constant.POOL_SCRIPT_HASH, - { - count, - order, - page, - } + { count, order, page } ); return utxos .filter((utxo) => @@ -96,36 +230,16 @@ export class BlockfrostAdapter { utxo.data_hash, `expect pool to have datum hash, got ${utxo.data_hash}` ); + const txIn: TxIn = { txHash: utxo.tx_hash, index: utxo.output_index }; return new PoolV1.State( utxo.address, - { txHash: utxo.tx_hash, index: utxo.output_index }, + txIn, utxo.amount, utxo.data_hash ); }); } - /** - * Get a specific pool by its ID. - * @param {Object} params - The parameters. - * @param {string} params.pool - The pool ID. This is the asset name of a pool's NFT and LP tokens. It can also be acquired by calling pool.id. - * @returns {PoolV1.State | null} - Returns the pool or null if not found. - */ - public async getV1PoolById({ - id, - }: GetPoolByIdParams): Promise { - const nft = `${DexV1Constant.POOL_NFT_POLICY_ID}${id}`; - const nftTxs = await this.api.assetsTransactions(nft, { - count: 1, - page: 1, - order: "desc", - }); - if (nftTxs.length === 0) { - return null; - } - return this.getV1PoolInTx({ txHash: nftTxs[0].tx_hash }); - } - public async getV1PoolHistory({ id, page = 1, @@ -133,11 +247,12 @@ export class BlockfrostAdapter { order = "desc", }: GetPoolHistoryParams): Promise { const nft = `${DexV1Constant.POOL_NFT_POLICY_ID}${id}`; - const nftTxs = await this.api.assetsTransactions(nft, { + const nftTxs = await this.blockFrostApi.assetsTransactions(nft, { count, page, order, }); + console.log(JSONBig.stringify(nftTxs)); return nftTxs.map( (tx): TxHistory => ({ txHash: tx.tx_hash, @@ -148,59 +263,6 @@ export class BlockfrostAdapter { ); } - /** - * Get pool state in a transaction. - * @param {Object} params - The parameters. - * @param {string} params.txHash - The transaction hash containing pool output. One of the way to acquire is by calling getPoolHistory. - * @returns {PoolV1.State} - Returns the pool state or null if the transaction doesn't contain pool. - */ - public async getV1PoolInTx({ - txHash, - }: GetPoolInTxParams): Promise { - const poolTx = await this.api.txsUtxos(txHash); - const poolUtxo = poolTx.outputs.find( - (o) => - getScriptHashFromAddress(o.address) === DexV1Constant.POOL_SCRIPT_HASH - ); - if (!poolUtxo) { - return null; - } - checkValidPoolOutput(poolUtxo.address, poolUtxo.amount, poolUtxo.data_hash); - invariant( - poolUtxo.data_hash, - `expect pool to have datum hash, got ${poolUtxo.data_hash}` - ); - return new PoolV1.State( - poolUtxo.address, - { txHash: txHash, index: poolUtxo.output_index }, - poolUtxo.amount, - poolUtxo.data_hash - ); - } - - public async getAssetDecimals(asset: string): Promise { - if (asset === "lovelace") { - return 6; - } - try { - const assetAInfo = await this.api.assetsById(asset); - return assetAInfo.metadata?.decimals ?? 0; - } catch (err) { - if (err instanceof BlockfrostServerError && err.status_code === 404) { - return 0; - } - throw err; - } - } - - /** - * Get pool price. - * @param {Object} params - The parameters to calculate pool price. - * @param {string} params.pool - The pool we want to get price. - * @param {string} [params.decimalsA] - The decimals of assetA in pool, if undefined then query from Blockfrost. - * @param {string} [params.decimalsB] - The decimals of assetB in pool, if undefined then query from Blockfrost. - * @returns {[string, string]} - Returns a pair of asset A/B price and B/A price, adjusted to decimals. - */ public async getV1PoolPrice({ pool, decimalsA, @@ -223,17 +285,12 @@ export class BlockfrostAdapter { return [priceAB, priceBA]; } - public async getDatumByDatumHash(datumHash: string): Promise { - const scriptsDatum = await this.api.scriptsDatumCbor(datumHash); - return scriptsDatum.cbor; - } - public async getAllV2Pools(): Promise<{ pools: PoolV2.State[]; errors: unknown[]; }> { const v2Config = DexV2Constant.CONFIG[this.networkId]; - const utxos = await this.api.addressesUtxosAssetAll( + const utxos = await this.blockFrostApi.addressesUtxosAssetAll( v2Config.poolScriptHashBech32, v2Config.poolAuthenAsset ); @@ -272,14 +329,10 @@ export class BlockfrostAdapter { errors: unknown[]; }> { const v2Config = DexV2Constant.CONFIG[this.networkId]; - const utxos = await this.api.addressesUtxosAsset( + const utxos = await this.blockFrostApi.addressesUtxosAsset( v2Config.poolScriptHashBech32, v2Config.poolAuthenAsset, - { - count, - order, - page, - } + { count, order, page } ); const pools: PoolV2.State[] = []; @@ -332,14 +385,6 @@ export class BlockfrostAdapter { ); } - /** - * Get pool price. - * @param {Object} params - The parameters to calculate pool price. - * @param {string} params.pool - The pool we want to get price. - * @param {string} [params.decimalsA] - The decimals of assetA in pool, if undefined then query from Blockfrost. - * @param {string} [params.decimalsB] - The decimals of assetB in pool, if undefined then query from Blockfrost. - * @returns {[string, string]} - Returns a pair of asset A/B price and B/A price, adjusted to decimals. - */ public async getV2PoolPrice({ pool, decimalsA, @@ -362,8 +407,63 @@ export class BlockfrostAdapter { return [priceAB, priceBA]; } + public async getAllFactoriesV2(): Promise<{ + factories: FactoryV2.State[]; + errors: unknown[]; + }> { + const v2Config = DexV2Constant.CONFIG[this.networkId]; + const utxos = await this.blockFrostApi.addressesUtxosAssetAll( + v2Config.factoryScriptHashBech32, + v2Config.factoryAsset + ); + + const factories: FactoryV2.State[] = []; + const errors: unknown[] = []; + for (const utxo of utxos) { + try { + if (!utxo.inline_datum) { + throw new Error( + `Cannot find datum of Factory V2, tx: ${utxo.tx_hash}` + ); + } + const factory = new FactoryV2.State( + this.networkId, + utxo.address, + { txHash: utxo.tx_hash, index: utxo.output_index }, + utxo.amount, + utxo.inline_datum + ); + factories.push(factory); + } catch (err) { + errors.push(err); + } + } + return { + factories: factories, + errors: errors, + }; + } + + public async getFactoryV2ByPair( + assetA: Asset, + assetB: Asset + ): Promise { + const factoryIdent = PoolV2.computeLPAssetName(assetA, assetB); + const { factories: allFactories } = await this.getAllFactoriesV2(); + for (const factory of allFactories) { + if ( + StringUtils.compare(factory.head, factoryIdent) < 0 && + StringUtils.compare(factoryIdent, factory.tail) < 0 + ) { + return factory; + } + } + + return null; + } + private async parseStablePoolState( - utxo: Awaited>[0] + utxo: Responses["address_utxo_content"][0] ): Promise { let datum: string; if (utxo.inline_datum) { @@ -393,7 +493,7 @@ export class BlockfrostAdapter { const pools: StablePool.State[] = []; const errors: unknown[] = []; for (const poolAddr of poolAddresses) { - const utxos = await this.api.addressesUtxosAll(poolAddr); + const utxos = await this.blockFrostApi.addressesUtxosAll(poolAddr); try { for (const utxo of utxos) { const pool = await this.parseStablePoolState(utxo); @@ -422,7 +522,7 @@ export class BlockfrostAdapter { lpAsset )}` ); - const poolUtxos = await this.api.addressesUtxosAssetAll( + const poolUtxos = await this.blockFrostApi.addressesUtxosAssetAll( config.poolAddress, config.nftAsset ); @@ -444,7 +544,7 @@ export class BlockfrostAdapter { `Cannot find Stable Pool having NFT ${Asset.toString(nft)}` ); } - const poolUtxos = await this.api.addressesUtxosAssetAll( + const poolUtxos = await this.blockFrostApi.addressesUtxosAssetAll( poolAddress, Asset.toString(nft) ); @@ -454,59 +554,255 @@ export class BlockfrostAdapter { } return null; } +} - public async getAllFactoriesV2(): Promise<{ - factories: FactoryV2.State[]; - errors: unknown[]; - }> { - const v2Config = DexV2Constant.CONFIG[this.networkId]; - const utxos = await this.api.addressesUtxosAssetAll( - v2Config.factoryScriptHashBech32, - v2Config.factoryAsset +export type MinswapAdapterConstructor = { + networkId: NetworkId; + networkEnv: NetworkEnvironment; + blockFrostApi: BlockFrostAPI; + repository: PostgresRepositoryReader; +}; + +export class MinswapAdapter extends BlockfrostAdapter { + private readonly networkEnv: NetworkEnvironment; + private readonly repository: PostgresRepositoryReader; + + constructor({ + networkId, + networkEnv, + blockFrostApi, + repository, + }: MinswapAdapterConstructor) { + super(networkId, blockFrostApi); + this.networkEnv = networkEnv; + this.repository = repository; + } + + private prismaPoolV1ToPoolV1State(prismaPool: Prisma.PoolV1): PoolV1.State { + const address = prismaPool.pool_address; + const txIn: TxIn = { + txHash: prismaPool.created_tx_id, + index: prismaPool.created_tx_index, + }; + const value: Value = JSONBig({ + alwaysParseAsBig: true, + useNativeBigInt: true, + }).parse(prismaPool.value); + const datumHash = C.hash_plutus_data( + C.PlutusData.from_bytes(fromHex(prismaPool.raw_datum)) + ).to_hex(); + return new PoolV1.State(address, txIn, value, datumHash); + } + + override async getV1PoolInTx({ + txHash, + }: GetPoolInTxParams): Promise { + const prismaPool = await this.repository.getPoolV1ByCreatedTxId(txHash); + if (!prismaPool) { + return null; + } + return this.prismaPoolV1ToPoolV1State(prismaPool); + } + + override async getV1PoolById({ + id, + }: GetPoolByIdParams): Promise { + const lpAsset = `${DexV1Constant.LP_POLICY_ID}${id}`; + const prismaPool = await this.repository.getPoolV1ByLpAsset(lpAsset); + if (!prismaPool) { + return null; + } + return this.prismaPoolV1ToPoolV1State(prismaPool); + } + + override async getV1Pools({ + page, + count = 100, + order = "asc", + }: GetPoolsParams): Promise { + const prismaPools = await this.repository.getLastPoolV1State( + page - 1, + count, + order ); + if (prismaPools.length === 0) { + return []; + } + return prismaPools.map(this.prismaPoolV1ToPoolV1State); + } - const factories: FactoryV2.State[] = []; - const errors: unknown[] = []; - for (const utxo of utxos) { - try { - if (!utxo.inline_datum) { - throw new Error( - `Cannot find datum of Factory V2, tx: ${utxo.tx_hash}` - ); - } - const factory = new FactoryV2.State( - this.networkId, - utxo.address, - { txHash: utxo.tx_hash, index: utxo.output_index }, - utxo.amount, - utxo.inline_datum - ); - factories.push(factory); - } catch (err) { - errors.push(err); - } + override async getV1PoolHistory({ + id, + page = 1, + count = 100, + order = "desc", + }: GetPoolHistoryParams): Promise { + const lpAsset = `${DexV1Constant.LP_POLICY_ID}${id}`; + const prismaPools = await this.repository.getHistoricalPoolV1ByLpAsset( + lpAsset, + page - 1, + count, + order + ); + if (prismaPools.length === 0) { + return []; } + + const network = networkEnvToLucidNetwork(this.networkEnv); + return prismaPools.map( + (prismaPool): TxHistory => ({ + txHash: prismaPool.created_tx_id, + txIndex: prismaPool.created_tx_index, + blockHeight: Number(prismaPool.block_id), + time: new Date( + slotToBeginUnixTime( + Number(prismaPool.slot), + SLOT_CONFIG_NETWORK[network] + ) + ), + }) + ); + } + + private prismaPoolV2ToPoolV2State(prismaPool: Prisma.PoolV2): PoolV2.State { + const txIn: TxIn = { + txHash: prismaPool.created_tx_id, + index: prismaPool.created_tx_index, + }; + const value: Value = JSONBig({ + alwaysParseAsBig: true, + useNativeBigInt: true, + }).parse(prismaPool.value); + return new PoolV2.State( + this.networkId, + prismaPool.pool_address, + txIn, + value, + prismaPool.raw_datum + ); + } + + override async getAllV2Pools(): Promise<{ + pools: PoolV2.State[]; + errors: unknown[]; + }> { + const prismaPools = await this.repository.getAllLastPoolV2State(); return { - factories: factories, - errors: errors, + pools: prismaPools.map((pool) => this.prismaPoolV2ToPoolV2State(pool)), + errors: [], }; } - public async getFactoryV2ByPair( + override async getV2Pools({ + page, + count = 100, + order = "asc", + }: GetPoolsParams): Promise<{ + pools: PoolV2.State[]; + errors: unknown[]; + }> { + const prismaPools = await this.repository.getLastPoolV2State( + page - 1, + count, + order + ); + return { + pools: prismaPools.map((pool) => this.prismaPoolV2ToPoolV2State(pool)), + errors: [], + }; + } + + override async getV2PoolByPair( assetA: Asset, assetB: Asset - ): Promise { - const factoryIdent = PoolV2.computeLPAssetName(assetA, assetB); - const { factories: allFactories } = await this.getAllFactoriesV2(); - for (const factory of allFactories) { - if ( - StringUtils.compare(factory.head, factoryIdent) < 0 && - StringUtils.compare(factoryIdent, factory.tail) < 0 - ) { - return factory; - } + ): Promise { + const prismaPool = await this.repository.getPoolV2ByPair(assetA, assetB); + if (!prismaPool) { + return null; } + return this.prismaPoolV2ToPoolV2State(prismaPool); + } - return null; + override async getV2PoolByLp(lpAsset: Asset): Promise { + const prismaPool = await this.repository.getPoolV2ByLpAsset(lpAsset); + if (!prismaPool) { + return null; + } + return this.prismaPoolV2ToPoolV2State(prismaPool); + } + + private prismaStablePoolToStablePoolState( + prismaPool: Prisma.StablePool + ): StablePool.State { + const txIn: TxIn = { + txHash: prismaPool.created_tx_id, + index: prismaPool.created_tx_index, + }; + const value: Value = JSONBig({ + alwaysParseAsBig: true, + useNativeBigInt: true, + }).parse(prismaPool.value); + return new StablePool.State( + this.networkId, + prismaPool.pool_address, + txIn, + value, + prismaPool.raw_datum + ); + } + + override async getAllStablePools(): Promise<{ + pools: StablePool.State[]; + errors: unknown[]; + }> { + const prismaPools = await this.repository.getAllLastStablePoolState(); + return { + pools: prismaPools.map((pool) => + this.prismaStablePoolToStablePoolState(pool) + ), + errors: [], + }; + } + + override async getStablePoolByNFT( + nft: Asset + ): Promise { + const config = StableswapConstant.CONFIG[this.networkId].find( + (cfg) => cfg.nftAsset === Asset.toString(nft) + ); + if (!config) { + throw new Error( + `Cannot find Stable Pool having NFT ${Asset.toString(nft)}` + ); + } + + const prismaStablePool = await this.repository.getStablePoolByLpAsset( + config.lpAsset + ); + if (!prismaStablePool) { + return null; + } + return this.prismaStablePoolToStablePoolState(prismaStablePool); + } + + override async getStablePoolByLpAsset( + lpAsset: Asset + ): Promise { + const config = StableswapConstant.CONFIG[this.networkId].find( + (cfg) => cfg.lpAsset === Asset.toString(lpAsset) + ); + if (!config) { + throw new Error( + `Cannot find Stable Pool having NFT ${Asset.toString(lpAsset)}` + ); + } + + const prismaStablePool = await this.repository.getStablePoolByLpAsset( + config.lpAsset + ); + if (!prismaStablePool) { + return null; + } + return this.prismaStablePoolToStablePoolState(prismaStablePool); } } diff --git a/src/hehe.ts b/src/hehe.ts deleted file mode 100644 index b35e1a3..0000000 --- a/src/hehe.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { BlockFrostAPI } from "@blockfrost/blockfrost-js"; -import JSONBig from "json-bigint"; - -import { BlockfrostAdapter, MinswapAdapter } from "./new-adapter"; -import { newPrismaClient } from "./syncer/connector"; -import { PostgresRepositoryReader } from "./syncer/repository/postgres-reposiotry"; -import { NetworkEnvironment, NetworkId } from "./types/network"; - -const blockFrostApi = new BlockFrostAPI({ - projectId: "preprodel6eWcyCZddTV1wezpV1uNlt0GpUVAcw", - network: "preprod", -}); -const blockFrostAdapter = new BlockfrostAdapter( - NetworkId.TESTNET, - blockFrostApi -); - -const prismaClient = await newPrismaClient( - "postgresql://postgres:minswap@localhost:5432/syncer?schema=public&connection_limit=5" -); -const repository = new PostgresRepositoryReader( - NetworkEnvironment.TESTNET_PREPROD, - prismaClient -); -const minswapAdapter = new MinswapAdapter({ - networkId: NetworkId.TESTNET, - networkEnv: NetworkEnvironment.TESTNET_PREPROD, - blockFrostApi: blockFrostApi, - repository: repository, -}); - -const a = await blockFrostAdapter.getAllStablePools(); -console.log("blockFrostAdapter", JSONBig.stringify(a.pools)); -const b = await minswapAdapter.getAllStablePools(); -console.log("minswapAdapter", JSONBig.stringify(b.pools)); diff --git a/src/new-adapter.ts b/src/new-adapter.ts deleted file mode 100644 index ea717a9..0000000 --- a/src/new-adapter.ts +++ /dev/null @@ -1,771 +0,0 @@ -import { - BlockFrostAPI, - BlockfrostServerError, -} from "@blockfrost/blockfrost-js"; -import { PaginationOptions } from "@blockfrost/blockfrost-js/lib/types"; -import invariant from "@minswap/tiny-invariant"; -import * as Prisma from "@prisma/client"; -import Big from "big.js"; -import JSONBig from "json-bigint"; -import { - C, - fromHex, - SLOT_CONFIG_NETWORK, - slotToBeginUnixTime, -} from "lucid-cardano"; - -import { PostgresRepositoryReader } from "./syncer/repository/postgres-reposiotry"; -import { Asset } from "./types/asset"; -import { - DexV1Constant, - DexV2Constant, - StableswapConstant, -} from "./types/constants"; -import { FactoryV2 } from "./types/factory"; -import { NetworkEnvironment, NetworkId } from "./types/network"; -import { PoolV1, PoolV2, StablePool } from "./types/pool"; -import { - checkValidPoolOutput, - isValidPoolOutput, - normalizeAssets, -} from "./types/pool.internal"; -import { StringUtils } from "./types/string"; -import { TxHistory, TxIn, Value } from "./types/tx.internal"; -import { getScriptHashFromAddress } from "./utils/address-utils.internal"; -import { networkEnvToLucidNetwork } from "./utils/network.internal"; - -export type GetPoolInTxParams = { - txHash: string; -}; - -export type GetPoolByIdParams = { - id: string; -}; - -export type GetPoolsParams = Omit & { - page: number; -}; - -export type GetPoolHistoryParams = PaginationOptions & { - id: string; -}; - -export type GetPoolPriceParams = { - pool: PoolV1.State; - decimalsA?: number; - decimalsB?: number; -}; - -export type GetV2PoolPriceParams = { - pool: PoolV2.State; - decimalsA?: number; - decimalsB?: number; -}; - -interface Adapter { - getAssetDecimals(asset: string): Promise; - - getDatumByDatumHash(datumHash: string): Promise; - - /** - * Get pool state in a transaction. - * @param {Object} params - The parameters. - * @param {string} params.txHash - The transaction hash containing pool output. One of the way to acquire is by calling getPoolHistory. - * @returns {PoolV1.State} - Returns the pool state or null if the transaction doesn't contain pool. - */ - getV1PoolInTx({ txHash }: GetPoolInTxParams): Promise; - - /** - * Get a specific pool by its ID. - * @param {Object} params - The parameters. - * @param {string} params.pool - The pool ID. This is the asset name of a pool's NFT and LP tokens. It can also be acquired by calling pool.id. - * @returns {PoolV1.State | null} - Returns the pool or null if not found. - */ - getV1PoolById({ id }: GetPoolByIdParams): Promise; - - /** - * @returns The latest pools or empty array if current page is after last page - */ - getV1Pools(params: GetPoolsParams): Promise; - - getV1PoolHistory(params: GetPoolHistoryParams): Promise; - - /** - * Get pool price. - * @param {Object} params - The parameters to calculate pool price. - * @param {string} params.pool - The pool we want to get price. - * @param {string} [params.decimalsA] - The decimals of assetA in pool, if undefined then query from Blockfrost. - * @param {string} [params.decimalsB] - The decimals of assetB in pool, if undefined then query from Blockfrost. - * @returns {[string, string]} - Returns a pair of asset A/B price and B/A price, adjusted to decimals. - */ - getV1PoolPrice(params: GetPoolPriceParams): Promise<[Big, Big]>; - - getAllV2Pools(): Promise<{ pools: PoolV2.State[]; errors: unknown[] }>; - - getV2Pools( - params: GetPoolsParams - ): Promise<{ pools: PoolV2.State[]; errors: unknown[] }>; - - getV2PoolByPair(assetA: Asset, assetB: Asset): Promise; - - getV2PoolByLp(lpAsset: Asset): Promise; - - /** - * Get pool price. - * @param {Object} params - The parameters to calculate pool price. - * @param {string} params.pool - The pool we want to get price. - * @param {string} [params.decimalsA] - The decimals of assetA in pool, if undefined then query from Blockfrost. - * @param {string} [params.decimalsB] - The decimals of assetB in pool, if undefined then query from Blockfrost. - * @returns {[string, string]} - Returns a pair of asset A/B price and B/A price, adjusted to decimals. - */ - getV2PoolPrice(params: GetV2PoolPriceParams): Promise<[Big, Big]>; - - getAllFactoriesV2(): Promise<{ - factories: FactoryV2.State[]; - errors: unknown[]; - }>; - - getFactoryV2ByPair( - assetA: Asset, - assetB: Asset - ): Promise; - - getAllStablePools(): Promise<{ - pools: StablePool.State[]; - errors: unknown[]; - }>; - - getStablePoolByNFT(nft: Asset): Promise; -} - -export class BlockfrostAdapter implements Adapter { - protected readonly networkId: NetworkId; - private readonly blockFrostApi: BlockFrostAPI; - - constructor(networkId: NetworkId, blockFrostApi: BlockFrostAPI) { - this.networkId = networkId; - this.blockFrostApi = blockFrostApi; - } - - public async getAssetDecimals(asset: string): Promise { - if (asset === "lovelace") { - return 6; - } - try { - const assetAInfo = await this.blockFrostApi.assetsById(asset); - return assetAInfo.metadata?.decimals ?? 0; - } catch (err) { - if (err instanceof BlockfrostServerError && err.status_code === 404) { - return 0; - } - throw err; - } - } - - public async getDatumByDatumHash(datumHash: string): Promise { - const scriptsDatum = await this.blockFrostApi.scriptsDatumCbor(datumHash); - return scriptsDatum.cbor; - } - - public async getV1PoolInTx({ - txHash, - }: GetPoolInTxParams): Promise { - const poolTx = await this.blockFrostApi.txsUtxos(txHash); - const poolUtxo = poolTx.outputs.find( - (o) => - getScriptHashFromAddress(o.address) === DexV1Constant.POOL_SCRIPT_HASH - ); - if (!poolUtxo) { - return null; - } - - checkValidPoolOutput(poolUtxo.address, poolUtxo.amount, poolUtxo.data_hash); - invariant( - poolUtxo.data_hash, - `expect pool to have datum hash, got ${poolUtxo.data_hash}` - ); - - const txIn: TxIn = { txHash: txHash, index: poolUtxo.output_index }; - return new PoolV1.State( - poolUtxo.address, - txIn, - poolUtxo.amount, - poolUtxo.data_hash - ); - } - - public async getV1PoolById({ - id, - }: GetPoolByIdParams): Promise { - const nft = `${DexV1Constant.POOL_NFT_POLICY_ID}${id}`; - const nftTxs = await this.blockFrostApi.assetsTransactions(nft, { - count: 1, - page: 1, - order: "desc", - }); - if (nftTxs.length === 0) { - return null; - } - return this.getV1PoolInTx({ txHash: nftTxs[0].tx_hash }); - } - - public async getV1Pools({ - page, - count = 100, - order = "asc", - }: GetPoolsParams): Promise { - const utxos = await this.blockFrostApi.addressesUtxos( - DexV1Constant.POOL_SCRIPT_HASH, - { count, order, page } - ); - return utxos - .filter((utxo) => - isValidPoolOutput(utxo.address, utxo.amount, utxo.data_hash) - ) - .map((utxo) => { - invariant( - utxo.data_hash, - `expect pool to have datum hash, got ${utxo.data_hash}` - ); - return new PoolV1.State( - utxo.address, - { txHash: utxo.tx_hash, index: utxo.output_index }, - utxo.amount, - utxo.data_hash - ); - }); - } - - public async getV1PoolHistory({ - id, - page = 1, - count = 100, - order = "desc", - }: GetPoolHistoryParams): Promise { - const nft = `${DexV1Constant.POOL_NFT_POLICY_ID}${id}`; - const nftTxs = await this.blockFrostApi.assetsTransactions(nft, { - count, - page, - order, - }); - console.log(JSONBig.stringify(nftTxs)); - return nftTxs.map( - (tx): TxHistory => ({ - txHash: tx.tx_hash, - txIndex: tx.tx_index, - blockHeight: tx.block_height, - time: new Date(Number(tx.block_time) * 1000), - }) - ); - } - - public async getV1PoolPrice({ - pool, - decimalsA, - decimalsB, - }: GetPoolPriceParams): Promise<[Big, Big]> { - if (decimalsA === undefined) { - decimalsA = await this.getAssetDecimals(pool.assetA); - } - if (decimalsB === undefined) { - decimalsB = await this.getAssetDecimals(pool.assetB); - } - const adjustedReserveA = Big(pool.reserveA.toString()).div( - Big(10).pow(decimalsA) - ); - const adjustedReserveB = Big(pool.reserveB.toString()).div( - Big(10).pow(decimalsB) - ); - const priceAB = adjustedReserveA.div(adjustedReserveB); - const priceBA = adjustedReserveB.div(adjustedReserveA); - return [priceAB, priceBA]; - } - - public async getAllV2Pools(): Promise<{ - pools: PoolV2.State[]; - errors: unknown[]; - }> { - const v2Config = DexV2Constant.CONFIG[this.networkId]; - const utxos = await this.blockFrostApi.addressesUtxosAssetAll( - v2Config.poolScriptHashBech32, - v2Config.poolAuthenAsset - ); - - const pools: PoolV2.State[] = []; - const errors: unknown[] = []; - for (const utxo of utxos) { - try { - if (!utxo.inline_datum) { - throw new Error(`Cannot find datum of Pool V2, tx: ${utxo.tx_hash}`); - } - const pool = new PoolV2.State( - this.networkId, - utxo.address, - { txHash: utxo.tx_hash, index: utxo.output_index }, - utxo.amount, - utxo.inline_datum - ); - pools.push(pool); - } catch (err) { - errors.push(err); - } - } - return { - pools: pools, - errors: errors, - }; - } - - public async getV2Pools({ - page, - count = 100, - order = "asc", - }: GetPoolsParams): Promise<{ - pools: PoolV2.State[]; - errors: unknown[]; - }> { - const v2Config = DexV2Constant.CONFIG[this.networkId]; - const utxos = await this.blockFrostApi.addressesUtxosAsset( - v2Config.poolScriptHashBech32, - v2Config.poolAuthenAsset, - { - count, - order, - page, - } - ); - - const pools: PoolV2.State[] = []; - const errors: unknown[] = []; - for (const utxo of utxos) { - try { - if (!utxo.inline_datum) { - throw new Error(`Cannot find datum of Pool V2, tx: ${utxo.tx_hash}`); - } - const pool = new PoolV2.State( - this.networkId, - utxo.address, - { txHash: utxo.tx_hash, index: utxo.output_index }, - utxo.amount, - utxo.inline_datum - ); - pools.push(pool); - } catch (err) { - errors.push(err); - } - } - return { - pools: pools, - errors: errors, - }; - } - - public async getV2PoolByPair( - assetA: Asset, - assetB: Asset - ): Promise { - const [normalizedAssetA, normalizedAssetB] = normalizeAssets( - Asset.toString(assetA), - Asset.toString(assetB) - ); - const { pools: allPools } = await this.getAllV2Pools(); - return ( - allPools.find( - (pool) => - pool.assetA === normalizedAssetA && pool.assetB === normalizedAssetB - ) ?? null - ); - } - - public async getV2PoolByLp(lpAsset: Asset): Promise { - const { pools: allPools } = await this.getAllV2Pools(); - return ( - allPools.find((pool) => Asset.compare(pool.lpAsset, lpAsset) === 0) ?? - null - ); - } - - public async getV2PoolPrice({ - pool, - decimalsA, - decimalsB, - }: GetV2PoolPriceParams): Promise<[Big, Big]> { - if (decimalsA === undefined) { - decimalsA = await this.getAssetDecimals(pool.assetA); - } - if (decimalsB === undefined) { - decimalsB = await this.getAssetDecimals(pool.assetB); - } - const adjustedReserveA = Big(pool.reserveA.toString()).div( - Big(10).pow(decimalsA) - ); - const adjustedReserveB = Big(pool.reserveB.toString()).div( - Big(10).pow(decimalsB) - ); - const priceAB = adjustedReserveA.div(adjustedReserveB); - const priceBA = adjustedReserveB.div(adjustedReserveA); - return [priceAB, priceBA]; - } - - public async getAllFactoriesV2(): Promise<{ - factories: FactoryV2.State[]; - errors: unknown[]; - }> { - const v2Config = DexV2Constant.CONFIG[this.networkId]; - const utxos = await this.blockFrostApi.addressesUtxosAssetAll( - v2Config.factoryScriptHashBech32, - v2Config.factoryAsset - ); - - const factories: FactoryV2.State[] = []; - const errors: unknown[] = []; - for (const utxo of utxos) { - try { - if (!utxo.inline_datum) { - throw new Error( - `Cannot find datum of Factory V2, tx: ${utxo.tx_hash}` - ); - } - const factory = new FactoryV2.State( - this.networkId, - utxo.address, - { txHash: utxo.tx_hash, index: utxo.output_index }, - utxo.amount, - utxo.inline_datum - ); - factories.push(factory); - } catch (err) { - errors.push(err); - } - } - return { - factories: factories, - errors: errors, - }; - } - - public async getFactoryV2ByPair( - assetA: Asset, - assetB: Asset - ): Promise { - const factoryIdent = PoolV2.computeLPAssetName(assetA, assetB); - const { factories: allFactories } = await this.getAllFactoriesV2(); - for (const factory of allFactories) { - if ( - StringUtils.compare(factory.head, factoryIdent) < 0 && - StringUtils.compare(factoryIdent, factory.tail) < 0 - ) { - return factory; - } - } - - return null; - } - - public async getAllStablePools(): Promise<{ - pools: StablePool.State[]; - errors: unknown[]; - }> { - const poolAddresses = StableswapConstant.CONFIG[this.networkId].map( - (cfg) => cfg.poolAddress - ); - const pools: StablePool.State[] = []; - const errors: unknown[] = []; - for (const poolAddr of poolAddresses) { - const utxos = await this.blockFrostApi.addressesUtxosAll(poolAddr); - try { - for (const utxo of utxos) { - let datum: string; - if (utxo.inline_datum) { - datum = utxo.inline_datum; - } else if (utxo.data_hash) { - datum = await this.getDatumByDatumHash(utxo.data_hash); - } else { - throw new Error("Cannot find datum of Stable Pool"); - } - const pool = new StablePool.State( - this.networkId, - utxo.address, - { txHash: utxo.tx_hash, index: utxo.output_index }, - utxo.amount, - datum - ); - pools.push(pool); - } - } catch (err) { - errors.push(err); - } - } - - return { - pools: pools, - errors: errors, - }; - } - - public async getStablePoolByNFT( - nft: Asset - ): Promise { - const poolAddress = StableswapConstant.CONFIG[this.networkId].find( - (cfg) => cfg.nftAsset === Asset.toString(nft) - )?.poolAddress; - if (!poolAddress) { - throw new Error( - `Cannot find Stable Pool having NFT ${Asset.toString(nft)}` - ); - } - const utxos = await this.blockFrostApi.addressesUtxosAssetAll( - poolAddress, - Asset.toString(nft) - ); - for (const utxo of utxos) { - let datum: string; - if (utxo.inline_datum) { - datum = utxo.inline_datum; - } else if (utxo.data_hash) { - datum = await this.getDatumByDatumHash(utxo.data_hash); - } else { - throw new Error("Cannot find datum of Stable Pool"); - } - const pool = new StablePool.State( - this.networkId, - utxo.address, - { txHash: utxo.tx_hash, index: utxo.output_index }, - utxo.amount, - datum - ); - return pool; - } - - return null; - } -} - -export type MinswapAdapterConstructor = { - networkId: NetworkId; - networkEnv: NetworkEnvironment; - blockFrostApi: BlockFrostAPI; - repository: PostgresRepositoryReader; -}; - -export class MinswapAdapter extends BlockfrostAdapter { - private readonly networkEnv: NetworkEnvironment; - private readonly repository: PostgresRepositoryReader; - - constructor({ - networkId, - networkEnv, - blockFrostApi, - repository, - }: MinswapAdapterConstructor) { - super(networkId, blockFrostApi); - this.networkEnv = networkEnv; - this.repository = repository; - } - - private prismaPoolV1ToPoolV1State(prismaPool: Prisma.PoolV1): PoolV1.State { - const address = prismaPool.pool_address; - const txIn: TxIn = { - txHash: prismaPool.created_tx_id, - index: prismaPool.created_tx_index, - }; - const value: Value = JSONBig({ - alwaysParseAsBig: true, - useNativeBigInt: true, - }).parse(prismaPool.value); - const datumHash = C.hash_plutus_data( - C.PlutusData.from_bytes(fromHex(prismaPool.raw_datum)) - ).to_hex(); - return new PoolV1.State(address, txIn, value, datumHash); - } - - override async getV1PoolInTx({ - txHash, - }: GetPoolInTxParams): Promise { - const prismaPool = await this.repository.getPoolV1ByCreatedTxId(txHash); - if (!prismaPool) { - return null; - } - return this.prismaPoolV1ToPoolV1State(prismaPool); - } - - override async getV1PoolById({ - id, - }: GetPoolByIdParams): Promise { - const lpAsset = `${DexV1Constant.LP_POLICY_ID}${id}`; - const prismaPool = await this.repository.getPoolV1ByLpAsset(lpAsset); - if (!prismaPool) { - return null; - } - return this.prismaPoolV1ToPoolV1State(prismaPool); - } - - override async getV1Pools({ - page, - count = 100, - order = "asc", - }: GetPoolsParams): Promise { - const prismaPools = await this.repository.getLastPoolV1State( - page - 1, - count, - order - ); - if (prismaPools.length === 0) { - return []; - } - return prismaPools.map(this.prismaPoolV1ToPoolV1State); - } - - override async getV1PoolHistory({ - id, - page = 1, - count = 100, - order = "desc", - }: GetPoolHistoryParams): Promise { - const lpAsset = `${DexV1Constant.LP_POLICY_ID}${id}`; - const prismaPools = await this.repository.getHistoricalPoolV1ByLpAsset( - lpAsset, - page - 1, - count, - order - ); - if (prismaPools.length === 0) { - return []; - } - - const network = networkEnvToLucidNetwork(this.networkEnv); - return prismaPools.map( - (prismaPool): TxHistory => ({ - txHash: prismaPool.created_tx_id, - txIndex: prismaPool.created_tx_index, - blockHeight: Number(prismaPool.block_id), - time: new Date( - slotToBeginUnixTime( - Number(prismaPool.slot), - SLOT_CONFIG_NETWORK[network] - ) - ), - }) - ); - } - - private prismaPoolV2ToPoolV2State(prismaPool: Prisma.PoolV2): PoolV2.State { - const txIn: TxIn = { - txHash: prismaPool.created_tx_id, - index: prismaPool.created_tx_index, - }; - const value: Value = JSONBig({ - alwaysParseAsBig: true, - useNativeBigInt: true, - }).parse(prismaPool.value); - return new PoolV2.State( - this.networkId, - prismaPool.pool_address, - txIn, - value, - prismaPool.raw_datum - ); - } - - override async getAllV2Pools(): Promise<{ - pools: PoolV2.State[]; - errors: unknown[]; - }> { - const prismaPools = await this.repository.getAllLastPoolV2State(); - return { - pools: prismaPools.map((pool) => this.prismaPoolV2ToPoolV2State(pool)), - errors: [], - }; - } - - override async getV2Pools({ - page, - count = 100, - order = "asc", - }: GetPoolsParams): Promise<{ - pools: PoolV2.State[]; - errors: unknown[]; - }> { - const prismaPools = await this.repository.getLastPoolV2State( - page - 1, - count, - order - ); - return { - pools: prismaPools.map((pool) => this.prismaPoolV2ToPoolV2State(pool)), - errors: [], - }; - } - - override async getV2PoolByPair( - assetA: Asset, - assetB: Asset - ): Promise { - const prismaPool = await this.repository.getPoolV2ByPair(assetA, assetB); - if (!prismaPool) { - return null; - } - return this.prismaPoolV2ToPoolV2State(prismaPool); - } - - override async getV2PoolByLp(lpAsset: Asset): Promise { - const prismaPool = await this.repository.getPoolV2ByLpAsset(lpAsset); - if (!prismaPool) { - return null; - } - return this.prismaPoolV2ToPoolV2State(prismaPool); - } - - private prismaStablePoolToStablePoolState( - prismaPool: Prisma.StablePool - ): StablePool.State { - const txIn: TxIn = { - txHash: prismaPool.created_tx_id, - index: prismaPool.created_tx_index, - }; - const value: Value = JSONBig({ - alwaysParseAsBig: true, - useNativeBigInt: true, - }).parse(prismaPool.value); - return new StablePool.State( - this.networkId, - prismaPool.pool_address, - txIn, - value, - prismaPool.raw_datum - ); - } - - override async getAllStablePools(): Promise<{ - pools: StablePool.State[]; - errors: unknown[]; - }> { - const prismaPools = await this.repository.getAllLastStablePoolState(); - return { - pools: prismaPools.map((pool) => - this.prismaStablePoolToStablePoolState(pool) - ), - errors: [], - }; - } - - override async getStablePoolByNFT( - nft: Asset - ): Promise { - const config = StableswapConstant.CONFIG[this.networkId].find( - (cfg) => cfg.nftAsset === Asset.toString(nft) - ); - if (!config) { - throw new Error( - `Cannot find Stable Pool having NFT ${Asset.toString(nft)}` - ); - } - const prismaStablePool = await this.repository.getStablePoolByLpAsset( - config.lpAsset - ); - if (!prismaStablePool) { - return null; - } - return this.prismaStablePoolToStablePoolState(prismaStablePool); - } -} diff --git a/test/adapter.test.ts b/test/adapter.test.ts index 87f2b95..4e4f734 100644 --- a/test/adapter.test.ts +++ b/test/adapter.test.ts @@ -26,21 +26,21 @@ const MIN_ADA_POOL_V1_ID_TESTNET = const MIN_ADA_POOL_V1_ID_MAINNET = "6aa2153e1ae896a95539c9d62f76cedcdabdcdf144e564b8955f609d660cf6a2"; -const adapterTestnet = new BlockfrostAdapter({ - networkId: NetworkId.TESTNET, - blockFrost: new BlockFrostAPI({ +const adapterTestnet = new BlockfrostAdapter( + NetworkId.TESTNET, + new BlockFrostAPI({ projectId: mustGetEnv("BLOCKFROST_PROJECT_ID_TESTNET"), network: "preprod", - }), -}); + }) +); -const adapterMainnet = new BlockfrostAdapter({ - networkId: NetworkId.MAINNET, - blockFrost: new BlockFrostAPI({ +const adapterMainnet = new BlockfrostAdapter( + NetworkId.MAINNET, + new BlockFrostAPI({ projectId: mustGetEnv("BLOCKFROST_PROJECT_ID_MAINNET"), network: "mainnet", - }), -}); + }) +); beforeAll(() => { jest.setTimeout(30_000);