5 changed files with 393 additions and 2 deletions
@ -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(); |
||||
|
``` |
@ -0,0 +1,165 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
var _ = require('lodash'); |
||||
|
var URL = require('url'); |
||||
|
|
||||
|
var Address = require('./address'); |
||||
|
var Unit = require('./unit'); |
||||
|
|
||||
|
/** |
||||
|
* |
||||
|
* 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.<string>} [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.knownParams = knownParams || []; |
||||
|
this.address = this.network = this.amount = this.message = null; |
||||
|
|
||||
|
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); |
||||
|
} else { |
||||
|
throw new TypeError('Unrecognized data format.'); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* |
||||
|
* 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.<string>} [knownParams] - Required non-standard params |
||||
|
* @returns {boolean} Result of uri validation |
||||
|
*/ |
||||
|
URI.isValid = function(arg, knownParams) { |
||||
|
try { |
||||
|
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); |
||||
|
|
||||
|
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; |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* |
||||
|
* Internal function to load the URI instance with an object. |
||||
|
* |
||||
|
* @param {Object} obj - Object with the information |
||||
|
* @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', 'r']; |
||||
|
|
||||
|
if (!Address.isValid(obj.address)) throw new TypeError('Invalid bitcoin address'); |
||||
|
|
||||
|
this.address = new Address(obj.address); |
||||
|
this.network = this.address.network; |
||||
|
this.amount = obj.amount; |
||||
|
|
||||
|
for (var key in obj) { |
||||
|
if (key === 'address' || key === 'amount') continue; |
||||
|
|
||||
|
if (/^req-/.exec(key) && this.knownParams.indexOf(key) === -1) { |
||||
|
throw Error('Unknown required argument ' + key); |
||||
|
} |
||||
|
|
||||
|
var destination = members.indexOf(key) > -1 ? this : this.extras; |
||||
|
destination[key] = obj[key]; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* |
||||
|
* 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 Unit.fromBTC(amount).toSatoshis(); |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* |
||||
|
* 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 = Unit.fromSatoshis(this.amount).toBTC(); |
||||
|
if (this.message) query.message = this.message; |
||||
|
|
||||
|
return URL.format({ |
||||
|
protocol: 'bitcoin:', |
||||
|
host: this.address, |
||||
|
query: query |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* |
||||
|
* Will return a string formatted for the console |
||||
|
* |
||||
|
* @returns {String} Bitcoin URI |
||||
|
*/ |
||||
|
URI.prototype.inspect = function() { |
||||
|
return '<URI: ' + this.toString()+ '>'; |
||||
|
} |
||||
|
|
||||
|
module.exports = URI; |
@ -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(120000000); |
||||
|
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(120000000); |
||||
|
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: 120000000, |
||||
|
other: 'param' |
||||
|
}); |
||||
|
uri.address.should.be.instanceof(bitcore.Address); |
||||
|
uri.amount.should.equal(120000000); |
||||
|
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: 120000000, |
||||
|
other: 'param', |
||||
|
'req-required': 'param' |
||||
|
}, ['req-required']); |
||||
|
uri.address.should.be.instanceof(bitcore.Address); |
||||
|
uri.amount.should.equal(120000000); |
||||
|
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(1210001000); |
||||
|
}); |
||||
|
|
||||
|
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: 110001000, |
||||
|
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()); |
||||
|
}); |
||||
|
}); |
Loading…
Reference in new issue