Skip to content

Commit 070f7ba

Browse files
authored
Store the transactions inputs and outputs to the DB (#1110)
This will populate the `inputAddresses` field of the Transaction when calling `getAddressDetails` from the client (endpoint address/transactions/<address>). Note: I fixed a couple lint issues in the test that are unrelated but prevented me from committing the changes. Test Plan: yarn run docker test
1 parent 2c9f05a commit 070f7ba

5 files changed

Lines changed: 170 additions & 24 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
-- CreateTable
2+
CREATE TABLE `TransactionInput` (
3+
`id` VARCHAR(191) NOT NULL DEFAULT (uuid()),
4+
`transactionId` VARCHAR(191) NOT NULL,
5+
`addressId` VARCHAR(191) NOT NULL,
6+
`index` INTEGER NOT NULL,
7+
`amount` DECIMAL(24, 8) NOT NULL,
8+
9+
INDEX `TransactionInput_transactionId_idx`(`transactionId`),
10+
INDEX `TransactionInput_addressId_idx`(`addressId`),
11+
PRIMARY KEY (`id`)
12+
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
13+
14+
-- CreateTable
15+
CREATE TABLE `TransactionOutput` (
16+
`id` VARCHAR(191) NOT NULL DEFAULT (uuid()),
17+
`transactionId` VARCHAR(191) NOT NULL,
18+
`addressId` VARCHAR(191) NOT NULL,
19+
`index` INTEGER NOT NULL,
20+
`amount` DECIMAL(24, 8) NOT NULL,
21+
22+
INDEX `TransactionOutput_transactionId_idx`(`transactionId`),
23+
INDEX `TransactionOutput_addressId_idx`(`addressId`),
24+
PRIMARY KEY (`id`)
25+
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
26+
27+
-- AddForeignKey
28+
ALTER TABLE `TransactionInput` ADD CONSTRAINT `TransactionInput_transactionId_fkey` FOREIGN KEY (`transactionId`) REFERENCES `Transaction`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
29+
30+
-- AddForeignKey
31+
ALTER TABLE `TransactionInput` ADD CONSTRAINT `TransactionInput_addressId_fkey` FOREIGN KEY (`addressId`) REFERENCES `Address`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
32+
33+
-- AddForeignKey
34+
ALTER TABLE `TransactionOutput` ADD CONSTRAINT `TransactionOutput_transactionId_fkey` FOREIGN KEY (`transactionId`) REFERENCES `Transaction`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
35+
36+
-- AddForeignKey
37+
ALTER TABLE `TransactionOutput` ADD CONSTRAINT `TransactionOutput_addressId_fkey` FOREIGN KEY (`addressId`) REFERENCES `Address`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

prisma-local/schema.prisma

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@ datasource db {
1111
}
1212

1313
model Address {
14-
id String @id @default(dbgenerated("(uuid())"))
15-
address String @unique @db.VarChar(255)
16-
createdAt DateTime @default(now())
17-
updatedAt DateTime @updatedAt
18-
networkId Int
19-
network Network @relation(fields: [networkId], references: [id], onUpdate: Restrict)
20-
userProfiles AddressesOnUserProfiles[]
21-
lastSynced DateTime?
22-
syncing Boolean @default(false)
23-
paybuttons AddressesOnButtons[]
24-
transactions Transaction[]
25-
clientPayments ClientPayment[]
14+
id String @id @default(dbgenerated("(uuid())"))
15+
address String @unique @db.VarChar(255)
16+
createdAt DateTime @default(now())
17+
updatedAt DateTime @updatedAt
18+
networkId Int
19+
network Network @relation(fields: [networkId], references: [id], onUpdate: Restrict)
20+
userProfiles AddressesOnUserProfiles[]
21+
lastSynced DateTime?
22+
syncing Boolean @default(false)
23+
paybuttons AddressesOnButtons[]
24+
transactions Transaction[]
25+
clientPayments ClientPayment[]
26+
transactionInputs TransactionInput[]
27+
transactionOutputs TransactionOutput[]
2628
2729
@@index([networkId], map: "Address_networkId_fkey")
2830
}
@@ -77,7 +79,9 @@ model Transaction {
7779
opReturn String @db.LongText @default("")
7880
address Address @relation(fields: [addressId], references: [id], onDelete: Cascade, onUpdate: Cascade)
7981
prices PricesOnTransactions[]
80-
invoices Invoice[]
82+
invoices Invoice[]
83+
inputs TransactionInput[]
84+
outputs TransactionOutput[]
8185
8286
createdAt DateTime @default(now())
8387
updatedAt DateTime @updatedAt
@@ -86,6 +90,32 @@ model Transaction {
8690
@@index([addressId, timestamp], map: "Transaction_addressId_timestamp_idx")
8791
}
8892

93+
model TransactionInput {
94+
id String @id @default(dbgenerated("(uuid())"))
95+
transactionId String
96+
addressId String
97+
index Int
98+
transaction Transaction @relation(fields: [transactionId], references: [id], onDelete: Cascade)
99+
address Address @relation(fields: [addressId], references: [id], onDelete: Cascade)
100+
amount Decimal @db.Decimal(24, 8)
101+
102+
@@index([transactionId])
103+
@@index([addressId])
104+
}
105+
106+
model TransactionOutput {
107+
id String @id @default(dbgenerated("(uuid())"))
108+
transactionId String
109+
addressId String
110+
index Int
111+
transaction Transaction @relation(fields: [transactionId], references: [id], onDelete: Cascade)
112+
address Address @relation(fields: [addressId], references: [id], onDelete: Cascade)
113+
amount Decimal @db.Decimal(24, 8)
114+
115+
@@index([transactionId])
116+
@@index([addressId])
117+
}
118+
89119
model Wallet {
90120
id String @id @default(dbgenerated("(uuid())"))
91121
createdAt DateTime @default(now())

services/chronikService.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import {
2020
import { Address, Prisma, ClientPaymentStatus } from '@prisma/client'
2121
import xecaddr from 'xecaddrjs'
2222
import { getAddressPrefix, satoshisToUnit } from 'utils/index'
23-
import { fetchAddressesArray, fetchAllAddressesForNetworkId, getEarliestUnconfirmedTxTimestampForAddress, getLatestConfirmedTxTimestampForAddress, setSyncing, setSyncingBatch, updateLastSynced, updateManyLastSynced } from './addressService'
23+
import { fetchAddressesArray, fetchAllAddressesForNetworkId, getEarliestUnconfirmedTxTimestampForAddress, getLatestConfirmedTxTimestampForAddress, setSyncing, setSyncingBatch, updateLastSynced, updateManyLastSynced, upsertAddress } from './addressService'
2424
import * as ws from 'ws'
2525
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, parseError, parseOpReturnData } from 'utils/validators'
29+
import { OpReturnData, parseAddress, 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'
@@ -285,13 +285,51 @@ export class ChronikBlockchainClient {
285285

286286
private async getTransactionFromChronikTransaction (transaction: Tx, address: Address): Promise<Prisma.TransactionUncheckedCreateInput> {
287287
const { amount, opReturn } = await this.getTransactionAmountAndData(transaction, address.address)
288+
const inputAddresses = this.getSortedInputAddresses(transaction)
289+
const outputAddresses = this.getSortedOutputAddresses(transaction)
290+
291+
const uniqueAddressStrings = [...new Set([
292+
...inputAddresses.map(({ address: addr }) => addr),
293+
...outputAddresses.map(({ address: addr }) => addr)
294+
])]
295+
const addressIdMap = new Map<string, string>()
296+
await Promise.all(
297+
uniqueAddressStrings.map(async (addrStr) => {
298+
try {
299+
const parsed = parseAddress(addrStr)
300+
const addr = await upsertAddress(parsed)
301+
addressIdMap.set(parsed, addr.id)
302+
} catch {
303+
// Skip invalid addresses: don't upsert, don't add to map
304+
}
305+
})
306+
)
307+
308+
const getAddressId = (addr: string): string | undefined => {
309+
try {
310+
return addressIdMap.get(parseAddress(addr))
311+
} catch {
312+
return undefined
313+
}
314+
}
315+
288316
return {
289317
hash: transaction.txid,
290318
amount,
291319
timestamp: transaction.block !== undefined ? transaction.block.timestamp : transaction.timeFirstSeen,
292320
addressId: address.id,
293321
confirmed: transaction.block !== undefined,
294-
opReturn
322+
opReturn,
323+
inputs: {
324+
create: inputAddresses
325+
.map(({ address: addr, amount: amt }, i) => ({ addressId: getAddressId(addr), index: i, amount: amt }))
326+
.filter((item): item is { addressId: string, index: number, amount: Prisma.Decimal } => item.addressId !== undefined)
327+
},
328+
outputs: {
329+
create: outputAddresses
330+
.map(({ address: addr, amount: amt }, i) => ({ addressId: getAddressId(addr), index: i, amount: amt }))
331+
.filter((item): item is { addressId: string, index: number, amount: Prisma.Decimal } => item.addressId !== undefined)
332+
}
295333
}
296334
}
297335

services/transactionService.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export function getSimplifiedTrasaction (tx: TransactionWithAddressAndPrices, in
5454

5555
const parsedOpReturn = resolveOpReturn(opReturn)
5656

57+
const dbInputsArr = (tx as { inputs?: Array<{ address: { address: string }, amount: Prisma.Decimal }> }).inputs
58+
const dbOutputsArr = (tx as { outputs?: Array<{ address: { address: string }, amount: Prisma.Decimal }> }).outputs
59+
const resolvedInputAddresses = inputAddresses ?? (Array.isArray(dbInputsArr) ? dbInputsArr.map(i => ({ address: i.address.address, amount: i.amount })) : [])
60+
const resolvedOutputAddresses = outputAddresses ?? (Array.isArray(dbOutputsArr) ? dbOutputsArr.map(o => ({ address: o.address.address, amount: o.amount })) : [])
61+
5762
const simplifiedTransaction: SimplifiedTransaction = {
5863
hash,
5964
amount,
@@ -63,8 +68,8 @@ export function getSimplifiedTrasaction (tx: TransactionWithAddressAndPrices, in
6368
timestamp,
6469
message: parsedOpReturn?.message ?? '',
6570
rawMessage: parsedOpReturn?.rawMessage ?? '',
66-
inputAddresses: inputAddresses ?? [],
67-
outputAddresses: outputAddresses ?? [],
71+
inputAddresses: resolvedInputAddresses,
72+
outputAddresses: resolvedOutputAddresses,
6873
prices: tx.prices
6974
}
7075

@@ -90,7 +95,9 @@ const includePrices = {
9095

9196
const includeAddressAndPrices = {
9297
address: true,
93-
...includePrices
98+
...includePrices,
99+
inputs: { include: { address: true }, orderBy: { index: 'asc' as const } },
100+
outputs: { include: { address: true }, orderBy: { index: 'asc' as const } }
94101
}
95102

96103
const transactionWithPrices = Prisma.validator<Prisma.TransactionDefaultArgs>()(
@@ -129,7 +136,9 @@ const includePaybuttonsAndPrices = {
129136
}
130137
}
131138
},
132-
...includePrices
139+
...includePrices,
140+
inputs: { include: { address: true }, orderBy: { index: 'asc' as const } },
141+
outputs: { include: { address: true }, orderBy: { index: 'asc' as const } }
133142
}
134143
export const includePaybuttonsAndPricesAndInvoices = {
135144
...includePaybuttonsAndPrices,

tests/unittests/transactionService.test.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ const includePaybuttonsAndPrices = {
2626
}
2727
}
2828
},
29-
...includePrices
29+
...includePrices,
30+
inputs: { include: { address: true }, orderBy: { index: 'asc' as const } },
31+
outputs: { include: { address: true }, orderBy: { index: 'asc' as const } }
3032
}
3133

3234
describe('Create services', () => {
@@ -194,6 +196,37 @@ describe('Address object arrays (input/output) integration', () => {
194196
expect(simplified.inputAddresses).toEqual(inputs)
195197
expect(simplified.outputAddresses).toEqual(outputs)
196198
})
199+
200+
it('getSimplifiedTrasaction uses inputs/outputs from tx when not provided explicitly', () => {
201+
const inputsFromDb = [
202+
{ address: { address: 'ecash:qqinput1' }, amount: new Prisma.Decimal(1.23) },
203+
{ address: { address: 'ecash:qqinput2' }, amount: new Prisma.Decimal(4.56) }
204+
]
205+
const outputsFromDb = [
206+
{ address: { address: 'ecash:qqout1' }, amount: new Prisma.Decimal(7.89) },
207+
{ address: { address: 'ecash:qqout2' }, amount: new Prisma.Decimal(0.12) }
208+
]
209+
const tx: any = {
210+
hash: 'hash1',
211+
amount: new Prisma.Decimal(5),
212+
confirmed: true,
213+
opReturn: '',
214+
address: { address: 'ecash:qqprimaryaddressxxxxxxxxxxxxxxxxxxxxx' },
215+
timestamp: 1700000000,
216+
prices: mockedTransaction.prices,
217+
inputs: inputsFromDb,
218+
outputs: outputsFromDb
219+
}
220+
const simplified = transactionService.getSimplifiedTrasaction(tx)
221+
expect(simplified.inputAddresses).toEqual([
222+
{ address: 'ecash:qqinput1', amount: new Prisma.Decimal(1.23) },
223+
{ address: 'ecash:qqinput2', amount: new Prisma.Decimal(4.56) }
224+
])
225+
expect(simplified.outputAddresses).toEqual([
226+
{ address: 'ecash:qqout1', amount: new Prisma.Decimal(7.89) },
227+
{ address: 'ecash:qqout2', amount: new Prisma.Decimal(0.12) }
228+
])
229+
})
197230
})
198231

199232
describe('Date and timezone filters for transactions', () => {
@@ -206,7 +239,7 @@ describe('Date and timezone filters for transactions', () => {
206239
{ label: 'negative offset (Canada)', timezone: 'America/Toronto' }
207240
]
208241

209-
const computeExpectedRange = (tz: string) => {
242+
const computeExpectedRange = (tz: string): { gte: number, lte: number } => {
210243
const start = new Date(startDate)
211244
const end = new Date(endDate)
212245

@@ -234,7 +267,7 @@ describe('Date and timezone filters for transactions', () => {
234267
}
235268
}
236269

237-
const computeYearFilter = (year: number, tz: string) => {
270+
const computeYearFilter = (year: number, tz: string): { timestamp: { gte: number, lte: number } } => {
238271
const startDateObj = new Date(year, 0, 1, 0, 0, 0)
239272
const endDateObj = new Date(year, 11, 31, 23, 59, 59)
240273

@@ -424,4 +457,3 @@ describe('Date and timezone filters for transactions', () => {
424457
expect(callArgs.where.OR).toBeUndefined()
425458
})
426459
})
427-

0 commit comments

Comments
 (0)