UNPKG

bch-slpjs

Version:

Simple Ledger Protocol (SLP) JavaScript Library

626 lines 35.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const bchaddr = require("bchaddrjs-slp"); const bignumber_js_1 = require("bignumber.js"); const slpjs_1 = require("./slpjs"); const slptokentype1_1 = require("./slptokentype1"); const utils_1 = require("./utils"); class Slp { constructor(BITBOX) { this.BITBOX = BITBOX; } get lokadIdHex() { return "534c5000"; } buildGenesisOpReturn(config, type = 0x01) { let hash; try { hash = config.hash.toString('hex'); } catch (_) { hash = null; } return slptokentype1_1.SlpTokenType1.buildGenesisOpReturn(config.ticker, config.name, config.documentUri, hash, config.decimals, config.batonVout, config.initialQuantity); } buildMintOpReturn(config, type = 0x01) { return slptokentype1_1.SlpTokenType1.buildMintOpReturn(config.tokenIdHex, config.batonVout, config.mintQuantity); } buildSendOpReturn(config, type = 0x01) { return slptokentype1_1.SlpTokenType1.buildSendOpReturn(config.tokenIdHex, config.outputQtyArray); } buildRawGenesisTx(config, type = 0x01) { if (config.mintReceiverSatoshis === undefined) config.mintReceiverSatoshis = new bignumber_js_1.default(546); if (config.batonReceiverSatoshis === undefined) config.batonReceiverSatoshis = new bignumber_js_1.default(546); // Make sure we're not spending any token or baton UTXOs config.input_utxos.forEach(txo => { if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.NOT_SLP) return; if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.SLP_TOKEN) { throw Error("Input UTXOs included a token for another tokenId."); } if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.SLP_BATON) throw Error("Cannot spend a minting baton."); if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.INVALID_TOKEN_DAG || txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.INVALID_BATON_DAG) throw Error("Cannot currently spend tokens and baton with invalid DAGs."); throw Error("Cannot spend utxo with no SLP judgement."); }); // Check for slp formatted addresses if (!bchaddr.isSlpAddress(config.mintReceiverAddress)) throw new Error("Not an SLP address."); if (config.batonReceiverAddress != null && !bchaddr.isSlpAddress(config.batonReceiverAddress)) throw new Error("Not an SLP address."); config.mintReceiverAddress = bchaddr.toCashAddress(config.mintReceiverAddress); let transactionBuilder = new this.BITBOX.TransactionBuilder(utils_1.Utils.txnBuilderString(config.mintReceiverAddress)); let satoshis = new bignumber_js_1.default(0); config.input_utxos.forEach(token_utxo => { transactionBuilder.addInput(token_utxo.txid, token_utxo.vout); satoshis = satoshis.plus(token_utxo.satoshis); }); let genesisCost = this.calculateGenesisCost(config.slpGenesisOpReturn.length, config.input_utxos.length, config.batonReceiverAddress, config.bchChangeReceiverAddress); let bchChangeAfterFeeSatoshis = satoshis.minus(genesisCost); // Genesis OpReturn transactionBuilder.addOutput(config.slpGenesisOpReturn, 0); // Genesis token mint transactionBuilder.addOutput(config.mintReceiverAddress, config.mintReceiverSatoshis.toNumber()); //bchChangeAfterFeeSatoshis -= config.mintReceiverSatoshis; // Baton address (optional) if (config.batonReceiverAddress != null) { config.batonReceiverAddress = bchaddr.toCashAddress(config.batonReceiverAddress); if (this.parseSlpOutputScript(config.slpGenesisOpReturn).batonVout !== 2) throw Error("batonVout in transaction does not match OP_RETURN data."); transactionBuilder.addOutput(config.batonReceiverAddress, config.batonReceiverSatoshis.toNumber()); //bchChangeAfterFeeSatoshis -= config.batonReceiverSatoshis; } // Change (optional) if (config.bchChangeReceiverAddress != null && bchChangeAfterFeeSatoshis.isGreaterThan(new bignumber_js_1.default(546))) { config.bchChangeReceiverAddress = bchaddr.toCashAddress(config.bchChangeReceiverAddress); transactionBuilder.addOutput(config.bchChangeReceiverAddress, bchChangeAfterFeeSatoshis.toNumber()); } // sign inputs let i = 0; for (const txo of config.input_utxos) { let paymentKeyPair = this.BITBOX.ECPair.fromWIF(txo.wif); transactionBuilder.sign(i, paymentKeyPair, null, transactionBuilder.hashTypes.SIGHASH_ALL, txo.satoshis.toNumber()); i++; } let tx = transactionBuilder.build().toHex(); // Check For Low Fee let outValue = transactionBuilder.transaction.tx.outs.reduce((v, o) => v += o.value, 0); let inValue = config.input_utxos.reduce((v, i) => v = v.plus(i.satoshis), new bignumber_js_1.default(0)); if (inValue.minus(outValue).isLessThanOrEqualTo(tx.length / 2)) throw Error("Transaction fee is not high enough."); // TODO: Check for fee too large or send leftover to target address return tx; } buildRawSendTx(config, type = 0x01) { const sendMsg = this.parseSlpOutputScript(config.slpSendOpReturn); config.tokenReceiverAddressArray.forEach(outputAddress => { if (!bchaddr.isSlpAddress(outputAddress)) throw new Error("Token receiver address not in SLP format."); }); // Make sure not spending any other tokens or baton UTXOs let tokenInputQty = new bignumber_js_1.default(0); config.input_token_utxos.forEach(txo => { if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.NOT_SLP) return; if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.SLP_TOKEN) { if (txo.slpTransactionDetails.tokenIdHex !== sendMsg.tokenIdHex) throw Error("Input UTXOs included a token for another tokenId."); tokenInputQty = tokenInputQty.plus(txo.slpUtxoJudgementAmount); return; } if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.SLP_BATON) throw Error("Cannot spend a minting baton."); if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.INVALID_TOKEN_DAG || txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.INVALID_BATON_DAG) throw Error("Cannot currently spend UTXOs with invalid DAGs."); throw Error("Cannot spend utxo with no SLP judgement."); }); // Make sure the number of output receivers matches the outputs in the OP_RETURN message. let chgAddr = config.bchChangeReceiverAddress ? 1 : 0; if (config.tokenReceiverAddressArray.length + chgAddr !== sendMsg.sendOutputs.length) throw Error("Number of token receivers in config does not match the OP_RETURN outputs"); // Make sure token inputs equals token outputs in OP_RETURN let outputTokenQty = sendMsg.sendOutputs.reduce((v, o) => v = v.plus(o), new bignumber_js_1.default(0)); if (!tokenInputQty.isEqualTo(outputTokenQty)) throw Error("Token input quantity does not match token outputs."); let transactionBuilder = new this.BITBOX.TransactionBuilder(utils_1.Utils.txnBuilderString(config.tokenReceiverAddressArray[0])); let inputSatoshis = new bignumber_js_1.default(0); config.input_token_utxos.forEach(token_utxo => { transactionBuilder.addInput(token_utxo.txid, token_utxo.vout); inputSatoshis = inputSatoshis.plus(token_utxo.satoshis); }); let sendCost = this.calculateSendCost(config.slpSendOpReturn.length, config.input_token_utxos.length, config.tokenReceiverAddressArray.length, config.bchChangeReceiverAddress); let bchChangeAfterFeeSatoshis = inputSatoshis.minus(sendCost); // Genesis OpReturn transactionBuilder.addOutput(config.slpSendOpReturn, 0); // Token distribution outputs config.tokenReceiverAddressArray.forEach((outputAddress) => { outputAddress = bchaddr.toCashAddress(outputAddress); transactionBuilder.addOutput(outputAddress, 546); }); // Change if (config.bchChangeReceiverAddress != null && bchChangeAfterFeeSatoshis.isGreaterThan(new bignumber_js_1.default(546))) { config.bchChangeReceiverAddress = bchaddr.toCashAddress(config.bchChangeReceiverAddress); transactionBuilder.addOutput(config.bchChangeReceiverAddress, bchChangeAfterFeeSatoshis.toNumber()); } // sign inputs let i = 0; for (const txo of config.input_token_utxos) { let paymentKeyPair = this.BITBOX.ECPair.fromWIF(txo.wif); transactionBuilder.sign(i, paymentKeyPair, null, transactionBuilder.hashTypes.SIGHASH_ALL, txo.satoshis.toNumber()); i++; } let tx = transactionBuilder.build().toHex(); // Check For Low Fee let outValue = transactionBuilder.transaction.tx.outs.reduce((v, o) => v += o.value, 0); let inValue = config.input_token_utxos.reduce((v, i) => v = v.plus(i.satoshis), new bignumber_js_1.default(0)); if (inValue.minus(outValue).isLessThanOrEqualTo(tx.length / 2)) throw Error("Transaction fee is not high enough."); // TODO: Check for fee too large or send leftover to target address return tx; } buildRawMintTx(config, type = 0x01) { let mintMsg = this.parseSlpOutputScript(config.slpMintOpReturn); if (config.mintReceiverSatoshis === undefined) config.mintReceiverSatoshis = new bignumber_js_1.default(546); if (config.batonReceiverSatoshis === undefined) config.batonReceiverSatoshis = new bignumber_js_1.default(546); // Check for slp formatted addresses if (!bchaddr.isSlpAddress(config.mintReceiverAddress)) { throw new Error("Mint receiver address not in SLP format."); } if (config.batonReceiverAddress != null && !bchaddr.isSlpAddress(config.batonReceiverAddress)) { throw new Error("Baton receiver address not in SLP format."); } config.mintReceiverAddress = bchaddr.toCashAddress(config.mintReceiverAddress); config.batonReceiverAddress = bchaddr.toCashAddress(config.batonReceiverAddress); // Make sure inputs don't include spending any tokens or batons for other tokenIds config.input_baton_utxos.forEach(txo => { if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.NOT_SLP) return; if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.SLP_TOKEN) throw Error("Input UTXOs should not include any tokens."); if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.SLP_BATON) { if (txo.slpTransactionDetails.tokenIdHex !== mintMsg.tokenIdHex) throw Error("Cannot spend a minting baton."); return; } if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.INVALID_TOKEN_DAG || txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.INVALID_BATON_DAG) throw Error("Cannot currently spend UTXOs with invalid DAGs."); throw Error("Cannot spend utxo with no SLP judgement."); }); // Make sure inputs include the baton for this tokenId if (!config.input_baton_utxos.find(o => o.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.SLP_BATON)) Error("There is no baton included with the input UTXOs."); let transactionBuilder = new this.BITBOX.TransactionBuilder(utils_1.Utils.txnBuilderString(config.mintReceiverAddress)); let satoshis = new bignumber_js_1.default(0); config.input_baton_utxos.forEach(baton_utxo => { transactionBuilder.addInput(baton_utxo.txid, baton_utxo.vout); satoshis = satoshis.plus(baton_utxo.satoshis); }); let mintCost = this.calculateGenesisCost(config.slpMintOpReturn.length, config.input_baton_utxos.length, config.batonReceiverAddress, config.bchChangeReceiverAddress); let bchChangeAfterFeeSatoshis = satoshis.minus(mintCost); // Mint OpReturn transactionBuilder.addOutput(config.slpMintOpReturn, 0); // Mint token mint transactionBuilder.addOutput(config.mintReceiverAddress, config.mintReceiverSatoshis.toNumber()); //bchChangeAfterFeeSatoshis -= config.mintReceiverSatoshis; // Baton address (optional) if (config.batonReceiverAddress !== null) { config.batonReceiverAddress = bchaddr.toCashAddress(config.batonReceiverAddress); if (this.parseSlpOutputScript(config.slpMintOpReturn).batonVout !== 2) throw Error("batonVout in transaction does not match OP_RETURN data."); transactionBuilder.addOutput(config.batonReceiverAddress, config.batonReceiverSatoshis.toNumber()); //bchChangeAfterFeeSatoshis -= config.batonReceiverSatoshis; } // Change (optional) if (config.bchChangeReceiverAddress !== null && bchChangeAfterFeeSatoshis.isGreaterThan(new bignumber_js_1.default(546))) { config.bchChangeReceiverAddress = bchaddr.toCashAddress(config.bchChangeReceiverAddress); transactionBuilder.addOutput(config.bchChangeReceiverAddress, bchChangeAfterFeeSatoshis.toNumber()); } // sign inputs let i = 0; for (const txo of config.input_baton_utxos) { let paymentKeyPair = this.BITBOX.ECPair.fromWIF(txo.wif); transactionBuilder.sign(i, paymentKeyPair, null, transactionBuilder.hashTypes.SIGHASH_ALL, txo.satoshis.toNumber()); i++; } let tx = transactionBuilder.build().toHex(); // Check For Low Fee let outValue = transactionBuilder.transaction.tx.outs.reduce((v, o) => v += o.value, 0); let inValue = config.input_baton_utxos.reduce((v, i) => v = v.plus(i.satoshis), new bignumber_js_1.default(0)); if (inValue.minus(outValue).isLessThanOrEqualTo(tx.length / 2)) throw Error("Transaction fee is not high enough."); // TODO: Check for fee too large or send leftover to target address return tx; } parseSlpOutputScript(outputScript) { let slpMsg = {}; let chunks; try { chunks = this.parseOpReturnToChunks(outputScript); } catch (e) { //console.log(e); throw Error('Bad OP_RETURN'); } if (chunks.length === 0) throw Error('Empty OP_RETURN'); if (!chunks[0].equals(Buffer.from(this.lokadIdHex, 'hex'))) throw Error('No SLP'); if (chunks.length === 1) throw Error("Missing token_type"); // # check if the token version is supported slpMsg.versionType = Slp.parseChunkToInt(chunks[1], 1, 2, true); // if(slpMsg.type !== SlpTypeVersion.TokenVersionType1) // throw Error('Unsupported token type:' + slpMsg.type); if (chunks.length === 2) throw Error('Missing SLP transaction type'); try { slpMsg.transactionType = slpjs_1.SlpTransactionType[chunks[2].toString('ascii')]; } catch (_) { throw Error('Bad transaction type'); } if (slpMsg.transactionType === slpjs_1.SlpTransactionType.GENESIS) { if (chunks.length !== 10) throw Error('GENESIS with incorrect number of parameters'); slpMsg.symbol = chunks[3] ? chunks[3].toString('utf8') : ''; slpMsg.name = chunks[4] ? chunks[4].toString('utf8') : ''; slpMsg.documentUri = chunks[5] ? chunks[5].toString('utf8') : ''; slpMsg.documentSha256 = chunks[6] ? chunks[6] : null; if (slpMsg.documentSha256) { if (slpMsg.documentSha256.length !== 0 && slpMsg.documentSha256.length !== 32) throw Error('Token document hash is incorrect length'); } slpMsg.decimals = Slp.parseChunkToInt(chunks[7], 1, 1, true); if (slpMsg.decimals > 9) throw Error('Too many decimals'); slpMsg.batonVout = chunks[8] ? Slp.parseChunkToInt(chunks[8], 1, 1) : null; if (slpMsg.batonVout !== null) { if (slpMsg.batonVout < 2) throw Error('Mint baton cannot be on vout=0 or 1'); slpMsg.containsBaton = true; } slpMsg.genesisOrMintQuantity = (new bignumber_js_1.default(chunks[9].readUInt32BE(0).toString())).multipliedBy(Math.pow(2, 32)).plus(chunks[9].readUInt32BE(4).toString()); } else if (slpMsg.transactionType === slpjs_1.SlpTransactionType.SEND) { if (chunks.length < 4) throw Error('SEND with too few parameters'); if (chunks[3].length !== 32) throw Error('token_id is wrong length'); slpMsg.tokenIdHex = chunks[3].toString('hex'); // # Note that we put an explicit 0 for ['token_output'][0] since it // # corresponds to vout=0, which is the OP_RETURN tx output. // # ['token_output'][1] is the first token output given by the SLP // # message, i.e., the number listed as `token_output_quantity1` in the // # spec, which goes to tx output vout=1. slpMsg.sendOutputs = []; slpMsg.sendOutputs.push(new bignumber_js_1.default(0)); chunks.slice(4).forEach(chunk => { if (chunk.length !== 8) throw Error('SEND quantities must be 8-bytes each.'); slpMsg.sendOutputs.push((new bignumber_js_1.default(chunk.readUInt32BE(0).toString())).multipliedBy(Math.pow(2, 32)).plus(new bignumber_js_1.default(chunk.readUInt32BE(4).toString()))); }); // # maximum 19 allowed token outputs, plus 1 for the explicit [0] we inserted. if (slpMsg.sendOutputs.length < 2) throw Error('Missing output amounts'); if (slpMsg.sendOutputs.length > 20) throw Error('More than 19 output amounts'); } else if (slpMsg.transactionType === slpjs_1.SlpTransactionType.MINT) { if (chunks.length != 6) throw Error('MINT with incorrect number of parameters'); if (chunks[3].length != 32) throw Error('token_id is wrong length'); slpMsg.tokenIdHex = chunks[3].toString('hex'); slpMsg.batonVout = chunks[4] ? Slp.parseChunkToInt(chunks[4], 1, 1) : null; if (slpMsg.batonVout !== null) { if (slpMsg.batonVout < 2) throw Error('Mint baton cannot be on vout=0 or 1'); slpMsg.containsBaton = true; } slpMsg.genesisOrMintQuantity = (new bignumber_js_1.default(chunks[5].readUInt32BE(0).toString())).multipliedBy(Math.pow(2, 32)).plus((new bignumber_js_1.default(chunks[5].readUInt32BE(4).toString()))); } else throw Error('Bad transaction type'); return slpMsg; } static parseChunkToInt(intBytes, minByteLen, maxByteLen, raise_on_Null = false) { // # Parse data as unsigned-big-endian encoded integer. // # For empty data different possibilities may occur: // # minByteLen <= 0 : return 0 // # raise_on_Null == False and minByteLen > 0: return None // # raise_on_Null == True and minByteLen > 0: raise SlpInvalidOutputMessage if (intBytes.length >= minByteLen && intBytes.length <= maxByteLen) return intBytes.readUIntBE(0, intBytes.length); if (intBytes.length === 0 && !raise_on_Null) return null; throw Error('Field has wrong length'); } // get list of data chunks resulting from data push operations parseOpReturnToChunks(script, allow_op_0 = false, allow_op_number = false) { // """Extract pushed bytes after opreturn. Returns list of bytes() objects, // one per push. let ops; // Strict refusal of non-push opcodes; bad scripts throw OpreturnError.""" try { ops = this.getScriptOperations(script); } catch (e) { //console.log(e); throw Error('Script error'); } if (ops[0].opcode != this.BITBOX.Script.opcodes.OP_RETURN) throw Error('No OP_RETURN'); let chunks = []; ops.slice(1).forEach(opitem => { if (opitem.opcode > this.BITBOX.Script.opcodes.OP_16) throw Error("Non-push opcode"); if (opitem.opcode > this.BITBOX.Script.opcodes.OP_PUSHDATA4) { if (opitem.opcode === 80) throw Error('Non-push opcode'); if (!allow_op_number) throw Error('OP_1NEGATE to OP_16 not allowed'); if (opitem.opcode === this.BITBOX.Script.opcodes.OP_1NEGATE) opitem.data = Buffer.from([0x81]); else // OP_1 - OP_16 opitem.data = Buffer.from([opitem.opcode - 80]); } if (opitem.opcode === this.BITBOX.Script.opcodes.OP_0 && !allow_op_0) { throw Error('OP_0 not allowed'); } chunks.push(opitem.data); }); //console.log(chunks); return chunks; } // Get a list of operations with accompanying push data (if a push opcode) getScriptOperations(script) { let ops = []; try { let n = 0; let dlen; while (n < script.length) { let op = { opcode: script[n], data: null }; n += 1; if (op.opcode <= this.BITBOX.Script.opcodes.OP_PUSHDATA4) { if (op.opcode < this.BITBOX.Script.opcodes.OP_PUSHDATA1) dlen = op.opcode; else if (op.opcode === this.BITBOX.Script.opcodes.OP_PUSHDATA1) { dlen = script[n]; n += 1; } else if (op.opcode === this.BITBOX.Script.opcodes.OP_PUSHDATA2) { dlen = script.slice(n, n + 2).readUIntLE(0, 2); n += 2; } else { dlen = script.slice(n, n + 4).readUIntLE(0, 4); n += 4; } if ((n + dlen) > script.length) { throw Error('IndexError'); } if (dlen > 0) op.data = script.slice(n, n + dlen); n += dlen; } ops.push(op); } } catch (e) { //console.log(e); throw Error('truncated script'); } return ops; } calculateGenesisCost(genesisOpReturnLength, inputUtxoSize, batonAddress, bchChangeAddress, feeRate = 1) { return this.calculateMintOrGenesisCost(genesisOpReturnLength, inputUtxoSize, batonAddress, bchChangeAddress, feeRate); } calculateMintCost(mintOpReturnLength, inputUtxoSize, batonAddress, bchChangeAddress, feeRate = 1) { return this.calculateMintOrGenesisCost(mintOpReturnLength, inputUtxoSize, batonAddress, bchChangeAddress, feeRate); } calculateMintOrGenesisCost(mintOpReturnLength, inputUtxoSize, batonAddress, bchChangeAddress, feeRate = 1) { let outputs = 1; let nonfeeoutputs = 546; if (batonAddress !== null && batonAddress !== undefined) { nonfeeoutputs += 546; outputs += 1; } if (bchChangeAddress !== null && bchChangeAddress !== undefined) { outputs += 1; } let fee = this.BITBOX.BitcoinCash.getByteCount({ P2PKH: inputUtxoSize }, { P2PKH: outputs }); fee += mintOpReturnLength; fee += 10; // added to account for OP_RETURN ammount of 0000000000000000 fee *= feeRate; //console.log("MINT/GENESIS cost before outputs: " + fee.toString()); fee += nonfeeoutputs; //console.log("MINT/GENESIS cost after outputs are added: " + fee.toString()); return fee; } calculateSendCost(sendOpReturnLength, inputUtxoSize, outputAddressArraySize, bchChangeAddress, feeRate = 1) { let outputs = outputAddressArraySize; let nonfeeoutputs = outputAddressArraySize * 546; if (bchChangeAddress != null) { outputs += 1; } let fee = this.BITBOX.BitcoinCash.getByteCount({ P2PKH: inputUtxoSize }, { P2PKH: outputs }); fee += sendOpReturnLength; fee += 10; // added to account for OP_RETURN ammount of 0000000000000000 fee *= feeRate; //console.log("SEND cost before outputs: " + fee.toString()); fee += nonfeeoutputs; //console.log("SEND cost after outputs are added: " + fee.toString()); return fee; } static preSendSlpJudgementCheck(txo, tokenId) { if (txo.slpUtxoJudgement === undefined || txo.slpUtxoJudgement === null || txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.UNKNOWN) throw Error("There at least one input UTXO that does not have a proper SLP judgement"); if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.SLP_BATON) throw Error("There is at least one input UTXO that is a baton. You can only spend batons in a MINT transaction."); if (txo.slpTransactionDetails) { if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.SLP_TOKEN) { if (!txo.slpUtxoJudgementAmount) throw Error("There is at least one input token that does not have the 'slpUtxoJudgementAmount' property set."); if (txo.slpTransactionDetails.tokenIdHex !== tokenId) throw Error("There is at least one input UTXO that is a different SLP token than the one specified."); return txo.slpTransactionDetails.tokenIdHex === tokenId; } } return false; } processUtxosForSlpAbstract(utxos, asyncSlpValidator) { return __awaiter(this, void 0, void 0, function* () { // 1) parse SLP OP_RETURN and cast initial SLP judgement, based on OP_RETURN only. for (let txo of utxos) { this.applyInitialSlpJudgement(txo); if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.UNKNOWN || txo.slpUtxoJudgement === undefined) throw Error('Utxo SLP judgement has not been set, unknown error.'); } // 2) Cast final SLP judgement using the supplied async validator yield this.applyFinalSlpJudgement(asyncSlpValidator, utxos); // 3) Prepare results object const result = this.computeSlpBalances(utxos); // 4) Check that all UTXOs have been categorized let tokenTxoCount = 0; for (let id in result.slpTokenUtxos) tokenTxoCount += result.slpTokenUtxos[id].length; let batonTxoCount = 0; for (let id in result.slpBatonUtxos) batonTxoCount += result.slpBatonUtxos[id].length; if (utxos.length !== (tokenTxoCount + batonTxoCount + result.nonSlpUtxos.length + result.invalidBatonUtxos.length + result.invalidTokenUtxos.length)) throw Error('Not all UTXOs have been categorized. Unknown Error.'); return result; }); } computeSlpBalances(utxos) { const result = { satoshis_available_bch: 0, satoshis_in_slp_baton: 0, satoshis_in_slp_token: 0, satoshis_in_invalid_token_dag: 0, satoshis_in_invalid_baton_dag: 0, slpTokenBalances: {}, slpTokenUtxos: {}, slpBatonUtxos: {}, nonSlpUtxos: [], invalidTokenUtxos: [], invalidBatonUtxos: [] }; // 5) Loop through UTXO set and accumulate balances for type of utxo, organize the Utxos into their categories. for (const txo of utxos) { if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.SLP_TOKEN) { if (!(txo.slpTransactionDetails.tokenIdHex in result.slpTokenBalances)) result.slpTokenBalances[txo.slpTransactionDetails.tokenIdHex] = new bignumber_js_1.default(0); if (txo.slpTransactionDetails.transactionType === slpjs_1.SlpTransactionType.GENESIS || txo.slpTransactionDetails.transactionType === slpjs_1.SlpTransactionType.MINT) { result.slpTokenBalances[txo.slpTransactionDetails.tokenIdHex] = result.slpTokenBalances[txo.slpTransactionDetails.tokenIdHex].plus(txo.slpTransactionDetails.genesisOrMintQuantity); } else if (txo.slpTransactionDetails.transactionType === slpjs_1.SlpTransactionType.SEND && txo.slpTransactionDetails.sendOutputs) { let qty = txo.slpTransactionDetails.sendOutputs[txo.vout]; result.slpTokenBalances[txo.slpTransactionDetails.tokenIdHex] = result.slpTokenBalances[txo.slpTransactionDetails.tokenIdHex].plus(qty); } else { throw Error('Unknown Error: cannot have an SLP_TOKEN that is not from GENESIS, MINT, or SEND.'); } result.satoshis_in_slp_token += txo.satoshis; if (!(txo.slpTransactionDetails.tokenIdHex in result.slpTokenUtxos)) result.slpTokenUtxos[txo.slpTransactionDetails.tokenIdHex] = []; result.slpTokenUtxos[txo.slpTransactionDetails.tokenIdHex].push(txo); } else if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.SLP_BATON) { result.satoshis_in_slp_baton += txo.satoshis; if (!(txo.slpTransactionDetails.tokenIdHex in result.slpBatonUtxos)) result.slpBatonUtxos[txo.slpTransactionDetails.tokenIdHex] = []; result.slpBatonUtxos[txo.slpTransactionDetails.tokenIdHex].push(txo); } else if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.INVALID_TOKEN_DAG) { result.satoshis_in_invalid_token_dag += txo.satoshis; result.invalidTokenUtxos.push(txo); } else if (txo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.INVALID_BATON_DAG) { result.satoshis_in_invalid_baton_dag += txo.satoshis; result.invalidBatonUtxos.push(txo); } else { result.satoshis_available_bch += txo.satoshis; result.nonSlpUtxos.push(txo); } } return result; } applyInitialSlpJudgement(txo) { try { let vout = txo.tx.vout.find(vout => vout.n === 0); if (!vout) throw 'Utxo contains no Vout!'; let vout0script = Buffer.from(vout.scriptPubKey.hex, 'hex'); txo.slpTransactionDetails = this.parseSlpOutputScript(vout0script); // populate txid for GENESIS if (txo.slpTransactionDetails.transactionType === slpjs_1.SlpTransactionType.GENESIS) txo.slpTransactionDetails.tokenIdHex = txo.txid; // apply initial SLP judgement to the UTXO (based on OP_RETURN parsing ONLY! Still need to validate the DAG for possible tokens and batons!) if (txo.slpTransactionDetails.transactionType === slpjs_1.SlpTransactionType.GENESIS || txo.slpTransactionDetails.transactionType === slpjs_1.SlpTransactionType.MINT) { if (txo.slpTransactionDetails.containsBaton && txo.slpTransactionDetails.batonVout === txo.vout) { txo.slpUtxoJudgement = slpjs_1.SlpUtxoJudgement.SLP_BATON; } else if (txo.vout === 1 && txo.slpTransactionDetails.genesisOrMintQuantity.isGreaterThan(0)) { txo.slpUtxoJudgement = slpjs_1.SlpUtxoJudgement.SLP_TOKEN; txo.slpUtxoJudgementAmount = txo.slpTransactionDetails.genesisOrMintQuantity; } else txo.slpUtxoJudgement = slpjs_1.SlpUtxoJudgement.NOT_SLP; } else if (txo.slpTransactionDetails.transactionType === slpjs_1.SlpTransactionType.SEND && txo.slpTransactionDetails.sendOutputs) { if (txo.vout > 0 && txo.vout < txo.slpTransactionDetails.sendOutputs.length) { txo.slpUtxoJudgement = slpjs_1.SlpUtxoJudgement.SLP_TOKEN; txo.slpUtxoJudgementAmount = txo.slpTransactionDetails.sendOutputs[txo.vout]; } else txo.slpUtxoJudgement = slpjs_1.SlpUtxoJudgement.NOT_SLP; } else { txo.slpUtxoJudgement = slpjs_1.SlpUtxoJudgement.NOT_SLP; } } catch (e) { // any errors in parsing SLP OP_RETURN means the TXN is NOT SLP. txo.slpUtxoJudgement = slpjs_1.SlpUtxoJudgement.NOT_SLP; } } applyFinalSlpJudgement(asyncSlpValidator, utxos) { return __awaiter(this, void 0, void 0, function* () { let validSLPTx = yield asyncSlpValidator.validateSlpTransactions([ ...new Set(utxos.filter(txOut => { if (txOut.slpTransactionDetails && txOut.slpUtxoJudgement !== slpjs_1.SlpUtxoJudgement.UNKNOWN && txOut.slpUtxoJudgement !== slpjs_1.SlpUtxoJudgement.NOT_SLP) return true; return false; }).map(txOut => txOut.txid)) ]); utxos.forEach(utxo => { if (!(validSLPTx.includes(utxo.txid))) { if (utxo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.SLP_TOKEN) { utxo.slpUtxoJudgement = slpjs_1.SlpUtxoJudgement.INVALID_TOKEN_DAG; } else if (utxo.slpUtxoJudgement === slpjs_1.SlpUtxoJudgement.SLP_BATON) { utxo.slpUtxoJudgement = slpjs_1.SlpUtxoJudgement.INVALID_BATON_DAG; } } }); }); } } exports.Slp = Slp; //# sourceMappingURL=slp.js.map