From 80937362bee208eff6c09e5a09c8375a62f0f49a Mon Sep 17 00:00:00 2001 From: Yemel Jardi Date: Wed, 26 Nov 2014 15:40:58 -0300 Subject: [PATCH 1/4] Address constructor allows an address instance --- lib/address.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/address.js b/lib/address.js index e199a29..957f53c 100644 --- a/lib/address.js +++ b/lib/address.js @@ -59,6 +59,8 @@ function Address(data, network, type) { info = Address._transformPublicKey(data); } else if (data.constructor && (data.constructor.name && data.constructor.name === 'Script')) { info = Address._transformScript(data); + } else if (data instanceof Address) { + return data; } else if (typeof(data) === 'string') { info = Address._transformString(data, network, type); } else { From c9951be2dc86a1871c78fa77d8129e7618a41446 Mon Sep 17 00:00:00 2001 From: Yemel Jardi Date: Wed, 26 Nov 2014 18:07:49 -0300 Subject: [PATCH 2/4] Base URI class and tests --- index.js | 2 +- lib/uri.js | 92 +++++++++++++++++++++++++++ test/uri.js | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 lib/uri.js create mode 100644 test/uri.js diff --git a/index.js b/index.js index c938e47..4c3d49a 100644 --- a/index.js +++ b/index.js @@ -41,9 +41,9 @@ bitcore.Script = require('./lib/script'); bitcore.Transaction = require('./lib/transaction'); bitcore.Txin = require('./lib/txin'); bitcore.Txout = require('./lib/txout'); +bitcore.URI = require('./lib/uri'); bitcore.Unit = require('./lib/unit'); - // dependencies, subject to change bitcore.deps = {}; bitcore.deps.bnjs = require('bn.js'); diff --git a/lib/uri.js b/lib/uri.js new file mode 100644 index 0000000..c8bc3eb --- /dev/null +++ b/lib/uri.js @@ -0,0 +1,92 @@ +'use strict'; + +var _ = require('lodash'); + +var URL = require('url'); +var Address = require('./address'); + +var URI = function(arg, knownArgs) { + this.extras = {}; + this.knownArgs = knownArgs || []; + this.address = this.network = this.amount = this.message = null; + + if (typeof(arg) == 'string') { + var params = URI.parse(arg); + this._fromObject(params); + } else if (typeof(arg) == 'object') { + this._fromObject(arg); + } else { + throw new TypeError('Unrecognized data format.'); + } +}; + +URI.isValid = function(arg, knownArgs) { + try { + var uri = new URI(arg, knownArgs); + return true; + } catch(err) { + return false; + } +}; + +URI.parse = function(uri) { + var info = URL.parse(uri, true); + + if (info.protocol != 'bitcoin:') { + throw new TypeError('Invalid bitcoin URI'); + } + + // workaround to host insensitiveness + var group = /[^:]*:\/?\/?([^?]*)/.exec(uri); + info.query.address = group && group[1] || undefined; + + return info.query; +}; + + +URI.prototype._fromObject = function(obj) { + var members = ['address', 'amount', 'message', 'label']; + + if (!Address.isValid(obj.address)) throw new TypeError('Invalid bitcoin address'); + + this.address = new Address(obj.address); + this.network = this.address.network; + + if (obj.amount) this.amount = this._parseAmount(obj.amount); + + for (var key in obj) { + if (key === 'address' || key === 'amount') continue; + + if (/^req-/.exec(key) && this.knownArgs.indexOf(key) === -1) { + throw Error('Unknown required argument ' + key); + } + + var destination = members.indexOf(key) > -1 ? this : this.extras; + destination[key] = obj[key]; + } +}; + +URI.prototype._parseAmount = function(amount) { + var amount = Number(amount); + if (isNaN(amount)) throw new TypeError('Invalid amount'); + return amount; // TODO: Convert to Satoshis (yemel) +}; + + +URI.prototype.toString = function() { + var query = _.clone(this.extras); + if (this.amount) query.amount = this.amount; // TODO: Convert to BTC (yemel) + if (this.message) query.message = this.message; + + return URL.format({ + protocol: 'bitcoin:', + host: this.address, + query: query + }); +}; + +URI.prototype.inspect = function() { + return ''; +} + +module.exports = URI; diff --git a/test/uri.js b/test/uri.js new file mode 100644 index 0000000..db77291 --- /dev/null +++ b/test/uri.js @@ -0,0 +1,180 @@ +'use strict'; + +var chai = chai || require('chai'); +var should = chai.should(); +var expect = chai.expect; +var bitcore = require('..'); +var URI = bitcore.URI; + +describe('URI', function() { + + it('should parse uris strings', function() { + var uri; + + URI.parse.bind(URI, 'badURI').should.throw(TypeError); + + uri = URI.parse('bitcoin:'); + expect(uri.address).to.be.undefined; + expect(uri.amount).to.be.undefined; + expect(uri.otherParam).to.be.undefined; + + uri = URI.parse('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj'); + uri.address.should.equal('1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj'); + expect(uri.amount).to.be.undefined; + expect(uri.otherParam).to.be.undefined; + + uri = URI.parse('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=123.22'); + uri.address.should.equal('1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj'); + uri.amount.should.equal('123.22'); + expect(uri.otherParam).to.be.undefined; + + uri = URI.parse('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=123.22&other-param=something&req-extra=param'); + uri.address.should.equal('1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj'); + uri.amount.should.equal('123.22'); + uri['other-param'].should.equal('something'); + uri['req-extra'].should.equal('param'); + }); + + it('should statically validate uris', function() { + URI.isValid('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj').should.be.true; + URI.isValid('bitcoin:mkYY5NRvikVBY1EPtaq9fAFgquesdjqECw').should.be.true; + + URI.isValid('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=1.2').should.be.true; + URI.isValid('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=1.2&other=param').should.be.true; + URI.isValid('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=1.2&req-other=param', ['req-other']).should.be.true; + URI.isValid('bitcoin:mmrqEBJxUCf42vdb3oozZtyz5mKr3Vb2Em?amount=0.1&r=https%3A%2F%2Ftest.bitpay.com%2Fi%2F6DKgf8cnJC388irbXk5hHu').should.be.true; + + URI.isValid('bitcoin:').should.be.false; + URI.isValid('bitcoin:badUri').should.be.false; + URI.isValid('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfk?amount=bad').should.be.false; + URI.isValid('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfk?amount=1.2&req-other=param').should.be.false; + URI.isValid('bitcoin:?r=https%3A%2F%2Ftest.bitpay.com%2Fi%2F6DKgf8cnJC388irbXk5hHu').should.be.false; + }); + + it('should fail creation with no params', function() { + (function(){ + new URI(); + }).should.throw(TypeError); + }); + + it('should create instance from bitcoin uri', function() { + var uri; + + uri = new URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj'); + uri.address.should.be.instanceof(bitcore.Address); + uri.network.should.equal('mainnet'); + + uri = new URI('bitcoin:mkYY5NRvikVBY1EPtaq9fAFgquesdjqECw'); + uri.address.should.be.instanceof(bitcore.Address); + uri.network.should.equal('testnet'); + + uri = new URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=1.2&other=param'); + uri.address.should.be.instanceof(bitcore.Address); + uri.amount.should.equal(1.2); + expect(uri.other).to.be.undefined; + uri.extras.other.should.equal('param'); + + (function() { + new URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=1.2&other=param&req-required=param'); + }).should.throw(Error); + + uri = new URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=1.2&other=param&req-required=param', ['req-required']); + uri.address.should.be.instanceof(bitcore.Address); + uri.amount.should.equal(1.2); + uri.extras.other.should.equal('param'); + uri.extras['req-required'].should.equal('param'); + }); + + it('should create instance from object', function() { + var uri; + + uri = new URI({ + address: '1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj' + }); + uri.address.should.be.instanceof(bitcore.Address); + uri.network.should.equal('mainnet'); + + uri = new URI({ + address: 'mkYY5NRvikVBY1EPtaq9fAFgquesdjqECw' + }); + uri.address.should.be.instanceof(bitcore.Address); + uri.network.should.equal('testnet'); + + + uri = new URI({ + address: '1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj', + amount: '1.2', + other: 'param' + }); + uri.address.should.be.instanceof(bitcore.Address); + uri.amount.should.equal(1.2); + expect(uri.other).to.be.undefined; + uri.extras.other.should.equal('param'); + + (function() { + new URI({ + address: '1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj', + 'req-required': param + }); + }).should.throw(Error); + + uri = new URI({ + address: '1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj', + amount: 1.2, + other: 'param', + 'req-required': 'param' + }, ['req-required']); + uri.address.should.be.instanceof(bitcore.Address); + uri.amount.should.equal(1.2); + uri.extras.other.should.equal('param'); + uri.extras['req-required'].should.equal('param'); + }); + + it('should support double slash scheme', function() { + var uri = new URI('bitcoin://1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj'); + uri.address.toString().should.equal('1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj'); + }); + + it('should support numeric amounts', function() { + var uri = new URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=12.10001'); + expect(uri.amount).to.be.equal(12.10001); + }); + + it('should support extra arguments', function() { + var uri = new URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?message=Donation%20for%20project%20xyz&label=myLabel&other=xD'); + + should.exist(uri.message); + uri.message.should.equal('Donation for project xyz'); + + should.exist(uri.label); + uri.label.should.equal('myLabel'); + + should.exist(uri.extras.other); + uri.extras.other.should.equal('xD'); + }); + + it('should generate a valid URI', function() { + new URI({ + address: '1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj', + }).toString().should.equal( + 'bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj' + ); + + new URI({ + address: '1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj', + amount: 1.10001, + message: 'Hello World', + something: 'else' + }).toString().should.equal( + 'bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?something=else&amount=1.10001&message=Hello%20World' + ); + + }); + + it('should be case insensitive to protocol', function() { + var uri1 = new URI('bItcOin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj'); + var uri2 = new URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj'); + + uri1.address.toString().should.equal(uri2.address.toString()); + }); +}); From db4561f8342cf49b8a64c351b62c461cb653e4f7 Mon Sep 17 00:00:00 2001 From: Yemel Jardi Date: Fri, 28 Nov 2014 12:35:53 -0300 Subject: [PATCH 3/4] Add documentation --- docs/URI.md | 44 +++++++++++++++++++++++ lib/address.js | 2 +- lib/uri.js | 98 +++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 130 insertions(+), 14 deletions(-) create mode 100644 docs/URI.md diff --git a/docs/URI.md b/docs/URI.md new file mode 100644 index 0000000..17578eb --- /dev/null +++ b/docs/URI.md @@ -0,0 +1,44 @@ +# URI + +Represents a bitcoin payment uri. Bitcoin URI strings became the most popular +way to share payment request, sometimes as a bitcoin link and others using a QR code. + +URI Examples: +``` +bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu +bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu?amount=1.2 +bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu?amount=1.2&message=Payment&label=Satoshi&extra=other-param +``` + +The main use that we expect you'll have for the `URI` class in bitcore is +validating and parsing bitcoin URIs. A `URI` instance exposes the address as a +bitcore `Address` object and the amount in Satoshis, if present. + +The code for validating uris looks like this: +```javascript +var uriString = 'bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu?amount=1.2'; +var valid = URI.isValid(uriString); +var uri = new URI(uriString); +console.log(uri.address.network, uri.amount); // 'livenet', 120000000 +``` + +All standard parameters can be found as members of the `URI` instance. However +a bitcoin uri may contain other non-standard parameters, all those can be found +under the `extra` namespace. + +See [the official BIP21 spec](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki) +for more information. + +Other usecase important usecase for the `URI` class is creating a bitcoin URI for +sharing a payment request. That can be acomplished by using an Object to create +an instance of URI. + +The code for creating an URI from an Object looks like this: +```javascript +var uriString = new URI({ + address: '12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu', + amount : 10000, // in satoshis + message: 'My payment request' +}); +var uriString = uri.toString(); +``` diff --git a/lib/address.js b/lib/address.js index 957f53c..fe3d6ec 100644 --- a/lib/address.js +++ b/lib/address.js @@ -329,7 +329,7 @@ Address.getValidationError = function(data, network, type) { * @param {String} data - The encoded data * @param {String} network - The network: 'mainnet' or 'testnet' * @param {String} type - The type of address: 'script' or 'pubkey' - * @returns {null|Error} The corresponding error message + * @returns {boolean} The corresponding error message */ Address.isValid = function(data, network, type) { return !Address.getValidationError(data, network, type); diff --git a/lib/uri.js b/lib/uri.js index c8bc3eb..668aa2c 100644 --- a/lib/uri.js +++ b/lib/uri.js @@ -1,34 +1,79 @@ 'use strict'; var _ = require('lodash'); - var URL = require('url'); + var Address = require('./address'); -var URI = function(arg, knownArgs) { +/** + * + * Bitcore URI + * + * Instantiate an URI from a bitcoin URI String or an Object. An URI instance + * can be created with a bitcoin uri string or an object. All instances of + * URI are valid, the static method isValid allows checking before instanciation. + * + * All standard parameters can be found as members of the class, the address + * is represented using an {Address} instance and the amount is represented in + * satoshis. Any other non-standard parameters can be found under the extra member. + * + * @example + * + * var uri = new URI('bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu?amount=1.2'); + * console.log(uri.address, uri.amount); + * + * @param {string|Object} data - A bitcoin URI string or an Object + * @param {Array.} [knownParams] - Required non-standard params + * @throws {TypeError} Invalid bitcoin address + * @throws {TypeError} Invalid amount + * @throws {Error} Unknown required argument + * @returns {URI} A new valid and frozen instance of URI + */ +var URI = function(data, knownParams) { this.extras = {}; - this.knownArgs = knownArgs || []; + this.knownParams = knownParams || []; this.address = this.network = this.amount = this.message = null; - if (typeof(arg) == 'string') { - var params = URI.parse(arg); + if (typeof(data) == 'string') { + var params = URI.parse(data); this._fromObject(params); - } else if (typeof(arg) == 'object') { - this._fromObject(arg); + } else if (typeof(data) == 'object') { + this._fromObject(data); } else { throw new TypeError('Unrecognized data format.'); } }; -URI.isValid = function(arg, knownArgs) { +/** + * + * Check if an bitcoin URI string is valid + * + * @example + * + * var valid = URI.isValid('bitcoin:12A1MyfXbW6RhdRAZEqofac5jCQQjwEPBu'); + * // true + * + * @param {string|Object} data - A bitcoin URI string or an Object + * @param {Array.} [knownParams] - Required non-standard params + * @returns {boolean} Result of uri validation + */ +URI.isValid = function(arg, knownParams) { try { - var uri = new URI(arg, knownArgs); + var uri = new URI(arg, knownParams); return true; } catch(err) { return false; } }; +/** + * + * Convert a bitcoin URI string into a simple object. + * + * @param {string} uri - A bitcoin URI string + * @throws {TypeError} Invalid bitcoin URI + * @returns {Object} An object with the parsed params + */ URI.parse = function(uri) { var info = URL.parse(uri, true); @@ -43,9 +88,17 @@ URI.parse = function(uri) { return info.query; }; - +/** + * + * Internal function to load the URI instance with an object. + * + * @param {Object} amount - Amount BTC string + * @throws {TypeError} Invalid bitcoin address + * @throws {TypeError} Invalid amount + * @throws {Error} Unknown required argument + */ URI.prototype._fromObject = function(obj) { - var members = ['address', 'amount', 'message', 'label']; + var members = ['address', 'amount', 'message', 'label', 'r']; if (!Address.isValid(obj.address)) throw new TypeError('Invalid bitcoin address'); @@ -57,7 +110,7 @@ URI.prototype._fromObject = function(obj) { for (var key in obj) { if (key === 'address' || key === 'amount') continue; - if (/^req-/.exec(key) && this.knownArgs.indexOf(key) === -1) { + if (/^req-/.exec(key) && this.knownParams.indexOf(key) === -1) { throw Error('Unknown required argument ' + key); } @@ -66,13 +119,26 @@ URI.prototype._fromObject = function(obj) { } }; +/** + * + * Internal function to transform a BTC string amount into satoshis + * + * @param {String} amount - Amount BTC string + * @throws {TypeError} Invalid amount + * @returns {Object} Amount represented in satoshis + */ URI.prototype._parseAmount = function(amount) { var amount = Number(amount); if (isNaN(amount)) throw new TypeError('Invalid amount'); return amount; // TODO: Convert to Satoshis (yemel) }; - +/** + * + * Will return a the string representation of the URI + * + * @returns {String} Bitcoin URI string + */ URI.prototype.toString = function() { var query = _.clone(this.extras); if (this.amount) query.amount = this.amount; // TODO: Convert to BTC (yemel) @@ -85,6 +151,12 @@ URI.prototype.toString = function() { }); }; +/** + * + * Will return a string formatted for the console + * + * @returns {String} Bitcoin URI + */ URI.prototype.inspect = function() { return ''; } From 9cd9eeba1f91e6033b63005922d7b447bcb5a3c6 Mon Sep 17 00:00:00 2001 From: Yemel Jardi Date: Mon, 1 Dec 2014 17:36:52 -0300 Subject: [PATCH 4/4] convert amout to satoshis --- lib/uri.js | 11 ++++++----- test/uri.js | 16 ++++++++-------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/uri.js b/lib/uri.js index 668aa2c..6356d44 100644 --- a/lib/uri.js +++ b/lib/uri.js @@ -4,6 +4,7 @@ var _ = require('lodash'); var URL = require('url'); var Address = require('./address'); +var Unit = require('./unit'); /** * @@ -36,6 +37,7 @@ var URI = function(data, knownParams) { if (typeof(data) == 'string') { var params = URI.parse(data); + if (params.amount) params.amount = this._parseAmount(params.amount); this._fromObject(params); } else if (typeof(data) == 'object') { this._fromObject(data); @@ -92,7 +94,7 @@ URI.parse = function(uri) { * * Internal function to load the URI instance with an object. * - * @param {Object} amount - Amount BTC string + * @param {Object} obj - Object with the information * @throws {TypeError} Invalid bitcoin address * @throws {TypeError} Invalid amount * @throws {Error} Unknown required argument @@ -104,8 +106,7 @@ URI.prototype._fromObject = function(obj) { this.address = new Address(obj.address); this.network = this.address.network; - - if (obj.amount) this.amount = this._parseAmount(obj.amount); + this.amount = obj.amount; for (var key in obj) { if (key === 'address' || key === 'amount') continue; @@ -130,7 +131,7 @@ URI.prototype._fromObject = function(obj) { URI.prototype._parseAmount = function(amount) { var amount = Number(amount); if (isNaN(amount)) throw new TypeError('Invalid amount'); - return amount; // TODO: Convert to Satoshis (yemel) + return Unit.fromBTC(amount).toSatoshis(); }; /** @@ -141,7 +142,7 @@ URI.prototype._parseAmount = function(amount) { */ URI.prototype.toString = function() { var query = _.clone(this.extras); - if (this.amount) query.amount = this.amount; // TODO: Convert to BTC (yemel) + if (this.amount) query.amount = Unit.fromSatoshis(this.amount).toBTC(); if (this.message) query.message = this.message; return URL.format({ diff --git a/test/uri.js b/test/uri.js index db77291..1eaa3da 100644 --- a/test/uri.js +++ b/test/uri.js @@ -70,7 +70,7 @@ describe('URI', function() { uri = new URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=1.2&other=param'); uri.address.should.be.instanceof(bitcore.Address); - uri.amount.should.equal(1.2); + uri.amount.should.equal(120000000); expect(uri.other).to.be.undefined; uri.extras.other.should.equal('param'); @@ -80,7 +80,7 @@ describe('URI', function() { uri = new URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=1.2&other=param&req-required=param', ['req-required']); uri.address.should.be.instanceof(bitcore.Address); - uri.amount.should.equal(1.2); + uri.amount.should.equal(120000000); uri.extras.other.should.equal('param'); uri.extras['req-required'].should.equal('param'); }); @@ -103,11 +103,11 @@ describe('URI', function() { uri = new URI({ address: '1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj', - amount: '1.2', + amount: 120000000, other: 'param' }); uri.address.should.be.instanceof(bitcore.Address); - uri.amount.should.equal(1.2); + uri.amount.should.equal(120000000); expect(uri.other).to.be.undefined; uri.extras.other.should.equal('param'); @@ -120,12 +120,12 @@ describe('URI', function() { uri = new URI({ address: '1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj', - amount: 1.2, + amount: 120000000, other: 'param', 'req-required': 'param' }, ['req-required']); uri.address.should.be.instanceof(bitcore.Address); - uri.amount.should.equal(1.2); + uri.amount.should.equal(120000000); uri.extras.other.should.equal('param'); uri.extras['req-required'].should.equal('param'); }); @@ -137,7 +137,7 @@ describe('URI', function() { it('should support numeric amounts', function() { var uri = new URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=12.10001'); - expect(uri.amount).to.be.equal(12.10001); + expect(uri.amount).to.be.equal(1210001000); }); it('should support extra arguments', function() { @@ -162,7 +162,7 @@ describe('URI', function() { new URI({ address: '1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj', - amount: 1.10001, + amount: 110001000, message: 'Hello World', something: 'else' }).toString().should.equal(