Skip to content

Commit f1cc9f2

Browse files
authored
Faster payments page (#1124)
* Don't fetch inputs unless requested This avoids loading the tx inputs where they are unlikely needed. Can still be requested by passing`includeInputs=true` to the query. The following api endpoints are affected: - /api/payments - /api/paybutton/transactions/[id] None of these needs the inputs by default. * Synchronous functions don't need async This just add overhead, especially when called in loops. * Improve performance of fetchDistinctPaymentYearsByUser This is called from the very niche endpoint /api/transaction/years. The amount > 0 was intended to only check for payments, but in practice this doesn't matter. We can just return the years of acticity for this userId and show no payments for the years it was not used on paybutton. This dramatically speeds up the query (from ~9s to ~100ms on my machine). * Add a new isPayment flag to Transaction and use it as an index This makes the payment requests much faster. * Fetch paginated payments and counts in parallel This speeds up loading of the payments page for some filters.
1 parent ea580dd commit f1cc9f2

8 files changed

Lines changed: 71 additions & 35 deletions

File tree

pages/api/paybutton/transactions/[id].ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RESPONSE_MESSAGES, TX_PAGE_SIZE_LIMIT } from 'constants/index'
1+
import { RESPONSE_MESSAGES, TX_PAGE_SIZE_LIMIT, DEFAULT_TX_PAGE_SIZE } from 'constants/index'
22
import { fetchTransactionsByPaybuttonIdWithPagination } from 'services/transactionService'
33
import * as paybuttonService from 'services/paybuttonService'
44
import { setSession } from 'utils/setSession'
@@ -13,6 +13,7 @@ export default async (req: any, res: any): Promise<void> => {
1313
const pageSize = (req.query.pageSize === '' || req.query.pageSize === undefined) ? DEFAULT_TX_PAGE_SIZE : Number(req.query.pageSize)
1414
const orderBy = (req.query.orderBy === '' || req.query.orderBy === undefined) ? undefined : req.query.orderBy as string
1515
const orderDesc: boolean = !!(req.query.orderDesc === '' || req.query.orderDesc === undefined || req.query.orderDesc === 'true')
16+
const includeInputs: boolean = req.query.includeInputs === 'true'
1617

1718
if (isNaN(page) || isNaN(pageSize)) {
1819
throw new Error(RESPONSE_MESSAGES.PAGE_SIZE_AND_PAGE_SHOULD_BE_NUMBERS_400.message)
@@ -27,7 +28,7 @@ export default async (req: any, res: any): Promise<void> => {
2728
throw new Error(RESPONSE_MESSAGES.RESOURCE_DOES_NOT_BELONG_TO_USER_400.message)
2829
}
2930

30-
const transactions = await fetchTransactionsByPaybuttonIdWithPagination(paybuttonId, page, pageSize, orderDesc, orderBy)
31+
const transactions = await fetchTransactionsByPaybuttonIdWithPagination(paybuttonId, page, pageSize, orderDesc, orderBy, undefined, includeInputs)
3132

3233
res.status(200).json({ transactions })
3334
} catch (err: any) {

pages/api/payments/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default async (req: any, res: any): Promise<void> => {
2828
if (typeof req.query.endDate === 'string' && req.query.endDate !== '') {
2929
endDate = req.query.endDate as string
3030
}
31+
const includeInputs = req.query.includeInputs === 'true'
3132
const userReqTimezone = req.headers.timezone as string
3233
const userPreferredTimezone = user?.preferredTimezone
3334
let timezone = userPreferredTimezone !== '' ? userPreferredTimezone : userReqTimezone
@@ -47,7 +48,8 @@ export default async (req: any, res: any): Promise<void> => {
4748
buttonIds,
4849
years,
4950
startDate,
50-
endDate
51+
endDate,
52+
includeInputs
5153
)
5254
res.status(200).json(resJSON)
5355
}

pages/payments/index.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -224,16 +224,16 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
224224
paymentsCountUrl += `${paymentsCountUrl.includes('?') ? '&' : '?'}endDate=${endDate}`
225225
}
226226

227-
const paymentsResponse = await fetch(url, {
228-
headers: {
229-
Timezone: timezone
230-
}
231-
})
232-
233-
const paymentsCountResponse = await fetch(
234-
paymentsCountUrl,
235-
{ headers: { Timezone: timezone } }
236-
)
227+
const [paymentsResponse, paymentsCountResponse] = await Promise.all([
228+
fetch(url, {
229+
headers: {
230+
Timezone: timezone
231+
}
232+
}),
233+
fetch(paymentsCountUrl, {
234+
headers: { Timezone: timezone }
235+
})
236+
])
237237

238238
if (!paymentsResponse.ok || !paymentsCountResponse.ok) {
239239
console.log('paymentsResponse status', paymentsResponse.status)
@@ -243,8 +243,10 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
243243
throw new Error('Failed to fetch payments or count')
244244
}
245245

246-
const totalCount = await paymentsCountResponse.json()
247-
const payments = await paymentsResponse.json()
246+
const [payments, totalCount] = await Promise.all([
247+
paymentsResponse.json(),
248+
paymentsCountResponse.json()
249+
])
248250

249251
return { data: payments, totalCount }
250252
} catch (error) {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Add isPayment column
2+
ALTER TABLE `Transaction` ADD COLUMN `isPayment` BOOLEAN NOT NULL DEFAULT FALSE;
3+
4+
-- Populate isPayment for existing data
5+
UPDATE `Transaction` SET `isPayment` = TRUE WHERE `amount` > 0;
6+
7+
-- Add composite index for addressId + isPayment queries
8+
CREATE INDEX `Transaction_addressId_isPayment_idx` ON `Transaction`(`addressId`, `isPayment`);

prisma-local/schema.prisma

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ model Transaction {
7272
amount Decimal @db.Decimal(24, 8)
7373
confirmed Boolean @default(false)
7474
orphaned Boolean @default(false)
75+
isPayment Boolean @default(false)
7576
timestamp Int
7677
addressId String
7778
opReturn String @db.LongText @default("")
@@ -85,6 +86,7 @@ model Transaction {
8586
8687
@@unique([hash, addressId], name: "Transaction_hash_addressId_unique_constraint")
8788
@@index([addressId, timestamp], map: "Transaction_addressId_timestamp_idx")
89+
@@index([addressId, isPayment])
8890
}
8991

9092
model TransactionInput {

redis/paymentCache.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ interface GroupedPaymentsAndInfoObject {
6969
info: AddressPaymentInfo
7070
}
7171

72-
export const generatePaymentFromTx = async (tx: TransactionsWithPaybuttonsAndPrices): Promise<Payment> => {
72+
export const generatePaymentFromTx = (tx: TransactionsWithPaybuttonsAndPrices): Payment => {
7373
const values = getTransactionValue(tx)
7474
let buttonDisplayDataList: Array<{ name: string, id: string}> = []
7575
if (tx.address.paybuttons !== undefined) {
@@ -97,7 +97,7 @@ export const generatePaymentFromTx = async (tx: TransactionsWithPaybuttonsAndPri
9797
}
9898
}
9999

100-
export const generatePaymentFromTxWithInvoices = async (tx: TransactionWithAddressAndPricesAndInvoices, userId?: string): Promise<Payment> => {
100+
export const generatePaymentFromTxWithInvoices = (tx: TransactionWithAddressAndPricesAndInvoices, userId?: string): Payment => {
101101
const values = getTransactionValue(tx)
102102
let buttonDisplayDataList: Array<{ name: string, id: string}> = []
103103
if (tx.address.paybuttons !== undefined) {
@@ -141,7 +141,7 @@ export const generateAndCacheGroupedPaymentsAndInfoForAddress = async (address:
141141
for (const tx of batch) {
142142
balance = balance.plus(tx.amount)
143143
if (tx.amount.gt(0)) {
144-
const payment = await generatePaymentFromTx(tx)
144+
const payment = generatePaymentFromTx(tx)
145145
paymentList.push(payment)
146146
paymentCount++
147147
}
@@ -235,7 +235,7 @@ const cacheGroupedPaymentsAppend = async (paymentsGroupedByKey: KeyValueT<Paymen
235235
export const cacheManyTxs = async (txs: TransactionsWithPaybuttonsAndPrices[]): Promise<void> => {
236236
const zero = new Prisma.Decimal(0)
237237
for (const tx of txs.filter(tx => tx.amount > zero)) {
238-
const payment = await generatePaymentFromTx(tx)
238+
const payment = generatePaymentFromTx(tx)
239239
if (payment.values.usd !== new Prisma.Decimal(0)) {
240240
const paymentsGroupedByKey = getPaymentsByWeek(tx.address.address, [payment])
241241
void await cacheGroupedPaymentsAppend(paymentsGroupedByKey)

services/chronikService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ export class ChronikBlockchainClient {
298298
timestamp: transaction.block !== undefined ? transaction.block.timestamp : transaction.timeFirstSeen,
299299
addressId: address.id,
300300
confirmed: transaction.block !== undefined,
301+
isPayment: amount > 0,
301302
opReturn,
302303
inputs: {
303304
create: inputAddresses

services/transactionService.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,8 @@ export async function fetchTransactionsByAddressListWithPagination (
183183
pageSize: number,
184184
orderBy?: string,
185185
orderDesc = true,
186-
networkIdsListFilter?: number[]
186+
networkIdsListFilter?: number[],
187+
includeInputs = false
187188
): Promise<TransactionsWithPaybuttonsAndPrices[]> {
188189
const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc'
189190

@@ -209,6 +210,14 @@ export async function fetchTransactionsByAddressListWithPagination (
209210
}
210211
}
211212

213+
// Build include conditionally - exclude inputs by default unless explicitly requested
214+
const include = includeInputs
215+
? includePaybuttonsAndPricesAndInvoices
216+
: (() => {
217+
const { inputs, ...rest } = includePaybuttonsAndPricesAndInvoices
218+
return rest
219+
})()
220+
212221
return await prisma.transaction.findMany({
213222
where: {
214223
addressId: {
@@ -220,11 +229,11 @@ export async function fetchTransactionsByAddressListWithPagination (
220229
}
221230
}
222231
},
223-
include: includePaybuttonsAndPricesAndInvoices,
232+
include,
224233
orderBy: orderByQuery,
225234
skip: page * pageSize,
226235
take: pageSize
227-
})
236+
}) as unknown as TransactionsWithPaybuttonsAndPrices[]
228237
}
229238

230239
export async function fetchTxCountByAddressString (addressString: string): Promise<number> {
@@ -570,6 +579,7 @@ export async function createManyTransactions (
570579
timestamp: tx.timestamp,
571580
addressId: tx.addressId,
572581
confirmed: tx.confirmed ?? false,
582+
isPayment: tx.amount > 0,
573583
opReturn: tx.opReturn ?? '',
574584
orphaned: false
575585
}))
@@ -823,15 +833,19 @@ export async function fetchTransactionsByPaybuttonIdWithPagination (
823833
pageSize: number,
824834
orderDesc: boolean,
825835
orderBy?: string,
826-
networkIds?: number[]): Promise<TransactionsWithPaybuttonsAndPrices[]> {
836+
networkIds?: number[],
837+
includeInputs = false
838+
): Promise<TransactionsWithPaybuttonsAndPrices[]> {
827839
const addressIdList = await fetchAddressesByPaybuttonId(paybuttonId)
828840
const transactions = await fetchTransactionsByAddressListWithPagination(
829841
addressIdList,
830842
page,
831843
pageSize,
832844
orderBy,
833845
orderDesc,
834-
networkIds)
846+
networkIds,
847+
includeInputs
848+
)
835849

836850
return transactions
837851
}
@@ -935,7 +949,7 @@ export async function getPaymentsByUserIdOrderedByButtonName (
935949
LEFT JOIN \`PricesOnTransactions\` pt ON t.\`id\` = pt.\`transactionId\`
936950
LEFT JOIN \`Price\` pb ON pt.\`priceId\` = pb.\`id\`
937951
LEFT JOIN \`Invoice\` i ON i.\`transactionId\` = t.\`id\`
938-
WHERE t.\`amount\` > 0
952+
WHERE t.\`isPayment\` = TRUE
939953
AND EXISTS (
940954
SELECT 1
941955
FROM \`AddressesOnUserProfiles\` au
@@ -1005,7 +1019,8 @@ export async function fetchAllPaymentsByUserIdWithPagination (
10051019
buttonIds?: string[],
10061020
years?: string[],
10071021
startDate?: string,
1008-
endDate?: string
1022+
endDate?: string,
1023+
includeInputs = false
10091024
): Promise<Payment[]> {
10101025
const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc'
10111026

@@ -1042,7 +1057,7 @@ export async function fetchAllPaymentsByUserIdWithPagination (
10421057
address: {
10431058
userProfiles: { some: { userId } }
10441059
},
1045-
amount: { gt: 0 }
1060+
isPayment: true
10461061
}
10471062

10481063
if (startDate !== undefined && endDate !== undefined && startDate !== '' && endDate !== '') {
@@ -1061,9 +1076,17 @@ export async function fetchAllPaymentsByUserIdWithPagination (
10611076
}
10621077
}
10631078

1079+
// Build include conditionally - exclude inputs by default unless explicitly requested
1080+
const include = includeInputs
1081+
? includePaybuttonsAndPricesAndInvoices
1082+
: (() => {
1083+
const { inputs, ...rest } = includePaybuttonsAndPricesAndInvoices
1084+
return rest
1085+
})()
1086+
10641087
const transactions = await prisma.transaction.findMany({
10651088
where,
1066-
include: includePaybuttonsAndPricesAndInvoices,
1089+
include,
10671090
orderBy: orderByQuery,
10681091
skip: page * Number(pageSize),
10691092
take: Number(pageSize)
@@ -1073,7 +1096,7 @@ export async function fetchAllPaymentsByUserIdWithPagination (
10731096
for (let index = 0; index < transactions.length; index++) {
10741097
const tx = transactions[index]
10751098
if (Number(tx.amount) > 0) {
1076-
const payment = await generatePaymentFromTxWithInvoices(tx, userId)
1099+
const payment = generatePaymentFromTxWithInvoices(tx as unknown as TransactionWithAddressAndPricesAndInvoices, userId)
10771100
transformedData.push(payment)
10781101
}
10791102
}
@@ -1160,9 +1183,7 @@ export async function fetchAllPaymentsByUserId (
11601183
in: networkIds ?? Object.values(NETWORK_IDS)
11611184
}
11621185
},
1163-
amount: {
1164-
gt: 0
1165-
}
1186+
isPayment: true
11661187
}
11671188

11681189
if (buttonIds !== undefined && buttonIds.length > 0) {
@@ -1216,7 +1237,7 @@ export const getFilteredTransactionCount = async (
12161237
some: { userId }
12171238
}
12181239
},
1219-
amount: { gt: 0 }
1240+
isPayment: true
12201241
}
12211242
if (buttonIds !== undefined && buttonIds.length > 0) {
12221243
where.address!.paybuttons = {
@@ -1243,8 +1264,7 @@ export const fetchDistinctPaymentYearsByUser = async (userId: string): Promise<n
12431264
FROM Transaction t
12441265
JOIN Address a ON a.id = t.addressId
12451266
JOIN AddressesOnUserProfiles ap ON ap.addressId = a.id
1246-
WHERE ap.userId = ${userId} AND
1247-
t.amount > 0
1267+
WHERE ap.userId = ${userId}
12481268
ORDER BY year ASC
12491269
`
12501270

0 commit comments

Comments
 (0)