bch-slpjs
Version:
Simple Ledger Protocol (SLP) JavaScript Library
915 lines (910 loc) • 1.91 MB
JavaScript
(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