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