@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
330 lines • 15.8 kB
JavaScript
"use strict";
/**
* StorageServer.ts
*
* A server-side class that "has a" local WalletStorage (like a StorageKnex instance),
* and exposes it via a JSON-RPC POST endpoint using Express.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StorageServer = void 0;
const express_1 = __importDefault(require("express"));
const auth_express_middleware_1 = require("@bsv/auth-express-middleware");
const payment_express_middleware_1 = require("@bsv/payment-express-middleware");
const WERR_errors_1 = require("../../sdk/WERR_errors");
const WalletError_1 = require("../../sdk/WalletError");
const WalletLogger_1 = require("../../WalletLogger");
class StorageServer {
constructor(storage, options) {
this.app = (0, express_1.default)();
this.storage = storage;
this.port = options.port;
this.wallet = options.wallet;
this.monetize = options.monetize;
this.calculateRequestPrice = options.calculateRequestPrice;
this.adminIdentityKeys = options.adminIdentityKeys;
this.makeLogger = options.makeLogger;
if (options['logShortReqs']) {
this.setupShortReqLogging();
}
this.setupRoutes();
}
setupShortReqLogging() {
this.app.use((req, res, next) => {
var _a;
const contentLength = Number(req.headers['content-length'] || 0);
if (contentLength > 0 && contentLength < 1000 && req.method === 'POST') {
const logObj = {
source: 'StorageServer short-request-log',
contentLength,
contentType: req.headers['content-type'] || '-',
ts: new Date().toISOString(),
url: req.originalUrl,
ip: req.ip || req.socket.remoteAddress,
ua: req.headers['user-agent'] || '-',
headers: { ...req.headers } // shallow copy
};
const traceContext = (_a = (req.headers['X-Cloud-Trace-Context'] || req.headers['x-cloud-trace-context'])) === null || _a === void 0 ? void 0 : _a.split('/')[0];
if (traceContext)
logObj['logging.googleapis.com/trace'] = `projects/computing-with-integrity/traces/${traceContext}`;
const chunks = [];
req.on('data', chunk => chunks.push(Buffer.from(chunk)));
req.on('end', () => {
const bodyBuffer = Buffer.concat(chunks);
try {
logObj.body = bodyBuffer.toString('utf8');
}
catch (_a) {
logObj.body = bodyBuffer.toString('hex');
logObj.bodyEncoding = 'hex';
}
console.log(JSON.stringify(logObj));
});
}
next();
});
}
setupRoutes() {
this.app.use(express_1.default.json({ limit: '30mb' }));
// This allows the API to be used everywhere when CORS is enforced
this.app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', '*');
res.header('Access-Control-Allow-Methods', '*');
res.header('Access-Control-Expose-Headers', '*');
res.header('Access-Control-Allow-Private-Network', 'true');
if (req.method === 'OPTIONS') {
// Handle CORS preflight requests to allow cross-origin POST/PUT requests
res.sendStatus(200);
}
else {
next();
}
});
this.app.get(`/robots.txt`, (req, res) => {
res.type('text/plain');
res.send(`User-agent: *\nDisallow: /`);
});
this.app.get(`/`, (req, res) => {
res.type('text/plain');
res.send(`BRC-100 ${this.wallet.chain}Net Storage Provider.`);
});
const options = {
wallet: this.wallet
};
this.app.use((0, auth_express_middleware_1.createAuthMiddleware)(options));
if (this.monetize) {
this.app.use((0, payment_express_middleware_1.createPaymentMiddleware)({
wallet: this.wallet,
calculateRequestPrice: this.calculateRequestPrice || (() => 100)
}));
}
// A single POST endpoint for JSON-RPC:
this.app.post('/', async (req, res) => {
var _a, _b, _c, _d, _e;
let { jsonrpc, method, params, id } = req.body;
// Basic JSON-RPC protocol checks:
if (jsonrpc !== '2.0' || !method || typeof method !== 'string') {
return res.status(400).json({ error: { code: -32600, message: 'Invalid Request' } });
}
const logObj = {
source: `StorageServer POST handler`,
method,
id,
user: req.auth.identityKey,
params: JSON.stringify(params || '').slice(0, 256)
};
const traceContext = (_a = (req.headers['X-Cloud-Trace-Context'] || req.headers['x-cloud-trace-context'])) === null || _a === void 0 ? void 0 : _a.split('/')[0];
if (traceContext)
logObj['logging.googleapis.com/trace'] = `projects/computing-with-integrity/traces/${traceContext}`;
console.log(JSON.stringify(logObj));
try {
// Dispatch the method call:
if (typeof this[method] === 'function') {
// if you wanted to handle certain methods on the server class itself
// e.g. this['someServerMethod'](params)
throw new Error('Server method dispatch not used in this approach.');
}
else if (typeof this.storage[method] === 'function') {
// method is on the walletStorage:
// Find user
switch (method) {
case 'destroy': {
logObj['result'] = undefined;
logObj['comment'] = 'IGNORED';
console.log(JSON.stringify(logObj));
return res.json({ jsonrpc: '2.0', result: undefined, id });
}
case 'getSettings':
{
/** */
}
break;
case 'findOrInsertUser':
{
if (params[0] !== req.auth.identityKey)
throw new WERR_errors_1.WERR_UNAUTHORIZED('function may only access authenticated user.');
}
break;
case 'adminStats':
{
// TODO: add check for admin user
if (params[0] !== req.auth.identityKey)
throw new WERR_errors_1.WERR_UNAUTHORIZED('function may only access authenticated admin user.');
if (!this.adminIdentityKeys || !this.adminIdentityKeys.includes(req.auth.identityKey))
throw new WERR_errors_1.WERR_UNAUTHORIZED('function may only be accessed by admin user.');
}
break;
case 'processSyncChunk':
{
await this.validateParam0(params, req);
//const args: RequestSyncChunkArgs = params[0]
const r = params[1];
if (r.certificateFields)
r.certificateFields = this.validateEntities(r.certificateFields);
if (r.certificates)
r.certificates = this.validateEntities(r.certificates);
if (r.commissions)
r.commissions = this.validateEntities(r.commissions);
if (r.outputBaskets)
r.outputBaskets = this.validateEntities(r.outputBaskets);
if (r.outputTagMaps)
r.outputTagMaps = this.validateEntities(r.outputTagMaps);
if (r.outputTags)
r.outputTags = this.validateEntities(r.outputTags);
if (r.outputs)
r.outputs = this.validateEntities(r.outputs);
if (r.provenTxReqs)
r.provenTxReqs = this.validateEntities(r.provenTxReqs);
if (r.provenTxs)
r.provenTxs = this.validateEntities(r.provenTxs);
if (r.transactions)
r.transactions = this.validateEntities(r.transactions);
if (r.txLabelMaps)
r.txLabelMaps = this.validateEntities(r.txLabelMaps);
if (r.txLabels)
r.txLabels = this.validateEntities(r.txLabels);
if (r.user)
r.user = this.validateEntity(r.user);
}
break;
default:
{
await this.validateParam0(params, req);
}
break;
}
// If makeLogger is valid, setup and potentially initialize to return data
let logger;
if (this.makeLogger && typeof params[1] === 'object') {
logger = this.makeLogger(params[1]['logger']);
params[1]['logger'] = logger;
logger.group(`StorageSever ${method}`);
const userId = (_b = params[0]) === null || _b === void 0 ? void 0 : _b['userId'];
const identityKey = (_c = params[0]) === null || _c === void 0 ? void 0 : _c['identityKey'];
if (userId)
logger.log(`userId: ${userId}`);
if (identityKey)
logger.log(`identityKey: ${identityKey}`);
}
try {
const result = await this.storage[method](...(params || []));
if (logger) {
logger.groupEnd();
(_d = logger.flush) === null || _d === void 0 ? void 0 : _d.call(logger);
if (logger.isOrigin) {
// Potentially only flush if isOrigin...
}
else if (logger.logs && typeof result === 'object') {
// If not the start of logging, return logged data with result.
result['log'] = { logs: logger.logs };
}
}
return res.json({ jsonrpc: '2.0', result, id });
}
catch (eu) {
(0, WalletLogger_1.logWalletError)(eu, logger, 'error executing requested method');
(_e = logger === null || logger === void 0 ? void 0 : logger.flush) === null || _e === void 0 ? void 0 : _e.call(logger);
throw eu;
}
}
else {
// Unknown method
return res.status(400).json({
jsonrpc: '2.0',
error: { code: -32601, message: `Method not found: ${method}` },
id
});
}
}
catch (error) {
/**
* Catch any thrown errors from the local walletStorage method.
*
* Convert errors to standard JSON object format that can be converted
* back to WalletError derived objects on the client side and re-thrown.
*
* Uses WalletError.fromJson(<error object>) on the client side to re-create
* an error object of the right class and properties.
*/
const json = WalletError_1.WalletError.unknownToJson(error);
return res.status(200).json({
jsonrpc: '2.0',
error: JSON.parse(json),
id
});
}
});
}
async validateParam0(params, req) {
if (typeof params[0] !== 'object' || !params[0]) {
params = [{}];
}
if (params[0]['identityKey'] && params[0]['identityKey'] !== req.auth.identityKey)
throw new WERR_errors_1.WERR_UNAUTHORIZED('identityKey does not match authentiation');
// console.log('looking up user with identityKey:', req.auth.identityKey)
const { user, isNew } = await this.storage.findOrInsertUser(req.auth.identityKey);
params[0].reqAuthUserId = user.userId;
if (params[0]['identityKey'])
params[0].userId = user.userId;
}
start() {
this.server = this.app.listen(this.port, () => {
console.log(`WalletStorageServer listening at http://localhost:${this.port}`);
});
}
async close() {
if (this.server) {
await this.server.close(() => {
// console.log('WalletStorageServer closed')
});
}
}
validateDate(date) {
let r;
if (date instanceof Date)
r = date;
else
r = new Date(date);
return r;
}
/**
* Helper to force uniform behavior across database engines.
* Use to process all individual records with time stamps retreived from database.
*/
validateEntity(entity, dateFields) {
entity.created_at = this.validateDate(entity.created_at);
entity.updated_at = this.validateDate(entity.updated_at);
if (dateFields) {
for (const df of dateFields) {
if (entity[df])
entity[df] = this.validateDate(entity[df]);
}
}
for (const key of Object.keys(entity)) {
const val = entity[key];
if (val === null) {
entity[key] = undefined;
}
else if (Buffer.isBuffer(val)) {
entity[key] = Array.from(val);
}
}
return entity;
}
/**
* Helper to force uniform behavior across database engines.
* Use to process all arrays of records with time stamps retreived from database.
* @returns input `entities` array with contained values validated.
*/
validateEntities(entities, dateFields) {
for (let i = 0; i < entities.length; i++) {
entities[i] = this.validateEntity(entities[i], dateFields);
}
return entities;
}
}
exports.StorageServer = StorageServer;
//# sourceMappingURL=StorageServer.js.map