Skip to content

Commit 0678643

Browse files
committed
feat: adding filter param for transactions
1 parent 4dbd5ea commit 0678643

File tree

8 files changed

+93
-45
lines changed

8 files changed

+93
-45
lines changed

src/api/openapi.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,16 @@ module.exports = {
346346
schema: {
347347
type: 'string'
348348
}
349+
},
350+
{
351+
name: 'flow',
352+
in: 'query',
353+
description: 'Filter by incoming or outgoing transactions',
354+
required: false,
355+
schema: {
356+
type: 'string',
357+
enum: ['all', 'to', 'from']
358+
}
349359
}
350360
],
351361
responses: {

src/blockscoutApi/index.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
fromApiToTokenWithBalance, fromApiToTokens, fromApiToTransaction
1313
} from './utils'
1414
import { GetEventLogsByAddressAndTopic0 } from '../service/address/AddressService'
15+
import { Flow } from '../types/event'
1516

1617
export class BlockscoutAPI extends DataSource {
1718
private chainId: number
@@ -50,18 +51,19 @@ export class BlockscoutAPI extends DataSource {
5051
.catch(this.errorHandling)
5152
}
5253

53-
async getEventsByAddress (address: string) {
54+
async getEventsByAddress (address: string, flow: Flow = Flow.ALL) {
5455
const params = {
5556
module: 'account',
5657
action: 'tokentx',
5758
address: address.toLowerCase()
5859
}
5960
return this.axios?.get<ServerResponse<TokenTransferApi>>(`${this.url}`, { params })
60-
.then(response =>
61-
response.data.result
62-
.map(tokenTranfer => {
63-
return fromApiToTEvents(tokenTranfer)
64-
}))
61+
.then(response => response.data.result.filter(event => {
62+
if (flow === Flow.ALL) return event
63+
return event[flow].toLowerCase() === address
64+
}))
65+
.then(response => response
66+
.map(tokenTranfer => fromApiToTEvents(tokenTranfer)))
6567
.catch(this.errorHandling)
6668
}
6769

@@ -72,19 +74,35 @@ export class BlockscoutAPI extends DataSource {
7274
.catch(this.errorHandling)
7375
}
7476

75-
getInternalTransactionByAddress (address: string) {
77+
getInternalTransactionByAddress (address: string, flow: Flow) {
78+
const params = flow === Flow.ALL ? {} : { filter: flow }
79+
7680
return this.axios?.get<InternalTransactionResponse>(
77-
`${this.url}/v2/addresses/${address.toLowerCase()}/internal-transactions`
81+
`${this.url}/v2/addresses/${address.toLowerCase()}/internal-transactions`,
82+
{ params }
7883
)
7984
.then(response => response.data.items.map(fromApiToInternalTransaction))
8085
.catch(this.errorHandling)
8186
}
8287

83-
getTransactionsByAddress (address: string) {
88+
getTransactionsByAddress (address:string,
89+
_limit?: string,
90+
_prev?: string,
91+
next?: string,
92+
_blockNumber?: string,
93+
flow?: Flow) {
94+
const params = {
95+
...(next ? { block_number: next, index: 0, items_count: 50 } : {}),
96+
...(flow === Flow.ALL ? {} : { filter: flow })
97+
}
8498
return this.axios?.get<TransactionsServerResponse>(
85-
`${this.url}/v2/addresses/${address.toLowerCase()}/transactions`
99+
`${this.url}/v2/addresses/${address.toLowerCase()}/transactions`,
100+
{ params }
86101
)
87-
.then(response => ({ data: response.data.items.map(fromApiToTransaction) }))
102+
.then(response => ({
103+
data: response.data.items.map(fromApiToTransaction),
104+
next: response.data.next_page_params?.block_number
105+
}))
88106
.catch(this.errorHandling)
89107
}
90108

src/controller/httpsAPI.ts

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ValidationError, object, string } from 'yup'
1111
import { utils } from 'ethers'
1212
import { AddressService } from '../service/address/AddressService'
1313
import { supportedFiat } from '../coinmarketcap/support'
14+
import { Flow } from '../types/event'
1415

1516
interface HttpsAPIDependencies {
1617
app: Express,
@@ -44,20 +45,21 @@ export class HttpsAPI {
4445
}
4546

4647
init () : void {
47-
const chainIdSchema = object({
48-
chainId: string().optional()
49-
.trim()
50-
.oneOf(Object.keys(this.dataSourceMapping), 'The current chainId is not supported')
51-
})
5248
const addressSchema = object({
5349
address: string().required('An address is invalid')
5450
.trim()
5551
.transform(address => utils.isAddress(address.toLowerCase()) ? address : '')
56-
}).required()
57-
const currencySchema = object({
52+
})
53+
const schema = object({
54+
chainId: string().optional()
55+
.trim()
56+
.oneOf(Object.keys(this.dataSourceMapping), 'The current chainId is not supported'),
5857
convert: string().optional()
5958
.trim()
60-
.oneOf(supportedFiat, 'The current currency is not supported')
59+
.oneOf(supportedFiat, 'The current currency is not supported'),
60+
flow: string().optional()
61+
.trim()
62+
.oneOf([Flow.ALL, Flow.FROM, Flow.TO], 'The transaction filter is invalid')
6163
})
6264

6365
const whilelist = [
@@ -79,7 +81,7 @@ export class HttpsAPI {
7981

8082
this.app.get('/tokens', ({ query: { chainId = '31' } }: Request, res: Response, next: NextFunction) => {
8183
try {
82-
chainIdSchema.validateSync({ chainId })
84+
schema.validateSync({ chainId })
8385
return this
8486
.dataSourceMapping[chainId as string].getTokens()
8587
.then(this.responseJsonOk(res))
@@ -93,8 +95,8 @@ export class HttpsAPI {
9395
'/address/:address/tokens',
9496
async ({ params: { address }, query: { chainId = '31' } }: Request, res: Response, next: NextFunction) => {
9597
try {
96-
chainIdSchema.validateSync({ chainId })
9798
addressSchema.validateSync({ address })
99+
schema.validateSync({ chainId })
98100
const balance = await this.addressService.getTokensByAddress({
99101
chainId: chainId as string,
100102
address: address as string
@@ -110,8 +112,8 @@ export class HttpsAPI {
110112
'/address/:address/events',
111113
({ params: { address }, query: { chainId = '31' } }: Request, res: Response, next: NextFunction) => {
112114
try {
113-
chainIdSchema.validateSync({ chainId })
114115
addressSchema.validateSync({ address })
116+
schema.validateSync({ chainId })
115117
return this
116118
.dataSourceMapping[chainId as string].getEventsByAddress(address)
117119
.then(this.responseJsonOk(res))
@@ -124,19 +126,23 @@ export class HttpsAPI {
124126

125127
this.app.get(
126128
'/address/:address/transactions',
127-
async ({ params: { address }, query: { limit, prev, next, chainId = '31', blockNumber = '0' } }: Request,
128-
res: Response, nextFunction: NextFunction) => {
129+
async ({
130+
params: { address },
131+
query: { limit, prev, next, chainId = '31', blockNumber = '0', flow = Flow.ALL }
132+
}: Request,
133+
res: Response, nextFunction: NextFunction) => {
129134
try {
130-
chainIdSchema.validateSync({ chainId })
131135
addressSchema.validateSync({ address })
136+
schema.validateSync({ chainId, flow })
132137

133138
const transactions = await this.addressService.getTransactionsByAddress({
134139
address: address as string,
135140
chainId: chainId as string,
136141
limit: limit as string,
137142
prev: prev as string,
138143
next: next as string,
139-
blockNumber: blockNumber as string
144+
blockNumber: blockNumber as string,
145+
flow: flow as Flow
140146
}).catch(nextFunction)
141147
return this.responseJsonOk(res)(transactions)
142148
} catch (e) {
@@ -149,7 +155,7 @@ export class HttpsAPI {
149155
async ({ params: { address }, query: { chainId = '31' } } : Request, res: Response,
150156
nextFunction: NextFunction) => {
151157
try {
152-
chainIdSchema.validateSync({ chainId })
158+
schema.validateSync({ chainId })
153159
addressSchema.validateSync({ address })
154160
const nft = await this.addressService.getNftInfo({ chainId: chainId as string, address }).catch(nextFunction)
155161
return this.responseJsonOk(res)(nft)
@@ -162,7 +168,7 @@ export class HttpsAPI {
162168
async ({ params: { nft, address }, query: { chainId = '31' } } : Request, res: Response,
163169
nextFunction: NextFunction) => {
164170
try {
165-
chainIdSchema.validateSync({ chainId })
171+
schema.validateSync({ chainId })
166172
addressSchema.validateSync({ address })
167173
const nftInfo = await this.addressService
168174
.getNftOwnedByAddress({ chainId: chainId as string, address, nftAddress: nft })
@@ -199,7 +205,7 @@ export class HttpsAPI {
199205
async (req: Request<{}, {}, {}, PricesQueryParams>, res: Response) => {
200206
try {
201207
const { convert = 'USD', addresses = '' } = req.query
202-
currencySchema.validateSync({ convert })
208+
schema.validateSync({ convert })
203209
addresses.split(',').forEach(address => addressSchema.validateSync({ address }))
204210
const prices = await this.addressService.getPrices({
205211
addresses,
@@ -228,17 +234,18 @@ export class HttpsAPI {
228234
'/address/:address',
229235
async (req, res) => {
230236
try {
231-
const { limit, prev, next, chainId = '31', blockNumber = '0' } = req.query
237+
const { limit, prev, next, chainId = '31', blockNumber = '0', flow = Flow.ALL } = req.query
232238
const { address } = req.params
233-
chainIdSchema.validateSync({ chainId })
234239
addressSchema.validateSync({ address })
240+
schema.validateSync({ chainId })
235241
const data = await this.addressService.getAddressDetails({
236242
chainId: chainId as string,
237243
address,
238244
blockNumber: blockNumber as string,
239245
limit: limit as string,
240246
prev: prev as string,
241-
next: next as string
247+
next: next as string,
248+
flow: flow as Flow
242249
})
243250
return this.responseJsonOk(res)(data)
244251
} catch (error) {

src/controller/webSocketAPI.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AddressService } from '../service/address/AddressService'
77
import { AddressQuery } from '../api/types'
88
import { RateLimiterMemory } from 'rate-limiter-flexible'
99
import BitcoinCore from '../service/bitcoin/BitcoinCore'
10+
import { Flow } from '../types/event'
1011
export class WebSocketAPI {
1112
private dataSourceMapping: RSKDatasource
1213
private lastPrice: LastPrice
@@ -75,7 +76,9 @@ export class WebSocketAPI {
7576
const profiler = this.tracker.get(key) || new Profiler(address, dataSource, this.lastPrice, provider)
7677
const newSuscription = this.tracker.has(key)
7778
this.tracker.set(key, profiler)
78-
const data = await this.addressService.getAddressDetails({ chainId, address, blockNumber, limit: '' })
79+
const data = await this.addressService.getAddressDetails(
80+
{ chainId, address, blockNumber, limit: '', flow: Flow.ALL }
81+
)
7982
socket.emit('init', data)
8083

8184
profiler.on('balances', (data) => {

src/repository/DataSource.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import _axios from 'axios'
22
import { ethers } from 'ethers'
33
import BitcoinCore from '../service/bitcoin/BitcoinCore'
44
import { GetEventLogsByAddressAndTopic0 } from '../service/address/AddressService'
5+
import { Flow } from '../types/event'
56

67
export abstract class DataSource {
78
readonly url: string
@@ -17,14 +18,15 @@ export abstract class DataSource {
1718
abstract getTokens();
1819
abstract getTokensByAddress(address: string);
1920
abstract getRbtcBalanceByAddress(address: string);
20-
abstract getEventsByAddress(address: string, limit?: string);
21+
abstract getEventsByAddress(address: string, limit?: string, flow?: Flow);
2122
abstract getTransaction(hash: string);
22-
abstract getInternalTransactionByAddress(address: string, limit?: string);
23+
abstract getInternalTransactionByAddress(address: string, limit?: string, flow?: Flow);
2324
abstract getTransactionsByAddress(address:string,
2425
limit?: string,
2526
prev?: string,
2627
next?: string,
27-
blockNumber?: string);
28+
blockNumber?: string,
29+
flow?: Flow);
2830

2931
abstract getNft(address: string);
3032
abstract getNftOwnedByAddress(address: string, nft: string);

src/service/address/AddressService.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { isMyTransaction } from '../transaction/utils'
33
import { IApiTransactions, IEvent, IInternalTransaction } from '../../rskExplorerApi/types'
44
import { LastPrice } from '../price/lastPrice'
55
import { fromApiToRtbcBalance } from '../../rskExplorerApi/utils'
6+
import { Flow } from '../../types/event'
67

78
interface AddressServiceDependencies {
89
dataSourceMapping: RSKDatasource
@@ -16,7 +17,8 @@ interface GetTransactionsByAddressFunction {
1617
chainId: string
1718
prev?: string
1819
next?: string
19-
blockNumber: string
20+
blockNumber: string,
21+
flow: Flow
2022
}
2123

2224
interface GetPricesFunction {
@@ -60,15 +62,15 @@ export class AddressService {
6062
}
6163

6264
async getTransactionsByAddress (
63-
{ chainId, address, limit, next, prev, blockNumber }: GetTransactionsByAddressFunction
65+
{ chainId, address, limit, next, prev, blockNumber, flow }: GetTransactionsByAddressFunction
6466
) {
6567
const dataSource = this.dataSourceMapping[chainId]
6668
/* A transaction has the following structure { to: string, from: string }
6769
* and to or from params should be our address when we send or receive a cryptocurrency
6870
* (such as RBTC).
6971
*/
7072
const transactions: {data: IApiTransactions[], prev: string, next: string} =
71-
await dataSource.getTransactionsByAddress(address, limit, prev, next, blockNumber)
73+
await dataSource.getTransactionsByAddress(address, limit, prev, next, blockNumber, flow)
7274

7375
/* We query events to find transactions when we send or receive a token(ERC20)
7476
* such as RIF,RDOC
@@ -77,8 +79,8 @@ export class AddressService {
7779
* Finally, we filter by blocknumber and duplicates
7880
*/
7981
const hashes: string[] = await Promise.all([
80-
dataSource.getEventsByAddress(address, limit as string),
81-
dataSource.getInternalTransactionByAddress(address, limit as string)
82+
dataSource.getEventsByAddress(address, limit as string, flow as Flow),
83+
dataSource.getInternalTransactionByAddress(address, limit as string, flow as Flow)
8284
])
8385
.then((promises) => {
8486
return promises.flat()
@@ -92,7 +94,6 @@ export class AddressService {
9294
.catch(() => [])
9395

9496
const result = await Promise.all(hashes.map(hash => dataSource.getTransaction(hash)))
95-
9697
return {
9798
prev: transactions.prev,
9899
next: transactions.next,
@@ -124,12 +125,13 @@ export class AddressService {
124125
blockNumber,
125126
limit,
126127
prev,
127-
next
128+
next,
129+
flow
128130
}: GetBalancesTransactionsPricesByAddress) {
129131
const [prices, tokens, transactions] = await Promise.all([
130132
this.getLatestPrices(),
131133
this.getTokensByAddress({ chainId, address }),
132-
this.getTransactionsByAddress({ chainId, address, blockNumber, limit, prev, next })
134+
this.getTransactionsByAddress({ chainId, address, blockNumber, limit, prev, next, flow })
133135
])
134136
return {
135137
prices,

src/types/event.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,9 @@ export type Event = {
44
type: string
55
payload: ITokenWithBalance | IApiTransactions | IEvent
66
}
7+
8+
export enum Flow {
9+
ALL = 'all',
10+
TO = 'to',
11+
FROM = 'from'
12+
}

test/address.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe('transactions', () => {
5555
.expect('Content-Type', /json/)
5656
.expect(200)
5757
expect(JSON.parse(text)).toEqual(transactionWithEventResponse)
58-
expect(getTransactionsByAddressMock).toHaveBeenCalledWith(mockAddress, '50', undefined, undefined, '0')
58+
expect(getTransactionsByAddressMock).toHaveBeenCalledWith(mockAddress, '50', undefined, undefined, '0', 'all')
5959
})
6060
})
6161

0 commit comments

Comments
 (0)