UNPKG

bch-slpjs

Version:

Simple Ledger Protocol (SLP) JavaScript Library

217 lines (196 loc) 11.8 kB
import BITBOX from 'bitbox-sdk/lib/bitbox-sdk'; import { SlpValidator, Slp } from './slp'; import { SlpTransactionType, SlpTransactionDetails } from './slpjs'; import * as bitcore from 'bitcore-lib-cash'; import { BitcoreTransaction } from './global'; import BigNumber from 'bignumber.js'; export interface Validation { hex: string|null; validity: boolean|null; parents: Parent[], details: SlpTransactionDetails|null, invalidReason: string|null } export type GetRawTransactionsAsync = (txid: string[]) => Promise<string[]|null>; const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) interface Parent { txid: string; versionType: number; valid: boolean|null; inputQty: BigNumber|null; } export class LocalValidator implements SlpValidator { BITBOX: BITBOX; cachedRawTransactions: { [txid: string]: string } cachedValidations: { [txid: string]: Validation } getRawTransactions: GetRawTransactionsAsync; slp: Slp; constructor(BITBOX: BITBOX, getRawTransactions: GetRawTransactionsAsync) { this.BITBOX = BITBOX; this.getRawTransactions = getRawTransactions; this.slp = new Slp(BITBOX); this.cachedValidations = {}; this.cachedRawTransactions = {}; } addValidationFromStore(hex: string, isValid: boolean) { let id = (<Buffer>this.BITBOX.Crypto.sha256(this.BITBOX.Crypto.sha256(Buffer.from(hex, 'hex'))).reverse()).toString('hex'); if(!this.cachedValidations[id]) this.cachedValidations[id] = { hex: hex, validity: isValid, parents: [], details: null, invalidReason: null } if(!this.cachedRawTransactions[id]) this.cachedRawTransactions[id] = hex; } async waitForCurrentValidationProcessing(txid: string) { // TODO: Add some timeout? let cached: Validation = this.cachedValidations[txid]; while(true) { if(typeof cached.validity === 'boolean') break await sleep(10); } } async waitForTransactionPreProcessing(txid: string){ // TODO: Add some timeout? while(true) { if(this.cachedValidations[txid].hex && (this.cachedValidations[txid].details || typeof this.cachedValidations.validity === 'boolean')) break await sleep(10); } //await sleep(100); // short wait to make sure parent's properties gets set first. return } async getRawTransaction(txid: string) { if(this.cachedRawTransactions[txid]) return this.cachedRawTransactions[txid]; this.cachedRawTransactions[txid] = (await this.getRawTransactions([txid]))[0] if(this.cachedRawTransactions[txid]) return this.cachedRawTransactions[txid]; return null; } async isValidSlpTxid(txid: string) { if(txid && !this.cachedValidations[txid]) { this.cachedValidations[txid] = { hex: null, validity: null, parents: [], details: null, invalidReason: null } this.cachedValidations[txid].hex = await this.getRawTransaction(txid); } // Check to see how we should proceed based on the validation-cache state if(!this.cachedValidations[txid].hex) await this.waitForTransactionPreProcessing(txid); if(typeof this.cachedValidations[txid].validity === 'boolean') return this.cachedValidations[txid].validity; if(this.cachedValidations[txid].details) await this.waitForCurrentValidationProcessing(txid); // Check SLP message validity let txn: BitcoreTransaction = new bitcore.Transaction(this.cachedValidations[txid].hex) let slpmsg: SlpTransactionDetails; try { slpmsg = this.cachedValidations[txid].details = this.slp.parseSlpOutputScript(txn.outputs[0]._scriptBuffer) } catch(e) { this.cachedValidations[txid].invalidReason = "SLP OP_RETURN parsing error (" + e.message + ")." return this.cachedValidations[txid].validity = false; } // Check DAG validity if(slpmsg.transactionType === SlpTransactionType.GENESIS) { return this.cachedValidations[txid].validity = true; } else if (slpmsg.transactionType === SlpTransactionType.MINT) { for(let i = 0; i < txn.inputs.length; i++) { let input_txid = txn.inputs[i].prevTxId.toString('hex') let input_txhex = await this.getRawTransaction(input_txid) if (input_txhex) { let input_tx: BitcoreTransaction = new bitcore.Transaction(input_txhex); try { let input_slpmsg = this.slp.parseSlpOutputScript(input_tx.outputs[0]._scriptBuffer) if(input_slpmsg.transactionType === SlpTransactionType.GENESIS) input_slpmsg.tokenIdHex = input_txid; if(input_slpmsg.tokenIdHex === slpmsg.tokenIdHex) { if(input_slpmsg.transactionType === SlpTransactionType.GENESIS || input_slpmsg.transactionType === SlpTransactionType.MINT) { if(txn.inputs[i].outputIndex === input_slpmsg.batonVout) this.cachedValidations[txid].parents.push({ txid: txn.inputs[i].prevTxId.toString('hex'), versionType: input_slpmsg.versionType ,valid: null, inputQty: null }) } } } catch(_) {} } } if(this.cachedValidations[txid].parents.length !== 1) { this.cachedValidations[txid].invalidReason = "MINT transaction must have 1 valid baton parent." return this.cachedValidations[txid].validity = false; } } else if(slpmsg.transactionType === SlpTransactionType.SEND) { let tokenOutQty = slpmsg.sendOutputs.reduce((t,v)=>{ return t.plus(v) }, new BigNumber(0)) let tokenInQty = new BigNumber(0); for(let i = 0; i < txn.inputs.length; i++) { let input_txid = txn.inputs[i].prevTxId.toString('hex') let input_txhex = await this.getRawTransaction(input_txid) if (input_txhex) { let input_tx: BitcoreTransaction = new bitcore.Transaction(input_txhex); try { let input_slpmsg = this.slp.parseSlpOutputScript(input_tx.outputs[0]._scriptBuffer) if(input_slpmsg.transactionType === SlpTransactionType.GENESIS) input_slpmsg.tokenIdHex = input_txid; if(input_slpmsg.tokenIdHex === slpmsg.tokenIdHex) { if(input_slpmsg.transactionType === SlpTransactionType.SEND) { tokenInQty = tokenInQty.plus(input_slpmsg.sendOutputs[txn.inputs[i].outputIndex]) this.cachedValidations[txid].parents.push({txid: txn.inputs[i].prevTxId.toString('hex'), versionType: input_slpmsg.versionType, valid: null, inputQty: input_slpmsg.sendOutputs[txn.inputs[i].outputIndex] }) } else if(input_slpmsg.transactionType === SlpTransactionType.GENESIS || input_slpmsg.transactionType === SlpTransactionType.MINT) { if(txn.inputs[i].outputIndex === 1) tokenInQty = tokenInQty.plus(input_slpmsg.genesisOrMintQuantity) this.cachedValidations[txid].parents.push({txid: txn.inputs[i].prevTxId.toString('hex'), versionType: input_slpmsg.versionType, valid: null, inputQty: input_slpmsg.genesisOrMintQuantity }) } } } catch(_) {} } } // Check token inputs are greater than token outputs (includes valid and invalid inputs) if(tokenOutQty.isGreaterThan(tokenInQty)) { this.cachedValidations[txid].invalidReason = "Token outputs are greater than possible token inputs." return this.cachedValidations[txid].validity = false; } } // Set validity validation-cache for parents, and handle MINT condition with no valid input for(let i = 0; i < this.cachedValidations[txid].parents.length; i++) { let valid = await this.isValidSlpTxid(this.cachedValidations[txid].parents[i].txid) this.cachedValidations[txid].parents.find(p => p.txid === this.cachedValidations[txid].parents[i].txid).valid = valid; if(this.cachedValidations[txid].details.transactionType === SlpTransactionType.MINT && !valid) { this.cachedValidations[txid].invalidReason = "MINT transaction with invalid baton parent." return this.cachedValidations[txid].validity = false; } } // Check valid inputs are greater than token outputs if(this.cachedValidations[txid].details.transactionType === SlpTransactionType.SEND) { let validInputQty = this.cachedValidations[txid].parents.reduce((t, v) => { return v.valid ? t.plus(v.inputQty) : t }, new BigNumber(0)); let tokenOutQty = slpmsg.sendOutputs.reduce((t,v)=>{ return t.plus(v) }, new BigNumber(0)) if(tokenOutQty.isGreaterThan(validInputQty)) { this.cachedValidations[txid].invalidReason = "Token outputs are greater than valid token inputs." return this.cachedValidations[txid].validity = false; } } // Check versionType is not different from any valid parent if(this.cachedValidations[txid].parents.filter(p => p.valid).length > 0) { let validVersionType = this.cachedValidations[txid].parents.find(p => p.valid).versionType; if(this.cachedValidations[txid].details.versionType !== validVersionType) { this.cachedValidations[txid].invalidReason = "SLP version/type mismatch from valid parent." return this.cachedValidations[txid].validity = false; } } // For case with 0 token SEND with no valid parents, must check GENESIS validity / versionType. else if(this.cachedValidations[txid].details.transactionType === SlpTransactionType.SEND) { let slpmsg = this.cachedValidations[txid].details let valid = await this.isValidSlpTxid(slpmsg.tokenIdHex); if(valid) { let genesisTxn: BitcoreTransaction = new bitcore.Transaction(this.cachedValidations[slpmsg.tokenIdHex].hex) let genesisMsg = this.slp.parseSlpOutputScript(genesisTxn.outputs[0]._scriptBuffer) if(genesisMsg.versionType !== slpmsg.versionType) { this.cachedValidations[txid].invalidReason = "SLP version/type mismatch from valid GENESIS." return this.cachedValidations[txid].validity = false; } } else { this.cachedValidations[txid].invalidReason = "SEND has 0 outputs, but has invalid token GENESIS." console.log(this.cachedValidations[slpmsg.tokenIdHex].invalidReason) return this.cachedValidations[txid].validity = false; } } return this.cachedValidations[txid].validity = true; } async validateSlpTransactions(txids: string[]): Promise<string[]> { let res = []; for (let i = 0; i < txids.length; i++) { res.push((await this.isValidSlpTxid) ? txids[i] : '') } return res.filter((id: string) => id.length > 0); } }