UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

244 lines (219 loc) 7.81 kB
import { GetMerklePathResult } from '../../sdk' import { EntityProvenTx, EntityProvenTxReq } from '../../storage/schema/entities' import { TableProvenTxReq } from '../../storage/schema/tables' import { doubleSha256BE } from '../../utility/utilityHelpers' import { asString } from '../../utility/utilityHelpers.noBuffer' import { Monitor } from '../Monitor' import { WalletMonitorTask } from './WalletMonitorTask' /** * `TaskCheckForProofs` is a WalletMonitor task that retreives merkle proofs for * transactions. * * It is normally triggered by the Chaintracks new block header event. * * When a new block is found, cwi-external-services are used to obtain proofs for * any transactions that are currently in the 'unmined' or 'unknown' state. * * If a proof is obtained and validated, a new ProvenTx record is created and * the original ProvenTxReq status is advanced to 'notifying'. */ export class TaskCheckForProofs extends WalletMonitorTask { static taskName = 'CheckForProofs' /** * An external service such as the chaintracks new block header * listener can set this true to cause */ static checkNow = false constructor( monitor: Monitor, public triggerMsecs = 0 ) { super(monitor, TaskCheckForProofs.taskName) } /** * Normally triggered by checkNow getting set by new block header found event from chaintracks */ trigger(nowMsecsSinceEpoch: number): { run: boolean } { return { run: TaskCheckForProofs.checkNow || (this.triggerMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerMsecs) } } async runTask(): Promise<string> { let log = '' const countsAsAttempt = TaskCheckForProofs.checkNow TaskCheckForProofs.checkNow = false const maxAcceptableHeight = this.monitor.lastNewHeader?.height if (maxAcceptableHeight === undefined) { return log } const limit = 100 let offset = 0 for (;;) { const reqs = await this.storage.findProvenTxReqs({ partial: {}, status: ['callback', 'unmined', 'sending', 'unknown', 'unconfirmed'], paged: { limit, offset } }) if (reqs.length === 0) break log += `${reqs.length} reqs with status 'callback', 'unmined', 'sending', 'unknown', or 'unconfirmed'\n` const r = await getProofs(this, reqs, 2, countsAsAttempt, false, maxAcceptableHeight) log += `${r.log}\n` //console.log(log); if (reqs.length < limit) break offset += limit } return log } } /** * Process an array of table.ProvenTxReq (typically with status 'unmined' or 'unknown') * * If req is invalid, set status 'invalid' * * Verify the requests are valid, lookup proofs or updated transaction status using the array of getProofServices, * * When proofs are found, create new ProvenTxApi records and transition the requests' status to 'unconfirmed' or 'notifying', * depending on chaintracks succeeding on proof verification. * * Increments attempts if proofs where requested. * * @param reqs * @returns reqs partitioned by status */ export async function getProofs( task: WalletMonitorTask, reqs: TableProvenTxReq[], indent = 0, countsAsAttempt = false, ignoreStatus = false, maxAcceptableHeight: number ): Promise<{ proven: TableProvenTxReq[] invalid: TableProvenTxReq[] log: string }> { const proven: TableProvenTxReq[] = [] const invalid: TableProvenTxReq[] = [] let log = '' for (const reqApi of reqs) { log += ' '.repeat(indent) log += `reqId ${reqApi.provenTxReqId} txid ${reqApi.txid}: ` if ( !ignoreStatus && reqApi.status !== 'callback' && reqApi.status !== 'unmined' && reqApi.status !== 'unknown' && reqApi.status !== 'unconfirmed' && reqApi.status !== 'nosend' && reqApi.status !== 'sending' ) { log += `status of '${reqApi.status}' is not ready to be proven.\n` continue } const req = new EntityProvenTxReq(reqApi) if (Number.isInteger(req.provenTxId)) { log += `Already linked to provenTxId ${req.provenTxId}.\n` req.notified = false req.status = 'completed' await req.updateStorageDynamicProperties(task.storage) proven.push(reqApi) continue } log += '\n' let reqIsValid = false if (req.rawTx) { const txid = asString(doubleSha256BE(req.rawTx)) if (txid === req.txid) reqIsValid = true } if (!reqIsValid) { log += ` rawTx doesn't hash to txid. status => invalid.\n` req.notified = false req.status = 'invalid' await req.updateStorageDynamicProperties(task.storage) invalid.push(reqApi) continue } const limit = task.monitor.chain === 'main' ? task.monitor.options.unprovenAttemptsLimitMain : task.monitor.options.unprovenAttemptsLimitTest if (!ignoreStatus && req.attempts > limit) { log += ` too many failed attempts ${req.attempts}\n` req.notified = false req.status = 'invalid' await req.updateStorageDynamicProperties(task.storage) invalid.push(reqApi) continue } const since = new Date() let r: GetMerklePathResult let ptx: EntityProvenTx | undefined // External services will try multiple providers until one returns a proof, // or they all fail. // There may also be an array of proofs to consider when a transaction // is recently mined and appears in orphan blocks in addition to active chain blocks. // Since orphan blocks can end up on chain again, multiple proofs has value. // // On failure, there may be a mapi response, or an error. // // The proofs returned are considered sequentially, validating and chaintracks confirming. // // If a good proof is found, proceed to using it. // // When all received proofs fail, force a bump to the next service provider and try // one more time. // r = await task.monitor.services.getMerklePath(req.txid) if (r.header && r.header.height > maxAcceptableHeight) { // Ignore proofs from bleeding edge of new blocks as these are the most often re-orged. log += ` ignoring possible proof from very new block at height ${r.header.height} ${r.header.hash}\n` continue } ptx = await EntityProvenTx.fromReq(req, r, countsAsAttempt && req.status !== 'nosend') if (ptx) { // We have a merklePath proof for the request (and a block header) await req.updateStorageDynamicProperties(task.storage) await req.refreshFromStorage(task.storage) const { provenTxReqId, status, txid, attempts, history } = req.toApi() const { index, height, blockHash, merklePath, merkleRoot } = ptx.toApi() const r = await task.storage.runAsStorageProvider(async sp => { return await sp.updateProvenTxReqWithNewProvenTx({ provenTxReqId, status, txid, attempts, history, index, height, blockHash, merklePath, merkleRoot }) }) req.status = r.status req.apiHistory = r.history req.provenTxId = r.provenTxId req.notified = true task.monitor.callOnProvenTransaction({ txid, txIndex: index, blockHeight: height, blockHash, merklePath, merkleRoot }) } else { if (countsAsAttempt && req.status !== 'nosend') { req.attempts++ } } await req.updateStorageDynamicProperties(task.storage) await req.refreshFromStorage(task.storage) log += req.historyPretty(since, indent + 2) + '\n' if (req.status === 'completed') proven.push(req.api) if (req.status === 'invalid') invalid.push(req.api) } return { proven, invalid, log } }