@bsv/spv-wallet-js-client
Version:
TypeScript library for connecting to a SPV Wallet server
1,354 lines (1,272 loc) • 75 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('cross-fetch/polyfill'), require('@bsv/sdk')) :
typeof define === 'function' && define.amd ? define(['exports', 'cross-fetch/polyfill', '@bsv/sdk'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.typescriptNpmPackage = {}, null, global.sdk));
})(this, (function (exports, polyfill, sdk) { 'use strict';
class SpvWalletError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
class ErrorInvalidClientOptions extends SpvWalletError {
constructor(logger, options) {
super('Invalid options. None of xPub, xPriv nor accessKey is set');
logger.debug('Invalid options: ', options);
}
}
class ErrorInvalidAdminClientOptions extends SpvWalletError {
constructor(logger, options) {
super('Invalid options. No adminKey set');
logger.debug('Invalid options: ', options);
}
}
class ErrorNoXPrivToSignTransaction extends SpvWalletError {
constructor() {
super('Cannot sign transaction without an xPriv');
}
}
class ErrorClientInitNoXpriv extends SpvWalletError {
constructor() {
super('Init client with xPriv first');
}
}
class ErrorTxIdsDontMatchToDraft extends SpvWalletError {
constructor(logger, input, index, draftInput) {
super('Input tx ids do not match in draft and transaction hex');
logger.debug('The input: ', input, 'Tx index: ', index, 'The draft', draftInput);
this.input = input;
this.draftInput = draftInput;
}
}
class ErrorNoAdminKey extends SpvWalletError {
constructor() {
super('Admin key has not been set. Cannot do admin queries.');
}
}
class ErrorResponse extends SpvWalletError {
constructor(logger, response, content) {
super('Received error response');
logger.debug('StatusCode:', response.status, 'Error response:', response, 'The content:', content);
this.response = response;
this.content = content;
}
}
class ErrorWrongHex extends SpvWalletError {
constructor(wrongHex) {
super('Provided hexHash is not a valid hex string');
this.value = wrongHex;
}
}
class ErrorNoXPrivToGenerateTOTP extends SpvWalletError {
constructor() {
super('Cannot generate TOTP without an xPrivKey set');
}
}
class ErrorNoXPrivToValidateTOTP extends SpvWalletError {
constructor() {
super('Cannot validate TOTP without an xPrivKey set');
}
}
class ErrorWrongTOTP extends SpvWalletError {
constructor() {
super('TOTP is invalid');
}
}
class ErrorSyncMerkleRootsTimeout extends SpvWalletError {
constructor() {
super('SyncMerkleRoots operation timed out');
}
}
class ErrorStaleLastEvaluatedKey extends SpvWalletError {
constructor() {
super('The last evaluated key has not changed between requests, indicating a possible loop or synchronization issue.');
}
}
const maxInt32 = 2147483648 - 1; // 0x80000000
// RandomHex returns a random hex string and error
const RandomHex = function (n) {
return sdk.Utils.toHex(sdk.Random(n));
};
// ToHash returns a sha256 hash of the string
const ToHash = function (string) {
const sha256 = sdk.Hash.sha256(string);
return sdk.Utils.toHex(sha256);
};
// isHex returns whether the given hex string a valid hex string is
const isHex = function (hexString) {
return !!hexString.match(/^[0-9a-f]*$/i);
};
// getChildNumsFromHex get an array of numbers from the hex string
const getChildNumsFromHex = function (hexHash) {
if (!isHex(hexHash)) {
throw new ErrorWrongHex(hexHash);
}
const strLen = hexHash.length;
const size = 8;
const splitLength = Math.ceil(strLen / size);
const childNums = [];
for (let i = 0; i < splitLength; i++) {
const start = i * size;
let stop = start + size;
if (stop > strLen) {
stop = strLen;
}
let num = Number('0x' + hexHash.substring(start, stop));
if (num > maxInt32) {
num = num - maxInt32;
}
childNums.push(num);
}
return childNums;
};
// deriveChildKeyFromHex derive the child extended key from the hex string
const deriveChildKeyFromHex = function (hdKey, hexHash) {
return deriveHDChildKeyFromHex(hdKey, hexHash);
};
const deriveHDChildKeyFromHex = function (hdKey, hexHash) {
let childKey = hdKey;
const childNums = getChildNumsFromHex(hexHash);
childNums.forEach((childNum) => {
childKey = childKey.deriveChild(childNum);
});
return childKey;
};
const generateKeys = function () {
const mnemonic = sdk.Mnemonic.fromRandom();
return getKeysFromMnemonic(mnemonic.toString());
};
const getKeysFromMnemonic = function (mnemonicStr) {
const mnemonic = sdk.Mnemonic.fromString(mnemonicStr);
const seed = mnemonic.toSeed();
const hdWallet = new sdk.HD().fromSeed(seed);
return {
xPriv: () => hdWallet.toString(),
mnemonic: mnemonic.toString(),
xPub: {
toString() {
return hdWallet.toPublic().toString();
},
},
};
};
const getKeysFromString = function (privateKey) {
let hdWallet = new sdk.HD().fromString(privateKey);
return {
xPriv: () => hdWallet.privKey.toString(),
xPub: {
toString() {
return hdWallet.toPublic().toString();
},
},
};
};
const signMessage = function (message, privateKey) {
const messageBuf = sdk.Utils.toArray(message);
const hash = sdk.BSM.magicHash(messageBuf);
const bnh = new sdk.BigNumber(hash);
const signature = sdk.ECDSA.sign(bnh, privateKey, true);
const recovery = signature.CalculateRecoveryFactor(privateKey.toPublicKey(), bnh);
return signature.toCompact(recovery, true, 'base64');
};
// AuthHeader is the header to use for authentication (raw xPub)
const AuthHeader = 'x-auth-xpub';
// AuthAccessKey is the header to use for access key authentication (access public key)
const AuthAccessKey = 'x-auth-key';
// AuthSignature is the given signature (body + timestamp)
const AuthSignature = 'x-auth-signature';
// AuthHeaderHash hash of the body coming from the request
const AuthHeaderHash = 'x-auth-hash';
// AuthHeaderNonce random nonce for the request
const AuthHeaderNonce = 'x-auth-nonce';
// AuthHeaderTime the time of the request, only valid for 30 seconds
const AuthHeaderTime = 'x-auth-time';
const setSignature = function (headers, signingKey, bodyString) {
// Create the signature
const authData = createSignature(signingKey, bodyString);
// Set the auth header
if (authData.xPub) {
headers[AuthHeader] = authData.xPub;
}
else if (authData.accessKey) {
headers[AuthAccessKey] = authData.accessKey;
}
return setSignatureHeaders(headers, authData);
};
const setSignatureHeaders = function (headers, authData) {
// Create the auth header hash
if (authData.AuthHash) {
headers[AuthHeaderHash] = authData.AuthHash;
}
// Set the nonce
if (authData.AuthNonce) {
headers[AuthHeaderNonce] = authData.AuthNonce;
}
// Set the time
if (authData.AuthTime) {
headers[AuthHeaderTime] = authData.AuthTime.toString();
}
// Set the signature
if (authData.Signature) {
headers[AuthSignature] = authData.Signature;
}
return headers;
};
const createSignature = function (signingKey, bodyString) {
const payload = {};
// x-auth-nonce is a random unique string to seed the signing message
// this can be checked server side to make sure the request is not being replayed
payload.AuthNonce = RandomHex(32);
let privateKey;
if (isHDWallet(signingKey)) {
// Get the xPub
payload.xPub = signingKey.toPublic().toString();
payload.accessKey = undefined;
// Derive the address for signing
const hdWallet = deriveHDChildKeyFromHex(signingKey, payload.AuthNonce);
privateKey = hdWallet.privKey;
}
else {
privateKey = signingKey;
payload.xPub = undefined;
payload.accessKey = privateKey.toPublicKey().toString();
}
return createSignatureCommon(payload, bodyString, privateKey);
};
const createSignatureCommon = function (payload, bodyString, privateKey) {
// Create the auth header hash
payload.AuthHash = ToHash(bodyString);
// x-auth-time is the current time and makes sure a request can not be sent after 30 secs
payload.AuthTime = +new Date();
let key = payload.xPub;
if (!key && payload.accessKey) {
key = payload.accessKey;
}
// Signature, using bitcoin signMessage
const message = getSigningMessage(key || '', payload);
payload.Signature = signMessage(message, privateKey);
return payload;
};
// getSigningMessage will build the signing message string
const getSigningMessage = function (xPub, auth) {
return `${xPub}${auth.AuthHash}${auth.AuthNonce}${auth.AuthTime}`;
};
const isHDWallet = (key) => {
return key != null && key instanceof sdk.HD;
};
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
var domain;
// This constructor is used to store event handlers. Instantiating this is
// faster than explicitly calling `Object.create(null)` to get a "clean" empty
// object (tested with v8 v4.9).
function EventHandlers() {}
EventHandlers.prototype = Object.create(null);
function EventEmitter() {
EventEmitter.init.call(this);
}
// nodejs oddity
// require('events') === require('events').EventEmitter
EventEmitter.EventEmitter = EventEmitter;
EventEmitter.usingDomains = false;
EventEmitter.prototype.domain = undefined;
EventEmitter.prototype._events = undefined;
EventEmitter.prototype._maxListeners = undefined;
// By default EventEmitters will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
EventEmitter.defaultMaxListeners = 10;
EventEmitter.init = function() {
this.domain = null;
if (EventEmitter.usingDomains) {
// if there is an active domain, then attach to it.
if (domain.active && !(this instanceof domain.Domain)) {
this.domain = domain.active;
}
}
if (!this._events || this._events === Object.getPrototypeOf(this)._events) {
this._events = new EventHandlers();
this._eventsCount = 0;
}
this._maxListeners = this._maxListeners || undefined;
};
// Obviously not all Emitters should be limited to 10. This function allows
// that to be increased. Set to zero for unlimited.
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
if (typeof n !== 'number' || n < 0 || isNaN(n))
throw new TypeError('"n" argument must be a positive number');
this._maxListeners = n;
return this;
};
function $getMaxListeners(that) {
if (that._maxListeners === undefined)
return EventEmitter.defaultMaxListeners;
return that._maxListeners;
}
EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
return $getMaxListeners(this);
};
// These standalone emit* functions are used to optimize calling of event
// handlers for fast cases because emit() itself often has a variable number of
// arguments and can be deoptimized because of that. These functions always have
// the same number of arguments and thus do not get deoptimized, so the code
// inside them can execute faster.
function emitNone(handler, isFn, self) {
if (isFn)
handler.call(self);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self);
}
}
function emitOne(handler, isFn, self, arg1) {
if (isFn)
handler.call(self, arg1);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self, arg1);
}
}
function emitTwo(handler, isFn, self, arg1, arg2) {
if (isFn)
handler.call(self, arg1, arg2);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self, arg1, arg2);
}
}
function emitThree(handler, isFn, self, arg1, arg2, arg3) {
if (isFn)
handler.call(self, arg1, arg2, arg3);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self, arg1, arg2, arg3);
}
}
function emitMany(handler, isFn, self, args) {
if (isFn)
handler.apply(self, args);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].apply(self, args);
}
}
EventEmitter.prototype.emit = function emit(type) {
var er, handler, len, args, i, events, domain;
var doError = (type === 'error');
events = this._events;
if (events)
doError = (doError && events.error == null);
else if (!doError)
return false;
domain = this.domain;
// If there is no 'error' event listener then throw.
if (doError) {
er = arguments[1];
if (domain) {
if (!er)
er = new Error('Uncaught, unspecified "error" event');
er.domainEmitter = this;
er.domain = domain;
er.domainThrown = false;
domain.emit('error', er);
} else if (er instanceof Error) {
throw er; // Unhandled 'error' event
} else {
// At least give some kind of context to the user
var err = new Error('Uncaught, unspecified "error" event. (' + er + ')');
err.context = er;
throw err;
}
return false;
}
handler = events[type];
if (!handler)
return false;
var isFn = typeof handler === 'function';
len = arguments.length;
switch (len) {
// fast cases
case 1:
emitNone(handler, isFn, this);
break;
case 2:
emitOne(handler, isFn, this, arguments[1]);
break;
case 3:
emitTwo(handler, isFn, this, arguments[1], arguments[2]);
break;
case 4:
emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]);
break;
// slower
default:
args = new Array(len - 1);
for (i = 1; i < len; i++)
args[i - 1] = arguments[i];
emitMany(handler, isFn, this, args);
}
return true;
};
function _addListener(target, type, listener, prepend) {
var m;
var events;
var existing;
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
events = target._events;
if (!events) {
events = target._events = new EventHandlers();
target._eventsCount = 0;
} else {
// To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener".
if (events.newListener) {
target.emit('newListener', type,
listener.listener ? listener.listener : listener);
// Re-assign `events` because a newListener handler could have caused the
// this._events to be assigned to a new object
events = target._events;
}
existing = events[type];
}
if (!existing) {
// Optimize the case of one listener. Don't need the extra array object.
existing = events[type] = listener;
++target._eventsCount;
} else {
if (typeof existing === 'function') {
// Adding the second element, need to change to array.
existing = events[type] = prepend ? [listener, existing] :
[existing, listener];
} else {
// If we've already got an array, just append.
if (prepend) {
existing.unshift(listener);
} else {
existing.push(listener);
}
}
// Check for listener leak
if (!existing.warned) {
m = $getMaxListeners(target);
if (m && m > 0 && existing.length > m) {
existing.warned = true;
var w = new Error('Possible EventEmitter memory leak detected. ' +
existing.length + ' ' + type + ' listeners added. ' +
'Use emitter.setMaxListeners() to increase limit');
w.name = 'MaxListenersExceededWarning';
w.emitter = target;
w.type = type;
w.count = existing.length;
emitWarning(w);
}
}
}
return target;
}
function emitWarning(e) {
typeof console.warn === 'function' ? console.warn(e) : console.log(e);
}
EventEmitter.prototype.addListener = function addListener(type, listener) {
return _addListener(this, type, listener, false);
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.prependListener =
function prependListener(type, listener) {
return _addListener(this, type, listener, true);
};
function _onceWrap(target, type, listener) {
var fired = false;
function g() {
target.removeListener(type, g);
if (!fired) {
fired = true;
listener.apply(target, arguments);
}
}
g.listener = listener;
return g;
}
EventEmitter.prototype.once = function once(type, listener) {
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
this.on(type, _onceWrap(this, type, listener));
return this;
};
EventEmitter.prototype.prependOnceListener =
function prependOnceListener(type, listener) {
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
this.prependListener(type, _onceWrap(this, type, listener));
return this;
};
// emits a 'removeListener' event iff the listener was removed
EventEmitter.prototype.removeListener =
function removeListener(type, listener) {
var list, events, position, i, originalListener;
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
events = this._events;
if (!events)
return this;
list = events[type];
if (!list)
return this;
if (list === listener || (list.listener && list.listener === listener)) {
if (--this._eventsCount === 0)
this._events = new EventHandlers();
else {
delete events[type];
if (events.removeListener)
this.emit('removeListener', type, list.listener || listener);
}
} else if (typeof list !== 'function') {
position = -1;
for (i = list.length; i-- > 0;) {
if (list[i] === listener ||
(list[i].listener && list[i].listener === listener)) {
originalListener = list[i].listener;
position = i;
break;
}
}
if (position < 0)
return this;
if (list.length === 1) {
list[0] = undefined;
if (--this._eventsCount === 0) {
this._events = new EventHandlers();
return this;
} else {
delete events[type];
}
} else {
spliceOne(list, position);
}
if (events.removeListener)
this.emit('removeListener', type, originalListener || listener);
}
return this;
};
EventEmitter.prototype.removeAllListeners =
function removeAllListeners(type) {
var listeners, events;
events = this._events;
if (!events)
return this;
// not listening for removeListener, no need to emit
if (!events.removeListener) {
if (arguments.length === 0) {
this._events = new EventHandlers();
this._eventsCount = 0;
} else if (events[type]) {
if (--this._eventsCount === 0)
this._events = new EventHandlers();
else
delete events[type];
}
return this;
}
// emit removeListener for all listeners on all events
if (arguments.length === 0) {
var keys = Object.keys(events);
for (var i = 0, key; i < keys.length; ++i) {
key = keys[i];
if (key === 'removeListener') continue;
this.removeAllListeners(key);
}
this.removeAllListeners('removeListener');
this._events = new EventHandlers();
this._eventsCount = 0;
return this;
}
listeners = events[type];
if (typeof listeners === 'function') {
this.removeListener(type, listeners);
} else if (listeners) {
// LIFO order
do {
this.removeListener(type, listeners[listeners.length - 1]);
} while (listeners[0]);
}
return this;
};
EventEmitter.prototype.listeners = function listeners(type) {
var evlistener;
var ret;
var events = this._events;
if (!events)
ret = [];
else {
evlistener = events[type];
if (!evlistener)
ret = [];
else if (typeof evlistener === 'function')
ret = [evlistener.listener || evlistener];
else
ret = unwrapListeners(evlistener);
}
return ret;
};
EventEmitter.listenerCount = function(emitter, type) {
if (typeof emitter.listenerCount === 'function') {
return emitter.listenerCount(type);
} else {
return listenerCount.call(emitter, type);
}
};
EventEmitter.prototype.listenerCount = listenerCount;
function listenerCount(type) {
var events = this._events;
if (events) {
var evlistener = events[type];
if (typeof evlistener === 'function') {
return 1;
} else if (evlistener) {
return evlistener.length;
}
}
return 0;
}
EventEmitter.prototype.eventNames = function eventNames() {
return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : [];
};
// About 1.5x faster than the two-arg version of Array#splice().
function spliceOne(list, index) {
for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)
list[i] = list[k];
list.pop();
}
function arrayClone(arr, i) {
var copy = new Array(i);
while (i--)
copy[i] = arr[i];
return copy;
}
function unwrapListeners(arr) {
var ret = new Array(arr.length);
for (var i = 0; i < ret.length; ++i) {
ret[i] = arr[i].listener || arr[i];
}
return ret;
}
class EventsMap {
constructor(logger) {
this.registered = new Map();
this.logger = logger;
}
store(eventName, handler) {
var _a;
if (!this.registered.has(eventName)) {
this.registered.set(eventName, new EventEmitter());
}
(_a = this.registered.get(eventName)) === null || _a === void 0 ? void 0 : _a.on(eventName, (event) => __awaiter(this, void 0, void 0, function* () {
try {
yield handler(event);
}
catch (error) {
this.logger.error(`Error in handler for event ${eventName}`, error);
}
}));
}
load(eventName) {
return this.registered.get(eventName);
}
}
class WebhookManager {
constructor(subscriber, url, options = {}) {
this.url = url;
this.subscriber = subscriber;
this.options = {
tokenValue: options.tokenValue || '',
tokenHeader: options.tokenHeader || '',
};
this.handlers = new EventsMap(subscriber.logger);
}
subscribe() {
return this.subscriber.subscribeWebhook(this.url, this.options.tokenHeader, this.options.tokenValue);
}
unsubscribe() {
return this.subscriber.unsubscribeWebhook(this.url);
}
handleIncomingEvents(httpHandler) {
return __awaiter(this, void 0, void 0, function* () {
const token = httpHandler.getHeader(this.options.tokenHeader);
if (this.options.tokenHeader !== '' && token !== this.options.tokenValue) {
httpHandler.handleResponse(401, { message: 'Unauthorized' });
return;
}
try {
const events = httpHandler.getBody();
events.forEach((event) => {
const handler = this.handlers.load(event.type);
if (handler) {
handler.emit(event.type, event.content);
}
else {
this.subscriber.logger.debug(`No handler registered for event type: ${event.type}`);
}
});
httpHandler.handleResponse(200);
}
catch (error) {
if (error instanceof Error) {
this.subscriber.logger.error(error.message);
}
else {
this.subscriber.logger.error('Unknown error during event handling');
}
httpHandler.handleResponse(500, { message: 'error processing events' });
}
});
}
registerHandler(eventName, handlerFunction) {
this.handlers.store(eventName, handlerFunction);
}
}
const isLogger = (loggerConfig) => {
const logger = loggerConfig;
return (typeof logger.debug === 'function' &&
typeof logger.info === 'function' &&
typeof logger.warn === 'function' &&
typeof logger.error === 'function');
};
const nop = () => { };
const defaultLogger = { level: 'info' };
const levelToNumber = (level) => {
switch (level) {
case 'debug':
return 1;
case 'info':
return 2;
case 'warn':
return 3;
case 'error':
return 4;
case 'disabled':
return 5;
default:
return 2;
}
};
const makeLogger = (loggerConfig) => {
if (isLogger(loggerConfig)) {
return loggerConfig;
}
else {
const { level } = loggerConfig;
const levelAsNumber = levelToNumber(level);
return {
debug: levelAsNumber <= 1 ? console.debug : nop,
info: levelAsNumber <= 2 ? console.info : nop,
warn: levelAsNumber <= 3 ? console.warn : nop,
error: levelAsNumber <= 4 ? console.error : nop,
};
}
};
class HttpClient {
constructor(logger, url, key, adminKey) {
if (key != null) {
if (typeof key === 'string') {
//only xPub can be a string
this.xPubString = key;
}
else {
this.signingKey = key;
}
}
if (adminKey) {
this.adminKey = new sdk.HD().fromString(adminKey);
}
this.logger = logger;
this.baseUrl = url.endsWith('/') ? url : url + '/'; //make sure the url ends with a '/'
}
adminRequest(path_1) {
return __awaiter(this, arguments, void 0, function* (path, method = 'GET', payload = null) {
if (!this.hasAdminKey()) {
throw new ErrorNoAdminKey();
}
this.logger.debug('Making request as admin on', method, path);
return this.makeRequest(path, method, payload, this.adminKey);
});
}
request(path_1) {
return __awaiter(this, arguments, void 0, function* (path, method = 'GET', payload = null) {
this.logger.debug('Making request on', method, path);
return this.makeRequest(path, method, payload, this.signingKey);
});
}
hasAdminKey() {
return this.adminKey != null;
}
makeRequest(path, method, payload, currentSigningKey) {
return __awaiter(this, void 0, void 0, function* () {
const json = payload ? JSON.stringify(payload) : null;
let headers = { 'content-type': 'application/json' };
if (currentSigningKey != null) {
headers = setSignature(headers, currentSigningKey, json || '');
}
else if (this.xPubString) {
headers[AuthHeader] = this.xPubString;
}
const res = yield globalThis.fetch(this.prepareUrl(path), {
method,
headers,
body: json,
});
if (res.ok) {
const contentType = res.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) {
return res.json();
}
return res.text();
}
else {
const rawContent = yield res.text();
throw new ErrorResponse(this.logger, res, rawContent);
}
});
}
prepareUrl(path) {
path = path.startsWith('/') ? path.substring(1) : path;
return this.baseUrl + path;
}
}
function addToURLSearchParams(urlSP, params, parentKey) {
Object.entries(params).forEach(([key, value]) => {
if (!value) {
return;
}
const newKey = parentKey ? `${parentKey}[${key}]` : key;
if (typeof value === 'object' && !Array.isArray(value)) {
// Recursively flatten nested objects
addToURLSearchParams(urlSP, value, newKey);
}
else if (Array.isArray(value)) {
value.forEach((element) => {
const arrayKey = `${newKey}[]`;
urlSP.append(arrayKey, element);
});
}
else {
urlSP.append(newKey, String(value)); // ensure value is a string
}
});
}
function buildQueryPath({ filter, metadata, page: queryParams }) {
const allParams = new URLSearchParams();
if (queryParams) {
addToURLSearchParams(allParams, queryParams);
}
if (filter) {
addToURLSearchParams(allParams, filter);
}
if (metadata) {
addToURLSearchParams(allParams, metadata, 'metadata');
}
const params = new URLSearchParams(allParams);
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
/**
* SPVWalletAdminAPI class for handling administrative operations
*
* @class SPVWalletAdminAPI
*/
class SPVWalletAdminAPI {
/**
* Creates a new instance of SPVWalletAdminAPI
*
* @param {string} serverUrl - The base URL of the SPV Wallet server
* @param {AdminClientOptions} options - Configuration options including adminKey
* @param {LoggerConfig} loggerConfig - Logger configuration (optional)
*/
constructor(serverUrl, options, loggerConfig = defaultLogger) {
serverUrl = this.ensureSuffix(serverUrl, '/api/v1');
this.logger = makeLogger(loggerConfig);
this.http = this.makeRequester(options, serverUrl);
}
ensureSuffix(serverUrl, suffix) {
return serverUrl.endsWith(suffix) ? serverUrl : serverUrl + suffix;
}
makeRequester(options, serverUrl) {
if (options.adminKey) {
this.logger.info('Using adminKey to sign admin requests');
return new HttpClient(this.logger, serverUrl, undefined, options.adminKey);
}
throw new ErrorInvalidAdminClientOptions(this.logger, options);
}
/**
* Check if the admin key is valid
*
* @returns {Promise<boolean>} True if admin key is valid
*/
status() {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest('admin/status');
});
}
/**
* Get server statistics
*
* @returns {Promise<AdminStats>} Server statistics
*/
stats() {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest('admin/stats');
});
}
/**
* Get a list of all access keys in the system
*
* @param {AdminAccessKeyFilter} conditions - Filter conditions for access keys
* @param {Metadata} metadata - Metadata filter
* @param {QueryPageParams} params - Pagination parameters
* @returns {Promise<PageModel<AccessKey>>} List of access keys
*/
accessKeys(conditions, metadata, params) {
return __awaiter(this, void 0, void 0, function* () {
const basePath = 'admin/users/keys';
const queryString = buildQueryPath({
metadata,
filter: conditions,
page: params,
});
return yield this.http.adminRequest(`${basePath}${queryString}`, 'GET');
});
}
/**
* Get a list of all contacts in the system
*
* @param {AdminContactFilter} conditions - Filter conditions for contacts
* @param {Metadata} metadata - Metadata filter
* @param {QueryPageParams} params - Pagination parameters
* @returns {Promise<PageModel<Contact>>} List of contacts
*/
contacts(conditions, metadata, params) {
return __awaiter(this, void 0, void 0, function* () {
const basePath = 'admin/contacts';
const queryString = buildQueryPath({
metadata,
filter: conditions,
page: params,
});
return yield this.http.adminRequest(`${basePath}${queryString}`, 'GET');
});
}
/**
* Update contact information
*
* @param {string} id - Contact ID
* @param {string} fullName - New full name for the contact
* @param {Metadata} metadata - Updated metadata
* @returns {Promise<Contact>} Updated contact information
*/
contactUpdate(id, fullName, metadata) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest(`admin/contacts/${id}`, 'PUT', { fullName, metadata });
});
}
/**
* Delete a contact
*
* @param {string} id - ID of the contact to delete
* @returns {Promise<void>}
*/
deleteContact(id) {
return __awaiter(this, void 0, void 0, function* () {
yield this.http.adminRequest(`admin/contacts/${id}`, 'DELETE', {});
});
}
/**
* Accept a contact invitation
*
* @param {string} id - ID of the invitation to accept
* @returns {Promise<Contact>} The accepted contact
*/
acceptInvitation(id) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest(`admin/invitations/${id}`, 'POST', {});
});
}
/**
* Reject a contact invitation
*
* @param {string} id - ID of the invitation to reject
* @returns {Promise<void>}
*/
rejectInvitation(id) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest(`admin/invitations/${id}`, 'DELETE', {});
});
}
/**
* Get a transaction by ID
*
* @param {string} id - Transaction ID
* @returns {Promise<AdminTx>} Transaction details
*/
transaction(id) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest(`admin/transactions/${id}`, 'GET');
});
}
/**
* Get a list of all transactions
*
* @param {TransactionFilter} conditions - Filter conditions for transactions
* @param {Metadata} metadata - Metadata filter
* @param {QueryPageParams} params - Pagination parameters
* @returns {Promise<PageModel<AdminTx>>} List of transactions
*/
transactions(conditions, metadata, params) {
return __awaiter(this, void 0, void 0, function* () {
const basePath = 'admin/transactions';
const queryString = buildQueryPath({
filter: conditions,
metadata,
page: params,
});
return yield this.http.adminRequest(`${basePath}${queryString}`, 'GET');
});
}
/**
* Get a list of all UTXOs
*
* @param {AdminUtxoFilter} conditions - Filter conditions for UTXOs
* @param {Metadata} metadata - Metadata filter
* @param {QueryPageParams} params - Pagination parameters
* @returns {Promise<PageModel<Utxo>>} List of UTXOs
*/
utxos(conditions, metadata, params) {
return __awaiter(this, void 0, void 0, function* () {
const basePath = 'admin/utxos';
const queryString = buildQueryPath({
filter: conditions,
metadata,
page: params,
});
return yield this.http.adminRequest(`${basePath}${queryString}`, 'GET');
});
}
/**
* Get a list of all xPubs
*
* @param {XpubFilter} conditions - Filter conditions for xPubs
* @param {Metadata} metadata - Metadata filter
* @param {QueryPageParams} params - Pagination parameters
* @returns {Promise<PageModel<XPub>>} List of xPubs
*/
xPubs(conditions, metadata, params) {
return __awaiter(this, void 0, void 0, function* () {
const basePath = 'admin/users';
const queryString = buildQueryPath({
filter: conditions,
metadata,
page: params,
});
return yield this.http.adminRequest(`${basePath}${queryString}`, 'GET');
});
}
/**
* Register a new xPub
*
* @param {string} rawXPub - Raw xPub key to register
* @param {Metadata} metadata - Metadata for the xPub
* @returns {Promise<XPub>} Registered xPub information
*/
createXPub(rawXPub, metadata) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest('admin/users', 'POST', {
key: rawXPub,
metadata,
});
});
}
/**
* Get a paymail by address
*
* @param {string} id - Paymail ID or address
* @returns {Promise<PaymailAddress>} Paymail information
*/
paymail(id) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest(`admin/paymails/${id}`, 'GET');
});
}
/**
* Get a list of all paymails
*
* @param {AdminPaymailFilter} conditions - Filter conditions for paymails
* @param {Metadata} metadata - Metadata filter
* @param {QueryPageParams} params - Pagination parameters
* @returns {Promise<PageModel<PaymailAddress>>} List of paymail addresses
*/
paymails(conditions, metadata, params) {
return __awaiter(this, void 0, void 0, function* () {
const basePath = 'admin/paymails';
const queryString = buildQueryPath({
metadata,
page: params,
filter: conditions,
});
return yield this.http.adminRequest(`${basePath}${queryString}`, 'GET');
});
}
/**
* Create a new paymail
*
* @param {string} rawXPub - Raw xpub to register the paymail to
* @param {string} address - Paymail address (e.g., alias@domain.com)
* @param {string} publicName - Public name for the paymail
* @param {string} avatar - Avatar URL
* @param {Metadata} metadata - Additional metadata
* @returns {Promise<PaymailAddress>} Created paymail address
*/
createPaymail(rawXPub, address, publicName, avatar, metadata) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest('admin/paymails', 'POST', {
metadata,
key: rawXPub,
address,
publicName,
avatar,
});
});
}
/**
* Delete a paymail
* @param {string} id - Paymail Id of user to be deleted
*/
deletePaymail(id) {
return __awaiter(this, void 0, void 0, function* () {
yield this.http.adminRequest(`admin/paymails/${id}`, 'DELETE');
});
}
/**
* Get webhook subscriptions
*
* @returns {Promise<Webhook[]>} List of webhook subscriptions
*/
webhooks() {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest('admin/webhooks/subscriptions', 'GET');
});
}
/**
* Subscribe to webhook
*
* @param {string} url - Webhook URL
* @param {string} tokenHeader - Header name for the authentication token
* @param {string} tokenValue - Value of the authentication token
* @returns {Promise<void>}
*/
subscribeWebhook(url, tokenHeader, tokenValue) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest('admin/webhooks/subscriptions', 'POST', { url, tokenHeader, tokenValue });
});
}
/**
* Unsubscribe from webhook
*
* @param {string} url - URL of the webhook to unsubscribe
* @returns {Promise<void>}
*/
unsubscribeWebhook(url) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest('admin/webhooks/subscriptions', 'DELETE', { url });
});
}
/**
* Create new contact
*
* @param {string} contactPaymail - Paymail address for the new contact
* @param {NewContact} newContact - Contact information
* @returns {Promise<Contact>} Created contact
*/
createContact(contactPaymail, newContact) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest(`admin/contacts/${contactPaymail}`, 'POST', newContact);
});
}
/**
* Confirm contacts
*
* @param {string} paymailA - First contact's paymail
* @param {string} paymailB - Second contact's paymail
* @returns {Promise<void>}
*/
confirmContacts(paymailA, paymailB) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest('/admin/contacts/confirmations', 'POST', { paymailA, paymailB });
});
}
/**
* Get shared configuration
*
* @returns {Promise<SharedConfig>} Shared configuration settings
*/
sharedConfig() {
return __awaiter(this, void 0, void 0, function* () {
return yield this.http.adminRequest('configs/shared', 'GET');
});
}
}
const DEFAULT_TOTP_PERIOD = 30;
const DEFAULT_TOTP_DIGITS = 2;
/*
Basic flow:
Alice generates passcodeForBob with (sharedSecret+(contact.Paymail as bobPaymail))
Alice sends passcodeForBob to Bob (e.g. via email)
Bob validates passcodeForBob with (sharedSecret+(requesterPaymail as bobPaymail))
The (sharedSecret+paymail) is a "directedSecret". This ensures that passcodeForBob-from-Alice != passcodeForAlice-from-Bob.
The flow looks the same for Bob generating passcodeForAlice.
*/
/**
* Generates a TOTP for a given contact
*
* @param clientXPriv - The client xpriv
* @param contact - The Contact
* @param period - The TOTP period (default: 30)
* @param digits - The number of TOTP digits (default: 2)
* @returns The generated TOTP as a string
*/
const generateTotpForContact = (clientXPriv, contact, period = DEFAULT_TOTP_PERIOD, digits = DEFAULT_TOTP_DIGITS) => {
const sharedSecret = makeSharedSecret(contact