Skip to content

Commit 5fec2a5

Browse files
committed
Optimize getTransactionFromChronikTransaction
This is a 2-step optimization: - remove unneeded async in satoshisToUnit() callsites, this reduce locks - optimize the inputs and outputs processing by avoiding looping several times, avoid copying arrays and doing redundant formatting/checks. This also fixes a logging error and a loop exit condition that could potentially cause an infinite loop. Inspired by 6298e84 and c4cb339. This is a ~500x improvement on my machine.
1 parent 707f221 commit 5fec2a5

3 files changed

Lines changed: 43 additions & 48 deletions

File tree

pages/api/address/balance/[address].ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default async (req: NextApiRequest, res: NextApiResponse): Promise<void>
1717
try {
1818
const address = parseAddress(req.query.address as string)
1919
const response = await multiBlockchainClient.getBalance(address)
20-
const balance = await satoshisToUnit(response, xecaddr.detectAddressFormat(address))
20+
const balance = satoshisToUnit(response, xecaddr.detectAddressFormat(address))
2121
res.status(200).send(balance)
2222
} catch (err: any) {
2323
switch (err.message) {

services/chronikService.ts

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { BroadcastTxData } from 'ws-service/types'
2626
import config from 'config'
2727
import io, { Socket } from 'socket.io-client'
2828
import moment from 'moment'
29-
import { OpReturnData, parseAddress, parseError, parseOpReturnData } from 'utils/validators'
29+
import { OpReturnData, parseError, parseOpReturnData } from 'utils/validators'
3030
import { executeAddressTriggers, executeTriggersBatch } from './triggerService'
3131
import { appendTxsToFile } from 'prisma-local/seeds/transactions'
3232
import { PHASE_PRODUCTION_BUILD } from 'next/dist/shared/lib/constants'
@@ -251,7 +251,7 @@ export class ChronikBlockchainClient {
251251
}
252252
}
253253

254-
private async getTransactionAmountAndData (transaction: Tx, addressString: string): Promise<{amount: Prisma.Decimal, opReturn: string}> {
254+
private getTransactionAmountAndData (transaction: Tx, addressString: string): {amount: Prisma.Decimal, opReturn: string} {
255255
let totalOutput = 0n
256256
let totalInput = 0n
257257
const addressFormat = xecaddr.detectAddressFormat(addressString)
@@ -271,31 +271,27 @@ export class ChronikBlockchainClient {
271271
}
272272
}
273273
}
274+
274275
for (const input of transaction.inputs) {
275276
if (input?.outputScript?.includes(script) === true) {
276277
totalInput += input.sats
277278
}
278279
}
280+
279281
const satoshis = totalOutput - totalInput
282+
const amount = satoshisToUnit(satoshis, addressFormat)
283+
280284
return {
281-
amount: await satoshisToUnit(satoshis, addressFormat),
285+
amount,
282286
opReturn
283287
}
284288
}
285289

286-
private async getTransactionFromChronikTransaction (transaction: Tx, address: Address): Promise<Prisma.TransactionUncheckedCreateInput> {
287-
const { amount, opReturn } = await this.getTransactionAmountAndData(transaction, address.address)
290+
private getTransactionFromChronikTransaction (transaction: Tx, address: Address): Prisma.TransactionUncheckedCreateInput {
291+
const { amount, opReturn } = this.getTransactionAmountAndData(transaction, address.address)
288292
const inputAddresses = this.getSortedInputAddresses(transaction)
289293
const outputAddresses = this.getSortedOutputAddresses(transaction)
290294

291-
const parseAddressString = (addr: string): string | undefined => {
292-
try {
293-
return parseAddress(addr)
294-
} catch {
295-
return undefined
296-
}
297-
}
298-
299295
return {
300296
hash: transaction.txid,
301297
amount,
@@ -305,13 +301,9 @@ export class ChronikBlockchainClient {
305301
opReturn,
306302
inputs: {
307303
create: inputAddresses
308-
.map(({ address: addr, amount: amt }, i) => ({ address: parseAddressString(addr), index: i, amount: amt }))
309-
.filter((item): item is { address: string, index: number, amount: Prisma.Decimal } => item.address !== undefined)
310304
},
311305
outputs: {
312306
create: outputAddresses
313-
.map(({ address: addr, amount: amt }, i) => ({ address: parseAddressString(addr), index: i, amount: amt }))
314-
.filter((item): item is { address: string, index: number, amount: Prisma.Decimal } => item.address !== undefined)
315307
}
316308
}
317309
}
@@ -367,11 +359,15 @@ export class ChronikBlockchainClient {
367359
pageTxs = []
368360
}
369361

370-
if (pageTxs.length === 0) {
362+
if (pageIndex === 0 && pageTxs.length === 0) {
371363
console.log(`${addrLogPrefix} EMPTY ADDRESS`)
372364
break
373365
}
374366

367+
if (pageTxs.length < CHRONIK_FETCH_N_TXS_PER_PAGE) {
368+
hasReachedStoppingCondition = true
369+
}
370+
375371
const newestTs = Number(pageTxs[0].block?.timestamp ?? pageTxs[0].timeFirstSeen)
376372

377373
if (newestTs < lastSyncedTimestampSeconds) {
@@ -455,9 +451,7 @@ export class ChronikBlockchainClient {
455451

456452
page += 1
457453

458-
const transactionsToPersist = await Promise.all(
459-
[...confirmedTransactions, ...unconfirmedTransactions].map(async tx => await this.getTransactionFromChronikTransaction(tx, address))
460-
)
454+
const transactionsToPersist = [...confirmedTransactions, ...unconfirmedTransactions].map(tx => this.getTransactionFromChronikTransaction(tx, address))
461455
const persistedTransactions = await createManyTransactions(transactionsToPersist)
462456
if (persistedTransactions.length > 0) {
463457
const simplifiedTransactions = getSimplifiedTransactions(persistedTransactions)
@@ -532,7 +526,7 @@ export class ChronikBlockchainClient {
532526
}
533527
}
534528

535-
private getSortedInputAddresses (transaction: Tx): Array<{address: string, amount: Prisma.Decimal}> {
529+
private getSortedInputAddresses (transaction: Tx): Array<{address: string, index: number, amount: Prisma.Decimal}> {
536530
const addressSatsMap = new Map<string, bigint>()
537531
transaction.inputs.forEach((inp) => {
538532
const address = outputScriptToAddress(this.networkSlug, inp.outputScript)
@@ -544,16 +538,18 @@ export class ChronikBlockchainClient {
544538
const unitDivisor = this.networkId === XEC_NETWORK_ID
545539
? 1e2
546540
: (this.networkId === BCH_NETWORK_ID ? 1e8 : 1)
547-
const sortedInputAddresses = Array.from(addressSatsMap.entries())
548-
.sort(([, valueA], [, valueB]) => Number(valueB - valueA))
549-
return sortedInputAddresses.map(([address, sats]) => {
541+
const result: Array<{address: string, index: number, amount: Prisma.Decimal}> = []
542+
let index = 0
543+
for (const [address, sats] of addressSatsMap.entries()) {
550544
const decimal = new Prisma.Decimal(sats.toString())
551545
const amount = decimal.dividedBy(unitDivisor)
552-
return { address, amount }
553-
})
546+
result.push({ address, index, amount })
547+
index++
548+
}
549+
return result
554550
}
555551

556-
private getSortedOutputAddresses (transaction: Tx): Array<{address: string, amount: Prisma.Decimal}> {
552+
private getSortedOutputAddresses (transaction: Tx): Array<{address: string, index: number, amount: Prisma.Decimal}> {
557553
const addressSatsMap = new Map<string, bigint>()
558554
transaction.outputs.forEach((out) => {
559555
const address = outputScriptToAddress(this.networkSlug, out.outputScript)
@@ -565,14 +561,15 @@ export class ChronikBlockchainClient {
565561
const unitDivisor = this.networkId === XEC_NETWORK_ID
566562
? 1e2
567563
: (this.networkId === BCH_NETWORK_ID ? 1e8 : 1)
568-
const sortedOutputAddresses = Array.from(addressSatsMap.entries())
569-
.sort(([, valueA], [, valueB]) => Number(valueB - valueA))
570-
.map(([address, sats]) => {
571-
const decimal = new Prisma.Decimal(sats.toString())
572-
const amount = decimal.dividedBy(unitDivisor)
573-
return { address, amount }
574-
})
575-
return sortedOutputAddresses
564+
const result: Array<{address: string, index: number, amount: Prisma.Decimal}> = []
565+
let index = 0
566+
for (const [address, sats] of addressSatsMap.entries()) {
567+
const decimal = new Prisma.Decimal(sats.toString())
568+
const amount = decimal.dividedBy(unitDivisor)
569+
result.push({ address, index, amount })
570+
index++
571+
}
572+
return result
576573
}
577574

578575
public async waitForSyncing (txId: string, addressStringArray: string[]): Promise<void> {
@@ -764,14 +761,14 @@ export class ChronikBlockchainClient {
764761
private async getAddressesForTransaction (transaction: Tx): Promise<AddressWithTransaction[]> {
765762
const relatedAddresses = this.getRelatedAddressesForTransaction(transaction)
766763
const addressesFromStringArray = await fetchAddressesArray(relatedAddresses)
767-
const addressesWithTransactions: AddressWithTransaction[] = await Promise.all(addressesFromStringArray.map(
768-
async address => {
764+
const addressesWithTransactions: AddressWithTransaction[] = addressesFromStringArray.map(
765+
address => {
769766
return {
770767
address,
771-
transaction: await this.getTransactionFromChronikTransaction(transaction, address)
768+
transaction: this.getTransactionFromChronikTransaction(transaction, address)
772769
}
773770
}
774-
))
771+
)
775772
const zero = new Prisma.Decimal(0)
776773
return addressesWithTransactions.filter(
777774
addressWithTransaction => !(zero.equals(addressWithTransaction.transaction.amount as Prisma.Decimal))
@@ -835,12 +832,10 @@ export class ChronikBlockchainClient {
835832
const involvedAddrIds = new Set(batch.chronikTxs.map(({ address }) => address.id))
836833

837834
try {
838-
const pairsFromBatch: RowWithRaw[] = await Promise.all(
839-
batch.chronikTxs.map(async ({ tx, address }) => {
840-
const row = await this.getTransactionFromChronikTransaction(tx, address)
841-
return { row, raw: tx }
842-
})
843-
)
835+
const pairsFromBatch: RowWithRaw[] = batch.chronikTxs.map(({ tx, address }) => {
836+
const row = this.getTransactionFromChronikTransaction(tx, address)
837+
return { row, raw: tx }
838+
})
844839

845840
for (const { row } of pairsFromBatch) {
846841
perAddrCount.set(row.addressId, (perAddrCount.get(row.addressId) ?? 0) + 1)

utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const getAddressPrefixed = function (addressString: string): string {
4949
return `${getAddressPrefix(addressString)}:${removeAddressPrefix(addressString)}`
5050
}
5151

52-
export async function satoshisToUnit (satoshis: bigint, networkFormat: string): Promise<Prisma.Decimal> {
52+
export function satoshisToUnit (satoshis: bigint, networkFormat: string): Prisma.Decimal {
5353
const decimal = new Prisma.Decimal(satoshis.toString())
5454
if (networkFormat === xecaddr.Format.Xecaddr) {
5555
return decimal.dividedBy(1e2)

0 commit comments

Comments
 (0)