bch-slpjs
Version:
Simple Ledger Protocol (SLP) JavaScript Library
217 lines (196 loc) • 11.8 kB
text/typescript
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);
}
}