Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
125 changes: 54 additions & 71 deletions services/transactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,57 @@ export async function fetchTransactionsByAddressList (
})
}

type TransactionOrderBy = Prisma.TransactionOrderByWithRelationInput

// Repeated query keys arrive as arrays. Only known fields become Prisma orderBy
// objects; anything else, including arrays, sorts by timestamp.
function transactionOrderBy (orderBy: unknown, orderDesc: boolean): TransactionOrderBy {
const direction: Prisma.SortOrder = orderDesc ? 'desc' : 'asc'
if (typeof orderBy !== 'string') {
return { timestamp: direction }
}
switch (orderBy) {
case 'id':
return { id: direction }
case 'hash':
return { hash: direction }
case 'amount':
return { amount: direction }
case 'confirmed':
return { confirmed: direction }
case 'createdAt':
return { createdAt: direction }
case 'address.address':
return { address: { address: direction } }
case 'address.networkId':
return { address: { networkId: direction } }
case 'timestamp':
default:
return { timestamp: direction }
}
}

function paymentOrderBy (orderBy: unknown, orderDesc: boolean): TransactionOrderBy {
const direction: Prisma.SortOrder = orderDesc ? 'desc' : 'asc'
if (typeof orderBy !== 'string') {
return { timestamp: direction }
}
switch (orderBy) {
case 'id':
return { id: direction }
case 'hash':
return { hash: direction }
case 'amount':
case 'values':
return { amount: direction }
case 'networkId':
return { address: { networkId: direction } }
case 'timestamp':
default:
return { timestamp: direction }
}
}

export async function fetchTransactionsByAddressListWithPagination (
addressIdList: string[],
page: number,
Expand All @@ -186,29 +237,7 @@ export async function fetchTransactionsByAddressListWithPagination (
networkIdsListFilter?: number[],
includeInputs = false
): Promise<TransactionsWithPaybuttonsAndPrices[]> {
const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc'

// Get query for orderBy that works with nested properties (e.g. `address.networkId`)
let orderByQuery
if (orderBy !== undefined && orderBy !== '') {
if (orderBy.includes('.')) {
const [relation, property] = orderBy.split('.')
orderByQuery = {
[relation]: {
[property]: orderDescString
}
}
} else {
orderByQuery = {
[orderBy]: orderDescString
}
}
} else {
// Default orderBy
orderByQuery = {
timestamp: orderDescString
}
}
const orderByQuery = transactionOrderBy(orderBy, orderDesc)

// Build include conditionally - exclude inputs by default unless explicitly requested
const include = includeInputs
Expand Down Expand Up @@ -288,29 +317,7 @@ export async function * generateTransactionsWithPaybuttonsAndPricesForAddress (a
}

export async function fetchPaginatedAddressTransactions (addressString: string, page: number, pageSize: number, orderBy?: string, orderDesc = true): Promise<TransactionWithAddressAndPrices[]> {
const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc'

// Get query for orderBy that works with nested properties (e.g. `address.networkId`)
let orderByQuery
if (orderBy !== undefined && orderBy !== '') {
if (orderBy.includes('.')) {
const [relation, property] = orderBy.split('.')
orderByQuery = {
[relation]: {
[property]: orderDescString
}
}
} else {
orderByQuery = {
[orderBy]: orderDescString
}
}
} else {
// Default orderBy
orderByQuery = {
timestamp: orderDescString
}
}
const orderByQuery = transactionOrderBy(orderBy, orderDesc)
const parsedAddress = parseAddress(addressString)
await addressExists(parsedAddress, true)
const txs = await prisma.transaction.findMany({
Expand Down Expand Up @@ -1161,36 +1168,12 @@ export async function fetchAllPaymentsByUserIdWithPagination (
endDate?: string,
includeInputs = false
): Promise<Payment[]> {
const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc'

if (orderBy === 'buttonDisplayDataList') {
return await getPaymentsByUserIdOrderedByButtonName(
userId, page, pageSize, orderDesc, buttonIds
)
}
// Get query for orderBy that works with nested properties (e.g. `address.networkId`)
let orderByQuery
if (orderBy !== undefined && orderBy !== '') {
if (orderBy === 'values') {
orderByQuery = {
amount: orderDescString
}
} else if (orderBy === 'networkId') {
orderByQuery = {
address: {
networkId: orderDescString
}
}
} else {
orderByQuery = {
[orderBy]: orderDescString
}
}
} else {
orderByQuery = {
timestamp: orderDescString
}
}
const orderByQuery = paymentOrderBy(orderBy, orderDesc)

const where: Prisma.TransactionWhereInput = {
address: {
Expand Down
68 changes: 68 additions & 0 deletions tests/unittests/transactionService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import prisma from 'prisma-local/clientInstance'
import * as transactionService from 'services/transactionService'
import { prismaMock } from 'prisma-local/mockedClient'
import { mockedBCHAddress, mockedUSDPriceOnTransaction, mockedCADPriceOnTransaction, mockedTransaction, mockedUserProfile, mockedAddressIdList, mockedTransactionList } from '../mockedObjects'
import { exampleAddresses } from '../utils'
import { CacheSet } from 'redis/index'
import { Prisma } from '@prisma/client'
import * as addressService from 'services/addressService'
Expand Down Expand Up @@ -455,4 +456,71 @@ describe('Date and timezone filters for transactions', () => {
expect(callArgs.where.timestamp).toBeUndefined()
expect(callArgs.where.OR).toBeUndefined()
})

it('maps payment sort fields and ignores values that are not on the blacklist', async () => {
prismaMock.transaction.findMany.mockResolvedValue([])
prisma.transaction.findMany = prismaMock.transaction.findMany

await transactionService.fetchAllPaymentsByUserIdWithPagination(
'user-1', 0, 10, 'UTC', 'values', false
)
expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({
orderBy: { amount: 'asc' }
}))

prismaMock.transaction.findMany.mockClear()
await transactionService.fetchAllPaymentsByUserIdWithPagination(
'user-1', 0, 10, 'UTC', 'networkId', true
)
expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({
orderBy: { address: { networkId: 'desc' } }
}))

prismaMock.transaction.findMany.mockClear()
await transactionService.fetchAllPaymentsByUserIdWithPagination(
'user-1', 0, 10, 'UTC', '__proto__', true
)
expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({
orderBy: { timestamp: 'desc' }
}))

prismaMock.transaction.findMany.mockClear()
await transactionService.fetchAllPaymentsByUserIdWithPagination(
'user-1', 0, 10, 'UTC', ['amount', 'hash'] as unknown as string, true
)
expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({
orderBy: { timestamp: 'desc' }
}))
})
})

describe('address transaction orderBy blacklist', () => {
const address = `ecash:${exampleAddresses.ecash}`

beforeEach(() => {
prismaMock.address.findUniqueOrThrow.mockResolvedValue(mockedBCHAddress)
prisma.address.findUniqueOrThrow = prismaMock.address.findUniqueOrThrow
prismaMock.transaction.findMany.mockResolvedValue([])
prisma.transaction.findMany = prismaMock.transaction.findMany
})

it('sorts by an allowed nested address field', async () => {
await transactionService.fetchPaginatedAddressTransactions(address, 0, 10, 'address.networkId', false)
expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({
orderBy: { address: { networkId: 'asc' } }
}))
})

it('falls back to timestamp when orderBy is an array or an unknown field', async () => {
await transactionService.fetchPaginatedAddressTransactions(address, 0, 10, ['timestamp'] as unknown as string, true)
expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({
orderBy: { timestamp: 'desc' }
}))

prismaMock.transaction.findMany.mockClear()
await transactionService.fetchPaginatedAddressTransactions(address, 0, 10, 'constructor', true)
expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({
orderBy: { timestamp: 'desc' }
}))
})
})
Loading