bitcore-lib-cash
Version:
A pure and powerful JavaScript Bitcoin Cash library.
319 lines (297 loc) • 11.6 kB
JavaScript
'use strict';
var _ = require('lodash');
var BN = require('../crypto/bn');
var buffer = require('buffer');
var bufferUtil = require('../util/buffer');
var JSUtil = require('../util/js');
var BufferWriter = require('../encoding/bufferwriter');
var BufferReader = require('../encoding/bufferreader');
var Script = require('../script');
var $ = require('../util/preconditions');
var errors = require('../errors');
var MAX_SAFE_INTEGER = 0x1fffffffffffff;
function Output(args) {
if (!(this instanceof Output)) {
return new Output(args);
}
if (_.isObject(args)) {
this.satoshis = args.satoshis;
if (bufferUtil.isBuffer(args.script)) {
this._scriptBuffer = args.script;
} else {
var script;
if (typeof args.script === 'string' && JSUtil.isHexa(args.script)) {
script = Buffer.from(args.script, 'hex');
} else {
script = args.script;
}
this.setScript(script);
}
this.tokenData = args.tokenData;
} else {
throw new TypeError('Unrecognized argument for Output');
}
}
Object.defineProperty(Output.prototype, 'script', {
configurable: false,
enumerable: true,
get: function() {
if (this._script) {
return this._script;
} else {
this.setScriptFromBuffer(this._scriptBuffer);
return this._script;
}
}
});
Object.defineProperty(Output.prototype, 'satoshis', {
configurable: false,
enumerable: true,
get: function() {
return this._satoshis;
},
set: function(num) {
if (num instanceof BN) {
this._satoshisBN = num;
this._satoshis = num.toNumber();
} else if (typeof num === 'string') {
this._satoshis = parseInt(num);
this._satoshisBN = BN.fromNumber(this._satoshis);
} else {
$.checkArgument(
JSUtil.isNaturalNumber(num),
'Output satoshis is not a natural number'
);
this._satoshisBN = BN.fromNumber(num);
this._satoshis = num;
}
$.checkState(
JSUtil.isNaturalNumber(this._satoshis),
'Output satoshis is not a natural number'
);
}
});
const maximumAmount = new BN('9223372036854775807');
const nftCapabilityNumberToLabel = ['none', 'mutable', 'minting'];
const nftCapabilityLabelToNumber = {
'none': 0,
'mutable': 1,
'minting': 2,
};
Object.defineProperty(Output.prototype, 'tokenData', {
configurable: false,
enumerable: true,
get: function() {
return this._tokenData;
},
set: function(tokenData) {
if (typeof tokenData === "object") {
$.checkState(typeof tokenData.category !== "undefined", 'tokenData must have a category (a hex-encoded string or buffer)');
const categoryBuf = typeof tokenData.category === 'string' ? Buffer.from(tokenData.category, 'hex') : Buffer.from(tokenData.category);
$.checkState(categoryBuf.length === 32, 'tokenData must have a 32-byte category');
const category = categoryBuf.toString('hex');
$.checkState(typeof tokenData.amount !== "undefined", 'tokenData must have an amount (from 0 to 9223372036854775807)');
$.checkState(typeof tokenData.amount !== "number" || tokenData.amount <= Number.MAX_SAFE_INTEGER, 'to avoid precision loss, tokenData amount must provided as a string for values greater than 9007199254740991.');
const amount = new BN(tokenData.amount);
$.checkState(amount.gten(0), 'tokenData amount must be greater than or equal to 0');
$.checkState(amount.lte(maximumAmount), 'tokenData amount must be less than or equal to 9223372036854775807.');
if(typeof tokenData.nft === "object"){
const nft = {};
nft.capability = tokenData.nft.capability === undefined ? 'none' : String(tokenData.nft.capability);
$.checkState(nftCapabilityNumberToLabel.includes(nft.capability), 'nft capability must be "none", "mutable", or "minting".');
const commitment = tokenData.nft.commitment === undefined ? Buffer.of() : typeof tokenData.nft.commitment === 'string' ? Buffer.from(tokenData.nft.commitment, 'hex') : Buffer.from(tokenData.nft.commitment);
$.checkState(commitment.length <= 40, 'nft commitment length must be less than or equal to 40 bytes.');
nft.commitment = commitment.toString('hex');
this._tokenData = { category, amount, nft };
} else {
$.checkState(amount.gtn(0), 'tokenData must encode at least one token');
this._tokenData = { category, amount };
}
}
}
});
Output.prototype.invalidSatoshis = function() {
if (this._satoshis > MAX_SAFE_INTEGER) {
return 'transaction txout satoshis greater than max safe integer';
}
if (this._satoshis !== this._satoshisBN.toNumber()) {
return 'transaction txout satoshis has corrupted value';
}
if (this._satoshis < 0) {
return 'transaction txout negative';
}
return false;
};
Object.defineProperty(Output.prototype, 'satoshisBN', {
configurable: false,
enumerable: true,
get: function() {
return this._satoshisBN;
},
set: function(num) {
this._satoshisBN = num;
this._satoshis = num.toNumber();
$.checkState(
JSUtil.isNaturalNumber(this._satoshis),
'Output satoshis is not a natural number'
);
}
});
Output.prototype.toObject = Output.prototype.toJSON = function toObject() {
var obj = {
satoshis: this.satoshis
};
obj.script = this._scriptBuffer.toString('hex');
if(this._tokenData !== undefined) {
obj.tokenData = this._tokenData;
obj.tokenData.amount = obj.tokenData.amount.toString();
}
return obj;
};
Output.fromObject = function(data) {
return new Output(data);
};
Output.prototype.setScriptFromBuffer = function(buffer) {
this._scriptBuffer = buffer;
try {
this._script = Script.fromBuffer(this._scriptBuffer);
this._script._isOutput = true;
} catch(e) {
if (e instanceof errors.Script.InvalidBuffer) {
this._script = null;
} else {
throw e;
}
}
};
Output.prototype.setScript = function(script) {
if (script instanceof Script) {
this._scriptBuffer = script.toBuffer();
this._script = script;
this._script._isOutput = true;
} else if (typeof script === 'string') {
this._script = Script.fromString(script);
this._scriptBuffer = this._script.toBuffer();
this._script._isOutput = true;
} else if (bufferUtil.isBuffer(script)) {
this.setScriptFromBuffer(script);
} else {
throw new TypeError('Invalid argument type: script');
}
$.checkState(this._scriptBuffer[0] !== PREFIX_TOKEN, 'Invalid output script: output script may not begin with PREFIX_TOKEN (239).');
return this;
};
Output.prototype.inspect = function() {
var scriptStr;
if (this.script) {
scriptStr = this.script.inspect();
} else {
scriptStr = this._scriptBuffer.toString('hex');
}
let tokenInfo = '';
if(typeof this._tokenData !== "undefined") {
const nftInfo = typeof this._tokenData.nft === "undefined" ?
'' : `; nft [capability: ${this._tokenData.nft.capability}; commitment: ${this._tokenData.nft.commitment}]`;
tokenInfo = `(token category: ${this._tokenData.category}; amount: ${this._tokenData.amount}${nftInfo} ) `
}
return '<Output (' + this.satoshis + ' sats) ' + tokenInfo + scriptStr + '>';
};
const PREFIX_TOKEN = 0xef;
const HAS_AMOUNT = 0b00010000;
const HAS_NFT = 0b00100000;
const HAS_COMMITMENT_LENGTH = 0b01000000;
const RESERVED_BIT = 0b10000000;
const categoryLength = 32;
const tokenFormatMask = 0xf0;
const nftCapabilityMask = 0x0f;
const maximumCapability = 2;
Output.fromBufferReader = function(br) {
var obj = {};
obj.satoshis = br.readUInt64LEBN();
var size = br.readVarintNum();
if (size !== 0) {
var scriptSlot = br.read(size);
if(scriptSlot[0] === PREFIX_TOKEN) {
$.checkState(scriptSlot.length >= 34, 'Invalid token prefix: insufficient length.');
const tokenDataAndBytecode = BufferReader(scriptSlot.slice(1));
obj.tokenData = {};
obj.tokenData.category = tokenDataAndBytecode.read(categoryLength).reverse();
const tokenBitfield = tokenDataAndBytecode.readUInt8();
const prefixStructure = tokenBitfield & tokenFormatMask;
$.checkState((prefixStructure & RESERVED_BIT) === 0, 'Invalid token prefix: reserved bit is set.');
const nftCapabilityInt = tokenBitfield & nftCapabilityMask;
$.checkState(nftCapabilityInt <= maximumCapability, `Invalid token prefix: capability must be none (0), mutable (1), or minting (2). Capability value: ${nftCapabilityInt}`);
const hasNft = (prefixStructure & HAS_NFT) !== 0;
const hasCommitmentLength = (prefixStructure & HAS_COMMITMENT_LENGTH) !== 0;
if (hasCommitmentLength && !hasNft) $.checkState(false, 'Invalid token prefix: commitment requires an NFT.');
const hasAmount = (prefixStructure & HAS_AMOUNT) !== 0;
if(hasNft) {
obj.tokenData.nft = {};
obj.tokenData.nft.capability = nftCapabilityNumberToLabel[nftCapabilityInt];
if(hasCommitmentLength) {
const length = tokenDataAndBytecode.readVarintNum();
$.checkState(length > 0, 'Invalid token prefix: if encoded, commitment length must be greater than 0.');
obj.tokenData.nft.commitment = tokenDataAndBytecode.read(length);
} else {
obj.tokenData.nft.commitment = Buffer.of();
}
} else {
$.checkState(nftCapabilityInt === 0, 'Invalid token prefix: capability requires an NFT.');
$.checkState(hasAmount, 'Invalid token prefix: must encode at least one token.');
}
obj.tokenData.amount = hasAmount? tokenDataAndBytecode.readVarintBN() : new BN(0);
obj.script = tokenDataAndBytecode.readAll();
} else {
obj.script = scriptSlot;
}
} else {
obj.script = Buffer.from([]);
}
return new Output(obj);
};
Output.prototype.toBufferWriter = function(writer) {
if (!writer) {
writer = new BufferWriter();
}
writer.writeUInt64LEBN(this._satoshisBN);
var script = this._scriptBuffer;
if(typeof this._tokenData !== "undefined") {
const tokenPrefix = new BufferWriter();
tokenPrefix.writeUInt8(PREFIX_TOKEN);
tokenPrefix.write(Buffer.from(this._tokenData.category, 'hex').reverse());
const hasNft = this._tokenData.nft === undefined ? 0 : HAS_NFT;
const capabilityInt = this._tokenData.nft === undefined ?
0 : nftCapabilityLabelToNumber[this._tokenData.nft.capability];
const hasCommitmentLength = this._tokenData.nft !== undefined &&
this._tokenData.nft.commitment.length > 0 ? HAS_COMMITMENT_LENGTH : 0;
const amount = new BN(this._tokenData.amount);
const hasAmount = amount.gtn(0) ? HAS_AMOUNT : 0;
const tokenBitfield =
hasNft | capabilityInt | hasCommitmentLength | hasAmount;
tokenPrefix.writeUInt8(tokenBitfield);
if(hasCommitmentLength) {
const commitment = Buffer.from(this._tokenData.nft.commitment, 'hex');
tokenPrefix.writeVarintNum(commitment.length);
tokenPrefix.write(commitment);
}
if(hasAmount) {
tokenPrefix.writeVarintBN(amount);
}
const tokenPrefixBuffer = tokenPrefix.toBuffer();
const totalLength = tokenPrefixBuffer.length + script.length;
writer.writeVarintNum(totalLength);
writer.write(tokenPrefixBuffer);
writer.write(script);
return writer;
}
writer.writeVarintNum(script.length);
writer.write(script);
return writer;
};
Output.prototype.calculateSize = function() {
let result = 8; // satoshis
result += BufferWriter.varintBufNum(this._scriptBuffer.length).length;
result += this._scriptBuffer.length;
return result;
};
module.exports = Output;