diff --git a/.idea/dictionaries/fuxing.xml b/.idea/dictionaries/fuxing.xml
index b831359a1..ab9d93f85 100644
--- a/.idea/dictionaries/fuxing.xml
+++ b/.idea/dictionaries/fuxing.xml
@@ -135,6 +135,7 @@
pooledtx
poolpair
poolpairs
+ poolswap
previousblockhash
prevout
prevouts
diff --git a/packages/whale-api-client/__tests__/api/poolpairs.test.ts b/packages/whale-api-client/__tests__/api/poolpairs.test.ts
index 9c0f4e18c..d5da08c53 100644
--- a/packages/whale-api-client/__tests__/api/poolpairs.test.ts
+++ b/packages/whale-api-client/__tests__/api/poolpairs.test.ts
@@ -3,7 +3,7 @@ import { StubWhaleApiClient } from '../stub.client'
import { StubService } from '../stub.service'
import { ApiPagedResponse, WhaleApiClient, WhaleApiException } from '../../src'
import { addPoolLiquidity, createPoolPair, createToken, getNewAddress, mintTokens, poolSwap } from '@defichain/testing'
-import { PoolPairData, PoolSwap, PoolSwapAggregated, PoolSwapAggregatedInterval } from '../../src/api/poolpairs'
+import { PoolPairData, PoolSwapAggregatedData, PoolSwapAggregatedInterval, PoolSwapData } from '../../src/api/poolpairs'
import { Testing } from '@defichain/jellyfish-testing'
let container: MasterNodeRegTestContainer
@@ -371,13 +371,95 @@ describe('poolswap', () => {
await container.generate(1)
await service.waitForIndexedHeight(height)
- const response: ApiPagedResponse = await client.poolpairs.listPoolSwaps('9')
- expect(response.length).toStrictEqual(2)
+ const response: ApiPagedResponse = await client.poolpairs.listPoolSwaps('9')
expect(response.hasNext).toStrictEqual(false)
- expect(response[0].fromAmount).toStrictEqual('50.00000000')
- expect(response[1].fromAmount).toStrictEqual('25.00000000')
- expect(response[0].fromTokenId).toStrictEqual(1)
- expect(response[1].fromTokenId).toStrictEqual(1)
+ expect([...response]).toStrictEqual([
+ {
+ id: expect.any(String),
+ txid: expect.stringMatching(/[0-f]{64}/),
+ txno: 1,
+ poolPairId: '9',
+ sort: expect.any(String),
+ fromAmount: '50.00000000',
+ fromTokenId: 1,
+ block: {
+ hash: expect.stringMatching(/[0-f]{64}/),
+ height: expect.any(Number),
+ time: expect.any(Number),
+ medianTime: expect.any(Number)
+ }
+ },
+ {
+ id: expect.any(String),
+ txid: expect.stringMatching(/[0-f]{64}/),
+ txno: 3,
+ poolPairId: '9',
+ sort: expect.any(String),
+ fromAmount: '25.00000000',
+ fromTokenId: 1,
+ block: {
+ hash: expect.stringMatching(/[0-f]{64}/),
+ height: expect.any(Number),
+ time: expect.any(Number),
+ medianTime: expect.any(Number)
+ }
+ }
+ ])
+
+ const verbose: ApiPagedResponse = await client.poolpairs.listPoolSwapsVerbose('9')
+ expect(verbose.hasNext).toStrictEqual(false)
+ expect([...verbose]).toStrictEqual([
+ {
+ id: expect.any(String),
+ txid: expect.stringMatching(/[0-f]{64}/),
+ txno: 1,
+ poolPairId: '9',
+ sort: expect.any(String),
+ fromAmount: '50.00000000',
+ fromTokenId: 1,
+ block: {
+ hash: expect.stringMatching(/[0-f]{64}/),
+ height: expect.any(Number),
+ time: expect.any(Number),
+ medianTime: expect.any(Number)
+ },
+ from: {
+ address: expect.any(String),
+ symbol: 'A',
+ amount: '50.00000000'
+ },
+ to: {
+ address: expect.any(String),
+ amount: '45.71428571',
+ symbol: 'DFI'
+ }
+ },
+ {
+ id: expect.any(String),
+ txid: expect.stringMatching(/[0-f]{64}/),
+ txno: 3,
+ poolPairId: '9',
+ sort: expect.any(String),
+ fromAmount: '25.00000000',
+ fromTokenId: 1,
+ block: {
+ hash: expect.stringMatching(/[0-f]{64}/),
+ height: expect.any(Number),
+ time: expect.any(Number),
+ medianTime: expect.any(Number)
+ },
+ from: {
+ address: expect.any(String),
+ symbol: 'A',
+ amount: '25.00000000'
+ },
+ to: {
+ address: expect.any(String),
+ amount: '39.99999999',
+ symbol: 'DFI'
+ }
+ }
+ ])
const poolPair: PoolPairData = await client.poolpairs.get('9')
expect(poolPair).toStrictEqual({
@@ -594,7 +676,7 @@ describe('poolswap aggregated', () => {
await service.waitForIndexedHeight(height)
}
- const dayAggregated: ApiPagedResponse = await client.poolpairs.listPoolSwapAggregates('10', PoolSwapAggregatedInterval.ONE_DAY, 10)
+ const dayAggregated: ApiPagedResponse = await client.poolpairs.listPoolSwapAggregates('10', PoolSwapAggregatedInterval.ONE_DAY, 10)
expect([...dayAggregated]).toStrictEqual([
{
aggregated: {
@@ -631,7 +713,7 @@ describe('poolswap aggregated', () => {
])
- const hourAggregated: ApiPagedResponse = await client.poolpairs.listPoolSwapAggregates('10', PoolSwapAggregatedInterval.ONE_HOUR, 3)
+ const hourAggregated: ApiPagedResponse = await client.poolpairs.listPoolSwapAggregates('10', PoolSwapAggregatedInterval.ONE_HOUR, 3)
expect([...hourAggregated]).toStrictEqual([
{
aggregated: {
diff --git a/packages/whale-api-client/src/api/poolpairs.ts b/packages/whale-api-client/src/api/poolpairs.ts
index 8de76fe40..b71a68679 100644
--- a/packages/whale-api-client/src/api/poolpairs.ts
+++ b/packages/whale-api-client/src/api/poolpairs.ts
@@ -35,12 +35,24 @@ export class PoolPairs {
* @param {string} id poolpair id
* @param {number} size of PoolSwap to query
* @param {string} next set of PoolSwap
- * @return {Promise>}
+ * @return {Promise>}
*/
- async listPoolSwaps (id: string, size: number = 30, next?: string): Promise> {
+ async listPoolSwaps (id: string, size: number = 30, next?: string): Promise> {
return await this.client.requestList('GET', `poolpairs/${id}/swaps`, size, next)
}
+ /**
+ * List pool swaps with from/to
+ *
+ * @param {string} id poolpair id
+ * @param {number} [size=10] of PoolSwap to query, max of 20 per page
+ * @param {string} next set of PoolSwap
+ * @return {Promise>}
+ */
+ async listPoolSwapsVerbose (id: string, size: number = 10, next?: string): Promise> {
+ return await this.client.requestList('GET', `poolpairs/${id}/swaps/verbose`, size, next)
+ }
+
/**
* List pool swap aggregates
*
@@ -48,9 +60,9 @@ export class PoolPairs {
* @param {PoolSwapAggregatedInterval} interval interval
* @param {number} size of PoolSwap to query
* @param {string} next set of PoolSwap
- * @return {Promise>}
+ * @return {Promise>}
*/
- async listPoolSwapAggregates (id: string, interval: PoolSwapAggregatedInterval, size: number = 30, next?: string): Promise> {
+ async listPoolSwapAggregates (id: string, interval: PoolSwapAggregatedInterval, size: number = 30, next?: string): Promise> {
return await this.client.requestList('GET', `poolpairs/${id}/swaps/aggregate/${interval as number}`, size, next)
}
}
@@ -103,7 +115,17 @@ export interface PoolPairData {
}
}
-export interface PoolSwap {
+/**
+ * @deprecated use PoolSwapData instead
+ */
+export type PoolSwap = PoolSwapData
+
+/**
+ * @deprecated use PoolSwapAggregatedData instead
+ */
+export type PoolSwapAggregated = PoolSwapAggregatedData
+
+export interface PoolSwapData {
id: string
sort: string
txid: string
@@ -113,6 +135,15 @@ export interface PoolSwap {
fromAmount: string
fromTokenId: number
+ /**
+ * To handle for optional value as Whale service might fail to resolve when indexing
+ */
+ from?: PoolSwapFromToData
+ /**
+ * To handle for optional value as Whale service might fail to resolve when indexing
+ */
+ to?: PoolSwapFromToData
+
block: {
hash: string
height: number
@@ -121,7 +152,13 @@ export interface PoolSwap {
}
}
-export interface PoolSwapAggregated {
+export interface PoolSwapFromToData {
+ address: string
+ amount: string
+ symbol: string
+}
+
+export interface PoolSwapAggregatedData {
id: string
key: string
bucket: number
diff --git a/src/module.api/poolpair.controller.ts b/src/module.api/poolpair.controller.ts
index 091e49024..e9f15386b 100644
--- a/src/module.api/poolpair.controller.ts
+++ b/src/module.api/poolpair.controller.ts
@@ -2,7 +2,7 @@ import { Controller, Get, NotFoundException, Param, ParseIntPipe, Query } from '
import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc'
import { ApiPagedResponse } from '@src/module.api/_core/api.paged.response'
import { DeFiDCache } from '@src/module.api/cache/defid.cache'
-import { PoolPairData, PoolSwap, PoolSwapAggregated } from '@whale-api-client/api/poolpairs'
+import { PoolPairData, PoolSwapAggregatedData, PoolSwapData } from '@whale-api-client/api/poolpairs'
import { PaginationQuery } from '@src/module.api/_core/api.query'
import { PoolPairService } from './poolpair.service'
import BigNumber from 'bignumber.js'
@@ -83,9 +83,35 @@ export class PoolPairController {
async listPoolSwaps (
@Param('id', ParseIntPipe) id: string,
@Query() query: PaginationQuery
- ): Promise> {
- const result = await this.poolSwapMapper.query(id, query.size, query.next)
- return ApiPagedResponse.of(result, query.size, item => {
+ ): Promise> {
+ const items = await this.poolSwapMapper.query(id, query.size, query.next)
+ return ApiPagedResponse.of(items, query.size, item => {
+ return item.sort
+ })
+ }
+
+ /**
+ * @param {string} id poolpair id
+ * @param {PaginationQuery} query with size restricted to 20
+ * @param {number} query.size
+ * @param {string} [query.next]
+ * @return {Promise>}
+ */
+ @Get('/:id/swaps/verbose')
+ async listPoolSwapsVerbose (
+ @Param('id', ParseIntPipe) id: string,
+ @Query() query: PaginationQuery
+ ): Promise> {
+ query.size = query.size > 20 ? 20 : query.size
+ const items: PoolSwapData[] = await this.poolSwapMapper.query(id, query.size, query.next)
+
+ for (const swap of items) {
+ const fromTo = await this.poolPairService.findSwapFromTo(swap.block.height, swap.txid, swap.txno)
+ swap.from = fromTo?.from
+ swap.to = fromTo?.to
+ }
+
+ return ApiPagedResponse.of(items, query.size, item => {
return item.sort
})
}
@@ -106,10 +132,10 @@ export class PoolPairController {
@Param('id', ParseIntPipe) id: string,
@Param('interval', ParseIntPipe) interval: string,
@Query() query: PaginationQuery
- ): Promise> {
+ ): Promise> {
const lt = query.next === undefined ? undefined : parseInt(query.next)
const aggregates = await this.poolSwapAggregatedMapper.query(`${id}-${interval}`, query.size, lt)
- const mapped: Array> = aggregates.map(async value => {
+ const mapped: Array> = aggregates.map(async value => {
return {
...value,
aggregated: {
@@ -126,8 +152,7 @@ export class PoolPairController {
}
}
-function mapPoolPair (id: string, info: PoolPairInfo, totalLiquidityUsd?: BigNumber, apr?: PoolPairData['apr'],
- volume?: PoolPairData['volume']): PoolPairData {
+function mapPoolPair (id: string, info: PoolPairInfo, totalLiquidityUsd?: BigNumber, apr?: PoolPairData['apr'], volume?: PoolPairData['volume']): PoolPairData {
const [symbolA, symbolB] = info.symbol.split('-')
return {
diff --git a/src/module.api/poolpair.service.ts b/src/module.api/poolpair.service.ts
index 20cfb988e..cfe3a1048 100644
--- a/src/module.api/poolpair.service.ts
+++ b/src/module.api/poolpair.service.ts
@@ -1,21 +1,38 @@
-import { Injectable, NotFoundException } from '@nestjs/common'
+import { Inject, Injectable, NotFoundException } from '@nestjs/common'
import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc'
import BigNumber from 'bignumber.js'
import { PoolPairInfo } from '@defichain/jellyfish-api-core/dist/category/poolpair'
import { SemaphoreCache } from '@src/module.api/cache/semaphore.cache'
-import { PoolPairData } from '@whale-api-client/api/poolpairs'
+import { PoolPairData, PoolSwapFromToData } from '@whale-api-client/api/poolpairs'
import { getBlockSubsidy } from '@src/module.api/subsidy'
import { BlockMapper } from '@src/module.model/block'
import { TokenMapper } from '@src/module.model/token'
import { PoolSwapAggregated, PoolSwapAggregatedMapper } from '@src/module.model/pool.swap.aggregated'
import { PoolSwapAggregatedInterval } from '@src/module.indexer/model/dftx/pool.swap.aggregated'
+import { TransactionVout, TransactionVoutMapper } from '@src/module.model/transaction.vout'
+import { SmartBuffer } from 'smart-buffer'
+import {
+ CCompositeSwap,
+ CompositeSwap,
+ CPoolSwap,
+ OP_DEFI_TX,
+ PoolSwap as PoolSwapDfTx,
+ toOPCodes
+} from '@defichain/jellyfish-transaction'
+import { fromScript } from '@defichain/jellyfish-address'
+import { NetworkName } from '@defichain/jellyfish-network'
+import { AccountHistory } from '@defichain/jellyfish-api-core/dist/category/account'
+import { DeFiDCache } from '@src/module.api/cache/defid.cache'
@Injectable()
export class PoolPairService {
constructor (
+ @Inject('NETWORK') protected readonly network: NetworkName,
protected readonly rpcClient: JsonRpcClient,
+ protected readonly deFiDCache: DeFiDCache,
protected readonly cache: SemaphoreCache,
protected readonly poolSwapAggregatedMapper: PoolSwapAggregatedMapper,
+ protected readonly voutMapper: TransactionVoutMapper,
protected readonly tokenMapper: TokenMapper,
protected readonly blockMapper: BlockMapper
) {
@@ -202,6 +219,40 @@ export class PoolPairService {
return value
}
+ public async findSwapFromTo (height: number, txid: string, txno: number): Promise<{ from?: PoolSwapFromToData, to?: PoolSwapFromToData } | undefined> {
+ const vouts = await this.voutMapper.query(txid, 1)
+ const dftx = findPoolSwapDfTx(vouts)
+ if (dftx === undefined) {
+ return undefined
+ }
+
+ const fromAddress = fromScript(dftx.fromScript, this.network)?.address
+ const fromToken = await this.deFiDCache.getTokenInfo(dftx.fromTokenId.toString())
+
+ const toAddress = fromScript(dftx.toScript, this.network)?.address
+ if (fromAddress === undefined || toAddress === undefined || fromToken === undefined) {
+ return undefined
+ }
+
+ const history = await this.getAccountHistory(toAddress, height, txno)
+ if (history === undefined) {
+ return undefined
+ }
+
+ return {
+ from: {
+ address: fromAddress,
+ symbol: fromToken.symbol,
+ amount: dftx.fromAmount.toFixed(8)
+ },
+ to: findPoolSwapFromTo(history, false)
+ }
+ }
+
+ private async getAccountHistory (address: string, height: number, txno: number): Promise {
+ return await this.rpcClient.account.getAccountHistory(address, height, txno)
+ }
+
private async getLoanTokenSplits (): Promise | undefined> {
return await this.cache.get>('LP_LOAN_TOKEN_SPLITS', async () => {
const result = await this.rpcClient.masternode.getGov('LP_LOAN_TOKEN_SPLITS')
@@ -324,3 +375,53 @@ export class PoolPairService {
}
}
}
+
+function findPoolSwapDfTx (vouts: TransactionVout[]): PoolSwapDfTx | undefined {
+ const hex = vouts[0].script.hex
+ const buffer = SmartBuffer.fromBuffer(Buffer.from(hex, 'hex'))
+ const stack = toOPCodes(buffer)
+ if (stack.length !== 2 || stack[1].type !== 'OP_DEFI_TX') {
+ return undefined
+ }
+
+ const dftx = (stack[1] as OP_DEFI_TX).tx
+ if (dftx === undefined) {
+ return undefined
+ }
+
+ switch (dftx.name) {
+ case CPoolSwap.OP_NAME:
+ return (dftx.data as PoolSwapDfTx)
+
+ case CCompositeSwap.OP_NAME:
+ return (dftx.data as CompositeSwap).poolSwap
+
+ default:
+ return undefined
+ }
+}
+
+function findPoolSwapFromTo (history: AccountHistory, from: boolean): PoolSwapFromToData | undefined {
+ for (const amount of history.amounts) {
+ const [value, symbol] = amount.split('@')
+ const isNegative = value.startsWith('-')
+
+ if (isNegative && from) {
+ return {
+ address: history.owner,
+ amount: new BigNumber(value).absoluteValue().toFixed(8),
+ symbol: symbol
+ }
+ }
+
+ if (!isNegative && !from) {
+ return {
+ address: history.owner,
+ amount: new BigNumber(value).absoluteValue().toFixed(8),
+ symbol: symbol
+ }
+ }
+ }
+
+ return undefined
+}