@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
152 lines (139 loc) • 5.17 kB
text/typescript
import { Transaction, Utils } from '@bsv/sdk'
import { Monitor } from '../Monitor'
import { WalletMonitorTask } from './WalletMonitorTask'
import { TableProvenTxReq } from '../../storage/schema/tables'
import { EntityProvenTxReq } from '../../storage/schema/entities'
import { StorageProvider } from '../../index.client'
/**
* Setting provenTxReq status to 'unfail' when 'invalid' will attempt to find a merklePath, and if successful:
*
* 1. set the req status to 'unmined'
* 2. set the referenced txs to 'unproven'
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
* 4. set the txs outputs to spendable
*
* If it fails (to find a merklePath), returns the req status to 'invalid'.
*/
export class TaskUnFail extends WalletMonitorTask {
static taskName = 'UnFail'
/**
* Set to true to trigger running this task
*/
static checkNow = false
constructor(
monitor: Monitor,
public triggerMsecs = monitor.oneMinute * 10
) {
super(monitor, TaskUnFail.taskName)
}
trigger(nowMsecsSinceEpoch: number): { run: boolean } {
return {
run:
TaskUnFail.checkNow ||
(this.triggerMsecs > 0 && nowMsecsSinceEpoch - this.lastRunMsecsSinceEpoch > this.triggerMsecs)
}
}
async runTask(): Promise<string> {
let log = ''
TaskUnFail.checkNow = false
const limit = 100
let offset = 0
for (;;) {
const reqs = await this.storage.findProvenTxReqs({
partial: {},
status: ['unfail'],
paged: { limit, offset }
})
if (reqs.length === 0) break
log += `${reqs.length} reqs with status 'unfail'\n`
const r = await this.unfail(reqs, 2)
log += `${r.log}\n`
//console.log(log);
if (reqs.length < limit) break
offset += limit
}
return log
}
async unfail(reqs: TableProvenTxReq[], indent = 0): Promise<{ log: string }> {
let log = ''
for (const reqApi of reqs) {
const req = new EntityProvenTxReq(reqApi)
log += ' '.repeat(indent)
log += `reqId ${reqApi.provenTxReqId} txid ${reqApi.txid}: `
const r = await this.monitor.services.getMerklePath(req.txid)
if (r.merklePath) {
// 1. set the req status to 'unmined'
req.status = 'unmined'
req.attempts = 0
log += `unfailed. status is now 'unmined'\n`
log += await this.unfailReq(req, indent + 2)
} else {
req.status = 'invalid'
log += `returned to status 'invalid'\n`
}
await req.updateStorageDynamicProperties(this.storage)
}
return { log }
}
/**
* 2. set the referenced txs to 'unproven'
* 3. determine if any inputs match user's existing outputs and if so update spentBy and spendable of those outputs.
* 4. set the txs outputs to spendable
*
* @param req
* @param indent
* @returns
*/
async unfailReq(req: EntityProvenTxReq, indent: number): Promise<string> {
let log = ''
const storage = this.storage
const services = this.monitor.services
const txIds = req.notify.transactionIds || []
for (const id of txIds) {
const bsvtx = Transaction.fromBinary(req.rawTx)
await this.storage.runAsStorageProvider(async sp => {
const spk = sp as StorageProvider
const tx = await sp.findTransactionById(id, undefined, true)
if (!tx) {
log += ' '.repeat(indent) + `transaction ${id} was not found\n`
return
}
await sp.updateTransaction(tx.transactionId, { status: 'unproven' })
tx.status = 'unproven'
log += ' '.repeat(indent) + `transaction ${id} status is now 'unproven'\n`
let vin = -1
for (const bi of bsvtx.inputs) {
vin++
const is = await sp.findOutputs({
partial: { userId: tx.userId, txid: bi.sourceTXID!, vout: bi.sourceOutputIndex }
})
if (is.length !== 1) {
log += ' '.repeat(indent + 2) + `input ${vin} not matched to user's outputs\n`
} else {
const oi = is[0]
log +=
' '.repeat(indent + 2) +
`input ${vin} matched to output ${oi.outputId} updated spentBy ${tx.transactionId}\n`
await sp.updateOutput(oi.outputId, { spendable: false, spentBy: tx.transactionId })
}
}
const outputs = await sp.findOutputs({ partial: { userId: tx.userId, transactionId: tx.transactionId } })
for (const o of outputs) {
await spk.validateOutputScript(o)
if (!o.lockingScript) {
log += ' '.repeat(indent + 2) + `output ${o.outputId} does not have a valid locking script\n`
} else {
const isUtxo = await services.isUtxo(o)
if (isUtxo !== o.spendable) {
log += ' '.repeat(indent + 2) + `output ${o.outputId} set to ${isUtxo ? 'spendable' : 'spent'}\n`
await sp.updateOutput(o.outputId, { spendable: isUtxo })
} else {
log += ' '.repeat(indent + 2) + `output ${o.outputId} unchanged\n`
}
}
}
})
}
return log
}
}