slpjs
Version:
Simple Ledger Protocol (SLP) JavaScript Library
982 lines (981 loc) • 70.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
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) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spread = (this && this.__spread) || function () {
for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i]));
return ar;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Slp = void 0;
var bchaddr = __importStar(require("bchaddrjs-slp"));
var bignumber_js_1 = __importDefault(require("bignumber.js"));
var index_1 = require("../index");
var script_1 = require("./script");
var slptokentype1_1 = require("./slptokentype1");
var utils_1 = require("./utils");
var Slp = /** @class */ (function () {
function Slp(bitbox) {
if (!bitbox) {
throw Error("Must provide BITBOX instance to class constructor.");
}
this.BITBOX = bitbox;
}
Object.defineProperty(Slp.prototype, "lokadIdHex", {
get: function () { return "534c5000"; },
enumerable: false,
configurable: true
});
Slp.buildGenesisOpReturn = function (config, type) {
if (type === void 0) { type = 0x01; }
var 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, type);
};
Slp.buildMintOpReturn = function (config, type) {
if (type === void 0) { type = 0x01; }
return slptokentype1_1.SlpTokenType1.buildMintOpReturn(config.tokenIdHex, config.batonVout, config.mintQuantity, type);
};
Slp.buildSendOpReturn = function (config, type) {
if (type === void 0) { type = 0x01; }
return slptokentype1_1.SlpTokenType1.buildSendOpReturn(config.tokenIdHex, config.outputQtyArray, type);
};
Slp.parseChunkToInt = function (intBytes, minByteLen, maxByteLen, raise_on_Null) {
if (raise_on_Null === void 0) { 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");
};
Slp.preSendSlpJudgementCheck = function (txo, tokenId) {
if (txo.slpUtxoJudgement === undefined ||
txo.slpUtxoJudgement === null ||
txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.UNKNOWN) {
throw Error("There at least one input UTXO that does not have a proper SLP judgement");
}
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.UNSUPPORTED_TYPE) {
throw Error("There is at least one input UTXO that is an Unsupported SLP type.");
}
if (txo.slpUtxoJudgement === index_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 === index_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;
};
Slp.prototype.buildRawNFT1GenesisTx = function (config, type) {
if (type === void 0) { type = 0x01; }
var config2 = {
slpGenesisOpReturn: config.slpNFT1GenesisOpReturn,
mintReceiverAddress: config.mintReceiverAddress,
mintReceiverSatoshis: config.mintReceiverSatoshis,
batonReceiverAddress: null,
bchChangeReceiverAddress: config.bchChangeReceiverAddress,
input_utxos: config.input_utxos,
allowed_token_burning: [config.parentTokenIdHex],
};
return this.buildRawGenesisTx(config2);
};
Slp.prototype.buildRawGenesisTx = function (config, type) {
var e_1, _a;
if (type === void 0) { 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(function (txo) {
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.NOT_SLP) {
return;
}
if (config.allowed_token_burning &&
txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.SLP_TOKEN &&
config.allowed_token_burning.includes(txo.slpTransactionDetails.tokenIdHex)) {
return;
}
else if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.SLP_TOKEN) {
throw Error("Input UTXOs included a token for another tokenId.");
}
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.SLP_BATON) {
throw Error("Cannot spend a minting baton.");
}
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.INVALID_TOKEN_DAG ||
txo.slpUtxoJudgement === index_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 && !bchaddr.isSlpAddress(config.batonReceiverAddress)) {
throw new Error("Not an SLP address.");
}
config.mintReceiverAddress = bchaddr.toCashAddress(config.mintReceiverAddress);
var transactionBuilder = new this.BITBOX.TransactionBuilder(utils_1.Utils.txnBuilderString(config.mintReceiverAddress));
var satoshis = new bignumber_js_1.default(0);
config.input_utxos.forEach(function (token_utxo) {
transactionBuilder.addInput(token_utxo.txid, token_utxo.vout);
satoshis = satoshis.plus(token_utxo.satoshis);
});
var genesisCost = this.calculateGenesisCost(config.slpGenesisOpReturn.length, config.input_utxos.length, config.batonReceiverAddress, config.bchChangeReceiverAddress) +
(config.mintReceiverSatoshis.gt(546) ? config.mintReceiverSatoshis.toNumber() - 546 : 0) +
(config.batonReceiverSatoshis.gt(546) ? config.batonReceiverSatoshis.toNumber() - 546 : 0);
var 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)
var batonvout = this.parseSlpOutputScript(config.slpGenesisOpReturn).batonVout;
if (config.batonReceiverAddress) {
config.batonReceiverAddress = bchaddr.toCashAddress(config.batonReceiverAddress);
if (batonvout !== 2) {
throw Error("batonVout in transaction does not match OP_RETURN data.");
}
transactionBuilder.addOutput(config.batonReceiverAddress, config.batonReceiverSatoshis.toNumber());
// bchChangeAfterFeeSatoshis -= config.batonReceiverSatoshis;
}
else {
// Make sure that batonVout is set to null
if (batonvout) {
throw Error("OP_RETURN has batonVout set to vout=" + batonvout + ", but a baton receiver address was not provided.");
}
}
// Change (optional)
if (config.bchChangeReceiverAddress && bchChangeAfterFeeSatoshis.isGreaterThan(new bignumber_js_1.default(546))) {
config.bchChangeReceiverAddress = bchaddr.toCashAddress(config.bchChangeReceiverAddress);
transactionBuilder.addOutput(config.bchChangeReceiverAddress, bchChangeAfterFeeSatoshis.toNumber());
}
// sign inputs
var i = 0;
try {
for (var _b = __values(config.input_utxos), _c = _b.next(); !_c.done; _c = _b.next()) {
var txo = _c.value;
var paymentKeyPair = this.BITBOX.ECPair.fromWIF(txo.wif);
transactionBuilder.sign(i, paymentKeyPair, undefined, transactionBuilder.hashTypes.SIGHASH_ALL, txo.satoshis.toNumber());
i++;
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_1) throw e_1.error; }
}
var tx = transactionBuilder.build().toHex();
// Check For Low Fee
var outValue = transactionBuilder.transaction.tx.outs.reduce(function (v, o) { return v += o.value; }, 0);
var inValue = config.input_utxos.reduce(function (v, i) { return v = v.plus(i.satoshis); }, new bignumber_js_1.default(0));
if (inValue.minus(outValue).isLessThanOrEqualTo(tx.length / 2)) {
throw Error("Transaction input BCH amount is too low. Add more BCH inputs to fund this transaction.");
}
// TODO: Check for fee too large or send leftover to target address
return tx;
};
Slp.prototype.buildRawSendTx = function (config, type) {
// Check proper address formats are given
var e_2, _a;
if (type === void 0) { type = 0x01; }
config.tokenReceiverAddressArray.forEach(function (outputAddress) {
if (!bchaddr.isSlpAddress(outputAddress)) {
throw new Error("Token receiver address not in SlpAddr format.");
}
});
if (!bchaddr.isSlpAddress(config.bchChangeReceiverAddress)) {
throw new Error("Token/BCH change receiver address is not in SLP format.");
}
// Parse the SLP SEND OP_RETURN message
var sendMsg = this.parseSlpOutputScript(config.slpSendOpReturn);
// Make sure we're not spending inputs from any other token or baton
var tokenInputQty = new bignumber_js_1.default(0);
config.input_token_utxos.forEach(function (txo) {
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.NOT_SLP) {
return;
}
if (txo.slpUtxoJudgement === index_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 === index_1.SlpUtxoJudgement.SLP_BATON) {
throw Error("Cannot spend a minting baton.");
}
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.INVALID_TOKEN_DAG ||
txo.slpUtxoJudgement === index_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.
var chgAddr = config.bchChangeReceiverAddress ? 1 : 0;
if (!sendMsg.sendOutputs) {
throw Error("OP_RETURN contains no SLP send outputs.");
}
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 == token outputs
var outputTokenQty = sendMsg.sendOutputs.reduce(function (v, o) { return v = v.plus(o); }, new bignumber_js_1.default(0));
if (!tokenInputQty.isEqualTo(outputTokenQty)) {
throw Error("Token input quantity does not match token outputs.");
}
// Create a transaction builder
var transactionBuilder = new this.BITBOX.TransactionBuilder(utils_1.Utils.txnBuilderString(config.tokenReceiverAddressArray[0]));
// let sequence = 0xffffffff - 1;
// let locktime = 0;
// Calculate the total input amount & add all inputs to the transaction
var inputSatoshis = config.input_token_utxos.reduce(function (t, i) { return t.plus(i.satoshis); }, new bignumber_js_1.default(0));
config.input_token_utxos.forEach(function (token_utxo) { return transactionBuilder.addInput(token_utxo.txid, token_utxo.vout); }); // , sequence);
// Calculate the amount of outputs set aside for special BCH-only outputs for fee calculation
var bchOnlyCount = config.requiredNonTokenOutputs ? config.requiredNonTokenOutputs.length : 0;
var bcOnlyOutputSatoshis = config.requiredNonTokenOutputs ?
config.requiredNonTokenOutputs.reduce(function (t, v) { return t += v.satoshis; }, 0) : 0;
// Calculate mining fee cost
var sendCost = this.calculateSendCost(config.slpSendOpReturn.length, config.input_token_utxos.length, config.tokenReceiverAddressArray.length + bchOnlyCount, config.bchChangeReceiverAddress)
+
(config.extraFee ? config.extraFee : 0);
// Compute BCH change amount
var bchChangeAfterFeeSatoshis = inputSatoshis
.minus(sendCost)
.minus(bcOnlyOutputSatoshis);
// Start adding outputs to transaction
// Add SLP SEND OP_RETURN message
transactionBuilder.addOutput(config.slpSendOpReturn, 0);
// Add dust dust outputs associated with tokens
config.tokenReceiverAddressArray.forEach(function (outputAddress) {
outputAddress = bchaddr.toCashAddress(outputAddress);
transactionBuilder.addOutput(outputAddress, 546);
});
// Add BCH-only outputs
if (config.requiredNonTokenOutputs && config.requiredNonTokenOutputs.length > 0) {
config.requiredNonTokenOutputs.forEach(function (output) {
var outputAddress = bchaddr.toCashAddress(output.receiverAddress);
transactionBuilder.addOutput(outputAddress, output.satoshis);
});
}
// Add change, if any
if (bchChangeAfterFeeSatoshis.isGreaterThan(new bignumber_js_1.default(546))) {
config.bchChangeReceiverAddress = bchaddr.toCashAddress(config.bchChangeReceiverAddress);
transactionBuilder.addOutput(config.bchChangeReceiverAddress, bchChangeAfterFeeSatoshis.toNumber());
}
// Sign txn and add sig to p2pkh input for convenience if wif is provided,
// otherwise skip signing.
var i = 0;
var isComplete = true;
try {
for (var _b = __values(config.input_token_utxos), _c = _b.next(); !_c.done; _c = _b.next()) {
var txo = _c.value;
if (txo.wif) {
var paymentKeyPair = this.BITBOX.ECPair.fromWIF(txo.wif);
transactionBuilder.sign(i, paymentKeyPair, undefined, transactionBuilder.hashTypes.SIGHASH_ALL, txo.satoshis.toNumber());
}
else {
isComplete = false;
}
i++;
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_2) throw e_2.error; }
}
// Build the transaction to hex and return
// warn user if the transaction was not fully signed
var hex;
if (!isComplete) {
console.log("WARNING: Transaction signing is not complete.");
var tx = transactionBuilder.transaction.buildIncomplete();
// tx.locktime = locktime;
hex = tx.toHex();
}
else {
hex = transactionBuilder.build().toHex();
}
// Check For Low Fee
var outValue = transactionBuilder.transaction.tx.outs.reduce(function (v, o) { return v += o.value; }, 0);
var inValue = config.input_token_utxos.reduce(function (v, i) { return v = v.plus(i.satoshis); }, new bignumber_js_1.default(0));
if (inValue.minus(outValue).isLessThanOrEqualTo(hex.length / 2)) {
throw Error("Transaction input BCH amount is too low. Add more BCH inputs to fund this transaction.");
}
return hex;
};
Slp.prototype.buildRawMintTx = function (config, type) {
var e_3, _a;
if (type === void 0) { type = 0x01; }
var mintMsg = this.parseSlpOutputScript(config.slpMintOpReturn);
if (type !== mintMsg.versionType) {
throw Error("The passed 'type' parameter does not match version/type in 'config.slpMintOpReturn'");
}
else if (type === index_1.SlpVersionType.TokenVersionType1_NFT_Child) {
throw Error("Cannot MINT with this verstion/type of token.");
}
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 && !bchaddr.isSlpAddress(config.batonReceiverAddress)) {
throw new Error("Baton receiver address not in SLP format.");
}
config.mintReceiverAddress = bchaddr.toCashAddress(config.mintReceiverAddress);
if (config.batonReceiverAddress) {
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(function (txo) {
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.NOT_SLP) {
return;
}
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.SLP_TOKEN) {
throw Error("Input UTXOs should not include any tokens.");
}
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.SLP_BATON) {
if (txo.slpTransactionDetails.tokenIdHex !== mintMsg.tokenIdHex) {
throw Error("Cannot spend a minting baton.");
}
return;
}
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.INVALID_TOKEN_DAG ||
txo.slpUtxoJudgement === index_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(function (o) { return o.slpUtxoJudgement === index_1.SlpUtxoJudgement.SLP_BATON; })) {
Error("There is no baton included with the input UTXOs.");
}
var transactionBuilder = new this.BITBOX.TransactionBuilder(utils_1.Utils.txnBuilderString(config.mintReceiverAddress));
var satoshis = new bignumber_js_1.default(0);
config.input_baton_utxos.forEach(function (baton_utxo) {
transactionBuilder.addInput(baton_utxo.txid, baton_utxo.vout);
satoshis = satoshis.plus(baton_utxo.satoshis);
});
var mintCost = this.calculateGenesisCost(config.slpMintOpReturn.length, config.input_baton_utxos.length, config.batonReceiverAddress, config.bchChangeReceiverAddress)
+
(config.extraFee ? config.extraFee : 0)
+
(config.batonReceiverSatoshis.gt(546) ? config.batonReceiverSatoshis.toNumber() - 546 : 0)
+
(config.mintReceiverSatoshis.gt(546) ? config.mintReceiverSatoshis.toNumber() - 546 : 0);
// BCH change
var 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.disableBchChangeOutput && config.bchChangeReceiverAddress && bchChangeAfterFeeSatoshis.isGreaterThan(new bignumber_js_1.default(546))) {
config.bchChangeReceiverAddress = bchaddr.toCashAddress(config.bchChangeReceiverAddress);
transactionBuilder.addOutput(config.bchChangeReceiverAddress, bchChangeAfterFeeSatoshis.toNumber());
}
// Sign txn and add sig to p2pkh input for convenience if wif is provided,
// otherwise skip signing.
var i = 0;
var isComplete = true;
try {
for (var _b = __values(config.input_baton_utxos), _c = _b.next(); !_c.done; _c = _b.next()) {
var txo = _c.value;
if (txo.wif) {
var paymentKeyPair = this.BITBOX.ECPair.fromWIF(txo.wif);
transactionBuilder.sign(i, paymentKeyPair, undefined, transactionBuilder.hashTypes.SIGHASH_ALL, txo.satoshis.toNumber());
}
else {
isComplete = false;
}
i++;
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_3) throw e_3.error; }
}
// Build the transaction to hex and return
// warn user if the transaction was not fully signed
var hex;
if (!isComplete) {
console.log("WARNING: Transaction signing is not complete.");
var tx = transactionBuilder.transaction.buildIncomplete();
// tx.locktime = locktime;
hex = tx.toHex();
}
else {
hex = transactionBuilder.build().toHex();
}
// Check For Low Fee
var outValue = transactionBuilder.transaction.tx.outs.reduce(function (v, o) { return v += o.value; }, 0);
var inValue = config.input_baton_utxos.reduce(function (v, i) { return v = v.plus(i.satoshis); }, new bignumber_js_1.default(0));
if (inValue.minus(outValue).isLessThanOrEqualTo(hex.length / 2)) {
throw Error("Transaction input BCH amount is too low. Add more BCH inputs to fund this transaction.");
}
// TODO: Check for fee too large or send leftover to target address
return hex;
};
Slp.prototype.buildRawBurnTx = function (burnAmount, config, type) {
var e_4, _a;
if (type === void 0) { type = 0x01; }
var sendMsg;
if (config.slpBurnOpReturn) {
sendMsg = this.parseSlpOutputScript(config.slpBurnOpReturn);
if (!sendMsg.sendOutputs) {
throw Error("OP_RETURN contains no SLP send outputs for token change.");
}
if (sendMsg.sendOutputs.length !== 2) {
throw Error("Burn transaction must have only a single change receiver for token change.");
}
if (sendMsg.sendOutputs.length === 2 && !config.bchChangeReceiverAddress) {
throw new Error("Token/BCH change address is not provided.");
}
if (!bchaddr.isSlpAddress(config.bchChangeReceiverAddress)) {
throw new Error("Token/BCH change receiver address is not in SLP format.");
}
}
else if (!config.tokenIdHex) {
console.log("[WARNING!] Include 'config.tokenIdHex' in order to accidental token burning. To supress this log message set 'config.tokenIdHex' to an empty string.");
}
// Make sure not spending any other tokens or baton UTXOs
var tokenInputQty = new bignumber_js_1.default(0);
config.input_token_utxos.forEach(function (txo) {
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.NOT_SLP) {
return;
}
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.SLP_TOKEN) {
if (sendMsg) {
if (txo.slpTransactionDetails.tokenIdHex !== sendMsg.tokenIdHex) {
throw Error("Input UTXOs included a token for another tokenId.");
}
}
else {
if (txo.slpTransactionDetails.tokenIdHex !== config.tokenIdHex) {
throw Error("Input UTXOs included a token for another tokenId.");
}
}
tokenInputQty = tokenInputQty.plus(txo.slpUtxoJudgementAmount);
return;
}
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.SLP_BATON) {
throw Error("Cannot spend a minting baton.");
}
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.INVALID_TOKEN_DAG || txo.slpUtxoJudgement === index_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.
if (config.slpBurnOpReturn) {
// let chgAddr = config.bchChangeReceiverAddress ? 1 : 0;
// Make sure token inputs equals token outputs in OP_RETURN
var outputTokenQty = sendMsg.sendOutputs.reduce(function (v, o) { return v = v.plus(o); }, new bignumber_js_1.default(0));
if (!tokenInputQty.minus(outputTokenQty).isEqualTo(burnAmount)) {
throw Error("Token burn output quantity must be less than token input quantity.");
}
}
var transactionBuilder = new this.BITBOX.TransactionBuilder(utils_1.Utils.txnBuilderString(config.bchChangeReceiverAddress));
var inputSatoshis = new bignumber_js_1.default(0);
config.input_token_utxos.forEach(function (token_utxo) {
transactionBuilder.addInput(token_utxo.txid, token_utxo.vout);
inputSatoshis = inputSatoshis.plus(token_utxo.satoshis);
});
var msgLength = config.slpBurnOpReturn ? config.slpBurnOpReturn.length : 0;
var sendCost = this.calculateSendCost(msgLength, config.input_token_utxos.length, msgLength > 0 ? 1 : 0, config.bchChangeReceiverAddress);
var bchChangeAfterFeeSatoshis = inputSatoshis.minus(sendCost);
// Burn change OpReturn / token change output
if (config.slpBurnOpReturn) {
transactionBuilder.addOutput(config.slpBurnOpReturn, 0);
var outputAddress = bchaddr.toCashAddress(config.bchChangeReceiverAddress);
transactionBuilder.addOutput(outputAddress, 546);
}
// Change
if (bchChangeAfterFeeSatoshis.isGreaterThan(new bignumber_js_1.default(546))) {
config.bchChangeReceiverAddress = bchaddr.toCashAddress(config.bchChangeReceiverAddress);
transactionBuilder.addOutput(config.bchChangeReceiverAddress, bchChangeAfterFeeSatoshis.toNumber());
}
// sign inputs
var i = 0;
try {
for (var _b = __values(config.input_token_utxos), _c = _b.next(); !_c.done; _c = _b.next()) {
var txo = _c.value;
var paymentKeyPair = this.BITBOX.ECPair.fromWIF(txo.wif);
transactionBuilder.sign(i, paymentKeyPair, undefined, transactionBuilder.hashTypes.SIGHASH_ALL, txo.satoshis.toNumber());
i++;
}
}
catch (e_4_1) { e_4 = { error: e_4_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_4) throw e_4.error; }
}
var tx = transactionBuilder.build().toHex();
// Check For Low Fee
var outValue = transactionBuilder.transaction.tx.outs.reduce(function (v, o) { return v += o.value; }, 0);
var inValue = config.input_token_utxos.reduce(function (v, i) { return v = v.plus(i.satoshis); }, new bignumber_js_1.default(0));
if (inValue.minus(outValue).isLessThanOrEqualTo(tx.length / 2)) {
throw Error("Transaction input BCH amount is too low. Add more BCH inputs to fund this transaction.");
}
return tx;
};
Slp.prototype.buildRawBchOnlyTx = function (config) {
var e_5, _a;
config.bchReceiverAddressArray.forEach(function (outputAddress) {
if (!bchaddr.isSlpAddress(outputAddress) && !bchaddr.isCashAddress(outputAddress)) {
throw new Error("Token receiver address not in SlpAddr or CashAddr format.");
}
});
// Make sure not spending ANY tokens or baton UTXOs
config.input_token_utxos.forEach(function (txo) {
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.NOT_SLP) {
return;
}
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.SLP_TOKEN) {
throw Error("Input UTXOs included a token for another tokenId.");
}
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.SLP_BATON) {
throw Error("Cannot spend a minting baton.");
}
if (txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.INVALID_TOKEN_DAG ||
txo.slpUtxoJudgement === index_1.SlpUtxoJudgement.INVALID_BATON_DAG) {
throw Error("Cannot currently spend UTXOs with invalid DAGs.");
}
throw Error("Cannot spend utxo with no SLP judgement.");
});
var transactionBuilder = new this.BITBOX.TransactionBuilder(utils_1.Utils.txnBuilderString(config.bchReceiverAddressArray[0]));
var inputSatoshis = new bignumber_js_1.default(0);
config.input_token_utxos.forEach(function (token_utxo) {
transactionBuilder.addInput(token_utxo.txid, token_utxo.vout);
inputSatoshis = inputSatoshis.plus(token_utxo.satoshis);
});
var sendCost = this.calculateSendCost(0, config.input_token_utxos.length, config.bchReceiverAddressArray.length, config.bchChangeReceiverAddress, 1, false);
var bchChangeAfterFeeSatoshis = inputSatoshis.minus(sendCost)
.minus(config.bchReceiverSatoshiAmounts.reduce(function (t, v) { return t = t.plus(v); }, new bignumber_js_1.default(0)));
// BCH outputs
config.bchReceiverAddressArray.forEach(function (outputAddress, i) {
outputAddress = bchaddr.toCashAddress(outputAddress);
transactionBuilder.addOutput(outputAddress, Math.round(config.bchReceiverSatoshiAmounts[i].toNumber()));
});
// Change
if (bchChangeAfterFeeSatoshis.isGreaterThan(new bignumber_js_1.default(546))) {
config.bchChangeReceiverAddress = bchaddr.toCashAddress(config.bchChangeReceiverAddress);
transactionBuilder.addOutput(config.bchChangeReceiverAddress, bchChangeAfterFeeSatoshis.toNumber());
}
// sign inputs
var i = 0;
try {
for (var _b = __values(config.input_token_utxos), _c = _b.next(); !_c.done; _c = _b.next()) {
var txo = _c.value;
var paymentKeyPair = this.BITBOX.ECPair.fromWIF(txo.wif);
transactionBuilder.sign(i, paymentKeyPair, undefined, transactionBuilder.hashTypes.SIGHASH_ALL, txo.satoshis.toNumber());
i++;
}
}
catch (e_5_1) { e_5 = { error: e_5_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_5) throw e_5.error; }
}
var tx = transactionBuilder.build().toHex();
// Check For Low Fee
var outValue = transactionBuilder.transaction.tx.outs.reduce(function (v, o) { return v += o.value; }, 0);
var inValue = config.input_token_utxos.reduce(function (v, i) { return v = v.plus(i.satoshis); }, new bignumber_js_1.default(0));
if (inValue.minus(outValue).isLessThanOrEqualTo(tx.length / 2)) {
throw Error("Transaction input BCH amount is too low. Add more BCH inputs to fund this transaction.");
}
// TODO: Check for fee too large or send leftover to target address
return tx;
};
Slp.prototype.parseSlpOutputScript = function (outputScript) {
var slpMsg = {};
var chunks;
try {
chunks = this.parseOpReturnToChunks(outputScript);
}
catch (e) {
throw Error("Bad OP_RETURN");
}
if (chunks.length === 0) {
throw Error("Empty OP_RETURN");
}
if (!chunks[0]) {
throw Error("Not SLP");
}
if (!chunks[0].equals(Buffer.from(this.lokadIdHex, "hex"))) {
throw Error("Not SLP");
}
if (chunks.length === 1) {
throw Error("Missing token versionType");
}
// # check if the token version is supported
if (!chunks[1]) {
throw Error("Bad versionType buffer");
}
slpMsg.versionType = Slp.parseChunkToInt(chunks[1], 1, 2, true);
var supportedTypes = [
index_1.SlpVersionType.TokenVersionType1,
index_1.SlpVersionType.TokenVersionType1_NFT_Parent,
index_1.SlpVersionType.TokenVersionType1_NFT_Child
];
if (!supportedTypes.includes(slpMsg.versionType)) {
throw Error("Unsupported token type: " + slpMsg.versionType);
}
if (chunks.length === 2) {
throw Error("Missing SLP transaction type");
}
if (!chunks[2]) {
throw Error("Bad transaction type");
}
if (chunks[2].toString("hex") === "47454e45534953") {
slpMsg.transactionType = index_1.SlpTransactionType.GENESIS;
}
else if (chunks[2].toString("hex") === "4d494e54") {
slpMsg.transactionType = index_1.SlpTransactionType.MINT;
}
else if (chunks[2].toString("hex") === "53454e44") {
slpMsg.transactionType = index_1.SlpTransactionType.SEND;
}
else {
throw Error("Bad transaction type");
}
if (slpMsg.transactionType === index_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");
}
}
if (!chunks[7]) {
throw Error("Bad decimals buffer");
}
slpMsg.decimals = Slp.parseChunkToInt(chunks[7], 1, 1, true);
if (slpMsg.versionType === 0x41 && slpMsg.decimals !== 0) {
throw Error("NFT1 child token must have divisibility set to 0 decimal places.");
}
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;
}
if (slpMsg.versionType === 0x41 && slpMsg.batonVout !== null) {
throw Error("NFT1 child token must not have a minting baton!");
}
if (!chunks[9]) {
throw Error("Bad Genesis quantity buffer");
}
if (chunks[9].length !== 8) {
throw Error("Genesis quantity must be provided as an 8-byte buffer");
}
slpMsg.genesisOrMintQuantity = utils_1.Utils.buffer2BigNumber(chunks[9]);
if (slpMsg.versionType === 0x41 && !slpMsg.genesisOrMintQuantity.isEqualTo(1)) {
throw Error("NFT1 child token must have GENESIS quantity of 1.");
}
}
else if (slpMsg.transactionType === index_1.SlpTransactionType.SEND) {
if (chunks.length < 4) {
throw Error("SEND with too few parameters");
}
if (!chunks[3]) {
throw Error("Bad tokenId buffer");
}
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(function (chunk) {
if (!chunk) {
throw Error("Bad send quantity buffer.");
}
if (chunk.length !== 8) {
throw Error("SEND quantities must be 8-bytes each.");
}
slpMsg.sendOutputs.push(utils_1.Utils.buffer2BigNumber(chunk));
});
// # 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 === index_1.SlpTransactionType.MINT) {
if (slpMsg.versionType === 0x41) {
throw Error("NFT1 Child cannot have MINT transaction type.");
}
if (chunks.length !== 6) {
throw Error("MINT with incorrect number of parameters");
}
if (!chunks[3]) {
throw Error("Bad token_id buffer");
}
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 && slpMsg.batonVout !== undefined) {
if (slpMsg.batonVout < 2) {
throw Error("Mint baton cannot be on vout=0 or 1");
}
slpMsg.containsBaton = true;
}
if (!chunks[5]) {
throw Error("Bad Mint quantity buffer");
}
if (chunks[5].length !== 8) {
throw Error("Mint quantity must be provided as an 8-byte buffer");
}
slpMsg.genesisOrMintQuantity = utils_1.Utils.buffer2BigNumber(chunks[5]);
}
else {
throw Error("Bad transaction type");
}
if (!slpMsg.genesisOrMintQuantity && (!slpMsg.sendOutputs || slpMsg.sendOutputs.length === 0)) {
throw Error("SLP message must have either Genesis/Mint outputs or Send outputs, both are missing");
}
return slpMsg;
};
// get list of data chunks resulting from data push operations
Slp.prototype.parseOpReturnToChunks = function (script, allow_op_0, allow_op_number) {
if (allow_op_0 === void 0) { allow_op_0 = false; }
if (allow_op_number === void 0) { allow_op_number = false; }
// """Extract pushed bytes after opreturn. Returns list of bytes() objects,
// one per push.
var 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 !== script_1.Script.opcodes.OP_RETURN) {
throw Error("No OP_RETURN");
}
var chunks = [];
ops.slice(1).forEach(function (opitem) {
if (opitem.opcode > script_1.Script.opcodes.OP_16) {
throw Error("Non-push opcode");
}
if (opitem.opcode > script_1.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 === script_1.Script.opcodes.OP_1NEGATE) {
opitem.data = Buffer.from([0x81]);
}
else { // OP_1 - OP_16
opitem.data = Buffer.from([opitem.opcode - 80]);
}
}
if (opitem.opcode === script_1.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)
Slp.prototype.getScriptOperations = function (script) {
var ops = [];
try {
var n = 0;
var dlen = void 0;
while (n < script.length) {
var op = { opcode: script[n], data: null };
n += 1;
if (op.opcode <= script_1.Script.opcodes.OP_PUSHDATA4) {
if (op.opcode < script_1.Script.opcodes.OP_PUSHDATA1) {
dlen = op.opcode;
}
else if (op.opcode === script_1.Script.opcodes.OP_PUSHDATA1) {
dlen = script[n];
n += 1;
}
else if (op.opcode === script_1.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;
};
Slp.prototype.calculateGenesisCost = function (genesisOpReturnLength, inputUtxoSize, batonAddress, bchChangeAddress, feeRate) {