Merge pull request #588 from yemel/refactor/bip21
Add support for bitcoin URIs (BIP21)
This commit is contained in:
commit
430b175d6e
|
@ -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();
|
||||
```
|
2
index.js
2
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');
|
||||
|
|
|
@ -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 {
|
||||
|
@ -327,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);
|
||||
|
|
|
@ -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