├── .gitignore ├── index.js ├── package.json ├── lib └── protocol │ ├── SpendableOutput.js │ ├── TransferParameters.js │ ├── OutputType.js │ ├── LEB128.js │ ├── TransactionOutput.js │ ├── TransactionBuilder.js │ ├── MarkerOutput.js │ └── ColoringEngine.js ├── test ├── LEB128.js ├── TransactionOutput.js ├── MarkerOutput.js └── ColoringEngine.js ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2014, Andrew Hart 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | var ColoringEngine = require('./lib/protocol/ColoringEngine'); 17 | 18 | module.exports.ColoringEngine = ColoringEngine; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openassets", 3 | "version": "0.1.1", 4 | "description": "JavaScript implementation of the Open Assets Protocol", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: see README for instructions on running tests\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/andrewfhart/openassets" 12 | }, 13 | "keywords": [ 14 | "Open", 15 | "Assets", 16 | "Protocol", 17 | "OAP", 18 | "Colored", 19 | "Coins", 20 | "Blockchain", 21 | "Smart", 22 | "Property" 23 | ], 24 | "author": "Andrew Hart ", 25 | "license": "Apache Software License Version 2", 26 | "bugs": { 27 | "url": "https://github.com/andrewfhart/openassets/issues" 28 | }, 29 | "homepage": "https://github.com/andrewfhart/openassets", 30 | "devDependencies": { 31 | "assert": "^1.1.2", 32 | "chai": "^1.9.2", 33 | "mocha": "^2.0.1", 34 | "should": "^4.2.1", 35 | "sinon": "^1.11.1" 36 | }, 37 | "dependencies": { 38 | "async": "^0.9.0", 39 | "bitcore": "^0.1.39", 40 | "bufferput": "^0.1.2", 41 | "buffertools": "^2.1.2", 42 | "leb": "^0.3.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/protocol/SpendableOutput.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2014, Andrew Hart 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | 'use strict'; 17 | 18 | /** 19 | * SpendableOutput 20 | * 21 | * Represents a transaction output with information about the asset quantity 22 | * and asset id associated with it 23 | **/ 24 | 25 | /** 26 | * Constructor 27 | * @param TransactionOut outPoint An object that can be used to locate the output 28 | * @param TransactionOutput output The actual output object 29 | **/ 30 | var SpendableOutput = function (outPoint, output) { 31 | this.outPoint = outPoint; 32 | this.output = output; 33 | }; 34 | 35 | // The public API for this object 36 | module.exports = SpendableOutput; 37 | -------------------------------------------------------------------------------- /lib/protocol/TransferParameters.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2014, Andrew Hart 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | 'use strict'; 17 | 18 | /** 19 | * TransferParameters 20 | * 21 | * Encapsulates the details of a Bitcoin or asset transfer 22 | **/ 23 | 24 | /** 25 | * Constructor 26 | * @param array(SpendableOutput) unspentOutputs The unspent outputs available for the transaction 27 | * @param Buffer toScript The output script to which to send Bitcoin/assets 28 | * @param Buffer changeScript The output script to which to send remaining change 29 | * @param int amount The asset quantity or number of satoshis sent in the tx 30 | **/ 31 | var TransferParameters = function (unspentOutputs, toScript, changeScript, amount) { 32 | self.unspentOutputs = unspentOutputs; 33 | self.toScript = toScript; 34 | self.changeScript = changeScript; 35 | self.amount = amount; 36 | }; 37 | 38 | // The public API for this object 39 | module.exports = TransferParameters; 40 | -------------------------------------------------------------------------------- /lib/protocol/OutputType.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2014, Andrew Hart 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | 'use strict'; 17 | 18 | /** 19 | * OutputType 20 | * 21 | * Represents the four types of transaction outputs defined 22 | * by the Open Assets Protocol[1]. 23 | * 24 | * References: 25 | * [1] https://github.com/OpenAssets/open-assets-protocol/blob/master/specification.mediawiki#Open_Assets_Transactions 26 | * 27 | **/ 28 | 29 | /** Constants **/ 30 | var UNCOLORED = 0; 31 | var MARKER_OUTPUT = 1; 32 | var ISSUANCE = 2; 33 | var TRANSFER = 3; 34 | 35 | function reverseMap (value) { 36 | switch (value) { 37 | case UNCOLORED: return "UNCOLORED"; 38 | case MARKER_OUTPUT: return "MARKER_OUTPUT"; 39 | case ISSUANCE: return "ISSUANCE"; 40 | case TRANSFER: return "TRANSFER"; 41 | default: return "Unknown"; 42 | } 43 | } 44 | 45 | // The public API for this object 46 | module.exports.reverseMap = reverseMap; 47 | module.exports.UNCOLORED = UNCOLORED; 48 | module.exports.MARKER_OUTPUT = MARKER_OUTPUT; 49 | module.exports.ISSUANCE = ISSUANCE; 50 | module.exports.TRANSFER = TRANSFER; 51 | -------------------------------------------------------------------------------- /lib/protocol/LEB128.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2014, Andrew Hart 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | 'use strict'; 17 | 18 | var leb = require('leb'); 19 | 20 | /** 21 | * LEB128 22 | * 23 | * Provide operations for serializing/deserializing integer data into 24 | * variable-length 64-bit LEB128 integer encoding [1]. 25 | * 26 | * References: 27 | * [1] https://en.wikipedia.org/wiki/LEB128 28 | * 29 | **/ 30 | var LEB128 = { 31 | 32 | /** 33 | * Encode an arbitrarily large positive integer value using a small 34 | * number of bytes. This function returns a Buffer containing the 35 | * byte values of the 64-bit LEB128-encoded integer. 36 | **/ 37 | encode: function (value) { 38 | 39 | if (value < 0) { 40 | throw new Error('Negative values are not supported'); 41 | } 42 | 43 | return leb.encodeUInt64(value); 44 | 45 | }, 46 | 47 | /** 48 | * Decode a data Buffer containing a 64-bit LEB128-encoded integer. 49 | **/ 50 | decode: function (data, offset) { 51 | 52 | if (!Buffer.isBuffer(data)) { 53 | throw new Error('Data to decode must be a Buffer object'); 54 | } 55 | 56 | return leb.decodeUInt64(data, offset ? offset : 0).value; 57 | 58 | }, 59 | 60 | /** 61 | * Decode a data Buffer containing a 64-bit LEB128-encoded integer 62 | * and return all metadata about the operation 63 | * 64 | * The metadata contains the following fields: 65 | * value: The value of the extracted LEB128-encoded integer 66 | * nextIndex: The next unseen/unused byte in the input buffer 67 | * lossy: Whether or not the extraction involved loss of precision 68 | * 69 | * Example return value: { value: 1, nextIndex: 6, lossy: false } 70 | **/ 71 | decodeWithMetadata: function (data, offset) { 72 | 73 | if (!Buffer.isBuffer(data)) { 74 | throw new Error('Data to decode must be a Buffer object'); 75 | } 76 | 77 | return leb.decodeUInt64(data, offset ? offset : 0); 78 | } 79 | 80 | }; 81 | 82 | 83 | // Public API for this object 84 | module.exports = LEB128; 85 | -------------------------------------------------------------------------------- /lib/protocol/TransactionOutput.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2014, Andrew Hart 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | 'use strict'; 17 | 18 | var LEB128 = require('./LEB128'), 19 | MarkerOutput = require('./MarkerOutput'), 20 | Opcode = require('bitcore/lib/Opcode'), 21 | OutputType = require('./OutputType'), 22 | Parser = require('bitcore/util/BinaryParser'), 23 | Put = require('bufferput'), 24 | Script = require('bitcore/lib/Script'); 25 | 26 | /** 27 | * Transaction Output 28 | * 29 | * Represents an Open Assets transaction output and its asset 30 | * ID and asset quantity. 31 | **/ 32 | 33 | /** 34 | * Constructor 35 | * @param int value The satoshi value of the output 36 | * @param Buffer script The script controlling redemption of the output 37 | * @param Buffer assetId The asset ID of the output 38 | * @param int assetQuantity The asset quantity of the output 39 | * @param int outputType The type of the output 40 | **/ 41 | function TransactionOutput (value, script, assetId, assetQuantity, outputType) { 42 | 43 | // Ensure script is a valid Buffer object 44 | if (script && !Buffer.isBuffer(script)) { 45 | throw new Error("Script must be a valid Buffer object"); 46 | } 47 | 48 | // Ensure assetId is a valid Buffer object 49 | if (assetId && !Buffer.isBuffer(assetId)) { 50 | throw new Error("Asset ID must be a valid Buffer object"); 51 | } 52 | 53 | // Ensure asset quantity is within range 54 | if (assetQuantity && (assetQuantity < 0 || assetQuantity > MarkerOutput.MAX_ASSET_QUANTITY)) { 55 | throw new Error( 56 | "Asset quantity out of supported range (0-" 57 | + MarkerOutput.MAX_ASSET_QUANTITY + ")"); 58 | } 59 | 60 | // Ensure outputType is a valid identifier 61 | if (outputType && (outputType < OutputType.UNCOLORED || outputType > OutputType.TRANSFER)) { 62 | throw new Error("Unsupported output type specified"); 63 | } 64 | 65 | // Assign arguments 66 | this.value = (value != undefined) ? value : null; 67 | this.script = script || null; 68 | this.assetId = assetId || null; 69 | this.assetQuantity = assetQuantity || null; 70 | this.outputType = outputType || OutputType.UNCOLORED; 71 | 72 | } 73 | 74 | TransactionOutput.prototype.toString = function () { 75 | return 'TransactionOutput(' + 76 | 'value=' + this.value + ', ' + 77 | 'script=' + ((this.script) ? '0x' + this.script.toString('hex') : null) + ', ' + 78 | 'assetId=' + ((this.assetId) ? '0x' + this.assetId.toString('hex'): null) + ', ' + 79 | 'assetQuantity=' + this.assetQuantity + ', ' + 80 | 'outputType=' + OutputType.reverseMap(this.outputType) + ')'; 81 | } 82 | 83 | module.exports = TransactionOutput; 84 | -------------------------------------------------------------------------------- /test/LEB128.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | process.env.NODE_ENV = process.env.NODE_ENV || 'test'; 6 | 7 | var assert = require('assert'), 8 | chai = require('chai'), 9 | expect = chai.expect, 10 | should = chai.should(), 11 | sinon = require('sinon'), 12 | LEB128 = require('../lib/protocol/LEB128'); 13 | 14 | describe("LEB128 Encoder/Decoder", function () { 15 | 16 | describe('::encode', function () { 17 | 18 | it('should accept only positive integer value', function (done) { 19 | var encode_string = function () { LEB128.encode('abc'); }; 20 | var encode_float = function () { LEB128.encode(1.5); }; 21 | var encode_negative = function () { LEB128.encode(-1); }; 22 | expect(encode_string).to.throw(Error); 23 | expect(encode_float).to.throw(Error); 24 | expect(encode_negative).to.throw(Error); 25 | done(); 26 | }); 27 | 28 | it('should encode non-negative integer value correctly', function (done) { 29 | var v0 = LEB128.encode(0); 30 | var v1 = LEB128.encode(300); 31 | var v2 = LEB128.encode(624485); 32 | v0.toString('hex').should.equal('00'); 33 | v1.toString('hex').should.equal('ac02'); 34 | v2.toString('hex').should.equal('e58e26'); 35 | done(); 36 | }); 37 | 38 | }); 39 | 40 | describe('::decode', function () { 41 | 42 | it('should accept only Buffer objects', function (done) { 43 | var decode_string = function () { LEB128.decode('0x00'); }; 44 | var decode_array = function () { LEB128.decode([0xac,0x02]); }; 45 | var decode_integer = function () { LEB128.decode(-1); }; 46 | var decode_nothing = function () { LEB128.decode(); }; 47 | expect(decode_string).to.throw(Error); 48 | expect(decode_array).to.throw(Error); 49 | expect(decode_integer).to.throw(Error); 50 | done(); 51 | }); 52 | 53 | it('should decode LEB128-encoded integer value correctly', function (done) { 54 | var v0 = LEB128.decode(Buffer([0])); 55 | var v1 = LEB128.decode(Buffer([0xac,0x02])); 56 | var v2 = LEB128.decode(Buffer([0xe5,0x8e,0x26])); 57 | v0.should.equal(0); 58 | v1.should.equal(300); 59 | v2.should.equal(624485); 60 | done(); 61 | }); 62 | 63 | it('should decode LEB128-encoded integer value correctly given an offset', function (done) { 64 | var v0 = LEB128.decode(Buffer([0xff,0x34,0xff,0xe5,0x8e,0x26,0x00,0x00]), 3); 65 | v0.should.equal(624485); 66 | done(); 67 | }); 68 | 69 | }); 70 | 71 | describe('::decodeWithMetadata', function () { 72 | 73 | it('should accept only Buffer object', function (done) { 74 | var decode_string = function () { LEB128.decodeWithMetadata('0x00'); }; 75 | var decode_array = function () { LEB128.decodeWithMetadata([0xac,0x02]); }; 76 | var decode_integer = function () { LEB128.decodeWithMetadata(-1); }; 77 | var decode_nothing = function () { LEB128.decodeWithMetadata(); }; 78 | expect(decode_string).to.throw(Error); 79 | expect(decode_array).to.throw(Error); 80 | expect(decode_integer).to.throw(Error); 81 | done(); 82 | }); 83 | 84 | it('should decode LEB128-encoded integer value correctly', function (done) { 85 | var v0 = LEB128.decodeWithMetadata(Buffer([0])); 86 | var v1 = LEB128.decodeWithMetadata(Buffer([0xac,0x02])); 87 | var v2 = LEB128.decodeWithMetadata(Buffer([0xe5,0x8e,0x26])); 88 | v0.should.deep.equal({value: 0, nextIndex: 1, lossy: false}); 89 | v1.should.deep.equal({value: 300, nextIndex: 2, lossy: false}); 90 | v2.should.deep.equal({value: 624485, nextIndex: 3, lossy: false}); 91 | done(); 92 | }); 93 | 94 | it('should decode LEB128-encoded integer value correctly given an offset', function (done) { 95 | var v0 = LEB128.decodeWithMetadata(Buffer([0xff,0xff,0xff,0xe5,0x8e,0x26,0x00,0x00]), 3); 96 | v0.should.deep.equal({value: 624485, nextIndex: 6, lossy: false}); 97 | done(); 98 | }); 99 | }); 100 | 101 | }); 102 | 103 | -------------------------------------------------------------------------------- /test/TransactionOutput.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | process.env.NODE_ENV = process.env.NODE_ENV || 'test'; 6 | 7 | var assert = require('assert'), 8 | chai = require('chai'), 9 | expect = chai.expect, 10 | should = chai.should(), 11 | sinon = require('sinon'), 12 | OutputType = require('../lib/protocol/OutputType'), 13 | TransactionOutput = require('../lib/protocol/TransactionOutput'); 14 | 15 | 16 | describe("TransactionOutput", function () { 17 | 18 | describe('::constructor', function () { 19 | 20 | var value, script, assetId, assetQuantity, outputType, 21 | assetQuantityValid, assetQuantityTooSmall, assetQuantityTooBig, 22 | outputTypeValid, outputTypeInvalid; 23 | 24 | beforeEach(function () { 25 | value = 500; 26 | script = new Buffer([0x01,0x02,0x03,0x04]); 27 | assetId = new Buffer([0x01,0x02]); 28 | assetQuantityValid = Math.pow(2,20); 29 | assetQuantityTooSmall = -1; 30 | assetQuantityTooBig = Math.pow(2,65); 31 | outputTypeValid = OutputType.TRANSFER; 32 | outputTypeInvalid = -1; 33 | }); 34 | 35 | it('should apply defaults if only given a value', function (done) { 36 | var to = new TransactionOutput(value); 37 | to.value.should.equal(value); 38 | expect(to.script).to.be.null; 39 | expect(to.assetId).to.be.null; 40 | expect(to.assetId).to.be.null; 41 | to.outputType.should.equal(OutputType.UNCOLORED); 42 | done(); 43 | }); 44 | 45 | it('should correctly store the provided script', function (done) { 46 | var to = new TransactionOutput(value, script); 47 | to.script.should.deep.equal(script); 48 | done(); 49 | }); 50 | 51 | it('should correctly store the provided asset ID', function (done) { 52 | var to = new TransactionOutput(value, script, assetId); 53 | to.assetId.should.deep.equal(assetId); 54 | done(); 55 | }); 56 | 57 | it('should correctly store the provided asset quantity', function (done) { 58 | var to = new TransactionOutput(value, script, assetId, assetQuantityValid); 59 | to.assetQuantity.should.deep.equal(assetQuantityValid); 60 | done(); 61 | }); 62 | 63 | it('should correctly store the provided output type', function (done) { 64 | var to = new TransactionOutput(value, script, assetId, assetQuantityValid, outputTypeValid); 65 | to.outputType.should.deep.equal(outputTypeValid); 66 | done(); 67 | }); 68 | 69 | it('should fail if asset quantity is out of range', function (done) { 70 | expect(function() {var to = new TransactionOutput(value, script, assetId, assetQuantityTooSmall);}) 71 | .to.throw(Error); 72 | expect(function() {var to = new TransactionOutput(value, script, assetId, assetQuantityTooBig);}) 73 | .to.throw(Error); 74 | done(); 75 | }); 76 | 77 | it('should fail if unsupported output type specified', function (done) { 78 | expect(function() {var to = new TransactionOutput(value, script, assetId, assetQuantityValid, outputTypeInvalid);}) 79 | .to.throw(Error); 80 | done(); 81 | }); 82 | 83 | }); 84 | 85 | describe('::toString', function () { 86 | 87 | var to, value, script, assetId, assetQuantityValid, outputTypeValid; 88 | 89 | beforeEach(function () { 90 | value = 500; 91 | script = new Buffer([0x01,0x02,0x03,0x04]); 92 | assetId = new Buffer([0x01,0x02]); 93 | assetQuantityValid = Math.pow(2,20); 94 | outputTypeValid = OutputType.TRANSFER; 95 | }); 96 | 97 | it('should generate a string representation of a default TransactionOutput', function (done) { 98 | to = new TransactionOutput(); 99 | expect(to.toString()).to.equal( 100 | 'TransactionOutput(value=null, script=null, assetId=null, assetQuantity=null, outputType=UNCOLORED)'); 101 | done(); 102 | }); 103 | 104 | it('should generate a string representation of a custom TransactionOutput', function (done) { 105 | to = new TransactionOutput(value, script, assetId, assetQuantityValid, outputTypeValid); 106 | expect(to.toString()).to.equal( 107 | 'TransactionOutput(value=500, script=0x01020304, assetId=0x0102, assetQuantity=1048576, outputType=TRANSFER)'); 108 | done(); 109 | }); 110 | }); 111 | 112 | }); 113 | -------------------------------------------------------------------------------- /lib/protocol/TransactionBuilder.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2014, Andrew Hart 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | 'use strict'; 17 | 18 | var bitcore = require('bitcore'), 19 | MarkerOutput = require('./MarkerOutput'); 20 | 21 | /** 22 | * TransactionBuilder 23 | * 24 | * Provides methods for constructing Open Assets transactions 25 | **/ 26 | 27 | /** 28 | * Constructor 29 | * @param int dustAmount The minimum allowed output data 30 | **/ 31 | var TransactionBuilder = function (dustAmount) { 32 | self.dustAmount = dustAmount; 33 | }; 34 | 35 | /** 36 | * Create a transaction for issuing an asset 37 | * @param TransferParameters issuanceSpec The parameters of the issuance 38 | * @param Buffer metadata The metadata to be embedded in the transaction 39 | * @param int fees The fees to include in the transaction 40 | * @return Transaction an unsigned transaction for issuing an asset 41 | **/ 42 | TransactionBuilder.prototype.issue = function (issuanceSpec, metadata, fees) { 43 | var inputs, totalAmount, outputInfo, builder, tx; 44 | 45 | // Returns an object containing two keys: inputs, totalAmount 46 | outputInfo = _collectUncoloredOutputs( 47 | issuanceSpec.unspentOutputs, 2 * self.dustAmount + fees); 48 | 49 | builder = (new bitcore.TransactionBuilder({ 50 | feeSat: fees, 51 | amountSat: outputInfo.totalAmount 52 | })) 53 | .setUnspent('foo') 54 | .setOutputs('foo'); 55 | 56 | 57 | 58 | }; 59 | 60 | TransactionBuilder.prototype.transfer = function (transferSpec, fees) { 61 | 62 | }; 63 | 64 | TransactionBuilder.prototype.transferBitcoin = function (transferSpec, fees) { 65 | 66 | }; 67 | 68 | TransactionBuilder.prototype.transferAssets = function (assetId, transferSpec, btcChangeScript, fees) { 69 | 70 | }; 71 | 72 | TransactionBuilder.prototype.bitcoinAssetSwap = function (btcTransferSpec, assetId, assetTransferSpec, fees) { 73 | 74 | }; 75 | 76 | TransactionBuilder.prototype.assetAssetSwap = function (asset1Id, asset1TransferSpec, asset2Id, asset2TransferSpec, fees) { 77 | 78 | }; 79 | 80 | /** 81 | * Return a list of uncolored outputs for the specified amount 82 | * @param array(SpendableOutput) unspentOutputs The list of available outputs 83 | * @param int amount The amount to collect 84 | * @return Object(outputs:array(SpendableOutput),totalAmount:int) 85 | **/ 86 | TransactionBuilder.prototype._collectUncoloredOutputs = function(unspentOutputs, amount) { 87 | var totalAmount = 0, result = []; 88 | 89 | unspentOutputs.forEach(function (spendableOutput) { 90 | if (spendableOutput.output.assetId == null) { 91 | result.push(spendableOutput); 92 | totalAmount += spendableOutput.output.value); 93 | } 94 | 95 | if (totalAmount >= amount) { 96 | return { 97 | outputs: result, 98 | totalAmount: totalAmount 99 | }; 100 | } 101 | 102 | throw new Error("Insufficient funds"); 103 | }); 104 | }; 105 | 106 | /** 107 | * Create an uncolored output 108 | * @param Buffer script The output script 109 | * @param int value The satoshi value of the output 110 | * @return Object An object representing the uncolored output 111 | **/ 112 | TransactionBuilder.prototype._getUncoloredOutput = function (script, value) { 113 | if (value < this.dustAmount) { 114 | throw Error('The value of the output would be too small, and the output would be considered "dust"'); 115 | } 116 | 117 | return {v: value, s: script}; 118 | }; 119 | 120 | /** 121 | * Return a list of colored outputs for the specified quantity 122 | * @param array(SpendableOutput) unspentOutputs The list of available outputs 123 | * @param Buffer assetId The ID of the asset to collect 124 | * @param int quantity The asset quantity to collect 125 | * @return Object(outputs:array(SpendableOutput),quantity:int) 126 | **/ 127 | TransactionBuilder.prototype._collectColoredOutputs = function (unspentOutputs, assetId, assetQuantity) { 128 | var totalQuantity = 0, 129 | result = []; 130 | 131 | unspentOutputs.forEach(function (spendableOutput) { 132 | if (spendableOutput.output.assetId == assetId) { 133 | result.push(spendableOutput); 134 | totalQuantity += spendableOutput.output.assetQuantity; 135 | } 136 | 137 | if (totalQuantity >= assetQuantity) { 138 | return {outputs: result, quantity: totalQuantity}; 139 | } 140 | }); 141 | 142 | throw new Error("An insufficient amount of assets is available"); 143 | }; 144 | 145 | /** 146 | * Create a colored output 147 | * @param Buffer script The output script 148 | * @return Object An object representing the colored output 149 | **/ 150 | TransactionBuilder.prototype._getColoredOutput = function (script) { 151 | return {v: this.dustAmount, s: script}; 152 | }; 153 | 154 | /** 155 | * Create a marker output 156 | * 157 | * @param array(int) assetQuantities The asset quantity list 158 | * @param Buffer metadata The metadata contained in the output 159 | * @return Object An object representing the marker output 160 | **/ 161 | TransactionBuilder.prototype._getMarkerOutput = function (assetQuantities, metadata) { 162 | var output = new MarkerOutput(assetQuantities, metadata), 163 | payload = mo.serializePayload(), 164 | script = mo.buildScript(payload); 165 | return {v: 0, s: script}; // Marker output has value 0 satoshis 166 | }; 167 | -------------------------------------------------------------------------------- /test/MarkerOutput.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | process.env.NODE_ENV = process.env.NODE_ENV || 'test'; 6 | 7 | var assert = require('assert'), 8 | chai = require('chai'), 9 | expect = chai.expect, 10 | should = chai.should(), 11 | sinon = require('sinon'), 12 | MarkerOutput = require('../lib/protocol/MarkerOutput'); 13 | 14 | describe("MarkerOutput", function () { 15 | 16 | describe('::constructor', function () { 17 | 18 | var mo; 19 | 20 | beforeEach( function () { 21 | mo = new MarkerOutput([1,2,3], Buffer('metadata', 'ascii')); 22 | }); 23 | 24 | it('should store provided asset quantities list', function (done) { 25 | mo.assetQuantities.should.deep.equal([1,2,3]); 26 | done(); 27 | }); 28 | 29 | it('should store provided metadata', function (done) { 30 | mo.metadata.toString().should.equal('metadata'); 31 | done(); 32 | }); 33 | 34 | it('should fail if an asset quantity is greater than MAX_ASSET_QUANTITY*', function (done) { 35 | // NOTE: See comments in MarkerOutput::constructor for caveats (*) to this test 36 | var fn = function () { var mo = new MarkerOutput([Math.pow(2,64)]); }; 37 | expect(fn).to.throw(Error); 38 | done(); 39 | }); 40 | 41 | it('should handle empty metadata', function (done) { 42 | var mo = new MarkerOutput([1, 2, 3]); 43 | mo.metadata.toString('hex').should.equal(''); 44 | done(); 45 | }); 46 | 47 | it('should ignore malformed metadata', function (done) { 48 | var mo = new MarkerOutput([1, 2, 3], 'metadata'); 49 | mo.metadata.toString('hex').should.equal(''); 50 | done(); 51 | }); 52 | }); 53 | 54 | 55 | describe('::serializePayload', function () { 56 | 57 | var mo, buf, bs; 58 | 59 | beforeEach( function () { 60 | /* 61 | * The marker output in this test corresponds to the following 62 | * asset definition: 63 | * Output 0: Issue 1 asset 64 | * Output 1: Marker output (0, skip) 65 | * Output 2: Transfer 300 assets 66 | * Output 3: Transfer 624485 assets 67 | * The metadata associated with this transaction is 'metadata' 68 | */ 69 | mo = new MarkerOutput([1,300,624485], Buffer('metadata', 'ascii')); 70 | buf = mo.serializePayload(); 71 | bs = [].slice.call(buf); 72 | }); 73 | 74 | it('should include the Open Assets tag', function (done) { 75 | bs.slice(0,2).should.deep.equal([0x4f,0x41]); 76 | done(); 77 | }); 78 | 79 | it('should include the Open Assets version number', function (done) { 80 | bs.slice(2,4).should.deep.equal([0x01,0x00]); 81 | done(); 82 | }); 83 | 84 | it('should include the length of the assetQuantities list', function (done) { 85 | bs.slice(4,5).should.deep.equal([0x03]); 86 | done(); 87 | }); 88 | 89 | it('should include each asset quantity as an LEB128-encoded varint', function (done) { 90 | // 1 -> 0x01 91 | // 300 -> 0xac 0x02 92 | // 624485 -> 0xe5 0x8e 0x26 93 | bs.slice(5,11).should.deep.equal([0x01, 0xac, 0x02, 0xe5, 0x8e, 0x26]); 94 | done(); 95 | }); 96 | 97 | it('should include the length of the metadata', function (done) { 98 | bs.slice(11,12).should.deep.equal([0x08]); // 'metadata'.length = 8 99 | done(); 100 | }); 101 | 102 | it('should include the metadata', function (done) { 103 | bs.slice(12,20).should.deep.equal( 104 | [0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61]); // 'metadata'.length = 8 105 | done(); 106 | }); 107 | }); 108 | 109 | describe('::deserializePayload', function () { 110 | 111 | var mo, buf, data; 112 | 113 | beforeEach(function () { 114 | mo = new MarkerOutput([1, 300, 624485], Buffer('metadata','ascii')); 115 | buf = mo.serializePayload(); 116 | data = mo.deserializePayload(buf); 117 | }); 118 | 119 | it('should recover each asset quantity', function (done) { 120 | data.assetQuantities.should.deep.equal([1, 300, 624485]); 121 | done(); 122 | }); 123 | 124 | it('should recover the content of the metadata', function (done) { 125 | data.metadata.toString('ascii').should.equal('metadata'); 126 | done(); 127 | }); 128 | 129 | it('should fail if not given a valid payload', function (done) { 130 | var fn = function () {mo.deserializePayload('asdfjkl;');}; 131 | expect(fn).to.throw(Error); 132 | done(); 133 | }); 134 | 135 | }); 136 | 137 | describe('::buildScript', function () { 138 | 139 | var mo, buf, sb, bs; 140 | 141 | beforeEach(function () { 142 | mo = new MarkerOutput([1, 300, 624485], Buffer('metadata','ascii')); 143 | buf = mo.serializePayload(); // 20 bytes 144 | sb = mo.buildScript(buf); // 22 bytes 145 | bs = [].slice.call(sb); 146 | }); 147 | 148 | it('should build an output script with the OP_RETURN opcode', function (done) { 149 | bs.slice(0,1).should.deep.equal([0x6a]); // OP_RETURN (106) 150 | done(); 151 | }); 152 | 153 | it('should apply the correct PUSHDATA opcode for the data length', function (done) { 154 | bs.slice(1,2).should.deep.equal([0x14]); // 20 byte payload 155 | done(); 156 | }); 157 | 158 | it('should have the correct length', function (done) { 159 | bs.length.should.equal(buf.length + 2); //(RETURN + PUSHDATA + buf (20 byte payload)) 160 | done(); 161 | }); 162 | 163 | it('should include the serialized payload', function (done) { 164 | Buffer(bs.slice(2,22)).should.deep.equal(buf); 165 | done(); 166 | }); 167 | 168 | it('should fail if not given a valid payload', function (done) { 169 | var fn = function () {mo.buildScript('asdfjkl;');}; 170 | expect(fn).to.throw(Error); 171 | done(); 172 | }); 173 | 174 | }); 175 | 176 | describe('::parseScript', function () { 177 | 178 | var mo, buf; 179 | 180 | beforeEach(function () { 181 | mo = new MarkerOutput([1, 300, 624485], Buffer('metadata','ascii')); 182 | buf = mo.buildScript(mo.serializePayload()); 183 | }); 184 | 185 | it('should fail if it is not an OP_RETURN script', function (done) { 186 | var test = Buffer.concat([Buffer([0x00]), buf.slice(1)]); // replace 1st byte with 0x00 187 | mo.parseScript(test).should.equal(false); 188 | done(); 189 | }); 190 | 191 | it('should fail if there is no payload after the opcode', function (done) { 192 | var test = Buffer([0x6a]); // Just OP_RETURN, nothing else 193 | mo.parseScript(test).should.equal(false); 194 | done(); 195 | }); 196 | 197 | it('should fail if the payload does not start with the Open Assets tag', function (done) { 198 | var test = Buffer([0x6a, 0x14, 0x00, 0x00]); // OP_RETURN, PUSHDATA 20 bytes, no OA tag 199 | mo.parseScript(test).should.equal(false); 200 | done(); 201 | }); 202 | 203 | it ('should correctly extract asset quantities', function (done) { 204 | var test = mo.parseScript(buf); 205 | mo.deserializePayload(test).assetQuantities.should.deep.equal([1, 300, 624485]); 206 | done(); 207 | }); 208 | 209 | it ('should correctly extract metadata', function (done) { 210 | var test = mo.parseScript(buf); 211 | mo.deserializePayload(test).metadata.toString('ascii').should.equal('metadata'); 212 | done(); 213 | }); 214 | 215 | }); 216 | 217 | 218 | 219 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | openassets 2 | ========== 3 | 4 | A JavaScript implementation of the [Open Assets Protocol](https://github.com/OpenAssets/open-assets-protocol). 5 | 6 | About the Open Assets Protocol 7 | ------------------------------ 8 | The Open Assets Protocol is a simple and powerful protocol built on top of the Bitcoin Blockchain. It allows issuance and transfer of user-created assets. The Open Assets Protocol is an evolution of the concept of colored coins. 9 | 10 | The protocol specification is publicly maintained at the official [OpenAssets repository](https://github.com/OpenAssets/open-assets-protocol). The [reference implementation](https://github.com/OpenAssets/openassets) exists as a Python module. This project provides a translation to JavaScript, packaged as an `npm` module. 11 | 12 | Installation 13 | ------------ 14 | 15 | OpenAssets is provided as an [npm](https://www.npmjs.org/package/openassets) module for easy installation: 16 | 17 | ```npm install openassets``` 18 | 19 | Configuration 20 | ------------- 21 | 22 | OpenAssets provides a recursive backtracking "coloring engine" that, when given a Bitcoin transaction hash and a transaction output index, is capable of traversing the Bitcoin blockchain to identify and trace colored coin issuance and transfer activity conforming to the Open Assets Protocol specification. To do so, the software establishes a connection to the [JSON-RPC port of a full Bitcoin node](https://en.bitcoin.it/wiki/API_reference_%28JSON-RPC%29). At the moment, the configuration needed for connecting to the node is expected to exist in the following environment variables, which must be specified when invoking Node: 23 | 24 | * `JSONRPC_USER` The username to use when connecting to the Bitcoin node's JSON-RPC service 25 | * `JSONRPC_PASS` The password corresponding to the username 26 | * `JSONRPC_HOST` The IP address or DNS name of the Bitcoin node 27 | * `JSONRPC_PORT` The port on which the JSON-RPC service is listening (default: 8332 (mainnet)) 28 | * `JSONRPC_PROTOCOL` The protocol to use: either 'http' or 'https' 29 | 30 | See the "Running Tests" section for an example of how to provide these environment variables to Node from the command line. 31 | 32 | Usage 33 | ----- 34 | 35 | The following is a heavily documented example that provides a high-level idea of what this engine does. Many more detailed examples and documentation are available at the repository for the Python [reference implemenation](https://github.com/OpenAssets/openassets). 36 | 37 | ```JavaScript 38 | // Required modules 39 | var openassets = require('openassets'), 40 | // Bitcore provides an excellent JSON-RPC client implementation. Substitute your favorite. 41 | RpcClient = require('bitcore/lib/RpcClient'); 42 | 43 | // JSON-RPC connection information (read from environment variables) 44 | config = { 45 | host: process.env.JSONRPC_HOST, 46 | port: process.env.JSONRPC_PORT, 47 | user: process.env.JSONRPC_USER, 48 | pass: process.env.JSONRPC_PASS, 49 | protocol: process.env.JSONRPC_PROTOCOL 50 | }; 51 | 52 | // A wrapper to generate a "transaction provider" given a config. 53 | // 54 | // For generality, connection to the Bitcoin JSON-RPC service is 55 | // externalized into the concept of a "transaction provider" that is 56 | // expected to conform to the following simple API: given a Bitcoin 57 | // transaction hash and a callback function, the provider must 58 | // populate the callback with the results of the 'getRawTransaction' 59 | // JSON-RPC call. 60 | getTransactionProvider = function getTransactionProvider(config) { 61 | return function transactionProvider(hash, cb) { 62 | var rpcc = new RpcClient(config); 63 | rpcc.getRawTransaction(hash,cb); 64 | }; 65 | }; 66 | 67 | // Create an instance of the Open Assets ColoringEngine, and pass to 68 | // it a configured transaction provider 69 | ce = new openassets.ColoringEngine(getTransactionProvider(config)); 70 | 71 | // Use the coloring engine to obtain information about a transaction. In 72 | // this case, get the 0th output of a known Open Assets 'issuance' transaction. 73 | // The first argument is the hash of the transaction, the 2nd is the index 74 | // of the output to retrieve, and the third is a callback function that will 75 | // be populated with the asset ID and asset quantity information, if any, associated with 76 | // that output. 77 | ce.getOutput( 78 | '77a6bbc65aa0326015835a3813778df4a037c15fb655e8678f234d8e2fc7439c', 79 | 0, function (err, data) { 80 | 81 | // If anything went wrong, say so 82 | if (err) console.log(err.message); 83 | 84 | // Print the asset information as a raw TransactionOutput object 85 | console.log(data); 86 | 87 | // Use the TransactionOutput.toString() method to get a more readable representation 88 | console.log(data.toString()); 89 | 90 | }); 91 | ``` 92 | 93 | The example above can be run via the command line using Node and providing the necessary environment variables as follows: 94 | 95 | ```bash 96 | ~$ JSONRPC_USER= JSONRPC_PASS= JSONRPC_HOST= JSONRPC_PORT=3332 JSONRPC_PROTOCOL=https node example.js 97 | ``` 98 | 99 | The expected output of the example above is: 100 | ```bash 101 | { value: 600, 102 | script: , 103 | assetId: , 104 | assetQuantity: 1, 105 | outputType: 2 } 106 | TransactionOutput(value=600, script=0x76a914d717483b5554670550f8e79a3b958d294ecf806088ac, assetId=0x1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0, assetQuantity=1, outputType=ISSUANCE) 107 | ``` 108 | ... in which we see that the ColoringEngine has identified this output as representing the issuance of 1 unit of the asset with id `0x1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0`. For reference, the transaction used in this example can be viewed using a Bitcoin [blockchain explorer](https://insight.bitpay.com/77a6bbc65aa0326015835a3813778df4a037c15fb655e8678f234d8e2fc7439c) and via the [Coinprism.info explorer](https://www.coinprism.info/tx/77a6bbc65aa0326015835a3813778df4a037c15fb655e8678f234d8e2fc7439c). The former provides raw value and script information, the latter provides a confirmation of the assetID and asset quantity details we obtained via the command line. 109 | 110 | 111 | Running Tests 112 | ------------- 113 | The test suite uses [Mocha](http://mochajs.org/) which must be available on your system. It can be easily installed via: `npm install -g mocha`. 114 | 115 | Tests can be found in the `/tests` directory. To run the tests, simply provide the required connection information as environment variables, followed by invoking mocha on whichever test suite you wish to run: 116 | 117 | ```bash 118 | ~$ JSONRPC_USER= JSONRPC_PASS= JSONRPC_HOST= JSONRPC_PORT=3332 JSONRPC_PROTOCOL=https mocha test/.js 119 | ``` 120 | 121 | To Do 122 | ----- 123 | 124 | This is a work in progress, with the ultimate aim of providing a complete JavaScript implementation of the protocol. The major unfinished work is the transaction builder ([reference implementation]( https://github.com/OpenAssets/openassets/blob/master/openassets/transactions.py)) that facilitates building and submitting Open Assets transactions to the bitcoin network. This section of the README will be updated when that is complete. 125 | 126 | Contributing 127 | ------------ 128 | 129 | Contributions in _any_ form (i.e.: code, documentation, comments, questions, etc.) are warmly welcomed. If contributing code, please fork this repository and then open a Pull Request with your changes. If you've found a bug or have a question, comment, or request, please open an Issue. 130 | 131 | -------------------------------------------------------------------------------- /lib/protocol/MarkerOutput.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2014, Andrew Hart 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | 'use strict'; 17 | 18 | var Opcode = require('bitcore/lib/Opcode'), 19 | Parser = require('bitcore/util/BinaryParser'), 20 | Put = require('bufferput'), 21 | Script = require('bitcore/lib/Script'), 22 | LEB128 = require('./LEB128'); 23 | 24 | /** 25 | * MarkerOutput 26 | * 27 | * Represents an Open Assets marker output[1]. The marker output 28 | * is what distinguishes a transaction as being an Open Assets 29 | * Protocol transaction. 30 | * 31 | * References: 32 | * [1] https://github.com/OpenAssets/open-assets-protocol/blob/master/specification.mediawiki#Marker_output 33 | * 34 | **/ 35 | 36 | /* Constants */ 37 | var OPEN_ASSETS_TAG = 0x4f41; 38 | var OPEN_ASSETS_VERSION = 0x0100; 39 | var MAX_ASSET_QUANTITY = Math.pow(2,63) -1; 40 | 41 | /** 42 | * Constructor 43 | * @param array(int) assetQuantities The list of asset quantities 44 | * @param Buffer metadata The metadata in the marker output 45 | **/ 46 | function MarkerOutput (assetQuantities, metadata) { 47 | 48 | // Validate provided asset quantities 49 | // NOTE: This check is not guaranteed to catch all values > MAX_ASSET_QUANTITY. 50 | // Because MAX_ASSET_QUANTITY is larger than Number.MAX_VALUE, exact 51 | // representation of values near MAX_ASSET_QUANTITY is sometimes odd. 52 | // For example (MAX_ASSET_QUANTITY === MAX_ASSET_QUANTITY+1) evaluates 53 | // to 'true'. However, if a number is significantly larger than 54 | // MAX_ASSET_QUANTITY, then this check will catch it. 55 | // TODO: Find a more precise check that will handle all cases 56 | if (assetQuantities) { 57 | assetQuantities.forEach(function (q) { 58 | if (q > MAX_ASSET_QUANTITY) { 59 | throw Error('Asset quantity ' 60 | + assetQuantities[i] 61 | + ' exceeds maximum allowed (' + MAX_ASSET_QUANTITY + ')'); 62 | } 63 | }); 64 | } 65 | 66 | // Store provided asset quantities 67 | this.assetQuantities = assetQuantities 68 | ? assetQuantities 69 | : []; 70 | 71 | // Validate and store provided metadata 72 | this.metadata = (metadata && Buffer.isBuffer(metadata)) 73 | ? metadata 74 | : new Buffer([]); 75 | }; 76 | 77 | /** 78 | * Serialize the marker output data into a payload buffer 79 | **/ 80 | MarkerOutput.prototype.serializePayload = function () { 81 | 82 | var i, buffer; 83 | 84 | // Initialize a buffer to hold the payload 85 | buffer = new Put() 86 | .word16be(OPEN_ASSETS_TAG) // Add Open Assets tag 87 | .word16be(OPEN_ASSETS_VERSION) // Add Open Assets version 88 | .varint(this.assetQuantities.length); // Add number of assetQuantities 89 | 90 | // LEB128-encode each asset quantity and store as varint 91 | for(i = 0; i < this.assetQuantities.length; i++) { 92 | buffer.put(LEB128.encode(this.assetQuantities[i])); 93 | } 94 | 95 | // Encode the output metadata 96 | buffer 97 | .varint(this.metadata.length) // Add the metadata length 98 | .put(this.metadata); // Add the metadata 99 | 100 | // Return the serialized payload buffer 101 | return buffer.buffer(); 102 | }; 103 | 104 | /** 105 | * Deserialize the marker output payload 106 | * @param Buffer payload A Buffer object containing the marker output payload 107 | * @return MarkerOutput The marker output object 108 | **/ 109 | MarkerOutput.prototype.deserializePayload = function (payload) { 110 | 111 | var parser = new Parser(payload), 112 | openAssetsTag, // Open Assets marker 113 | openAssetsVersion, // Open Assets version tag 114 | outputCount, // Number of outputs (excl. marker output) 115 | assetQuantities = [], // Asset quantities for each output 116 | metadataLength = 0, // Length of metadata 117 | metadata, // Metadata content 118 | decodeData, // Intermediate data structure 119 | i; 120 | 121 | try { 122 | 123 | if (!payload || !Buffer.isBuffer(payload)) { 124 | throw new Error('Payload must be a valid Buffer object'); 125 | } 126 | 127 | // Extract the Open Assets Protocol metadata 128 | openAssetsTag = parser.word16be(); 129 | openAssetsVersion = parser.word16be(); 130 | 131 | // Extract the number of asset quantities 132 | outputCount = parser.varInt(); 133 | 134 | // Extract the asset quantities 135 | for (i = 0; i < outputCount; i++) { 136 | // Decode an LEB128-encoded integer, and get stream metadata 137 | decodeData = LEB128.decodeWithMetadata(payload, parser.pos); 138 | // Add the decoded value to the assetQuantities array 139 | assetQuantities.push(decodeData.value); 140 | // Update the parser position to the next unseen byte 141 | parser.pos = decodeData.nextIndex; 142 | } 143 | 144 | // Extract the metadata length 145 | metadataLength = parser.varInt(); 146 | 147 | // Extract the metadata itself 148 | metadata = parser.buffer(metadataLength); 149 | 150 | // Return the resulting object 151 | return new MarkerOutput(assetQuantities, metadata); 152 | 153 | } catch (err) { 154 | throw new Error("Deserialization error: " + err.message) 155 | } 156 | 157 | }; 158 | 159 | 160 | /** 161 | * Create an output script containing OP_RETURN and a PUSHDATA 162 | * @param Buffer data The content of the PUSHDATA 163 | * @return Buffer The final script 164 | **/ 165 | MarkerOutput.prototype.buildScript = function (data) { 166 | 167 | // Ensure data is a valid Buffer object 168 | if (!data || !Buffer.isBuffer(data)) { 169 | throw new Error('Data must be a valid Buffer object'); 170 | } 171 | 172 | // Create a Script object for the output script 173 | var script = new Script(); 174 | 175 | // Add the OP_RETURN opcode 176 | script.writeOp(Opcode.map.OP_RETURN); 177 | 178 | // Add the data (OP_PUSHDATA automatically calculated) 179 | script.writeBytes(data); 180 | 181 | // Return the buffer for the built script 182 | return script.getBuffer(); 183 | 184 | }; 185 | 186 | /** 187 | * Parse an output script and return the payload if the output matches 188 | * the right pattern for an output. 189 | * @param Buffer outputScript The output script to be parsed 190 | * @return Buffer The marker output payload 191 | **/ 192 | MarkerOutput.prototype.parseScript = function (outputScript) { 193 | 194 | var script, payload; 195 | 196 | // Ensure outputScript is a valid Buffer object 197 | if (!outputScript || !Buffer.isBuffer(outputScript)) { 198 | throw new Error('Output script must be a valid Buffer object'); 199 | } 200 | 201 | // Create a Script object from the provided outputScript and parse it 202 | script = new Script(outputScript); 203 | script.parse(); 204 | 205 | // The opcode must be OP_RETURN 206 | if (!script.chunks[0] || script.chunks[0] != Opcode.map.OP_RETURN) { 207 | return false; 208 | } 209 | 210 | // There must be exactly one data section following the opcode 211 | if (!script.chunks[1] || script.chunks.length > 2) { 212 | return false; 213 | } 214 | 215 | // The payload must begin with the Open Assets tag and Open Assets version 216 | payload = script.chunks[1]; 217 | if ('0x' + payload.slice(0,2).toString('hex') != OPEN_ASSETS_TAG || 218 | '0x' + payload.slice(2,4).toString('hex') != OPEN_ASSETS_VERSION) { 219 | return false; 220 | } 221 | 222 | return payload; 223 | 224 | }; 225 | 226 | 227 | // Public API for this object 228 | module.exports = MarkerOutput; 229 | module.exports.OPEN_ASSETS_TAG = OPEN_ASSETS_TAG; 230 | module.exports.OPEN_ASSETS_VERSION = OPEN_ASSETS_VERSION; 231 | module.exports.MAX_ASSET_QUANTITY = MAX_ASSET_QUANTITY; 232 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /lib/protocol/ColoringEngine.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2014, Andrew Hart 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | 'use strict'; 17 | 18 | var async = require('async'), 19 | buffertools = require('buffertools'), 20 | HashUtil = require('bitcore/util'), 21 | LEB128 = require('./LEB128'), 22 | MarkerOutput = require('./MarkerOutput'), 23 | Opcode = require('bitcore/lib/Opcode'), 24 | Base58 = require('bitcore/lib/Base58'), 25 | OutputType = require('./OutputType'), 26 | Parser = require('bitcore/util/BinaryParser'), 27 | Put = require('bufferput'), 28 | Script = require('bitcore/lib/Script'), 29 | Transaction = require('bitcore/lib/Transaction'), 30 | TransactionOutput = require('./TransactionOutput'); 31 | 32 | /** 33 | * ColoringEngine 34 | * 35 | * The recursive backtracking engine used to find the asset ID and asset quantity 36 | * of any transaction output. 37 | **/ 38 | 39 | /** 40 | * Constructor 41 | * @param function transactionProvider A function that accepts a transaction hash, 42 | * performs a transaction lookup, and populates 43 | * a callback. The first parameter to this 44 | * function should be the transaction hash to 45 | * look up. The second parameter should be a 46 | * callback function with the following 47 | * signature: cb(err, data). See the test cases 48 | * for this class for an example provider using 49 | * the Bitcore RpcClient library. 50 | **/ 51 | function ColoringEngine (transactionProvider) { 52 | 53 | this.transactionProvider = transactionProvider; 54 | 55 | } 56 | 57 | /** 58 | * Get an output and information about its asset ID and asset quantity. 59 | * @param Buffer transactionHash The hash of the transaction containing the output 60 | * @param int outputIndex The index of the output 61 | * @param function cb A callback function to invoke with the result 62 | **/ 63 | ColoringEngine.prototype.getOutput = function (transactionHash, outputIndex, cb) { 64 | 65 | var coloringEngine = this; 66 | 67 | coloringEngine.transactionProvider(transactionHash, function (err, data) { 68 | if (err) return cb(err); 69 | 70 | // Propagate error message from underlying JSON RPC call 71 | if (data.message) return cb(data.message); 72 | 73 | // Successful lookups will always populate 'result' 74 | if (!data.result) return cb('Transaction could not be retrieved.'); 75 | 76 | // Create and populate a Transaction object using the raw data 77 | var tx = new Transaction(); 78 | tx.parse(new Buffer(data.result,'hex')); 79 | 80 | // Compute ID and asset quantity of transaction outputs 81 | coloringEngine.colorTransaction(tx, function (err, data) { 82 | if (err) return cb(err); 83 | 84 | // If an output matching outputIndex exists, return it 85 | if (data[outputIndex]) { 86 | return cb(null, data[outputIndex]); 87 | } 88 | // Otherwise, report the error 89 | else { 90 | return cb('No data for output matching index ' + outputIndex); 91 | } 92 | 93 | }); 94 | 95 | }); 96 | 97 | } 98 | 99 | /** 100 | * Compute the asset ID and and asset quantity of every output in the transaction 101 | * @param Buffer transaction The transaction to color 102 | * @param function cb A callback function to invoke with the result. The 103 | * function should have the signature cb(err, data). If 104 | * successful, 'data' will be populated with an array 105 | * containing all the colored outputs of the transaction. 106 | **/ 107 | ColoringEngine.prototype.colorTransaction = function (transaction, cb) { 108 | 109 | var coloringEngine = this, 110 | foundMarkerOutput = false, 111 | markerOutputPayload = null, 112 | markerOutput = null; 113 | 114 | // Helper function to make the appropriate response in the case 115 | // where no valid asset ids were found in a transaction. In 116 | // this case all of the transaction outputs are considered uncolored. 117 | var makeUncoloredResponse = function (tx) { 118 | var outs = []; 119 | tx.outs.forEach(function (o) { 120 | outs.push(new TransactionOutput(o.v.readInt32LE(0),o.s)); 121 | }); 122 | return outs; 123 | }; 124 | 125 | // If the transaction is a coinbase transaction, the marker output is always invalid 126 | if (transaction.isCoinBase()) { 127 | return cb(null, makeUncoloredResponse(transaction)); 128 | } 129 | 130 | // Search transaction outputs for an Open Assets "Marker Output" 131 | transaction.outs.forEach(function (o, outIdx) { 132 | // If a valid marker is found, we can stop processing subsequent outputs 133 | // since, according to the spec: "if multiple valid marker outputs 134 | // exist in the same transaction, the first one is used and the other 135 | // ones are considered as regular outputs." [1] 136 | // [1] https://github.com/OpenAssets/open-assets-protocol/blob/master/specification.mediawiki 137 | if (!foundMarkerOutput) { 138 | // Attempt to decode this output as a Marker Output. 139 | markerOutputPayload = MarkerOutput.prototype.parseScript(o.s); 140 | // If a valid marker output payload was decoded 141 | if (markerOutputPayload) { 142 | // Extract the marker output (asset quantity and metadata) information 143 | markerOutput = MarkerOutput.prototype.deserializePayload(markerOutputPayload); 144 | // If valid marker output information was extracted, we have all the 145 | // information necessary to compute the colored outputs for this tx 146 | if (markerOutput) { 147 | foundMarkerOutput = true; 148 | 149 | // Build a recursive backtracking function for each of this 150 | // transactions inputs. Looking at the colored outputs from 151 | // transactions linked to this transaction's inputs will allow 152 | // us to dertermine which assets flow into the current transaction. 153 | var prevouts = []; 154 | transaction.inputs().forEach(function (i, idx) { 155 | 156 | prevouts.push(function (fcb) { 157 | var outHash = buffertools.reverse(i[0]).toString('hex'); 158 | coloringEngine.getOutput(outHash, i[1], fcb); 159 | }); 160 | }, coloringEngine); 161 | 162 | // Fetch the colored outputs for each previous transaction 163 | async.parallel(prevouts, function (err, inTxs){ 164 | if (err) return cb(err); 165 | 166 | // Store results of all recursive backtracking 167 | var inputs = inTxs; 168 | 169 | // Ensure all inputs were processed 170 | if (inputs.length !== transaction.ins.length) { 171 | return ("Error processing inputs: expected " 172 | + transaction.ins.length + " results, got " + inputs.length); 173 | } 174 | 175 | // Compute the asset ids of the colored outputs 176 | var outputsWithAssetIds = ColoringEngine.prototype._computeAssetIds( 177 | inputs, 178 | outIdx, 179 | transaction.outs, 180 | markerOutput.assetQuantities); 181 | 182 | if (outputsWithAssetIds) { 183 | // If successful, return the colored outputs 184 | return cb(null, outputsWithAssetIds); 185 | } else { 186 | // Otherwise, the transaction should be considered uncolored 187 | return cb(null, makeUncoloredResponse(transaction)); 188 | } 189 | }); 190 | } 191 | } 192 | } 193 | }, coloringEngine); 194 | 195 | // If no marker output was encountered in any of the transaction 196 | // outputs, all transaction outputs are considered uncolored. 197 | if (!foundMarkerOutput) { 198 | return cb(null, makeUncoloredResponse(transaction)); 199 | } 200 | 201 | }; 202 | 203 | 204 | /** 205 | * Compute Asset IDs of every output in a transaction 206 | * @param array(TransactionOutput) inputs The outputs referenced by the inputs of the transaction 207 | * @param int markerOutputIndex The position of the marker output in the transaction 208 | * @param array(TransactionOut) outputs The outputs of the transaction 209 | * @param array(int) assetQuantities The list of asset quantities of the outputs 210 | * @param function cb A callback to invoke with the array of computed asset ids 211 | * @return array(TransactionOutput) An array of transaction outputs with computed asset ids 212 | **/ 213 | ColoringEngine.prototype._computeAssetIds = function (inputs, markerOutputIndex, outputs, assetQuantities) { 214 | 215 | var coloringEngine = this, 216 | result = [], 217 | assetId, 218 | issuanceAssetId, 219 | outputAssetQuantity, 220 | curInput, 221 | inputUnitsLeft, 222 | outputUnitsLeft, 223 | progress, 224 | i; 225 | 226 | // If there are more items in the asset quantities list than outputs in 227 | // the transaction (excluding the marker output), the marker output is 228 | // deemed invalid 229 | if (assetQuantities.length > outputs.length - 1) { 230 | return false; 231 | } 232 | 233 | // If there is no input in the transaction, the marker output is always invalid 234 | if (inputs.length == 0) { 235 | return false; 236 | } 237 | 238 | // Add the issuance outputs 239 | issuanceAssetId = coloringEngine.hashScript(inputs[0].script); 240 | for (i = 0; i < markerOutputIndex; i++) { 241 | if (i < assetQuantities.length && assetQuantities[i] > 0) { 242 | result.push(new TransactionOutput( 243 | outputs[i].v.readInt32LE(0), 244 | outputs[i].s, 245 | issuanceAssetId, 246 | assetQuantities[i], 247 | OutputType.ISSUANCE)); 248 | } else { 249 | result.push(new TransactionOutput( 250 | outputs[i].v.readInt32LE(0), 251 | outputs[i].s, 252 | null, 253 | null, 254 | OutputType.ISSUANCE)); 255 | } 256 | } 257 | 258 | // Add the marker output 259 | result.push(new TransactionOutput( 260 | outputs[markerOutputIndex].v.readInt32LE(0), 261 | outputs[markerOutputIndex].s, 262 | null, 263 | null, 264 | OutputType.MARKER_OUTPUT)); 265 | 266 | // Add the transfer outputs 267 | for (i = markerOutputIndex + 1; i < outputs.length; i++) { 268 | 269 | if (i <= assetQuantities.length) { 270 | outputAssetQuantity = assetQuantities[i-1]; 271 | } else { 272 | outputAssetQuantity = 0; 273 | } 274 | 275 | outputUnitsLeft = outputAssetQuantity; 276 | assetId = null; 277 | 278 | curInput = 0; 279 | assetId = (inputs[curInput]) 280 | ? inputs[curInput].assetId 281 | : null; 282 | inputUnitsLeft = (inputs[curInput]) 283 | ? ((null == inputs[curInput].assetQuantity) ? 0 : inputs[curInput].assetQuantity) 284 | : 0; 285 | 286 | while (outputUnitsLeft > 0) { 287 | // Move to the next input if the current one is depleted 288 | if (inputUnitsLeft == 0) { 289 | curInput++; 290 | 291 | // If there are less asset units available than in the outputs 292 | // the marker output is considered invalid 293 | if (!inputs[curInput]) { 294 | return false; 295 | // Otherwise, use the assetQuantity associated with the current input 296 | } else { 297 | inputUnitsLeft = (null == inputs[curInput].assetQuantity) 298 | ? 0 299 | : inputs[curInput].assetQuantity; 300 | } 301 | } 302 | 303 | // If the current input is colored, assign its asset id to the 304 | // current output 305 | if (inputs[curInput].assetId != null) { 306 | progress = Math.min(inputUnitsLeft, outputUnitsLeft); 307 | outputUnitsLeft -= progress; 308 | inputUnitsLeft -= progress; 309 | 310 | if (assetId == null) { 311 | // This is the first input to map to this output 312 | assetId = inputs[curInput].assetId; 313 | } else if (!buffertools.equals(assetId, inputs[curInput].assetId)) { 314 | // Another different asset ID has already been assigned to 315 | // that output. The marker output is considered invalid 316 | return false; 317 | } 318 | } 319 | } 320 | 321 | result.push(new TransactionOutput( 322 | outputs[i].v.readInt32LE(0), 323 | outputs[i].s, 324 | (outputAssetQuantity > 0) ? assetId : null, 325 | (outputAssetQuantity > 0) ? outputAssetQuantity : null, 326 | OutputType.TRANSFER)); 327 | 328 | } 329 | 330 | return result; 331 | } 332 | 333 | /** 334 | * Hash a script into an Asset ID using SHA256 followed by RIPEMD160 335 | * @param Buffer data The data to hash 336 | * @return String The resulting Asset ID 337 | **/ 338 | ColoringEngine.prototype.hashScript = function (data) { 339 | 340 | return HashUtil.sha256ripe160(data); 341 | 342 | } 343 | 344 | /** 345 | * Convert a bitcoin address into an OpenAsset address 346 | * @param String btcAddress The bitcoin public address 347 | * @return String The resulting OpenAsset address 348 | **/ 349 | ColoringEngine.prototype.addressFromBitcoinAddress = function (btcAddress) { 350 | var btcAddr = Base58.decode(btcAddress) 351 | var btcBuff = new Put() 352 | .word8(19) 353 | .put(btcAddr.slice(0, -4)) 354 | var btcCheck = HashUtil.twoSha256(btcBuff.buffer()) 355 | btcBuff.put(btcCheck.slice(0,4)) 356 | 357 | return Base58.encode(btcBuff.buffer()); 358 | } 359 | 360 | module.exports = ColoringEngine; 361 | -------------------------------------------------------------------------------- /test/ColoringEngine.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | process.env.NODE_ENV = process.env.NODE_ENV || 'test'; 6 | 7 | var assert = require('assert'), 8 | async = require('async'), 9 | chai = require('chai'), 10 | expect = chai.expect, 11 | should = chai.should(), 12 | sinon = require('sinon'), 13 | ColoringEngine = require('../lib/protocol/ColoringEngine'), 14 | OutputType = require('../lib/protocol/OutputType'), 15 | TransactionOutput = require('../lib/protocol/TransactionOutput'), 16 | bitcore = require('bitcore'), 17 | RpcClient = require('bitcore/lib/RpcClient'), 18 | EncodedData = require('bitcore/util/EncodedData'); 19 | 20 | 21 | describe("TransactionOutput", function () { 22 | 23 | describe('::constructor', function () { 24 | 25 | }); 26 | 27 | describe('::_computeAssetIds', function () { 28 | 29 | var config, getTransactionProvider, ce, 30 | inputs, outputs, markerOutputIndex, assetQuantities, result; 31 | 32 | 33 | beforeEach(function () { 34 | // RPC connection information 35 | config = { 36 | host: process.env.JSONRPC_HOST, 37 | port: process.env.JSONRPC_PORT, 38 | user: process.env.JSONRPC_USER, 39 | pass: process.env.JSONRPC_PASS, 40 | protocol: process.env.JSONRPC_PROTOCOL 41 | }; 42 | 43 | // A wrapper to generate transaction providers given a config 44 | getTransactionProvider = function getTransactionProvider(config) { 45 | return function transactionProvider(hash, cb) { 46 | var rpcc = new RpcClient(config); 47 | rpcc.getRawTransaction(hash,cb); 48 | }; 49 | }; 50 | 51 | // Create an instance of the Coloring Engine 52 | ce = new ColoringEngine(getTransactionProvider(config)); 53 | }) 54 | 55 | it('should compute the asset id of an Issuance output', function (done) { 56 | 57 | // This is a simulation of the following transaction: 58 | // https://www.coinprism.info/tx/77a6bbc65aa0326015835a3813778df4a037c15fb655e8678f234d8e2fc7439c 59 | // https://insight.bitpay.com/tx/77a6bbc65aa0326015835a3813778df4a037c15fb655e8678f234d8e2fc7439c 60 | inputs = [ 61 | new TransactionOutput(100000, new Buffer('76a914d717483b5554670550f8e79a3b958d294ecf806088ac','hex')) 62 | ]; 63 | markerOutputIndex=1; 64 | outputs = [ 65 | { v: new Buffer('5802000000000000','hex'), 66 | s: new Buffer('76a914d717483b5554670550f8e79a3b958d294ecf806088ac','hex')}, 67 | { v: new Buffer('0000000000000000','hex'), 68 | s: new Buffer('6a224f41010001011b753d68747470733a2f2f6370722e736d2f63463557584459643642','hex')}, 69 | { v: new Buffer('385d010000000000','hex'), 70 | s: new Buffer('76a914d717483b5554670550f8e79a3b958d294ecf806088ac','hex')} 71 | ]; 72 | assetQuantities = [1]; 73 | 74 | result = ce._computeAssetIds(inputs, markerOutputIndex, outputs, assetQuantities); 75 | result.length.should.equal(3); 76 | 77 | // Issuance output 78 | result[0].value.should.equal(600); 79 | result[0].outputType.should.equal(OutputType.ISSUANCE); 80 | result[0].assetId.toString('hex').should.equal('1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0'); 81 | result[0].toString().should.equal('TransactionOutput(value=600, script=0x76a914d717483b5554670550f8e79a3b958d294ecf806088ac, assetId=0x1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0, assetQuantity=1, outputType=ISSUANCE)'); 82 | 83 | // Marker output 84 | result[1].value.should.equal(0); 85 | result[1].outputType.should.equal(OutputType.MARKER_OUTPUT); 86 | expect(result[1].assetId).to.be.null; 87 | result[1].toString().should.equal('TransactionOutput(value=0, script=0x6a224f41010001011b753d68747470733a2f2f6370722e736d2f63463557584459643642, assetId=null, assetQuantity=null, outputType=MARKER_OUTPUT)'); 88 | 89 | // Transfer output (technically "uncolored" since this there is no asset id - this is just a "change" output) 90 | expect(result[2].assetId).to.be.null; 91 | expect(result[2].assetQuantity).to.be.null; 92 | result[2].value.should.equal(89400); 93 | result[2].outputType.should.equal(OutputType.TRANSFER); 94 | 95 | done(); 96 | }); 97 | 98 | it('should compute the asset id for a Transfer from an Issuance', function (done) { 99 | 100 | // This is a simulation of the following transaction: 101 | // https://www.coinprism.info/tx/b26bbbf5cf5a783241397236227640402ecdd10955f8aecf2ad19b3b744bde42 102 | // https://insight.bitpay.com/tx/b26bbbf5cf5a783241397236227640402ecdd10955f8aecf2ad19b3b744bde42 103 | inputs = [ 104 | new TransactionOutput(600, new Buffer('76a914d717483b5554670550f8e79a3b958d294ecf806088ac','hex'), new Buffer('1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0','hex'), 1, OutputType.ISSUANCE), 105 | new TransactionOutput(0, new Buffer('6a224f41010001011b753d68747470733a2f2f6370722e736d2f63463557584459643642', null, null, OutputType.MARKER_OUTPUT)) 106 | ]; 107 | markerOutputIndex=0; 108 | outputs = [ 109 | { v: new Buffer('0000000000000000','hex'), 110 | s: new Buffer('6a074f410100010100','hex')}, 111 | { v: new Buffer('5802000000000000','hex'), 112 | s: new Buffer('76a91475c37d8aaeb2cd9859a7b212d21e422903cf00a288ac','hex')}, 113 | { v: new Buffer('2836010000000000','hex'), 114 | s: new Buffer('76a914d717483b5554670550f8e79a3b958d294ecf806088ac','hex')} 115 | ]; 116 | assetQuantities = [1]; 117 | 118 | result = ce._computeAssetIds(inputs, markerOutputIndex, outputs, assetQuantities); 119 | result.length.should.equal(3); 120 | 121 | // Marker output 122 | result[0].value.should.equal(0); 123 | result[0].outputType.should.equal(OutputType.MARKER_OUTPUT); 124 | expect(result[0].assetId).to.be.null; 125 | result[0].toString().should.equal('TransactionOutput(value=0, script=0x6a074f410100010100, assetId=null, assetQuantity=null, outputType=MARKER_OUTPUT)'); 126 | 127 | // Transfer output 128 | result[1].value.should.equal(600); 129 | result[1].outputType.should.equal(OutputType.TRANSFER); 130 | result[1].assetId.toString('hex').should.equal('1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0'); 131 | result[1].toString().should.equal('TransactionOutput(value=600, script=0x76a91475c37d8aaeb2cd9859a7b212d21e422903cf00a288ac, assetId=0x1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0, assetQuantity=1, outputType=TRANSFER)'); 132 | 133 | // Transfer output (technically "uncolored" since this there is no asset id - this is just a "change" output) 134 | expect(result[2].assetId).to.be.null; 135 | expect(result[2].assetQuantity).to.be.null; 136 | result[2].value.should.equal(79400); 137 | result[2].outputType.should.equal(OutputType.TRANSFER); 138 | 139 | done(); 140 | }); 141 | 142 | it('should compute the asset id for a Transfer from a Transfer', function (done) { 143 | 144 | // This is a simulation of the following transaction: 145 | // https://www.coinprism.info/tx/56a4bde85b9f2de5be0f17ad0fa666efebbf6cc7961b2720bdf0750282f8d18c 146 | // https://insight.bitpay.com/tx/56a4bde85b9f2de5be0f17ad0fa666efebbf6cc7961b2720bdf0750282f8d18c 147 | inputs = [ 148 | new TransactionOutput(600, new Buffer('76a91475c37d8aaeb2cd9859a7b212d21e422903cf00a288ac','hex'), new Buffer('1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0','hex'), 1, OutputType.TRANSFER), 149 | new TransactionOutput(0, new Buffer('6a224f41010001011b753d68747470733a2f2f6370722e736d2f63463557584459643642', null, null, OutputType.MARKER_OUTPUT)) 150 | ]; 151 | markerOutputIndex=0; 152 | outputs = [ 153 | { v: new Buffer('0000000000000000','hex'), 154 | s: new Buffer('6a074f410100010100','hex')}, 155 | { v: new Buffer('5802000000000000','hex'), 156 | s: new Buffer('76a914d717483b5554670550f8e79a3b958d294ecf806088ac','hex')}, 157 | { v: new Buffer('905f010000000000','hex'), 158 | s: new Buffer('76a91475c37d8aaeb2cd9859a7b212d21e422903cf00a288ac','hex')} 159 | ]; 160 | assetQuantities = [1]; 161 | 162 | result = ce._computeAssetIds(inputs, markerOutputIndex, outputs, assetQuantities); 163 | result.length.should.equal(3); 164 | 165 | // Marker output 166 | result[0].value.should.equal(0); 167 | result[0].outputType.should.equal(OutputType.MARKER_OUTPUT); 168 | expect(result[0].assetId).to.be.null; 169 | result[0].toString().should.equal('TransactionOutput(value=0, script=0x6a074f410100010100, assetId=null, assetQuantity=null, outputType=MARKER_OUTPUT)'); 170 | 171 | // Transfer output 172 | result[1].value.should.equal(600); 173 | result[1].outputType.should.equal(OutputType.TRANSFER); 174 | result[1].assetId.toString('hex').should.equal('1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0'); 175 | result[1].toString().should.equal('TransactionOutput(value=600, script=0x76a914d717483b5554670550f8e79a3b958d294ecf806088ac, assetId=0x1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0, assetQuantity=1, outputType=TRANSFER)'); 176 | 177 | // Transfer output (technically "uncolored" since this there is no asset id - this is just a "change" output) 178 | expect(result[2].assetId).to.be.null; 179 | expect(result[2].assetQuantity).to.be.null; 180 | result[2].value.should.equal(90000); 181 | result[2].outputType.should.equal(OutputType.TRANSFER); 182 | 183 | done(); 184 | }); 185 | 186 | }); 187 | 188 | 189 | describe('::getOutput', function () { 190 | 191 | var txFunding, txIssue1, txIssue2, txXfer1, txXfer2, txXfer3, 192 | config, getTransactionProvider, ce, 193 | seriesCallback; 194 | 195 | beforeEach( function () { 196 | 197 | // Set up a few transaction hashes to use with testing 198 | // see https://www.coinprism.info/tx/ for information on each: 199 | // Transfer 0.001 BTC to address: 1LcJAv8c81q9BRv45znsVSdsuMRtKtpzKL 200 | txFunding = '405b36e856c6f89952116948268ffcd4deffb845c232514cf81a324f343eddf5'; 201 | // Issue 1 'AJS39eYsPGYo3S8L73xWGv8DwPHT4LYp8B' asset to address: akWaBR5wwbViq3g5R8cu49JYTExc4GrNpeT 202 | txIssue1 = '77a6bbc65aa0326015835a3813778df4a037c15fb655e8678f234d8e2fc7439c'; 203 | // Transfer 1 'AJS39eYsPGYo3S8L73xWGv8DwPHT4LYp8B' asset to address: akMhZVXQsR7mUkud8LGDoQbnP3um6iFF1JC 204 | txXfer1 = 'b26bbbf5cf5a783241397236227640402ecdd10955f8aecf2ad19b3b744bde42'; 205 | // (return) Transfer the 1 'AJS39eYsPGYo3S8L73xWGv8DwPHT4LYp8B' asset back to address: akWaBR5wwbViq3g5R8cu49JYTExc4GrNpeT 206 | txXfer2 = '56a4bde85b9f2de5be0f17ad0fa666efebbf6cc7961b2720bdf0750282f8d18c'; 207 | // Issue 200 'AJS39eYsPGYo3S8L73xWGv8DwPHT4LYp8B' assets to address: akWaBR5wwbViq3g5R8cu49JYTExc4GrNpeT 208 | txIssue2 = 'dcccfdf72171964bfa895def45eeca746b126159473162a74d2c99c88bba469d'; 209 | // Transfer 2 'AJS39eYsPGYo3S8L73xWGv8DwPHT4LYp8B' assets to address akMhZVXQsR7mUkud8LGDoQbnP3um6iFF1JC 210 | txXfer3 = '5bcc5beaf1ded56e22757b05329fb00c8e37f53593ec56e7303e0a8c99ecd169'; 211 | 212 | // RPC connection information 213 | config = { 214 | host: process.env.JSONRPC_HOST, 215 | port: process.env.JSONRPC_PORT, 216 | user: process.env.JSONRPC_USER, 217 | pass: process.env.JSONRPC_PASS, 218 | protocol: process.env.JSONRPC_PROTOCOL 219 | }; 220 | 221 | // A wrapper to generate transaction providers given a config 222 | getTransactionProvider = function getTransactionProvider(config) { 223 | return function transactionProvider(hash, cb) { 224 | var rpcc = new RpcClient(config); 225 | rpcc.getRawTransaction(hash,cb); 226 | }; 227 | }; 228 | 229 | // Create an instance of the Coloring Engine 230 | ce = new ColoringEngine(getTransactionProvider(config)); 231 | }); 232 | 233 | 234 | it('should handle a non-Open Assets transaction given a valid hash', function (done) { 235 | this.timeout(5000); 236 | async.series([ 237 | function (cb) { 238 | ce.getOutput(txFunding, 0, function (err, data) { 239 | expect(err).to.not.exist; 240 | expect(data.toString()).to.equal( 241 | 'TransactionOutput(value=100000, script=0x76a914d717483b5554670550f8e79a3b958d294ecf806088ac, assetId=null, assetQuantity=null, outputType=UNCOLORED)'); 242 | cb(); 243 | }); 244 | }, 245 | function (cb) { 246 | ce.getOutput(txFunding, 1, function (err, data) { 247 | expect(err).to.not.exist; 248 | expect(data.toString()).to.equal( 249 | 'TransactionOutput(value=390000, script=0x76a9141937a416988ea705aa9c9112e67a35543c6854fb88ac, assetId=null, assetQuantity=null, outputType=UNCOLORED)'); 250 | cb(); 251 | }); 252 | } 253 | ],function () {done();}); 254 | }); 255 | 256 | it('should detect an Open Assets issuance transaction output', function (done) { 257 | async.series([ 258 | // Open Assets Issuance output 259 | function (cb) { 260 | ce.getOutput(txIssue1, 0, function (err, data) { 261 | expect(err).to.not.exist; 262 | expect(data.toString()).to.equal( 263 | 'TransactionOutput(value=600, script=0x76a914d717483b5554670550f8e79a3b958d294ecf806088ac, assetId=0x1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0, assetQuantity=1, outputType=ISSUANCE)'); 264 | cb(); 265 | }); 266 | }, 267 | // Open Assets Marker output 268 | function (cb) { 269 | ce.getOutput(txIssue1, 1, function (err, data) { 270 | expect(err).to.not.exist; 271 | expect(data.toString()).to.equal( 272 | 'TransactionOutput(value=0, script=0x6a224f41010001011b753d68747470733a2f2f6370722e736d2f63463557584459643642, assetId=null, assetQuantity=null, outputType=MARKER_OUTPUT)'); 273 | cb(); 274 | }); 275 | } 276 | ],function () {done();}); 277 | }); 278 | 279 | it('should detect an Open Assets transfer transaction output', function (done) { 280 | this.timeout(5000); 281 | async.series([ 282 | // Open Assets Marker output 283 | function (cb) { 284 | ce.getOutput(txXfer1, 0, function (err, data) { 285 | expect(err).to.not.exist; 286 | expect(data.toString()).to.equal( 287 | 'TransactionOutput(value=0, script=0x6a074f410100010100, assetId=null, assetQuantity=null, outputType=MARKER_OUTPUT)'); 288 | cb(); 289 | }); 290 | }, 291 | // Open Assets Transfer output 292 | function (cb) { 293 | ce.getOutput(txXfer1, 1, function (err, data) { 294 | expect(err).to.not.exist; 295 | expect(data.toString()).to.equal( 296 | 'TransactionOutput(value=600, script=0x76a91475c37d8aaeb2cd9859a7b212d21e422903cf00a288ac, assetId=0x1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0, assetQuantity=1, outputType=TRANSFER)'); 297 | cb(); 298 | }); 299 | } 300 | ],function () {done();}); 301 | }); 302 | 303 | it('should detect an Open Assets issuance transaction output', function (done) { 304 | async.series([ 305 | // Open Assets Issuance output 306 | function (cb) { 307 | ce.getOutput(txIssue2, 0, function (err, data) { 308 | expect(err).to.not.exist; 309 | expect(data.toString()).to.equal( 310 | 'TransactionOutput(value=600, script=0x76a914d717483b5554670550f8e79a3b958d294ecf806088ac, assetId=0x1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0, assetQuantity=200, outputType=ISSUANCE)'); 311 | cb(); 312 | }); 313 | }, 314 | // Open Assets Marker output 315 | function (cb) { 316 | ce.getOutput(txIssue2, 1, function (err, data) { 317 | expect(err).to.not.exist; 318 | expect(data.toString()).to.equal( 319 | 'TransactionOutput(value=0, script=0x6a234f41010001c8011b753d68747470733a2f2f6370722e736d2f63463557584459643642, assetId=null, assetQuantity=null, outputType=MARKER_OUTPUT)'); 320 | cb(); 321 | }); 322 | } 323 | ],function () {done();}); 324 | }); 325 | 326 | it('should detect an Open Assets transfer transaction output', function (done) { 327 | this.timeout(5000); 328 | async.series([ 329 | // Open Assets Marker output 330 | function (cb) { 331 | ce.getOutput(txXfer2, 0, function (err, data) { 332 | expect(err).to.not.exist; 333 | expect(data.toString()).to.equal( 334 | 'TransactionOutput(value=0, script=0x6a074f410100010100, assetId=null, assetQuantity=null, outputType=MARKER_OUTPUT)'); 335 | cb(); 336 | }); 337 | }, 338 | // Open Assets Transfer output 339 | function (cb) { 340 | ce.getOutput(txXfer2, 1, function (err, data) { 341 | expect(err).to.not.exist; 342 | expect(data.toString()).to.equal( 343 | 'TransactionOutput(value=600, script=0x76a914d717483b5554670550f8e79a3b958d294ecf806088ac, assetId=0x1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0, assetQuantity=1, outputType=TRANSFER)'); 344 | cb(); 345 | }); 346 | } 347 | ],function () {done();}); 348 | }); 349 | 350 | it('should detect an Open Assets transfer transaction output with "asset change"', function (done) { 351 | this.timeout(5000); 352 | async.series([ 353 | // Open Assets Marker output 354 | function (cb) { 355 | ce.getOutput(txXfer3, 0, function (err, data) { 356 | expect(err).to.not.exist; 357 | expect(data.toString()).to.equal( 358 | 'TransactionOutput(value=0, script=0x6a094f4101000202c70100, assetId=null, assetQuantity=null, outputType=MARKER_OUTPUT)'); 359 | cb(); 360 | }); 361 | }, 362 | // Open Assets Transfer output (Transfer 1 units from tx 56a...18c and 1 unit from tx dcc...69d to 1BjGFM...) 363 | function (cb) { 364 | ce.getOutput(txXfer3, 1, function (err, data) { 365 | expect(err).to.not.exist; 366 | expect(data.toString()).to.equal( 367 | 'TransactionOutput(value=600, script=0x76a91475c37d8aaeb2cd9859a7b212d21e422903cf00a288ac, assetId=0x1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0, assetQuantity=2, outputType=TRANSFER)'); 368 | cb(); 369 | }); 370 | }, 371 | // Open Assets Transfer output (Transfer the remaining 199 units from tx dcc...69d back to the 1LcJAv...) 372 | function (cb) { 373 | ce.getOutput(txXfer3, 2, function (err, data) { 374 | expect(err).to.not.exist; 375 | expect(data.toString()).to.equal( 376 | 'TransactionOutput(value=600, script=0x76a914d717483b5554670550f8e79a3b958d294ecf806088ac, assetId=0x1d27fd8fac0cda221b3fccc6ecc1fc46cd9178d0, assetQuantity=199, outputType=TRANSFER)'); 377 | cb(); 378 | }); 379 | }, 380 | // Transfer output (technically "uncolored" since there is no asset id - this is just a "change" output) 381 | function (cb) { 382 | ce.getOutput(txXfer3, 3, function (err, data) { 383 | expect(err).to.not.exist; 384 | expect(data.assetId).to.be.null; 385 | expect(data.assetQuantity).to.be.null; 386 | data.value.should.equal(58800); 387 | data.outputType.should.equal(OutputType.TRANSFER); 388 | expect(data.toString()).to.equal( 389 | 'TransactionOutput(value=58800, script=0x76a914d717483b5554670550f8e79a3b958d294ecf806088ac, assetId=null, assetQuantity=null, outputType=TRANSFER)'); 390 | cb(); 391 | }); 392 | } 393 | ],function () {done();}); 394 | }); 395 | 396 | 397 | 398 | }); 399 | 400 | 401 | 402 | describe('::hashScript', function () { 403 | it('should compute the RIPEMD160 of the SHA256 of the data', function (done) { 404 | var data = new Buffer('18E14A7B6A307F426A94F8114701E7C8E774E7F9A47E2C2035DB29A206321725','hex'); 405 | var hash = new ColoringEngine().hashScript(data); 406 | hash.toString('hex').should.equal('29a4be50be7ff6ea37b9dcc1aa9642ab928c6dcb'); 407 | done(); 408 | }); 409 | }); 410 | 411 | describe('::addressFromBitcoinAddress', function () { 412 | it('should compute the OpenAsset address from a Bitcoin public address', function (done) { 413 | var btcAddr = '16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM'; 414 | var oaAddr = new ColoringEngine().addressFromBitcoinAddress(btcAddr); 415 | oaAddr.should.equal('akB4NBW9UuCmHuepksob6yfZs6naHtRCPNy'); 416 | done(); 417 | }); 418 | }); 419 | 420 | }); 421 | --------------------------------------------------------------------------------