diff --git a/services/gunDB/contact-api/key.js b/services/gunDB/contact-api/key.js index 837ffc7d..06aaa1fe 100644 --- a/services/gunDB/contact-api/key.js +++ b/services/gunDB/contact-api/key.js @@ -62,3 +62,8 @@ exports.TOTAL_TIPS = 'totalTips' exports.PROFILE_BINARY = 'profileBinary' exports.POSTS_NEW = 'posts' + +// For Coordinates +exports.COORDINATES = 'coordinates' + +exports.COORDINATE_INDEX = 'coordinateIndex' diff --git a/services/schema/index.js b/services/schema/index.js new file mode 100644 index 00000000..6bccac2a --- /dev/null +++ b/services/schema/index.js @@ -0,0 +1,186 @@ +const getGunUser = () => require('../gunDB/Mediator').getUser() +const Key = require('../gunDB/contact-api/key') +/** + * @typedef {import('../gunDB/contact-api/SimpleGUN').ISEA} ISEA + * @typedef { 'spontaneousPayment' | 'tip' | 'service' | 'product' | 'other' } OrderType + * + * This represents a settled order only, unsettled orders have no coordinate + * @typedef {object} CoordinateOrder + * @prop {string} fromLndPub + * @prop {string} toLndPub + * @prop {string} fromGunPub + * @prop {string} toGunPub + * @prop {boolean} inbound + * + * @prop {string=} ownerGunPub Reserved for buddy system: + * can be undefined, '', 'me', or node owner pub key to represent node owner, + * otherwise it represents a buddy + * + * @prop {number} coordinateIndex can be payment_index, or add_index depending on if it's a payment or an invoice + * @prop {string} coordinateHash can be payment_hash, or r_hash depending on if it's a payment or an invoice, + * if it's a r_hash, must be hex encoded + * + * @prop {OrderType} type + * @prop {number} amount + * @prop {string=} description + * @prop {string=} metadata JSON encoded string to store extra data for special use cases + * @prop {number=} timestamp timestamp will be added at processing time if empty + * + */ + +/** + * @param {CoordinateOrder} order + */ +const checkOrderInfo = (order = {}) => { + const { + fromLndPub, + toLndPub, + fromGunPub, + toGunPub, + inbound, + type, + amount, + description, + coordinateIndex, + coordinateHash, + metadata, + } = order + + if (typeof fromLndPub !== 'string' || fromLndPub === '') { + return 'invalid or no "fromLndPub" field provided to order coordinate' + } + if (typeof toLndPub !== 'string' || toLndPub === '') { + return 'invalid or no "toLndPub" field provided to order coordinate' + } + if (typeof fromGunPub !== 'string' || fromGunPub === '') { + return 'invalid or no "fromGunPub" field provided to order coordinate' + } + if (typeof toGunPub !== 'string' || toGunPub === '') { + return 'invalid or no "toGunPub" field provided to order coordinate' + } + if (typeof inbound !== 'boolean') { + return 'invalid or no "inbound" field provided to order coordinate' + } + if (typeof type !== 'string' || type === '') { + return 'invalid or no "type" field provided to order coordinate' + } + if (typeof amount !== 'number') { + return 'invalid or no "amount" field provided to order coordinate' + } + + if (typeof coordinateIndex !== 'number') { + return 'invalid or no "coordinateIndex" field provided to order coordinate' + } + if (typeof coordinateHash !== 'string' || coordinateHash === '') { + return 'invalid or no "coordinateHash" field provided to order coordinate' + } + + if (description && (typeof description !== 'string' || description === '')) { + return 'invalid "description" field provided to order coordinate' + } + if (metadata && (typeof metadata !== 'string' || metadata === '')) { + return 'invalid "metadata" field provided to order coordinate' + } + return null +} + +class SchemaManager { + constructor({ memIndex = false }) {//config flag? + this.memIndex = memIndex + this.orderCreateIndexCallbacks.push(this.dateIndexCreateCb) //create more Cbs and p + } + + dateIndexName = 'dateIndex' + + memIndex = false //save the index data in memory for faster access + + // MEM INDEX, will be used only if memIndex === true + memDateIndex = {} //not implemented yet + + memGunPubIndex = {} //not implemented yet + + memLndPubIndex = {} //not implemented yet + + memTypeIndex = {} //not implemented yet + // + + /** + * @type {((order : CoordinateOrder,coordinate : string)=>void)[]} + */ + orderCreateIndexCallbacks = [] + + /** + * @param {CoordinateOrder} orderInfo + * @param {ISEA} SEA + */ + async AddOrder(orderInfo, SEA) { + + const checkErr = checkOrderInfo(orderInfo) + if (checkErr) { + throw new Error(checkErr) + } + + /** + * @type {CoordinateOrder} + */ + const filteredOrder = { + fromLndPub: orderInfo.fromLndPub, + toLndPub: orderInfo.toLndPub, + fromGunPub: orderInfo.fromGunPub, + toGunPub: orderInfo.toGunPub, + inbound: orderInfo.inbound, + ownerGunPub: orderInfo.ownerGunPub, + coordinateIndex: orderInfo.coordinateIndex, + coordinateHash: orderInfo.coordinateHash, + type: orderInfo.type, + amount: orderInfo.amount, + description: orderInfo.description, + metadata: orderInfo.metadata, + + timestamp: orderInfo.timestamp || Date.now(), + } + const orderString = JSON.stringify(filteredOrder) + const mySecret = require('../gunDB/Mediator').getMySecret() + const encryptedOrderString = await SEA.encrypt(orderString, mySecret) + const coordinatePub = filteredOrder.inbound ? filteredOrder.toLndPub : filteredOrder.fromLndPub + const coordinate = `${coordinatePub}__${filteredOrder.coordinateIndex}__${filteredOrder.coordinateHash}` + await new Promise((res, rej) => { + getGunUser() + .get(Key.COORDINATES) + .get(coordinate) + .put(encryptedOrderString, ack => { + if (ack.err && typeof ack.err !== 'number') { + rej( + new Error( + `Error saving coordinate order to user-graph: ${ack}` + ) + ) + } else { + res() + } + }) + }) + + //update all indexes with + this.orderCreateIndexCallbacks.forEach(cb => cb(filteredOrder, coordinate)) + } + + /** + * + * @param {CoordinateOrder} orderInfo + * @param {string} coordinate + */ + dateIndexCreateCb(orderInfo, coordinate) { + const date = new Date(orderInfo.timestamp) + //use UTC for consistency? + const year = date.getUTCFullYear().toString() + const month = date.getUTCMonth().toString() + + getGunUser() + .get(Key.COORDINATE_INDEX) + .get(this.dateIndexName) + .get(year) + .get(month) + .set(coordinate) + } +} \ No newline at end of file