UNPKG

bch-slpjs

Version:

Simple Ledger Protocol (SLP) JavaScript Library

915 lines (910 loc) 1.91 MB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.slpjs = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const slpjs = require("./lib/slpjs"); //export default Slpjs; exports.slpjs = slpjs; },{"./lib/slpjs":7}],2:[function(require,module,exports){ "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 bignumber_js_1 = require("bignumber.js"); const _ = require("lodash"); const bchaddr = require("bchaddrjs-slp"); const bitcore = require("bitcore-lib-cash"); const slp_1 = require("./slp"); const axios_1 = require("axios"); const utils_1 = require("./utils"); const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); class BitboxNetwork { constructor(BITBOX, validator) { this.BITBOX = BITBOX; this.slp = new slp_1.Slp(BITBOX); if (validator) this.validator = validator; else { this.validatorUrl = BITBOX.restURL.replace('v1', 'v2'); this.validatorUrl = this.validatorUrl.concat('/slp/validate'); this.validatorUrl = this.validatorUrl.replace('//slp', '/slp'); } } getTokenInformation(txid) { return __awaiter(this, void 0, void 0, function* () { let txhex = (yield this.BITBOX.RawTransactions.getRawTransaction([txid]))[0]; let txn = new bitcore.Transaction(txhex); return this.slp.parseSlpOutputScript(txn.outputs[0]._scriptBuffer); }); } getUtxos(address) { return __awaiter(this, void 0, void 0, function* () { // must be a cash or legacy addr let res; if (!bchaddr.isCashAddress(address) && !bchaddr.isLegacyAddress(address)) throw new Error("Not an a valid address format, must be cashAddr or Legacy address format."); res = (yield this.BITBOX.Address.utxo([address]))[0]; return res; }); } getAllSlpBalancesAndUtxos(address) { return __awaiter(this, void 0, void 0, function* () { address = bchaddr.toCashAddress(address); let result = yield this.getUtxoWithTxDetails(address); return yield this.processUtxosForSlp(result); }); } // Sent SLP tokens to a single output address with change handled (Warning: Sweeps all BCH/SLP UTXOs for the funding address) simpleTokenSend(tokenId, sendAmount, inputUtxos, tokenReceiverAddress, changeReceiverAddress) { return __awaiter(this, void 0, void 0, function* () { // 1) Set the token send amounts, we'll send 100 tokens to a new receiver and send token change back to the sender let totalTokenInputAmount = inputUtxos .filter(txo => { return slp_1.Slp.preSendSlpJudgementCheck(txo, tokenId); }) .reduce((tot, txo) => { return tot.plus(txo.slpUtxoJudgementAmount); }, new bignumber_js_1.default(0)); // 2) Compute the token Change amount. let tokenChangeAmount = totalTokenInputAmount.minus(sendAmount); let txHex; if (tokenChangeAmount.isGreaterThan(new bignumber_js_1.default(0))) { // 3) Create the Send OP_RETURN message let sendOpReturn = this.slp.buildSendOpReturn({ tokenIdHex: tokenId, outputQtyArray: [sendAmount, tokenChangeAmount], }); // 4) Create the raw Send transaction hex txHex = this.slp.buildRawSendTx({ slpSendOpReturn: sendOpReturn, input_token_utxos: utils_1.Utils.mapToUtxoArray(inputUtxos), tokenReceiverAddressArray: [tokenReceiverAddress, changeReceiverAddress], bchChangeReceiverAddress: changeReceiverAddress }); } else if (tokenChangeAmount.isEqualTo(new bignumber_js_1.default(0))) { // 3) Create the Send OP_RETURN message let sendOpReturn = this.slp.buildSendOpReturn({ tokenIdHex: tokenId, outputQtyArray: [sendAmount], }); // 4) Create the raw Send transaction hex txHex = this.slp.buildRawSendTx({ slpSendOpReturn: sendOpReturn, input_token_utxos: utils_1.Utils.mapToUtxoArray(inputUtxos), tokenReceiverAddressArray: [tokenReceiverAddress], bchChangeReceiverAddress: changeReceiverAddress }); } else { throw Error('Token inputs less than the token outputs'); } // 5) Broadcast the transaction over the network using this.BITBOX return yield this.sendTx(txHex); }); } simpleTokenGenesis(tokenName, tokenTicker, tokenAmount, documentUri, documentHash, decimals, tokenReceiverAddress, batonReceiverAddress, bchChangeReceiverAddress, inputUtxos) { return __awaiter(this, void 0, void 0, function* () { let genesisOpReturn = this.slp.buildGenesisOpReturn({ ticker: tokenTicker, name: tokenName, documentUri: documentUri, hash: documentHash, decimals: decimals, batonVout: 2, initialQuantity: tokenAmount, }); // 4) Create/sign the raw transaction hex for Genesis let genesisTxHex = this.slp.buildRawGenesisTx({ slpGenesisOpReturn: genesisOpReturn, mintReceiverAddress: tokenReceiverAddress, batonReceiverAddress: batonReceiverAddress, bchChangeReceiverAddress: bchChangeReceiverAddress, input_utxos: utils_1.Utils.mapToUtxoArray(inputUtxos) }); return yield this.sendTx(genesisTxHex); }); } // Sent SLP tokens to a single output address with change handled (Warning: Sweeps all BCH/SLP UTXOs for the funding address) simpleTokenMint(tokenId, mintAmount, inputUtxos, tokenReceiverAddress, batonReceiverAddress, changeReceiverAddress) { return __awaiter(this, void 0, void 0, function* () { // // convert address to cashAddr from SLP format. // let fundingAddress_cashfmt = bchaddr.toCashAddress(fundingAddress); // 1) Create the Send OP_RETURN message let mintOpReturn = this.slp.buildMintOpReturn({ tokenIdHex: tokenId, mintQuantity: mintAmount, batonVout: 2 }); // 2) Create the raw Mint transaction hex let txHex = this.slp.buildRawMintTx({ input_baton_utxos: utils_1.Utils.mapToUtxoArray(inputUtxos), slpMintOpReturn: mintOpReturn, mintReceiverAddress: tokenReceiverAddress, batonReceiverAddress: batonReceiverAddress, bchChangeReceiverAddress: changeReceiverAddress }); //console.log(txHex); // 5) Broadcast the transaction over the network using this.BITBOX return yield this.sendTx(txHex); }); } getUtxoWithRetry(address, retries = 40) { return __awaiter(this, void 0, void 0, function* () { let result; let count = 0; while (result === undefined) { result = yield this.getUtxos(address); count++; if (count > retries) throw new Error("this.BITBOX.Address.utxo endpoint experienced a problem"); yield sleep(250); } return result; }); } getUtxoWithTxDetails(address) { return __awaiter(this, void 0, void 0, function* () { let utxos = utils_1.Utils.mapToSlpAddressUtxoResultArray(yield this.getUtxoWithRetry(address)); let txIds = utxos.map(i => i.txid); if (txIds.length === 0) return []; // Split txIds into chunks of 20 (BitBox limit), run the detail queries in parallel let txDetails = (yield Promise.all(_.chunk(txIds, 20).map((txids) => { return this.getTransactionDetailsWithRetry([...new Set(txids)]); }))); // concat the chunked arrays txDetails = [].concat(...txDetails); utxos = utxos.map(i => { i.tx = txDetails.find((d) => d.txid === i.txid); return i; }); return utxos; }); } getTransactionDetailsWithRetry(txids, retries = 40) { return __awaiter(this, void 0, void 0, function* () { let result; let count = 0; while (result === undefined) { result = yield this.BITBOX.Transaction.details(txids); count++; if (count > retries) throw new Error("this.BITBOX.Address.details endpoint experienced a problem"); yield sleep(500); } return result; }); } getAddressDetailsWithRetry(address, retries = 40) { return __awaiter(this, void 0, void 0, function* () { // must be a cash or legacy addr if (!bchaddr.isCashAddress(address) && !bchaddr.isLegacyAddress(address)) throw new Error("Not an a valid address format, must be cashAddr or Legacy address format."); let result; let count = 0; while (result === undefined) { result = yield this.BITBOX.Address.details([address]); count++; if (count > retries) throw new Error("this.BITBOX.Address.details endpoint experienced a problem"); yield sleep(250); } return result; }); } sendTx(hex) { return __awaiter(this, void 0, void 0, function* () { let res = yield this.BITBOX.RawTransactions.sendRawTransaction(hex); //console.log(res); return res; }); } monitorForPayment(paymentAddress, fee, onPaymentCB) { return __awaiter(this, void 0, void 0, function* () { let utxo; // must be a cash or legacy addr if (!bchaddr.isCashAddress(paymentAddress) && !bchaddr.isLegacyAddress(paymentAddress)) throw new Error("Not an a valid address format, must be cashAddr or Legacy address format."); while (true) { try { utxo = yield this.getUtxos(paymentAddress); if (utxo) if (utxo.utxos[0].satoshis >= fee) break; } catch (ex) { console.log(ex); } yield sleep(2000); } onPaymentCB(); }); } isValidSlpTxid(txid) { throw new Error("Method not implemented."); } validateSlpTransactions(txids) { return __awaiter(this, void 0, void 0, function* () { const result = yield axios_1.default({ method: "post", url: this.validatorUrl, data: { txids: txids } }); if (result && result.data) { return result.data; } else { return []; } }); } getRawTransactions(txid) { throw Error("Method not implemented."); } processUtxosForSlp(utxos) { return __awaiter(this, void 0, void 0, function* () { if (this.validator) return yield this.slp.processUtxosForSlpAbstract(utxos, this.validator); return yield this.slp.processUtxosForSlpAbstract(utxos, this); }); } } exports.BitboxNetwork = BitboxNetwork; },{"./slp":6,"./utils":9,"axios":25,"bchaddrjs-slp":52,"bignumber.js":54,"bitcore-lib-cash":55,"lodash":198}],3:[function(require,module,exports){ (function (Buffer){ "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 axios_1 = require("axios"); const bignumber_js_1 = require("bignumber.js"); const slpjs_1 = require("./slpjs"); class BitdbNetwork { constructor(bitdbUrl = 'https://bitdb.bch.sx/q/') { this.bitdbUrl = bitdbUrl; } getTokenInformation(tokenId) { return __awaiter(this, void 0, void 0, function* () { let query = { "v": 3, "q": { "find": { "out.h1": "534c5000", "out.s3": "GENESIS", "tx.h": tokenId } }, "r": { "f": "[ .[] | { token_type: .out[0].h2, timestamp: (if .blk? then (.blk.t | strftime(\"%Y-%m-%d %H:%M\")) else null end), symbol: .out[0].s4, name: .out[0].s5, document: .out[0].s6, document_sha256: .out[0].h7, decimals: .out[0].h8, baton: .out[0].h9, quantity: .out[0].h10, URI: \"https://tokengraph.network/token/\\(.tx.h)\" } ]" } }; const data = Buffer.from(JSON.stringify(query)).toString('base64'); let config = { method: 'GET', url: this.bitdbUrl + data }; const response = (yield axios_1.default(config)).data; const list = []; if (response.c) { list.push(...response.c); } if (response.u) { list.push(...response.u); } if (list.length === 0) { throw new Error('Token not found'); } let tokenDetails = { transactionType: slpjs_1.SlpTransactionType.GENESIS, tokenIdHex: tokenId, versionType: parseInt(list[0].token_type, 16), timestamp: list[0].timestamp, symbol: list[0].symbol, name: list[0].name, documentUri: list[0].document, documentSha256: Buffer.from(list[0].document_sha256), decimals: parseInt(list[0].decimals, 16) || 0, containsBaton: Buffer.from(list[0].baton, 'hex').readUIntBE(0, 1) >= 2, batonVout: Buffer.from(list[0].baton, 'hex').readUIntBE(0, 1), genesisOrMintQuantity: new bignumber_js_1.default(list[0].quantity, 16).dividedBy(Math.pow(10, (parseInt(list[0].decimals, 16)))) }; return tokenDetails; }); } } exports.BitdbNetwork = BitdbNetwork; }).call(this,require("buffer").Buffer) },{"./slpjs":7,"axios":25,"bignumber.js":54,"buffer":139}],4:[function(require,module,exports){ "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 axios_1 = require("axios"); const slp_1 = require("./slp"); class JsonRpcProxyValidator { constructor(BITBOX, validatorUrl) { this.validatorUrl = validatorUrl; this.slp = new slp_1.Slp(BITBOX); } isValidSlpTxid(txid) { return __awaiter(this, void 0, void 0, function* () { let data = { jsonrpc: "2.0", id: "slpvalidate", method: "slpvalidate", params: [txid, false, false] }; const result = yield axios_1.default({ method: "post", url: this.validatorUrl, data: data }); if (result && result.data && result.data.result === "Valid") { return true; } else { return false; } }); } getRawTransactions(txid) { return __awaiter(this, void 0, void 0, function* () { throw new Error("Not implemented."); }); } ; validateSlpTransactions(txids) { return __awaiter(this, void 0, void 0, function* () { // Validate each txid const validatePromises = txids.map((txid) => __awaiter(this, void 0, void 0, function* () { const isValid = yield this.isValidSlpTxid(txid); return isValid ? txid : ''; })); // Filter array to only valid txid results const validateResults = yield axios_1.default.all(validatePromises); return validateResults.filter((result) => result.length > 0); }); } } exports.JsonRpcProxyValidator = JsonRpcProxyValidator; },{"./slp":6,"axios":25}],5:[function(require,module,exports){ (function (Buffer){ "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 slp_1 = require("./slp"); const slpjs_1 = require("./slpjs"); const bitcore = require("bitcore-lib-cash"); const bignumber_js_1 = require("bignumber.js"); const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); class LocalValidator { constructor(BITBOX, getRawTransactions) { this.BITBOX = BITBOX; this.getRawTransactions = getRawTransactions; this.slp = new slp_1.Slp(BITBOX); this.cachedValidations = {}; this.cachedRawTransactions = {}; } addValidationFromStore(hex, isValid) { let id = 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; } waitForCurrentValidationProcessing(txid) { return __awaiter(this, void 0, void 0, function* () { // TODO: Add some timeout? let cached = this.cachedValidations[txid]; while (true) { if (typeof cached.validity === 'boolean') break; yield sleep(10); } }); } waitForTransactionPreProcessing(txid) { return __awaiter(this, void 0, void 0, function* () { // TODO: Add some timeout? while (true) { if (this.cachedValidations[txid].hex && (this.cachedValidations[txid].details || typeof this.cachedValidations.validity === 'boolean')) break; yield sleep(10); } //await sleep(100); // short wait to make sure parent's properties gets set first. return; }); } getRawTransaction(txid) { return __awaiter(this, void 0, void 0, function* () { if (this.cachedRawTransactions[txid]) return this.cachedRawTransactions[txid]; this.cachedRawTransactions[txid] = (yield this.getRawTransactions([txid]))[0]; if (this.cachedRawTransactions[txid]) return this.cachedRawTransactions[txid]; return null; }); } isValidSlpTxid(txid) { return __awaiter(this, void 0, void 0, function* () { if (txid && !this.cachedValidations[txid]) { this.cachedValidations[txid] = { hex: null, validity: null, parents: [], details: null, invalidReason: null }; this.cachedValidations[txid].hex = yield this.getRawTransaction(txid); } // Check to see how we should proceed based on the validation-cache state if (!this.cachedValidations[txid].hex) yield this.waitForTransactionPreProcessing(txid); if (typeof this.cachedValidations[txid].validity === 'boolean') return this.cachedValidations[txid].validity; if (this.cachedValidations[txid].details) yield this.waitForCurrentValidationProcessing(txid); // Check SLP message validity let txn = new bitcore.Transaction(this.cachedValidations[txid].hex); let slpmsg; 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 === slpjs_1.SlpTransactionType.GENESIS) { return this.cachedValidations[txid].validity = true; } else if (slpmsg.transactionType === slpjs_1.SlpTransactionType.MINT) { for (let i = 0; i < txn.inputs.length; i++) { let input_txid = txn.inputs[i].prevTxId.toString('hex'); let input_txhex = yield this.getRawTransaction(input_txid); if (input_txhex) { let input_tx = new bitcore.Transaction(input_txhex); try { let input_slpmsg = this.slp.parseSlpOutputScript(input_tx.outputs[0]._scriptBuffer); if (input_slpmsg.transactionType === slpjs_1.SlpTransactionType.GENESIS) input_slpmsg.tokenIdHex = input_txid; if (input_slpmsg.tokenIdHex === slpmsg.tokenIdHex) { if (input_slpmsg.transactionType === slpjs_1.SlpTransactionType.GENESIS || input_slpmsg.transactionType === slpjs_1.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 === slpjs_1.SlpTransactionType.SEND) { let tokenOutQty = slpmsg.sendOutputs.reduce((t, v) => { return t.plus(v); }, new bignumber_js_1.default(0)); let tokenInQty = new bignumber_js_1.default(0); for (let i = 0; i < txn.inputs.length; i++) { let input_txid = txn.inputs[i].prevTxId.toString('hex'); let input_txhex = yield this.getRawTransaction(input_txid); if (input_txhex) { let input_tx = new bitcore.Transaction(input_txhex); try { let input_slpmsg = this.slp.parseSlpOutputScript(input_tx.outputs[0]._scriptBuffer); if (input_slpmsg.transactionType === slpjs_1.SlpTransactionType.GENESIS) input_slpmsg.tokenIdHex = input_txid; if (input_slpmsg.tokenIdHex === slpmsg.tokenIdHex) { if (input_slpmsg.transactionType === slpjs_1.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 === slpjs_1.SlpTransactionType.GENESIS || input_slpmsg.transactionType === slpjs_1.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 = yield 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 === slpjs_1.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 === slpjs_1.SlpTransactionType.SEND) { let validInputQty = this.cachedValidations[txid].parents.reduce((t, v) => { return v.valid ? t.plus(v.inputQty) : t; }, new bignumber_js_1.default(0)); let tokenOutQty = slpmsg.sendOutputs.reduce((t, v) => { return t.plus(v); }, new bignumber_js_1.default(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 === slpjs_1.SlpTransactionType.SEND) { let slpmsg = this.cachedValidations[txid].details; let valid = yield this.isValidSlpTxid(slpmsg.tokenIdHex); if (valid) { let genesisTxn = 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; }); } validateSlpTransactions(txids) { return __awaiter(this, void 0, void 0, function* () { let res = []; for (let i = 0; i < txids.length; i++) { res.push((yield this.isValidSlpTxid) ? txids[i] : ''); } return res.filter((id) => id.length > 0); }); } } exports.LocalValidator = LocalValidator; }).call(this,require("buffer").Buffer) },{"./slp":6,"./slpjs":7,"bignumber.js":54,"bitcore-lib-cash":55,"buffer":139}],6:[function(require,module,exports){ (function (Buffer){ "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