UNPKG

openassets-js

Version:

JavaScript implementation of the Open Assets Protocol

361 lines (305 loc) 13 kB
/* Copyright (c) 2014, Andrew Hart <hello@andrewfhart.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; var async = require('async'), buffertools = require('buffertools'), HashUtil = require('bitcore/util'), LEB128 = require('./LEB128'), MarkerOutput = require('./MarkerOutput'), Opcode = require('bitcore/lib/Opcode'), Base58 = require('bitcore/lib/Base58'), OutputType = require('./OutputType'), Parser = require('bitcore/util/BinaryParser'), Put = require('bufferput'), Script = require('bitcore/lib/Script'), Transaction = require('bitcore/lib/Transaction'), TransactionOutput = require('./TransactionOutput'); /** * ColoringEngine * * The recursive backtracking engine used to find the asset ID and asset quantity * of any transaction output. **/ /** * Constructor * @param function transactionProvider A function that accepts a transaction hash, * performs a transaction lookup, and populates * a callback. The first parameter to this * function should be the transaction hash to * look up. The second parameter should be a * callback function with the following * signature: cb(err, data). See the test cases * for this class for an example provider using * the Bitcore RpcClient library. **/ function ColoringEngine (transactionProvider) { this.transactionProvider = transactionProvider; } /** * Get an output and information about its asset ID and asset quantity. * @param Buffer transactionHash The hash of the transaction containing the output * @param int outputIndex The index of the output * @param function cb A callback function to invoke with the result **/ ColoringEngine.prototype.getOutput = function (transactionHash, outputIndex, cb) { var coloringEngine = this; coloringEngine.transactionProvider(transactionHash, function (err, data) { if (err) return cb(err); // Propagate error message from underlying JSON RPC call if (data.message) return cb(data.message); // Successful lookups will always populate 'result' if (!data.result) return cb('Transaction could not be retrieved.'); // Create and populate a Transaction object using the raw data var tx = new Transaction(); tx.parse(new Buffer(data.result,'hex')); // Compute ID and asset quantity of transaction outputs coloringEngine.colorTransaction(tx, function (err, data) { if (err) return cb(err); // If an output matching outputIndex exists, return it if (data[outputIndex]) { return cb(null, data[outputIndex]); } // Otherwise, report the error else { return cb('No data for output matching index ' + outputIndex); } }); }); } /** * Compute the asset ID and and asset quantity of every output in the transaction * @param Buffer transaction The transaction to color * @param function cb A callback function to invoke with the result. The * function should have the signature cb(err, data). If * successful, 'data' will be populated with an array * containing all the colored outputs of the transaction. **/ ColoringEngine.prototype.colorTransaction = function (transaction, cb) { var coloringEngine = this, foundMarkerOutput = false, markerOutputPayload = null, markerOutput = null; // Helper function to make the appropriate response in the case // where no valid asset ids were found in a transaction. In // this case all of the transaction outputs are considered uncolored. var makeUncoloredResponse = function (tx) { var outs = []; tx.outs.forEach(function (o) { outs.push(new TransactionOutput(o.v.readInt32LE(0),o.s)); }); return outs; }; // If the transaction is a coinbase transaction, the marker output is always invalid if (transaction.isCoinBase()) { return cb(null, makeUncoloredResponse(transaction)); } // Search transaction outputs for an Open Assets "Marker Output" transaction.outs.forEach(function (o, outIdx) { // If a valid marker is found, we can stop processing subsequent outputs // since, according to the spec: "if multiple valid marker outputs // exist in the same transaction, the first one is used and the other // ones are considered as regular outputs." [1] // [1] https://github.com/OpenAssets/open-assets-protocol/blob/master/specification.mediawiki if (!foundMarkerOutput) { // Attempt to decode this output as a Marker Output. markerOutputPayload = MarkerOutput.prototype.parseScript(o.s); // If a valid marker output payload was decoded if (markerOutputPayload) { // Extract the marker output (asset quantity and metadata) information markerOutput = MarkerOutput.prototype.deserializePayload(markerOutputPayload); // If valid marker output information was extracted, we have all the // information necessary to compute the colored outputs for this tx if (markerOutput) { foundMarkerOutput = true; // Build a recursive backtracking function for each of this // transactions inputs. Looking at the colored outputs from // transactions linked to this transaction's inputs will allow // us to dertermine which assets flow into the current transaction. var prevouts = []; transaction.inputs().forEach(function (i, idx) { prevouts.push(function (fcb) { var outHash = buffertools.reverse(i[0]).toString('hex'); coloringEngine.getOutput(outHash, i[1], fcb); }); }, coloringEngine); // Fetch the colored outputs for each previous transaction async.parallel(prevouts, function (err, inTxs){ if (err) return cb(err); // Store results of all recursive backtracking var inputs = inTxs; // Ensure all inputs were processed if (inputs.length !== transaction.ins.length) { return ("Error processing inputs: expected " + transaction.ins.length + " results, got " + inputs.length); } // Compute the asset ids of the colored outputs var outputsWithAssetIds = ColoringEngine.prototype._computeAssetIds( inputs, outIdx, transaction.outs, markerOutput.assetQuantities); if (outputsWithAssetIds) { // If successful, return the colored outputs return cb(null, outputsWithAssetIds); } else { // Otherwise, the transaction should be considered uncolored return cb(null, makeUncoloredResponse(transaction)); } }); } } } }, coloringEngine); // If no marker output was encountered in any of the transaction // outputs, all transaction outputs are considered uncolored. if (!foundMarkerOutput) { return cb(null, makeUncoloredResponse(transaction)); } }; /** * Compute Asset IDs of every output in a transaction * @param array(TransactionOutput) inputs The outputs referenced by the inputs of the transaction * @param int markerOutputIndex The position of the marker output in the transaction * @param array(TransactionOut) outputs The outputs of the transaction * @param array(int) assetQuantities The list of asset quantities of the outputs * @param function cb A callback to invoke with the array of computed asset ids * @return array(TransactionOutput) An array of transaction outputs with computed asset ids **/ ColoringEngine.prototype._computeAssetIds = function (inputs, markerOutputIndex, outputs, assetQuantities) { var coloringEngine = this, result = [], assetId, issuanceAssetId, outputAssetQuantity, curInput, inputUnitsLeft, outputUnitsLeft, progress, i; // If there are more items in the asset quantities list than outputs in // the transaction (excluding the marker output), the marker output is // deemed invalid if (assetQuantities.length > outputs.length - 1) { return false; } // If there is no input in the transaction, the marker output is always invalid if (inputs.length == 0) { return false; } // Add the issuance outputs issuanceAssetId = coloringEngine.hashScript(inputs[0].script); for (i = 0; i < markerOutputIndex; i++) { if (i < assetQuantities.length && assetQuantities[i] > 0) { result.push(new TransactionOutput( outputs[i].v.readInt32LE(0), outputs[i].s, issuanceAssetId, assetQuantities[i], OutputType.ISSUANCE)); } else { result.push(new TransactionOutput( outputs[i].v.readInt32LE(0), outputs[i].s, null, null, OutputType.ISSUANCE)); } } // Add the marker output result.push(new TransactionOutput( outputs[markerOutputIndex].v.readInt32LE(0), outputs[markerOutputIndex].s, null, null, OutputType.MARKER_OUTPUT)); // Add the transfer outputs for (i = markerOutputIndex + 1; i < outputs.length; i++) { if (i <= assetQuantities.length) { outputAssetQuantity = assetQuantities[i-1]; } else { outputAssetQuantity = 0; } outputUnitsLeft = outputAssetQuantity; assetId = null; curInput = 0; assetId = (inputs[curInput]) ? inputs[curInput].assetId : null; inputUnitsLeft = (inputs[curInput]) ? ((null == inputs[curInput].assetQuantity) ? 0 : inputs[curInput].assetQuantity) : 0; while (outputUnitsLeft > 0) { // Move to the next input if the current one is depleted if (inputUnitsLeft == 0) { curInput++; // If there are less asset units available than in the outputs // the marker output is considered invalid if (!inputs[curInput]) { return false; // Otherwise, use the assetQuantity associated with the current input } else { inputUnitsLeft = (null == inputs[curInput].assetQuantity) ? 0 : inputs[curInput].assetQuantity; } } // If the current input is colored, assign its asset id to the // current output if (inputs[curInput].assetId != null) { progress = Math.min(inputUnitsLeft, outputUnitsLeft); outputUnitsLeft -= progress; inputUnitsLeft -= progress; if (assetId == null) { // This is the first input to map to this output assetId = inputs[curInput].assetId; } else if (!buffertools.equals(assetId, inputs[curInput].assetId)) { // Another different asset ID has already been assigned to // that output. The marker output is considered invalid return false; } } } result.push(new TransactionOutput( outputs[i].v.readInt32LE(0), outputs[i].s, (outputAssetQuantity > 0) ? assetId : null, (outputAssetQuantity > 0) ? outputAssetQuantity : null, OutputType.TRANSFER)); } return result; } /** * Hash a script into an Asset ID using SHA256 followed by RIPEMD160 * @param Buffer data The data to hash * @return String The resulting Asset ID **/ ColoringEngine.prototype.hashScript = function (data) { return HashUtil.sha256ripe160(data); } /** * Convert a bitcoin address into an OpenAsset address * @param String btcAddress The bitcoin public address * @return String The resulting OpenAsset address **/ ColoringEngine.prototype.addressFromBitcoinAddress = function (btcAddress) { var btcAddr = Base58.decode(btcAddress) var btcBuff = new Put() .word8(19) .put(btcAddr.slice(0, -4)) var btcCheck = HashUtil.twoSha256(btcBuff.buffer()) btcBuff.put(btcCheck.slice(0,4)) return Base58.encode(btcBuff.buffer()); } module.exports = ColoringEngine;