2026-06-02 16:47:09 -07:00
|
|
|
/*
|
|
|
|
|
Business logic for indexing blocks with Memo transactions.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import RetryQueue from '@chris.troutner/retry-queue'
|
2026-06-02 17:11:42 -07:00
|
|
|
import PQueue from 'p-queue'
|
|
|
|
|
import config from '../../config/index.js'
|
2026-06-02 16:47:09 -07:00
|
|
|
import FilterBlock from './filter-block.js'
|
|
|
|
|
import { findMemoOutputs, getSignerAddress } from '../lib/memo-parser.js'
|
2026-06-03 12:00:11 -07:00
|
|
|
import { logMemoTxResult } from '../lib/debug-log.js'
|
2026-06-02 16:47:09 -07:00
|
|
|
import { dispatchMemoAction } from './action-types/index.js'
|
|
|
|
|
|
|
|
|
|
class IndexBlocks {
|
|
|
|
|
constructor (localConfig = {}) {
|
|
|
|
|
if (!localConfig.adapters) {
|
|
|
|
|
throw new Error('Adapters required for index-blocks.js')
|
|
|
|
|
}
|
|
|
|
|
this.adapters = localConfig.adapters
|
|
|
|
|
this.filterBlock = new FilterBlock({ adapters: this.adapters })
|
|
|
|
|
this.retryQueue = new RetryQueue()
|
2026-06-02 17:11:42 -07:00
|
|
|
this.pQueue = new PQueue({ concurrency: config.memoTxConcurrency })
|
2026-06-02 16:47:09 -07:00
|
|
|
this.processBlock = this.processBlock.bind(this)
|
|
|
|
|
this.processMemoTx = this.processMemoTx.bind(this)
|
|
|
|
|
this.processMemoTxs = this.processMemoTxs.bind(this)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 12:00:11 -07:00
|
|
|
async getProcessErrorMessage (txid) {
|
|
|
|
|
try {
|
|
|
|
|
const record = await this.adapters.processErrorDb.get(txid)
|
|
|
|
|
return record && record.error ? record.error : null
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async processMemoTx (txid, blockHeight, seen = Date.now()) {
|
|
|
|
|
let actions = []
|
|
|
|
|
|
2026-06-02 16:47:09 -07:00
|
|
|
try {
|
|
|
|
|
try {
|
|
|
|
|
await this.adapters.ptxDb.get(txid)
|
2026-06-03 12:00:11 -07:00
|
|
|
logMemoTxResult({ txid, actions, skipped: true, success: true })
|
2026-06-02 16:47:09 -07:00
|
|
|
return true
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// not processed yet
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const txDetails = await this.adapters.transaction.get(txid)
|
2026-06-03 12:00:11 -07:00
|
|
|
const memoOutputs = findMemoOutputs(txDetails)
|
|
|
|
|
actions = memoOutputs.map((output) => output.action)
|
|
|
|
|
|
2026-06-02 16:47:09 -07:00
|
|
|
const signerAddr = getSignerAddress(txDetails)
|
|
|
|
|
if (!signerAddr) {
|
2026-06-03 12:00:11 -07:00
|
|
|
const error = 'could not find input address for memo tx'
|
2026-06-02 16:47:09 -07:00
|
|
|
await this.adapters.processErrorDb.create(txid, {
|
2026-06-03 12:00:11 -07:00
|
|
|
error,
|
2026-06-02 16:47:09 -07:00
|
|
|
ts: Date.now()
|
|
|
|
|
})
|
2026-06-03 12:00:11 -07:00
|
|
|
logMemoTxResult({ txid, actions, success: false, error })
|
2026-06-02 16:47:09 -07:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 12:00:11 -07:00
|
|
|
const processedAt = Date.now()
|
2026-06-02 16:47:09 -07:00
|
|
|
|
|
|
|
|
for (const decoded of memoOutputs) {
|
|
|
|
|
await dispatchMemoAction({
|
|
|
|
|
adapters: this.adapters,
|
|
|
|
|
txid,
|
|
|
|
|
txDetails,
|
|
|
|
|
signerAddr,
|
|
|
|
|
decoded,
|
|
|
|
|
seen,
|
|
|
|
|
blockHeight
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 12:00:11 -07:00
|
|
|
const handlerError = await this.getProcessErrorMessage(txid)
|
|
|
|
|
if (handlerError) {
|
|
|
|
|
logMemoTxResult({ txid, actions, success: false, error: handlerError })
|
|
|
|
|
await this.adapters.ptxDb.create(txid, { processedAt, blockHeight })
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.adapters.ptxDb.create(txid, { processedAt, blockHeight })
|
|
|
|
|
logMemoTxResult({ txid, actions, success: true })
|
2026-06-02 16:47:09 -07:00
|
|
|
return true
|
|
|
|
|
} catch (err) {
|
2026-06-03 12:00:11 -07:00
|
|
|
logMemoTxResult({ txid, actions, success: false, error: err.message })
|
2026-06-02 16:47:09 -07:00
|
|
|
console.error(`Error processing memo tx ${txid}:`, err.message)
|
|
|
|
|
throw err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 12:00:11 -07:00
|
|
|
async processMemoTxs (txids, blockHeight, seen) {
|
2026-06-02 17:11:42 -07:00
|
|
|
const tasks = txids.map((txid) => async () => {
|
2026-06-03 12:00:11 -07:00
|
|
|
await this.processMemoTx(txid, blockHeight, seen)
|
2026-06-02 17:11:42 -07:00
|
|
|
})
|
|
|
|
|
await this.pQueue.addAll(tasks)
|
2026-06-02 16:47:09 -07:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async processBlock (blockHeight) {
|
|
|
|
|
const blockHash = await this.retryQueue.addToQueue(
|
|
|
|
|
this.adapters.rpc.getBlockHash,
|
|
|
|
|
{ height: blockHeight }
|
|
|
|
|
)
|
|
|
|
|
const block = await this.retryQueue.addToQueue(
|
|
|
|
|
this.adapters.rpc.getBlock,
|
|
|
|
|
{ hash: blockHash }
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const txs = block.tx
|
|
|
|
|
const now = new Date()
|
|
|
|
|
console.log(
|
|
|
|
|
`\nIndexing block ${blockHeight} with ${txs.length} txs. ${now.toLocaleString()}`
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const memoTxs = await this.filterBlock.filterMemoTxs(txs)
|
|
|
|
|
if (memoTxs.length) {
|
|
|
|
|
console.log(`Memo txs in block: ${memoTxs.length}`)
|
2026-06-03 12:00:11 -07:00
|
|
|
const blockSeen = block.time * 1000
|
|
|
|
|
await this.processMemoTxs(memoTxs, blockHeight, blockSeen)
|
2026-06-02 16:47:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default IndexBlocks
|