diff --git a/packages/indexer-common/src/allocations/__tests__/tap.test.ts b/packages/indexer-common/src/allocations/__tests__/tap.test.ts index 4dd3ea4c9..b644ba528 100644 --- a/packages/indexer-common/src/allocations/__tests__/tap.test.ts +++ b/packages/indexer-common/src/allocations/__tests__/tap.test.ts @@ -4,9 +4,10 @@ import { GraphNode, Network, QueryFeeModels, - QueryResult, + TapSubgraphResponse, } from '@graphprotocol/indexer-common' import { + Address, connectDatabase, createLogger, createMetrics, @@ -15,7 +16,7 @@ import { toAddress, } from '@graphprotocol/common-ts' import { testNetworkSpecification } from '../../indexer-management/__tests__/util' -import { Sequelize } from 'sequelize' +import { Op, Sequelize } from 'sequelize' import { utils } from 'ethers' // Make global Jest variables available @@ -63,8 +64,17 @@ const setup = async () => { receiptCollector = network.receiptCollector } +const ALLOCATION_ID_1 = toAddress('edde47df40c29949a75a6693c77834c00b8ad626') +const ALLOCATION_ID_2 = toAddress('dead47df40c29949a75a6693c77834c00b8ad624') +const ALLOCATION_ID_3 = toAddress('6aea8894b5ab5a36cdc2d8be9290046801dd5fed') + +const SENDER_ADDRESS_1 = toAddress('ffcf8fdee72ac11b5c542428b35eef5769c409f0') +const SENDER_ADDRESS_2 = toAddress('dead47df40c29949a75a6693c77834c00b8ad624') +const SENDER_ADDRESS_3 = toAddress('6aea8894b5ab5a36cdc2d8be9290046801dd5fed') + +// last rav not redeemed const rav = { - allocationId: toAddress('edde47df40c29949a75a6693c77834c00b8ad626'), + allocationId: ALLOCATION_ID_1, last: true, final: false, timestampNs: 1709067401177959664n, @@ -73,15 +83,31 @@ const rav = { 'ede3f7ca5ace3629009f190bb51271f30c1aeaf565f82c25c447c7c9501f3ff31b628efcaf69138bf12960dd663924a692ee91f401785901848d8d7a639003ad1b', 'hex', ), - senderAddress: toAddress('ffcf8fdee72ac11b5c542428b35eef5769c409f0'), + senderAddress: SENDER_ADDRESS_1, redeemedAt: null, - createdAt: new Date(), - updatedAt: new Date(), } +const SIGNATURE = Buffer.from( + 'ede3f7ca5ace3629009f190bb51271f30c1aeaf565f82c25c447c7c9501f3ff31b628efcaf69138bf12960dd663924a692ee91f401785901848d8d7a639003ad1b', + 'hex', +) + const setupEach = async () => { sequelize = await sequelize.sync({ force: true }) await queryFeeModels.receiptAggregateVouchers.create(rav) + + jest + .spyOn(receiptCollector, 'findTransactionsForRavs') + .mockImplementation(async (): Promise => { + return { + transactions: [], + _meta: { + block: { + timestamp: Date.now(), + }, + }, + } + }) } const teardownEach = async () => { // Clear out query fee model tables @@ -125,25 +151,207 @@ describe('TAP', () => { timeout, ) + test('`revertRavsRedeemed` should revert RAV redeem status in DB only if older than subgraph last block', async () => { + // we have a redeemed non-final rav in our database + const nowSecs = Math.floor(Date.now() / 1000) + // redeemed rav but non-final + const ravList = [ + createLastNonFinalRav( + ALLOCATION_ID_3, + SENDER_ADDRESS_1, + new Date((nowSecs - 1) * 1000), + ), + createLastNonFinalRav(ALLOCATION_ID_3, SENDER_ADDRESS_2, new Date(nowSecs * 1000)), + createLastNonFinalRav( + ALLOCATION_ID_3, + SENDER_ADDRESS_3, + new Date((nowSecs + 1) * 1000), + ), + ] + + await queryFeeModels.receiptAggregateVouchers.bulkCreate(ravList) + + // it's not showing on the subgraph on a specific point in time + // the timestamp of the subgraph is greater than the receipt id + // should revert the rav + await receiptCollector['revertRavsRedeemed'](ravList, nowSecs - 1) + + let lastRedeemedRavs = await queryFeeModels.receiptAggregateVouchers.findAll({ + where: { + last: true, + final: false, + redeemedAt: { + [Op.ne]: null, + }, + }, + }) + expect(lastRedeemedRavs).toEqual([ + expect.objectContaining(ravList[0]), + expect.objectContaining(ravList[1]), + expect.objectContaining(ravList[2]), + ]) + + await receiptCollector['revertRavsRedeemed'](ravList, nowSecs) + + lastRedeemedRavs = await queryFeeModels.receiptAggregateVouchers.findAll({ + where: { + last: true, + final: false, + redeemedAt: { + [Op.ne]: null, + }, + }, + }) + expect(lastRedeemedRavs).toEqual([ + expect.objectContaining(ravList[1]), + expect.objectContaining(ravList[2]), + ]) + + await receiptCollector['revertRavsRedeemed'](ravList, nowSecs + 1) + + lastRedeemedRavs = await queryFeeModels.receiptAggregateVouchers.findAll({ + where: { + last: true, + final: false, + redeemedAt: { + [Op.ne]: null, + }, + }, + }) + expect(lastRedeemedRavs).toEqual([expect.objectContaining(ravList[2])]) + + await receiptCollector['revertRavsRedeemed'](ravList, nowSecs + 2) + + lastRedeemedRavs = await queryFeeModels.receiptAggregateVouchers.findAll({ + where: { + last: true, + final: false, + redeemedAt: { + [Op.ne]: null, + }, + }, + }) + expect(lastRedeemedRavs).toEqual([]) + }) + + test('revertRavsRedeemed` should not revert the RAV redeem status in DB if (allocation, sender) not in the revert list', async () => { + const nowSecs = Math.floor(Date.now() / 1000) + const ravList = [ + createLastNonFinalRav( + ALLOCATION_ID_3, + SENDER_ADDRESS_1, + new Date((nowSecs - 1) * 1000), + ), + createLastNonFinalRav(ALLOCATION_ID_3, SENDER_ADDRESS_2, new Date(nowSecs * 1000)), + createLastNonFinalRav( + ALLOCATION_ID_3, + SENDER_ADDRESS_3, + new Date((nowSecs + 1) * 1000), + ), + ] + + await queryFeeModels.receiptAggregateVouchers.bulkCreate(ravList) + + // it's showing on the subgraph on a specific point in time + await receiptCollector['revertRavsRedeemed']( + [ + { + allocationId: ALLOCATION_ID_1, + senderAddress: SENDER_ADDRESS_1, + }, + ], + nowSecs + 2, + ) + // the timestamp of the subgraph is greater than the receipt id + // should not revert the rav + + const lastRedeemedRavs = await queryFeeModels.receiptAggregateVouchers.findAll({ + where: { + last: true, + final: false, + redeemedAt: { + [Op.ne]: null, + }, + }, + }) + expect(lastRedeemedRavs).toEqual([ + expect.objectContaining(ravList[0]), + expect.objectContaining(ravList[1]), + expect.objectContaining(ravList[2]), + ]) + }) + + test('should mark ravs as final via `markRavsAsFinal`', async () => { + // we have a redeemed non-final rav in our database + const nowSecs = Math.floor(Date.now() / 1000) + // redeemed rav but non-final + const default_finality_time = 3600 + const ravList = [ + createLastNonFinalRav( + ALLOCATION_ID_3, + SENDER_ADDRESS_1, + new Date((nowSecs - default_finality_time - 1) * 1000), + ), + createLastNonFinalRav( + ALLOCATION_ID_3, + SENDER_ADDRESS_2, + new Date((nowSecs - default_finality_time) * 1000), + ), + createLastNonFinalRav( + ALLOCATION_ID_3, + SENDER_ADDRESS_3, + new Date((nowSecs - default_finality_time + 1) * 1000), + ), + ] + await queryFeeModels.receiptAggregateVouchers.bulkCreate(ravList) + + await receiptCollector['markRavsAsFinal'](nowSecs - 1) + + let finalRavs = await queryFeeModels.receiptAggregateVouchers.findAll({ + where: { last: true, final: true }, + }) + + expect(finalRavs).toEqual([]) + + await receiptCollector['markRavsAsFinal'](nowSecs) + finalRavs = await queryFeeModels.receiptAggregateVouchers.findAll({ + where: { last: true, final: true }, + }) + expect(finalRavs).toEqual([expect.objectContaining({ ...ravList[0], final: true })]) + + await receiptCollector['markRavsAsFinal'](nowSecs + 1) + finalRavs = await queryFeeModels.receiptAggregateVouchers.findAll({ + where: { last: true, final: true }, + }) + expect(finalRavs).toEqual([ + expect.objectContaining({ ...ravList[0], final: true }), + expect.objectContaining({ ...ravList[1], final: true }), + ]) + + await receiptCollector['markRavsAsFinal'](nowSecs + 2) + finalRavs = await queryFeeModels.receiptAggregateVouchers.findAll({ + where: { last: true, final: true }, + }) + expect(finalRavs).toEqual([ + expect.objectContaining({ ...ravList[0], final: true }), + expect.objectContaining({ ...ravList[1], final: true }), + expect.objectContaining({ ...ravList[2], final: true }), + ]) + }) test( 'test ignore final rav', async () => { const date = new Date() const redeemDate = date.setHours(date.getHours() - 2) const rav2 = { - allocationId: toAddress('dead47df40c29949a75a6693c77834c00b8ad624'), + allocationId: ALLOCATION_ID_2, last: true, final: true, timestampNs: 1709067401177959664n, valueAggregate: 20000000000000n, - signature: Buffer.from( - 'ede3f7ca5ace3629009f190bb51271f30c1aeaf565f82c25c447c7c9501f3ff31b628efcaf69138bf12960dd663924a692ee91f401785901848d8d7a639003ad1b', - 'hex', - ), - senderAddress: toAddress('deadbeefcafedeadceefcafedeadbeefcafedead'), + signature: SIGNATURE, + senderAddress: SENDER_ADDRESS_3, redeemedAt: new Date(redeemDate), - createdAt: new Date(), - updatedAt: new Date(), } await queryFeeModels.receiptAggregateVouchers.create(rav2) const ravs = await receiptCollector['pendingRAVs']() @@ -171,23 +379,21 @@ describe('TAP', () => { const date = new Date() const redeemDate = date.setHours(date.getHours() - 2) const rav2 = { - allocationId: toAddress('dead47df40c29949a75a6693c77834c00b8ad624'), + allocationId: ALLOCATION_ID_2, last: true, final: false, timestampNs: 1709067401177959664n, valueAggregate: 20000000000000n, - signature: Buffer.from( - 'ede3f7ca5ace3629009f190bb51271f30c1aeaf565f82c25c447c7c9501f3ff31b628efcaf69138bf12960dd663924a692ee91f401785901848d8d7a639003ad1b', - 'hex', - ), - senderAddress: toAddress('deadbeefcafedeadceefcafedeadbeefcafedead'), + signature: SIGNATURE, + senderAddress: SENDER_ADDRESS_3, redeemedAt: new Date(redeemDate), - createdAt: new Date(), - updatedAt: new Date(), } await queryFeeModels.receiptAggregateVouchers.create(rav2) - const ravs = await receiptCollector['pendingRAVs']() + + let ravs = await receiptCollector['pendingRAVs']() + ravs = await receiptCollector['filterAndUpdateRavs'](ravs) // The point is it will only return the rav that is not final + expect(ravs).toEqual([ expect.objectContaining({ allocationId: rav.allocationId, @@ -245,68 +451,72 @@ describe('TAP', () => { ) test( - 'test mark final rav', + 'test mark final rav via `filterAndUpdateRavs`', async () => { + const date = new Date() + const redeemDate = date.setHours(date.getHours() - 2) + const redeemDateSecs = Math.floor(redeemDate / 1000) + const nowSecs = Math.floor(Date.now() / 1000) const anotherFuncSpy = jest - .spyOn(receiptCollector.tapSubgraph!, 'query') - .mockImplementation(async (): Promise> => { + .spyOn(receiptCollector, 'findTransactionsForRavs') + .mockImplementation(async (): Promise => { return { - data: { - transactions: [ - { allocationID: '0xdead47df40c29949a75a6693c77834c00b8ad624' }, - ], + transactions: [ + { + allocationID: ALLOCATION_ID_2.toString().toLowerCase().replace('0x', ''), + timestamp: redeemDateSecs, + sender: { + id: SENDER_ADDRESS_3.toString().toLowerCase().replace('0x', ''), + }, + }, + ], + _meta: { + block: { + timestamp: nowSecs, + }, }, } }) - const date = new Date() - const redeemDate = date.setHours(date.getHours() - 2) const rav2 = { - allocationId: toAddress('dead47df40c29949a75a6693c77834c00b8ad624'), + allocationId: ALLOCATION_ID_2, last: true, final: false, timestampNs: 1709067401177959664n, valueAggregate: 20000000000000n, - signature: Buffer.from( - 'ede3f7ca5ace3629009f190bb51271f30c1aeaf565f82c25c447c7c9501f3ff31b628efcaf69138bf12960dd663924a692ee91f401785901848d8d7a639003ad1b', - 'hex', - ), - senderAddress: toAddress('deadbeefcafedeadceefcafedeadbeefcafedead'), + signature: SIGNATURE, + senderAddress: SENDER_ADDRESS_3, redeemedAt: new Date(redeemDate), - createdAt: new Date(), - updatedAt: new Date(), } await queryFeeModels.receiptAggregateVouchers.create(rav2) - const ravs = await receiptCollector['pendingRAVs']() + let ravs = await receiptCollector['pendingRAVs']() + ravs = await receiptCollector['filterAndUpdateRavs'](ravs) expect(anotherFuncSpy).toBeCalled() const finalRavs = await queryFeeModels.receiptAggregateVouchers.findAll({ where: { last: true, final: true }, }) //Final rav wont be returned here - expect(ravs).toEqual([ - expect.objectContaining({ - allocationId: rav.allocationId, - final: rav.final, - last: rav.last, - senderAddress: rav.senderAddress, - signature: rav.signature, - timestampNs: rav.timestampNs, - valueAggregate: rav.valueAggregate, - }), - ]) + expect(ravs).toEqual([expect.objectContaining(rav)]) // Expect final rav to be returned here - expect(finalRavs).toEqual([ - expect.objectContaining({ - allocationId: rav2.allocationId, - final: true, - last: rav2.last, - senderAddress: rav2.senderAddress, - signature: rav2.signature, - timestampNs: rav2.timestampNs, - valueAggregate: rav2.valueAggregate, - }), - ]) + expect(finalRavs).toEqual([expect.objectContaining({ ...rav2, final: true })]) }, timeout, ) }) + +function createLastNonFinalRav( + allocationId: Address, + senderAddress: Address, + redeemedAt: Date, +) { + return { + allocationId, + last: true, + final: false, + timestampNs: 1709067401177959664n, + valueAggregate: 20000000000000n, + signature: SIGNATURE, + senderAddress, + redeemedAt, + } +} diff --git a/packages/indexer-common/src/allocations/query-fees.ts b/packages/indexer-common/src/allocations/query-fees.ts index 9d9434671..a914b3d93 100644 --- a/packages/indexer-common/src/allocations/query-fees.ts +++ b/packages/indexer-common/src/allocations/query-fees.ts @@ -106,6 +106,21 @@ interface RavWithAllocation { sender: Address } +export interface TapSubgraphResponse { + transactions: { + allocationID: string + timestamp: number + sender: { + id: string + } + }[] + _meta: { + block: { + timestamp: number + } + } +} + export class AllocationReceiptCollector implements ReceiptCollector { declare logger: Logger declare metrics: ReceiptMetrics @@ -178,9 +193,13 @@ export class AllocationReceiptCollector implements ReceiptCollector { // flag during startup. collector.startReceiptCollecting() collector.startVoucherProcessing() - if (collector.tapContracts) { + if (collector.tapContracts && collector.tapSubgraph) { collector.logger.info(`RAV processing is initiated`) collector.startRAVProcessing() + } else { + collector.logger.info(`RAV process not initiated. + Tap Contracts: ${!!collector.tapContracts}. + Tap Subgraph: ${!!collector.tapSubgraph}.`) } await collector.queuePendingReceiptsFromDatabase() return collector @@ -462,13 +481,18 @@ export class AllocationReceiptCollector implements ReceiptCollector { timer: timer(30_000), }).tryMap( async () => { - const ravs = await this.pendingRAVs() + let ravs = await this.pendingRAVs() if (ravs.length === 0) { this.logger.info(`No pending RAVs to process`) return [] } + if (ravs.length > 0) { + ravs = await this.filterAndUpdateRavs(ravs) + } const allocations: Allocation[] = await this.getAllocationsfromAllocationIds(ravs) - this.logger.info(`Retrieved allocations for pending RAVs \n: ${allocations}`) + this.logger.info( + `Retrieved allocations for pending RAVs \n: ${JSON.stringify(allocations)}`, + ) return ravs .map((rav) => { const signedRav = rav.getSignedRAV() @@ -564,73 +588,165 @@ export class AllocationReceiptCollector implements ReceiptCollector { // redeem only if last is true // Later can add order and limit private async pendingRAVs(): Promise { - const unfinalizedRAVs = await this.models.receiptAggregateVouchers.findAll({ + return await this.models.receiptAggregateVouchers.findAll({ where: { last: true, final: false }, }) - // Obtain allocationIds to use as filter in subgraph - const unfinalizedRavsAllocationIds = unfinalizedRAVs.map((rav) => - rav.getSignedRAV().rav.allocationId.toLowerCase(), + } + + private async filterAndUpdateRavs( + ravsLastNotFinal: ReceiptAggregateVoucher[], + ): Promise { + const tapSubgraphResponse = await this.findTransactionsForRavs(ravsLastNotFinal) + + const redeemedRavsNotOnOurDatabase = tapSubgraphResponse.transactions.filter( + (tx) => + !ravsLastNotFinal.find( + (rav) => + toAddress(rav.senderAddress) === toAddress(tx.sender.id) && + toAddress(rav.allocationId) === toAddress(tx.allocationID), + ), ) - if (unfinalizedRavsAllocationIds.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let tapSubgraphResponse: any - if (!this.tapSubgraph) { - tapSubgraphResponse = { data: { transactions: [] } } - } else { - tapSubgraphResponse = await this.tapSubgraph!.query( - gql` - query transactions($unfinalizedRavsAllocationIds: [String!]!) { - transactions( - where: { type: "redeem", allocationID_in: $unfinalizedRavsAllocationIds } - ) { - allocationID - } - } - `, - { unfinalizedRavsAllocationIds }, + // for each transaction that is not redeemed on our database + // but was redeemed on the blockchain, update it to redeemed + if (redeemedRavsNotOnOurDatabase.length > 0) { + for (const rav of redeemedRavsNotOnOurDatabase) { + await this.markRavAsRedeemed( + toAddress(rav.allocationID), + toAddress(rav.sender.id), + rav.timestamp, ) } - const alreadyRedeemedAllocations = tapSubgraphResponse.data.transactions.map( - (transaction) => transaction.allocationID, - ) + } - // Filter unfinalized RAVS fetched from DB, keeping RAVs that have not yet been redeemed on-chain - const nonRedeemedAllocationIDAddresses = unfinalizedRavsAllocationIds.filter( - (allocationID) => !alreadyRedeemedAllocations.includes(allocationID), - ) - // Lowercase and remove '0x' prefix of addresses to match format in TAP DB Tables - const nonRedeemedAllocationIDsTrunc = nonRedeemedAllocationIDAddresses.map( - (allocationID) => allocationID.toLowerCase().replace('0x', ''), + // Filter unfinalized RAVS fetched from DB, keeping RAVs that have not yet been redeemed on-chain + const nonRedeemedRavs = ravsLastNotFinal + .filter((rav) => !!rav.redeemedAt) + .filter( + (rav) => + !tapSubgraphResponse.transactions.find( + (tx) => + toAddress(rav.senderAddress) === toAddress(tx.sender.id) && + toAddress(rav.allocationId) === toAddress(tx.allocationID), + ), ) - // Mark RAVs as unredeemed in DB if the TAP subgraph couldn't find the redeem Tx. - // To handle a chain reorg that "unredeemed" the RAVs. - // WE use sql directly due to a bug in sequelize update: - // https://github.com/sequelize/sequelize/issues/7664 (bug been open for 7 years no fix yet or ever) + // we use the subgraph timestamp to make decisions + // block timestamp minus 1 minute (because of blockchain timestamp uncertainty) + const ONE_MINUTE = 60 + const blockTimestampSecs = tapSubgraphResponse._meta.block.timestamp - ONE_MINUTE + + // Mark RAVs as unredeemed in DB if the TAP subgraph couldn't find the redeem Tx. + // To handle a chain reorg that "unredeemed" the RAVs. + if (nonRedeemedRavs.length > 0) { + await this.revertRavsRedeemed(nonRedeemedRavs, blockTimestampSecs) + } + + // For all RAVs that passed finality time, we mark it as final + await this.markRavsAsFinal(blockTimestampSecs) + + return await this.models.receiptAggregateVouchers.findAll({ + where: { redeemedAt: null, final: false, last: true }, + }) + } + + public async findTransactionsForRavs( + ravs: ReceiptAggregateVoucher[], + ): Promise { + const response = await this.tapSubgraph!.query( + gql` + query transactions( + $unfinalizedRavsAllocationIds: [String!]! + $senderAddresses: [String!]! + ) { + transactions( + where: { + type: "redeem" + allocationID_in: $unfinalizedRavsAllocationIds + sender_: { id_in: $senderAddresses } + } + ) { + allocationID + timestamp + sender { + id + } + } + _meta { + block { + timestamp + } + } + } + `, + { + unfinalizedRavsAllocationIds: ravs.map((value) => + toAddress(value.allocationId).toLowerCase(), + ), + senderAddresses: ravs.map((value) => + toAddress(value.senderAddress).toLowerCase(), + ), + }, + ) + if (!response.data) { + throw `There was an error while querying Tap Subgraph. Errors: ${response.error}` + } + + return response.data + } + + // for every allocation_id of this list that contains the redeemedAt less than the current + // subgraph timestamp + private async revertRavsRedeemed( + ravsNotRedeemed: { allocationId: Address; senderAddress: Address }[], + blockTimestampSecs: number, + ) { + if (ravsNotRedeemed.length == 0) { + return + } - let query = ` + // WE use sql directly due to a bug in sequelize update: + // https://github.com/sequelize/sequelize/issues/7664 (bug been open for 7 years no fix yet or ever) + const query = ` UPDATE scalar_tap_ravs SET redeemed_at = NULL - WHERE allocation_id IN ('${nonRedeemedAllocationIDsTrunc.join("', '")}') + WHERE (allocation_id::char(40), sender_address::char(40)) IN (VALUES ${ravsNotRedeemed + .map( + (rav) => + `('${rav.allocationId + .toString() + .toLowerCase() + .replace('0x', '')}'::char(40), '${rav.senderAddress + .toString() + .toLowerCase() + .replace('0x', '')}'::char(40))`, + ) + .join(', ')}) + AND redeemed_at < to_timestamp(${blockTimestampSecs}) ` - await this.models.receiptAggregateVouchers.sequelize?.query(query) - // // Update those that redeemed_at is older than 60 minutes and mark as final - query = ` + await this.models.receiptAggregateVouchers.sequelize?.query(query) + + this.logger.warn( + `Reverted Redeemed RAVs: ${ravsNotRedeemed + .map((rav) => `(${rav.senderAddress},${rav.allocationId})`) + .join(', ')}`, + ) + } + + // we use blockTimestamp instead of NOW() because we must be older than + // the subgraph timestamp + private async markRavsAsFinal(blockTimestampSecs: number) { + const query = ` UPDATE scalar_tap_ravs SET final = TRUE - WHERE last = TRUE AND final = FALSE - AND redeemed_at < NOW() - INTERVAL '${this.finalityTime} second' + WHERE last = TRUE + AND final = FALSE AND redeemed_at IS NOT NULL + AND redeemed_at < to_timestamp(${blockTimestampSecs - this.finalityTime}) ` - await this.models.receiptAggregateVouchers.sequelize?.query(query) - return await this.models.receiptAggregateVouchers.findAll({ - where: { redeemedAt: null, final: false, last: true }, - }) - } - return [] + await this.models.receiptAggregateVouchers.sequelize?.query(query) } private encodeReceiptBatch(receipts: AllocationReceipt[]): BytesWriter { @@ -942,18 +1058,9 @@ export class AllocationReceiptCollector implements ReceiptCollector { ) try { - const addressWithoutPrefix = rav.allocationId.toLowerCase().replace('0x', '') - // WE use sql directly due to a bug in sequelize update: - // https://github.com/sequelize/sequelize/issues/7664 (bug been open for 7 years no fix yet or ever) - const query = ` - UPDATE scalar_tap_ravs - SET redeemed_at = NOW() - WHERE allocation_id = '${addressWithoutPrefix}' - ` - await this.models.receiptAggregateVouchers.sequelize?.query(query) - + await this.markRavAsRedeemed(toAddress(rav.allocationId), sender) logger.info( - `Updated receipt aggregate vouchers table with redeemed_at for allocation ${addressWithoutPrefix}`, + `Updated receipt aggregate vouchers table with redeemed_at for allocation ${rav.allocationId} and sender ${sender}`, ) } catch (err) { logger.warn( @@ -1005,6 +1112,29 @@ export class AllocationReceiptCollector implements ReceiptCollector { ) } + private async markRavAsRedeemed( + allocationId: Address, + senderAddress: Address, + timestamp?: number, + ) { + // WE use sql directly due to a bug in sequelize update: + // https://github.com/sequelize/sequelize/issues/7664 (bug been open for 7 years no fix yet or ever) + const query = ` + UPDATE scalar_tap_ravs + SET redeemed_at = ${timestamp ? timestamp : 'NOW()'} + WHERE allocation_id = '${allocationId + .toString() + .toLowerCase() + .replace('0x', '')}' + AND sender_address = '${senderAddress + .toString() + .toLowerCase() + .replace('0x', '')}' + ` + + await this.models.receiptAggregateVouchers.sequelize?.query(query) + } + public async queuePendingReceiptsFromDatabase(): Promise { // Obtain all closed allocations const closedAllocations = await this.models.allocationSummaries.findAll({