/**
* uri.js - handshake uri parsing for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const Address = require('../primitives/address');
const Amount = require('./amount');
const pkg = require('../pkg');
/** @typedef {import('../types').Amount} AmountValue */
/** @typedef {import('../types').NetworkType} NetworkType */
/** @typedef {import('../protocol/network')} Network */
/**
* @typedef {Object} URIOptions
* @property {Address} address
* @property {AmountValue?} [amount]
* @property {String?} [label]
* @property {String?} [message]
* @property {String?} [request]
*/
/**
* URI
* Represents a handshake URI.
* @alias module:ui.URI
* @property {Address} address
* @property {AmountValue} amount
* @property {String|null} label
* @property {String|null} message
* @property {String|null} request
*/
class URI {
/**
* Create a handshake URI.
* @alias module:ui.URI
* @constructor
* @param {(URIOptions|String)?} [options]
*/
constructor(options) {
this.address = new Address();
this.amount = -1;
/** @type {String?} */
this.label = null;
/** @type {String?} */
this.message = null;
/** @type {String?} */
this.request = null;
if (options)
this.fromOptions(options);
}
/**
* Inject properties from options object.
* @param {URIOptions|String} options
* @returns {URI}
*/
fromOptions(options) {
if (typeof options === 'string')
return this.fromString(options);
if (options.address)
this.address.fromOptions(options.address);
if (options.amount != null) {
assert(Number.isSafeInteger(options.amount) && options.amount >= 0,
'Amount must be a uint64.');
this.amount = options.amount;
}
if (options.label) {
assert(typeof options.label === 'string', 'Label must be a string.');
this.label = options.label;
}
if (options.message) {
assert(typeof options.message === 'string', 'Message must be a string.');
this.message = options.message;
}
if (options.request) {
assert(typeof options.request === 'string', 'Request must be a string.');
this.request = options.request;
}
return this;
}
/**
* Instantiate URI from options.
* @param {URIOptions|String} options
* @returns {URI}
*/
static fromOptions(options) {
return new this().fromOptions(options);
}
/**
* Parse and inject properties from string.
* @private
* @param {String} str
* @param {(NetworkType|Network)?} [network]
* @returns {URI}
*/
fromString(str, network) {
assert(typeof str === 'string');
assert(str.length <= 1000, 'Invalid URI.');
const i = str.indexOf(':');
assert(i !== -1, 'Invalid URI.');
const prefix = str.substring(0, i);
assert(prefix === pkg.currency, 'Invalid URI.');
str = str.substring(i + 1);
const index = str.indexOf('?');
let addr, qs;
if (index === -1) {
addr = str;
} else {
addr = str.substring(0, index);
qs = str.substring(index + 1);
}
this.address.fromString(addr, network);
if (!qs)
return this;
const query = parsePairs(qs);
if (query.amount) {
assert(query.amount.length > 0, 'Value is empty.');
assert(query.amount[0] !== '-', 'Value is negative.');
this.amount = Amount.value(query.amount);
}
if (query.label)
this.label = query.label;
if (query.message)
this.message = query.message;
if (query.r)
this.request = query.r;
return this;
}
/**
* Instantiate uri from string.
* @param {String} str
* @param {(NetworkType|Network)?} [network]
* @returns {URI}
*/
static fromString(str, network) {
return new this().fromString(str, network);
}
/**
* Serialize uri to a string.
* @returns {String}
*/
toString() {
let str = `${pkg.currency}:`;
str += this.address.toString();
const query = [];
if (this.amount !== -1)
query.push(`amount=${Amount.coin(this.amount)}`);
if (this.label)
query.push(`label=${escape(this.label)}`);
if (this.message)
query.push(`message=${escape(this.message)}`);
if (this.request)
query.push(`r=${escape(this.request)}`);
if (query.length > 0)
str += '?' + query.join('&');
return str;
}
/**
* Inspect handshake uri.
* @returns {String}
*/
inspect() {
return `<URI: ${this.toString()}>`;
}
}
/*
* Helpers
*/
class HandshakeQuery {
constructor() {
this.amount = null;
this.label = null;
this.message = null;
this.r = null;
}
}
function parsePairs(str) {
const parts = str.split('&');
const data = new HandshakeQuery();
let size = 0;
for (const pair of parts) {
const index = pair.indexOf('=');
let key, value;
if (index === -1) {
key = pair;
value = '';
} else {
key = pair.substring(0, index);
value = pair.substring(index + 1);
}
if (key.length === 0) {
assert(value.length === 0, 'Empty key in querystring.');
continue;
}
assert(size < 4, 'Too many keys in querystring.');
switch (key) {
case 'amount':
assert(data.amount == null, 'Duplicate key in querystring (amount).');
data.amount = unescape(value);
break;
case 'label':
assert(data.label == null, 'Duplicate key in querystring (label).');
data.label = unescape(value);
break;
case 'message':
assert(data.message == null, 'Duplicate key in querystring (message).');
data.message = unescape(value);
break;
case 'r':
assert(data.r == null, 'Duplicate key in querystring (r).');
data.r = unescape(value);
break;
default:
assert(false, `Unknown querystring key: ${value}.`);
break;
}
size += 1;
}
return data;
}
function unescape(str) {
try {
str = decodeURIComponent(str);
str = str.replace(/\+/g, ' ');
} catch (e) {
throw new Error('Malformed URI.');
}
if (str.indexOf('\0') !== -1)
throw new Error('Malformed URI.');
return str;
}
function escape(str) {
str = encodeURIComponent(str);
str = str.replace(/%20/g, '+');
return str;
}
/*
* Expose
*/
module.exports = URI;