From 1ac8f4279e0e53e2d4e18b59ff4b54acee2d2c57 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 15 Dec 2022 13:35:30 -0800 Subject: prettier --- lib/redis.js | 4 +- lib/sendInternal.js | 4 +- lib/sync.js | 128 ++++++++++++++++++++++++++-------------------------- lib/utils.js | 8 ++-- 4 files changed, 72 insertions(+), 72 deletions(-) (limited to 'lib') diff --git a/lib/redis.js b/lib/redis.js index 98a71856..3aed1bd0 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -1,10 +1,10 @@ import { createClient } from "redis"; const redis = createClient({ - url: "redis://redis", + url: "redis://redis" }); -redis.on("error", (err) => console.log("redis error", err)); +redis.on("error", err => console.log("redis error", err)); await redis.connect(); diff --git a/lib/sendInternal.js b/lib/sendInternal.js index 7204f769..b49a6c64 100644 --- a/lib/sendInternal.js +++ b/lib/sendInternal.js @@ -149,7 +149,7 @@ export default async ( if (tip) { params.tip = tip; params.amount -= tip; - } + } await invoice.increment({ received: amount }, { transaction }); await invoice.reload({ transaction }); @@ -200,7 +200,7 @@ export default async ( hash: `#${v4().substr(0, 6)} ${ username ? `Payment to ${username}` : "Internal Transfer" }`, - network: "COINOS", + network: "COINOS" }; if (!username) { diff --git a/lib/sync.js b/lib/sync.js index 42291a94..97207752 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -16,7 +16,7 @@ import lq from "./liquid"; const init = async () => { try { - read(fs.createReadStream("exceptions"), (data) => + read(fs.createReadStream("exceptions"), data => store.exceptions.push(data) ); } catch (e) { @@ -30,13 +30,13 @@ const init = async () => { await db.Invoice.findAll({ where: { createdAt: { - [Op.gt]: thirtyDays, - }, + [Op.gt]: thirtyDays + } }, include: { model: db.User, - as: "user", - }, + as: "user" + } }) ).map(({ address, user, unconfidential }) => { if (address && user) store.addresses[address] = user.username; @@ -49,9 +49,9 @@ const init = async () => { store.payments = ( await db.Payment.findAll({ - attributes: ["hash"], + attributes: ["hash"] }) - ).map((p) => p.hash); + ).map(p => p.hash); }; init(); @@ -66,13 +66,13 @@ const sync = async () => { include: [ { model: db.Account, - as: "account", + as: "account" }, { model: db.User, - as: "user", - }, - ], + as: "user" + } + ] }); let missed, invoices, recent; @@ -81,49 +81,49 @@ const sync = async () => { recent = invoices .filter( - (i) => + i => i.is_confirmed && differenceInDays(new Date(), new Date(i.created_at)) < 2 ) - .map((i) => ({ + .map(i => ({ amount: parseInt(i.received), preimage: i.secret, pr: i.request, - createdAt: new Date(i.created_at), + createdAt: new Date(i.created_at) })); } else { ({ invoices } = await ln.listinvoices()); recent = invoices .filter( - (i) => + i => i.status === "paid" && Math.round(Date.now() / 1000) - i.paid_at < 60 * 60 * 48 ) - .map((i) => ({ + .map(i => ({ amount: parseInt(Math.round(i.msatoshi_received / 1000)), preimage: i.payment_preimage, pr: i.bolt11, - createdAt: new Date(i.paid_at * 1000), + createdAt: new Date(i.paid_at * 1000) })); } - await db.transaction(async (transaction) => { + await db.transaction(async transaction => { const twoDaysAgo = new Date(new Date().setDate(new Date().getDate() - 2)); let settled = ( await db.Payment.findAll( { where: { network: "lightning", - createdAt: { [Op.gt]: twoDaysAgo }, + createdAt: { [Op.gt]: twoDaysAgo } }, - attributes: ["preimage"], + attributes: ["preimage"] }, transaction ) - ).map((p) => p.preimage); + ).map(p => p.preimage); - missed = recent.filter((i) => !settled.includes(i.preimage)); + missed = recent.filter(i => !settled.includes(i.preimage)); for (let i = 0; i < missed.length; i++) { let { amount, preimage, pr: text, createdAt } = missed[i]; if (!text || !text.length) continue; @@ -133,9 +133,9 @@ const sync = async () => { where: { text }, include: { model: db.User, - as: "user", + as: "user" }, - transaction, + transaction }); if (invoice && invoice.user_id) { @@ -143,9 +143,9 @@ const sync = async () => { where: { user_id: invoice.user_id, asset: config.liquid.btcasset, - pubkey: null, + pubkey: null }, - transaction, + transaction }); if (account) { @@ -163,7 +163,7 @@ const sync = async () => { tip: invoice.tip, invoice_id: invoice.id, createdAt, - updatedAt: createdAt, + updatedAt: createdAt }); await invoice.increment({ received: amount }, { transaction }); @@ -181,48 +181,48 @@ const sync = async () => { const unconfirmed = await db.Payment.findAll({ where: { - confirmed: 0, + confirmed: 0 }, include: [ { model: db.Account, - as: "account", + as: "account" }, { model: db.Invoice, - as: "invoice", + as: "invoice" }, { model: db.User, - as: "user", - }, - ], + as: "user" + } + ] }); - let hashes = unconfirmed.map((p) => p.hash); + let hashes = unconfirmed.map(p => p.hash); const transactions = [ ...(await bc.listTransactions("*", 500)), - ...(await lq.listTransactions("*", 500)), + ...(await lq.listTransactions("*", 500)) ].filter( - (tx) => + tx => ["send", "receive"].includes(tx.category) && new Date(tx.time * 1000) > twoWeeksAgo ); missed = transactions.filter( - (tx) => + tx => tx.category === "receive" && tx.confirmations > 2 && hashes.includes(tx.txid) ); for (let i = 0; i < missed.length; i++) { - let p = unconfirmed.find((p) => p.hash === missed[i].txid); + let p = unconfirmed.find(p => p.hash === missed[i].txid); p.confirmed = 1; try { - await db.transaction(async (transaction) => { + await db.transaction(async transaction => { l("confirming payment", p.id, p.user_id, p.hash, p.address); await p.save({ transaction }); @@ -242,11 +242,11 @@ const sync = async () => { store.unaccounted = []; - transactions.map((tx) => { + transactions.map(tx => { if ( - !payments.find((p) => p.hash === tx.txid) && + !payments.find(p => p.hash === tx.txid) && !store.exceptions.includes(tx.txid) && - !store.unaccounted.find((a) => a.txid === tx.txid) && + !store.unaccounted.find(a => a.txid === tx.txid) && new Date(tx.blocktime * 1000) >= twoWeeksAgo ) { store.unaccounted.push(tx); @@ -256,34 +256,34 @@ const sync = async () => { if (store.unaccounted.length) warn( "wallet transactions missing from database", - store.unaccounted.map((tx) => tx.txid) + store.unaccounted.map(tx => tx.txid) ); let receipts = store.unaccounted .filter( - (tx) => + tx => tx.category === "receive" && (!tx.asset || tx.asset === config.liquid.btcasset) ) - .map((tx) => tx.address); + .map(tx => tx.address); invoices = await db.Invoice.findAll({ where: { [Op.or]: { address: receipts, - unconfidential: receipts, - }, + unconfidential: receipts + } }, include: [ { model: db.Account, - as: "account", + as: "account" }, { model: db.User, - as: "user", - }, - ], + as: "user" + } + ] }); for (let i = 0; i < invoices.length; i++) { @@ -298,13 +298,13 @@ const sync = async () => { rate, tip, user, - user_id, + user_id } = invoices[i]; l("unaccounted", address); let pending = store.unaccounted.filter( - (tx) => tx.address === address || tx.address === unconfidential + tx => tx.address === address || tx.address === unconfidential ); for (let i = 0; i < pending.length; i++) { @@ -313,7 +313,7 @@ const sync = async () => { amount = toSats(amount); try { - await db.transaction(async (transaction) => { + await db.transaction(async transaction => { l("found pending transaction", user.username, amount, hash); store.payments.push(hash); let p = await db.Payment.create( @@ -329,7 +329,7 @@ const sync = async () => { rate, received: true, tip, - user_id, + user_id }, { transaction } ); @@ -357,8 +357,8 @@ setTimeout(sync, 5000); app.get("/conversions", async (req, res) => { const transactions = [ ...(await bc.listTransactions("*", 500)), - ...(await lq.listTransactions("*", 500)), - ].filter((tx) => ["receive"].includes(tx.category)); + ...(await lq.listTransactions("*", 500)) + ].filter(tx => ["receive"].includes(tx.category)); const { addr } = req.query; const { invoices } = require("./invoices"); @@ -366,19 +366,19 @@ app.get("/conversions", async (req, res) => { let conversions = persist("data/conversions.json"); const payments = ( await db.Payment.findAll({ - attributes: ["hash"], + attributes: ["hash"] }) - ).map((p) => p.hash); + ).map(p => p.hash); res.send( Object.keys(conversions) - .filter((c) => conversions[c].address === addr) - .filter((c) => { + .filter(c => conversions[c].address === addr) + .filter(c => { return ( invoices.find( - (i) => i.payment_request === c && i.state === "SETTLED" - ) || transactions.find((tx) => tx.txid === c) + i => i.payment_request === c && i.state === "SETTLED" + ) || transactions.find(tx => tx.txid === c) ); }) - .filter((c) => !payments.includes(c)) + .filter(c => !payments.includes(c)) ); }); diff --git a/lib/utils.js b/lib/utils.js index c9670339..45e50243 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -166,8 +166,8 @@ export const derivePayRequest = async ({ amount, memo, tip }) => { if (!tip) tip = 0; let value = amount + tip; - if (!memo) memo = "coinos"; - return ( - await ln.invoice(value ? `${value}sat` : "any", new Date(), memo, 360) - ).bolt11; + if (!memo) memo = "coinos"; + return ( + await ln.invoice(value ? `${value}sat` : "any", new Date(), memo, 360) + ).bolt11; }; -- cgit v1.2.3 From 9ebd4105fa412d16bfe58facc9ebb53847b3316a Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 20 Dec 2022 12:21:47 -0800 Subject: nostr --- index.js | 1 + lib/nostr.js | 48 ++++++++++++++++++++ package.json | 2 + pnpm-lock.yaml | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 lib/nostr.js (limited to 'lib') diff --git a/index.js b/index.js index 759893dc..c4c6ebc6 100644 --- a/index.js +++ b/index.js @@ -15,6 +15,7 @@ import "./lib/sync"; import "./lib/rates"; import "./lib/notifications"; import "./lib/mail"; +import "./lib/nostr"; import "./routes/assets"; import "./routes/locations"; diff --git a/lib/nostr.js b/lib/nostr.js new file mode 100644 index 00000000..b1b24b87 --- /dev/null +++ b/lib/nostr.js @@ -0,0 +1,48 @@ +import { Relay, RelayPool } from "nostr"; +import { got } from "got"; +import { broadcast } from "$lib/sockets"; + +const { relays } = await got("https://nostr.watch/relays.json").json(); +const pool = RelayPool(relays); + +let coinos; +pool.on("open", (relay) => { + if (relay.url.includes("coinos")) coinos = relay; + relay.subscribe("subid", { limit: 1 }); +}); + +let seen = []; +pool.on("event", (relay, sub_id, ev) => { + if (seen.includes(ev.id)) return; + if (Math.abs(Math.floor(Date.now()/1000) - ev.created_at) > 7200) return; + seen.push(ev.id); + seen.length > 1000 && seen.shift(); + + if ( + coinos && + ev.kind < 5 && + !ev.content.startsWith("test") && + !ev.content.startsWith("running branle") + ) { + let parsed; + try { + parsed = JSON.parse(ev.content); + } catch (e) { + parsed = ev.content; + } + + coinos.send(["EVENT", ev]); + if (ev.kind === 1) + broadcast("event", ev); + } +}); + +function formatDate(date) { + var hours = date.getHours(); + var minutes = date.getMinutes(); + hours = hours % 12; + hours = hours ? hours : 12; + minutes = minutes < 10 ? "0" + minutes : minutes; + var strTime = hours + ":" + minutes; + return date.getMonth() + 1 + "/" + date.getDate() + " " + strTime; +} diff --git a/package.json b/package.json index b868531e..1bca9dba 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "express-session": "^1.16.2", "fastify": "^4.5.3", "file-type": "^18.0.0", + "got": "^12.5.3", "http-proxy-middleware": "^0.20.0", "imap": "^0.8.19", "jsonwebtoken": "^8.5.1", @@ -48,6 +49,7 @@ "mysql2": "^2.2.5", "node-fetch": "^2.6.1", "nodemon": "^2.0.7", + "nostr": "^0.2.7", "otplib": "^12.0.1", "passport-jwt": "^4.0.0", "passport-local": "1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75af23d3..b6722b1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,7 @@ specifiers: express-session: ^1.16.2 fastify: ^4.5.3 file-type: ^18.0.0 + got: ^12.5.3 http-proxy-middleware: ^0.20.0 imap: ^0.8.19 jsonwebtoken: ^8.5.1 @@ -42,6 +43,7 @@ specifiers: mysql2: ^2.2.5 node-fetch: ^2.6.1 nodemon: ^2.0.7 + nostr: ^0.2.7 otplib: ^12.0.1 passport-jwt: ^4.0.0 passport-local: 1.0.0 @@ -91,6 +93,7 @@ dependencies: express-session: 1.17.3 fastify: 4.8.1 file-type: 18.0.0 + got: 12.5.3 http-proxy-middleware: 0.20.0 imap: 0.8.19 jsonwebtoken: 8.5.1 @@ -104,6 +107,7 @@ dependencies: mysql2: 2.3.3 node-fetch: 2.6.7 nodemon: 2.0.20 + nostr: 0.2.7 otplib: 12.0.1 passport-jwt: 4.0.0 passport-local: 1.0.0 @@ -412,6 +416,18 @@ packages: - supports-color dev: false + /@sindresorhus/is/5.3.0: + resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==} + engines: {node: '>=14.16'} + dev: false + + /@szmarczak/http-timer/5.0.1: + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + dependencies: + defer-to-connect: 2.0.1 + dev: false + /@tokenizer/token/0.3.0: resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} dev: false @@ -438,6 +454,10 @@ packages: resolution: {integrity: sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==} dev: false + /@types/http-cache-semantics/4.0.1: + resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} + dev: false + /@types/ms/0.7.31: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: false @@ -1011,6 +1031,24 @@ packages: dev: false optional: true + /cacheable-lookup/7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + dev: false + + /cacheable-request/10.2.3: + resolution: {integrity: sha512-6BehRBOs7iurNjAYN9iPazTwFDaMQavJO8W1MEm3s2pH8q/tkPTtLDRUZaweWK87WFGf2Y5wLAlaCJlR5kOz3w==} + engines: {node: '>=14.16'} + dependencies: + '@types/http-cache-semantics': 4.0.1 + get-stream: 6.0.1 + http-cache-semantics: 4.1.0 + keyv: 4.5.2 + mimic-response: 4.0.0 + normalize-url: 8.0.0 + responselike: 3.0.0 + dev: false + /call-bind/1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: @@ -1359,6 +1397,11 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: false + /defer-to-connect/2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + dev: false + /degenerator/1.0.4: resolution: {integrity: sha512-EMAC+riLSC64jKfOs1jp8J7M4ZXstUUwTdwFBEv6HOzL/Ae+eAzMKEK0nJnpof2fnw9IOjmE6u6qXFejVyk8AA==} dependencies: @@ -1854,6 +1897,11 @@ packages: optional: true dev: false + /form-data-encoder/2.1.4: + resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} + engines: {node: '>= 14.17'} + dev: false + /form-data/2.5.1: resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} engines: {node: '>= 0.12'} @@ -1962,6 +2010,11 @@ packages: engines: {node: '>=8.0.0'} dev: false + /get-stream/6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: false + /get-uri/2.0.4: resolution: {integrity: sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==} dependencies: @@ -2012,6 +2065,23 @@ packages: once: 1.4.0 dev: false + /got/12.5.3: + resolution: {integrity: sha512-8wKnb9MGU8IPGRIo+/ukTy9XLJBwDiCpIf5TVzQ9Cpol50eMTpBq2GAuDsuDIz7hTYmZgMgC1e9ydr6kSDWs3w==} + engines: {node: '>=14.16'} + dependencies: + '@sindresorhus/is': 5.3.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.3 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.0 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + dev: false + /graceful-fs/4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} dev: false @@ -2082,7 +2152,6 @@ packages: /http-cache-semantics/4.1.0: resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==} dev: false - optional: true /http-errors/1.7.2: resolution: {integrity: sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==} @@ -2162,6 +2231,14 @@ packages: - debug dev: false + /http2-wrapper/2.2.0: + resolution: {integrity: sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==} + engines: {node: '>=10.19.0'} + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: false + /http_ece/1.1.0: resolution: {integrity: sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==} engines: {node: '>=4'} @@ -2370,6 +2447,10 @@ packages: engines: {node: '>=10'} dev: false + /json-buffer/3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: false + /json-schema-traverse/1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} dev: false @@ -2420,6 +2501,12 @@ packages: safe-buffer: 5.2.1 dev: false + /keyv/4.5.2: + resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==} + dependencies: + json-buffer: 3.0.1 + dev: false + /knex/2.3.0_miwdxfk6g3dfg4gj6dnwby2onu: resolution: {integrity: sha512-WMizPaq9wRMkfnwKXKXgBZeZFOSHGdtoSz5SaLAVNs3WRDfawt9O89T4XyH52PETxjV8/kRk0Yf+8WBEP/zbYw==} engines: {node: '>=12'} @@ -2587,6 +2674,11 @@ packages: resolution: {integrity: sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w==} dev: false + /lowercase-keys/3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + /lru-cache/4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} dependencies: @@ -2724,6 +2816,11 @@ packages: engines: {node: '>=10'} dev: false + /mimic-response/4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + /minimalistic-assert/1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} dev: false @@ -2902,6 +2999,11 @@ packages: engines: {node: '>= 0.4.0'} dev: false + /noble-secp256k1/1.2.14: + resolution: {integrity: sha512-GSCXyoZBUaaPwVWdYncMEmzlSUjF9J/YeEHpklYJCyg8wPuJP3NzDx0BkiwArzINkdX2HJHvUJhL6vVWPOQQcg==} + deprecated: Switch to namespaced @noble/secp256k1 for security and feature updates + dev: false + /node-abi/3.26.0: resolution: {integrity: sha512-jRVtMFTChbi2i/jqo/i2iP9634KMe+7K1v35mIdj3Mn59i5q27ZYhn+sW6npISM/PQg7HrP2kwtRBMmh5Uvzdg==} engines: {node: '>=10'} @@ -2997,6 +3099,21 @@ packages: engines: {node: '>=0.10.0'} dev: false + /normalize-url/8.0.0: + resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} + engines: {node: '>=14.16'} + dev: false + + /nostr/0.2.7: + resolution: {integrity: sha512-Yq5tkiCTJtohs7H7Fx+Ki83bR8b3xapdwYi17HunhW5EQfZ9rI0cIXFyihj2ZRkv/0YYvVBiCp/KIaLYt5LyfQ==} + dependencies: + noble-secp256k1: 1.2.14 + ws: 8.9.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /npmlog/5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} dependencies: @@ -3079,6 +3196,11 @@ packages: '@otplib/preset-v11': 12.0.1 dev: false + /p-cancelable/3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + dev: false + /p-limit/3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -3442,6 +3564,11 @@ packages: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} dev: false + /quick-lru/5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: false + /random-bytes/1.0.0: resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} engines: {node: '>= 0.8'} @@ -3578,6 +3705,10 @@ packages: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: false + /resolve-alpn/1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + dev: false + /resolve-from/5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -3592,6 +3723,13 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: false + /responselike/3.0.0: + resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} + engines: {node: '>=14.16'} + dependencies: + lowercase-keys: 3.0.0 + dev: false + /ret/0.2.2: resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} engines: {node: '>=4'} -- cgit v1.2.3 From aecec231b6d94b0707b840a7153bd05a057f49c7 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 20 Dec 2022 20:26:01 +0000 Subject: nip5 --- index.js | 6 ++++ lib/send.js | 2 +- reg.yml | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 reg.yml (limited to 'lib') diff --git a/index.js b/index.js index c4c6ebc6..84554422 100644 --- a/index.js +++ b/index.js @@ -40,6 +40,12 @@ app.post('/echo', optionalAuth, async (req, res) => { res.send(req.body); }); +app.get('/nostr.json', async (req, res) => { + res.send({ + names: { adam: "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8", asoltys: "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8" } + }); +}); + app.listen({ host, port }, (e, address) => { e && err(e) && process.exit(1); l(`CoinOS Server listening on ${address}`); diff --git a/lib/send.js b/lib/send.js index 54bcd103..eefea080 100644 --- a/lib/send.js +++ b/lib/send.js @@ -20,7 +20,7 @@ export default async (amount, memo, hash, user) => { routingInfo && routingInfo.data.find(d => d.fee_proportional_millionths > 10000) ) { - warn("fee rate too high", d.fee_proportional_millionths); + warn("fee rate too high", routingInfo); throw new Error( "Couldn't find a suitable route to pay to your destination" ); diff --git a/reg.yml b/reg.yml new file mode 100644 index 00000000..e03d9352 --- /dev/null +++ b/reg.yml @@ -0,0 +1,96 @@ +version: '3.6' +services: + app: + depends_on: + - cl + - maria + - bitcoin + - liquid + container_name: app + image: gcr.io/coinos-326717/github.com/coinos/coinos-server:prod + restart: always + environment: + NODE_ENV: production + NODE_OPTIONS: "--es-module-specifier-resolution=node --experimental-modules --loader ./loader.js --openssl-legacy-provider" + HOSTNAME: "${HOSTNAME}" + volumes: + - ./:/app + - ./logs:/logs + networks: + default: + ipv4_address: 172.42.0.22 + maria: + container_name: maria + image: mariadb + restart: always + environment: + MARIADB_ROOT_PASSWORD: password + volumes: + - ./mysql:/var/lib/mysql + bitcoin: + image: ghcr.io/vulpemventures/bitcoin + container_name: bitcoin + volumes: + - ./config/bitcoin:/home/bitcoin/.bitcoin + restart: always + networks: + default: + ipv4_address: 172.42.0.23 + liquid: + image: ghcr.io/vulpemventures/elements + container_name: liquid + volumes: + - ./config/liquid:/home/elements/.elements + restart: always + networks: + default: + ipv4_address: 172.42.0.24 + cl: + depends_on: + - bitcoin + image: elementsproject/lightningd:v0.12.0 + container_name: cl + restart: always + volumes: + - ./config/lightning:/root/.lightning + ports: + - "9736:9735" + networks: + default: + ipv4_address: 172.42.0.25 + lnd: + depends_on: + - bitcoin + - tor + image: lightninglabs/lnd:v0.15.4-beta + container_name: lnd + volumes: + - ./config/lnd:/root/.lnd + - ./config/bitcoin:/root/.bitcoin + - ./config/tor:/etc/tor + ports: + - "9735:9735" + restart: always + networks: + default: + ipv4_address: 172.42.0.26 + tor: + container_name: tor + image: dperson/torproxy + restart: always + volumes: + - ./config/tor:/etc/tor + - ./config/tordata:/var/lib/tor + networks: + default: + ipv4_address: 172.42.0.27 + redis: + restart: always + container_name: redis + image: redis + volumes: + - ./config/redis:/data +networks: + default: + name: net + external: true -- cgit v1.2.3 From aa662369cc7a293d0be5f4d419543a51ad40a7bc Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 20 Dec 2022 17:39:28 -0800 Subject: send user events --- index.js | 1 + lib/nostr.js | 72 ++++++++++++++++++++++++++++++-------------------------- lib/store.js | 4 +++- lib/utils.js | 8 +++++++ routes/events.js | 30 +++++++++++++++++++++++ routes/users.js | 2 +- 6 files changed, 81 insertions(+), 36 deletions(-) create mode 100644 routes/events.js (limited to 'lib') diff --git a/index.js b/index.js index c4c6ebc6..d83afd91 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,7 @@ import "./lib/mail"; import "./lib/nostr"; import "./routes/assets"; +import "./routes/events"; import "./routes/locations"; import "./routes/info"; import "./routes/users"; diff --git a/lib/nostr.js b/lib/nostr.js index b1b24b87..677f14e5 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -1,48 +1,52 @@ import { Relay, RelayPool } from "nostr"; import { got } from "got"; import { broadcast } from "$lib/sockets"; +import store from "$lib/store"; +import redis from "$lib/redis"; const { relays } = await got("https://nostr.watch/relays.json").json(); -const pool = RelayPool(relays); +export const pool = RelayPool(relays); let coinos; -pool.on("open", (relay) => { +pool.on("open", relay => { if (relay.url.includes("coinos")) coinos = relay; - relay.subscribe("subid", { limit: 1 }); + relay.subscribe("live", { limit: 1 }); }); let seen = []; -pool.on("event", (relay, sub_id, ev) => { - if (seen.includes(ev.id)) return; - if (Math.abs(Math.floor(Date.now()/1000) - ev.created_at) > 7200) return; - seen.push(ev.id); - seen.length > 1000 && seen.shift(); - - if ( - coinos && - ev.kind < 5 && - !ev.content.startsWith("test") && - !ev.content.startsWith("running branle") - ) { - let parsed; - try { - parsed = JSON.parse(ev.content); - } catch (e) { - parsed = ev.content; - } +let timeout; +pool.on("event", async (relay, sub, ev) => { + try { + if (sub === "live") { + if (Math.abs(Math.floor(Date.now() / 1000) - ev.created_at) > 7200) + return; + + if (seen.includes(ev.id)) return; + seen.push(ev.id); + seen.length > 1000 && seen.shift(); + + if (coinos && ev.kind < 5) { + let parsed; + try { + parsed = JSON.parse(ev.content); + } catch (e) { + parsed = ev.content; + } + + coinos.send(["EVENT", ev]); + if (ev.kind === 1) broadcast("event", ev); + } + } else { + let pubkey = sub; + clearTimeout(store.timeouts[pubkey]); - coinos.send(["EVENT", ev]); - if (ev.kind === 1) - broadcast("event", ev); + store.fetching[pubkey] = true; + await redis.sAdd(pubkey, ev.id); + await redis.set(`ev:${ev.id}`, JSON.stringify(ev)); + + store.timeouts[pubkey] = setTimeout(() => (store.fetching[pubkey] = false), 100); + } + } catch (e) { + console.log(e); } }); - -function formatDate(date) { - var hours = date.getHours(); - var minutes = date.getMinutes(); - hours = hours % 12; - hours = hours ? hours : 12; - minutes = minutes < 10 ? "0" + minutes : minutes; - var strTime = hours + ":" + minutes; - return date.getMonth() + 1 + "/" + date.getDate() + " " + strTime; -} diff --git a/lib/store.js b/lib/store.js index 6f2e47ce..f6f65521 100644 --- a/lib/store.js +++ b/lib/store.js @@ -20,5 +20,7 @@ export default { bcAddressIndex: 0, lqAddressIndex: 0, last: {}, - payments: [] + payments: [], + fetching: {}, + timeouts: {}, }; diff --git a/lib/utils.js b/lib/utils.js index 45e50243..7fdd10b3 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -171,3 +171,11 @@ export const derivePayRequest = async ({ amount, memo, tip }) => { await ln.invoice(value ? `${value}sat` : "any", new Date(), memo, 360) ).bolt11; }; + +export const sleep = (n) => new Promise((r) => setTimeout(r, n)); +export const wait = async (f, s = 300, n = 1000) => { + let i = 0; + while (!(await f()) && i < s) (await sleep(n)) && i++; + if (i >= s) throw new Error("timeout"); + return f(); +}; diff --git a/routes/events.js b/routes/events.js new file mode 100644 index 00000000..460d09a5 --- /dev/null +++ b/routes/events.js @@ -0,0 +1,30 @@ +import app from "$app"; +import redis from "$lib/redis"; +import { pool } from "$lib/nostr"; +import store from "$lib/store"; +import { wait } from "$lib/utils"; + +app.get("/:pubkey/events", async (req, res) => { + try { + let { pubkey } = req.params; + store.fetching[pubkey] = true; + store.timeouts[pubkey] = setTimeout(() => (store.fetching[pubkey] = false), 500); + + pool.subscribe(pubkey, { + limit: 500, + kinds: [1], + authors: [pubkey] + }); + + let events = []; + await wait(() => !store.fetching[pubkey], 10, 10); + for (let id of await redis.sMembers(pubkey)) { + events.push(JSON.parse(await redis.get(`ev:${id}`))); + } + + res.send(events); + } catch (e) { + console.log(e); + res.code(500).send("problem fetching user events"); + } +}); diff --git a/routes/users.js b/routes/users.js index c4b613eb..8d45e8c3 100644 --- a/routes/users.js +++ b/routes/users.js @@ -56,7 +56,7 @@ app.get("/me", auth, async (req, res) => { app.get("/users/:username", async (req, res) => { const { username } = req.params; - const user = await db.User.findOne({ + let user = await db.User.findOne({ attributes: ["username", "banner", "profile", "address", "currency"], where: Sequelize.where( Sequelize.fn("lower", Sequelize.col("username")), -- cgit v1.2.3 From 316039601e5170d6f753cf09eac92a94176b4756 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 21 Dec 2022 21:50:06 -0800 Subject: store nostr keys --- db/models/users.js | 9 +++++++++ lib/nostr.js | 2 +- lib/whitelist.js | 3 +++ routes/events.js | 7 +++++++ routes/users.js | 7 +++++++ 5 files changed, 27 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/db/models/users.js b/db/models/users.js index faf41576..fd38facf 100644 --- a/db/models/users.js +++ b/db/models/users.js @@ -224,6 +224,15 @@ const attributes = { comment: null, field: "twofa" }, + cipher: { + type: DataTypes.STRING(255), + }, + pubkey: { + type: DataTypes.STRING(255), + }, + salt: { + type: DataTypes.STRING(255), + }, pin: { type: DataTypes.STRING(255), allowNull: true, diff --git a/lib/nostr.js b/lib/nostr.js index 677f14e5..c75955bd 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -7,12 +7,12 @@ import redis from "$lib/redis"; const { relays } = await got("https://nostr.watch/relays.json").json(); export const pool = RelayPool(relays); -let coinos; pool.on("open", relay => { if (relay.url.includes("coinos")) coinos = relay; relay.subscribe("live", { limit: 1 }); }); +let coinos; let seen = []; let timeout; pool.on("event", async (relay, sub, ev) => { diff --git a/lib/whitelist.js b/lib/whitelist.js index 93e601c7..5a9c0ba3 100644 --- a/lib/whitelist.js +++ b/lib/whitelist.js @@ -5,6 +5,7 @@ export default [ "address", "admin", "banner", + "cipher", "currencies", "currency", "email", @@ -14,7 +15,9 @@ export default [ "index", "keys", "payments", + "pubkey", "profile", + "salt", "seed", "symbol", "theme", diff --git a/routes/events.js b/routes/events.js index 460d09a5..ab682b46 100644 --- a/routes/events.js +++ b/routes/events.js @@ -28,3 +28,10 @@ app.get("/:pubkey/events", async (req, res) => { res.code(500).send("problem fetching user events"); } }); + +app.post('/nostr/send', async (req, res) => { + let { event } = req.body; + pool.send(["EVENT", event]); + res.send(event); +}); + diff --git a/routes/users.js b/routes/users.js index 8d45e8c3..82a71549 100644 --- a/routes/users.js +++ b/routes/users.js @@ -132,6 +132,8 @@ app.post("/user", auth, async (req, res) => { let { user } = req; let { address, + cipher, + pubkey, confirm, currencies, currency, @@ -140,6 +142,7 @@ app.post("/user", auth, async (req, res) => { password, newpin, pin, + salt, seed, tokens, twofa, @@ -170,6 +173,10 @@ app.post("/user", auth, async (req, res) => { } if (unit) user.unit = unit; + if (cipher) user.cipher = cipher; + if (pubkey) user.pubkey = pubkey; + if (salt) user.salt = salt; + user.currency = currency; user.currencies = currencies; user.pin = newpin; -- cgit v1.2.3 From 728ac80f9cc8a3268bdced6339f58936bd69648b Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 22 Dec 2022 09:51:53 -0800 Subject: prettier --- lib/nostr.js | 5 ++++- lib/store.js | 2 +- lib/utils.js | 2 +- routes/events.js | 8 +++++--- 4 files changed, 11 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/nostr.js b/lib/nostr.js index c75955bd..53c8d7d1 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -44,7 +44,10 @@ pool.on("event", async (relay, sub, ev) => { await redis.sAdd(pubkey, ev.id); await redis.set(`ev:${ev.id}`, JSON.stringify(ev)); - store.timeouts[pubkey] = setTimeout(() => (store.fetching[pubkey] = false), 100); + store.timeouts[pubkey] = setTimeout( + () => (store.fetching[pubkey] = false), + 100 + ); } } catch (e) { console.log(e); diff --git a/lib/store.js b/lib/store.js index f6f65521..e42afeda 100644 --- a/lib/store.js +++ b/lib/store.js @@ -22,5 +22,5 @@ export default { last: {}, payments: [], fetching: {}, - timeouts: {}, + timeouts: {} }; diff --git a/lib/utils.js b/lib/utils.js index 7fdd10b3..46267dbd 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -172,7 +172,7 @@ export const derivePayRequest = async ({ amount, memo, tip }) => { ).bolt11; }; -export const sleep = (n) => new Promise((r) => setTimeout(r, n)); +export const sleep = n => new Promise(r => setTimeout(r, n)); export const wait = async (f, s = 300, n = 1000) => { let i = 0; while (!(await f()) && i < s) (await sleep(n)) && i++; diff --git a/routes/events.js b/routes/events.js index ab682b46..18abf1d7 100644 --- a/routes/events.js +++ b/routes/events.js @@ -8,7 +8,10 @@ app.get("/:pubkey/events", async (req, res) => { try { let { pubkey } = req.params; store.fetching[pubkey] = true; - store.timeouts[pubkey] = setTimeout(() => (store.fetching[pubkey] = false), 500); + store.timeouts[pubkey] = setTimeout( + () => (store.fetching[pubkey] = false), + 500 + ); pool.subscribe(pubkey, { limit: 500, @@ -29,9 +32,8 @@ app.get("/:pubkey/events", async (req, res) => { } }); -app.post('/nostr/send', async (req, res) => { +app.post("/nostr/send", async (req, res) => { let { event } = req.body; pool.send(["EVENT", event]); res.send(event); }); - -- cgit v1.2.3 From a487b5a2b80d270771f9932c17d2b6babf0fd49a Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 22 Dec 2022 13:42:00 -0800 Subject: return mock user objects for anon pubkeys --- lib/nostr.js | 21 +++++++++++++++++++-- routes/events.js | 8 ++++++++ routes/users.js | 9 ++++++++- 3 files changed, 35 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/nostr.js b/lib/nostr.js index 53c8d7d1..477d2b12 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -18,6 +18,8 @@ let timeout; pool.on("event", async (relay, sub, ev) => { try { if (sub === "live") { + let { pubkey } = ev; + if (Math.abs(Math.floor(Date.now() / 1000) - ev.created_at) > 7200) return; @@ -34,16 +36,31 @@ pool.on("event", async (relay, sub, ev) => { } coinos.send(["EVENT", ev]); - if (ev.kind === 1) broadcast("event", ev); + + ev.user = JSON.parse(await redis.get(`user:${pubkey}`)) || { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; + + if (ev.kind === 1) { + broadcast("event", ev); + } } } else { let pubkey = sub; clearTimeout(store.timeouts[pubkey]); - store.fetching[pubkey] = true; + ev.user = JSON.parse(await redis.get(`user:${pubkey}`)) || { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; + await redis.sAdd(pubkey, ev.id); await redis.set(`ev:${ev.id}`, JSON.stringify(ev)); + store.fetching[pubkey] = true; store.timeouts[pubkey] = setTimeout( () => (store.fetching[pubkey] = false), 100 diff --git a/routes/events.js b/routes/events.js index 18abf1d7..07062140 100644 --- a/routes/events.js +++ b/routes/events.js @@ -1,4 +1,5 @@ import app from "$app"; +import db from "$db"; import redis from "$lib/redis"; import { pool } from "$lib/nostr"; import store from "$lib/store"; @@ -13,6 +14,13 @@ app.get("/:pubkey/events", async (req, res) => { 500 ); + let user = await db.User.findOne({ + where: { pubkey } + }); + + if (!user) user = { username: pubkey.substr(0,6), pubkey, anon: true } + await redis.set(`user:${pubkey}`, JSON.stringify(user)); + pool.subscribe(pubkey, { limit: 500, kinds: [1], diff --git a/routes/users.js b/routes/users.js index 82a71549..70186145 100644 --- a/routes/users.js +++ b/routes/users.js @@ -57,7 +57,14 @@ app.get("/users/:username", async (req, res) => { const { username } = req.params; let user = await db.User.findOne({ - attributes: ["username", "banner", "profile", "address", "currency"], + attributes: [ + "username", + "banner", + "profile", + "address", + "currency", + "pubkey" + ], where: Sequelize.where( Sequelize.fn("lower", Sequelize.col("username")), username.toLowerCase() -- cgit v1.2.3 From 2d2c991645f3b0ac7fb3ad88afdad0bb67c28b74 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 27 Dec 2022 22:51:33 -0800 Subject: follower endpoints --- lib/nostr.js | 59 +++++++++++++++++++++---- lib/utils.js | 21 +++++++++ lib/whitelist.js | 2 + routes/events.js | 32 +++++++++----- routes/users.js | 132 +++++++++++++++++++++++++++++++++++++++++++++++-------- 5 files changed, 206 insertions(+), 40 deletions(-) (limited to 'lib') diff --git a/lib/nostr.js b/lib/nostr.js index 477d2b12..257bca9e 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -5,6 +5,7 @@ import store from "$lib/store"; import redis from "$lib/redis"; const { relays } = await got("https://nostr.watch/relays.json").json(); + export const pool = RelayPool(relays); pool.on("open", relay => { @@ -12,11 +13,18 @@ pool.on("open", relay => { relay.subscribe("live", { limit: 1 }); }); -let coinos; +export let coinos; let seen = []; let timeout; pool.on("event", async (relay, sub, ev) => { try { + let parsed; + try { + parsed = JSON.parse(ev.content); + } catch (e) { + parsed = ev.content; + } + if (sub === "live") { let { pubkey } = ev; @@ -28,26 +36,59 @@ pool.on("event", async (relay, sub, ev) => { seen.length > 1000 && seen.shift(); if (coinos && ev.kind < 5) { - let parsed; - try { - parsed = JSON.parse(ev.content); - } catch (e) { - parsed = ev.content; - } - coinos.send(["EVENT", ev]); - ev.user = JSON.parse(await redis.get(`user:${pubkey}`)) || { + let newuser = { username: pubkey.substr(0, 6), pubkey, anon: true }; + ev.user = JSON.parse(await redis.get(`user:${pubkey}`)) || newuser; + + if (!ev.user.initialized) { + relay.subscribe("zero", { + limit: 1, + kinds: [0], + authors: [pubkey] + }); + } + if (ev.kind === 1) { broadcast("event", ev); } } + } else if (sub === "zero") { + let { pubkey } = ev; + let newuser = { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; + + let user = JSON.parse(await redis.get(`user:${pubkey}`)) || newuser; + if (parsed.name && user.username === pubkey.substr(0, 6)) + user.username = parsed.name; + + delete parsed.name; + + user = { + ...user, + ...parsed, + initialized: true + }; + + await redis.set(`user:${pubkey}`, JSON.stringify(user)); + } else if (sub.includes("follows")) { + let pubkey = sub.split(":")[0]; + await redis.set(`${pubkey}:follows`, JSON.stringify(ev.tags)); + for (let f of ev.tags) { + await redis.sAdd(`${f[1]}:followers`, pubkey); + } + store.fetching[pubkey] = false; } else { + if (!relay.url.includes("coinos")) return; + let pubkey = sub; clearTimeout(store.timeouts[pubkey]); diff --git a/lib/utils.js b/lib/utils.js index 46267dbd..2854fefc 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,4 @@ +import store from "$lib/store"; import config from "$config"; import db from "$db"; import sequelize from "@sequelize/core"; @@ -37,6 +38,26 @@ export const getUser = async (username, transaction) => { user.haspin = !!user.pin; + let { pubkey } = user; + + if (pubkey) { + store.fetching[pubkey] = true; + store.timeouts[pubkey] = setTimeout( + () => (store.fetching[pubkey] = false), + 500 + ); + + coinos.subscribe(`${pubkey}:follows`, { + limit: 1, + kinds: [3], + authors: [pubkey] + }); + + await wait(() => !store.fetching[pubkey], 100, 10); + user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)); + user.followers = await redis.sMembers(`${pubkey}:followers`); + } + return user; }; diff --git a/lib/whitelist.js b/lib/whitelist.js index 5a9c0ba3..e78bc8e5 100644 --- a/lib/whitelist.js +++ b/lib/whitelist.js @@ -10,6 +10,8 @@ export default [ "currency", "email", "fiat", + "follows", + "followers", "haspin", "id", "index", diff --git a/routes/events.js b/routes/events.js index 07062140..4dcc44f0 100644 --- a/routes/events.js +++ b/routes/events.js @@ -5,33 +5,41 @@ import { pool } from "$lib/nostr"; import store from "$lib/store"; import { wait } from "$lib/utils"; -app.get("/:pubkey/events", async (req, res) => { +app.get("/nostr/:pubkey", async (req, res) => { try { let { pubkey } = req.params; + store.fetching[pubkey] = true; store.timeouts[pubkey] = setTimeout( () => (store.fetching[pubkey] = false), 500 ); - let user = await db.User.findOne({ - where: { pubkey } - }); + let user = JSON.parse(await redis.get(`user:${pubkey}`)); - if (!user) user = { username: pubkey.substr(0,6), pubkey, anon: true } - await redis.set(`user:${pubkey}`, JSON.stringify(user)); + if (!user) { + user = await db.User.findOne({ + where: { pubkey } + }); + } + + if (!user) user = { username: pubkey.substr(0, 6), pubkey, anon: true }; + + let { since } = user; pool.subscribe(pubkey, { - limit: 500, + since, kinds: [1], authors: [pubkey] }); - let events = []; - await wait(() => !store.fetching[pubkey], 10, 10); - for (let id of await redis.sMembers(pubkey)) { - events.push(JSON.parse(await redis.get(`ev:${id}`))); - } + user.since = Math.round(Date.now() / 1000); + await redis.set(`user:${pubkey}`, JSON.stringify(user)); + + await wait(() => !store.fetching[pubkey], 100, 100); + let events = ( + await redis.mGet((await redis.sMembers(pubkey)).map(k => "ev:" + k)) + ).map(JSON.parse); res.send(events); } catch (e) { diff --git a/routes/users.js b/routes/users.js index 70186145..4c6305b5 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1,8 +1,9 @@ import app from "$app"; +import redis from "$lib/redis"; import config from "$config"; import store from "$lib/store"; import { auth, optionalAuth } from "$lib/passport"; -import { getUser } from "$lib/utils"; +import { getUser, wait } from "$lib/utils"; import axios from "axios"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; @@ -21,6 +22,7 @@ import lq from "$lib/liquid"; import { emit } from "$lib/sockets"; import register from "$lib/register"; import { requirePin } from "$lib/utils"; +import { coinos } from "$lib/nostr"; const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); @@ -44,35 +46,84 @@ app.get("/me", auth, async (req, res) => { } ] }); + + let { pubkey } = user; + + if (pubkey) { + store.fetching[pubkey] = true; + store.timeouts[pubkey] = setTimeout( + () => (store.fetching[pubkey] = false), + 500 + ); + + coinos.subscribe(`${pubkey}:follows`, { + limit: 1, + kinds: [3], + authors: [pubkey] + }); + + await wait(() => !store.fetching[pubkey], 100, 10); + user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)); + user.followers = await redis.sMembers(`${pubkey}:followers`); + } + user.accounts = await req.user.getAccounts(); user.payments = payments; user.haspin = !!user.pin; + res.send(pick(user, ...whitelist)); } catch (e) { + console.log("problem fetching user", e); res.code(500).send(e.message); } }); app.get("/users/:username", async (req, res) => { - const { username } = req.params; - - let user = await db.User.findOne({ - attributes: [ - "username", - "banner", - "profile", - "address", - "currency", - "pubkey" - ], - where: Sequelize.where( - Sequelize.fn("lower", Sequelize.col("username")), - username.toLowerCase() - ) - }); + try { + const { username } = req.params; - if (user) res.send(user); - else res.code(500).send("User not found"); + let user = await db.User.findOne({ + attributes: [ + "username", + "banner", + "profile", + "address", + "currency", + "pubkey" + ], + where: Sequelize.where( + Sequelize.fn("lower", Sequelize.col("username")), + username.toLowerCase() + ) + }); + + if (!user) return res.code(500).send("User not found"); + + user = user.get({ plain: true }); + let { pubkey } = user; + + if (pubkey) { + store.fetching[pubkey] = true; + store.timeouts[pubkey] = setTimeout( + () => (store.fetching[pubkey] = false), + 500 + ); + + coinos.subscribe(`${pubkey}:follows`, { + limit: 1, + kinds: [3], + authors: [pubkey] + }); + + await wait(() => !store.fetching[pubkey], 100, 10); + user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)); + user.followers = await redis.sMembers(`${pubkey}:followers`); + } + + res.send(user); + } catch (e) { + res.code(500).send(e.message); + } }); app.post("/register", async (req, res) => { @@ -603,3 +654,46 @@ app.get("/contacts", auth, async function(req, res) { res.code(500).send(e.message); } }); + +app.post("/checkPassword", auth, async function(req, res) { + let { user } = req; + let result = await bcrypt.compare(req.body.password, user.password); + if (!result) return res.code(500).send("Invalid password"); + res.send(result); +}); + +app.get("/:pubkey/follows", async (req, res) => { + try { + let { pubkey } = req.params; + let follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; + + if (follows.length) { + follows = await db.User.findAll({ + where: { pubkey: follows.map(t => t[1]) } + }); + + res.send(follows); + } + } catch (e) { + console.log(e); + res.code(500).send(e.message); + } +}); + +app.get("/:pubkey/followers", async (req, res) => { + try { + let { pubkey } = req.params; + let followers = await redis.sMembers(`${pubkey}:followers`); + + if (followers.length) { + followers = await db.User.findAll({ + where: { pubkey: followers } + }); + + res.send(followers); + } + } catch (e) { + console.log(e); + res.code(500).send(e.message); + } +}); -- cgit v1.2.3 From d45fb3d4b40096aa4f3650c73bad873eea5a885f Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 28 Dec 2022 07:01:06 +0000 Subject: missing imports --- lib/utils.js | 18 +++++++++++------- routes/events.js | 8 +++++--- routes/users.js | 1 + 3 files changed, 17 insertions(+), 10 deletions(-) (limited to 'lib') diff --git a/lib/utils.js b/lib/utils.js index 2854fefc..49f95a00 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,5 @@ import store from "$lib/store"; +import redis from "$lib/redis"; import config from "$config"; import db from "$db"; import sequelize from "@sequelize/core"; @@ -7,9 +8,18 @@ import { payments as bcPayments, networks as bcNetworks } from "bitcoinjs-lib"; import { payments as lqPayments, networks as lqNetworks } from "liquidjs-lib"; import lnd from "$lib/lnd"; import ln from "$lib/ln"; +import { coinos } from "$lib/nostr"; const { Op } = sequelize; +export const sleep = n => new Promise(r => setTimeout(r, n)); +export const wait = async (f, s = 300, n = 1000) => { + let i = 0; + while (!(await f()) && i < s) (await sleep(n)) && i++; + if (i >= s) throw new Error("timeout"); + return f(); +}; + export const requirePin = async ({ body, user }) => { if (!user || (user.pin && user.pin !== body.pin)) throw new Error("Invalid pin"); @@ -47,6 +57,7 @@ export const getUser = async (username, transaction) => { 500 ); + await wait(() => !!coinos, 100, 10); coinos.subscribe(`${pubkey}:follows`, { limit: 1, kinds: [3], @@ -193,10 +204,3 @@ export const derivePayRequest = async ({ amount, memo, tip }) => { ).bolt11; }; -export const sleep = n => new Promise(r => setTimeout(r, n)); -export const wait = async (f, s = 300, n = 1000) => { - let i = 0; - while (!(await f()) && i < s) (await sleep(n)) && i++; - if (i >= s) throw new Error("timeout"); - return f(); -}; diff --git a/routes/events.js b/routes/events.js index 4dcc44f0..de6a253e 100644 --- a/routes/events.js +++ b/routes/events.js @@ -37,9 +37,11 @@ app.get("/nostr/:pubkey", async (req, res) => { await redis.set(`user:${pubkey}`, JSON.stringify(user)); await wait(() => !store.fetching[pubkey], 100, 100); - let events = ( - await redis.mGet((await redis.sMembers(pubkey)).map(k => "ev:" + k)) - ).map(JSON.parse); + let ids = await redis.sMembers(pubkey); + + let events = ids.length ? ( + await redis.mGet((id).map(k => "ev:" + k)) + ).map(JSON.parse) : []; res.send(events); } catch (e) { diff --git a/routes/users.js b/routes/users.js index 4c6305b5..50d882ad 100644 --- a/routes/users.js +++ b/routes/users.js @@ -56,6 +56,7 @@ app.get("/me", auth, async (req, res) => { 500 ); + await wait(() => !!coinos, 100, 10); coinos.subscribe(`${pubkey}:follows`, { limit: 1, kinds: [3], -- cgit v1.2.3 From df3f91fbc6d0b75c4c875c787d8ab29b2f1d2768 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 28 Dec 2022 18:43:04 +0000 Subject: handle empty follows --- lib/utils.js | 2 +- routes/events.js | 6 +++--- routes/users.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/utils.js b/lib/utils.js index 49f95a00..6a4bc1b2 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -65,7 +65,7 @@ export const getUser = async (username, transaction) => { }); await wait(() => !store.fetching[pubkey], 100, 10); - user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)); + user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; user.followers = await redis.sMembers(`${pubkey}:followers`); } diff --git a/routes/events.js b/routes/events.js index a773cb1c..63189f70 100644 --- a/routes/events.js +++ b/routes/events.js @@ -1,7 +1,7 @@ import app from "$app"; import db from "$db"; import redis from "$lib/redis"; -import { pool } from "$lib/nostr"; +import { coinos } from "$lib/nostr"; import store from "$lib/store"; import { wait } from "$lib/utils"; @@ -27,7 +27,7 @@ app.get("/nostr/:pubkey", async (req, res) => { let { since } = user; - pool.subscribe(pubkey, { + coinos.subscribe(pubkey, { since, kinds: [1], authors: [pubkey] @@ -52,6 +52,6 @@ app.get("/nostr/:pubkey", async (req, res) => { app.post("/nostr/send", async (req, res) => { let { event } = req.body; - pool.send(["EVENT", event]); + coinos.send(["EVENT", event]); res.send(event); }); diff --git a/routes/users.js b/routes/users.js index 50d882ad..a276fd4e 100644 --- a/routes/users.js +++ b/routes/users.js @@ -64,7 +64,7 @@ app.get("/me", auth, async (req, res) => { }); await wait(() => !store.fetching[pubkey], 100, 10); - user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)); + user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; user.followers = await redis.sMembers(`${pubkey}:followers`); } @@ -117,7 +117,7 @@ app.get("/users/:username", async (req, res) => { }); await wait(() => !store.fetching[pubkey], 100, 10); - user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)); + user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; user.followers = await redis.sMembers(`${pubkey}:followers`); } -- cgit v1.2.3 From 581a905b2fc14240d0bcbe6e2c14023fae3589a4 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 28 Dec 2022 10:48:45 -0800 Subject: always define follows --- lib/utils.js | 6 +++--- routes/users.js | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/utils.js b/lib/utils.js index 6a4bc1b2..17283836 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -65,10 +65,11 @@ export const getUser = async (username, transaction) => { }); await wait(() => !store.fetching[pubkey], 100, 10); - user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; - user.followers = await redis.sMembers(`${pubkey}:followers`); } + user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; + user.followers = await redis.sMembers(`${pubkey}:followers`); + return user; }; @@ -203,4 +204,3 @@ export const derivePayRequest = async ({ amount, memo, tip }) => { await ln.invoice(value ? `${value}sat` : "any", new Date(), memo, 360) ).bolt11; }; - diff --git a/routes/users.js b/routes/users.js index a276fd4e..7a01f25e 100644 --- a/routes/users.js +++ b/routes/users.js @@ -64,10 +64,11 @@ app.get("/me", auth, async (req, res) => { }); await wait(() => !store.fetching[pubkey], 100, 10); - user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; - user.followers = await redis.sMembers(`${pubkey}:followers`); } + user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; + user.followers = await redis.sMembers(`${pubkey}:followers`); + user.accounts = await req.user.getAccounts(); user.payments = payments; user.haspin = !!user.pin; @@ -117,10 +118,11 @@ app.get("/users/:username", async (req, res) => { }); await wait(() => !store.fetching[pubkey], 100, 10); - user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; - user.followers = await redis.sMembers(`${pubkey}:followers`); } + user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; + user.followers = await redis.sMembers(`${pubkey}:followers`); + res.send(user); } catch (e) { res.code(500).send(e.message); -- cgit v1.2.3 From 33d8a7d5235281d705bc1ac27113e150b2b9c617 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 28 Dec 2022 15:05:56 -0800 Subject: fix follows and followers endpoints --- lib/nostr.js | 46 +++++++++++++++++++++++++++++++--------------- lib/utils.js | 3 +++ routes/events.js | 5 ++++- routes/users.js | 54 ++++++++++++++++++++++++++++++++++++++---------------- 4 files changed, 76 insertions(+), 32 deletions(-) (limited to 'lib') diff --git a/lib/nostr.js b/lib/nostr.js index 257bca9e..034a4aaa 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -41,32 +41,35 @@ pool.on("event", async (relay, sub, ev) => { let newuser = { username: pubkey.substr(0, 6), pubkey, - anon: true + anon: true, + follows: [], + followers: [] }; ev.user = JSON.parse(await redis.get(`user:${pubkey}`)) || newuser; - if (!ev.user.initialized) { - relay.subscribe("zero", { - limit: 1, - kinds: [0], - authors: [pubkey] - }); - } - - if (ev.kind === 1) { - broadcast("event", ev); - } + relay.subscribe("zero", { + limit: 1, + kinds: [0], + authors: [pubkey] + }); + + broadcast("event", ev); } } else if (sub === "zero") { let { pubkey } = ev; let newuser = { username: pubkey.substr(0, 6), pubkey, - anon: true + anon: true, + follows: [], + followers: [] }; let user = JSON.parse(await redis.get(`user:${pubkey}`)) || newuser; + + if (user.profileUpdated > ev.created_at) return; + if (parsed.name && user.username === pubkey.substr(0, 6)) user.username = parsed.name; @@ -75,17 +78,30 @@ pool.on("event", async (relay, sub, ev) => { user = { ...user, ...parsed, - initialized: true + initialized: true, + profileUpdated: ev.created_at }; await redis.set(`user:${pubkey}`, JSON.stringify(user)); + await redis.set(`user:${user.username}`, JSON.stringify(user)); } else if (sub.includes("follows")) { let pubkey = sub.split(":")[0]; await redis.set(`${pubkey}:follows`, JSON.stringify(ev.tags)); for (let f of ev.tags) { - await redis.sAdd(`${f[1]}:followers`, pubkey); + let [_, followPubkey] = f; + pool.subscribe("zero", { + limit: 1, + kinds: [0], + authors: [followPubkey] + }); + await redis.sAdd(`${followPubkey}:followers`, pubkey); } store.fetching[pubkey] = false; + } else if (sub.includes("followers")) { + let followed = sub.split(":")[0]; + let { pubkey } = ev; + await redis.sAdd(`${followed}:followers`, pubkey); + store.fetching[pubkey] = false; } else { if (!relay.url.includes("coinos")) return; diff --git a/lib/utils.js b/lib/utils.js index 17283836..5355cc2c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -10,6 +10,7 @@ import lnd from "$lib/lnd"; import ln from "$lib/ln"; import { coinos } from "$lib/nostr"; + const { Op } = sequelize; export const sleep = n => new Promise(r => setTimeout(r, n)); @@ -204,3 +205,5 @@ export const derivePayRequest = async ({ amount, memo, tip }) => { await ln.invoice(value ? `${value}sat` : "any", new Date(), memo, 360) ).bolt11; }; + +export const uniq = (a, k) => [...new Map(a.map(x => [k(x), x])).values()]; diff --git a/routes/events.js b/routes/events.js index 63189f70..3f42fead 100644 --- a/routes/events.js +++ b/routes/events.js @@ -23,7 +23,7 @@ app.get("/nostr/:pubkey", async (req, res) => { }); } - if (!user) user = { username: pubkey.substr(0, 6), pubkey, anon: true }; + if (!user) user = { username: pubkey.substr(0, 6), pubkey, anon: true, follows: [], followers: [] }; let { since } = user; @@ -33,6 +33,9 @@ app.get("/nostr/:pubkey", async (req, res) => { authors: [pubkey] }); + coinos.subscribe(`${pubkey}:followers`, { since, kinds: [3], "#p": [pubkey] }); + + user.since = Math.round(Date.now() / 1000); await redis.set(`user:${pubkey}`, JSON.stringify(user)); diff --git a/routes/users.js b/routes/users.js index 7a01f25e..d19e42ea 100644 --- a/routes/users.js +++ b/routes/users.js @@ -3,7 +3,7 @@ import redis from "$lib/redis"; import config from "$config"; import store from "$lib/store"; import { auth, optionalAuth } from "$lib/passport"; -import { getUser, wait } from "$lib/utils"; +import { getUser, uniq, wait } from "$lib/utils"; import axios from "axios"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; @@ -99,9 +99,23 @@ app.get("/users/:username", async (req, res) => { ) }); + if (user) user = user.get({ plain: true }); + else user = JSON.parse(await redis.get(`user:${username}`)); + + if (!user && username.length === 64) { + let newuser = { + username: username.substr(0, 6), + pubkey: username, + anon: true, + follows: [], + followers: [] + }; + + user = JSON.parse(await redis.get(`user:${newuser.pubkey}`)) || newuser; + } + if (!user) return res.code(500).send("User not found"); - user = user.get({ plain: true }); let { pubkey } = user; if (pubkey) { @@ -125,6 +139,7 @@ app.get("/users/:username", async (req, res) => { res.send(user); } catch (e) { + console.log(e); res.code(500).send(e.message); } }); @@ -668,15 +683,19 @@ app.post("/checkPassword", auth, async function(req, res) { app.get("/:pubkey/follows", async (req, res) => { try { let { pubkey } = req.params; - let follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; + let tags = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; - if (follows.length) { - follows = await db.User.findAll({ - where: { pubkey: follows.map(t => t[1]) } - }); - - res.send(follows); + let follows = []; + for (let f of tags) { + let [_, pubkey] = f; + let user = JSON.parse(await redis.get(`user:${pubkey}`)); + if (user) follows.push(user); } + + follows = uniq(follows, e => e.pubkey); + follows.sort((a, b) => a.username.localeCompare(b.username)); + + res.send(follows); } catch (e) { console.log(e); res.code(500).send(e.message); @@ -686,15 +705,18 @@ app.get("/:pubkey/follows", async (req, res) => { app.get("/:pubkey/followers", async (req, res) => { try { let { pubkey } = req.params; - let followers = await redis.sMembers(`${pubkey}:followers`); + let pubkeys = await redis.sMembers(`${pubkey}:followers`); - if (followers.length) { - followers = await db.User.findAll({ - where: { pubkey: followers } - }); - - res.send(followers); + let followers = []; + for (let pubkey of pubkeys) { + let user = JSON.parse(await redis.get(`user:${pubkey}`)); + if (user) followers.push(user); } + + followers = uniq(followers, e => e.pubkey); + followers.sort((a, b) => a.username.localeCompare(b.username)); + + res.send(followers); } catch (e) { console.log(e); res.code(500).send(e.message); -- cgit v1.2.3 From 43d11679dda199924632e7c1825b6cdbc7fd5387 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 29 Dec 2022 08:56:43 -0800 Subject: prettier --- lib/utils.js | 1 - routes/events.js | 22 ++++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/utils.js b/lib/utils.js index 5355cc2c..133be3c6 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -10,7 +10,6 @@ import lnd from "$lib/lnd"; import ln from "$lib/ln"; import { coinos } from "$lib/nostr"; - const { Op } = sequelize; export const sleep = n => new Promise(r => setTimeout(r, n)); diff --git a/routes/events.js b/routes/events.js index 3f42fead..074579b0 100644 --- a/routes/events.js +++ b/routes/events.js @@ -23,7 +23,14 @@ app.get("/nostr/:pubkey", async (req, res) => { }); } - if (!user) user = { username: pubkey.substr(0, 6), pubkey, anon: true, follows: [], followers: [] }; + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true, + follows: [], + followers: [] + }; let { since } = user; @@ -33,8 +40,11 @@ app.get("/nostr/:pubkey", async (req, res) => { authors: [pubkey] }); - coinos.subscribe(`${pubkey}:followers`, { since, kinds: [3], "#p": [pubkey] }); - + coinos.subscribe(`${pubkey}:followers`, { + since, + kinds: [3], + "#p": [pubkey] + }); user.since = Math.round(Date.now() / 1000); await redis.set(`user:${pubkey}`, JSON.stringify(user)); @@ -42,9 +52,9 @@ app.get("/nostr/:pubkey", async (req, res) => { await wait(() => !store.fetching[pubkey], 100, 100); let ids = await redis.sMembers(pubkey); - let events = ids.length ? ( - await redis.mGet((ids).map(k => "ev:" + k)) - ).map(JSON.parse) : []; + let events = ids.length + ? (await redis.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) + : []; res.send(events); } catch (e) { -- cgit v1.2.3 From 33bfa7d2d4d74f4724d117ad58a825253e53d321 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 29 Dec 2022 10:32:48 -0800 Subject: move follower endpoints --- lib/utils.js | 22 -------------- routes/users.js | 93 ++++++++++++++++++++++++++++----------------------------- 2 files changed, 46 insertions(+), 69 deletions(-) (limited to 'lib') diff --git a/lib/utils.js b/lib/utils.js index 133be3c6..08a582eb 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -48,28 +48,6 @@ export const getUser = async (username, transaction) => { user.haspin = !!user.pin; - let { pubkey } = user; - - if (pubkey) { - store.fetching[pubkey] = true; - store.timeouts[pubkey] = setTimeout( - () => (store.fetching[pubkey] = false), - 500 - ); - - await wait(() => !!coinos, 100, 10); - coinos.subscribe(`${pubkey}:follows`, { - limit: 1, - kinds: [3], - authors: [pubkey] - }); - - await wait(() => !store.fetching[pubkey], 100, 10); - } - - user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; - user.followers = await redis.sMembers(`${pubkey}:followers`); - return user; }; diff --git a/routes/users.js b/routes/users.js index d19e42ea..19060f9f 100644 --- a/routes/users.js +++ b/routes/users.js @@ -47,28 +47,6 @@ app.get("/me", auth, async (req, res) => { ] }); - let { pubkey } = user; - - if (pubkey) { - store.fetching[pubkey] = true; - store.timeouts[pubkey] = setTimeout( - () => (store.fetching[pubkey] = false), - 500 - ); - - await wait(() => !!coinos, 100, 10); - coinos.subscribe(`${pubkey}:follows`, { - limit: 1, - kinds: [3], - authors: [pubkey] - }); - - await wait(() => !store.fetching[pubkey], 100, 10); - } - - user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; - user.followers = await redis.sMembers(`${pubkey}:followers`); - user.accounts = await req.user.getAccounts(); user.payments = payments; user.haspin = !!user.pin; @@ -106,9 +84,7 @@ app.get("/users/:username", async (req, res) => { let newuser = { username: username.substr(0, 6), pubkey: username, - anon: true, - follows: [], - followers: [] + anon: true }; user = JSON.parse(await redis.get(`user:${newuser.pubkey}`)) || newuser; @@ -116,27 +92,6 @@ app.get("/users/:username", async (req, res) => { if (!user) return res.code(500).send("User not found"); - let { pubkey } = user; - - if (pubkey) { - store.fetching[pubkey] = true; - store.timeouts[pubkey] = setTimeout( - () => (store.fetching[pubkey] = false), - 500 - ); - - coinos.subscribe(`${pubkey}:follows`, { - limit: 1, - kinds: [3], - authors: [pubkey] - }); - - await wait(() => !store.fetching[pubkey], 100, 10); - } - - user.follows = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; - user.followers = await redis.sMembers(`${pubkey}:followers`); - res.send(user); } catch (e) { console.log(e); @@ -674,16 +629,42 @@ app.get("/contacts", auth, async function(req, res) { }); app.post("/checkPassword", auth, async function(req, res) { + try { let { user } = req; let result = await bcrypt.compare(req.body.password, user.password); - if (!result) return res.code(500).send("Invalid password"); + if (!result) throw new Error(); res.send(result); + } catch(e) { + res.code(500).send("Invalid password"); + } }); +let since = {}; app.get("/:pubkey/follows", async (req, res) => { try { let { pubkey } = req.params; + let { tagsonly } = req.query; + + store.fetching[pubkey] = true; + store.timeouts[pubkey] = setTimeout( + () => (store.fetching[pubkey] = false), + 500 + ); + + let sub = `${pubkey}:follows`; + coinos.subscribe(sub, { + since: since[sub], + limit: 1, + kinds: [3], + authors: [pubkey] + }); + + since[sub] = Math.round(Date.now() / 1000); + + await wait(() => !store.fetching[pubkey], 100, 10); + let tags = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; + if (tagsonly) return res.send(tags); let follows = []; for (let f of tags) { @@ -705,6 +686,24 @@ app.get("/:pubkey/follows", async (req, res) => { app.get("/:pubkey/followers", async (req, res) => { try { let { pubkey } = req.params; + + store.fetching[pubkey] = true; + store.timeouts[pubkey] = setTimeout( + () => (store.fetching[pubkey] = false), + 500 + ); + + let sub = `${pubkey}:followers`; + coinos.subscribe(sub, { + since: since[sub], + kinds: [3], + "#p": [pubkey] + }); + + since[sub] = Math.round(Date.now() / 1000); + + await wait(() => !store.fetching[pubkey], 100, 10); + let pubkeys = await redis.sMembers(`${pubkey}:followers`); let followers = []; -- cgit v1.2.3 From 0f20d1cb0ca5a65ff6ae5fc4bb97657fc7ba4ee5 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 29 Dec 2022 16:07:13 -0800 Subject: follower query caching --- lib/nostr.js | 120 ++++++++++++++++++++++++++++++------------------------- lib/utils.js | 2 +- routes/events.js | 21 +--------- routes/users.js | 63 ++++++++++++++--------------- 4 files changed, 97 insertions(+), 109 deletions(-) (limited to 'lib') diff --git a/lib/nostr.js b/lib/nostr.js index 034a4aaa..dc400b7f 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -14,15 +14,58 @@ pool.on("open", relay => { }); export let coinos; +let now = () => Math.round(Date.now() / 1000); + +let timeouts = {}; +export let q = async (sub, query, timeout = 8000) => + new Promise(async (r, j) => { + let rejected; + if (!coinos) return j(new Error("relay not initialized")); + query.since = await redis.get(`since:${sub}`); + if (now() - query.since < 3600) return r(); + + coinos.on("eose", s => !rejected && s === sub && r(timeouts[sub].clear())); + + coinos.subscribe(sub, query); + + timeouts[sub] = { + async clear() { + clearTimeout(this.timer); + delete timeouts[sub]; + coinos.unsubscribe(sub); + await redis.set(`since:${sub}`, now()); + }, + extend(t = timeout) { + clearTimeout(this.timer); + this.timer = setTimeout(() => { + j(new Error(`query timed out: ${sub} ${Date.now()}`)); + coinos.unsubscribe(sub); + rejected = true; + }, t); + }, + timer: undefined + }; + + timeouts[sub].extend(timeout); + }); + let seen = []; -let timeout; + +let getUser = async pubkey => + JSON.parse(await redis.get(`user:${pubkey}`)) || { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; + pool.on("event", async (relay, sub, ev) => { + if (timeouts[sub]) timeouts[sub].extend(); try { - let parsed; + let content; try { - parsed = JSON.parse(ev.content); + content = JSON.parse(ev.content); } catch (e) { - parsed = ev.content; + content = ev.content; } if (sub === "live") { @@ -38,48 +81,25 @@ pool.on("event", async (relay, sub, ev) => { if (coinos && ev.kind < 5) { coinos.send(["EVENT", ev]); - let newuser = { - username: pubkey.substr(0, 6), - pubkey, - anon: true, - follows: [], - followers: [] - }; - - ev.user = JSON.parse(await redis.get(`user:${pubkey}`)) || newuser; - - relay.subscribe("zero", { - limit: 1, - kinds: [0], - authors: [pubkey] - }); + ev.user = await getUser(pubkey); broadcast("event", ev); } - } else if (sub === "zero") { + } else if (sub.includes("profile")) { let { pubkey } = ev; - let newuser = { - username: pubkey.substr(0, 6), - pubkey, - anon: true, - follows: [], - followers: [] - }; - - let user = JSON.parse(await redis.get(`user:${pubkey}`)) || newuser; + let user = await getUser(pubkey); - if (user.profileUpdated > ev.created_at) return; + if (user.updated > ev.created_at) return; - if (parsed.name && user.username === pubkey.substr(0, 6)) - user.username = parsed.name; + if (content.name && user.username === pubkey.substr(0, 6)) + user.username = content.name; - delete parsed.name; + delete content.name; user = { ...user, - ...parsed, - initialized: true, - profileUpdated: ev.created_at + ...content, + updated: ev.created_at }; await redis.set(`user:${pubkey}`, JSON.stringify(user)); @@ -89,39 +109,29 @@ pool.on("event", async (relay, sub, ev) => { await redis.set(`${pubkey}:follows`, JSON.stringify(ev.tags)); for (let f of ev.tags) { let [_, followPubkey] = f; - pool.subscribe("zero", { - limit: 1, - kinds: [0], - authors: [followPubkey] - }); await redis.sAdd(`${followPubkey}:followers`, pubkey); } - store.fetching[pubkey] = false; } else if (sub.includes("followers")) { let followed = sub.split(":")[0]; let { pubkey } = ev; await redis.sAdd(`${followed}:followers`, pubkey); - store.fetching[pubkey] = false; } else { if (!relay.url.includes("coinos")) return; let pubkey = sub; - clearTimeout(store.timeouts[pubkey]); + ev.user = await getUser(pubkey); - ev.user = JSON.parse(await redis.get(`user:${pubkey}`)) || { - username: pubkey.substr(0, 6), - pubkey, - anon: true - }; + // console.log(ev.user.username, "UPDATED AT", ev.user.updated) + if (!ev.user.updated) { + relay.subscribe(`${pubkey}:${ev.id}:profile`, { + limit: 1, + kinds: [0], + authors: [pubkey] + }); + } await redis.sAdd(pubkey, ev.id); await redis.set(`ev:${ev.id}`, JSON.stringify(ev)); - - store.fetching[pubkey] = true; - store.timeouts[pubkey] = setTimeout( - () => (store.fetching[pubkey] = false), - 100 - ); } } catch (e) { console.log(e); diff --git a/lib/utils.js b/lib/utils.js index 08a582eb..0d2d3efb 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -13,7 +13,7 @@ import { coinos } from "$lib/nostr"; const { Op } = sequelize; export const sleep = n => new Promise(r => setTimeout(r, n)); -export const wait = async (f, s = 300, n = 1000) => { +export const wait = async (f, s = 300, n = 50) => { let i = 0; while (!(await f()) && i < s) (await sleep(n)) && i++; if (i >= s) throw new Error("timeout"); diff --git a/routes/events.js b/routes/events.js index 074579b0..fd2d97cd 100644 --- a/routes/events.js +++ b/routes/events.js @@ -1,7 +1,7 @@ import app from "$app"; import db from "$db"; import redis from "$lib/redis"; -import { coinos } from "$lib/nostr"; +import { coinos, q } from "$lib/nostr"; import store from "$lib/store"; import { wait } from "$lib/utils"; @@ -9,12 +9,6 @@ app.get("/nostr/:pubkey", async (req, res) => { try { let { pubkey } = req.params; - store.fetching[pubkey] = true; - store.timeouts[pubkey] = setTimeout( - () => (store.fetching[pubkey] = false), - 500 - ); - let user = JSON.parse(await redis.get(`user:${pubkey}`)); if (!user) { @@ -32,24 +26,13 @@ app.get("/nostr/:pubkey", async (req, res) => { followers: [] }; - let { since } = user; - - coinos.subscribe(pubkey, { - since, + await q(pubkey, { kinds: [1], authors: [pubkey] }); - coinos.subscribe(`${pubkey}:followers`, { - since, - kinds: [3], - "#p": [pubkey] - }); - - user.since = Math.round(Date.now() / 1000); await redis.set(`user:${pubkey}`, JSON.stringify(user)); - await wait(() => !store.fetching[pubkey], 100, 100); let ids = await redis.sMembers(pubkey); let events = ids.length diff --git a/routes/users.js b/routes/users.js index 19060f9f..e40f1b7e 100644 --- a/routes/users.js +++ b/routes/users.js @@ -22,7 +22,7 @@ import lq from "$lib/liquid"; import { emit } from "$lib/sockets"; import register from "$lib/register"; import { requirePin } from "$lib/utils"; -import { coinos } from "$lib/nostr"; +import { coinos, q } from "$lib/nostr"; const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); @@ -630,45 +630,40 @@ app.get("/contacts", auth, async function(req, res) { app.post("/checkPassword", auth, async function(req, res) { try { - let { user } = req; - let result = await bcrypt.compare(req.body.password, user.password); - if (!result) throw new Error(); - res.send(result); - } catch(e) { + let { user } = req; + let result = await bcrypt.compare(req.body.password, user.password); + if (!result) throw new Error(); + res.send(result); + } catch (e) { res.code(500).send("Invalid password"); - } + } }); -let since = {}; app.get("/:pubkey/follows", async (req, res) => { try { let { pubkey } = req.params; let { tagsonly } = req.query; - store.fetching[pubkey] = true; - store.timeouts[pubkey] = setTimeout( - () => (store.fetching[pubkey] = false), - 500 - ); - let sub = `${pubkey}:follows`; - coinos.subscribe(sub, { - since: since[sub], + await q(sub, { limit: 1, kinds: [3], authors: [pubkey] }); - since[sub] = Math.round(Date.now() / 1000); - - await wait(() => !store.fetching[pubkey], 100, 10); - let tags = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; if (tagsonly) return res.send(tags); let follows = []; for (let f of tags) { let [_, pubkey] = f; + + await q(`${pubkey}:profile:f1`, { + limit: 1, + kinds: [0], + authors: [pubkey] + }); + let user = JSON.parse(await redis.get(`user:${pubkey}`)); if (user) follows.push(user); } @@ -679,7 +674,7 @@ app.get("/:pubkey/follows", async (req, res) => { res.send(follows); } catch (e) { console.log(e); - res.code(500).send(e.message); + res.code(500).send(e && e.message); } }); @@ -687,28 +682,28 @@ app.get("/:pubkey/followers", async (req, res) => { try { let { pubkey } = req.params; - store.fetching[pubkey] = true; - store.timeouts[pubkey] = setTimeout( - () => (store.fetching[pubkey] = false), - 500 - ); - let sub = `${pubkey}:followers`; - coinos.subscribe(sub, { - since: since[sub], + await q(sub, { kinds: [3], "#p": [pubkey] }); - since[sub] = Math.round(Date.now() / 1000); - - await wait(() => !store.fetching[pubkey], 100, 10); - let pubkeys = await redis.sMembers(`${pubkey}:followers`); let followers = []; + for (let pubkey of pubkeys) { let user = JSON.parse(await redis.get(`user:${pubkey}`)); + if (!user || !user.updated) { + await q(`${pubkey}:profile:f2`, { + limit: 1, + kinds: [0], + authors: [pubkey] + }); + + user = JSON.parse(await redis.get(`user:${pubkey}`)); + } + if (user) followers.push(user); } @@ -718,6 +713,6 @@ app.get("/:pubkey/followers", async (req, res) => { res.send(followers); } catch (e) { console.log(e); - res.code(500).send(e.message); + res.code(500).send(e && e.message); } }); -- cgit v1.2.3 From bd47fe6e17139a3afdbaef92a65cf9b5afba75a1 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 31 Dec 2022 07:21:59 +0000 Subject: default relays --- db/models/users.js | 4 +++- lib/nostr.js | 5 ++++- lib/utils.js | 1 - 3 files changed, 7 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/db/models/users.js b/db/models/users.js index fd38facf..cc63ac57 100644 --- a/db/models/users.js +++ b/db/models/users.js @@ -253,7 +253,9 @@ const attributes = { const options = { tableName: "users", comment: "", - indexes: [] + indexes: [], + charset: "utf8mb4", + collate: "utf8mb4_unicode_ci" }; db["User"] = db.define("users_model", attributes, options); diff --git a/lib/nostr.js b/lib/nostr.js index dc400b7f..3894819d 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -4,7 +4,10 @@ import { broadcast } from "$lib/sockets"; import store from "$lib/store"; import redis from "$lib/redis"; -const { relays } = await got("https://nostr.watch/relays.json").json(); +const relays = [ + "wss://relay.damus.io", + "wss://nostr.coinos.io" +]; export const pool = RelayPool(relays); diff --git a/lib/utils.js b/lib/utils.js index 0d2d3efb..748b2a74 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -8,7 +8,6 @@ import { payments as bcPayments, networks as bcNetworks } from "bitcoinjs-lib"; import { payments as lqPayments, networks as lqNetworks } from "liquidjs-lib"; import lnd from "$lib/lnd"; import ln from "$lib/ln"; -import { coinos } from "$lib/nostr"; const { Op } = sequelize; -- cgit v1.2.3 From 572bf373a9771dfa11ccaf027a45c0c1611a26a8 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 3 Jan 2023 00:24:03 -0800 Subject: whitelist registration fields --- lib/nostr.js | 5 +---- routes/bitcoin/send.js | 1 - routes/users.js | 28 +++++++++++++++++++++------- 3 files changed, 22 insertions(+), 12 deletions(-) (limited to 'lib') diff --git a/lib/nostr.js b/lib/nostr.js index 3894819d..285fdde2 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -4,10 +4,7 @@ import { broadcast } from "$lib/sockets"; import store from "$lib/store"; import redis from "$lib/redis"; -const relays = [ - "wss://relay.damus.io", - "wss://nostr.coinos.io" -]; +const relays = ["wss://relay.damus.io", "wss://nostr.coinos.io"]; export const pool = RelayPool(relays); diff --git a/routes/bitcoin/send.js b/routes/bitcoin/send.js index c52592ae..3e030413 100644 --- a/routes/bitcoin/send.js +++ b/routes/bitcoin/send.js @@ -187,7 +187,6 @@ export default async (req, res) => { if (e.message === "low") e.message = "Insufficient funds in account" err("error sending bitcoin", e.message); - console.log(e); return res.code(500).send(e.message); } }; diff --git a/routes/users.js b/routes/users.js index 19ddc7b3..ff188fad 100644 --- a/routes/users.js +++ b/routes/users.js @@ -103,7 +103,18 @@ app.get("/users/:username", async (req, res) => { app.post("/register", async (req, res) => { try { const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; - const user = await register(req.body.user, ip, false); + let { cipher, pubkey, password, username, salt } = req.body.user; + + let user = { + cipher, + pubkey, + password, + username, + salt + }; + + user = await register(user, ip, false); + info("registered new user", username); res.send(pick(user, ...whitelist)); } catch (e) { res.code(500).send(e.message); @@ -683,7 +694,9 @@ app.get("/:pubkey/followers", async (req, res) => { try { let { pubkey } = req.params; - let pubkeys = await got(`https://coinos.io/nostr/followers?pubkey=${pubkey}`).json(); + let pubkeys = await got( + `https://coinos.io/nostr/followers?pubkey=${pubkey}` + ).json(); let followers = []; @@ -699,11 +712,12 @@ app.get("/:pubkey/followers", async (req, res) => { user = JSON.parse(await redis.get(`user:${pubkey}`)); } - if (!user) user = { - username: pubkey.substr(0, 6), - pubkey, - anon: true - }; + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; followers.push(user); } -- cgit v1.2.3 From acd94921684759ac3d582ec87db7ceaeb2fefe45 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 5 Jan 2023 07:43:24 -0800 Subject: proxy to nostr api --- docker-compose.yml | 15 +++++++++++++++ lib/nostr.js | 32 ++++++++++++++++++++++---------- package.json | 2 ++ pnpm-lock.yaml | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ routes/events.js | 5 +++-- routes/users.js | 24 ++++++++++++++++++------ 6 files changed, 110 insertions(+), 18 deletions(-) (limited to 'lib') diff --git a/docker-compose.yml b/docker-compose.yml index 4ab6ff37..8d61b689 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -220,6 +220,21 @@ services: networks: default: ipv4_address: 172.18.2.2 + api: + container_name: api + image: nostr-api + build: /home/adam/nostr-api + command: "pnpm dev" + volumes: + - "/home/adam/nostr-api:/app" + - "./config/nostr/data:/data" + environment: + DB: "/data/nostr.db" + ports: + - '9393:9393' + networks: + default: + ipv4_address: 172.18.2.4 networks: default: name: net diff --git a/lib/nostr.js b/lib/nostr.js index 285fdde2..8fc92be4 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -1,15 +1,29 @@ +import app from "$app"; +import config from "$config"; import { Relay, RelayPool } from "nostr"; import { got } from "got"; import { broadcast } from "$lib/sockets"; import store from "$lib/store"; import redis from "$lib/redis"; +import proxy from "@fastify/http-proxy"; -const relays = ["wss://relay.damus.io", "wss://nostr.coinos.io"]; +app.register(proxy, { + upstream: "http://api:9393", + prefix: "/nostr", + rewritePrefix: "", +}); + +const relays = [ + config.relay, + "wss://relay.damus.io", + "wss://nostr-pub.wellorder.net", + "wss://nostr.rocks", +]; export const pool = RelayPool(relays); pool.on("open", relay => { - if (relay.url.includes("coinos")) coinos = relay; + if (relay.url.includes(config.relay)) coinos = relay; relay.subscribe("live", { limit: 1 }); }); @@ -24,9 +38,9 @@ export let q = async (sub, query, timeout = 8000) => query.since = await redis.get(`since:${sub}`); if (now() - query.since < 3600) return r(); - coinos.on("eose", s => !rejected && s === sub && r(timeouts[sub].clear())); - - coinos.subscribe(sub, query); + coinos.on("eose", s => { + return !rejected && s === sub && r(timeouts[sub].clear()); + }); timeouts[sub] = { async clear() { @@ -47,6 +61,7 @@ export let q = async (sub, query, timeout = 8000) => }; timeouts[sub].extend(timeout); + coinos.subscribe(sub, query); }); let seen = []; @@ -79,11 +94,9 @@ pool.on("event", async (relay, sub, ev) => { seen.length > 1000 && seen.shift(); if (coinos && ev.kind < 5) { - coinos.send(["EVENT", ev]); - + // coinos.send(["EVENT", ev]); ev.user = await getUser(pubkey); - - broadcast("event", ev); + // broadcast("event", ev); } } else if (sub.includes("profile")) { let { pubkey } = ev; @@ -121,7 +134,6 @@ pool.on("event", async (relay, sub, ev) => { let pubkey = sub; ev.user = await getUser(pubkey); - // console.log(ev.user.username, "UPDATED AT", ev.user.updated) if (!ev.user.updated) { relay.subscribe(`${pubkey}:${ev.id}:profile`, { limit: 1, diff --git a/package.json b/package.json index 1bca9dba..ad806617 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "nodemon index.js" }, "dependencies": { + "@fastify/http-proxy": "^8.4.0", "@fastify/multipart": "^7.1.2", "@fastify/passport": "^2.2.0", "@fastify/secure-session": "^5.2.0", @@ -16,6 +17,7 @@ "async-mutex": "^0.2.4", "axios": "^0.21.1", "bcrypt": "^5.0.0", + "bech32": "^2.0.0", "bip32": "^2.0.5", "bip39": "^3.0.2", "bitcoinjs-lib": "6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6722b1d..3bbe6727 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,6 +1,7 @@ lockfileVersion: 5.4 specifiers: + '@fastify/http-proxy': ^8.4.0 '@fastify/multipart': ^7.1.2 '@fastify/passport': ^2.2.0 '@fastify/secure-session': ^5.2.0 @@ -10,6 +11,7 @@ specifiers: async-mutex: ^0.2.4 axios: ^0.21.1 bcrypt: ^5.0.0 + bech32: ^2.0.0 bip32: ^2.0.5 bip39: ^3.0.2 bitcoinjs-lib: '6' @@ -65,6 +67,7 @@ specifiers: zeromq: 6.0.0-beta.6 dependencies: + '@fastify/http-proxy': 8.4.0 '@fastify/multipart': 7.2.0 '@fastify/passport': 2.2.0 '@fastify/secure-session': 5.2.0 @@ -74,6 +77,7 @@ dependencies: async-mutex: 0.2.6 axios: 0.21.4 bcrypt: 5.1.0 + bech32: 2.0.0 bip32: 2.0.6 bip39: 3.0.4 bitcoinjs-lib: 6.0.2 @@ -186,6 +190,16 @@ packages: fastify-plugin: 4.3.0 dev: false + /@fastify/http-proxy/8.4.0: + resolution: {integrity: sha512-H8nwsmawFtKKRE6uhh1BtF1gQi/l147SmLsDGxB0HdYTHzjXz6uSQO3lEVmY7unKMzbArRjdoJQkEGpScszdSw==} + dependencies: + '@fastify/reply-from': 8.3.1 + ws: 8.9.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /@fastify/multipart/7.2.0: resolution: {integrity: sha512-LwfwbCLy30Be1pa5q7F8xCTygxJdEWkfkQhD4OWZ13+vMH4tP/6Bu3OkSTlFatxLAmbEl2UpHLf7CU7w7csRIw==} dependencies: @@ -206,6 +220,18 @@ packages: fastify-plugin: 4.3.0 dev: false + /@fastify/reply-from/8.3.1: + resolution: {integrity: sha512-fRByAvTMXuBuYIimcinukOB3YdmqtYPeoybXIBNY0aPVgetHkmCVffBo/M4pEOib9Pes8wuoYL4VawI65aHl4w==} + dependencies: + '@fastify/error': 3.0.0 + end-of-stream: 1.4.4 + fast-querystring: 1.0.0 + fastify-plugin: 4.3.0 + pump: 3.0.0 + tiny-lru: 10.0.1 + undici: 5.14.0 + dev: false + /@fastify/secure-session/5.2.0: resolution: {integrity: sha512-oRWKdHwWUzp4rsJ3GnR8w8doE+7vllWMtohBVp77bENkhm4/OvofaO6PJLJuQvKF7XJAKiGip29dX1RsllKT7g==} hasBin: true @@ -989,6 +1015,13 @@ packages: ieee754: 1.2.1 dev: false + /busboy/1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: false + /bytes/3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} @@ -4136,6 +4169,11 @@ packages: engines: {node: '>=4.0.0'} dev: false + /streamsearch/1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: false + /strict-uri-encode/2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} @@ -4268,6 +4306,11 @@ packages: engines: {node: '>=8'} dev: false + /tiny-lru/10.0.1: + resolution: {integrity: sha512-Vst+6kEsWvb17Zpz14sRJV/f8bUWKhqm6Dc+v08iShmIJ/WxqWytHzCTd6m88pS33rE2zpX34TRmOpAJPloNCA==} + engines: {node: '>=6'} + dev: false + /tiny-lru/9.0.3: resolution: {integrity: sha512-/i9GruRjXsnDgehxvy6iZ4AFNVxngEFbwzirhdulomMNPGPVV3ECMZOWSw0w4sRMZ9Al9m4jy08GPvRxRUGYlw==} engines: {node: '>=6'} @@ -4394,6 +4437,13 @@ packages: resolution: {integrity: sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==} dev: false + /undici/5.14.0: + resolution: {integrity: sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==} + engines: {node: '>=12.18'} + dependencies: + busboy: 1.6.0 + dev: false + /unique-filename/1.1.1: resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} dependencies: diff --git a/routes/events.js b/routes/events.js index fd2d97cd..f09cea23 100644 --- a/routes/events.js +++ b/routes/events.js @@ -1,7 +1,7 @@ import app from "$app"; import db from "$db"; import redis from "$lib/redis"; -import { coinos, q } from "$lib/nostr"; +import { coinos, pool, q } from "$lib/nostr"; import store from "$lib/store"; import { wait } from "$lib/utils"; @@ -48,6 +48,7 @@ app.get("/nostr/:pubkey", async (req, res) => { app.post("/nostr/send", async (req, res) => { let { event } = req.body; - coinos.send(["EVENT", event]); + pool.send(["EVENT", event]); + console.log("SENDING", JSON.stringify(event)) res.send(event); }); diff --git a/routes/users.js b/routes/users.js index ff188fad..ae64997b 100644 --- a/routes/users.js +++ b/routes/users.js @@ -24,6 +24,9 @@ import register from "$lib/register"; import { requirePin } from "$lib/utils"; import { coinos, q } from "$lib/nostr"; import got from "got"; +import { bech32 } from "bech32"; + +const { encode, decode, fromWords, toWords } = bech32; const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); @@ -61,7 +64,11 @@ app.get("/me", auth, async (req, res) => { app.get("/users/:username", async (req, res) => { try { - const { username } = req.params; + let { username } = req.params; + + if (username.startsWith("npub")) { + username = Buffer.from(fromWords(decode(username).words)).toString("hex") + } let user = await db.User.findOne({ attributes: [ @@ -72,10 +79,15 @@ app.get("/users/:username", async (req, res) => { "currency", "pubkey" ], - where: Sequelize.where( - Sequelize.fn("lower", Sequelize.col("username")), - username.toLowerCase() - ) + where: { + [Sequelize.Op.or]: [ + Sequelize.where( + Sequelize.fn("lower", Sequelize.col("username")), + username.toLowerCase() + ), + { pubkey: username } + ] + } }); if (user) user = user.get({ plain: true }); @@ -695,7 +707,7 @@ app.get("/:pubkey/followers", async (req, res) => { let { pubkey } = req.params; let pubkeys = await got( - `https://coinos.io/nostr/followers?pubkey=${pubkey}` + `${config.nostr}/followers?pubkey=${pubkey}` ).json(); let followers = []; -- cgit v1.2.3 From d42768e2dea7d157a8f28180e5aabc84f8a241b2 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 5 Jan 2023 16:40:48 -0800 Subject: get nostr api url from config --- lib/nostr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/nostr.js b/lib/nostr.js index 8fc92be4..6c5af6a7 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -8,7 +8,7 @@ import redis from "$lib/redis"; import proxy from "@fastify/http-proxy"; app.register(proxy, { - upstream: "http://api:9393", + upstream: config.nostr, prefix: "/nostr", rewritePrefix: "", }); -- cgit v1.2.3 From f45849a39dfc73f84fbae36a0c98771c592e96cd Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 6 Jan 2023 04:52:08 +0000 Subject: logging --- lib/nostr.js | 1 + routes/events.js | 1 - routes/users.js | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/nostr.js b/lib/nostr.js index 6c5af6a7..e6174a57 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -11,6 +11,7 @@ app.register(proxy, { upstream: config.nostr, prefix: "/nostr", rewritePrefix: "", + disableRequestLogging: true, }); const relays = [ diff --git a/routes/events.js b/routes/events.js index f09cea23..1734af24 100644 --- a/routes/events.js +++ b/routes/events.js @@ -49,6 +49,5 @@ app.get("/nostr/:pubkey", async (req, res) => { app.post("/nostr/send", async (req, res) => { let { event } = req.body; pool.send(["EVENT", event]); - console.log("SENDING", JSON.stringify(event)) res.send(event); }); diff --git a/routes/users.js b/routes/users.js index ae64997b..fbe28fa3 100644 --- a/routes/users.js +++ b/routes/users.js @@ -185,6 +185,8 @@ app.get("/exists", async (req, res) => { app.post("/user", auth, async (req, res) => { try { let { user } = req; + info("updating user", user.username); + let { address, cipher, -- cgit v1.2.3 From 418b5052e276ec3e55297c24453f1cab8a354d34 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 5 Jan 2023 22:40:17 -0800 Subject: relative path --- docker-compose.yml | 3 ++- lib/nostr.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/docker-compose.yml b/docker-compose.yml index efb21a57..d6117776 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -223,10 +223,11 @@ services: api: container_name: api image: nostr-api + restart: always build: ../nostr-api command: "pnpm dev" volumes: - - "/home/adam/nostr-api:/app" + - "../nostr-api:/app" - "./config/nostr/data:/data" environment: DB: "/data/nostr.db" diff --git a/lib/nostr.js b/lib/nostr.js index e6174a57..99265f7d 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -1,7 +1,6 @@ import app from "$app"; import config from "$config"; import { Relay, RelayPool } from "nostr"; -import { got } from "got"; import { broadcast } from "$lib/sockets"; import store from "$lib/store"; import redis from "$lib/redis"; @@ -39,6 +38,7 @@ export let q = async (sub, query, timeout = 8000) => query.since = await redis.get(`since:${sub}`); if (now() - query.since < 3600) return r(); + coinos.on("eose", s => { return !rejected && s === sub && r(timeouts[sub].clear()); }); -- cgit v1.2.3 From 8266d3adad8d093212b208ff93e6bee41f3370e3 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 6 Jan 2023 13:59:03 -0800 Subject: locktime field, uuid for token --- db/models/users.js | 3 +++ lib/nostr.js | 61 +++++++++++++++++++++++++++++++++++------------------- lib/passport.js | 14 +++++++++---- lib/whitelist.js | 1 + routes/users.js | 31 +++++++++++---------------- 5 files changed, 66 insertions(+), 44 deletions(-) (limited to 'lib') diff --git a/db/models/users.js b/db/models/users.js index cc63ac57..23dab0bf 100644 --- a/db/models/users.js +++ b/db/models/users.js @@ -247,6 +247,9 @@ const attributes = { }, phone: { type: DataTypes.STRING(255) + }, + locktime: { + type: DataTypes.INTEGER(11) } }; diff --git a/lib/nostr.js b/lib/nostr.js index 99265f7d..4730883f 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -5,22 +5,16 @@ import { broadcast } from "$lib/sockets"; import store from "$lib/store"; import redis from "$lib/redis"; import proxy from "@fastify/http-proxy"; +import { wait } from "$lib/utils"; app.register(proxy, { upstream: config.nostr, prefix: "/nostr", rewritePrefix: "", - disableRequestLogging: true, + disableRequestLogging: true }); -const relays = [ - config.relay, - "wss://relay.damus.io", - "wss://nostr-pub.wellorder.net", - "wss://nostr.rocks", -]; - -export const pool = RelayPool(relays); +export const pool = RelayPool(config.relays); pool.on("open", relay => { if (relay.url.includes(config.relay)) coinos = relay; @@ -31,38 +25,63 @@ export let coinos; let now = () => Math.round(Date.now() / 1000); let timeouts = {}; -export let q = async (sub, query, timeout = 8000) => +export let q = async (sub, query, timeout = 20000) => new Promise(async (r, j) => { + let start = Date.now(); + let seen = []; let rejected; - if (!coinos) return j(new Error("relay not initialized")); query.since = await redis.get(`since:${sub}`); if (now() - query.since < 3600) return r(); + let done = { [sub]: [] }; - coinos.on("eose", s => { - return !rejected && s === sub && r(timeouts[sub].clear()); - }); + let check = s => { + let elapsed = Date.now() - start; + if (rejected) return true; + if (done[s].length === pool.relays.length || elapsed > 200) { + r(); + if (timeouts[s]) timeouts[s].clear(); + return true; + } + }; timeouts[sub] = { async clear() { clearTimeout(this.timer); delete timeouts[sub]; - coinos.unsubscribe(sub); + pool.unsubscribe(sub); await redis.set(`since:${sub}`, now()); }, - extend(t = timeout) { + extend(ev) { + if (ev) { + if (seen.includes(ev.id)) return; + seen.push(ev.id); + seen.length > 1000 && seen.shift(); + } + clearTimeout(this.timer); this.timer = setTimeout(() => { j(new Error(`query timed out: ${sub} ${Date.now()}`)); - coinos.unsubscribe(sub); + pool.unsubscribe(sub); rejected = true; - }, t); + }, timeout); }, timer: undefined }; - timeouts[sub].extend(timeout); - coinos.subscribe(sub, query); + timeouts[sub].extend(); + pool.subscribe(sub, query); + + pool.on("eose", (relay, s) => { + if (done[s]) done[s].push(relay.url); + check(s); + }); + + try { + await wait(() => check(sub)); + } catch (e) { + console.log(e); + } }); let seen = []; @@ -75,7 +94,7 @@ let getUser = async pubkey => }; pool.on("event", async (relay, sub, ev) => { - if (timeouts[sub]) timeouts[sub].extend(); + if (timeouts[sub]) timeouts[sub].extend(ev); try { let content; try { diff --git a/lib/passport.js b/lib/passport.js index ac2949a5..1244aab5 100644 --- a/lib/passport.js +++ b/lib/passport.js @@ -35,13 +35,19 @@ fastifyPassport.use( }, async (payload, next) => { try { - let { username } = payload; + let { username, uuid } = payload; + let where = {}; + + if (username) + where.username = { [Op.or]: [username, username.replace(/ /g, "")] }; + + if (uuid) where.uuid = uuid; + let user = await db.User.findOne({ - where: { - username: { [Op.or]: [username, username.replace(/ /g, "")] } - }, + where, include: { model: db.Account, as: "account" } }); + if (user.locked) throw new Error("account is locked"); next(null, user); } catch (e) { diff --git a/lib/whitelist.js b/lib/whitelist.js index e78bc8e5..4e4dc14e 100644 --- a/lib/whitelist.js +++ b/lib/whitelist.js @@ -16,6 +16,7 @@ export default [ "id", "index", "keys", + "locktime", "payments", "pubkey", "profile", diff --git a/routes/users.js b/routes/users.js index c935a137..764d1f03 100644 --- a/routes/users.js +++ b/routes/users.js @@ -4,13 +4,10 @@ import config from "$config"; import store from "$lib/store"; import { auth, optionalAuth } from "$lib/passport"; import { getUser, uniq, wait } from "$lib/utils"; -import axios from "axios"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { authenticator } from "otplib"; import Sequelize from "@sequelize/core"; -import bitcoin from "bitcoinjs-lib"; -import liquid from "liquidjs-lib"; import { fromBase58, fromPrivateKey } from "bip32"; import { Mutex } from "async-mutex"; import bip32 from "bip32"; @@ -191,7 +188,7 @@ app.post("/user", auth, async (req, res) => { let { confirm, password, pin, newpin, username } = req.body; if (user.pin && !(pin === user.pin)) throw new Error("Pin required"); - if (typeof newpin !== 'undefined') user.pin = newpin; + if (typeof newpin !== "undefined") user.pin = newpin; if (!user.pin) user.pin = null; let exists; @@ -210,26 +207,22 @@ app.post("/user", auth, async (req, res) => { if (user.username !== username) l("changing username", user.username, username); user.username = username; - - token = jwt.sign({ username }, config.jwt); - res.cookie("token", token, { - expires: new Date(Date.now() + 432000000) - }); } let attributes = [ - "unit", + "address", "cipher", + "currencies", + "currency", + "email", + "fiat", + "locktime", "pubkey", "salt", - "currency", - "currencies", + "seed", "tokens", "twofa", - "seed", - "fiat", - "email", - "address" + "unit" ]; for (let a of attributes) { @@ -356,8 +349,8 @@ let login = async (req, res) => { try { const url = `${callback}&sig=${sig}&key=${key}`; - const response = await axios.get(url); - res.send(response.data); + const response = await got(url).json(); + res.send(response); } catch (e) { err("problem calling lnurl login", e.message); res.code(500).send(e.message); @@ -393,7 +386,7 @@ let login = async (req, res) => { req.headers["x-forwarded-for"] || req.connection.remoteAddress ); - let payload = { username: user.username }; + let payload = { uuid: user.uuid }; let token = jwt.sign(payload, config.jwt); res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); user.accounts = await user.getAccounts(); -- cgit v1.2.3 From 8076c6c8da495331a6013671f80f178e57d42d19 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 6 Jan 2023 22:10:54 +0000 Subject: still encode username in token --- lib/sockets.js | 4 ++-- routes/users.js | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) (limited to 'lib') diff --git a/lib/sockets.js b/lib/sockets.js index 7498db62..3bd0c2c2 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -92,8 +92,8 @@ app.register(async function(app) { try { if (!token || token === "null" || !jwt.decode(token)) return ws.close(code, `bad token ${token}`); - const username = jwt.decode(token).username; - if (!username) throw new Error("Invalid JWT token"); + const { username, uuid } = jwt.decode(token); + if (!(username || uuid)) throw new Error("Invalid JWT token"); if (!store.sockets[username]) store.sockets[username] = {}; const existing = Object.keys(store.sockets[username]); diff --git a/routes/users.js b/routes/users.js index 764d1f03..20b3bfee 100644 --- a/routes/users.js +++ b/routes/users.js @@ -342,7 +342,7 @@ app.post("/accounts", auth, async (req, res) => { let login = async (req, res) => { try { - const { params, sig, key } = req.body; + const { params, sig, key, password, username, token: twofa } = req.body; if (sig) { const { callback } = params; @@ -359,16 +359,14 @@ let login = async (req, res) => { return; } - let twofa = req.body.token; - - let user = await getUser(req.body.username); + let user = await getUser(username); if ( !user || (user.password && - !(await bcrypt.compare(req.body.password, user.password))) + !(await bcrypt.compare(password, user.password))) ) { - warn("invalid username or password attempt", req.body.username); + warn("invalid username or password attempt", username); return res.code(401).send({}); } @@ -382,11 +380,11 @@ let login = async (req, res) => { l( "login", - req.body.username, + username, req.headers["x-forwarded-for"] || req.connection.remoteAddress ); - let payload = { uuid: user.uuid }; + let payload = { username, uuid: user.uuid }; let token = jwt.sign(payload, config.jwt); res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); user.accounts = await user.getAccounts(); -- cgit v1.2.3 From e3266ddecd825b8716df7dc418246441d4dcb0f7 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 6 Jan 2023 17:31:22 -0800 Subject: background nostr fetching --- db/models/users.js | 6 ++++++ lib/nostr.js | 21 +++++++++++-------- lib/sockets.js | 7 +++++-- lib/utils.js | 7 ++++++- lib/whitelist.js | 4 +++- routes/events.js | 5 +++-- routes/users.js | 59 ++++++++++++++++++++++++++++++++++++++++-------------- 7 files changed, 80 insertions(+), 29 deletions(-) (limited to 'lib') diff --git a/db/models/users.js b/db/models/users.js index 72680fd8..fce79537 100644 --- a/db/models/users.js +++ b/db/models/users.js @@ -250,6 +250,12 @@ const attributes = { }, locktime: { type: DataTypes.INTEGER(11) + }, + prompt: { + type: DataTypes.BOOLEAN + }, + display: { + type: DataTypes.STRING(255) } }; diff --git a/lib/nostr.js b/lib/nostr.js index 4730883f..4ee5cb13 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -25,20 +25,24 @@ export let coinos; let now = () => Math.round(Date.now() / 1000); let timeouts = {}; -export let q = async (sub, query, timeout = 20000) => +export let q = async (sub, query, timeout = 20000, since = 3600, eager = 200) => new Promise(async (r, j) => { let start = Date.now(); let seen = []; let rejected; query.since = await redis.get(`since:${sub}`); - if (now() - query.since < 3600) return r(); + if (now() - query.since < since) return r(); let done = { [sub]: [] }; let check = s => { let elapsed = Date.now() - start; if (rejected) return true; - if (done[s].length === pool.relays.length || elapsed > 200) { + if ( + done[s] && + (done[s].length === pool.relays.length || + (done[s].length && elapsed > eager)) + ) { r(); if (timeouts[s]) timeouts[s].clear(); return true; @@ -78,7 +82,7 @@ export let q = async (sub, query, timeout = 20000) => }); try { - await wait(() => check(sub)); + await wait(() => check(sub), 100, 200); } catch (e) { console.log(e); } @@ -137,6 +141,7 @@ pool.on("event", async (relay, sub, ev) => { await redis.set(`user:${pubkey}`, JSON.stringify(user)); await redis.set(`user:${user.username}`, JSON.stringify(user)); + coinos.send(ev); } else if (sub.includes("follows")) { let pubkey = sub.split(":")[0]; await redis.set(`${pubkey}:follows`, JSON.stringify(ev.tags)); @@ -144,14 +149,14 @@ pool.on("event", async (relay, sub, ev) => { let [_, followPubkey] = f; await redis.sAdd(`${followPubkey}:followers`, pubkey); } + coinos.send(ev); } else if (sub.includes("followers")) { let followed = sub.split(":")[0]; let { pubkey } = ev; await redis.sAdd(`${followed}:followers`, pubkey); - } else { - if (!relay.url.includes("coinos")) return; - - let pubkey = sub; + coinos.send(ev); + } else if (sub.includes("notes")) { + let pubkey = sub.split(":")[0]; ev.user = await getUser(pubkey); if (!ev.user.updated) { diff --git a/lib/sockets.js b/lib/sockets.js index 3bd0c2c2..82cf3f51 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -85,6 +85,7 @@ app.register(async function(app) { break; case "login": + let user; let token = data; if (lastSeen[token] > subtractSeconds(5)) return; lastSeen[token] = new Date(); @@ -92,7 +93,9 @@ app.register(async function(app) { try { if (!token || token === "null" || !jwt.decode(token)) return ws.close(code, `bad token ${token}`); - const { username, uuid } = jwt.decode(token); + let { username, uuid } = jwt.decode(token); + if (uuid) user = await getUser(uuid); + username = user.username; if (!(username || uuid)) throw new Error("Invalid JWT token"); if (!store.sockets[username]) store.sockets[username] = {}; @@ -110,7 +113,7 @@ app.register(async function(app) { store.sockets[username][id] = ws; names[id] = username; - let user = await getUser(username); + if (!user) user = await getUser(username); let { haspin } = user; diff --git a/lib/utils.js b/lib/utils.js index 748b2a74..95ee5fb1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -11,6 +11,8 @@ import ln from "$lib/ln"; const { Op } = sequelize; +export const nada = () => {}; + export const sleep = n => new Promise(r => setTimeout(r, n)); export const wait = async (f, s = 300, n = 50) => { let i = 0; @@ -33,7 +35,10 @@ export const getUser = async (username, transaction) => { } ], where: { - username: { [Op.or]: [username, username.replace(/ /g, "")] } + [Op.or]: { + username: { [Op.or]: [username, username.replace(/ /g, "")] }, + uuid: { [Op.or]: [username, username.replace(/ /g, "")] } + } } }; diff --git a/lib/whitelist.js b/lib/whitelist.js index 4e4dc14e..96db6cdc 100644 --- a/lib/whitelist.js +++ b/lib/whitelist.js @@ -8,6 +8,7 @@ export default [ "cipher", "currencies", "currency", + "display", "email", "fiat", "follows", @@ -18,8 +19,9 @@ export default [ "keys", "locktime", "payments", - "pubkey", "profile", + "prompt", + "pubkey", "salt", "seed", "symbol", diff --git a/routes/events.js b/routes/events.js index 1734af24..bbdd8891 100644 --- a/routes/events.js +++ b/routes/events.js @@ -26,14 +26,15 @@ app.get("/nostr/:pubkey", async (req, res) => { followers: [] }; - await q(pubkey, { + q(`${pubkey}:notes`, { kinds: [1], authors: [pubkey] - }); + }).catch(console.log); await redis.set(`user:${pubkey}`, JSON.stringify(user)); let ids = await redis.sMembers(pubkey); + console.log("IDS", ids); let events = ids.length ? (await redis.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) diff --git a/routes/users.js b/routes/users.js index d2cb1540..d209c3d9 100644 --- a/routes/users.js +++ b/routes/users.js @@ -3,7 +3,7 @@ import redis from "$lib/redis"; import config from "$config"; import store from "$lib/store"; import { auth, optionalAuth } from "$lib/passport"; -import { getUser, uniq, wait } from "$lib/utils"; +import { getUser, nada, uniq, wait } from "$lib/utils"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { authenticator } from "otplib"; @@ -74,7 +74,9 @@ app.get("/users/:username", async (req, res) => { "profile", "address", "currency", - "pubkey" + "pubkey", + "display", + "prompt" ], where: { [Sequelize.Op.or]: [ @@ -214,9 +216,11 @@ app.post("/user", auth, async (req, res) => { "cipher", "currencies", "currency", + "display", "email", "fiat", "locktime", + "prompt", "pubkey", "salt", "seed", @@ -654,11 +658,17 @@ app.get("/:pubkey/follows", async (req, res) => { let { tagsonly } = req.query; let sub = `${pubkey}:follows`; - await q(sub, { - limit: 1, - kinds: [3], - authors: [pubkey] - }); + q( + sub, + { + limit: 1, + kinds: [3], + authors: [pubkey] + }, + 60000, + 3600, + 60000 + ).catch(nada); let tags = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; if (tagsonly) return res.send(tags); @@ -667,14 +677,22 @@ app.get("/:pubkey/follows", async (req, res) => { for (let f of tags) { let [_, pubkey] = f; - await q(`${pubkey}:profile:f1`, { + q(`${pubkey}:profile:f1`, { limit: 1, kinds: [0], authors: [pubkey] - }); + }).catch(nada); let user = JSON.parse(await redis.get(`user:${pubkey}`)); - if (user) follows.push(user); + + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; + + follows.push(user); } follows = uniq(follows, e => e.pubkey); @@ -691,20 +709,31 @@ app.get("/:pubkey/followers", async (req, res) => { try { let { pubkey } = req.params; - let pubkeys = await got( - `${config.nostr}/followers?pubkey=${pubkey}` - ).json(); + let pubkeys = [ + ...new Set([ + ...(await got(`${config.nostr}/followers?pubkey=${pubkey}`).json()), + ...(await redis.sMembers(`${pubkey}:followers`)) + ]) + ]; let followers = []; + q( + `${pubkey}:followers`, + { kinds: [3], "#p": [pubkey] }, + 60000, + 3600, + 60000 + ).catch(nada); + for (let pubkey of pubkeys) { let user = JSON.parse(await redis.get(`user:${pubkey}`)); if (!user || !user.updated) { - await q(`${pubkey}:profile:f2`, { + q(`${pubkey}:profile:f2`, { limit: 1, kinds: [0], authors: [pubkey] - }); + }).catch(nada); user = JSON.parse(await redis.get(`user:${pubkey}`)); } -- cgit v1.2.3 From b02ba909998a8a61145eaaaadb3beb1845d4a151 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 7 Jan 2023 01:36:16 +0000 Subject: quiet --- lib/sockets.js | 2 +- routes/events.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/sockets.js b/lib/sockets.js index 82cf3f51..5980304b 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -95,7 +95,7 @@ app.register(async function(app) { return ws.close(code, `bad token ${token}`); let { username, uuid } = jwt.decode(token); if (uuid) user = await getUser(uuid); - username = user.username; + if (user) ({ username } = user); if (!(username || uuid)) throw new Error("Invalid JWT token"); if (!store.sockets[username]) store.sockets[username] = {}; diff --git a/routes/events.js b/routes/events.js index bbdd8891..554be5d8 100644 --- a/routes/events.js +++ b/routes/events.js @@ -3,7 +3,7 @@ import db from "$db"; import redis from "$lib/redis"; import { coinos, pool, q } from "$lib/nostr"; import store from "$lib/store"; -import { wait } from "$lib/utils"; +import { nada, wait } from "$lib/utils"; app.get("/nostr/:pubkey", async (req, res) => { try { @@ -29,12 +29,11 @@ app.get("/nostr/:pubkey", async (req, res) => { q(`${pubkey}:notes`, { kinds: [1], authors: [pubkey] - }).catch(console.log); + }).catch(nada); await redis.set(`user:${pubkey}`, JSON.stringify(user)); let ids = await redis.sMembers(pubkey); - console.log("IDS", ids); let events = ids.length ? (await redis.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) -- cgit v1.2.3 From 34e87ab8192928cb0ec2096caa27f6bcdff686b1 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 6 Jan 2023 21:11:39 -0800 Subject: rename images endpoint --- lib/sync.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'lib') diff --git a/lib/sync.js b/lib/sync.js index 97207752..9e2fc31c 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -13,6 +13,7 @@ import ln from "./ln"; import lnd from "./lnd"; import bc from "./bitcoin"; import lq from "./liquid"; +import { adminAuth } from "$lib/passport"; const init = async () => { try { @@ -382,3 +383,32 @@ app.get("/conversions", async (req, res) => { .filter(c => !payments.includes(c)) ); }); + +app.post("/renameImages", adminAuth, async (req, res) => { + let d = './uploads/'; + fs.readdir(d, (err, files) => { + files.forEach(async file => { + try { + let m = file.match(/(.*)-profile/); + + let username; + + if (m && m[1]) username = m[1]; + else m = file.match(/(.*)-banner/); + if (m && m[1]) username = m[1]; + + console.log(username); + + if (username) { + let { uuid } = await db.User.findOne({ where: { username } }); + fs.rename(d + file, d + uuid + file.replace(username, ""), function(err) { + if (err) console.log("ERROR: " + err); + }); + } + } catch (e) { + console.log(e); + } + }); + }); + res.send({}); +}); -- cgit v1.2.3 From b6b806d142590a121c79c811b7fbb1f5ac6cdaa6 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 6 Jan 2023 21:20:44 -0800 Subject: whitelist uuid --- lib/whitelist.js | 1 + routes/users.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/whitelist.js b/lib/whitelist.js index 96db6cdc..47e9ed4e 100644 --- a/lib/whitelist.js +++ b/lib/whitelist.js @@ -27,6 +27,7 @@ export default [ "symbol", "theme", "twofa", + "uuid", "unit", "username", "verified" diff --git a/routes/users.js b/routes/users.js index d209c3d9..329210c8 100644 --- a/routes/users.js +++ b/routes/users.js @@ -76,7 +76,8 @@ app.get("/users/:username", async (req, res) => { "currency", "pubkey", "display", - "prompt" + "prompt", + "uuid" ], where: { [Sequelize.Op.or]: [ -- cgit v1.2.3 From 6df7873c907024285504f6ad684ecc1d1f9b7141 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 6 Jan 2023 21:22:22 -0800 Subject: use uuid for images --- lib/upload.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/upload.js b/lib/upload.js index 8d1c85ef..95dfe21a 100644 --- a/lib/upload.js +++ b/lib/upload.js @@ -32,7 +32,7 @@ app.post("/upload/:type", auth, async function(req, res) { if (format !== "image" && !["jpg", "jpeg", "png"].includes(ext)) throw new Error("unsupported file type"); - let name = `${req.user.username}-${type}.webp`; + let name = `${req.user.uuid}-${type}.webp`; let path = `/app/uploads/${name}`; let size = type === "banner" ? 1920 : 240; -- cgit v1.2.3 From 018732b4544fd95b7f1830db8b43dce26ebec1aa Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 6 Jan 2023 23:02:01 -0800 Subject: event endpoint, send uuid with contacts --- lib/nostr.js | 17 ++++++----------- routes/events.js | 33 ++++++++++++++++++++++++++++----- routes/users.js | 10 +++------- 3 files changed, 37 insertions(+), 23 deletions(-) (limited to 'lib') diff --git a/lib/nostr.js b/lib/nostr.js index 4ee5cb13..e1d2f68e 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -25,7 +25,11 @@ export let coinos; let now = () => Math.round(Date.now() / 1000); let timeouts = {}; -export let q = async (sub, query, timeout = 20000, since = 3600, eager = 200) => +export let q = async ( + sub, + query, + { timeout = 20000, since = 3600, eager = 200 } +) => new Promise(async (r, j) => { let start = Date.now(); let seen = []; @@ -157,16 +161,7 @@ pool.on("event", async (relay, sub, ev) => { coinos.send(ev); } else if (sub.includes("notes")) { let pubkey = sub.split(":")[0]; - ev.user = await getUser(pubkey); - - if (!ev.user.updated) { - relay.subscribe(`${pubkey}:${ev.id}:profile`, { - limit: 1, - kinds: [0], - authors: [pubkey] - }); - } - + if (relay.url === config.relay) console.log(ev) await redis.sAdd(pubkey, ev.id); await redis.set(`ev:${ev.id}`, JSON.stringify(ev)); } diff --git a/routes/events.js b/routes/events.js index 554be5d8..e52b8a7c 100644 --- a/routes/events.js +++ b/routes/events.js @@ -5,6 +5,25 @@ import { coinos, pool, q } from "$lib/nostr"; import store from "$lib/store"; import { nada, wait } from "$lib/utils"; +app.get("/event/:id", async (req, res) => { + try { + let event = JSON.parse(await redis.get(`ev:${req.params.id}`)); + let { pubkey } = event; + + event.user = JSON.parse(await redis.get(`user:${pubkey}`)) || { + username: pubkey.substr(0, 6), + pubkey, + anon: true, + follows: [], + followers: [] + }; + + res.send(event); + } catch (e) { + res.code(500).send("Problem fetching event"); + } +}); + app.get("/nostr/:pubkey", async (req, res) => { try { let { pubkey } = req.params; @@ -26,10 +45,14 @@ app.get("/nostr/:pubkey", async (req, res) => { followers: [] }; - q(`${pubkey}:notes`, { - kinds: [1], - authors: [pubkey] - }).catch(nada); + q( + `${pubkey}:notes`, + { + kinds: [1], + authors: [pubkey] + }, + { since: 0 } + ).catch(nada); await redis.set(`user:${pubkey}`, JSON.stringify(user)); @@ -39,7 +62,7 @@ app.get("/nostr/:pubkey", async (req, res) => { ? (await redis.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) : []; - res.send(events); + res.send(events.map(e => ({ ...e, user }))); } catch (e) { console.log(e); res.code(500).send("problem fetching user events"); diff --git a/routes/users.js b/routes/users.js index 329210c8..4e6b8a63 100644 --- a/routes/users.js +++ b/routes/users.js @@ -619,7 +619,7 @@ app.get("/contacts", auth, async function(req, res) { with_id: { [Sequelize.Op.ne]: null } }, include: { - attributes: ["username", "profile"], + attributes: ["username", "profile", "uuid"], model: db.User, as: "with" }, @@ -666,9 +666,7 @@ app.get("/:pubkey/follows", async (req, res) => { kinds: [3], authors: [pubkey] }, - 60000, - 3600, - 60000 + { timeout: 60000, eager: 60000 } ).catch(nada); let tags = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; @@ -722,9 +720,7 @@ app.get("/:pubkey/followers", async (req, res) => { q( `${pubkey}:followers`, { kinds: [3], "#p": [pubkey] }, - 60000, - 3600, - 60000 + { timeout: 60000, eager: 60000 } ).catch(nada); for (let pubkey of pubkeys) { -- cgit v1.2.3 From 50548775d05d6ac3c14595f7564607dc028495b6 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 8 Jan 2023 07:22:23 +0000 Subject: prioritize uuid in jwt payload --- lib/nostr.js | 1 - lib/passport.js | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/nostr.js b/lib/nostr.js index e1d2f68e..d170fed3 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -161,7 +161,6 @@ pool.on("event", async (relay, sub, ev) => { coinos.send(ev); } else if (sub.includes("notes")) { let pubkey = sub.split(":")[0]; - if (relay.url === config.relay) console.log(ev) await redis.sAdd(pubkey, ev.id); await redis.set(`ev:${ev.id}`, JSON.stringify(ev)); } diff --git a/lib/passport.js b/lib/passport.js index 1244aab5..8b58e305 100644 --- a/lib/passport.js +++ b/lib/passport.js @@ -38,10 +38,10 @@ fastifyPassport.use( let { username, uuid } = payload; let where = {}; - if (username) + if (uuid) where.uuid = uuid; + else if (username) where.username = { [Op.or]: [username, username.replace(/ /g, "")] }; - if (uuid) where.uuid = uuid; let user = await db.User.findOne({ where, @@ -51,6 +51,7 @@ fastifyPassport.use( if (user.locked) throw new Error("account is locked"); next(null, user); } catch (e) { + console.log("payload", payload) if (!e.message.includes("locked")) { console.log(e); err("problem logging in", e); -- cgit v1.2.3 From dce07aa93b3c2f47aca0c24cea6adde9ec37f1a7 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 8 Jan 2023 21:05:34 -0800 Subject: tech debt jubilee --- Caddyfile | 21 - apidoc/api_data.js | 762 ------------- apidoc/api_data.json | 742 ------------- apidoc/api_project.js | 16 - apidoc/api_project.json | 16 - apidoc/css/style.css | 525 --------- apidoc/fonts/glyphicons-halflings-regular.eot | Bin 20127 -> 0 bytes apidoc/fonts/glyphicons-halflings-regular.svg | 288 ----- apidoc/fonts/glyphicons-halflings-regular.ttf | Bin 45404 -> 0 bytes apidoc/fonts/glyphicons-halflings-regular.woff | Bin 23424 -> 0 bytes apidoc/fonts/glyphicons-halflings-regular.woff2 | Bin 18028 -> 0 bytes apidoc/img/android-chrome-192x192.png | Bin 7672 -> 0 bytes apidoc/img/android-chrome-512x512.png | Bin 28928 -> 0 bytes apidoc/img/apple-touch-icon.png | Bin 8775 -> 0 bytes apidoc/img/favicon-16x16.png | Bin 566 -> 0 bytes apidoc/img/favicon-32x32.png | Bin 1154 -> 0 bytes apidoc/img/favicon.ico | Bin 24838 -> 0 bytes apidoc/index.html | 816 -------------- apidoc/locales/ca.js | 25 - apidoc/locales/cs.js | 25 - apidoc/locales/de.js | 25 - apidoc/locales/es.js | 25 - apidoc/locales/fr.js | 25 - apidoc/locales/it.js | 25 - apidoc/locales/locale.js | 51 - apidoc/locales/nl.js | 25 - apidoc/locales/pl.js | 25 - apidoc/locales/pt_br.js | 25 - apidoc/locales/ro.js | 25 - apidoc/locales/ru.js | 25 - apidoc/locales/tr.js | 25 - apidoc/locales/vi.js | 25 - apidoc/locales/zh.js | 25 - apidoc/locales/zh_cn.js | 27 - apidoc/main.js | 1146 -------------------- apidoc/utils/handlebars_helper.js | 416 ------- apidoc/utils/send_sample_request.js | 272 ----- apidoc/utils/send_sample_request_utils.js | 97 -- apidoc/vendor/bootstrap.min.css | 6 - apidoc/vendor/bootstrap.min.js | 6 - apidoc/vendor/diff_match_patch.min.js | 49 - apidoc/vendor/handlebars.min.js | 29 - apidoc/vendor/jquery.min.js | 2 - apidoc/vendor/list.min.js | 2 - apidoc/vendor/lodash.custom.min.js | 41 - apidoc/vendor/path-to-regexp/LICENSE | 21 - apidoc/vendor/path-to-regexp/index.js | 204 ---- apidoc/vendor/polyfill.js | 96 -- apidoc/vendor/prism.css | 125 --- apidoc/vendor/prism.js | 21 - apidoc/vendor/require.min.js | 37 - apidoc/vendor/semver.min.js | 1 - apidoc/vendor/webfontloader.js | 17 - db/db.js | 5 - db/index.js | 157 --- db/init/prodschema.sql | 312 ------ db/migrations/20210612181508_init_referrals.js | 24 - .../20210612192804_init_waiting_list.js.js | 24 - db/migrations/20210612203333_schema_diff_merges.js | 39 - .../20210625232223_add_invoice_id_to_payments.js | 13 - .../20210628221727_add_webhook_to_invoices.js | 11 - db/migrations/20210713174217_add_admin_to_users.js | 16 - db/migrations/20210714123455_data_fixes_merge.js | 22 - db/migrations/20210714123456_add_fks_merge.js | 45 - db/migrations/s2/user_adjustments.js | 67 -- db/models/accounts.js | 217 ---- db/models/codes.js | 32 - db/models/deposits.js | 76 -- db/models/invoices.js | 202 ---- db/models/keys.js | 40 - db/models/orders.js | 112 -- db/models/payments.js | 220 ---- db/models/referrals.js | 90 -- db/models/requests.js | 67 -- db/models/users.js | 270 ----- db/models/waiting_list.js | 45 - db/models/withdrawals.js | 103 -- db/normalized/README.md | 48 - db/normalized/accounts.js | 231 ---- db/normalized/bank_accounts.js | 24 - db/normalized/credits.js | 236 ---- db/normalized/debits.js | 236 ---- db/normalized/deposits.js | 100 -- db/normalized/exchanges.js | 232 ---- db/normalized/invoices.js | 206 ---- db/normalized/liquid_assets.js | 21 - db/normalized/ln_keys.js | 16 - db/normalized/lnurl_codes.js | 14 - db/normalized/non_custodial_accounts.js | 91 -- db/normalized/orders.js | 29 - db/normalized/referrals.js | 20 - db/normalized/transactions.js | 41 - db/normalized/transfers.js | 243 ----- db/normalized/user_preferences.js | 20 - db/normalized/user_security.js | 22 - db/normalized/users.js | 18 - db/normalized/vouchers.js | 18 - db/normalized/waiting_list.js | 60 - db/normalized/withdrawals.js | 128 --- db/schema.sql | 479 -------- doc/PAYMENTS.md | 52 - doc/fund-server-bitcoin-cli.md | 44 - doc/img/50-bitcoin-server-balance.gif | Bin 7791 -> 0 bytes docker-compose.yml | 224 +--- fx.sample | 1 - git_hooks/pre-commit | 11 - index.js | 30 +- k8s/app-claim0-persistentvolumeclaim.yaml | 14 - k8s/app-deployment.yaml | 44 - k8s/app-service.yaml | 19 - k8s/bitcoin-claim0-persistentvolumeclaim.yaml | 14 - k8s/bitcoin-deployment.yaml | 42 - k8s/bitcoin-service.yaml | 22 - k8s/docker-compose-resolved.yaml | 118 -- ...lectrs-liquid-claim0-persistentvolumeclaim.yaml | 14 - k8s/electrs-liquid-deployment.yaml | 62 -- k8s/electrs-liquid-service.yaml | 22 - k8s/esplora-liquid-deployment.yaml | 40 - k8s/esplora-liquid-service.yaml | 19 - k8s/la-claim0-persistentvolumeclaim.yaml | 14 - k8s/la-deployment.yaml | 42 - k8s/lb-claim0-persistentvolumeclaim.yaml | 14 - k8s/lb-deployment.yaml | 42 - k8s/liquid-claim0-persistentvolumeclaim.yaml | 14 - k8s/liquid-deployment.yaml | 44 - k8s/liquid-service.yaml | 28 - k8s/maria-claim0-persistentvolumeclaim.yaml | 14 - k8s/maria-deployment.yaml | 44 - k8s/maria-service.yaml | 19 - k8s/ui-claim0-persistentvolumeclaim.yaml | 14 - k8s/ui-deployment.yaml | 44 - k8s/ui-service.yaml | 19 - lib/account.js | 49 - lib/buildliquidtx.js | 160 --- lib/buildtx.js | 90 -- lib/countries.js | 249 ----- lib/liquid.js | 3 - lib/lnd.js | 1 - lib/logo.js | 1 - lib/nostr.js | 194 +++- lib/passport.js | 18 +- lib/persist.js | 22 - lib/rates.js | 18 +- lib/read.js | 25 - lib/register.js | 56 +- lib/rpc.js | 24 - lib/send.js | 16 +- lib/sendInternal.js | 22 - lib/sockets.js | 103 +- lib/store.js | 5 +- lib/sync.js | 414 ------- lib/utils.js | 148 +-- lib/wretch.js | 4 - mine.sh | 8 - nodemon.json | 3 - package.json | 37 +- prod.yml | 96 -- reg.yml | 96 -- routes/admin.js | 566 ---------- routes/assets.js | 536 --------- routes/bitcoin/broadcast.js | 56 - routes/bitcoin/conversionFee.js | 12 - routes/bitcoin/fee.js | 73 -- routes/bitcoin/generate.js | 5 - routes/bitcoin/index.js | 13 - routes/bitcoin/receive.js | 281 ----- routes/bitcoin/send.js | 192 ---- routes/bitcoin/sweep.js | 87 -- routes/events.js | 69 -- routes/funding.js | 96 -- routes/invoices.js | 7 +- routes/lightning/query.js | 1 - routes/lightning/receive.js | 25 - routes/liquid/broadcast.js | 58 - routes/liquid/conversionFee.js | 12 - routes/liquid/fee.js | 89 -- routes/liquid/generate.js | 5 - routes/liquid/index.js | 11 - routes/liquid/receive.js | 406 ------- routes/liquid/send.js | 276 ----- routes/lnurl.js | 3 - routes/locations.js | 12 +- routes/payments.js | 402 +++---- routes/referrals.js | 272 ----- routes/requests.js | 2 - routes/swaps.js | 587 ---------- routes/tickets.js | 22 - routes/users.js | 331 +----- sampleconfig/certbot/.gitkeep | 1 - sampleconfig/index.js | 74 +- sampleconfig/liquid/elements.conf | 18 - sampleconfig/lnd/lnd.conf | 25 - sampleconfig/lnd/pwd | 1 - sampleconfig/lnurl.js | 36 - sampleconfig/nginx/default.conf.template | 38 - sampledata/conversions.json | 2 - sampledata/emails.json | 4 - sampledata/logins.json | 1 - sampledata/payments.json | 2 - sampledata/recipients.json | 2 - sampledata/withdrawals.json | 1 - scripts/apiParse.js | 46 - scripts/check.js | 24 - scripts/purge-except-git.sh | 31 - secret | 0 205 files changed, 449 insertions(+), 18314 deletions(-) delete mode 100644 Caddyfile delete mode 100644 apidoc/api_data.js delete mode 100644 apidoc/api_data.json delete mode 100644 apidoc/api_project.js delete mode 100644 apidoc/api_project.json delete mode 100644 apidoc/css/style.css delete mode 100644 apidoc/fonts/glyphicons-halflings-regular.eot delete mode 100644 apidoc/fonts/glyphicons-halflings-regular.svg delete mode 100644 apidoc/fonts/glyphicons-halflings-regular.ttf delete mode 100644 apidoc/fonts/glyphicons-halflings-regular.woff delete mode 100644 apidoc/fonts/glyphicons-halflings-regular.woff2 delete mode 100644 apidoc/img/android-chrome-192x192.png delete mode 100644 apidoc/img/android-chrome-512x512.png delete mode 100644 apidoc/img/apple-touch-icon.png delete mode 100644 apidoc/img/favicon-16x16.png delete mode 100644 apidoc/img/favicon-32x32.png delete mode 100644 apidoc/img/favicon.ico delete mode 100644 apidoc/index.html delete mode 100644 apidoc/locales/ca.js delete mode 100644 apidoc/locales/cs.js delete mode 100644 apidoc/locales/de.js delete mode 100644 apidoc/locales/es.js delete mode 100644 apidoc/locales/fr.js delete mode 100644 apidoc/locales/it.js delete mode 100644 apidoc/locales/locale.js delete mode 100644 apidoc/locales/nl.js delete mode 100644 apidoc/locales/pl.js delete mode 100644 apidoc/locales/pt_br.js delete mode 100644 apidoc/locales/ro.js delete mode 100644 apidoc/locales/ru.js delete mode 100644 apidoc/locales/tr.js delete mode 100644 apidoc/locales/vi.js delete mode 100644 apidoc/locales/zh.js delete mode 100644 apidoc/locales/zh_cn.js delete mode 100644 apidoc/main.js delete mode 100644 apidoc/utils/handlebars_helper.js delete mode 100644 apidoc/utils/send_sample_request.js delete mode 100644 apidoc/utils/send_sample_request_utils.js delete mode 100644 apidoc/vendor/bootstrap.min.css delete mode 100644 apidoc/vendor/bootstrap.min.js delete mode 100644 apidoc/vendor/diff_match_patch.min.js delete mode 100644 apidoc/vendor/handlebars.min.js delete mode 100644 apidoc/vendor/jquery.min.js delete mode 100644 apidoc/vendor/list.min.js delete mode 100644 apidoc/vendor/lodash.custom.min.js delete mode 100644 apidoc/vendor/path-to-regexp/LICENSE delete mode 100644 apidoc/vendor/path-to-regexp/index.js delete mode 100644 apidoc/vendor/polyfill.js delete mode 100644 apidoc/vendor/prism.css delete mode 100644 apidoc/vendor/prism.js delete mode 100644 apidoc/vendor/require.min.js delete mode 100644 apidoc/vendor/semver.min.js delete mode 100644 apidoc/vendor/webfontloader.js delete mode 100644 db/db.js delete mode 100644 db/index.js delete mode 100644 db/init/prodschema.sql delete mode 100644 db/migrations/20210612181508_init_referrals.js delete mode 100644 db/migrations/20210612192804_init_waiting_list.js.js delete mode 100644 db/migrations/20210612203333_schema_diff_merges.js delete mode 100644 db/migrations/20210625232223_add_invoice_id_to_payments.js delete mode 100644 db/migrations/20210628221727_add_webhook_to_invoices.js delete mode 100644 db/migrations/20210713174217_add_admin_to_users.js delete mode 100644 db/migrations/20210714123455_data_fixes_merge.js delete mode 100644 db/migrations/20210714123456_add_fks_merge.js delete mode 100644 db/migrations/s2/user_adjustments.js delete mode 100644 db/models/accounts.js delete mode 100644 db/models/codes.js delete mode 100644 db/models/deposits.js delete mode 100644 db/models/invoices.js delete mode 100644 db/models/keys.js delete mode 100644 db/models/orders.js delete mode 100644 db/models/payments.js delete mode 100644 db/models/referrals.js delete mode 100644 db/models/requests.js delete mode 100644 db/models/users.js delete mode 100644 db/models/waiting_list.js delete mode 100644 db/models/withdrawals.js delete mode 100644 db/normalized/README.md delete mode 100644 db/normalized/accounts.js delete mode 100644 db/normalized/bank_accounts.js delete mode 100644 db/normalized/credits.js delete mode 100644 db/normalized/debits.js delete mode 100644 db/normalized/deposits.js delete mode 100644 db/normalized/exchanges.js delete mode 100644 db/normalized/invoices.js delete mode 100644 db/normalized/liquid_assets.js delete mode 100644 db/normalized/ln_keys.js delete mode 100644 db/normalized/lnurl_codes.js delete mode 100644 db/normalized/non_custodial_accounts.js delete mode 100644 db/normalized/orders.js delete mode 100644 db/normalized/referrals.js delete mode 100644 db/normalized/transactions.js delete mode 100644 db/normalized/transfers.js delete mode 100644 db/normalized/user_preferences.js delete mode 100644 db/normalized/user_security.js delete mode 100644 db/normalized/users.js delete mode 100644 db/normalized/vouchers.js delete mode 100644 db/normalized/waiting_list.js delete mode 100644 db/normalized/withdrawals.js delete mode 100644 db/schema.sql delete mode 100644 doc/PAYMENTS.md delete mode 100644 doc/fund-server-bitcoin-cli.md delete mode 100644 doc/img/50-bitcoin-server-balance.gif delete mode 100644 fx.sample delete mode 100644 git_hooks/pre-commit delete mode 100644 k8s/app-claim0-persistentvolumeclaim.yaml delete mode 100644 k8s/app-deployment.yaml delete mode 100644 k8s/app-service.yaml delete mode 100644 k8s/bitcoin-claim0-persistentvolumeclaim.yaml delete mode 100644 k8s/bitcoin-deployment.yaml delete mode 100644 k8s/bitcoin-service.yaml delete mode 100644 k8s/docker-compose-resolved.yaml delete mode 100644 k8s/electrs-liquid-claim0-persistentvolumeclaim.yaml delete mode 100644 k8s/electrs-liquid-deployment.yaml delete mode 100644 k8s/electrs-liquid-service.yaml delete mode 100644 k8s/esplora-liquid-deployment.yaml delete mode 100644 k8s/esplora-liquid-service.yaml delete mode 100644 k8s/la-claim0-persistentvolumeclaim.yaml delete mode 100644 k8s/la-deployment.yaml delete mode 100644 k8s/lb-claim0-persistentvolumeclaim.yaml delete mode 100644 k8s/lb-deployment.yaml delete mode 100644 k8s/liquid-claim0-persistentvolumeclaim.yaml delete mode 100644 k8s/liquid-deployment.yaml delete mode 100644 k8s/liquid-service.yaml delete mode 100644 k8s/maria-claim0-persistentvolumeclaim.yaml delete mode 100644 k8s/maria-deployment.yaml delete mode 100644 k8s/maria-service.yaml delete mode 100644 k8s/ui-claim0-persistentvolumeclaim.yaml delete mode 100644 k8s/ui-deployment.yaml delete mode 100644 k8s/ui-service.yaml delete mode 100644 lib/account.js delete mode 100644 lib/buildliquidtx.js delete mode 100644 lib/buildtx.js delete mode 100644 lib/countries.js delete mode 100644 lib/liquid.js delete mode 100644 lib/lnd.js delete mode 100644 lib/logo.js delete mode 100644 lib/persist.js delete mode 100644 lib/read.js delete mode 100644 lib/rpc.js delete mode 100644 lib/sync.js delete mode 100644 lib/wretch.js delete mode 100755 mine.sh delete mode 100644 nodemon.json delete mode 100644 prod.yml delete mode 100644 reg.yml delete mode 100644 routes/admin.js delete mode 100644 routes/assets.js delete mode 100644 routes/bitcoin/broadcast.js delete mode 100644 routes/bitcoin/conversionFee.js delete mode 100644 routes/bitcoin/fee.js delete mode 100644 routes/bitcoin/generate.js delete mode 100644 routes/bitcoin/index.js delete mode 100644 routes/bitcoin/receive.js delete mode 100644 routes/bitcoin/send.js delete mode 100644 routes/bitcoin/sweep.js delete mode 100644 routes/funding.js delete mode 100644 routes/liquid/broadcast.js delete mode 100644 routes/liquid/conversionFee.js delete mode 100644 routes/liquid/fee.js delete mode 100644 routes/liquid/generate.js delete mode 100644 routes/liquid/index.js delete mode 100644 routes/liquid/receive.js delete mode 100644 routes/liquid/send.js delete mode 100644 routes/referrals.js delete mode 100644 routes/swaps.js delete mode 100644 routes/tickets.js delete mode 100644 sampleconfig/certbot/.gitkeep delete mode 100644 sampleconfig/liquid/elements.conf delete mode 100644 sampleconfig/lnd/lnd.conf delete mode 100644 sampleconfig/lnd/pwd delete mode 100644 sampleconfig/lnurl.js delete mode 100644 sampleconfig/nginx/default.conf.template delete mode 100644 sampledata/conversions.json delete mode 100644 sampledata/emails.json delete mode 100644 sampledata/logins.json delete mode 100644 sampledata/payments.json delete mode 100644 sampledata/recipients.json delete mode 100644 sampledata/withdrawals.json delete mode 100644 scripts/apiParse.js delete mode 100644 scripts/check.js delete mode 100755 scripts/purge-except-git.sh delete mode 100644 secret (limited to 'lib') diff --git a/Caddyfile b/Caddyfile deleted file mode 100644 index 3e41ca11..00000000 --- a/Caddyfile +++ /dev/null @@ -1,21 +0,0 @@ -:80 { - handle_path /api* { - reverse_proxy app:3119 - } - - handle_path /.well-known* { - reverse_proxy app:3119 - } - - handle /lnurl* { - reverse_proxy app:3118 - } - - handle /ws* { - reverse_proxy app:3119 - } - - handle { - reverse_proxy ui:3000 - } -} diff --git a/apidoc/api_data.js b/apidoc/api_data.js deleted file mode 100644 index 44a2a9c4..00000000 --- a/apidoc/api_data.js +++ /dev/null @@ -1,762 +0,0 @@ -define({ - api: [ - { - type: "get", - url: "/accounts", - title: "Retrieve list of accounts", - name: "accounts", - group: "Admin", - permission: [ - { - name: "admin" - } - ], - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - optional: false, - field: "returns", - description: "

list of accounts

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - 'HTTP/1.1 200 OK\n{\n "accounts" : [\n { "user": \'Adam\', "account_id": \'123\', balance: 500 ... },\n ...\n ]\n}', - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/admin.js", - groupTitle: "Admin" - }, - { - type: "get", - url: "/deposits", - title: "Retrieve summary of deposits", - name: "deposits", - group: "Admin", - permission: [ - { - name: "admin" - } - ], - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - optional: false, - field: "returns", - description: "

list of deposits

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - 'HTTP/1.1 200 OK\n{\n "deposits" : [\n { "seller": \'Adam\', "buyer": \'bob\' ... },\n ...\n ]\n}', - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/admin.js", - groupTitle: "Admin" - }, - { - type: "get", - url: "/users", - title: "Retrieve user list", - name: "getUsers", - group: "Admin", - permission: [ - { - name: "admin" - } - ], - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - type: "Object", - optional: false, - field: "users", - description: "

List of users

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - 'HTTP/1.1 200 OK\n{\n "users" : [\n { "username": \'John Doe\', "email": \'johndoe@gmail.com\', ...},\n { "username": \'Jane Doe\', "email": \'janedoe@gmail.com\', ...}\n ]\n}', - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/admin.js", - groupTitle: "Admin" - }, - { - type: "get", - url: "/invoices", - title: "Retrieve summary of invoices", - name: "invoices", - group: "Admin", - permission: [ - { - name: "admin" - } - ], - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - optional: false, - field: "returns", - description: "

list of invoices

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - 'HTTP/1.1 200 OK\n{\n "invoices" : [\n { "seller": \'Adam\', "buyer": \'bob\' ... },\n ...\n ]\n}', - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/admin.js", - groupTitle: "Admin" - }, - { - type: "get", - url: "/orders", - title: "Retrieve summary of orders", - name: "orders", - group: "Admin", - permission: [ - { - name: "admin" - } - ], - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - optional: false, - field: "returns", - description: "

list of orders

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - 'HTTP/1.1 200 OK\n{\n "orders" : [\n { "seller": \'Adam\', "buyer": \'bob\' ... },\n ...\n ]\n}', - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/admin.js", - groupTitle: "Admin" - }, - { - type: "get", - url: "/payments", - title: "Retrieve summary of payments", - name: "payments", - group: "Admin", - permission: [ - { - name: "admin" - } - ], - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - optional: false, - field: "returns", - description: "

list of payments

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - 'HTTP/1.1 200 OK\n{\n "payments" : [\n { "seller": \'Adam\', "buyer": \'bob\' ... },\n ...\n ]\n}', - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/admin.js", - groupTitle: "Admin" - }, - { - type: "get", - url: "/referrals", - title: "Retrieve list of referral tokens", - name: "referrals", - group: "Admin", - permission: [ - { - name: "admin" - } - ], - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - optional: false, - field: "returns", - description: "

list of referral tokens

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - "HTTP/1.1 200 OK\n{\n \"referrals\" : [\n { \"token\": '***', \"status\": 'pending', sponsor: 'Adam', user: 'newUsername1', expiry: null },\n { \"token\": '***', \"status\": 'pending', sponsor: 'Adam', user: 'newUsername2', expiry: null },\n ]\n}", - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/admin.js", - groupTitle: "Admin" - }, - { - type: "get", - url: "/referrals", - title: "Retrieve list of referral tokens", - name: "referrals", - group: "Admin", - permission: [ - { - name: "admin" - } - ], - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - optional: false, - field: "returns", - description: "

list of referral tokens

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - "HTTP/1.1 200 OK\n{\n \"referrals\" : [\n { \"token\": '***', \"status\": 'pending', sponsor: 'Adam', user: 'newUsername1', expiry: null },\n { \"token\": '***', \"status\": 'pending', sponsor: 'Adam', user: 'newUsername2', expiry: null },\n ]\n}", - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/admin.js", - groupTitle: "Admin" - }, - { - type: "get", - url: "/user_kyc", - title: "Retrieve user kyc details", - name: "user_kyc", - group: "Admin", - permission: [ - { - name: "admin" - } - ], - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - optional: false, - field: "returns", - description: "

list of kyc details for each user

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - 'HTTP/1.1 200 OK\n{\n "users" : [\n { "username": \'Adam\', "email": \'adam@gmail.com\', ... kyc_data... },\n ...\n ]\n}', - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/admin.js", - groupTitle: "Admin" - }, - { - type: "get", - url: "/transactions", - title: "Retrieve summary of user transactions", - name: "user_transactions", - group: "Admin", - permission: [ - { - name: "admin" - } - ], - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - optional: false, - field: "returns", - description: "

list of invoices, payments, orders for users

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - 'HTTP/1.1 200 OK\n{\n "transactions" : [\n { "username": \'Adam\', "email": \'adam@gmail.com\', invoices: 1, orders: 0, payments: 2 },\n ...\n ]\n}', - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/admin.js", - groupTitle: "Admin" - }, - { - type: "get", - url: "/withdrawals", - title: "Retrieve summary of withdrawals", - name: "withdrawals", - group: "Admin", - permission: [ - { - name: "admin" - } - ], - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - optional: false, - field: "returns", - description: "

list of withdrawals

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - 'HTTP/1.1 200 OK\n{\n "withdrawals" : [\n { "seller": \'Adam\', "buyer": \'bob\' ... },\n ...\n ]\n}', - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/admin.js", - groupTitle: "Admin" - }, - { - type: "get", - url: "/info", - title: "Request server information", - name: "GetInfo", - group: "Info", - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - type: "Object", - optional: false, - field: "fx", - description: - "

Exchange rates relative to USD - fx.CUR is the value of one CUR in USD.

" - }, - { - group: "Success 200", - type: "String[]", - optional: false, - field: "networks", - description: - "

Array of supported networks; possible values are "bitcoin", "liquid" and "lightning".

" - }, - { - group: "Success 200", - type: "String", - optional: false, - field: "clientVersion", - description: "

the current git commit of the ui

" - } - ] - } - }, - version: "0.0.0", - filename: "routes/info.js", - groupTitle: "Info" - }, - { - type: "post", - url: "/grant", - title: "Grant Referral code", - name: "Grant", - group: "Referrals", - permission: [ - { - name: "member" - } - ], - parameter: { - fields: { - Parameter: [ - { - group: "Parameter", - type: "sponsor_id", - optional: false, - field: "user_id", - description: "

of member generating referral code.

" - }, - { - group: "Parameter", - type: "expiry", - optional: false, - field: "optional", - description: "

expiry date for token.

" - } - ] - } - }, - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - type: "String", - optional: false, - field: "token", - description: "

referral token

" - }, - { - group: "Success 200", - type: "Date", - optional: false, - field: "expiry", - description: "

optional expiry

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - 'HTTP/1.1 200 OK\n{\n "token": "*********", \n "status": "available", \n "expiry": null\n}', - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/referrals.js", - groupTitle: "Referrals" - }, - { - type: "get", - url: "/grant", - title: "Grant Referral code (variation using get)", - name: "Grant", - group: "Referrals", - permission: [ - { - name: "member" - }, - { - name: "member" - } - ], - parameter: { - fields: { - Parameter: [ - { - group: "Parameter", - type: "sponsor_id", - optional: false, - field: "user_id", - description: "

of member generating referral code.

" - }, - { - group: "Parameter", - type: "expiry", - optional: false, - field: "optional", - description: "

expiry date for token.

" - } - ] - } - }, - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - type: "String", - optional: false, - field: "token", - description: "

referral token

" - }, - { - group: "Success 200", - type: "Status", - optional: false, - field: "status", - description: "

referral status

" - }, - { - group: "Success 200", - type: "Date", - optional: false, - field: "expiry", - description: "

optional expiry

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - 'HTTP/1.1 200 OK\n{\n "token": "*********", \n "status": "available", \n "expiry": null\n}', - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/referrals.js", - groupTitle: "Referrals" - }, - { - type: "get", - url: "/checkTokens/:sponsor_id", - title: "Retrieve list of referral tokens generated by this user", - name: "checkTokens", - group: "Referrals", - permission: [ - { - name: "member" - } - ], - parameter: { - fields: { - Parameter: [ - { - group: "Parameter", - type: "sponsor_id", - optional: false, - field: "user_id", - description: "

of member generating referral code.

" - }, - { - group: "Parameter", - type: "status", - optional: false, - field: "optional", - description: "

token status.

" - } - ] - } - }, - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - optional: false, - field: "tokens", - description: "

array of token objects

" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - 'HTTP/1.1 200 OK\n{\n "tokens": [\n {"token": "*********", "status": "available", "expiry": null}\n ]\n}', - type: "json" - } - ] - }, - version: "0.0.0", - filename: "routes/referrals.js", - groupTitle: "Referrals" - }, - { - type: "get", - url: "/isReferred/:user_id", - title: "Check if user is referred", - name: "isReferred", - group: "Referrals", - parameter: { - fields: { - Parameter: [ - { - group: "Parameter", - type: "user_id", - optional: false, - field: "user", - description: "

id

" - } - ] - } - }, - success: { - examples: [ - { - title: "Success-Response:", - content: "HTTP/1.1 200 OK\n{\n true\n}", - type: "json" - } - ] - }, - description: - "

returns boolean indicating if user is referred or not

", - version: "0.0.0", - filename: "routes/referrals.js", - groupTitle: "Referrals" - }, - { - type: "post", - url: "/joinQueue", - title: "Join waiting list (track email & phone)", - name: "joinQueue", - group: "Referrals", - parameter: { - fields: { - Parameter: [ - { - group: "Parameter", - type: "email", - optional: false, - field: "email", - description: "

address

" - }, - { - group: "Parameter", - type: "phone", - optional: false, - field: "phone", - description: "

number

" - } - ] - } - }, - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - optional: false, - field: "verified", - defaultValue: "true", - description: "" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: - 'HTTP/1.1 200 OK\n{\n "success": true\n "message": \'Added to waiting list\'\n}', - type: "json" - } - ] - }, - description: "

referral token is updated with existing user_id

", - version: "0.0.0", - filename: "routes/referrals.js", - groupTitle: "Referrals" - }, - { - type: "get", - url: "/verify/:user_id", - title: "Verify token and apply to existing user", - name: "verify", - group: "Referrals", - permission: [ - { - name: "member" - } - ], - parameter: { - fields: { - Parameter: [ - { - group: "Parameter", - type: "user_id", - optional: false, - field: "Current", - description: "

user

" - }, - { - group: "Parameter", - type: "token", - optional: false, - field: "Token", - description: "

to validate

" - } - ] - } - }, - success: { - fields: { - "Success 200": [ - { - group: "Success 200", - optional: false, - field: "verified", - defaultValue: "true", - description: "" - } - ] - }, - examples: [ - { - title: "Success-Response:", - content: 'HTTP/1.1 200 OK\n{\n "verified": true\n}', - type: "json" - } - ] - }, - description: "

referral token is updated with existing user_id

", - version: "0.0.0", - filename: "routes/referrals.js", - groupTitle: "Referrals" - } - ] -}); diff --git a/apidoc/api_data.json b/apidoc/api_data.json deleted file mode 100644 index 5bab3ee7..00000000 --- a/apidoc/api_data.json +++ /dev/null @@ -1,742 +0,0 @@ -[ - { - "type": "get", - "url": "/accounts", - "title": "Retrieve list of accounts", - "name": "accounts", - "group": "Admin", - "permission": [ - { - "name": "admin" - } - ], - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "optional": false, - "field": "returns", - "description": "

list of accounts

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"accounts\" : [\n { \"user\": 'Adam', \"account_id\": '123', balance: 500 ... },\n ...\n ]\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/admin.js", - "groupTitle": "Admin" - }, - { - "type": "get", - "url": "/deposits", - "title": "Retrieve summary of deposits", - "name": "deposits", - "group": "Admin", - "permission": [ - { - "name": "admin" - } - ], - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "optional": false, - "field": "returns", - "description": "

list of deposits

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"deposits\" : [\n { \"seller\": 'Adam', \"buyer\": 'bob' ... },\n ...\n ]\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/admin.js", - "groupTitle": "Admin" - }, - { - "type": "get", - "url": "/users", - "title": "Retrieve user list", - "name": "getUsers", - "group": "Admin", - "permission": [ - { - "name": "admin" - } - ], - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "type": "Object", - "optional": false, - "field": "users", - "description": "

List of users

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"users\" : [\n { \"username\": 'John Doe', \"email\": 'johndoe@gmail.com', ...},\n { \"username\": 'Jane Doe', \"email\": 'janedoe@gmail.com', ...}\n ]\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/admin.js", - "groupTitle": "Admin" - }, - { - "type": "get", - "url": "/invoices", - "title": "Retrieve summary of invoices", - "name": "invoices", - "group": "Admin", - "permission": [ - { - "name": "admin" - } - ], - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "optional": false, - "field": "returns", - "description": "

list of invoices

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"invoices\" : [\n { \"seller\": 'Adam', \"buyer\": 'bob' ... },\n ...\n ]\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/admin.js", - "groupTitle": "Admin" - }, - { - "type": "get", - "url": "/orders", - "title": "Retrieve summary of orders", - "name": "orders", - "group": "Admin", - "permission": [ - { - "name": "admin" - } - ], - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "optional": false, - "field": "returns", - "description": "

list of orders

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"orders\" : [\n { \"seller\": 'Adam', \"buyer\": 'bob' ... },\n ...\n ]\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/admin.js", - "groupTitle": "Admin" - }, - { - "type": "get", - "url": "/payments", - "title": "Retrieve summary of payments", - "name": "payments", - "group": "Admin", - "permission": [ - { - "name": "admin" - } - ], - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "optional": false, - "field": "returns", - "description": "

list of payments

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"payments\" : [\n { \"seller\": 'Adam', \"buyer\": 'bob' ... },\n ...\n ]\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/admin.js", - "groupTitle": "Admin" - }, - { - "type": "get", - "url": "/referrals", - "title": "Retrieve list of referral tokens", - "name": "referrals", - "group": "Admin", - "permission": [ - { - "name": "admin" - } - ], - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "optional": false, - "field": "returns", - "description": "

list of referral tokens

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"referrals\" : [\n { \"token\": '***', \"status\": 'pending', sponsor: 'Adam', user: 'newUsername1', expiry: null },\n { \"token\": '***', \"status\": 'pending', sponsor: 'Adam', user: 'newUsername2', expiry: null },\n ]\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/admin.js", - "groupTitle": "Admin" - }, - { - "type": "get", - "url": "/referrals", - "title": "Retrieve list of referral tokens", - "name": "referrals", - "group": "Admin", - "permission": [ - { - "name": "admin" - } - ], - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "optional": false, - "field": "returns", - "description": "

list of referral tokens

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"referrals\" : [\n { \"token\": '***', \"status\": 'pending', sponsor: 'Adam', user: 'newUsername1', expiry: null },\n { \"token\": '***', \"status\": 'pending', sponsor: 'Adam', user: 'newUsername2', expiry: null },\n ]\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/admin.js", - "groupTitle": "Admin" - }, - { - "type": "get", - "url": "/user_kyc", - "title": "Retrieve user kyc details", - "name": "user_kyc", - "group": "Admin", - "permission": [ - { - "name": "admin" - } - ], - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "optional": false, - "field": "returns", - "description": "

list of kyc details for each user

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"users\" : [\n { \"username\": 'Adam', \"email\": 'adam@gmail.com', ... kyc_data... },\n ...\n ]\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/admin.js", - "groupTitle": "Admin" - }, - { - "type": "get", - "url": "/transactions", - "title": "Retrieve summary of user transactions", - "name": "user_transactions", - "group": "Admin", - "permission": [ - { - "name": "admin" - } - ], - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "optional": false, - "field": "returns", - "description": "

list of invoices, payments, orders for users

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"transactions\" : [\n { \"username\": 'Adam', \"email\": 'adam@gmail.com', invoices: 1, orders: 0, payments: 2 },\n ...\n ]\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/admin.js", - "groupTitle": "Admin" - }, - { - "type": "get", - "url": "/withdrawals", - "title": "Retrieve summary of withdrawals", - "name": "withdrawals", - "group": "Admin", - "permission": [ - { - "name": "admin" - } - ], - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "optional": false, - "field": "returns", - "description": "

list of withdrawals

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"withdrawals\" : [\n { \"seller\": 'Adam', \"buyer\": 'bob' ... },\n ...\n ]\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/admin.js", - "groupTitle": "Admin" - }, - { - "type": "get", - "url": "/info", - "title": "Request server information", - "name": "GetInfo", - "group": "Info", - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "type": "Object", - "optional": false, - "field": "fx", - "description": "

Exchange rates relative to USD - fx.CUR is the value of one CUR in USD.

" - }, - { - "group": "Success 200", - "type": "String[]", - "optional": false, - "field": "networks", - "description": "

Array of supported networks; possible values are "bitcoin", "liquid" and "lightning".

" - }, - { - "group": "Success 200", - "type": "String", - "optional": false, - "field": "clientVersion", - "description": "

the current git commit of the ui

" - } - ] - } - }, - "version": "0.0.0", - "filename": "routes/info.js", - "groupTitle": "Info" - }, - { - "type": "post", - "url": "/grant", - "title": "Grant Referral code", - "name": "Grant", - "group": "Referrals", - "permission": [ - { - "name": "member" - } - ], - "parameter": { - "fields": { - "Parameter": [ - { - "group": "Parameter", - "type": "sponsor_id", - "optional": false, - "field": "user_id", - "description": "

of member generating referral code.

" - }, - { - "group": "Parameter", - "type": "expiry", - "optional": false, - "field": "optional", - "description": "

expiry date for token.

" - } - ] - } - }, - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "type": "String", - "optional": false, - "field": "token", - "description": "

referral token

" - }, - { - "group": "Success 200", - "type": "Date", - "optional": false, - "field": "expiry", - "description": "

optional expiry

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"token\": \"*********\", \n \"status\": \"available\", \n \"expiry\": null\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/referrals.js", - "groupTitle": "Referrals" - }, - { - "type": "get", - "url": "/grant", - "title": "Grant Referral code (variation using get)", - "name": "Grant", - "group": "Referrals", - "permission": [ - { - "name": "member" - }, - { - "name": "member" - } - ], - "parameter": { - "fields": { - "Parameter": [ - { - "group": "Parameter", - "type": "sponsor_id", - "optional": false, - "field": "user_id", - "description": "

of member generating referral code.

" - }, - { - "group": "Parameter", - "type": "expiry", - "optional": false, - "field": "optional", - "description": "

expiry date for token.

" - } - ] - } - }, - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "type": "String", - "optional": false, - "field": "token", - "description": "

referral token

" - }, - { - "group": "Success 200", - "type": "Status", - "optional": false, - "field": "status", - "description": "

referral status

" - }, - { - "group": "Success 200", - "type": "Date", - "optional": false, - "field": "expiry", - "description": "

optional expiry

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"token\": \"*********\", \n \"status\": \"available\", \n \"expiry\": null\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/referrals.js", - "groupTitle": "Referrals" - }, - { - "type": "get", - "url": "/checkTokens/:sponsor_id", - "title": "Retrieve list of referral tokens generated by this user", - "name": "checkTokens", - "group": "Referrals", - "permission": [ - { - "name": "member" - } - ], - "parameter": { - "fields": { - "Parameter": [ - { - "group": "Parameter", - "type": "sponsor_id", - "optional": false, - "field": "user_id", - "description": "

of member generating referral code.

" - }, - { - "group": "Parameter", - "type": "status", - "optional": false, - "field": "optional", - "description": "

token status.

" - } - ] - } - }, - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "optional": false, - "field": "tokens", - "description": "

array of token objects

" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"tokens\": [\n {\"token\": \"*********\", \"status\": \"available\", \"expiry\": null}\n ]\n}", - "type": "json" - } - ] - }, - "version": "0.0.0", - "filename": "routes/referrals.js", - "groupTitle": "Referrals" - }, - { - "type": "get", - "url": "/isReferred/:user_id", - "title": "Check if user is referred", - "name": "isReferred", - "group": "Referrals", - "parameter": { - "fields": { - "Parameter": [ - { - "group": "Parameter", - "type": "user_id", - "optional": false, - "field": "user", - "description": "

id

" - } - ] - } - }, - "success": { - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n true\n}", - "type": "json" - } - ] - }, - "description": "

returns boolean indicating if user is referred or not

", - "version": "0.0.0", - "filename": "routes/referrals.js", - "groupTitle": "Referrals" - }, - { - "type": "post", - "url": "/joinQueue", - "title": "Join waiting list (track email & phone)", - "name": "joinQueue", - "group": "Referrals", - "parameter": { - "fields": { - "Parameter": [ - { - "group": "Parameter", - "type": "email", - "optional": false, - "field": "email", - "description": "

address

" - }, - { - "group": "Parameter", - "type": "phone", - "optional": false, - "field": "phone", - "description": "

number

" - } - ] - } - }, - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "optional": false, - "field": "verified", - "defaultValue": "true", - "description": "" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"success\": true\n \"message\": 'Added to waiting list'\n}", - "type": "json" - } - ] - }, - "description": "

referral token is updated with existing user_id

", - "version": "0.0.0", - "filename": "routes/referrals.js", - "groupTitle": "Referrals" - }, - { - "type": "get", - "url": "/verify/:user_id", - "title": "Verify token and apply to existing user", - "name": "verify", - "group": "Referrals", - "permission": [ - { - "name": "member" - } - ], - "parameter": { - "fields": { - "Parameter": [ - { - "group": "Parameter", - "type": "user_id", - "optional": false, - "field": "Current", - "description": "

user

" - }, - { - "group": "Parameter", - "type": "token", - "optional": false, - "field": "Token", - "description": "

to validate

" - } - ] - } - }, - "success": { - "fields": { - "Success 200": [ - { - "group": "Success 200", - "optional": false, - "field": "verified", - "defaultValue": "true", - "description": "" - } - ] - }, - "examples": [ - { - "title": "Success-Response:", - "content": "HTTP/1.1 200 OK\n{\n \"verified\": true\n}", - "type": "json" - } - ] - }, - "description": "

referral token is updated with existing user_id

", - "version": "0.0.0", - "filename": "routes/referrals.js", - "groupTitle": "Referrals" - } -] diff --git a/apidoc/api_project.js b/apidoc/api_project.js deleted file mode 100644 index 3c0eb6aa..00000000 --- a/apidoc/api_project.js +++ /dev/null @@ -1,16 +0,0 @@ -define({ - title: "CoinOS Server API Documentation", - url: "https://coinos.io/api", - name: "coinos", - version: "0.0.1", - description: "", - sampleUrl: false, - defaultVersion: "0.0.0", - apidoc: "0.3.0", - generator: { - name: "apidoc", - time: "2021-08-18T19:13:24.486Z", - url: "https://apidocjs.com", - version: "0.29.0" - } -}); diff --git a/apidoc/api_project.json b/apidoc/api_project.json deleted file mode 100644 index 42943876..00000000 --- a/apidoc/api_project.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "title": "CoinOS Server API Documentation", - "url": "https://coinos.io/api", - "name": "coinos", - "version": "0.0.1", - "description": "", - "sampleUrl": false, - "defaultVersion": "0.0.0", - "apidoc": "0.3.0", - "generator": { - "name": "apidoc", - "time": "2021-08-18T19:13:24.486Z", - "url": "https://apidocjs.com", - "version": "0.29.0" - } -} diff --git a/apidoc/css/style.css b/apidoc/css/style.css deleted file mode 100644 index 9f3c88ac..00000000 --- a/apidoc/css/style.css +++ /dev/null @@ -1,525 +0,0 @@ -/* ------------------------------------------------------------------------------------------ - * Content - * ------------------------------------------------------------------------------------------ */ -body { - max-width: 1280px; -} - -body, p, a, div, th, td { - font-family: "Source Sans Pro", sans-serif; - font-weight: 400; - font-size: 16px; -} - -@media (min-width: 1200px) { - body.container-fluid { - padding-right: 0px; - padding-left: 0px; - margin-right: 0px; - margin-left: 0px; - } -} - -td.code { - font-size: 14px; - font-family: "Source Code Pro", monospace; - font-style: normal; - font-weight: 400; -} - -#content { - padding-top: 16px; - z-Index: -1; - margin-left: 270px; -} - -p { - color: #808080; -} - -h1 { - font-family: "Source Sans Pro Semibold", sans-serif; - font-weight: normal; - font-size: 44px; - line-height: 50px; - margin: 0 0 10px 0; - padding: 0; -} - -h2 { - font-family: "Source Sans Pro", sans-serif; - font-weight: normal; - font-size: 24px; - line-height: 40px; - margin: 0 0 20px 0; - padding: 0; -} - -section { - border-top: 1px solid #ebebeb; - padding: 30px 0; -} - -section h1 { - font-family: "Source Sans Pro", sans-serif; - font-weight: 700; - font-size: 32px; - line-height: 40px; - margin: 0 0 20px 0; - padding: 0; -} - -article { - padding: 14px 0 30px 0; -} - -article h1 { - font-family: "Source Sans Pro Bold", sans-serif; - font-weight: 600; - font-size: 24px; - line-height: 26px; -} - -article h2 { - font-family: "Source Sans Pro", sans-serif; - font-weight: 600; - font-size: 18px; - line-height: 24px; - margin: 0 0 10px 0; -} - -article h3 { - font-family: "Source Sans Pro", sans-serif; - font-weight: 600; - font-size: 16px; - line-height: 18px; - margin: 0 0 10px 0; -} - -article h4 { - font-family: "Source Sans Pro", sans-serif; - font-weight: 600; - font-size: 14px; - line-height: 16px; - margin: 0 0 8px 0; -} - -table { - border-collapse: collapse; - width: 100%; - margin: 0 0 20px 0; -} - -th { - background-color: #f5f5f5; - text-align: left; - font-family: "Source Sans Pro", sans-serif; - font-weight: 700; - padding: 4px 8px; - border: #e0e0e0 1px solid; -} - -td { - vertical-align: top; - padding: 10px 8px 0 8px; - border: #e0e0e0 1px solid; -} - -#generator .content { - color: #b0b0b0; - border-top: 1px solid #ebebeb; - padding: 10px 0; -} - -.label-optional { - float: right; - background-color: grey; - margin-top: 4px; - user-select: none; -} - -.open-left { - right: 0; - left: auto; -} - -/* ------------------------------------------------------------------------------------------ - * apidoc - intro - * ------------------------------------------------------------------------------------------ */ - -#apidoc .apidoc { - border-top: 1px solid #ebebeb; - padding: 30px 0; -} - -#apidoc h1 { - font-family: "Source Sans Pro", sans-serif; - font-weight: 700; - font-size: 32px; - line-height: 40px; - margin: 0 0 20px 0; - padding: 0; -} - -#apidoc h2 { - font-family: "Source Sans Pro Bold", sans-serif; - font-weight: 600; - font-size: 22px; - line-height: 26px; - padding-top: 14px; -} - -/* ------------------------------------------------------------------------------------------ - * Request type - * ------------------------------------------------------------------------------------------ */ -.type { - font-family: "Source Sans Pro", sans-serif; - font-weight: 600; - font-size: 15px; - display: inline-block; - margin: 0 0 5px 0; - padding: 4px 5px; - border-radius: 6px; - text-transform: uppercase; - background-color: #3387CC; - color: #ffffff; -} - -.type__get { - background-color: green; -} - -.type__put { - background-color: #e5c500; -} - -.type__post { - background-color: #4070ec; -} - -.type__delete { - background-color: #ed0039; -} - -/* ------------------------------------------------------------------------------------------ - * Sidenav - * ------------------------------------------------------------------------------------------ */ -.sidenav { - width: 228px; - margin: 0; - padding: 0 20px 20px 20px; - position: fixed; - top: 50px; - left: 0; - bottom: 0; - overflow-x: hidden; - overflow-y: auto; - background-color: #f5f5f5; - z-index: 10; -} - -.sidenav > li > a { - display: block; - width: 192px; - margin: 0; - padding: 2px 11px; - border: 0; - border-left: transparent 4px solid; - border-right: transparent 4px solid; - font-family: "Source Sans Pro", sans-serif; - font-weight: 400; - font-size: 14px; -} - -.sidenav > li.nav-header { - margin-top: 8px; - margin-bottom: 8px; -} - -.sidenav > li.nav-header > a { - padding: 5px 15px; - border: 1px solid #e5e5e5; - width: 190px; - font-family: "Source Sans Pro", sans-serif; - font-weight: 700; - font-size: 16px; - background-color: #ffffff; -} - -.sidenav > li.active > a { - position: relative; - z-index: 2; - background-color: #0088cc; - color: #ffffff; -} - -.sidenav > li.has-modifications a { - border-right: #60d060 4px solid; -} - -.sidenav > li.is-new a { - border-left: #e5e5e5 4px solid; -} - -/* ------------------------------------------------------------------------------------------ - * Side nav search - * ------------------------------------------------------------------------------------------ */ -.sidenav-search { - width: 228px; - left: 0px; - position: fixed; - padding: 16px 20px 10px 20px; - background-color: #F5F5F5; - z-index: 11; -} - -.sidenav-search .search { - height: 26px; -} - -.search-reset { - position: absolute; - display: block; - cursor: pointer; - width: 20px; - height: 20px; - text-align: center; - right: 28px; - top: 17px; - background-color: #fff; -} - -/* ------------------------------------------------------------------------------------------ - * Compare - * ------------------------------------------------------------------------------------------ */ - -ins { - background: #60d060; - text-decoration: none; - color: #000000; -} - -del { - background: #f05050; - color: #000000; -} - -.label-ins { - background-color: #60d060; -} - -.label-del { - background-color: #f05050; - text-decoration: line-through; -} - -pre.ins { - background-color: #60d060; -} - -pre.del { - background-color: #f05050; - text-decoration: line-through; -} - -table.ins th, -table.ins td { - background-color: #60d060; -} - -table.del th, -table.del td { - background-color: #f05050; - text-decoration: line-through; -} - -tr.ins td { - background-color: #60d060; -} - -tr.del td { - background-color: #f05050; - text-decoration: line-through; -} - -/* ------------------------------------------------------------------------------------------ - * Spinner - * ------------------------------------------------------------------------------------------ */ - -#loader { - position: absolute; - width: 100%; -} - -#loader p { - padding-top: 80px; - margin-left: -4px; -} - -.spinner { - margin: 200px auto; - width: 60px; - height: 60px; - position: relative; -} - -.container1 > div, .container2 > div, .container3 > div { - width: 14px; - height: 14px; - background-color: #0088cc; - - border-radius: 100%; - position: absolute; - -webkit-animation: bouncedelay 1.2s infinite ease-in-out; - animation: bouncedelay 1.2s infinite ease-in-out; - /* Prevent first frame from flickering when animation starts */ - -webkit-animation-fill-mode: both; - animation-fill-mode: both; -} - -.spinner .spinner-container { - position: absolute; - width: 100%; - height: 100%; -} - -.container2 { - -webkit-transform: rotateZ(45deg); - transform: rotateZ(45deg); -} - -.container3 { - -webkit-transform: rotateZ(90deg); - transform: rotateZ(90deg); -} - -.circle1 { top: 0; left: 0; } -.circle2 { top: 0; right: 0; } -.circle3 { right: 0; bottom: 0; } -.circle4 { left: 0; bottom: 0; } - -.container2 .circle1 { - -webkit-animation-delay: -1.1s; - animation-delay: -1.1s; -} - -.container3 .circle1 { - -webkit-animation-delay: -1.0s; - animation-delay: -1.0s; -} - -.container1 .circle2 { - -webkit-animation-delay: -0.9s; - animation-delay: -0.9s; -} - -.container2 .circle2 { - -webkit-animation-delay: -0.8s; - animation-delay: -0.8s; -} - -.container3 .circle2 { - -webkit-animation-delay: -0.7s; - animation-delay: -0.7s; -} - -.container1 .circle3 { - -webkit-animation-delay: -0.6s; - animation-delay: -0.6s; -} - -.container2 .circle3 { - -webkit-animation-delay: -0.5s; - animation-delay: -0.5s; -} - -.container3 .circle3 { - -webkit-animation-delay: -0.4s; - animation-delay: -0.4s; -} - -.container1 .circle4 { - -webkit-animation-delay: -0.3s; - animation-delay: -0.3s; -} - -.container2 .circle4 { - -webkit-animation-delay: -0.2s; - animation-delay: -0.2s; -} - -.container3 .circle4 { - -webkit-animation-delay: -0.1s; - animation-delay: -0.1s; -} - -@-webkit-keyframes bouncedelay { - 0%, 80%, 100% { -webkit-transform: scale(0.0) } - 40% { -webkit-transform: scale(1.0) } -} - -@keyframes bouncedelay { - 0%, 80%, 100% { - transform: scale(0.0); - -webkit-transform: scale(0.0); - } 40% { - transform: scale(1.0); - -webkit-transform: scale(1.0); - } -} - -/* ------------------------------------------------------------------------------------------ - * Tabs - * ------------------------------------------------------------------------------------------ */ -ul.nav-tabs { - margin: 0; -} - -p.deprecated span{ - color: #ff0000; - font-weight: bold; - text-decoration: underline; -} - -/* ------------------------------------------------------------------------------------------ - * Print - * ------------------------------------------------------------------------------------------ */ - -@media print { - - #sidenav, - #version, - #versions, - section .version, - section .versions { - display: none; - } - - #content { - margin-left: 0; - } - - a { - text-decoration: none; - color: inherit; - } - - a:after { - content: " [" attr(href) "] "; - } - - p { - color: #000000 - } - - pre { - background-color: #ffffff; - color: #000000; - padding: 10px; - border: #808080 1px solid; - border-radius: 6px; - position: relative; - margin: 10px 0 20px 0; - } - -} /* /@media print */ diff --git a/apidoc/fonts/glyphicons-halflings-regular.eot b/apidoc/fonts/glyphicons-halflings-regular.eot deleted file mode 100644 index b93a4953..00000000 Binary files a/apidoc/fonts/glyphicons-halflings-regular.eot and /dev/null differ diff --git a/apidoc/fonts/glyphicons-halflings-regular.svg b/apidoc/fonts/glyphicons-halflings-regular.svg deleted file mode 100644 index 94fb5490..00000000 --- a/apidoc/fonts/glyphicons-halflings-regular.svg +++ /dev/null @@ -1,288 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apidoc/fonts/glyphicons-halflings-regular.ttf b/apidoc/fonts/glyphicons-halflings-regular.ttf deleted file mode 100644 index 1413fc60..00000000 Binary files a/apidoc/fonts/glyphicons-halflings-regular.ttf and /dev/null differ diff --git a/apidoc/fonts/glyphicons-halflings-regular.woff b/apidoc/fonts/glyphicons-halflings-regular.woff deleted file mode 100644 index 9e612858..00000000 Binary files a/apidoc/fonts/glyphicons-halflings-regular.woff and /dev/null differ diff --git a/apidoc/fonts/glyphicons-halflings-regular.woff2 b/apidoc/fonts/glyphicons-halflings-regular.woff2 deleted file mode 100644 index 64539b54..00000000 Binary files a/apidoc/fonts/glyphicons-halflings-regular.woff2 and /dev/null differ diff --git a/apidoc/img/android-chrome-192x192.png b/apidoc/img/android-chrome-192x192.png deleted file mode 100644 index 5c3d085d..00000000 Binary files a/apidoc/img/android-chrome-192x192.png and /dev/null differ diff --git a/apidoc/img/android-chrome-512x512.png b/apidoc/img/android-chrome-512x512.png deleted file mode 100644 index 2381b094..00000000 Binary files a/apidoc/img/android-chrome-512x512.png and /dev/null differ diff --git a/apidoc/img/apple-touch-icon.png b/apidoc/img/apple-touch-icon.png deleted file mode 100644 index 10a12dcc..00000000 Binary files a/apidoc/img/apple-touch-icon.png and /dev/null differ diff --git a/apidoc/img/favicon-16x16.png b/apidoc/img/favicon-16x16.png deleted file mode 100644 index 8d7a6dc8..00000000 Binary files a/apidoc/img/favicon-16x16.png and /dev/null differ diff --git a/apidoc/img/favicon-32x32.png b/apidoc/img/favicon-32x32.png deleted file mode 100644 index 361d8bce..00000000 Binary files a/apidoc/img/favicon-32x32.png and /dev/null differ diff --git a/apidoc/img/favicon.ico b/apidoc/img/favicon.ico deleted file mode 100644 index 8cc49dd4..00000000 Binary files a/apidoc/img/favicon.ico and /dev/null differ diff --git a/apidoc/index.html b/apidoc/index.html deleted file mode 100644 index 88b0f86d..00000000 --- a/apidoc/index.html +++ /dev/null @@ -1,816 +0,0 @@ - - - - - Loading... - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- -
- -
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-

Loading...

-
-
- - - - diff --git a/apidoc/locales/ca.js b/apidoc/locales/ca.js deleted file mode 100644 index 65af5df2..00000000 --- a/apidoc/locales/ca.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - ca: { - 'Allowed values:' : 'Valors permesos:', - 'Compare all with predecessor': 'Comparar tot amb versió anterior', - 'compare changes to:' : 'comparar canvis amb:', - 'compared to' : 'comparat amb', - 'Default value:' : 'Valor per defecte:', - 'Description' : 'Descripció', - 'Field' : 'Camp', - 'General' : 'General', - 'Generated with' : 'Generat amb', - 'Name' : 'Nom', - 'No response values.' : 'Sense valors en la resposta.', - 'optional' : 'opcional', - 'Parameter' : 'Paràmetre', - 'Permission:' : 'Permisos:', - 'Response' : 'Resposta', - 'Send' : 'Enviar', - 'Send a Sample Request' : 'Enviar una petició d\'exemple', - 'show up to version:' : 'mostrar versió:', - 'Size range:' : 'Tamany de rang:', - 'Type' : 'Tipus', - 'url' : 'url' - } -}); diff --git a/apidoc/locales/cs.js b/apidoc/locales/cs.js deleted file mode 100644 index b7796d8f..00000000 --- a/apidoc/locales/cs.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - cs: { - 'Allowed values:' : 'Povolené hodnoty:', - 'Compare all with predecessor': 'Porovnat vše s předchozími verzemi', - 'compare changes to:' : 'porovnat změny s:', - 'compared to' : 'porovnat s', - 'Default value:' : 'Výchozí hodnota:', - 'Description' : 'Popis', - 'Field' : 'Pole', - 'General' : 'Obecné', - 'Generated with' : 'Vygenerováno pomocí', - 'Name' : 'Název', - 'No response values.' : 'Nebyly vráceny žádné hodnoty.', - 'optional' : 'volitelné', - 'Parameter' : 'Parametr', - 'Permission:' : 'Oprávnění:', - 'Response' : 'Odpověď', - 'Send' : 'Odeslat', - 'Send a Sample Request' : 'Odeslat ukázkový požadavek', - 'show up to version:' : 'zobrazit po verzi:', - 'Size range:' : 'Rozsah velikosti:', - 'Type' : 'Typ', - 'url' : 'url' - } -}); diff --git a/apidoc/locales/de.js b/apidoc/locales/de.js deleted file mode 100644 index f66420d0..00000000 --- a/apidoc/locales/de.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - de: { - 'Allowed values:' : 'Erlaubte Werte:', - 'Compare all with predecessor': 'Vergleiche alle mit ihren Vorgängern', - 'compare changes to:' : 'vergleiche Änderungen mit:', - 'compared to' : 'verglichen mit', - 'Default value:' : 'Standardwert:', - 'Description' : 'Beschreibung', - 'Field' : 'Feld', - 'General' : 'Allgemein', - 'Generated with' : 'Erstellt mit', - 'Name' : 'Name', - 'No response values.' : 'Keine Rückgabewerte.', - 'optional' : 'optional', - 'Parameter' : 'Parameter', - 'Permission:' : 'Berechtigung:', - 'Response' : 'Antwort', - 'Send' : 'Senden', - 'Send a Sample Request' : 'Eine Beispielanfrage senden', - 'show up to version:' : 'zeige bis zur Version:', - 'Size range:' : 'Größenbereich:', - 'Type' : 'Typ', - 'url' : 'url' - } -}); diff --git a/apidoc/locales/es.js b/apidoc/locales/es.js deleted file mode 100644 index 3d47e800..00000000 --- a/apidoc/locales/es.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - es: { - 'Allowed values:' : 'Valores permitidos:', - 'Compare all with predecessor': 'Comparar todo con versión anterior', - 'compare changes to:' : 'comparar cambios con:', - 'compared to' : 'comparado con', - 'Default value:' : 'Valor por defecto:', - 'Description' : 'Descripción', - 'Field' : 'Campo', - 'General' : 'General', - 'Generated with' : 'Generado con', - 'Name' : 'Nombre', - 'No response values.' : 'Sin valores en la respuesta.', - 'optional' : 'opcional', - 'Parameter' : 'Parámetro', - 'Permission:' : 'Permisos:', - 'Response' : 'Respuesta', - 'Send' : 'Enviar', - 'Send a Sample Request' : 'Enviar una petición de ejemplo', - 'show up to version:' : 'mostrar a versión:', - 'Size range:' : 'Tamaño de rango:', - 'Type' : 'Tipo', - 'url' : 'url' - } -}); diff --git a/apidoc/locales/fr.js b/apidoc/locales/fr.js deleted file mode 100644 index 100a6429..00000000 --- a/apidoc/locales/fr.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - fr: { - 'Allowed values:' : 'Valeurs autorisées :', - 'Compare all with predecessor': 'Tout comparer avec ...', - 'compare changes to:' : 'comparer les changements à :', - 'compared to' : 'comparer à', - 'Default value:' : 'Valeur par défaut :', - 'Description' : 'Description', - 'Field' : 'Champ', - 'General' : 'Général', - 'Generated with' : 'Généré avec', - 'Name' : 'Nom', - 'No response values.' : 'Aucune valeur de réponse.', - 'optional' : 'optionnel', - 'Parameter' : 'Paramètre', - 'Permission:' : 'Permission :', - 'Response' : 'Réponse', - 'Send' : 'Envoyer', - 'Send a Sample Request' : 'Envoyer une requête représentative', - 'show up to version:' : 'Montrer à partir de la version :', - 'Size range:' : 'Ordre de grandeur :', - 'Type' : 'Type', - 'url' : 'url' - } -}); diff --git a/apidoc/locales/it.js b/apidoc/locales/it.js deleted file mode 100644 index 8117108c..00000000 --- a/apidoc/locales/it.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - it: { - 'Allowed values:' : 'Valori permessi:', - 'Compare all with predecessor': 'Confronta tutto con versioni precedenti', - 'compare changes to:' : 'confronta modifiche con:', - 'compared to' : 'confrontato con', - 'Default value:' : 'Valore predefinito:', - 'Description' : 'Descrizione', - 'Field' : 'Campo', - 'General' : 'Generale', - 'Generated with' : 'Creato con', - 'Name' : 'Nome', - 'No response values.' : 'Nessun valore di risposta.', - 'optional' : 'opzionale', - 'Parameter' : 'Parametro', - 'Permission:' : 'Permessi:', - 'Response' : 'Risposta', - 'Send' : 'Invia', - 'Send a Sample Request' : 'Invia una richiesta di esempio', - 'show up to version:' : 'mostra alla versione:', - 'Size range:' : 'Intervallo dimensione:', - 'Type' : 'Tipo', - 'url' : 'url' - } -}); diff --git a/apidoc/locales/locale.js b/apidoc/locales/locale.js deleted file mode 100644 index 3530705e..00000000 --- a/apidoc/locales/locale.js +++ /dev/null @@ -1,51 +0,0 @@ -define([ - './locales/ca.js', - './locales/cs.js', - './locales/de.js', - './locales/es.js', - './locales/fr.js', - './locales/it.js', - './locales/nl.js', - './locales/pl.js', - './locales/pt_br.js', - './locales/ro.js', - './locales/ru.js', - './locales/tr.js', - './locales/vi.js', - './locales/zh.js', - './locales/zh_cn.js' -], function() { - var langId = (navigator.language || navigator.userLanguage).toLowerCase().replace('-', '_'); - var language = langId.substr(0, 2); - var locales = {}; - - for (index in arguments) { - for (property in arguments[index]) - locales[property] = arguments[index][property]; - } - if ( ! locales['en']) - locales['en'] = {}; - - if ( ! locales[langId] && ! locales[language]) - language = 'en'; - - var locale = (locales[langId] ? locales[langId] : locales[language]); - - function __(text) { - var index = locale[text]; - if (index === undefined) - return text; - return index; - }; - - function setLanguage(language) { - locale = locales[language]; - } - - return { - __ : __, - locales : locales, - locale : locale, - setLanguage: setLanguage - }; -}); diff --git a/apidoc/locales/nl.js b/apidoc/locales/nl.js deleted file mode 100644 index bddfeeb1..00000000 --- a/apidoc/locales/nl.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - nl: { - 'Allowed values:' : 'Toegestane waarden:', - 'Compare all with predecessor': 'Vergelijk alle met voorgaande versie', - 'compare changes to:' : 'vergelijk veranderingen met:', - 'compared to' : 'vergelijk met', - 'Default value:' : 'Standaard waarde:', - 'Description' : 'Omschrijving', - 'Field' : 'Veld', - 'General' : 'Algemeen', - 'Generated with' : 'Gegenereerd met', - 'Name' : 'Naam', - 'No response values.' : 'Geen response waardes.', - 'optional' : 'optioneel', - 'Parameter' : 'Parameter', - 'Permission:' : 'Permissie:', - 'Response' : 'Antwoorden', - 'Send' : 'Sturen', - 'Send a Sample Request' : 'Stuur een sample aanvragen', - 'show up to version:' : 'toon tot en met versie:', - 'Size range:' : 'Maatbereik:', - 'Type' : 'Type', - 'url' : 'url' - } -}); diff --git a/apidoc/locales/pl.js b/apidoc/locales/pl.js deleted file mode 100644 index db645ee1..00000000 --- a/apidoc/locales/pl.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - pl: { - 'Allowed values:' : 'Dozwolone wartości:', - 'Compare all with predecessor': 'Porównaj z poprzednimi wersjami', - 'compare changes to:' : 'porównaj zmiany do:', - 'compared to' : 'porównaj do:', - 'Default value:' : 'Wartość domyślna:', - 'Description' : 'Opis', - 'Field' : 'Pole', - 'General' : 'Generalnie', - 'Generated with' : 'Wygenerowano z', - 'Name' : 'Nazwa', - 'No response values.' : 'Brak odpowiedzi.', - 'optional' : 'opcjonalny', - 'Parameter' : 'Parametr', - 'Permission:' : 'Uprawnienia:', - 'Response' : 'Odpowiedź', - 'Send' : 'Wyślij', - 'Send a Sample Request' : 'Wyślij przykładowe żądanie', - 'show up to version:' : 'pokaż do wersji:', - 'Size range:' : 'Zakres rozmiaru:', - 'Type' : 'Typ', - 'url' : 'url' - } -}); diff --git a/apidoc/locales/pt_br.js b/apidoc/locales/pt_br.js deleted file mode 100644 index 2bd78b0d..00000000 --- a/apidoc/locales/pt_br.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - 'pt_br': { - 'Allowed values:' : 'Valores permitidos:', - 'Compare all with predecessor': 'Compare todos com antecessores', - 'compare changes to:' : 'comparar alterações com:', - 'compared to' : 'comparado com', - 'Default value:' : 'Valor padrão:', - 'Description' : 'Descrição', - 'Field' : 'Campo', - 'General' : 'Geral', - 'Generated with' : 'Gerado com', - 'Name' : 'Nome', - 'No response values.' : 'Sem valores de resposta.', - 'optional' : 'opcional', - 'Parameter' : 'Parâmetro', - 'Permission:' : 'Permissão:', - 'Response' : 'Resposta', - 'Send' : 'Enviar', - 'Send a Sample Request' : 'Enviar um Exemplo de Pedido', - 'show up to version:' : 'aparecer para a versão:', - 'Size range:' : 'Faixa de tamanho:', - 'Type' : 'Tipo', - 'url' : 'url' - } -}); diff --git a/apidoc/locales/ro.js b/apidoc/locales/ro.js deleted file mode 100644 index 8d4e4ed8..00000000 --- a/apidoc/locales/ro.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - ro: { - 'Allowed values:' : 'Valori permise:', - 'Compare all with predecessor': 'Compară toate cu versiunea precedentă', - 'compare changes to:' : 'compară cu versiunea:', - 'compared to' : 'comparat cu', - 'Default value:' : 'Valoare implicită:', - 'Description' : 'Descriere', - 'Field' : 'Câmp', - 'General' : 'General', - 'Generated with' : 'Generat cu', - 'Name' : 'Nume', - 'No response values.' : 'Nici o valoare returnată.', - 'optional' : 'opțional', - 'Parameter' : 'Parametru', - 'Permission:' : 'Permisiune:', - 'Response' : 'Răspuns', - 'Send' : 'Trimite', - 'Send a Sample Request' : 'Trimite o cerere de probă', - 'show up to version:' : 'arată până la versiunea:', - 'Size range:' : 'Interval permis:', - 'Type' : 'Tip', - 'url' : 'url' - } -}); diff --git a/apidoc/locales/ru.js b/apidoc/locales/ru.js deleted file mode 100644 index c5f33821..00000000 --- a/apidoc/locales/ru.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - ru: { - 'Allowed values:' : 'Допустимые значения:', - 'Compare all with predecessor': 'Сравнить с предыдущей версией', - 'compare changes to:' : 'сравнить с:', - 'compared to' : 'в сравнении с', - 'Default value:' : 'По умолчанию:', - 'Description' : 'Описание', - 'Field' : 'Название', - 'General' : 'Общая информация', - 'Generated with' : 'Сгенерировано с помощью', - 'Name' : 'Название', - 'No response values.' : 'Нет значений для ответа.', - 'optional' : 'необязательный', - 'Parameter' : 'Параметр', - 'Permission:' : 'Разрешено:', - 'Response' : 'Ответ', - 'Send' : 'Отправить', - 'Send a Sample Request' : 'Отправить тестовый запрос', - 'show up to version:' : 'показать версию:', - 'Size range:' : 'Ограничения:', - 'Type' : 'Тип', - 'url' : 'URL' - } -}); diff --git a/apidoc/locales/tr.js b/apidoc/locales/tr.js deleted file mode 100644 index 5c64e52d..00000000 --- a/apidoc/locales/tr.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - tr: { - 'Allowed values:' : 'İzin verilen değerler:', - 'Compare all with predecessor': 'Tümünü öncekiler ile karşılaştır', - 'compare changes to:' : 'değişiklikleri karşılaştır:', - 'compared to' : 'karşılaştır', - 'Default value:' : 'Varsayılan değer:', - 'Description' : 'Açıklama', - 'Field' : 'Alan', - 'General' : 'Genel', - 'Generated with' : 'Oluşturan', - 'Name' : 'İsim', - 'No response values.' : 'Dönüş verisi yok.', - 'optional' : 'opsiyonel', - 'Parameter' : 'Parametre', - 'Permission:' : 'İzin:', - 'Response' : 'Dönüş', - 'Send' : 'Gönder', - 'Send a Sample Request' : 'Örnek istek gönder', - 'show up to version:' : 'bu versiyona kadar göster:', - 'Size range:' : 'Boyut aralığı:', - 'Type' : 'Tip', - 'url' : 'url' - } -}); diff --git a/apidoc/locales/vi.js b/apidoc/locales/vi.js deleted file mode 100644 index 7ce77050..00000000 --- a/apidoc/locales/vi.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - vi: { - 'Allowed values:' : 'Giá trị chấp nhận:', - 'Compare all with predecessor': 'So sánh với tất cả phiên bản trước', - 'compare changes to:' : 'so sánh sự thay đổi với:', - 'compared to' : 'so sánh với', - 'Default value:' : 'Giá trị mặc định:', - 'Description' : 'Chú thích', - 'Field' : 'Trường dữ liệu', - 'General' : 'Tổng quan', - 'Generated with' : 'Được tạo bởi', - 'Name' : 'Tên', - 'No response values.' : 'Không có kết quả trả về.', - 'optional' : 'Tùy chọn', - 'Parameter' : 'Tham số', - 'Permission:' : 'Quyền hạn:', - 'Response' : 'Kết quả', - 'Send' : 'Gửi', - 'Send a Sample Request' : 'Gửi một yêu cầu mẫu', - 'show up to version:' : 'hiển thị phiên bản:', - 'Size range:' : 'Kích cỡ:', - 'Type' : 'Kiểu', - 'url' : 'liên kết' - } -}); diff --git a/apidoc/locales/zh.js b/apidoc/locales/zh.js deleted file mode 100644 index ca5042fe..00000000 --- a/apidoc/locales/zh.js +++ /dev/null @@ -1,25 +0,0 @@ -define({ - zh: { - 'Allowed values​​:' : '允許值:', - 'Compare all with predecessor': '預先比較所有', - 'compare changes to:' : '比較變更:', - 'compared to' : '對比', - 'Default value:' : '預設值:', - 'Description' : '描述', - 'Field' : '欄位', - 'General' : '概括', - 'Generated with' : '生成工具', - 'Name' : '名稱', - 'No response values​​.' : '無對應資料.', - 'optional' : '選填', - 'Parameter' : '參數', - 'Permission:' : '權限:', - 'Response' : '回應', - 'Send' : '發送', - 'Send a Sample Request' : '發送試用需求', - 'show up to version:' : '顯示到版本:', - 'Size range:' : '區間:', - 'Type' : '類型', - 'url' : '網址' - } -}); diff --git a/apidoc/locales/zh_cn.js b/apidoc/locales/zh_cn.js deleted file mode 100644 index 50913e25..00000000 --- a/apidoc/locales/zh_cn.js +++ /dev/null @@ -1,27 +0,0 @@ -define({ - 'zh_cn': { - 'Allowed values:' : '允许值:', - 'Compare all with predecessor': '与所有较早的比较', - 'compare changes to:' : '将当前版本与指定版本比较:', - 'compared to' : '相比于', - 'Default value:' : '默认值:', - 'Description' : '描述', - 'Field' : '字段', - 'General' : '概要', - 'Generated with' : '基于', - 'Name' : '名称', - 'No response values.' : '无返回值.', - 'optional' : '可选', - 'Parameter' : '参数', - 'Parameters' : '参数', - 'Headers' : '头部参数', - 'Permission:' : '权限:', - 'Response' : '返回', - 'Send' : '发送', - 'Send a Sample Request' : '发送示例请求', - 'show up to version:' : '显示到指定版本:', - 'Size range:' : '取值范围:', - 'Type' : '类型', - 'url' : '网址' - } -}); diff --git a/apidoc/main.js b/apidoc/main.js deleted file mode 100644 index bdcf58cb..00000000 --- a/apidoc/main.js +++ /dev/null @@ -1,1146 +0,0 @@ -require.config({ - paths: { - bootstrap: "./vendor/bootstrap.min", - diffMatchPatch: "./vendor/diff_match_patch.min", - handlebars: "./vendor/handlebars.min", - handlebarsExtended: "./utils/handlebars_helper", - jquery: "./vendor/jquery.min", - locales: "./locales/locale", - lodash: "./vendor/lodash.custom.min", - pathToRegexp: "./vendor/path-to-regexp/index", - prismjs: "./vendor/prism", - semver: "./vendor/semver.min", - utilsSampleRequest: "./utils/send_sample_request", - webfontloader: "./vendor/webfontloader", - list: "./vendor/list.min", - apiData: "./api_data", - apiProject: "./api_project" - }, - shim: { - bootstrap: { - deps: ["jquery"] - }, - diffMatchPatch: { - exports: "diff_match_patch" - }, - handlebars: { - exports: "Handlebars" - }, - handlebarsExtended: { - deps: ["jquery", "handlebars"], - exports: "Handlebars" - }, - prismjs: { - exports: "Prism" - } - }, - urlArgs: "v=" + new Date().getTime(), - waitSeconds: 150 -}); - -require([ - "jquery", - "lodash", - "locales", - "handlebarsExtended", - "apiProject", - "apiData", - "prismjs", - "utilsSampleRequest", - "semver", - "webfontloader", - "bootstrap", - "pathToRegexp", - "list" -], function( - $, - _, - locale, - Handlebars, - apiProject, - apiData, - Prism, - sampleRequest, - semver, - WebFont -) { - // Load google web fonts. - WebFont.load({ - active: function() { - // Only init after fonts are loaded. - init( - $, - _, - locale, - Handlebars, - apiProject, - apiData, - Prism, - sampleRequest, - semver - ); - }, - inactive: function() { - // Run init, even if loading fonts fails - init( - $, - _, - locale, - Handlebars, - apiProject, - apiData, - Prism, - sampleRequest, - semver - ); - }, - google: { - families: ["Source Code Pro", "Source Sans Pro:n4,n6,n7"] - } - }); -}); - -function init( - $, - _, - locale, - Handlebars, - apiProject, - apiData, - Prism, - sampleRequest, - semver -) { - var api = apiData.api; - - // - // Templates - // - var templateHeader = Handlebars.compile($("#template-header").html()); - var templateFooter = Handlebars.compile($("#template-footer").html()); - var templateArticle = Handlebars.compile($("#template-article").html()); - var templateCompareArticle = Handlebars.compile( - $("#template-compare-article").html() - ); - var templateGenerator = Handlebars.compile($("#template-generator").html()); - var templateProject = Handlebars.compile($("#template-project").html()); - var templateSections = Handlebars.compile($("#template-sections").html()); - var templateSidenav = Handlebars.compile($("#template-sidenav").html()); - - // - // Default host url used if no sampleUrl is present in config - // - var baseURL = window.location.origin; - - // - // apiProject defaults - // - if (!apiProject.template) apiProject.template = {}; - - if (apiProject.template.withCompare == null) - apiProject.template.withCompare = true; - - if (apiProject.template.withGenerator == null) - apiProject.template.withGenerator = true; - - if (apiProject.template.forceLanguage) - locale.setLanguage(apiProject.template.forceLanguage); - - if (apiProject.template.aloneDisplay == null) - apiProject.template.aloneDisplay = false; - - // Setup jQuery Ajax - $.ajaxSetup(apiProject.template.jQueryAjaxSetup); - - // - // Data transform - // - // grouped by group - var apiByGroup = _.groupBy(api, function(entry) { - return entry.group; - }); - - // grouped by group and name - var apiByGroupAndName = {}; - $.each(apiByGroup, function(index, entries) { - apiByGroupAndName[index] = _.groupBy(entries, function(entry) { - return entry.name; - }); - }); - - // - // sort api within a group by title ASC and custom order - // - var newList = []; - var umlauts = { ä: "ae", ü: "ue", ö: "oe", ß: "ss" }; // TODO: remove in version 1.0 - $.each(apiByGroupAndName, function(index, groupEntries) { - // get titles from the first entry of group[].name[] (name has versioning) - var titles = []; - $.each(groupEntries, function(titleName, entries) { - var title = entries[0].title; - if (title !== undefined) { - title.toLowerCase().replace(/[äöüß]/g, function($0) { - return umlauts[$0]; - }); - titles.push(title + "#~#" + titleName); // '#~#' keep reference to titleName after sorting - } - }); - // sort by name ASC - titles.sort(); - - // custom order - if (apiProject.order) titles = sortByOrder(titles, apiProject.order, "#~#"); - - // add single elements to the new list - titles.forEach(function(name) { - var values = name.split("#~#"); - var key = values[1]; - groupEntries[key].forEach(function(entry) { - newList.push(entry); - }); - }); - }); - // api overwrite with ordered list - api = newList; - - // - // Group- and Versionlists - // - var apiGroups = {}; - var apiGroupTitles = {}; - var apiVersions = {}; - apiVersions[apiProject.version] = 1; - - $.each(api, function(index, entry) { - apiGroups[entry.group] = 1; - apiGroupTitles[entry.group] = entry.groupTitle || entry.group; - apiVersions[entry.version] = 1; - }); - - // sort groups - apiGroups = Object.keys(apiGroups); - apiGroups.sort(); - - // custom order - if (apiProject.order) apiGroups = sortByOrder(apiGroups, apiProject.order); - - // sort versions DESC - apiVersions = Object.keys(apiVersions); - apiVersions.sort(semver.compare); - apiVersions.reverse(); - - // - // create Navigationlist - // - var nav = []; - apiGroups.forEach(function(group) { - // Mainmenu entry - nav.push({ - group: group, - isHeader: true, - title: apiGroupTitles[group] - }); - - // Submenu - var oldName = ""; - api.forEach(function(entry) { - if (entry.group === group) { - if (oldName !== entry.name) { - nav.push({ - title: entry.title, - group: group, - name: entry.name, - type: entry.type, - version: entry.version, - url: entry.url - }); - } else { - nav.push({ - title: entry.title, - group: group, - hidden: true, - name: entry.name, - type: entry.type, - version: entry.version, - url: entry.url - }); - } - oldName = entry.name; - } - }); - }); - - /** - * Add navigation items by analyzing the HTML content and searching for h1 and h2 tags - * @param nav Object the navigation array - * @param content string the compiled HTML content - * @param index where to insert items - * @return boolean true if any good-looking (i.e. with a group identifier)

tag was found - */ - function add_nav(nav, content, index) { - var found_level1 = false; - if (!content) { - return found_level1; - } - var topics = content.match(/(.+?)<\/h(1|2)>/gi); - if (topics) { - topics.forEach(function(entry) { - var level = entry.substring(2, 3); - var title = entry.replace(/<.+?>/g, ""); // Remove all HTML tags for the title - var entry_tags = entry.match(/id="api-([^\-]+)(?:-(.+))?"/); // Find the group and name in the id property - var group = entry_tags ? entry_tags[1] : null; - var name = entry_tags ? entry_tags[2] : null; - if (level == 1 && title && group) { - nav.splice(index, 0, { - group: group, - isHeader: true, - title: title, - isFixed: true - }); - index++; - found_level1 = true; - } - if (level == 2 && title && group && name) { - nav.splice(index, 0, { - group: group, - name: name, - isHeader: false, - title: title, - isFixed: false, - version: "1.0" - }); - index++; - } - }); - } - return found_level1; - } - - // Mainmenu Header entry - if (apiProject.header) { - var found_level1 = add_nav(nav, apiProject.header.content, 0); // Add level 1 and 2 titles - if (!found_level1) { - // If no Level 1 tags were found, make a title - nav.unshift({ - group: "_", - isHeader: true, - title: - apiProject.header.title == null - ? locale.__("General") - : apiProject.header.title, - isFixed: true - }); - } - } - - // Mainmenu Footer entry - if (apiProject.footer) { - var last_nav_index = nav.length; - var found_level1 = add_nav(nav, apiProject.footer.content, nav.length); // Add level 1 and 2 titles - if (!found_level1 && apiProject.footer.title != null) { - // If no Level 1 tags were found, make a title - nav.splice(last_nav_index, 0, { - group: "_footer", - isHeader: true, - title: apiProject.footer.title, - isFixed: true - }); - } - } - - // render pagetitle - var title = apiProject.title - ? apiProject.title - : "apiDoc: " + apiProject.name + " - " + apiProject.version; - $(document).attr("title", title); - - // remove loader - $("#loader").remove(); - - // render sidenav - var fields = { - nav: nav - }; - $("#sidenav").append(templateSidenav(fields)); - - // render Generator - $("#generator").append(templateGenerator(apiProject)); - - // render Project - _.extend(apiProject, { versions: apiVersions }); - $("#project").append(templateProject(apiProject)); - - // render apiDoc, header/footer documentation - if (apiProject.header) $("#header").append(templateHeader(apiProject.header)); - - if (apiProject.footer) $("#footer").append(templateFooter(apiProject.footer)); - - // - // Render Sections and Articles - // - var articleVersions = {}; - var content = ""; - apiGroups.forEach(function(groupEntry) { - var articles = []; - var oldName = ""; - var fields = {}; - var title = groupEntry; - var description = ""; - articleVersions[groupEntry] = {}; - - // render all articles of a group - api.forEach(function(entry) { - if (groupEntry === entry.group) { - if (oldName !== entry.name) { - // determine versions - api.forEach(function(versionEntry) { - if ( - groupEntry === versionEntry.group && - entry.name === versionEntry.name - ) { - if (!articleVersions[entry.group].hasOwnProperty(entry.name)) { - articleVersions[entry.group][entry.name] = []; - } - articleVersions[entry.group][entry.name].push( - versionEntry.version - ); - } - }); - fields = { - article: entry, - versions: articleVersions[entry.group][entry.name] - }; - } else { - fields = { - article: entry, - hidden: true, - versions: articleVersions[entry.group][entry.name] - }; - } - - if (apiProject.sampleUrl == false) { - fields.article.sampleRequest = [ - { - url: baseURL + fields.article.url - } - ]; - } - - // add prefix URL for endpoint unless it's already absolute - if (apiProject.url) { - if (fields.article.url.substr(0, 4).toLowerCase() !== "http") { - fields.article.url = apiProject.url + fields.article.url; - } - } - - addArticleSettings(fields, entry); - - if (entry.groupTitle) title = entry.groupTitle; - - // TODO: make groupDescription compareable with older versions (not important for the moment) - if (entry.groupDescription) description = entry.groupDescription; - - articles.push({ - article: templateArticle(fields), - group: entry.group, - name: entry.name, - aloneDisplay: apiProject.template.aloneDisplay - }); - oldName = entry.name; - } - }); - - // render Section with Articles - var fields = { - group: groupEntry, - title: title, - description: description, - articles: articles, - aloneDisplay: apiProject.template.aloneDisplay - }; - content += templateSections(fields); - }); - $("#sections").append(content); - - // Bootstrap Scrollspy - $(this).scrollspy({ target: "#scrollingNav" }); - - // Content-Scroll on Navigation click. - $(".sidenav") - .find("a") - .on("click", function(e) { - e.preventDefault(); - var id = $(this).attr("href"); - if ($(id).length > 0) - $("html,body").animate( - { scrollTop: parseInt($(id).offset().top) }, - 400 - ); - window.location.hash = $(this).attr("href"); - }); - - /** - * Check if Parameter (sub) List has a type Field. - * Example: @apiSuccess varname1 No type. - * @apiSuccess {String} varname2 With type. - * - * @param {Object} fields - */ - function _hasTypeInFields(fields) { - var result = false; - $.each(fields, function(name) { - result = - result || - _.some(fields[name], function(item) { - return item.type; - }); - }); - return result; - } - - /** - * On Template changes, recall plugins. - */ - function initDynamic() { - // Bootstrap popover - $('button[data-toggle="popover"]') - .popover() - .click(function(e) { - e.preventDefault(); - }); - - var version = $("#version strong").html(); - $("#sidenav li").removeClass("is-new"); - if (apiProject.template.withCompare) { - $("#sidenav li[data-version='" + version + "']").each(function() { - var group = $(this).data("group"); - var name = $(this).data("name"); - var length = $( - "#sidenav li[data-group='" + group + "'][data-name='" + name + "']" - ).length; - var index = $( - "#sidenav li[data-group='" + group + "'][data-name='" + name + "']" - ).index($(this)); - if (length === 1 || index === length - 1) $(this).addClass("is-new"); - }); - } - - // tabs - $(".nav-tabs-examples a").click(function(e) { - e.preventDefault(); - $(this).tab("show"); - }); - $(".nav-tabs-examples") - .find("a:first") - .tab("show"); - - // sample header-content-type switch - $(".sample-header-content-type-switch").change(function() { - var paramName = "." + $(this).attr("name") + "-fields"; - var bodyName = "." + $(this).attr("name") + "-body"; - var selectName = "select[name=" + $(this).attr("name") + "]"; - if ($(this).val() == "body-json") { - $(selectName).val("undefined"); - $(this).val("body-json"); - $(paramName).removeClass("hide"); - $(this) - .parent() - .nextAll(paramName) - .first() - .addClass("hide"); - $(bodyName).addClass("hide"); - $(this) - .parent() - .nextAll(bodyName) - .first() - .removeClass("hide"); - } else if ($(this).val() == "body-form-data") { - $(selectName).val("undefined"); - $(this).val("body-form-data"); - $(bodyName).addClass("hide"); - $(paramName).removeClass("hide"); - } else { - $(this) - .parent() - .nextAll(paramName) - .first() - .removeClass("hide"); - $(this) - .parent() - .nextAll(bodyName) - .first() - .addClass("hide"); - } - $(this) - .prev(".sample-request-switch") - .prop("checked", true); - }); - - // sample request switch - $(".sample-request-switch").click(function(e) { - var paramName = "." + $(this).attr("name") + "-fields"; - var bodyName = "." + $(this).attr("name") + "-body"; - var select = $(this) - .next("." + $(this).attr("name") + "-select") - .val(); - if ($(this).prop("checked")) { - if (select == "body-json") { - $(this) - .parent() - .nextAll(bodyName) - .first() - .removeClass("hide"); - } else { - $(this) - .parent() - .nextAll(paramName) - .first() - .removeClass("hide"); - } - } else { - if (select == "body-json") { - $(this) - .parent() - .nextAll(bodyName) - .first() - .addClass("hide"); - } else { - $(this) - .parent() - .nextAll(paramName) - .first() - .addClass("hide"); - } - } - }); - - if (apiProject.template.aloneDisplay) { - //show group - $(".show-group").click(function() { - var apiGroup = "." + $(this).attr("data-group") + "-group"; - var apiGroupArticle = "." + $(this).attr("data-group") + "-article"; - $(".show-api-group").addClass("hide"); - $(apiGroup).removeClass("hide"); - $(".show-api-article").addClass("hide"); - $(apiGroupArticle).removeClass("hide"); - }); - - //show api - $(".show-api").click(function() { - var apiName = "." + $(this).attr("data-name") + "-article"; - var apiGroup = "." + $(this).attr("data-group") + "-group"; - $(".show-api-group").addClass("hide"); - $(apiGroup).removeClass("hide"); - $(".show-api-article").addClass("hide"); - $(apiName).removeClass("hide"); - }); - } - - // call scrollspy refresh method - $(window).scrollspy("refresh"); - - // init modules - sampleRequest.initDynamic(); - Prism.highlightAll(); - } - initDynamic(); - - if (apiProject.template.aloneDisplay) { - var hashVal = window.location.hash; - if (hashVal != null && hashVal.length !== 0) { - $("." + hashVal.slice(1) + "-init").click(); - } - } - - // - // HTML-Template specific jQuery-Functions - // - // Change Main Version - function setMainVersion(selectedVersion) { - if (typeof selectedVersion === "undefined") { - selectedVersion = $("#version strong").html(); - } else { - $("#version strong").html(selectedVersion); - } - - // hide all - $("article").addClass("hide"); - $("#sidenav li:not(.nav-fixed)").addClass("hide"); - - // show 1st equal or lower Version of each entry - $("article[data-version]").each(function(index) { - var group = $(this).data("group"); - var name = $(this).data("name"); - var version = $(this).data("version"); - - if (semver.lte(version, selectedVersion)) { - if ( - $( - "article[data-group='" + - group + - "'][data-name='" + - name + - "']:visible" - ).length === 0 - ) { - // enable Article - $( - "article[data-group='" + - group + - "'][data-name='" + - name + - "'][data-version='" + - version + - "']" - ).removeClass("hide"); - // enable Navigation - $( - "#sidenav li[data-group='" + - group + - "'][data-name='" + - name + - "'][data-version='" + - version + - "']" - ).removeClass("hide"); - $("#sidenav li.nav-header[data-group='" + group + "']").removeClass( - "hide" - ); - } - } - }); - - // show 1st equal or lower Version of each entry - $("article[data-version]").each(function(index) { - var group = $(this).data("group"); - $("section#api-" + group).removeClass("hide"); - if ($("section#api-" + group + " article:visible").length === 0) { - $("section#api-" + group).addClass("hide"); - } else { - $("section#api-" + group).removeClass("hide"); - } - }); - - initDynamic(); - return; - } - setMainVersion(); - - $("#versions li.version a").on("click", function(e) { - e.preventDefault(); - - setMainVersion($(this).html()); - }); - - // compare all article with their predecessor - $("#compareAllWithPredecessor").on("click", changeAllVersionCompareTo); - - // change version of an article - $("article .versions li.version a").on("click", changeVersionCompareTo); - - // compare url-parameter - $.urlParam = function(name) { - var results = new RegExp("[\\?&]" + name + "=([^&#]*)").exec( - window.location.href - ); - return results && results[1] ? results[1] : null; - }; - - if ($.urlParam("compare")) { - // URL Paramter ?compare=1 is set - $("#compareAllWithPredecessor").trigger("click"); - } - - // Quick jump on page load to hash position. - // Should happen after setting the main version - // and after triggering the click on the compare button, - // as these actions modify the content - // and would make it jump to the wrong position or not jump at all. - if (window.location.hash) { - var id = decodeURI(window.location.hash); - if ($(id).length > 0) - $("html,body").animate({ scrollTop: parseInt($(id).offset().top) }, 0); - } - - /** - * Initialize search - */ - var options = { - valueNames: ["nav-list-item", "nav-list-url-item"] - }; - var endpointsList = new List("scrollingNav", options); - - /** - * Set initial focus to search input - */ - $("#scrollingNav .sidenav-search input.search").focus(); - - /** - * Detect ESC key to reset search - */ - $(document).keyup(function(e) { - if (e.keyCode === 27) $("span.search-reset").click(); - }); - - /** - * Search reset - */ - $("span.search-reset").on("click", function() { - $("#scrollingNav .sidenav-search input.search") - .val("") - .focus(); - endpointsList.search(); - }); - - /** - * Change version of an article to compare it to an other version. - */ - function changeVersionCompareTo(e) { - e.preventDefault(); - - var $root = $(this).parents("article"); - var selectedVersion = $(this).html(); - var $button = $root.find(".version"); - var currentVersion = $button.find("strong").html(); - $button.find("strong").html(selectedVersion); - - var group = $root.data("group"); - var name = $root.data("name"); - var version = $root.data("version"); - - var compareVersion = $root.data("compare-version"); - - if (compareVersion === selectedVersion) return; - - if (!compareVersion && version == selectedVersion) return; - - if ( - (compareVersion && articleVersions[group][name][0] === selectedVersion) || - version === selectedVersion - ) { - // the version of the entry is set to the highest version (reset) - resetArticle(group, name, version); - } else { - var $compareToArticle = $( - "article[data-group='" + - group + - "'][data-name='" + - name + - "'][data-version='" + - selectedVersion + - "']" - ); - - var sourceEntry = {}; - var compareEntry = {}; - $.each(apiByGroupAndName[group][name], function(index, entry) { - if (entry.version === version) sourceEntry = entry; - if (entry.version === selectedVersion) compareEntry = entry; - }); - - var fields = { - article: sourceEntry, - compare: compareEntry, - versions: articleVersions[group][name] - }; - - // add unique id - // TODO: replace all group-name-version in template with id. - fields.article.id = - fields.article.group + - "-" + - fields.article.name + - "-" + - fields.article.version; - fields.article.id = fields.article.id.replace(/\./g, "_"); - - fields.compare.id = - fields.compare.group + - "-" + - fields.compare.name + - "-" + - fields.compare.version; - fields.compare.id = fields.compare.id.replace(/\./g, "_"); - - var entry = sourceEntry; - if (entry.parameter && entry.parameter.fields) - fields._hasTypeInParameterFields = _hasTypeInFields( - entry.parameter.fields - ); - - if (entry.error && entry.error.fields) - fields._hasTypeInErrorFields = _hasTypeInFields(entry.error.fields); - - if (entry.success && entry.success.fields) - fields._hasTypeInSuccessFields = _hasTypeInFields(entry.success.fields); - - if (entry.info && entry.info.fields) - fields._hasTypeInInfoFields = _hasTypeInFields(entry.info.fields); - - var entry = compareEntry; - if ( - fields._hasTypeInParameterFields !== true && - entry.parameter && - entry.parameter.fields - ) - fields._hasTypeInParameterFields = _hasTypeInFields( - entry.parameter.fields - ); - - if ( - fields._hasTypeInErrorFields !== true && - entry.error && - entry.error.fields - ) - fields._hasTypeInErrorFields = _hasTypeInFields(entry.error.fields); - - if ( - fields._hasTypeInSuccessFields !== true && - entry.success && - entry.success.fields - ) - fields._hasTypeInSuccessFields = _hasTypeInFields(entry.success.fields); - - if ( - fields._hasTypeInInfoFields !== true && - entry.info && - entry.info.fields - ) - fields._hasTypeInInfoFields = _hasTypeInFields(entry.info.fields); - - var content = templateCompareArticle(fields); - $root.after(content); - var $content = $root.next(); - - // Event on.click re-assign - $content - .find(".versions li.version a") - .on("click", changeVersionCompareTo); - - // select navigation - $( - "#sidenav li[data-group='" + - group + - "'][data-name='" + - name + - "'][data-version='" + - currentVersion + - "']" - ).addClass("has-modifications"); - - $root.remove(); - // TODO: on change main version or select the highest version re-render - } - - initDynamic(); - } - - /** - * Compare all currently selected Versions with their predecessor. - */ - function changeAllVersionCompareTo(e) { - e.preventDefault(); - $("article:visible .versions").each(function() { - var $root = $(this).parents("article"); - var currentVersion = $root.data("version"); - var $foundElement = null; - $(this) - .find("li.version a") - .each(function() { - var selectVersion = $(this).html(); - if (selectVersion < currentVersion && !$foundElement) - $foundElement = $(this); - }); - - if ($foundElement) $foundElement.trigger("click"); - }); - initDynamic(); - } - - /** - * Sort the fields. - */ - function sortFields(fields_object) { - $.each(fields_object, function(key, fields) { - // Find only object fields - var objects = fields.filter(function(item) { - return item.type === "Object"; - }); - - // Check if has any object - if (objects.length === 0) { - return; - } - - // Iterate over all objects - for (var object of objects) { - // Retrieve the index - var index = fields.indexOf(object); - - // Find all child fields for this object - var objectFields = fields.filter(function(item) { - return item.field.indexOf(object.field + ".") > -1; - }); - - // Get the child index - var firstIndex = fields.indexOf(objectFields[0]); - - // Put the object it before the first child index - fields.splice(index, 1); - fields.splice(firstIndex, 0, object); - - // Startup the last index with the object index - var lastIndex = firstIndex; - - // Iterate over all children - for (var child of objectFields) { - lastIndex++; - - // Retrieve the index - var childIndex = fields.indexOf(child); - - // Put it after the object declaration - fields.splice(childIndex, 1); - fields.splice(lastIndex, 0, child); - } - } - - // Retrieve the first object field index - var firstObjectIndex = fields.indexOf(objects[0]); - - // Find all non-object fields that doesn't contain dot notation - var nonObjects = fields.filter(function(item) { - return item.field.indexOf(".") === -1 && item.type !== "Object"; - }); - - // Iterate over all non-objects - for (var nonObject of nonObjects) { - // Put it before the first object field - var index = fields.indexOf(nonObject); - fields.splice(index, 1); - fields.splice(firstObjectIndex, 0, nonObject); - } - }); - } - - /** - * Add article settings. - */ - function addArticleSettings(fields, entry) { - // add unique id - // TODO: replace all group-name-version in template with id. - fields.id = - fields.article.group + - "-" + - fields.article.name + - "-" + - fields.article.version; - fields.id = fields.id.replace(/\./g, "_"); - - if (entry.header && entry.header.fields) { - sortFields(entry.header.fields); - fields._hasTypeInHeaderFields = _hasTypeInFields(entry.header.fields); - } - - if (entry.parameter && entry.parameter.fields) { - sortFields(entry.parameter.fields); - fields._hasTypeInParameterFields = _hasTypeInFields( - entry.parameter.fields - ); - } - - if (entry.error && entry.error.fields) { - sortFields(entry.error.fields); - fields._hasTypeInErrorFields = _hasTypeInFields(entry.error.fields); - } - - if (entry.success && entry.success.fields) { - sortFields(entry.success.fields); - fields._hasTypeInSuccessFields = _hasTypeInFields(entry.success.fields); - } - - if (entry.info && entry.info.fields) { - sortFields(entry.info.fields); - fields._hasTypeInInfoFields = _hasTypeInFields(entry.info.fields); - } - - // add template settings - fields.template = apiProject.template; - } - - /** - * Render Article. - */ - function renderArticle(group, name, version) { - var entry = {}; - $.each(apiByGroupAndName[group][name], function(index, currentEntry) { - if (currentEntry.version === version) entry = currentEntry; - }); - var fields = { - article: entry, - versions: articleVersions[group][name] - }; - - addArticleSettings(fields, entry); - - return templateArticle(fields); - } - - /** - * Render original Article and remove the current visible Article. - */ - function resetArticle(group, name, version) { - var $root = $( - "article[data-group='" + group + "'][data-name='" + name + "']:visible" - ); - var content = renderArticle(group, name, version); - - $root.after(content); - var $content = $root.next(); - - // Event on.click needs to be reassigned (should actually work with on ... automatically) - $content.find(".versions li.version a").on("click", changeVersionCompareTo); - - $( - "#sidenav li[data-group='" + - group + - "'][data-name='" + - name + - "'][data-version='" + - version + - "']" - ).removeClass("has-modifications"); - - $root.remove(); - return; - } - - /** - * Return ordered entries by custom order and append not defined entries to the end. - * @param {String[]} elements - * @param {String[]} order - * @param {String} splitBy - * @return {String[]} Custom ordered list. - */ - function sortByOrder(elements, order, splitBy) { - var results = []; - order.forEach(function(name) { - if (splitBy) - elements.forEach(function(element) { - var parts = element.split(splitBy); - var key = parts[0]; // reference keep for sorting - if (key == name || parts[1] == name) results.push(element); - }); - else - elements.forEach(function(key) { - if (key == name) results.push(name); - }); - }); - // Append all other entries that ar not defined in order - elements.forEach(function(element) { - if (results.indexOf(element) === -1) results.push(element); - }); - return results; - } - Prism.highlightAll(); -} diff --git a/apidoc/utils/handlebars_helper.js b/apidoc/utils/handlebars_helper.js deleted file mode 100644 index ed1dc1da..00000000 --- a/apidoc/utils/handlebars_helper.js +++ /dev/null @@ -1,416 +0,0 @@ -define([ - 'locales', - 'handlebars', - 'diffMatchPatch' -], function(locale, Handlebars, DiffMatchPatch) { - - /** - * Return a text as markdown. - * Currently only a little helper to replace apidoc-inline Links (#Group:Name). - * Should be replaced with a full markdown lib. - * @param string text - */ - Handlebars.registerHelper('markdown', function(text) { - if ( ! text ) { - return text; - } - text = text.replace(/((\[(.*?)\])?\(#)((.+?):(.+?))(\))/mg, function(match, p1, p2, p3, p4, p5, p6) { - var link = p3 || p5 + '/' + p6; - return '' + link + ''; - }); - return text; - }); - - /** - * set paramater type. - */ - Handlebars.registerHelper("setInputType", function(text) { - if (text === "File") { - return "file"; - } - return "text"; - }); - - /** - * start/stop timer for simple performance check. - */ - var timer; - Handlebars.registerHelper('startTimer', function(text) { - timer = new Date(); - return ''; - }); - - Handlebars.registerHelper('stopTimer', function(text) { - console.log(new Date() - timer); - return ''; - }); - - /** - * Return localized Text. - * @param string text - */ - Handlebars.registerHelper('__', function(text) { - return locale.__(text); - }); - - /** - * Console log. - * @param mixed obj - */ - Handlebars.registerHelper('cl', function(obj) { - console.log(obj); - return ''; - }); - - /** - * Replace underscore with space. - * @param string text - */ - Handlebars.registerHelper('underscoreToSpace', function(text) { - return text.replace(/(_+)/g, ' '); - }); - - /** - * - */ - Handlebars.registerHelper('assign', function(name) { - if(arguments.length > 0) { - var type = typeof(arguments[1]); - var arg = null; - if(type === 'string' || type === 'number' || type === 'boolean') arg = arguments[1]; - Handlebars.registerHelper(name, function() { return arg; }); - } - return ''; - }); - - /** - * - */ - Handlebars.registerHelper('nl2br', function(text) { - return _handlebarsNewlineToBreak(text); - }); - - /** - * - */ - Handlebars.registerHelper('if_eq', function(context, options) { - var compare = context; - // Get length if context is an object - if (context instanceof Object && ! (options.hash.compare instanceof Object)) - compare = Object.keys(context).length; - - if (compare === options.hash.compare) - return options.fn(this); - - return options.inverse(this); - }); - - /** - * - */ - Handlebars.registerHelper('if_gt', function(context, options) { - var compare = context; - // Get length if context is an object - if (context instanceof Object && ! (options.hash.compare instanceof Object)) - compare = Object.keys(context).length; - - if(compare > options.hash.compare) - return options.fn(this); - - return options.inverse(this); - }); - - /** - * - */ - var templateCache = {}; - Handlebars.registerHelper('subTemplate', function(name, sourceContext) { - if ( ! templateCache[name]) - templateCache[name] = Handlebars.compile($('#template-' + name).html()); - - var template = templateCache[name]; - var templateContext = $.extend({}, this, sourceContext.hash); - return new Handlebars.SafeString( template(templateContext) ); - }); - - /** - * - */ - Handlebars.registerHelper('toLowerCase', function(value) { - return (value && typeof value === 'string') ? value.toLowerCase() : ''; - }); - - /** - * - */ - Handlebars.registerHelper('splitFill', function(value, splitChar, fillChar) { - var splits = value.split(splitChar); - return new Array(splits.length).join(fillChar) + splits[splits.length - 1]; - }); - - /** - * Convert Newline to HTML-Break (nl2br). - * - * @param {String} text - * @returns {String} - */ - function _handlebarsNewlineToBreak(text) { - return ('' + text).replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + '
' + '$2'); - } - - /** - * - */ - Handlebars.registerHelper('each_compare_list_field', function(source, compare, options) { - var fieldName = options.hash.field; - var newSource = []; - if (source) { - source.forEach(function(entry) { - var values = entry; - values['key'] = entry[fieldName]; - newSource.push(values); - }); - } - - var newCompare = []; - if (compare) { - compare.forEach(function(entry) { - var values = entry; - values['key'] = entry[fieldName]; - newCompare.push(values); - }); - } - return _handlebarsEachCompared('key', newSource, newCompare, options); - }); - - /** - * - */ - Handlebars.registerHelper('each_compare_keys', function(source, compare, options) { - var newSource = []; - if (source) { - var sourceFields = Object.keys(source); - sourceFields.forEach(function(name) { - var values = {}; - values['value'] = source[name]; - values['key'] = name; - newSource.push(values); - }); - } - - var newCompare = []; - if (compare) { - var compareFields = Object.keys(compare); - compareFields.forEach(function(name) { - var values = {}; - values['value'] = compare[name]; - values['key'] = name; - newCompare.push(values); - }); - } - return _handlebarsEachCompared('key', newSource, newCompare, options); - }); - - /** - * - */ - Handlebars.registerHelper('gen_body', function(context, options) { - let strBody = {}; - context.forEach(element => { - element.field = element.field.replace("]", ""); - switch (element.type.toLowerCase()) { - case "string": - if (element.field.includes('[')) { - if (strBody[element.field.split("[")[0]] === undefined) { - strBody[element.field.split("[")[0]] = {}; - } - strBody[element.field.split("[")[0]][element.field.split("[")[1]] = (element.defaultValue || ""); - break; - } - strBody[element.field] = (element.defaultValue || ""); - break; - case "number": - if (element.field.includes('[')) { - if (strBody[element.field.split("[")[0]] === undefined) { - strBody[element.field.split("[")[0]] = {}; - } - strBody[element.field.split("[")[0]][element.field.split("[")[1]] = (element.defaultValue || 0); - break; - } - strBody[element.field] = (element.defaultValue || 0); - break; - case "object": - if (strBody[element.field] === undefined) { - strBody[element.field] = {}; - } - break; - default: - strBody[element.field] = null; - } - }); - return JSON.stringify(strBody, null, 4); - }); - - /** - * - */ - Handlebars.registerHelper('each_compare_field', function(source, compare, options) { - return _handlebarsEachCompared('field', source, compare, options); - }); - - /** - * - */ - Handlebars.registerHelper('each_compare_title', function(source, compare, options) { - return _handlebarsEachCompared('title', source, compare, options); - }); - - /** - * - */ - Handlebars.registerHelper('reformat', function(source, type){ - if (type == 'json') - try { - return JSON.stringify(JSON.parse(source.trim()),null, " "); - } catch(e) { - - } - return source - }); - - /** - * - */ - Handlebars.registerHelper('showDiff', function(source, compare, options) { - var ds = ''; - if(source === compare) { - ds = source; - } else { - if( ! source) - return compare; - - if( ! compare) - return source; - - var d = diffMatchPatch.diff_main(stripHtml(compare), stripHtml(source)); - diffMatchPatch.diff_cleanupSemantic(d); - ds = diffMatchPatch.diff_prettyHtml(d); - ds = ds.replace(/¶/gm, ''); - } - if(options === 'nl2br') - ds = _handlebarsNewlineToBreak(ds); - - return ds; - }); - - /** - * - */ - function _handlebarsEachCompared(fieldname, source, compare, options) - { - var dataList = []; - var index = 0; - if(source) { - source.forEach(function(sourceEntry) { - var found = false; - if (compare) { - compare.forEach(function(compareEntry) { - if(sourceEntry[fieldname] === compareEntry[fieldname]) { - var data = { - typeSame: true, - source: sourceEntry, - compare: compareEntry, - index: index - }; - dataList.push(data); - found = true; - index++; - } - }); - } - if ( ! found) { - var data = { - typeIns: true, - source: sourceEntry, - index: index - }; - dataList.push(data); - index++; - } - }); - } - - if (compare) { - compare.forEach(function(compareEntry) { - var found = false; - if (source) { - source.forEach(function(sourceEntry) { - if(sourceEntry[fieldname] === compareEntry[fieldname]) - found = true; - }); - } - if ( ! found) { - var data = { - typeDel: true, - compare: compareEntry, - index: index - }; - dataList.push(data); - index++; - } - }); - } - - var ret = ''; - var length = dataList.length; - for (var index in dataList) { - if(index == (length - 1)) - dataList[index]['_last'] = true; - ret = ret + options.fn(dataList[index]); - } - return ret; - } - - var diffMatchPatch = new DiffMatchPatch(); - - /** - * Overwrite Colors - */ - DiffMatchPatch.prototype.diff_prettyHtml = function(diffs) { - var html = []; - var pattern_amp = /&/g; - var pattern_lt = //g; - var pattern_para = /\n/g; - for (var x = 0; x < diffs.length; x++) { - var op = diffs[x][0]; // Operation (insert, delete, equal) - var data = diffs[x][1]; // Text of change. - var text = data.replace(pattern_amp, '&').replace(pattern_lt, '<') - .replace(pattern_gt, '>').replace(pattern_para, '¶
'); - switch (op) { - case DIFF_INSERT: - html[x] = '' + text + ''; - break; - case DIFF_DELETE: - html[x] = '' + text + ''; - break; - case DIFF_EQUAL: - html[x] = '' + text + ''; - break; - } - } - return html.join(''); - }; - - /** - * Fixes html after comparison (#506, #538, #616, #825) - */ - function stripHtml(html){ - var div = document.createElement("div"); - div.innerHTML = html; - return div.textContent || div.innerText || ""; - } - - // Exports - return Handlebars; -}); diff --git a/apidoc/utils/send_sample_request.js b/apidoc/utils/send_sample_request.js deleted file mode 100644 index 4d695d14..00000000 --- a/apidoc/utils/send_sample_request.js +++ /dev/null @@ -1,272 +0,0 @@ -define([ - 'jquery', - 'lodash', - './utils/send_sample_request_utils' -], function($, _, utils) { - - var initDynamic = function() { - // Button send - $(".sample-request-send").off("click"); - $(".sample-request-send").on("click", function(e) { - e.preventDefault(); - var $root = $(this).parents("article"); - var group = $root.data("group"); - var name = $root.data("name"); - var version = $root.data("version"); - sendSampleRequest(group, name, version, $(this).data("sample-request-type")); - }); - - // Button clear - $(".sample-request-clear").off("click"); - $(".sample-request-clear").on("click", function(e) { - e.preventDefault(); - var $root = $(this).parents("article"); - var group = $root.data("group"); - var name = $root.data("name"); - var version = $root.data("version"); - clearSampleRequest(group, name, version); - }); - }; // initDynamic - - function sendSampleRequest(group, name, version, type) - { - var $root = $('article[data-group="' + group + '"][data-name="' + name + '"][data-version="' + version + '"]'); - - // Optional header - var header = {}; - $root.find(".sample-request-header:checked").each(function(i, element) { - var group = $(element).data("sample-request-header-group-id"); - $root.find("[data-sample-request-header-group=\"" + group + "\"]").each(function(i, element) { - var key = $(element).data("sample-request-header-name"); - var value = element.value; - if (typeof element.optional === 'undefined') { - element.optional = true; - } - if ( ! element.optional && element.defaultValue !== '') { - value = element.defaultValue; - } - header[key] = value; - }); - }); - - - // create JSON dictionary of parameters - var param = {}; - var paramType = {}; - var bodyFormData = new FormData(); - var bodyJson = ''; - $root.find(".sample-request-param:checked").each(function(i, element) { - var group = $(element).data("sample-request-param-group-id"); - var contentType = $(element).nextAll('.sample-header-content-type-switch').first().val(); - if (contentType == "body-json"){ - $root.find("[data-sample-request-body-group=\"" + group + "\"]").not(function(){ - return $(this).val() == "" && $(this).is("[data-sample-request-param-optional='true']"); - }).each(function(i, element) { - if (isJson(element.value)){ - header['Content-Type'] = 'application/json'; - bodyJson = element.value; - } - }); - }else { - $root.find("[data-sample-request-param-group=\"" + group + "\"]").not(function(){ - return $(this).val() == "" && $(this).is("[data-sample-request-param-optional='true']"); - }).each(function(i, element) { - var key = $(element).data("sample-request-param-name"); - var value = element.value; - if ( ! element.optional && element.defaultValue !== '') { - value = element.defaultValue; - } - if (contentType == "body-form-data"){ - header['Content-Type'] = 'multipart/form-data' - if (element.type == "file") { - value = element.files[0]; - } - bodyFormData.append(key,value); - }else { - param[key] = value; - paramType[key] = $(element).next().text(); - } - }); - } - }); - - // grab user-inputted URL - var url = $root.find(".sample-request-url").val(); - - //Convert {param} form to :param - url = utils.convertPathParams(url); - - // Insert url parameter - var pattern = pathToRegexp(url, null); - var matches = pattern.exec(url); - for (var i = 1; i < matches.length; i++) { - var key = matches[i].substr(1); - var optional = false - if (key[key.length - 1] === '?') { - optional = true; - key = key.substr(0, key.length - 1); - } - if (param[key] !== undefined) { - url = url.replace(matches[i], encodeURIComponent(param[key])); - - // remove URL parameters from list - delete param[key]; - } else if (optional) { - // if parameter is optional denoted by ending '?' in param (:param?) - // and no parameter is given, replace parameter with empty string instead - url = url.replace(matches[i], ''); - delete param[key]; - } - } // for - - //handle nested objects and parsing fields - param = utils.handleNestedAndParsingFields(param, paramType); - - //add url search parameter - if (header['Content-Type'] == 'application/json') { - if (bodyJson) { - // bodyJson is set to value if request body: 'body/json' was selected and manual json was input - // in this case, use the given bodyJson and add other params in query string - url = url + encodeSearchParams(param); - param = bodyJson; - } else { - // bodyJson not set, but Content-Type: application/json header was set. In this case, send parameters - // as JSON body. First, try parsing fields of object with given paramType definition so that the json - // is valid against the parameter spec (e.g. Boolean params are boolean instead of strings in final json) - param = utils.tryParsingWithTypes(param, paramType); - param = JSON.stringify(param); - } - }else if (header['Content-Type'] == 'multipart/form-data'){ - url = url + encodeSearchParams(param); - param = bodyFormData; - } - - $root.find(".sample-request-response").fadeTo(250, 1); - $root.find(".sample-request-response-json").html("Loading..."); - refreshScrollSpy(); - - // send AJAX request, catch success or error callback - var ajaxRequest = { - url : url, - headers : header, - data : param, - type : type.toUpperCase(), - success : displaySuccess, - error : displayError - }; - - if(header['Content-Type'] == 'multipart/form-data'){ - delete ajaxRequest.headers['Content-Type']; - ajaxRequest.contentType=false; - ajaxRequest.processData=false; - } - $.ajax(ajaxRequest); - - - function displaySuccess(data, status, jqXHR) { - var jsonResponse; - try { - jsonResponse = JSON.parse(jqXHR.responseText); - jsonResponse = JSON.stringify(jsonResponse, null, 4); - } catch (e) { - jsonResponse = jqXHR.responseText; - } - $root.find(".sample-request-response-json").text(jsonResponse); - refreshScrollSpy(); - }; - - function displayError(jqXHR, textStatus, error) { - var message = "Error " + jqXHR.status + ": " + error; - var jsonResponse; - try { - jsonResponse = JSON.parse(jqXHR.responseText); - jsonResponse = JSON.stringify(jsonResponse, null, 4); - } catch (e) { - jsonResponse = jqXHR.responseText; - } - - if (jsonResponse) - message += "\n" + jsonResponse; - - // flicker on previous error to make clear that there is a new response - if($root.find(".sample-request-response").is(":visible")) - $root.find(".sample-request-response").fadeTo(1, 0.1); - - $root.find(".sample-request-response").fadeTo(250, 1); - $root.find(".sample-request-response-json").text(message); - refreshScrollSpy(); - }; - } - - function clearSampleRequest(group, name, version) - { - var $root = $('article[data-group="' + group + '"][data-name="' + name + '"][data-version="' + version + '"]'); - - // hide sample response - $root.find(".sample-request-response-json").html(""); - $root.find(".sample-request-response").hide(); - - // reset value of parameters - $root.find(".sample-request-param").each(function(i, element) { - element.value = ""; - }); - - // restore default URL - var $urlElement = $root.find(".sample-request-url"); - $urlElement.val($urlElement.prop("defaultValue")); - - refreshScrollSpy(); - } - - function refreshScrollSpy() - { - $('[data-spy="scroll"]').each(function () { - $(this).scrollspy("refresh"); - }); - } - - function escapeHtml(str) { - var div = document.createElement("div"); - div.appendChild(document.createTextNode(str)); - return div.innerHTML; - } - - - /** - * is Json - */ - function isJson(str) { - if (typeof str == 'string') { - try { - var obj=JSON.parse(str); - if(typeof obj == 'object' && obj ){ - return true; - }else{ - return false; - } - } catch(e) { - return false; - } - } - } - - /** - * encode Search Params - */ - function encodeSearchParams(obj) { - const params = []; - Object.keys(obj).forEach((key) => { - let value = obj[key]; - params.push([key, encodeURIComponent(value)].join('=')); - }) - return params.length === 0 ? '' : '?' + params.join('&'); - } - - /** - * Exports. - */ - return { - initDynamic: initDynamic - }; - -}); diff --git a/apidoc/utils/send_sample_request_utils.js b/apidoc/utils/send_sample_request_utils.js deleted file mode 100644 index fbda1ffd..00000000 --- a/apidoc/utils/send_sample_request_utils.js +++ /dev/null @@ -1,97 +0,0 @@ -//this block is used to make this module works with Node (CommonJS module format) -if (typeof define !== 'function') { - var define = require('amdefine')(module) -} - -define(['lodash'], function (_) { - - var log = console; - - function handleNestedFields(object, key, params, paramType) { - var attributes = key.split('.'); - var field = attributes[0]; - params.push(field); - if (attributes.length > 1 && paramType[params.join('.')] == 'Object') { - var nestedField = attributes.slice(1).join('.'); - if (!object[field]) - object[field] = {}; - if (typeof object[field] == 'object') { - object[field][nestedField] = object[key]; - delete object[key]; - handleNestedFields(object[field], nestedField, params, paramType); - } - } - } - - function handleNestedFieldsForAllParams(param, paramType) { - var result = Object.assign({}, param); - Object.keys(result).forEach(function (key) { - handleNestedFields(result, key, [], paramType); - }); - return result - } - - function handleArraysAndObjectFields(param, paramType) { - var result = Object.assign({}, param); - Object.keys(paramType).forEach(function (key) { - if (result[key] && (paramType[key].endsWith('[]') || paramType[key] === 'Object')) { - try { - result[key] = JSON.parse(result[key]); - } catch (e) {;} - } - }); - return result - } - - function tryParsingAsType(object, path, type) { - var val = _.get(object, path); - if (val !== undefined) { - if (type === 'Boolean') { - if (val === 'true') { - _.set(object, path, true); - } else if (val === 'false') { - _.set(object, path, false); - } else { - log.warn('Failed to parse object value at path [' + path + ']. Value: (' + val + '). Type: (' + type + ')'); - } - } else if (type === 'Number') { - var parsedInt = parseInt(val, 10); - if (!_.isNaN(parsedInt)) { - _.set(object, path, parsedInt); - } else { - log.warn('Failed to parse object value at path [' + path + ']. Value: (' + val + '). Type: (' + type + ')'); - } - } - } - } - - function handleNestedAndParsingFields(param, paramType) { - var result = handleArraysAndObjectFields(param, paramType); - result = handleNestedFieldsForAllParams(result, paramType); - return result; - } - - function tryParsingWithTypes(param, paramType) { - var result = Object.assign({}, param); - Object.keys(paramType).forEach(function (key) { - tryParsingAsType(result, key, paramType[key]); - }); - return result; - } - - // Converts path params in the {param} format to the accepted :param format, used before inserting the URL params. - function convertPathParams(url) { - return url.replace(/{(.+?)}/g, ':$1'); - } - - function setLogger(logger) { - log = logger; - } - - return { - handleNestedAndParsingFields, - convertPathParams, - tryParsingWithTypes, - setLogger - }; -}); diff --git a/apidoc/vendor/bootstrap.min.css b/apidoc/vendor/bootstrap.min.css deleted file mode 100644 index 5b96335f..00000000 --- a/apidoc/vendor/bootstrap.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Bootstrap v3.4.1 (https://getbootstrap.com/) - * Copyright 2011-2019 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:"Glyphicons Halflings";src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format("embedded-opentype"),url(../fonts/glyphicons-halflings-regular.woff2) format("woff2"),url(../fonts/glyphicons-halflings-regular.woff) format("woff"),url(../fonts/glyphicons-halflings-regular.ttf) format("truetype"),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*=col-]{padding-right:0;padding-left:0}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],.input-group-sm input[type=time],input[type=date].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm,input[type=time].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],.input-group-lg input[type=time],input[type=date].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg,input[type=time].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;background-image:none;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;background-image:none;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;background-image:none;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;background-image:none;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:14px;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;filter:alpha(opacity=90);opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/apidoc/vendor/bootstrap.min.js b/apidoc/vendor/bootstrap.min.js deleted file mode 100644 index 6290896a..00000000 --- a/apidoc/vendor/bootstrap.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Bootstrap v3.4.1 (https://getbootstrap.com/) - * Copyright 2011-2019 Twitter, Inc. - * Licensed under the MIT license - */ -if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");!function(t){"use strict";var e=jQuery.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1||3this.$items.length-1||t<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){e.to(t)}):i==t?this.pause().cycle():this.slide(idocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&t?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!t?this.scrollbarWidth:""})},s.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},s.prototype.checkScrollbar=function(){var t=window.innerWidth;if(!t){var e=document.documentElement.getBoundingClientRect();t=e.right-Math.abs(e.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0},sanitize:!0,sanitizeFn:null,whiteList:t},m.prototype.init=function(t,e,i){if(this.enabled=!0,this.type=t,this.$element=g(e),this.options=this.getOptions(i),this.$viewport=this.options.viewport&&g(document).find(g.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var o=this.options.trigger.split(" "),n=o.length;n--;){var s=o[n];if("click"==s)this.$element.on("click."+this.type,this.options.selector,g.proxy(this.toggle,this));else if("manual"!=s){var a="hover"==s?"mouseenter":"focusin",r="hover"==s?"mouseleave":"focusout";this.$element.on(a+"."+this.type,this.options.selector,g.proxy(this.enter,this)),this.$element.on(r+"."+this.type,this.options.selector,g.proxy(this.leave,this))}}this.options.selector?this._options=g.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},m.prototype.getDefaults=function(){return m.DEFAULTS},m.prototype.getOptions=function(t){var e=this.$element.data();for(var i in e)e.hasOwnProperty(i)&&-1!==g.inArray(i,o)&&delete e[i];return(t=g.extend({},this.getDefaults(),e,t)).delay&&"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),t.sanitize&&(t.template=n(t.template,t.whiteList,t.sanitizeFn)),t},m.prototype.getDelegateOptions=function(){var i={},o=this.getDefaults();return this._options&&g.each(this._options,function(t,e){o[t]!=e&&(i[t]=e)}),i},m.prototype.enter=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusin"==t.type?"focus":"hover"]=!0),e.tip().hasClass("in")||"in"==e.hoverState)e.hoverState="in";else{if(clearTimeout(e.timeout),e.hoverState="in",!e.options.delay||!e.options.delay.show)return e.show();e.timeout=setTimeout(function(){"in"==e.hoverState&&e.show()},e.options.delay.show)}},m.prototype.isInStateTrue=function(){for(var t in this.inState)if(this.inState[t])return!0;return!1},m.prototype.leave=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusout"==t.type?"focus":"hover"]=!1),!e.isInStateTrue()){if(clearTimeout(e.timeout),e.hoverState="out",!e.options.delay||!e.options.delay.hide)return e.hide();e.timeout=setTimeout(function(){"out"==e.hoverState&&e.hide()},e.options.delay.hide)}},m.prototype.show=function(){var t=g.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(t);var e=g.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(t.isDefaultPrevented()||!e)return;var i=this,o=this.tip(),n=this.getUID(this.type);this.setContent(),o.attr("id",n),this.$element.attr("aria-describedby",n),this.options.animation&&o.addClass("fade");var s="function"==typeof this.options.placement?this.options.placement.call(this,o[0],this.$element[0]):this.options.placement,a=/\s?auto?\s?/i,r=a.test(s);r&&(s=s.replace(a,"")||"top"),o.detach().css({top:0,left:0,display:"block"}).addClass(s).data("bs."+this.type,this),this.options.container?o.appendTo(g(document).find(this.options.container)):o.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var l=this.getPosition(),h=o[0].offsetWidth,d=o[0].offsetHeight;if(r){var p=s,c=this.getPosition(this.$viewport);s="bottom"==s&&l.bottom+d>c.bottom?"top":"top"==s&&l.top-dc.width?"left":"left"==s&&l.left-ha.top+a.height&&(n.top=a.top+a.height-l)}else{var h=e.left-s,d=e.left+s+i;ha.right&&(n.left=a.left+a.width-d)}return n},m.prototype.getTitle=function(){var t=this.$element,e=this.options;return t.attr("data-original-title")||("function"==typeof e.title?e.title.call(t[0]):e.title)},m.prototype.getUID=function(t){for(;t+=~~(1e6*Math.random()),document.getElementById(t););return t},m.prototype.tip=function(){if(!this.$tip&&(this.$tip=g(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},m.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},m.prototype.enable=function(){this.enabled=!0},m.prototype.disable=function(){this.enabled=!1},m.prototype.toggleEnabled=function(){this.enabled=!this.enabled},m.prototype.toggle=function(t){var e=this;t&&((e=g(t.currentTarget).data("bs."+this.type))||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e))),t?(e.inState.click=!e.inState.click,e.isInStateTrue()?e.enter(e):e.leave(e)):e.tip().hasClass("in")?e.leave(e):e.enter(e)},m.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide(function(){t.$element.off("."+t.type).removeData("bs."+t.type),t.$tip&&t.$tip.detach(),t.$tip=null,t.$arrow=null,t.$viewport=null,t.$element=null})},m.prototype.sanitizeHtml=function(t){return n(t,this.options.whiteList,this.options.sanitizeFn)};var e=g.fn.tooltip;g.fn.tooltip=function i(o){return this.each(function(){var t=g(this),e=t.data("bs.tooltip"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.tooltip",e=new m(this,i)),"string"==typeof o&&e[o]())})},g.fn.tooltip.Constructor=m,g.fn.tooltip.noConflict=function(){return g.fn.tooltip=e,this}}(jQuery),function(n){"use strict";var s=function(t,e){this.init("popover",t,e)};if(!n.fn.tooltip)throw new Error("Popover requires tooltip.js");s.VERSION="3.4.1",s.DEFAULTS=n.extend({},n.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),((s.prototype=n.extend({},n.fn.tooltip.Constructor.prototype)).constructor=s).prototype.getDefaults=function(){return s.DEFAULTS},s.prototype.setContent=function(){var t=this.tip(),e=this.getTitle(),i=this.getContent();if(this.options.html){var o=typeof i;this.options.sanitize&&(e=this.sanitizeHtml(e),"string"===o&&(i=this.sanitizeHtml(i))),t.find(".popover-title").html(e),t.find(".popover-content").children().detach().end()["string"===o?"html":"append"](i)}else t.find(".popover-title").text(e),t.find(".popover-content").children().detach().end().text(i);t.removeClass("fade top bottom left right in"),t.find(".popover-title").html()||t.find(".popover-title").hide()},s.prototype.hasContent=function(){return this.getTitle()||this.getContent()},s.prototype.getContent=function(){var t=this.$element,e=this.options;return t.attr("data-content")||("function"==typeof e.content?e.content.call(t[0]):e.content)},s.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var t=n.fn.popover;n.fn.popover=function e(o){return this.each(function(){var t=n(this),e=t.data("bs.popover"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.popover",e=new s(this,i)),"string"==typeof o&&e[o]())})},n.fn.popover.Constructor=s,n.fn.popover.noConflict=function(){return n.fn.popover=t,this}}(jQuery),function(s){"use strict";function n(t,e){this.$body=s(document.body),this.$scrollElement=s(t).is(document.body)?s(window):s(t),this.options=s.extend({},n.DEFAULTS,e),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",s.proxy(this.process,this)),this.refresh(),this.process()}function e(o){return this.each(function(){var t=s(this),e=t.data("bs.scrollspy"),i="object"==typeof o&&o;e||t.data("bs.scrollspy",e=new n(this,i)),"string"==typeof o&&e[o]()})}n.VERSION="3.4.1",n.DEFAULTS={offset:10},n.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},n.prototype.refresh=function(){var t=this,o="offset",n=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),s.isWindow(this.$scrollElement[0])||(o="position",n=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var t=s(this),e=t.data("target")||t.attr("href"),i=/^#./.test(e)&&s(e);return i&&i.length&&i.is(":visible")&&[[i[o]().top+n,e]]||null}).sort(function(t,e){return t[0]-e[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},n.prototype.process=function(){var t,e=this.$scrollElement.scrollTop()+this.options.offset,i=this.getScrollHeight(),o=this.options.offset+i-this.$scrollElement.height(),n=this.offsets,s=this.targets,a=this.activeTarget;if(this.scrollHeight!=i&&this.refresh(),o<=e)return a!=(t=s[s.length-1])&&this.activate(t);if(a&&e=n[t]&&(n[t+1]===undefined||e .active"),n=i&&r.support.transition&&(o.length&&o.hasClass("fade")||!!e.find("> .fade").length);function s(){o.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),t.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),n?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu").length&&t.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),i&&i()}o.length&&n?o.one("bsTransitionEnd",s).emulateTransitionEnd(a.TRANSITION_DURATION):s(),o.removeClass("in")};var t=r.fn.tab;r.fn.tab=e,r.fn.tab.Constructor=a,r.fn.tab.noConflict=function(){return r.fn.tab=t,this};var i=function(t){t.preventDefault(),e.call(r(this),"show")};r(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',i).on("click.bs.tab.data-api",'[data-toggle="pill"]',i)}(jQuery),function(l){"use strict";var h=function(t,e){this.options=l.extend({},h.DEFAULTS,e);var i=this.options.target===h.DEFAULTS.target?l(this.options.target):l(document).find(this.options.target);this.$target=i.on("scroll.bs.affix.data-api",l.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",l.proxy(this.checkPositionWithEventLoop,this)),this.$element=l(t),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};function i(o){return this.each(function(){var t=l(this),e=t.data("bs.affix"),i="object"==typeof o&&o;e||t.data("bs.affix",e=new h(this,i)),"string"==typeof o&&e[o]()})}h.VERSION="3.4.1",h.RESET="affix affix-top affix-bottom",h.DEFAULTS={offset:0,target:window},h.prototype.getState=function(t,e,i,o){var n=this.$target.scrollTop(),s=this.$element.offset(),a=this.$target.height();if(null!=i&&"top"==this.affixed)return n=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[[0,a]]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b);c=a.substring(0,f);a=a.substring(f);b=b.substring(f);var f=this.diff_commonSuffix(a,b),g=a.substring(a.length-f);a=a.substring(0,a.length-f);b=b.substring(0,b.length-f);a=this.diff_compute_(a, -b,e,d);c&&a.unshift([0,c]);g&&a.push([0,g]);this.diff_cleanupMerge(a);return a}; -diff_match_patch.prototype.diff_compute_=function(a,b,c,d){if(!a)return[[1,b]];if(!b)return[[-1,a]];var e=a.length>b.length?a:b,f=a.length>b.length?b:a,g=e.indexOf(f);return-1!=g?(c=[[1,e.substring(0,g)],[0,f],[1,e.substring(g+f.length)]],a.length>b.length&&(c[0][0]=c[2][0]=-1),c):1==f.length?[[-1,a],[1,b]]:(e=this.diff_halfMatch_(a,b))?(f=e[0],a=e[1],g=e[2],b=e[3],e=e[4],f=this.diff_main(f,g,c,d),c=this.diff_main(a,b,c,d),f.concat([[0,e]],c)):c&&100c);v++){for(var n=-v+r;n<=v-t;n+=2){var l=g+n,m;m=n==-v||n!=v&&j[l-1]d)t+=2;else if(s>e)r+=2;else if(q&&(l=g+k-n,0<=l&&l= -u)return this.diff_bisectSplit_(a,b,m,s,c)}}for(n=-v+p;n<=v-w;n+=2){l=g+n;u=n==-v||n!=v&&i[l-1]d)w+=2;else if(m>e)p+=2;else if(!q&&(l=g+k-n,0<=l&&(l=u)))return this.diff_bisectSplit_(a,b,m,s,c)}}return[[-1,a],[1,b]]}; -diff_match_patch.prototype.diff_bisectSplit_=function(a,b,c,d,e){var f=a.substring(0,c),g=b.substring(0,d);a=a.substring(c);b=b.substring(d);f=this.diff_main(f,g,!1,e);e=this.diff_main(a,b,!1,e);return f.concat(e)}; -diff_match_patch.prototype.diff_linesToChars_=function(a,b){function c(a){for(var b="",c=0,f=-1,g=d.length;fd?a=a.substring(c-d):c=a.length?[h,j,n,l,g]:null}if(0>=this.Diff_Timeout)return null; -var d=a.length>b.length?a:b,e=a.length>b.length?b:a;if(4>d.length||2*e.lengthd[4].length?g:d:d:g;var j;a.length>b.length?(g=h[0],d=h[1],e=h[2],j=h[3]):(e=h[0],j=h[1],g=h[2],d=h[3]);h=h[4];return[g,d,e,j,h]}; -diff_match_patch.prototype.diff_cleanupSemantic=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=0,h=0,j=0,i=0;f=e){if(d>=b.length/2||d>=c.length/2)a.splice(f,0,[0,c.substring(0,d)]),a[f-1][1]=b.substring(0,b.length-d),a[f+1][1]=c.substring(d),f++}else if(e>=b.length/2||e>=c.length/2)a.splice(f,0,[0,b.substring(0,e)]),a[f-1][0]=1,a[f-1][1]=c.substring(0,c.length-e),a[f+1][0]=-1,a[f+1][1]=b.substring(e),f++;f++}f++}}; -diff_match_patch.prototype.diff_cleanupSemanticLossless=function(a){function b(a,b){if(!a||!b)return 6;var c=a.charAt(a.length-1),d=b.charAt(0),e=c.match(diff_match_patch.nonAlphaNumericRegex_),f=d.match(diff_match_patch.nonAlphaNumericRegex_),g=e&&c.match(diff_match_patch.whitespaceRegex_),h=f&&d.match(diff_match_patch.whitespaceRegex_),c=g&&c.match(diff_match_patch.linebreakRegex_),d=h&&d.match(diff_match_patch.linebreakRegex_),i=c&&a.match(diff_match_patch.blanklineEndRegex_),j=d&&b.match(diff_match_patch.blanklineStartRegex_); -return i||j?5:c||d?4:e&&!g&&h?3:g||h?2:e||f?1:0}for(var c=1;c=i&&(i=k,g=d,h=e,j=f)}a[c-1][1]!=g&&(g?a[c-1][1]=g:(a.splice(c-1,1),c--),a[c][1]= -h,j?a[c+1][1]=j:(a.splice(c+1,1),c--))}c++}};diff_match_patch.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/;diff_match_patch.whitespaceRegex_=/\s/;diff_match_patch.linebreakRegex_=/[\r\n]/;diff_match_patch.blanklineEndRegex_=/\n\r?\n$/;diff_match_patch.blanklineStartRegex_=/^\r?\n\r?\n/; -diff_match_patch.prototype.diff_cleanupEfficiency=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=!1,h=!1,j=!1,i=!1;fb)break;e=c;f=d}return a.length!=g&&-1===a[g][0]?f:f+(b-e)}; -diff_match_patch.prototype.diff_prettyHtml=function(a){for(var b=[],c=/&/g,d=//g,f=/\n/g,g=0;g");switch(h){case 1:b[g]=''+j+"";break;case -1:b[g]=''+j+"";break;case 0:b[g]=""+j+""}}return b.join("")}; -diff_match_patch.prototype.diff_text1=function(a){for(var b=[],c=0;cthis.Match_MaxBits)throw Error("Pattern too long for this browser.");var e=this.match_alphabet_(b),f=this,g=this.Match_Threshold,h=a.indexOf(b,c);-1!=h&&(g=Math.min(d(0,h),g),h=a.lastIndexOf(b,c+b.length),-1!=h&&(g=Math.min(d(0,h),g)));for(var j=1<=i;p--){var w=e[a.charAt(p-1)];k[p]=0===t?(k[p+1]<<1|1)&w:(k[p+1]<<1|1)&w|((r[p+1]|r[p])<<1|1)|r[p+1];if(k[p]&j&&(w=d(t,p-1),w<=g))if(g=w,h=p-1,h>c)i=Math.max(1,2*c-h);else break}if(d(t+1,c)>g)break;r=k}return h}; -diff_match_patch.prototype.match_alphabet_=function(a){for(var b={},c=0;c=2*this.Patch_Margin&& -e&&(this.patch_addContext_(a,h),c.push(a),a=new diff_match_patch.patch_obj,e=0,h=d,f=g)}1!==i&&(f+=k.length);-1!==i&&(g+=k.length)}e&&(this.patch_addContext_(a,h),c.push(a));return c};diff_match_patch.prototype.patch_deepCopy=function(a){for(var b=[],c=0;cthis.Match_MaxBits){if(j=this.match_main(b,h.substring(0,this.Match_MaxBits),g),-1!=j&&(i=this.match_main(b,h.substring(h.length-this.Match_MaxBits),g+h.length-this.Match_MaxBits),-1==i||j>=i))j=-1}else j=this.match_main(b,h,g); -if(-1==j)e[f]=!1,d-=a[f].length2-a[f].length1;else if(e[f]=!0,d=j-g,g=-1==i?b.substring(j,j+h.length):b.substring(j,i+this.Match_MaxBits),h==g)b=b.substring(0,j)+this.diff_text2(a[f].diffs)+b.substring(j+h.length);else if(g=this.diff_main(h,g,!1),h.length>this.Match_MaxBits&&this.diff_levenshtein(g)/h.length>this.Patch_DeleteThreshold)e[f]=!1;else{this.diff_cleanupSemanticLossless(g);for(var h=0,k,i=0;ie[0][1].length){var f=b-e[0][1].length;e[0][1]=c.substring(e[0][1].length)+e[0][1];d.start1-=f;d.start2-=f;d.length1+=f;d.length2+=f}d=a[a.length-1];e=d.diffs;0==e.length||0!=e[e.length-1][0]?(e.push([0, -c]),d.length1+=b,d.length2+=b):b>e[e.length-1][1].length&&(f=b-e[e.length-1][1].length,e[e.length-1][1]+=c.substring(0,f),d.length1+=f,d.length2+=f);return c}; -diff_match_patch.prototype.patch_splitMax=function(a){for(var b=this.Match_MaxBits,c=0;c2*b?(h.length1+=i.length,e+=i.length,j=!1,h.diffs.push([g,i]),d.diffs.shift()):(i=i.substring(0,b-h.length1-this.Patch_Margin),h.length1+=i.length,e+=i.length,0===g?(h.length2+=i.length,f+=i.length):j=!1,h.diffs.push([g,i]),i==d.diffs[0][1]?d.diffs.shift():d.diffs[0][1]=d.diffs[0][1].substring(i.length))}g=this.diff_text2(h.diffs);g=g.substring(g.length-this.Patch_Margin);i=this.diff_text1(d.diffs).substring(0,this.Patch_Margin);""!==i&& -(h.length1+=i.length,h.length2+=i.length,0!==h.diffs.length&&0===h.diffs[h.diffs.length-1][0]?h.diffs[h.diffs.length-1][1]+=i:h.diffs.push([0,i]));j||a.splice(++c,0,h)}}};diff_match_patch.prototype.patch_toText=function(a){for(var b=[],c=0;c= 2.0.0-beta.1",7:">= 4.0.0 <4.3.0",8:">= 4.3.0"};b.REVISION_CHANGES=q;var r="[object Object]";d.prototype={constructor:d,logger:l["default"],log:l["default"].log,registerHelper:function(a,b){if(f.toString.call(a)===r){if(b)throw new h["default"]("Arg not supported with multiple helpers");f.extend(this.helpers,a)}else this.helpers[a]=b},unregisterHelper:function(a){delete this.helpers[a]},registerPartial:function(a,b){if(f.toString.call(a)===r)f.extend(this.partials,a);else{if("undefined"==typeof b)throw new h["default"]('Attempting to register a partial called "'+a+'" as undefined');this.partials[a]=b}},unregisterPartial:function(a){delete this.partials[a]},registerDecorator:function(a,b){if(f.toString.call(a)===r){if(b)throw new h["default"]("Arg not supported with multiple decorators");f.extend(this.decorators,a)}else this.decorators[a]=b},unregisterDecorator:function(a){delete this.decorators[a]},resetLoggedPropertyAccesses:function(){m.resetLoggedProperties()}};var s=l["default"].log;b.log=s,b.createFrame=f.createFrame,b.logger=l["default"]},function(a,b){"use strict";function c(a){return k[a]}function d(a){for(var b=1;b":">",'"':""","'":"'","`":"`","=":"="},l=/[&<>"'`=]/g,m=/[&<>"'`=]/,n=Object.prototype.toString;b.toString=n;var o=function(a){return"function"==typeof a};o(/x/)&&(b.isFunction=o=function(a){return"function"==typeof a&&"[object Function]"===n.call(a)}),b.isFunction=o;var p=Array.isArray||function(a){return!(!a||"object"!=typeof a)&&"[object Array]"===n.call(a)};b.isArray=p},function(a,b,c){"use strict";function d(a,b){var c=b&&b.loc,g=void 0,h=void 0,i=void 0,j=void 0;c&&(g=c.start.line,h=c.end.line,i=c.start.column,j=c.end.column,a+=" - "+g+":"+i);for(var k=Error.prototype.constructor.call(this,a),l=0;l0?(c.ids&&(c.ids=[c.name]),a.helpers.each(b,c)):e(this);if(c.data&&c.ids){var g=d.createFrame(c.data);g.contextPath=d.appendContextPath(c.data.contextPath,c.name),c={data:g}}return f(b,c)})},a.exports=b["default"]},function(a,b,c){(function(d){"use strict";var e=c(13)["default"],f=c(1)["default"];b.__esModule=!0;var g=c(5),h=c(6),i=f(h);b["default"]=function(a){a.registerHelper("each",function(a,b){function c(b,c,d){l&&(l.key=b,l.index=c,l.first=0===c,l.last=!!d,m&&(l.contextPath=m+b)),k+=f(a[b],{data:l,blockParams:g.blockParams([a[b],b],[m+b,null])})}if(!b)throw new i["default"]("Must pass iterator to #each");var f=b.fn,h=b.inverse,j=0,k="",l=void 0,m=void 0;if(b.data&&b.ids&&(m=g.appendContextPath(b.data.contextPath,b.ids[0])+"."),g.isFunction(a)&&(a=a.call(this)),b.data&&(l=g.createFrame(b.data)),a&&"object"==typeof a)if(g.isArray(a))for(var n=a.length;j=0?b:parseInt(a,10)}return a},log:function(a){if(a=e.lookupLevel(a),"undefined"!=typeof console&&e.lookupLevel(e.level)<=a){var b=e.methodMap[a];console[b]||(b="log");for(var c=arguments.length,d=Array(c>1?c-1:0),f=1;f=v.LAST_COMPATIBLE_COMPILER_REVISION&&b<=v.COMPILER_REVISION)){if(b2&&v.push("'"+this.terminals_[s]+"'");x=this.lexer.showPosition?"Parse error on line "+(i+1)+":\n"+this.lexer.showPosition()+"\nExpecting "+v.join(", ")+", got '"+(this.terminals_[n]||n)+"'":"Parse error on line "+(i+1)+": Unexpected "+(1==n?"end of input":"'"+(this.terminals_[n]||n)+"'"),this.parseError(x,{text:this.lexer.match,token:this.terminals_[n]||n,line:this.lexer.yylineno,loc:l,expected:v})}}if(q[0]instanceof Array&&q.length>1)throw new Error("Parse Error: multiple actions possible at state: "+p+", token: "+n);switch(q[0]){case 1:d.push(n),e.push(this.lexer.yytext),f.push(this.lexer.yylloc),d.push(q[1]),n=null,o?(n=o,o=null):(j=this.lexer.yyleng,h=this.lexer.yytext,i=this.lexer.yylineno,l=this.lexer.yylloc,k>0&&k--);break;case 2:if(t=this.productions_[q[1]][1],w.$=e[e.length-t],w._$={first_line:f[f.length-(t||1)].first_line,last_line:f[f.length-1].last_line,first_column:f[f.length-(t||1)].first_column,last_column:f[f.length-1].last_column},m&&(w._$.range=[f[f.length-(t||1)].range[0],f[f.length-1].range[1]]),r=this.performAction.call(w,h,j,i,this.yy,q[1],e,f),"undefined"!=typeof r)return r;t&&(d=d.slice(0,-1*t*2),e=e.slice(0,-1*t),f=f.slice(0,-1*t)),d.push(this.productions_[q[1]][0]),e.push(w.$),f.push(w._$),u=g[d[d.length-2]][d[d.length-1]],d.push(u);break;case 3:return!0}}return!0}},c=function(){var a={EOF:1,parseError:function(a,b){if(!this.yy.parser)throw new Error(a);this.yy.parser.parseError(a,b)},setInput:function(a){return this._input=a,this._more=this._less=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var a=this._input[0];this.yytext+=a,this.yyleng++,this.offset++,this.match+=a,this.matched+=a;var b=a.match(/(?:\r\n?|\n).*/g);return b?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),a},unput:function(a){var b=a.length,c=a.split(/(?:\r\n?|\n)/g);this._input=a+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-b-1),this.offset-=b;var d=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),c.length-1&&(this.yylineno-=c.length-1);var e=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:c?(c.length===d.length?this.yylloc.first_column:0)+d[d.length-c.length].length-c[0].length:this.yylloc.first_column-b},this.options.ranges&&(this.yylloc.range=[e[0],e[0]+this.yyleng-b]),this},more:function(){return this._more=!0,this},less:function(a){this.unput(this.match.slice(a))},pastInput:function(){var a=this.matched.substr(0,this.matched.length-this.match.length);return(a.length>20?"...":"")+a.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var a=this.match;return a.length<20&&(a+=this._input.substr(0,20-a.length)),(a.substr(0,20)+(a.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var a=this.pastInput(),b=new Array(a.length+1).join("-");return a+this.upcomingInput()+"\n"+b+"^"},next:function(){if(this.done)return this.EOF;this._input||(this.done=!0);var a,b,c,d,e;this._more||(this.yytext="",this.match="");for(var f=this._currentRules(),g=0;gb[0].length)||(b=c,d=g,this.options.flex));g++);return b?(e=b[0].match(/(?:\r\n?|\n).*/g),e&&(this.yylineno+=e.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:e?e[e.length-1].length-e[e.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+b[0].length},this.yytext+=b[0],this.match+=b[0],this.matches=b,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._input=this._input.slice(b[0].length),this.matched+=b[0],a=this.performAction.call(this,this.yy,this,f[d],this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),a?a:void 0):""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){var a=this.next();return"undefined"!=typeof a?a:this.lex()},begin:function(a){this.conditionStack.push(a)},popState:function(){return this.conditionStack.pop()},_currentRules:function(){return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules},topState:function(){return this.conditionStack[this.conditionStack.length-2]},pushState:function(a){this.begin(a)}};return a.options={},a.performAction=function(a,b,c,d){function e(a,c){return b.yytext=b.yytext.substring(a,b.yyleng-c+a)}switch(c){case 0:if("\\\\"===b.yytext.slice(-2)?(e(0,1),this.begin("mu")):"\\"===b.yytext.slice(-1)?(e(0,1),this.begin("emu")):this.begin("mu"),b.yytext)return 15;break;case 1:return 15;case 2:return this.popState(),15;case 3:return this.begin("raw"),15;case 4:return this.popState(),"raw"===this.conditionStack[this.conditionStack.length-1]?15:(e(5,9),"END_RAW_BLOCK");case 5:return 15;case 6:return this.popState(),14;case 7:return 65;case 8:return 68;case 9:return 19;case 10:return this.popState(),this.begin("raw"),23;case 11:return 55;case 12:return 60;case 13:return 29;case 14:return 47;case 15:return this.popState(),44;case 16:return this.popState(),44;case 17:return 34;case 18:return 39;case 19:return 51;case 20:return 48;case 21:this.unput(b.yytext),this.popState(),this.begin("com");break;case 22:return this.popState(),14;case 23:return 48;case 24:return 73;case 25:return 72;case 26:return 72;case 27:return 87;case 28:break;case 29:return this.popState(),54;case 30:return this.popState(),33;case 31:return b.yytext=e(1,2).replace(/\\"/g,'"'),80;case 32:return b.yytext=e(1,2).replace(/\\'/g,"'"),80;case 33:return 85;case 34:return 82;case 35:return 82;case 36:return 83;case 37:return 84;case 38:return 81;case 39:return 75;case 40:return 77;case 41:return 72;case 42:return b.yytext=b.yytext.replace(/\\([\\\]])/g,"$1"),72;case 43:return"INVALID";case 44:return 5}},a.rules=[/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|\\\{\{|\\\\\{\{|$)))/,/^(?:\{\{\{\{(?=[^\/]))/,/^(?:\{\{\{\{\/[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.])\}\}\}\})/,/^(?:[^\x00]+?(?=(\{\{\{\{)))/,/^(?:[\s\S]*?--(~)?\}\})/,/^(?:\()/,/^(?:\))/,/^(?:\{\{\{\{)/,/^(?:\}\}\}\})/,/^(?:\{\{(~)?>)/,/^(?:\{\{(~)?#>)/,/^(?:\{\{(~)?#\*?)/,/^(?:\{\{(~)?\/)/,/^(?:\{\{(~)?\^\s*(~)?\}\})/,/^(?:\{\{(~)?\s*else\s*(~)?\}\})/,/^(?:\{\{(~)?\^)/,/^(?:\{\{(~)?\s*else\b)/,/^(?:\{\{(~)?\{)/,/^(?:\{\{(~)?&)/,/^(?:\{\{(~)?!--)/,/^(?:\{\{(~)?![\s\S]*?\}\})/,/^(?:\{\{(~)?\*?)/,/^(?:=)/,/^(?:\.\.)/,/^(?:\.(?=([=~}\s\/.)|])))/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}(~)?\}\})/,/^(?:(~)?\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=([~}\s)])))/,/^(?:false(?=([~}\s)])))/,/^(?:undefined(?=([~}\s)])))/,/^(?:null(?=([~}\s)])))/,/^(?:-?[0-9]+(?:\.[0-9]+)?(?=([~}\s)])))/,/^(?:as\s+\|)/,/^(?:\|)/,/^(?:([^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=([=~}\s\/.)|]))))/,/^(?:\[(\\\]|[^\]])*\])/,/^(?:.)/,/^(?:$)/],a.conditions={mu:{rules:[7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44],inclusive:!1},emu:{rules:[2],inclusive:!1},com:{rules:[6],inclusive:!1},raw:{rules:[3,4,5],inclusive:!1},INITIAL:{rules:[0,1,44],inclusive:!0}},a}();return b.lexer=c,a.prototype=b,b.Parser=a,new a}();b["default"]=c,a.exports=b["default"]},function(a,b,c){"use strict";function d(){var a=arguments.length<=0||void 0===arguments[0]?{}:arguments[0];this.options=a}function e(a,b,c){void 0===b&&(b=a.length);var d=a[b-1],e=a[b-2];return d?"ContentStatement"===d.type?(e||!c?/\r?\n\s*?$/:/(^|\r?\n)\s*?$/).test(d.original):void 0:c}function f(a,b,c){void 0===b&&(b=-1);var d=a[b+1],e=a[b+2];return d?"ContentStatement"===d.type?(e||!c?/^\s*?\r?\n/:/^\s*?(\r?\n|$)/).test(d.original):void 0:c}function g(a,b,c){var d=a[null==b?0:b+1];if(d&&"ContentStatement"===d.type&&(c||!d.rightStripped)){var e=d.value;d.value=d.value.replace(c?/^\s+/:/^[ \t]*\r?\n?/,""),d.rightStripped=d.value!==e}}function h(a,b,c){var d=a[null==b?a.length-1:b-1];if(d&&"ContentStatement"===d.type&&(c||!d.leftStripped)){var e=d.value;return d.value=d.value.replace(c?/\s+$/:/[ \t]+$/,""),d.leftStripped=d.value!==e,d.leftStripped}}var i=c(1)["default"];b.__esModule=!0;var j=c(49),k=i(j);d.prototype=new k["default"],d.prototype.Program=function(a){var b=!this.options.ignoreStandalone,c=!this.isRootSeen;this.isRootSeen=!0;for(var d=a.body,i=0,j=d.length;i0)throw new q["default"]("Invalid path: "+d,{loc:c});".."===i&&f++}}return{type:"PathExpression",data:a,depth:f,parts:e,original:d,loc:c}}function j(a,b,c,d,e,f){var g=d.charAt(3)||d.charAt(2),h="{"!==g&&"&"!==g,i=/\*/.test(d);return{type:i?"Decorator":"MustacheStatement",path:a,params:b,hash:c,escaped:h,strip:e,loc:this.locInfo(f)}}function k(a,b,c,e){d(a,c),e=this.locInfo(e);var f={type:"Program",body:b,strip:{},loc:e};return{type:"BlockStatement",path:a.path,params:a.params,hash:a.hash,program:f,openStrip:{},inverseStrip:{},closeStrip:{},loc:e}}function l(a,b,c,e,f,g){e&&e.path&&d(a,e);var h=/\*/.test(a.open);b.blockParams=a.blockParams;var i=void 0,j=void 0;if(c){if(h)throw new q["default"]("Unexpected inverse block on decorator",c);c.chain&&(c.program.body[0].closeStrip=e.strip),j=c.strip,i=c.program}return f&&(f=i,i=b,b=f),{type:h?"DecoratorBlock":"BlockStatement",path:a.path,params:a.params,hash:a.hash,program:b,inverse:i,openStrip:a.strip,inverseStrip:j,closeStrip:e&&e.strip,loc:this.locInfo(g)}}function m(a,b){if(!b&&a.length){var c=a[0].loc,d=a[a.length-1].loc;c&&d&&(b={source:c.source,start:{line:c.start.line,column:c.start.column},end:{line:d.end.line,column:d.end.column}})}return{type:"Program",body:a,strip:{},loc:b}}function n(a,b,c,e){return d(a,c),{type:"PartialBlockStatement",name:a.path,params:a.params,hash:a.hash,program:b,openStrip:a.strip,closeStrip:c&&c.strip,loc:this.locInfo(e)}}var o=c(1)["default"];b.__esModule=!0,b.SourceLocation=e,b.id=f,b.stripFlags=g,b.stripComment=h,b.preparePath=i,b.prepareMustache=j,b.prepareRawBlock=k,b.prepareBlock=l,b.prepareProgram=m,b.preparePartialBlock=n;var p=c(6),q=o(p)},function(a,b,c){"use strict";function d(){}function e(a,b,c){if(null==a||"string"!=typeof a&&"Program"!==a.type)throw new l["default"]("You must pass a string or Handlebars AST to Handlebars.precompile. You passed "+a);b=b||{},"data"in b||(b.data=!0),b.compat&&(b.useDepths=!0);var d=c.parse(a,b),e=(new c.Compiler).compile(d,b);return(new c.JavaScriptCompiler).compile(e,b)}function f(a,b,c){function d(){var d=c.parse(a,b),e=(new c.Compiler).compile(d,b),f=(new c.JavaScriptCompiler).compile(e,b,void 0,!0);return c.template(f)}function e(a,b){return f||(f=d()),f.call(this,a,b)}if(void 0===b&&(b={}),null==a||"string"!=typeof a&&"Program"!==a.type)throw new l["default"]("You must pass a string or Handlebars AST to Handlebars.compile. You passed "+a);b=m.extend({},b),"data"in b||(b.data=!0),b.compat&&(b.useDepths=!0);var f=void 0;return e._setup=function(a){return f||(f=d()),f._setup(a)},e._child=function(a,b,c,e){return f||(f=d()),f._child(a,b,c,e)},e}function g(a,b){if(a===b)return!0;if(m.isArray(a)&&m.isArray(b)&&a.length===b.length){for(var c=0;c1)throw new l["default"]("Unsupported number of partial arguments: "+c.length,a);c.length||(this.options.explicitPartialContext?this.opcode("pushLiteral","undefined"):c.push({type:"PathExpression",parts:[],depth:0});var d=a.name.original,e="SubExpression"===a.name.type;e&&this.accept(a.name),this.setupFullMustacheParams(a,b,void 0,!0);var f=a.indent||"";this.options.preventIndent&&f&&(this.opcode("appendContent",f),f=""),this.opcode("invokePartial",e,d,f),this.opcode("append")},PartialBlockStatement:function(a){this.PartialStatement(a)},MustacheStatement:function(a){this.SubExpression(a),a.escaped&&!this.options.noEscape?this.opcode("appendEscaped"):this.opcode("append")},Decorator:function(a){this.DecoratorBlock(a)},ContentStatement:function(a){a.value&&this.opcode("appendContent",a.value)},CommentStatement:function(){},SubExpression:function(a){h(a);var b=this.classifySexpr(a);"simple"===b?this.simpleSexpr(a):"helper"===b?this.helperSexpr(a):this.ambiguousSexpr(a)},ambiguousSexpr:function(a,b,c){var d=a.path,e=d.parts[0],f=null!=b||null!=c;this.opcode("getContext",d.depth),this.opcode("pushProgram",b),this.opcode("pushProgram",c),d.strict=!0,this.accept(d),this.opcode("invokeAmbiguous",e,f)},simpleSexpr:function(a){var b=a.path;b.strict=!0,this.accept(b),this.opcode("resolvePossibleLambda")},helperSexpr:function(a,b,c){var d=this.setupFullMustacheParams(a,b,c),e=a.path,f=e.parts[0];if(this.options.knownHelpers[f])this.opcode("invokeKnownHelper",d.length,f);else{if(this.options.knownHelpersOnly)throw new l["default"]("You specified knownHelpersOnly, but used the unknown helper "+f,a);e.strict=!0,e.falsy=!0,this.accept(e),this.opcode("invokeHelper",d.length,e.original,o["default"].helpers.simpleId(e))}},PathExpression:function(a){this.addDepth(a.depth),this.opcode("getContext",a.depth);var b=a.parts[0],c=o["default"].helpers.scopedId(a),d=!a.depth&&!c&&this.blockParamIndex(b);d?this.opcode("lookupBlockParam",d,a.parts):b?a.data?(this.options.data=!0,this.opcode("lookupData",a.depth,a.parts,a.strict)):this.opcode("lookupOnContext",a.parts,a.falsy,a.strict,c):this.opcode("pushContext")},StringLiteral:function(a){this.opcode("pushString",a.value)},NumberLiteral:function(a){this.opcode("pushLiteral",a.value)},BooleanLiteral:function(a){this.opcode("pushLiteral",a.value)},UndefinedLiteral:function(){this.opcode("pushLiteral","undefined")},NullLiteral:function(){this.opcode("pushLiteral","null")},Hash:function(a){var b=a.pairs,c=0,d=b.length;for(this.opcode("pushHash");c=0)return[b,e]}}}},function(a,b,c){"use strict";function d(a){this.value=a}function e(){}function f(a,b,c,d){var e=b.popStack(),f=0,g=c.length;for(a&&g--;f0&&(c+=", "+d.join(", "));var e=0;g(this.aliases).forEach(function(a){var d=b.aliases[a];d.children&&d.referenceCount>1&&(c+=", alias"+ ++e+"="+a,d.children[0]="alias"+e)}),this.lookupPropertyFunctionIsUsed&&(c+=", "+this.lookupPropertyFunctionVarDeclaration());var f=["container","depth0","helpers","partials","data"];(this.useBlockParams||this.useDepths)&&f.push("blockParams"),this.useDepths&&f.push("depths");var h=this.mergeSource(c);return a?(f.push(h),Function.apply(this,f)):this.source.wrap(["function(",f.join(","),") {\n ",h,"}"])},mergeSource:function(a){var b=this.environment.isSimple,c=!this.forceBuffer,d=void 0,e=void 0,f=void 0,g=void 0;return this.source.each(function(a){a.appendToBuffer?(f?a.prepend(" + "):f=a,g=a):(f&&(e?f.prepend("buffer += "):d=!0,g.add(";"),f=g=void 0),e=!0,b||(c=!1))}),c?f?(f.prepend("return "),g.add(";")):e||this.source.push('return "";'):(a+=", buffer = "+(d?"":this.initializeBuffer()),f?(f.prepend("return buffer + "),g.add(";")):this.source.push("return buffer;")),a&&this.source.prepend("var "+a.substring(2)+(d?"":";\n")),this.source.merge()},lookupPropertyFunctionVarDeclaration:function(){return"\n lookupProperty = container.lookupProperty || function(parent, propertyName) {\n if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {\n return parent[propertyName];\n }\n return undefined\n }\n ".trim()},blockValue:function(a){var b=this.aliasable("container.hooks.blockHelperMissing"),c=[this.contextName(0)];this.setupHelperArgs(a,0,c);var d=this.popStack();c.splice(1,0,d),this.push(this.source.functionCall(b,"call",c))},ambiguousBlockValue:function(){var a=this.aliasable("container.hooks.blockHelperMissing"),b=[this.contextName(0)];this.setupHelperArgs("",0,b,!0),this.flushInline();var c=this.topStack();b.splice(1,0,c),this.pushSource(["if (!",this.lastHelper,") { ",c," = ",this.source.functionCall(a,"call",b),"}"])},appendContent:function(a){this.pendingContent?a=this.pendingContent+a:this.pendingLocation=this.source.currentLocation,this.pendingContent=a},append:function(){if(this.isInline())this.replaceStack(function(a){return[" != null ? ",a,' : ""']}),this.pushSource(this.appendToBuffer(this.popStack()));else{var a=this.popStack();this.pushSource(["if (",a," != null) { ",this.appendToBuffer(a,void 0,!0)," }"]),this.environment.isSimple&&this.pushSource(["else { ",this.appendToBuffer("''",void 0,!0)," }"])}},appendEscaped:function(){this.pushSource(this.appendToBuffer([this.aliasable("container.escapeExpression"),"(",this.popStack(),")"]))},getContext:function(a){this.lastContext=a},pushContext:function(){this.pushStackLiteral(this.contextName(this.lastContext))},lookupOnContext:function(a,b,c,d){var e=0;d||!this.options.compat||this.lastContext?this.pushContext():this.push(this.depthedLookup(a[e++])),this.resolvePath("context",a,e,b,c)},lookupBlockParam:function(a,b){this.useBlockParams=!0,this.push(["blockParams[",a[0],"][",a[1],"]"]),this.resolvePath("context",b,1)},lookupData:function(a,b,c){a?this.pushStackLiteral("container.data(data, "+a+")"):this.pushStackLiteral("data"),this.resolvePath("data",b,0,!0,c)},resolvePath:function(a,b,c,d,e){var g=this;if(this.options.strict||this.options.assumeObjects)return void this.push(f(this.options.strict&&e,this,b,a));for(var h=b.length;cthis.stackVars.length&&this.stackVars.push("stack"+this.stackSlot),this.topStackName()},topStackName:function(){return"stack"+this.stackSlot},flushInline:function(){var a=this.inlineStack;this.inlineStack=[];for(var b=0,c=a.length;b+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")}),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0r.page,h=new s(a[f],d,e),r.items.push(h),c.push(h)}return r.update(),c}},this.show=function(a,b){return this.i=a,this.page=b,r.update(),r},this.remove=function(a,b,c){for(var d=0,e=0,f=r.items.length;e-1&&c.splice(d,1),r},this.trigger=function(a){for(var b=r.handlers[a].length;b--;)r.handlers[a][b](r);return r},this.reset={filter:function(){for(var a=r.items,b=a.length;b--;)a[b].filtered=!1;return r},search:function(){for(var a=r.items,b=a.length;b--;)a[b].found=!1;return r}},this.update=function(){var a=r.items,b=a.length;r.visibleItems=[],r.matchingItems=[],r.templater.clear();for(var c=0;c=r.i&&r.visibleItems.length0?setTimeout(function(){b(c,d,e)},1):(a.update(),d(e))};return b}},{}],3:[function(a,b,c){b.exports=function(a){return a.handlers.filterStart=a.handlers.filterStart||[],a.handlers.filterComplete=a.handlers.filterComplete||[],function(b){if(a.trigger("filterStart"),a.i=1,a.reset.filter(),void 0===b)a.filtered=!1;else{a.filtered=!0;for(var c=a.items,d=0,e=c.length;d0?setTimeout(function(){f(a,c)},1):(b.update(),b.trigger("parseComplete"))};return b.handlers.parseComplete=b.handlers.parseComplete||[],function(){var a=d(b.list),c=b.valueNames;b.indexAsync?f(a,c):e(a,c)}}},{"./item":4}],6:[function(a,b,c){b.exports=function(a){var b,c,d,e,f={resetList:function(){a.i=1,a.templater.clear(),e=void 0},setOptions:function(a){2==a.length&&a[1]instanceof Array?c=a[1]:2==a.length&&"function"==typeof a[1]?(c=void 0,e=a[1]):3==a.length?(c=a[1],e=a[2]):c=void 0},setColumns:function(){0!==a.items.length&&void 0===c&&(c=void 0===a.searchColumns?f.toArray(a.items[0].values()):a.searchColumns)},setSearchString:function(b){b=a.utils.toString(b).toLowerCase(),b=b.replace(/[-[\]{}()*+?.,\\^$|#]/g,"\\$&"),d=b},toArray:function(a){var b=[];for(var c in a)b.push(c);return b}},g={list:function(){for(var b=0,c=a.items.length;b-1))},reset:function(){a.reset.search(),a.searched=!1}},h=function(b){return a.trigger("searchStart"),f.resetList(),f.setSearchString(b),f.setOptions(arguments),f.setColumns(),""===d?g.reset():(a.searched=!0,e?e(d,c):g.list()),a.update(),a.trigger("searchComplete"),a.visibleItems};return a.handlers.searchStart=a.handlers.searchStart||[],a.handlers.searchComplete=a.handlers.searchComplete||[],a.utils.events.bind(a.utils.getByClass(a.listContainer,a.searchClass),"keyup",function(b){var c=b.target||b.srcElement,d=""===c.value&&!a.searched;d||h(c.value)}),a.utils.events.bind(a.utils.getByClass(a.listContainer,a.searchClass),"input",function(a){var b=a.target||a.srcElement;""===b.value&&h("")}),h}},{}],7:[function(a,b,c){b.exports=function(a){a.sortFunction=a.sortFunction||function(b,c,d){return d.desc="desc"==d.order,a.utils.naturalSort(b.values()[d.valueName],c.values()[d.valueName],d)};var b={els:void 0,clear:function(){for(var c=0,d=b.els.length;c]/g.exec(b)){var f=document.createElement("tbody");return f.innerHTML=b,f.firstChild}if(b.indexOf("<")!==-1){var g=document.createElement("div");return g.innerHTML=b,g.firstChild}var h=document.getElementById(a.item);if(h)return h}},this.get=function(b,d){c.create(b);for(var e={},f=0,g=d.length;f=1;)a.list.removeChild(a.list.firstChild)},d()};b.exports=function(a){return new d(a)}},{}],9:[function(a,b,c){function d(a){if(!a||!a.nodeType)throw new Error("A DOM element reference is required");this.el=a,this.list=a.classList}var e=a("./index-of"),f=/\s+/,g=Object.prototype.toString;b.exports=function(a){return new d(a)},d.prototype.add=function(a){if(this.list)return this.list.add(a),this;var b=this.array(),c=e(b,a);return~c||b.push(a),this.el.className=b.join(" "),this},d.prototype.remove=function(a){if("[object RegExp]"==g.call(a))return this.removeMatching(a);if(this.list)return this.list.remove(a),this;var b=this.array(),c=e(b,a);return~c&&b.splice(c,1),this.el.className=b.join(" "),this},d.prototype.removeMatching=function(a){for(var b=this.array(),c=0;cs)return 1}for(var u=0,v=p.length,w=q.length,x=Math.max(v,w);ue)return 1}return 0}},{}],16:[function(a,b,c){function d(a){return"[object Array]"===Object.prototype.toString.call(a)}b.exports=function(a){if("undefined"==typeof a)return[];if(null===a)return[null];if(a===window)return[window];if("string"==typeof a)return[a];if(d(a))return a;if("number"!=typeof a.length)return[a];if("function"==typeof a&&a instanceof Function)return[a];for(var b=[],c=0;ca))return false; -if((f=u.get(t))&&u.get(e))return f==e;var f=-1,s=true,l=2&r?new y:Ut;for(u.set(t,e),u.set(e,t);++f=t}function vt(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}function gt(t){return null!=t&&typeof t=="object"}function dt(t){return typeof t=="number"||gt(t)&&"[object Number]"==k(t)}function At(t){return typeof t=="symbol"||gt(t)&&"[object Symbol]"==k(t)}function wt(t){return null==t?"":V(t)}function mt(t,e,r){return t=null==t?Ut:S(t,e),t===Ut?r:t}function Ot(t,e){var r;if(r=null!=t){r=t;var n;n=C(e,r);for(var o=-1,c=n.length,u=false;++ot)&&(t==e.length-1?e.pop():je.call(e,t,1),--this.size,true)},h.prototype.get=function(t){var e=this.__data__;return t=g(e,t),0>t?Ut:e[t][1]},h.prototype.has=function(t){return-1n?(++this.size,r.push([t,e])):r[n][1]=e,this},p.prototype.clear=function(){this.size=0,this.__data__={hash:new b,map:new(Se||h),string:new b}},p.prototype.delete=function(t){ -return t=Z(this,t).delete(t),this.size-=t?1:0,t},p.prototype.get=function(t){return Z(this,t).get(t)},p.prototype.has=function(t){return Z(this,t).has(t)},p.prototype.set=function(t,e){var r=Z(this,t),n=r.size;return r.set(t,e),this.size+=r.size==n?0:1,this},y.prototype.add=y.prototype.push=function(t){return this.__data__.set(t,"__lodash_hash_undefined__"),this},y.prototype.has=function(t){return this.__data__.has(t)},j.prototype.clear=function(){this.__data__=new h,this.size=0},j.prototype.delete=function(t){ -var e=this.__data__;return t=e.delete(t),this.size=e.size,t},j.prototype.get=function(t){return this.__data__.get(t)},j.prototype.has=function(t){return this.__data__.has(t)},j.prototype.set=function(t,e){var r=this.__data__;if(r instanceof h){var n=r.__data__;if(!Se||199>n.length)return n.push([t,e]),this.size=++r.size,this;r=this.__data__=new p(n)}return r.set(t,e),this.size=r.size,this};var Pe=function(t,e){return function(r,n){if(null==r)return r;if(!yt(r))return t(r,n);for(var o=r.length,c=e?o:-1,u=Object(r);(e?c--:++co?Ut:c,o=1),e=Object(e);++n ["/", "test", "\d+", undefined, "?"] - // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined] - '([\\/.])?(?:\\:(\\w+)(?:\\(((?:\\\\.|[^)])*)\\))?|\\(((?:\\\\.|[^)])*)\\))([+*?])?', - // Match regexp special characters that are always escaped. - '([.+*?=^!:${}()[\\]|\\/])' -].join('|'), 'g'); - -/** - * Escape the capturing group by escaping special characters and meaning. - * - * @param {String} group - * @return {String} - */ -function escapeGroup (group) { - return group.replace(/([=!:$\/()])/g, '\\$1'); -} - -/** - * Attach the keys as a property of the regexp. - * - * @param {RegExp} re - * @param {Array} keys - * @return {RegExp} - */ -function attachKeys (re, keys) { - re.keys = keys; - return re; -} - -/** - * Get the flags for a regexp from the options. - * - * @param {Object} options - * @return {String} - */ -function flags (options) { - return options.sensitive ? '' : 'i'; -} - -/** - * Pull out keys from a regexp. - * - * @param {RegExp} path - * @param {Array} keys - * @return {RegExp} - */ -function regexpToRegexp (path, keys) { - // Use a negative lookahead to match only capturing groups. - var groups = path.source.match(/\((?!\?)/g); - - if (groups) { - for (var i = 0; i < groups.length; i++) { - keys.push({ - name: i, - delimiter: null, - optional: false, - repeat: false - }); - } - } - - return attachKeys(path, keys); -} - -/** - * Transform an array into a regexp. - * - * @param {Array} path - * @param {Array} keys - * @param {Object} options - * @return {RegExp} - */ -function arrayToRegexp (path, keys, options) { - var parts = []; - - for (var i = 0; i < path.length; i++) { - parts.push(pathToRegexp(path[i], keys, options).source); - } - - var regexp = new RegExp('(?:' + parts.join('|') + ')', flags(options)); - return attachKeys(regexp, keys); -} - -/** - * Replace the specific tags with regexp strings. - * - * @param {String} path - * @param {Array} keys - * @return {String} - */ -function replacePath (path, keys) { - var index = 0; - - function replace (_, escaped, prefix, key, capture, group, suffix, escape) { - if (escaped) { - return escaped; - } - - if (escape) { - return '\\' + escape; - } - - var repeat = suffix === '+' || suffix === '*'; - var optional = suffix === '?' || suffix === '*'; - - keys.push({ - name: key || index++, - delimiter: prefix || '/', - optional: optional, - repeat: repeat - }); - - prefix = prefix ? ('\\' + prefix) : ''; - capture = escapeGroup(capture || group || '[^' + (prefix || '\\/') + ']+?'); - - if (repeat) { - capture = capture + '(?:' + prefix + capture + ')*'; - } - - if (optional) { - return '(?:' + prefix + '(' + capture + '))?'; - } - - // Basic parameter support. - return prefix + '(' + capture + ')'; - } - - return path.replace(PATH_REGEXP, replace); -} - -/** - * Normalize the given path string, returning a regular expression. - * - * An empty array can be passed in for the keys, which will hold the - * placeholder key descriptions. For example, using `/user/:id`, `keys` will - * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. - * - * @param {(String|RegExp|Array)} path - * @param {Array} [keys] - * @param {Object} [options] - * @return {RegExp} - */ -function pathToRegexp (path, keys, options) { - keys = keys || []; - - if (!isArray(keys)) { - options = keys; - keys = []; - } else if (!options) { - options = {}; - } - - if (path instanceof RegExp) { - return regexpToRegexp(path, keys, options); - } - - if (isArray(path)) { - return arrayToRegexp(path, keys, options); - } - - var strict = options.strict; - var end = options.end !== false; - var route = replacePath(path, keys); - var endsWithSlash = path.charAt(path.length - 1) === '/'; - - // In non-strict mode we allow a slash at the end of match. If the path to - // match already ends with a slash, we remove it for consistency. The slash - // is valid at the end of a path match, not in the middle. This is important - // in non-ending mode, where "/test/" shouldn't match "/test//route". - if (!strict) { - route = (endsWithSlash ? route.slice(0, -2) : route) + '(?:\\/(?=$))?'; - } - - if (end) { - route += '$'; - } else { - // In non-ending mode, we need the capturing groups to match as much as - // possible by using a positive lookahead to the end or next path segment. - route += strict && endsWithSlash ? '' : '(?=\\/|$)'; - } - - return attachKeys(new RegExp('^' + route, flags(options)), keys); -} diff --git a/apidoc/vendor/polyfill.js b/apidoc/vendor/polyfill.js deleted file mode 100644 index 1d8c61cd..00000000 --- a/apidoc/vendor/polyfill.js +++ /dev/null @@ -1,96 +0,0 @@ -// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys -if (!Object.keys) { - Object.keys = (function () { - 'use strict'; - var hasOwnProperty = Object.prototype.hasOwnProperty, - hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), - dontEnums = [ - 'toString', - 'toLocaleString', - 'valueOf', - 'hasOwnProperty', - 'isPrototypeOf', - 'propertyIsEnumerable', - 'constructor' - ], - dontEnumsLength = dontEnums.length; - - return function (obj) { - if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { - throw new TypeError('Object.keys called on non-object'); - } - - var result = [], prop, i; - - for (prop in obj) { - if (hasOwnProperty.call(obj, prop)) { - result.push(prop); - } - } - - if (hasDontEnumBug) { - for (i = 0; i < dontEnumsLength; i++) { - if (hasOwnProperty.call(obj, dontEnums[i])) { - result.push(dontEnums[i]); - } - } - } - return result; - }; - }()); -} - -//Production steps of ECMA-262, Edition 5, 15.4.4.18 -//Reference: http://es5.github.com/#x15.4.4.18 -if (!Array.prototype.forEach) { - Array.prototype.forEach = function (callback, thisArg) { - var T, k; - - if (this == null) { - throw new TypeError(' this is null or not defined'); - } - - // 1. Let O be the result of calling ToObject passing the |this| value as the argument. - var O = Object(this); - - // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length". - // 3. Let len be ToUint32(lenValue). - var len = O.length >>> 0; - - // 4. If IsCallable(callback) is false, throw a TypeError exception. - // See: http://es5.github.com/#x9.11 - if (typeof callback !== "function") { - throw new TypeError(callback + " is not a function"); - } - - // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. - if (arguments.length > 1) { - T = thisArg; - } - - // 6. Let k be 0 - k = 0; - - // 7. Repeat, while k < len - while (k < len) { - var kValue; - - // a. Let Pk be ToString(k). - // This is implicit for LHS operands of the in operator - // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk. - // This step can be combined with c - // c. If kPresent is true, then - if (k in O) { - // i. Let kValue be the result of calling the Get internal method of O with argument Pk. - kValue = O[k]; - - // ii. Call the Call internal method of callback with T as the this value and - // argument list containing kValue, k, and O. - callback.call(T, kValue, k, O); - } - // d. Increase k by 1. - k++; - } - // 8. return undefined - }; -} diff --git a/apidoc/vendor/prism.css b/apidoc/vendor/prism.css deleted file mode 100644 index 4fac8142..00000000 --- a/apidoc/vendor/prism.css +++ /dev/null @@ -1,125 +0,0 @@ -/* PrismJS 1.21.0 -https://prismjs.com/download.html#themes=prism-tomorrow&languages=clike+javascript+bash+c+csharp+cpp+clojure+elixir+erlang+go+http+json+json5+jsonp+lua+perl+python+rust */ -/** - * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML - * Based on https://github.com/chriskempson/tomorrow-theme - * @author Rose Pritchard - */ - -code[class*="language-"], -pre[class*="language-"] { - color: #ccc; - background: none; - font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; - font-size: 1em; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - line-height: 1.5; - - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; - -} - -/* Code blocks */ -pre[class*="language-"] { - padding: 1em; - margin: .5em 0; - overflow: auto; -} - -:not(pre) > code[class*="language-"], -pre[class*="language-"] { - background: #2d2d2d; -} - -/* Inline code */ -:not(pre) > code[class*="language-"] { - padding: .1em; - border-radius: .3em; - white-space: normal; -} - -.token.comment, -.token.block-comment, -.token.prolog, -.token.doctype, -.token.cdata { - color: #999; -} - -.token.punctuation { - color: #ccc; -} - -.token.tag, -.token.attr-name, -.token.namespace, -.token.deleted { - color: #e2777a; -} - -.token.function-name { - color: #6196cc; -} - -.token.boolean, -.token.number, -.token.function { - color: #f08d49; -} - -.token.property, -.token.class-name, -.token.constant, -.token.symbol { - color: #f8c555; -} - -.token.selector, -.token.important, -.token.atrule, -.token.keyword, -.token.builtin { - color: #cc99cd; -} - -.token.string, -.token.char, -.token.attr-value, -.token.regex, -.token.variable { - color: #7ec699; -} - -.token.operator, -.token.entity, -.token.url { - color: #67cdcc; -} - -.token.important, -.token.bold { - font-weight: bold; -} -.token.italic { - font-style: italic; -} - -.token.entity { - cursor: help; -} - -.token.inserted { - color: green; -} - diff --git a/apidoc/vendor/prism.js b/apidoc/vendor/prism.js deleted file mode 100644 index 137746f1..00000000 --- a/apidoc/vendor/prism.js +++ /dev/null @@ -1,21 +0,0 @@ -/* PrismJS 1.21.0 -https://prismjs.com/download.html#themes=prism-tomorrow&languages=clike+javascript+bash+c+csharp+cpp+clojure+elixir+erlang+go+http+json+json5+jsonp+lua+perl+python+rust */ -var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(u){var c=/\blang(?:uage)?-([\w-]+)\b/i,n=0,M={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof W?new W(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=l.reach);k+=y.value.length,y=y.next){var b=y.value;if(t.length>n.length)return;if(!(b instanceof W)){var x=1;if(h&&y!=t.tail.prev){m.lastIndex=k;var w=m.exec(n);if(!w)break;var A=w.index+(f&&w[1]?w[1].length:0),P=w.index+w[0].length,S=k;for(S+=y.value.length;S<=A;)y=y.next,S+=y.value.length;if(S-=y.value.length,k=S,y.value instanceof W)continue;for(var E=y;E!==t.tail&&(Sl.reach&&(l.reach=j);var C=y.prev;L&&(C=I(t,C,L),k+=L.length),z(t,C,x);var _=new W(o,g?M.tokenize(O,g):O,v,O);y=I(t,C,_),N&&I(t,y,N),1"+a.content+""},!u.document)return u.addEventListener&&(M.disableWorkerMessageHandler||u.addEventListener("message",function(e){var n=JSON.parse(e.data),t=n.language,r=n.code,a=n.immediateClose;u.postMessage(M.highlight(r,M.languages[t],t)),a&&u.close()},!1)),M;var e=M.util.currentScript();function t(){M.manual||M.highlightAll()}if(e&&(M.filename=e.src,e.hasAttribute("data-manual")&&(M.manual=!0)),!M.manual){var r=document.readyState;"loading"===r||"interactive"===r&&e&&e.defer?document.addEventListener("DOMContentLoaded",t):window.requestAnimationFrame?window.requestAnimationFrame(t):window.setTimeout(t,16)}return M}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); -Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|interface|extends|implements|trait|instanceof|new)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; -Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])[_$A-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|})\s*)(?:catch|finally)\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|(?:get|set)(?=\s*[\[$\w\xA0-\uFFFF])|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],number:/\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/,function:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*]|\\.|[^/\\\[\r\n])+\/[gimyus]{0,6}(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)?\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=>)/i,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\${)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.js=Prism.languages.javascript; -!function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",n={environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--?|-=|\+\+?|\+=|!=?|~|\*\*?|\*=|\/=?|%=?|<<=?|>>=?|<=?|>=?|==?|&&?|&=|\^=?|\|\|?|\|=|\?|:/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)\w+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b\w+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+?)\s*(?:\r?\n|\r)[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:n},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s*(?:\r?\n|\r)[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\](?:\\\\)*)(["'])(?:\\[\s\S]|\$\([^)]+\)|`[^`]+`|(?!\2)[^\\])*\2/,lookbehind:!0,greedy:!0,inside:n}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:n.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|aptitude|apt-cache|apt-get|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:if|then|else|elif|fi|for|while|in|case|esac|function|select|do|done|until)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|break|cd|continue|eval|exec|exit|export|getopts|hash|pwd|readonly|return|shift|test|times|trap|umask|unset|alias|bind|builtin|caller|command|declare|echo|enable|help|let|local|logout|mapfile|printf|read|readarray|source|type|typeset|ulimit|unalias|set|shopt)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:true|false)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|==?|!=?|=~|<<[<-]?|[&\d]?>>|\d?[<>]&?|&[>&]?|\|[&|]?|<=?|>=?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}};for(var a=["comment","function-name","for-or-select","assign-left","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],r=n.variable[1].inside,s=0;s>=?|<<=?|->|([-+&|:])\1|[?:~]|[-+*/%&|^!=<>]=?/,number:/(?:\b0x(?:[\da-f]+\.?[\da-f]*|\.[\da-f]+)(?:p[+-]?\d+)?|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?)[ful]*/i}),Prism.languages.insertBefore("c","string",{macro:{pattern:/(^\s*)#\s*[a-z]+(?:[^\r\n\\/]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|\\(?:\r\n|[\s\S]))*/im,lookbehind:!0,greedy:!0,alias:"property",inside:{string:[{pattern:/^(#\s*include\s*)<[^>]+>/,lookbehind:!0},Prism.languages.c.string],comment:Prism.languages.c.comment,directive:{pattern:/^(#\s*)[a-z]+/,lookbehind:!0,alias:"keyword"},"directive-hash":/^#/,punctuation:/##|\\(?=[\r\n])/,expression:{pattern:/\S[\s\S]*/,inside:Prism.languages.c}}},constant:/\b(?:__FILE__|__LINE__|__DATE__|__TIME__|__TIMESTAMP__|__func__|EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|stdin|stdout|stderr)\b/}),delete Prism.languages.c.boolean; -!function(s){function a(e,s){return e.replace(/<<(\d+)>>/g,function(e,n){return"(?:"+s[+n]+")"})}function t(e,n,s){return RegExp(a(e,n),s||"")}function e(e,n){for(var s=0;s>/g,function(){return"(?:"+e+")"});return e.replace(/<>/g,"[^\\s\\S]")}var n="bool byte char decimal double dynamic float int long object sbyte short string uint ulong ushort var void",r="class enum interface struct",i="add alias and ascending async await by descending from get global group into join let nameof not notnull on or orderby partial remove select set unmanaged value when where where",o="abstract as base break case catch checked const continue default delegate do else event explicit extern finally fixed for foreach goto if implicit in internal is lock namespace new null operator out override params private protected public readonly ref return sealed sizeof stackalloc static switch this throw try typeof unchecked unsafe using virtual volatile while yield";function l(e){return"\\b(?:"+e.trim().replace(/ /g,"|")+")\\b"}var d=l(r),p=RegExp(l(n+" "+r+" "+i+" "+o)),c=l(r+" "+i+" "+o),u=l(n+" "+r+" "+o),g=e("<(?:[^<>;=+\\-*/%&|^]|<>)*>",2),b=e("\\((?:[^()]|<>)*\\)",2),h="@?\\b[A-Za-z_]\\w*\\b",f=a("<<0>>(?:\\s*<<1>>)?",[h,g]),m=a("(?!<<0>>)<<1>>(?:\\s*\\.\\s*<<1>>)*",[c,f]),k="\\[\\s*(?:,\\s*)*\\]",y=a("<<0>>(?:\\s*(?:\\?\\s*)?<<1>>)*(?:\\s*\\?)?",[m,k]),w=a("(?:<<0>>|<<1>>)(?:\\s*(?:\\?\\s*)?<<2>>)*(?:\\s*\\?)?",[a("\\(<<0>>+(?:,<<0>>+)+\\)",[a("[^,()<>[\\];=+\\-*/%&|^]|<<0>>|<<1>>|<<2>>",[g,b,k])]),m,k]),v={keyword:p,punctuation:/[<>()?,.:[\]]/},x="'(?:[^\r\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'",$='"(?:\\\\.|[^\\\\"\r\n])*"';s.languages.csharp=s.languages.extend("clike",{string:[{pattern:t("(^|[^$\\\\])<<0>>",['@"(?:""|\\\\[^]|[^\\\\"])*"(?!")']),lookbehind:!0,greedy:!0},{pattern:t("(^|[^@$\\\\])<<0>>",[$]),lookbehind:!0,greedy:!0},{pattern:RegExp(x),greedy:!0,alias:"character"}],"class-name":[{pattern:t("(\\busing\\s+static\\s+)<<0>>(?=\\s*;)",[m]),lookbehind:!0,inside:v},{pattern:t("(\\busing\\s+<<0>>\\s*=\\s*)<<1>>(?=\\s*;)",[h,w]),lookbehind:!0,inside:v},{pattern:t("(\\busing\\s+)<<0>>(?=\\s*=)",[h]),lookbehind:!0},{pattern:t("(\\b<<0>>\\s+)<<1>>",[d,f]),lookbehind:!0,inside:v},{pattern:t("(\\bcatch\\s*\\(\\s*)<<0>>",[m]),lookbehind:!0,inside:v},{pattern:t("(\\bwhere\\s+)<<0>>",[h]),lookbehind:!0},{pattern:t("(\\b(?:is(?:\\s+not)?|as)\\s+)<<0>>",[y]),lookbehind:!0,inside:v},{pattern:t("\\b<<0>>(?=\\s+(?!<<1>>)<<2>>(?:\\s*[=,;:{)\\]]|\\s+(?:in|when)\\b))",[w,u,h]),inside:v}],keyword:p,number:/(?:\b0(?:x[\da-f_]*[\da-f]|b[01_]*[01])|(?:\B\.\d+(?:_+\d+)*|\b\d+(?:_+\d+)*(?:\.\d+(?:_+\d+)*)?)(?:e[-+]?\d+(?:_+\d+)*)?)(?:ul|lu|[dflmu])?\b/i,operator:/>>=?|<<=?|[-=]>|([-+&|])\1|~|\?\?=?|[-+*/%&|^!=<>]=?/,punctuation:/\?\.?|::|[{}[\];(),.:]/}),s.languages.insertBefore("csharp","number",{range:{pattern:/\.\./,alias:"operator"}}),s.languages.insertBefore("csharp","punctuation",{"named-parameter":{pattern:t("([(,]\\s*)<<0>>(?=\\s*:)",[h]),lookbehind:!0,alias:"punctuation"}}),s.languages.insertBefore("csharp","class-name",{namespace:{pattern:t("(\\b(?:namespace|using)\\s+)<<0>>(?:\\s*\\.\\s*<<0>>)*(?=\\s*[;{])",[h]),lookbehind:!0,inside:{punctuation:/\./}},"type-expression":{pattern:t("(\\b(?:default|typeof|sizeof)\\s*\\(\\s*)(?:[^()\\s]|\\s(?!\\s*\\))|<<0>>)*(?=\\s*\\))",[b]),lookbehind:!0,alias:"class-name",inside:v},"return-type":{pattern:t("<<0>>(?=\\s+(?:<<1>>\\s*(?:=>|[({]|\\.\\s*this\\s*\\[)|this\\s*\\[))",[w,m]),inside:v,alias:"class-name"},"constructor-invocation":{pattern:t("(\\bnew\\s+)<<0>>(?=\\s*[[({])",[w]),lookbehind:!0,inside:v,alias:"class-name"},"generic-method":{pattern:t("<<0>>\\s*<<1>>(?=\\s*\\()",[h,g]),inside:{function:t("^<<0>>",[h]),generic:{pattern:RegExp(g),alias:"class-name",inside:v}}},"type-list":{pattern:t("\\b((?:<<0>>\\s+<<1>>|where\\s+<<2>>)\\s*:\\s*)(?:<<3>>|<<4>>)(?:\\s*,\\s*(?:<<3>>|<<4>>))*(?=\\s*(?:where|[{;]|=>|$))",[d,f,h,w,p.source]),lookbehind:!0,inside:{keyword:p,"class-name":{pattern:RegExp(w),greedy:!0,inside:v},punctuation:/,/}},preprocessor:{pattern:/(^\s*)#.*/m,lookbehind:!0,alias:"property",inside:{directive:{pattern:/(\s*#)\b(?:define|elif|else|endif|endregion|error|if|line|pragma|region|undef|warning)\b/,lookbehind:!0,alias:"keyword"}}}});var _=$+"|"+x,B=a("/(?![*/])|//[^\r\n]*[\r\n]|/\\*(?:[^*]|\\*(?!/))*\\*/|<<0>>",[_]),E=e(a("[^\"'/()]|<<0>>|\\(<>*\\)",[B]),2),R="\\b(?:assembly|event|field|method|module|param|property|return|type)\\b",P=a("<<0>>(?:\\s*\\(<<1>>*\\))?",[m,E]);s.languages.insertBefore("csharp","class-name",{attribute:{pattern:t("((?:^|[^\\s\\w>)?])\\s*\\[\\s*)(?:<<0>>\\s*:\\s*)?<<1>>(?:\\s*,\\s*<<1>>)*(?=\\s*\\])",[R,P]),lookbehind:!0,greedy:!0,inside:{target:{pattern:t("^<<0>>(?=\\s*:)",[R]),alias:"keyword"},"attribute-arguments":{pattern:t("\\(<<0>>*\\)",[E]),inside:s.languages.csharp},"class-name":{pattern:RegExp(m),inside:{punctuation:/\./}},punctuation:/[:,]/}}});var z=":[^}\r\n]+",S=e(a("[^\"'/()]|<<0>>|\\(<>*\\)",[B]),2),j=a("\\{(?!\\{)(?:(?![}:])<<0>>)*<<1>>?\\}",[S,z]),A=e(a("[^\"'/()]|/(?!\\*)|/\\*(?:[^*]|\\*(?!/))*\\*/|<<0>>|\\(<>*\\)",[_]),2),F=a("\\{(?!\\{)(?:(?![}:])<<0>>)*<<1>>?\\}",[A,z]);function U(e,n){return{interpolation:{pattern:t("((?:^|[^{])(?:\\{\\{)*)<<0>>",[e]),lookbehind:!0,inside:{"format-string":{pattern:t("(^\\{(?:(?![}:])<<0>>)*)<<1>>(?=\\}$)",[n,z]),lookbehind:!0,inside:{punctuation:/^:/}},punctuation:/^\{|\}$/,expression:{pattern:/[\s\S]+/,alias:"language-csharp",inside:s.languages.csharp}}},string:/[\s\S]+/}}s.languages.insertBefore("csharp","string",{"interpolation-string":[{pattern:t('(^|[^\\\\])(?:\\$@|@\\$)"(?:""|\\\\[^]|\\{\\{|<<0>>|[^\\\\{"])*"',[j]),lookbehind:!0,greedy:!0,inside:U(j,S)},{pattern:t('(^|[^@\\\\])\\$"(?:\\\\.|\\{\\{|<<0>>|[^\\\\"{])*"',[F]),lookbehind:!0,greedy:!0,inside:U(F,A)}]})}(Prism),Prism.languages.dotnet=Prism.languages.cs=Prism.languages.csharp; -!function(e){var t=/\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char8_t|char16_t|char32_t|class|compl|concept|const|consteval|constexpr|constinit|const_cast|continue|co_await|co_return|co_yield|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|float|for|friend|goto|if|inline|int|int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|long|mutable|namespace|new|noexcept|nullptr|operator|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/;e.languages.cpp=e.languages.extend("c",{"class-name":[{pattern:RegExp("(\\b(?:class|concept|enum|struct|typename)\\s+)(?!)\\w+".replace(//g,function(){return t.source}),lookbehind:!0},/\b[A-Z]\w*(?=\s*::\s*\w+\s*\()/,/\b[A-Z_]\w*(?=\s*::\s*~\w+\s*\()/i,/\w+(?=\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\s*::\s*\w+\s*\()/],keyword:t,number:{pattern:/(?:\b0b[01']+|\b0x(?:[\da-f']+\.?[\da-f']*|\.[\da-f']+)(?:p[+-]?[\d']+)?|(?:\b[\d']+\.?[\d']*|\B\.[\d']+)(?:e[+-]?[\d']+)?)[ful]*/i,greedy:!0},operator:/>>=?|<<=?|->|([-+&|:])\1|[?:~]|<=>|[-+*/%&|^!=<>]=?|\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/,boolean:/\b(?:true|false)\b/}),e.languages.insertBefore("cpp","string",{"raw-string":{pattern:/R"([^()\\ ]{0,16})\([\s\S]*?\)\1"/,alias:"string",greedy:!0}}),e.languages.insertBefore("cpp","class-name",{"base-clause":{pattern:/(\b(?:class|struct)\s+\w+\s*:\s*)(?:[^;{}"'])+?(?=\s*[;{])/,lookbehind:!0,greedy:!0,inside:e.languages.extend("cpp",{})}}),e.languages.insertBefore("inside","operator",{"class-name":/\b[a-z_]\w*\b(?!\s*::)/i},e.languages.cpp["base-clause"])}(Prism); -Prism.languages.clojure={comment:/;.*/,string:{pattern:/"(?:[^"\\]|\\.)*"/,greedy:!0},operator:/(?:::|[:|'])\b[a-z][\w*+!?-]*\b/i,keyword:{pattern:/([^\w+*'?-])(?:def|if|do|let|\.\.|quote|var|->>|->|fn|loop|recur|throw|try|monitor-enter|\.|new|set!|def\-|defn|defn\-|defmacro|defmulti|defmethod|defstruct|defonce|declare|definline|definterface|defprotocol|==|defrecord|>=|deftype|<=|defproject|ns|\*|\+|\-|\/|<|=|>|accessor|agent|agent-errors|aget|alength|all-ns|alter|and|append-child|apply|array-map|aset|aset-boolean|aset-byte|aset-char|aset-double|aset-float|aset-int|aset-long|aset-short|assert|assoc|await|await-for|bean|binding|bit-and|bit-not|bit-or|bit-shift-left|bit-shift-right|bit-xor|boolean|branch\?|butlast|byte|cast|char|children|class|clear-agent-errors|comment|commute|comp|comparator|complement|concat|conj|cons|constantly|cond|if-not|construct-proxy|contains\?|count|create-ns|create-struct|cycle|dec|deref|difference|disj|dissoc|distinct|doall|doc|dorun|doseq|dosync|dotimes|doto|double|down|drop|drop-while|edit|end\?|ensure|eval|every\?|false\?|ffirst|file-seq|filter|find|find-doc|find-ns|find-var|first|float|flush|for|fnseq|frest|gensym|get-proxy-class|get|hash-map|hash-set|identical\?|identity|if-let|import|in-ns|inc|index|insert-child|insert-left|insert-right|inspect-table|inspect-tree|instance\?|int|interleave|intersection|into|into-array|iterate|join|key|keys|keyword|keyword\?|last|lazy-cat|lazy-cons|left|lefts|line-seq|list\*|list|load|load-file|locking|long|loop|macroexpand|macroexpand-1|make-array|make-node|map|map-invert|map\?|mapcat|max|max-key|memfn|merge|merge-with|meta|min|min-key|name|namespace|neg\?|new|newline|next|nil\?|node|not|not-any\?|not-every\?|not=|ns-imports|ns-interns|ns-map|ns-name|ns-publics|ns-refers|ns-resolve|ns-unmap|nth|nthrest|or|parse|partial|path|peek|pop|pos\?|pr|pr-str|print|print-str|println|println-str|prn|prn-str|project|proxy|proxy-mappings|quot|rand|rand-int|range|re-find|re-groups|re-matcher|re-matches|re-pattern|re-seq|read|read-line|reduce|ref|ref-set|refer|rem|remove|remove-method|remove-ns|rename|rename-keys|repeat|replace|replicate|resolve|rest|resultset-seq|reverse|rfirst|right|rights|root|rrest|rseq|second|select|select-keys|send|send-off|seq|seq-zip|seq\?|set|short|slurp|some|sort|sort-by|sorted-map|sorted-map-by|sorted-set|special-symbol\?|split-at|split-with|str|string\?|struct|struct-map|subs|subvec|symbol|symbol\?|sync|take|take-nth|take-while|test|time|to-array|to-array-2d|tree-seq|true\?|union|up|update-proxy|val|vals|var-get|var-set|var\?|vector|vector-zip|vector\?|when|when-first|when-let|when-not|with-local-vars|with-meta|with-open|with-out-str|xml-seq|xml-zip|zero\?|zipmap|zipper)(?=[^\w+*'?-])/,lookbehind:!0},boolean:/\b(?:true|false|nil)\b/,number:/\b[\da-f]+\b/i,punctuation:/[{}\[\](),]/}; -Prism.languages.elixir={comment:/#.*/m,regex:{pattern:/~[rR](?:("""|''')(?:\\[\s\S]|(?!\1)[^\\])+\1|([\/|"'])(?:\\.|(?!\2)[^\\\r\n])+\2|\((?:\\.|[^\\)\r\n])+\)|\[(?:\\.|[^\\\]\r\n])+\]|\{(?:\\.|[^\\}\r\n])+\}|<(?:\\.|[^\\>\r\n])+>)[uismxfr]*/,greedy:!0},string:[{pattern:/~[cCsSwW](?:("""|''')(?:\\[\s\S]|(?!\1)[^\\])+\1|([\/|"'])(?:\\.|(?!\2)[^\\\r\n])+\2|\((?:\\.|[^\\)\r\n])+\)|\[(?:\\.|[^\\\]\r\n])+\]|\{(?:\\.|#\{[^}]+\}|#(?!\{)|[^#\\}\r\n])+\}|<(?:\\.|[^\\>\r\n])+>)[csa]?/,greedy:!0,inside:{}},{pattern:/("""|''')[\s\S]*?\1/,greedy:!0,inside:{}},{pattern:/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0,inside:{}}],atom:{pattern:/(^|[^:]):\w+/,lookbehind:!0,alias:"symbol"},"attr-name":/\w+\??:(?!:)/,capture:{pattern:/(^|[^&])&(?:[^&\s\d()][^\s()]*|(?=\())/,lookbehind:!0,alias:"function"},argument:{pattern:/(^|[^&])&\d+/,lookbehind:!0,alias:"variable"},attribute:{pattern:/@\w+/,alias:"variable"},number:/\b(?:0[box][a-f\d_]+|\d[\d_]*)(?:\.[\d_]+)?(?:e[+-]?[\d_]+)?\b/i,keyword:/\b(?:after|alias|and|case|catch|cond|def(?:callback|exception|impl|module|p|protocol|struct)?|do|else|end|fn|for|if|import|not|or|require|rescue|try|unless|use|when)\b/,boolean:/\b(?:true|false|nil)\b/,operator:[/\bin\b|&&?|\|[|>]?|\\\\|::|\.\.\.?|\+\+?|-[->]?|<[-=>]|>=|!==?|\B!|=(?:==?|[>~])?|[*\/^]/,{pattern:/([^<])<(?!<)/,lookbehind:!0},{pattern:/([^>])>(?!>)/,lookbehind:!0}],punctuation:/<<|>>|[.,%\[\]{}()]/},Prism.languages.elixir.string.forEach(function(e){e.inside={interpolation:{pattern:/#\{[^}]+\}/,inside:{delimiter:{pattern:/^#\{|\}$/,alias:"punctuation"},rest:Prism.languages.elixir}}}}); -Prism.languages.erlang={comment:/%.+/,string:{pattern:/"(?:\\.|[^\\"\r\n])*"/,greedy:!0},"quoted-function":{pattern:/'(?:\\.|[^\\'\r\n])+'(?=\()/,alias:"function"},"quoted-atom":{pattern:/'(?:\\.|[^\\'\r\n])+'/,alias:"atom"},boolean:/\b(?:true|false)\b/,keyword:/\b(?:fun|when|case|of|end|if|receive|after|try|catch)\b/,number:[/\$\\?./,/\d+#[a-z0-9]+/i,/(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i],function:/\b[a-z][\w@]*(?=\()/,variable:{pattern:/(^|[^@])(?:\b|\?)[A-Z_][\w@]*/,lookbehind:!0},operator:[/[=\/<>:]=|=[:\/]=|\+\+?|--?|[=*\/!]|\b(?:bnot|div|rem|band|bor|bxor|bsl|bsr|not|and|or|xor|orelse|andalso)\b/,{pattern:/(^|[^<])<(?!<)/,lookbehind:!0},{pattern:/(^|[^>])>(?!>)/,lookbehind:!0}],atom:/\b[a-z][\w@]*/,punctuation:/[()[\]{}:;,.#|]|<<|>>/}; -Prism.languages.go=Prism.languages.extend("clike",{keyword:/\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,builtin:/\b(?:bool|byte|complex(?:64|128)|error|float(?:32|64)|rune|string|u?int(?:8|16|32|64)?|uintptr|append|cap|close|complex|copy|delete|imag|len|make|new|panic|print(?:ln)?|real|recover)\b/,boolean:/\b(?:_|iota|nil|true|false)\b/,operator:/[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./,number:/(?:\b0x[a-f\d]+|(?:\b\d+\.?\d*|\B\.\d+)(?:e[-+]?\d+)?)i?/i,string:{pattern:/(["'`])(?:\\[\s\S]|(?!\1)[^\\])*\1/,greedy:!0}}),delete Prism.languages.go["class-name"]; -!function(t){t.languages.http={"request-line":{pattern:/^(?:POST|GET|PUT|DELETE|OPTIONS|PATCH|TRACE|CONNECT)\s(?:https?:\/\/|\/)\S+\sHTTP\/[0-9.]+/m,inside:{property:/^(?:POST|GET|PUT|DELETE|OPTIONS|PATCH|TRACE|CONNECT)\b/,"attr-name":/:\w+/}},"response-status":{pattern:/^HTTP\/1.[01] \d+.*/m,inside:{property:{pattern:/(^HTTP\/1.[01] )\d+.*/i,lookbehind:!0}}},"header-name":{pattern:/^[\w-]+:(?=.)/m,alias:"keyword"}};var a,e,n,i=t.languages,p={"application/javascript":i.javascript,"application/json":i.json||i.javascript,"application/xml":i.xml,"text/xml":i.xml,"text/html":i.html,"text/css":i.css},s={"application/json":!0,"application/xml":!0};for(var r in p)if(p[r]){a=a||{};var T=s[r]?(void 0,n=(e=r).replace(/^[a-z]+\//,""),"(?:"+e+"|\\w+/(?:[\\w.-]+\\+)+"+n+"(?![+\\w.-]))"):r;a[r.replace(/\//g,"-")]={pattern:RegExp("(content-type:\\s*"+T+"[\\s\\S]*?)(?:\\r?\\n|\\r){2}[\\s\\S]*","i"),lookbehind:!0,inside:p[r]}}a&&t.languages.insertBefore("http","header-name",a)}(Prism); -Prism.languages.json={property:{pattern:/"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,greedy:!0},string:{pattern:/"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:true|false)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json; -!function(n){var e=/("|')(?:\\(?:\r\n?|\n|.)|(?!\1)[^\\\r\n])*\1/;n.languages.json5=n.languages.extend("json",{property:[{pattern:RegExp(e.source+"(?=\\s*:)"),greedy:!0},{pattern:/[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*:)/,alias:"unquoted"}],string:{pattern:e,greedy:!0},number:/[+-]?\b(?:NaN|Infinity|0x[a-fA-F\d]+)\b|[+-]?(?:\b\d+\.?\d*|\B\.\d+)(?:[eE][+-]?\d+\b)?/})}(Prism); -Prism.languages.jsonp=Prism.languages.extend("json",{punctuation:/[{}[\]();,.]/}),Prism.languages.insertBefore("jsonp","punctuation",{function:/[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*\()/}); -Prism.languages.lua={comment:/^#!.+|--(?:\[(=*)\[[\s\S]*?\]\1\]|.*)/m,string:{pattern:/(["'])(?:(?!\1)[^\\\r\n]|\\z(?:\r\n|\s)|\\(?:\r\n|[\s\S]))*\1|\[(=*)\[[\s\S]*?\]\2\]/,greedy:!0},number:/\b0x[a-f\d]+\.?[a-f\d]*(?:p[+-]?\d+)?\b|\b\d+(?:\.\B|\.?\d*(?:e[+-]?\d+)?\b)|\B\.\d+(?:e[+-]?\d+)?\b/i,keyword:/\b(?:and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,function:/(?!\d)\w+(?=\s*(?:[({]))/,operator:[/[-+*%^&|#]|\/\/?|<[<=]?|>[>=]?|[=~]=?/,{pattern:/(^|[^.])\.\.(?!\.)/,lookbehind:!0}],punctuation:/[\[\](){},;]|\.+|:+/}; -Prism.languages.perl={comment:[{pattern:/(^\s*)=\w+[\s\S]*?=cut.*/m,lookbehind:!0},{pattern:/(^|[^\\$])#.*/,lookbehind:!0}],string:[{pattern:/\b(?:q|qq|qx|qw)\s*([^a-zA-Z0-9\s{(\[<])(?:(?!\1)[^\\]|\\[\s\S])*\1/,greedy:!0},{pattern:/\b(?:q|qq|qx|qw)\s+([a-zA-Z0-9])(?:(?!\1)[^\\]|\\[\s\S])*\1/,greedy:!0},{pattern:/\b(?:q|qq|qx|qw)\s*\((?:[^()\\]|\\[\s\S])*\)/,greedy:!0},{pattern:/\b(?:q|qq|qx|qw)\s*\{(?:[^{}\\]|\\[\s\S])*\}/,greedy:!0},{pattern:/\b(?:q|qq|qx|qw)\s*\[(?:[^[\]\\]|\\[\s\S])*\]/,greedy:!0},{pattern:/\b(?:q|qq|qx|qw)\s*<(?:[^<>\\]|\\[\s\S])*>/,greedy:!0},{pattern:/("|`)(?:(?!\1)[^\\]|\\[\s\S])*\1/,greedy:!0},{pattern:/'(?:[^'\\\r\n]|\\.)*'/,greedy:!0}],regex:[{pattern:/\b(?:m|qr)\s*([^a-zA-Z0-9\s{(\[<])(?:(?!\1)[^\\]|\\[\s\S])*\1[msixpodualngc]*/,greedy:!0},{pattern:/\b(?:m|qr)\s+([a-zA-Z0-9])(?:(?!\1)[^\\]|\\[\s\S])*\1[msixpodualngc]*/,greedy:!0},{pattern:/\b(?:m|qr)\s*\((?:[^()\\]|\\[\s\S])*\)[msixpodualngc]*/,greedy:!0},{pattern:/\b(?:m|qr)\s*\{(?:[^{}\\]|\\[\s\S])*\}[msixpodualngc]*/,greedy:!0},{pattern:/\b(?:m|qr)\s*\[(?:[^[\]\\]|\\[\s\S])*\][msixpodualngc]*/,greedy:!0},{pattern:/\b(?:m|qr)\s*<(?:[^<>\\]|\\[\s\S])*>[msixpodualngc]*/,greedy:!0},{pattern:/(^|[^-]\b)(?:s|tr|y)\s*([^a-zA-Z0-9\s{(\[<])(?:(?!\2)[^\\]|\\[\s\S])*\2(?:(?!\2)[^\\]|\\[\s\S])*\2[msixpodualngcer]*/,lookbehind:!0,greedy:!0},{pattern:/(^|[^-]\b)(?:s|tr|y)\s+([a-zA-Z0-9])(?:(?!\2)[^\\]|\\[\s\S])*\2(?:(?!\2)[^\\]|\\[\s\S])*\2[msixpodualngcer]*/,lookbehind:!0,greedy:!0},{pattern:/(^|[^-]\b)(?:s|tr|y)\s*\((?:[^()\\]|\\[\s\S])*\)\s*\((?:[^()\\]|\\[\s\S])*\)[msixpodualngcer]*/,lookbehind:!0,greedy:!0},{pattern:/(^|[^-]\b)(?:s|tr|y)\s*\{(?:[^{}\\]|\\[\s\S])*\}\s*\{(?:[^{}\\]|\\[\s\S])*\}[msixpodualngcer]*/,lookbehind:!0,greedy:!0},{pattern:/(^|[^-]\b)(?:s|tr|y)\s*\[(?:[^[\]\\]|\\[\s\S])*\]\s*\[(?:[^[\]\\]|\\[\s\S])*\][msixpodualngcer]*/,lookbehind:!0,greedy:!0},{pattern:/(^|[^-]\b)(?:s|tr|y)\s*<(?:[^<>\\]|\\[\s\S])*>\s*<(?:[^<>\\]|\\[\s\S])*>[msixpodualngcer]*/,lookbehind:!0,greedy:!0},{pattern:/\/(?:[^\/\\\r\n]|\\.)*\/[msixpodualngc]*(?=\s*(?:$|[\r\n,.;})&|\-+*~<>!?^]|(?:lt|gt|le|ge|eq|ne|cmp|not|and|or|xor|x)\b))/,greedy:!0}],variable:[/[&*$@%]\{\^[A-Z]+\}/,/[&*$@%]\^[A-Z_]/,/[&*$@%]#?(?=\{)/,/[&*$@%]#?(?:(?:::)*'?(?!\d)[\w$]+)+(?:::)*/i,/[&*$@%]\d+/,/(?!%=)[$@%][!"#$%&'()*+,\-.\/:;<=>?@[\\\]^_`{|}~]/],filehandle:{pattern:/<(?![<=])\S*>|\b_\b/,alias:"symbol"},vstring:{pattern:/v\d+(?:\.\d+)*|\d+(?:\.\d+){2,}/,alias:"string"},function:{pattern:/sub [a-z0-9_]+/i,inside:{keyword:/sub/}},keyword:/\b(?:any|break|continue|default|delete|die|do|else|elsif|eval|for|foreach|given|goto|if|last|local|my|next|our|package|print|redo|require|return|say|state|sub|switch|undef|unless|until|use|when|while)\b/,number:/\b(?:0x[\dA-Fa-f](?:_?[\dA-Fa-f])*|0b[01](?:_?[01])*|(?:\d(?:_?\d)*)?\.?\d(?:_?\d)*(?:[Ee][+-]?\d+)?)\b/,operator:/-[rwxoRWXOezsfdlpSbctugkTBMAC]\b|\+[+=]?|-[-=>]?|\*\*?=?|\/\/?=?|=[=~>]?|~[~=]?|\|\|?=?|&&?=?|<(?:=>?|<=?)?|>>?=?|![~=]?|[%^]=?|\.(?:=|\.\.?)?|[\\?]|\bx(?:=|\b)|\b(?:lt|gt|le|ge|eq|ne|cmp|not|and|or|xor)\b/,punctuation:/[{}[\];(),:]/}; -Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0},"string-interpolation":{pattern:/(?:f|rf|fr)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:{{)*){(?!{)(?:[^{}]|{(?!{)(?:[^{}]|{(?!{)(?:[^{}])+})+})+}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|rb|br)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|rb|br)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^\s*)@\w+(?:\.\w+)*/im,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:and|as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:True|False|None)\b/,number:/(?:\b(?=\d)|\B(?=\.))(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,operator:/[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python; -!function(e){for(var a="/\\*(?:[^*/]|\\*(?!/)|/(?!\\*)|)*\\*/",t=0;t<2;t++)a=a.replace(//g,function(){return a});a=a.replace(//g,function(){return"[^\\s\\S]"}),e.languages.rust={comment:[{pattern:RegExp("(^|[^\\\\])"+a),lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/b?"(?:\\[\s\S]|[^\\"])*"|b?r(#*)"(?:[^"]|"(?!\1))*"\1/,greedy:!0},char:{pattern:/b?'(?:\\(?:x[0-7][\da-fA-F]|u{(?:[\da-fA-F]_*){1,6}|.)|[^\\\r\n\t'])'/,greedy:!0,alias:"string"},attribute:{pattern:/#!?\[(?:[^\[\]"]|"(?:\\[\s\S]|[^\\"])*")*\]/,greedy:!0,alias:"attr-name",inside:{string:null}},"closure-params":{pattern:/([=(,:]\s*|\bmove\s*)\|[^|]*\||\|[^|]*\|(?=\s*(?:\{|->))/,lookbehind:!0,greedy:!0,inside:{"closure-punctuation":{pattern:/^\||\|$/,alias:"punctuation"},rest:null}},"lifetime-annotation":{pattern:/'\w+/,alias:"symbol"},"fragment-specifier":{pattern:/(\$\w+:)[a-z]+/,lookbehind:!0,alias:"punctuation"},variable:/\$\w+/,"function-definition":{pattern:/(\bfn\s+)\w+/,lookbehind:!0,alias:"function"},"type-definition":{pattern:/(\b(?:enum|struct|union)\s+)\w+/,lookbehind:!0,alias:"class-name"},"module-declaration":[{pattern:/(\b(?:crate|mod)\s+)[a-z][a-z_\d]*/,lookbehind:!0,alias:"namespace"},{pattern:/(\b(?:crate|self|super)\s*)::\s*[a-z][a-z_\d]*\b(?:\s*::(?:\s*[a-z][a-z_\d]*\s*::)*)?/,lookbehind:!0,alias:"namespace",inside:{punctuation:/::/}}],keyword:[/\b(?:abstract|as|async|await|become|box|break|const|continue|crate|do|dyn|else|enum|extern|final|fn|for|if|impl|in|let|loop|macro|match|mod|move|mut|override|priv|pub|ref|return|self|Self|static|struct|super|trait|try|type|typeof|union|unsafe|unsized|use|virtual|where|while|yield)\b/,/\b(?:[ui](?:8|16|32|64|128|size)|f(?:32|64)|bool|char|str)\b/],function:/\b[a-z_]\w*(?=\s*(?:::\s*<|\())/,macro:{pattern:/\w+!/,alias:"property"},constant:/\b[A-Z_][A-Z_\d]+\b/,"class-name":/\b[A-Z]\w*\b/,namespace:{pattern:/(?:\b[a-z][a-z_\d]*\s*::\s*)*\b[a-z][a-z_\d]*\s*::(?!\s*<)/,inside:{punctuation:/::/}},number:/\b(?:0x[\dA-Fa-f](?:_?[\dA-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*|(?:\d(?:_?\d)*)?\.?\d(?:_?\d)*(?:[Ee][+-]?\d+)?)(?:_?(?:[iu](?:8|16|32|64|size)?|f32|f64))?\b/,boolean:/\b(?:false|true)\b/,punctuation:/->|\.\.=|\.{1,3}|::|[{}[\];(),:]/,operator:/[-+*\/%!^]=?|=[=>]?|&[&=]?|\|[|=]?|<>?=?|[@?]/},e.languages.rust["closure-params"].inside.rest=e.languages.rust,e.languages.rust.attribute.inside.string=e.languages.rust.string}(Prism); diff --git a/apidoc/vendor/require.min.js b/apidoc/vendor/require.min.js deleted file mode 100644 index de7c7d2f..00000000 --- a/apidoc/vendor/require.min.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - RequireJS 2.1.22 Copyright (c) 2010-2015, The Dojo Foundation All Rights Reserved. - Available via the MIT or new BSD license. - see: http://github.com/jrburke/requirejs for details -*/ -var requirejs,require,define; -(function(ha){function L(b){return"[object Function]"===R.call(b)}function M(b){return"[object Array]"===R.call(b)}function x(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(L(l)){try{f=h.execCb(c,l,b,f)}catch(d){a=d}this.map.isDefine&&void 0===f&&((b=this.module)?f=b.exports: -this.usingExports&&(f=this.exports));if(a){if(this.events.error&&this.map.isDefine||k.onError!==ia)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",B(this.error=a);if("undefined"!==typeof console&&console.error)console.error(a);else k.onError(a)}}else f=l;this.exports=f;if(this.map.isDefine&&!this.ignore&&(r[c]=f,k.onResourceLoad)){var e=[];x(this.depMaps,function(a){e.push(a.normalizedMap||a)});k.onResourceLoad(h, -this.map,e)}D(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}},callPlugin:function(){var a=this.map,b=a.id,d=q(a.prefix);this.depMaps.push(d);v(d,"defined",y(this,function(f){var l,d,e=g(ga,this.map.id),N=this.map.name,p=this.map.parentMap?this.map.parentMap.name:null,r=h.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(N=f.normalize(N,function(a){return c(a, -p,!0)})||""),d=q(a.prefix+"!"+N,this.map.parentMap),v(d,"defined",y(this,function(a){this.map.normalizedMap=d;this.init([],function(){return a},null,{enabled:!0,ignore:!0})}),f=g(t,d.id)){this.depMaps.push(d);if(this.events.error)f.on("error",y(this,function(a){this.emit("error",a)});f.enable()}}else e?(this.map.url=h.nameToUrl(e),this.load()):(l=y(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),l.error=y(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b]; -E(t,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&D(a.map.id)});B(a)}),l.fromText=y(this,function(f,c){var d=a.name,e=q(d),N=T;c&&(f=c);N&&(T=!1);u(e);w(m.config,b)&&(m.config[d]=m.config[b]);try{k.exec(f)}catch(g){return B(G("fromtexteval","fromText eval for "+b+" failed: "+g,g,[b]))}N&&(T=!0);this.depMaps.push(e);h.completeLoad(d);r([d],l)}),f.load(a.name,r,l,m))});h.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){aa[this.map.id]=this;this.enabling=this.enabled=!0;x(this.depMaps, -y(this,function(a,b){var c,f;if("string"===typeof a){a=q(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=g(S,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;v(a,"defined",y(this,function(a){this.undefed||(this.defineDep(b,a),this.check())});this.errback?v(a,"error",y(this,this.errback)):this.events.error&&v(a,"error",y(this,function(a){this.emit("error",a)})}c=a.id;f=t[c];w(S,c)||!f||f.enabled||h.enable(a,this)});E(this.pluginMaps,y(this,function(a){var b= -g(t,a.id);b&&!b.enabled&&h.enable(a,this)});this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){x(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};h={config:m,contextName:b,registry:t,defined:r,urlFetched:X,defQueue:H,defQueueMap:{},Module:ea,makeModuleMap:q,nextTick:k.nextTick,onError:B,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=m.shim,c={paths:!0, -bundles:!0,config:!0,map:!0};E(a,function(a,b){c[b]?(m[b]||(m[b]={}),Z(m[b],a,!0,!0)):m[b]=a});a.bundles&&E(a.bundles,function(a,b){x(a,function(a){a!==b&&(ga[a]=b)})});a.shim&&(E(a.shim,function(a,c){M(a)&&(a={deps:a});!a.exports&&!a.init||a.exportsFn||(a.exportsFn=h.makeShimExports(a));b[c]=a}),m.shim=b);a.packages&&x(a.packages,function(a){var b;a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(m.paths[b]=a.location);m.pkgs[b]=a.name+"/"+(a.main||"main").replace(na,"").replace(V,"")});E(t, -function(a,b){a.inited||a.map.unnormalized||(a.map=q(b,null,!0))});(a.deps||a.callback)&&h.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ha,arguments));return b||a.exports&&ja(a.exports)}},makeRequire:function(a,n){function e(c,d,g){var m,p;n.enableBuildCallback&&d&&L(d)&&(d.__requireJsBuild=!0);if("string"===typeof c){if(L(d))return B(G("requireargs","Invalid require call"),g);if(a&&w(S,c))return S[c](t[a.id]);if(k.get)return k.get(h, -c,a,e);m=q(c,a,!1,!0);m=m.id;return w(r,m)?r[m]:B(G("notloaded",'Module name "'+m+'" has not been loaded yet for context: '+b+(a?"":". Use require([])")))}Q();h.nextTick(function(){Q();p=u(q(null,a));p.skipMap=n.skipMap;p.init(c,d,g,{enabled:!0});I()});return e}n=n||{};Z(e,{isBrowser:F,toUrl:function(b){var d,e=b.lastIndexOf("."),n=b.split("/")[0];-1!==e&&("."!==n&&".."!==n||1e.attachEvent.toString().indexOf("[native code")||da?(e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)):(T=!0,e.attachEvent("onreadystatechange",b.onScriptLoad));e.src=d;Q=e;I?D.insertBefore(e,I):D.appendChild(e);Q=null;return e}if(ka)try{importScripts(d),b.completeLoad(c)}catch(q){b.onError(G("importscripts","importScripts failed for "+ -c+" at "+d,q,[c]))}};F&&!v.skipDataMain&&Y(document.getElementsByTagName("script"),function(b){D||(D=b.parentNode);if(P=b.getAttribute("data-main"))return u=P,v.baseUrl||(J=u.split("/"),u=J.pop(),U=J.length?J.join("/")+"/":"./",v.baseUrl=U),u=u.replace(V,""),k.jsExtRegExp.test(u)&&(u=P),v.deps=v.deps?v.deps.concat(u):[u],!0});define=function(b,c,d){var g,e;"string"!==typeof b&&(d=c,c=b,b=null);M(c)||(d=c,c=null);!c&&L(d)&&(c=[],d.length&&(d.toString().replace(qa,"").replace(ra,function(b,d){c.push(d)}), -c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));T&&(g=Q||pa())&&(b||(b=g.getAttribute("data-requiremodule")),e=K[g.getAttribute("data-requirecontext")]);e?(e.defQueue.push([b,c,d]),e.defQueueMap[b]=!0):W.push([b,c,d])};define.amd={jQuery:!0};k.exec=function(b){return eval(b)};k(v)}})(this); diff --git a/apidoc/vendor/semver.min.js b/apidoc/vendor/semver.min.js deleted file mode 100644 index c2b3ff48..00000000 --- a/apidoc/vendor/semver.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(e){if(typeof module==="object"&&module.exports===e)e=module.exports=K;e.SEMVER_SPEC_VERSION="2.0.0";var r=256;var t=Number.MAX_SAFE_INTEGER||9007199254740991;var n=e.re=[];var i=e.src=[];var s=0;var o=s++;i[o]="0|[1-9]\\d*";var a=s++;i[a]="[0-9]+";var f=s++;i[f]="\\d*[a-zA-Z-][a-zA-Z0-9-]*";var u=s++;i[u]="("+i[o]+")\\."+"("+i[o]+")\\."+"("+i[o]+")";var l=s++;i[l]="("+i[a]+")\\."+"("+i[a]+")\\."+"("+i[a]+")";var p=s++;i[p]="(?:"+i[o]+"|"+i[f]+")";var h=s++;i[h]="(?:"+i[a]+"|"+i[f]+")";var c=s++;i[c]="(?:-("+i[p]+"(?:\\."+i[p]+")*))";var v=s++;i[v]="(?:-?("+i[h]+"(?:\\."+i[h]+")*))";var m=s++;i[m]="[0-9A-Za-z-]+";var g=s++;i[g]="(?:\\+("+i[m]+"(?:\\."+i[m]+")*))";var w=s++;var y="v?"+i[u]+i[c]+"?"+i[g]+"?";i[w]="^"+y+"$";var d="[v=\\s]*"+i[l]+i[v]+"?"+i[g]+"?";var j=s++;i[j]="^"+d+"$";var b=s++;i[b]="((?:<|>)?=?)";var E=s++;i[E]=i[a]+"|x|X|\\*";var $=s++;i[$]=i[o]+"|x|X|\\*";var k=s++;i[k]="[v=\\s]*("+i[$]+")"+"(?:\\.("+i[$]+")"+"(?:\\.("+i[$]+")"+"(?:"+i[c]+")?"+i[g]+"?"+")?)?";var R=s++;i[R]="[v=\\s]*("+i[E]+")"+"(?:\\.("+i[E]+")"+"(?:\\.("+i[E]+")"+"(?:"+i[v]+")?"+i[g]+"?"+")?)?";var S=s++;i[S]="^"+i[b]+"\\s*"+i[k]+"$";var x=s++;i[x]="^"+i[b]+"\\s*"+i[R]+"$";var I=s++;i[I]="(?:~>?)";var T=s++;i[T]="(\\s*)"+i[I]+"\\s+";n[T]=new RegExp(i[T],"g");var V="$1~";var A=s++;i[A]="^"+i[I]+i[k]+"$";var C=s++;i[C]="^"+i[I]+i[R]+"$";var M=s++;i[M]="(?:\\^)";var N=s++;i[N]="(\\s*)"+i[M]+"\\s+";n[N]=new RegExp(i[N],"g");var _="$1^";var z=s++;i[z]="^"+i[M]+i[k]+"$";var P=s++;i[P]="^"+i[M]+i[R]+"$";var X=s++;i[X]="^"+i[b]+"\\s*("+d+")$|^$";var Z=s++;i[Z]="^"+i[b]+"\\s*("+y+")$|^$";var q=s++;i[q]="(\\s*)"+i[b]+"\\s*("+d+"|"+i[k]+")";n[q]=new RegExp(i[q],"g");var L="$1$2$3";var F=s++;i[F]="^\\s*("+i[k]+")"+"\\s+-\\s+"+"("+i[k]+")"+"\\s*$";var G=s++;i[G]="^\\s*("+i[R]+")"+"\\s+-\\s+"+"("+i[R]+")"+"\\s*$";var O=s++;i[O]="(<|>)?=?\\s*\\*";for(var B=0;Br)return null;var i=t?n[j]:n[w];if(!i.test(e))return null;try{return new K(e,t)}catch(s){return null}}e.valid=H;function H(e,r){var t=D(e,r);return t?t.version:null}e.clean=J;function J(e,r){var t=D(e.trim().replace(/^[=v]+/,""),r);return t?t.version:null}e.SemVer=K;function K(e,i){if(e instanceof K){if(e.loose===i)return e;else e=e.version}else if(typeof e!=="string"){throw new TypeError("Invalid Version: "+e)}if(e.length>r)throw new TypeError("version is longer than "+r+" characters");if(!(this instanceof K))return new K(e,i);this.loose=i;var s=e.trim().match(i?n[j]:n[w]);if(!s)throw new TypeError("Invalid Version: "+e);this.raw=e;this.major=+s[1];this.minor=+s[2];this.patch=+s[3];if(this.major>t||this.major<0)throw new TypeError("Invalid major version");if(this.minor>t||this.minor<0)throw new TypeError("Invalid minor version");if(this.patch>t||this.patch<0)throw new TypeError("Invalid patch version");if(!s[4])this.prerelease=[];else this.prerelease=s[4].split(".").map(function(e){if(/^[0-9]+$/.test(e)){var r=+e;if(r>=0&&r'};K.prototype.toString=function(){return this.version};K.prototype.compare=function(e){if(!(e instanceof K))e=new K(e,this.loose);return this.compareMain(e)||this.comparePre(e)};K.prototype.compareMain=function(e){if(!(e instanceof K))e=new K(e,this.loose);return Y(this.major,e.major)||Y(this.minor,e.minor)||Y(this.patch,e.patch)};K.prototype.comparePre=function(e){if(!(e instanceof K))e=new K(e,this.loose);if(this.prerelease.length&&!e.prerelease.length)return-1;else if(!this.prerelease.length&&e.prerelease.length)return 1;else if(!this.prerelease.length&&!e.prerelease.length)return 0;var r=0;do{var t=this.prerelease[r];var n=e.prerelease[r];if(t===undefined&&n===undefined)return 0;else if(n===undefined)return 1;else if(t===undefined)return-1;else if(t===n)continue;else return Y(t,n)}while(++r)};K.prototype.inc=function(e,r){switch(e){case"premajor":this.prerelease.length=0;this.patch=0;this.minor=0;this.major++;this.inc("pre",r);break;case"preminor":this.prerelease.length=0;this.patch=0;this.minor++;this.inc("pre",r);break;case"prepatch":this.prerelease.length=0;this.inc("patch",r);this.inc("pre",r);break;case"prerelease":if(this.prerelease.length===0)this.inc("patch",r);this.inc("pre",r);break;case"major":if(this.minor!==0||this.patch!==0||this.prerelease.length===0)this.major++;this.minor=0;this.patch=0;this.prerelease=[];break;case"minor":if(this.patch!==0||this.prerelease.length===0)this.minor++;this.patch=0;this.prerelease=[];break;case"patch":if(this.prerelease.length===0)this.patch++;this.prerelease=[];break;case"pre":if(this.prerelease.length===0)this.prerelease=[0];else{var t=this.prerelease.length;while(--t>=0){if(typeof this.prerelease[t]==="number"){this.prerelease[t]++;t=-2}}if(t===-1)this.prerelease.push(0)}if(r){if(this.prerelease[0]===r){if(isNaN(this.prerelease[1]))this.prerelease=[r,0]}else this.prerelease=[r,0]}break;default:throw new Error("invalid increment argument: "+e)}this.format();return this};e.inc=Q;function Q(e,r,t,n){if(typeof t==="string"){n=t;t=undefined}try{return new K(e,t).inc(r,n).version}catch(i){return null}}e.diff=U;function U(e,r){if(pr(e,r)){return null}else{var t=D(e);var n=D(r);if(t.prerelease.length||n.prerelease.length){for(var i in t){if(i==="major"||i==="minor"||i==="patch"){if(t[i]!==n[i]){return"pre"+i}}}return"prerelease"}for(var i in t){if(i==="major"||i==="minor"||i==="patch"){if(t[i]!==n[i]){return i}}}}}e.compareIdentifiers=Y;var W=/^[0-9]+$/;function Y(e,r){var t=W.test(e);var n=W.test(r);if(t&&n){e=+e;r=+r}return t&&!n?-1:n&&!t?1:er?1:0}e.rcompareIdentifiers=er;function er(e,r){return Y(r,e)}e.major=rr;function rr(e,r){return new K(e,r).major}e.minor=tr;function tr(e,r){return new K(e,r).minor}e.patch=nr;function nr(e,r){return new K(e,r).patch}e.compare=ir;function ir(e,r,t){return new K(e,t).compare(r)}e.compareLoose=sr;function sr(e,r){return ir(e,r,true)}e.rcompare=or;function or(e,r,t){return ir(r,e,t)}e.sort=ar;function ar(r,t){return r.sort(function(r,n){return e.compare(r,n,t)})}e.rsort=fr;function fr(r,t){return r.sort(function(r,n){return e.rcompare(r,n,t)})}e.gt=ur;function ur(e,r,t){return ir(e,r,t)>0}e.lt=lr;function lr(e,r,t){return ir(e,r,t)<0}e.eq=pr;function pr(e,r,t){return ir(e,r,t)===0}e.neq=hr;function hr(e,r,t){return ir(e,r,t)!==0}e.gte=cr;function cr(e,r,t){return ir(e,r,t)>=0}e.lte=vr;function vr(e,r,t){return ir(e,r,t)<=0}e.cmp=mr;function mr(e,r,t,n){var i;switch(r){case"===":if(typeof e==="object")e=e.version;if(typeof t==="object")t=t.version;i=e===t;break;case"!==":if(typeof e==="object")e=e.version;if(typeof t==="object")t=t.version;i=e!==t;break;case"":case"=":case"==":i=pr(e,t,n);break;case"!=":i=hr(e,t,n);break;case">":i=ur(e,t,n);break;case">=":i=cr(e,t,n);break;case"<":i=lr(e,t,n);break;case"<=":i=vr(e,t,n);break;default:throw new TypeError("Invalid operator: "+r)}return i}e.Comparator=gr;function gr(e,r){if(e instanceof gr){if(e.loose===r)return e;else e=e.value}if(!(this instanceof gr))return new gr(e,r);this.loose=r;this.parse(e);if(this.semver===wr)this.value="";else this.value=this.operator+this.semver.version}var wr={};gr.prototype.parse=function(e){var r=this.loose?n[X]:n[Z];var t=e.match(r);if(!t)throw new TypeError("Invalid comparator: "+e);this.operator=t[1];if(this.operator==="=")this.operator="";if(!t[2])this.semver=wr;else this.semver=new K(t[2],this.loose)};gr.prototype.inspect=function(){return''};gr.prototype.toString=function(){return this.value};gr.prototype.test=function(e){if(this.semver===wr)return true;if(typeof e==="string")e=new K(e,this.loose);return mr(e,this.operator,this.semver,this.loose)};e.Range=yr;function yr(e,r){if(e instanceof yr&&e.loose===r)return e;if(!(this instanceof yr))return new yr(e,r);this.loose=r;this.raw=e;this.set=e.split(/\s*\|\|\s*/).map(function(e){return this.parseRange(e.trim())},this).filter(function(e){return e.length});if(!this.set.length){throw new TypeError("Invalid SemVer Range: "+e)}this.format()}yr.prototype.inspect=function(){return''};yr.prototype.format=function(){this.range=this.set.map(function(e){return e.join(" ").trim()}).join("||").trim();return this.range};yr.prototype.toString=function(){return this.range};yr.prototype.parseRange=function(e){var r=this.loose;e=e.trim();var t=r?n[G]:n[F];e=e.replace(t,Tr);e=e.replace(n[q],L);e=e.replace(n[T],V);e=e.replace(n[N],_);e=e.split(/\s+/).join(" ");var i=r?n[X]:n[Z];var s=e.split(" ").map(function(e){return jr(e,r)}).join(" ").split(/\s+/);if(this.loose){s=s.filter(function(e){return!!e.match(i)})}s=s.map(function(e){return new gr(e,r)});return s};e.toComparators=dr;function dr(e,r){return new yr(e,r).set.map(function(e){return e.map(function(e){return e.value}).join(" ").trim().split(" ")})}function jr(e,r){e=kr(e,r);e=Er(e,r);e=Sr(e,r);e=Ir(e,r);return e}function br(e){return!e||e.toLowerCase()==="x"||e==="*"}function Er(e,r){return e.trim().split(/\s+/).map(function(e){return $r(e,r)}).join(" ")}function $r(e,r){var t=r?n[C]:n[A];return e.replace(t,function(e,r,t,n,i){var s;if(br(r))s="";else if(br(t))s=">="+r+".0.0 <"+(+r+1)+".0.0";else if(br(n))s=">="+r+"."+t+".0 <"+r+"."+(+t+1)+".0";else if(i){if(i.charAt(0)!=="-")i="-"+i;s=">="+r+"."+t+"."+n+i+" <"+r+"."+(+t+1)+".0"}else s=">="+r+"."+t+"."+n+" <"+r+"."+(+t+1)+".0";return s})}function kr(e,r){return e.trim().split(/\s+/).map(function(e){return Rr(e,r)}).join(" ")}function Rr(e,r){var t=r?n[P]:n[z];return e.replace(t,function(e,r,t,n,i){var s;if(br(r))s="";else if(br(t))s=">="+r+".0.0 <"+(+r+1)+".0.0";else if(br(n)){if(r==="0")s=">="+r+"."+t+".0 <"+r+"."+(+t+1)+".0";else s=">="+r+"."+t+".0 <"+(+r+1)+".0.0"}else if(i){if(i.charAt(0)!=="-")i="-"+i;if(r==="0"){if(t==="0")s=">="+r+"."+t+"."+n+i+" <"+r+"."+t+"."+(+n+1);else s=">="+r+"."+t+"."+n+i+" <"+r+"."+(+t+1)+".0"}else s=">="+r+"."+t+"."+n+i+" <"+(+r+1)+".0.0"}else{if(r==="0"){if(t==="0")s=">="+r+"."+t+"."+n+" <"+r+"."+t+"."+(+n+1);else s=">="+r+"."+t+"."+n+" <"+r+"."+(+t+1)+".0"}else s=">="+r+"."+t+"."+n+" <"+(+r+1)+".0.0"}return s})}function Sr(e,r){return e.split(/\s+/).map(function(e){return xr(e,r)}).join(" ")}function xr(e,r){e=e.trim();var t=r?n[x]:n[S];return e.replace(t,function(e,r,t,n,i,s){var o=br(t);var a=o||br(n);var f=a||br(i);var u=f;if(r==="="&&u)r="";if(o){if(r===">"||r==="<"){e="<0.0.0"}else{e="*"}}else if(r&&u){if(a)n=0;if(f)i=0;if(r===">"){r=">=";if(a){t=+t+1;n=0;i=0}else if(f){n=+n+1;i=0}}else if(r==="<="){r="<";if(a)t=+t+1;else n=+n+1}e=r+t+"."+n+"."+i}else if(a){e=">="+t+".0.0 <"+(+t+1)+".0.0"}else if(f){e=">="+t+"."+n+".0 <"+t+"."+(+n+1)+".0"}return e})}function Ir(e,r){return e.trim().replace(n[O],"")}function Tr(e,r,t,n,i,s,o,a,f,u,l,p,h){if(br(t))r="";else if(br(n))r=">="+t+".0.0";else if(br(i))r=">="+t+"."+n+".0";else r=">="+r;if(br(f))a="";else if(br(u))a="<"+(+f+1)+".0.0";else if(br(l))a="<"+f+"."+(+u+1)+".0";else if(p)a="<="+f+"."+u+"."+l+"-"+p;else a="<="+a;return(r+" "+a).trim()}yr.prototype.test=function(e){if(!e)return false;if(typeof e==="string")e=new K(e,this.loose);for(var r=0;r0){var n=e[t].semver;if(n.major===r.major&&n.minor===r.minor&&n.patch===r.patch)return true}}return false}return true}e.satisfies=Ar;function Ar(e,r,t){try{r=new yr(r,t)}catch(n){return false}return r.test(e)}e.maxSatisfying=Cr;function Cr(e,r,t){return e.filter(function(e){return Ar(e,r,t)}).sort(function(e,r){return or(e,r,t)})[0]||null}e.validRange=Mr;function Mr(e,r){try{return new yr(e,r).range||"*"}catch(t){return null}}e.ltr=Nr;function Nr(e,r,t){return zr(e,r,"<",t)}e.gtr=_r;function _r(e,r,t){return zr(e,r,">",t)}e.outside=zr;function zr(e,r,t,n){e=new K(e,n);r=new yr(r,n);var i,s,o,a,f;switch(t){case">":i=ur;s=vr;o=lr;a=">";f=">=";break;case"<":i=lr;s=cr;o=ur;a="<";f="<=";break;default:throw new TypeError('Must provide a hilo val of "<" or ">"')}if(Ar(e,r,n)){return false}for(var u=0;u=b.f?e():a.fonts.load(fa(b.a),b.h).then(function(a){1<=a.length?c():setTimeout(k,25)},function(){e()})}k()}),e=new Promise(function(a,c){setTimeout(c,b.f)});Promise.race([e,c]).then(function(){b.g(b.a)},function(){b.j(b.a)})};function R(a,b,d,c,e,f,g){this.v=a;this.B=b;this.c=d;this.a=c;this.s=g||"BESbswy";this.f={};this.w=e||3E3;this.u=f||null;this.o=this.j=this.h=this.g=null;this.g=new N(this.c,this.s);this.h=new N(this.c,this.s);this.j=new N(this.c,this.s);this.o=new N(this.c,this.s);a=new H(this.a.c+",serif",K(this.a));a=P(a);this.g.a.style.cssText=a;a=new H(this.a.c+",sans-serif",K(this.a));a=P(a);this.h.a.style.cssText=a;a=new H("serif",K(this.a));a=P(a);this.j.a.style.cssText=a;a=new H("sans-serif",K(this.a));a= -P(a);this.o.a.style.cssText=a;O(this.g);O(this.h);O(this.j);O(this.o)}var S={D:"serif",C:"sans-serif"},T=null;function U(){if(null===T){var a=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent);T=!!a&&(536>parseInt(a[1],10)||536===parseInt(a[1],10)&&11>=parseInt(a[2],10))}return T}R.prototype.start=function(){this.f.serif=this.j.a.offsetWidth;this.f["sans-serif"]=this.o.a.offsetWidth;this.A=q();la(this)}; -function ma(a,b,d){for(var c in S)if(S.hasOwnProperty(c)&&b===a.f[S[c]]&&d===a.f[S[c]])return!0;return!1}function la(a){var b=a.g.a.offsetWidth,d=a.h.a.offsetWidth,c;(c=b===a.f.serif&&d===a.f["sans-serif"])||(c=U()&&ma(a,b,d));c?q()-a.A>=a.w?U()&&ma(a,b,d)&&(null===a.u||a.u.hasOwnProperty(a.a.c))?V(a,a.v):V(a,a.B):na(a):V(a,a.v)}function na(a){setTimeout(p(function(){la(this)},a),50)}function V(a,b){setTimeout(p(function(){v(this.g.a);v(this.h.a);v(this.j.a);v(this.o.a);b(this.a)},a),0)};function W(a,b,d){this.c=a;this.a=b;this.f=0;this.o=this.j=!1;this.s=d}var X=null;W.prototype.g=function(a){var b=this.a;b.g&&w(b.f,[b.a.c("wf",a.c,K(a).toString(),"active")],[b.a.c("wf",a.c,K(a).toString(),"loading"),b.a.c("wf",a.c,K(a).toString(),"inactive")]);L(b,"fontactive",a);this.o=!0;oa(this)}; -W.prototype.h=function(a){var b=this.a;if(b.g){var d=y(b.f,b.a.c("wf",a.c,K(a).toString(),"active")),c=[],e=[b.a.c("wf",a.c,K(a).toString(),"loading")];d||c.push(b.a.c("wf",a.c,K(a).toString(),"inactive"));w(b.f,c,e)}L(b,"fontinactive",a);oa(this)};function oa(a){0==--a.f&&a.j&&(a.o?(a=a.a,a.g&&w(a.f,[a.a.c("wf","active")],[a.a.c("wf","loading"),a.a.c("wf","inactive")]),L(a,"active")):M(a.a))};function pa(a){this.j=a;this.a=new ja;this.h=0;this.f=this.g=!0}pa.prototype.load=function(a){this.c=new ca(this.j,a.context||this.j);this.g=!1!==a.events;this.f=!1!==a.classes;qa(this,new ha(this.c,a),a)}; -function ra(a,b,d,c,e){var f=0==--a.h;(a.f||a.g)&&setTimeout(function(){var a=e||null,k=c||null||{};if(0===d.length&&f)M(b.a);else{b.f+=d.length;f&&(b.j=f);var h,m=[];for(h=0;h { - console.debug("Error connecting to database: " + err.message); -}); - -import "./models/accounts"; -import "./models/codes"; -import "./models/deposits"; -import "./models/invoices"; -import "./models/keys"; -import "./models/payments"; -import "./models/orders"; -import "./models/requests"; -import "./models/users"; -import "./models/withdrawals"; - -import "./models/referrals"; -import "./models/waiting_list"; - -const { User, Account, Payment, Invoice, Key, Order, Referral, Request } = db; - -User.hasMany(Account, { - as: "accounts", - foreignKey: "user_id" -}); - -User.hasMany(Invoice, { - as: "invoices", - foreignKey: "user_id" -}); - -User.hasMany(Key, { - as: "keys", - foreignKey: "user_id" -}); - -User.hasMany(Order, { - as: "orders", - foreignKey: "user_id" -}); - -User.hasMany(Payment, { - as: "payments", - foreignKey: "user_id" -}); - -User.belongsTo(Account, { - as: "account", - foreignKey: "account_id" -}); - -Invoice.belongsTo(Account, { - as: "account", - foreignKey: "account_id" -}); - -Account.belongsTo(User, { - as: "user", - foreignKey: "user_id" -}); - -Invoice.belongsTo(User, { - as: "user", - foreignKey: "user_id" -}); - -Key.belongsTo(User, { - as: "user", - foreignKey: "user_id" -}); - -Payment.belongsTo(Account, { - as: "account", - foreignKey: "account_id" -}); - -Payment.belongsTo(User, { - as: "user", - foreignKey: "user_id" -}); - -Payment.belongsTo(Invoice, { - as: "invoice", - foreignKey: "invoice_id" -}); - -Order.belongsTo(User, { - as: "user", - foreignKey: "user_id" -}); - -Order.belongsTo(Account, { - as: "acc1", - foreignKey: "a1_id" -}); - -Order.belongsTo(Account, { - as: "acc2", - foreignKey: "a2_id" -}); - -User.hasMany(Referral, { - as: "referral_codes", - foreignKey: "sponsor_id" -}); - -User.hasMany(Referral, { - as: "sponsors", - foreignKey: "user_id" -}); - -Referral.belongsTo(User, { - as: "sponsor", - foreignKey: "sponsor_id" -}); - -Referral.belongsTo(User, { - as: "user", - foreignKey: "user_id" -}); - -Payment.belongsTo(Payment, { - as: "fee_payment", - foreignKey: "fee_payment_id" -}); - -Payment.belongsTo(User, { - as: "with", - foreignKey: "with_id" -}); - -Request.belongsTo(User, { - as: "requester", - foreignKey: "requester_id" -}); - -Request.belongsTo(User, { - as: "recipient", - foreignKey: "recipient_id" -}); - -Request.belongsTo(Invoice, { - as: "invoice", - foreignKey: "invoice_id" -}); - -Invoice.hasOne(Request, { - as: "request", - foreignKey: "invoice_id" -}); - -db.Order = Order; - -export default db; diff --git a/db/init/prodschema.sql b/db/init/prodschema.sql deleted file mode 100644 index 38e25324..00000000 --- a/db/init/prodschema.sql +++ /dev/null @@ -1,312 +0,0 @@ --- MySQL dump 10.14 Distrib 5.5.68-MariaDB, for Linux (x86_64) --- --- Host: localhost Database: coinos --- ------------------------------------------------------ --- Server version 5.5.68-MariaDB - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!40101 SET NAMES utf8 */; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; - --- --- Table structure for table `SequelizeMeta` --- - -DROP TABLE IF EXISTS `SequelizeMeta`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `SequelizeMeta` ( - `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL, - PRIMARY KEY (`name`), - UNIQUE KEY `name` (`name`), - UNIQUE KEY `SequelizeMeta_name_unique` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `accounts` --- - -DROP TABLE IF EXISTS `accounts`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `accounts` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) DEFAULT NULL, - `asset` varchar(255) DEFAULT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `balance` double DEFAULT NULL, - `pending` double DEFAULT NULL, - `name` varchar(255) DEFAULT NULL, - `ticker` varchar(255) DEFAULT NULL, - `precision` int(11) DEFAULT NULL, - `domain` varchar(255) DEFAULT NULL, - `contract` text, - `index` int(11) NOT NULL DEFAULT '0', - `pubkey` varchar(255) DEFAULT NULL, - `hide` tinyint(1) DEFAULT NULL, - `seed` varchar(255) DEFAULT NULL, - `path` varchar(255) DEFAULT NULL, - `network` varchar(255) DEFAULT NULL, - `privkey` varchar(255) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `asset` (`asset`) -) ENGINE=InnoDB AUTO_INCREMENT=20553 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `cheaters` --- - -DROP TABLE IF EXISTS `cheaters`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `cheaters` ( - `id` int(11) NOT NULL DEFAULT '0', - `username` varchar(255) DEFAULT NULL, - `sum` double DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `codes` --- - -DROP TABLE IF EXISTS `codes`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `codes` ( - `code` varchar(255) NOT NULL DEFAULT '', - `text` text, - PRIMARY KEY (`code`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `deposits` --- - -DROP TABLE IF EXISTS `deposits`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `deposits` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) DEFAULT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `amount` double DEFAULT NULL, - `credited` tinyint(1) DEFAULT NULL, - `code` varchar(255) DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=4554 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `invoices` --- - -DROP TABLE IF EXISTS `invoices`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `invoices` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) DEFAULT NULL, - `text` text, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `rate` double DEFAULT NULL, - `currency` varchar(255) NOT NULL, - `address` varchar(255) DEFAULT NULL, - `received` double DEFAULT NULL, - `amount` double DEFAULT NULL, - `tip` double DEFAULT NULL, - `network` varchar(255) DEFAULT NULL, - `unconfidential` varchar(255) DEFAULT NULL, - `uuid` varchar(255) DEFAULT NULL, - `memo` text, - `account_id` int(11) DEFAULT NULL, - `path` varchar(255) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `part_of_unconfidential` (`unconfidential`(10)), - KEY `text_index` (`text`(100)), - KEY `unconfidential_index` (`unconfidential`(100)), - KEY `address_index` (`address`(100)) -) ENGINE=InnoDB AUTO_INCREMENT=36355 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `linkingkeys` --- - -DROP TABLE IF EXISTS `linkingkeys`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `linkingkeys` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) DEFAULT NULL, - `hex` varchar(255) DEFAULT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=11822 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `naughty` --- - -DROP TABLE IF EXISTS `naughty`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `naughty` ( - `id` int(11) NOT NULL DEFAULT '0', - `username` varchar(255) DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `orders` --- - -DROP TABLE IF EXISTS `orders`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `orders` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `a1` varchar(255) DEFAULT NULL, - `a2` varchar(255) DEFAULT NULL, - `v1` double DEFAULT NULL, - `v2` double DEFAULT NULL, - `user_id` int(11) DEFAULT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `accepted` tinyint(1) NOT NULL DEFAULT '0', - `a1_id` int(11) DEFAULT NULL, - `a2_id` int(11) DEFAULT NULL, - `rate` double AS (v2/v1) VIRTUAL, - `completedAt` datetime DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=389 DEFAULT CHARSET=utf8mb4; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `payments` --- - -DROP TABLE IF EXISTS `payments`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `payments` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) DEFAULT NULL, - `hash` text, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `rate` double DEFAULT NULL, - `currency` varchar(255) DEFAULT NULL, - `address` varchar(255) DEFAULT NULL, - `received` tinyint(1) DEFAULT NULL, - `amount` double DEFAULT NULL, - `tip` double NOT NULL DEFAULT '0', - `confirmed` tinyint(1) NOT NULL, - `fee` double NOT NULL DEFAULT '0', - `network` varchar(255) DEFAULT NULL, - `account_id` int(11) DEFAULT NULL, - `preimage` varchar(255) DEFAULT NULL, - `memo` text, - `redeemed` tinyint(1) NOT NULL DEFAULT '0', - `redeemcode` varchar(255) DEFAULT NULL, - `path` varchar(255) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `idx_redeemcode` (`redeemcode`) -) ENGINE=InnoDB AUTO_INCREMENT=25828 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `reset` --- - -DROP TABLE IF EXISTS `reset`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `reset` ( - `password` varchar(255) DEFAULT NULL, - `seed` varchar(255) DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `users` --- - -DROP TABLE IF EXISTS `users`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `users` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `username` varchar(255) DEFAULT NULL, - `password` varchar(255) DEFAULT NULL, - `unit` varchar(255) DEFAULT 'SAT', - `symbol` varchar(255) DEFAULT NULL, - `currency` varchar(255) DEFAULT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `twofa` tinyint(1) DEFAULT NULL, - `pin` varchar(255) DEFAULT NULL, - `currencies` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, - `otpsecret` varchar(255) DEFAULT NULL, - `account_id` int(11) DEFAULT NULL, - `ip` int(10) unsigned DEFAULT NULL, - `subscriptions` text, - `seed` varchar(255) DEFAULT NULL, - `fiat` tinyint(1) NOT NULL DEFAULT '0', - `index` int(11) NOT NULL DEFAULT '0', - `verified` varchar(255) DEFAULT NULL, - `locked` tinyint(1) DEFAULT '0', - PRIMARY KEY (`id`), - KEY `ip` (`ip`), - KEY `username` (`username`) -) ENGINE=InnoDB AUTO_INCREMENT=14579 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `withdrawals` --- - -DROP TABLE IF EXISTS `withdrawals`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `withdrawals` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) NOT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `amount` double DEFAULT NULL, - `completed` tinyint(1) DEFAULT NULL, - `transit` varchar(255) DEFAULT NULL, - `account` varchar(255) DEFAULT NULL, - `institution` varchar(255) DEFAULT NULL, - `notes` text, - `email` varchar(255) DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=4118 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; -/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; - -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; - --- Dump completed on 2021-07-15 1:26:35 diff --git a/db/migrations/20210612181508_init_referrals.js b/db/migrations/20210612181508_init_referrals.js deleted file mode 100644 index 63b20ce4..00000000 --- a/db/migrations/20210612181508_init_referrals.js +++ /dev/null @@ -1,24 +0,0 @@ -export const up = function(knex) { - return Promise.all([ - knex.schema.dropTableIfExists('referrals'), - - knex.schema.createTable('referrals', function(t) { - t.increments('id').unsigned().primary(); - t.integer('user_id').nullable(); - t.foreign('user_id').references('users.id'); - - t.integer('sponsor_id').notNull(); - t.foreign('sponsor_id').references('users.id'); - - t.string('token').notNull(); - t.string('expiry').nullable(); - t.enum('status', ['available', 'used', 'expired', 'cancelled']).notNull().defaultTo('available'); - - t.timestamps(true, true); - }) - ]) -}; - -export const down = function(knex) { - return knex.schema.dropTable('referrals'); -}; diff --git a/db/migrations/20210612192804_init_waiting_list.js.js b/db/migrations/20210612192804_init_waiting_list.js.js deleted file mode 100644 index 0cef7356..00000000 --- a/db/migrations/20210612192804_init_waiting_list.js.js +++ /dev/null @@ -1,24 +0,0 @@ -export const up = function(knex) { - - return Promise.all([ - knex.schema.dropTableIfExists('waiting_list'), - - knex.schema.createTable('waiting_list', function(t) { - t.increments('id').unsigned().primary(); - - t.string('email').notNull(); - t.string('phone').notNull(); - t.enu('status', ['pending', 'activated', 'expired', 'cancelled']).notNull(); - - t.integer('user_id').nullable(); - t.foreign('user_id').references('users.id'); - t.text('notes').nullable(); - - t.timestamps(true, true); - }) - ]) -}; - -export const down = function(knex) { - return knex.schema.dropTable('waiting_list'); -}; diff --git a/db/migrations/20210612203333_schema_diff_merges.js b/db/migrations/20210612203333_schema_diff_merges.js deleted file mode 100644 index 418105d2..00000000 --- a/db/migrations/20210612203333_schema_diff_merges.js +++ /dev/null @@ -1,39 +0,0 @@ -export const up = function(knex) { - return Promise.all([ - knex.schema.dropTableIfExists('reset'), - knex.schema.dropTableIfExists('naughty'), - knex.schema.dropTableIfExists('cheaters'), - - knex.raw("UPDATE accounts set createdAt = '2020-01-01' where createdAt < '2020-01-01'"), - knex.raw("UPDATE accounts set updatedAt = '2020-01-01' where updatedAt < '2020-01-01'"), - knex.raw("ALTER TABLE accounts modify `contract` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL"), - - knex.schema.alterTable('orders', function(t) { - t.dropColumn('a1'); - t.dropColumn('a2'); - }), - - knex.schema.alterTable("users", function(t) { - t.string('authyId').nullable(); - t.dropColumn('symbol'); - }), - - knex.raw("ALTER TABLE users MODIFY `subscriptions` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL"), - - ]) -}; - -export const down = async function(knex) { - return Promise.all([ - knex.schema.table("orders", function(t) { - t.integer("a1"); - t.integer("a2") - }), - - knex.schema.table("users", function(t) { - t.string("symbol"); - t.dropColumn("unit"); - t.dropColumn("authyId"); - }), - ]) -}; \ No newline at end of file diff --git a/db/migrations/20210625232223_add_invoice_id_to_payments.js b/db/migrations/20210625232223_add_invoice_id_to_payments.js deleted file mode 100644 index adf99d24..00000000 --- a/db/migrations/20210625232223_add_invoice_id_to_payments.js +++ /dev/null @@ -1,13 +0,0 @@ -export const up = async function (knex) { - return knex.schema.table("payments", function(table) { - table.integer("invoice_id"); - table.foreign("invoice_id").references("invoices.id"); - }) -}; - -export const down = async function (knex) { - return knex.schema.table("payments", function(table) { - table.dropForeign("invoice_id"); - table.dropColumn("invoice_id"); - }) -}; diff --git a/db/migrations/20210628221727_add_webhook_to_invoices.js b/db/migrations/20210628221727_add_webhook_to_invoices.js deleted file mode 100644 index 434819e5..00000000 --- a/db/migrations/20210628221727_add_webhook_to_invoices.js +++ /dev/null @@ -1,11 +0,0 @@ -export const up = async function(knex) { - return knex.schema.table("invoices", function(table) { - table.text("webhook"); - }); -}; - -export const down = async function(knex) { - return knex.schema.table("invoices", function(table) { - table.dropColumn("webhook"); - }); -}; diff --git a/db/migrations/20210713174217_add_admin_to_users.js b/db/migrations/20210713174217_add_admin_to_users.js deleted file mode 100644 index 879c8cb7..00000000 --- a/db/migrations/20210713174217_add_admin_to_users.js +++ /dev/null @@ -1,16 +0,0 @@ -export const up = async function(knex) { - return knex.schema.table("users", function(t) { - t.boolean("admin"); - t.string('email'); - t.string('phone'); - - }); -}; - -export const down = async function(knex) { - return knex.schema.table("users", function(t) { - t.dropColumn("admin"); - t.dropColumn('email') - t.dropColumn('phone') - }); -}; diff --git a/db/migrations/20210714123455_data_fixes_merge.js b/db/migrations/20210714123455_data_fixes_merge.js deleted file mode 100644 index dfe96509..00000000 --- a/db/migrations/20210714123455_data_fixes_merge.js +++ /dev/null @@ -1,22 +0,0 @@ -export const up = function(knex) { - return Promise.all([ - knex.raw("delete from invoices where invoices.account_id not in (select id from accounts)"), - knex.raw("delete from orders where orders.a1_id not in (select id from accounts) or orders.a2_id not in (select id from accounts)"), - knex.raw("update users set account_id = 10427 where id = 6262 and account_id = 10428"), - knex.raw("delete from payments where account_id not in (select id from accounts)"), - knex.raw("alter table orders drop rate"), - knex.raw("alter table orders add rate double"), - knex.raw("update orders set rate = v1/v2"), - - knex.raw('update accounts left join users on accounts.user_id = users.id set user_id = null where length(username) > 32'), - knex.raw('update accounts left join users on accounts.user_id = users.id set user_id = null where users.id is null'), - knex.raw('delete from accounts where user_id is null'), - knex.raw('delete from users where length(username) > 32') - ]) -}; - -export const down = function(knex) { - return Promise.all([ - - ]) -}; diff --git a/db/migrations/20210714123456_add_fks_merge.js b/db/migrations/20210714123456_add_fks_merge.js deleted file mode 100644 index 3d5f3e94..00000000 --- a/db/migrations/20210714123456_add_fks_merge.js +++ /dev/null @@ -1,45 +0,0 @@ -export const up = function(knex) { - return Promise.all([ - - // alter table invoices add foreign key (user_id) references users (id); - // alter table invoices add foreign key (account_id) references accounts (id); - knex.schema.alterTable("invoices", function(t) { - t.foreign('user_id').references('users.id') - t.foreign('account_id').references('accounts.id') - t.index('updatedAt') - }), - - // alter table payments add foreign key (user_id) references users (id); - // alter table payments add foreign key (account_id) references accounts (id); - knex.schema.alterTable("payments", function(t) { - t.foreign('user_id').references('users.id') - t.foreign('account_id').references('accounts.id') - t.index('updatedAt') - }), - - // alter table orders add foreign key (user_id) references users (id); - // alter table orders add foreign key (a1_id) references accounts (id); - // alter table orders add foreign key (a2_id) references accounts (id); - knex.schema.alterTable("orders", function(t) { - t.foreign('user_id').references('users.id') - t.foreign('a1_id').references('accounts.id') - t.foreign('a2_id').references('accounts.id') - t.index('updatedAt') - }), - - // alter table deposits add foreign key (user_id) references users (id) - knex.schema.alterTable("deposits", function(t) { - t.foreign('user_id').references('users.id') - t.index('updatedAt') - }), - - // alter table withdrawals add foreign key (user_id) references users (id) - knex.schema.alterTable("withdrawals", function(t) { - t.foreign('user_id').references('users.id') - t.index('updatedAt') - }), - ]) -}; - -export const down = function(knex) { -}; diff --git a/db/migrations/s2/user_adjustments.js b/db/migrations/s2/user_adjustments.js deleted file mode 100644 index cbe17977..00000000 --- a/db/migrations/s2/user_adjustments.js +++ /dev/null @@ -1,67 +0,0 @@ - -export const up = function(knex) { - return Promise.all([ - - // move security fields to separate t (potentially different access permissions) - - knex.schema.createTable('user_security', function (t) { - t.increments('id').unsigned().primary(); - t.int('user_id').notNull(); - t.foreign('user_id').references('users.user_id'); - - t.boolean('twofa').notNull().defaultTo(false); - t.string('pin').notNull(); - t.string('otp_secret').nullable(); - t.string('seed').nullable(); - }), - - knex.raw("INSERT INTO user_security (user_id, twofa, pin, otp_secret, seed) SELECT users.id, twofa, pin, otpsecret, seed FROM users"), - - // move user-customizeable fields to separate t (potentially different access permissions) - - knex.schema.createTable('user_preferences', function (t) { - t.increments('id').unsigned().primary(); - t.int('user_id').notNull(); - t.foreign('user_id').references('users.user_id'); - - t.enum('default_unit', ['SAT', 'BTC', 'CAD']).notNull().defaultTo('SAT'); - t.enum('default_currency', ['CAD', 'USD']).notNull().defaultTo('CAD'); - t.enum('default_fiat', ['CAD', 'USD']).notNull().defaultTo('CAD'); - t.set('show_currencies', ['CAD', 'USD']).notNull().defaultTo('CAD'); - }), - - knex.raw("INSERT INTO user_preferences (user_id, default_unit, default_currency, default_fiat, show_currencies) SELECT users.id, unit, currency, fiat, currencies FROM users"), - - // remove deprecated fields - - knex.schema.table('users', function (t) { - t.string('uuid'); - t.enum('access', ['Anonymous', 'Member', 'Admin']).defaultTo('Anonymous'); - - t.dropColumn('account_id'); - t.dropColumn('unit'); - t.dropColumn('fiat'); - t.dropColumn('currency'); - t.dropColumn('currencies'); - }) - ]) - -}; - -export const down = function(knex) { - return Promise.all([ - knex.schema.table('users', function (t) { - t.dropColumn('uuid'); - t.dropColumn('access'); - - t.int('account_id'); - t.string('unit'); - t.string('fiat'); - t.string('currency'); - t.string('currencies'); - }), - - knex.schema.dropTable('user_preferences'), - knex.schema.dropTable('user_security'), - ]); -}; diff --git a/db/models/accounts.js b/db/models/accounts.js deleted file mode 100644 index 9485bc99..00000000 --- a/db/models/accounts.js +++ /dev/null @@ -1,217 +0,0 @@ -import db from "$db/db"; -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - user_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "user_id" - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "createdAt" - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "updatedAt" - }, - path: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "path" - }, - seed: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "seed" - }, - network: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "network" - }, - name: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "name" - }, - contract: { - type: DataTypes.TEXT, - get: function() { - return JSON.parse(this.getDataValue("contract")); - }, - set: function(value) { - return this.setDataValue("contract", JSON.stringify(value)); - }, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "contract" - }, - domain: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "domain" - }, - ticker: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "ticker" - }, - asset: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "asset" - }, - balance: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "balance" - }, - btc_credits: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: 0.0, - primaryKey: false, - autoIncrement: false, - comment: "Credits that can be used towards paying Bitcoin withdrawal fees", - field: "btc_credits" - }, - liquid_credits: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: 0.0, - primaryKey: false, - autoIncrement: false, - comment: "Credits that can be used towards paying Liquid withdrawal fees", - field: "liquid_credits" - }, - lightning_credits: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: 0.0, - primaryKey: false, - autoIncrement: false, - comment: "Credits that can be used towards paying Lightning withdrawal fees", - field: "lightning_credits" - }, - pending: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "pending" - }, - hide: { - type: DataTypes.BOOLEAN, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "hide" - }, - index: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "index" - }, - privkey: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "privkey" - }, - pubkey: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "pubkey" - }, - precision: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "precision" - }, -}; - -const options = { - tableName: "accounts", - comment: "", - indexes: [] -}; - -db["Account"] = db.define("accounts_model", attributes, options); diff --git a/db/models/codes.js b/db/models/codes.js deleted file mode 100644 index 0981ac5e..00000000 --- a/db/models/codes.js +++ /dev/null @@ -1,32 +0,0 @@ -import db from "$db/db"; -import { DataTypes } from '@sequelize/core'; - -const attributes = { - code: { - type: DataTypes.STRING(255), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: false, - comment: null, - field: "code" - }, - text: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "text" - }, -}; - -const options = { - tableName: "codes", - comment: "", - indexes: [], - timestamps: false -}; - -db["Code"] = db.define("codes_model", attributes, options); diff --git a/db/models/deposits.js b/db/models/deposits.js deleted file mode 100644 index c486f468..00000000 --- a/db/models/deposits.js +++ /dev/null @@ -1,76 +0,0 @@ -import db from "$db/db"; -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - amount: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "amount" - }, - user_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "user_id" - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "createdAt" - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "updatedAt" - }, - credited: { - type: DataTypes.BOOLEAN, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "credited" - }, - code: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "code" - }, -}; - -const options = { - tableName: "deposits", - comment: "", - indexes: [] -}; - -db["Deposit"] = db.define("deposits_model", attributes, options); diff --git a/db/models/invoices.js b/db/models/invoices.js deleted file mode 100644 index 82e64427..00000000 --- a/db/models/invoices.js +++ /dev/null @@ -1,202 +0,0 @@ -import db from "$db/db"; -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - account_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "account_id" - }, - user_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "user_id" - }, - text: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "text" - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "createdAt" - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "updatedAt" - }, - path: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "path" - }, - rate: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "rate" - }, - uuid: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: DataTypes.UUIDV4, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "uuid" - }, - memo: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "memo" - }, - network: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "network" - }, - currency: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "currency" - }, - unconfidential: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "unconfidential" - }, - address: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "address" - }, - received: { - type: DataTypes.DOUBLE, - allowNull: false, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "received" - }, - amount: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "amount" - }, - pending: { - type: DataTypes.DOUBLE, - allowNull: false, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "pending" - }, - webhook: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "webhook" - }, - status: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: "unpaid", - primaryKey: false, - autoIncrement: false, - comment: null, - field: "status" - }, - tip: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "tip" - }, - prompt: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "prompt" - }, -}; - -const options = { - tableName: "invoices", - comment: "", - indexes: [] -}; - -db["Invoice"] = db.define("invoices_model", attributes, options); diff --git a/db/models/keys.js b/db/models/keys.js deleted file mode 100644 index 5a5fda0c..00000000 --- a/db/models/keys.js +++ /dev/null @@ -1,40 +0,0 @@ -import db from "$db/db"; -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - user_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "user_id" - }, - hex: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "hex" - }, -}; - -const options = { - tableName: "linkingkeys", - comment: "", - indexes: [] -}; - -db["Key"] = db.define("keys_model", attributes, options); diff --git a/db/models/orders.js b/db/models/orders.js deleted file mode 100644 index 50a74c40..00000000 --- a/db/models/orders.js +++ /dev/null @@ -1,112 +0,0 @@ -import db from "$db/db"; -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - a1_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "a1_id" - }, - a2_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "a2_id" - }, - user_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "user_id" - }, - completedAt: { - type: DataTypes.DATE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "completedAt" - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "createdAt" - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "updatedAt" - }, - v1: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "v1" - }, - v2: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "v2" - }, - rate: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "rate" - }, - accepted: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "accepted" - }, -}; - -const options = { - tableName: "orders", - comment: "", - indexes: [] -}; - -db.Order = db.define("orders_model", attributes, options); diff --git a/db/models/payments.js b/db/models/payments.js deleted file mode 100644 index fac8ff56..00000000 --- a/db/models/payments.js +++ /dev/null @@ -1,220 +0,0 @@ -import db from "$db/db"; -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - with_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "with_id" - }, - account_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "account_id" - }, - invoice_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "invoice_id" - }, - user_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "user_id" - }, - path: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "path" - }, - memo: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "memo" - }, - hash: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "hash" - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "createdAt" - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "updatedAt" - }, - rate: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "rate" - }, - preimage: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "preimage" - }, - network: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "network" - }, - currency: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "currency" - }, - address: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "address" - }, - received: { - type: DataTypes.INTEGER(1), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "received" - }, - amount: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "amount" - }, - fee: { - type: DataTypes.DOUBLE, - allowNull: false, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "fee" - }, - tip: { - type: DataTypes.DOUBLE, - allowNull: false, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "tip" - }, - redeemed: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "redeemed" - }, - redeemcode: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "redeemcode" - }, - confirmed: { - type: DataTypes.INTEGER(1), - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "confirmed" - }, - fee_payment_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "fee_payment_id" - } -}; - -const options = { - tableName: "payments", - comment: "", - indexes: [] -}; - -db["Payment"] = db.define("payments_model", attributes, options); diff --git a/db/models/referrals.js b/db/models/referrals.js deleted file mode 100644 index 8aebde2e..00000000 --- a/db/models/referrals.js +++ /dev/null @@ -1,90 +0,0 @@ -import db from "$db/db"; -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - user_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "user_id" - }, - sponsor_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "sponsor_id" - }, - - token: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "token" - }, - expiry: { - type: DataTypes.DATE(), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "token" - }, - status: { - type: DataTypes.ENUM, - values: ['available', 'used', 'expired', 'cancelled'], - allowNull: true, - defaultValue: 'available', - primaryKey: false, - autoIncrement: false, - comment: null, - field: "token" - } -}; - -const options = { - tableName: "referrals", - comment: "", - indexes: [] -}; - -db["Referral"] = db.define("referrals_model", attributes, options); - -// exports.up = function(knex) { -// return knex.schema.createTable('referrals', function(t) { -// t.increments('id').unsigned().primary(); -// t.int('user_id').nullable(); -// t.foreign('user_id').references('users.id'); - -// t.int('sponsor_id').notNull(); -// t.foreign('sponsor_id').references('users.id'); - -// t.string('token').notNull(); -// t.string('expiry').nullable(); -// t.enum('status', ['available', 'used', 'expired', 'cancelled']).notNull().defaultTo('available'); - -// t.timestamps(); -// }) -// }; - -// exports.down = function(knex) { -// return knex.schema.dropTable('referrals'); -// }; diff --git a/db/models/requests.js b/db/models/requests.js deleted file mode 100644 index 76703a72..00000000 --- a/db/models/requests.js +++ /dev/null @@ -1,67 +0,0 @@ -import db from "$db/db"; -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - requester_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "requester_id" - }, - recipient_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "recipient_id" - }, - memo: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "memo" - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "createdAt" - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "updatedAt" - }, -}; - -const options = { - tableName: "requests", - comment: "", - indexes: [] -}; - -db["Request"] = db.define("requests_model", attributes, options); diff --git a/db/models/users.js b/db/models/users.js deleted file mode 100644 index fce79537..00000000 --- a/db/models/users.js +++ /dev/null @@ -1,270 +0,0 @@ -import db from "$db/db"; -import { DataTypes } from "@sequelize/core"; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - uuid: { - type: DataTypes.STRING, - allowNull: false, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - autoIncrement: false, - comment: null, - field: "uuid" - }, - address: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "address" - }, - theme: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: false, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "theme" - }, - profile: { - type: DataTypes.BOOLEAN, - allowNull: true, - defaultValue: false, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "profile" - }, - banner: { - type: DataTypes.BOOLEAN, - allowNull: true, - defaultValue: false, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "banner" - }, - admin: { - type: DataTypes.BOOLEAN, - allowNull: true, - defaultValue: false, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "admin" - }, - locked: { - type: DataTypes.BOOLEAN, - allowNull: true, - defaultValue: false, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "locked" - }, - verified: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "verified" - }, - fiat: { - type: DataTypes.BOOLEAN, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "fiat" - }, - index: { - type: DataTypes.INTEGER, - allowNull: true, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "index" - }, - ip: { - type: DataTypes.INTEGER.UNSIGNED, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "ip" - }, - seed: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "seed" - }, - username: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "username" - }, - password: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "password" - }, - unit: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: "SAT", - primaryKey: false, - autoIncrement: false, - comment: null, - field: "unit" - }, - account_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "account_id" - }, - otpsecret: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: "CAD", - primaryKey: false, - autoIncrement: false, - comment: null, - field: "otpsecret" - }, - currency: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: "CAD", - primaryKey: false, - autoIncrement: false, - comment: null, - field: "currency" - }, - currencies: { - type: DataTypes.JSON, - allowNull: false, - defaultValue: ["USD"], - primaryKey: false, - autoIncrement: false, - comment: null, - field: "currencies" - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "createdAt" - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "updatedAt" - }, - subscriptions: { - type: DataTypes.TEXT, - get: function() { - return JSON.parse(this.getDataValue("subscriptions")); - }, - set: function(value) { - return this.setDataValue("subscriptions", JSON.stringify(value)); - }, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "subscriptions" - }, - twofa: { - type: DataTypes.INTEGER(1), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "twofa" - }, - cipher: { - type: DataTypes.STRING(255) - }, - pubkey: { - type: DataTypes.STRING(255) - }, - salt: { - type: DataTypes.STRING(255) - }, - pin: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "pin" - }, - email: { - type: DataTypes.STRING(255) - }, - phone: { - type: DataTypes.STRING(255) - }, - locktime: { - type: DataTypes.INTEGER(11) - }, - prompt: { - type: DataTypes.BOOLEAN - }, - display: { - type: DataTypes.STRING(255) - } -}; - -const options = { - tableName: "users", - comment: "", - indexes: [], - charset: "utf8mb4", - collate: "utf8mb4_unicode_ci" -}; - -db["User"] = db.define("users_model", attributes, options); diff --git a/db/models/waiting_list.js b/db/models/waiting_list.js deleted file mode 100644 index 37f2889b..00000000 --- a/db/models/waiting_list.js +++ /dev/null @@ -1,45 +0,0 @@ -import db from "$db/db"; -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - primaryKey: true, - autoIncrement: true, - allowNull: false - }, - email: { - type: DataTypes.STRING(255), - validate: { - isEmail: true - } - }, - phone: { - type: DataTypes.STRING(255), - validate: { - is: /^[0-9]+$/ // replace spaces, brackets, dashes dynamically prior to create - } - }, - status: { - type: DataTypes.ENUM, - values: ['pending', 'activated', 'expired', 'cancelled'], - defaultValue: 'pending', - allowNull: false - } -} - -const options = { - tableName: "waiting_list", - comment: "Users on waiting list", - timestamps: true, - createdAt: 'created_at', - updatedAt: 'updated_at', - indexes: [ - { - unique: true, - fields: ['email', 'phone'] - } - ] -}; - -db["WaitingList"] = db.define("waiting_list_model", attributes, options); diff --git a/db/models/withdrawals.js b/db/models/withdrawals.js deleted file mode 100644 index fa2afb22..00000000 --- a/db/models/withdrawals.js +++ /dev/null @@ -1,103 +0,0 @@ -import db from "$db/db"; -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - amount: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "amount" - }, - user_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "user_id" - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "createdAt" - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "updatedAt" - }, - transit: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "transit" - }, - institution: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "institution" - }, - account: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "account" - }, - email: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "email" - }, - notes: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "notes" - }, -}; - -const options = { - tableName: "withdrawals", - comment: "", - indexes: [] -}; - -db["Withdrawal"] = db.define("withdrawals_model", attributes, options); diff --git a/db/normalized/README.md b/db/normalized/README.md deleted file mode 100644 index fd3cb0d3..00000000 --- a/db/normalized/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# General guidelines for Database schema: - -- String fields whose values are NOT open-ended (ie can contain random strings) should be changed to either: - - an FK to a separate lookup table (when the domain of options is large or can grow) - - an enum (when there are a short list of essentailly static options) - -- Field names should follow SQL standards: - - lower case - - underscore_separated_words - -- options for designated models should be standardized with: - { - tableName: "table_name", - comment: "description of table if not self-evident", - timestamps: true, - createdAt: 'created_at', - updatedAt: 'updated_at', - indexes: [ - { } - ] - }; - - ## Note regarding field attributes specified in model: - - - createdAt, updatedAt are renamed to abide by SQL standard. - - - by default allowNull is false, so this attribute need only be specified when fields are NOT NULL. - - - comment attribute should added to any field that is not self-evident - - - may need to add indexes to more fields for rapid access - - ## Notes regarding changes / updates - - - Some 1-1 tables are split up simply to separate scope or to control access permission (ie only user_preferences table may be edited by users themselves) - - ## Sequelize Operational Questions: - - Are indexes automatically generated by Sequelize migration for primary keys and/or foreign keys ? - - Does Sequelize add non-unique indexes for data mining if specified: eg { fields: ['username'] } - - Do timestamp fields require default value spec (eg Sequelize.NOW) ... or how can we define this in model ? - - ## Ongoing Questions: - - payments table needs to be investigated and refactored - - is invoices table required or is this just summary information ? - - orders table needs to be investigated and refactored - - need to add subscriptions table if applicable ( what is this comprised of ?) - - user_keys table should be commented to clarify application of each of these values. - diff --git a/db/normalized/accounts.js b/db/normalized/accounts.js deleted file mode 100644 index 8e5aad33..00000000 --- a/db/normalized/accounts.js +++ /dev/null @@ -1,231 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('accounts', function(t) { - t.increments('id').unsigned().primary(); - t.int('user_id').notNull(); - t.foreign('user_id').references('users.id'); - - t.int('uuid').notNull(); - - t.double('balance'); - t.double('pending'); - - t.enu('network', ['Bitcoin', 'Liquid', 'Lightning', 'CoinOS']).notNull(); - - t.longtext('contract').collate('utf8mb4').nullable(); // custodial ? - - // knex.raw("ALTER TABLE accounts MODIFY `contract` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL") - - // t.string('hide'); - // t.string('index'); - - t.int('liquid_asset_id').notNull(); - t.foreign('liquid_asset_id').references('liquid_assets.id'); - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('accounts'); -}; - -// Change -// -// move assets fields (asset, ticker, name, precision...) to separate liquid_assets reference - -// separate network specific fields to bitcoin_accounts / liquid_accounts / coinos_accounts tables ? or just non_custodial ? -// eg liquid -> liquid_asset info -// non-custodial ? -> privkey, pubkey, path, seed ? -// contract, hide, index ? -// - - -// const { DataTypes } = require('@sequelize/core'); - -// const attributes = { -// id: { -// type: DataTypes.INTEGER(11), -// allowNull: false, -// defaultValue: null, -// primaryKey: true, -// autoIncrement: true, -// comment: null, -// field: "id" -// }, -// user_id: { -// type: DataTypes.INTEGER(11), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "user_id" -// }, -// createdAt: { -// type: DataTypes.DATE, -// allowNull: false, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "createdAt" -// }, -// updatedAt: { -// type: DataTypes.DATE, -// allowNull: false, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "updatedAt" -// }, -// path: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "path" -// }, -// seed: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "seed" -// }, -// network: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "network" -// }, -// name: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "name" -// }, -// contract: { -// type: DataTypes.TEXT, -// get: function() { -// return JSON.parse(this.getDataValue("contract")); -// }, -// set: function(value) { -// return this.setDataValue("contract", JSON.stringify(value)); -// }, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "contract" -// }, -// domain: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "domain" -// }, -// ticker: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "ticker" -// }, -// asset: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "asset" -// }, -// balance: { -// type: DataTypes.DOUBLE, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "balance" -// }, -// pending: { -// type: DataTypes.DOUBLE, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "pending" -// }, -// hide: { -// type: DataTypes.BOOLEAN, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "hide" -// }, -// index: { -// type: DataTypes.INTEGER(11), -// allowNull: false, -// defaultValue: 0, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "index" -// }, -// privkey: { -// type: DataTypes.STRING, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "privkey" -// }, -// pubkey: { -// type: DataTypes.STRING, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "pubkey" -// }, -// precision: { -// type: DataTypes.INTEGER(11), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "precision" -// }, -// }; - -// const options = { -// tableName: "accounts", -// comment: "", -// indexes: [] -// }; - -// db["Account"] = db.define("accounts_model", attributes, options); diff --git a/db/normalized/bank_accounts.js b/db/normalized/bank_accounts.js deleted file mode 100644 index 5d8ea0dc..00000000 --- a/db/normalized/bank_accounts.js +++ /dev/null @@ -1,24 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('bank_accounts', function(t) { - t.increments('id').unsigned().primary(); - t.int('user_id').notNull(); - t.foreign('user_id').references('users.id'); - - t.int('transit_number'); - t.int('institution_number'); - t.int('account_number'); - - t.string('swift'); - t.string('iban'); - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('user_security'); -}; - -// Changes: -// -// bank info -> bank_account records diff --git a/db/normalized/credits.js b/db/normalized/credits.js deleted file mode 100644 index 5511b616..00000000 --- a/db/normalized/credits.js +++ /dev/null @@ -1,236 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('credits', function(t) { - t.increments('id').unsigned().primary(); - - t.enu('type', ['deposit', 'order', 'transfer', 'invoice', 'exchange']) - t.string('description') - - t.int('account_id').notNull(); - t.foreign('account_id').references('accounts.id'); - - t.double('amount'); - - t.enu('status', ['pending', 'completed', 'reversed', 'cancelled']) - - t.timestamps(); - }) -}; - -export const down = function(knex) { -return knex.schema.dropTable('credits'); -}; - -// Changes: -// -// Should these be tracked as transactions ? -// SAT -> SAT -// tracking various exchange variations (outside of external networks ?) -// -// Is user / account / amount info redundant ? -// + transaction_type: deposit / withdrawal / transfer / exchange ? -// -// add transfers table ? (for internal transfers only ?) -// add redemptions table ? (for bitcoin transfers only ?) -// add exchanges table ? (for conversion from one currency to another) -// -// network -> scope: internal / bitcoin / liquid / lightning ? -// add bitcoin_transaction ? (with redemption attributes) -// add liquid_transaction ? () -// add lightning_transaction ? (with LN_invoice_hash) -// add internal_transaction ? () -// -// context for path, memo, rate, preimage, address, received, fee, tip, confirmed, redeemcode ? - - -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - account_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "account_id" - }, - user_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "user_id" - }, - path: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "path" - }, - memo: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "memo" - }, - hash: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "hash" - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "createdAt" - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "updatedAt" - }, - rate: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "rate" - }, - preimage: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "preimage" - }, - network: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "network" - }, - currency: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "currency" - }, - address: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "address" - }, - received: { - type: DataTypes.INTEGER(1), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "received" - }, - amount: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "amount" - }, - fee: { - type: DataTypes.DOUBLE, - allowNull: false, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "fee" - }, - tip: { - type: DataTypes.DOUBLE, - allowNull: false, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "tip" - }, - redeemed: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "redeemed" - }, - redeemcode: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "redeemcode" - }, - confirmed: { - type: DataTypes.INTEGER(1), - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "confirmed" - } -}; - -const options = { - tableName: "payments", - comment: "", - indexes: [] -}; - -db["Payment"] = db.define("payments_model", attributes, options); diff --git a/db/normalized/debits.js b/db/normalized/debits.js deleted file mode 100644 index 6fcee974..00000000 --- a/db/normalized/debits.js +++ /dev/null @@ -1,236 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('debits', function(t) { - t.increments('id').unsigned().primary(); - - t.enu('type', ['withdrawal', 'order', 'transfer', 'invoice', 'exchange']) - - t.int('account_id').notNull(); - t.foreign('account_id').references('accounts.id'); - - t.double('amount'); - - t.enu('type', ['withdrawal', 'transfer', 'order', 'invoice']) - t.enu('status', ['pending', 'completed', 'reversed', 'cancelled']) - - t.timestamps(); - }) -}; - -export const down = function(knex) { -return knex.schema.dropTable('debits'); -}; - -// Changes: -// -// Should these be tracked as transactions ? -// SAT -> SAT -// tracking various exchange variations (outside of external networks ?) -// -// Is user / account / amount info redundant ? -// + transaction_type: deposit / withdrawal / transfer / exchange ? -// -// add transfers table ? (for internal transfers only ?) -// add redemptions table ? (for bitcoin transfers only ?) -// add exchanges table ? (for conversion from one currency to another) -// -// network -> scope: internal / bitcoin / liquid / lightning ? -// add bitcoin_transaction ? (with redemption attributes) -// add liquid_transaction ? () -// add lightning_transaction ? (with LN_invoice_hash) -// add internal_transaction ? () -// -// context for path, memo, rate, preimage, address, received, fee, tip, confirmed, redeemcode ? - - -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - account_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "account_id" - }, - user_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "user_id" - }, - path: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "path" - }, - memo: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "memo" - }, - hash: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "hash" - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "createdAt" - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "updatedAt" - }, - rate: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "rate" - }, - preimage: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "preimage" - }, - network: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "network" - }, - currency: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "currency" - }, - address: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "address" - }, - received: { - type: DataTypes.INTEGER(1), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "received" - }, - amount: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "amount" - }, - fee: { - type: DataTypes.DOUBLE, - allowNull: false, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "fee" - }, - tip: { - type: DataTypes.DOUBLE, - allowNull: false, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "tip" - }, - redeemed: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "redeemed" - }, - redeemcode: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "redeemcode" - }, - confirmed: { - type: DataTypes.INTEGER(1), - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "confirmed" - } -}; - -const options = { - tableName: "payments", - comment: "", - indexes: [] -}; - -db["Payment"] = db.define("payments_model", attributes, options); diff --git a/db/normalized/deposits.js b/db/normalized/deposits.js deleted file mode 100644 index 17a7c500..00000000 --- a/db/normalized/deposits.js +++ /dev/null @@ -1,100 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('deposits', function(t) { - t.increments('id').unsigned().primary(); - - t.int('credit_id').notNull(); - t.foreign('credit_id').references('credits.id'); - - t.string('confirmation_code'); - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('deposits'); -}; - -// Changes: -// -// reference transaction -// reference account rather than user -// credited - redundant with confirmation_code ? -// code -> confirmation_code - - -// const { DataTypes } = require('@sequelize/core'); - -// const attributes = { -// id: { -// type: DataTypes.INTEGER(11), -// allowNull: false, -// defaultValue: null, -// primaryKey: true, -// autoIncrement: true, -// comment: null, -// field: "id" -// }, -// amount: { -// type: DataTypes.DOUBLE, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "amount" -// }, -// user_id: { -// type: DataTypes.INTEGER(11), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "user_id" -// }, -// createdAt: { -// type: DataTypes.DATE, -// allowNull: false, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "createdAt" -// }, -// updatedAt: { -// type: DataTypes.DATE, -// allowNull: false, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "updatedAt" -// }, -// credited: { -// type: DataTypes.BOOLEAN, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "credited" -// }, -// code: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "code" -// }, -// }; - -// const options = { -// tableName: "deposits", -// comment: "", -// indexes: [] -// }; - -// db["Deposit"] = db.define("deposits_model", attributes, options); diff --git a/db/normalized/exchanges.js b/db/normalized/exchanges.js deleted file mode 100644 index 2be91990..00000000 --- a/db/normalized/exchanges.js +++ /dev/null @@ -1,232 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('exchanges', function(t) { - t.increments('id').unsigned().primary(); - - t.int('transfer_id').notNull(); - t.foreign('transfer_id').references('transfers.id'); - - t.double('exchange_rate').notNull() - t.double('exchange_fee').notNull().defaultTo(0) - - t.timestamps(); - }) -}; - -export const down = function(knex) { -return knex.schema.dropTable('exchanges'); -}; - -// Changes: -// -// Should these be tracked as transactions ? -// SAT -> SAT -// tracking various exchange variations (outside of external networks ?) -// -// Is user / account / amount info redundant ? -// + transaction_type: deposit / withdrawal / transfer / exchange ? -// -// add transfers table ? (for internal transfers only ?) -// add redemptions table ? (for bitcoin transfers only ?) -// add exchanges table ? (for conversion from one currency to another) -// -// network -> scope: internal / bitcoin / liquid / lightning ? -// add bitcoin_transaction ? (with redemption attributes) -// add liquid_transaction ? () -// add lightning_transaction ? (with LN_invoice_hash) -// add internal_transaction ? () -// -// context for path, memo, rate, preimage, address, received, fee, tip, confirmed, redeemcode ? - - -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - account_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "account_id" - }, - user_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "user_id" - }, - path: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "path" - }, - memo: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "memo" - }, - hash: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "hash" - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "createdAt" - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "updatedAt" - }, - rate: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "rate" - }, - preimage: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "preimage" - }, - network: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "network" - }, - currency: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "currency" - }, - address: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "address" - }, - received: { - type: DataTypes.INTEGER(1), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "received" - }, - amount: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "amount" - }, - fee: { - type: DataTypes.DOUBLE, - allowNull: false, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "fee" - }, - tip: { - type: DataTypes.DOUBLE, - allowNull: false, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "tip" - }, - redeemed: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "redeemed" - }, - redeemcode: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "redeemcode" - }, - confirmed: { - type: DataTypes.INTEGER(1), - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "confirmed" - } -}; - -const options = { - tableName: "payments", - comment: "", - indexes: [] -}; - -db["Payment"] = db.define("payments_model", attributes, options); diff --git a/db/normalized/invoices.js b/db/normalized/invoices.js deleted file mode 100644 index 823ad11d..00000000 --- a/db/normalized/invoices.js +++ /dev/null @@ -1,206 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('invoices', function(t) { - t.increments('id').unsigned().primary(); - - t.int('transfer_id').notNull(); - t.foreign('transfer_id').references('transfers.id'); - - t.enu('network', ['Bitcoin', 'Liquid', 'Lightning', 'CoinOS']).notNull(); - - t.string('bolt11'); // was 'text' - - // Clarify ??? - break out ? - context ? include everything encoded in hash... - t.double('amount'); - - // Exchanges - // ( are there a couple of types of invoices ? - eg checkout / voucher / receive ... with different attributes ?) - t.double('rate') - t.string('currency') - t.double('tip') - - t.string('path') - t.string('uuid') - t.string('memo') - t.string('unconfidential') - t.string('address') - t.string('received') - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('invoices'); -}; - - -// Variations: ? -// wallet -> receive - fields ? -// checkout - fields ? -// voucher - fields ? - -// const { DataTypes } = require('@sequelize/core'); - -// const attributes = { -// id: { -// type: DataTypes.INTEGER(11), -// allowNull: false, -// defaultValue: null, -// primaryKey: true, -// autoIncrement: true, -// comment: null, -// field: "id" -// }, -// account_id: { -// type: DataTypes.INTEGER(11), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "account_id" -// }, -// user_id: { -// type: DataTypes.INTEGER(11), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "user_id" -// }, -// text: { -// type: DataTypes.TEXT, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "text" -// }, -// createdAt: { -// type: DataTypes.DATE, -// allowNull: false, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "createdAt" -// }, -// updatedAt: { -// type: DataTypes.DATE, -// allowNull: false, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "updatedAt" -// }, -// path: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "path" -// }, -// rate: { -// type: DataTypes.DOUBLE, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "rate" -// }, -// uuid: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "uuid" -// }, -// memo: { -// type: DataTypes.TEXT, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "memo" -// }, -// network: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "network" -// }, -// currency: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "currency" -// }, -// unconfidential: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "unconfidential" -// }, -// address: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "address" -// }, -// received: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "received" -// }, -// amount: { -// type: DataTypes.DOUBLE, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "amount" -// }, -// tip: { -// type: DataTypes.DOUBLE, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "tip" -// }, -// }; - -// const options = { -// tableName: "invoices", -// comment: "", -// indexes: [] -// }; - -// db["Invoice"] = db.define("invoices_model", attributes, options); diff --git a/db/normalized/liquid_assets.js b/db/normalized/liquid_assets.js deleted file mode 100644 index fab96dc5..00000000 --- a/db/normalized/liquid_assets.js +++ /dev/null @@ -1,21 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('liquid_assets', function(t) { - t.increments('id').unsigned().primary(); - - t.string('asset_id').notNull(); - t.string('name').notNull(); - - t.string('domain') - t.string('ticker').notNull(); - t.int('precision').defaultTo(8); - t.string('issuer_pubkey') - - t.boolean('registered').defaultTo(false); - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('liquid_assets'); -}; \ No newline at end of file diff --git a/db/normalized/ln_keys.js b/db/normalized/ln_keys.js deleted file mode 100644 index 7922e40e..00000000 --- a/db/normalized/ln_keys.js +++ /dev/null @@ -1,16 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('ln_keys', function(t) { - t.increments('id').unsigned().primary(); - - t.int('user_id').notNull(); - t.foreign('user_id').references('users.id'); - - t.string('hex').notNull(); - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('ln_keys'); -}; \ No newline at end of file diff --git a/db/normalized/lnurl_codes.js b/db/normalized/lnurl_codes.js deleted file mode 100644 index 5ed81135..00000000 --- a/db/normalized/lnurl_codes.js +++ /dev/null @@ -1,14 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('lnurl_codes', function(t) { - t.increments('id').unsigned().primary(); - - t.string('code').notNull(); - t.text('text'); - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('lnurl_codes'); -}; diff --git a/db/normalized/non_custodial_accounts.js b/db/normalized/non_custodial_accounts.js deleted file mode 100644 index e8190ce6..00000000 --- a/db/normalized/non_custodial_accounts.js +++ /dev/null @@ -1,91 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('non_custodial_accounts', function(t) { - t.increments('id').unsigned().primary(); - t.int('account_id').notNull(); - t.foreign('account_id').references('accounts.id'); - - // what is the scope / context of these attributes ? - t.string('path'); - t.string('seed'); - // t.string('hide'); - // t.string('index'); - t.string('pub_key'); - t.string('priv_key'); - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('non_custodial_accounts'); -}; - -// const { DataTypes } = require('@sequelize/core'); - -// const attributes = { -// id: { -// type: DataTypes.INTEGER(11), -// primaryKey: true, -// autoIncrement: true, -// allowNull: false -// }, -// account_id: { -// type: DataTypes.INTEGER(11), -// references: { model: Account, key: 'id' }, -// allowNull: false, -// comment: "Reference to account" -// }, -// path: { -// type: DataTypes.STRING(255), -// comment: "What is this ?" -// }, -// seed: { -// type: DataTypes.STRING(255), -// comment: "What is this ?" -// }, -// network: { -// type: DataTypes.INTEGER(11), -// references: { model: Network, key: 'id' }, -// allowNull: false, -// comment: "Reference to network" -// }, -// domain: { -// type: DataTypes.STRING(255), -// comment: "What is this ? ... is it unique or an enum ?" -// }, -// ticker: { -// type: DataTypes.STRING(255), -// comment: "What is this ?" -// }, -// hide: { -// type: DataTypes.STRING(255), -// comment: "What is this ?" -// }, -// index: { -// type: DataTypes.STRING(255), -// comment: "What is this ?" -// }, -// pub_key: { -// type: DataTypes.STRING(255), -// comment: "What is this ?" -// }, -// priv_key: { -// type: DataTypes.STRING(255), -// comment: "What is this ?" -// } -// } -// const options = { -// tableName: "non_custodial_accounts", -// comment: "", -// timestamps: true, -// createdAt: 'created_at', -// updatedAt: 'updated_at', -// indexes: [ -// { -// unique: true, -// fields: ['account_id'] -// } -// ] -// }; - -// db["NonCustodialAccount"] = db.define("non_custodial_accounts_model", attributes, options); diff --git a/db/normalized/orders.js b/db/normalized/orders.js deleted file mode 100644 index 06e83db1..00000000 --- a/db/normalized/orders.js +++ /dev/null @@ -1,29 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('orders', function(t) { - t.increments('id').unsigned().primary(); - - t.int('sell_transfer_id').notNull(); - t.foreign('sell_transfer_id').references('transfers.id'); - - t.int('buy_transfer_id').notNull(); - t.foreign('buy_transfer_id').references('transfers.id'); - - // t.int('exchange_id').notNull(); - // t.foreign('exchange_id').references('exchanges.id'); - - t.double('exchange_rate').notNull(); - // t.double('amount').notNull(); - - t.enu('status', ['Submitted', 'Accepted', 'Cancelled']).notNull(); - t.date('completion_date') - - t.timestamps(); - }) -}; - -// Bid / Accepted ... separate into Bid (credit + debit), Accepted (credit + debit) - -export const down = function(knex) { - return knex.schema.dropTable('orders'); -}; - diff --git a/db/normalized/referrals.js b/db/normalized/referrals.js deleted file mode 100644 index 7380f2cf..00000000 --- a/db/normalized/referrals.js +++ /dev/null @@ -1,20 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('referrals', function(t) { - t.increments('id').unsigned().primary(); - t.int('user_id').nullable(); - t.foreign('user_id').references('users.id'); - - t.int('sponsor_id').notNull(); - t.foreign('sponsor_id').references('users.id'); - - t.string('token').notNull(); - t.string('expiry').nullable(); - t.enum('status', ['available', 'used', 'expired', 'cancelled']).notNull().defaultTo('available'); - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('referrals'); -}; diff --git a/db/normalized/transactions.js b/db/normalized/transactions.js deleted file mode 100644 index 230faca4..00000000 --- a/db/normalized/transactions.js +++ /dev/null @@ -1,41 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('transactions', function(t) { - t.increments('id').unsigned().primary(); - - t.enu('type', ['deposit', 'withdrawal', 'transfer', 'exchange', 'invoice', 'voucher', 'new asset']) - - // t.text('notes'); - - t.timestamps(); - }) -}; - -export const down = function(knex) { -return knex.schema.dropTable('transactions'); -}; - -// Changes: -// -// Deposit = deposit + credit -// Withdrawal = withdrawal + debit -// Transfer = credit + debit -// Exchange = credit + debit + debit + credit -// Invoice = invoice + debit ? -// Voucher = invoice + debit ? -// New Asset = liquid_asset + credit - -// -// context for path, memo, rate, preimage, address, received, fee, tip, confirmed, redeemcode ? -export const up = function(knex) { - return knex.schema.createTable('transactions', function(t) { - t.increments('id').unsigned().primary(); - - t.enu('type', ['deposit', 'withdrawal', 'order', 'transfer', 'invoice', 'voucher']) - - t.timestamps(); - }) -}; - -export const down = function(knex) { -return knex.schema.dropTable('transactions'); -}; \ No newline at end of file diff --git a/db/normalized/transfers.js b/db/normalized/transfers.js deleted file mode 100644 index 64f5c392..00000000 --- a/db/normalized/transfers.js +++ /dev/null @@ -1,243 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('transfers', function(t) { - t.increments('id').unsigned().primary(); - - t.int('transaction_id').notNull(); - t.foreign('transaction_id').references('accounts.id'); - - t.int('debit_id').notNull(); - t.foreign('debit_id').references('debits.id'); - - t.int('credit_id').notNull(); - t.foreign('credit_id').references('credits.id'); - - // t.int('liquid_asset_id').notNull(); - // t.foreign('liquid_asset_id').references('liquid_assets.id'); - - // t.double('amount'); - t.double('transfer_fee').notNull().defaultTo(0) - - t.enu('status', ['pending', 'completed', 'reversed', 'cancelled']) - - t.timestamps(); - }) -}; - -export const down = function(knex) { -return knex.schema.dropTable('transfers'); -}; - -// Changes: -// -// Should these be tracked as transactions ? -// SAT -> SAT -// tracking various exchange variations (outside of external networks ?) -// -// Is user / account / amount info redundant ? -// + transaction_type: deposit / withdrawal / transfer / exchange ? -// -// add transfers table ? (for internal transfers only ?) -// add redemptions table ? (for bitcoin transfers only ?) -// add exchanges table ? (for conversion from one currency to another) -// -// network -> scope: internal / bitcoin / liquid / lightning ? -// add bitcoin_transaction ? (with redemption attributes) -// add liquid_transaction ? () -// add lightning_transaction ? (with LN_invoice_hash) -// add internal_transaction ? () -// -// context for path, memo, rate, preimage, address, received, fee, tip, confirmed, redeemcode ? - - -import { DataTypes } from '@sequelize/core'; - -const attributes = { - id: { - type: DataTypes.INTEGER(11), - allowNull: false, - defaultValue: null, - primaryKey: true, - autoIncrement: true, - comment: null, - field: "id" - }, - account_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "account_id" - }, - user_id: { - type: DataTypes.INTEGER(11), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "user_id" - }, - path: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "path" - }, - memo: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "memo" - }, - hash: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "hash" - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "createdAt" - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "updatedAt" - }, - rate: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "rate" - }, - preimage: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "preimage" - }, - network: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "network" - }, - currency: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "currency" - }, - address: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "address" - }, - received: { - type: DataTypes.INTEGER(1), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "received" - }, - amount: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "amount" - }, - fee: { - type: DataTypes.DOUBLE, - allowNull: false, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "fee" - }, - tip: { - type: DataTypes.DOUBLE, - allowNull: false, - defaultValue: 0, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "tip" - }, - redeemed: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "redeemed" - }, - redeemcode: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "redeemcode" - }, - confirmed: { - type: DataTypes.INTEGER(1), - allowNull: false, - defaultValue: null, - primaryKey: false, - autoIncrement: false, - comment: null, - field: "confirmed" - } -}; - -const options = { - tableName: "payments", - comment: "", - indexes: [] -}; - -db["Payment"] = db.define("payments_model", attributes, options); diff --git a/db/normalized/user_preferences.js b/db/normalized/user_preferences.js deleted file mode 100644 index 6af69f3f..00000000 --- a/db/normalized/user_preferences.js +++ /dev/null @@ -1,20 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('user_preferences', function(t) { - t.increments('id').unsigned().primary(); - t.int('user_id').notNull(); - t.foreign('user_id').references('users.id'); - - t.enum('default_unit', ['SAT', 'BTC', 'CAD']).notNull().defaultTo('SAT'); - t.enum('default_currency', ['CAD', 'USD']).notNull().defaultTo('CAD'); - t.enum('default_fiat', ['CAD', 'USD']).notNull().defaultTo('CAD'); - t.set('show_currencies', ['CAD', 'USD']).notNull().defaultTo('CAD'); - t.longtext(`subscriptions`).collate('utf8mb4').nullable(); // CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL") - t.timestamps(); - - // knex.raw("ALTER TABLE users MODIFY `subscriptions` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL") - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('user_preferences'); -}; diff --git a/db/normalized/user_security.js b/db/normalized/user_security.js deleted file mode 100644 index 919b2902..00000000 --- a/db/normalized/user_security.js +++ /dev/null @@ -1,22 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('user_security', function(t) { - t.increments('id').unsigned().primary(); - t.int('user_id').notNull(); - t.foreign('user_id').references('users.id'); - - t.boolean('twofa').notNull().defaultTo(false); - t.string('pin').notNull(); - t.string('otp_secret').nullable(); - t.string('seed').nullable(); - - t.boolean('email_verified').notNull().default(false) - t.boolean('phone_verified').notNull().default(false) - t.boolean('kyc_verified').notNull().default(false) - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('user_security'); -}; \ No newline at end of file diff --git a/db/normalized/users.js b/db/normalized/users.js deleted file mode 100644 index dd239f24..00000000 --- a/db/normalized/users.js +++ /dev/null @@ -1,18 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('users', function(t) { - t.increments('id').unsigned().primary(); - - t.string('uuid').notNull().index(); - t.string('username').notNull().index(); - t.string('password').notNull(); - t.string('email').nullable().index(); - t.string('phone').nullable(); - t.enum('access', ['Anonymous', 'Registered', 'Admin']).notNull().defaultTo('Anonymous'); - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('users'); -}; \ No newline at end of file diff --git a/db/normalized/vouchers.js b/db/normalized/vouchers.js deleted file mode 100644 index 35d54de2..00000000 --- a/db/normalized/vouchers.js +++ /dev/null @@ -1,18 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('vouchers', function(t) { - t.increments('id').unsigned().primary(); - - t.int('invoice_id').notNull(); - t.foreign('invoice_id').references('invoices.id'); - - t.string('redemption_code').notNull(); - - t.enu('status', ['pending', 'redeemed', 'cancelled']).notNull(); - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('vouchers'); -}; \ No newline at end of file diff --git a/db/normalized/waiting_list.js b/db/normalized/waiting_list.js deleted file mode 100644 index 5a37dc52..00000000 --- a/db/normalized/waiting_list.js +++ /dev/null @@ -1,60 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('waiting_list', function(t) { - t.increments('id').unsigned().primary(); - - t.string('email').notNull(); - t.string('phone').notNull(); - t.enu('status', ['pending', 'activated', 'expired', 'cancelled']).notNull(); - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('waiting_list'); -}; - -// const { DataTypes } = require('@sequelize/core'); - -// const attributes = { -// id: { -// type: DataTypes.INTEGER(11), -// primaryKey: true, -// autoIncrement: true, -// allowNull: false -// }, -// email: { -// type: DataTypes.STRING(255), -// validate: { -// isEmail: true -// } -// }, -// sms: { -// type: DataTypes.STRING(255), -// validate: { -// is: /^[0-9]+$/ // replace spaces, brackets, dashes dynamically prior to create -// } -// }, -// status: { -// type: DataTypes.ENUM, -// values: ['pending', 'activated', 'expired', 'cancelled'], -// defaultValue: 'pending', -// allowNull: false -// } -// } - -// const options = { -// tableName: "waiting_list", -// comment: "Users on waiting list", -// timestamps: true, -// createdAt: 'created_at', -// updatedAt: 'updated_at', -// indexes: [ -// { -// unique: true, -// fields: ['email', 'sms'] -// } -// ] -// }; - -// db["WaitingList"] = db.define("waiting_list_model", attributes, options); diff --git a/db/normalized/withdrawals.js b/db/normalized/withdrawals.js deleted file mode 100644 index 742f3dbf..00000000 --- a/db/normalized/withdrawals.js +++ /dev/null @@ -1,128 +0,0 @@ -export const up = function(knex) { - return knex.schema.createTable('withdrawals', function(t) { - t.increments('id').unsigned().primary(); - - t.int('debit_id').notNull(); - t.foreign('debit_id').references('debits.id'); - - t.int('bank_account_id').notNull(); - t.foreign('bank_account_id').references('bank_accounts.id'); - - t.double('withdrawal_fee').notNull().defaultTo(0) - - t.text('notes'); - - t.timestamps(); - }) -}; - -export const down = function(knex) { - return knex.schema.dropTable('withdrawals'); -}; - -// Changes: -// -// bank info -> bank_account records - -// const { DataTypes } = require('@sequelize/core'); - -// const attributes = { -// id: { -// type: DataTypes.INTEGER(11), -// allowNull: false, -// defaultValue: null, -// primaryKey: true, -// autoIncrement: true, -// comment: null, -// field: "id" -// }, -// amount: { -// type: DataTypes.DOUBLE, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "amount" -// }, -// user_id: { -// type: DataTypes.INTEGER(11), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "user_id" -// }, -// createdAt: { -// type: DataTypes.DATE, -// allowNull: false, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "createdAt" -// }, -// updatedAt: { -// type: DataTypes.DATE, -// allowNull: false, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "updatedAt" -// }, -// transit: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "transit" -// }, -// institution: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "institution" -// }, -// account: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "account" -// }, -// email: { -// type: DataTypes.STRING(255), -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "email" -// }, -// notes: { -// type: DataTypes.TEXT, -// allowNull: true, -// defaultValue: null, -// primaryKey: false, -// autoIncrement: false, -// comment: null, -// field: "notes" -// }, -// }; - -// const options = { -// tableName: "withdrawals", -// comment: "", -// indexes: [] -// }; - -// db["Withdrawal"] = db.define("withdrawals_model", attributes, options); diff --git a/db/schema.sql b/db/schema.sql deleted file mode 100644 index e6e2e03f..00000000 --- a/db/schema.sql +++ /dev/null @@ -1,479 +0,0 @@ --- MariaDB dump 10.19 Distrib 10.6.1-MariaDB, for debian-linux-gnu (x86_64) --- --- Host: localhost Database: coinos --- ------------------------------------------------------ --- Server version 10.6.1-MariaDB-1:10.6.1+maria~focal - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!40101 SET NAMES utf8mb4 */; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; - --- --- Current Database: `coinos` --- - -CREATE DATABASE /*!32312 IF NOT EXISTS*/ `coinos` /*!40100 DEFAULT CHARACTER SET latin1 */; - -USE `coinos`; - --- --- Table structure for table `SequelizeMeta` --- - -DROP TABLE IF EXISTS `SequelizeMeta`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `SequelizeMeta` ( - `name` varchar(255) COLLATE utf8mb3_unicode_ci NOT NULL, - PRIMARY KEY (`name`), - UNIQUE KEY `name` (`name`), - UNIQUE KEY `SequelizeMeta_name_unique` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `accounts` --- - -DROP TABLE IF EXISTS `accounts`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `accounts` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) DEFAULT NULL, - `asset` varchar(255) DEFAULT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `balance` double DEFAULT NULL, - `pending` double DEFAULT NULL, - `name` varchar(255) DEFAULT NULL, - `ticker` varchar(255) DEFAULT NULL, - `precision` int(11) DEFAULT NULL, - `domain` varchar(255) DEFAULT NULL, - `contract` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, - `index` int(11) NOT NULL DEFAULT 0, - `pubkey` varchar(255) DEFAULT NULL, - `hide` tinyint(1) DEFAULT NULL, - `seed` varchar(255) DEFAULT NULL, - `path` varchar(255) DEFAULT NULL, - `network` varchar(255) DEFAULT NULL, - `privkey` varchar(255) DEFAULT NULL, - `btc_credits` double NOT NULL DEFAULT 0, - `liquid_credits` double NOT NULL DEFAULT 0, - `lightning_credits` double NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - KEY `asset` (`asset`) -) ENGINE=InnoDB AUTO_INCREMENT=21826 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `codes` --- - -DROP TABLE IF EXISTS `codes`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `codes` ( - `code` varchar(255) NOT NULL DEFAULT '', - `text` text DEFAULT NULL, - PRIMARY KEY (`code`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `deposits` --- - -DROP TABLE IF EXISTS `deposits`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `deposits` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) DEFAULT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `amount` double DEFAULT NULL, - `credited` tinyint(1) DEFAULT NULL, - `code` varchar(255) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `deposits_user_id_foreign` (`user_id`), - CONSTRAINT `deposits_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=4568 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `invoices` --- - -DROP TABLE IF EXISTS `invoices`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `invoices` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) DEFAULT NULL, - `text` text DEFAULT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `rate` double DEFAULT NULL, - `currency` varchar(255) NOT NULL, - `address` varchar(255) DEFAULT NULL, - `received` bigint(20) NOT NULL DEFAULT 0, - `amount` bigint(20) DEFAULT NULL, - `tip` double DEFAULT NULL, - `network` varchar(255) DEFAULT NULL, - `unconfidential` varchar(255) DEFAULT NULL, - `uuid` varchar(255) DEFAULT uuid(), - `memo` text DEFAULT NULL, - `account_id` int(11) DEFAULT NULL, - `path` varchar(255) DEFAULT NULL, - `webhook` text DEFAULT NULL, - `status` varchar(255) DEFAULT 'unpaid', - `prompt` tinyint(1) NOT NULL DEFAULT 0, - `pending` bigint(20) DEFAULT 0, - PRIMARY KEY (`id`), - KEY `part_of_unconfidential` (`unconfidential`(10)), - KEY `text_index` (`text`(100)), - KEY `unconfidential_index` (`unconfidential`(100)), - KEY `address_index` (`address`(100)), - KEY `invoices_user_id_foreign` (`user_id`), - KEY `invoices_account_id_foreign` (`account_id`), - KEY `uuid` (`uuid`(10)), - CONSTRAINT `invoices_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`), - CONSTRAINT `invoices_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=49340 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `linkingkeys` --- - -DROP TABLE IF EXISTS `linkingkeys`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `linkingkeys` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) DEFAULT NULL, - `hex` varchar(255) DEFAULT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=12640 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `lnurl_migrations` --- - -DROP TABLE IF EXISTS `lnurl_migrations`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `lnurl_migrations` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `name` varchar(255) DEFAULT NULL, - `batch` int(11) DEFAULT NULL, - `migration_time` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `lnurl_migrations_lock` --- - -DROP TABLE IF EXISTS `lnurl_migrations_lock`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `lnurl_migrations_lock` ( - `index` int(10) unsigned NOT NULL AUTO_INCREMENT, - `is_locked` int(11) DEFAULT NULL, - PRIMARY KEY (`index`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `migrations` --- - -DROP TABLE IF EXISTS `migrations`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `migrations` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `name` varchar(255) DEFAULT NULL, - `batch` int(11) DEFAULT NULL, - `migration_time` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb4; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `migrations_lock` --- - -DROP TABLE IF EXISTS `migrations_lock`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `migrations_lock` ( - `index` int(10) unsigned NOT NULL AUTO_INCREMENT, - `is_locked` int(11) DEFAULT NULL, - PRIMARY KEY (`index`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `orders` --- - -DROP TABLE IF EXISTS `orders`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `orders` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `v1` double DEFAULT NULL, - `v2` double DEFAULT NULL, - `user_id` int(11) DEFAULT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `accepted` tinyint(1) NOT NULL DEFAULT 0, - `a1_id` int(11) DEFAULT NULL, - `a2_id` int(11) DEFAULT NULL, - `completedAt` datetime DEFAULT NULL, - `rate` double DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `orders_user_id_foreign` (`user_id`), - KEY `orders_a1_id_foreign` (`a1_id`), - KEY `orders_a2_id_foreign` (`a2_id`), - CONSTRAINT `orders_a1_id_foreign` FOREIGN KEY (`a1_id`) REFERENCES `accounts` (`id`), - CONSTRAINT `orders_a2_id_foreign` FOREIGN KEY (`a2_id`) REFERENCES `accounts` (`id`), - CONSTRAINT `orders_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=337 DEFAULT CHARSET=utf8mb4; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `payments` --- - -DROP TABLE IF EXISTS `payments`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `payments` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) DEFAULT NULL, - `hash` text DEFAULT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `rate` double DEFAULT NULL, - `currency` varchar(255) DEFAULT NULL, - `address` varchar(255) DEFAULT NULL, - `received` tinyint(1) DEFAULT NULL, - `amount` double DEFAULT NULL, - `tip` double NOT NULL DEFAULT 0, - `confirmed` tinyint(1) NOT NULL, - `fee` double NOT NULL DEFAULT 0, - `network` varchar(255) DEFAULT NULL, - `account_id` int(11) DEFAULT NULL, - `preimage` varchar(255) DEFAULT NULL, - `memo` text DEFAULT NULL, - `redeemed` tinyint(1) NOT NULL DEFAULT 0, - `redeemcode` varchar(255) DEFAULT NULL, - `path` varchar(255) DEFAULT NULL, - `invoice_id` int(11) DEFAULT NULL, - `fee_payment_id` int(11) DEFAULT NULL, - `with_id` int(11) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `idx_redeemcode` (`redeemcode`), - KEY `payments_invoice_id_foreign` (`invoice_id`), - KEY `payments_user_id_foreign` (`user_id`), - KEY `payments_account_id_foreign` (`account_id`), - KEY `fk_payments` (`fee_payment_id`), - KEY `fk_with` (`with_id`), - CONSTRAINT `fk_payments` FOREIGN KEY (`fee_payment_id`) REFERENCES `payments` (`id`), - CONSTRAINT `fk_with` FOREIGN KEY (`with_id`) REFERENCES `users` (`id`) ON DELETE SET NULL, - CONSTRAINT `payments_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`), - CONSTRAINT `payments_invoice_id_foreign` FOREIGN KEY (`invoice_id`) REFERENCES `invoices` (`id`), - CONSTRAINT `payments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=30378 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `prs` --- - -DROP TABLE IF EXISTS `prs`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `prs` ( - `text` text DEFAULT NULL, - `preimage` text DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `referrals` --- - -DROP TABLE IF EXISTS `referrals`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `referrals` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `user_id` int(11) DEFAULT NULL, - `sponsor_id` int(11) NOT NULL, - `token` varchar(255) NOT NULL, - `expiry` varchar(255) DEFAULT NULL, - `status` enum('available','used','expired','cancelled') NOT NULL DEFAULT 'available', - `created_at` datetime DEFAULT NULL, - `updated_at` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `referrals_user_id_foreign` (`user_id`), - KEY `referrals_sponsor_id_foreign` (`sponsor_id`), - CONSTRAINT `referrals_sponsor_id_foreign` FOREIGN KEY (`sponsor_id`) REFERENCES `users` (`id`), - CONSTRAINT `referrals_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `requests` --- - -DROP TABLE IF EXISTS `requests`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `requests` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `requester_id` int(11) DEFAULT NULL, - `recipient_id` int(11) DEFAULT NULL, - `memo` text DEFAULT NULL, - `createdAt` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), - `updatedAt` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', - `invoice_id` int(11) DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `urls` --- - -DROP TABLE IF EXISTS `urls`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `urls` ( - `hash` varchar(255) DEFAULT NULL, - `apiKeyId` varchar(255) DEFAULT NULL, - `tag` varchar(255) DEFAULT NULL, - `params` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`params`)), - `initialUses` int(10) unsigned DEFAULT 1, - `remainingUses` int(10) unsigned DEFAULT 0, - `createdAt` timestamp NOT NULL DEFAULT current_timestamp(), - `updatedAt` timestamp NOT NULL DEFAULT current_timestamp(), - UNIQUE KEY `urls_hash_unique` (`hash`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `users` --- - -DROP TABLE IF EXISTS `users`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `users` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `username` varchar(255) DEFAULT NULL, - `password` varchar(255) DEFAULT NULL, - `unit` varchar(255) DEFAULT 'SAT', - `currency` varchar(255) DEFAULT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `twofa` tinyint(1) DEFAULT NULL, - `pin` varchar(255) DEFAULT NULL, - `currencies` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, - `otpsecret` varchar(255) DEFAULT NULL, - `account_id` int(11) DEFAULT NULL, - `ip` int(10) unsigned DEFAULT NULL, - `subscriptions` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, - `seed` varchar(255) DEFAULT NULL, - `fiat` tinyint(1) NOT NULL DEFAULT 0, - `index` int(11) NOT NULL DEFAULT 0, - `verified` varchar(255) DEFAULT NULL, - `locked` tinyint(1) DEFAULT 0, - `authyId` varchar(255) DEFAULT NULL, - `admin` tinyint(1) DEFAULT NULL, - `email` varchar(255) DEFAULT NULL, - `phone` varchar(255) DEFAULT NULL, - `address` text DEFAULT NULL, - `profile` tinyint(1) DEFAULT 0, - `banner` tinyint(1) DEFAULT 0, - `theme` varchar(255) DEFAULT NULL, - `uuid` varchar(255) NOT NULL DEFAULT uuid(), - PRIMARY KEY (`id`), - KEY `ip` (`ip`), - KEY `username` (`username`) -) ENGINE=InnoDB AUTO_INCREMENT=15518 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `waiting_list` --- - -DROP TABLE IF EXISTS `waiting_list`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `waiting_list` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `email` varchar(255) NOT NULL, - `phone` varchar(255) NOT NULL, - `status` enum('pending','activated','expired','cancelled') NOT NULL, - `user_id` int(11) DEFAULT NULL, - `notes` text DEFAULT NULL, - `created_at` datetime DEFAULT NULL, - `updated_at` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `waiting_list_user_id_foreign` (`user_id`), - CONSTRAINT `waiting_list_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `withdrawals` --- - -DROP TABLE IF EXISTS `withdrawals`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `withdrawals` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) NOT NULL, - `createdAt` datetime NOT NULL, - `updatedAt` datetime NOT NULL, - `amount` double DEFAULT NULL, - `completed` tinyint(1) DEFAULT NULL, - `transit` varchar(255) DEFAULT NULL, - `account` varchar(255) DEFAULT NULL, - `institution` varchar(255) DEFAULT NULL, - `notes` text DEFAULT NULL, - `email` varchar(255) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `withdrawals_user_id_foreign` (`user_id`), - CONSTRAINT `withdrawals_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=4119 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; -/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; - -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; - --- Dump completed on 2022-11-24 19:44:06 diff --git a/doc/PAYMENTS.md b/doc/PAYMENTS.md deleted file mode 100644 index 2934aae8..00000000 --- a/doc/PAYMENTS.md +++ /dev/null @@ -1,52 +0,0 @@ -# This document describes the types of transactions and accounts that can occur in CoinOS - -## Receive bitcoin - -There are four ways a user can receive bitcoin - -- Bitcoin -- Liquid -- Lightning -- Internal - -In every case, a payment record is added to the `payments` table -The payment `type` will be one of BTC, LBTC, LNBTC, or COINOS -The `amount` will be a positive integer representing the number of satoshis received - -In the case of Bitcoin and Liquid payments, the `confirmed` column will be set to `false` initially until the transaction -is mined into a block on the blockchain. - -Also, the payment amount will be reflected in users' active account record in the `accounts` table under the `pending` column. The `balance` column will be updated when confirmations are received and the `pending` amount will be debited. - -## Send bitcoin - -Users can also send bitcoin over any of the four networks by pasting in or scanning a QR code of a bitcoin or liquid address, lightning invoice, or username. - -In the case of lightning, it's also possible to paste an [LNURL](https://github.com/fiatjaf/lnurl-rfc) which is a way of getting a lightning invoice from a remote server or other wallet over HTTP. - -If scanning or pasting an address or invoice that happens to belong to another coinos user, coinos will detect that and automatically change the payment type to an internal COINOS transfer as if you had typed in the recipient's username. - -## Creating Liquid Assets - -Users can issue new assets on the Liquid network at https://coinos.io/asset - -This will create an on-chain transaction that will issue a specified amount of new tokens. A new account record will be created in the `accounts` table for the user to track their balance of the new asset. As with receiving bitcoin over the Liquid network, the balance will be `pending` until one block confirmation has been received and then it will be updated. - -A separate payment record will be created to cover the fee for the issuance transaction, which is paid in L-BTC. - - -## Receiving Liquid Assets - -If the user has not received a particular asset before, a new account will be generated for them at the time the payment is detected. - -## Sending Liquid Assets - -Users can choose from a drop down menu to pick which account they want to be the active one. In the case of Liquid assets, they will only be able to an internal transfer to another coinos user, or send to a Liquid address. Users will receive an error if they paste in an LN invoice or on-chain BTC address while they have a Liquid asset accounted selected as their active account. - -## Depositing Fiat - -After having their account verified, users can fill out the deposit form at https://coinos.io/funding to indicate that they intend to send an interac or wire transfer. If the user is added to the list of accounts enabled for auto-deposit (under the `imap` config in config/index.js) then coinos will parse the Interac email it receives and automatically generate a new payment record and update the users Bitcoin account balance to credit them with an amount of bitcoin calculated equivalent to the fiat amount received, based on the current exchange rate less 2%. - -## Withdrawing Fiat - -Fiat withdrawals are currently processed manually. When a user requests to withdraw, we manually calculate how much bitcoin to remove from their account and do that with a database query before sending them an interac or wire transfer. diff --git a/doc/fund-server-bitcoin-cli.md b/doc/fund-server-bitcoin-cli.md deleted file mode 100644 index e0a2c5e3..00000000 --- a/doc/fund-server-bitcoin-cli.md +++ /dev/null @@ -1,44 +0,0 @@ -## Fund Coinos server (via bitcoin-cli) - -Bitcoin Regtest Mode allows us to create our own Bitcoin sandbox blockchain for use in testing & development. - - -From the coinos-server dir on your localfilesystem (ie- after following the 'Getting Started' section of our [root README]) - -```bash - -docker exec -it bitcoin bash - -# get an address -bitcoin-cli -regtest -rpcuser=admin1 -rpcpassword=123 getnewaddress -> bcrt1qzneawwyxe0fts5p8a6tsp4gu2mfnqzs92235hm # (example) - -# mine a block to the new address -bitcoin-cli -regtest -datadir=/home/bitcoin/.bitcoin generatetoaddress 101 theaddressgeneratedabove - -# verify you got the reward by doing: -bitcoin-cli -regtest -rpcuser=admin1 -rpcpassword=123 getbalance -> 50.00000000 - -# send 10 bitcoin to a coinos wallet account you created -# (get the address to send to from ./receive, select 'Bitcoin') -bitcoin-cli -regtest -rpcuser=admin1 -rpcpassword=123 sendtoaddress [account-address] 10 - -# confirm the transaction -# (do the mining command again) -bitcoin-cli -regtest -rpcuser=admin1 -rpcpassword=123 generatetoaddress 101 theoriginaladdressgeneratedabove -``` - -![](./img/50-bitcoin-server-balance.gif) - - - -#### further reading - -On why to generate 101 blocks and some general info on Regtest Mode: - -https://developer.bitcoin.org/examples/testing.html - - -[root README]:https://github.com/coinos/coinos-server - diff --git a/doc/img/50-bitcoin-server-balance.gif b/doc/img/50-bitcoin-server-balance.gif deleted file mode 100644 index 2fba5088..00000000 Binary files a/doc/img/50-bitcoin-server-balance.gif and /dev/null differ diff --git a/docker-compose.yml b/docker-compose.yml index d6117776..b4a73394 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,212 +1,41 @@ version: '3.6' services: - ui: - depends_on: - - app - container_name: ui - image: asoltys/coinos-ui:dev - ports: - - '8085:8085' - environment: - NODE_ENV: development - volumes: - - ../coinos-ui:/app - networks: - default: - ipv4_address: 172.18.0.27 app: - restart: always - depends_on: - - maria - - bitcoin - - liquid - - lnd - container_name: app image: cs + container_name: app + depends_on: + - rd + - bc + - cl build: . command: "pnpm dev" - ports: - - '3119:3119' environment: - NODE_OPTIONS: "--es-module-specifier-resolution=node --experimental-modules --loader ./loader.js --openssl-legacy-provider" + NODE_OPTIONS: "--es-module-specifier-resolution=node --experimental-modules --loader ./loader.js" NODE_ENV: development volumes: - ./:/app - ./logs:/logs - extra_hosts: - - host.docker.internal:host-gateway - networks: - default: - ipv4_address: 172.18.0.22 - bitcoin: + restart: always + bc: image: asoltys/bitcoin - container_name: bitcoin - ports: - - '18443:18443' + container_name: bc volumes: - ./config/bitcoin:/home/bitcoin/.bitcoin - restart: unless-stopped - networks: - default: - ipv4_address: 172.18.0.23 - liquid: - image: ghcr.io/vulpemventures/elements - container_name: liquid - volumes: - - ./config/liquid:/home/elements/.elements - restart: unless-stopped - networks: - default: - ipv4_address: 172.18.0.24 - lnd: - image: lightninglabs/lnd:v0.15.1-beta - container_name: lnd - volumes: - - ./config/lnd:/root/.lnd - - ./config/bitcoin:/root/.bitcoin - restart: unless-stopped - networks: - default: - ipv4_address: 172.18.0.25 - clightning: + restart: always + cl: image: elementsproject/lightningd:latest container_name: cl environment: LIGHTNINGD_NETWORK: regtest volumes: - ./config/lightning:/root/.lightning - restart: unless-stopped - networks: - default: - ipv4_address: 172.18.0.31 - lb: - image: elementsproject/lightningd:latest - container_name: lb - environment: - LIGHTNINGD_NETWORK: regtest - volumes: - - ./config/lb:/root/.lightning - restart: unless-stopped - networks: - default: - ipv4_address: 172.18.0.36 - lc: - image: elementsproject/lightningd:latest - container_name: lc - environment: - LIGHTNINGD_NETWORK: regtest - volumes: - - ./config/lc:/root/.lightning - restart: unless-stopped - networks: - default: - ipv4_address: 172.18.0.37 - maria: - restart: always - container_name: maria - image: mariadb:10.6.1 restart: always - environment: - MARIADB_ROOT_PASSWORD: ${ROOT_PASSWORD} - volumes: - - ./mysql:/var/lib/mysql - ports: - - 3307:3306 - networks: - default: - ipv4_address: 172.18.0.26 - electrsb: - depends_on: - - bitcoin - image: ghcr.io/vulpemventures/electrs - container_name: electrsb - entrypoint: - - /build/electrs - command: - - -vvvv - - --network - - regtest - - --daemon-dir - - /config - - --daemon-rpc-addr - - bitcoin:18443 - - --cookie - - admin1:123 - - --http-addr - - 0.0.0.0:3095 - - --electrum-rpc-addr - - 0.0.0.0:60400 - - --cors - - "*" - volumes: - - ./config/bitcoin/:/config - restart: unless-stopped - networks: - default: - ipv4_address: 172.18.0.28 - electrs-liquid: - image: ghcr.io/vulpemventures/electrs-liquid - container_name: electrs - depends_on: - - liquid - entrypoint: - - /build/electrs - command: - - --jsonrpc-import - - -vvvv - - --parent-network - - regtest - - --network - - liquidregtest - - --daemon-dir - - /config - - --daemon-rpc-addr - - liquid:7040 - - --cookie - - admin1:123 - - --http-addr - - 0.0.0.0:3002 - - --electrum-rpc-addr - - 0.0.0.0:60401 - - --cors - - "*" - volumes: - - ./config/liquid/:/config - restart: unless-stopped - ports: - - '3012:3002' - networks: - default: - ipv4_address: 172.18.0.29 - esplora: - image: vulpemventures/esplora:latest - container_name: esplora - depends_on: - - electrs-liquid - environment: - API_URL: ${LIQUID_ELECTRS_URL} - NATIVE_ASSET_ID: 5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225 - IS_ELEMENTS: 1 - ports: - - ${LIQUID_ESPLORA_PORT}:5000 - restart: unless-stopped - networks: - default: - ipv4_address: 172.18.0.30 - redis: - container_name: redis + rd: image: redis + container_name: rd volumes: - ./config/redis:/data - eb: - container_name: eb - image: asoltys/electrs-batch - environment: - PORT: 7111 - ELECTRS_URL: http://electrs:3002 - CONCURRENCY: 10 - ports: - - '7111:7111' + restart: always nostr: container_name: nostr image: asoltys/nostr-rs-relay @@ -216,27 +45,4 @@ services: - "./config/nostr/data:/usr/src/app/db" - "./config/nostr/config.toml:/usr/src/app/config.toml" ports: - - '7001:8080' - networks: - default: - ipv4_address: 172.18.2.2 - api: - container_name: api - image: nostr-api - restart: always - build: ../nostr-api - command: "pnpm dev" - volumes: - - "../nostr-api:/app" - - "./config/nostr/data:/data" - environment: - DB: "/data/nostr.db" - ports: - - '9393:9393' - networks: - default: - ipv4_address: 172.18.2.4 -networks: - default: - name: net - external: true + - '8080:8080' diff --git a/fx.sample b/fx.sample deleted file mode 100644 index bdfbf9ea..00000000 --- a/fx.sample +++ /dev/null @@ -1 +0,0 @@ -{"fx":{"AED":3.6732,"AFN":76.916238,"ALL":102.317329,"AMD":481.616267,"ANG":1.794577,"AOA":655.221553,"ARS":81.969207,"AUD":1.343089,"AWG":1.8,"AZN":1.7025,"BAM":1.614261,"BBD":2,"BDT":84.775754,"BGN":1.617356,"BHD":0.377015,"BIF":1940.93179,"BMD":1,"BND":1.335611,"BOB":6.883324,"BRL":5.1698,"BSD":1,"BTC":0.000054,"BTN":73.603314,"BWP":10.890769,"BYN":2.553493,"BZD":2.015195,"CAD":1.28192,"CDF":1968.605194,"CHF":0.889253,"CLF":0.026934,"CLP":743.201103,"CNH":6.52695,"CNY":6.5432,"COP":3498.827763,"CRC":601.611868,"CUC":0.999757,"CUP":25.750002,"CVE":91.517508,"CZK":21.767701,"DJF":177.984204,"DKK":6.160477,"DOP":58.1879,"DZD":131.135347,"EGP":15.688401,"ERN":14.999681,"ETB":38.380578,"EUR":0.827603,"FJD":2.0594,"FKP":0.747215,"GBP":0.747215,"GEL":3.295,"GGP":0.747215,"GHS":5.865177,"GIP":0.747215,"GMD":51.775005,"GNF":9900.000797,"GTQ":7.803103,"GYD":209.079747,"HKD":7.75215,"HNL":24.277427,"HRK":6.247201,"HTG":67.978168,"HUF":295.195892,"IDR":14112.940636,"ILS":3.24973,"IMP":0.747215,"INR":73.753254,"IQD":1192.518145,"IRR":42105.00339,"ISK":127.030011,"JEP":0.747215,"JMD":143.995324,"JOD":0.709,"JPY":104.271008,"KES":111.400009,"KGS":84.795464,"KHR":4054.500326,"KMF":408.250238,"KPW":900.000073,"KRW":1085.489331,"KWD":0.304948,"KYD":0.833116,"KZT":420.239329,"LAK":9290.000748,"LBP":1519.000122,"LKR":185.75786,"LRD":160.450008,"LSL":14.960001,"LYD":1.35,"MAD":8.998358,"MDL":17.334117,"MGA":3867.375108,"MKD":51.003691,"MMK":1331.69855,"MNT":2855.764265,"MOP":7.98247,"MRO":357.000029,"MRU":37.121155,"MUR":39.903163,"MVR":15.455002,"MWK":765.388159,"MXN":19.864809,"MYR":4.0675,"MZN":74.150003,"NAD":14.960001,"NGN":381.321569,"NIO":34.83301,"NOK":8.836006,"NPR":117.76525,"NZD":1.423995,"OMR":0.385003,"PAB":1,"PEN":3.598707,"PGK":3.518051,"PHP":48.147447,"PKR":160.450013,"PLN":3.665672,"PYG":7034.254563,"QAR":3.64075,"RON":4.0275,"RSD":97.340008,"RUB":73.672606,"RWF":985.000079,"SAR":3.751408,"SBD":8.018641,"SCR":21.205532,"SDG":55.325005,"SEK":8.489209,"SGD":1.336996,"SHP":0.747215,"SLL":10194.893602,"SOS":584.000047,"SRD":14.154001,"SSP":130.26001,"STD":20900.545921,"STN":20.500001,"SVC":8.747973,"SYP":512.818375,"SZL":14.960001,"THB":30.045003,"TJS":11.324825,"TMT":3.51,"TND":2.7105,"TOP":2.288166,"TRY":7.823654,"TTD":6.793277,"TWD":28.256202,"TZS":2318.448187,"UAH":28.0933,"UGX":3680.09987,"USD":1,"UYU":42.677098,"UZS":10440.165624,"VEF":248487.662247,"VES":930536.054919,"VND":23187.74545,"VUV":111.531348,"WST":2.5692,"XAF":542.871898,"XAG":0.041832,"XAU":0.000545,"XCD":2.70255,"XDR":0.696164,"XOF":542.871898,"XPD":0.000439,"XPF":98.759293,"XPT":0.000995,"YER":250.350086,"ZAR":14.964601,"ZMW":21.038963,"ZWL":322.000026}} \ No newline at end of file diff --git a/git_hooks/pre-commit b/git_hooks/pre-commit deleted file mode 100644 index 78876285..00000000 --- a/git_hooks/pre-commit +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') -[ -z "$FILES" ] && exit 0 - -# Prettify all selected files -echo "$FILES" | xargs ./node_modules/.bin/prettier --ignore-unknown --write - -# Add back the modified/prettified files to staging -echo "$FILES" | xargs git add - -exit 0 diff --git a/index.js b/index.js index e77ba2a8..b5ec0dca 100644 --- a/index.js +++ b/index.js @@ -1,52 +1,24 @@ import app from "./app"; -import persist from "./lib/persist"; -import config from "./config/index"; -import store from "./lib/store"; import { l, err } from "./lib/logging"; -import { optionalAuth } from "$lib/passport"; -import "./db"; import "./lib/redis"; import "./lib/sockets"; -import "./lib/webhooks"; import "./lib/passport"; import "./lib/upload"; -import "./lib/sync"; import "./lib/rates"; import "./lib/notifications"; import "./lib/mail"; import "./lib/nostr"; -import "./routes/assets"; -import "./routes/events"; import "./routes/locations"; -import "./routes/info"; -import "./routes/users"; import "./routes/invoices"; import "./routes/payments"; +import "./routes/users"; import "./routes/requests"; import "./routes/lnurl"; -import "./routes/tickets"; - -if (config.bitcoin) store.networks.push("bitcoin"); -if (config.liquid) store.networks.push("liquid"); -if (config.lna) store.networks.push("lightning"); let host = process.env.HOST || "0.0.0.0"; let port = process.env.PORT || 3119; - -app.post('/echo', optionalAuth, async (req, res) => { - console.log(req.user.username) - console.log(req.body) - res.send(req.body); -}); - -app.get('/nostr.json', async (req, res) => { - res.send({ - names: { adam: "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8", asoltys: "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8" } - }); -}); - app.listen({ host, port }, (e, address) => { e && err(e) && process.exit(1); l(`CoinOS Server listening on ${address}`); diff --git a/k8s/app-claim0-persistentvolumeclaim.yaml b/k8s/app-claim0-persistentvolumeclaim.yaml deleted file mode 100644 index f4d8470e..00000000 --- a/k8s/app-claim0-persistentvolumeclaim.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: null - labels: - io.kompose.service: app-claim0 - name: app-claim0 -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi -status: {} diff --git a/k8s/app-deployment.yaml b/k8s/app-deployment.yaml deleted file mode 100644 index 8dc491fa..00000000 --- a/k8s/app-deployment.yaml +++ /dev/null @@ -1,44 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: app - name: app -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: app - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: app - spec: - containers: - - env: - - name: NODE_ENV - value: development - image: asoltys/coinos-server - name: app - ports: - - containerPort: 3119 - resources: {} - volumeMounts: - - mountPath: /app - name: app-claim0 - restartPolicy: Always - volumes: - - name: app-claim0 - persistentVolumeClaim: - claimName: app-claim0 -status: {} diff --git a/k8s/app-service.yaml b/k8s/app-service.yaml deleted file mode 100644 index 38a3b515..00000000 --- a/k8s/app-service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: app - name: app -spec: - ports: - - name: "3119" - port: 3119 - targetPort: 3119 - selector: - io.kompose.service: app -status: - loadBalancer: {} diff --git a/k8s/bitcoin-claim0-persistentvolumeclaim.yaml b/k8s/bitcoin-claim0-persistentvolumeclaim.yaml deleted file mode 100644 index 8ea5e7d0..00000000 --- a/k8s/bitcoin-claim0-persistentvolumeclaim.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: null - labels: - io.kompose.service: bitcoin-claim0 - name: bitcoin-claim0 -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi -status: {} diff --git a/k8s/bitcoin-deployment.yaml b/k8s/bitcoin-deployment.yaml deleted file mode 100644 index 2192ece4..00000000 --- a/k8s/bitcoin-deployment.yaml +++ /dev/null @@ -1,42 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: bitcoin - name: bitcoin -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: bitcoin - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: bitcoin - spec: - containers: - - image: asoltys/bitcoin:latest - name: bitcoin - ports: - - containerPort: 18442 - - containerPort: 18443 - resources: {} - volumeMounts: - - mountPath: /config - name: bitcoin-claim0 - restartPolicy: Always - volumes: - - name: bitcoin-claim0 - persistentVolumeClaim: - claimName: bitcoin-claim0 -status: {} diff --git a/k8s/bitcoin-service.yaml b/k8s/bitcoin-service.yaml deleted file mode 100644 index 1d6a6c12..00000000 --- a/k8s/bitcoin-service.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: bitcoin - name: bitcoin -spec: - ports: - - name: "18442" - port: 18442 - targetPort: 18442 - - name: "18443" - port: 18443 - targetPort: 18443 - selector: - io.kompose.service: bitcoin -status: - loadBalancer: {} diff --git a/k8s/docker-compose-resolved.yaml b/k8s/docker-compose-resolved.yaml deleted file mode 100644 index 8ec00d5e..00000000 --- a/k8s/docker-compose-resolved.yaml +++ /dev/null @@ -1,118 +0,0 @@ -services: - app: - container_name: app - environment: - NODE_ENV: development - image: asoltys/coinos-server - ports: - - published: 3119 - target: 3119 - volumes: - - /home/adam/coinos-server:/app:rw - bitcoin: - container_name: bitcoin - image: asoltys/bitcoin:latest - ports: - - published: 18442 - target: 18442 - - published: 18443 - target: 18443 - restart: unless-stopped - volumes: - - /home/adam/coinos-server/config/bitcoin:/config:rw - electrs-liquid: - command: - - -vvvv - - --parent-network - - regtest - - --network - - liquidregtest - - --daemon-dir - - /config - - --daemon-rpc-addr - - liquid:7040 - - --cookie - - admin1:123 - - --http-addr - - 0.0.0.0:3002 - - --electrum-rpc-addr - - 0.0.0.0:60401 - - --cors - - '*' - container_name: electrs-liquid - entrypoint: - - /build/electrs - image: vulpemventures/electrs-liquid:latest - ports: - - published: 3013 - target: 3002 - - published: 60402 - target: 60401 - restart: unless-stopped - volumes: - - /home/adam/coinos-server/liquid:/config:rw - esplora-liquid: - container_name: esplora-liquid - environment: - API_URL: http://localhost:3012 - IS_ELEMENTS: 1 - NATIVE_ASSET_ID: 5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225 - image: vulpemventures/esplora:latest - ports: - - published: 5006 - target: 5000 - restart: unless-stopped - la: - container_name: la - environment: - LIGHTNINGD_NETWORK: regtest - image: elementsproject/lightningd:latest - restart: unless-stopped - volumes: - - /home/adam/coinos-server/config/lightning:/root/.lightning:rw - lb: - container_name: lb - environment: - LIGHTNINGD_NETWORK: regtest - image: elementsproject/lightningd:latest - restart: unless-stopped - volumes: - - /home/adam/coinos-server/config/lightningb:/root/.lightning:rw - liquid: - container_name: liquid - image: asoltys/liquid:latest - ports: - - published: 7040 - target: 7040 - - published: 18444 - target: 18444 - - published: 18602 - target: 18602 - - published: 18603 - target: 18603 - restart: unless-stopped - volumes: - - /home/adam/coinos-server/config/liquid:/config:rw - maria: - container_name: mariadb - environment: - MARIADB_ROOT_PASSWORD: password - image: mariadb:10.6.1 - ports: - - published: 3307 - target: 3306 - restart: always - volumes: - - /home/adam/coinos-server/mysql:/var/lib/mysql:rw - ui: - container_name: ui - environment: - NODE_ENV: development - image: asoltys/coinos-ui - ports: - - published: 8085 - target: 8085 - volumes: - - /home/adam/coinos-ui:/coinos:rw -version: '3.6' - diff --git a/k8s/electrs-liquid-claim0-persistentvolumeclaim.yaml b/k8s/electrs-liquid-claim0-persistentvolumeclaim.yaml deleted file mode 100644 index 43109416..00000000 --- a/k8s/electrs-liquid-claim0-persistentvolumeclaim.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: null - labels: - io.kompose.service: electrs-liquid-claim0 - name: electrs-liquid-claim0 -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi -status: {} diff --git a/k8s/electrs-liquid-deployment.yaml b/k8s/electrs-liquid-deployment.yaml deleted file mode 100644 index e33098f2..00000000 --- a/k8s/electrs-liquid-deployment.yaml +++ /dev/null @@ -1,62 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: electrs-liquid - name: electrs-liquid -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: electrs-liquid - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: electrs-liquid - spec: - containers: - - args: - - -vvvv - - --parent-network - - regtest - - --network - - liquidregtest - - --daemon-dir - - /config - - --daemon-rpc-addr - - liquid:7040 - - --cookie - - admin1:123 - - --http-addr - - 0.0.0.0:3002 - - --electrum-rpc-addr - - 0.0.0.0:60401 - - --cors - - '*' - command: - - /build/electrs - image: vulpemventures/electrs-liquid:latest - name: electrs-liquid - ports: - - containerPort: 3002 - - containerPort: 60401 - resources: {} - volumeMounts: - - mountPath: /config - name: electrs-liquid-claim0 - restartPolicy: Always - volumes: - - name: electrs-liquid-claim0 - persistentVolumeClaim: - claimName: electrs-liquid-claim0 -status: {} diff --git a/k8s/electrs-liquid-service.yaml b/k8s/electrs-liquid-service.yaml deleted file mode 100644 index 83e57bd1..00000000 --- a/k8s/electrs-liquid-service.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: electrs-liquid - name: electrs-liquid -spec: - ports: - - name: "3013" - port: 3013 - targetPort: 3002 - - name: "60402" - port: 60402 - targetPort: 60401 - selector: - io.kompose.service: electrs-liquid -status: - loadBalancer: {} diff --git a/k8s/esplora-liquid-deployment.yaml b/k8s/esplora-liquid-deployment.yaml deleted file mode 100644 index 4606a416..00000000 --- a/k8s/esplora-liquid-deployment.yaml +++ /dev/null @@ -1,40 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: esplora-liquid - name: esplora-liquid -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: esplora-liquid - strategy: {} - template: - metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: esplora-liquid - spec: - containers: - - env: - - name: API_URL - value: http://localhost:3012 - - name: IS_ELEMENTS - value: "1" - - name: NATIVE_ASSET_ID - value: 5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225 - image: vulpemventures/esplora:latest - name: esplora-liquid - ports: - - containerPort: 5000 - resources: {} - restartPolicy: Always -status: {} diff --git a/k8s/esplora-liquid-service.yaml b/k8s/esplora-liquid-service.yaml deleted file mode 100644 index 17875690..00000000 --- a/k8s/esplora-liquid-service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: esplora-liquid - name: esplora-liquid -spec: - ports: - - name: "5006" - port: 5006 - targetPort: 5000 - selector: - io.kompose.service: esplora-liquid -status: - loadBalancer: {} diff --git a/k8s/la-claim0-persistentvolumeclaim.yaml b/k8s/la-claim0-persistentvolumeclaim.yaml deleted file mode 100644 index d8c062a8..00000000 --- a/k8s/la-claim0-persistentvolumeclaim.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: null - labels: - io.kompose.service: la-claim0 - name: la-claim0 -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi -status: {} diff --git a/k8s/la-deployment.yaml b/k8s/la-deployment.yaml deleted file mode 100644 index a42eee8c..00000000 --- a/k8s/la-deployment.yaml +++ /dev/null @@ -1,42 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: la - name: la -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: la - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: la - spec: - containers: - - env: - - name: LIGHTNINGD_NETWORK - value: regtest - image: elementsproject/lightningd:latest - name: la - resources: {} - volumeMounts: - - mountPath: /root/.lightning - name: la-claim0 - restartPolicy: Always - volumes: - - name: la-claim0 - persistentVolumeClaim: - claimName: la-claim0 -status: {} diff --git a/k8s/lb-claim0-persistentvolumeclaim.yaml b/k8s/lb-claim0-persistentvolumeclaim.yaml deleted file mode 100644 index 2e77148f..00000000 --- a/k8s/lb-claim0-persistentvolumeclaim.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: null - labels: - io.kompose.service: lb-claim0 - name: lb-claim0 -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi -status: {} diff --git a/k8s/lb-deployment.yaml b/k8s/lb-deployment.yaml deleted file mode 100644 index 064df112..00000000 --- a/k8s/lb-deployment.yaml +++ /dev/null @@ -1,42 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: lb - name: lb -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: lb - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: lb - spec: - containers: - - env: - - name: LIGHTNINGD_NETWORK - value: regtest - image: elementsproject/lightningd:latest - name: lb - resources: {} - volumeMounts: - - mountPath: /root/.lightning - name: lb-claim0 - restartPolicy: Always - volumes: - - name: lb-claim0 - persistentVolumeClaim: - claimName: lb-claim0 -status: {} diff --git a/k8s/liquid-claim0-persistentvolumeclaim.yaml b/k8s/liquid-claim0-persistentvolumeclaim.yaml deleted file mode 100644 index 2d9030df..00000000 --- a/k8s/liquid-claim0-persistentvolumeclaim.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: null - labels: - io.kompose.service: liquid-claim0 - name: liquid-claim0 -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi -status: {} diff --git a/k8s/liquid-deployment.yaml b/k8s/liquid-deployment.yaml deleted file mode 100644 index 006a06e6..00000000 --- a/k8s/liquid-deployment.yaml +++ /dev/null @@ -1,44 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: liquid - name: liquid -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: liquid - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: liquid - spec: - containers: - - image: asoltys/liquid:latest - name: liquid - ports: - - containerPort: 7040 - - containerPort: 18444 - - containerPort: 18602 - - containerPort: 18603 - resources: {} - volumeMounts: - - mountPath: /config - name: liquid-claim0 - restartPolicy: Always - volumes: - - name: liquid-claim0 - persistentVolumeClaim: - claimName: liquid-claim0 -status: {} diff --git a/k8s/liquid-service.yaml b/k8s/liquid-service.yaml deleted file mode 100644 index d3b56800..00000000 --- a/k8s/liquid-service.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: liquid - name: liquid -spec: - ports: - - name: "7040" - port: 7040 - targetPort: 7040 - - name: "18444" - port: 18444 - targetPort: 18444 - - name: "18602" - port: 18602 - targetPort: 18602 - - name: "18603" - port: 18603 - targetPort: 18603 - selector: - io.kompose.service: liquid -status: - loadBalancer: {} diff --git a/k8s/maria-claim0-persistentvolumeclaim.yaml b/k8s/maria-claim0-persistentvolumeclaim.yaml deleted file mode 100644 index 55503cdb..00000000 --- a/k8s/maria-claim0-persistentvolumeclaim.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: null - labels: - io.kompose.service: maria-claim0 - name: maria-claim0 -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi -status: {} diff --git a/k8s/maria-deployment.yaml b/k8s/maria-deployment.yaml deleted file mode 100644 index 75d6153f..00000000 --- a/k8s/maria-deployment.yaml +++ /dev/null @@ -1,44 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: maria - name: maria -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: maria - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: maria - spec: - containers: - - env: - - name: MARIADB_ROOT_PASSWORD - value: password - image: mariadb:10.6.1 - name: mariadb - ports: - - containerPort: 3306 - resources: {} - volumeMounts: - - mountPath: /var/lib/mysql - name: maria-claim0 - restartPolicy: Always - volumes: - - name: maria-claim0 - persistentVolumeClaim: - claimName: maria-claim0 -status: {} diff --git a/k8s/maria-service.yaml b/k8s/maria-service.yaml deleted file mode 100644 index 31789fe1..00000000 --- a/k8s/maria-service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: maria - name: maria -spec: - ports: - - name: "3307" - port: 3307 - targetPort: 3306 - selector: - io.kompose.service: maria -status: - loadBalancer: {} diff --git a/k8s/ui-claim0-persistentvolumeclaim.yaml b/k8s/ui-claim0-persistentvolumeclaim.yaml deleted file mode 100644 index aafbd9b4..00000000 --- a/k8s/ui-claim0-persistentvolumeclaim.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: null - labels: - io.kompose.service: ui-claim0 - name: ui-claim0 -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi -status: {} diff --git a/k8s/ui-deployment.yaml b/k8s/ui-deployment.yaml deleted file mode 100644 index 86912390..00000000 --- a/k8s/ui-deployment.yaml +++ /dev/null @@ -1,44 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: ui - name: ui -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: ui - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: ui - spec: - containers: - - env: - - name: NODE_ENV - value: development - image: asoltys/coinos-ui - name: ui - ports: - - containerPort: 8085 - resources: {} - volumeMounts: - - mountPath: /coinos - name: ui-claim0 - restartPolicy: Always - volumes: - - name: ui-claim0 - persistentVolumeClaim: - claimName: ui-claim0 -status: {} diff --git a/k8s/ui-service.yaml b/k8s/ui-service.yaml deleted file mode 100644 index 21019ff6..00000000 --- a/k8s/ui-service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose convert -f docker-compose-resolved.yaml - kompose.version: 1.22.0 (955b78124) - creationTimestamp: null - labels: - io.kompose.service: ui - name: ui -spec: - ports: - - name: "8085" - port: 8085 - targetPort: 8085 - selector: - io.kompose.service: ui -status: - loadBalancer: {} diff --git a/lib/account.js b/lib/account.js deleted file mode 100644 index 1576546a..00000000 --- a/lib/account.js +++ /dev/null @@ -1,49 +0,0 @@ -import axios from "axios"; -import db from "$db"; -import store from "$lib/store"; - -export default async (asset, user, transaction) => { - const account = await db.Account.findOne({ - where: { - user_id: user.id, - asset, - pubkey: null - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (account) return account; - - let params = { - user_id: user.id, - asset - }; - let name = asset.substr(0, 6); - let domain = ""; - let ticker = asset.substr(0, 3).toUpperCase(); - let precision = 8; - - if (store.assets[asset]) { - ({ domain, ticker, precision, name } = store.assets[asset]); - } else { - const existing = await db.Account.findOne({ - where: { - asset - }, - order: [["id", "ASC"]], - limit: 1, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (existing) { - ({ domain, ticker, precision, name } = existing); - } - } - - params = { ...params, ...{ domain, ticker, precision, name } }; - params.balance = 0; - params.pending = 0; - return db.Account.create(params, { transaction }); -}; diff --git a/lib/buildliquidtx.js b/lib/buildliquidtx.js deleted file mode 100644 index 4aad18f4..00000000 --- a/lib/buildliquidtx.js +++ /dev/null @@ -1,160 +0,0 @@ -import config from "$config"; -import db from "$db"; -import store from "./store"; -import { prod } from "./utils"; -import axios from "axios"; -import liquid from "liquidjs-lib"; -import coinselect from "coinselect"; -import split from "coinselect/split"; -const api = prod - ? "https://blockstream.info/liquid/api" - : "http://localhost:3002"; -const SATS = 100000000; -const network = prod ? liquid.networks["liquid"] : liquid.networks["regtest"]; -import { Op } from "@sequelize/core"; - -export default async ({ - address, - asset, - amount, - feeRate, - replaceable, - user -}) => { - const { btcasset: btc } = config.liquid; - - if (!feeRate) { - let { feerate } = await lq.estimateSmartFee(6); - if (!feerate) feerate = 0.00002; - feeRate = feerate * SATS; - } - feeRate = Math.round(feeRate / 100); - if (asset !== btc) feeRate = 0; - - let payments; - if (asset === btc) { - payments = await db.Payment.findAll({ - where: { - received: true, - account_id: user.account_id, - network: "liquid" - } - }); - } else { - payments = await db.Payment.findAll({ - where: { - "$account.pubkey": { [Op.ne]: null }, - received: true, - user_id: user.id, - network: "liquid" - }, - include: { - model: db.Account, - as: "account" - } - }); - } - - let utxos = []; - for (let i = 0; i < payments.length; i++) { - let url = `${api}/address/${payments[i].address}/utxo`; - try { - utxos.push(...(await axios.get(url)).data); - } catch (e) { - err("problem fetching utxos", e.message, url); - } - } - - let feeInput = utxos.find(utxo => utxo.asset === btc && utxo.value > 1000); - if (!feeInput) - throw new Error( - "Insufficient balance, couldn't find an input to cover the fee" - ); - - utxos = utxos.filter(utxo => utxo.asset === asset); - - let feeChange = feeInput.value - 1000; - - let targets = [ - { - address, - value: amount - } - ]; - - let { inputs, outputs, fee } = coinselect(utxos, targets, feeRate); - if (!inputs || !outputs) { - l( - "utxos", - amount, - feeRate, - utxos.map(u => u.value), - inputs, - outputs, - fee - ); - - throw new Error( - "Couldn't find inputs for transaction, check your balance/fee" - ); - } - - let psbt = new liquid.Psbt({ network }); - let total = 0; - - let { address: changeAddress } = payments[payments.length - 1]; - - outputs.map(({ address, value }) => { - total += value; - if (!address) { - address = changeAddress; - store.change.push(address); - } - - psbt = psbt.addOutput({ - address, - asset, - value - }); - }); - - for (let i = 0; i < inputs.length; i++) { - let { txid, vout } = inputs[i]; - psbt = psbt.addInput({ - hash: txid, - index: vout, - nonWitnessUtxo: Buffer.from((await lq.getTransaction(txid)).hex, "hex") - }); - } - - if (asset !== btc) { - psbt.addInput({ - hash: feeInput.txid, - index: feeInput.vout, - nonWitnessUtxo: Buffer.from( - await lq.getTransaction(feeInput.txid).hex, - "hex" - ) - }); - - psbt.addOutput({ - changeAddress, - asset: btc, - value: feeChange - }); - } - - psbt.addOutput({ - asset: btc, - script: Buffer.alloc(0), - value: fee, - nonce: Buffer.alloc(1, 0) - }); - - if (replaceable) psbt.setInputSequence(0, 0xffffffff - 2); - - feeRate = Math.round(feeRate * 1000); - psbt = psbt.toBase64(); - - return { tx: psbt, feeRate }; -}; diff --git a/lib/buildtx.js b/lib/buildtx.js deleted file mode 100644 index daed2e73..00000000 --- a/lib/buildtx.js +++ /dev/null @@ -1,90 +0,0 @@ -import store from "./store"; -import { prod } from "./utils"; -import axios from "axios"; -import bitcoin from "bitcoinjs-lib"; -import coinselect from "coinselect"; -import split from "coinselect/split"; -const api = prod ? "https://blockstream.info/api" : "http://electrsb:3095"; -const SATS = 100000000; -const network = prod - ? bitcoin.networks["bitcoin"] - : bitcoin.networks["regtest"]; - -export default async ({ address, amount, feeRate, replaceable, user }) => { - if (!feeRate) feeRate = (await bc.estimateSmartFee(6)).feerate * SATS; - feeRate = Math.round(feeRate / 1000); - - const payments = await db.Payment.findAll({ - where: { - received: true, - account_id: user.account_id, - network: "bitcoin" - } - }); - - const addresses = [...new Set(payments.map(p => p.address))]; - const utxos = []; - for (let i = 0; i < addresses.length; i++) { - try { - utxos.push( - ...(await axios.get(`${api}/address/${addresses[i]}/utxo`)).data - ); - } catch (e) { - err("problem fetching utxos", e.message); - } - } - - let targets = [ - { - address, - value: amount - } - ]; - - let { inputs, outputs, fee } = coinselect(utxos, targets, feeRate); - - if (!inputs || !outputs) { - l( - "utxos", - amount, - feeRate, - utxos.map(u => u.value) - ); - throw new Error( - "Couldn't find inputs for transaction, check your balance/fee" - ); - } - - let psbt = new bitcoin.Psbt({ network }); - let total = 0; - - outputs.map(({ address, value }) => { - total += value; - if (!address) { - address = payments[payments.length - 1].address; - store.change.push(address); - } - - psbt = psbt.addOutput({ - address, - value - }); - }); - - for (let i = 0; i < inputs.length; i++) { - let { txid, vout } = inputs[i]; - let nonWitnessUtxo = Buffer.from(await bc.getRawTransaction(txid), "hex"); - psbt = psbt.addInput({ - hash: txid, - index: vout, - nonWitnessUtxo - }); - } - - if (replaceable) psbt.setInputSequence(0, 0xffffffff - 2); - - feeRate = Math.round(feeRate * 1000); - psbt = psbt.toBase64(); - - return { tx: psbt, feeRate }; -}; diff --git a/lib/countries.js b/lib/countries.js deleted file mode 100644 index 4d5a48dc..00000000 --- a/lib/countries.js +++ /dev/null @@ -1,249 +0,0 @@ -export default { - AF: "AFG", - AL: "ALB", - DZ: "DZA", - AD: "AND", - AO: "AGO", - AI: "AIA", - AQ: "ATA", - AR: "ARG", - AM: "ARM", - AW: "ABW", - AU: "AUS", - AT: "AUT", - AZ: "AZE", - BS: "BHS", - BH: "BHR", - BD: "BGD", - BB: "BRB", - BY: "BLR", - BE: "BEL", - BZ: "BLZ", - BJ: "BEN", - BM: "BMU", - BT: "BTN", - BO: "BOL", - BW: "BWA", - BV: "NOK", - BR: "BRA", - IO: "USD", - VG: "USD", - BN: "BRN", - BG: "BGR", - BF: "XOF", - BI: "BDI", - KH: "KHM", - CM: "CMR", - CA: "CAN", - IC: "EUR", - CV: "CVE", - KY: "KYD", - CF: "XAF", - TD: "TCD", - CL: "CHL", - CN: "CHN", - CX: "AUD", - CC: "AUD", - CO: "COL", - KM: "COM", - CG: "XAF", - CD: "CDF", - CK: "NZD", - CR: "CRC", - CI: "XOF", - HR: "HRV", - CU: "CUB", - CW: "ANG", - CY: "CYP", - CZ: "CZK", - DK: "DNK", - DJ: "DJI", - DM: "DMA", - DO: "DOP", - TL: "IDS", - EC: "ECU", - EG: "EGY", - SV: "SVC", - GQ: "XAF", - ER: "ERI", - EE: "EST", - ET: "ETH", - FK: "FKP", - FO: "DKK", - FJ: "FJI", - FI: "FIN", - FR: "FRA", - GF: "EUR", - PF: "XPF", - TF: "EUR", - GA: "GAB", - GM: "GMB", - GE: "GEO", - DE: "DEU", - GH: "GHA", - GI: "GIB", - GR: "GRC", - GL: "GRL", - GD: "GRD", - GP: "GLP", - GU: "GUM", - GT: "GTM", - GG: "GGY", - GN: "GIN", - GW: "XOF", - GY: "GUY", - HT: "HTI", - HM: "AUD", - HN: "HND", - HK: "HKD", - HU: "HUN", - IS: "ISL", - IN: "IND", - ID: "IDN", - IR: "IRN", - IQ: "IRQ", - IE: "IRL", - Of: "Man", - IL: "ISR", - IT: "ITA", - JM: "JAM", - JP: "JPN", - JE: "JEY", - JO: "JOR", - KZ: "KAZ", - KE: "KEN", - KI: "KIR", - XK: "XKX", - KW: "KWT", - KG: "KGZ", - LA: "LAO", - LV: "LVA", - LB: "LBN", - LS: "LSO", - LR: "LBR", - LY: "LBY", - LI: "LIE", - LT: "LTU", - LU: "LUX", - MO: "MAC", - MK: "MKD", - MG: "MDG", - MW: "MWI", - MY: "MYS", - MV: "MDV", - ML: "MLI", - MT: "MLT", - MH: "USD", - MQ: "MTQ", - MR: "MRT", - MU: "MUS", - YT: "MYT", - MX: "MEX", - FM: "FSM", - MD: "MDA", - MC: "MCO", - MN: "MNG", - ME: "MNE", - MS: "MSR", - MA: "MAR", - MZ: "MOZ", - MM: "MMR", - NA: "NAM", - NR: "NRU", - NP: "NPL", - NL: "NLD", - AN: "ANG", - NC: "XPF", - NZ: "NZD", - NI: "NIC", - NE: "NER", - NG: "NGA", - NU: "NIU", - NF: "AUD", - KP: "KPW", - MP: "USD", - NO: "NOR", - OM: "OMN", - PK: "PAK", - PW: "PLW", - PS: "PSE", - PA: "PAN", - PG: "PGK", - PY: "PRY", - PE: "PER", - PH: "PHL", - PN: "PCN", - PL: "POL", - PT: "PRT", - PR: "USD", - QA: "QAT", - RE: "REU", - RO: "ROU", - RU: "RUS", - RW: "RWA", - BL: "EUR", - SH: "GPB", - KN: "XCD", - LC: "XCD", - MF: "EUR", - PM: "EUR", - VC: "XCD", - WS: "WSM", - SM: "EUR", - ST: "STN", - SA: "SAR", - SN: "SEN", - RS: "SRB", - CS: "RSD", - SC: "SYC", - SL: "SLL", - SG: "SGP", - SX: "ANG", - SK: "SVK", - SI: "SVN", - SB: "SBD", - SO: "SOM", - ZA: "ZAR", - GS: "GBP", - KR: "KRW", - SS: "SSP", - ES: "ESP", - LK: "LKR", - SD: "SDN", - SR: "SUR", - SJ: "NOK", - SZ: "SWZ", - SE: "SWE", - CH: "CHE", - SY: "SYR", - TW: "TWN", - TJ: "TJK", - TZ: "TZA", - TH: "THA", - TG: "TGO", - TK: "TKL", - TO: "TON", - TT: "TTD", - TA: "SHP", - TN: "TUN", - TR: "TUR", - TM: "TKM", - TC: "USD", - TV: "TUV", - UM: "USD", - VI: "USD", - UG: "UGA", - UA: "UKR", - AE: "AED", - GB: "GBP", - US: "USD", - UY: "URY", - UZ: "UZB", - VU: "VUT", - VA: "VAT", - VE: "VEN", - VN: "VNM", - YE: "YEM", - ZM: "ZMB", - ZW: "ZWE" -}; diff --git a/lib/liquid.js b/lib/liquid.js deleted file mode 100644 index ccf6aafb..00000000 --- a/lib/liquid.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from "$config"; -import rpc from "./rpc"; -export default rpc(config.liquid); diff --git a/lib/lnd.js b/lib/lnd.js deleted file mode 100644 index ff8b4c56..00000000 --- a/lib/lnd.js +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/lib/logo.js b/lib/logo.js deleted file mode 100644 index 922716ac..00000000 --- a/lib/logo.js +++ /dev/null @@ -1 +0,0 @@ -export default "iVBORw0KGgoAAAANSUhEUgAABbYAAAW2CAYAAABXn/ThAAAgAElEQVR4Xuza2W7d5bnA4c/OyLQRUxg2IEoACaG2opV6F0jcTg/KCRfAQS+qJ5VQq1Y9aCuKxFyGTUMgk5Ot729WmrJ35WQ18Vo/+4nkOkm9sl4/7xs7/pmdMcaN4QcBAgQIECBAgAABAgQIECBAgAABAgQIEIgI7AjbkU0ZkwABAgQIECBAgAABAgQIECBAgAABAgQWAWHbIRAgQIAAAQIECBAgQIAAAQIECBAgQIBASkDYTq3LsAQIECBAgAABAgQIECBAgAABAgQIECAgbLsBAgQIECBAgAABAgQIECBAgAABAgQIEEgJCNupdRmWAAECBAgQIECAAAECBAgQIECAAAECBIRtN0CAAAECBAgQIECAAAECBAgQIECAAAECKQFhO7UuwxIgQIAAAQIECBAgQIAAAQIECBAgQICAsO0GCBAgQIAAAQIECBAgQIAAAQIECBAgQCAlIGyn1mVYAgQIECBAgAABAgQIECBAgAABAgQIEBC23QABAgQIECBAgAABAgQIECBAgAABAgQIpASE7dS6DEuAAAECBAgQIECAAAECBAgQIECAAAECwrYbIECAAAECBAgQIECAAAECBAgQIECAAIGUgLCdWpdhCRAgQIAAAQIECBAgQIAAAQIECBAgQEDYdgMECBAgQIAAAQIECBAgQIAAAQIECBAgkBIQtlPrMiwBAgQIECBAgAABAgQIECBAgAABAgQICNtugAABAgQIECBAgAABAgQIECBAgAABAgRSAsJ2al2GJUCAAAECBAgQIECAAAECBAgQIECAAAFh2w0QIECAAAECBAgQIECAAAECBAgQIECAQEpA2E6ty7AECBAgQIAAAQIECBAgQIAAAQIECBAgIGy7AQIECBAgQIAAAQIECBAgQIAAAQIECBBICQjbqXUZlgABAgQIECBAgAABAgQIECBAgAABAgSEbTdAgAABAgQIECBAgAABAgQIECBAgAABAikBYTu1LsMSIECAAAECBAgQIECAAAECBAgQIECAgLDtBggQIECAAAECBAgQIECAAAECBAgQIEAgJSBsp9ZlWAIECBAgQIAAAQIECBAgQIAAAQIECBAQtt0AAQIECBAgQIAAAQIECBAgQIAAAQIECKQEhO3UugxLgAABAgQIECBAgAABAgQIECBAgAABAsK2GyBAgAABAgQIECBAgAABAgQIECBAgACBlICwnVqXYQkQIECAAAECBAgQIECAAAECBAgQIEBA2HYDBAgQIECAAAECBAgQIECAAAECBAgQIJASELZT6zIsAQIECBAgQIAAAQIECBAgQIAAAQIECAjbboAAAQIECBAgQIAAAQIECBAgQIAAAQIEUgLCdmpdhiVAgAABAgQIECBAgAABAgQIECBAgAABYdsNECBAgAABAgQIECBAgAABAgQIECBAgEBKQNhOrcuwBAgQIECAAAECBAgQIECAAAECBAgQICBsuwECBAgQIECAAAECBAgQIECAAAECBAgQSAkI26l1GZYAAQIECBAgQIAAAQIECBAgQIAAAQIEhG03QIAAAQIECBAgQIAAAQIECBAgQIAAAQIpAWE7tS7DEiBAgAABAgQIECBAgAABAgQIECBAgICw7QYIECBAgAABAgQIECBAgAABAgQIECBAICUgbKfWZVgCBAgQIECAAAECBAgQIECAAAECBAgQELbdAAECBAgQIECAAAECBAgQIECAAAECBAikBITt1LoMS4AAAQIECBAgQIAAAQIECBAgQIAAAQLCthsgQIAAAQIECBAgQIAAAQIECBAgQIAAgZSAsJ1al2EJECBAgAABAgQIECBAgAABAgQIECBAQNh2AwQIECBAgAABAgQIECBAgAABAgQIECCQEhC2U+syLAECBAgQIECAAAECBAgQIECAAAECBAgI226AAAECBAgQIECAAAECBAgQIECAAAECBFICwnZqXYYlQIAAAQIECBAgQIAAAQIECBAgQIAAAWHbDRAgQIAAAQIECBAgQIAAAQIECBAgQIBASkDYTq3LsAQIECBAgAABAgQIECBAgAABAgQIECAgbLsBAgQIECBAgAABAgQIECBAgAABAgQIEEgJCNupdRmWAAECBAgQIECAAAECBAgQIECAAAECBIRtN0CAAAECBAgQIECAAAECBAgQIECAAAECKQFhO7UuwxIgQIAAAQIECBAgQIAAAQIECBAgQICAsO0GCBAgQIAAAQIECBAgQIAAAQIECBAgQCAlIGyn1mVYAgQIECBAgAABAgQIECBAgAABAgQIEBC23QABAgQIECBAgAABAgQIECBAgAABAgQIpASE7dS6DEuAAAECBAgQIECAAAECBAgQIECAAAECwrYbIECAAAECBAgQIECAAAECBAgQIECAAIGUgLCdWpdhCRAgQIAAAQIECBAgQIAAAQIECBAgQEDYdgMECBAgQIAAAQIECBAgQIAAAQIECBAgkBIQtlPrMiwBAgQIECBAgAABAgQIECBAgAABAgQICNtugAABAgQIECBAgAABAgQIECBAgAABAgRSAsJ2al2GJUCAAAECBAgQIECAAAECBAgQIECAAAFh2w0QIECAAAECBAgQIECAAAECBAgQIECAQEpA2E6ty7AECBAgQIAAAQIECBAgQIAAAQIECBAgIGy7AQIECBAgQIAAAQIECBAgQIAAAQIECBBICQjbqXUZlgABAgQIECBAgAABAgQIECBAgAABAgSEbTdAgAABAgQIECBAgAABAgQIECBAgAABAikBYTu1LsMSIECAAAECBAgQIECAAAECBAgQIECAgLDtBggQIECAAAECBAgQIECAAAECBAgQIEAgJSBsp9ZlWAIECBAgQIAAAQIECBAgQIAAAQIECBAQtt0AAQIECBAgQIAAAQIECBAgQIAAAQIECKQEhO3UugxLgAABAgQIECBAgAABAgQIECBAgAABAsK2GyBAgAABAgQIECBAgAABAgQIECBAgACBlICwnVqXYQkQIECAAAECBAgQIECAAAECBAgQIEBA2HYDBAgQIECAAAECBAgQIECAAAECBAgQIJASELZT6zIsAQIECBAgQIAAAQIECBAgQIAAAQIECAjbboAAAQIECBAgQIAAAQIECBAgQIAAAQIEUgLCdmpdhiVAgAABAgQIECBAgAABAgQIECBAgAABYdsNECBAgAABAgQIECBAgAABAgQIECBAgEBKQNhOrcuwBAgQIECAAAECBAgQIECAAAECBAgQICBsuwECBAgQIECAAAECBAgQIECAAAECBAgQSAkI26l1GZYAAQIECBAgQIAAAQIECBAgQIAAAQIEhG03QIAAAQIECBAgQIAAAQIECBAgQIAAAQIpAWE7tS7DEiBAgAABAgQIECBAgAABAgQIECBAgICw7QYIECBAgAABAgQIECBAgAABAgQIECBAICUgbKfWZVgCBAgQIECAAAECBAgQIECAAAECBAgQELbdAAECBAgQIECAAAECBAgQIECAAAECBAikBITt1LoMS4AAAQIECBAgQIAAAQIECBAgQIAAAQLCthsgQIAAAQIECBAgQIAAAQIECBAgQIAAgZSAsJ1al2EJECBAgAABAgQIECBAgAABAgQIECBAQNh2AwQIECBAgAABAgQIECBAgAABAgQIECCQEhC2U+syLAECBAgQIECAAAECBAgQIECAAAECBAgI226AAAECBAgQIECAAAECBAgQIECAAAECBFICwnZqXYYlQIAAAQIECBAgQIAAAQIECBAgQIAAAWHbDRAgQIAAAQIECBAgQIAAAQIECBAgQIBASkDYTq3LsAQIECBAgAABAgQIECBAgAABAgQIECAgbLsBAgQIECBAgAABAgQIECBAgAABAgQIEEgJCNupdRmWAAECBAgQIECAAAECBAgQIECAAAECBIRtN0CAAAECBAgQIECAAAECBAgQIECAAAECKQFhO7UuwxIgQIAAAQIECBAgQIAAAQIECBAgQICAsO0GCBAgQIAAAQIECBAgQIAAAQIECBAgQCAlIGyn1mVYAgQIECBAgAABAgQIECBAgAABAgQIEBC23QABAgQIECBAgAABAgQIECBAgAABAgQIpASE7dS6DEuAAAECBAgQIECAAAECBAgQIECAAAECwrYbIECAAAECBAgQIECAAAECBAgQIECAAIGUgLCdWpdhCRAgQIAAAQIECBAgQIAAAQIECBAgQEDYdgMECBAgQIAAAQIECBAgQIAAAQIECBAgkBIQtlPrMiwBAgQIECBAgAABAgQIECBAgAABAgQICNtugAABAgQIECBAgAABAgQIECBAgAABAgRSAsJ2al2GJUCAAAECBAgQIECAAAECBAgQIECAAAFh2w0QIECAAAECBAgQIECAAAECBAgQIECAQEpA2E6ty7AECBAgQIAAAQIECBAgQIAAAQIECBAgIGy7AQIECBAgQIAAAQIECBAgQIAAAQIECBBICQjbqXUZlgABAgQIECBAgAABAgQIECBAgAABAgSEbTdAgAABAgQIECBAgAABAgQIECBAgAABAikBYTu1LsMSIECAAAECBAgQIECAAAECBAgQIECAgLDtBggQIECAAAECBAgQIECAAAECBAgQIEAgJSBsp9ZlWAIECBAgQIAAAQIECBAgQIAAAQIECBAQtt0AAQIECBAgQIAAAQIECBAgQIAAAQIECKQEhO3UugxLgAABAgQIECBAgAABAgQIECBAgAABAsK2GyBAgAABAgQIECBAgAABAgQIECBAgACBlICwnVqXYQkQIECAAAECBAgQIECAAAECBAgQIEBA2HYDBAgQIECAAAECBAgQIECAAAECBAgQIJASELZT6zIsAQIECBAgQIAAAQIECBAgQIAAAQIECAjbboAAAQIECBAgQIAAAQIECBAgQIAAAQIEUgLCdmpdhiVAgAABAgQIECBAgAABAgQIECBAgAABYdsNECBAgAABAgQIECBAgAABAgQIECBAgEBKQNhOrcuwBAgQIECAAAECBAgQIECAAAECBAgQICBsuwECBAgQIECAAAECBAgQIECAAAECBAgQSAkI26l1GZYAAQIECBAgQIAAAQIECBAgQIAAAQIEhG03QIAAAQIECBAgQIAAAQIECBAgQIAAAQIpAWE7tS7DEiBAgAABAgQIECBAgAABAgQIECBAgICw7QYIECBAgAABAgQIECBAgAABAgQIECBAICUgbKfWZVgCBAgQIECAAAECBAgQIECAAAECBAgQELbdAAECBAgQIECAAAECBAgQIECAAAECBAikBITt1LoMS4AAAQIECBAgQIAAAQIECBAgQIAAAQLCthsgQIAAAQIECBAgQIAAAQIECBAgQIAAgZSAsJ1al2EJECBAgAABAgQIECBAgAABAgQIECBAQNh2AwQIECBAgAABAgQIECBAgAABAgQIECCQEhC2U+syLAECBAgQIECAAAECBAgQIECAAAECBAgI226AAAECBAgQIECAAAECBAgQIECAAAECBFICwnZqXYYlQIAAAQIECBAgQIAAAQIECBAgQIAAAWHbDRAgQIAAAQIECBAgQIAAAQIECBAgQIBASkDYTq3LsAQIECBAgAABAgQIECBAgAABAgQIECAgbLsBAgQIECBAgAABAgQIECBAgAABAgQIEEgJCNupdRmWAAECBAgQIECAAAECBAgQIECAAAECBIRtN0CAAAECBAgQIECAAAECBAgQIECAAAECKQFhO7UuwxIgQIAAAQIECBAgQIAAAQIECBAgQICAsO0GCBAgQIAAAQIECBAgQIAAAQIECBAgQCAlIGyn1mVYAgQIECBAgAABAgQIECBAgAABAgQIEBC23QABAgQIECBAgAABAgQIECBAgAABAgQIpASE7dS6DEuAAAECBAgQIECAAAECBAgQIECAAAECwrYbIECAAAECBAgQIECAAAECBAgQIECAAIGUgLCdWpdhCRAgQIAAAQIECBAgQIAAAQIECBAgQEDYdgMECBAgQIAAAQIECBAgQIAAAQIECBAgkBIQtlPrMiwBAgQIECBAgAABAgQIECBAgAABAgQICNtugAABAgQIECBAgAABAgQIECBAgAABAgRSAsJ2al2GJUCAAAECBAgQIECAAAECBAgQIECAAAFh2w0QIECAAAECBAgQIECAAAECBAgQIECAQEpA2E6ty7AECBAgQIAAAQIECBAgQIAAAQIECBAgIGy7AQIECBAgQIAAAQIECBAgQIAAAQIECBBICQjbqXUZlgABAgQIECBAgAABAgQIECBAgAABAgSEbTdAgAABAgQIECBAgAABAgQIECBAgAABAikBYTu1LsMSIECAAAECBAgQIECAAAECBAgQIECAgLDtBggQIECAAAECBAgQIECAAAECBAgQIEAgJSBsp9ZlWAIECBAgQIAAAQIECBAgQIAAAQIECBAQtt0AAQIECBAgQIAAAQIECBAgQIAAAQIECKQEhO3UugxLgAABAgQIECBAgAABAgQIECBAgAABAsK2GyBAgAABAgQIECBAgAABAgQIECBAgACBlICwnVqXYQkQIECAAAECBAgQIECAAAECBAgQIEBA2HYDBAgQIECAAAECBAgQIECAAAECBAgQIJASELZT6zIsAQIECBAgQIAAAQIECBAgQIAAAQIECAjbboAAAQIECBAgQIAAAQIECBAgQIAAAQIEUgLCdmpdhiVAgAABAgQIECBAgAABAgQIECBAgAABYdsNECBAgAABAgQIECBAgAABAgQIECBAgEBKQNhOrcuwBAgQIECAAAECBAgQIECAAAECBAgQICBsuwECBAgQIECAAAECBAgQIECAAAECBAgQSAkI26l1GZYAAQIECBAgQIAAAQIECBAgQIAAAQIEhG03QIAAAQIECBAgQIAAAQIECBAgQIAAAQIpAWE7tS7DEiBAgAABAgQIECBAgAABAgQIECBAgICw7QYIECBAgAABAgQIECBAgAABAgQIECBAICUgbKfWZVgCBAgQIECAAAECBAgQIECAAAECBAgQELbdAAECBAgQIECAAAECBAgQIECAAAECBAikBITt1LoMS4AAAQIECBAgQIAAAQIECBAgQIAAAQLCthsgQIAAAQIECBAgQIAAAQIECBAgQIAAgZSAsJ1al2EJECBAgAABAgQIECBAgAABAgQIECBAQNh2AwQIECBAgAABAgQIECBAgAABAgQIECCQEhC2U+syLAECBAgQIECAAAECBAgQIECAAAECBAgI226AAAECBAgQIECAAAECBAgQIECAAAECBFICwnZqXYYlQIAAAQIECBAgQIAAAQIECBAgQIAAAWHbDRAgQIAAAQIECBAgQIAAAQIECBAgQIBASkDYTq3LsAQIECBAgAABAgQIECBAgAABAgQIECAgbLsBAgQIECBAgAABAgQIECBAgAABAgQIEEgJCNupdRmWAAECBAgQIECAAAECBAgQIECAAAECBIRtN0CAAAECBAgQIECAAAECBAgQIECAAAECKQFhO7UuwxIgQIAAAQIECBAgQIAAAQIECBAgQICAsO0GCBAgQIAAAQIECBAgQIAAAQIECBAgQCAlIGyn1mVYAgQIECBAgAABAgQIECBAgAABAgQIEBC23QABAgQIECBAgAABAgQIECBAgAABAgQIpASE7dS6DEuAAAECBAgQIECAAAECBAgQIECAAAECwrYbIECAAAECBAgQIECAAAECBAgQIECAAIGUgLCdWpdhCRAgQIAAAQIECBAgQIAAAQIECBAgQEDYdgMECBAgQIAAAQIECBAgQIAAAQIECBAgkBIQtlPrMiwBAgQIECBAgAABAgQIECBAgAABAgQICNtugAABAgQIECBAgAABAgQIECBAgAABAgRSAsJ2al2GJUCAAAECBAgQIECAAAECBAgQIECAAAFh2w0QIECAAAECBAgQIECAAAECBAgQIECAQEpA2E6ty7AECBAgQIAAAQIECBAgQIAAAQIECBAgIGy7AQIECBAgQIAAAQIECBAgQIAAAQIECBBICQjbqXUZlgABAgQIECBAgAABAgQIECBAgAABAgSEbTdAgAABAgQIECBAgAABAgQIECBAgAABAikBYTu1LsMSIECAAAECBAgQIECAAAECBAgQIECAgLDtBggQIECAAAECBAgQIECAAAECBAgQIEAgJSBsp9ZlWAIECBAgQIAAAQIECBAgQIAAAQIECBAQtt0AAQIECBAgQIAAAQIECBAgQIAAAQIECKQEhO3UugxLgAABAgQIECBAgAABAgQIECBAgAABAsK2GyBAgAABAgQIECBAgAABAgQIECBAgACBlICwnVqXYQkQIECAAAECBAgQIECAAAECBAgQIEBA2HYDBAgQIECAAAECBAgQIECAAAECBAgQIJASELZT6zIsAQIECBAgQIAAAQIECBAgQIAAAQIECAjbboAAAQIECBAgQIAAAQIECBAgQIAAAQIEUgLCdmpdhiVAgAABAgQIECBAgAABAgQIECBAgAABYdsNECBAgAABAgQIECBAgAABAgQIECBAgEBKQNhOrcuwBAgQIECAAAECBAgQIECAAAECBAgQICBsuwECBAgQIECAAAECBAgQIECAAAECBAgQSAkI26l1GZYAAQIECBAgQIAAAQIECBAgQIAAAQIEhG03QIAAAQIECBAgQIAAAQIECBAgQIAAAQIpAWE7tS7DEiBAgAABAgQIECBAgAABAgQIECBAgICw7QYIECBAgAABAgQIECBAgAABAgQIECBAICUgbKfWZVgCBAgQIECAAAECBAgQIECAAAECBAgQELbdAAECBAgQIECAAAECBAgQIECAAAECBAikBITt1LoMS4AAAQIECBAgQIAAAQIECBAgQIAAAQLCthsgQIAAAQIECBAgQIAAAQIECBAgQIAAgZSAsJ1al2EJECBAgAABAgQIECBAgAABAgQIECBAQNh2AwQIECBAgAABAgQIECBAgAABAgQIECCQEhC2U+syLAECBAgQIECAAAECBAgQIECAAAECBAgI226AAAECBAgQIECAAAECBAgQIECAAAECBFICwnZqXYYlQIAAAQIECBAgQIAAAQIECBAgQIAAAWHbDRAgQIAAAQIECBAgQIAAAQIECBAgQIBASkDYTq3LsAQIECBAgAABAgQIECBAgAABAgQIECAgbLsBAgQIECBAgAABAgQIECBAgAABAgQIEEgJCNupdRmWAAECBAgQIECAAAECBAgQIECAAAECBIRtN0CAAAECBAgQIECAAAECBAgQIECAAAECKQFhO7UuwxIgQIAAAQIECBAgQIAAAQIECBAgQICAsO0GCBAgQIAAAQIECBAgQIAAAQIECBAgQCAlIGyn1mVYAgQIECBAgAABAgQIECBAgAABAgQIEBC23QABAgQIECBAgAABAgQIECBAgAABAgQIpASE7dS6DEuAAAECBAgQIECAAAECBAgQIECAAAECwrYbIECAAAECBAgQIECAAAECBAgQIECAAIGUgLCdWpdhCRAgQIAAAQIECBAgQIAAAQIECBAgQEDYdgMECBAgQIAAAQIECBAgQIAAAQIECBAgkBIQtlPrMiwBAgQIECBAgAABAgQIECBAgAABAgQICNtugAABAgQIECBAgAABAgQIECBAgAABAgRSAsJ2al2GJUCAAAECBAgQIECAAAECBAgQIECAAAFh2w0QIECAAAECBAgQIECAAAECBAgQIECAQEpA2E6ty7AECBAgQIAAAQIECBAgQIAAAQIECBAgIGy7AQIECBAgQIAAAQIECBAgQIAAAQIECBBICQjbqXUZlgABAgQIECBAgAABAgQIECBAgAABAgSEbTdAgAABAgQIECBAgAABAgQIECBAgAABAikBYTu1LsMSIECAAAECBAgQIECAAAECBAgQIECAgLDtBggQIECAAAECBAgQIECAAAECBAgQIEAgJSBsp9ZlWAIECBAgQIAAAQIECBAgQIAAAQIECBAQtt0AAQIECBAgQIAAAQIECBAgQIAAAQIECKQEhO3UugxLgAABAgQIECBAgAABAgQIECBAgAABAsK2GyBAgAABAgQIECBAgAABAgQIECBAgACBlICwnVqXYQkQIECAAAECBAgQIECAAAECBAgQIEBA2HYDBAgQIECAAAECBAgQIECAAAECBAgQIJASELZT6zIsAQIECBAgQIAAAQIECBAgQIAAAQIECAjbboAAAQIECBAgQIAAAQIECBAgQIAAAQIEUgLCdmpdhiVAgAABAgQIECBAgAABAgQIECBAgAABYdsNECBAgAABAgQIECBAgAABAgQIECBAgEBKQNhOrcuwBAgQIECAAAECBAgQIECAAAECBAgQICBsuwECBAgQIECAAAECBAgQIECAAAECBAgQSAkI26l1GZYAAQIECBAgQIAAAQIECBAgQIAAAQIEhG03QIAAAQIECBAgQIAAAQIECBAgQIAAAQIpAWE7tS7DEiBAgAABAgQIECBAgAABAgQIECBAgICw7QYIECBAgAABAgQIECBAgAABAgQIECBAICUgbKfWZVgCBAgQIECAAAECBAgQIECAAAECBAgQELbdAAECBAgQIECAAAECBAgQIECAAAECBAikBITt1LoMS4AAAQIECBAgQIAAAQIECBAgQIAAAQLCthsgQIAAAQIECBAgQIAAAQIECBAgQIAAgZSAsJ1al2EJECBAgAABAgQIECBAgAABAgQIECBAQNh2AwQIECBAgAABAgQIECBAgAABAgQIECCQEhC2U+syLAECBAgQIECAAAECBAgQIECAAAECBAgI226AAAECBAgQIECAAAECBAgQIECAAAECBFICwnZqXYYlQIAAAQIECBAgQIAAAQIECBAgQIAAAWHbDRAgQIAAAQIECBAgQIAAAQIECBAgQIBASkDYTq3LsAQIEDNS+v0AACAASURBVCBAgAABAgQIECBAgAABAgQIECAgbLsBAgQIECBAgAABAgQIECBAgAABAgQIEEgJCNupdRmWAAECBAgQIECAAAECBAgQIECAAAECBIRtN0CAAAECBAgQIECAAAECBAgQIECAAAECKQFhO7UuwxIgQIAAAQIECBAgQIAAAQIECBAgQICAsO0GCBAgQIAAAQIECBAgQIAAAQIECBAgQCAlIGyn1mVYAgQIECBAgAABAgQIECBAgAABAgQIEBC23QABAgQIECBAgAABAgQIECBAgAABAgQIpASE7dS6DEuAAAECBAgQIECAAAECBAgQIECAAAECwrYbIECAAAECBAgQIECAAAECBAgQIECAAIGUgLCdWpdhCRAgQIAAAQIECBAgQIAAAQIECBAgQEDYdgMECBAgQIAAAQIECBAgQIAAAQIECBAgkBIQtlPrMiwBAgQIECBAgAABAgQIECBAgAABAgQICNtugAABAgQIECBAgAABAgQIECBAgAABAgRSAsJ2al2GJUCAAAECBAgQIECAAAECBAgQIECAAAFh2w0QIECAAAECBAgQIECAAAECBAgQIECAQEpA2E6ty7AECBAgQIAAAQIECBAgQIAAAQIECBAgIGy7AQIECBAgQIAAAQIECBAgQIAAAQIECBBICQjbqXUZlgABAgQIECBAgAABAgQIECBAgAABAgSEbTdAgAABAgQIECBAgAABAgQIECBAgAABAikBYTu1LsMSIECAAAECBAgQIECAAAECBAgQIECAgLDtBggQIECAAAECBAgQIECAAAECBAgQIEAgJSBsp9ZlWAIECBAgQIAAAQIECBAgQIAAAQIECBAQtt0AAQIECBAgQIAAAQIECBAgQIAAAQIECKQEhO3UugxLgAABAgQIECBAgAABAgQIECBAgAABAsK2GyBAgAABAgQIECBAgAABAgQIECBAgACBlICwnVqXYQkQIECAAAECBAgQIECAAAECBAgQIEBA2HYDBAgQIECAAAECBAgQIECAAAECBAgQIJASELZT6zIsAQIECBAgQIAAAQIECBAgQIAAAQIECAjbboAAAQIECBAgQIAAAQIECBAgQIAAAQIEUgLCdmpdhiVAgAABAgQIECBAgAABAgQIECBAgAABYdsNECBAgAABAgQIECBAgAABAgQIECBAgEBKQNhOrcuwBAgQIECAAAECBAgQIECAAAECBAgQICBsuwECBAgQIECAAAECBAgQIECAAAECBAgQSAkI26l1GZYAAQIECBAgQIAAAQIECBAgQIAAAQIEhG03QIAAAQIECBAgQIAAAQIECBAgQIAAAQIpAWE7tS7DEiBAgAABAgQIECBAgAABAgQIECBAgICw7QYIECBAgAABAgQIECBAgAABAgQIECBAICUgbKfWZVgCBAgQIECAAAECBAgQIECAAAECBAgQELbdAAECBAgQIECAAAECBAgQIECAAAECBAikBITt1LoMS4AAAQIECBAgQIAAAQIECBAgQIAAAQLCthsgQIAAAQIECBAgQIAAAQIECBAgQIAAgZSAsJ1al2EJECBAgAABAgQIECBAgAABAgQIECBAQNh2AwQIECBAgAABAgQIECBAgAABAgQIECCQEhC2U+syLAECBAgQIECAAAECBAgQIECAAAECBAgI226AAAECBAgQIECAAAECBAgQIECAAAECBFICwnZqXYYlQIAAAQIECBAgQIAAAQIECBAgQIAAAWHbDRAgQIAAAQIECBAgQIAAAQIECBAgQIBASkDYTq3LsAQIECBAgAABAgQIECBAgAABAgQIECAgbLsBAgQIECBAgAABAgQIECBAgAABAgQIEEgJCNupdRmWAAECBAgQIECAAAECBAgQIECAAAECBIRtN0CAAAECBAgQIECAAAECBAgQIECAAAECKQFhO7UuwxIgQIAAAQIECBAgQIAAAQIECBAgQICAsO0GCBAgQIAAAQIECBAgQIAAAQIECBAgQCAlIGyn1mVYAgQIECBAgAABAgQIECBAgAABAgQIEBC23QABAgQIECBAgAABAgQIECBAgAABAgQIpASE7dS6DEuAAAECBAgQIECAAAECBAgQIECAAAECwrYbIECAAAECBAgQIECAAAECBAgQIECAAIGUgLCdWpdhCRAgQIAAAQIECBAgQIAAAQIECBAgQEDYdgMECBAgQIAAAQIECBAgQIAAAQIECBAgkBIQtlPrMiwBAgQIECBAgAABAgQIECBAgAABAgQICNtugAABAgQIECBAgAABAgQIECBAgAABAgRSAsJ2al2GJUCAAAECBAgQIECAAAECBAgQIECAAAFh2w0QIECAAAECBAgQIECAAAECBAgQIECAQEpA2E6ty7AECBAgQIAAAQIECBAgQIAAAQIECBAgIGy7AQIECBAgQIAAAQIECBAgQIAAAQIECBBICQjbqXUZlgABAgQIECBAgAABAgQIECBAgAABAgSEbTdAgAABAgQIECBAgAABAgQIECBAgAABAikBYTu1LsMSIECAAAECBAgQIECAAAECBAgQIECAgLDtBggQIECAAAECBAgQIECAAAECBAgQIEAgJSBsp9ZlWAIECBAgQIAAAQIECBAgQIAAAQIECBAQtt0AAQIECBAgQIAAAQIECBAgQIAAAQIECKQEhO3UugxLgAABAgQIECBAgAABAgQIECBAgAABAsK2GyBAgAABAgQIECBAgAABAgQIECBAgACBlICwnVqXYQkQIECAAAECBAgQIECAAAECBAgQIEBA2HYDBAgQIECAAAECBAgQIECAAAECBAgQIJASELZT6zIsAQIECBAgQIAAAQIECBAgQIAAAQIECAjbboAAAQIECBAgQIAAAQIECBAgQIAAAQIEUgLCdmpdhiVAgAABAgQIECBAgAABAgQIECBAgAABYdsNECBAgAABAgQIECBAgAABAgQIECBAgEBKQNhOrcuwBAgQIECAAAECBAgQIECAAAECBAgQICBsuwECBAgQIECAAAECBAgQIECAAAECBAgQSAkI26l1GZYAAQIECBAgQIAAAQIECBAgQIAAAQIEhG03QIAAAQIECBAgQIAAAQIECBAgQIAAAQIpAWE7tS7DEiBAgAABAgQIECBAgAABAgQIECBAgICw7QYIECBAgAABAgQIECBAgAABAgQIECBAICUgbKfWZVgCBAgQIECAAAECBAgQIECAAAECBAgQELbdAAECBAgQIECAAAECBAgQIECAAAECBAikBITt1LoMS4AAAQIECBAgQIAAAQIECBAgQIAAAQLCthsgQIAAAQIECBAgQIAAAQIECBAgQIAAgZSAsJ1al2EJECBAgAABAgQIECBAgAABAgQIECBAQNh2AwQIECBAgAABAgQIECBAgAABAgQIECCQEhC2U+syLAECBAgQIECAAAECBAgQIECAAAECBAgI226AAAECBAgQIECAAAECBAgQIECAAAECBFICwnZqXYYlQIAAAQIECBAgQIAAAQIECBAgQIAAAWHbDRAgQIAAAQIECBAgQIAAAQIECBAgQIBASkDYTq3LsAQIECBAgAABAgQIECBAgAABAgQIECAgbLsBAgQIECBAgAABAgQIECBAgAABAgQIEEgJCNupdRmWAAECBAgQIECAAAECBAgQIECAAAECBIRtN0CAAAECBAgQIECAAAECBAgQIECAAAECKQFhO7UuwxIgQIAAAQIECBAgQIAAAQIECBAgQICAsO0GCBAgQIAAAQIECBAgQIAAAQIECBAgQCAlIGyn1mVYAgQIECBAgAABAgQIECBAgAABAgQIEBC23QABAgQIECBAgAABAgQIECBAgAABAgQIpASE7dS6DEuAAAECBAgQIECAAAECBAgQIECAAAECwrYbIECAAAECBAgQIECAAAECBAgQIECAAIGUgLCdWpdhCRAgQIAAAQIECBAgQIAAAQIECBAgQEDYdgMECBAgQIAAAQIECBAgQIAAAQIECBAgkBIQtlPrMiwBAgQIECBAgAABAgQIECBAgAABAgQICNtugAABAgQIECBAgAABAgQIECBAgAABAgRSAsJ2al2GJUCAAAECBAgQIECAAAECBAgQIECAAAFh2w0QIECAAAECBAgQIECAAAECBAgQIECAQEpA2E6ty7AECBAgQIAAAQIECBAgQIAAAQIECBAgIGy7AQIECBAgQIAAAQIECBAgQIAAAQIECBBICQjbqXUZlgABAgQIECBAgAABAgQIECBAgAABAgSEbTdAgAABAgQIECBAgAABAgQIECBAgAABAikBYTu1LsMSIECAAAECBAgQIECAAAECBAgQIECAgLDtBggQIECAAAECBAgQIECAAAECBAgQIEAgJSBsp9ZlWAIECBAgQIAAAQIECBAgQIAAAQIECBAQtt0AAQIECBAgQIAAAQIECBAgQIAAAQIECKQEhO3UugxLgAABAgQIECBAgAABAgQIECBAgAABAsK2GyBAgAABAgQIECBAgAABAgQIECBAgACBlICwnVqXYQkQIECAAAECBAgQIECAAAECBAgQIEBA2HYDBAgQIECAAAECBAgQIECAAAECBAgQIJASELZT6zIsAQIECBAgQIAAAQIECBAgQIAAAQIECAjbboAAAQIECBAgQIAAAQIECBAgQIAAAQIEUgLCdmpdhiVAgAABAgQIECBAgAABAgQIECBAgAABYdsNECBAgAABAgQIECBAgAABAgQIECBAgEBKQNhOrcuwBAgQIECAAAECBAgQIECAAAECBAgQICBsuwECBAgQIECAAAECBAgQIECAAAECBAgQSAkI26l1GZYAAQIECBAgQIAAAQIECBAgQIAAAQIEhG03QIAAAQIECBAgQIAAAQIECBAgQIAAAQIpAWE7tS7DEiBAgAABAgQIECBAgAABAgQIECBAgICw7QYIECBAgAABAgQIECBAgAABAgQIECBAICUgbKfWZVgCBAgQIECAAAECBAgQIECAAAECBAgQELbdAAECBAgQIECAAAECBAgQIECAAAECBAikBITt1LoMS4AAAQIECBAgQIAAAQIECBAgQIAAAQLCthsgQIAAAQIECBAgQIAAAQIECBAgQIAAgZSAsJ1al2EJECBAgAABAgQIECBAgAABAgQIECBAQNh2AwQIECBAgAABAgQIECBAgAABAgQIECCQEhC2U+syLAECBAgQIECAAAECBAgQIECAAAECBAgI226AAAECBAgQIECAAAECBAgQIECAAAECBFICwnZqXYYlQIAAAQIECBAgQIAAAQIECBAgQIAAAWHbDRAgQIAAAQIECBAgQIAAAQIECBAgQIBASkDYTq3LsAQIECBAgAABAgQIECBAgAABAgQIECAgbLsBAgQIECBAgAABAgQIECBAgAABAgQIEEgJCNupdRmWAAECBAgQIECAAAECBAgQIECAAAECBIRtN0CAAAECBAgQIECAAAECBAgQIECAAAECKQFhO7UuwxIgQIAAAQIECBAgQIAAAQIECBAgQICAsO0GCBAgQIAAAQIECBAgQIAAAQIECBAgQCAlIGyn1mVYAgQIECBAgAABAgQIECBAgAABAgQIEBC23QABAgQIECBAgAABAgQIECBAgAABAgQIpASE7dS6DEuAAAECBAgQIECAAAECBAgQIECAAAECwrYbIECAAAECBAgQIECAAAECBAgQIECAAIGUgLCdWpdhCRAgQIAAAQIECBAgQIAAAQIECBAgQEDYdgMECBAgQIAAAQIECBAgQIAAAQIECBAgkBIQtlPrMiwBAgQIECBAgAABAgQIECBAgAABAgQICNtugAABAgQIECBAgAABAgQIECBAgAABAgRSAsJ2al2GJUCAAAECBAgQIECAAAECBAgQIECAAAFh2w0QIECAAAECBAgQIECAAAECBAgQIECAQEpA2E6ty7AECBAgQIAAAQIECBAgQIAAAQIECBAgIGy7AQIECBAgQIAAAQIECBAgQIAAAQIECBBICQjbqXUZlgABAgQIECBAgAABAgQIECBAgAABAgSEbTdAgAABAgQIECBAgAABAgQIECBAgAABAikBYTu1LsMSIECAAAECBAgQIECAAAECBAgQIECAgLDtBggQIECAAAECBAgQIECAAAECBAgQIEAgJSBsp9ZlWAIECBAgQIAAAQIECBAgQIAAAQIECBAQtt0AAQIECBAgQIAAAQIECBAgQIAAAQIECKQEhO3UugxLgAABAgQIECBAgAABAgQIECBAgAABAsK2GyBAgAABAgQIECBAgAABAgQIECBAgACBlICwnVqXYQkQIECAAAECBAgQIECAAAECBAgQIEBA2HYDBAgQIECAAAECBAgQIECAAAECBAgQIJASELZT6zIsAQIECBAgQIAAAQIECBAgQIAAAQIECAjbboAAAQIECBAgQIAAAQIECBAgQIAAAQIEUgLCdmpdhiVAgAABAgQIECBAgAABAgQIECBAgAABYdsNECBAgAABAgQIECBAgAABAgQIECBAgEBKQNhOrcuwBAgQIECAAAECBAgQIECAAAECBAgQICBsuwECBAgQIECAAAECBAgQIECAAAECBAgQSAkI26l1GZYAAQIECBAgQIAAAQIECBAgQIAAAQIEhG03QIAAAQIECBAgQIAAAQIECBAgQIAAAQIpAWE7tS7DEiBAgAABAgQIECBAgAABAgQIECBAgICw7QYIECBAgAABAgQIECBAgAABAgQIECBAICUgbKfWZVgCBAgQIECAAAECBAgQIECAAAECBAgQELbdAAECBAgQIECAAAECBAgQIECAAAECBAikBITt1LoMS4AAAQIECBAgQIAAAQIECBAgQIAAAQLCthsgQIAAAQIECBAgQIAAAQIECBAgQIAAgZSAsJ1al2EJECBAgAABAgQIECBAgAABAgQIECBAQNh2AwQIECBAgAABAgQIECBAgAABAgQIECCQEhC2U+syLAECBAgQIECAAAECBAgQIECAAAECBAgI226AAAECBAgQIECAAAECBAgQIECAAAECBFICwnZqXYYlQIAAAQIECBAgQIAAAQIECBAgQIAAAWHbDRAgQIAAAQIECBAgQIAAAQIECBAgQIBASkDYTq3LsAQIECBAgAABAgQIECBAgAABAgQIECAgbLsBAgQIECBAgAABAgQIECBAgAABAgQIEEgJCNupdRmWAAECBAgQIECAAAECBAgQIECAAAECBIRtN0CAAAECBAgQIECAAAECBAgQIECAAAECKQFhO7UuwxIgQIAAAQIECBAgQIAAAQIECBAgQICAsO0GCBAgQIAAAQIECBAgQIAAAQIECBAgQCAlIGyn1mVYAgQIECBAgAABAgQIECBAgAABAgQIEBC23QABAgQIECBAgAABAgQIECBAgAABAgQIpASE7dS6DEuAAAECBAgQIECAAAECBAgQIECAAAECwrYbIECAAAECBAgQIECAAAECBAgQIECAAIGUgLCdWpdhCRAgQIAAAQIECBAgQIAAAQIECBAgQEDYdgMECBAgQIAAAQIECBAgQIAAAQIECBAgkBIQtlPrMiwBAgQIECBAgAABAgQIECBAgAABAgQICNtugAABAgQIECBAgAABAgQIECBAgAABAgRSAsJ2al2GJUCAAAECBAgQIECAAAECBAgQIECAAAFh2w0QIECAAAECBAgQIECAAAECBAgQIECAQEpA2E6ty7AECBAgQIAAAQIECBAgQIAAAQIECBAgIGy7AQIECBAgQIAAAQIECBAgQIAAAQIECBBICQjbqXUZlgABAgQIECBAgAABAgQIECBAgAABAgSEbTdAgAABAgQIECBAgAABAgQIECBAgAABAikBYTu1LsMSIECAAAECBAgQIECAAAECBAgQIECAgLDtBggQIECAAAECBAgQIECAAAECBAgQIEAgJSBsp9ZlWAIECBAgQIAAAQIECBAgQIAAAQIECBAQtt0AAQIECBAgQIAAAQIECBAgQIAAAQIECKQEhO3UugxLgAABAgQIECBAgAABAgQIECBAgAABAsK2GyBAgAABAgQIECBAgAABAgQIECBAgACBlICwnVqXYQkQIECAAAECBAgQIECAAAECBAgQIEBA2HYDBAgQIECAAAECBAgQIECAAAECBAgQIJASELZT6zIsAQIECBAgQIAAAQIECBAgQIAAAQIECAjbboAAAQIECBAgQIAAAQIECBAgQIAAAQIEUgLCdmpdhiVAgAABAgQIECBAgAABAgQIECBAgAABYdsNECBAgAABAgQIECBAgAABAgQIECBAgEBKQNhOrcuwBAgQIECAAAECBAgQIECAAAECBAgQICBsuwECBAgQIECAAAECBAgQIECAAAECBAgQSAkI26l1GZYAAQIECBAgQIAAAQIECBAgQIAAAQIEhG03QIAAAQIECBAgQIAAAQIECBAgQIAAAQIpAWE7tS7DEiBAgAABAgQIECBAgAABAgQIECBAgICw7QYIECBAgAABAgQIECBAgAABAgQIECBAICUgbKfWZVgCBAgQIECAAAECBAgQIECAAAECBAgQELbdAAECBAgQIECAAAECBAgQIECAAAECBAikBITt1LoMS4AAAQIECBAgQIAAAQIECBAgQIAAAQLCthsgQIAAAQIECBAgQIAAAQIECBAgQIAAgZSAsJ1al2EJECBAgAABAgQIECBAgAABAgQIECBAQNh2AwQIECBAgAABAgQIECBAgAABAgQIECCQEhC2U+syLAECBAgQIECAAAECBAgQIECAAAECBAgI226AAAECBAgQIECAAAECBAgQIECAAAECBFICwnZqXYYlQIAAAQIECBAgQIAAAQIECBAgQIAAAWHbDRAgQIAAAQIECBAgQIAAAQIECBAgQIBASkDYTq3LsAQIECBAgAABAgQIECBAgAABAgQIECAgbLsBAgQIECBAgAABAgQIECBAgAABAgQIEEgJCNupdRmWAAECBAgQIECAAAECBAgQIECAAAECBIRtN0CAAAECBAgQIECAAAECBAgQIECAAAECKQFhO7UuwxIgQIAAAQIECBAgQIAAAQIECBAgQICAsO0GCBAgQIAAAQIECBAgQIAAAQIECBAgQCAlIGyn1mVYAgQIECBAgAABAgQIECBAgAABAgQIEBC23QABAgQIECBAgAABAgQIECBAgAABAgQIpASE7dS6DEuAAAECBAgQIECAAAECBAgQIECAAAECwrYbIECAAAECBAgQIECAAAECBAgQIECAAIGUgLCdWpdhCRAgQIAAAQIECBAgQIAAAQIECBAgQEDYdgMECBAgQIAAAQIECBAgQIAAAQIECBAgkBIQtlPrMiwBAgQIECBAgAABAgQIECBAgAABAgQICNtugAABAgQIECBAgAABAgQIECBAgAABAgRSAsJ2al2GJUCAAAECBAgQIECAAAECBAgQIECAAAFh2w0QIECAAAECBAgQIECAAAECBAgQIECAQEpA2E6ty7AECBAgQIAAAQIECBAgQIAAAQIECBAgIGy7AQIECBAgQIAAAQIECBAgQIAAAQIECBBICQjbqXUZlgABAgQIECBAgAABAgQIECBAgAABAgSEbTdAgAABAgQIECBAgAABAgQIECBAgAABAikBYTu1LsMSIECAAAECBAgQIECAAAECBAgQIECAgLDtBggQIECAAAECBAgQIECAAAECBAgQIEAgJSBsp9ZlWAIECBAgQIAAAQIECBAgQIAAAQIECBAQtt0AAQIECBAgQIAAAQIECBAgQIAAAQIECKQEhO3UugxLgAABAgQIECBAgAABAgQIECBAgAABAsK2GyBAgAABAgQIECBAgAABAgQIECBAgACBlICwnVqXYQkQIECAAAECBAgQIECAAAECBAgQIEBA2HYDBAgQIECAAAECBAgQIECAAAECBAgQIJASELZT6zIsAQIECBAgQIAAAQIECBAgQIAAAQIECAjbboAAAQIECBAgQIAAAQIECBAgQIAAAQIEUgLCdmpdhiVAgAABAgQIECBAgAABAgQIECBAgAABYdsNECBAgAABAgQIECBAgAABAgQIECBAgEBKQNhOrcuwBAgQIECAAAECBAgQIECAAAECBAgQICBsuwECBAgQIECAAAECBAgQIECAAAECBAgQSAkI26l1GZYAAQIECBAgQIAAAQIECBAgQIAAAQIEhG03QIAAAQIECBAgQIAAAQIECBAgQIAAAQIpAWE7tS7DEiBAgAABAgQIECBAgAABAgQIECBAgICw7QYIECBAgAABAgQIECBAgAABAgQIECBAICUgbKfWZVgCBAgQIECAAAECBAgQIECAAAECBAgQELbdAAECBAgQIECAAAECBAgQIECAAAECBAikBITt1LoMS4AAAQIECBAgQIAAAQIECBAgQIAAAQLCthsgQIAAAQIECBAgQIAAAQIECBAgQIAAgZSAsJ1al2EJECBAgAABAgQIECBAgAABAgQIECBAQNh2AwQIECBAgAABAgQIECBAgAABAgQIECCQEhC2U+syLAECBAgQIECAAAECBAgQIECAAAECBAgI226AAAECBAgQIECAAAECBAgQIECAAAECBFICwnZqXYYlQIAAAQIECBAgQIAAAQIECBAgQIAAAWHbDRAgQIAAAQIECBAgQIAAAQIECBAgQIBASkDYTq3LsAQIECBAgAABAgQIECBAgAABAgQIECAgbLsBAgQIECBAgAABAgQIECBAgAABAgQIEEgJCNupdRmWAAECBAgQIECAAAECBAgQIECAAAECBIRtN0CAAAECBAgQIECAAAECBAgQIECAAAECKQFhO7UuwxIgQIAAAQIECBAgQIAAAQIECBAgQICAsO0GCBAgQIAAAQIECBAgQIAAAQIECBAgQCAlIGyn1mVYAgQIECBAgAABAgQIECBAgAABzP6fVgAAIABJREFUAgQIEBC23QABAgQIECBAgAABAgQIECBAgAABAgQIpASE7dS6DEuAAAECBAgQIECAAAECBAgQIECAAAECwrYbIECAAAECBAgQIECAAAECBAgQIECAAIGUgLCdWpdhCRAgQIAAAQIECBAgQIAAAQIECBAgQEDYdgMECBAgQIAAAQIECBAgQIAAAQIECBAgkBIQtlPrMiwBAgQIECBAgAABAgQIECBAgAABAgQICNtugAABAgQIECBAgAABAgQIECBAgAABAgRSAsJ2al2GJUCAAAECBAgQIECAAAECBAgQIECAAAFh2w0QIECAAAECBAgQIECAAAECBAgQIECAQEpA2E6ty7AECBAgQIAAAQIECBAgQIAAAQIECBAgIGy7AQIECBAgQIAAAQIECBAgQIAAAQIECBBICQjbqXUZlgABAgQIECBAgAABAgQIECBAgAABAgSEbTdAgAABAgQIECBAgAABAgQIECBAgAABAikBYTu1LsMSIECAAAECBAgQIECAAAECBAgQIECAgLDtBggQIECAAAECBAgQIECAAAECBAgQIEAgJSBsp9ZlWAIECBAgQIAAAQIECBAgQIAAAQIECBAQtt0AAQIECBAgQIAAAQIECBAgQIAAAQIECKQEhO3UugxLgAABAgQIECBAgAABAgQIECBAgAABAsK2GyBAgAABAgQIbJHAm2++Od56663x+uuvH+pUv/rVr8Y777wzLl68eKjP68kIECBAgAABAgQIECCwjoCwvY6axxAgQIAAAQIE7pGAsH2PYP2xBAgQIECAAAECBAgcKQFh+0it0ztDgAABAgQI1AWE7foGzU+AAAECBAgQIECAwGEICNuHoew5CBAgQIAAAQK3KSBs3yaUNyNAgAABAgQIECBA4FgLCNvHev3eeQIECBAgQGDbBITtbduIeQgQIECAAAECBAgQ2EYBYXsbt2ImAgQIECBA4NgKCNvHdvXecQIECBAgQIAAAQIE7kBA2L4DLG9KgAABAgQIELjXAsL2vRb25xMgQIAAAQIECBAgcBQEhO2jsEXvAwECBAgQIHBkBITtI7NK7wgBAgQIECBAgAABAvdQQNi+h7j+aAIECBAgQIDAnQoI23cq5u0JECBAgAABAgQIEDiOAsL2cdy695kAAQIECBDYWgFhe2tXYzACBAgQIECAAAECBLZIQNjeomUYhQABAgQIECAgbLsBAgQIECBAgAABAgQIHCwgbB9s5C0IECBAgAABAocmIGwfGrUnIkCAAAECBAgQIEAgLCBsh5dndAIECBAgQODoCQjbR2+n3iMCBAgQIECAAAECBO6+gLB99039iQQIECBAgACBtQWE7bXpPJAAAQIECBAgQIAAgWMkIGwfo2V7VwkQIECAAIHtFxC2t39HJiRAgAABAgQIECBAYPMCwvbmd2ACAgQIECBAgMBNAWHbMRAgQIAAAQIECBAgQOBgAWH7YCNvQYAAAQIECBA4NAFh+9CoPREBAgQIECBAgAABAmEBYTu8PKMTIECAAAECR09A2D56O/UeESBAgAABAgQIECBw9wWE7btv6k8kQIAAAQIECKwtIGyvTeeBBAgQIECAAAECBAgcIwFh+xgt27tKgAABAgQIbL+AsL39OzIhAQIECBAgQIAAAQKbFxC2N78DExAgQIAAAQIEbgoI246BAAECBAgQIECAAAECBwsI2wcbeQsCBAgQIECAwKEJCNuHRu2JCBAgQIAAAQIECBAICwjb4eUZnQABAgQIEDh6AsL20dup94gAAQIECBAgQIAAgbsvIGzffVN/IgECBAgQIEBgbQFhe206DyRAgAABAgQIECBA4BgJCNvHaNneVQIECBAgQGD7BYTt7d+RCQkQIECAAAECBAgQ2LyAsL35HZiAAAECBAgQIHBTQNh2DAQIECBAgAABAgQIEDhYQNg+2MhbECBAgAABAgQOTUDYPjRqT0SAAAECBAgQIECAQFhA2A4vz+gECBAgQIDA0RMQto/eTr1HBAgQIECAAAECBAjcfQFh++6b+hMJECBAgAABAmsLCNtr03kgAQIECBAgQIAAAQLHSEDYPkbL9q4SIECAAAEC2y8gbG//jkxIgAABAgQIECBAgMDmBYTtze/ABAQIECBAgACBmwLCtmMgQIAAAQIECBAgQIDAwQLC9sFG3oIAAQIECBAgcGgCwvahUXsiAgQIECBAgAABAgTCAsJ2eHlGJ0CAAAECBI6egLB99HbqPSJAgAABAgQIECBA4O4LCNt339SfSIAAAQIECBBYW0DYXpvOAwkQIECAAAECBAgQOEYCwvYxWrZ3lQABAgQIENh+AWF7+3dkQgIECBAgQIAAAQIENi8gbG9+ByYgQIAAAQIECNwUELYdAwECBAgQIECAAAECBA4WELYPNvIWBAgQIECAAIFDE3jjjTfGL3/5y/HTn/700J5zPtHbb789fv3rX4+LFy8e6vN6MgIECBAgQIAAAQIECKwjIGyvo+YxBAgQIECAAIF7JHD+/Pnxi1/8Ypw7d+4ePcP//8f+5je/Ge++++64evXqoT6vJyNAgAABAgQIECBAgMA6AsL2OmoeQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIbExC2N0bviQkQIECAAAECBAgQIECAAAECBAgQIEBgHQFhex01jyFAgAABAgQIECBAgAABAgQIECBAgACBjQkI2xuj98QECBAgQIAAAQIECBAgQIAAAQIECBAgsI6AsL2OmscQIECAAAECBAgQIECAAAECBAgQIECAwMYEhO2N0XtiAgQIECBAgAABAgQIECBAgAABAgQIEFhHQNheR81jCBAgQIAAAQIECBAgQIAAAQIECBAgQGBjAsL2xug9MQECBAgQIECAAAECBAgQIECAAAECBAisIyBsr6PmMQQIECBAgAABAgQIECBAgAABAgQIECCwMQFhe2P0npgAAQIECBAgQIAAAQIECBAgQIAAAQIE1hEQttdR8xgCBAgQIECAAAECBAgQIECAAAECBAgQ2JiAsL0xek9MgAABAgQIECBAgAABAgQIECBAgAABAusICNvrqHkMAQIECBAgQIAAAQIECBAgQIAAAQIECGxMQNjeGL0nJkCAAAECBAgQIECAAAECBAgQIECAAIF1BITtddQ8hgABAgQIECBAgAABAgQIECBAgAABAgQ2JiBsb4zeExMgQIAAAQIECBAgQIAAAQIECBAgQIDAOgLC9jpqHkOAAAECBAgQIECAAAECBAgQIECAAAECGxMQtjdG74kJECBAgAABAgQIECBAgAABAgQIECBAYB0BYXsdNY8hQIAAAQIECBAgQIAAAQIECBAgQIAAgY0JCNsbo/fEBAgQIECAAAECBAgQIECAAAECBAgQILCOgLC9jprHECBAgAABAgQIECBAgAABAgQIECBAgMDGBITtjdF7YgIECBAgQIAAAQIECBAgQIAAAQIECBBYR0DYXkfNYwgQIECAAAECBAgQIECAAAECBAgQIEBgYwLC9sboPTEBAgQIECBAgAABAgQIECBAgAABAgQIrCMgbK+j5jEECBAgQIAAAQIECBAgQIAAAQIECBAgsDEBYXtj9J6YAAECBAgQIECAAAECBAgQIECAAAECBNYRELbXUfMYAgS2QmBnZ2fMl9WPW39948aNMV/mj9XrH/58K94JQxw7gdu929W93nq/xw7LO0yAQEbg331s++Hn4ls/P2feOYMSOEBg/nN0/5+k+/8uXX79/WPmv0a//yfp/Nny83/+Gi2BpsDqS7DV3d/89XLl+/+z/5XY/i9uvXn339y5qQlsq4Cwva2bMReBYyqwu7s75suJEyeWaL369Xy9+qJ5vj5z5sx46KGHxv3337+8zfy9++67b5w+fXqR++6778bly5fH3t7euHTp0rh27dryevV78wvr69evL9H71tfz7eev54sfBO5E4NbbXd3kD+933vUDDzyw3O6pU6f+z91euXJludN5f/Nmv/nmm+Vl/vyHt7q6Uzd7J1vytgQIrCNw6+fj+XFsvtz6cW7+en4Onh/b5ufnkydPjrNnzy4f5+bHqvlxbX58my8XLlwY33777fL5efVx7NbX8/fni2/qrbMpj7nbAru7Y5zYnf8enS/j+5edsTvD9e78DyzGOHVqZzz0wInxwP3z78bOOLE7xtmzu8vvzx+XLl8fly/vh70rV66PCxf3xrff7Y29a2NcXyL3/LfoGNev31h+vbzeG2Pv+r/GwLv9vvnzCNwqMG95/9bnne+Mnfl6+YbN/u/dfD3GOH16d5w9s7Pc+33Lre+O06d3xtkz8+u4Ma5duzEuXbo+ru3tP8Pl+Xfgyvy6a//vw3yZN7//TZ5/frNn9evl78P8/eX1/t8JMdy9EiDw7wSEbbdBgMBGBFbxen7xu/oieUbpp556avzoRz8azz333HjyySeXX8/X586dG48++uh4+OGHl5f5hfOt/7X27b4TM2x/9dVX4/PPPx9ffPHF+PLLL5fXn3322fjggw/G+++/P957773l/59fgK++8J5hcfVzX2zfrvbRfLtV4Jm3u7rf+fqJJ54YL7744njhhRfGM888M55++unl9bzhRx55ZLnbGX3m297Jj3l7Fy9eHF9//fVyu5988sn46KOPbr787W9/G3/961+Xm7169epyp/Mx82X1TZs7eT5vS4AAgdXn5dXHufmxa35ePn/+/PI5en6se/7555ePe/Nz8+OPP76E7Nv9MT9WzY9r8+PWxx9/vLx8+OGHy+ffv/zlL//yMW318Wz1Db7bfQ5vR+BOBJaod2JnnJxhennZj3dPnzs1zr9wdjz/7Nnx30+fHk8/eWo889Tpce6J0+ORh0+Oh//rxLj//t0lCN7Jj6tXbyyB++t/XBtffHltfPLplfHRp1fGx59eHR98fHm89/6l8d77l8dXX8/P5zfG3t6NcW1vvh7Lz/0gsK7A6ps1qzufEXv+/NFHTo4Xnjsznn/2zHjq3OnxxGOnxuOPnhyPP3Zq/+ePnRyPP3pq+QbO/DP+kx8zcl/45vq4cOHa+Mc3e+PC9y//8/X+34fPPr86Pvnsyvj071e/f7myvM38uzCj9/ymz+qbP/P1EsH9tfhPVuKxBNICwnZ6fYYn0BCYIXB+kTz/y635RfKM0s8+++x47bXXxo9//OPx6quvLl8szy+S5xfP2/Bj/hdlM3TPL7L//Oc/jz/+8Y/j97///RK+539ptgrdq9fbMLMZ7o3AvN9V3Jk3/OCDD46XX355vP766+MnP/nJcr8vvfTSEnfmN2w28WMG7PlNmhm4//SnP43f/e534913310C0bzlGZFWccg3ZjaxIc9JYLsF5seu+XFufoybgXqG65///OfjZz/72fLyyiuvHOrn5/kxbcbuP/zhD8vHs9/+9rfLx7T5Tej58Wz1Tbz/Ze86wKsouvZLeiMJhBAICQlFmiiIgBRBOggqgnwiRRAQRHrvoUPovYpIkQ4iFgRRaaKAIF2kCIT0Cuk94X/OxPgrguSSvTu7e895vvtEP2fnzLxn7u7dd868h+9n2l5XWh4d8dDWNkVga0PP+DxSr0pFJ9R8zhk1nnVGlWcc4V/WHm5Fbf6UGFF/NkRgR8dk4eaddFy9nooLV1Jw/nIK7oamIzWVTnY9QNafpDeTeurHRy8e8zdsKK+CThHQxkzVSk6oUtERFcs7io2biuUc4elBCRumbdCoiUFySg4io7MQHJaB20HpuH03Xfy9FZSO8MhMpKTmik2f/A9tBPEBXDUjxL4YAXkIMLEtD3v2zAgYFoG/E9n0kkyEHxGADRo0QJ06dQSZ7eHhIY0ELAzw8fHxuHHjhiC56WX7119/xe3bt0XmWT5xyEeoC4OwNq7N34ih9UtrtUaNGmjcuDEaNmwoiGxXV1dtDPQJo0hMTBRE908//YTjx4+LNUsnFP5ODOliIjxIRoARUBwBelbTPY5OS9Hpknr16qFp06biXkcbzaaeLlF8gA91SM/Zq1evinvZoUOHcPnyZXGSJf90FZPc5o6A/vsngs/OltZ9EZT2skPtmi5oXN8V9WoXFcSek6OczWlTkY29l4VLv6Xip18ScfxkoiC9E5NykJWVK4huJvNMRdRY7SnHwtYmTwrH2ckKFco5oE7NoqhbywV1XigKPx97IRtiJCNpEyK3r91Mw5XfU3Hpaor4S/8f/bf8Ew/5Gd9GmjvPhRFgBPJqWvChDV4JjAAjUGgE8l+Q6SWZsq6JyG7evDmaNGmCatWqiSxXIxq9SEdFReHcuXM4deoUTp48KYhEIhSZPNRXxPPJbMpWpAzsFi1aoFWrViIz293dXV+TecxoaWOG1uq3336L77//XpxIID36/OxHQ0zSAJOg+2i+TrGa08mvQ2ApBOHDtRnUwjq/9gN972RY/skpkkt65ZVX0LFjR7HxrKf7HK1ROkH1ww8/YO/evWKTOZ/k5hoZMlaVdn2StIitXRE4O1rh2SpOaNW0GFo1cUe1Sk5CQsQIRlndp35NwsEf7uPYzwkIjchERuYDQXQzyW2ECD95DiSFQ0Q2EdYkJdKkoRtaN3NH3ReKCvkQSQcKnzxwM7agUwy0CURk96XfUvDrxWScvZAsvh+kdZ9/4oG/I2YMAnfNCKiEABPbKgHNbhgBIyKQL9FA2V6kgf3yyy/j1VdfRaNGjUT2lyxZBtlYkyQEHZn+7rvvcPjwYSEP8Xfy0FJII9lxKIj//A0ZIrOrVKmCtm3b4rXXXhMyOaboxRbEl9bakN48nTzYv3+/+JDkTv465TUqN1q0oTJkyBCxDtW0RYsWYf369UJuyRKMThNNnDhRELtqGmUdz58/H0ePHlXNbf69jjaZKTO7W7duYvOOnt16N9ooIEw/++wz7Nu3Tzxz84tG631uPP6nQ0BkrNpaCTL7heec8Vrr4ni1eTEhuaBlqYWnm+0/ryIdYiLvvjx4D4eOxiM4NEMU7SMSj+VKlEBYO32QnA6tc0dHK1Sq4IAWjd3RtkUx1KjuDBdna+0MVEMjoRMNpF9/+tdk/HwmEad/TRLSJlToknTv6b/z90RDAeOhMAIFRICJ7QICxc0YAUbg/xGgzFYis4kUqF+/viAF6PgyFclj+ycClJF37do1kVVGWbJEJFLWLB+dlrtS8tcwZSw2a9YMXbp0Qd26dQ17suBJaCcnJ4vTBtu3bxdkGxWozF+jT7qW/7vyCLz55puYPHmyOC2gpgUEBGDx4sVCWskSjAoeLly4ED169FB1uhcuXMCECRNw4MABs/slQpue13Sva9OmDXr27CnWFZ0IMKLFxMSIjbpNmzaJDeZ8mTAjzpXn9G8EKDvb3r4I/Ms6oG3zYnj7zRJ4rpoT7O2MkZltaszj7mfjyIkEbN8bg1Nnk3DvfjYyOYvbVBg1154kdeztiohipm1bFkfXtzzxfDUnONhb5jovTIBIouRuaAZ+PJWIQ0fiBdFNhSszieTOIlkfFjcoDL58LSOgFgJMbKuFNPthBHSOQH52NmWxUhZhp06d0KFDB/j7+1tsZrapIaUs2JCQEJHJvWfPHvHSTZIlTCCaiuTTt6cj+LSGSR6HyOy33noLvr6+T9+hAa+k4/20Ponkvn79Omc+SogxE9vqgG5kYjuf0KYNZ7rPvf/++6hcubLFPK+paC4R3GvWrBHyS3QKgTK72YyHAL3M2tgWEfrYtWsWxXvveKJNs2Io4WFrvMk+5YwoA5V0uLfsjsG+A3F/ZnHnFdlj0wcCf1/nlJH9TgdPvNG6uCC32ZRD4H5CNs6cT8ahw/dx+EQCgoIzkJaeK4huJrmVw5l7YgSURoCJbaUR5f4YAYMhkH98mQrokV42ZbaRFqdeiudpNRz0gk2k4Zdffik+lNVNL96U4c36oMpHjQhtZ2dnccKgV69e4gg+nThgezwCJKlDGaWffPKJ0K/lzEf1VgsT2+pgbVRimzTavby8xOZz3759xWa0pUqD0Qmp3bt3C4Kb5EpIgomlltT5fqnhhTSF3V1t0KyRG97v7oX6dVxFsTy2xyMQGp6Jz7+Jw6e7YgTZTRIMOZyVquklI9a5mw2aN3JDn25eotgpS42YP2Skw33paiq+OBCH/d/dx+276X9JlvBXxvz4swdGwBQEmNg2BS1uywhYEAL52V6kv9muXTuR7VWjRg0QQcimLAKkBUrEIWXIklxJeHi4ePnm7LLC40ySI05OTkJT9sMPPxSENhU3ZSs4AvmZj2vXrhWZj0Rw89osOH5P05KJ7adBzfRrjEZsE3lNG3h0nxs+fLi47xHJzQahu03k9s6dOxEREYHs7GyGRccIkE62a1FrNG/sjoG9S6Peiy6wZxkGkyJKBSd3fRGLDdujRHG99AwuNGkSgCo0pnXu7mqNlk2K4cNepVC3lovFyuqoAPd/ukhOyRG63Hu+isUPPyYgNCwDGfSd4UMPskPD/hkBgQAT27wQGAFG4B8I5GdoU/FHyvbq06cPqlatarHZXmovD5KB2Lt3L3bs2CGyuKmYH72Ac4aZaZEggoc0ZGvXri0IbSpq6u7ublon3PofCFDmIxVmI4L74sWLYvOFTxeYZ5EwsW0eXB/u1UjENuloU2b2oEGDxLO7WLFi6oCoIy+0IUebxwsWLMCpU6eEzBI/W3UUQADWVkXg5GSFJg1dMaiPNxrVd4WjA2doFyaK4ZGZ2LInBpt3RuOP2+lCg5uL5xUG0cJfS8VPHR2t8fJLRTG0nzeaNHTjdV54WBXrISYuC8vWRWDd5khExWQp1i93xAgwAk+PABPbT48dX8kIGA4ByuwieYY33ngDAwYMwPPPP8+EtqQok/b24cOHhQzE8ePHkZCQIGkk+nNLBI+fn5/YlHn33XdBmzRsyiFAOvEff/wxtm7dKjTjST6HySHl8KWemNhWFs/H9WYEYps28egUyttvv41hw4aJ+gFs/40AbSAvXboU27ZtAxWb5A067a+YIkUAOzsrVK/ihIG9S+HNth4o5s4nCJWM3G/XU7Hy4wh8cfCeKJ6Xnc2pqEriW9C+7OyKoFJ5RwzoXRpvty8Bj+K8zguKnZrtNmyLxuwlofjjTpqabtkXI8AIPAYBJrZ5aTACjABIrsHFxQUtW7bEkCFD8NJLL4HIQTb5CAQFBWHs2LHYtWuX/MFofARE8Li5uYlsRVrHzz33HG/MmClmRGSfPXsWixcvxsGDB8XGC5NDyoHNxLZyWP5XT3ontmkzumLFioLQJmKbT6UUfN1QTYvNmzdj+fLluHHjBkuTFBw61VtaWxeBl6ct3n27JD7o4YVyfg6qj8FSHGZlP8APx+OxZE04TpxOQlpaDkstqBR8ytJ2LWqDju08MPSD0niuqjNoQ4dNmwgwsa3NuPCoLBcBJrYtN/Y8c0YA+Tra1atXx+DBg0WWIBGDbNpBgDLLxowZw8T2E0JCGzGUqUi6skRss462OmuY9Le3bNmCVatWCXIoMzNTHccG98LEtjoB1jOxTVJLrVu3FhufpKVtqcUhC7NSaIPu0KFDCAwMxM8//yxOn7BpBwEi9eztrFDnBReMHlgGrZq6s462SuEheYWPNkViw45ohIRlcPa2mXEnLe2K5RwwvL83Or9ZAm6unKVtZsgL3T0T24WGkDtgBBRFgIltReHkzhgB/SBAWdqenp7o2rWr0CCmrC827SHAxPZ/xyT/GP5bb70lshZpk4Y2bNjURYCKn5Ju7YEDB0BkN2dvFw5/JrYLh19Br9YjsU33N9q4e++998TJlAoVKhR0utzuMQicOXMGs2bNEvrbpLvNJh8BK6siKFnCFr26lMQHPUvBz9de/qAsbAQ5OZS9nYAFq8Lw0+lEpKWz9rY5lgBJj7Ro7I7xQ33QoK4rKHObTfsIMLGt/RjxCC0LASa2LSvePFtGQCBA2a116tQRmcCtWrWCgwMf69Tq0mBi+/GRoc2ZsmXLCkK7e/fuQh+eTR4C9+7dw+rVq4X+NmlvU6E2tqdDgIntp8PN1Kv0RmzTRh7VDKCTKT179oSHh4epU+b2j0Hg+vXrmDlzpijeTDIlbPIQsLUpgmqVnTBmcBmhpe3kyEyfvGgAoeGZWLImTBSYpKJ5ubkyR2Mc35SD4eJsjW6dPEWmdqUKjsaZnAXMhIltCwgyT1FXCDCxratw8WAZgcIhQC/FpMFJWpyU6VW1atXCdchXmx0BJrYfDTFtztSuXRvjxo0Tx/FZE97sS7FADrKzs7F//37MmzcPlAXJR/sLBNu/GjGx/XS4mXqVnohten4/88wzmDhxIjp16gRHRyZBTI33k9rfvHlTkNt79uxhcvtJYJnpvzs6WOHVFsUwdrCPkCDhA1hmAtrEblNSc7BxezRWrI/AH7fTkZ3DhSVNhPAfzWlde5awxfAPvPF+dy+U8LAtTHd8rQQEmNiWADq7ZAT+AwEmtnl5MAIWgoCNjQ0qV66MUaNGiZdiKhbJpn0EmNj+d4yI0Pnf//6H0aNHC+kRNu0hcO7cOcyePVtIk6SlpYG0bNkKjgAT2wXHqjAt9UJs0+kUutcFBATg9ddf5428wgT9CddSrYDp06fjs88+Y1kSM+L8cNdE9Lm72qBfTy8M7F0avmVYekRF+Avkih7jh47EI3BpCH4+k4SsLH6uFwi4hxpZFQHKlLbH2CFlREFU16LWT9MNXyMZASa2JQeA3TMCDyHAxDYvCUbAAhCgbNamTZti/PjxaNSoEReZ0lHMmdj+/2CRtiwVN+3Xrx8GDhwoZEjYtItAUFAQ5s6dix07diAhIYHJbRNCxcS2CWAVoqkeiG0itel0ypQpU4R0GP07m3kRuHjxIqZOnSpOn/CpE/NiTb2TpnCpknYYOaAM3utSEsXduXCe+VF/eg+/XkzG7CWhOPDDfaSz7rZJQNIGTlkfewSM9EWXjp4ss2MSetpqzMS2tuLBo2EEmNjmNcAIGBgBIgIpM7tA+t9/AAAgAElEQVRLly4YMWKEyNhm0xcCTGznxYvWspeXl9DT7tOnD4iQYtM+AnFxcVi2bBnWrVuHyMhIJrcLGDImtgsIVCGbaZ3Yzie1iWQlUpvkSNjUQeDYsWMic5v+cr0A82FORSLL+9lj/FBfdO5QAs5OvMbNh7ZyPd8NycDc5aHY9lksEpOywYeynowtkdo+3vaYNMIX3Tt5wonX+pNB03ALJrY1HBwemkUiwMS2RYadJ20JCBAR6OnpiaFDh6Jv377in9n0hwAT25TNZQUfHx9x4oCKRLKMjr7WcUpKCpYvX46VK1ciPDwcuVx56okBZGL7iRAp0kDLxDaR2rVq1RKZw23atGFSW5GIm9YJaW3PmjULly5d4vuWadAVqLW1dRE8W8UJU0b54vU2xUFFI9n0g0BkdJYgtzftiEZ8ApPbT4pcyRK2IlObTiVQ0Ug2fSPAxLa+48ejNx4CTGwbL6Y8I0bgLyKQNIjfffddId/Apk8ELJ3YJlKbJEcmTJiArl27wtnZWZ+BtPBRk872mjVrRPZ2cHAwk0RPWA9MbKvzhdEqsU33veeff16Q2q+99hrLj6izHP7lhYrhrlixAgsXLkRoaKikURjTLZHaNZ51xpTRvmjbshhsrJnU1mOkY+KysGBFGNZvi8K9+0xuPy6GLk7WGDHAGwP7lAYR3Gz6R4CJbf3HkGdgLASY2DZWPHk2jIAgtatWrYqJEyeiQ4cOcHBwYFR0jIAlE9t06qBChQqiYNrbb7/Na1nH65iGnpGRgbVr12LJkiWgdc2Z248PKBPb6ix2LRLbdN+rVKkSZsyYIZ7hVPiZTR4C9+/fF5IkJKdEp0/YCo+AtVURvFjTBVNH+6J1s2JCY5tNvwjci8/GwpVhWPdpFGLvZbEsyUOhtLUtgr7dvTBqUBmUK8vvZPpd6f8cORPbRokkz8MoCDCxbZRI8jwYAUBkdVWrVk0UmWrfvj2/EBtgVVgqsU3kDsmPTJ48Gd26dYOjo6MBoslTIHKbJEmWLl2KkJAQ1tx+zJJgYlud74oWiW1vb29x3+vRowff99RZBk/0cuHCBRGTb775hvW2n4jWfzcgEvu5as6YNrYsXmtVDERys+kfASK35ywNxcdbonA/Plv/E1JoBrTeWzcthsmjfVHvxaIK9crdaAEBJra1EAUeAyPw/wgwsc2rgREwCAJEateoUUOQ2m3btrVoUvvBQ1VsiCTVq1kisZ1fKJJOHbz33nuG19R+eL3SWtXzmn3Sd42yHonYpiP+XFDy0Wgxsf2kVaTMf9casU2yYWPGjEH//v1RvHhxZSbJvSiCwLZt2zB79mxcvXqVN+SeElEi+SpXdBSkdoe2HrAxuKb2owoq6vjn6BOjTprbsxaHCM3tpOScJ7a3hAbPlHfA7Il+6PhaCUOeTKA1nvvggcjSz//gAUX2Qf6ff7BORVAEf/7vr7/0naAisnr7bjCxbQnfYJ6jnhBgYltP0eKxMgKPQYBI7Zo1a/5FatO/G8mI+MvJyREfki/4+0f8fBI/qvI+mZmZSE9P/0vmgDJ97e3t/yIKiTCkD0m2PO6jJVLREoltDw8PjBs3Du+//z7c3d11vZRpreav3b+v3/z1SpOj9Zq/Zmnt2draCtkV+vv3tUrf6/wPrV09W0JCAubPny90t+Pi4vQ8FbOMnYlts8D6r061RGzTc2rw4MEYNmwYypQpow4AJnihexbdz/7+vM3flMv/m/98pW7/fu/S0jPVhCn/oyltyJEkyerVq5GUlPS03VjsdURa+Zd1EPIjnTuUgL2dvp9hubkPkJ0D5OTQ94I+EB/x/RC/S4H0tFykZ+SKf6f529tawcHBCvQTvYhVEVCyOmmNW1v9+ddaf+Tewws6ODQD0xeEYPveGKSm5VrseqeJOzpYYfwwHwzqUxrF3PUpKUVrO4fWOa3xnDwSO2+tPxBrmzYwEhJzkJiULf4mp+YgO/sBMrMeICM9V1yXbw721rC3LyKKxDrYW4mNLcKIsPEobgvXotaws81/R8v7zhDhTd8XOtlB/6yln75MbFv015snr0EEmNjWYFB4SIyAKQgQwVW9enVMmzYNr7/+uu6LTNFLARVsog8RgVlZWYiNjcWdO3fwxx9/ICgoCOHh4YiIiEBMTIzQvKRPamoqqEAdXfcoI6KQiAMnJycQcerl5YXSpUuLT6lSpcRff39/+Pr6omjRon8RiEQkksapLCLR0ohtig8RO0TwUFz0ZkT80JqldUibLJSR/Pvvv4vP7du3hbY0/X+JiYli3SYnJ4t2D5udnR1cXV3h6ekpSC4qoFm+fHmhn09yQ7R+qQ2tTSLAZa3PwsQnKipKEEWbNm1i7dqHgGRiuzArq+DXaoXYpudMx44dRZFc2qSWafmbcXQPy/9n+ktk7t+fu7QZl//cpb80B7p/07OWivy6uLiIezjdv+if6R6V/zyl+5YeN+DPnj0rJEm+/fZbrhFg4iL19LBFwEhf9OpaEi7O+ku+IAI7K5t+nz5ARsYDhIZn4Mq1VFy7mYY7weni36NispCYmCMI3ZTUHNH+YbO3t0IxN2t4etihjLcd/H3sUaG8A56t5ITKzziieDFb2NkUgY1tEUH86VGp5bfrqZg6NxhfHLyHrKx/Y2Di0tFlcyJl32zrIdb8C8/po+g5EdbZOXlrXKzzzFyER2bi+s003Lidjruh6QiLyBT/H/2Nj88W7ZUyO1sruLlaw7OELeh+UcLDBr7e9ijv54Dy/nl/6f+3tbP6azOIviO0OSTje8LEtlKR534YAWUQYGJbGRy5F0ZACgL5xfWI1O7UqZMguvRoRGATuZdPYl+8eBH0Akm6lvTPRIA9jrBWer70Yu7n54cqVar8RSISkUh6z5T9TSSimi/llkRsE66kKzt27FhRPE0vRqQPrV/Sj6YNmJ9++gknTpzA6dOnhY600muXCCFao/Xq1cPLL78sPkR808YN3QP0RHJfu3ZNnDT5/PPPxfefLQ8BJrbVWQlaILbpOf7iiy9i5syZaN26tToT/5sXun/RPSp/Q4423i5duiSev/mbyfQcopMVT3Mvo/s6bdCVK1dOPFNr166NunXrinsWPVPzN+j0kNVNG++rVq3CvHnzEBwcrHqs9OrQ2ckKw/uXweD3S6Okp61upkFkNmWepqXl4Nof6ThxKhEnTifi7IVkRMVkisxVJY2y2CuWd0D92kXRuL4r6tUuilIl7WBvR0S3lRTy7mnnd+REAqYvDMGPJxNFVrulmXcpO8wJ8EPXTp6a1ZGnUwX5azwz84EgrH+9lIxTZ5Nw7mIyrt9KQ3xCtqaKgbq72cCPNoPKOeDZKk5i06B6FSdBeBPJLT50CkKF0w9MbFvat5rnq3UEmNjWeoR4fIzAYxCgl0DKhKLsoe7du+uuyFQ+mU1Zq7/99hu+++47HDlyBFeuXBHZrFoyIgqJ2K5Tpw4aNGggPpQ9SyQ4Ed3mzJi1FGKbyFoidQICAgRhq3XLP1lAWYs3b94URcW+/PJLsZYpe1FNo3X4wgsviIKxbdq0EScPKGuSCCU9kEXHjx8XJ06OHTvGhdn+XDhMbKvzDdICsU3PcTq5QM9xtTan80+W0IYcPWNOnjwpNuR++eUXsTlH9zVzWv4ztVGjRnjjjTfEPZ9ikU9ym9N3YfsODQ3F+PHjsX37dr5fFQBMIpp6di6JsUPK4Jny2i8CTWQfZRmnpuXg8tVUfPntPXzz/X38cTtNkNxqGpF4VHCwY7viaNrIDd5e9rCzzyPutG6E4859sQhcEorLv6doihw1N3Ykl9G/ZymMGewDP197c7szuX8iszMyHyApKQfnr6TgwPf38P3xBNy6ky6ytPVmlOld1sceNaoT0e2C2jVdUK2SI+j7Y2ubJ31iRUS3whNjYlthQLk7RqCQCDCxXUgA+XJGQBYCxYoVE8eW+/XrJyQL9GD5GthE/FFG2BdffIH9+/cLiYanyQSTNWd6Kff29haZZ82aNcMrr7zyV8YskdxE0ipFKFoCsU1YkZwOZSySnI5S2JljfdAapszG+Ph4HD16FJs3bxbZ2aQZrQWj+0Lz5s3Rs2dPQRZRMTo9ENxUmC0wMFBsDDyqmKYWsFVzDExsq4O2bGKbNnNHjRqFoUOHCmLX3Eb3LjpZQuTs4cOHxUkJIrNlbybTCZRWrVqhc+fOqFWrlpADo/uWVm3r1q2YNWuWkJhiezwCRPC1aOyOKaN90aCutn+nEhGbmZmLqNgsHPzhPjbvisa5iylIS9cG0UcZwO1aFkP3/3mixrPOcHYimTylqTplVzPJWSz9KByLVoeLbGBLsUoVHEW2dod2HpqZstiwyc47fXDhSir2fBmL/d/dw93QDMVPHmhh0h7FbPBiTRc0aeAmTj88U8ERzo5WsLXN0/ZWQqubiW0tRJrHwAj8PwJMbPNqYAR0iABJDgwcOBDDhw8XmcRaN8oOo5dp0uc8cOAAtmzZIo45mzsrTC1cKDv2ueeeQ8uWLcWHjlvTizllnxWW5LYEYps0z6dOnYrevXuLLHitWr5UDmVnf/TRR2INP0ofWwvjp7VHJwv69++Ppk2bgghv2nTRqtH9gYpJLlu2TGjnW7oxsa3OCpBJbNPpFPpOTpo0SZC55rL8zTjSyKbTEbQZR6cj7t+/by6XT90v3f9btGgh7lu0MadVgpvqflBiwcaNG1lC6T+iTXIBsyb4odMbHpqWY8jMyhWawXu+isPG7VG4/keaZsm+oi7WaPGKOz7o6YWXXiyKos5UVFq7BHdMXBamzQvBhu1RFlFMkgjTgX28MWZQGfh4a0Mekk4gxN3PElnZG7ZF4fSvSUhJ1caGzVM/LEy4kGLiW8Ye9Wu7ouUrbni5nitoo4gyvUnL/mn1uZnYNiEI3JQRUAEBJrZVAJldMAJKIkBZTKSnTS9VRKZq2fIJ7Rs3bmDnzp3YsWOHKP5o5IxMyuYmKQjKPqNiYFQIjF7O8zO5TY2X0YltwmXAgAEYOXKkKNypRSPZHMpoPHjwIFasWIEzZ87ohswggptOFQwaNEhocdNa1KoGd1hYmNDbpo0vIrot2ZjYVif6MoltIkVJiqNDhw5m+U7mE9r37t0TUl/r1q0T9y49bCgTwU3P0CFDhuCll14SUmtaO8lDv2nolBHJp7H9GwEiYCeN8EX/90rBtag2i0VSRnF0bBb2fh2HtZsicfVGqmYJ7YcRJnxfb10cH/YqhVrPu8DB3kqRLFRzrOULl1MweW6wkHQxut42FTicO8UfnV6Xn61NWCcm5eDQ0Xis+DgCv5xLUl1OxxzrqbB9FnO3QcO6rnizbXGRzV3aK4/kJtkSKvpZUGNiu6BIcTtGQB0EmNhWB2f2wggoggC92DVs2FDocVIWplYtX3KEtIc//fRTkMwAHX22NKNNCJLYIOKCJDYqVKggtI+JzC3oS7qRiW3CgHS1iczUoq52/jo+d+4clixZInS0k5OTdbmMSZKka9euIhOSCqOasgbVnDDJulD2Pkk00MaYpRoT2+pEXhaxff36dVB9CarV4O7urvhkaTOOMrJJ95+KHVIRZj3JfeUDQvrjtPFJ+uMk/6UleZLo6GiMGzdOZMAT3mz/jwBlEPd8xxPjhvhoUlebZBlIQ/voT4lYtDpMFIVUWz9bqfVCmad93/VCj84lRVYq6Qlr0XZ/EYtZi0Nx6apx9baJFO35TklMGCZ33QtZnaxcUeSUZGBIWic1zXJ/T/3X94EkSxq+RCS3x18kNxVrLcgpCCa2tXin4TFZMgJMbFty9HnuukOAMlopQ4gIKi294P0dSJJriIqKEmQ2yTXcunVLdzibY8CUKVu/fn0RuyZNmsDLy0tIlTwpe9bIxDbJ6MyePRtdunTR3HomooIyHYm0WLlypSioZgSjUx6k6UsbLUR2P2n9qT1n2kxYs2YN5s2bJ053WKoxsa1O5GUR2/mnlgq6wVlQNKhfOu1w+vRpLF68WGRqq13MtqBjLWg7+q1D2dtjxowRz1C1CmwWZHyUdU/PMNrEZ8tDgMg9yiCeNaEsWjcrpjlYKEs7NDwDKzdEYvOOaJGxrXcjqYVG9d0wZmAZNGnoBkdHK5MyT9WYP2mVUyHJ5R9HID4hWw2Xqvso5maDuVP80LublzTpHSK1Cd+tn8Vg2boI3LyVpjoOenXoW8YOr7Uqjnc6eOK5qk5Cx/6/sriZ2NZrpHncRkWAiW2jRpbnZTgE6Cju6NGjMXjwYFWKTJkKIGVXpqWl4dChQ1i0aBFOnTqlywwxU+f9NO0rV64syNy33noL5cqVE1ncpMX9KDMqsU0Zw6QRT59SpUo9DYxmu4YyG+l4OWk+U4FTyqw0khGh3adPH5G9TRmjj1t7suZM+rWUxf/JJ5/oQjbBHDgxsW0OVP/dpyxi2xyzo/sWnYxav349NmzYAJL2MZI9//zzQoKNNuW0Ik1CG/fjx4/Hnj17DC2xZso6cnO1wdQxvuj3bik4OVmZcqnZ22ZmPsCPpxMwd1kYjp5IEMX0jGRlStthcN/SePd/JVGqpC2snlY82EygXLuZhkmBd7Hvm3uGlCQh7fNpY33RoI6cQql0yC0sIgNzl4fh013RQoaEzXQESNbnpdou6N6pJJo3ckOpknawsy/yr80KJrZNx5avYATMiQAT2+ZEl/tmBBRCgIgn0tWeOHGiJnW16YWaCFg68kzSI1z8rWCBp6xteknv0aOHiKuzs/O/CvwZkdimLEUqEEaSE1TgUEtGxSApyzEwMBAnT540rBwGZWrT2qMsyDp16miusCRJkhC5TZIkRtbkf9zaZ2JbnbuCUYhtum9RQcg5c+aIv0aVxqD6FWPHjhUbw66urgWW9DLXaqJ7E22ALliwgH/3AELjucfbJTFuqA8qV3Q0F+wm9yukR1JzsePzGCxcHY7fb6Sa3IdeLiBSrkdnTwzt541KFR1ho7HCkru+iMXMRaG4QpIkegG1AOOkkwpjBvtg5ABveHqoX6ibSO0/7qRh1qIQ7P4yDpQhz1Z4BPx87dHp9RLo8pYnqlR0hAMR3H9+p5jYLjy+3AMjoCQCTGwriSb3xQiYCQHSaabjrkREac3o2PPRo0fFC/WPP/5o2Bdqc+JOhDZppn/wwQfiqDW9sFNGM5kRiW0ik2g9v/fee5oiVOnIPm3M0IkDKnhqCUakNm2YkdY5nRzQitEJkKVLlwrCKDw8XCvDUm0cTGyrA7XeiW0iVkn3f/v27eK+RdrdRreSJUuKLOlevXoJOSXZ9tNPP2Hy5Mk4fPiw7KFI91+1kiMCJ/mj/avFpY8lfwBEat+Lz8aKdRFYsykSkdGZmhmbuQZCJGvbFsUwfqgP6r5YVFO62ympOZg2PwSrN0QiOcU4GcVeJW2xYGo5dP+fp7nC+th+aY3fCU7HlLnB2LUvVrd68aoDZ4JDzxK24r7Wq4uXkClxdLDCp7tiMHtJqNhQYGMEGAH5CDCxLT8GPAJG4D8RcHJywqRJkzBw4EBBeGrF6IU6ISFBaBAvW7aMtbQVCIy9vT0aNWqEQYMGoXHjxiBdbjpaThlqu3btUsCD/C4oU5gIbSq69cwzz8gfECAygpOSkrB27VosX74cISEhmhiXWoOgYpJEbnfs2BF0v9GK0aYOjWvHjh0Wt2HGxLY6q1DPxDZt/kRGRop7Fsn2UDFDSzGqz0Bkcrdu3aTfs0iCjZ5nq1evBtUYsVSzsy2CsUN8MPQDb1BBNi0YEX7RsZlYuCoc67dG4d59Y2o7Pw7rhnVdMWmkr5BTIK1grdjJM0mChP3+eDwoRkYwwnja2LKiEKHaFhOXhZkLQ/DJtmhDbRaojWNB/Lm72eDV5sXwfncvXPk9FcvWheNWUHpBLuU2jAAjYGYEmNg2M8DcPSNQGASIBCSyiYjtGjVqFKYrRa/Nf6GmDDHS8qQie2zKIeDi4oI2bdoIDWRPT09Mnz4dn332mXIOJPZUqVIlkd3foUMHiaP4p2vaoKHsYJLSocKnlmiktU33mc6dO0sniv6OP5Has2bNEprnlmRMbKsTbb0S2/QMvn37NmbMmCE2PdPTLe/FumrVqpg2bRrat28vvaCkpReRpAxhKlpIxF6jeuoTe4+6WxBhGh6ZgVmLQ0VmpZGyg025O9atVRSTR/miVRN3zZDbubkPsGRtOBasDEdElDEy6Af1KS02dny87UwJT6HbZmbmYtGacCxZE46oGMvdWCs0kCZ24OJsDdei1khMzkFysnFOHpgIAzdnBDSFABPbmgoHD4YR+CcCRDaR1i/paxPJrQWjF+o//vhDjIteqEm+gc08CFCGPklFRERE4OrVq+ZxomKvNjY2ogAqFYwkwl4LRpnadOJgxYoVIvvRkq1ixYoICAjA//73P1GcTQuWmJgoxrRu3TpRnNZSjIltdSKtR2KbnsFUtJA2PHfv3g2SA7NUo5NNVKvhlVdekfob6fLly6Kw5ddff22RoSCCZ8Z4P/Tr4QXSeJZtRGqT5MjsxaHYtDMaSRZOPNWvUxQBI31BxQ1tbbSRuX0nOAMTZgaBNLdJH1rPRtnwC6b5Y0Dv0qpqmtM6p6z3qfNC8PMviXqGkMfOCDACjEChEWBiu9AQcgeMgHkQIBJwxIgR4kNFBrVgVJDq999/Fy/UX3zxBahgFRsjUFAEiKSn7NuWLVsW9BKztiOidOXKlViyZAnCwsLM6ksvnVMWJBFFRKza2ambefQ4jL799luRmUnFPC3FmNhWJ9J6I7bzN5bpGbxnzx6LJrVphVAh4n79+gm5rnLlyqmzaB7hhQpoE7FNsjCWlj1P2drtX/UQWcEvPOcsLQZ/dxx3PwuBS8Lw8ZYoJCRalvzI4wJAGfUUo8b1Xf8qfic7WJ9sjULgUtIo1veJk/L+Dpg72R+dXvdQFVKSIJk46y427ohGVpZBNF1URZCdMQKMgJEQYGLbSNHkuRgKgXr16oljxi1atNDEvOiFmrKSiGD66quvQC9ybIxAQRGgYpiklzx06FC4u7sX9DKztSMtVNKHnzdvnsUUiiwomHTvmTJlitiAsLa2LuhlZmtHGxCkp0tSMZZyQoSJbbMtp390rCdim2oB0GkpegYzqf3/YaTnCW3Gvf/++6BCzLJs06ZNoiiypRQezse5mJsNAgP80Lublyaygak44eI14VjxcQRLMzz0Zej4mgcmDPMRGxBWVvIzt8MiMzF+RhC2fRaLnBz9ErPNG/+pr11XPRkeytamQpEzF4cIrWc2RoARYAQsHQEmti19BfD8NYkAFXAjIocKRpLesmwjUvvatWuC7Nq3bx+T2rIDokP/DRo0EBs1zZo1kz56OnnwzTffiPGcOXNG+ni0OIDXX39daG7Xrl1b6hH/fGwOHz4syKsTJ06IYp9GNya21Ymwnoht0v8nUpsIVEvZ4CnoKqD7FN3PW7VqJe1+de7cObF5e/DgwYIOW/ftKFu70xslMGmEL56vJr/wMGWtfrItCvNXhHFBt0esLlI0pKJ3Ywb5gLKMKX6y7dPd0UIy5tpN/UqN9XynpNgwqFRBPQm3xKQcjJsehHVbopCdbfzfRLLXKftnBBgB7SPAxLb2Y8QjtDAE6Ght27ZtBbFdt25d6bMnEikoKEiQSjt37rT4o8/SA6LDAVC2Nq3nIUOGgHTDZRqt50uXLgnSdv/+/RZBkj4N3pSp/cEHHwhNdD8/P3HkX6aR7BFtrNFR/5SUFJlDUcU3E9uqwAy9ENu05hcsWCCkk2JiYtQBR0de6P40bNgwcb8qXbq0lJHTZsOYMWOwZs0a0OapJVhxdxvMmeyPXl1Lqqot/ChsqSDhoSPxmLYgBKfOJlkC/E81R2cna4wbUkboQRcvZvNUfSh5UXRMFsbOCBIFPvWatT1uqA9GDSwDDxXx/O5oPKbMC8bJM7zWlVyP3BcjwAjoFwEmtvUbOx65QREoWrSo0CEm3Uh7e3vps4yOjhaa2hs3brQIQkk64AYcgJa0tSnrkQhSynq0NC1UU5cW3YsoA7F///5wc3Mz9XLF2x86dEhssFmC1jYT24ovn0d2qAdim2STNmzYgPnz5wspErZHI1CpUiXx26ljx47SsraXLl0q5K3Cw8MtIkxvttWGtjYd4rl2MxUTZwfjiwNxui9GaO7F41vGHtPHlkXXtzxhZyd305rmumF7NAKXhOLmbf1lbVtbF8HC6f4Y9H5pWKsk70LrfdHqcCxYGSaKpLIxAowAI8AIAExs8ypgBDSEAGUdvfbaayK7lY7Wyrbk5GQsXLiQs8RkB0LH/q2srDBq1CiMHDkSJUuWlDoTyvpdtmwZFi1ahIiICKlj0YvzatWqiSP+7du3l663TfcjItrXrl1r+JMjTGyr8w3ROrFNMmAkbUGby6dPn1YHFB17oVMm48aNg7+/v5RZkMQVycX88ssvUvyr6dTJ0QqzJvihf69ScLC3UtP1v3wlJOVgxoIQfLQ5EknJlpEtX1jAWzR2w+TRZdHwJVeoxMc+dshBwRkYNzMIu7+I1d2mhEdxGyycVg4kR6KWkQzJyCl3sH5LFCxAmU0tWNkPI8AI6BwBJrZ1HkAevrEQ0FK2NhWHXL9+vcgSu3XrlrGA5tmohkDlypUxZ84cEFEn04ggOnDggCCILIF0UBLrd955BxMmTED16tWlS5Ls3btXEO0XLlxQcoqa64uJbXVConVi+86dOxg/fjx2794Nuoex/TcCJJtEz5u3335bStY2ZdSPHTsWdJ8yujVrlFcw7+WX5MqLkXwF6WrPXca62qasOaoLPbSfN0YOKAPvUnamXGqWtsvWRWDe8lCERegrA7liOQfMnewPKsyplp27lIKJs+/i4A/31XLJfhgBRoAR0DwCTGxrPkQ8QEtCoF27dkImgaQbZBq9QB85cuSvYm0yx8K+9YsAnUDo27evyKArV66c1IkEBwcLgmjHjh1MEJkYCSpmS5nSVMxWtiQJ6QsTcURSMkYm+pjYNnGRPg+4RoUAACAASURBVGVzLRPbGRkZYmOZ5C1iY2OfcoaWd9nw4cOlaW2TbAzpfJMWOiUHGNVsbIogYKQvhn3gDdei1tKmSdmqZy8kC5KPNIfZTEOgtJcdZo4vi+7/KyldkuTS1RRMnHUX+7+7r6ss5No1XcTJhVZN3U0DvxCt9393D9Pmh+DM+eRC9MKXMgKMACNgLASY2DZWPHk2OkaA9LQpE5HIIyKSZBoViyQia9euXYZ+OZOJsSX4LlasmCBmevXqJSV7Lh9jIhuWLFkiJEgiIyMtAXrF51i1alXMnDlTE5Ikq1evxty5c3H37l3F56mVDpnYVicSWiW2qcgtnTCxFFkLJaNNRbfpt1SrVq2U7LbAfdFzhp57Rn7WPFvFCYGT/PB66+IFxsUcDUl2ZOq8YKzdFImUVD7R8DQYt3jFHVNG+6JhXVfIrBFNmfdT5oZg6UfhSE7Rj5xMo/qumDGuLF5poF4dEpIgCVwailtB6U8Tcr6GEWAEGAFDIsDEtiHDypPSIwINGjQQxFHTpk2lDp8K6tFR3uXLl+PevXtSx8LO9Y3Aq6++KrL+iWiQacePHxe69ceOHZM5DN377tq1q9jwIt1tmUYyJJMmTQLp2RIBaERjYludqGqV2A4LCxPyP9u2bePNZROXgp2dnfgtNWjQIDg6Opp4deGbG10uicjPPt28MH6oD8r7OxQesKfsgW79Oz+PwazFobhyLfUpe+HLSJJk+IdlMPLDMihV0lYqIN98fx/T5gfjl3P6yUSmjYGpY/I2BtSyucvDsGBFGGLvZanlkv0wAowAI6B5BJjY1nyIeICWgAAV2KPj9SNGjAC9aMsyOtr/5Zdfimync+fOyRoG+zUAAtbW1kJWZ+jQoXB1Ve8H/8PQpaWliXGsWrUKKSkpBkBW3hQoA3/27Nno3bs3iDySZXTEn2JKEg1GjSkT2+qsLi0S2/QcpvvVvHnzEBISog4QBvPSsWNHBAQEoGbNmqrP7NdffxUbgN9++63qvtVw6OZqg3lT/NCnuxesJVYdjIzOxNjpQdiyO0Z3BQfViJMpPipXdMSsiX7o0NYDVhLrgMbdz8bYaUHYsD1KNzFt17IYpowuizovuJgCeaHakgzJotVhoCKSbIwAI8AIMAJ5CDCxzSuBEdAAAlopsEcSJKRDTBIkRtav1UDIDT8EyuoNDAzEG2+8IW2ulM178OBBcZz/9OnT0sZhJMcUTyKMateuLXVatAFHhUCJRDKiMbGtTlS1SGzfuHFD1CX4/PPP1QHBgF4qVKggTp699dZbqhe8jYqKwqhRo7BlyxYDIgtQhuq0sb5oUEfehjXVUaWCkXOWheLWHZZjUGKhfdirFMYO8YGfj70S3T11H6s3RGLuslDcDc146j7UvLBDOw9MHuWLmtWdVXM7YeZdLF4TjvQMlt9RDXR2xAgwAppHgIltzYeIB2gJCJAGMRHKzzzzjLTp5uTkCB3ihQsXIiIiQto42LExEOjevbvIWqtSpYq0CSUmJooxrFu3DlSIja3wCDg7OwtCuX///lJrAYSGhopTLtu3bzekHAkT24VfqwXpQWvENm0okwwYaTSTHAnb0yFAp+BIjoRODKlds4R+S40cORIrVqwA/bORjGRIxgz2wcgB3vD0kCdbceduOsbOuIs9X8bqqtCgltcCEdqzJ/nhnQ4lYCUxE59kSCYF6qcYKOE1aYQvSHdeLRs1JUhokWdnG1OKTS0c2Q8jwAgYCwEmto0VT56NDhEgooiOHPfr1w82NjbSZkAZraRb+/3330sbAzs2BgIkU0GSFVQI1cFBjgYnZWvna51evHjRGMBqZBYtW7YUUiBUF6CIxGpTdCKANuLi4uI0goxyw2BiWzks/6snrRHb169fF9na+/btUwcAA3uRWRNg1qxZ4t50//59QyHs5WmLBdPKofv/PKXNKzf3AVauj8T8FWEICecNayUD8d47JTF+mA8qVVBfmz5/HqlpuRg3PQhrNkUiK0v7xG2PziUxYZgPSM5FLRs/8y6WcMa2WnCzH0aAEdAJAkxs6yRQPEzjIvDyyy8LTesmTZpImyRls5JcA2UYJSUlSRsHOzYGAs8//7wgttu1aydtQlT4lDJ6N27cyMXXFI6Cvb29uF/QxoWLi3q6kg9PI19m5tSpUwrPUH53TGyrEwMtEduUrb1s2TKRrR0eHq4OAAb2IvM5tH79eiHFdevWLUMh3KZ5MVEo76VaRaXN69rNVIybcRdfHODi5koHwaukLeZM8se7b3vC2pooAjm2fksUApeG4laQ9mVm3n27JCYOV5fYnjovT2M7KdlYJ0LkrDb2yggwAkZBgIlto0SS56FLBCjbcfjw4Rg9ejRKlSolbQ5HjhwRGZg//vijtDGwY+Mg0KNHDyEBUqlSJWmTMroGszRg/3TcunVrcc+oX7++tKGQli3dO0nLljL0jWRMbKsTTS0R28HBwRgzZgx27typzuQN7sXR0VFkTX/wwQcgaRI17auvvhKSTWfPnlXTrVl90eGccUN9MPLDMvAoLud0Id3m127K02AOCuFsbXMEvFfXkpgw1AcVy6uXgfzwPM5dSsGk2Xdx4Aftn3jo/KcUSXUVpUiI1KYTC5HRWeZYAtwnI8AIMAK6RICJbV2GjQdtFATopXrBggXo2bOntCmlp6cLgmrlypVISUmRNg52bAwESE6HsrUHDx4sTYYkMzMTkydPFlq1qampxgBWY7NwdXUVxdnef/992NrK01qltUbkFWXoG8mY2FYnmloitqloM+lCX758WZ3JW4AXknWhQo4eHh6qzvaXX34R0m7fffedqn7N6ayEhy0WTvMHSS/IspjYLIyeFoTNO6NZW9tMQSAZksBJfqCiiLKUxtLSckWc12yMRE6OtjetO77mgYCR6haP3LonBrMWh+D3G2lmWgXcLSPACDAC+kOAiW39xYxHbCAEmjdvLo70N2zYUNqsjh49KkhAztaWFgJDOa5YsSLmzp2Ljh07SpuXEUkFaWD+h2PakKOit5UrV5Y2PNIipszI8+fPSxuDORwzsW0OVP/dp1aIbZIDo1MutMFMm81syiDwzjvvCIL52WefVabDAvZixOz7Vxq4Yvo4PzSu71pAFJRv9vk3cZixIATnL3MShvLo5vVIZPb4oT4YMaAMPIrJycyncaxcH4G5y8MQEqbtzPzXWxXH5NG+qF1TPVm2IycSMGVuMH48lWiuZcD9MgKMACOgOwSY2NZdyHjARkFACzIk2dnZIkNs6dKliI+PNwq0PA+JCLRv315slNSqVUvKKEindvHixeIkRGRkpJQxWIpT2sQgHdm33npLWhFJKrZH5Prnn39uKNiZ2FYnnFohtkmygojtQ4cOqTNxC/FCUklUw4SSCNS0tLQ0jBgxAmvWrFHTrVl9fdirNMYNKYOyPvZm9fO4zjMycjEpMFgQnmnpuVLGYClOG9WjTYyyaNLQTdqUD/+YR96eOK1t8rZNM3dMGVMW9V5UT3c+NDwTY6cHYfveGD65IG2FsmNGgBHQGgJMbGstIjwei0HA3d1dHKHv1auXNFKIshwnTJgAKsLGxggUFgHarKGj3yNHjlT96Hf+2I2YKVfYuJjzeirQSUf9iSCUYbQ5R2uOCt9S1qtRjIltdSKpBWKb9OEpU3vevHkICQlRZ+IW4qV06dKiGGe3bt1UnzFtuC1ZssQQGfj29laYP8UfH/YqBRsbOUUFT51NEsT2D8c5CcPci9nRwQqzJ/mJeNvbqatPnz830o8eMy0IW3ZrW3am4UuumDGuLJq+rN4mAGnNE+m/eE04klO4gKS5vw/cPyPACOgDASa29REnHqUBEXj55ZdFJlGTJk2kzI5epkmDmF76QkNDpYyBnRoLgfzNmt69e0ubGBVeo1MIV65ckTYGS3JM9zGSAmnatKm0aa9bt07ofd++fVvaGJR2zMS20og+uj8tENtU24I2A2kd04kTNuUQoKKRdHqHaj5Q/Qc1jfT/yff9+9ovgPckXKpVdkJggB/eaF38SU3N8t+JyCMSL69gXqZZfHCn/0Sg85slMGmEL6pXdZIGzdR5eeRtYpJ2ydtqlRwRGOCPN9qo+93Y/919TJsfjDPnk6XFhx0zAowAI6AlBJjY1lI0eCwWhQAVXaOMnvLly0uZd3R0NEaPHo1PP/0URHKzMQKFReDFF1/ErFmz0Lp168J29VTX5+TkCBkUktbhQqhPBaHJF1ERSdoco/sZkUgy7MiRIyLuJ06ckOHeLD6Z2DYLrP/qVAvE9rlz54QMCZ+cMk/M6VQabRwUL64u8bRs2TKRhR8WFmaeianYK5F2k0f54sUa6ukI/3168QnZGDklCBu2RbH0gkpxr1wxr4jkm23lFZHcsjuvSOK1m9otkliqpC0WTCuHbp08VYpMnpu4+9kYNyMIG7dHIzub3+FUBZ+dMQKMgCYRYGJbk2HhQRkdAcocIm1ayiKyt5ejV/jll1+KwpX0Us3GCCiBQOfOnUWhrurVqyvRncl93Lp1CySN8dlnn5l8LV/w9AgMGTIEY8aMQZkyZZ6+k0JcSfIzFPcdO3YUohdtXcrEtjrx0AKxvXXrVrEh+Pvvv6szaQvz0rdvXyFXpHYSwYYNG8TvvJs3b+oe8REfemP0oDIoVdJOylyOn0zE5Dl3cexnbestSwHHTE6trYGZ4/0wpJ83nBzlbFqfPJOEgDnalp8hmZ7FM8uh/3uloLZIz54v4zBzUQgu/sbFVM30NeBuGQFGQEcIMLGto2DxUI2DQLly5UQmT6dOnaRMio47E6lN+o+JifyiICUIBnNK+tp0AkFGZlw+lFRAkGQxLly4YDB0tT0dklMi3Bs1aiRloHQ/o7VHmfpG0dlmYludpSSb2Ka1O2XKFPEsTk7mI+XmiDp9lwICAlQvaLxnzx4hN3fp0iVzTEu1PkljecH0PH1tayu1qTuIDO1Vn0Rg7vIwhIQZp46CagEshCPKQp443AdVK8mRI4mOzcLoqUH4dJe2dbZJj3z4B95wcFB3AyApOUdoba/dFInUNJaxKsRS50sZAUbAAAgwsW2AIPIU9IdAmzZtMHXqVLz00ktSBk9atJThSC9ebIyAEgi4uLhg0aJFQpKCSG61jQgi0tZevHgx4uO5uJSa+BM5SBt17733npTY01zXrFkjdLbv3r2r5tTN5ouJbbNB+4+OZRPbVN+CTjts375dnQlboJcGDRoIgrlZs2aqzv7bb78Vv/NOnTqlql+lnVWq4Ig5AX7o0M5D6a4L1F9KSi5GTb2DjzZHgiXoCwSZYo2eq+okiki+1kpdGZ/8CdCmxuQ5eTrbKana1dke2Kc0xg4uA98y6p/AvXA5BZPnBuOb7+8jJ4clSRRb/NwRI8AI6A4BJrZ1FzIesBEQGDRokCCWfXx8pExn7969IsPy4sWLUvyzU+MhUK1aNXHs+o033pAyOdIxJc14JoikwC+wp4+np7o6k/mzPXDggDiFcvr0aTkAKOyViW2FAX1Md7KJ7aNHjwp9+B9//FGdCVugl0qVKolNrw4dOqg6+59//llkih8+fFhVv0o7a9nEHdPGlEX9OkWV7rpA/f1yLgmTZgfju2O8YV0gwBRsZGdrhTmT/UDErZ2t+gkLNJWPt0QhcGkobgelKzgzZbt6vVVxTB7ti9o15WjQf74/DrOXhOL8pRTk5DK5rWx0uTdGgBHQCwJMbOslUjxOwyBgbW2NuXPnCn1tOzv19QqpUCRlL1F2bUJCgmFw5YnIRYBOIdCR+nr16kkZiFGy46SAp4BT2fG/cuUKqEjcV199pcBs5HfBxLY6MZBNbG/ZskXoa1+7dk2dCVugl9KlS2PBggXo2rWrqrO/fPmykEjav3+/qn6Vdta7mxcmDPNBBX8HpbsuUH+fbItC4JJQ/HFHu8RmgSai00YU//FDfVCxnJz4f3vkPqbOC8Gps0maRfD5anmZ7e1ayslsz819gK17YjBvRRh+v57G5LZmVwoPjBFgBMyJABPb5kSX+2YEHoEAFVibP38+unTpIgWfuLg4oYO8adMmKf7ZqTERkFWgKx/NVatWiQ0jKiTIpj4CFSpUEFmRsuoG0CYd3dc++eQT0Oad3o2JbXUiKJvYpnsWka6xsbHqTNgCvbi6uoqN/D59+qg6eyNIvpGq2ORRZUHFI12LWquKHzmjW/mkwLtYsjYcqamsIax6AAA0ru+K6ePK4pUGbjLc4/cbqRg/4y6+OHhPiv+COHV3s8HCaf7o1dULEpT4xBBJhmT73lgsXBWGK9dSkZ2t/99BBcGe2zACjAAjkI8AE9u8FhgBlRF4+eWXRcY0FVyTYUY5HisDO/b5aARIU5tkIIYPHw7S2lbbcnJyhLTP8uXLkZmZqbZ79gfA1tZWEHQDBgyAjY2NFExI0oE01o1QhI+JbXWWkExiOysrS2zGrFy5ElQjgM08CNApObovkAScmvUfQkJChH76jh07zDMxFXp1cbbGohnl0PddLxW8/dtFTFwWRk2+g827YqT4Z6eAj7cd5k3xR5eOcmTGqEDiyMl3hCSJlvesJ4/yFRtAbq5yfv/QWiV8fjiegLnLQnHil0RkZORqGjP+fjECjAAjoCQCTGwriSb3xQgUAIFu3bph4sSJqFq1agFaK99k48aNmD17Nm7evKl859yjRSJAZDYRB1Q4UoZRATbSd9YzgSADN6V90sYGxYGO/suwtWvXiqzxoKAgGe4V9cnEtqJwPrYzmcQ2EZ/0fdm5c6c6k7VgL1T/YdiwYXBwUE9OISIiQsR369atukW+csW8wpFvtpVTOPL0r0mYFBiM71lfW9oasrYG5k72x+C+3tJ0tqk44uLV4UhO0W4ByQ5tPRAwyhcvPOcsLVb5joNCMrByfQR2fB6LqOhMZHH2tvSY8AAYAUbA/AgwsW1+jNkDI/APBGQWWaOssEmTJmHp0qVITU3lyDACiiBQsWJFIQPSsWNHRfoztZPjx4+LAmzHjh0z9VJuryAC7du3F3GoVauWgr0WvKsvvvhCFMU9d+5cwS/SaEsmttUJjExi++TJk6K44A8//KDOZC3YC/3uoex4d3d31VCIiYnBqFGjsHnzZtV8Ku2I5CdIhoLkKGTYjs9jMHNRKH67xr9XZeCf73NA79IYO7gMyvrYSxnGqg0RmLssDMGhGVL8F8QpaZDTJlDH10pIkyP5+zhJiuTozwlY8XEEfjyViMSkHJYnKUgguQ0jwAjoFgEmtnUbOh64HhGwsrISx/WpcKSM4/phYWEig2j79u16hI/HrFEEGjZsKOR1mjZtKmWEpBdPpxBu3LghxT87zUPg+eefF3Fo166dFEhOnz4tNu6+//57Kf6VdMrEtpJoPr4vmcQ2FTqljZizZ8+qM1kL9jJixAghC+LlpZ6kRnx8PMjvhg0bdIv8/94ogYCRPniumvpZqCSrQEUjSTP4Xny2bjE0wsDbNC+GqaN98dKLRaVMZ+/XcZixMAQXrqRI8V8Qp1ZWwMzxfhjazxtOTlYFuUSVNmlpufjuWDzWbIrEL78mgaRdKINby7IuqgDDThgBRsBwCDCxbbiQ8oS0jAC9VBGx3b17dynDPHLkCKZMmYIff/xRin92akwEKFObMg9r1qyp+gSpUCDpe1NxsKSkJNX9s8P/R6Bo0aJYuHChkKRRU8s2fwQkQULk1e7du3UfFia21QmhTGL7008/FRtB165dU2eyFuxlyJAhog6Dt7e3aiiQ1j/JM3388ceq+VTa0aD38zJ1fbzVz9RNSc3BiIA7WPeptrWVlcZci/3lSdL44822xaUM76fTiQiYE4wjJxKk+C+o085vlsDEEb54rqpTQS9RrR3pbZ88m4wN26MEjrFxWcjMeiCKTrIxAowAI2AEBJjYNkIUeQ66QYCIP3qRffXVV6WMmV6kZ82ahevXr0vxz06NicCHH34oSAM/Pz/VJ0jkAWXFrVu3TnXf7PDfCNDGGcXD1VX9o+skr0S+P/roI9CGh56NiW11oieT2F6xYoWQcKIaAWzmRYCK2o4bNw6+vr7mdfS33tPT04WuN2n/69VmjPfD8P7ecJaQgXr9jzSMm3EX+76J0yt8hhk3ZSAvml4O/XqUkiKz8ceddIydHgTK3Nay+ZaxExsA73TwBGVwa9Hop9GNW2n44sA9fL4/Dtf+SENaei6ysnLBNYy1GDEeEyPACBQUASa2C4oUt2MEFECgTZs2mDp1Kl566SUFejO9C3qJpozx2NhY0y/mKxiBxyBAuspEKLq5uamO0a1btwSp/tlnn6numx3+G4H+/fsLAknGJgeNhqRIqJCp3msIMLGtzrdLJrE9c+ZMccKBJCvYzItAv379MH78ePj7+5vX0d96z87OxtChQ7Fq1SrVfCrpyMHeCotnlkP/90op2W2B+yJd4MlzgnH0J21n6RZ4QjpvOGN8WQzvX0bKJgfpQ1P2/vqtUZpHcfD7pTFmsA98vO00P1YitM+cTxYE9/fH44WGeUbmAya5NR85HiAjwAg8CgEmtnldMAIqIkASJBMnTkSVKlVU9JrnKicnRxQyoiwxeuFiYwSUQIB040kGhHTj6Z/VNirARmTm4cOH1XbN/h6BgExZGhoOEYXz589HVJT2X4D/awExsa3O10sWsU0nCqjexbJly5CVlaXOZC3YS58+fQSxXaFCBdVQoBgTsb18+XLVfCrpyLuUHeZP9UfXtzyV7LbAfe35Kk9X+dJv2tVVLvBkDNBwcN8/CdvS6hO2lGU8akoQln0UjmyNS2fUeNYZMyeURbuWxaVktz/tUktOycHZC8n4+tB9fH8sHneJ5M7IFXrcJFei80NwTwsLX8cIMAI6QoCJbR0Fi4eqfwToJYc0YNXUecxHLSIiQrxIb926Vf9A8gw0g4C7u7sgtnv16iVlTF988YUowHbu3Dkp/tnpPxGQXUiUCrUFBgbi5s2bug4NE9vqhE8WsZ2Wlib0l/UsU6FOhJTx0rNnT0yYMAGVKlVSpsMC9kInmWjzghIL9GbPVnZCYIAfXm8tR1d51SeRmLssFMFhGXqDzpDjffvNEpgkUT965sIQLFwdjvgEbSfmWFsDE4b5CgmfYu42ulwL6Rm5+O1aKn44Ho/vjyfgyu+pfxWdzP6T6NblxHjQjAAjYGgEmNg2dHh5clpDYMaMGeJl1tlZ/QrzRPxRtvjBgwe1BguPR8cIlC9fXujEdurUScosSE95zpw5uHPnjhT/7PSfCFSsWFGsB8rclmFG2ehgYlud1SOL2I6Li8PIkSOxadMmdSZq4V5knZajGC9dulSXxHajeq6YPq4smjRUX2KMluuUucFYvCZcEGps8hF4pQGtBz80rq9+/Qya/cpPIjB3WRhCdLDRUfM5Z8wYVxZtWxTXrNZ2QVcUZWqHRWbil1+TcOJ0Ik6eTcLtoPQ/dbkfiIzu3FzO6C4ontyOEWAEzIcAE9vmw5Z7ZgT+gYCjo6M4Ik9FjIoUoa+eurZ//35MmzYNZ86cUdcxezM0Ai+++KIoSNq6dWsp86TNIsoYZ51aKfD/yykVjaR40NF/GXb8+HGQ5vuxY8dkuFfMJxPbikH5nx3JIrYjIyOFNBifoFInzt26dRMb+1WrVlXH4Z9e9Exsv9GmOCaP8sWLNVxUxYycZWTmYsSkO1i9MZIlEFRH/9EOq1ZyROAkf7R/VU4G/7bPYjBrcSiuXk/VCCL/PYyBfUoJre2yZex1Md6CDpKI7uiYLFz4LRknTiXh+MlEUYySpEwom5uJ7oIiye0YAUZAaQSY2FYaUe6PEXgMAqSrTUdh3333XSkYffzxx+KI/u3bt6X4Z6fGRKB58+Ziw4QkKNQ20oqno94rV65ELpdzVxv+x/qjDPphw4bB3l79F7pLly6J+yxt5OnZmNhWJ3qyiO2QkBAhS7Zjxw51JmrhXpjYNn0B9Hi7JCYM90Hlio6mX1zIK0IjMjBmahC27+VC54WEUrHLPYrZYMH0cnjvnZKK9WlKRwe+v4+p80Pwy7kkUy6T1rasjz1mT/RD5w4lYGOtfjKTWhMnojsyOgu/XkrGz6cT8fMZIrrT/yK6SROdNLr5J7paEWE/jIDlIsDEtuXGnmeuMgJNmzbFoEGDpB3RnzdvHhYsWICYmBiVZ87ujIwAEXABAQGoVauW6tPk4/yqQ14gh6TlTx9PT/WLjpEkDRGGe/bsKdBYtdqIiW11IiOL2KYN5rFjx+p+naoTpcJ7YWLbdAwH9C6NcUPKwFdCxikVjJww6y72f3ff9IHzFWZBgGqDL5pRDoPfLw0rK/WJ2hOnEhEwJxhHf0owy/zM0elbr3lg4ghf1KzurKtCkoXFIvZeltDlpmKUZ84n4+JvKYiOzUJW1gOR1S10ulm+pLAw8/WMACPwEAJMbPOSYARUQqBLly6gAkayJBvoGO7ixYtBRavYGAGlEJClXUrjDwoKEiTm7t27lZoO96MAAh9++KEg7fz8/BTozbQuaOOOJB42b95s2oUaa83EtjoBkUVsX79+HePGjcO+ffvUmaiFe2Fi2/QFMHaID0YN9EaJ4ramX1zIK376JREBgcE4ckI/JGYhp6yLy2dO8BNFEZ0crVQf74UreZsdlLmtF7OztcKIAd4Y9oE3vDzV/x5pBafMrFwEh2bg0tVUnD2fLHS6Sb6E9PPziW7K7KbsbzZGgBFgBJ4WASa2nxY5vo4RMBEBOprfoUMHNG7c2MQrC988JydHFK1csWIFHvAvh8IDyj38hUD//v0FQSODxLx8+TLGjx+ve9kJoy0n2uwgORC19WwJR9q4o3vd2rVrdQ0rE9vqhE8WsX3lyhXxHfnqq6/UmaiFe2Fi2/QFMH1sWQz/0BsuztamX1zIK749Eo+p84Jx6qw+ZCcKOV3dXC5zs+OPO+kYOz0Ie7+O0w1eNFDvUnaiCGv3Tp6wt1d/Q0CrYMXdz8aV31Nw+tdknPo1CXRKgzK9RVZ3Tl5WN8uXaDV6FZg+cAAAIABJREFUPC5GQJsIMLGtzbjwqAyIwOzZs9GyZUvUrl1b9dlRYT0qYvTJJ5+o7psdGhsBWlckO+Hl5aX6RE+ePIlJkybh8OHDqvtmh49HoH379qKAowx5Gtq4ozW5bNky0IaeXo2JbXUiJ4vYvnjxoiC2v/nmG3UmauFemNg2fQGQ7MSQvqVhLUEfeM+XsZixMBSXrqaYPnC+wmwIDOhdCkRuyyiIGBmdidFTgrBlj/7kFBvWdcXk0b5o3shNyvfJbAtCwY5T03JFFvfpc0mC7P71QjJCwzOQnpErClIS0c15WQoCzl0xAgZEgIltAwaVp6RNBKh4Y7169fDss8+qPsC7d+8KyYZdu3ap7psdGhsBIjCpgKObm5vqEz106BCmTp0KIrjZtINAs2bNREHRl19+WcqgSPOdZJdSUvRLijCxrc7SkUVsX7hwQRDbBw4cUGeiFu6FiW3TFgBlli6ZWQ793ytl2oUKtd6wPRqBS0Jw83a6Qj1yN0og8O7bnpg43FdKQVGSrRgRcAcfb4lSYiqq9/FGm+KYONwHL9ZwYXK7AOgTmf3H7XQc+zkBh08kCL3u2LgsQXJTVjcVpGRjBBgBRuDvCDCxzeuBEVAJASpmVqNGDVSsWFElj//v5tKlS+Ilev/+/ar7ZofGRoBOIpD0g4ODg+oT/eyzzzBjxgxQ9iObdhCoU6cOZs2aJU6oyDDyvXDhQty/rx8tzodxYmJbnZXDxLY6OMv2wsS2aRFwc7UWhQJ7d1X/JBaNdPm6CMxdHoqwiEzTBs6tzYpAh7YeCBjlixeeczarn0d1ToTmsIl3sOqTCNV9K+GwSBGgS0dPjBvig2erOEopwKnEPGT1kZCYjfOXU3D4xwRRQPT6H2lITslB5p8kN2dzy4oM+2UEtIMAE9vaiQWPxOAIUHZp5cqVUbZsWdVneuLECSENcOTIEdV9s0NjI0AE4tChQ2Ftrb4O58aNGxEYGIgbN24YG2SdzY60tSkuJEkiwxYsWID58+cjOjpahntFfDKxrQiMT+yEie0nQmSIBkxsmxbGkiVssWBaOVCGrgybvSQUC1eF4d79bBnu2edjEGjxijumjimLhnWLSsFoeMAdLPsoXLfay/QzuXc3L4z8sAwqlneAtRXRMGymIkDZ2pTN/e3R+/j62/tCsogy+pnkNhVJbs8IGAsBJraNFU+ejYYRILkEf39/lCql/tHOb7/9Vkg2nDp1SsMI8dD0hgCR2UuXLsXAgQOlDH3lypWYO3cuQkJCpPhnp49GwNfXF/PmzcM777wjBaLly5cL/6GhoVL8K+GUiW0lUHxyH0xsPxkjI7RgYtu0KJKG8ryp/uj8ZgnTLlSo9cRZd7F4TTjS0nMV6pG7UQKBerWLYub4smje2F2J7kzuY9yMu1iyNhwZGfpdFzY2RdC1YwmMHFAG1ao4wUaChr3JwGv4gtzcB7h1Jx1UcPbrQ/dw8bcUJCTmIDPzAXJyWa5Ew6HjoTECiiPAxLbikHKHjMCjEfjtt98EqV28eHHVIfr666+F5u3Zs2dV980OjYuAk5OT0DLu16+flEkuWbJEEJgREfo8mioFNBWc0n2OMqa7d++ugrd/u/joo48wZ84c3LlzR4p/JZwysa0Eik/ug4ntJ2NkhBZMbJsWxYrlHDF3sh86vuZh2oUKtR4zLUgQmKSly6YdBGrVcMGsCX5o00wOsT1lbjAWrQlHcrJ+C0NTNEmWpG2LYhg31Acv1SoKW1vO3FZilRPJff1WOj7fHyc+N2+nIS0tr/gkS5UogTD3wQhoGwEmtrUdHx6dgRAIDg6Gu7s7ihZV/wjf559/LrSIz58/byBEeSqyEShWrJjQMu7Vq5eUoZBvIlCjovRZTEgKaCo49fDwEOuiZ8+eKnj7t4tNmzaBtN/1LFHDxLY6S4eJbXVwlu2FiW3TIvBsFScETvLD663VT8SgkVKRwKU6lpwwDW39tH7+WWfMnuiHdi2LSRm0kKhZGYZ78caQqKEM+PFDfUASL44OVoLwZlMGAZImOXE6Edv2xOD4qUTExGYhI/MBiPxmYwQYAWMiwMS2MePKs9IgAqT36uLiAkdHR9VHt3v3bkFsX758WXXf7NC4CHh6eoL0jHv06CFlkiRDQv5jY2Ol+Genj0bAzc0NixYtQu/evaVAtHXrVlG88vfff5fiXwmnTGwrgeKT+2Bi+8kYGaEFE9umRbEGEZiT/ERWqdpGmZVDJ94WBSTZtIVAtcp5Gx5vtJGz4TFveRgWrAxDTFyWtoApxGh8vO0wuG9pdHurJEqVtIU1S5MUAs1/X0r3kzt307H7y1js3BeLm7fTkZ6ei+wcJrgVBZo7YwQ0gAAT2xoIAg/BMhC4d+8eSLrB3t5e9Qlv27ZNED1Xr15V3Tc7NC4CXl5egliWJTkxc+ZMkRkcHx9vXJB1ODNnZ2chUdO3b18po9+xYwdobZD8k16NiW11IsfEtjo4y/bCxLZpEaj1vHOe5ERz9Ynt7OwHgthe9UmkaYPm1mZHoFJFR8wJ8EOHtnIkaojUnr8yDNExxiG2KWi2NkXwWqviGDmwDGrXdIadLWdvm2Mxx93LxsHD97F+axTOXUxGSlou6H7DxggwAsZAgIltY8SRZ6EDBJKSkgSpbWtrq/poN2/eLIhtPR/NVx00dvhEBEqXLi2kQIg0kGFUEJUI1MTERBnu2edjEKD7HOmf9+/fXwpGu3btEsS2nk+oMLGtztJhYlsdnGV7YWLbtAjUrpmnpdyqqfpaypRNOWzSHazdxMS2aVEzf+vyfg6YO9kfnd6QQ2xTQdH5K8IQEZVp/slK8FC9ihOG9PMWEkAlituACk2yKY9AQiIR3PH4+NMonL2QjKSUHORwBrfyQHOPjIDKCDCxrTLg7M5yEUhLSxOktrW1teogrF+/HoGBgbh165bqvtmhcRHw8fERxRu7dOkiZZIBAQGC2E5JSZHin50+GgG6xy1duhQDBw6UAtGePXsEsX3x4kUp/pVwysS2Eig+uQ8mtp+MkRFaMLFtWhTr1iqKmRPKouUr6hPbySk5GB5wR5BObNpCoKyPPeZN8UfnN0tIGdjSteGYtyIM4ZHGJLYJVCok2byRO4Z94I0GdYvC2ckKVlZMcJtjwSUm5eDLg/ewemMkLlxJFhIlubnm8MR9MgKMgBoIMLGtBsrsgxEAkJ2dDSsrOl6m/g+UtWvXYs6cOQgKCuJYMAKKIVC2bFlBbHfu3FmxPk3paPz48SIzOD093ZTLuK0KCFBchgwZIuV+Z4RiuUxsq7BIATCxrQ7Osr0wsW1aBOq9SMS2H5o3djPtQgVaxyfkYMTkO9iwjYltBeBUtAvvUnaYP9UfXd/yVLTfgna24uMIzF0eitBw4xLb+Vh4FLPBOx090be7F0gCxt6uCBPcBV0oJrYLj8rEp7uisXlHNG4FZSAzKxekzc3GCDAC+kKAiW19xYtHq2MEcv/cBpZBbK9evVoQ28HBwTpGkIeuNQT8/PwEsf32229LGdrYsWNFZnBGRoYU/+z08QhQ8cihQ4eKzTy1bd++fZg+fTrOnz+vtmvF/DGxrRiU/9kRE9vq4CzbCxPbpkWgfp2imDneD80aqU9s34vPxsjJd7Bxe7Rpg+bWZkegVEk7LJjmj26d5BDbKz+JwNxlYQgJs5zffJQl36VjCbGZULFcHsHNBSbNs9Sv3UzFsnUR+Hx/HGLislmexDwwc6+MgNkQYGLbbNByx4zAPxHIysoSJI8Mouejjz4SxPadO3c4LIyAYgj4+voKYvudd95RrE9TOpowYYLI2CaZHzbtIECbd/kZ2zJGtXfvXsyYMQMXLlyQ4V4Rn0xsKwLjEzthYvuJEBmiARPbpoWRMrZnTCiLFo3VlyJJSMzBiIA7+IQztk0LmgqtfUrbYd5Uf3TpKIfYXr4uL2M7LML4GdsPh5M2Fd5sWxy9upTEs5WdYe9QBDbW6p8AVmGZSXWRkZGLfQfuYcnacJy/lIzMrAecvS01IuycESg4AkxsFxwrbskIFAqB1NRUobFtY2NTqH6e5uINGzZg9uzZ+OOPP57mcr6GEXgkAmXKlBHEdteuXaUgNHnyZFBmMGtsS4H/sU7pHkeZ9AMGDJAyMNLYJmL70qVLUvwr4ZSJbSVQfHIfTGw/GSMjtGBi27Qo1nkhr3hkyybqE9spqTkYPukO1rHGtmlBU6G1v6895k7xx9vtWWNbBbgf6cKrpC3atSgusuZrVHeGi7MVbG2twDLcykbk2s00ULHSvV/H4V58FmtvKwsv98YImAUBJrbNAit3ygj8G4HExETY29vDzs5OdXg+/fRTQWxfu3ZNdd/s0LgIlCpVCgsWLACRBjKM5CYWLlwI+m6xaQcBBwcHkbH9wQcfSBnUzp07RfHIK1euSPGvhFMmtpVA8cl9MLH9ZIyM0IKJbdOi+GINF8ya6IfWTdUntiljctikO1izMdK0QXNrsyNQsZwD5kz2x1uveZjd16McLFodjvkrwhAZbXkZ2w/jYWdnhTo1XQTB3aqJO0p52f0lUyKhlJOU9WBup6mpudi0Mxp0UuDG7TSWJjE34Nw/I1BIBJjYLiSAfDkjUFAE4uLi4OTkBCJ91LYdO3YIoue3335T2zX7MzACXl5emD9/Pt59910ps6TNGiLW79+/L8U/O300Ai4uLli8eDHef/99KRBt375d3O+uXr0qxb8STpnYVgLFJ/fBxPaTMTJCCya2TYviC885C2L71ebFTLtQgdY5OQ8wdOIdrFwfoUBv3IWSCFR5xhGBAf5489XiSnZb4L4WrAwTxHZ0bFaBr7GEhr5l7NCqaTF0aOuBWs87w83VBna2eVrcTHIXbgXk5j7A14fuY+6yUJw5n4ysbK4qWThE+WpGwHwIMLFtPmy5Z0bgHwhERETA1dVVkNtqGx3NJ6Ln4sWLartmfwZGgEghIpZ79uwpZZYkg0L+Y2JipPhnp49GwN3dXUjE9OrVSwpEW7ZswaxZs3R9QoWJbXWWDhPb6uAs2wsT26ZF4PlnnTF7oh/atVSf2KaRDp14W2RJPmAOybTAmbl19SpOmD3JD6+3lkNsU+FIIrdj7zGx/ahQE4ld3s9BkNztXy2OGs86wdXFBra2RWBjwyR3Yb4ep39NwqzFofjuaDzSM3IL0xVfywgwAmZCgIltMwHL3TICDyNA+tb0Eu3mpn6V+X379gnN2XPnznFgGAHFEKCNGiIw+/Tpo1ifpnREvoncjoqKMuUybmtmBDw9PcWGQ48ePczs6dHdG6GmABPb6iwdJrbVwVm2Fya2TYtA1UpOCJzkJ8gxGTZychCWfRSO7BxmtmXg/zifNavnZfK3bSFnw2PmwhAsXB2O+IRsLcGiybFYWQEV/B3Q9GU3NG/sjto1XVDCwxZ2NkUE0W1lxUS3qYG7cSsNMxeF4rOvYpGaxuS2qfhxe0bA3AgwsW1uhLl/RuBPBM6fPw8qtkekj9q2f/9+TJs2DWfOnFHbNfszMAKkGU9ayv3795cyy2XLlgliOywsTIp/dvpoBEqXLi0kamRpr69ZswZz5szB3bt3dRsiJrbVCR0T2+rgLNsLE9umRYCyPqlIYKfX5Wgpj50ehCVrw5GZycS2aZEzb2siR6moaCsJ2us0s0mBwVi8JgykfcxmGgIuztai2GTLV9zQrJE7KlVwhLOTlcjkpo81E90FAvTmrTTMWBiKPV/FIi2d12GBQONGjIBKCDCxrRLQ7IYROHbsGCpUqCDIbbXt+++/x5QpU/Dzz//H3n3AZ1Wkexx/0hsJCQECKTQRVKRYQBRRsIFrWwvocu0VO733Kk0EsWDD3teGLFiwICgiiogIClJCEgg9lZDG/TwTs7IqkuQ95S2/87n5JF7fM3POd4as/M+cZ750umv682OBoKAgE2zfe++9oj87fcydO9cEmFu2bHG6a/r7G4EmTZrIlClTpGfPnq44zZo1yzzwyMrKcqV/Kzol2LZC8ehtEGwf3cgfPkGwXb1RTGkYLtPGNJF/XeH8Qgy90lEPaICZJfkFZdW7cD5tq0CnDnEyfmgjswrYjWPgmC0y64ksKSnhgYcn/rqaO6VhhJzcppZ0PKWWdDglVloeEyVxsSEVQXcIpUv+znfdL4UyfsY2eXvBXsqSeDIRORcBiwUIti0GpTkEjiQwf/58adWqlTRt2tRxpBUrVsiIESPko48+crxvOvRvAV2Ze//990tYWJjjN/rSSy+ZWsrr1q1zvG86PLJA69atRTf2vPjii11h0lBdS6Hs3r3blf6t6JRg2wrFo7dBsH10I3/4BMF29UYxsU6ozBjbVG64pn71TrTo01MfrqilvGsPtZQtIrWkme7nJMjoQWnS8ZRYS9qrTiNab72P1l5/itrr1XGr6mcT4kOlzQkxctopteS0k2OlbasYU7okNFRM2B0WWlG+hKNCYM26QhkzNV3mL9rLhpJMCgS8RIBg20sGgsvwfwEN4U455RRp2bKl4ze7fv16GTJkiLz77ruO902H/i2gm5L27dvXlU1R33vvPVNih9rx3jXHzjjjDFPT/5xzznHlwsaNG2dqv+fk5LjSvxWdEmxboXj0Ngi2j27kD58g2K7eKMbGhMiDE5rKrdcmVe9Eiz792Lwd8sDsDEnPOGhRizRjhUCPS+vKiP6pJgB1+jh4sFz6jNgsjz+7w+muA7K/iPBgadYkUjqcXBF0dzy1ljRKjZTI8IrV3PoV6HW6P/x0nylL8uU3uVJOVZKA/HPCTXuXAMG2d40HV+PHAvp6/Nlnny1t27Z1/C63b98uAwcOFA3XORCwUmDYsGEyYMAASUhwfjOhTz75xJTYWbp0qZW3RFseClxwwQUyZswYOf300z1sqWan65ycOXOmFBUV1awBLziLYNuZQSDYdsbZ7V4Itqs3AhpaPTShqdx9S8PqnWjRp194Y6dMmpkh6zccsKhFmrFC4KZe9WVYnzRp3jTSiuaq1YZuGNlv1BaZ9zKbhVcLzqIPa3mSxmkRZhNKXbGv5Uu0Fr+p0/1b6ZKQEA27LerQB5opLz8kT7+0U6Y+nCG/bikSfauAAwEE3BMg2HbPnp4DTEDDFn01342wp6CgwKyqffLJJwNMndu1W+C+++6TwYMHS3Jyst1d/al93Qx1+PDhlNhxXP7vO7zyyitl5MiRrjzEKysrM7/r5syZI4d8+G8ZBNvOTGqCbWec3e6FYLv6IzBlVBPpc0eyhIc7X37gnYV7ZPz0bfLdDwXVv3DOsE3gvtsbyuB7UyW5QbhtfRyp4W2ZB2Xg2C3y2tu+W2LMcTSbO6wTHyqtW8XIKW1ryaltY0zN7gb1w03ZktCwilrd/h50Fx4ol6mzM2TO09tlz75Sm8VpHgEE/k6AYJv5gYBDAjfffLP06tVLzj33XId6/L0bDXj69+8vs2fPFg1+OBCwSkDntT600Y1RnT60tvbQoUMpseM0/FH6u/HGG824tGjRwvEry83NlX79+snTTz/teN9WdkiwbaXmkdsi2HbG2e1eCLarPwIjB6RJ/zuTpXZcaPVP9vCMj5fslzFT0mXZijwPW+J0KwWG9U2VAXeliNZjdvpY98sBGTp+i7y7aK/TXdNfFQWio4OlRbMo6XByrJzRIVbat6slDRuES3hYcEXYbcqXVLExH/rYhk0HZPjEdHnr/T1SVs6ybR8aOi7VzwQItv1sQLkd7xW46KKL5I477pBLLrnElYvU0gBadzYvj78ouDIAftrp1VdfbTYmPfHEEx2/w4yMDFNi59VXX3W8bzo8ssC9994rgwYNktTUVMeZsrKyzJx4+eWXHe/byg4Jtq3UPHJbBNvOOLvdC8F29Ueg350pMvCeFGlQ3/mNob/+Nk9GTE6Xjz/fX/0L5wzbBB4Yqav4G0pEhPPp5Irv8mTEpHT5iDlh2/ha3bAG2lqnu1OHWOl0Wqx0OClWUhqGm/mjQbeWLgly/oUQq2/TtPeilk96KEP0AQwHAgi4I0Cw7Y47vQaggG4cqbWIr7nmGlfuXmvOTps2TbTeNgcCVgnoAxutc92+fXurmqxyO7o5oL6J4Ourc6t8wz7yQV3Br+NSp04dx6/4l19+MRvlvv322473bWWHBNtWah65LYJtZ5zd7oVgu/ojcNt1STL0/lRp2tj5esoaDg0Zv0XeY3Vu9QfOpjPCwoJk5vimctfNDV0JIz9dmiOjpqTL0uW5Nt0hzdotEB0VLK2Oi5ZzOsfLuWfVllYtoyUuNsSs6Pb11dw5uaUycnK6PPVithwoYidJu+cS7SPwVwIE28wLBBwSSEpKkgkTJsitt97qUI//282LL74oEydOlPXr17vSP536p0Dnzp1l3Lhx0qVLF8dvUEvs6OpcLbFTUlLieP90+GeB4OBgmT59uuiq7dBQ519XXrlypam7/uGHH/r08BBsOzN8BNvOOLvdC8F29Ufgmsvryoh+aSaIcvrYubtEBo7eIs+/vtPprunvCAINksJk+pim8n9X1XPF6N2Fe2QcddddsberU63Vfkb7WOl+boKc1TFOGiRp2ZIg0YcowcG+t5T7s2U5MmZquiz5KpeNJO2aNLSLwN8IEGwzPRBwSCAoKEimTp0q999/v4SFOf9q5+LFi83K2mXLljl0x3QTCAJagmTy5MlmY1Q3Dv0zpW8i7N7NhkJu+P+xT7eCwsrr+Oijj8zvua+++sobOGp8DQTbNaar1oluzdfvv//e7E2wcOHCal0vH66ZAMF29d26nRMvYwY2ko6nxlb/ZA/PKCs7JP1HbTEbsunPHO4LtDsxRiYObyz/OC/BlYt59pVsU+phw6YiV/qnU3sF4muHymknx8qF58ZL1zNrS5NGkRIZWVGyxFfKlZSWHpKx09Jl1hPbJS+f/azsnTG0jsCfBQi2mRUIOCigr8jrK/r6l2mnj59++sls6Pbee+853TX9+bFASkqKeWCjG6O6ccybN88E6xs2bHCje/r8g0CrVq3MeLi1l8Brr71m3oz58ccffXpsCLadGT6CbWec3e6FYLv6I9DhpFoyYVhjOb9LfPVPtuCMiTMzZMajmbJvf6kFrdGEpwIXdI2XMYMayekuPOjQa3/wsUyZNidTduzk7TxPx9Lbz68dFyJndoyTq/9ZTzp3jJP6dcMkPDxIQkO8fxW37gswZuo2WbaCkjnePs+4Pv8TINj2vzHljrxYQMuQaLjcrFkzx69y3759psb3M88843jfdOi/AtHR0WZTUt0Y1Y3jP//5j4wdO1ZWrFjhRvf0+QeBrl27mtI0Z555pis2jz/+uDzwwAOydetWV/q3qlOCbask/74dgm1nnN3uhWC7+iPQvGmkPDCqiVx5cWL1T7bgjCdfyJYHZmXIpq2s0LWA0+Mmru1RT4b3TZPjjo3yuK2aNDBy8laZ+XiWFBRSv7gmfr56TqPUCPOWgJZGanNCjMTEhHj1Ku6CwjIZNmGrzH1uhxws5m0TX513XLdvChBs++a4cdU+KnD55ZfLyJEj5aSTTnL8DrQesb76/NBDD0lREX9RcHwA/LjDKVOmmBI7ERERjt+lv9RUdhzOpg51c9wRI0aIrtx245g0aZLMmDFD9u7d60b3lvVJsG0Z5d82RLDtjLPbvRBsV38EEhNCZfq4pnLjNfWrf7IFZ7y7cK+Mm54u3/1QYEFrNOGpQP+7UmTgPSmSVM/5UoolJYek78jN8ugz26ld7OlA+uj5kRHBcnr7WLn12gbS9cw4SawT5rUB91sL9sj4Gdvk+zX87vLR6cZl+6gAwbaPDhyX7ZsCnTp1kvHjx4uuanTj0E32tGxEZmamG93Tp58KDBo0yLwNUK+e85sKpaeni/avJSg43BfQBxw6HsnJyY5fTHl5uenbHzYTJdh2ZvoQbDvj7HYvBNvVH4GQEJEZ45rKPbc0lBAXSgAsX5knIyany+Il+6t/8ZxhqYDWOH5gZBO5/46GEhEebGnbVWlMNxMdMHqLvMBmolXh8uvP6O+l006JlbtvbijnnR0vdeJDJTTUu0qUZO0olkFjt8jL/97Fgxi/no3cnLcJEGx724hwPX4t0LRpU9HVrT169HDlPt944w1Tf/aHH35wpX869U+BG264wbwN0KJFC8dv8MCBA6Zu/dy5c0WDTQ73BHSD3IkTJ0qfPn0kKsr515V1lbbOhWeffdY9BIt6Jti2CPIozRBsO+Psdi8E2zUbgWF9U0VX6mp45PSxeWuRDB63Vd54j42hnbb/Y3+6sd+D45rKTb3cWb3/w08FprzDgo/2uU1B/14iEB4eLOecWVvuu62hqccdEx0iwc4/czmixqTf9gjYyx4BXjJjuIxAECDYDoRR5h69RiAyMlJmzpxp6hFrCOT08c0335gyAR9++KHTXdOfHwtceOGFMnr0aDnttNNcuUt/KT/hCp6FneqK/enTp8v1119vYatVb8qfNsgl2K76uHvySYJtT/R851yC7ZqN1S3/lyRD+6TKMU0ia9aAB2cVl5SbVbqPPrNDysqoVesBpcentjsxRiYNbywXnpfgcVs1aeCjz3RDvnT58pu8mpzOOX4soA/dbrk2SW6/voE0aRThNRtM/ufjfTJ22jZZ8R1z1o+nH7fmZQIE2142IFyO/wuMGTNG+vbtK3FxcY7f7P79+/+7gaTW3OZAwAqBk08+2azU7d69uxXNVbuNV1991byJsHbt2mqfywnWCXTo0MGMw/nnn29do9Vo6dNPP5VRo0bJ0qVLq3GWd36UYNuZcSHYdsbZ7V4Itms2AhdfkCCjBzaSU9vVqlkDHp4149FMmTYnU7J3lXjYEqd7InD5RYkyqn+atGsd40kzNT73xTd2ycSZ22T9hgM1boMT/VdAV2p3PydBhtyfKh1PjTW1t90+MrKKZfC4LfLKW5QjcXss6D9wBAi2A2esuVMvEbjttttk6NChomVJ3DhY3eqGun/3qfWUp02bJr169XLlRr/++mvzJsLHH3+bSaJoAAAgAElEQVTsSv90WiFw9dVXm3E48cQTXSHxpwccBNvOTCGCbWec3e6FYLtmI3DqSbVk4rDGckGX+Jo14OFZb73/2yZsP7IJm4eUHp3u5saReuHTH8mUaY9kyk4ecHg0jv5+stbeHtEv1fy+0lIlbh66dmzstHSZ+XiW5OaVuXkp9I1AwAgQbAfMUHOj3iLQrVs30VXbHTt2dOWS3nzzTbOBJXW2XeH3y07DwsJMCYq7775bQnRnF4eP7OxsGThwoLz44ovCmwgO4x/WnT6w0xrXiYmJrlzEgw8+aB6w7Nixw5X+reyUYNtKzSO3RbDtjLPbvRBs12wEUpPDZeroJvKvK5zfGFqveNWaAhk+cassXExt5ZqNoOdnhYcFy/RxTeSumxtISLDzK2HLysRsxPfwU1lSUsKbpp6PqH+3cHKbWjJ6YJopm+P2yu3nX98pWmv75428aeDfs4678xYBgm1vGQmuI2AEWrZsKQ888IBocOHGoYG2bvS3YMECN7qnTz8V0GBZv7TOstOHhtm6Uljr1+tmkhzOC0RHR4sGy7fffrsr+weUlZWZ+ffwww9LaWmp8wAW90iwbTHoEZoj2HbG2e1eCLZrNgKhIUEybUwTuefWhhLqwuv9+3JKZcCoLTLvlWyhel7NxtDTs5o3jZIpoxrLFRe788B6e3axDByzRV56c5ent8L5ASLQpVNtGTUwTc46Pc6VhzGVzEu+ypVRD6TL51/mBIg8t4mAuwIE2+7603sACugGkjNmzJDevXtLsAtbOBcWFpo623PnzpXy8vIAHAFu2Q6Ba665xoTLrVq1sqP5o7b55JNPyuTJk2Xz5s1H/SwfsF5Ax139L7nkEusbr0KLmZmZJth+5ZVXqvBp7/8IwbYzY0Sw7Yyz270QbNd8BPrflSwD70mRpHrhNW/EgzN100Be5/cA0MNTL+gaL2MHNTK1i904VnyXL8MnbZWPP9/vRvf06aMC1/aoJ8P6pMpxx0ZLkPMvGhi19MyDMnjsFnn17d0+qshlI+BbAgTbvjVeXK2fCAwZMsSEy7yy7ycDym1Ip06dTImbrl27uqKhGweOHj1avvjiC1f6D/ROL7/8crNxY7t27VyhWL58uYwcOdJv6qwTbDszjQi2nXF2uxeC7ZqPQM/L6sqI/mnS+vjomjfiwZkvvrlLJj2YIes2FHrQCqfWREBDgrtuaSiD702RtJSImjTh8Tn/nl9RZ331Wuqse4wZQA3UigmRkQPS5M4bG0hsLedLJCp1adkh87bBQ49nBZA8t4qAewIE2+7Z03MAC/Ts2dOsbm3durUrCrrJntb5XrZsmSv906n/CTRu3FimTJliNhB049i5c6cMGjRInn/+eepsOzwA+uaJhsp9+/aV2rVrO9x7RXevv/66TJgwQdasWeNK/1Z3SrBttehft0ew7Yyz270QbNd8BHSl7oShjeXcs9z53a6BptbZXvARdbZrPoo1O7NWTLBMG9NUbr++gbjwgqm5aA0Fp87JFC1JwoFAdQR0M8lxQxrJ+WfHu7ZqWzc+1XCbAwEE7Bcg2LbfmB4Q+JOArmqcOHGi/OMf/3BFZ/fu3ea1/eeee44Q0JUR8L9OQ0NDZerUqXLPPfeIbibpxqG163UTyz179rjRfcD2mZqaajZt1HI0bhxaY13nno69/m7zh4Ng25lRJNh2xtntXgi2az4CDeqHmzrb/3dVPVfCoYPF5TJ47FZ59JntUlLK5oE1H8nqn6nB4IShjeS8s+Orf7IFZxSXHJJBY7bII89sl1LG3gLRwGpC97Ifcn+q9O2dIokJoa7c/LyXs+Xm+ze60jedIhBoAgTbgTbi3K9XCMTExJgQRjdac6POtiJo//qVnZ3tFSZchO8L3H333TJ48GBJS0tz5WZ0Q9Rx48bJihUrXOk/UDvVB3T6Bkj79u1dIdB9A/r16ydPPPGE3zyoI9h2ZioRbDvj7HYvBNs1HwGtTztxWGO5/45kiY4KrnlDHpz55PPZMnl2hmzeWuRBK5xaXYHbrksywWCzxpHVPdWSz2/cfEAGj9sqb73PYgVLQAOwEX3jZLw+nDnLnYcz7/xnj1x+w/oAlOeWEXBegGDbeXN6RMAI9O/f36yaTkpKckVkyZIlpibu559/7kr/dOp/At27dzd1rjt27OjKzaWnp5tg/bXXXvObgNMVyGp0GhQUJLpngP4+c2vPAC0/MmzYMHn//ferceXe/VGCbWfGh2DbGWe3eyHY9mwEbv6/JBl2f6oc09SdgHP5yjwZMTldFi9hA0HPRrLqZ0dGBMvU0Y3lzpsaSmioO7vvLfpkn4yZuk2+/jav6hfOJxE4TEDn8eSRjeWumxpKeLjz83jJVzly9qU/MiYIIOCAAMG2A8h0gcBfCVx88cUmWHZzlaMGUo8//riUlJQwSAh4LNC8eXNTZ/uKK67wuK2aNKAlKXTl8MyZMyUvj78I1cSwuufUr1/flCG5/vrrq3uqZZ9/++23zcalq1atsqxNtxsi2HZmBAi2nXF2uxeCbc9G4Owz4mTs4EZy9hnu1NnOyy+TgWM2y1MvZEtZuWf3wtlVE2jXOkYmDW8sF56bULUTbPjUI09vlymzM2RbFvW1beANmCavv7q+DOuTKi2bRzl+zz+uL5TWnf3nv00dB6RDBKohQLBdDSw+ioCVAk2bNhWtCdyjRw/RVY9uHC+88IJMmjRJ1q/nNSk3/P2tT62trbWOtSSJW3W2/THk9OZ50q1bN/Mwwa1V+vowo7Kskm4g6i8HwbYzI0mw7Yyz270QbHs2AikNwmXK6CbS60p36mzr1c95artMfThTtmUd9OxmOPuoAvpXkpv+lSRD+6RKc5dW6RcXH5JBY7dQW/2oo8UHjibgZq34bZkHpVG7lUe7RP49AghYIECwbQEiTSBQEwENszUQ6tu3r8TGxtakCY/P2bx5swwdOlTeeOMNKS9nGYzHoDQgd9xxhylN0aRJE1c0duzYIYMGDZKXXnqJOW3zCOiGoVp65v7773ftd1hOTo4pg/LMM8/4VfkZgm2bJ+9vzRNsO+Psdi8E256PwJhBjaTfnckSWyvE88Zq0MJ3q/NNORItT3GIPSRrIFj1U+LjQmXqmMaiJWhCgt1ZeLPul0IZMn6rvLdob9UvnE8i8BcCdRPDZPqYJqIrt51eR7ZjZ7E0bPUN44IAAg4IEGw7gEwXCBxJ4KqrrpKRI0dKmzZtXEOaPXu2WWWbmZnp2jXQsf8IdO7c2Wzg2KVLF9du6uGHHzZzOiMjw7VrCISO27Zta9740M0j3TqWL18uI0aMkMWLF7t1Cbb0S7BtC+ufGiXYdsbZ7V4Itj0fgasvrysj+qXJicdFe95YDVooLT0ko6aky+wnsqSgkIUYNSCs8ikXnpcgYwamSYeT3Vl0oxf67/m7ZfyMDFm9tqDK180HETiSgJbV6dM7WaIind0Ad9/+Uqlz7NcMDAIIOCBAsO0AMl0gcCSBFi1ayOTJk+Xyyy93rRzJ6tWrTTC0YMECv1rxyKxzR0CDIq25fMMNN7g2p7/77jszpxctWsSctmka6Bsnd911l1kd36hRI5t6OXqzulJbf4du3Ljx6B/2oU8QbDszWATbzji73QvBtucj0Pr4aJk0orFcfEEdzxurYQvvf7hPxk1Pl29W5dewBU47mkBYaJCMHtRI7rutoWur83VF/vgZ22TmY1myP7f0aJfMv0fgqAJ9eyfLoHtSpUFS2FE/a+UH8gvKJLbJciubpC0EEDiCAME2UwMBFwVCQkLM6lZ9lT8mJsaVK9ESJBpEzpgxQ3bt2uXKNdCpfwloeRstD5GYmOjKjZWWlpoSGfo2Qn4+fwG2YxCSkpLM741rr73WtQcYuumtzrU5c+bIwYP+VXeVYNuOWfvnNgm2nXF2uxeCbc9HICIiWKaObiJ33dRAQkPdKU+xZ2+pDBq3RZ57hU0kPR/Rv27hpN82jezu4qaR2btKZMDozfLiG/ydxK5xDrR2r+tZT4b3TXN8A8niknKJSP4q0Li5XwRcESDYdoWdThH4XcCtAOPwMWDVNjPSSgEtTaHBcocOHaxstlpt6RsIY8eOlW++obZdteCq8GFdrX3FFVeYMkpajsStY+3atSbYnj9/vluXYFu/bv3vgo7pzJkzpaAgMF7/Jti2bQp7VcME29YMx23XJcmQ+1OlWeNIaxqsQStPvrBDHpiVKZu2FtXgbE75O4HgYJF7bkmWgfekSGpyuGtYH366X8ZMTZevVua5dg107F8Cl1+UKKP6p0m71s4uItO3D4LrL/MvTO4GAS8VINj20oHhsgJHICUlRaZMmSK9evVybeXjoUOHTJihKzB18z0OBDwRSE5ONnNawwQNQd049u/fL8OGDZOnn35aiouL3bgEv+2zdu3aMnHiRLntttskPNy9v/y+8MILpsb3+vXr/c6aYNuZISXYdsbZ7V4Itq0ZgVPb1pIJwxpJt3MSrGmwBq388usBGTZxq7y9YI+w53kNAP/mlGZNImXKqCZy1SXuvG2nl1Z+SGTq7AyZ8WiW7N5bYu0N0lrAClx0fh0ZPTBN2p9Uy1ED3RsgrOGXjvZJZwgEqgDBdqCOPPftVQJatmHgwIGir/e7dfz6668mCHzzzTdFy5NwIOCJwODBg2XAgAGiwZFbx7vvvivjx4+Xb7/91q1L8Lt+9UHFlVdeaWqYu7laWx9W6GrtRx55xO/KkOikIdh25o8OwbYzzm73QrBtzQhERgbLlFGN5c4bG0pYmDsPrfVOHpu3XR6YnSnpGf5VgsqaUapZKyHBQXL3LQ1dX62dtaNYBo7ZIi//mzIkNRtJzvorgfO7xMvYQY3k9PbObohadLBcolIpRcKsRMAJAYJtJ5TpA4GjCHTu3NnU2u7SpYurVs8//7xZAfnzzz+7eh107vsC3bp1M+VITj/9dNduJi8vz5TLmDt3rhQV8dqyFQOhQeADDzxgNgcNDQ21oskatbFq1SoZPny4LFy4sEbne/tJBNvOjBDBtjPObvdCsG3dCNzcK0mG9kmV5k3dK0eyZdtBGTZhi7z2zm5WbVs0tK2Oi5bJIxrLJd3c2xxUb8VsEDotXb75nv1RLBpamhERrRk/ZmCanHaKs8E2m0cy/RBwToBg2zlrekLgiAJRUVHm1f4777xTIiPd+8vC3r17ZdSoUaZ8A0EgE9YTgXr16plyJBqABmvhRpeODz74wNTaXr58uWjJHY6aC+hqbQ2I9M2O448/vuYNeXimjuNTTz1lAvZNmzZ52Jp3nk6w7cy4EGw74+x2LwTb1o1AmxNiZOLwRqKv9rtUaczczDMvZ8vkhzJk42YeWns6uqEhQTLg7hTpe2ey1K8b5mlzNT6/rOyQjJ+RIQ/NzZKc3NIat8OJCPxR4J//qCOjBjQS3RzVyWPn7hJJOn6Fk13SFwIBK0CwHbBDz417m8Dll19uVpeedNJJrl7a0qVLZcyYMfLpp59SksTVkfD9zu+66y7RkiSNGjVy7WYOHDhg5rOWrAiUDfHswq7cD+Bf//qXqw8r9u3bZ0o3zZs3z29/RxFs2zWL/7ddgm1nnN3uhWDbuhEICREZO7iR3H97stSKCbGu4Wq2tGNnsQydsFVefH2XlJbx0LqafP/z8fbtasmE4Y3lgi7xnjTj8bk//VIoQ8dvlfcW7fW4LRpA4HCBXlfWk+H9UuWEFtGOwugmt8ecSjlER9HpLGAFCLYDdui5cW8TqFzhev3110uI/s3BpUNXQz766KMydepUSU9Pd+kq6NYfBPQhjb6J0L17d9c2kVTHJUuWmHD7888/99sg1O75or+TevfubQLlxo0b293d37a/aNEiM55ff/21q9dhZ+cE23bq/t42wbYzzm73QrBt7QhcdH6CjB7YyPGN2P54F6++vVsmztwma9cXCi9k1WyMo6OCZXjfNLn7lgZSO8698mI6fpWr8H/dwir8mo0mZx1JQOvHD743RdJSIhxFWvNTgbQ5+3tH+6QzBAJVgGA7UEee+/ZKgVtuucVsiHbMMce4en179uwxNb+1JAmrXF0dCp/uXGsw6+aN9957r8TEOPv63+Fwuhnq7NmzZfr06ZKZmenTpm5cvJYg6dixoxnLc889141L+G+fZWVl5nfTQw89JLm5ua5ei52dE2zbqUuw7Yyu9/RCsG3tWNRLDJMpo5rIDdfUFxcrjUlBYZlMmJEhj87bLrl5ZdbeZAC0pmN3xcV1ZUTfVGl7onv/jabUObllMmjsFnn6xWwpK2cFfgBMP8duUUsmTRjaWPr0ThZ9kOPk8fmXOdLlsh+d7JK+EAhYAYLtgB16btwbBZo0aSKTJ0+Wnj17uvqqv9p8//33ZvO/BQsWiIZJHAjURODiiy82ddvbt29fk9MtO2fHjh0yYsQIeeGFF6S4uNiydgOhocTERBMm33zzza7uAaDWa9asMTW+33//fb+mJ9h2ZnhZse2Ms9u9EGxbPwJ33NBAhtyfKk3SnF0B+cc7Wb22QEY9kG42HSwnEK3WQB/bLNJsGKnhtpv10vWiF32yX8ZMTZevv82r1j3wYQSOJhBbK0Rmjm8qt1ybdLSPWv7v31qwR668cb3l7dIgAgj8WYBgm1mBgJcJ3H777TJkyBBp2rSp61f21ltvmVISGnLrqlcOBKorEB8fbzb507cRdAW3m8fHH39sNpJctmwZG0lWcSC0BMltt90mgwYNcv13kv4OmjNnjkybNk0yMjKqeAe++TGCbWfGjWDbGWe3eyHYtn4Ejjs2SiYNbyz//Eei66Ho86/tlEkPZcgvGw8Ia32rNtZRkcEytE+q3HNLQ0mId/e/zQ4ePCSjpmyVR57eLgWF/F2jaiPIp6oqcFKbWjJpeCPpfk5CVU+x7HNPPL9D7uj/q2Xt0RACCBxZgGCb2YGAlwlo/VpdtX311Ve7vmpbV2o/8cQTpoTD5s2bCQO9bK74yuX06tVLhg8fLieccIKrl6zB6KxZs8x8zsrKcvVafKFzbypBol5bt241m5G+9tprvsDn0TUSbHvEV+WTCbarTOXTHyTYtmf4Bt6TIgPuTpH6dcPs6aCKrebll8m46dvk8ed2SH4+bxgejU1LkFx+UaKM6Jcm7VwuQaLXumxFroycnC6fLs052qXz7xGotoCWTBrWJ1VaHBNV7XM9OUHrxutbCPq7iQMBBOwXINi235geEKi2gDet2s7PzzcrJHVDSa29rZtLciBQHYHU1FTzsEYD7mA3C3KKmEBbS+y8+OKLUlTEBkV/N44NGjQwK9x1Q9vIyMjqDLnln9XfO/PmzTPzaOPGjZa3720NEmw7MyIE2844u90LwbY9I9D+pFoyfkhjuaBrvOurtlf/WCCjpqTLgo/2SVkZ/516pBHXkiPHt4iWicMayaXdE12tka7XWFp2SCY/lCEzH8+SfftL7ZmotBqwAuHhQTJ5RBO5++YGEhHhbH1t3QOg38jN8sTz2QHrz40j4KQAwbaT2vSFQBUFUlJSZNKkSSYIdLt8g17y7t27TTkJ3Uxy//79VbwLPobA7wJazkI3RvWGEjsrV66UMWPGyKJFi6gff4RJGh0dLf379zcbf9arV8/1qaw10nW1tj6QCISySATbzkw5gm1nnN3uhWDbnhEIDQmS4f3SpM8dDSW+trvlLPQO53+wVyY8uE1Wfp8vVM/76zHX1fWjB6XJjdckOb6R3l9d0ao1BTJs4lZZtHifPZPUxlbDwzRGESkpPSSs+bER2oOmz2gfJ+OHNpJzOtf2oJWanbp+wwEZMn6LvLtwb80a4CwEEKiWAMF2tbj4MALOCWgpEi3f0Lp1a+c6/ZuedKWrhu26+V5eXh4rt71iVHznIho1amRW215zzTWur9pWtXfeecfM52+//TYggtLqzJSwsDC57rrrTF3tli1bVudUWz6rq7WfffZZ83Dtl19+saUPb2uUYNuZESHYdsbZ7V4Itu0bgdNOiZXxQxrJeWe7v2pbV2prTdvpj2TJ5vQiwsY/DHtMdIj0vSNZ7rmtoSTVc7d8jF5aaekhmTI7U2Y+nil79vneau2e/6wrEeHB8ukX+2XXnhIpLiHgtu83TfVb1gdvWke+T+9kqeNCHfn3P9wrY6dVPGjjQAAB+wUItu03pgcEaiQQFxcn48ePFy1L4nYZgMobyMzMNOGShtu5ubmE2zUa2cA96aabbpJhw4ZJ8+bNXUfQVb86j6dMmSI///wz4fZvI6JviFx00UUyYsQIOfXUU10fJ70A/b2jq7VfeeWVgBkngm1nph7BtjPObvdCsG3fCISEiAy9P1X69k6ROgnur9ouPFAuDz2eJQ8/lSXZu0oIt38bei3JcMv/JZma6M0au1tarHI2rvguT0ZMSpePPvfNN0GnjWki997aUNasK5SnXsyWhYv3SfZODbjLmXf2/cqpUstackdLJU0Y2ljO7xJfpXOs/JCu4NfyOtPmZMqOncVWNk1bCCBwBAGCbaYGAl4scO6555p6wGeeeaboRm7ecGhJAA0Dn3vuOVOWhJrb9o1KSEiI6OpZ3cSzpKTEvo4cajk5Odms2taQQe/N7aO4uNjUjn/ooYdk27ZtAROaHsldx6RTp06mTEvXrl3dHh7Tvz6AeOqpp8zvnE2bNnnFNTlxEQTbTiiLEGw74+x2LwTb9o7AKW1rmdf9u5+T4Hqtbb3TfTmlZiXwky/sMHWbA71MRFhYkFx5caJZvdrmhBh7J0MVW9fVzZNmbpNZT2yX/Tm+t1pbb1OD7ftvTxb11Tm2em2BPP1StuhK3e3ZJVJcTMBdxelg+cdqx4XKmEFpcvt1DSQ62tna2noz+QXlMnCM1tfeQVkky0eXBhH4awGCbWYGAl4soKsntc5t3759JSkpyWuuVDeRnD17tgmcNOgOhJq3TuLrBosRERFy4oknynnnnSdfffWVfPbZZ05egm19aSkSLbGj9+YNx4EDB+SRRx4x81lXBgfqXNZQu2PHjuZBms45b3mQpqVHhgwZIm+//bY3TBfHroFg2xlqgm1nnN3uhWDb3hHQPaH79k42q4Eb1A+3t7Mqtr5rd4lMeThTnnk52wSngRpua+h6Wfc6MqxvqpzUulYV9ez9mI7F51/myOgp6bLkq1x7O7Ox9cOD7cpu9N7Wri808+69RXsla0exHCwuJ9y0cRz+2HRoaJBc17O+DL4vRVoeE+Vgz793pW8jDJ+ULh/76NsIrqDRKQIeChBsewjI6QjYLdCkSROZMGGCaM1tb9hIsvJ+NRB85pln5OGHH5Zff/1VSkt9c8WF3eNXnfYrA22ta6xlO3r06CG6qlhrHb/++uvVacprP1tZYkc3k4yKcuc/OP+IU1hYaMLtOXPmSEZGRsCF2/p75bTTTjOhtr4lovPQGw59S2H69Okyc+ZM2bVrlzdckmPXQLDtDDXBtjPObvdCsG3/CGh5i0nDG8tVlyVKSLB3vGG4c3eJTH04U+a9kh2QK7d1c8NLulWE2ie38Y5QW2diTm6ZjJy8VZ58IVuKDpbbPzlt6uGvgu3DA+4Nmw7I6+/sljfn75FftxSZe9W64hz2CQQHB8nZZ8TJmEGN5KzT4+zr6G9a1ocbT7+YLZNnZcimrUWuXAOdIhCIAgTbgTjq3LPPCVx++eWm5u1JJ53kNSspFVFXty5evNiUCVi6dKkJYSlNUv3pVRloH3PMMdKrVy9TqkM3W9Rj69atfhVs6z1pmQstd9G5c2evmc86d+fNm2fKkmzYsMGUfwmEQ0vdaKmjkSNHytlnn+01obb+HlmyZImMGjXKfA+0g2DbmREn2HbG2e1eCLadGYFrr6pnQtTjW0Q702EVetGyJLPmZskTz2dL9q7igFk5GxERLFddnCiD70uV1id4z3iUl4u8+vYumfRQhlnZ7MvH3wXbh9+Xboy5eMl+ee7VnbJiVb7k5pVKCRtNWj70uiajVcsYGTekkVzavY64tUYjv6BMBo/dIk+8kM2DDMtHmQYROLIAwTazAwEfENDNI3XztHvvvVcSExO97orXr19vVlZquYCcnJyACQU9HQgt/6AlR4477jjREh09e/aUxo0b/0+z/hhs6wphnc99+vQxNW695dAwW1fGz5gxQ3744Qe/qGv+d7b6e+XSSy81pT70oZk3Hfp7REPtJ598UvTtkEA7CLadGXGCbWec3e6FYNuZEYivHWpqbd/6f0kSGekdb/7onRcUlplg+5Gnt8uWbQelrMx/V83qdjyxtULkxn/VN/WfvWWjyMoZuCW9SIaM3ypvvLfb5x8yVDXYrrx3LUmyfGW+PPdqtnzyRY7oGwXFxYekrNx/56Mzv3lEQkKC5ISWUTKyX5pcdmGi6Gapbh2fLc2RUVPS5Yvlvltmxy07+kXAEwGCbU/0OBcBBwU08Bw/frwJQHWVpbcd+fn58uqrr5qSDuvWrWP19hEGSGsXa7CrZThat24tN9xwg1x22WVSv379vzzDH4NtvdFmzZrJxIkT5aqrrvKqEjt6bStWrJBJkybJhx9+KEVFRX73FoK+IVC7dm1T7kYflmm5I2869AHDCy+8IA888ID8/PPP3nRpjl0LwbYz1ATbzji73QvBtnMj0LljRRmALp1qu7Zi8q/uVsPsdxfulWmPZMp3q/NFNy/0t0PDveSkMLn/jmS58ZokSawT6lW3qMHu9EcyZdbc7bJrj+9viF7dYLtyMLRUhZYpefO9PfLWgj2ycfMBKSoqN6u4ybirP2XDQoPk1Ha1ZET/NOnWNd6E3G4d+pBiyqxMefCxTNGV+hwIIOCcAMG2c9b0hIDHAueff75ZxXjGGWd4TcmAP97Ujz/+aOpuz58/39TFpfZ2hZCGieHh4aI1prt06SK33HKLGcdatf6+7qG/BttqcsUVV5gSO+3atfOakiSV81ndZ82aZR7W7Ny502/eQtCHYscee6zZkFZruGvA7U2HliDR1fK6weiCBQu86dIcvRaCbQ5ILN0AACAASURBVGe4CbadcXa7F4Jt50ZAX/+/++aGMvCeFElLiXCu4yr29P2PBSZcXfDRPlMSQktj+Pqhf5kPC9dwL1YG3ZMi3c6Jl8gI71kxr74a2C5avE/GTdsmX3+X5+vk5vprGmwffvP6NsHyb/PljXd3m1XcutlkcXG5lJYdCtgNT6s6OfTtBH0zpFvXBBlyX4p0ODlW9P/n5qHldYZO2CrzP9jr5mXQNwIBKUCwHZDDzk37qoCGo3fddZcMGDDA1GDW1b/eeOimbx9//LHMnj1bli9fLrqaOxAD7srV2VpupHnz5nLxxRebFconnHBClVfd+3OwraUwhg4dalYNJyQkeN1UPnjwoCmvo3W3V69eLfrPvlpDXn93xMTESLdu3czvj/bt23vlw7G8vDwZO3aszJ071/zeCNSDYNuZkSfYdsbZ7V4Itp0dgfr1wmT8kEZywzX1JSLcuwJWldifU2rqHc99Plt+3XLAp+sd6+rUhNoh0uOyunLfbcly3LHesSn3H2fctqyDMmzCVnnlrd1+UwrGimD7cKedu0rkk6U58vq7u2XFd3lmw1N9s0DfNtBV3hy/C4SGBklSvTC57boGctt1SZLcINx1nvLyQzLn6e0ybU6mZGQVu349XAACgSZAsB1oI879+ryArrAcNmyY3HHHHV632vKPuBpSLVy40IRU3377rRQWFpqA21fDwapMHg2ztXa2rs7W0OSss84y5WM6deok8fHxVWnifz7jz8G23miLFi1MiR1dva0lWrzx2Lx5szz++OPy2muvyfbt203tbV+Zwzof9cFKy5YtzUMxfbBSp04db2Q2q+IDvQRJ5cAQbDszRQm2nXF2uxeCbedH4Owz4mT0wEai34ODvXMRhq7envVElvzn432yd1+pT230pivjdYPI9u1qSd/eyXJB1wSJjvK+hwg687TMxoOPZxlrDW/95bA62K500RD71y1FZl7OX7RXflxfKHn5FSF3aWlgh9z6ICcmOtj8XtEa8meeFmf+HHjDseHXA6Z+vJaX4UAAAecFCLadN6dHBDwW0Jq4uqrx6quvNqGVtx8acOsK7meeeUa+/vpryc3NNeFguT+8AypiVs5rKKthdmxsrCmtoXWzL7zwQo9X1vt7sK1z9x//+IeMHDlSOnTo4JWriPUaNXTV2tuPPvqomct79+716oBbV2hr2ZGUlBTze0JL3xxzzDFe+6tCHxR89dVXZh588sknXnudTl0YwbYz0gTbzji73QvBtvMjoC8U3vJ/STL4vlQ5pkmk6yUCjiSgdZ+1BIRuLPnlN3mSn1/m1WUgTKAdHizNm0XKzb2S5Op/1pWGSe6vVj2Sr6421qBv4oMZsnptgfMT0cYe7Qq2D79kXQW8NeOgfL4sRxZ9st+s5N61p9S8ZVBSWu4XpXSqMkS6QjsqMlhOah0jd97UQC48N0Fqx3nPYhid5zMfz5IZj2bJjp2s1q7KmPIZBKwWINi2WpT2EHBI4PTTT5fRo0fLeeedZ1YI+8KhYfbKlSvNylfdmG/btm1mk0lfWgFb6azmGmZreJiYmCinnXaaCWi7du0qqamplgW0gRBsq+Xdd99tSmSonbeW2NGx17mqAexTTz0ln376qezevdvMYW95SFP5toA6ajh64403ynHHHWfZfLTr90xGRoapt/7yyy8b40A/CLadmQEE2844u90LwbY7I1ArJkSG902V3jc1kHgvCqH+SkNrHX+8JEeefnGHqXmcm1vmVcGhBnsR4UFybLMoueaKenLNP+tK4zTvXtiiK481zB4+catZfexvhxPB9h/N9uwtleXf5srCxfvl8y9zJSProJSUlEtJqa7mFtEg3F8OfdMjPCxI9PfIGR1i5aZeSdK1U5xXBdqV1iu/z5fhk7bKh5/u9xd+7gMBnxMg2Pa5IeOCEfhdQMMP3WTt5JNP9vrg6k//cbZnjyxZskTeffddWbZsmWRnZ5tAq3IltzeVeqgsL6JBtn5FRUVJ06ZNTZh97rnnmk0g69WrZ8vUDIRgW+G0PIau1r311luPuqGmLdDVbFSD7LVr18obb7xhNkrdtGmTqcHtxpsIGmbrAxatWd66dWtTbkTfGPDmOvyHcxcUFMj06dNlzpw55kEBh5iHErpR8EknneQoh/4ZnDlzpuiYBMJBsB0IoyxCsO3eODdvGiljhzSSqy6pa0Iqbz90Jew3q/Lklbd3m5Dqv5v5lR4yGyA6eWjZBTWLiQkxJUd6XVnXlBypXzfMycuocV/Zu0pkzJR0efa1naYcib8dbgTbhxsWHSyXjZuKZMWqPFnxXb58832+bMs4KAeKyk3JEt2A0pfqc+tvh+CQIAnThzgRQdK8aZR0PydBLr+ojpx4fLRX1uvX8cgvKJOx07bJ48/uMD9zIICAOwIE2+640ysClghouYHrrrtOBg8ebGro6j/74rFv3z755ptv5LPPPpOlS5fKhg0bTLCi9bj1S8tAOLEiVgNsNdSg8PAvLS+iq141yNavU045RRo0aOCId6AE2zpvtd62ltjRetta1sVXDi2to5ukvv/++2YOV76JUDl/rZ67OkcrH7JoKSJ10wcsl1xyibRp00aio6N9hc48CHjuuedk6tSp5s89R4UAwbYzM4Fg2xlnt3sh2HZ3BM46PU5GDWgkXTrFiYa1vnJoPeglX+XK+x/tlS9X5En2zuLfVsZWBIZWB91qExoiEhoabOpla5jXrWu8XHR+HWnZPErCfODBQOXY6gp4Lcsw56ntsmuPf76F5Xaw/cc/R7paO3N7saxaU2BKlnyzKl/WbzwgObllUlb+25w1YbeYf3Z7Q0r9K2tIcJCEhOq815XZwXJ8i2jR3xfduiZI21YxEh3t3X+vVcc33t0jk2ZukzXrCn3lVxvXiYBfChBs++WwclOBJKArNXv37i39+vWTxo0be3UZh6qMi67U1tXba9askVWrVsnq1atl3bp1ZtO+AwcOmID78C/9fOXq7sqfD1/trWF1ZWmLyu8aDFaG2Ppz5ZeuxE5LSzMPCTTIrvyurrVq1arK5Vv+mUAKthWvc+fOpsROly5dfKbEzuGDrnP0559/Nm8haMkSnb87duww5UoqH9BUzt+/mq/aVuWc/eMc1YctGmRrmRFdyatvCuimpM2aNfOpBwGVXuqhm8vq5qFav5zjdwGCbWdmA8G2M85u90Kw7fYIiPS8rK4M65MqrU+IEV9cg5GbVyZr1hXIsq/z5Otv88yGfrv3lJigWwPFsvKKMhAaFh4qF9HF3RXBYeUyb/1vUf3f94qvYLOQomKFakiwmCC7SaNIObVtLenUIVY6to+T1ORwE/j52qGr3ue9slOmzsmQXzcX+drlV/l6vS3Y/qsL1xryWduLZf3GIvl5Y6H8vPGA+dq0tcgE3uW/PaDRuasPaswc1rlc+bMHbynonrFBwRXzXue4lhap+BLzkKZB/XA5vkWUtD4+Rk5tV8vM/fr1wry2Hv8fffXP99qfC02pnfkf7HX9QUGVJy4fRMBPBQi2/XRgua3AEtAVmvfdd5/cc889kpyc7PPh9l+Nnq7g1pWwGvRmZWWZoHvXrl2Sn59vVncXFhaa4Ft/LioqMiGiBoMaVmsYePh3rYmtpUN01XVSUtJ/v9evX998zpuOQAu21b5Hjx4ydOhQadu2rSOr4u0cbw2xdZ5u3LhRNm/eLOnp6Wb+asmN/fv3S05OjilhUvkwRueslhTRtwQSEhJEgzf9M60PV3Tzx+bNm5uyLb76dkaltbp89913ptzGokWL/nv/do6FL7VNsO3MaBFsO+Psdi8E226PgK5CDpK7bmog/e5MkUapET4TXh1JTldsa5mSDZuKZEt6kaRnHpQdu0pEayDvzymVvLwyKS75beGFBnwSZMLruLgQSagdKvUSQyWlYYQ0aRRhSi40axIptWN9Y7+cv5tNWv5C62lPmLHNrBj258MXgu0j+ReXlEv2zhJJzyw2byLohodaOmbHTv3Sfy6RXbtLTGkNU87kvw9q9IGNPr05/JGNrsiomOPm/4JEIiOCpXblXK8bJqkNIyQtJVzSUiJMbXgtUVQvMcyn3uD4o+W+/aWmBMlTL+6QgkL/K7Xjz392uTf/FCDY9s9x5a4CUCAmJsaE27oJn7+G2wE4rCbIHzRokLz++usBc/u6MvmGG26QgQMHmjIbvh7iBszAVfFGNdT+5ZdfZMyYMfLWW2+xWeRfuBFsV3Eyefgxgm0PAX3kdIJt7xiomOgQ6ds7We6+pYEk1Qv3+XDbO1S95yo0AF36da6MmbpNPluW4z0XZtOV+HKwXR0SfStBa6Sbr+Lfvh88ZFZ766EPrSIjtYxjRW3suFohEhUZ4pNvZlTVpbj4kDz27HZ58NEs81CLAwEE3Bcg2HZ/DLgCBCwT0HIZGm7fddddhNuWqbrbUCAG2yquJXZuv/12U2JHN+qsLCPj7mjQu6cCutJHV65rLfXXXnvNrFbn+LMAwbYzs4Jg2xlnt3sh2HZ7BH7vPy42RAbekyK9b2ggiXV8p+yA9wh655VoreHvVhfI6Cnp8sGn+6Q8ABawBkqw7Z0zzr2r0rcS3l6wVybO3CarfwyMjbbd06ZnBKouQLBddSs+iYBPCGgJg3vvvdeE2w0bNmS1q0+M2pEvMlCDbRXRkhz6oEbns76FwMpt357MGmpnZGSYmtovvfSSKR/E8dcCBNvOzAyCbWec3e6FYNvtEfjf/usmhsmQ+1Pl5l71JT4ulJXb3jU81b4aDbV//KnQhNrvf7jPbEwYCAfBdiCM8v/eo76VsGxFxVsJny71/7cSAm+EuWNfFiDY9uXR49oROIKABoK62lVDwSZNmvjkJnwMboVAIAfbev9a8/zOO+80c1k39iTc9s0/GVp+REPtiRMnmlBba+FzHFmAYNuZ2UGw7Yyz270QbLs9An/uv17dMLNy++ZeSVInnnDb+0aoalekQZ9uoDdu2jZ5b9Fes5lmoBwE24Ey0hX3qQ9sVv1Q8VbCok8C462EwBph7tbXBQi2fX0EuX4EjiAQGhoqvXr1kv79+0urVq0It310pgR6sK3Dpg9qevfubcJt3USRcNu3JrOG2jqPJ02aJC+//DIrtaswfATbVUCy4CME2xYg+kATBNveOUh164TJgLuT5ZZrkyQxgbIk3jlKR74qDbW/+6FAxk1Pl4WL95tNBgPpINgOnNE2D3DWF5qV2vM/2CtajoQDAQS8S4Bg27vGg6tBwFIBDQAvvfRSGTx4sJxyyimmbjGHbwkQbFeMV0REhNx6663Sp08fU3NbN5jk8H6BsrIy2bhxo0yYMEHefPNNKSoq8v6L9oIrJNh2ZhAItp1xdrsXgm23R+DI/SfUDpW+dybL7dc1EF3FHRzsvdfKlf0uUFp6SFasyjeh9kef7Q+Imtp/HH+C7cD4E6Gh9uq1hTJ2Wrr856N9hNqBMezcpQ8KEGz74KBxyQhUV6BDhw4ydOhQ6datm1n9ykZ81RV07/ME27/b61sIPXr0kAEDBkibNm1E/5nDewVKS0tl9erVpvzIggULpLi42Hsv1suujGDbmQEh2HbG2e1eCLbdHoG/779WTIjcfn2S3H1zQ2mcFiEhIfrXUw5vFSguOWTCbN08b/nKPDkUoItXCba9dYZad136AGfl9/oAZ5t8+On+gKkfb50gLSHgnADBtnPW9ISAqwJawkEDwX/961+SkJBAOQdXR6PqnRNs/9nqvPPOMw9qzjzzTAkPD686Jp90TEBD7M8//9yE2l988YVoORKOqgsQbFfdypNPEmx7ouc75xJse/9YhYYESc9/1pUBd6dI6+OjJTSUcNvbRk0D7KKicnn9vd0ybU6mKc0QyAfBtn+PfnHxIVn8xX6Z+GCGfPlNbsA+wPHvUebu/EmAYNufRpN7QeAoAnFxcXLzzTebesXNmjWjNIkPzBiC7b8epHbt2smQIUPkoosukpiYGN5C8JK5fOjQIbMx5Ouvvy4zZsyQn376yUuuzLcug2DbmfEi2HbG2e1eCLbdHoGq93/e2fEy5L5U6XRarESEB0sQ+XbV8Wz8pD6b3ruvRJ54Plsee3a7ZGTxBhbBto0TzsWm9QFOQWGZvP7ObpnxWJb89HNgP8BxcSjoGoFqCRBsV4uLDyPgHwKdOnUyoeA555wjUVFRhIJePKwE20cenHr16sk999xjHtY0bNiQutsuz2Otp52dnS1z5syRZ555xvzMUTMBgu2auVX3LILt6or55ucJtn1r3I5pEmlWbve8rK7E1w6l7rbLw6flGDZuLpLpj2TK6+/ulrz8MpevyDu6J9j2jnGw8iq0nnb2rhJ55Ont8vRL2eZnDgQQ8A0Bgm3fGCeuEgHLBdLS0kwo2KtXL2nQoAH1ii0XtqZBgu2/d9RNJa+++mqzqWSrVq3MWwjUkLdm7lWnFS09ovW0p0yZYupps0lkdfT+/FmCbc/8qno2wXZVpXz7cwTbvjd+teNCTd3t269vYOpuh1GaxPFB1JWrB4vL5bOlOfLArExZ+nUuNYYPGwWCbcenpK0dau341WsLZNrDGfL+h/vkQBEl9GwFp3EELBYg2LYYlOYQ8CUB3Xzv3HPPlX79+omu4tbV28FsSe9VQ0iwXbXhaN26tfTv318uueQSiY+PZx5Xjc3jT2nt7NzcXJk/f748+OCDJtzWciQcngkQbHvmV9WzCbarKuXbnyPY9s3x0/8cPfuM2mb19tlnxElUVIgEU5rEkcEsLTsku3aXyHOv7pTHn9shW7cddKRfX+qEYNuXRuvI11pWfkjy88vkrQV7ZeZjmbJmHaVH/GNkuYtAEyDYDrQR534R+AsBLeNw2223yfXXXy+6kptVr94xTTQg3LRpkwwePFj+/e9/e8dFefFVaA35a6+9Vu68805p0aIF89jGsdK5qau0161bJw899JC8/fbbJuDmsEaAYNsax6O1QrB9NCH/+PcE2749jg3qh0vvGxvI9VfXl9TkcFZv2zicZpX2wXL5amWeKT2yeEmOWbXN8WeBKaObSJ/bkyU8nKctvjg/dK4XF5fLj+sLZfYTWfLOwr2Sm0eZHV8cS64ZARUg2GYeIICAEdDyDaeccorce++90q1bN6lTpw6bS7o0NypDw/3798t//vMfmTVrllkJy1E1gbZt28p9991nNpZMTEykzE7V2Kr8qdLSUtmzZ4+8+eab8sgjj8j69etZpV1lvap9kGC7ak6efopg21NB3zifYNs3xunvrjIkRKRzx9rS545ks3o7tlaIhIQQKFo5siUlhyQj66A8++pOmffKTtmWySrtv/Ptd2eK3HtrQ6mbGCphYUESFhpMPXgrJ6SNbelc3723RF59e7c8Nm+7bNhUZGNvNI0AAk4IEGw7oUwfCPiQgK7WPuuss0ww2LlzZ4mNjSUYdGj8dPO9gwcPyvbt202g/eyzz8oPP/wgGiRyVE9A57GW2dHa22eccYZER0ezuWT1CP/0aZ2fBw4ckK+//lpmzpwpixcvppa2h6ZHOp1g2ybYPzRLsO2Ms9u9EGy7PQLW9a+B9pUXJ8pdNzeUE4+LlogIwkRPdXVzSN0QctEn+2XW3CxZ+X0+tbSriJpUL0xObx8n3c+Jl7NOj5PU5IiKkDssSEKom1NFRec+ZuZ6QZksXrJfHn5yuyz/Nk+Kiymf59wI0BMC9gkQbNtnS8sI+LSABtrdu3eXO+64w6zkrlWrFgG3DSOqq7NLSkpMQPjjjz/KK6+8Iu+8845kZGTY0FvgNakrtq+44gq59dZbzeaSkZGRBNzVnAYaaOv81LcG5s6dK++//77s3bu3mq3w8eoIEGxXR6vmnyXYrrmdL51JsO1Lo1W1a22UGiHX9agvva6sK80aR0p4OAF31eR+/5TW0S4sLDebQj76zHb5bFmOFBRSdqS6jpWfj4sNkVPb1ZLu5yTIuWfVlqaNIiUyIrgi5A4JkiBeMKgprcfnaaCtc3vZilx5bN4OM9fzCyg74jEsDSDgRQIE2140GFwKAt4ooIH2BRdcYALuU0891QTc1OD2bKQ0zK5cnZ2dnS2ffvqpvPbaa2YlLHWKPbM90tlJSUnSs2dPuemmm0z9bQLuoztXztG1a9fK008/LW+99Zbs2rXr6CfyCY8FCLY9JqxSAwTbVWLy+Q8RbPv8EB7xBpo1iZSb/lVfel5WV9JSIiQiPEiCWSn7twOuId+BA+WyYlWeCfk++my/5OYT8ln5p0QftLRqGSXndI6XrmfWljYnREt87cqSJRVBN4e9AlpDW0uO5BeWyZcrcmXuc9kE2vaS0zoCrgoQbLvKT+cI+I6ABoFa0uHGG2+ULl26iAYC4eHhrH6txhBqSRFdna3h9YoVK0yN4o8++siUHuFwRqBBgwZy6aWXmo1STzzxRFOiJDQ01NSY5xBTK1vnqZYcWbVqlTz33HOyYMEC2blzJzwOChBsO4NNsO2Ms9u9EGy7PQL2939Mk0jp+c+6JuBu3jRSIiODJZTw8L/wlSGflmFY9nWuPPOyhny5kpNLqTv7Z6dI3TphZjV3lzNrm5IlOkejo4JNXe7QUOFhjIWDUFZ2SIq1hvaeEvPQRuvFr1ydL0VFvI1gITNNIeB1AgTbXjckXBAC3i2gAeBxxx0nPXr0EA1fmjVrZla/6iru4OBg7754h6+ucmV2cXGx5Ofny5o1a2ThwoUmKNy4cSO1sx0ej8O701I755xzjnlQc9ppp0l8fHxAP6gpLy8Xnac5OTnyxRdfyLx588z3vLw8F0cpcLsm2HZm7Am2nXF2uxeCbbdHwLn+6yWGyUUXJMj1PetLmxNipFaMloII3DIlWm5Eawjv2l0R8j37arZ8u7pAig4S8jk3K/+3J/2rUuO0SOnUIVbOOqO2nHZyLUlpWPG2QVhokISGVrx1wHqLqo9QWbmWdTwkBw+Wy9r1B+StBXvknYV7ZPPWIilnqlcdkk8i4MMCBNs+PHhcOgJuC+hqV62/fdVVV5lyJampqSbgDuSQW8s3VK7M3r9/v3z//fdmI8gPPvhAtm7dakqQcHiPgD6MOeaYY8xDGp3HlWVK9G0Ef39Qo3NR3yDQQPvXX3818/T111+Xn376iYcuLk/Riy++WIYMGSJt27Z19ErGjx8vjzzyiBQUFDjar1udabA9ceJE6dWrl6OXoJsCjx07Vj788ENH+w3Uzq6++moZOHCgtGzZ0lGCoUOHymOPPcb/7juqXtFZeFiwnNQmRnpcmigXnptgypSEm+DQ/0PuyhWrhQfK5fs1BfLm/N2y4KO9kpFVLLpym8O7BEJCRLRmfPt2taTjqbHS4eQ40TcQYqJ1NXdF0K0bUbJ26H/HrXKea5i9cXORfPLFfnnvg33y/Zp8asV71xTnahBwRIBg2xFmOkHA/wU0IDj99NPlwgsvlLPOOsuE3BoOasgdEhLit6UeDg+ytXyDBoTLly+XTz75xHynJrHvzP2YmBjzoEZDxfPOO0+aNm0qERG6w71/zOHKNwg0zD548KCZqx9//LHMnz/fPIAJlDDTF2Zkw4YNTQhXu3ZtRy93/fr1Zl7ow7lAOPR/o7QkUVpamqO3q29GrFu3TnSPBQ77BfS/R/Shpb6p4+ShG0Jv2rTJlHjicE8gIT5UzmgfK5d0qyNnnV5bUhqG/xZy+8fKWJ1eujJbV6weKCozK1Y/+GSfLPh4n6z/5YAcLGbJqnuzr2Y9J9ULk7YnxkiHk2PlpNYx0qpltNSrGybhv21EqWV2tE53IIXd5eVaKk9Ea8QX/jbPP/x0nyxcvE/W/XKAtxBqNtU4CwG/ESDY9puh5EYQ8B4BDQjbtGkjXbt2lc6dO0urVq1MqQetZaxfGnT72mpY/YuplmuoDLL1e1FRkWzYsMEE2MuWLTN1s7VeNquyvWcu1vRKtLyOzmENuLWm/AknnCBxcXEm5K6cw95el7syyNaQUr+0trsGLZ9//rlZKaqbQurDGA4EEEAAAQQCQSAuNsTUOj7/7HjpbGodR5mVsb5UAqL8kIiuVtWAT7927y2R738skE+/yJGPP8+RjZsPSEkpD1P8aT5rkK0PZFqfEC1tWsVI21YxcuJx0ZJUX8PuYLOiW1d+B2vg7Qeru/VhjZnjv81zLZ2zddtB+e6HAln+bZ588VWObE4/aOY/BwIIIKACBNvMAwQQsF0gISFB2rVrZ2oZn3zyydK6dWupX7/+f2saa8itYbc3rOzW8PrwLw2p9auwsFC2bNligkH90nrZGgzqimz9PId/C9SrV09OPfVUs4Gqruo+/vjjRed1Zch9+Px1I/CufOhSOV81yN63b5/oCtyVK1eaBy/ffvut7N69m9WD/j1VuTsEEEAAgSoI6GpXrW182imxcvqpsXJK2xgTdMfW0v8eDZJQl4NCDfd0lWpZuUh5ZchXdkh27iqRtesL5ZtV+bL061z54adCNoGswnj720c0yK5fL1xaNIuSFsdESovm+j1KWjSLFK01r7XlTdj9W9D93+9B3lO/W//6pPWxdX7rPNcwW/85J7dM1v9SKCtW5cs33+XLdz/ky46dJebfcSCAAAJ/JUCwzbxAAAHHBTQMTElJMa+Aa0DYvHlz89WkSRMTFlau6NbAu/JLw8K/+qq8+Mow8fBXfit/rvyu4Z/+XLn6+vCf9d9piYY9e/ZIenq6qYet3zdv3mzCwV9++cWseOVAQAWioqJMbW59YKObqepr7scee6xoCQld7X343NWfazp/K+fo4d//+PBF3xzIysoyG5LqGwRa4kBLi+gr8PpAhgMBBBBAAAEEji6gK7pbNo8yq2KPax4lxzaLkubNIqW+KQNRUZ+74itIgoNEgn7b5E83+qv4CjKrxvQ4fPO/w6vR6M8V/5uu3yu+yvWfNcDWkO+3QLugsFwyMg/KL5uKZMPmIvnp50JTPzg9s1iKKS9y9MEM0E/o/KyTEGbqdB/TOFIapUVIanK4pDasFZ60VQAAIABJREFU+N6gfrh5S+H3OfzbvA2SP8/pw+bx4Ys2Kud25byu+F4xpyt++m1u65yunNu/zXkNs3X+F5ccMmH1r1uK5NfNB0yd7A2biswbBzuySyihE6Dzl9tGoKYCBNs1leM8BBCwXEAD7cTERFOfW4Pv5ORk8z0pKckE3lrORGvOakkI/VnrH+uh3yvDRC2toPWDdeWq/qwb42lgrQGfbuaowfUfv3bu3Cnbtm0zXxpeUw/T8qENmAa1hqvOWZ3DlV86j3Ve65yt/NJ5rPV99dCQXOevHhpS65fOQZ3HOh+1Hq/O3cr5qyF2RkaGZGZmmjmrP+fl5QWMMTeKAAIIIICAUwIa4sXXDpW05IpgUEtCpCZHSMOkcKmTECrxtUMkPi7UfOlq77CwioUY0VHBppa3HrqRo25yp8eBg+VmRWpOTqnsN19lsmtPiWzPLpZtWQfNJo/bMg9KVnaxHDjAG4FOjXOg9KNld+omhklaSrg0rB8uiYlhUic+VBITQs181q/EhDDRhzy1Yirms9b2jozQFeBBEhUVbH7WMiE6P03Na53fxeVSXHzI1LrWeu95+WWye0+J7NxTItk7S2Tn7hLzz9m7SiR7d7H5/+UXlLGhaaBMPO4TAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAWsFCLat9aQ1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAZsFCLZtBqZ5BBBAAAEEEEAAAQQQQAABBBBAAIH/b8cOCQAAABiE9W/9EtytAGISAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBBSidlBAAAgAElEQVQgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaARW1geMAAAewSURBVGO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUwtltPNQIECBAgQIAAAQIECBAgQIAAAQIECBA4CxjbZ2B5AgQIECBAgAABAgQIECBAgAABAgQIEGgFjO3WU40AAQIECBAgQIAAAQIECBAgQIAAAQIEzgLG9hlYngABAgQIECBAgAABAgQIECBAgAABAgRaAWO79VQjQIAAAQIECBAgQIAAAQIECBAgQIAAgbOAsX0GlidAgAABAgQIECBAgAABAgQIECBAgACBVsDYbj3VCBAgQIAAAQIECBAgQIAAAQIECBAgQOAsYGyfgeUJECBAgAABAgQIECBAgAABAgQIECBAoBUYUqkaGlqg5VYAAAAASUVORK5CYII="; diff --git a/lib/nostr.js b/lib/nostr.js index d170fed3..bc75cc96 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -5,16 +5,15 @@ import { broadcast } from "$lib/sockets"; import store from "$lib/store"; import redis from "$lib/redis"; import proxy from "@fastify/http-proxy"; -import { wait } from "$lib/utils"; +import { nada, wait } from "$lib/utils"; -app.register(proxy, { - upstream: config.nostr, - prefix: "/nostr", - rewritePrefix: "", - disableRequestLogging: true -}); +export let pool; +try { + pool = RelayPool(config.relays); +} catch(e) { + console.log(e) +} -export const pool = RelayPool(config.relays); pool.on("open", relay => { if (relay.url.includes(config.relay)) coinos = relay; @@ -168,3 +167,182 @@ pool.on("event", async (relay, sub, ev) => { console.log(e); } }); + +app.get("/event/:id", async (req, res) => { + try { + let event = JSON.parse(await redis.get(`ev:${req.params.id}`)); + let { pubkey } = event; + + event.user = JSON.parse(await redis.get(`user:${pubkey}`)) || { + username: pubkey.substr(0, 6), + pubkey, + anon: true, + follows: [], + followers: [] + }; + + res.send(event); + } catch (e) { + res.code(500).send("Problem fetching event"); + } +}); + +app.get("/:pubkey/notes", async (req, res) => { + try { + let { pubkey } = req.params; + + let user = JSON.parse(await redis.get(`user:${pubkey}`)); + + if (!user) { + user = await db.User.findOne({ + where: { pubkey } + }); + } + + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true, + follows: [], + followers: [] + }; + + q( + `${pubkey}:notes`, + { + kinds: [1], + authors: [pubkey] + }, + { since: 0 } + ).catch(nada); + + await redis.set(`user:${pubkey}`, JSON.stringify(user)); + + let ids = await redis.sMembers(pubkey); + + let events = ids.length + ? (await redis.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) + : []; + + res.send(events.map(e => ({ ...e, user }))); + } catch (e) { + console.log(e); + res.code(500).send("problem fetching user events"); + } +}); + +app.post("/event", async (req, res) => { + let { event } = req.body; + pool.send(["EVENT", event]); + res.send(event); +}); + +app.get("/:pubkey/follows", async (req, res) => { + try { + let { pubkey } = req.params; + let { tagsonly } = req.query; + + let sub = `${pubkey}:follows`; + q( + sub, + { + limit: 1, + kinds: [3], + authors: [pubkey] + }, + { timeout: 60000, eager: 60000 } + ).catch(nada); + + let tags = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; + if (tagsonly) return res.send(tags); + + let follows = []; + for (let f of tags) { + let [_, pubkey] = f; + + q(`${pubkey}:profile:f1`, { + limit: 1, + kinds: [0], + authors: [pubkey] + }).catch(nada); + + let user = JSON.parse(await redis.get(`user:${pubkey}`)); + + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; + + follows.push(user); + } + + follows = uniq(follows, e => e.pubkey); + follows.sort((a, b) => a.username.localeCompare(b.username)); + + res.send(follows); + } catch (e) { + console.log(e); + res.code(500).send(e && e.message); + } +}); + +app.get("/:pubkey/followers", async (req, res) => { + try { + let { pubkey } = req.params; + + let pubkeys = [ + ...new Set([ + ...(await got(`${config.nostr}/followers?pubkey=${pubkey}`).json()), + ...(await redis.sMembers(`${pubkey}:followers`)) + ]) + ]; + + let followers = []; + + q( + `${pubkey}:followers`, + { kinds: [3], "#p": [pubkey] }, + { timeout: 60000, eager: 60000 } + ).catch(nada); + + for (let pubkey of pubkeys) { + let user = JSON.parse(await redis.get(`user:${pubkey}`)); + if (!user || !user.updated) { + q(`${pubkey}:profile:f2`, { + limit: 1, + kinds: [0], + authors: [pubkey] + }).catch(nada); + + user = JSON.parse(await redis.get(`user:${pubkey}`)); + } + + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; + + followers.push(user); + } + + followers = uniq(followers, e => e.pubkey); + followers.sort((a, b) => a.username.localeCompare(b.username)); + + res.send(followers); + } catch (e) { + console.log(e); + res.code(500).send(e && e.message); + } +}); + +app.get('/nostr.json', async (req, res) => { + res.send({ + names: { adam: "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8", asoltys: "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8" } + }); +}); + diff --git a/lib/passport.js b/lib/passport.js index 8b58e305..6638fe73 100644 --- a/lib/passport.js +++ b/lib/passport.js @@ -1,7 +1,5 @@ import app from "$app"; -import db from "$db"; import jwt from "passport-jwt"; -import { Op } from "@sequelize/core"; import fastifyPassport from "@fastify/passport"; import fastifySecureSession from "@fastify/secure-session"; import config from "$config"; @@ -35,20 +33,8 @@ fastifyPassport.use( }, async (payload, next) => { try { - let { username, uuid } = payload; - let where = {}; - - if (uuid) where.uuid = uuid; - else if (username) - where.username = { [Op.or]: [username, username.replace(/ /g, "")] }; - - - let user = await db.User.findOne({ - where, - include: { model: db.Account, as: "account" } - }); - - if (user.locked) throw new Error("account is locked"); + let { uuid } = payload; + let user = await redis.get(`user:${uuid}`); next(null, user); } catch (e) { console.log("payload", payload) diff --git a/lib/persist.js b/lib/persist.js deleted file mode 100644 index fd6fb146..00000000 --- a/lib/persist.js +++ /dev/null @@ -1,22 +0,0 @@ -import fs from "fs"; - -export default (path, initial = {}) => { - if (!initial || !Object.keys(initial).length) { - try { - initial = JSON.parse(fs.readFileSync("/app/" + path)); - } catch (e) { - console.log(e); - } - } - - return new Proxy(initial, { - set(obj, prop, value) { - fs.writeFileSync( - path, - JSON.stringify({ ...obj, [prop]: value }, null, 2), - "utf-8" - ); - return Reflect.set(...arguments); - } - }); -}; diff --git a/lib/rates.js b/lib/rates.js index e896c8cc..8970ca64 100644 --- a/lib/rates.js +++ b/lib/rates.js @@ -2,19 +2,18 @@ import app from "$app"; import axios from "axios"; import config from "$config"; import fs from "fs"; -import binance from "./binance"; +// import binance from "./binance"; import { broadcast } from "./sockets"; import store from "./store"; import { err, warn } from "$lib/logging"; -let b = binance(); -setInterval(() => { - b.terminate(); - b = binance(); -}, 360000); +// let b = binance(); +// setInterval(() => { +// b.terminate(); +// b = binance(); +// }, 360000); -let fetchRates; -(fetchRates = async () => { +let fetchRates = async () => { let date, fx; try { @@ -58,7 +57,8 @@ let fetchRates; store.fx = fx; setTimeout(fetchRates, 7200000); -})(); +}; +// fetchRates(); app.get("/rate", async (req, res, next) => { res.send(store.last); diff --git a/lib/read.js b/lib/read.js deleted file mode 100644 index c021cb10..00000000 --- a/lib/read.js +++ /dev/null @@ -1,25 +0,0 @@ -export default (stream, func) => { - var remaining = ""; - - stream.on("data", function(data) { - remaining += data; - var index = remaining.indexOf("\n"); - var last = 0; - while (index > -1) { - var line = remaining.substring(last, index); - last = index + 1; - func(line); - index = remaining.indexOf("\n", last); - } - - remaining = remaining.substring(last); - }); - - stream.on("error", () => {}); - - stream.on("end", function() { - if (remaining.length > 0) { - func(remaining); - } - }); -}; diff --git a/lib/register.js b/lib/register.js index 6f8f366b..fd6f27ae 100644 --- a/lib/register.js +++ b/lib/register.js @@ -1,59 +1,49 @@ import countries from "$lib/countries"; import config from "$config"; import { authenticator } from "otplib"; -import db from "$db"; -import axios from "axios"; import bcrypt from "bcrypt"; import { l } from "$lib/logging"; +import { v4 } from "uuid"; +import redis from "$lib/redis"; export default async (user, ip, requireChallenge) => { - if (!(user && user.username)) throw new Error("Username required"); - user.username = user.username.replace(/ /g, ""); + let { password, username } = user; - let exists = await db.User.count({ where: { username: user.username } }); - if (exists) throw new Error(`Username ${user.username} taken`); + if (!username) throw new Error("Username required"); - if (user.password) { - user.password = await bcrypt.hash(user.password, 1); + username = username.replace(/ /g, ""); + let uuid = v4(); + user.uuid = uuid; + + let exists = await redis.get(`user:${username}`); + if (exists) throw new Error(`Username ${username} taken`); + + if (password) { + user.password = await bcrypt.hash(password, 1); } if (!config.ipxapi || ip.startsWith("127") || ip.startsWith("192")) { user.currency = "CAD"; } else { - let { data } = await axios.get(`https://ipxapi.com/api/ip?ip=${ip}`, { + let { countryCode } = await got(`https://ipxapi.com/api/ip?ip=${ip}`, { headers: { authorization: `Bearer ${config.ipxapi}` } - }); + }).json(); - user.currency = countries[data.countryCode] || "USD"; + user.currency = countries[countryCode] || "USD"; } user.currencies = [...new Set([user.currency, "CAD", "USD"])]; user.fiat = false; user.otpsecret = authenticator.generateSecret(); - user = await db.User.create(user); - - let account = await db.Account.create({ - user_id: user.id, - asset: config.liquid.btcasset, - balance: 0, - pending: 0, - name: "Bitcoin", - ticker: "BTC", - precision: 8 - }); - - user.accounts = [account]; - - const d = ip.split("."); - const numericIp = ((+d[0] * 256 + +d[1]) * 256 + +d[2]) * 256 + +d[3]; - if (Number.isInteger(numericIp)) { - user.ip = numericIp; - const ipExists = await db.User.findOne({ where: { ip: numericIp } }); + let d = ip.split("."); + ip = ((+d[0] * 256 + +d[1]) * 256 + +d[2]) * 256 + +d[3]; + if (Number.isInteger(ip)) { + user.ip = ip; } - user.account_id = account.id; - await user.save(); - l("new user", user.username, ip); + await redis.set(`user:${user.uuid}`, user); + await redis.set(`user:${user.username}`, user); + l("new user", user.username); return user; }; diff --git a/lib/rpc.js b/lib/rpc.js deleted file mode 100644 index 9186f4cd..00000000 --- a/lib/rpc.js +++ /dev/null @@ -1,24 +0,0 @@ -import wretch from "./wretch"; - -export default ({ host, port, wallet, username, password }) => - new Proxy( - {}, - { - get: (target, prop) => (...params) => - ((method, ...params) => { - let url = `http://${host}:${port}/wallet/${wallet}`; - return wretch() - .url(url) - .auth( - `Basic ${Buffer.from(`${username}:${password}`).toString( - "base64" - )}` - ) - .post({ - method, - params - }) - .json(({ result }) => result); - })(prop.toLowerCase(), ...params) - } - ); diff --git a/lib/send.js b/lib/send.js index eefea080..f378cfc1 100644 --- a/lib/send.js +++ b/lib/send.js @@ -1,9 +1,6 @@ import config from "$config"; -import db from "$db"; import { emit } from "$lib/sockets"; import store from "./store"; -import bolt11 from "bolt11"; -import { Op } from "@sequelize/core"; import { computeConversionFee, conversionFeeReceiver @@ -14,18 +11,7 @@ import { l, warn, err } from "$lib/logging"; export default async (amount, memo, hash, user) => { l("attempting lightning payment", user.username, amount, hash); - let payreq = bolt11.decode(hash); - let routingInfo = payreq.tags.find(t => t.tagName === "routing_info"); - if ( - routingInfo && - routingInfo.data.find(d => d.fee_proportional_millionths > 10000) - ) { - warn("fee rate too high", routingInfo); - throw new Error( - "Couldn't find a suitable route to pay to your destination" - ); - } - + // decode payreq with cl if (!amount || payreq.satoshis > amount) amount = payreq.satoshis; amount = parseInt(amount); diff --git a/lib/sendInternal.js b/lib/sendInternal.js index b49a6c64..5a8b0b9d 100644 --- a/lib/sendInternal.js +++ b/lib/sendInternal.js @@ -1,12 +1,9 @@ import config from "$config"; -import db from "$db"; import { emit } from "$lib/sockets"; import store from "$lib/store"; import { callWebhook } from "$lib/webhooks"; -import axios from "axios"; import { v4 } from "uuid"; import { notify } from "$lib/notifications"; -import { sendLiquid } from "$routes/liquid/send"; import { l, err } from "$lib/logging"; import { fail } from "$lib/utils"; @@ -153,25 +150,6 @@ export default async ( await invoice.increment({ received: amount }, { transaction }); await invoice.reload({ transaction }); - - let c = store.convert[invoice.text]; - if (c) { - l( - "internal payment detected for conversion", - invoice.text, - c.address, - recipient.username - ); - - recipient.account = a2; - - sendLiquid({ - address: c.address, - amount: amount - 100, - user: recipient, - limit: amount - }).catch(console.log); - } } let p2 = await db.Payment.create(params, { transaction }); diff --git a/lib/sockets.js b/lib/sockets.js index 5980304b..ecf15dd7 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -1,4 +1,4 @@ -import store from "./store"; +import store from "$lib/store"; import config from "$config"; import app from "$app"; import jwt from "jsonwebtoken"; @@ -9,7 +9,6 @@ import fastifyWs from "@fastify/websocket"; import whitelist from "./whitelist"; import { l, err, warn } from "$lib/logging"; import { getUser } from "$lib/utils"; -import db from "$db"; const code = 1000; @@ -20,8 +19,8 @@ const all = {}; const subscriptions = []; const lastSeen = {}; -export const emit = (username, type, data) => { - if (data.username !== undefined) data = pick(data, ...whitelist); +export const emit = (uuid, type, data) => { + if (data.uuid !== undefined) data = pick(data, ...whitelist); if (type === "payment" && data.amount > 0) { for (let i = subscriptions.length - 1; i >= 0; i--) { @@ -33,18 +32,18 @@ export const emit = (username, type, data) => { } } - if (!store.sockets[username]) return; - for (let id in store.sockets[username]) { - const ws = store.sockets[username][id]; + if (!store.sockets[uuid]) return; + for (let id in store.sockets[uuid]) { + const ws = store.sockets[uuid][id]; ws.send(JSON.stringify({ type, data })); } }; export const broadcast = (type, data) => { const sent = []; - for (const username in store.sockets) { - for (const id in store.sockets[username]) { - const ws = store.sockets[username][id]; + for (const uuid in store.sockets) { + for (const id in store.sockets[uuid]) { + const ws = store.sockets[uuid][id]; ws.send(JSON.stringify({ type, data })); sent.push(ws.id); } @@ -85,7 +84,6 @@ app.register(async function(app) { break; case "login": - let user; let token = data; if (lastSeen[token] > subtractSeconds(5)) return; lastSeen[token] = new Date(); @@ -93,51 +91,26 @@ app.register(async function(app) { try { if (!token || token === "null" || !jwt.decode(token)) return ws.close(code, `bad token ${token}`); - let { username, uuid } = jwt.decode(token); - if (uuid) user = await getUser(uuid); - if (user) ({ username } = user); - if (!(username || uuid)) throw new Error("Invalid JWT token"); + let { uuid } = jwt.decode(token); + if (!uuid) throw new Error("Invalid JWT token"); + let user = await getUser(uuid); + if (!user) throw new Error("User not found"); - if (!store.sockets[username]) store.sockets[username] = {}; - const existing = Object.keys(store.sockets[username]); + if (!store.sockets[uuid]) store.sockets[uuid] = {}; + const existing = Object.keys(store.sockets[uuid]); if (existing.length > 4) { const p = existing.find(sid => sid !== id); - store.sockets[username][p].send( - JSON.stringify({ type: "logout" }) - ); + store.sockets[uuid][p].send(JSON.stringify({ type: "logout" })); setTimeout(() => { - if (store.sockets[username][p]) - store.sockets[username][p].close(code, "too many sockets"); + if (store.sockets[uuid][p]) + store.sockets[uuid][p].close(code, "too many sockets"); }, 5000); } - store.sockets[username][id] = ws; - names[id] = username; - - if (!user) user = await getUser(username); + store.sockets[uuid][id] = ws; + names[id] = uuid; let { haspin } = user; - if (user.locked) throw new Error("Account is locked"); - let payments = await user.getPayments({ - where: { - account_id: user.account_id - }, - order: [["id", "DESC"]], - limit: 12, - include: [ - { - model: db.Account, - as: "account" - }, - { - model: db.Payment, - as: "fee_payment" - } - ] - }); - - let accounts = await user.getAccounts(); - let keys = await user.getKeys(); user = user.get({ plain: true }); user.accounts = accounts; @@ -154,10 +127,6 @@ app.register(async function(app) { ); } catch (e) { err("caught socket error", e.message); - if (e.message.includes("locked")) { - ws.send(JSON.stringify({ type: "locked" })); - ws.send(JSON.stringify({ type: "logout" })); - } if (e.message.includes("not found")) ws.send(JSON.stringify({ type: "logout" })); setTimeout( @@ -173,23 +142,6 @@ app.register(async function(app) { case "lnurl": store.sessions[data] = ws; break; - case "updateMemo": - const payment = await db.Payment.findOne({ - where: { - user_id: ws.user.id, - id: data.id - }, - include: { - model: db.Account, - as: "account" - } - }); - if (payment) { - payment.memo = data.memo; - await payment.save(); - emit(ws.user.username, "payment", payment); - } - break; case "subscribe": subscriptions.push({ invoice: data, ws }); break; @@ -199,11 +151,10 @@ app.register(async function(app) { }); ws.on("close", function(code, reason) { - // console.log("closing socket", reason && reason.toString(), id); try { - const username = names[id]; - if (store.sockets[username] && store.sockets[username][id]) { - delete store.sockets[username][id]; + const uuid = names[id]; + if (store.sockets[uuid] && store.sockets[uuid][id]) { + delete store.sockets[uuid][id]; } if (all[id]) delete all[id]; @@ -217,11 +168,11 @@ app.register(async function(app) { const names = {}; setInterval(() => { - for (const username in store.sockets) { - for (const id in store.sockets[username]) { - let ws = store.sockets[username][id]; + for (const uuid in store.sockets) { + for (const id in store.sockets[uuid]) { + let ws = store.sockets[uuid][id]; ws.beats++; - if (ws.beats > 10) ws.close(code, `${username} ${id} lost heartbeat`); + if (ws.beats > 10) ws.close(code, `${uuid} ${id} lost heartbeat`); } } }, 2000); diff --git a/lib/store.js b/lib/store.js index e42afeda..6f0c1c97 100644 --- a/lib/store.js +++ b/lib/store.js @@ -1,5 +1,3 @@ -import persist from "./persist"; - export default { addresses: {}, challenge: {}, @@ -13,8 +11,7 @@ export default { sockets: {}, unaccounted: [], networks: [], - convert: persist("data/conversions.json"), - rates: {}, + rates: { USD: 20000 }, fx: {}, assets: {}, bcAddressIndex: 0, diff --git a/lib/sync.js b/lib/sync.js deleted file mode 100644 index 9e2fc31c..00000000 --- a/lib/sync.js +++ /dev/null @@ -1,414 +0,0 @@ -import config from "$config"; -import { toSats } from "$lib/utils"; -import { emit } from "$lib/sockets"; -import db from "$db/db"; -import app from "$app"; -import { differenceInDays } from "date-fns"; -import fs from "fs"; -import read from "./read"; -import { Op } from "@sequelize/core"; -import store from "./store"; -import { l, err, warn } from "./logging"; -import ln from "./ln"; -import lnd from "./lnd"; -import bc from "./bitcoin"; -import lq from "./liquid"; -import { adminAuth } from "$lib/passport"; - -const init = async () => { - try { - read(fs.createReadStream("exceptions"), data => - store.exceptions.push(data) - ); - } catch (e) { - warn("couldn't read exceptions file", e.message); - } - - try { - const thirtyDays = new Date(new Date().setDate(new Date().getDate() - 30)); - - ( - await db.Invoice.findAll({ - where: { - createdAt: { - [Op.gt]: thirtyDays - } - }, - include: { - model: db.User, - as: "user" - } - }) - ).map(({ address, user, unconfidential }) => { - if (address && user) store.addresses[address] = user.username; - if (unconfidential && user) - store.addresses[unconfidential] = user.username; - }); - } catch (e) { - console.log(e); - } - - store.payments = ( - await db.Payment.findAll({ - attributes: ["hash"] - }) - ).map(p => p.hash); -}; - -init(); - -const sync = async () => { - const twoWeeksAgo = new Date(new Date().setDate(new Date().getDate() - 14)); - - try { - const payments = await db.Payment.findAll({ - attributes: ["hash", "confirmed", "preimage"], - where: { createdAt: { [Op.gt]: twoWeeksAgo } }, - include: [ - { - model: db.Account, - as: "account" - }, - { - model: db.User, - as: "user" - } - ] - }); - - let missed, invoices, recent; - if (config.lnd) { - ({ invoices } = await getInvoices({ lnd, limit: 1000 })); - - recent = invoices - .filter( - i => - i.is_confirmed && - differenceInDays(new Date(), new Date(i.created_at)) < 2 - ) - .map(i => ({ - amount: parseInt(i.received), - preimage: i.secret, - pr: i.request, - createdAt: new Date(i.created_at) - })); - } else { - ({ invoices } = await ln.listinvoices()); - - recent = invoices - .filter( - i => - i.status === "paid" && - Math.round(Date.now() / 1000) - i.paid_at < 60 * 60 * 48 - ) - .map(i => ({ - amount: parseInt(Math.round(i.msatoshi_received / 1000)), - preimage: i.payment_preimage, - pr: i.bolt11, - createdAt: new Date(i.paid_at * 1000) - })); - } - - await db.transaction(async transaction => { - const twoDaysAgo = new Date(new Date().setDate(new Date().getDate() - 2)); - let settled = ( - await db.Payment.findAll( - { - where: { - network: "lightning", - createdAt: { [Op.gt]: twoDaysAgo } - }, - attributes: ["preimage"] - }, - transaction - ) - ).map(p => p.preimage); - - missed = recent.filter(i => !settled.includes(i.preimage)); - for (let i = 0; i < missed.length; i++) { - let { amount, preimage, pr: text, createdAt } = missed[i]; - if (!text || !text.length) continue; - - try { - let invoice = await db.Invoice.findOne({ - where: { text }, - include: { - model: db.User, - as: "user" - }, - transaction - }); - - if (invoice && invoice.user_id) { - let account = await db.Account.findOne({ - where: { - user_id: invoice.user_id, - asset: config.liquid.btcasset, - pubkey: null - }, - transaction - }); - - if (account) { - let payment = await db.Payment.create({ - account_id: account.id, - user_id: invoice.user_id, - hash: text, - amount, - currency: invoice.currency, - preimage, - rate: invoice.rate, - received: true, - confirmed: true, - network: "lightning", - tip: invoice.tip, - invoice_id: invoice.id, - createdAt, - updatedAt: createdAt - }); - - await invoice.increment({ received: amount }, { transaction }); - await account.increment({ balance: amount }, { transaction }); - - l("lightning missing from account", account.id, amount); - } - } - } catch (e) { - console.log(e); - err("problem syncing lightning payment", JSON.stringify(missed[i])); - } - } - }); - - const unconfirmed = await db.Payment.findAll({ - where: { - confirmed: 0 - }, - include: [ - { - model: db.Account, - as: "account" - }, - { - model: db.Invoice, - as: "invoice" - }, - { - model: db.User, - as: "user" - } - ] - }); - - let hashes = unconfirmed.map(p => p.hash); - - const transactions = [ - ...(await bc.listTransactions("*", 500)), - ...(await lq.listTransactions("*", 500)) - ].filter( - tx => - ["send", "receive"].includes(tx.category) && - new Date(tx.time * 1000) > twoWeeksAgo - ); - - missed = transactions.filter( - tx => - tx.category === "receive" && - tx.confirmations > 2 && - hashes.includes(tx.txid) - ); - - for (let i = 0; i < missed.length; i++) { - let p = unconfirmed.find(p => p.hash === missed[i].txid); - p.confirmed = 1; - - try { - await db.transaction(async transaction => { - l("confirming payment", p.id, p.user_id, p.hash, p.address); - await p.save({ transaction }); - - if (p.invoice) - await p.invoice.increment({ received: p.amount }, { transaction }); - - await p.account.increment({ balance: p.amount }, { transaction }); - await p.account.decrement({ pending: p.amount }, { transaction }); - }); - - emit(p.user.username, "account", p.account); - emit(p.user.username, "payment", p); - } catch (e) { - err("problem confirming payment", p.id, e.message, e.stack); - } - } - - store.unaccounted = []; - - transactions.map(tx => { - if ( - !payments.find(p => p.hash === tx.txid) && - !store.exceptions.includes(tx.txid) && - !store.unaccounted.find(a => a.txid === tx.txid) && - new Date(tx.blocktime * 1000) >= twoWeeksAgo - ) { - store.unaccounted.push(tx); - } - }); - - if (store.unaccounted.length) - warn( - "wallet transactions missing from database", - store.unaccounted.map(tx => tx.txid) - ); - - let receipts = store.unaccounted - .filter( - tx => - tx.category === "receive" && - (!tx.asset || tx.asset === config.liquid.btcasset) - ) - .map(tx => tx.address); - - invoices = await db.Invoice.findAll({ - where: { - [Op.or]: { - address: receipts, - unconfidential: receipts - } - }, - include: [ - { - model: db.Account, - as: "account" - }, - { - model: db.User, - as: "user" - } - ] - }); - - for (let i = 0; i < invoices.length; i++) { - let { - address, - unconfidential, - account, - account_id, - currency, - id: invoice_id, - network, - rate, - tip, - user, - user_id - } = invoices[i]; - - l("unaccounted", address); - - let pending = store.unaccounted.filter( - tx => tx.address === address || tx.address === unconfidential - ); - - for (let i = 0; i < pending.length; i++) { - let { amount, txid: hash } = pending[i]; - - amount = toSats(amount); - - try { - await db.transaction(async transaction => { - l("found pending transaction", user.username, amount, hash); - store.payments.push(hash); - let p = await db.Payment.create( - { - account_id, - address: address || unconfidential, - amount, - confirmed: false, - currency, - hash, - invoice_id, - network, - rate, - received: true, - tip, - user_id - }, - { transaction } - ); - - await account.increment({ pending: amount }, { transaction }); - - emit(user.username, "account", account); - emit(user.username, "payment", p); - }); - } catch (e) { - err("problem updating pending transaction", hash, e.message, e.stack); - } - } - } - } catch (e) { - console.log(e); - err("sync check failed", e.message, e.stack); - } - - setTimeout(sync, 300000); -}; - -setTimeout(sync, 5000); - -app.get("/conversions", async (req, res) => { - const transactions = [ - ...(await bc.listTransactions("*", 500)), - ...(await lq.listTransactions("*", 500)) - ].filter(tx => ["receive"].includes(tx.category)); - - const { addr } = req.query; - const { invoices } = require("./invoices"); - const persist = require("./persist"); - let conversions = persist("data/conversions.json"); - const payments = ( - await db.Payment.findAll({ - attributes: ["hash"] - }) - ).map(p => p.hash); - res.send( - Object.keys(conversions) - .filter(c => conversions[c].address === addr) - .filter(c => { - return ( - invoices.find( - i => i.payment_request === c && i.state === "SETTLED" - ) || transactions.find(tx => tx.txid === c) - ); - }) - .filter(c => !payments.includes(c)) - ); -}); - -app.post("/renameImages", adminAuth, async (req, res) => { - let d = './uploads/'; - fs.readdir(d, (err, files) => { - files.forEach(async file => { - try { - let m = file.match(/(.*)-profile/); - - let username; - - if (m && m[1]) username = m[1]; - else m = file.match(/(.*)-banner/); - if (m && m[1]) username = m[1]; - - console.log(username); - - if (username) { - let { uuid } = await db.User.findOne({ where: { username } }); - fs.rename(d + file, d + uuid + file.replace(username, ""), function(err) { - if (err) console.log("ERROR: " + err); - }); - } - } catch (e) { - console.log(e); - } - }); - }); - res.send({}); -}); diff --git a/lib/utils.js b/lib/utils.js index 95ee5fb1..1513138b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,16 +1,8 @@ import store from "$lib/store"; import redis from "$lib/redis"; import config from "$config"; -import db from "$db"; -import sequelize from "@sequelize/core"; -import { fromBase58 } from "bip32"; -import { payments as bcPayments, networks as bcNetworks } from "bitcoinjs-lib"; -import { payments as lqPayments, networks as lqNetworks } from "liquidjs-lib"; -import lnd from "$lib/lnd"; import ln from "$lib/ln"; -const { Op } = sequelize; - export const nada = () => {}; export const sleep = n => new Promise(r => setTimeout(r, n)); @@ -26,57 +18,13 @@ export const requirePin = async ({ body, user }) => { throw new Error("Invalid pin"); }; -export const getUser = async (username, transaction) => { - let params = { - include: [ - { - model: db.Account, - as: "account" - } - ], - where: { - [Op.or]: { - username: { [Op.or]: [username, username.replace(/ /g, "")] }, - uuid: { [Op.or]: [username, username.replace(/ /g, "")] } - } - } - }; - - if (transaction) { - params.lock = transaction.LOCK.UPDATE; - params.transaction = transaction; - } - - let user = await db.User.findOne(params); - if (!user) throw new Error(`User ${username} not found`); - +export const getUser = async (uuid) => { + let user = await redis.get(`user:${uuid}`); user.haspin = !!user.pin; return user; }; -export const getUserById = async (id, transaction) => { - let params = { - include: [ - { - model: db.Account, - as: "account" - } - ], - where: { id } - }; - - if (transaction) { - params.lock = transaction.LOCK.UPDATE; - params.transaction = transaction; - } - - let user = await db.User.findOne(params); - if (!user) throw new Error("User not found"); - - return user; -}; - export const prod = process.env.NODE_ENV === "production"; export const fail = msg => { throw new Error(msg); @@ -85,98 +33,6 @@ export const fail = msg => { export const SATS = 100000000; export const toSats = n => Math.round(n * SATS); -export const getBlockHeight = message => { - let buffer = Buffer.from(message, "hex"); - if (buffer.length < 80) throw new Error("Buffer too small (< 80 bytes)"); - - let offset = 0; - const readSlice = n => { - offset += n; - return buffer.slice(offset - n, offset); - }; - - const readUInt32 = () => { - const i = buffer.readUInt32LE(offset); - offset += 4; - return i; - }; - - const readUInt8 = () => { - const i = buffer.readUInt8(offset); - offset += 1; - return i; - }; - - const readVarInt = () => { - const vi = varuint.decode(buffer, offset); - offset += varuint.decode.bytes; - return vi; - }; - - readUInt32(); - readSlice(32); - readSlice(32); - readUInt32(); - return readUInt32(); -}; - -export const deriveAddress = async (account, type) => { - let { index, pubkey, path, network } = account; - let n = { bitcoin: bcNetworks, liquid: lqNetworks }[network][ - prod ? "bitcoin" : "regtest" - ]; - let p = { bitcoin: bcPayments, liquid: lqPayments }[network]; - let root = fromBase58(pubkey, n); - let parts = path.split("/"); - let hd = root.derive(parseInt(parts[parts.length - 1])); - - type = { - bech32: "p2wpkh", - "p2sh-segwit": "p2sh", - legacy: "p2pkh" - }[type]; - - console.log("TYPE", type); - - let r; - if (type !== "p2sh") { - r = p[type]({ - pubkey: hd.publicKey, - network: n - }); - } else { - r = p[type]({ - redeem: p.p2wpkh({ - pubkey: hd.publicKey, - network: n - }), - network: n - }); - } - - index++; - account.index = index; - account.address = r.address; - await account.save(); - - return r; -}; - -export const bip21 = ({ address, amount, memo, tip, network }, { asset }) => { - if (network === "liquid") network = "liquidnetwork"; - - let url = amount || memo ? `${network}:${address}?` : address; - if (amount) - url += `amount=${((amount + tip) / SATS).toFixed(8)}${memo ? "&" : ""}`; - if (memo) url += `message=${memo}`; - - if (network === "liquidnetwork" && amount) { - url += `&asset=${asset}`; - } - - return url; -}; - export const derivePayRequest = async ({ amount, memo, tip }) => { if (!tip) tip = 0; let value = amount + tip; diff --git a/lib/wretch.js b/lib/wretch.js deleted file mode 100644 index 0be00d10..00000000 --- a/lib/wretch.js +++ /dev/null @@ -1,4 +0,0 @@ -import wretch from "wretch"; -import fetch from "node-fetch"; -wretch().polyfills({ fetch }); -export default wretch; diff --git a/mine.sh b/mine.sh deleted file mode 100755 index 0df5b6ba..00000000 --- a/mine.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - - -while true -do - docker exec -it liquid elements-cli -datadir=/config generatetoaddress 1 $(docker exec -it liquid elements-cli -datadir=/config getnewaddress) - sleep 8 -done diff --git a/nodemon.json b/nodemon.json deleted file mode 100644 index 44ce3e3b..00000000 --- a/nodemon.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ignore": ["data/*"] -} diff --git a/package.json b/package.json index ad806617..631f61e9 100644 --- a/package.json +++ b/package.json @@ -7,49 +7,19 @@ "dev": "nodemon index.js" }, "dependencies": { - "@fastify/http-proxy": "^8.4.0", "@fastify/multipart": "^7.1.2", "@fastify/passport": "^2.2.0", "@fastify/secure-session": "^5.2.0", "@fastify/static": "^6.5.0", "@fastify/websocket": "^7.0.1", - "@sequelize/core": "7.0.0-alpha.10", - "async-mutex": "^0.2.4", - "axios": "^0.21.1", "bcrypt": "^5.0.0", - "bech32": "^2.0.0", - "bip32": "^2.0.5", - "bip39": "^3.0.2", - "bitcoinjs-lib": "6", - "body-parser": "^1.19.0", - "bolt11": "^1.3.2", - "buffer-reverse": "^1.0.1", "clightning-client": "^0.1.2", - "coinselect": "^3.1.12", - "compression": "^1.7.4", - "cookie-parser": "^1.4.4", - "cors": "^2.8.5", "date-fns": "^2.21.3", - "ecpair": "^2.0.1", - "express": "4.17.1", - "express-async-handler": "^1.1.4", - "express-pino-logger": "^6.0.0", - "express-session": "^1.16.2", "fastify": "^4.5.3", "file-type": "^18.0.0", "got": "^12.5.3", - "http-proxy-middleware": "^0.20.0", - "imap": "^0.8.19", "jsonwebtoken": "^8.5.1", - "knex": "^2.3.0", - "liquidjs-lib": "6.0.2-liquid.8", "lnurl": "^0.24.2", - "long": "^4.0.0", - "mailgun-js": "^0.22.0", - "mariadb": "^2.4.0", - "mysql": "^2.18.1", - "mysql2": "^2.2.5", - "node-fetch": "^2.6.1", "nodemon": "^2.0.7", "nostr": "^0.2.7", "otplib": "^12.0.1", @@ -60,16 +30,11 @@ "postmark": "^3.0.11", "pump": "^3.0.0", "query-string": "^6.13.1", - "readable-stream-clone": "^0.0.7", "redis": "^4.3.1", "sharp": "^0.31.1", - "sqlite3": "^5.0.11", - "tiny-secp256k1": "^2.1.2", "uuid": "8", "web-push": "^3.4.4", - "wretch": "^1.7.4", - "ws": "^7.4.6", - "zeromq": "6.0.0-beta.6" + "ws": "^7.4.6" }, "devDependencies": { "prettier": "^1.19.1" diff --git a/prod.yml b/prod.yml deleted file mode 100644 index 58729359..00000000 --- a/prod.yml +++ /dev/null @@ -1,96 +0,0 @@ -version: '3.6' -services: - app: - depends_on: - - cl - - maria - - bitcoin - - liquid - container_name: app - image: gcr.io/coinos-326717/github.com/coinos/coinos-server:prod - restart: always - environment: - NODE_ENV: production - NODE_OPTIONS: "--es-module-specifier-resolution=node --experimental-modules --loader ./loader.js --openssl-legacy-provider" - HOSTNAME: "${HOSTNAME}" - volumes: - - ./:/app - - ./logs:/logs - networks: - default: - ipv4_address: 172.42.0.22 - maria: - container_name: maria - image: mariadb - restart: always - environment: - MARIADB_ROOT_PASSWORD: password - volumes: - - ./mysql:/var/lib/mysql - bitcoin: - image: asoltys/bitcoin - container_name: bitcoin - volumes: - - ./config/bitcoin:/home/bitcoin/.bitcoin - restart: always - networks: - default: - ipv4_address: 172.42.0.23 - liquid: - image: ghcr.io/vulpemventures/elements - container_name: liquid - volumes: - - ./config/liquid:/home/elements/.elements - restart: always - networks: - default: - ipv4_address: 172.42.0.24 - cl: - depends_on: - - bitcoin - image: elementsproject/lightningd:v22.11.1 - container_name: cl - restart: always - volumes: - - ./config/lightning:/root/.lightning - ports: - - "9736:9735" - networks: - default: - ipv4_address: 172.42.0.25 - lnd: - depends_on: - - bitcoin - - tor - image: lightninglabs/lnd:v0.15.4-beta - container_name: lnd - volumes: - - ./config/lnd:/root/.lnd - - ./config/bitcoin:/root/.bitcoin - - ./config/tor:/etc/tor - ports: - - "9735:9735" - restart: always - networks: - default: - ipv4_address: 172.42.0.26 - tor: - container_name: tor - image: dperson/torproxy - restart: always - volumes: - - ./config/tor:/etc/tor - - ./config/tordata:/var/lib/tor - networks: - default: - ipv4_address: 172.42.0.27 - redis: - restart: always - container_name: redis - image: redis - volumes: - - ./config/redis:/data -networks: - default: - name: net - external: true diff --git a/reg.yml b/reg.yml deleted file mode 100644 index e03d9352..00000000 --- a/reg.yml +++ /dev/null @@ -1,96 +0,0 @@ -version: '3.6' -services: - app: - depends_on: - - cl - - maria - - bitcoin - - liquid - container_name: app - image: gcr.io/coinos-326717/github.com/coinos/coinos-server:prod - restart: always - environment: - NODE_ENV: production - NODE_OPTIONS: "--es-module-specifier-resolution=node --experimental-modules --loader ./loader.js --openssl-legacy-provider" - HOSTNAME: "${HOSTNAME}" - volumes: - - ./:/app - - ./logs:/logs - networks: - default: - ipv4_address: 172.42.0.22 - maria: - container_name: maria - image: mariadb - restart: always - environment: - MARIADB_ROOT_PASSWORD: password - volumes: - - ./mysql:/var/lib/mysql - bitcoin: - image: ghcr.io/vulpemventures/bitcoin - container_name: bitcoin - volumes: - - ./config/bitcoin:/home/bitcoin/.bitcoin - restart: always - networks: - default: - ipv4_address: 172.42.0.23 - liquid: - image: ghcr.io/vulpemventures/elements - container_name: liquid - volumes: - - ./config/liquid:/home/elements/.elements - restart: always - networks: - default: - ipv4_address: 172.42.0.24 - cl: - depends_on: - - bitcoin - image: elementsproject/lightningd:v0.12.0 - container_name: cl - restart: always - volumes: - - ./config/lightning:/root/.lightning - ports: - - "9736:9735" - networks: - default: - ipv4_address: 172.42.0.25 - lnd: - depends_on: - - bitcoin - - tor - image: lightninglabs/lnd:v0.15.4-beta - container_name: lnd - volumes: - - ./config/lnd:/root/.lnd - - ./config/bitcoin:/root/.bitcoin - - ./config/tor:/etc/tor - ports: - - "9735:9735" - restart: always - networks: - default: - ipv4_address: 172.42.0.26 - tor: - container_name: tor - image: dperson/torproxy - restart: always - volumes: - - ./config/tor:/etc/tor - - ./config/tordata:/var/lib/tor - networks: - default: - ipv4_address: 172.42.0.27 - redis: - restart: always - container_name: redis - image: redis - volumes: - - ./config/redis:/data -networks: - default: - name: net - external: true diff --git a/routes/admin.js b/routes/admin.js deleted file mode 100644 index 3053d813..00000000 --- a/routes/admin.js +++ /dev/null @@ -1,566 +0,0 @@ -import express from "express"; -var router = express.Router(); - -import * as parseInput from "../scripts/apiParse"; -import Sequelize from "@sequelize/core"; -const Op = Sequelize.Op; - -/** - * @api {get} /users Retrieve user list - * @apiName getUsers - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess {Object} users List of users - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "users" : [ - * { "username": 'John Doe', "email": 'johndoe@gmail.com', ...}, - * { "username": 'Jane Doe', "email": 'janedoe@gmail.com', ...} - * ] - * } - */ -router.get( - "/users", - // auth, - async (req, res) => { - var { search, starts_with, contains } = req.query; - var users = knex - .select( - "username", - "email", - "phone", - "verified", - knex.raw("LEFT(createdAt,10) as created_at") - ) - .from("users"); - - var timeCondition = parseInput.addTimeSearch(req.query, "users.createdAt"); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) users = users.whereRaw(timeCondition); - if (userCondition) users = users.whereRaw(userCondition); - - // Alternative Sequelize query syntax - // await db.User.findAll({ - // attributes: ['username', 'email', 'phone', 'verified', 'createdAt'] - // }) - - const found = await users; - return res.send({ users: found }); - } -); - -/** - * @api {get} /referrals Retrieve list of referral tokens - * @apiName referrals - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of referral tokens - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "referrals" : [ - * { "token": '***', "status": 'pending', sponsor: 'Adam', user: 'newUsername1', expiry: null }, - * { "token": '***', "status": 'pending', sponsor: 'Adam', user: 'newUsername2', expiry: null }, - * ] - * } - */ -router.get("/referrals", auth, async (req, res) => { - var referrals = knex - .select( - "token", - "users.username as user", - "sponsor.username as sponsor", - knex.raw("LEFT(created_at, 10) as created_at"), - "referrals.status" - ) - .from("referrals") - .leftJoin("users", "users.id", "referrals.user_id") - .leftJoin("users as sponsor", "sponsor.id", "referrals.sponsor_id"); - - // Alternative Sequelize query syntax (returns sub-tables as hash) - // var referrals = await db.Referral.findAll({ - // include: [ - // {model: db.User, as: 'user', attributes: [['username', 'user']]}, - // {model: db.User, as: 'sponsor', attributes: [['username', 'sponsor']]} - // ] - // }) - - var timeCondition = parseInput.addTimeSearch( - req.query, - "referrals.updated_at" - ); - var userCondition = parseInput.addUserSearch(req.query, "sponsor"); - - if (userCondition) { - referrals = referrals.whereRaw(userCondition); - } - if (timeCondition) { - referrals = referrals.whereRaw(timeCondition); - } - - var found = await referrals; - return res.send({ referrals: found }); -}); - -/** - * @api {get} /waiting_list Retrieve waiting_list - * @apiName waiting_list - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of referral tokens - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "waiting_list" : [ - * { email: "johndoe@hotmail.ca", phone: '604 123-4567 }, - * { email: "janedoe@hotmail.ca", phone: '604 246-8910 }, - * ] - * } - */ -router.get( - "/waiting_list", - // auth, - async (req, res) => { - var { search, starts_with, contains } = req.query; - - var queue = knex - .select( - "waiting_list.email", - "waiting_list.phone", - "waiting_list.created_at as requested", - "users.id as current_user_id" - ) - .from("waiting_list") - .leftJoin("users", "users.email", "waiting_list.email") - .where("waiting_list.id", ">", 0); - - var timeCondition = parseInput.addTimeSearch( - req.query, - "waiting_list.updated_at" - ); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) queue = queue.whereRaw(timeCondition); - if (userCondition) queue = queue.whereRaw(userCondition); - - const found = await queue; - - return res.send({ queue: found }); - } -); - -/** - * @api {get} /accounts Retrieve list of accounts - * @apiName accounts - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of accounts - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "accounts" : [ - * { "user": 'Adam', "account_id": '123', balance: 500 ... }, - * ... - * ] - * } - */ -router.get("/accounts", auth, async (req, res) => { - var { nonZero, search, starts_with, contains } = req.query; - - var accounts = knex - .select( - "username", - "accounts.id as account_id", - "ticker", - "balance", - knex.raw("LEFT(accounts.createdAt, 10) as created"), - knex.raw("LEFT(accounts.updatedAt, 10) as updated") - ) - .from("accounts") - .leftJoin("users", "users.id", "accounts.user_id"); - - if (nonZero) { - accounts = accounts.where("balance", ">", 0); - } - - var timeCondition = parseInput.addTimeSearch(req.query, "accounts.updatedAt"); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) accounts = accounts.whereRaw(timeCondition); - if (userCondition) accounts = accounts.whereRaw(userCondition); - - const found = await accounts; - - return res.send({ accounts: found }); -}); - -/** - * @api {get} /transactions Retrieve summary of user transactions - * - * @apiName user_transactions - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of invoices, payments, orders for users - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "transactions" : [ - * { "username": 'Adam', "email": 'adam@gmail.com', invoices: 1, orders: 0, payments: 2 }, - ... - * ] - * } - */ -router.get( - "/transactions", - // auth, - async (req, res) => { - var { type, since, search, starts_with, contains } = req.query; - - var transactions = knex - .select( - "users.username", - "users.email" - // knex.raw('LEFT(created_at, 10) as created') - // knex.raw('LEFT(updatedAt, 10) as updated') - // knex.raw('COUNT(invoices.id) as invoices'), - // knex.raw('COUNT(payments.id) as payments'), - // knex.raw('COUNT(orders.id) as orders') - ) - .from("users"); - - const types = ["deposits", "withdrawals", "orders", "payments", "invoices"]; - for (var i = 0; i < types.length; i++) { - var stamp = types[i].replace(/s$/, "") + "_date"; - - transactions = transactions - .select( - knex.raw("COUNT(DISTINCT " + types[i] + ".id) as " + types[i]), - knex.raw("LEFT(MAX(" + types[i] + ".updatedAt), 10) as " + stamp) - ) - .leftJoin(types[i], "users.id", types[i] + ".user_id"); - // .havingRaw(types[i] + ' > ?', [0]) - - if (since) { - transactions = transactions.havingRaw( - "(" + stamp + " >= ? OR " + stamp + " IS NULL)", - [since] - ); - } - } - - if (since) - transactions = transactions.havingRaw( - "invoices + payments + deposits + orders > ?", - [0] - ); - - var userCondition = parseInput.addUserSearch(req.query); - if (userCondition) transactions = transactions.whereRaw(userCondition); - - transactions = transactions.groupBy("users.id"); - - const found = await transactions; - - return res.send({ transactions: found }); - } -); - -/** - * @api {get} /orders Retrieve summary of orders - * - * @apiName orders - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of orders - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "orders" : [ - * { "seller": 'Adam', "buyer": 'bob' ... }, - ... - * ] - * } - */ -router.get("/orders", auth, async (req, res) => { - var { type, since, search, starts_with, contains } = req.query; - - var orders = knex - .select( - "u1.username as seller", - "u2.username as buyer", - "v1 as sell_amount", - "v2 as buy_amount", - "rate", - "a1.name as from", - "a2.name as to", - knex.raw("Left(completedAt,16) as completed") - ) - .from("orders") - .leftJoin("accounts as a1", "a1.id", "orders.a1_id") - .leftJoin("accounts as a2", "a2.id", "orders.a2_id") - .leftJoin("users as u1", "a1.user_id", "u1.id") - .leftJoin("users as u2", "a2.user_id", "u2.id"); - - var timeCondition = parseInput.addTimeSearch(req.query, "orders.updatedAt"); - if (timeCondition) orders = orders.whereRaw(timeCondition); - - var userCondition1 = parseInput.addUserSearch(req.query, "u1"); - var userCondition2 = parseInput.addUserSearch(req.query, "u2"); - - if (userCondition1) - orders = orders.where(function() { - this.whereRaw(userCondition1); - this.orWhere(knex.raw(userCondition2)); - }); - - var found = await orders; - return res.send({ orders: found }); -}); - -/** - * @api {get} /invoices Retrieve summary of invoices - * - * @apiName invoices - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of invoices - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "invoices" : [ - * { "seller": 'Adam', "buyer": 'bob' ... }, - ... - * ] - * } - */ -router.get("/invoices", auth, async (req, res) => { - var { type, since, search, starts_with, contains } = req.query; - - var invoices = knex - .select( - "username", - "users.email", - "invoices.currency", - "invoices.amount", - "accounts.network", - knex.raw("Left(invoices.updatedAt,16) as updated") - ) - .from("invoices") - .leftJoin("accounts", "invoices.account_id", "accounts.id") - .leftJoin("users", "accounts.user_id", "users.id"); - - var timeCondition = parseInput.addTimeSearch(req.query, "invoices.updatedAt"); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) invoices = invoices.whereRaw(timeCondition); - if (userCondition) invoices = invoices.whereRaw(userCondition); - - var found = await invoices; - return res.send({ invoices: found }); -}); - -/** - * @api {get} /deposits Retrieve summary of deposits - * - * @apiName deposits - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of deposits - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "deposits" : [ - * { "seller": 'Adam', "buyer": 'bob' ... }, - ... - * ] - * } - */ -router.get("/deposits", auth, async (req, res) => { - var { type, since, search, starts_with, contains } = req.query; - - var deposits = knex - .select( - "username", - "users.email", - "amount", - "credited", - knex.raw("Left(deposits.updatedAt,16) as deposited") - ) - .from("deposits") - .leftJoin("users", "deposits.user_id", "users.id"); - - var timeCondition = parseInput.addTimeSearch(req.query, "deposits.updatedAt"); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) deposits = deposits.whereRaw(timeCondition); - if (userCondition) deposits = deposits.whereRaw(userCondition); - - var found = await deposits; - return res.send({ deposits: found }); -}); - -/** - * @api {get} /withdrawals Retrieve summary of withdrawals - * - * @apiName withdrawals - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of withdrawals - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "withdrawals" : [ - * { "seller": 'Adam', "buyer": 'bob' ... }, - ... - * ] - * } - */ -router.get("/withdrawals", auth, async (req, res) => { - var { type, since, search, starts_with, contains } = req.query; - - var withdrawals = knex - .select( - "username", - "users.email", - "amount", - "notes", - knex.raw("Left(withdrawals.updatedAt,16) as withdrawn") - ) - .from("withdrawals") - .leftJoin("users", "withdrawals.user_id", "users.id"); - - var timeCondition = parseInput.addTimeSearch( - req.query, - "withdrawals.updatedAt" - ); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) withdrawals = withdrawals.whereRaw(timeCondition); - if (userCondition) withdrawals = withdrawals.whereRaw(userCondition); - - var found = await withdrawals; - return res.send({ withdrawals: found }); -}); - -/** - * @api {get} /payments Retrieve summary of payments - * - * @apiName payments - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of payments - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "payments" : [ - * { "seller": 'Adam', "buyer": 'bob' ... }, - ... - * ] - * } -**/ -router.get("/payments", auth, async (req, res) => { - var { type, since, search, starts_with, contains } = req.query; - - var payments = knex - .select( - "username", - "users.email", - "amount", - "payments.network", - knex.raw("LEFT(hash,20) as hash"), - knex.raw("Left(payments.updatedAt,16) as deposited") - ) - .from("payments") - .leftJoin("accounts", "payments.account_id", "accounts.id") - .leftJoin("users", "accounts.user_id", "users.id"); - - var timeCondition = parseInput.addTimeSearch(req.query, "payments.updatedAt"); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) payments = payments.whereRaw(timeCondition); - if (userCondition) payments = payments.whereRaw(userCondition); - - var found = await payments; - return res.send({ payments: found }); -}); - -/** - * @api {get} /user_kyc Retrieve user kyc details - * - * @apiName user_kyc - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of kyc details for each user - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "users" : [ - * { "username": 'Adam', "email": 'adam@gmail.com', ... kyc_data... }, - ... - * ] - * } - */ -router.get("/kyc_required", auth, async (req, res) => { - var { since, search, starts_with, contains } = req.query; - - var threshold = "0.2"; - - var kyc = knex - .select( - knex.raw("CONCAT( FORMAT(Max(amount)/1000000, 1), ' M') as max"), - "username", - "users.email", - "users.verified as kyc_verified", - // 'payments.currency', - // 'payments.network', - // knex.raw('LEFT(hash,20) as hash'), - // knex.raw('Left(payments.updatedAt,16) as transferred') - // knex.raw( parseInput.readableDate('Max(payments.updatedAt)', 'last_changed') ) - knex.raw( - "DATE_FORMAT(Max(payments.updatedAt), ?) AS last_changed", - "%M %d, %Y" - ) - ) - .from("payments") - .leftJoin("accounts", "payments.account_id", "accounts.id") - .leftJoin("users", "payments.user_id", "users.id") // should really point to accounts - .groupBy("users.id"); - // .groupBy('payments.currency') - // .groupBy('payments.network') - - var timeCondition = parseInput.addTimeSearch(req.query, "payments.updatedAt"); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) kyc = kyc.whereRaw(timeCondition); - if (userCondition) kyc = kyc.whereRaw(userCondition); - - var found = await kyc.having("max", ">=", threshold); - - return res.send({ found: found }); -}); - -export default router; diff --git a/routes/assets.js b/routes/assets.js deleted file mode 100644 index ed0d3036..00000000 --- a/routes/assets.js +++ /dev/null @@ -1,536 +0,0 @@ -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import db from "$db"; -import config from "$config"; -import app from "$app"; -import { auth } from "$lib/passport"; -import axios from "axios"; -import crypto from "crypto"; -import fs from "fs"; -import { err, l } from "$lib/logging"; -import lq from "$lib/liquid"; -import { SATS } from "$lib/utils"; - -let fetchAssets; -(fetchAssets = async () => { - try { - const { data } = await axios.get("https://assets.blockstream.info/"); - - store.assets = data; - } catch (e) { - var liquid_assets = JSON.parse(fs.readFileSync("/app/assets.json")); - if (liquid_assets) { - store.assets = liquid_assets; - console.debug("using static assets..." + e.message); - } else { - err("error fetching assets", e.message); - res.code(500).send("error fetching assets"); - } - } - - setTimeout(fetchAssets, 7200000); -})(); - -app.get("/assets", async (req, res) => { - if (store.assets) { - const accounts = await db.Account.findAll({ - // group: ["asset"] - }); - Object.keys(store.assets).map(a => { - store.assets[a].registered = true; - if (!store.assets[a].asset) - store.assets[a].asset = store.assets[a].asset_id; - if ( - (store.assets[a].ticker === "BTC" && - store.assets[a].asset !== config.liquid.btcasset) || - (store.assets[a].ticker === "EUR" && - store.assets[a].asset !== config.liquid.eurasset) || - (store.assets[a].ticker === "CAD" && - store.assets[a].asset !== config.liquid.cadasset) || - (store.assets[a].ticker === "USDt" && - store.assets[a].asset !== config.liquid.usdtasset) - ) - delete store.assets[a]; - }); - accounts.map(({ asset, name, domain, ticker, precision }) => { - if (!store.assets[asset]) - store.assets[asset] = { - asset, - name, - domain, - ticker, - precision - }; - }); - - res.send(store.assets); - } else { - console.log("error getting blockstream assets"); - res.code(500).send("Problem fetching blockstream asset registry data"); - } -}); - -app.post("/assets", auth, async (req, res) => { - try { - const sha256 = crypto.createHash("sha256"); - const { id: user_id } = req.user; - const blind = false; - - if (!user_id) throw new Error("unauthorized"); - - const { - address, - domain, - name, - asset_amount, - filename, - token_amount, - precision, - pubkey, - ticker - } = req.body; - - const asset_address = address - ? address - : await lq.getNewAddress("", "legacy"); - - const token_address = - token_amount && (await lq.getNewAddress("", "legacy")); - - const issuer_pubkey = pubkey - ? pubkey - : (await lq.getAddressInfo(asset_address)).pubkey; - const version = 0; - - let contract = { - entity: { domain }, - issuer_pubkey, - name, - precision, - version - }; - - if (filename) contract.file = filename; - if (ticker) contract.ticker = ticker; - - contract = Object.keys(contract) - .sort() - .reduce((r, k) => ((r[k] = contract[k]), r), {}); - - l( - "attempting issuance", - req.user.username, - JSON.stringify(contract), - "address", - address, - "pubkey", - pubkey - ); - - sha256.update(JSON.stringify(contract)); - const hash = sha256.digest("hex"); - const contract_hash = hash - .match(/[a-f0-9]{2}/g) - .reverse() - .join(""); - const rawtx = await lq.createRawTransaction([], [{ data: "00" }]); - const funded = await lq.fundRawTransaction(rawtx, { feeRate: 0.000002 }); - const params = { - asset_amount, - asset_address, - blind, - contract_hash - }; - - params.asset_amount = asset_amount / (SATS / 10 ** precision); - - if (token_amount) { - params.token_amount = token_amount; - params.token_address = token_address; - params.token_amount = parseInt(params.token_amount); - } - - let ria; - try { - ria = await lq.rawIssueAsset(funded.hex, [params]); - } catch (e) { - l(asset_amount, token_amount, params); - throw new Error(e.message); - } - - const { asset, hex, token } = ria[0]; - const brt = await lq.blindRawTransaction(hex, true, [], false); - const srt = await lq.signRawTransactionWithWallet(brt); - const allowed = (await lq.testMempoolAccept([srt.hex]))[0].allowed; - if (!allowed) throw new Error("issuance rejected by mempool", srt.hex); - const txid = await lq.sendRawTransaction(srt.hex); - - await db.transaction(async transaction => { - let account = await db.Account.findOne({ - where: { - user_id, - asset: config.liquid.btcasset, - pubkey: null - }, - include: { - model: db.User, - as: "user" - }, - order: [["balance", "DESC"]], - lock: transaction.LOCK.UPDATE, - transaction - }); - - let { user } = account; - - if (Math.round(funded.fee * SATS) > account.balance) { - err( - "amount exceeds balance", - asset_amount, - funded.fee, - account.balance - ); - throw new Error(`Insufficient funds to pay fee of ${funded.fee} BTC`); - } - - await account.decrement( - { balance: Math.round(funded.fee * SATS) }, - { transaction } - ); - await account.reload({ transaction }); - emit(user.username, "account", account); - - let invoice; - let recipient = user; - if (address) { - invoice = await db.Invoice.findOne({ - where: { address }, - include: { - model: db.User, - as: "user" - } - }); - recipient = invoice.user; - } - - if (!invoice) { - invoice = await db.Invoice.create( - { - account_id: account.id, - user_id, - amount: Math.round(params.asset_amount * SATS), - address: asset_address, - currency: user.currency, - rate: store.rates[user.currency], - network: "liquid" - }, - { transaction } - ); - } - - account = await db.Account.create( - { - asset, - contract, - domain, - user_id: invoice.user_id, - ticker, - precision, - name, - balance: 0, - network: "liquid", - pending: Math.round(params.asset_amount * SATS) - }, - { transaction } - ); - - emit(recipient.username, "account", account); - l( - "issued asset", - user.username, - params.asset_amount, - ticker, - name, - account.id - ); - - const asset_payment = await db.Payment.create( - { - account_id: account.id, - user_id: invoice.user_id, - invoice_id: invoice.id, - hash: txid, - amount: Math.round(params.asset_amount * SATS), - received: true, - confirmed: false, - address: asset_address, - network: "liquid" - }, - { transaction } - ); - emit(recipient.username, "payment", asset_payment); - - if (invoice.user_id !== user.id) { - const asset_send = await db.Payment.create( - { - account_id: account.id, - user_id, - hash: txid, - amount: -Math.round(params.asset_amount * SATS), - received: false, - confirmed: false, - address, - network: "liquid" - }, - { transaction } - ); - emit(recipient.username, "payment", asset_send); - } - - store.issuances[txid] = { - user_id, - asset, - asset_amount: params.asset_amount, - asset_payment_id: asset_payment.id - }; - - if (token_amount) { - invoice = null; - recipient = user; - - if (address) { - invoice = await db.Invoice.findOne({ - where: { address }, - include: { - model: db.User, - as: "user" - } - }); - recipient = invoice.user; - } - - if (!invoice) { - invoice = await db.Invoice.create( - { - account_id: account.id, - user_id, - amount: Math.round(params.token_amount * SATS), - address: token_address, - currency: user.currency, - rate: store.rates[user.currency], - network: "liquid" - }, - { transaction } - ); - } - - account = await db.Account.create( - { - asset: token, - user_id: invoice.user_id, - domain, - ticker: `${ticker}REISSUANCETOKEN`, - precision: 8, - name: `${name} Reissuance Token`, - balance: 0, - pending: address ? 0 : Math.round(token_amount * SATS) - }, - { transaction } - ); - emit(recipient.username, "account", account); - - const token_payment = await db.Payment.create( - { - account_id: account.id, - invoice_id: invoice.id, - user_id, - hash: txid, - amount: Math.round(token_amount * SATS), - received: true, - confirmed: false, - address: token_address, - network: "liquid" - }, - { transaction } - ); - emit(recipient.username, "payment", token_payment); - - if (invoice.user_id !== user.id) { - const token_send = await db.Payment.create( - { - account_id: account.id, - invoice_id: invoice.id, - user_id, - hash: txid, - amount: -Math.round(token_amount * SATS), - received: false, - confirmed: false, - address, - network: "liquid" - }, - { transaction } - ); - emit(recipient.username, "payment", token_send); - } - - store.issuances[txid].token = token; - store.issuances[txid].token_amount = token_amount; - store.issuances[txid].token_payment_id = token_payment.id; - } - }); - - res.send(store.issuances[txid] ? store.issuances[txid] : { asset }); - } catch (e) { - err("asset issuance failed", e.message, e.stack); - res.code(500).send(e.message); - } -}); - -app.post("/assets/register", auth, async (req, res) => { - const { asset } = req.body; - const account = await db.Account.findOne({ - where: { - user_id: req.user.id, - asset - } - }); - - l("registering", asset, account.contract); - - try { - const { data: result } = await axios.post( - "https://assets.blockstream.info/", - { - asset_id: asset, - contract: account.contract - } - ); - l("register asset result", req.user.username, result); - res.send(result); - } catch (e) { - err("asset registration failed", e.message); - res.code(500).send(e.message); - } -}); - -app.post("/loadFaucet", auth, async (req, res) => { - return res.code(500).send("Faucet feature temporarily disabled"); - - const { user } = req; - let { asset, amount } = req.body; - amount = parseInt(amount); - - try { - if (amount <= 0) throw new Error("Amount to load cannot be negative"); - - await db.transaction(async transaction => { - let account = await getAccount(config.liquid.btcasset, user, transaction); - - if (account.asset !== config.liquid.btcasset) - throw new Error( - "Faucet has to be funded with bitcoin. Try sending from another wallet." - ); - if (account.balance < amount) throw new Error("Insufficient funds"); - - let fee = 0; - - await account.decrement({ balance: amount }, { transaction }); - await account.reload({ transaction }); - - let a2; - let acc = { - user_id: null, - asset - }; - - a2 = await db.Account.findOne({ - where: acc, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (a2) { - await a2.increment({ balance: amount }, { transaction }); - await a2.reload({ transaction }); - } else { - let name = asset.substr(0, 6); - let domain; - let ticker = asset.substr(0, 3).toUpperCase(); - let precision = 8; - - const assets = assets; - - if (assets[asset]) { - ({ domain, ticker, precision, name } = assets[asset]); - } else { - const existing = await db.Account.findOne({ - where: { - asset - }, - order: [["id", "ASC"]], - limit: 1, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (existing) { - ({ domain, ticker, precision, name } = existing); - } - } - - acc = { ...acc, ...{ domain, ticker, precision, name } }; - acc.balance = amount; - acc.pending = 0; - acc.network = "liquid"; - a2 = await db.Account.create(acc, { transaction }); - } - - let params = { - amount: -amount, - account_id: account.id, - user_id: user.id, - rate: store.rates[user.currency], - currency: user.currency, - confirmed: true, - hash: `Loaded Faucet - ${a2.ticker}`, - network: "COINOS" - }; - - let payment = await db.Payment.create(params, { transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - - l("sent internal", user.username, -payment.amount); - - emit(user.username, "payment", payment); - emit(user.username, "account", account); - emit(user.username, "user", user); - - l("loaded faucet", asset, amount); - res.send({}); - }); - } catch (e) { - err("problem loading faucet", user.username, e.message); - return res.code(500).send(e.message); - } -}); - -app.get("/faucet", auth, async (req, res) => { - let { asset } = req.query; - let faucet = await db.Account.findOne({ - where: { - asset, - user_id: null - } - }); - - if (!faucet) - faucet = { - asset, - balance: 0, - ticker: "Unknown" - }; - - res.send(faucet); -}); diff --git a/routes/bitcoin/broadcast.js b/routes/bitcoin/broadcast.js deleted file mode 100644 index 85c8cf66..00000000 --- a/routes/bitcoin/broadcast.js +++ /dev/null @@ -1,56 +0,0 @@ -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import { requirePin } from "$lib/utils"; - -export default async (req, res) => { - try { - await requirePin(req); - - const { user } = req; - let hash = await bc.sendRawTransaction(req.body.tx); - - if (user) { - await db.transaction(async transaction => { - let account = await db.Account.findOne({ - where: { - id: user.account_id - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - let payment = await db.Payment.create( - { - ...req.body.payment, - hash, - amount: -req.body.payment.amount, - tip: 0, - account_id: account.id, - user_id: user.id, - rate: store.rates[user.currency], - currency: user.currency, - confirmed: true, - received: false, - network: "bitcoin" - }, - { transaction } - ); - - await account.increment( - { balance: payment.amount - payment.fee }, - { transaction } - ); - await account.reload({ transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - emit(user.username, "payment", payment); - emit(user.username, "account", payment.account); - res.send(payment); - }); - } else { - res.send(result); - } - } catch (e) { - res.code(500).send(e.message); - } -}; diff --git a/routes/bitcoin/conversionFee.js b/routes/bitcoin/conversionFee.js deleted file mode 100644 index 7e305659..00000000 --- a/routes/bitcoin/conversionFee.js +++ /dev/null @@ -1,12 +0,0 @@ -// Code for the conversion fee -// This is in a separate file so that the same function can be used by ./send and ./receive - -const conversionFeeMultiplier = 0.001; -export const conversionFeeReceiver = "coinosfees"; // account that receives the fees - -/** - * Returns the fee you have to pay for withdrawing 'amount' bitcoin. - */ -export function computeConversionFee(amount) { - return Math.floor(amount * conversionFeeMultiplier); -} diff --git a/routes/bitcoin/fee.js b/routes/bitcoin/fee.js deleted file mode 100644 index 74c26a92..00000000 --- a/routes/bitcoin/fee.js +++ /dev/null @@ -1,73 +0,0 @@ -import config from "$config"; -import db from "$db"; -import bc from "$lib/bitcoin"; -import { err } from "$lib/logging"; -import { emit } from "$lib/sockets"; -import buildTx from "$lib/buildtx"; -import { SATS } from "$lib/utils"; - -export default async (req, res) => { - let { user } = req; - let { address, amount, feeRate, replaceable = true } = req.body; - let tx, fee; - - if (user.account.pubkey) { - try { - let psbt = await buildTx({ address, amount, feeRate, replaceable, user }); - return res.send(psbt); - } catch (e) { - return res.code(500).send(e.message); - } - } - - let invoice = await db.Invoice.findOne({ - where: { address }, - include: { - attributes: ["username"], - model: db.User, - as: "user" - } - }); - - if (invoice) { - let { ismine } = await bc.getAddressInfo(address); - if (ismine) { - emit(user.username, "to", invoice.user); - return res.send({}); - } - } - - try { - amount = parseInt(amount); - - let partial = await bc.createRawTransaction( - [], - { - [address]: (amount / SATS).toFixed(8) - }, - 0, - replaceable - ); - - let params = { - subtractFeeFromOutputs: amount === user.balance ? [0] : [], - replaceable - }; - - if (feeRate) params.feeRate = (feeRate / SATS).toFixed(8); - - tx = await bc.fundRawTransaction(partial, params); - - if (config.bitcoin.walletpass) - await bc.walletPassphrase(config.bitcoin.walletpass, 300); - - let signed = await bc.signRawTransactionWithWallet(tx.hex); - let decoded = await bc.decodeRawTransaction(signed.hex); - feeRate = Math.round((tx.fee * SATS * 1000) / decoded.vsize); - - res.send({ feeRate, tx }); - } catch (e) { - err("bitcoin fee estimation error", e); - return res.code(500).send(e.message); - } -}; diff --git a/routes/bitcoin/generate.js b/routes/bitcoin/generate.js deleted file mode 100644 index 6285492d..00000000 --- a/routes/bitcoin/generate.js +++ /dev/null @@ -1,5 +0,0 @@ -export default async (req, res) => { - let address = await bc.getNewAddress(); - await bc.generateToAddress(1, address); - res.send({}); -}; diff --git a/routes/bitcoin/index.js b/routes/bitcoin/index.js deleted file mode 100644 index 58b0d193..00000000 --- a/routes/bitcoin/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import broadcast from "./broadcast"; -import generate from "./generate"; -import sweep from "./sweep"; -import fee from "./fee"; -import send from "./send"; - -export default { - broadcast, - generate, - sweep, - fee, - send -}; diff --git a/routes/bitcoin/receive.js b/routes/bitcoin/receive.js deleted file mode 100644 index 10193ceb..00000000 --- a/routes/bitcoin/receive.js +++ /dev/null @@ -1,281 +0,0 @@ -import config from "$config"; -import db from "$db"; -import bc from "$lib/bitcoin"; -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import { notify } from "$lib/notifications"; -import { callWebhook } from "$lib/webhooks"; -import reverse from "buffer-reverse"; -import zmq from "zeromq/v5-compat"; -import { Op } from "@sequelize/core"; -import { fromBase58 } from "bip32"; -import bitcoin from "bitcoinjs-lib"; -import { sendLiquid } from "$routes/liquid/send"; -import { computeConversionFee } from "./conversionFee"; -import { l, warn, err } from "$lib/logging"; - -const zmqRawBlock = zmq.socket("sub"); -zmqRawBlock.connect(config.bitcoin.zmqrawblock); -zmqRawBlock.subscribe("rawblock"); - -const zmqRawTx = zmq.socket("sub"); -zmqRawTx.connect(config.bitcoin.zmqrawtx); -zmqRawTx.subscribe("rawtx"); - -const network = - bitcoin.networks[ - config.bitcoin.network === "mainnet" ? "bitcoin" : config.bitcoin.network - ]; - -const queue = {}; -const seen = []; - -console.warn = () => {}; - -zmqRawTx.on("message", async (topic, message, sequence) => { - try { - const hex = message.toString("hex"); - let tx = bitcoin.Transaction.fromHex(message); - - let hash = reverse(tx.getHash()).toString("hex"); - - if (seen.includes(hash)) return; - seen.push(hash); - if (seen.length > 5000) seen.shift(); - - Promise.all( - tx.outs.map(async o => { - try { - const { value } = o; - - let address; - try { - address = bitcoin.address.fromOutputScript(o.script, network); - } catch (e) { - return; - } - - if ( - Object.keys(store.addresses).includes(address) && - !store.change.includes(address) - ) { - await db.transaction(async transaction => { - let user = await db.User.findOne({ - where: { - username: store.addresses[address] - }, - transaction - }); - - let invoice = await db.Invoice.findOne({ - where: { - address, - user_id: user.id, - network: "bitcoin" - }, - order: [["id", "DESC"]], - include: { - model: db.Account, - as: "account" - }, - transaction - }); - - if (!invoice) return; - - const currency = invoice ? invoice.currency : user.currency; - const rate = invoice ? invoice.rate : store.rates[user.currency]; - const tip = invoice ? invoice.tip : 0; - const memo = invoice ? invoice.memo : ""; - - let confirmed = false; - - let { account } = invoice; - - if (account.asset !== config.liquid.btcasset) { - account = await db.Account.findOne({ - where: { - user_id: user.id, - asset: config.liquid.btcasset, - pubkey: null - }, - transaction - }); - } - - await invoice.increment({ pending: value }, { transaction }); - await invoice.reload({ transaction }); - - await account.increment({ pending: value }, { transaction }); - await account.reload({ transaction }); - - if (config.bitcoin.walletpass) - await bc.walletPassphrase(config.bitcoin.walletpass, 300); - - let totalOutputs = tx.outs.reduce((a, b) => a + b.value, 0); - let totalInputs = 0; - for (let i = 0; i < tx.ins.length; i++) { - let { hash, index } = tx.ins[i]; - hash = reverse(hash).toString("hex"); - let hex = await bc.getRawTransaction(hash); - let inputTx = bitcoin.Transaction.fromHex(hex); - totalInputs += inputTx.outs[index].value; - } - let fee = totalInputs - totalOutputs; - - let payment = await db.Payment.create( - { - account_id: account.id, - user_id: user.id, - hash, - fee, - memo, - amount: value - tip, - currency, - rate, - received: true, - tip, - confirmed, - address, - network: "bitcoin", - invoice_id: invoice.id - }, - { transaction } - ); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - payment.invoice = invoice.get({ plain: true }); - - emit(user.username, "account", account); - emit(user.username, "payment", payment); - l("bitcoin detected", user.username, value); - notify(user, `${value} SAT payment detected`); - callWebhook(invoice, payment); - }); - } - } catch (e) { - console.log("problem parsing bitcoin tx", e); - } - }) - ); - } catch (e) { - console.log("problem receiving bitcoin tx", e); - } -}); - -zmqRawBlock.on("message", async (topic, message, sequence) => { - try { - const payments = await db.Payment.findAll({ - where: { confirmed: false } - }); - - let block = bitcoin.Block.fromHex(message.toString("hex")); - block.transactions.map(tx => { - let hash = reverse(tx.getHash()).toString("hex"); - if (payments.find(p => p.hash === hash)) queue[hash] = 1; - }); - } catch (e) { - console.log(e); - } -}); - -setInterval(async () => { - try { - const arr = Object.keys(queue); - for (let i = 0; i < arr.length; i++) { - const hash = arr[i]; - - let account, address, invoice, user, total; - await db.transaction(async transaction => { - let payments = await db.Payment.findAll({ - where: { hash, confirmed: 0, received: 1 }, - include: [ - { - model: db.Account, - as: "account" - }, - { - model: db.Invoice, - as: "invoice" - }, - { - model: db.User, - as: "user" - } - ], - lock: transaction.LOCK.UPDATE, - transaction - }); - - for (let i = 0; i < payments.length; i++) { - let p = payments[i]; - if (p && p.address) address = p.address; - if (p && p.user) user = p.user; - - if (p && p.account) { - ({ account, invoice } = p); - total = p.amount + p.tip; - - p.confirmed = 1; - await account.save({ transaction }); - - await invoice.increment({ received: total }, { transaction }); - await invoice.decrement( - { pending: Math.min(invoice.pending, total) }, - { transaction } - ); - - await account.increment({ balance: total }, { transaction }); - await account.decrement( - { pending: Math.min(account.pending, total) }, - { transaction } - ); - // get the # of fee credits you would need to pay off this amount of bitcoin - await account.increment( - { btc_credits: computeConversionFee(total) }, - { transaction } - ); - await account.reload({ transaction }); - await invoice.reload({ transaction }); - await p.save({ transaction }); - - p = p.get({ plain: true }); - p.account = account.get({ plain: true }); - p.invoice = invoice.get({ plain: true }); - - emit(user.username, "account", account); - emit(user.username, "payment", p); - l("bitcoin confirmed", user.username, p.amount, p.tip); - notify(user, `${total} SAT payment confirmed`); - callWebhook(p.invoice, p); - - let c = store.convert[address]; - if (address && c) { - l( - "bitcoin detected to conversion address", - address, - c.address, - user.username - ); - user.account = account; - - sendLiquid({ - address: c.address, - amount: total - 100, - user, - limit: total - }); - } - } else { - warn("couldn't find bitcoin payment", hash); - } - } - - delete queue[hash]; - }); - } - } catch (e) { - err("problem processing queued bitcoin transaction", e.message, e.stack); - } -}, 1000); diff --git a/routes/bitcoin/send.js b/routes/bitcoin/send.js deleted file mode 100644 index 3e030413..00000000 --- a/routes/bitcoin/send.js +++ /dev/null @@ -1,192 +0,0 @@ -import bc from "$lib/bitcoin"; -import { emit } from "$lib/sockets"; -import db from "$db"; -import { toSats } from "$lib/utils"; -import store from "$lib/store"; -import config from "$config"; -import { l, warn, err } from "$lib/logging"; -import { requirePin } from "$lib/utils"; - -const btc = config.liquid.btcasset; -import { - computeConversionFee, - conversionFeeReceiver, -} from "./conversionFee.js"; - -export default async (req, res) => { - try { - await requirePin(req); - - let { user } = req; - let { address, memo, tx } = req.body; - let { hex } = tx; - - let fee = toSats(tx.fee); - if (fee < 0) throw new Error("fee cannot be negative"); - - const isChange = async ({ address }) => - (await bc.getAddressInfo(address)).ismine && - !Object.keys(store.addresses).includes(address); - - tx = await bc.decodeRawTransaction(hex); - - let total = 0; - let change = 0; - - for (let i = 0; i < tx.vout.length; i++) { - let o = tx.vout[i]; - total += toSats(o.value); - - if (await isChange(o.scriptPubKey)) { - change += toSats(o.value); - } - } - - total = total - change + fee; - let amount = total - fee; - - // get conversion fee - // 'total' refers to the total before the conversion fee - // (i.e. the total bitcoin that leaves this server) - let conversionFee = computeConversionFee(amount); - - // withdraw bitcoin - await db.transaction(async (transaction) => { - let { account } = user; - if (account.asset !== btc) { - account = await db.Account.findOne({ - where: { - user_id: user.id, - asset: btc, - pubkey: null, - }, - lock: transaction.LOCK.UPDATE, - transaction, - }); - } - - // use user's credits to reduce fee, if available - let conversionFeeDeduction = Math.min(account.btc_credits, conversionFee); - if (conversionFeeDeduction) { - await account.decrement( - { btc_credits: conversionFeeDeduction }, - { transaction } - ); - await account.reload({ transaction }); - conversionFee -= conversionFeeDeduction; - } - - l( - "attempting to send bitcoin", - user.username, - account.id, - address, - total, - conversionFee, - conversionFeeDeduction - ); - - if (total > account.balance) { - err("amount exceeds balance", amount, fee, account.balance); - throw new Error("low"); - } else if (total + conversionFee > account.balance) { - err( - "total (after conversion fee) exceeds balance", - amount, - fee, - account.balance - ); - throw new Error("low"); - } - - await account.decrement( - { balance: total + conversionFee }, - { transaction } - ); - await account.reload({ transaction }); - - let receiverAccount = await db.Account.findOne({ - where: { - "$user.username$": conversionFeeReceiver, - }, - include: [ - { - model: db.User, - as: "user", - }, - ], - lock: transaction.LOCK.UPDATE, - transaction, - }); - - let fee_payment; - let fee_payment_id = null; - if (conversionFee) { - await receiverAccount.increment( - { balance: conversionFee }, - { transaction } - ); - await receiverAccount.reload({ transaction }); - fee_payment = await db.Payment.create( - { - amount: conversionFee, - fee: 0, - memo: "Bitcoin conversion fee", - account_id: receiverAccount.id, - user_id: receiverAccount.user_id, - rate: store.rates[receiverAccount.user.currency], - currency: receiverAccount.user.currency, - confirmed: true, - received: true, - network: "COINOS", - }, - { transaction } - ); - fee_payment_id = fee_payment.id; - } - - // record the external bitcoin transaction - let params = { - amount: -amount, - fee, - memo, - account_id: account.id, - user_id: user.id, - rate: store.rates[user.currency], - currency: user.currency, - address, - confirmed: true, - received: false, - network: "bitcoin", - fee_payment_id: fee_payment_id, - }; - - if (config.bitcoin.walletpass) - await bc.walletPassphrase(config.bitcoin.walletpass, 300); - - hex = (await bc.signRawTransactionWithWallet(hex)).hex; - params.hash = await bc.sendRawTransaction(hex); - - let payment = await db.Payment.create(params, { transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - payment.fee_payment = fee_payment && fee_payment.get({ plain: true }); - - emit(user.username, "payment", payment); - res.send(payment); - - store.payments.push(params.hash); - l("sent bitcoin", user.username, total); - }); - } catch (e) { - if (e.message.includes("Insufficient")) - e.message = - "Our server has insufficient funds, try again later"; - - if (e.message === "low") e.message = "Insufficient funds in account" - - err("error sending bitcoin", e.message); - return res.code(500).send(e.message); - } -}; diff --git a/routes/bitcoin/sweep.js b/routes/bitcoin/sweep.js deleted file mode 100644 index 49fb1f04..00000000 --- a/routes/bitcoin/sweep.js +++ /dev/null @@ -1,87 +0,0 @@ -import config from "$config"; -import { prod } from "$lib/utils"; -import axios from "axios"; -import bitcoin from "bitcoinjs-lib"; -import coinselect from "coinselect"; -import split from "coinselect/split"; - -const api = prod ? "https://blockstream.info/api" : config.bitcoin.electrs; -const SATS = 100000000; -const network = prod - ? bitcoin.networks["bitcoin"] - : bitcoin.networks["regtest"]; - -export default async (req, res) => { - let { address: from, amount, feeRate, target } = req.body; - let utxos; - if (!feeRate) feeRate = (await bc.estimateSmartFee(6)).feerate * SATS; - feeRate = Math.round(feeRate / 1000); - l("sweeping", req.user.username, from, amount, feeRate); - - try { - let { - data: { - chain_stats: { funded_txo_sum: funded, spent_txo_sum: spent } - } - } = await axios.get(`${api}/address/${from}`); - let balance = funded - spent; - console.log("boom"); - - try { - console.log("fetching utxo"); - ({ data: utxos } = await axios.get(`${api}/address/${from}/utxo`)); - console.log("got utxos", utxos); - } catch (e) { - err("problem fetching utxos", e.message); - } - - let targets = [ - { - address: target, - value: amount - } - ]; - - let { inputs, outputs, fee } = coinselect(utxos, targets, feeRate); - - if (balance === amount) { - delete targets[0].value; - ({ inputs, outputs, fee } = split(utxos, targets, feeRate)); - l("split", inputs, outputs, fee); - } - - if (!inputs || !outputs) - return res - .code(500) - .send("Unable to construct sweep transaction, try a lower fee rate?"); - - let psbt = new bitcoin.Psbt({ network }); - let total = 0; - - outputs.map(({ address, value }) => { - total += value; - psbt = psbt.addOutput({ - address: address || from, - value - }); - }); - - for (let i = 0; i < inputs.length; i++) { - let { txid, vout } = inputs[i]; - let nonWitnessUtxo = Buffer.from(await bc.getRawTransaction(txid), "hex"); - psbt = psbt.addInput({ - hash: txid, - index: vout, - nonWitnessUtxo - }); - } - - feeRate = Math.round(feeRate * 1000); - psbt = psbt.toBase64(); - - if (balance !== amount) total = 0; - res.send({ feeRate, psbt, total }); - } catch (e) { - err("problem getting address stats", from, e.message); - } -}; diff --git a/routes/events.js b/routes/events.js index e52b8a7c..c105dc40 100644 --- a/routes/events.js +++ b/routes/events.js @@ -5,72 +5,3 @@ import { coinos, pool, q } from "$lib/nostr"; import store from "$lib/store"; import { nada, wait } from "$lib/utils"; -app.get("/event/:id", async (req, res) => { - try { - let event = JSON.parse(await redis.get(`ev:${req.params.id}`)); - let { pubkey } = event; - - event.user = JSON.parse(await redis.get(`user:${pubkey}`)) || { - username: pubkey.substr(0, 6), - pubkey, - anon: true, - follows: [], - followers: [] - }; - - res.send(event); - } catch (e) { - res.code(500).send("Problem fetching event"); - } -}); - -app.get("/nostr/:pubkey", async (req, res) => { - try { - let { pubkey } = req.params; - - let user = JSON.parse(await redis.get(`user:${pubkey}`)); - - if (!user) { - user = await db.User.findOne({ - where: { pubkey } - }); - } - - if (!user) - user = { - username: pubkey.substr(0, 6), - pubkey, - anon: true, - follows: [], - followers: [] - }; - - q( - `${pubkey}:notes`, - { - kinds: [1], - authors: [pubkey] - }, - { since: 0 } - ).catch(nada); - - await redis.set(`user:${pubkey}`, JSON.stringify(user)); - - let ids = await redis.sMembers(pubkey); - - let events = ids.length - ? (await redis.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) - : []; - - res.send(events.map(e => ({ ...e, user }))); - } catch (e) { - console.log(e); - res.code(500).send("problem fetching user events"); - } -}); - -app.post("/nostr/send", async (req, res) => { - let { event } = req.body; - pool.send(["EVENT", event]); - res.send(event); -}); diff --git a/routes/funding.js b/routes/funding.js deleted file mode 100644 index e65a704e..00000000 --- a/routes/funding.js +++ /dev/null @@ -1,96 +0,0 @@ -import { emit } from "$lib/sockets"; -import app from "$app"; -import config from "$config"; -import mailgunFactory from "mailgun-js"; -const mailgun = mailgunFactory(config.mailgun); - -app.post("/id", auth, upload.single("id"), async (req, res) => { - const { user } = req; - if (user.verified === "proof") user.verified = "pending"; - else user.verified = "id"; - await user.save(); - emit(user.username, "user", user); - - let data = { - subject: "KYC Documents", - text: `ID uploaded for ${user.username}`, - from: "server@coinos.io", - to: "kyc@coinos.io" - }; - - mailgun.messages().send(data); - l("id uploaded", user.username); - - res.send({}); -}); - -app.post("/proof", auth, upload.single("proof"), async (req, res) => { - const { user } = req; - if (user.verified === "id") user.verified = "pending"; - else user.verified = "proof"; - await user.save(); - emit(user.username, "user", user); - l("id proof uploaded", user.username); - - res.send({}); -}); - -app.post("/funding", auth, async (req, res) => { - try { - let { id, amount, code } = req.body; - - if (!code) { - code = ""; - let d = "ABCDEFGHIJKLMNPQRSTUVWXYZ23456789"; - let l = d.length; - for (let i = 0; i < 8; i++) { - code += d.charAt(Math.floor(Math.random() * l)); - } - } - - let params = { - user_id: req.user.id, - amount, - code - }; - - let deposit; - if (id) { - if (!parseFloat(amount) || parseFloat(amount) < 0) - throw new Error("Invalid amount"); - deposit = await db.Deposit.findOne({ where: { id } }); - deposit.update(params); - await deposit.save(); - } else { - deposit = await db.Deposit.create(params); - } - - res.send(deposit); - } catch (e) { - err("funding error", e.message); - res.code(500).send("Funding request failed"); - } -}); - -app.post("/withdrawal", auth, async (req, res) => { - try { - const { account, amount, email, institution, transit, notes } = req.body; - if (!parseFloat(amount) || parseFloat(amount) < 0) - throw new Error("Invalid amount"); - - await db.Withdrawal.create({ - user_id: req.user.id, - account, - amount, - email, - institution, - transit, - notes - }); - - res.send({}); - } catch (e) { - err("withdrawaerr", e.message); - res.code(500).send("Withdrawal request failed"); - } -}); diff --git a/routes/invoices.js b/routes/invoices.js index 338c66e7..f3c95a21 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -1,13 +1,8 @@ import app from "$app"; -import db from "$db"; import store from "$lib/store"; import { optionalAuth } from "$lib/passport"; -import { Op } from "@sequelize/core"; -import axios from "axios"; import { err, l } from "$lib/logging"; -import bc from "$lib/bitcoin"; -import lq from "$lib/liquid"; -import { SATS, bip21, deriveAddress, derivePayRequest } from "$lib/utils"; +import { SATS, derivePayRequest } from "$lib/utils"; import { emit } from "$lib/sockets"; app.get("/invoice", async (req, res, next) => { diff --git a/routes/lightning/query.js b/routes/lightning/query.js index 1c217a08..c5456a6f 100644 --- a/routes/lightning/query.js +++ b/routes/lightning/query.js @@ -1,4 +1,3 @@ -import db from "$db"; import { emit } from "$lib/sockets"; export default async (req, res) => { diff --git a/routes/lightning/receive.js b/routes/lightning/receive.js index ef973acc..a778271b 100644 --- a/routes/lightning/receive.js +++ b/routes/lightning/receive.js @@ -1,11 +1,9 @@ -import db from "$db"; import store from "$lib/store"; import { emit } from "$lib/sockets"; import config from "$config"; import { notify } from "$lib/notifications"; import { callWebhook } from "$lib/webhooks"; import { computeConversionFee } from "./conversionFee"; -import { sendLiquid } from "$routes/liquid/send"; import ln from "$lib/ln"; import { l, err, warn } from "$lib/logging"; @@ -101,29 +99,6 @@ const handlePayment = async msg => { payment.tip ); }); - - let c = store.convert[msg.request]; - if (msg.request && c) { - l( - "lightning detected for conversion request", - msg.request, - c.address, - user.username - ); - - user.account = account; - - try { - sendLiquid({ - address: c.address, - amount: total - 100, - user, - limit: total - }); - } catch (e) { - err("problem sending liquid payment", e.message, e.stack); - } - } } catch (e) { console.log(e); err("problem receiving lightning payment", e.message); diff --git a/routes/liquid/broadcast.js b/routes/liquid/broadcast.js deleted file mode 100644 index 820c0be1..00000000 --- a/routes/liquid/broadcast.js +++ /dev/null @@ -1,58 +0,0 @@ -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import { requirePin } from "$lib/utils"; - -export default async (req, res) => { - try { - await requirePin(req); - - const { user } = req; - let hash = await lq.sendRawTransaction(req.body.tx); - - if (user) { - await db.transaction(async transaction => { - let account = await db.Account.findOne({ - where: { - id: user.account_id - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - let payment = await db.Payment.create( - { - ...req.body.payment, - hash, - amount: -req.body.payment.amount, - tip: 0, - account_id: account.id, - user_id: user.id, - rate: store.rates[user.currency], - currency: user.currency, - confirmed: true, - received: false, - network: "liquid" - }, - { transaction } - ); - - await account.increment( - { balance: payment.amount - payment.fee }, - { transaction } - ); - await account.reload({ transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - emit(user.username, "payment", payment); - emit(user.username, "account", payment.account); - res.send(payment); - }); - } else { - res.send(hash); - } - } catch (e) { - err("problem broadcasting liquid transaction", e); - res.code(500).send(e.message); - } -}; diff --git a/routes/liquid/conversionFee.js b/routes/liquid/conversionFee.js deleted file mode 100644 index a25c2e7d..00000000 --- a/routes/liquid/conversionFee.js +++ /dev/null @@ -1,12 +0,0 @@ -// Code for the conversion fee -// This is in a separate file so that the same function can be used by ./send and ./receive - -const conversionFeeMultiplier = 0.001; -export const conversionFeeReceiver = "coinosfees"; // account that receives the fees - -/** - * Returns the fee you have to pay for withdrawing 'amount' L-BTC. - */ -export function computeConversionFee(amount) { - return Math.floor(amount * conversionFeeMultiplier); -} diff --git a/routes/liquid/fee.js b/routes/liquid/fee.js deleted file mode 100644 index ae6e4393..00000000 --- a/routes/liquid/fee.js +++ /dev/null @@ -1,89 +0,0 @@ -import config from "$config"; -import db from "$db"; -import { emit } from "$lib/sockets"; -import buildTx from "$lib/buildliquidtx"; -import { err, l } from "$lib/logging"; -import { SATS } from "$lib/utils"; -import lq from "$lib/liquid"; - -export const liquidTx = async ({ address, asset, amount, feeRate, replaceable, user }) => { - let tx, fee; - - if (user.account.pubkey) { - let psbt = await buildTx({ - address, - asset, - amount, - feeRate, - replaceable, - user - }); - return psbt; - } - - let invoice = await db.Invoice.findOne({ - where: { address }, - include: { - attributes: ["username"], - model: db.User, - as: "user" - } - }); - - if (invoice) { - let { ismine } = await lq.getAddressInfo(address); - if (ismine) { - emit(user.username, "to", invoice.user); - return; - } - } - - if (config.liquid.walletpass) - await lq.walletPassphrase(config.liquid.walletpass, 300); - - amount = parseInt(amount); - - let params = { - subtractFeeFromOutputs: amount === user.balance ? [0] : [] - }; - - let value = (amount / SATS).toFixed(8); - if (feeRate) params.feeRate = (feeRate / SATS).toFixed(8); - - let info = await lq.getAddressInfo(address); - - tx = await lq.createRawTransaction( - [], - [ - { - [address]: (amount / SATS).toFixed(8), - asset - } - ], - 0, - replaceable - ); - - l("funding tx for fee estimate", tx); - - tx = await lq.fundRawTransaction(tx, params); - - let blinded = await lq.blindRawTransaction(tx.hex); - let signed = await lq.signRawTransactionWithWallet(blinded); - - let decoded = await lq.decodeRawTransaction(signed.hex); - feeRate = Math.round((tx.fee * SATS * 1000) / decoded.vsize); - l("estimated", asset, feeRate); - - return { feeRate, tx }; -}; - -export default async (req, res) => { - try { - let tx = await liquidTx({ ...req.body, user: req.user }); - res.send(tx); - } catch (e) { - err("error estimating liquid fee", e.message, e.stack); - return res.code(500).send(e.message); - } -}; diff --git a/routes/liquid/generate.js b/routes/liquid/generate.js deleted file mode 100644 index 423d8b6a..00000000 --- a/routes/liquid/generate.js +++ /dev/null @@ -1,5 +0,0 @@ -export default async (req, res) => { - let address = await lq.getNewAddress(); - await lq.generateToAddress(1, address); - res.send({}); -}; diff --git a/routes/liquid/index.js b/routes/liquid/index.js deleted file mode 100644 index c4c50021..00000000 --- a/routes/liquid/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import broadcast from "./broadcast"; -import generate from "./generate"; -import fee from "./fee"; -import send from "./send"; - -export default { - broadcast, - generate, - fee, - send, -}; diff --git a/routes/liquid/receive.js b/routes/liquid/receive.js deleted file mode 100644 index 718feba5..00000000 --- a/routes/liquid/receive.js +++ /dev/null @@ -1,406 +0,0 @@ -import db from "$db"; -import config from "$config"; -import { getBlockHeight, getUser, getUserById, toSats, SATS } from "$lib/utils"; -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import { sendLiquid } from "$routes/liquid/send"; -import { notify } from "$lib/notifications"; -import { callWebhook } from "$lib/webhooks"; -import reverse from "buffer-reverse"; -import zmq from "zeromq/v5-compat"; -import { Op } from "@sequelize/core"; -import { fromBase58 } from "bip32"; -import bitcoin from "bitcoinjs-lib"; -import liquidJs from "liquidjs-lib"; -import { l, err, warn } from "$lib/logging"; -import lq from "$lib/liquid"; - -const { Block, networks, Transaction } = liquidJs; - -const network = - networks[ - config.liquid.network === "mainnet" ? "liquid" : config.liquid.network - ]; - -import { computeConversionFee } from "./conversionFee"; - -const getAccount = async (params, transaction) => { - let account = await db.Account.findOne({ - where: params, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (account) { - l("found account", params, account.asset, account.id); - return account; - } - - let { asset, pubkey } = params; - let name = asset.substr(0, 6); - let domain = ""; - let ticker = asset.substr(0, 3).toUpperCase(); - let precision = 8; - - if (pubkey) { - const nc = await db.Account.findOne({ - where: { pubkey }, - order: [["id", "ASC"]], - limit: 1, - lock: transaction.LOCK.UPDATE, - transaction - }); - - params.index = 0; - params.path = nc.path; - params.pubkey = nc.pubkey; - params.privkey = nc.privkey; - params.seed = nc.seed; - } - - if (store.assets[asset]) { - ({ domain, ticker, precision, name } = store.assets[asset]); - } else { - const existing = await db.Account.findOne({ - where: { - asset - }, - order: [["id", "ASC"]], - limit: 1, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (existing) { - l("existing", existing.id); - ({ domain, ticker, precision, name } = existing); - } - } - - params = { ...params, ...{ domain, ticker, precision, name } }; - params.balance = 0; - params.pending = 0; - params.network = "liquid"; - return db.Account.create(params, { transaction }); -}; - -const zmqRawBlock = zmq.socket("sub"); -zmqRawBlock.connect(config.liquid.zmqrawblock); -zmqRawBlock.subscribe("rawblock"); - -const zmqRawTx = zmq.socket("sub"); -zmqRawTx.connect(config.liquid.zmqrawtx); -zmqRawTx.subscribe("rawtx"); - -const queue = {}; -const seen = []; - -zmqRawTx.on("message", async (topic, message, sequence) => { - try { - const hex = message.toString("hex"); - - let unblinded, tx, blinded, txid; - try { - txid = Transaction.fromHex(hex).getId(); - if (seen.includes(txid)) return; - seen.push(txid); - if (seen.length > 5000) seen.shift(); - - unblinded = await lq.unblindRawTransaction(hex); - tx = await lq.decodeRawTransaction(unblinded.hex); - blinded = await lq.decodeRawTransaction(hex); - } catch (e) { - return err("problem decoding liquid tx", e.message); - } - - for (let o of tx.vout) { - try { - if (!(o.scriptPubKey && o.scriptPubKey.address)) return; - - const { asset } = o; - const value = toSats(o.value); - const { address } = o.scriptPubKey; - - if ( - Object.keys(store.addresses).includes(address) && - !store.change.includes(address) - ) { - await db.transaction(async transaction => { - let user = await getUser(store.addresses[address], transaction); - - let invoice = await db.Invoice.findOne({ - where: { - [Op.or]: { - unconfidential: address, - address - }, - user_id: user.id, - network: "liquid" - }, - order: [["id", "DESC"]], - transaction - }); - - if (!invoice) return; - - let confirmed = 0; - - let account = await db.Account.findOne({ - where: { - id: invoice.account_id, - asset - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (!account) { - let pubkey = account ? account.pubkey : null; - - account = await getAccount( - { - user_id: user.id, - asset, - pubkey - }, - transaction - ); - } - - await account.increment({ pending: value }, { transaction }); - await account.reload({ transaction }); - - if (config.liquid.walletpass) - await lq.walletPassphrase(config.liquid.walletpass, 300); - - await user.save({ transaction }); - - const currency = invoice ? invoice.currency : user.currency; - const rate = invoice ? invoice.rate : store.rates[user.currency]; - const tip = invoice ? invoice.tip : 0; - const memo = invoice ? invoice.memo : ""; - - let payment = await db.Payment.create( - { - account_id: account.id, - user_id: user.id, - hash: blinded.txid, - amount: value - tip, - currency, - memo, - rate, - received: true, - tip, - confirmed, - address, - network: "liquid", - invoice_id: invoice.id - }, - { transaction } - ); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - payment.invoice = invoice.get({ plain: true }); - - emit(user.username, "payment", payment); - emit(user.username, "account", payment.account); - l("liquid detected", address, user.username, asset, value); - notify(user, `${value} SAT payment detected`); - callWebhook(invoice, payment); - }); - } - } catch (e) { - console.log(e); - err("Problem processing transaction", e.message, e.stack); - } - } - } catch (e) { - console.log(e); - } -}); - -zmqRawBlock.on("message", async (topic, message, sequence) => { - try { - const payments = await db.Payment.findAll({ - where: { confirmed: 0 } - }); - - let hash, json; - - hash = await lq.getBlockHash(getBlockHeight(message)); - json = await lq.getBlock(hash, 2); - - for (let tx of json.tx) { - if (store.issuances[tx.txid]) { - await db.transaction(async transaction => { - const { - user_id, - asset, - asset_amount, - asset_payment_id, - token, - token_amount, - token_payment_id - } = store.issuances[tx.txid]; - - const user = await getUserById(user_id); - - let account = await db.Account.findOne({ - where: { user_id, asset }, - lock: transaction.LOCK.UPDATE, - transaction - }); - account.balance = Math.round(asset_amount * SATS); - account.pending = 0; - await account.save({ transaction }); - - let payment = await db.Payment.findOne({ - where: { id: asset_payment_id }, - include: { - model: db.Account, - as: "account" - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - payment.confirmed = true; - await payment.save({ transaction }); - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - - emit(user.username, "account", account); - emit(user.username, "payment", payment); - - if (token) { - account = await db.Account.findOne({ - where: { user_id, asset: token }, - lock: transaction.LOCK.UPDATE, - transaction - }); - account.balance = token_amount * SATS; - account.pending = 0; - await account.save({ transaction }); - - payment = await db.Payment.findOne({ - where: { id: token_payment_id }, - include: { - model: db.Account, - as: "account" - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - payment.confirmed = true; - await payment.save({ transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - - emit(user.username, "account", account); - emit(user.username, "payment", payment); - } - }); - } else if (payments.find(p => p.hash === tx.txid)) queue[tx.txid] = 1; - } - } catch (e) { - return console.log(e); - } -}); - -setInterval(async () => { - try { - const arr = Object.keys(queue); - - for (let i = 0; i < arr.length; i++) { - const hash = arr[i]; - - let account, invoice, address, user, total, p; - await db.transaction(async transaction => { - p = await db.Payment.findOne({ - where: { hash, confirmed: 0, received: 1 }, - include: [ - { - model: db.Account, - as: "account" - }, - { - model: db.Invoice, - as: "invoice" - }, - { - model: db.User, - as: "user" - } - ], - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (p && p.address) address = p.address; - if (p && p.user) user = p.user; - - if (p && p.account) { - ({ account, invoice } = p); - total = p.amount + p.tip; - p.confirmed = 1; - await account.save({ transaction }); - - await invoice.increment({ received: total }, { transaction }); - await account.increment({ balance: total }, { transaction }); - await account.decrement( - { pending: Math.min(account.pending, total) }, - { transaction } - ); - await account.increment( - { liquid_credits: computeConversionFee(total) }, - { transaction } - ); - await account.reload({ transaction }); - await invoice.reload({ transaction }); - - await p.save({ transaction }); - - p = p.get({ plain: true }); - account = account.get({ plain: true }); - invoice = invoice.get({ plain: true }); - - emit(user.username, "account", account); - emit(user.username, "payment", p); - - l("liquid confirmed", user.username, account.asset, p.amount, p.tip); - - notify(user, `${total} SAT payment confirmed`); - callWebhook(p.invoice, p); - } else { - warn("couldn't find liquid payment", hash); - } - - delete queue[hash]; - }); - - let c = store.convert[address]; - if (address && c && p.account.asset === network.assetHash) { - l( - "liquid detected for conversion request", - address, - c.address, - user.username - ); - - user.account = p.account; - - sendLiquid({ - address: c.address, - amount: total - 100, - user, - limit: total - }); - } - } - } catch (e) { - err("problem processing queued liquid transaction", e.message, e.stack); - } -}, 1000); diff --git a/routes/liquid/send.js b/routes/liquid/send.js deleted file mode 100644 index 83b5d6ca..00000000 --- a/routes/liquid/send.js +++ /dev/null @@ -1,276 +0,0 @@ -import db from "$db"; -import lq from "$lib/liquid"; -import { toSats } from "$lib/utils"; -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import config from "$config"; -import { l, warn, err } from "$lib/logging"; -import { liquidTx } from "$routes/liquid/fee"; -import { Transaction } from 'liquidjs-lib'; -import { requirePin } from "$lib/utils"; - -import { - computeConversionFee, - conversionFeeReceiver -} from "./conversionFee.js"; - -const btc = config.liquid.btcasset; -const lcad = config.liquid.cadasset; - -export const sendLiquid = async ({ asset, amount, user, address, memo, tx, limit }) => { - try { - l("sending liquid", amount, address); - if (!tx) { - ({ tx } = await liquidTx({ - address, - asset, - amount, - feeRate: 200, - replaceable: false, - user - })); - } - - const isChange = async address => - (await lq.getAddressInfo(address)).ismine && - !Object.keys(store.addresses).includes(address); - - let totals = {}; - let change = {}; - let fee = 0; - - let { vout } = await lq.decodeRawTransaction(tx.hex); - - for (let i = 0; i < vout.length; i++) { - let { - asset, - value, - scriptPubKey - } = vout[i]; - - if (scriptPubKey.type === "fee") fee = toSats(value); - else { - if (!totals[asset]) totals[asset] = change[asset] = 0; - totals[asset] += toSats(value); - - if (scriptPubKey.address) { - if (await isChange(scriptPubKey.address)) { - change[asset] += toSats(value); - } - } - } - } - - const assets = Object.keys(totals); - const payments = []; - - let main, signed; - await db.transaction(async transaction => { - let fee_payment; - let fee_payment_id = null; - - for (let i = 0; i < assets.length; i++) { - let asset = assets[i]; - let amount = totals[asset]; - if (change[asset]) amount -= change[asset]; - let total = amount; - - if (asset === btc) { - let covered = 0; - let nonbtc = assets.filter(a => a !== btc); - if (nonbtc.length === 1) { - let faucet = await db.Account.findOne({ - where: { - asset: nonbtc[0], - user_id: null - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (faucet) { - covered = faucet.balance; - if (covered > fee) covered = fee; - await faucet.decrement({ balance: covered }, { transaction }); - await faucet.reload({ transaction }); - await faucet.save({ transaction }); - } - } - - total += fee - covered; - } - - var conversionFee = computeConversionFee(amount); - - if (limit && total > limit + fee) - throw new Error("Tx amount exceeds authorized amount"); - - if (asset !== btc || total) { - l("creating liquid payment", user.username, asset, total, fee); - - let account = await db.Account.findOne({ - where: { - user_id: user.id, - asset, - pubkey: null - }, - lock: transaction.LOCK.UPDATE, - order: [["balance", "DESC"]], - transaction - }); - - // use user's credits to reduce fee, if available - let conversionFeeDeduction = Math.min( - account.liquid_credits, - conversionFee - ); - if (conversionFeeDeduction) { - await account.decrement( - { liquid_credits: conversionFeeDeduction }, - { transaction } - ); - await account.reload({ transaction }); - conversionFee -= conversionFeeDeduction; - } - - if (asset !== btc) conversionFee = 0; - - if (total > account.balance) { - warn("amount exceeds balance", { - total, - fee, - balance: account.balance - }); - throw new Error( - `Insufficient funds, need ${total} ${ - account.ticker === "BTC" ? "SAT" : account.ticker - }, have ${account.balance}` - ); - } else if (total + conversionFee > account.balance) { - warn("amount plus conversion fee exceeds balance", { - total, - fee, - conversionFee, - balance: account.balance - }); - throw new Error( - `Insufficient funds, need ${total + conversionFee} ${ - account.ticker === "BTC" ? "SAT" : account.ticker - }, have ${account.balance}` - ); - } - - await account.decrement( - { balance: total + conversionFee }, - { transaction } - ); - await account.reload({ transaction }); - - let receiverAccount = await db.Account.findOne({ - where: { - "$user.username$": conversionFeeReceiver - }, - include: [ - { - model: db.User, - as: "user" - } - ], - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (conversionFee) { - await receiverAccount.increment( - { balance: conversionFee }, - { transaction } - ); - await receiverAccount.reload({ transaction }); - fee_payment = { - amount: conversionFee, - fee: 0, - memo: "Liquid conversion fee", - account_id: receiverAccount.id, - user_id: receiverAccount.user_id, - rate: store.rates[receiverAccount.user.currency], - currency: receiverAccount.user.currency, - confirmed: true, - received: true, - network: "COINOS" - }; - fee_payment.account = receiverAccount; - ({ id: fee_payment_id } = await db.Payment.create(fee_payment, { - transaction - })); - } - - let payment = { - amount: -amount, - account_id: account.id, - fee, - memo, - user_id: user.id, - rate: store.rates[user.currency], - currency: user.currency, - address, - confirmed: true, - received: false, - network: "liquid", - fee_payment_id - }; - - payment.account = account; - payments.push(payment); - } - } - - signed = await lq.signRawTransactionWithWallet( - await lq.blindRawTransaction(tx.hex) - ); - let txid = Transaction.fromHex(signed.hex).getId(); - - for (let i = 0; i < payments.length; i++) { - let p = payments[i]; - if (p) { - let { account } = p; - p.hash = txid; - p = await db.Payment.create(p, { transaction }); - if (p.user_id === user.id && (account.ticker !== "BTC" || !main)) { - main = p.get({ plain: true }); - main.fee_payment = fee_payment; - main.account = account.get({ plain: true }); - } - } - } - - emit(user.username, "account", main.account); - emit(user.username, "payment", main); - }); - - if (config.liquid.walletpass) - await lq.walletPassphrase(config.liquid.walletpass, 300); - - let txid = await lq.sendRawTransaction(signed.hex); - l("sent liquid tx", txid, address); - - return main; - } catch (e) { - err("problem sending liquid", e.message); - return e; - } -}; - -export default async (req, res) => { - let { user } = req; - - try { - await requirePin(req); - let result = await sendLiquid({ ...req.body, user }) - if (result instanceof Error) throw result; - res.send(result); - } catch (e) { - console.log(e) - err("problem sending liquid", user.username, e.message); - return res.code(500).send(e); - } -}; diff --git a/routes/lnurl.js b/routes/lnurl.js index 1ad7b814..e9a0b167 100644 --- a/routes/lnurl.js +++ b/routes/lnurl.js @@ -1,6 +1,5 @@ import app from "$app"; import config from "$config"; -import db from "$db"; import store from "$lib/store"; import { optionalAuth, auth } from "$lib/passport"; import { l, err, warn } from "$lib/logging"; @@ -9,11 +8,9 @@ import ln from "$lib/ln"; import persist from "$lib/persist"; import { emit } from "$lib/sockets"; -import axios from "axios"; import lnurl from "lnurl"; import jwt from "jsonwebtoken"; import qs from "query-string"; -import bolt11 from "bolt11"; import { computeConversionFee, diff --git a/routes/locations.js b/routes/locations.js index 63bfe1a1..1cebc883 100644 --- a/routes/locations.js +++ b/routes/locations.js @@ -1,19 +1,21 @@ import app from "$app"; import redis from "$lib/redis"; -import axios from "axios"; +import got from "got"; -let locations; +let locations = []; app.get("/locations", async (req, res) => { res.send({ locations }); }); let getLocations = async () => { - let { data } = await axios.get( + try { + let r = await got( "https://api.btcmap.org/v2/elements?updated_since=2022-09-19" - ); - locations = data.filter( + ).json(); + locations = r.filter( l => l["osm_json"].tags && l["osm_json"].tags["payment:coinos"] === "yes" ); + } catch(e) {} setTimeout(getLocations, 60000); }; diff --git a/routes/payments.js b/routes/payments.js index 279588d1..7b094ccc 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -1,279 +1,171 @@ import app from "$app"; -import db from "$db"; import config from "$config"; import store from "$lib/store"; -import getAccount from "$lib/account"; import { auth, adminAuth, optionalAuth } from "$lib/passport"; import fs from "fs"; import { join } from "path"; -import { Op } from "@sequelize/core"; import send from "./send"; import { err, l, warn } from "$lib/logging"; import { fail } from "$lib/utils"; import { emit } from "$lib/sockets"; -import bc from "$lib/bitcoin"; -import lq from "$lib/liquid"; - -import btcRoutes from "./bitcoin/index"; import lnRoutes from "./lightning/index"; -import lqRoutes from "./liquid/index"; - -import "./bitcoin/receive"; import "./lightning/receive"; -import "./liquid/receive"; app.post("/send", auth, send); -app.post("/sendToTokenHolders", auth, async (req, res, next) => { - let { asset, amount } = req.body; - - let accounts = await db.Account.findAll({ - where: { - asset, - "$user.username$": { [Op.ne]: "gh" } - }, - - include: [{ model: db.User, as: "user" }] - }); - - let totalTokens = accounts.reduce((a, b) => a + b.balance, 0); - - let totalSats = Math.floor(amount / totalTokens); - - if (totalSats < 1) throw new Error("amount is too low to distribute"); - - for (let i = 0; i < accounts.length; i++) { - let account = accounts[i]; - console.log(account.user.username, account.balance); - } - accounts.map(({ balance, user: { username } }) => ({ username, balance })); - - res.send({ success: "it worked" }); -}); - -app.get("/except", adminAuth, (req, res) => { - let s = fs.createWriteStream("exceptions", { flags: "a" }); - store.unaccounted.map(tx => s.write(tx.txid + "\n")); - l("updated exceptions"); - res.send("updated exceptions"); -}); app.post("/lightning/parse", lnRoutes.parse); -app.post("/lightning/channel", lnRoutes.channel); app.post("/lightning/query", auth, lnRoutes.query); app.post("/lightning/send", auth, lnRoutes.send); -if (config.lnurl) { - let { channelRequest } = await import("$routes/lightning/channelRequest"); - app.post("/lightning/channelRequest", channelRequest); -} - -app.post("/bitcoin/broadcast", optionalAuth, btcRoutes.broadcast); -app.get("/bitcoin/generate", auth, btcRoutes.generate); -app.post("/bitcoin/sweep", auth, btcRoutes.sweep); -app.post("/bitcoin/fee", auth, btcRoutes.fee); -app.post("/bitcoin/send", auth, btcRoutes.send); - -setTimeout(async () => { - try { - const address = await bc.getNewAddress(); - const { hdkeypath } = await bc.getAddressInfo(address); - const parts = hdkeypath.split("/"); - store.bcAddressIndex = parts[parts.length - 1].replace("'", ""); - } catch (e) { - console.error(e); - } -}, 50); - -app.post("/liquid/broadcast", optionalAuth, lqRoutes.broadcast); -app.get("/liquid/generate", auth, lqRoutes.generate); -app.post("/liquid/fee", auth, lqRoutes.fee); -app.post("/liquid/send", auth, lqRoutes.send); - -setTimeout(async () => { - try { - const address = await lq.getNewAddress(); - const { hdkeypath } = await lq.getAddressInfo(address); - const parts = hdkeypath.split("/"); - store.lqAddressIndex = parts[parts.length - 1].slice(0, -1); - } catch (e) { - warn("Problem getting liquid address index", e.message); - } -}, 50); - -app.get("/payments", auth, async (req, res) => { - let { start, end, limit, offset, v2 } = req.query; - - if (limit) limit = parseInt(limit); - if (offset) offset = parseInt(offset); - - let where = { - account_id: req.user.account_id - }; - - if (start || end) where.createdAt = {}; - if (start) where.createdAt[Op.gte] = new Date(parseInt(start)); - if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); - - if (!req.user.account_id) return res.send([]); - - let total = await db.Payment.count({ where }); - - let payments = await db.Payment.findAll({ - where, - order: [["id", "DESC"]], - include: { - model: db.Account, - as: "account" - }, - include: { - attributes: ["username", "profile", "uuid"], - model: db.User, - as: "with" - }, - limit, - offset - }); - - if (v2) { - res.send({ transactions: payments, total }); - } else { - res.send(payments); - } -}); - -app.get("/voucher/:redeemcode", async (req, res) => { - try { - const { redeemcode } = req.params; - let payment = await db.Payment.findOne({ - where: { - redeemcode - }, - include: { all: true } - }); - - payment = payment.get({ plain: true }); - payment.redeemer = payment["with"]; - - if (!payment) fail("invalid code"); - - res.send(payment); - } catch (e) { - res.code(500).send(e.message); - } -}); - -let redeeming = {}; -app.post("/redeem", optionalAuth, async function(req, res) { - const { redeemcode } = req.body; - try { - await db.transaction(async transaction => { - if (redeeming[redeemcode]) fail("redemption in progress"); - redeeming[redeemcode] = true; - if (!redeemcode) fail("no code provided"); - - let { user } = req; - - const source = await db.Payment.findOne({ - where: { - redeemcode: req.body.redeemcode - }, - include: { - model: db.Account, - as: "account" - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - l("redeeming", redeemcode); - - if (!source) fail("Invalid code"); - if (source.redeemed) fail("Voucher has already been redeemed"); - let { amount } = source; - amount = -amount; - - if (!user) { - const ip = - req.headers["x-forwarded-for"] || req.connection.remoteAddress; - - user = await register( - { - username: redeemcode.substr(0, 8), - password: "" - }, - ip, - false - ); - - let payload = { username: user.username }; - let token = jwt.sign(payload, config.jwt); - res.cookie("token", token, { - expires: new Date(Date.now() + 432000000) - }); - - delete redeeming[redeemcode]; - return res.send({ user }); - } - - let account = await getAccount(source.account.asset, user, transaction); - let { hash, memo, confirmed, fee, network } = source; - - source.redeemed = true; - (source.with_id = user.id), await source.save({ transaction }); - - let payment = await db.Payment.create( - { - amount, - account_id: account.id, - user_id: user.id, - hash: "Voucher " + redeemcode, - memo, - rate: store.rates[user.currency], - currency: user.currency, - confirmed, - network, - received: true, - fee, - with_id: source.user_id - }, - { transaction } - ); - - await account.increment({ balance: amount }, { transaction }); - await account.reload({ transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - emit(user.username, "payment", payment); - emit(user.username, "account", account); - - res.send({ payment }); - }); - } catch (e) { - delete redeeming[redeemcode]; - console.log(e); - err("problem redeeming", e.message); - return res.code(500).send("There was a problem redeeming the voucher"); - } -}); - -app.post("/checkRedeemCode", auth, async function(req, res) { - const { redeemcode } = req.body; - - const payment = await db.Payment.findOne({ where: { redeemcode } }); - res.send(payment); -}); +app.get( + "/payments", + auth, + async ({ uuid, query: { start, end, limit, offset } }, res) => { + if (limit) limit = parseInt(limit); + if (offset) offset = parseInt(offset); -app.get("/payments/:hash", auth, async function(req, res) { - try { - let payment = await db.Payment.findOne({ - where: { user_id: req.user.id, hash: req.params.hash } - }); + // if (start || end) where.createdAt = {}; + // if (start) where.createdAt[Op.gte] = new Date(parseInt(start)); + // if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); - return payment.get({ plain: true }); - } catch (e) { - console.log(e); + let payments = await redis.get(`${uuid}:payments`); + res.send({ payments, total }); } -}); +); + +// app.get("/voucher/:redeemcode", async (req, res) => { +// try { +// const { redeemcode } = req.params; +// let payment = await db.Payment.findOne({ +// where: { +// redeemcode +// }, +// include: { all: true } +// }); +// +// payment = payment.get({ plain: true }); +// payment.redeemer = payment["with"]; +// +// if (!payment) fail("invalid code"); +// +// res.send(payment); +// } catch (e) { +// res.code(500).send(e.message); +// } +// }); + +// let redeeming = {}; +// app.post("/redeem", optionalAuth, async function(req, res) { +// const { redeemcode } = req.body; +// try { +// await db.transaction(async transaction => { +// if (redeeming[redeemcode]) fail("redemption in progress"); +// redeeming[redeemcode] = true; +// if (!redeemcode) fail("no code provided"); +// +// let { user } = req; +// +// const source = await db.Payment.findOne({ +// where: { +// redeemcode: req.body.redeemcode +// }, +// include: { +// model: db.Account, +// as: "account" +// }, +// lock: transaction.LOCK.UPDATE, +// transaction +// }); +// +// l("redeeming", redeemcode); +// +// if (!source) fail("Invalid code"); +// if (source.redeemed) fail("Voucher has already been redeemed"); +// let { amount } = source; +// amount = -amount; +// +// if (!user) { +// const ip = +// req.headers["x-forwarded-for"] || req.connection.remoteAddress; +// +// user = await register( +// { +// username: redeemcode.substr(0, 8), +// password: "" +// }, +// ip, +// false +// ); +// +// let payload = { username: user.username }; +// let token = jwt.sign(payload, config.jwt); +// res.cookie("token", token, { +// expires: new Date(Date.now() + 432000000) +// }); +// +// delete redeeming[redeemcode]; +// return res.send({ user }); +// } +// +// let account = await getAccount(source.account.asset, user, transaction); +// let { hash, memo, confirmed, fee, network } = source; +// +// source.redeemed = true; +// (source.with_id = user.id), await source.save({ transaction }); +// +// let payment = await db.Payment.create( +// { +// amount, +// account_id: account.id, +// user_id: user.id, +// hash: "Voucher " + redeemcode, +// memo, +// rate: store.rates[user.currency], +// currency: user.currency, +// confirmed, +// network, +// received: true, +// fee, +// with_id: source.user_id +// }, +// { transaction } +// ); +// +// await account.increment({ balance: amount }, { transaction }); +// await account.reload({ transaction }); +// +// payment = payment.get({ plain: true }); +// payment.account = account.get({ plain: true }); +// emit(user.username, "payment", payment); +// emit(user.username, "account", account); +// +// res.send({ payment }); +// }); +// } catch (e) { +// delete redeeming[redeemcode]; +// console.log(e); +// err("problem redeeming", e.message); +// return res.code(500).send("There was a problem redeeming the voucher"); +// } +// }); +// +// app.post("/checkRedeemCode", auth, async function(req, res) { +// const { redeemcode } = req.body; +// +// const payment = await db.Payment.findOne({ where: { redeemcode } }); +// res.send(payment); +// }); +// +// app.get("/payments/:hash", auth, async function(req, res) { +// try { +// let payment = await db.Payment.findOne({ +// where: { user_id: req.user.id, hash: req.params.hash } +// }); +// +// return payment.get({ plain: true }); +// } catch (e) { +// console.log(e); +// } +// }); diff --git a/routes/referrals.js b/routes/referrals.js deleted file mode 100644 index 7193964d..00000000 --- a/routes/referrals.js +++ /dev/null @@ -1,272 +0,0 @@ -import express from "express"; -var router = express.Router(); - -import { v4 } from "uuid"; - -router.get("/", function(req, res, next) { - res.send("list of referrals..."); -}); - -// Grant & Retrieve Referral tokens for existing user - -// usage: GET '/grant?sponsor_id=N&expiry=2021-09-01' -// -// Required: -// sponsor_id (= user.id) -// Changes to DB: -// adds record to referrals table: -// { token: , sponsor_id: , status: "available" } - -/** - * @api {post} /grant Grant Referral code - * @apiName Grant - * @apiGroup Referrals - * - * - * @apiPermission member - * - * @apiParam {sponsor_id} user_id of member generating referral code. - * @apiParam {expiry} optional expiry date for token. - * - * @apiSuccess {String} token referral token - * @apiSuccess {Date} expiry optional expiry - * - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "token": "*********", - * "status": "available", - * "expiry": null - * } - */ -router.post("/grant", auth, async (req, res) => { - const { sponsor_id, expiry } = req.body; - var token = v4(); - - var ref = await knex.table("referrals").insert({ - sponsor_id: sponsor_id, - token: token, - status: "available" - }); - - return res.send({ token: token, status: "available", expiry: expiry }); -}); - -/** - * @api {get} /grant Grant Referral code (variation using get) - * @apiName Grant - * @apiGroup Referrals - * - * @apiPermission member - * - * @apiPermission member - * @apiParam {sponsor_id} user_id of member generating referral code. - * @apiParam {expiry} optional expiry date for token. - * - * @apiSuccess {String} token referral token - * @apiSuccess {Status} status referral status - * @apiSuccess {Date} expiry optional expiry - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "token": "*********", - * "status": "available", - * "expiry": null - * } - */ -router.get("/grant", auth, async (req, res) => { - const { sponsor_id, expiry } = req.query; - var token = v4(); - - var ref = await knex.table("referrals").insert({ - sponsor_id: sponsor_id, - token: token, - status: "available" - }); - - return res.send({ token: token, status: "available", expiry: expiry }); -}); - -/** - * @api {get} /checkTokens/:sponsor_id Retrieve list of referral tokens generated by this user - * - * @apiName checkTokens - * @apiGroup Referrals - * - * - * @apiPermission member - * - * @apiParam {sponsor_id} user_id of member generating referral code. - * @apiParam {status} optional token status. - * - * @apiSuccess tokens array of token objects - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "tokens": [ - * {"token": "*********", "status": "available", "expiry": null} - * ] - * } - */ - -// TO FIX - change sponsor_id so that it is retrieved from current payload (NOT FROM URL) - -router.get("/checkTokens/:sponsor_id", auth, async (req, res) => { - const { sponsor_id } = req.params; - const { status } = req.query; - - var tokens = knex - .select( - "token", - "created_at", - knex.raw("LEFT(created_at,10) as created"), - "username", - "status" - ) - .from("referrals") - .leftJoin("users", "users.id", "referrals.user_id") - .where("sponsor_id", "like", sponsor_id); - - if (status && status !== "all") - tokens = tokens.where("status", "like", status); - const found = await tokens; - - return res.send({ tokens: found }); -}); - -/** - * @api {get} /verify/:user_id Verify token and apply to existing user - * - * @apiName verify - * @apiGroup Referrals - * - * @apiPermission member - * - * @apiParam {user_id} Current user - * @apiParam {token} Token to validate - * - * @apiSuccess verified = true - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "verified": true - * } - * - * @apiDescription referral token is updated from 'available' to 'used' - * @apiDescription referral token is updated with existing user_id - */ -router.get("/verify/:user_id/:token", auth, async (req, res) => { - const { user_id, token } = req.params; - - const found = await knex - .select("status") - .from("referrals") - .where("token", "like", token) - .whereNull("user_id"); - - if (found && found.length) { - if (found[0].status === "available") { - await knex("referrals") - .where("token", "like", token) - .update({ - status: "used", - user_id: user_id, - updated_at: new Date().toISOString().substring(0, 10) - }); - - return res.send({ - verified: true, - sponsor_id: found.sponsor_id, - updated: found.updated_at - }); - } else { - res.code(500).send({ - verified: false, - message: "Referral already " + found[0].status - }); - } - } else { - res.code(500).send("Invalid referral token"); - } -}); - -/** - * @api {post} /joinQueue Join waiting list (track email & phone) - * @apiName joinQueue - * @apiGroup Referrals - * - * @apiParam {email} email address - * @apiParam {phone} phone number - * - * @apiSuccess verified = true - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "success": true - * "message": 'Added to waiting list' - * } - * - * @apiDescription referral token is updated from 'available' to 'used' - * @apiDescription referral token is updated with existing user_id - */ -router.post("/joinQueue", async (req, res) => { - const { email, phone, user_id } = req.body; - - await knex.table("waiting_list").insert({ - email: email, - phone: phone, - user_id: user_id - }); - - res.send({ - success: true, - message: "Added " + email + " to waiting list " + phone - }); -}); - -router.get("/joinQueue", async (req, res) => { - const { email, phone, user_id } = req.query; - - await knex("waiting_list").insert({ - email: email, - phone: phone, - user_id: user_id - }); - - res.send({ - success: true, - message: "Added " + email + " to waiting list " + phone - }); -}); - -/** - * @api {get} /isReferred/:user_id Check if user is referred - * @apiName isReferred - * @apiGroup Referrals - * - * @apiParam {user_id} user id - * - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * true - * } - * - * @apiDescription returns boolean indicating if user is referred or not - */ -router.get("/isReferred/:user_id", async (req, res) => { - const { user_id } = req.params; - - var referred = await knex("referrals") - .select("referrals.id") - .where("user_id", "like", user_id) - .where("status", "like", "used"); - - if (referred && referred.length) { - res.send({ referred: true }); - } else { - res.send({ referred: false }); - } -}); - -export default router; diff --git a/routes/requests.js b/routes/requests.js index 70ab2b07..0b3832e8 100644 --- a/routes/requests.js +++ b/routes/requests.js @@ -1,8 +1,6 @@ import app from "$app"; -import db from "$db"; import { auth } from "$lib/passport"; import { emit } from "$lib/sockets"; -import { Op, col } from "@sequelize/core"; app.get("/request/:id", auth, async (req, res) => { try { diff --git a/routes/swaps.js b/routes/swaps.js deleted file mode 100644 index 169c2b8f..00000000 --- a/routes/swaps.js +++ /dev/null @@ -1,587 +0,0 @@ -import { emit } from "$lib/sockets"; -import app from "$app"; -import config from "$config"; -import { auth, optionalAuth } from "$lib/passport"; -import store from "$lib/store"; -import axios from "axios"; -import crypto from "crypto"; -import fs from "fs"; -import getAccount from "../lib/account"; -import { Op, col } from "@sequelize/core"; - -const shallow = a => { - let b = {}; - for (x in a) { - if (a[x] instanceof Date || typeof a[x] !== "object") b[x] = a[x]; - } - return b; -}; - -const assets = { - b2e15d0d7a0c94e4e2ce0fe6e8691b9e451377f6e46e8045a86f7c4b5d4f0f23: "bitcoin", - "4eebe36eb0819e6daa5dd3c97733251ff4eb728c810d949365d6dacaad5ef6e8": "tether" -}; - -const timeout = 20000; - -const swap = async (user, { a1, a2, v1, v2 }) => { - if (!parseInt(v1) || v1 < 0 || !parseInt(v2) || v2 < 0) - throw new Error("Invalid amount"); - let rate = v2 / v1; - - const b = await lq.getBalance(); - - Object.keys(b).map(asset => { - assets[asset] = asset; - }); - - assets[config.liquid.btcasset] = "bitcoin"; - assets["ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"] = - "tether"; - assets["a0c358a0f6947864af3a06f3f6a2aeb304df7fd95c922f2f22d7412399ce7691"] = - "adamcoin"; - - if (!assets[a1]) throw new Error("Asset not found on server"); - if (v1 > Math.round(b[assets[a1]] * SATS)) - throw new Error(`Insufficient server funds, ${v1} ${b[assets[a1]]}`); - - await db.transaction(async transaction => { - let a1acc = await getAccount(a1, user, transaction); - let a2acc = await getAccount(a2, user, transaction); - - if (v1 > a1acc.balance) - throw new Error(`Insufficient funds, ${v1} ${a1acc.balance}`); - - l( - `placing order ${v1} ${a1.substr(0, 6)} for ${v2} ${a2.substr(0, 6)}`, - user.username - ); - - await a1acc.decrement({ balance: v1 }, { transaction }); - await a1acc.reload({ transaction }); - - let payment = await db.Payment.create( - { - hash: "Trade Funding", - amount: -v1, - account_id: a1acc.id, - user_id: user.id, - currency: user.currency, - rate: store.rates[user.currency], - confirmed: true, - received: false, - network: "COINOS" - }, - { transaction } - ); - - emit(user.username, "payment", payment.get({ plain: true })); - emit(user.username, "account", a1acc.get({ plain: true })); - - const orders = await db.Order.findAll( - { - where: { - "$acc1.asset$": a2, - "$acc2.asset$": a1, - rate: { [Op.lte]: v1 / v2 }, - accepted: false - }, - order: [ - ["rate", "ASC"], - ["id", "ASC"] - ], - include: [ - { - model: db.User, - as: "user" - }, - { - model: db.Account, - as: "acc1" - }, - { - model: db.Account, - as: "acc2" - } - ] - }, - transaction - ); - - for (let i = 0; i < orders.length; i++) { - if (!v1) break; - let order = orders[i]; - if (order.v2 > v1) { - let rate = order.rate; - await order.decrement( - { - v2: v1 - }, - { transaction } - ); - await order.save({ transaction }); - await order.reload({ transaction }); - order.v1 = Math.round(order.v2 / rate); - await order.save({ transaction }); - - await order.acc1.increment({ balance: v2 }, { transaction }); - await order.acc1.reload({ transaction }); - - emit(order.user.username, "account", order.acc1.get({ plain: true })); - - payment = await db.Payment.create( - { - hash: "Trade Fill", - amount: v1, - account_id: a1acc.id, - user_id: user.id, - currency: user.currency, - rate: store.rates[user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - emit(user.username, "payment", payment.get({ plain: true })); - emit(user.username, "account", a1acc.get({ plain: true })); - - if (order.fee) { - const btc = await getAccount( - config.liquid.btcasset, - order.user, - transaction - ); - - payment = await db.Payment.create( - { - hash: "Swap Fee Refund", - amount: order.fee, - account_id: btc.id, - user_id: order.user_id, - currency: order.user.currency, - rate: store.rates[order.user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - await btc.increment({ balance: order.fee }, { transaction }); - await btc.reload({ transaction }); - - emit(order.user.username, "account", btc.get({ plain: true })); - } - - emit(order.user.username, "payment", payment.get({ plain: true })); - - order = order.get({ plain: true }); - order.a1 = order.acc1.asset; - order.a2 = order.acc2.asset; - order.rate = order.v2 / order.v1; - - broadcast("order", shallow(order)); - v1 = 0; - } else { - v1 -= order.v2; - v2 = Math.round(v1 * rate); - - order.accepted = true; - order.completedAt = new Date(); - - await order.save({ transaction }); - - let payment = await db.Payment.create( - { - hash: "Trade Fill", - amount: order.v1, - account_id: a2acc.id, - user_id: user.id, - currency: user.currency, - rate: store.rates[user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - await a2acc.increment({ balance: order.v1 }, { transaction }); - await a2acc.reload({ transaction }); - - emit(user.username, "payment", payment.get({ plain: true })); - emit(user.username, "account", a2acc.get({ plain: true })); - - payment = await db.Payment.create( - { - hash: "Trade Fill", - amount: order.v2, - account_id: order.a2_id, - user_id: order.user_id, - currency: order.user.currency, - rate: store.rates[order.user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - emit(order.user.username, "payment", payment.get({ plain: true })); - await order.acc2.increment({ balance: order.v2 }, { transaction }); - await order.acc2.reload({ transaction }); - - order = order.get({ plain: true }); - order.a1 = order.acc1.asset; - order.a2 = order.acc2.asset; - order.rate = order.v2 / order.v1; - - broadcast("order", shallow(order)); - } - } - - let order; - if (v1) { - order = await db.Order.create( - { - v1, - v2, - user_id: user.id, - a1_id: a1acc.id, - a2_id: a2acc.id - }, - { transaction } - ); - - order = order.get({ plain: true }); - order.rate = v2 / v1; - order.a1 = a1; - order.a2 = a2; - - broadcast("order", order); - } - }); -}; - -const cancel = async (user, id) => { - await db.transaction(async transaction => { - let order = await db.Order.findOne( - { - where: { - id, - user_id: user.id - }, - include: { - model: db.Account, - as: "acc1" - } - }, - - { transaction } - ); - - let { acc1: account, v1, v2 } = order; - await account.increment({ balance: v1 }, { transaction }); - await account.reload({ transaction }); - - let rate = v2 / v1; - let payment = await db.Payment.create( - { - hash: "Trade Cancelled", - amount: v1, - account_id: account.id, - user_id: user.id, - currency: user.currency, - rate: store.rates[user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - emit(user.username, "account", account.get({ plain: true })); - emit(user.username, "payment", payment.get({ plain: true })); - - if (order.fee) { - const btc = await getAccount(config.liquid.btcasset, user, transaction); - - payment = await db.Payment.create( - { - hash: "Swap Fee Refund", - amount: order.fee, - account_id: btc.id, - user_id: user.id, - currency: user.currency, - rate: store.rates[user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - await btc.increment({ balance: order.fee }, { transaction }); - await btc.reload({ transaction }); - - emit(user.username, "account", btc.get({ plain: true })); - emit(user.username, "payment", payment.get({ plain: true })); - } - - broadcast("removeOrder", id); - await order.destroy({ transaction }); - }); -}; - -app.delete("/order/:id", auth, async (req, res) => { - const { id } = req.params; - const { user } = req; - - await cancel(user, id); - res.send({}); -}); - -app.post("/orders", auth, async (req, res) => { - return res.code(500).send("Trading temporarily disabled"); - const { user } = req; - try { - await swap(user, req.body); - res.send({}); - } catch (e) { - err(req.user.username, e.message); - res.code(500).send(e.message); - } -}); - -app.get("/orders", optionalAuth, async (req, res) => { - try { - let orders = await db.Order.findAll({ - attributes: { - include: [ - [col("acc1.asset"), "a1"], - [col("acc2.asset"), "a2"] - ] - }, - include: [ - { - model: db.Account, - as: "acc1", - attributes: [] - }, - { - model: db.Account, - as: "acc2", - attributes: [] - } - ] - }); - - res.send( - orders.map(order => { - if (order.user_id !== req.user.id) order.user_id = null; - return order; - }) - ); - } catch (e) { - res.code(500).send(e.message); - } -}); - -if (config.maker) { - debug("setup maker account..."); - config.maker.map( - ({ amount, c1, c2, currency, askMultiplier, bidMultiplier }) => { - setInterval(async () => { - if (!rates) return; - - const user = await db.User.findOne({ - where: { - username: "maker" - } - }); - - if (!user) { - debug("maker user not defined in database"); - } else { - debug("maker: " + JSON.stringify(user)); - await db.transaction(async transaction => { - let order = await db.Order.findOne( - { - where: { - user_id: user.id, - "$acc1.asset$": c1, - "$acc2.asset$": c2, - accepted: false - }, - include: [ - { - model: db.Account, - as: "acc1" - }, - { - model: db.Account, - as: "acc2" - } - ] - }, - { transaction } - ); - - let params = { - a1: c1, - a2: c2, - v1: amount * SATS, - v2: Math.round( - amount * - SATS * - (((app.get("ask") * store.rates[currency]) / - store.rates["USD"]) * - askMultiplier) - ) - }; - - if (order) { - let bestBid = await db.Order.findOne( - { - where: { - "$acc1.asset$": c2, - "$acc2.asset$": c1, - accepted: false, - id: { - [Op.ne]: order.id - } - }, - include: [ - { - model: db.Account, - as: "acc1" - }, - { - model: db.Account, - as: "acc2" - } - ], - order: [["rate", "DESC"]], - limit: 1 - }, - { transaction } - ); - if ( - !bestBid || - (params.v2 / params.v1 > bestBid.rate && - order.acc2.balance >= params.v2) - ) { - order.v1 = params.v1; - order.v2 = params.v2; - await order.save(); - order = order.get({ plain: true }); - order.a1 = params.a1; - order.a2 = params.a2; - order.rate = order.v2 / order.v1; - - broadcast("order", shallow(order)); - } - } else { - try { - await swap(user, params); - } catch (e) { - warn("Failed to make ask", e.message); - } - } - - order = await db.Order.findOne( - { - where: { - user_id: user.id, - "$acc1.asset$": c2, - "$acc2.asset$": c1, - accepted: false - }, - include: [ - { - model: db.Account, - as: "acc1" - }, - { - model: db.Account, - as: "acc2" - } - ] - }, - { transaction } - ); - - params = { - a1: c2, - a2: c1, - v1: Math.round( - amount * - SATS * - ((app.get("bid") * store.rates[currency]) / - store.rates["USD"]) * - bidMultiplier - ), - v2: amount * SATS - }; - - if (order) { - let bestAsk = await db.Order.findOne( - { - where: { - "$acc1.asset$": c1, - "$acc2.asset$": c2, - accepted: false, - id: { - [Op.ne]: order.id - } - }, - include: [ - { - model: db.Account, - as: "acc1" - }, - { - model: db.Account, - as: "acc2" - } - ], - order: [["rate", "ASC"]], - limit: 1 - }, - transaction - ); - - if (bestAsk) - if ( - !bestAsk || - (params.v1 / params.v2 < bestAsk.rate && - order.acc1.balance >= params.v1) - ) { - order.v1 = params.v1; - order.v2 = params.v2; - await order.save(); - order = order.get({ plain: true }); - order.a1 = params.a1; - order.a2 = params.a2; - order.rate = order.v2 / order.v1; - - broadcast("order", shallow(order)); - } else { - broadcast("removeOrder", order.id); - await order.destroy({ transaction }); - } - } else { - try { - await swap(user, params); - } catch (e) { - warn("Failed to make bid", e.message); - } - } - }); - } - }, 5000); - } - ); -} diff --git a/routes/tickets.js b/routes/tickets.js deleted file mode 100644 index 7ca56740..00000000 --- a/routes/tickets.js +++ /dev/null @@ -1,22 +0,0 @@ -import app from "$app"; -import redis from "$lib/redis"; -import { auth } from "$lib/passport"; - -app.get("/ticket", async (req, res) => { - res.send({ ticket: await redis.lLen("tickets") }); -}); - -app.get("/tickets", async (req, res) => { - res.send(await redis.lRange("tickets", 0, -1)); -}); - -app.post("/ticket", auth, async (req, res) => { - try { - let { asset } = req.body; - if (!asset || asset.length !== 64) throw new Error("invalid asset"); - - res.send(await redis.rPush("tickets", req.body.asset)); - } catch (e) { - res.code(500).send(e.message); - } -}); diff --git a/routes/users.js b/routes/users.js index 4e6b8a63..4a01cc1e 100644 --- a/routes/users.js +++ b/routes/users.js @@ -7,19 +7,11 @@ import { getUser, nada, uniq, wait } from "$lib/utils"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { authenticator } from "otplib"; -import Sequelize from "@sequelize/core"; -import { fromBase58, fromPrivateKey } from "bip32"; -import { Mutex } from "async-mutex"; -import bip32 from "bip32"; import whitelist from "$lib/whitelist"; import { l, err, warn } from "$lib/logging"; -import db from "$db"; -import bc from "$lib/bitcoin"; -import lq from "$lib/liquid"; import { emit } from "$lib/sockets"; import register from "$lib/register"; import { requirePin } from "$lib/utils"; -import { coinos, q } from "$lib/nostr"; import got from "got"; import { bech32 } from "bech32"; @@ -30,28 +22,7 @@ const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); app.get("/me", auth, async (req, res) => { try { let user = req.user.get({ plain: true }); - let payments = await req.user.getPayments({ - where: { - account_id: user.account_id - }, - order: [["id", "DESC"]], - limit: 12, - include: [ - { - model: db.Account, - as: "account" - }, - { - model: db.Payment, - as: "fee_payment" - } - ] - }); - - user.accounts = await req.user.getAccounts(); - user.payments = payments; user.haspin = !!user.pin; - res.send(pick(user, ...whitelist)); } catch (e) { console.log("problem fetching user", e); @@ -59,53 +30,37 @@ app.get("/me", auth, async (req, res) => { } }); -app.get("/users/:username", async (req, res) => { +app.get("/users/:key", async (req, res) => { try { - let { username } = req.params; - - if (username.startsWith("npub")) { - username = Buffer.from(fromWords(decode(username).words)).toString("hex"); + if (key.startsWith("npub")) { + key = Buffer.from(fromWords(decode(key).words)).toString("hex"); } - let user = await db.User.findOne({ - attributes: [ - "username", - "banner", - "profile", - "address", - "currency", - "pubkey", - "display", - "prompt", - "uuid" - ], - where: { - [Sequelize.Op.or]: [ - Sequelize.where( - Sequelize.fn("lower", Sequelize.col("username")), - username.toLowerCase() - ), - { pubkey: username } - ] - } - }); - - if (user) user = user.get({ plain: true }); - else user = JSON.parse(await redis.get(`user:${username}`)); + user = JSON.parse(await redis.get(`user:${key}`)); - if (!user && username.length === 64) { - let newuser = { - username: username.substr(0, 6), - pubkey: username, + if (!user && key.length === 64) { + user = { + username: key.substr(0, 6), + pubkey: key, anon: true }; - - user = JSON.parse(await redis.get(`user:${newuser.pubkey}`)) || newuser; } if (!user) return res.code(500).send("User not found"); - res.send(user); + let whitelist = [ + "username", + "banner", + "profile", + "address", + "currency", + "pubkey", + "display", + "prompt", + "uuid" + ]; + + res.send(pick(user, ...whitelist)); } catch (e) { console.log(e); res.code(500).send(e.message); @@ -402,8 +357,6 @@ let login = async (req, res) => { }; app.post("/login", login); -app.post("/taboggan", login); -app.post("/doggin", login); app.post("/logout", optionalAuth, async (req, res) => { let { subscription } = req.body; @@ -428,100 +381,6 @@ app.post("/logout", optionalAuth, async (req, res) => { res.send({}); }); -app.post("/account", auth, async (req, res) => { - const { user } = req; - const { - id, - name, - ticker, - precision, - domain, - seed, - pubkey, - privkey, - path, - hide, - index - } = req.body; - - try { - await db.transaction(async transaction => { - let account = await db.Account.findOne({ - where: { id, user_id: user.id }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - let params = { - name, - ticker, - precision, - domain, - seed, - pubkey, - path, - hide, - index, - privkey - }; - - if (!account || (account.pubkey && !pubkey)) delete params.pubkey; - - await db.Account.update(params, { - where: { id, user_id: user.id }, - transaction - }); - - await account.reload({ transaction }); - emit(user.username, "account", account); - res.send({}); - }); - } catch (e) { - err("problem updating account", e.message); - return res.code(500).send("There was a problem updating the account"); - } -}); - -app.post("/shiftAccount", auth, async (req, res) => { - let { user } = req; - let { id } = req.body; - - try { - const account = await db.Account.findOne({ - where: { id } - }); - - if (account.user_id !== user.id) - return res.code(500).send("Failed to open wallet"); - - user.account_id = account.id; - await user.save(); - let payments = await db.Payment.findAll({ - where: { - account_id: id - }, - order: [["id", "DESC"]], - limit: 12, - include: { - model: db.Account, - as: "account" - } - }); - - user = user.get({ plain: true }); - user.payments = payments; - user.account = account.get({ plain: true }); - - emit(user.username, "account", user.account); - emit(user.username, "user", user); - - res.send(user); - } catch (e) { - err("problem switching account", e.message); - return res.code(500).send("There was a problem switching accounts"); - } -}); - app.get("/vapidPublicKey", function(req, res) { res.send(config.vapid.publicKey); }); @@ -548,36 +407,6 @@ app.post("/password", auth, async function(req, res) { res.send(await bcrypt.compare(password, user.password)); }); -app.get("/isInternal", auth, async function(req, res) { - let { user } = req; - let { address } = req.query; - if (!address) throw new Error("Address not provided"); - - let invoice = await db.Invoice.findOne({ - where: { address }, - include: { - attributes: ["username"], - model: db.User, - as: "user" - } - }); - - if (invoice) { - let info; - try { - info = await bc.getAddressInfo(address); - } catch (e) { - info = await lq.getAddressInfo(address); - } - if (info.ismine) { - emit(user.username, "to", invoice.user); - return res.send(true); - } - } - - res.send(false); -}); - app.get("/invoices", auth, async function(req, res) { let invoices = await db.Invoice.findAll({ where: { user_id: req.user.id } @@ -585,22 +414,6 @@ app.get("/invoices", auth, async function(req, res) { res.send(invoices); }); -app.post("/signMessage", auth, async function(req, res) { - let { address, message } = req.body; - - let invoices = await db.Invoice.findAll({ - where: { user_id: req.user.id } - }); - - if (invoices.find(i => i.address === address)) { - if (config.bitcoin.walletpass) - await bc.walletPassphrase(config.bitcoin.walletpass, 300); - return res.send(await bc.signMessage(address, message)); - } - - res.code(500).send("Address not found for user"); -}); - app.post("/otpsecret", auth, async function(req, res) { try { await requirePin(req); @@ -652,105 +465,3 @@ app.post("/checkPassword", auth, async function(req, res) { res.code(500).send("Invalid password"); } }); - -app.get("/:pubkey/follows", async (req, res) => { - try { - let { pubkey } = req.params; - let { tagsonly } = req.query; - - let sub = `${pubkey}:follows`; - q( - sub, - { - limit: 1, - kinds: [3], - authors: [pubkey] - }, - { timeout: 60000, eager: 60000 } - ).catch(nada); - - let tags = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; - if (tagsonly) return res.send(tags); - - let follows = []; - for (let f of tags) { - let [_, pubkey] = f; - - q(`${pubkey}:profile:f1`, { - limit: 1, - kinds: [0], - authors: [pubkey] - }).catch(nada); - - let user = JSON.parse(await redis.get(`user:${pubkey}`)); - - if (!user) - user = { - username: pubkey.substr(0, 6), - pubkey, - anon: true - }; - - follows.push(user); - } - - follows = uniq(follows, e => e.pubkey); - follows.sort((a, b) => a.username.localeCompare(b.username)); - - res.send(follows); - } catch (e) { - console.log(e); - res.code(500).send(e && e.message); - } -}); - -app.get("/:pubkey/followers", async (req, res) => { - try { - let { pubkey } = req.params; - - let pubkeys = [ - ...new Set([ - ...(await got(`${config.nostr}/followers?pubkey=${pubkey}`).json()), - ...(await redis.sMembers(`${pubkey}:followers`)) - ]) - ]; - - let followers = []; - - q( - `${pubkey}:followers`, - { kinds: [3], "#p": [pubkey] }, - { timeout: 60000, eager: 60000 } - ).catch(nada); - - for (let pubkey of pubkeys) { - let user = JSON.parse(await redis.get(`user:${pubkey}`)); - if (!user || !user.updated) { - q(`${pubkey}:profile:f2`, { - limit: 1, - kinds: [0], - authors: [pubkey] - }).catch(nada); - - user = JSON.parse(await redis.get(`user:${pubkey}`)); - } - - if (!user) - user = { - username: pubkey.substr(0, 6), - pubkey, - anon: true - }; - - followers.push(user); - } - - followers = uniq(followers, e => e.pubkey); - followers.sort((a, b) => a.username.localeCompare(b.username)); - - res.send(followers); - } catch (e) { - console.log(e); - res.code(500).send(e && e.message); - } -}); diff --git a/sampleconfig/certbot/.gitkeep b/sampleconfig/certbot/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/sampleconfig/certbot/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/sampleconfig/index.js b/sampleconfig/index.js index 3b2f9d02..8683bfa6 100644 --- a/sampleconfig/index.js +++ b/sampleconfig/index.js @@ -1,80 +1,16 @@ -import lnurl from "./lnurl"; - -const db = { - user: "root", - password: "password", - database: "coinos", - options: { - logging: false, - host: "maria", - dialect: "mariadb", - dialectOptions: { multipleStatements: true, timezone: "Etc/GMT+7" } - } -}; - -const btcasset = - "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"; - export default { - nostr: "http://api:9393", - relay: "ws://nostr:8080", - ipxapi: "4275|PqKjKaxIlzooToria2m4XFLzvHihSpsZMX4hvgxu", - webhooks: { - "http://172.18.0.1:5173/wallet/purchase": "horsey" - }, - postmark: "d64dffd2-84ea-47c7-ba9f-95505332d0ae", - db, - clientVersion: "3b96359d6a6ce68fe4a32f495fc9a6f78af0aa63", - lnurl, - knex: { - client: "mysql2", - connection: db - }, - jwt: Buffer.from( - "00d0ab4f7738a83feb37f661526512063c41e49278b7c32cba87314269a5788b", - "hex" - ), port: 3119, - taxi: "864234bccdd230ecb6db05ebdf5061c3af42fefb9f186b3326e6454fdd3a9475", + nostr: "ws://nostr:8080", + ipxapi: "4275|PqKjKaxIlzooToria2m4XFLzvHihSpsZMX4hvgxu", + postmark: "f64dffd2-84ea-47c7-ba9f-95e053a2d0ae", + jwt: "00d0ab4f7738a83feb37f661526512063c41e49278b7c32cba87314269a5788b", bitcoin: { - masterkey: - "tprv8ZgxMBicQKsPdTUoagV41nang9pQUA2DKoVWLmTrWmb2PfsC8pDTHveCMyzUyMguKJCjd5uyHcqg27r7gDzz4TY3MgucpLsSXwCbjn2C3Q1", host: "bitcoin", wallet: "coinos", username: "admin1", password: "123", network: "regtest", port: 18443, - zmqrawblock: "tcp://bitcoin:18506", - zmqrawtx: "tcp://bitcoin:18507" - }, - liquid: { - masterkey: - "tprv8ZgxMBicQKsPfBjZp16yQUmZM3DWh5fb366inBXF3Xk6ANhJYqxXYL4FeQxAW5kjT3ku1A1wmS79c5y6RajKr2TkCADG8A3h4WdggbFMXX1", - blindkey: - "099e599b1d79dbc536c6f8461772d7f2f9583d7b6f3fba70394d60b599c3bd96", - host: "liquid", - username: "admin1", - password: "123", - network: "regtest", - wallet: "coinos", - port: 7040, - zmqrawblock: "tcp://liquid:18606", - zmqrawtx: "tcp://liquid:18607", - btcasset }, - lna: { - clightning: true, - dir: "/app/config/lightning/regtest/lightning-rpc" - /* - lnd: true - socket: "lnd:10009", - // get this with: sudo base64 config/lnd/tls.cert | tr -d '\n' - cert: - "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNLVENDQWMrZ0F3SUJBZ0lRVE4wWEhJdTI1eitKaUpmVUxSOTROVEFLQmdncWhrak9QUVFEQWpBNE1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1SVXdFd1lEVlFRREV3d3haREkyWVRkagpObUZqTVRZd0hoY05Nakl3TlRFMU1qSTFOVFEyV2hjTk1qTXdOekV3TWpJMU5UUTJXakE0TVI4d0hRWURWUVFLCkV4WnNibVFnWVhWMGIyZGxibVZ5WVhSbFpDQmpaWEowTVJVd0V3WURWUVFERXd3eFpESTJZVGRqTm1Gak1UWXcKV1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFjdzRtR0lNbEpkTWsvT2pEWkJ6U21HTm5kY3pQZgphSGlCQytqd0F5RVBnOUhqY3o2MlhYSXdxcTFiV2xmOGtuck5lamg2cm5zQTgrYkNOWTBkNFlxRW80RzZNSUczCk1BNEdBMVVkRHdFQi93UUVBd0lDcERBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEUKQlRBREFRSC9NQjBHQTFVZERnUVdCQlNZTmx4NTBXU2VIN0VTSTZ2eVJCL2JNMG01QmpCZ0JnTlZIUkVFV1RCWApnZ3d4WkRJMllUZGpObUZqTVRhQ0NXeHZZMkZzYUc5emRJSURiRzVrZ2dSMWJtbDRnZ3AxYm1sNGNHRmphMlYwCmdnZGlkV1pqYjI1dWh3Ui9BQUFCaHhBQUFBQUFBQUFBQUFBQUFBQUFBQUFCaHdTc0VnQUhNQW9HQ0NxR1NNNDkKQkFNQ0EwZ0FNRVVDSUU5M3hBWEFxQzBuejdqRWVXWVN3czB2dlZWZmcvSEFNYnNLSzVXMFFNMCtBaUVBME5EVgpOV0VVdU9Vdk4ydTZ0UjBpRFh3dXhPMmJtTDltVEZwejhHc3RUKzA9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", - // get this with: sudo base64 config/lnd/data/chain/bitcoin/regtest/admin.macaroon | tr -d '\n' - macaroon: - "AgEDbG5kAvgBAwoQFdsmSt7tRRKg8kFR+avH0BIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaIQoIbWFjYXJvb24SCGdlbmVyYXRlEgRyZWFkEgV3cml0ZRoWCgdtZXNzYWdlEgRyZWFkEgV3cml0ZRoXCghvZmZjaGFpbhIEcmVhZBIFd3JpdGUaFgoHb25jaGFpbhIEcmVhZBIFd3JpdGUaFAoFcGVlcnMSBHJlYWQSBXdyaXRlGhgKBnNpZ25lchIIZ2VuZXJhdGUSBHJlYWQAAAYgR93/t3TVABMMqjYOdcYG/B70ZczhSoLXVT7NlocedG0=" - */ - } + lightning: "/app/config/lightning/regtest/lightning-rpc" }; diff --git a/sampleconfig/liquid/elements.conf b/sampleconfig/liquid/elements.conf deleted file mode 100644 index 3aa177d0..00000000 --- a/sampleconfig/liquid/elements.conf +++ /dev/null @@ -1,18 +0,0 @@ -chain=liquidregtest -listen=1 -txindex=1 - -[liquidregtest] -rpcuser=admin1 -rpcpassword=123 -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 -rpcwallet=coinos - -validatepegin=0 -initialfreecoins=2100000000000000 -wallet=coinos -wallet=external - -zmqpubrawblock=tcp://0.0.0.0:18606 -zmqpubrawtx=tcp://0.0.0.0:18607 diff --git a/sampleconfig/lnd/lnd.conf b/sampleconfig/lnd/lnd.conf deleted file mode 100644 index a979284a..00000000 --- a/sampleconfig/lnd/lnd.conf +++ /dev/null @@ -1,25 +0,0 @@ -externalip=lnd:9735 -alias=coinos -color=#FF3200 -tlsextradomain=lnd -listen=0.0.0.0:9735 -rpclisten=0.0.0.0:10009 -restlisten=0.0.0.0:8080 -accept-amp=1 -accept-keysend=1 - -# uncomment this after lnd wallet is initialized -# wallet-unlock-password-file=/root/.lnd/pwd - -[Bitcoin] -bitcoin.active=1 -bitcoin.node=bitcoind -bitcoin.regtest=1 - -[Bitcoind] -bitcoind.rpchost=bitcoin -bitcoind.dir=/root/.bitcoin -bitcoind.rpcuser=admin1 -bitcoind.rpcpass=123 -bitcoind.zmqpubrawblock=tcp://bitcoin:18506 -bitcoind.zmqpubrawtx=tcp://bitcoin:18507 diff --git a/sampleconfig/lnd/pwd b/sampleconfig/lnd/pwd deleted file mode 100644 index f3097ab1..00000000 --- a/sampleconfig/lnd/pwd +++ /dev/null @@ -1 +0,0 @@ -password diff --git a/sampleconfig/lnurl.js b/sampleconfig/lnurl.js deleted file mode 100644 index b101e23c..00000000 --- a/sampleconfig/lnurl.js +++ /dev/null @@ -1,36 +0,0 @@ -import fs from "fs"; - -export default { - host: "app", - port: 3118, - protocol: "http", - url: "https://staging.coinos.io", - endpoint: "/lnurl", - listen: true, - auth: { - apiKeys: [ - { - id: "50bdeb634d", - key: "ac9bdd15a983072311c74c13877d122f16a27a8f40cb4dfa5014eece4da5cda2" - } - ] - }, - lightning: { - backend: "c-lightning", - config: { - unixSockPath: "/app/config/lightning/regtest/lightning-rpc" - } - }, - store: { - backend: "knex", - config: { - client: "mysql", - connection: { - host: "maria", - user: "root", - password: "password", - database: "coinos" - } - } - } -}; diff --git a/sampleconfig/nginx/default.conf.template b/sampleconfig/nginx/default.conf.template deleted file mode 100644 index 453f368f..00000000 --- a/sampleconfig/nginx/default.conf.template +++ /dev/null @@ -1,38 +0,0 @@ -server { - listen 80; - server_name .${NGINX_HOST}; - server_tokens off; - - location @rewrites { - rewrite ^(.+)$ /index.html last; - } - - location / { - root /coinos; - index index.html; - try_files $uri $uri/ /index.html; - } - - location /api/ { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto https; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Forwarded-Host $remote_addr; - rewrite ^ $request_uri?; - rewrite ^/api/(.*) /$1 break; - return 400; - proxy_pass http://app:3119$uri$is_args; - } - - location /ws { - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_http_version 1.1; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; - rewrite ^/ws/(.*) /$1 break; - - proxy_pass http://app:3119; - } -} diff --git a/sampledata/conversions.json b/sampledata/conversions.json deleted file mode 100644 index 2c63c085..00000000 --- a/sampledata/conversions.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/sampledata/emails.json b/sampledata/emails.json deleted file mode 100644 index 5fbac846..00000000 --- a/sampledata/emails.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "emails": [ - ] -} diff --git a/sampledata/logins.json b/sampledata/logins.json deleted file mode 100644 index 0967ef42..00000000 --- a/sampledata/logins.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/sampledata/payments.json b/sampledata/payments.json deleted file mode 100644 index 2c63c085..00000000 --- a/sampledata/payments.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/sampledata/recipients.json b/sampledata/recipients.json deleted file mode 100644 index 2c63c085..00000000 --- a/sampledata/recipients.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/sampledata/withdrawals.json b/sampledata/withdrawals.json deleted file mode 100644 index 0967ef42..00000000 --- a/sampledata/withdrawals.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/scripts/apiParse.js b/scripts/apiParse.js deleted file mode 100644 index ec2a33c1..00000000 --- a/scripts/apiParse.js +++ /dev/null @@ -1,46 +0,0 @@ -export const addUserSearch = function(input, table) { - var { search, starts_with, contains } = input; - - if (!table) { - table = "users"; - } - // if (since) cmd = cmd.havingRaw('invoices + payments + deposits + orders > ?', [0]) - - if (search) { - if (starts_with) { - search = search + "%"; - } else if (contains) { - search = "%" + search + "%"; - } - - var condition = - "(" + - table + - ".email like '" + - search + - "' OR " + - table + - ".username like '" + - search + - "')"; - - return condition; - } else { - return ""; - } -}; - -export const addTimeSearch = function(input, field) { - if (!field) { - field = "updatedAt"; - } - - var { since } = input; - if (since) { - condition = field + " >= '" + since + "'"; - - return condition; - } else { - return ""; - } -}; diff --git a/scripts/check.js b/scripts/check.js deleted file mode 100644 index e86940da..00000000 --- a/scripts/check.js +++ /dev/null @@ -1,24 +0,0 @@ -let config = require("./config"); -let fs = require("fs"); - -let mailgun = require("mailgun-js")({ - apiKey: config.mailgun, - domain: config.domain -}); - -let data = { - ...config.mail, - subject: "User Documents", - text: "Documents uploaded" -}; - -fs.readdir("./uploads", (err, files) => { - files.forEach(file => { - console.log(file); - data.text = file; - fs.rename(file, "../verified" + file); - mailgun.messages().send(data, function(error, body) { - console.log(body); - }); - }); -}); diff --git a/scripts/purge-except-git.sh b/scripts/purge-except-git.sh deleted file mode 100755 index 066b082f..00000000 --- a/scripts/purge-except-git.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -# purge coinos-ui and coinos-server -# ie- fresh install/clean working dirs but without the clone - -# useful for local dev when you have stashed changes or branches -# you don't want to destroy - -# start script from coinos-server/scripts ie: -# $ cd /var/node/coinos-server/scripts -# $ ./purge-except-git.sh - -# you may have to run it with sudo to remove the directories created by docker - -# and now the script will move up a dir and do its thing: -cd ../ - -# if you have docker-compose process running -docker-compose down - -#purge: -rm -rf config -rm -rf mysql -rm -rf node_modules -rm .env -rm fx -rm docker-compose.override.yml - -#also on ui: -cd ../coinos-ui -rm -rf node_modules - -# (and then follow instructions in root README to rebuild/install) diff --git a/secret b/secret deleted file mode 100644 index e69de29b..00000000 -- cgit v1.2.3 From ded2c69ce89438326523a512b570be6eee4b23f0 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 9 Jan 2023 01:50:34 -0800 Subject: use data folder for container data --- .env | 17 ++ .gitignore | 25 +-- .pnpm-debug.log | 22 ++ docker-compose.yml | 10 +- index.js | 1 - lib/mail.js | 5 - routes/lnurl.js | 575 ----------------------------------------------------- 7 files changed, 46 insertions(+), 609 deletions(-) create mode 100644 .env create mode 100644 .pnpm-debug.log delete mode 100644 routes/lnurl.js (limited to 'lib') diff --git a/.env b/.env new file mode 100644 index 00000000..9fedb2dc --- /dev/null +++ b/.env @@ -0,0 +1,17 @@ +NGINX_PORT=8080 +APP_SERVER_PORT=3220 +BITCOIN_PEER_PORT=18442 +BITCOIN_NODE_PORT=18443 +LIQUID_PEER_PORT=18444 +LIQUID_NODE_PORT=7040 +BITCOIN_ELECTRS_RPC_PORT=51401 +BITCOIN_ELECTRS_PORT=3002 +LIQUID_ELECTRS_RPC_PORT=60401 +LIQUID_ELECTRS_PORT=3012 +LIQUID_BLOCK_PORT=18606 +LIQUID_TX_PORT=18607 +LIQUID_ESPLORA_URL=http://localhost:5005 +LIQUID_ESPLORA_PORT=5005 +LIQUID_ELECTRS_URL=http://localhost:3012 +ROOT_PASSWORD=password +HOST=dev.coinos.io diff --git a/.gitignore b/.gitignore index 3b6aa213..af41b69f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,5 @@ node_modules/ -.env -yarn-error.log -.pnp* -.yarn* -tls.* -leveldb -swaps -exceptions -fx +.pnpm-store uploads -verified data -lnurl-server.sqlite3 -watcher.log -config -mysql -build.json -build.sh -watcher.sh -logs -docker-compose.mainnet.yml -assets.json -lm.yml -lt.yml -caddy +config.js diff --git a/.pnpm-debug.log b/.pnpm-debug.log new file mode 100644 index 00000000..ad5f9d74 --- /dev/null +++ b/.pnpm-debug.log @@ -0,0 +1,22 @@ +{ + "0 debug pnpm:scope": { + "selected": 1 + }, + "1 error pnpm": { + "code": "ELIFECYCLE", + "errno": "ENOENT", + "syscall": "spawn", + "file": "sh", + "pkgid": "coinos@0.0.1", + "stage": "start", + "script": "nodemon index.js", + "pkgname": "coinos", + "err": { + "name": "pnpm", + "message": "coinos@0.0.1 start: `nodemon index.js`\nspawn ENOENT", + "code": "ELIFECYCLE", + "stack": "pnpm: coinos@0.0.1 start: `nodemon index.js`\nspawn ENOENT\n at ChildProcess. (/usr/local/lib/node_modules/pnpm/dist/pnpm.cjs:92977:22)\n at ChildProcess.emit (node:events:394:28)\n at maybeClose (node:internal/child_process:1067:16)\n at Process.ChildProcess._handle.onexit (node:internal/child_process:301:5)" + } + }, + "2 warn pnpm:global": " Local package.json exists, but node_modules missing, did you mean to install?" +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b4a73394..72c3cec1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: image: asoltys/bitcoin container_name: bc volumes: - - ./config/bitcoin:/home/bitcoin/.bitcoin + - ./data/bitcoin:/home/bitcoin/.bitcoin restart: always cl: image: elementsproject/lightningd:latest @@ -28,13 +28,13 @@ services: environment: LIGHTNINGD_NETWORK: regtest volumes: - - ./config/lightning:/root/.lightning + - ./data/lightning:/root/.lightning restart: always rd: image: redis container_name: rd volumes: - - ./config/redis:/data + - ./data/redis:/data restart: always nostr: container_name: nostr @@ -42,7 +42,7 @@ services: restart: always user: 100:100 volumes: - - "./config/nostr/data:/usr/src/app/db" - - "./config/nostr/config.toml:/usr/src/app/config.toml" + - "./data/nostr/data:/usr/src/app/db" + - "./data/nostr/data.toml:/usr/src/app/config.toml" ports: - '8080:8080' diff --git a/index.js b/index.js index b5ec0dca..bec89372 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,6 @@ import "./routes/invoices"; import "./routes/payments"; import "./routes/users"; import "./routes/requests"; -import "./routes/lnurl"; let host = process.env.HOST || "0.0.0.0"; let port = process.env.PORT || 3119; diff --git a/lib/mail.js b/lib/mail.js index a146d128..966ce578 100644 --- a/lib/mail.js +++ b/lib/mail.js @@ -1,13 +1,8 @@ import app from "$app"; import config from "$config"; -import persist from "./persist"; -const emails = persist("data/emails.json"); app.post("/email", async (req, res) => { try { - let file = persist("data/emails.json"); - file.emails = [...file.emails, req.body]; - try { var postmark = await import("postmark"); diff --git a/routes/lnurl.js b/routes/lnurl.js deleted file mode 100644 index e9a0b167..00000000 --- a/routes/lnurl.js +++ /dev/null @@ -1,575 +0,0 @@ -import app from "$app"; -import config from "$config"; -import store from "$lib/store"; -import { optionalAuth, auth } from "$lib/passport"; -import { l, err, warn } from "$lib/logging"; - -import ln from "$lib/ln"; -import persist from "$lib/persist"; -import { emit } from "$lib/sockets"; - -import lnurl from "lnurl"; -import jwt from "jsonwebtoken"; -import qs from "query-string"; - -import { - computeConversionFee, - conversionFeeReceiver -} from "./lightning/conversionFee.js"; - -import send from "$lib/send"; -import sendInternal from "$lib/sendInternal"; -import register from "$lib/register"; - -const logins = persist("data/logins.json"); -const recipients = persist("data/recipients.json"); -const lnurlPayments = persist("data/payments.json"); -const withdrawals = persist("data/withdrawals.json"); - -const { HOSTNAME: hostname } = process.env; - -export const lnurlServer = lnurl.createServer(config.lnurl); - -app.get("/url", async (req, res, next) => { - try { - const code = await db.Code.findOne({ - where: { - code: req.query.code - } - }); - - if (code) { - res.send(code.text); - } else { - throw new Error("code not found"); - } - } catch (e) { - err("couldn't find url", e.message); - } -}); - -app.get("/withdraw", auth, async (req, res, next) => { - const { min, max } = req.query; - try { - const result = await lnurlServer.generateNewUrl("withdrawRequest", { - minWithdrawable: min * 1000, - maxWithdrawable: max * 1000, - defaultDescription: "coinos voucher" - }); - - withdrawals[result.secret] = req.user; - - res.send(result); - } catch (e) { - err("problem generating withdrawl url", e.message); - res.code(500).send(e.message); - } -}); - -app.post("/code", async (req, res, next) => { - try { - const { encoded } = req.body.lnurl; - const code = await db.Code.findOrCreate({ - where: { - code: `lnurl:${encoded.substr(-32)}`, - text: encoded - } - }); - - res.send(code); - } catch (e) { - err(e.errors); - res.code(500).send(e.message); - } -}); - -app.post("/withdraw", auth, async (req, res, next) => { - const { user } = req; - const { - amount: value, - params: { callback, k1 } - } = req.body; - - let invoice; - let label = new Date(); - let desc = label; - - invoice = await ln.invoice(`${value}sats`, label, desc); - - const { payment_request: pr } = invoice; - const url = `${callback}?k1=${k1}&pr=${pr}`; - - await db.Invoice.create({ - user_id: user.id, - text: pr, - rate: store.rates[user.currency], - currency: user.currency, - amount: value, - tip: 0, - network: "bitcoin" - }); - - try { - result = (await axios.get(url)).data; - res.send(result); - } catch (e) { - err("failed to withdraw", e.message); - res.code(500).send(e.message); - } -}); - -app.get("/lnurlp/:username", async (req, res, next) => { - try { - const { username } = req.params; - let user = await db.User.findOne({ - where: { - username - } - }); - - if (!user) throw new Error("user not found"); - - let { amount, minSendable, maxSendable } = req.query; - minSendable = minSendable || 1000; - maxSendable = maxSendable || 1000000000; - if (parseInt(amount)) minSendable = maxSendable = amount * 1000; - - let result = await lnurlServer.generateNewUrl("payRequest", { - minSendable, - maxSendable, - metadata: JSON.stringify([ - ["text/plain", `paying ${user.username}`], - ["text/identifier", `${user.username}@coinos.io`] - ]) - }); - - recipients[result.secret] = { - id: user.id, - currency: user.currency, - username - }; - - l("recipient", user.username, result.secret); - - result = await axios.get(result.url); - - res.send(result.data); - } catch (e) { - err("problem generating payment url", e.message); - res.code(500).send(e.message); - } -}); - -app.get("/pay/:username", async (req, res, next) => { - const { username } = req.params; - let user = await db.User.findOne({ - where: { - username - } - }); - - let { amount, minSendable, maxSendable } = req.query; - minSendable = minSendable || 1000; - maxSendable = maxSendable || 1000000000; - if (parseInt(amount)) minSendable = maxSendable = amount * 1000; - - try { - const result = await lnurlServer.generateNewUrl("payRequest", { - minSendable, - maxSendable, - metadata: JSON.stringify([ - ["text/plain", `paying ${user.username}`], - ["text/identifier", `${user.username}@coinos.io`] - ]) - }); - - recipients[result.secret] = { - id: user.id, - currency: user.currency, - username - }; - - l("recipient", user.username, result.secret); - res.send(result); - } catch (e) { - err("problem generating payment url", e.message); - res.code(500).send(e.message); - } -}); - -app.post("/pay", auth, async (req, res, next) => { - const { user } = req; - - const { - amount, - comment, - params: { callback } - } = req.body; - - let url = `${callback}${callback.includes("?") ? "&" : "?"}amount=${amount * - 1000}${comment ? "&comment=" + comment : ""}`; - - try { - const parts = callback.split("/"); - const secret = parts[parts.length - 1]; - lnurlPayments[secret] = user.id; - - if (recipients[secret]) { - let data = await sendInternal( - { - amount, - memo: comment, - username: recipients[secret].username - }, - user - ); - - res.send(data); - } else { - const { data } = await axios.get(url); - res.send(await send(amount, "", data.pr, user)); - } - } catch (e) { - console.log(e); - err("failed to send payment", e.message); - res.code(500).send(e.message); - } -}); - -app.get("/login", optionalAuth, async (req, res, next) => { - try { - const result = await lnurlServer.generateNewUrl("login"); - - if (req.user) { - logins[result.secret] = req.user.username; - } - - res.send(result); - } catch (e) { - err("problem generating login url", e.message); - res.code(500).send(e.message); - } -}); - -app.get("/encode", async (req, res, next) => { - let { domain, name } = req.query; - let url = `https://${domain}/.well-known/lnurlp/${name}`; - res.send(lnurl.encode(url)); -}); - -app.get("/decode", async (req, res, next) => { - const { text } = req.query; - - try { - const url = lnurl.decode(text); - let spl = url.split("?"); - if (spl.length > 1 && qs.parse(spl[1]).tag === "login") { - return res.send({ - tag: "login", - k1: qs.parse(spl[1]).k1, - callback: url, - domain: url - .split("://")[1] - .split("/")[0] - .split("@") - .slice(-1)[0] - .split(":")[0] - }); - } - - const { data: params } = await axios.get(url); - - res.send(params); - } catch (e) { - err("problem decoding lnurl", e.message); - res.code(500).send(e.message); - } -}); - -lnurlServer.on("payRequest:action:processed", async function(event) { - const { secret, params, result } = event; - const { id, invoice } = result; - const recipient = recipients[secret]; - let payreq = bolt11.decode(invoice); - - try { - let i = await db.Invoice.create({ - user_id: recipient.id, - text: invoice, - rate: store.rates[recipient.currency], - currency: recipient.currency, - amount: payreq.satoshis, - tip: 0, - network: "lightning" - }); - - l("invoice created", i.text, i.amount); - } catch (e) { - err("problem finding lnurl invoice", e.message); - } -}); - -lnurlServer.bindToHook( - "middleware:signedLnurl:afterCheckSignature", - async (req, res, next) => { - try { - let user; - const { amount: msats, key, tag, pr, k1 } = req.query; - - if (msats) { - let amount = Math.round(msats / 1000); - const parts = req.originalUrl.split("/"); - const secret = parts[parts.length - 1].split("?")[0]; - let user_id = lnurlPayments[secret]; - - if (user_id) { - let account = await db.Account.findOne({ - where: { - user_id, - asset: config.liquid.btcasset, - pubkey: null - } - }); - - if (account.balance < amount) { - throw new Error("Insufficient funds"); - } - } - } - - if (tag === "login") { - let username = logins[k1]; - - if (!username) { - const keyObj = await db.Key.findOne({ - where: { hex: key }, - include: [ - { - model: db.User, - as: "user" - } - ] - }); - - if (keyObj) ({ user } = keyObj); - - if (!user) { - let ip = - req.headers["x-forwarded-for"] || req.connection.remoteAddress; - - let username = `satoshi-${key.substr(0, 12)}`; - let existing = await db.User.findOne({ where: { username } }); - if (existing) user = existing; - else { - user = await register( - { - username, - password: key - }, - ip - ); - } - } - - ({ username } = user); - } - - logins[key] = { k1, username }; - } - - if (pr) { - user = withdrawals[k1]; - if (!user) { - if (next) next(new Error("withdrawal not found")); - return; - } - - try { - let amount; - - let { msatoshi } = await ln.decodepay(pr); - amount = parseInt(msatoshi / 1000); - let conversionFee = computeConversionFee(amount); - - await db.transaction(async transaction => { - let account = await db.Account.findOne({ - where: { - user_id: user.id, - asset: config.liquid.btcasset - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - // account that receives conversion fees - let receiverAccount = await db.Account.findOne({ - where: { - "$user.username$": conversionFeeReceiver - }, - include: [ - { - model: db.User, - as: "user" - } - ], - lock: transaction.LOCK.UPDATE, - transaction - }); - - let conversionFeeDeduction = Math.min( - account.lightning_credits, - conversionFee - ); - if (conversionFeeDeduction) { - await account.decrement( - { lightning_credits: conversionFeeDeduction }, - { transaction } - ); - await account.reload({ transaction }); - conversionFee -= conversionFeeDeduction; - } - - if (account.balance < amount) { - throw new Error("Insufficient funds"); - } else if (account.balance < amount + conversionFee) { - throw new Error("Insufficient funds for conversion fee"); - } - - let fee_payment_id = null; - if (conversionFee) { - await receiverAccount.increment( - { balance: conversionFee }, - { transaction } - ); - await receiverAccount.reload({ transaction }); - let fee_payment = await db.Payment.create( - { - amount: conversionFee, - fee: 0, - memo: "Bitcoin conversion fee", - account_id: receiverAccount.id, - user_id: receiverAccount.user_id, - rate: store.rates[receiverAccount.user.currency], - currency: receiverAccount.user.currency, - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - fee_payment_id = fee_payment.id; - } - - let payment = await db.Payment.create( - { - amount: -amount, - account_id: account.id, - user_id: user.id, - hash: pr, - rate: store.rates[user.currency], - currency: user.currency, - confirmed: true, - network: "lightning", - fee_payment_id - }, - { transaction } - ); - - setTimeout(async () => { - try { - let { payments } = await ln.getPayments({ - limit: 5 - }); - - let p = payments.find(p => p.request === pr); - if (p) { - l("found payment", pr); - payment.fee = p.fee; - await account.decrement({ balance: p.fee }, { transaction }); - await account.reload({ transaction }); - await payment.save(); - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - emit(user.username, "account", account); - emit(user.username, "payment", payment); - } else { - warn("payment not found, fee not set", pr); - } - } catch (e) { - err("problem trying to get ln withdrawal payment", e); - } - }, 1000); - - await account.decrement( - { balance: amount + conversionFee }, - { transaction } - ); - await account.reload({ transaction }); - - let p = payment.get({ plain: true }); - p.account = account.get({ plain: true }); - - emit(user.username, "account", p.account); - emit(user.username, "payment", p); - }); - } catch (e) { - err("failed to process withdrawal", e.message); - return next(e); - } - } - - if (next) next(); - } catch (e) { - err("unhandled lnurerr", e.message); - } - } -); - -lnurlServer.bindToHook("login", async key => { - try { - if (!key) throw new Error("login key not defined"); - - const exists = await db.Key.findOne({ - where: { hex: key }, - include: [{ model: db.User, as: "user" }] - }); - - let user; - if (logins[key] && logins[key] !== "undefined") { - const { username } = logins[key]; - user = await db.User.findOne({ - where: { username } - }); - - if (user) { - const [k, created] = await db.Key.findOrCreate({ - where: { - user_id: user.id, - hex: key - } - }); - - if (created) { - l("added key", username, k); - emit(username, "key", k); - } - } else { - l("user not found"); - user = await register({ - username: key.substr(0, 8), - password: key - }); - } - } else if (exists) ({ user } = exists); - - if (user && user.username) { - const payload = { username: user.username }; - const token = jwt.sign(payload, config.jwt); - const ws = store.sessions[logins[key].k1]; - if (ws && ws.send) - ws.send(JSON.stringify({ type: "token", data: { token, key } })); - } - } catch (e) { - err("problem with login hook", e.message); - } -}); -- cgit v1.2.3 From c3565838644123e77881ff4f8536792f074047b9 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 11 Jan 2023 22:09:48 -0800 Subject: migrating to redis --- docker-compose.yml | 2 + index.js | 6 +- lib/binance.js | 3 - lib/countries.js | 249 +++++ lib/ln.js | 2 +- lib/nostr.js | 1 - lib/passport.js | 7 +- lib/rates.js | 34 +- lib/redis.js | 6 +- lib/register.js | 33 +- lib/sockets.js | 12 +- lib/utils.js | 6 +- lib/webhooks.js | 13 +- lib/whitelist.js | 4 +- package.json | 3 +- pnpm-lock.yaml | 1958 +----------------------------------- routes/invoices.js | 188 +--- routes/lightning/channel.js | 29 - routes/lightning/channelRequest.js | 24 - routes/lightning/index.js | 2 - routes/requests.js | 130 +-- routes/users.js | 68 +- 22 files changed, 413 insertions(+), 2367 deletions(-) create mode 100644 lib/countries.js delete mode 100644 routes/lightning/channel.js delete mode 100644 routes/lightning/channelRequest.js (limited to 'lib') diff --git a/docker-compose.yml b/docker-compose.yml index 72c3cec1..95516fe0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: environment: NODE_OPTIONS: "--es-module-specifier-resolution=node --experimental-modules --loader ./loader.js" NODE_ENV: development + ports: + - '3119:3119' volumes: - ./:/app - ./logs:/logs diff --git a/index.js b/index.js index bec89372..580435e0 100644 --- a/index.js +++ b/index.js @@ -18,7 +18,7 @@ import "./routes/requests"; let host = process.env.HOST || "0.0.0.0"; let port = process.env.PORT || 3119; -app.listen({ host, port }, (e, address) => { - e && err(e) && process.exit(1); - l(`CoinOS Server listening on ${address}`); + +app.listen({ host, port }, (e, h) => { + if (e) return err("Failed to start:", e.message); }); diff --git a/lib/binance.js b/lib/binance.js index 9f7f149f..e583d312 100644 --- a/lib/binance.js +++ b/lib/binance.js @@ -18,10 +18,7 @@ export default () => { store.rates[symbol] = msg.c * store.fx[symbol]; }); - // app.set("ask", msg.a); - // app.set("bid", msg.b); store.last = msg.c; - // console.log("last", msg.c, store.fx["CAD"], msg.c * store.fx["CAD"]); } catch (e) { err("binance message error", e.message); } diff --git a/lib/countries.js b/lib/countries.js new file mode 100644 index 00000000..4d5a48dc --- /dev/null +++ b/lib/countries.js @@ -0,0 +1,249 @@ +export default { + AF: "AFG", + AL: "ALB", + DZ: "DZA", + AD: "AND", + AO: "AGO", + AI: "AIA", + AQ: "ATA", + AR: "ARG", + AM: "ARM", + AW: "ABW", + AU: "AUS", + AT: "AUT", + AZ: "AZE", + BS: "BHS", + BH: "BHR", + BD: "BGD", + BB: "BRB", + BY: "BLR", + BE: "BEL", + BZ: "BLZ", + BJ: "BEN", + BM: "BMU", + BT: "BTN", + BO: "BOL", + BW: "BWA", + BV: "NOK", + BR: "BRA", + IO: "USD", + VG: "USD", + BN: "BRN", + BG: "BGR", + BF: "XOF", + BI: "BDI", + KH: "KHM", + CM: "CMR", + CA: "CAN", + IC: "EUR", + CV: "CVE", + KY: "KYD", + CF: "XAF", + TD: "TCD", + CL: "CHL", + CN: "CHN", + CX: "AUD", + CC: "AUD", + CO: "COL", + KM: "COM", + CG: "XAF", + CD: "CDF", + CK: "NZD", + CR: "CRC", + CI: "XOF", + HR: "HRV", + CU: "CUB", + CW: "ANG", + CY: "CYP", + CZ: "CZK", + DK: "DNK", + DJ: "DJI", + DM: "DMA", + DO: "DOP", + TL: "IDS", + EC: "ECU", + EG: "EGY", + SV: "SVC", + GQ: "XAF", + ER: "ERI", + EE: "EST", + ET: "ETH", + FK: "FKP", + FO: "DKK", + FJ: "FJI", + FI: "FIN", + FR: "FRA", + GF: "EUR", + PF: "XPF", + TF: "EUR", + GA: "GAB", + GM: "GMB", + GE: "GEO", + DE: "DEU", + GH: "GHA", + GI: "GIB", + GR: "GRC", + GL: "GRL", + GD: "GRD", + GP: "GLP", + GU: "GUM", + GT: "GTM", + GG: "GGY", + GN: "GIN", + GW: "XOF", + GY: "GUY", + HT: "HTI", + HM: "AUD", + HN: "HND", + HK: "HKD", + HU: "HUN", + IS: "ISL", + IN: "IND", + ID: "IDN", + IR: "IRN", + IQ: "IRQ", + IE: "IRL", + Of: "Man", + IL: "ISR", + IT: "ITA", + JM: "JAM", + JP: "JPN", + JE: "JEY", + JO: "JOR", + KZ: "KAZ", + KE: "KEN", + KI: "KIR", + XK: "XKX", + KW: "KWT", + KG: "KGZ", + LA: "LAO", + LV: "LVA", + LB: "LBN", + LS: "LSO", + LR: "LBR", + LY: "LBY", + LI: "LIE", + LT: "LTU", + LU: "LUX", + MO: "MAC", + MK: "MKD", + MG: "MDG", + MW: "MWI", + MY: "MYS", + MV: "MDV", + ML: "MLI", + MT: "MLT", + MH: "USD", + MQ: "MTQ", + MR: "MRT", + MU: "MUS", + YT: "MYT", + MX: "MEX", + FM: "FSM", + MD: "MDA", + MC: "MCO", + MN: "MNG", + ME: "MNE", + MS: "MSR", + MA: "MAR", + MZ: "MOZ", + MM: "MMR", + NA: "NAM", + NR: "NRU", + NP: "NPL", + NL: "NLD", + AN: "ANG", + NC: "XPF", + NZ: "NZD", + NI: "NIC", + NE: "NER", + NG: "NGA", + NU: "NIU", + NF: "AUD", + KP: "KPW", + MP: "USD", + NO: "NOR", + OM: "OMN", + PK: "PAK", + PW: "PLW", + PS: "PSE", + PA: "PAN", + PG: "PGK", + PY: "PRY", + PE: "PER", + PH: "PHL", + PN: "PCN", + PL: "POL", + PT: "PRT", + PR: "USD", + QA: "QAT", + RE: "REU", + RO: "ROU", + RU: "RUS", + RW: "RWA", + BL: "EUR", + SH: "GPB", + KN: "XCD", + LC: "XCD", + MF: "EUR", + PM: "EUR", + VC: "XCD", + WS: "WSM", + SM: "EUR", + ST: "STN", + SA: "SAR", + SN: "SEN", + RS: "SRB", + CS: "RSD", + SC: "SYC", + SL: "SLL", + SG: "SGP", + SX: "ANG", + SK: "SVK", + SI: "SVN", + SB: "SBD", + SO: "SOM", + ZA: "ZAR", + GS: "GBP", + KR: "KRW", + SS: "SSP", + ES: "ESP", + LK: "LKR", + SD: "SDN", + SR: "SUR", + SJ: "NOK", + SZ: "SWZ", + SE: "SWE", + CH: "CHE", + SY: "SYR", + TW: "TWN", + TJ: "TJK", + TZ: "TZA", + TH: "THA", + TG: "TGO", + TK: "TKL", + TO: "TON", + TT: "TTD", + TA: "SHP", + TN: "TUN", + TR: "TUR", + TM: "TKM", + TC: "USD", + TV: "TUV", + UM: "USD", + VI: "USD", + UG: "UGA", + UA: "UKR", + AE: "AED", + GB: "GBP", + US: "USD", + UY: "URY", + UZ: "UZB", + VU: "VUT", + VA: "VAT", + VE: "VEN", + VN: "VNM", + YE: "YEM", + ZM: "ZMB", + ZW: "ZWE" +}; diff --git a/lib/ln.js b/lib/ln.js index 13bbd004..c4445b26 100644 --- a/lib/ln.js +++ b/lib/ln.js @@ -2,6 +2,6 @@ import config from "$config"; let ln; let { LightningClient } = (await import("clightning-client")).default; -ln = new LightningClient(config.lna.dir); +ln = new LightningClient(config.lightning); export default ln; diff --git a/lib/nostr.js b/lib/nostr.js index bc75cc96..ad089bb5 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -4,7 +4,6 @@ import { Relay, RelayPool } from "nostr"; import { broadcast } from "$lib/sockets"; import store from "$lib/store"; import redis from "$lib/redis"; -import proxy from "@fastify/http-proxy"; import { nada, wait } from "$lib/utils"; export let pool; diff --git a/lib/passport.js b/lib/passport.js index 6638fe73..b559b2f9 100644 --- a/lib/passport.js +++ b/lib/passport.js @@ -6,9 +6,10 @@ import config from "$config"; import fs from "fs"; import path from "path"; import { err } from "$lib/logging"; +import { g } from "$lib/redis"; app.register(fastifySecureSession, { - key: config.jwt + key: Buffer.from(config.jwt, "hex") }); app.register(fastifyPassport.initialize()); @@ -34,10 +35,10 @@ fastifyPassport.use( async (payload, next) => { try { let { uuid } = payload; - let user = await redis.get(`user:${uuid}`); + let user = await g(`user:${uuid}`); next(null, user); } catch (e) { - console.log("payload", payload) + console.log("payload", payload); if (!e.message.includes("locked")) { console.log(e); err("problem logging in", e); diff --git a/lib/rates.js b/lib/rates.js index 8970ca64..8064a117 100644 --- a/lib/rates.js +++ b/lib/rates.js @@ -1,23 +1,23 @@ import app from "$app"; -import axios from "axios"; +import got from "got"; import config from "$config"; import fs from "fs"; -// import binance from "./binance"; +import binance from "./binance"; import { broadcast } from "./sockets"; import store from "./store"; import { err, warn } from "$lib/logging"; -// let b = binance(); -// setInterval(() => { -// b.terminate(); -// b = binance(); -// }, 360000); +let b = binance(); +setInterval(() => { + b.terminate(); + b = binance(); +}, 360000); let fetchRates = async () => { let date, fx; try { - ({ date, fx } = JSON.parse(fs.readFileSync("fx"))); + ({ date, fx } = JSON.parse(fs.readFileSync("data/fx"))); } catch (e) { warn("Failed to read fx file"); } @@ -28,26 +28,22 @@ let fetchRates = async () => { if (date !== today) { try { - let { - data: { rates: fx } - } = await axios.get( + let { rates: fx } = await got( `https://api.exchangerate.host/latest?base=USD&v={(new Date()).toISOString().split('T')[0]}` - ); - fs.writeFileSync("fx", JSON.stringify({ date, fx })); + ).json(); + fs.writeFileSync("data/fx", JSON.stringify({ date, fx })); } catch (e) { try { - let { - data: { rates: fx } - } = await axios.get( + let { rates: fx } = await got( `http://data.fixer.io/api/latest?access_key=${config.fixer}` - ); + ).json(); let USD = fx["USD"]; Object.keys(fx).map(k => { fx[k] = parseFloat((fx[k] / fx["USD"]).toFixed(6)); }); - fs.writeFileSync("fx", JSON.stringify({ date, fx })); + fs.writeFileSync("data/fx", JSON.stringify({ date, fx })); } catch (e) { err("error fetching rates", e.message); } @@ -58,7 +54,7 @@ let fetchRates = async () => { setTimeout(fetchRates, 7200000); }; -// fetchRates(); +fetchRates(); app.get("/rate", async (req, res, next) => { res.send(store.last); diff --git a/lib/redis.js b/lib/redis.js index 3aed1bd0..6aec2af7 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -1,7 +1,7 @@ import { createClient } from "redis"; const redis = createClient({ - url: "redis://redis" + url: "redis://rd" }); redis.on("error", err => console.log("redis error", err)); @@ -9,3 +9,7 @@ redis.on("error", err => console.log("redis error", err)); await redis.connect(); export default redis; + +export let g = async k => JSON.parse(await redis.get(k)); +export let s = (k, v) => redis.set(k, JSON.stringify(v)); +export let rd = redis; diff --git a/lib/register.js b/lib/register.js index fd6f27ae..9f18c15e 100644 --- a/lib/register.js +++ b/lib/register.js @@ -4,10 +4,12 @@ import { authenticator } from "otplib"; import bcrypt from "bcrypt"; import { l } from "$lib/logging"; import { v4 } from "uuid"; -import redis from "$lib/redis"; +import { s, g } from "$lib/redis"; +import { got } from "got"; export default async (user, ip, requireChallenge) => { let { password, username } = user; + l("registering", username); if (!username) throw new Error("Username required"); @@ -15,26 +17,35 @@ export default async (user, ip, requireChallenge) => { let uuid = v4(); user.uuid = uuid; - let exists = await redis.get(`user:${username}`); + let exists = await g(`user:${username}`); if (exists) throw new Error(`Username ${username} taken`); if (password) { user.password = await bcrypt.hash(password, 1); } + console.log("BOO"); + if (!config.ipxapi || ip.startsWith("127") || ip.startsWith("192")) { user.currency = "CAD"; } else { - let { countryCode } = await got(`https://ipxapi.com/api/ip?ip=${ip}`, { - headers: { authorization: `Bearer ${config.ipxapi}` } - }).json(); + let countryCode = "US"; + try { + let r = await got(`https://ipxapi.com/api/ip?ip=${ip}`, { + headers: { authorization: `Bearer ${config.ipxapi}` } + }).json(); + if (r.success) ({ countryCode } = r); + } catch (e) { + console.log(e); + } - user.currency = countries[countryCode] || "USD"; + user.currency = countries[countryCode]; } user.currencies = [...new Set([user.currency, "CAD", "USD"])]; user.fiat = false; user.otpsecret = authenticator.generateSecret(); + user.balance = 0; let d = ip.split("."); ip = ((+d[0] * 256 + +d[1]) * 256 + +d[2]) * 256 + +d[3]; @@ -42,8 +53,14 @@ export default async (user, ip, requireChallenge) => { user.ip = ip; } - await redis.set(`user:${user.uuid}`, user); - await redis.set(`user:${user.username}`, user); + console.log("user uuid", user.uuid, user.username); + + try { + await s(`user:${user.uuid}`, user); + await s(`user:${user.username}`, user); + } catch (e) { + console.log("HUH", e); + } l("new user", user.username); return user; }; diff --git a/lib/sockets.js b/lib/sockets.js index ecf15dd7..0f343937 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -91,8 +91,10 @@ app.register(async function(app) { try { if (!token || token === "null" || !jwt.decode(token)) return ws.close(code, `bad token ${token}`); + let { uuid } = jwt.decode(token); if (!uuid) throw new Error("Invalid JWT token"); + console.log("UUID", uuid) let user = await getUser(uuid); if (!user) throw new Error("User not found"); @@ -109,15 +111,6 @@ app.register(async function(app) { store.sockets[uuid][id] = ws; names[id] = uuid; - let { haspin } = user; - - let keys = await user.getKeys(); - user = user.get({ plain: true }); - user.accounts = accounts; - user.keys = keys; - user.payments = payments; - user.haspin = haspin; - ws.user = user; ws.send( JSON.stringify({ type: "login", data: pick(user, ...whitelist) }) @@ -126,6 +119,7 @@ app.register(async function(app) { JSON.stringify({ type: "version", data: config.clientVersion }) ); } catch (e) { + console.log(e) err("caught socket error", e.message); if (e.message.includes("not found")) ws.send(JSON.stringify({ type: "logout" })); diff --git a/lib/utils.js b/lib/utils.js index 1513138b..559f8531 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,5 +1,5 @@ import store from "$lib/store"; -import redis from "$lib/redis"; +import { g } from "$lib/redis"; import config from "$config"; import ln from "$lib/ln"; @@ -19,7 +19,9 @@ export const requirePin = async ({ body, user }) => { }; export const getUser = async (uuid) => { - let user = await redis.get(`user:${uuid}`); + let user = g(`user:${uuid}`); + if (!user) throw new Error("user not found"); + user.haspin = !!user.pin; return user; diff --git a/lib/webhooks.js b/lib/webhooks.js index 4741932a..c071eccb 100644 --- a/lib/webhooks.js +++ b/lib/webhooks.js @@ -1,5 +1,5 @@ import config from "$config"; -import wretch from "./wretch"; +import got from "got"; import { l } from "$lib/logging"; export const callWebhook = async (invoice, payment) => { @@ -14,10 +14,9 @@ export const callWebhook = async (invoice, payment) => { let secret = config.webhooks[webhook]; l("calling webhook", webhook, amount, hash, address, text); - let res = await wretch() - .headers({ "x-webhook": secret }) - .url(webhook) - .post({ + let res = await got(webhook, { + headers: { "x-webhook": secret }, + json: { address, amount, asset, @@ -27,8 +26,8 @@ export const callWebhook = async (invoice, payment) => { received, text, webhook - }) - .res(); + } + }).res(); return res; } } catch (e) { diff --git a/lib/whitelist.js b/lib/whitelist.js index 47e9ed4e..523e9d37 100644 --- a/lib/whitelist.js +++ b/lib/whitelist.js @@ -1,7 +1,5 @@ export default [ - "account", - "account_id", - "accounts", + "balance", "address", "admin", "banner", diff --git a/package.json b/package.json index 631f61e9..408df40e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "private": true, "scripts": { - "start": "node index.js", + "start": "bun index.js", "dev": "nodemon index.js" }, "dependencies": { @@ -30,6 +30,7 @@ "postmark": "^3.0.11", "pump": "^3.0.0", "query-string": "^6.13.1", + "readable-stream-clone": "^0.0.7", "redis": "^4.3.1", "sharp": "^0.31.1", "uuid": "8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bbe6727..5c9f09dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,49 +1,19 @@ lockfileVersion: 5.4 specifiers: - '@fastify/http-proxy': ^8.4.0 '@fastify/multipart': ^7.1.2 '@fastify/passport': ^2.2.0 '@fastify/secure-session': ^5.2.0 '@fastify/static': ^6.5.0 '@fastify/websocket': ^7.0.1 - '@sequelize/core': 7.0.0-alpha.10 - async-mutex: ^0.2.4 - axios: ^0.21.1 bcrypt: ^5.0.0 - bech32: ^2.0.0 - bip32: ^2.0.5 - bip39: ^3.0.2 - bitcoinjs-lib: '6' - body-parser: ^1.19.0 - bolt11: ^1.3.2 - buffer-reverse: ^1.0.1 clightning-client: ^0.1.2 - coinselect: ^3.1.12 - compression: ^1.7.4 - cookie-parser: ^1.4.4 - cors: ^2.8.5 date-fns: ^2.21.3 - ecpair: ^2.0.1 - express: 4.17.1 - express-async-handler: ^1.1.4 - express-pino-logger: ^6.0.0 - express-session: ^1.16.2 fastify: ^4.5.3 file-type: ^18.0.0 got: ^12.5.3 - http-proxy-middleware: ^0.20.0 - imap: ^0.8.19 jsonwebtoken: ^8.5.1 - knex: ^2.3.0 - liquidjs-lib: 6.0.2-liquid.8 lnurl: ^0.24.2 - long: ^4.0.0 - mailgun-js: ^0.22.0 - mariadb: ^2.4.0 - mysql: ^2.18.1 - mysql2: ^2.2.5 - node-fetch: ^2.6.1 nodemon: ^2.0.7 nostr: ^0.2.7 otplib: ^12.0.1 @@ -58,58 +28,24 @@ specifiers: readable-stream-clone: ^0.0.7 redis: ^4.3.1 sharp: ^0.31.1 - sqlite3: ^5.0.11 - tiny-secp256k1: ^2.1.2 uuid: '8' web-push: ^3.4.4 - wretch: ^1.7.4 ws: ^7.4.6 - zeromq: 6.0.0-beta.6 dependencies: - '@fastify/http-proxy': 8.4.0 '@fastify/multipart': 7.2.0 '@fastify/passport': 2.2.0 '@fastify/secure-session': 5.2.0 '@fastify/static': 6.5.0 '@fastify/websocket': 7.1.0 - '@sequelize/core': 7.0.0-alpha.10_qwzqhxtweawy7vupwb2a75td54 - async-mutex: 0.2.6 - axios: 0.21.4 bcrypt: 5.1.0 - bech32: 2.0.0 - bip32: 2.0.6 - bip39: 3.0.4 - bitcoinjs-lib: 6.0.2 - body-parser: 1.20.1 - bolt11: 1.4.0 - buffer-reverse: 1.0.1 clightning-client: 0.1.4 - coinselect: 3.1.13 - compression: 1.7.4 - cookie-parser: 1.4.6 - cors: 2.8.5 date-fns: 2.29.3 - ecpair: 2.1.0 - express: 4.17.1 - express-async-handler: 1.2.0 - express-pino-logger: 6.0.0 - express-session: 1.17.3 fastify: 4.8.1 file-type: 18.0.0 got: 12.5.3 - http-proxy-middleware: 0.20.0 - imap: 0.8.19 jsonwebtoken: 8.5.1 - knex: 2.3.0_miwdxfk6g3dfg4gj6dnwby2onu - liquidjs-lib: 6.0.2-liquid.8 lnurl: 0.24.2 - long: 4.0.0 - mailgun-js: 0.22.0 - mariadb: 2.5.6 - mysql: 2.18.1 - mysql2: 2.3.3 - node-fetch: 2.6.7 nodemon: 2.0.20 nostr: 0.2.7 otplib: 12.0.1 @@ -123,13 +59,9 @@ dependencies: readable-stream-clone: 0.0.7 redis: 4.3.1 sharp: 0.31.1 - sqlite3: 5.1.2 - tiny-secp256k1: 2.2.1 uuid: 8.3.2 web-push: 3.5.0 - wretch: 1.7.10 ws: 7.5.9 - zeromq: 6.0.0-beta.6 devDependencies: prettier: 1.19.1 @@ -190,16 +122,6 @@ packages: fastify-plugin: 4.3.0 dev: false - /@fastify/http-proxy/8.4.0: - resolution: {integrity: sha512-H8nwsmawFtKKRE6uhh1BtF1gQi/l147SmLsDGxB0HdYTHzjXz6uSQO3lEVmY7unKMzbArRjdoJQkEGpScszdSw==} - dependencies: - '@fastify/reply-from': 8.3.1 - ws: 8.9.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: false - /@fastify/multipart/7.2.0: resolution: {integrity: sha512-LwfwbCLy30Be1pa5q7F8xCTygxJdEWkfkQhD4OWZ13+vMH4tP/6Bu3OkSTlFatxLAmbEl2UpHLf7CU7w7csRIw==} dependencies: @@ -220,18 +142,6 @@ packages: fastify-plugin: 4.3.0 dev: false - /@fastify/reply-from/8.3.1: - resolution: {integrity: sha512-fRByAvTMXuBuYIimcinukOB3YdmqtYPeoybXIBNY0aPVgetHkmCVffBo/M4pEOib9Pes8wuoYL4VawI65aHl4w==} - dependencies: - '@fastify/error': 3.0.0 - end-of-stream: 1.4.4 - fast-querystring: 1.0.0 - fastify-plugin: 4.3.0 - pump: 3.0.0 - tiny-lru: 10.0.1 - undici: 5.14.0 - dev: false - /@fastify/secure-session/5.2.0: resolution: {integrity: sha512-oRWKdHwWUzp4rsJ3GnR8w8doE+7vllWMtohBVp77bENkhm4/OvofaO6PJLJuQvKF7XJAKiGip29dX1RsllKT7g==} hasBin: true @@ -265,11 +175,6 @@ packages: - utf-8-validate dev: false - /@gar/promisify/1.1.3: - resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - dev: false - optional: true - /@mapbox/node-pre-gyp/1.0.10: resolution: {integrity: sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==} hasBin: true @@ -288,23 +193,6 @@ packages: - supports-color dev: false - /@npmcli/fs/1.1.1: - resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} - dependencies: - '@gar/promisify': 1.1.3 - semver: 7.3.8 - dev: false - optional: true - - /@npmcli/move-file/1.1.2: - resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} - engines: {node: '>=10'} - dependencies: - mkdirp: 1.0.4 - rimraf: 3.0.2 - dev: false - optional: true - /@otplib/core/12.0.1: resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==} dev: false @@ -387,61 +275,6 @@ packages: '@redis/client': 1.3.0 dev: false - /@sequelize/core/7.0.0-alpha.10_qwzqhxtweawy7vupwb2a75td54: - resolution: {integrity: sha512-uikpWaCiIaijsgoa9cH+3jCQl8r+FaQPhoE8AOJYvPwxOx52E1JZnRPMXNnlpbFy3Xu0qekrc5LBJq7IR8UB0Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - ibm_db: '*' - mariadb: '*' - mysql2: '*' - odbc: '*' - pg: '*' - pg-hstore: '*' - snowflake-sdk: '*' - sqlite3: '*' - tedious: '*' - peerDependenciesMeta: - ibm_db: - optional: true - mariadb: - optional: true - mysql2: - optional: true - odbc: - optional: true - pg: - optional: true - pg-hstore: - optional: true - snowflake-sdk: - optional: true - sqlite3: - optional: true - tedious: - optional: true - dependencies: - '@types/debug': 4.1.7 - debug: 4.3.4 - dottie: 2.0.2 - inflection: 1.13.4 - lodash: 4.17.21 - mariadb: 2.5.6 - moment: 2.29.4 - moment-timezone: 0.5.37 - mysql2: 2.3.3 - pg-connection-string: 2.5.0 - retry-as-promised: 5.0.0 - semver: 7.3.8 - sequelize-pool: 7.1.0 - sqlite3: 5.1.2 - toposort-class: 1.0.1 - uuid: 8.3.2 - validator: 13.7.0 - wkx: 0.5.0 - transitivePeerDependencies: - - supports-color - dev: false - /@sindresorhus/is/5.3.0: resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==} engines: {node: '>=14.16'} @@ -458,76 +291,20 @@ packages: resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} dev: false - /@tootallnate/once/1.1.2: - resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} - engines: {node: '>= 6'} - dev: false - optional: true - /@types/bn.js/4.11.6: resolution: {integrity: sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==} dependencies: '@types/node': 18.8.5 dev: false - /@types/debug/4.1.7: - resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} - dependencies: - '@types/ms': 0.7.31 - dev: false - - /@types/geojson/7946.0.10: - resolution: {integrity: sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==} - dev: false - /@types/http-cache-semantics/4.0.1: resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} dev: false - /@types/ms/0.7.31: - resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} - dev: false - - /@types/node/10.12.18: - resolution: {integrity: sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==} - dev: false - - /@types/node/11.11.6: - resolution: {integrity: sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==} - dev: false - - /@types/node/13.13.52: - resolution: {integrity: sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==} - dev: false - - /@types/node/17.0.45: - resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - dev: false - /@types/node/18.8.5: resolution: {integrity: sha512-Bq7G3AErwe5A/Zki5fdD3O6+0zDChhg671NfPjtIcbtzDNZTv4NPKMRFr7gtYPG7y+B8uTiNK4Ngd9T0FTar6Q==} dev: false - /@types/randombytes/2.0.0: - resolution: {integrity: sha512-bz8PhAVlwN72vqefzxa14DKNT8jK/mV66CSjwdVQM/k3Th3EPKfUtdMniwZgMedQTFuywAsfjnZsg+pEnltaMA==} - dependencies: - '@types/node': 18.8.5 - dev: false - - /@types/wif/2.0.2: - resolution: {integrity: sha512-IiIuBeJzlh4LWJ7kVTrX0nwB60OG0vvGTaWC/SgSbVFw7uYUTF6gEuvDZ1goWkeirekJDD58Y8g7NljQh2fNkA==} - dependencies: - '@types/node': 18.8.5 - dev: false - - /@vulpemventures/secp256k1-zkp/2.1.0: - resolution: {integrity: sha512-86EGAkPqEwy5KKeC8Ow8+fMhSiFtorvIQQ9yhTwwHqT0UdUVOW/Fu2ZDoooQK5KAQbyISBB+vf0jK3eYH+Dlqw==} - engines: {node: '>=12.0.0'} - dependencies: - '@types/node': 13.13.52 - long: 4.0.0 - dev: false - /abbrev/1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} dev: false @@ -551,20 +328,6 @@ packages: negotiator: 0.6.3 dev: false - /agent-base/4.2.1: - resolution: {integrity: sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==} - engines: {node: '>= 4.0.0'} - dependencies: - es6-promisify: 5.0.0 - dev: false - - /agent-base/4.3.0: - resolution: {integrity: sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==} - engines: {node: '>= 4.0.0'} - dependencies: - es6-promisify: 5.0.0 - dev: false - /agent-base/6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -574,27 +337,6 @@ packages: - supports-color dev: false - /agentkeepalive/4.2.1: - resolution: {integrity: sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==} - engines: {node: '>= 8.0.0'} - dependencies: - debug: 4.3.4 - depd: 1.1.2 - humanize-ms: 1.2.1 - transitivePeerDependencies: - - supports-color - dev: false - optional: true - - /aggregate-error/3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} - engines: {node: '>=8'} - dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 - dev: false - optional: true - /ajv-formats/2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependenciesMeta: @@ -649,15 +391,6 @@ packages: readable-stream: 3.6.0 dev: false - /are-we-there-yet/3.0.1: - resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - delegates: 1.0.0 - readable-stream: 3.6.0 - dev: false - optional: true - /args/5.0.3: resolution: {integrity: sha512-h6k/zfFgusnv3i5TU08KQkVKuCPBtL/PWQbWkHUxvJrZ2nAyeaUupneemcrgn1xmqxPQsPIzwkUhOpoqPDRZuA==} engines: {node: '>= 6.0.0'} @@ -681,25 +414,6 @@ packages: safer-buffer: 2.1.2 dev: false - /ast-types/0.14.2: - resolution: {integrity: sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==} - engines: {node: '>=4'} - dependencies: - tslib: 2.4.0 - dev: false - - /async-mutex/0.2.6: - resolution: {integrity: sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw==} - dependencies: - tslib: 2.4.0 - dev: false - - /async/2.6.4: - resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} - dependencies: - lodash: 4.17.21 - dev: false - /async/3.2.3: resolution: {integrity: sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==} dev: false @@ -708,10 +422,6 @@ packages: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} dev: false - /asynckit/0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: false - /atomic-sleep/1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -727,14 +437,6 @@ packages: - supports-color dev: false - /axios/0.21.4: - resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} - dependencies: - follow-redirects: 1.15.2 - transitivePeerDependencies: - - debug - dev: false - /axios/0.25.0: resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==} dependencies: @@ -777,10 +479,6 @@ packages: resolution: {integrity: sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==} dev: false - /bignumber.js/9.0.0: - resolution: {integrity: sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==} - dev: false - /bignumber.js/9.0.2: resolution: {integrity: sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==} dev: false @@ -790,54 +488,11 @@ packages: engines: {node: '>=8'} dev: false - /bindings/1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - dependencies: - file-uri-to-path: 1.0.0 - dev: false - - /bip174-liquid/1.0.3: - resolution: {integrity: sha512-e69sC0Cq2tBJuhG2+wieXv40DN13YBR/wbIjZp4Mqwpar5vQm8Ldqijdd6N33XG7LtfvQi/zKm5fSzdPY/9mgw==} - engines: {node: '>=8.0.0'} - dev: false - /bip174/2.1.0: resolution: {integrity: sha512-lkc0XyiX9E9KiVAS1ZiOqK1xfiwvf4FXDDdkDq5crcDzOq+xGytY+14qCsqz7kCiy8rpN1CRNfacRhf9G3JNSA==} engines: {node: '>=8.0.0'} dev: false - /bip32/2.0.6: - resolution: {integrity: sha512-HpV5OMLLGTjSVblmrtYRfFFKuQB+GArM0+XP8HGWfJ5vxYBqo+DesvJwOdC2WJ3bCkZShGf0QIfoIpeomVzVdA==} - engines: {node: '>=6.0.0'} - dependencies: - '@types/node': 10.12.18 - bs58check: 2.1.2 - create-hash: 1.2.0 - create-hmac: 1.1.7 - tiny-secp256k1: 1.1.6 - typeforce: 1.18.0 - wif: 2.0.6 - dev: false - - /bip39/3.0.4: - resolution: {integrity: sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==} - dependencies: - '@types/node': 11.11.6 - create-hash: 1.2.0 - pbkdf2: 3.1.2 - randombytes: 2.1.0 - dev: false - - /bip66/1.1.5: - resolution: {integrity: sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==} - dependencies: - safe-buffer: 5.2.1 - dev: false - - /bitcoin-ops/1.4.1: - resolution: {integrity: sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==} - dev: false - /bitcoinjs-lib/6.0.2: resolution: {integrity: sha512-I994pGt9cL5s5OA6mkv1e8IuYcsKN2ORXnWbkqAXLNGvEnOHBhKBSvCjFl7YC2uVoJnfr/iwq7JMrq575SYO5w==} engines: {node: '>=8.0.0'} @@ -860,35 +515,10 @@ packages: readable-stream: 3.6.0 dev: false - /blech32/1.1.2: - resolution: {integrity: sha512-C5qxzoF9KyX88X8Zz18cZ6BOeL0n5/Eg/cDot1frntkArRMwg1djNim5wA6QFWwu0lJ1LN8iiRMN4Lp2kZzdfA==} - engines: {node: '>=10'} - dependencies: - long: 4.0.0 - dev: false - /bn.js/4.12.0: resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} dev: false - /body-parser/1.19.0: - resolution: {integrity: sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==} - engines: {node: '>= 0.8'} - dependencies: - bytes: 3.1.0 - content-type: 1.0.4 - debug: 2.6.9 - depd: 1.1.2 - http-errors: 1.7.2 - iconv-lite: 0.4.24 - on-finished: 2.3.0 - qs: 6.7.0 - raw-body: 2.4.0 - type-is: 1.6.18 - transitivePeerDependencies: - - supports-color - dev: false - /body-parser/1.20.0: resolution: {integrity: sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -909,26 +539,6 @@ packages: - supports-color dev: false - /body-parser/1.20.1: - resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dependencies: - bytes: 3.1.2 - content-type: 1.0.4 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.11.0 - raw-body: 2.5.1 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - dev: false - /bolt11/1.3.4: resolution: {integrity: sha512-x4lHDv0oid13lGlZU7cl/5gx9nRwjB2vgK/uB3c50802Wh+9WjWQMwzD2PCETHylUijx2iBAqUQYbx3ZgwF06Q==} dependencies: @@ -942,19 +552,6 @@ packages: secp256k1: 4.0.3 dev: false - /bolt11/1.4.0: - resolution: {integrity: sha512-TeGIOpFNrvNA1XGvwBr+lYmzKfoJeyMRt7nsGmIci+5gbmSz7s46jwKLNYXH3IoxV7fjmYUEUu2NM19n9w0msg==} - dependencies: - '@types/bn.js': 4.11.6 - bech32: 1.1.4 - bitcoinjs-lib: 6.0.2 - bn.js: 4.12.0 - create-hash: 1.2.0 - lodash: 4.17.21 - safe-buffer: 5.2.1 - secp256k1: 4.0.3 - dev: false - /brace-expansion/1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -997,10 +594,6 @@ packages: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} dev: false - /buffer-reverse/1.0.1: - resolution: {integrity: sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg==} - dev: false - /buffer/5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -1015,55 +608,11 @@ packages: ieee754: 1.2.1 dev: false - /busboy/1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - dependencies: - streamsearch: 1.1.0 - dev: false - - /bytes/3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} - engines: {node: '>= 0.8'} - dev: false - - /bytes/3.1.0: - resolution: {integrity: sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==} - engines: {node: '>= 0.8'} - dev: false - /bytes/3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} dev: false - /cacache/15.3.0: - resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} - engines: {node: '>= 10'} - dependencies: - '@npmcli/fs': 1.1.1 - '@npmcli/move-file': 1.1.2 - chownr: 2.0.0 - fs-minipass: 2.1.0 - glob: 7.2.3 - infer-owner: 1.0.4 - lru-cache: 6.0.0 - minipass: 3.3.4 - minipass-collect: 1.0.2 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - mkdirp: 1.0.4 - p-map: 4.0.0 - promise-inflight: 1.0.1 - rimraf: 3.0.2 - ssri: 8.0.1 - tar: 6.1.11 - unique-filename: 1.1.1 - transitivePeerDependencies: - - bluebird - dev: false - optional: true - /cacheable-lookup/7.0.0: resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} engines: {node: '>=14.16'} @@ -1134,12 +683,6 @@ packages: safe-buffer: 5.2.1 dev: false - /clean-stack/2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} - dev: false - optional: true - /clightning-client/0.1.4: resolution: {integrity: sha512-M5UclkZcKvh15CmZOpbM0p6Dyua3ixRlgp7EP5IhL9jYWW8AL7qpROWLQFqSW2ANy48W1vGlw9igSyWV2A/iqw==} dependencies: @@ -1154,15 +697,6 @@ packages: engines: {node: '>=0.10.0'} dev: false - /co/4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - dev: false - - /coinselect/3.1.13: - resolution: {integrity: sha512-iJOrKH/7N9gX0jRkxgOHuGjvzvoxUMSeylDhH1sHn+CjLjdin5R0Hz2WEBu/jrZV5OrHcm+6DMzxwu9zb5mSZg==} - dev: false - /color-convert/1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -1208,45 +742,11 @@ packages: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} dev: false - /combined-stream/1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - dependencies: - delayed-stream: 1.0.0 - dev: false - /commander/9.3.0: resolution: {integrity: sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==} engines: {node: ^12.20.0 || >=14} dev: false - /commander/9.4.1: - resolution: {integrity: sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==} - engines: {node: ^12.20.0 || >=14} - dev: false - - /compressible/2.0.18: - resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} - engines: {node: '>= 0.6'} - dependencies: - mime-db: 1.52.0 - dev: false - - /compression/1.7.4: - resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} - engines: {node: '>= 0.8.0'} - dependencies: - accepts: 1.3.8 - bytes: 3.0.0 - compressible: 2.0.18 - debug: 2.6.9 - on-headers: 1.0.2 - safe-buffer: 5.1.2 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - dev: false - /concat-map/0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: false @@ -1255,13 +755,6 @@ packages: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} dev: false - /content-disposition/0.5.3: - resolution: {integrity: sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==} - engines: {node: '>= 0.6'} - dependencies: - safe-buffer: 5.1.2 - dev: false - /content-disposition/0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -1274,50 +767,15 @@ packages: engines: {node: '>= 0.6'} dev: false - /cookie-parser/1.4.6: - resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==} - engines: {node: '>= 0.8.0'} - dependencies: - cookie: 0.4.1 - cookie-signature: 1.0.6 - dev: false - /cookie-signature/1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: false - /cookie/0.4.0: - resolution: {integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==} + /cookie/0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} dev: false - /cookie/0.4.1: - resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} - engines: {node: '>= 0.6'} - dev: false - - /cookie/0.4.2: - resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} - engines: {node: '>= 0.6'} - dev: false - - /cookie/0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} - engines: {node: '>= 0.6'} - dev: false - - /core-util-is/1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: false - - /cors/2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - dev: false - /create-hash/1.2.0: resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} dependencies: @@ -1328,21 +786,6 @@ packages: sha.js: 2.4.11 dev: false - /create-hmac/1.1.7: - resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} - dependencies: - cipher-base: 1.0.4 - create-hash: 1.2.0 - inherits: 2.0.4 - ripemd160: 2.0.2 - safe-buffer: 5.2.1 - sha.js: 2.4.11 - dev: false - - /data-uri-to-buffer/1.2.0: - resolution: {integrity: sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==} - dev: false - /date-fns/2.29.3: resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==} engines: {node: '>=0.11'} @@ -1363,28 +806,6 @@ packages: ms: 2.0.0 dev: false - /debug/3.1.0: - resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.0.0 - dev: false - - /debug/3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - dev: false - /debug/3.2.7_supports-color@5.5.0: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -1426,51 +847,20 @@ packages: engines: {node: '>=4.0.0'} dev: false - /deep-is/0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: false - /defer-to-connect/2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} dev: false - /degenerator/1.0.4: - resolution: {integrity: sha512-EMAC+riLSC64jKfOs1jp8J7M4ZXstUUwTdwFBEv6HOzL/Ae+eAzMKEK0nJnpof2fnw9IOjmE6u6qXFejVyk8AA==} - dependencies: - ast-types: 0.14.2 - escodegen: 1.14.3 - esprima: 3.1.3 - dev: false - - /delayed-stream/1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - dev: false - /delegates/1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false - /denque/2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} - dev: false - - /depd/1.1.2: - resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} - engines: {node: '>= 0.6'} - dev: false - /depd/2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} dev: false - /destroy/1.0.4: - resolution: {integrity: sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==} - dev: false - /destroy/1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -1481,10 +871,6 @@ packages: engines: {node: '>=8'} dev: false - /dottie/2.0.2: - resolution: {integrity: sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==} - dev: false - /duplexify/4.1.2: resolution: {integrity: sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==} dependencies: @@ -1500,15 +886,6 @@ packages: safe-buffer: 5.2.1 dev: false - /ecpair/2.1.0: - resolution: {integrity: sha512-cL/mh3MtJutFOvFc27GPZE2pWL3a3k4YvzUWEOvilnfZVlH3Jwgx/7d6tlD7/75tNk8TG2m+7Kgtz0SI1tWcqw==} - engines: {node: '>=8.0.0'} - dependencies: - randombytes: 2.1.0 - typeforce: 1.18.0 - wif: 2.0.6 - dev: false - /ee-first/1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: false @@ -1534,52 +911,18 @@ packages: engines: {node: '>= 0.8'} dev: false - /encoding/0.1.13: - resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} - requiresBuild: true - dependencies: - iconv-lite: 0.6.3 - dev: false - optional: true - /end-of-stream/1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: once: 1.4.0 dev: false - /env-paths/2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - dev: false - optional: true - - /err-code/2.0.3: - resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - dev: false - optional: true - /error/7.2.1: resolution: {integrity: sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==} dependencies: string-template: 0.2.1 dev: false - /es6-promise/4.2.8: - resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} - dev: false - - /es6-promisify/5.0.0: - resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} - dependencies: - es6-promise: 4.2.8 - dev: false - - /escalade/3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - dev: false - /escape-html/1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} dev: false @@ -1589,46 +932,6 @@ packages: engines: {node: '>=0.8.0'} dev: false - /escodegen/1.14.3: - resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} - engines: {node: '>=4.0'} - hasBin: true - dependencies: - esprima: 4.0.1 - estraverse: 4.3.0 - esutils: 2.0.3 - optionator: 0.8.3 - optionalDependencies: - source-map: 0.6.1 - dev: false - - /esm/3.2.25: - resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} - engines: {node: '>=6'} - dev: false - - /esprima/3.1.3: - resolution: {integrity: sha512-AWwVMNxwhN8+NIPQzAQZCm7RkLC4RbM3B1OobMuyp3i+w73X57KCKaVIxaRZb+DYCojq7rspo+fmuQfAboyhFg==} - engines: {node: '>=4'} - hasBin: true - dev: false - - /esprima/4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: false - - /estraverse/4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - dev: false - - /esutils/2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: false - /etag/1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -1639,10 +942,6 @@ packages: engines: {node: '>=6'} dev: false - /eventemitter3/4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - dev: false - /events/3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -1653,10 +952,6 @@ packages: engines: {node: '>=6'} dev: false - /express-async-handler/1.2.0: - resolution: {integrity: sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==} - dev: false - /express-handlebars/6.0.3: resolution: {integrity: sha512-cG6/CeriAhAYKC9+VivXtj41+HNDB6xnigwzcXv/m9BBtijR2Yg5I5VWsK6nPc1+E06FIS23nGfr2COUv8sG3A==} engines: {node: '>=v12.22.9'} @@ -1666,66 +961,6 @@ packages: handlebars: 4.7.7 dev: false - /express-pino-logger/6.0.0: - resolution: {integrity: sha512-YjBnalqgsNylRnWEpQGf8YzBP54stpoqX/o+SnpGr04OB7dRIQlsC1qvutFOyRjhLhXIWCe43pYJcjp9zM1Ccg==} - dependencies: - pino-http: 5.8.0 - dev: false - - /express-session/1.17.3: - resolution: {integrity: sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==} - engines: {node: '>= 0.8.0'} - dependencies: - cookie: 0.4.2 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - on-headers: 1.0.2 - parseurl: 1.3.3 - safe-buffer: 5.2.1 - uid-safe: 2.1.5 - transitivePeerDependencies: - - supports-color - dev: false - - /express/4.17.1: - resolution: {integrity: sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==} - engines: {node: '>= 0.10.0'} - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.19.0 - content-disposition: 0.5.3 - content-type: 1.0.4 - cookie: 0.4.0 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 1.1.2 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.1.2 - fresh: 0.5.2 - merge-descriptors: 1.0.1 - methods: 1.1.2 - on-finished: 2.3.0 - parseurl: 1.3.3 - path-to-regexp: 0.1.7 - proxy-addr: 2.0.7 - qs: 6.7.0 - range-parser: 1.2.1 - safe-buffer: 5.1.2 - send: 0.17.1 - serve-static: 1.14.1 - setprototypeof: 1.1.1 - statuses: 1.5.0 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - dev: false - /express/4.18.1: resolution: {integrity: sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==} engines: {node: '>= 0.10.0'} @@ -1765,10 +1000,6 @@ packages: - supports-color dev: false - /extend/3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - dev: false - /fast-decode-uri-component/1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} dev: false @@ -1788,10 +1019,6 @@ packages: rfdc: 1.3.0 dev: false - /fast-levenshtein/2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: false - /fast-querystring/1.0.0: resolution: {integrity: sha512-3LQi62IhQoDlmt4ULCYmh17vRO2EtS7hTSsG4WwoKWgV7GLMKBOecEh+aiavASnLx8I2y89OD33AGLo0ccRhzA==} dependencies: @@ -1811,12 +1038,6 @@ packages: resolution: {integrity: sha512-qKRta6N7BWEFVlyonVY/V+BMLgFqktCUV0QjT259ekAIlbVrMaFnFLxJ4s/JPl4tou56S1BzPufI60bLe29fHA==} dev: false - /fast-url-parser/1.1.3: - resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==} - dependencies: - punycode: 1.4.1 - dev: false - /fastify-plugin/3.0.1: resolution: {integrity: sha512-qKcDXmuZadJqdTm6vlCqioEbyewF60b/0LOFCcYN1B6BIZGlYJumWWOYs70SFYLDAH4YqdE1cxH/RKMG7rFxgA==} dev: false @@ -1861,10 +1082,6 @@ packages: token-types: 5.0.1 dev: false - /file-uri-to-path/1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - dev: false - /fill-range/7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -1877,21 +1094,6 @@ packages: engines: {node: '>=0.10.0'} dev: false - /finalhandler/1.1.2: - resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} - engines: {node: '>= 0.8'} - dependencies: - debug: 2.6.9 - encodeurl: 1.0.2 - escape-html: 1.0.3 - on-finished: 2.3.0 - parseurl: 1.3.3 - statuses: 1.5.0 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - dev: false - /finalhandler/1.2.0: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} engines: {node: '>= 0.8'} @@ -1916,10 +1118,6 @@ packages: safe-regex2: 2.0.0 dev: false - /flatstr/1.0.12: - resolution: {integrity: sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==} - dev: false - /follow-redirects/1.15.2: resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} @@ -1935,15 +1133,6 @@ packages: engines: {node: '>= 14.17'} dev: false - /form-data/2.5.1: - resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} - engines: {node: '>= 0.12'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - dev: false - /forwarded/0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1977,14 +1166,6 @@ packages: dev: false optional: true - /ftp/0.3.10: - resolution: {integrity: sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==} - engines: {node: '>=0.8.0'} - dependencies: - readable-stream: 1.1.14 - xregexp: 2.0.0 - dev: false - /function-bind/1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} dev: false @@ -2004,27 +1185,6 @@ packages: wide-align: 1.1.5 dev: false - /gauge/4.0.4: - resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - aproba: 2.0.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - signal-exit: 3.0.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wide-align: 1.1.5 - dev: false - optional: true - - /generate-function/2.3.1: - resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} - dependencies: - is-property: 1.0.2 - dev: false - /generic-pool/3.8.2: resolution: {integrity: sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==} engines: {node: '>= 4'} @@ -2038,33 +1198,11 @@ packages: has-symbols: 1.0.3 dev: false - /get-package-type/0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - dev: false - /get-stream/6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} dev: false - /get-uri/2.0.4: - resolution: {integrity: sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==} - dependencies: - data-uri-to-buffer: 1.2.0 - debug: 2.6.9 - extend: 3.0.2 - file-uri-to-path: 1.0.0 - ftp: 0.3.10 - readable-stream: 2.3.7 - transitivePeerDependencies: - - supports-color - dev: false - - /getopts/2.3.0: - resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} - dev: false - /github-from-package/0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} dev: false @@ -2186,28 +1324,6 @@ packages: resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==} dev: false - /http-errors/1.7.2: - resolution: {integrity: sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==} - engines: {node: '>= 0.6'} - dependencies: - depd: 1.1.2 - inherits: 2.0.3 - setprototypeof: 1.1.1 - statuses: 1.5.0 - toidentifier: 1.0.0 - dev: false - - /http-errors/1.7.3: - resolution: {integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==} - engines: {node: '>= 0.6'} - dependencies: - depd: 1.1.2 - inherits: 2.0.4 - setprototypeof: 1.1.1 - statuses: 1.5.0 - toidentifier: 1.0.0 - dev: false - /http-errors/2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -2219,51 +1335,6 @@ packages: toidentifier: 1.0.1 dev: false - /http-proxy-agent/2.1.0: - resolution: {integrity: sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==} - engines: {node: '>= 4.5.0'} - dependencies: - agent-base: 4.3.0 - debug: 3.1.0 - transitivePeerDependencies: - - supports-color - dev: false - - /http-proxy-agent/4.0.1: - resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} - engines: {node: '>= 6'} - dependencies: - '@tootallnate/once': 1.1.2 - agent-base: 6.0.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: false - optional: true - - /http-proxy-middleware/0.20.0: - resolution: {integrity: sha512-dNJAk71nEJhPiAczQH9hGvE/MT9kEs+zn2Dh+Hi94PGZe1GluQirC7mw5rdREUtWx6qGS1Gu0bZd4qEAg+REgw==} - engines: {node: '>=8.0.0'} - dependencies: - http-proxy: 1.18.1 - is-glob: 4.0.3 - lodash: 4.17.21 - micromatch: 4.0.5 - transitivePeerDependencies: - - debug - dev: false - - /http-proxy/1.18.1: - resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} - engines: {node: '>=8.0.0'} - dependencies: - eventemitter3: 4.0.7 - follow-redirects: 1.15.2 - requires-port: 1.0.0 - transitivePeerDependencies: - - debug - dev: false - /http2-wrapper/2.2.0: resolution: {integrity: sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==} engines: {node: '>=10.19.0'} @@ -2279,16 +1350,6 @@ packages: urlsafe-base64: 1.0.0 dev: false - /https-proxy-agent/3.0.1: - resolution: {integrity: sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==} - engines: {node: '>= 4.5.0'} - dependencies: - agent-base: 4.3.0 - debug: 3.2.7 - transitivePeerDependencies: - - supports-color - dev: false - /https-proxy-agent/5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -2299,13 +1360,6 @@ packages: - supports-color dev: false - /humanize-ms/1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - dependencies: - ms: 2.1.3 - dev: false - optional: true - /iconv-lite/0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2313,13 +1367,6 @@ packages: safer-buffer: 2.1.2 dev: false - /iconv-lite/0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: false - /ieee754/1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: false @@ -2328,76 +1375,19 @@ packages: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} dev: false - /imap/0.8.19: - resolution: {integrity: sha512-z5DxEA1uRnZG73UcPA4ES5NSCGnPuuouUx43OPX7KZx1yzq3N8/vx2mtXEShT5inxB3pRgnfG1hijfu7XN2YMw==} - engines: {node: '>=0.8.0'} - dependencies: - readable-stream: 1.1.14 - utf7: 1.0.2 - dev: false - - /imurmurhash/0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: false - optional: true - - /indent-string/4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - dev: false - optional: true - - /infer-owner/1.0.4: - resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} - dev: false - optional: true - - /inflection/1.12.0: - resolution: {integrity: sha512-lRy4DxuIFWXlJU7ed8UiTJOSTqStqYdEb4CEbtXfNbkdj3nH1L+reUWiE10VWcJS2yR7tge8Z74pJjtBjNwj0w==} - engines: {'0': node >= 0.4.0} - dev: false - - /inflection/1.13.4: - resolution: {integrity: sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==} - engines: {'0': node >= 0.4.0} - dev: false - - /inflection/1.3.8: - resolution: {integrity: sha512-xRvG6XhAkbneGO5BXP0uKyGkzmZ2bBbrFkx4ZVNx2TmsECbiq/pJapbbx/NECh+E85IfZwW5+IeVNJfkQgavag==} - engines: {'0': node >= 0.4.0} - dev: false - /inflight/1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: once: 1.4.0 - wrappy: 1.0.2 - dev: false - - /inherits/2.0.3: - resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} - dev: false - - /inherits/2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: false - - /ini/1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - dev: false - - /interpret/2.2.0: - resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} - engines: {node: '>= 0.10'} + wrappy: 1.0.2 dev: false - /ip/1.1.5: - resolution: {integrity: sha512-rBtCAQAJm8A110nbwn6YdveUnuZH3WrC36IwkRXxDnq53JvXA2NVQvB7IHyKomxK1MJ4VDNw3UtFDdXQ+AvLYA==} + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: false - /ip/1.1.8: - resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} + /ini/1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} dev: false /ip/2.0.0: @@ -2420,12 +1410,6 @@ packages: binary-extensions: 2.2.0 dev: false - /is-core-module/2.10.0: - resolution: {integrity: sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==} - dependencies: - has: 1.0.3 - dev: false - /is-extglob/2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2443,38 +1427,11 @@ packages: is-extglob: 2.1.1 dev: false - /is-lambda/1.0.1: - resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} - dev: false - optional: true - /is-number/7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} dev: false - /is-property/1.0.2: - resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} - dev: false - - /is-stream/1.1.0: - resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} - engines: {node: '>=0.10.0'} - dev: false - - /isarray/0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - dev: false - - /isarray/1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - dev: false - - /isexe/2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: false - optional: true - /joycon/3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -2540,68 +1497,11 @@ packages: json-buffer: 3.0.1 dev: false - /knex/2.3.0_miwdxfk6g3dfg4gj6dnwby2onu: - resolution: {integrity: sha512-WMizPaq9wRMkfnwKXKXgBZeZFOSHGdtoSz5SaLAVNs3WRDfawt9O89T4XyH52PETxjV8/kRk0Yf+8WBEP/zbYw==} - engines: {node: '>=12'} - hasBin: true - peerDependencies: - better-sqlite3: '*' - mysql: '*' - mysql2: '*' - pg: '*' - pg-native: '*' - sqlite3: '*' - tedious: '*' - peerDependenciesMeta: - better-sqlite3: - optional: true - mysql: - optional: true - mysql2: - optional: true - pg: - optional: true - pg-native: - optional: true - sqlite3: - optional: true - tedious: - optional: true - dependencies: - colorette: 2.0.19 - commander: 9.4.1 - debug: 4.3.4 - escalade: 3.1.1 - esm: 3.2.25 - get-package-type: 0.1.0 - getopts: 2.3.0 - interpret: 2.2.0 - lodash: 4.17.21 - mysql: 2.18.1 - mysql2: 2.3.3 - pg-connection-string: 2.5.0 - rechoir: 0.8.0 - resolve-from: 5.0.0 - sqlite3: 5.1.2 - tarn: 3.0.2 - tildify: 2.0.0 - transitivePeerDependencies: - - supports-color - dev: false - /leven/2.1.0: resolution: {integrity: sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==} engines: {node: '>=0.10.0'} dev: false - /levn/0.3.0: - resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.1.2 - type-check: 0.3.2 - dev: false - /light-my-request/5.6.1: resolution: {integrity: sha512-sbJnC1UBRivi9L1kICr3CESb82pNiPNB3TvtdIrZZqW0Qh8uDXvoywMmWKZlihDcmw952CMICCzM+54LDf+E+g==} dependencies: @@ -2622,29 +1522,6 @@ packages: - supports-color dev: false - /liquidjs-lib/6.0.2-liquid.8: - resolution: {integrity: sha512-KCtO/lXfKyZUJeZ1Mok3FQ5/viNLmKEEhGEC3iySnBakIII2rd1ijn3np16/3LzBiQhQ+tFm4Q1NYEG3YhWBTg==} - engines: {node: '>=8.0.0'} - dependencies: - '@types/randombytes': 2.0.0 - '@types/wif': 2.0.2 - '@vulpemventures/secp256k1-zkp': 2.1.0 - axios: 0.21.4 - bech32: 2.0.0 - bip174-liquid: 1.0.3 - bip66: 1.1.5 - bitcoin-ops: 1.4.1 - blech32: 1.1.2 - bs58check: 2.1.2 - create-hash: 1.2.0 - ecpair: 2.1.0 - typeforce: 1.18.0 - varuint-bitcoin: 1.1.2 - wif: 2.0.6 - transitivePeerDependencies: - - debug - dev: false - /lnurl-offline/1.1.1: resolution: {integrity: sha512-iyryv7hLS/MdDzNg6wzVgRZyA8rwjyZpn61KXHlLwr9YoDc8Vf+YXvH273fNUD+5WQC6rh+pxZN0lrNvvUnVQQ==} dev: false @@ -2699,32 +1576,11 @@ packages: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: false - /long/4.0.0: - resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} - dev: false - - /long/5.2.0: - resolution: {integrity: sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w==} - dev: false - /lowercase-keys/3.0.0: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: false - /lru-cache/4.1.5: - resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} - dependencies: - pseudomap: 1.0.2 - yallist: 2.1.2 - dev: false - - /lru-cache/5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - dependencies: - yallist: 3.1.1 - dev: false - /lru-cache/6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -2732,24 +1588,6 @@ packages: yallist: 4.0.0 dev: false - /mailgun-js/0.22.0: - resolution: {integrity: sha512-a2alg5nuTZA9Psa1pSEIEsbxr1Zrmqx4VkgGCQ30xVh0kIH7Bu57AYILo+0v8QLSdXtCyLaS+KVmdCrQo0uWFA==} - engines: {node: '>=6.0.0'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - dependencies: - async: 2.6.4 - debug: 4.3.4 - form-data: 2.5.1 - inflection: 1.12.0 - is-stream: 1.1.0 - path-proxy: 1.0.0 - promisify-call: 2.0.4 - proxy-agent: 3.1.1 - tsscmp: 1.0.6 - transitivePeerDependencies: - - supports-color - dev: false - /make-dir/3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -2757,45 +1595,6 @@ packages: semver: 6.3.0 dev: false - /make-fetch-happen/9.1.0: - resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} - engines: {node: '>= 10'} - dependencies: - agentkeepalive: 4.2.1 - cacache: 15.3.0 - http-cache-semantics: 4.1.0 - http-proxy-agent: 4.0.1 - https-proxy-agent: 5.0.1 - is-lambda: 1.0.1 - lru-cache: 6.0.0 - minipass: 3.3.4 - minipass-collect: 1.0.2 - minipass-fetch: 1.4.1 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - negotiator: 0.6.3 - promise-retry: 2.0.1 - socks-proxy-agent: 6.2.1 - ssri: 8.0.1 - transitivePeerDependencies: - - bluebird - - supports-color - dev: false - optional: true - - /mariadb/2.5.6: - resolution: {integrity: sha512-zBx7loYY5GzLl8Y6AKxGXfY9DUYIIdGrmEORPOK9FEu0pg5ZLBKCGJuucHwKADxTBxKY7eM4rxndqxRcnMZKIw==} - engines: {node: '>= 10.13'} - dependencies: - '@types/geojson': 7946.0.10 - '@types/node': 17.0.45 - denque: 2.1.0 - iconv-lite: 0.6.3 - long: 5.2.0 - moment-timezone: 0.5.37 - please-upgrade-node: 3.2.0 - dev: false - /md5.js/1.3.5: resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} dependencies: @@ -2818,14 +1617,6 @@ packages: engines: {node: '>= 0.6'} dev: false - /micromatch/4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} - dependencies: - braces: 3.0.2 - picomatch: 2.3.1 - dev: false - /mime-db/1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -2879,50 +1670,6 @@ packages: resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} dev: false - /minipass-collect/1.0.2: - resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.4 - dev: false - optional: true - - /minipass-fetch/1.4.1: - resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} - engines: {node: '>=8'} - dependencies: - minipass: 3.3.4 - minipass-sized: 1.0.3 - minizlib: 2.1.2 - optionalDependencies: - encoding: 0.1.13 - dev: false - optional: true - - /minipass-flush/1.0.5: - resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.4 - dev: false - optional: true - - /minipass-pipeline/1.2.4: - resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} - engines: {node: '>=8'} - dependencies: - minipass: 3.3.4 - dev: false - optional: true - - /minipass-sized/1.0.3: - resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} - engines: {node: '>=8'} - dependencies: - minipass: 3.3.4 - dev: false - optional: true - /minipass/3.3.4: resolution: {integrity: sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==} engines: {node: '>=8'} @@ -2948,16 +1695,6 @@ packages: hasBin: true dev: false - /moment-timezone/0.5.37: - resolution: {integrity: sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg==} - dependencies: - moment: 2.29.4 - dev: false - - /moment/2.29.4: - resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} - dev: false - /mri/1.1.4: resolution: {integrity: sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==} engines: {node: '>=4'} @@ -2967,10 +1704,6 @@ packages: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: false - /ms/2.1.1: - resolution: {integrity: sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==} - dev: false - /ms/2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: false @@ -2979,41 +1712,6 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: false - /mysql/2.18.1: - resolution: {integrity: sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==} - engines: {node: '>= 0.6'} - dependencies: - bignumber.js: 9.0.0 - readable-stream: 2.3.7 - safe-buffer: 5.1.2 - sqlstring: 2.3.1 - dev: false - - /mysql2/2.3.3: - resolution: {integrity: sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==} - engines: {node: '>= 8.0'} - dependencies: - denque: 2.1.0 - generate-function: 2.3.1 - iconv-lite: 0.6.3 - long: 4.0.0 - lru-cache: 6.0.0 - named-placeholders: 1.1.2 - seq-queue: 0.0.5 - sqlstring: 2.3.3 - dev: false - - /named-placeholders/1.1.2: - resolution: {integrity: sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA==} - engines: {node: '>=6.0.0'} - dependencies: - lru-cache: 4.1.5 - dev: false - - /nan/2.17.0: - resolution: {integrity: sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==} - dev: false - /napi-build-utils/1.0.2: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} dev: false @@ -3027,11 +1725,6 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: false - /netmask/1.0.6: - resolution: {integrity: sha512-3DWDqAtIiPSkBXZyYEjwebfK56nrlQfRGt642fu8RPaL+ePu750+HCMHxjJCG3iEHq/0aeMvX6KIzlv7nuhfrA==} - engines: {node: '>= 0.4.0'} - dev: false - /noble-secp256k1/1.2.14: resolution: {integrity: sha512-GSCXyoZBUaaPwVWdYncMEmzlSUjF9J/YeEHpklYJCyg8wPuJP3NzDx0BkiwArzINkdX2HJHvUJhL6vVWPOQQcg==} deprecated: Switch to namespaced @noble/secp256k1 for security and feature updates @@ -3048,10 +1741,6 @@ packages: resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} dev: false - /node-addon-api/4.3.0: - resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} - dev: false - /node-addon-api/5.0.0: resolution: {integrity: sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==} dev: false @@ -3073,28 +1762,6 @@ packages: hasBin: true dev: false - /node-gyp/8.4.1: - resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} - engines: {node: '>= 10.12.0'} - hasBin: true - requiresBuild: true - dependencies: - env-paths: 2.2.1 - glob: 7.2.3 - graceful-fs: 4.2.10 - make-fetch-happen: 9.1.0 - nopt: 5.0.0 - npmlog: 6.0.2 - rimraf: 3.0.2 - semver: 7.3.8 - tar: 6.1.11 - which: 2.0.2 - transitivePeerDependencies: - - bluebird - - supports-color - dev: false - optional: true - /nodemon/2.0.20: resolution: {integrity: sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==} engines: {node: '>=8.10.0'} @@ -3156,17 +1823,6 @@ packages: set-blocking: 2.0.0 dev: false - /npmlog/6.0.2: - resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - are-we-there-yet: 3.0.1 - console-control-strings: 1.1.0 - gauge: 4.0.4 - set-blocking: 2.0.0 - dev: false - optional: true - /object-assign/4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3184,13 +1840,6 @@ packages: resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==} dev: false - /on-finished/2.3.0: - resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} - engines: {node: '>= 0.8'} - dependencies: - ee-first: 1.1.1 - dev: false - /on-finished/2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3198,29 +1847,12 @@ packages: ee-first: 1.1.1 dev: false - /on-headers/1.0.2: - resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} - engines: {node: '>= 0.8'} - dev: false - /once/1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 dev: false - /optionator/0.8.3: - resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} - engines: {node: '>= 0.8.0'} - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.3.0 - prelude-ls: 1.1.2 - type-check: 0.3.2 - word-wrap: 1.2.3 - dev: false - /otplib/12.0.1: resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} dependencies: @@ -3241,39 +1873,6 @@ packages: yocto-queue: 0.1.0 dev: false - /p-map/4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} - dependencies: - aggregate-error: 3.1.0 - dev: false - optional: true - - /pac-proxy-agent/3.0.1: - resolution: {integrity: sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ==} - dependencies: - agent-base: 4.3.0 - debug: 4.3.4 - get-uri: 2.0.4 - http-proxy-agent: 2.1.0 - https-proxy-agent: 3.0.1 - pac-resolver: 3.0.0 - raw-body: 2.5.1 - socks-proxy-agent: 4.0.2 - transitivePeerDependencies: - - supports-color - dev: false - - /pac-resolver/3.0.0: - resolution: {integrity: sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==} - dependencies: - co: 4.6.0 - degenerator: 1.0.4 - ip: 1.1.8 - netmask: 1.0.6 - thunkify: 2.1.2 - dev: false - /parseurl/1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -3303,40 +1902,15 @@ packages: engines: {node: '>=0.10.0'} dev: false - /path-parse/1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: false - - /path-proxy/1.0.0: - resolution: {integrity: sha512-p9IuY9FRY1nU59RDW+tnLL6qMxmBnY03WGYxzy1FcqE5OMO5ggz7ahmOBH0JBS+9f95Yc7V5TZ+kHpTeFWaLQA==} - dependencies: - inflection: 1.3.8 - dev: false - /path-to-regexp/0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} dev: false - /pbkdf2/3.1.2: - resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} - engines: {node: '>=0.12'} - dependencies: - create-hash: 1.2.0 - create-hmac: 1.1.7 - ripemd160: 2.0.2 - safe-buffer: 5.2.1 - sha.js: 2.4.11 - dev: false - /peek-readable/5.0.0: resolution: {integrity: sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==} engines: {node: '>=14.16'} dev: false - /pg-connection-string/2.5.0: - resolution: {integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==} - dev: false - /picomatch/2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -3356,14 +1930,6 @@ packages: split2: 4.1.0 dev: false - /pino-http/5.8.0: - resolution: {integrity: sha512-YwXiyRb9y0WCD1P9PcxuJuh3Dc5qmXde/paJE86UGYRdiFOi828hR9iUGmk5gaw6NBT9gLtKANOHFimvh19U5w==} - dependencies: - fast-url-parser: 1.1.3 - pino: 6.14.0 - pino-std-serializers: 4.0.0 - dev: false - /pino-pretty/7.6.1: resolution: {integrity: sha512-H7N6ZYkiyrfwBGW9CSjx0uyO9Q2Lyt73881+OTYk8v3TiTdgN92QHrWlEq/LeWw5XtDP64jeSk3mnc6T+xX9/w==} hasBin: true @@ -3383,31 +1949,10 @@ packages: strip-json-comments: 3.1.1 dev: false - /pino-std-serializers/3.2.0: - resolution: {integrity: sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==} - dev: false - - /pino-std-serializers/4.0.0: - resolution: {integrity: sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==} - dev: false - /pino-std-serializers/6.0.0: resolution: {integrity: sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ==} dev: false - /pino/6.14.0: - resolution: {integrity: sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==} - hasBin: true - dependencies: - fast-redact: 3.1.2 - fast-safe-stringify: 2.1.1 - flatstr: 1.0.12 - pino-std-serializers: 3.2.0 - process-warning: 1.0.0 - quick-format-unescaped: 4.0.4 - sonic-boom: 1.4.1 - dev: false - /pino/8.6.1: resolution: {integrity: sha512-fi+V2K98eMZjQ/uEHHSiMALNrz7HaFdKNYuyA3ZUrbH0f1e8sPFDmeRGzg7ZH2q4QDxGnJPOswmqlEaTAZeDPA==} hasBin: true @@ -3425,12 +1970,6 @@ packages: thread-stream: 2.2.0 dev: false - /please-upgrade-node/3.2.0: - resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} - dependencies: - semver-compare: 1.0.0 - dev: false - /postmark/3.0.14: resolution: {integrity: sha512-Bkt6msv+I4Z3Y/jwt2owTNVmahOOyJlS1aQLiWsFAz5fzeJPfEqIUMSPPQTk7lYyJvYnLXQuzhFNG7M8yxqPhA==} dependencies: @@ -3458,58 +1997,19 @@ packages: tunnel-agent: 0.6.0 dev: false - /prelude-ls/1.1.2: - resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} - engines: {node: '>= 0.8.0'} - dev: false - /prettier/1.19.1: resolution: {integrity: sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==} - engines: {node: '>=4'} - hasBin: true - dev: true - - /process-nextick-args/2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - dev: false - - /process-warning/1.0.0: - resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} - dev: false - - /process-warning/2.0.0: - resolution: {integrity: sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww==} - dev: false - - /process/0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - dev: false - - /promise-inflight/1.0.1: - resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} - peerDependencies: - bluebird: '*' - peerDependenciesMeta: - bluebird: - optional: true - dev: false - optional: true + engines: {node: '>=4'} + hasBin: true + dev: true - /promise-retry/2.0.1: - resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} - engines: {node: '>=10'} - dependencies: - err-code: 2.0.3 - retry: 0.12.0 + /process-warning/2.0.0: + resolution: {integrity: sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww==} dev: false - optional: true - /promisify-call/2.0.4: - resolution: {integrity: sha512-ZX68J1+1Pe0I8NC0P6Ji3fDDcJceVfpoygfDLgdb1fp5vW9IRlwSpDaxe1T5HgwchyHV2DsL/pWzWikUiWEbLQ==} - engines: {node: '>=4.0'} - dependencies: - with-callback: 1.0.2 + /process/0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} dev: false /proxy-addr/2.0.7: @@ -3520,30 +2020,6 @@ packages: ipaddr.js: 1.9.1 dev: false - /proxy-agent/3.1.1: - resolution: {integrity: sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw==} - engines: {node: '>=6'} - dependencies: - agent-base: 4.3.0 - debug: 4.3.4 - http-proxy-agent: 2.1.0 - https-proxy-agent: 3.0.1 - lru-cache: 5.1.1 - pac-proxy-agent: 3.0.1 - proxy-from-env: 1.1.0 - socks-proxy-agent: 4.0.2 - transitivePeerDependencies: - - supports-color - dev: false - - /proxy-from-env/1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - dev: false - - /pseudomap/1.0.2: - resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - dev: false - /pstree.remy/1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} dev: false @@ -3555,10 +2031,6 @@ packages: once: 1.4.0 dev: false - /punycode/1.4.1: - resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} - dev: false - /punycode/2.1.1: resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} engines: {node: '>=6'} @@ -3571,18 +2043,6 @@ packages: side-channel: 1.0.4 dev: false - /qs/6.11.0: - resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} - engines: {node: '>=0.6'} - dependencies: - side-channel: 1.0.4 - dev: false - - /qs/6.7.0: - resolution: {integrity: sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==} - engines: {node: '>=0.6'} - dev: false - /query-string/6.14.1: resolution: {integrity: sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==} engines: {node: '>=6'} @@ -3602,32 +2062,11 @@ packages: engines: {node: '>=10'} dev: false - /random-bytes/1.0.0: - resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} - engines: {node: '>= 0.8'} - dev: false - - /randombytes/2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - dependencies: - safe-buffer: 5.2.1 - dev: false - /range-parser/1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} dev: false - /raw-body/2.4.0: - resolution: {integrity: sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==} - engines: {node: '>= 0.8'} - dependencies: - bytes: 3.1.0 - http-errors: 1.7.2 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - dev: false - /raw-body/2.5.1: resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} engines: {node: '>= 0.8'} @@ -3652,27 +2091,6 @@ packages: resolution: {integrity: sha512-mdkQtdg5elliOR64xcHVMkghSHQGjpsdCFAs6fSe28wTaVypR0GywmK5ndKpOSQzzuNpSrzC12Rv9UEkDLX73A==} dev: false - /readable-stream/1.1.14: - resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==} - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 0.0.1 - string_decoder: 0.10.31 - dev: false - - /readable-stream/2.3.7: - resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - dev: false - /readable-stream/3.6.0: resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} engines: {node: '>= 6'} @@ -3711,13 +2129,6 @@ packages: engines: {node: '>= 12.13.0'} dev: false - /rechoir/0.8.0: - resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} - engines: {node: '>= 10.13.0'} - dependencies: - resolve: 1.22.1 - dev: false - /redis/4.3.1: resolution: {integrity: sha512-cM7yFU5CA6zyCF7N/+SSTcSJQSRMEKN0k0Whhu6J7n9mmXRoXugfWDBo5iOzGwABmsWKSwGPTU5J4Bxbl+0mrA==} dependencies: @@ -3734,28 +2145,10 @@ packages: engines: {node: '>=0.10.0'} dev: false - /requires-port/1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - dev: false - /resolve-alpn/1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} dev: false - /resolve-from/5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - dev: false - - /resolve/1.22.1: - resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} - hasBin: true - dependencies: - is-core-module: 2.10.0 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - dev: false - /responselike/3.0.0: resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} engines: {node: '>=14.16'} @@ -3768,16 +2161,6 @@ packages: engines: {node: '>=4'} dev: false - /retry-as-promised/5.0.0: - resolution: {integrity: sha512-6S+5LvtTl2ggBumk04hBo/4Uf6fRJUwIgunGZ7CYEBCeufGFW1Pu6ucUf/UskHeWOIsUcLOGLFXPig5tR5V1nA==} - dev: false - - /retry/0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - dev: false - optional: true - /reusify/1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3801,10 +2184,6 @@ packages: inherits: 2.0.4 dev: false - /safe-buffer/5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - dev: false - /safe-buffer/5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} dev: false @@ -3838,15 +2217,6 @@ packages: resolution: {integrity: sha512-ZQruFgZnIWH+WyO9t5rWt4ZEGqCKPwhiw+YbzTwpmT9elgLrLcfuyUiSnwwjUiVy9r4VM3urtbNF1xmEh9IL2w==} dev: false - /semver-compare/1.0.0: - resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} - dev: false - - /semver/5.3.0: - resolution: {integrity: sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==} - hasBin: true - dev: false - /semver/5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} hasBin: true @@ -3870,27 +2240,6 @@ packages: lru-cache: 6.0.0 dev: false - /send/0.17.1: - resolution: {integrity: sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==} - engines: {node: '>= 0.8.0'} - dependencies: - debug: 2.6.9 - depd: 1.1.2 - destroy: 1.0.4 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 1.7.3 - mime: 1.6.0 - ms: 2.1.1 - on-finished: 2.3.0 - range-parser: 1.2.1 - statuses: 1.5.0 - transitivePeerDependencies: - - supports-color - dev: false - /send/0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} @@ -3912,27 +2261,6 @@ packages: - supports-color dev: false - /seq-queue/0.0.5: - resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} - dev: false - - /sequelize-pool/7.1.0: - resolution: {integrity: sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==} - engines: {node: '>= 10.0.0'} - dev: false - - /serve-static/1.14.1: - resolution: {integrity: sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==} - engines: {node: '>= 0.8.0'} - dependencies: - encodeurl: 1.0.2 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.17.1 - transitivePeerDependencies: - - supports-color - dev: false - /serve-static/1.15.0: resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} engines: {node: '>= 0.8.0'} @@ -3953,10 +2281,6 @@ packages: resolution: {integrity: sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==} dev: false - /setprototypeof/1.1.1: - resolution: {integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==} - dev: false - /setprototypeof/1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false @@ -4026,14 +2350,6 @@ packages: engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} dev: false - /socks-proxy-agent/4.0.2: - resolution: {integrity: sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==} - engines: {node: '>= 6'} - dependencies: - agent-base: 4.2.1 - socks: 2.3.3 - dev: false - /socks-proxy-agent/6.2.0: resolution: {integrity: sha512-wWqJhjb32Q6GsrUqzuFkukxb/zzide5quXYcMVpIjxalDBBYy2nqKCFQ/9+Ie4dvOYSQdOk3hUlZSdzZOd3zMQ==} engines: {node: '>= 10'} @@ -4045,26 +2361,6 @@ packages: - supports-color dev: false - /socks-proxy-agent/6.2.1: - resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} - engines: {node: '>= 10'} - dependencies: - agent-base: 6.0.2 - debug: 4.3.4 - socks: 2.7.1 - transitivePeerDependencies: - - supports-color - dev: false - optional: true - - /socks/2.3.3: - resolution: {integrity: sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - dependencies: - ip: 1.1.5 - smart-buffer: 4.2.0 - dev: false - /socks/2.7.1: resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} @@ -4080,13 +2376,6 @@ packages: node-gyp-build: 4.5.0 dev: false - /sonic-boom/1.4.1: - resolution: {integrity: sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==} - dependencies: - atomic-sleep: 1.0.0 - flatstr: 1.0.12 - dev: false - /sonic-boom/2.8.0: resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==} dependencies: @@ -4114,47 +2403,6 @@ packages: engines: {node: '>= 10.x'} dev: false - /sqlite3/5.1.2: - resolution: {integrity: sha512-D0Reg6pRWAFXFUnZKsszCI67tthFD8fGPewRddDCX6w4cYwz3MbvuwRICbL+YQjBAh9zbw+lJ/V9oC8nG5j6eg==} - requiresBuild: true - peerDependenciesMeta: - node-gyp: - optional: true - dependencies: - '@mapbox/node-pre-gyp': 1.0.10 - node-addon-api: 4.3.0 - tar: 6.1.11 - optionalDependencies: - node-gyp: 8.4.1 - transitivePeerDependencies: - - bluebird - - encoding - - supports-color - dev: false - - /sqlstring/2.3.1: - resolution: {integrity: sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==} - engines: {node: '>= 0.6'} - dev: false - - /sqlstring/2.3.3: - resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} - engines: {node: '>= 0.6'} - dev: false - - /ssri/8.0.1: - resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.4 - dev: false - optional: true - - /statuses/1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} - dev: false - /statuses/2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -4169,11 +2417,6 @@ packages: engines: {node: '>=4.0.0'} dev: false - /streamsearch/1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - dev: false - /strict-uri-encode/2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} @@ -4192,16 +2435,6 @@ packages: strip-ansi: 6.0.1 dev: false - /string_decoder/0.10.31: - resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} - dev: false - - /string_decoder/1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - dependencies: - safe-buffer: 5.1.2 - dev: false - /string_decoder/1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -4240,11 +2473,6 @@ packages: has-flag: 3.0.0 dev: false - /supports-preserve-symlinks-flag/1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - dev: false - /tar-fs/2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} dependencies: @@ -4277,11 +2505,6 @@ packages: yallist: 4.0.0 dev: false - /tarn/3.0.2: - resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==} - engines: {node: '>=8.0.0'} - dev: false - /text-decoding/1.0.0: resolution: {integrity: sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==} dev: false @@ -4297,44 +2520,11 @@ packages: real-require: 0.2.0 dev: false - /thunkify/2.1.2: - resolution: {integrity: sha512-w9foI80XcGImrhMQ19pxunaEC5Rp2uzxZZg4XBAFRfiLOplk3F0l7wo+bO16vC2/nlQfR/mXZxcduo0MF2GWLg==} - dev: false - - /tildify/2.0.0: - resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} - engines: {node: '>=8'} - dev: false - - /tiny-lru/10.0.1: - resolution: {integrity: sha512-Vst+6kEsWvb17Zpz14sRJV/f8bUWKhqm6Dc+v08iShmIJ/WxqWytHzCTd6m88pS33rE2zpX34TRmOpAJPloNCA==} - engines: {node: '>=6'} - dev: false - /tiny-lru/9.0.3: resolution: {integrity: sha512-/i9GruRjXsnDgehxvy6iZ4AFNVxngEFbwzirhdulomMNPGPVV3ECMZOWSw0w4sRMZ9Al9m4jy08GPvRxRUGYlw==} engines: {node: '>=6'} dev: false - /tiny-secp256k1/1.1.6: - resolution: {integrity: sha512-FmqJZGduTyvsr2cF3375fqGHUovSwDi/QytexX1Se4BPuPZpTE5Ftp5fg+EFSuEf3lhZqgCRjEG3ydUQ/aNiwA==} - engines: {node: '>=6.0.0'} - requiresBuild: true - dependencies: - bindings: 1.5.0 - bn.js: 4.12.0 - create-hmac: 1.1.7 - elliptic: 6.5.4 - nan: 2.17.0 - dev: false - - /tiny-secp256k1/2.2.1: - resolution: {integrity: sha512-/U4xfVqnVxJXN4YVsru0E6t5wVncu2uunB8+RVR40fYUxkKYUPS10f+ePQZgFBoE/Jbf9H1NBveupF2VmB58Ng==} - engines: {node: '>=14.0.0'} - dependencies: - uint8array-tools: 0.0.7 - dev: false - /to-regex-range/5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4342,11 +2532,6 @@ packages: is-number: 7.0.0 dev: false - /toidentifier/1.0.0: - resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==} - engines: {node: '>=0.6'} - dev: false - /toidentifier/1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -4360,10 +2545,6 @@ packages: ieee754: 1.2.1 dev: false - /toposort-class/1.0.1: - resolution: {integrity: sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==} - dev: false - /touch/3.1.0: resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} hasBin: true @@ -4375,28 +2556,12 @@ packages: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: false - /tslib/2.4.0: - resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} - dev: false - - /tsscmp/1.0.6: - resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} - engines: {node: '>=0.6.x'} - dev: false - /tunnel-agent/0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: safe-buffer: 5.2.1 dev: false - /type-check/0.3.2: - resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.1.2 - dev: false - /type-is/1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -4417,18 +2582,6 @@ packages: dev: false optional: true - /uid-safe/2.1.5: - resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} - engines: {node: '>= 0.8'} - dependencies: - random-bytes: 1.0.0 - dev: false - - /uint8array-tools/0.0.7: - resolution: {integrity: sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==} - engines: {node: '>=14.0.0'} - dev: false - /undefsafe/2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} dev: false @@ -4437,27 +2590,6 @@ packages: resolution: {integrity: sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==} dev: false - /undici/5.14.0: - resolution: {integrity: sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==} - engines: {node: '>=12.18'} - dependencies: - busboy: 1.6.0 - dev: false - - /unique-filename/1.1.1: - resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} - dependencies: - unique-slug: 2.0.2 - dev: false - optional: true - - /unique-slug/2.0.2: - resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} - dependencies: - imurmurhash: 0.1.4 - dev: false - optional: true - /unpipe/1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -4473,12 +2605,6 @@ packages: resolution: {integrity: sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA==} dev: false - /utf7/1.0.2: - resolution: {integrity: sha512-qQrPtYLLLl12NF4DrM9CvfkxkYI97xOb5dsnGZHE3teFr0tWiEZ9UdgMPczv24vl708cYMpe6mGXGHrotIp3Bw==} - dependencies: - semver: 5.3.0 - dev: false - /util-deprecate/1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false @@ -4493,11 +2619,6 @@ packages: hasBin: true dev: false - /validator/13.7.0: - resolution: {integrity: sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==} - engines: {node: '>= 0.10'} - dev: false - /varuint-bitcoin/1.1.2: resolution: {integrity: sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==} dependencies: @@ -4535,15 +2656,6 @@ packages: webidl-conversions: 3.0.1 dev: false - /which/2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: false - optional: true - /wide-align/1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} dependencies: @@ -4556,22 +2668,6 @@ packages: bs58check: 2.1.2 dev: false - /with-callback/1.0.2: - resolution: {integrity: sha512-zaUhn7OWgikdqWlPYpZ4rTX/6IAV0czMVyd+C6QLVrif2tATF28CYUnHBmHs2a5EaZo7bB1+plBUPHto+HW8uA==} - engines: {node: '>=4'} - dev: false - - /wkx/0.5.0: - resolution: {integrity: sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==} - dependencies: - '@types/node': 18.8.5 - dev: false - - /word-wrap/1.2.3: - resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} - engines: {node: '>=0.10.0'} - dev: false - /wordwrap/1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} dev: false @@ -4580,10 +2676,6 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: false - /wretch/1.7.10: - resolution: {integrity: sha512-UgF2o63bZRsz3LoOxaxzAUdFdlIJzVYbCHHhQ+LNMSBD1FeFJn8ADaekopJclHUm6sN8Lhu0DQFGQloliS0Twg==} - dev: false - /ws/7.5.9: resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} engines: {node: '>=8.3.0'} @@ -4610,18 +2702,6 @@ packages: optional: true dev: false - /xregexp/2.0.0: - resolution: {integrity: sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==} - dev: false - - /yallist/2.1.2: - resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} - dev: false - - /yallist/3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: false - /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: false @@ -4630,11 +2710,3 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: false - - /zeromq/6.0.0-beta.6: - resolution: {integrity: sha512-wLf6M7pBHijl+BRltUL2VoDpgbQcOZetiX8UzycHL8CcYFxYnRrpoG5fi3UX3+Umavz1lk4/dGaQez8qiDgr/Q==} - engines: {node: '>= 10.2'} - requiresBuild: true - dependencies: - node-gyp-build: 4.5.0 - dev: false diff --git a/routes/invoices.js b/routes/invoices.js index f3c95a21..ddd840ec 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -4,64 +4,25 @@ import { optionalAuth } from "$lib/passport"; import { err, l } from "$lib/logging"; import { SATS, derivePayRequest } from "$lib/utils"; import { emit } from "$lib/sockets"; +import { v4 } from "uuid"; +import { rd, g, s } from "$lib/redis"; -app.get("/invoice", async (req, res, next) => { - try { - const invoice = await db.Invoice.findOne({ - where: { - uuid: req.query.uuid - }, - include: [ - { - model: db.User, - as: "user", - attributes: ["username", "currency", "profile", "uuid"] - }, - { - model: db.Request, - as: "request" - } - ] - }); - - res.send(invoice); - } catch (e) { - err("invoice not found", e); - res.code(500).send("Invoice not found"); - } -}); - -app.get("/invoice/:text", async (req, res, next) => { - try { - let { text } = req.params; - let where = - text.split("-").length > 4 - ? { uuid: text } - : text.startsWith("ln") - ? { text } - : { - [Op.or]: [{ unconfidential: text }, { address: text }] - }; - - const invoice = await db.Invoice.findOne({ - where, - include: { - model: db.User, - as: "user", - attributes: ["username", "currency", "profile", "uuid"] - } - }); - - res.send(invoice); - } catch (e) { - err("invoice not found", e); - res.code(500).send("Invoice not found"); - } +app.get("/invoice", async ({ query: { id } }, res) => { + let invoice = await g(`invoice:${id}`); + invoice.user = await g(`user:${invoice.user_id}`); + res.send(invoice); }); -app.post("/invoice", optionalAuth, async (req, res, next) => { - try { - let { type = "bech32", liquidAddress, id, invoice, user, tx } = req.body; +app.post( + "/invoice", + optionalAuth, + async ( + { + body: { type = "bech32", liquidAddress, id, invoice, user, tx }, + user: me + }, + res + ) => { let { blindkey, currency, @@ -71,49 +32,24 @@ app.post("/invoice", optionalAuth, async (req, res, next) => { network, request_id } = invoice; - let address, unconfidential, text; + let text; if (amount < 0) throw new Error("amount out of range"); if (tip > 5 * amount || tip < 0) throw new Error("tip amount out of range"); + tip = tip || 0; - if (!user) ({ user } = req); - else { - user = await db.User.findOne({ - where: { - username: user.username - }, - include: { - model: db.Account, - as: "account" - } - }); - } + if (!user) user = me; if (!user) throw new Error("user not provided"); + + user = await g(`user:${user.id}`); + if (!currency) currency = user.currency; if (!rate) rate = store.rates[currency]; if (amount < 0) throw new Error("invalid amount"); - if (user.account.pubkey) { - let { address, confidentialAddress } = await deriveAddress( - user.account, - type - ); - if (confidentialAddress) { - address = confidentialAddress; - unconfidential = address; - } else { - address = address; - } - } else if (network !== "lightning") { - address = await { bitcoin: bc, liquid: lq }[network].getNewAddress(); - if (network === "liquid") - ({ unconfidential } = await lq.getAddressInfo(address)); - } - invoice = { ...invoice, - account_id: user.account.id, - address, + id: v4(), amount, currency, rate, @@ -122,83 +58,29 @@ app.post("/invoice", optionalAuth, async (req, res, next) => { user_id: user.id }; - if (network === "lightning") { - invoice.text = await derivePayRequest(invoice); - } else { - invoice.text = bip21(invoice, user.account); - } - - if (liquidAddress) { - if (network === "lightning") { - l("conversion request for", liquidAddress, invoice.text); - store.convert[invoice.text] = { address: liquidAddress, tx }; - } else if (network === "liquid") { - l("conversion request for", liquidAddress, invoice.unconfidential); - store.convert[invoice.unconfidential] = { address: liquidAddress, tx }; - } else { - l("conversion request for", liquidAddress, invoice.address); - store.convert[invoice.address] = { address: liquidAddress, tx }; - } - } + invoice.text = await derivePayRequest(invoice); l( "creating invoice", user.username, - invoice.network, - invoice.amount, - invoice.tip, - invoice.currency, - invoice.text && `${invoice.text.substr(0, 8)}..${invoice.text.substr(-6)}` + network, + amount, + tip, + currency, + `${text.substr(0, 8)}..${text.substr(-6)}` ); - if (!invoice.tip) invoice.tip = 0; - - invoice = await db.Invoice.create(invoice); - store.addresses[invoice.address] = user.username; - if (invoice.unconfidential) { - store.addresses[invoice.unconfidential] = user.username; - if (blindkey) await lq.importBlindingKey(invoice.address, blindkey); - } + await rd.lPush(`${id}:invoices`, invoice.id); if (request_id) { - let request = await db.Request.findOne({ - where: { id: request_id }, - - include: { - model: db.User, - as: "requester", - attributes: ["username"] - } - }); + let request = await g(`request:${request_id}`); + let { invoice_id: prev } = request; + request.invoice_id = invoice.id; + await s(`request:${request_id}`, request); - if (request) { - let existing = request.invoice_id; - request.invoice_id = invoice.id; - await request.save(); - - if (!existing) - emit(request.requester.username, "invoice", { - amount: invoice.amount, - currency: invoice.currency, - rate: invoice.rate, - request_id, - uuid: invoice.uuid, - user: { username: user.username, profile: user.profile } - }); - } + if (!prev) emit(request.requester.username, "invoice", invoice); } res.send(invoice); - } catch (e) { - err(e.message, e.stack); - res.code(500).send(`Problem during invoice creation: ${e.message}`); - } -}); - -app.post( - "/:username/:network/invoice", - optionalAuth, - async (req, res, next) => { - let { network, username } = req.params; } ); diff --git a/routes/lightning/channel.js b/routes/lightning/channel.js deleted file mode 100644 index d5d5b7b2..00000000 --- a/routes/lightning/channel.js +++ /dev/null @@ -1,29 +0,0 @@ -import axios from "axios"; -import ln from "$lib/ln"; - -export default async (req, res) => { - const { params } = req.body; - const [public_key, socket] = params.uri.split("@"); - - l("connecting to peer", req.user.username, public_key, socket); - let result; - - const { callback, k1 } = params; - let remoteid; - try { - remoteid = (await ln.getinfo()).id; - } catch (e) { - err("problem getting lightning node info", e.message); - return res.code(500).send(e.message); - } - - let url = `${callback}?k1=${k1}&remoteid=${remoteid}&private=0`; - - try { - let response = await axios.get(url); - res.send(response.data); - } catch (e) { - err("problem sending channel request", e.message); - return res.code(500).send({ status: "ERROR", reason: e.message }); - } -}; diff --git a/routes/lightning/channelRequest.js b/routes/lightning/channelRequest.js deleted file mode 100644 index 74e4cbba..00000000 --- a/routes/lightning/channelRequest.js +++ /dev/null @@ -1,24 +0,0 @@ -import { lnurlServer } from "$routes/lnurl"; -import { err } from "$lib/logging"; - -export const channelRequest = async (req, res) => { - let { localAmt, pushAmt } = req.body; - - pushAmt = 0; - - try { - if (localAmt < 20000) - throw new Error("amount must be greater than 20000 SAT"); - if (localAmt > 1000000) throw new Error("amount must be <= 1000000"); - - const result = await lnurlServer.generateNewUrl("channelRequest", { - localAmt, - pushAmt - }); - - res.send(result); - } catch (e) { - err("problem generating channel request", e.message); - res.code(500).send(e.message); - } -}; diff --git a/routes/lightning/index.js b/routes/lightning/index.js index b4611ef5..ddc10bfe 100644 --- a/routes/lightning/index.js +++ b/routes/lightning/index.js @@ -1,11 +1,9 @@ -import channel from "./channel"; import invoice from "./invoice"; import query from "./query"; import send from "./send"; import parse from "./parse"; export default { - channel, invoice, query, send, diff --git a/routes/requests.js b/routes/requests.js index 0b3832e8..bea8f7fd 100644 --- a/routes/requests.js +++ b/routes/requests.js @@ -1,92 +1,23 @@ import app from "$app"; import { auth } from "$lib/passport"; import { emit } from "$lib/sockets"; +import { g, rd } from "$lib/redis"; -app.get("/request/:id", auth, async (req, res) => { +app.get("/request/:id", auth, async ({ params: { id } }, res) => { try { - let request = await db.Request.findOne({ - where: { id: req.params.id }, - include: [ - { - attributes: ["username", "profile"], - model: db.User, - as: "recipient" - }, - { - attributes: ["username", "profile"], - model: db.User, - as: "requester" - }, - { - model: db.Invoice, - as: "invoice" - } - ] - }); - - res.send({ request }); + res.send({ request: await g(id) }); } catch (e) { console.log(e); res.code(500).send(e.message); } }); -app.get("/requests", auth, async (req, res) => { +app.get("/requests", auth, async ({ user: { uuid } }, res) => { try { const day = new Date(new Date().setDate(new Date().getDate() - 1)); - let invoices = ( - await db.Request.findAll({ - where: { - [Op.or]: { - recipient_id: req.user.id, - requester_id: req.user.id - }, - createdAt: { [Op.gt]: day } - }, - order: [["createdAt", "DESC"]], - include: [ - { - model: db.Invoice, - as: "invoice", - where: { received: { [Op.lt]: col("invoice.amount") } }, - include: { - model: db.User, - as: "user", - attributes: ["username", "profile"] - } - }, - { - model: db.User, - as: "requester", - attributes: ["username", "profile"] - } - ] - }) - ) - .map( - r => - r.invoice && { - request_id: r.id, - requester: r.requester.get({ plain: true }), - ...r.invoice.get({ plain: true }) - } - ) - .filter(r => r); - - let requests = await db.Request.findAll({ - where: { - recipient_id: req.user.id, - createdAt: { [Op.gt]: day }, - invoice_id: { [Op.eq]: null } - }, - order: [["createdAt", "DESC"]], - include: { - attributes: ["username", "profile"], - model: db.User, - as: "requester" - } - }); + let invoices = await rd.lRange(`${uuid}:invoices`, 0, -1); + let requests = await rd.lRange(`${uuid}:requests`, 0, -1); res.send({ invoices, requests }); } catch (e) { @@ -95,41 +26,32 @@ app.get("/requests", auth, async (req, res) => { } }); -app.post("/requests", auth, async (req, res) => { - try { - let { recipient: username, ...params } = req.body; - let { id: recipient_id } = await db.User.findOne({ where: { username } }); +app.post( + "/requests", + auth, + async ( + { body: { recipient, ...params }, user: { username, profile } }, + res + ) => { + let { id: recipient_id } = await g(`user:${recipient}`); - let request = await db.Request.create({ recipient_id, ...params }); - request = request.get({ plain: true }); + let request = { recipient_id, ...params }; request.requester = { - username: req.user.username, - profile: req.user.profile + username, + profile }; - emit(username, "request", request); + + emit(recipient, "request", request); res.send(request); - } catch (e) { - console.log(e); - res.code(500).send(e.message); } -}); - -app.post("/requests/delete", auth, async (req, res) => { - try { - let r = await db.Request.findOne({ - where: { - id: req.body.request_id, - [Op.or]: { requester_id: req.user.id, recipient_id: req.user.id } - } - }); - - if (!r) throw new Error("request not found"); - await r.destroy(); +); +app.post( + "/requests/delete", + auth, + async ({ body: { request_id }, user: { id } }, res) => { + await rd.lrem(`user:${id}:requests`, request_id); res.send(); - } catch (e) { - console.log(e); - res.code(500).send(e.message); } -}); +); diff --git a/routes/users.js b/routes/users.js index 4a01cc1e..8f0b9106 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1,5 +1,5 @@ import app from "$app"; -import redis from "$lib/redis"; +import { g, s } from "$lib/redis"; import config from "$config"; import store from "$lib/store"; import { auth, optionalAuth } from "$lib/passport"; @@ -13,30 +13,25 @@ import { emit } from "$lib/sockets"; import register from "$lib/register"; import { requirePin } from "$lib/utils"; import got from "got"; -import { bech32 } from "bech32"; - -const { encode, decode, fromWords, toWords } = bech32; const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); app.get("/me", auth, async (req, res) => { try { - let user = req.user.get({ plain: true }); - user.haspin = !!user.pin; - res.send(pick(user, ...whitelist)); + res.send(pick(req.user, ...whitelist)); } catch (e) { console.log("problem fetching user", e); res.code(500).send(e.message); } }); -app.get("/users/:key", async (req, res) => { +app.get("/users/:key", async ({ params: { key } }, res) => { try { if (key.startsWith("npub")) { key = Buffer.from(fromWords(decode(key).words)).toString("hex"); } - user = JSON.parse(await redis.get(`user:${key}`)); + let user = await g(`user:${key}`); if (!user && key.length === 64) { user = { @@ -46,6 +41,7 @@ app.get("/users/:key", async (req, res) => { }; } + console.log(user) if (!user) return res.code(500).send("User not found"); let whitelist = [ @@ -69,7 +65,7 @@ app.get("/users/:key", async (req, res) => { app.post("/register", async (req, res) => { try { - const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress; let { cipher, pubkey, password, username, salt } = req.body.user; let user = { @@ -129,14 +125,6 @@ app.post("/2fa", auth, async (req, res) => { res.send({}); }); -app.get("/exists", async (req, res) => { - let exists = await db.User.findOne({ - where: { username: req.query.username } - }); - - res.send(!!exists); -}); - app.post("/user", auth, async (req, res) => { try { let { user } = req; @@ -303,6 +291,7 @@ app.post("/accounts", auth, async (req, res) => { let login = async (req, res) => { try { const { params, sig, key, password, username, token: twofa } = req.body; + l("logging in", username); if (sig) { const { callback } = params; @@ -340,18 +329,17 @@ let login = async (req, res) => { l( "login", username, - req.headers["x-forwarded-for"] || req.connection.remoteAddress + req.headers["x-forwarded-for"] || req.socket.remoteAddress ); let payload = { username, uuid: user.uuid }; let token = jwt.sign(payload, config.jwt); res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); - user.accounts = await user.getAccounts(); - user.keys = await user.getKeys(); user = pick(user, ...whitelist); res.send({ user, token }); } catch (e) { - err("login error", e.message, req.connection.remoteAddress); + console.log(e); + err("login error", e.message, req.socket.remoteAddress); res.code(401).send({}); } }; @@ -424,35 +412,13 @@ app.post("/otpsecret", auth, async function(req, res) { } }); -app.get("/contacts", auth, async function(req, res) { - try { - let contacts = await db.Payment.findAll({ - where: { - user_id: req.user.id, - with_id: { [Sequelize.Op.ne]: null } - }, - include: { - attributes: ["username", "profile", "uuid"], - model: db.User, - as: "with" - }, - attributes: [ - [Sequelize.fn("max", Sequelize.col("payments_model.createdAt")), "last"] - ], - group: ["with_id"], - order: [[{ model: db.User, as: "with" }, "username", "ASC"]] - }); - - res.send( - contacts.map(c => { - c = c.get({ plain: true }); - return { ...c.with, last: c.last }; - }) - ); - } catch (e) { - console.log(e); - res.code(500).send(e.message); - } +app.get("/contacts", auth, async function({ user: { uuid } }, res) { + let payments = await g(`${uuid}:payments`) || []; + res.send( + Promise.all( + [...new Set(payments.map(p => p.with_id))].map(id => g(`user:${id}`)) + ) + ); }); app.post("/checkPassword", auth, async function(req, res) { -- cgit v1.2.3 From 8a114c50efaac41767042766f05390aa906da157 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 13 Jan 2023 20:27:18 -0800 Subject: migrating to redis --- docker-compose.yml | 13 +++- index.js | 30 ++++---- lib/nostr.js | 1 - lib/passport.js | 5 +- lib/register.js | 15 +--- lib/sockets.js | 26 +++---- lib/utils.js | 21 +----- lib/whitelist.js | 1 - package.json | 2 +- routes/invoices.js | 49 +++++------- routes/lightning/receive.js | 179 ++++++++++++++++---------------------------- routes/users.js | 26 ++++--- 12 files changed, 144 insertions(+), 224 deletions(-) (limited to 'lib') diff --git a/docker-compose.yml b/docker-compose.yml index 95516fe0..83214b92 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,6 @@ services: - rd - bc - cl - build: . command: "pnpm dev" environment: NODE_OPTIONS: "--es-module-specifier-resolution=node --experimental-modules --loader ./loader.js" @@ -32,8 +31,16 @@ services: volumes: - ./data/lightning:/root/.lightning restart: always + clb: + image: elementsproject/lightningd:latest + container_name: clb + environment: + LIGHTNINGD_NETWORK: regtest + volumes: + - ./data/lightningb:/root/.lightning + restart: always rd: - image: redis + image: eqalpha/keydb container_name: rd volumes: - ./data/redis:/data @@ -45,6 +52,6 @@ services: user: 100:100 volumes: - "./data/nostr/data:/usr/src/app/db" - - "./data/nostr/data.toml:/usr/src/app/config.toml" + - "./data/nostr/config.toml:/usr/src/app/config.toml" ports: - '8080:8080' diff --git a/index.js b/index.js index 580435e0..640f7389 100644 --- a/index.js +++ b/index.js @@ -1,20 +1,20 @@ -import app from "./app"; -import { l, err } from "./lib/logging"; +import app from "./app.js"; +import { l, err } from "$lib/logging"; -import "./lib/redis"; -import "./lib/sockets"; -import "./lib/passport"; -import "./lib/upload"; -import "./lib/rates"; -import "./lib/notifications"; -import "./lib/mail"; -import "./lib/nostr"; +import "$lib/redis"; +import "$lib/sockets"; +import "$lib/passport"; +import "$lib/upload"; +import "$lib/rates"; +import "$lib/notifications"; +import "$lib/mail"; +import "$lib/nostr"; -import "./routes/locations"; -import "./routes/invoices"; -import "./routes/payments"; -import "./routes/users"; -import "./routes/requests"; +import "$routes/locations"; +import "$routes/invoices"; +import "$routes/payments"; +import "$routes/users"; +import "$routes/requests"; let host = process.env.HOST || "0.0.0.0"; let port = process.env.PORT || 3119; diff --git a/lib/nostr.js b/lib/nostr.js index ad089bb5..08ed8b61 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -142,7 +142,6 @@ pool.on("event", async (relay, sub, ev) => { }; await redis.set(`user:${pubkey}`, JSON.stringify(user)); - await redis.set(`user:${user.username}`, JSON.stringify(user)); coinos.send(ev); } else if (sub.includes("follows")) { let pubkey = sub.split(":")[0]; diff --git a/lib/passport.js b/lib/passport.js index b559b2f9..25dc0103 100644 --- a/lib/passport.js +++ b/lib/passport.js @@ -34,8 +34,9 @@ fastifyPassport.use( }, async (payload, next) => { try { - let { uuid } = payload; - let user = await g(`user:${uuid}`); + let { id } = payload; + let user = await g(`user:${id}`); + user.haspin = !!user.pin; next(null, user); } catch (e) { console.log("payload", payload); diff --git a/lib/register.js b/lib/register.js index 9f18c15e..8ac5355f 100644 --- a/lib/register.js +++ b/lib/register.js @@ -14,8 +14,8 @@ export default async (user, ip, requireChallenge) => { if (!username) throw new Error("Username required"); username = username.replace(/ /g, ""); - let uuid = v4(); - user.uuid = uuid; + let id = v4(); + user.id = id; let exists = await g(`user:${username}`); if (exists) throw new Error(`Username ${username} taken`); @@ -24,8 +24,6 @@ export default async (user, ip, requireChallenge) => { user.password = await bcrypt.hash(password, 1); } - console.log("BOO"); - if (!config.ipxapi || ip.startsWith("127") || ip.startsWith("192")) { user.currency = "CAD"; } else { @@ -53,14 +51,9 @@ export default async (user, ip, requireChallenge) => { user.ip = ip; } - console.log("user uuid", user.uuid, user.username); + await s(`user:${id}`, user); + await s(`user:${user.username}`, id); - try { - await s(`user:${user.uuid}`, user); - await s(`user:${user.username}`, user); - } catch (e) { - console.log("HUH", e); - } l("new user", user.username); return user; }; diff --git a/lib/sockets.js b/lib/sockets.js index 0f343937..c94c29a0 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -8,7 +8,7 @@ import { differenceInSeconds, subSeconds } from "date-fns"; import fastifyWs from "@fastify/websocket"; import whitelist from "./whitelist"; import { l, err, warn } from "$lib/logging"; -import { getUser } from "$lib/utils"; +import { g } from "$lib/redis"; const code = 1000; @@ -92,24 +92,24 @@ app.register(async function(app) { if (!token || token === "null" || !jwt.decode(token)) return ws.close(code, `bad token ${token}`); - let { uuid } = jwt.decode(token); - if (!uuid) throw new Error("Invalid JWT token"); - console.log("UUID", uuid) - let user = await getUser(uuid); + let { id: user_id } = jwt.decode(token); + if (!user_id) throw new Error("Invalid JWT token"); + let user = await g(`user:${user_id}`); if (!user) throw new Error("User not found"); + user.haspin = !!user.pin; - if (!store.sockets[uuid]) store.sockets[uuid] = {}; - const existing = Object.keys(store.sockets[uuid]); + if (!store.sockets[user_id]) store.sockets[user_id] = {}; + const existing = Object.keys(store.sockets[user_id]); if (existing.length > 4) { - const p = existing.find(sid => sid !== id); - store.sockets[uuid][p].send(JSON.stringify({ type: "logout" })); + const p = existing.find(sid => sid !== user_id); + store.sockets[user_id][p].send(JSON.stringify({ type: "logout" })); setTimeout(() => { - if (store.sockets[uuid][p]) - store.sockets[uuid][p].close(code, "too many sockets"); + if (store.sockets[user_id][p]) + store.sockets[user_id][p].close(code, "too many sockets"); }, 5000); } - store.sockets[uuid][id] = ws; - names[id] = uuid; + store.sockets[user_id][id] = ws; + names[id] = user_id; ws.user = user; ws.send( diff --git a/lib/utils.js b/lib/utils.js index 559f8531..b556c46a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,7 +1,6 @@ import store from "$lib/store"; import { g } from "$lib/redis"; import config from "$config"; -import ln from "$lib/ln"; export const nada = () => {}; @@ -18,15 +17,6 @@ export const requirePin = async ({ body, user }) => { throw new Error("Invalid pin"); }; -export const getUser = async (uuid) => { - let user = g(`user:${uuid}`); - if (!user) throw new Error("user not found"); - - user.haspin = !!user.pin; - - return user; -}; - export const prod = process.env.NODE_ENV === "production"; export const fail = msg => { throw new Error(msg); @@ -35,14 +25,5 @@ export const fail = msg => { export const SATS = 100000000; export const toSats = n => Math.round(n * SATS); -export const derivePayRequest = async ({ amount, memo, tip }) => { - if (!tip) tip = 0; - let value = amount + tip; - - if (!memo) memo = "coinos"; - return ( - await ln.invoice(value ? `${value}sat` : "any", new Date(), memo, 360) - ).bolt11; -}; - export const uniq = (a, k) => [...new Map(a.map(x => [k(x), x])).values()]; +export const pick = (O, K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); diff --git a/lib/whitelist.js b/lib/whitelist.js index 523e9d37..49cc9af3 100644 --- a/lib/whitelist.js +++ b/lib/whitelist.js @@ -25,7 +25,6 @@ export default [ "symbol", "theme", "twofa", - "uuid", "unit", "username", "verified" diff --git a/package.json b/package.json index 408df40e..2c1b7258 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "private": true, "scripts": { - "start": "bun index.js", + "start": "node index.js", "dev": "nodemon index.js" }, "dependencies": { diff --git a/routes/invoices.js b/routes/invoices.js index ddd840ec..f7ab78cf 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -1,76 +1,67 @@ import app from "$app"; import store from "$lib/store"; import { optionalAuth } from "$lib/passport"; -import { err, l } from "$lib/logging"; -import { SATS, derivePayRequest } from "$lib/utils"; +import { l } from "$lib/logging"; import { emit } from "$lib/sockets"; -import { v4 } from "uuid"; import { rd, g, s } from "$lib/redis"; +import ln from "$lib/ln"; app.get("/invoice", async ({ query: { id } }, res) => { let invoice = await g(`invoice:${id}`); invoice.user = await g(`user:${invoice.user_id}`); + console.log("INV", invoice) res.send(invoice); }); app.post( "/invoice", optionalAuth, - async ( - { - body: { type = "bech32", liquidAddress, id, invoice, user, tx }, - user: me - }, - res - ) => { - let { - blindkey, - currency, - tip, - amount, - rate, - network, - request_id - } = invoice; - let text; + async ({ body: { invoice, user }, user: me }, res) => { + let { currency, tip, amount, rate, request_id } = invoice; if (amount < 0) throw new Error("amount out of range"); if (tip > 5 * amount || tip < 0) throw new Error("tip amount out of range"); tip = tip || 0; if (!user) user = me; - if (!user) throw new Error("user not provided"); + let user_id = await g(`user:${user.username}`); + user = await g(`user:${user_id}`); - user = await g(`user:${user.id}`); + if (!user) throw new Error("user not provided"); if (!currency) currency = user.currency; if (!rate) rate = store.rates[currency]; if (amount < 0) throw new Error("invalid amount"); + let { payment_hash: id, bolt11: text } = await ln.invoice( + amount ? `${amount + tip}sat` : "any", + new Date(), + "", + 3600 + ); + invoice = { ...invoice, - id: v4(), + id, amount, currency, rate, tip, - unconfidential, - user_id: user.id + user_id, + text }; - invoice.text = await derivePayRequest(invoice); - l( "creating invoice", user.username, - network, amount, tip, currency, `${text.substr(0, 8)}..${text.substr(-6)}` ); - await rd.lPush(`${id}:invoices`, invoice.id); + await s(`invoice:${id}`, invoice); + await rd.lPush(`${user.id}:invoices`, id); if (request_id) { let request = await g(`request:${request_id}`); diff --git a/routes/lightning/receive.js b/routes/lightning/receive.js index a778271b..a37dc716 100644 --- a/routes/lightning/receive.js +++ b/routes/lightning/receive.js @@ -6,132 +6,79 @@ import { callWebhook } from "$lib/webhooks"; import { computeConversionFee } from "./conversionFee"; import ln from "$lib/ln"; import { l, err, warn } from "$lib/logging"; +import { g, s, rd } from "$lib/redis"; + +let handle = async () => { + let { + payment_hash, + bolt11: request, + pay_index, + status, + msatoshi_received, + payment_preimage: secret + } = await ln.waitanyinvoice(await g("pay_index")); + + await s("pay_index", pay_index); + + let received = Math.round(msatoshi_received / 1000); -const handlePayment = async msg => { try { - l("incoming lightning payment", msg.received); + l("incoming lightning payment", received); - if (!msg.secret) return; + if (!secret) return; let account, total, user; - const invoice = await db.Invoice.findOne({ - where: { - text: msg.request - } - }); - - if (!invoice) - return warn("received lightning with no invoice", msg.request); - - await db.transaction(async transaction => { - const { text: hash, currency, memo, rate, tip, user_id } = invoice; - const amount = parseInt(msg.received) - tip; - if (amount < 0) throw new Error("amount out of range"); - - account = await db.Account.findOne({ - where: { - user_id, - asset: config.liquid.btcasset, - pubkey: null - }, - include: { - model: db.User, - as: "user" - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - ({ user } = account); - - let preimage = msg.secret; - - let payment = await db.Payment.create( - { - account_id: account.id, - user_id, - hash, - memo, - amount, - currency, - preimage, - rate, - received: true, - confirmed: true, - network: "lightning", - tip, - invoice_id: invoice.id - }, - { transaction } - ); - - total = amount + tip; - - invoice.status = "paid"; - await invoice.save({ transaction }); - - await invoice.increment({ received: total }, { transaction }); - await account.increment({ balance: total }, { transaction }); - // get the # of fee credits you would need to pay off this amount of bitcoin - await account.increment( - { lightning_credits: computeConversionFee(total) }, - { transaction } - ); - await account.reload({ transaction }); - await invoice.reload({ transaction }); - await payment.save({ transaction }); - store.payments.push(msg.request); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - payment.invoice = invoice.get({ plain: true }); - - callWebhook(invoice, payment); - - emit(user.username, "payment", payment); - emit(user.username, "account", payment.account); - notify(user, `Received ${total} SAT`); - - l( - "lightning payment received", - user.username, - payment.amount, - payment.tip - ); - }); + let invoice = await g(`invoice:${payment_hash}`); + if (!invoice) return warn("received lightning with no invoice", request); + + let { text: hash, currency, memo, rate, tip, user_id } = invoice; + let amount = parseInt(received) - tip; + if (amount < 0) throw new Error("amount out of range"); + + let payment = { + user_id, + hash, + memo, + amount, + currency, + secret, + rate, + tip, + invoice_id: invoice.id + }; + + total = amount + tip; + + invoice.status = "paid"; + invoice.received += total; + await s(`invoice:${payment_hash}`, invoice); + + let t = async () => { + await rd.watch(`user:${user_id}`); + user = await g(`user:${user_id}`); + user.balance += total; + let m = await rd.multi(); + await s(`user:${user.id}`, user); + if (!(await m.exec())) t(); + }; + await t(); + + payment.invoice = invoice; + callWebhook(invoice, payment); + + emit(user.username, "payment", payment); + notify(user, `Received ${total} SAT`); + + l("lightning payment received", user.username, payment.amount, payment.tip); } catch (e) { console.log(e); err("problem receiving lightning payment", e.message); } }; -const poll = async ln => { - try { - const wait = async i => { - const { - bolt11: request, - pay_index, - status, - msatoshi_received, - payment_preimage: secret - } = await ln.waitanyinvoice(i); - - let settled = status === "paid"; - let received = parseInt(msatoshi_received / 1000); - - await handlePayment({ - request, - received, - secret - }); - await wait(pay_index); - }; - - const { invoices } = await ln.listinvoices(); - await wait(Math.max(...invoices.map(i => i.pay_index).filter(n => n))); - } catch (e) { - err("problem waiting for c-lightning invoice", e.message); - } +let listen = async () => { + await handle(); + listen(); }; -poll(ln); +listen(); diff --git a/routes/users.js b/routes/users.js index 8f0b9106..97ff9dd4 100644 --- a/routes/users.js +++ b/routes/users.js @@ -3,7 +3,7 @@ import { g, s } from "$lib/redis"; import config from "$config"; import store from "$lib/store"; import { auth, optionalAuth } from "$lib/passport"; -import { getUser, nada, uniq, wait } from "$lib/utils"; +import { nada, pick, uniq, wait } from "$lib/utils"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { authenticator } from "otplib"; @@ -14,11 +14,9 @@ import register from "$lib/register"; import { requirePin } from "$lib/utils"; import got from "got"; -const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); - app.get("/me", auth, async (req, res) => { try { - res.send(pick(req.user, ...whitelist)); + res.send(pick(req.user, whitelist)); } catch (e) { console.log("problem fetching user", e); res.code(500).send(e.message); @@ -32,6 +30,9 @@ app.get("/users/:key", async ({ params: { key } }, res) => { } let user = await g(`user:${key}`); + if (typeof user === "string") { + user = await g(`user:${user}`); + } if (!user && key.length === 64) { user = { @@ -41,7 +42,6 @@ app.get("/users/:key", async ({ params: { key } }, res) => { }; } - console.log(user) if (!user) return res.code(500).send("User not found"); let whitelist = [ @@ -53,10 +53,10 @@ app.get("/users/:key", async ({ params: { key } }, res) => { "pubkey", "display", "prompt", - "uuid" + "id" ]; - res.send(pick(user, ...whitelist)); + res.send(pick(user, whitelist)); } catch (e) { console.log(e); res.code(500).send(e.message); @@ -78,7 +78,7 @@ app.post("/register", async (req, res) => { user = await register(user, ip, false); l("registered new user", username); - res.send(pick(user, ...whitelist)); + res.send(pick(user, whitelist)); } catch (e) { res.code(500).send(e.message); } @@ -308,7 +308,8 @@ let login = async (req, res) => { return; } - let user = await getUser(username); + let id = await g(`user:${username}`); + let user = await g(`user:${id}`); if ( !user || @@ -332,10 +333,11 @@ let login = async (req, res) => { req.headers["x-forwarded-for"] || req.socket.remoteAddress ); - let payload = { username, uuid: user.uuid }; + let payload = { username, id: user.id }; let token = jwt.sign(payload, config.jwt); res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); - user = pick(user, ...whitelist); + user = pick(user, whitelist); + console.log("SENDING", user, token) res.send({ user, token }); } catch (e) { console.log(e); @@ -413,7 +415,7 @@ app.post("/otpsecret", auth, async function(req, res) { }); app.get("/contacts", auth, async function({ user: { uuid } }, res) { - let payments = await g(`${uuid}:payments`) || []; + let payments = (await g(`${uuid}:payments`)) || []; res.send( Promise.all( [...new Set(payments.map(p => p.with_id))].map(id => g(`user:${id}`)) -- cgit v1.2.3 From 6bafea9bffe24d2227d269812a73a9a513d92ddf Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 13 Jan 2023 23:50:30 -0800 Subject: internal payments --- lib/sendInternal.js | 264 +++++++++++++++----------------------------- routes/invoices.js | 4 +- routes/lightning/receive.js | 10 +- routes/payments.js | 9 +- routes/send.js | 1 + routes/users.js | 159 ++++---------------------- 6 files changed, 130 insertions(+), 317 deletions(-) (limited to 'lib') diff --git a/lib/sendInternal.js b/lib/sendInternal.js index 5a8b0b9d..f801c9cd 100644 --- a/lib/sendInternal.js +++ b/lib/sendInternal.js @@ -6,197 +6,111 @@ import { v4 } from "uuid"; import { notify } from "$lib/notifications"; import { l, err } from "$lib/logging"; import { fail } from "$lib/utils"; +import ln from "$lib/ln"; +import { g, s, rd } from "$lib/redis"; const { HOSTNAME: hostname } = process.env; export default async ( - { amount, address, payreq, unconfidential, asset, memo, username, tip }, + { amount, address, payreq, unconfidential, memo, username, tip }, user ) => { amount = parseInt(amount); - if (!asset) asset = config.liquid.btcasset; - if (!amount || amount < 0) fail("Amount must be greater than zero"); - return db.transaction(async transaction => { - let account; - if (user.account.asset === asset && !user.account.pubkey) { - account = await db.Account.findOne({ - where: { - id: user.account.id, - pubkey: null - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - } else { - account = await db.Account.findOne({ - where: { - user_id: user.id, - asset, - pubkey: null - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - } - - if (!account) throw new Error("Account not found"); - - if (account.balance < amount) { - throw new Error("Insufficient funds"); - } - - let fee = 0; - - await account.decrement({ balance: amount }, { transaction }); - await account.reload({ transaction }); - - let params, recipient, payment; - - if (username) { - recipient = await db.User.findOne( - { - where: { username }, - include: { - model: db.Account, - as: "account" - } - }, - { transaction } - ); - - params = { - where: { user_id: recipient.id }, - order: [["id", "DESC"]] - }; - if (address) params.where.address = address; - else if (payreq) params.where.text = payreq; - let invoice = (address || payreq) && (await db.Invoice.findOne(params)); - - let a2; - let acc = { - user_id: recipient.id, - asset, - pubkey: null - }; - - if (recipient.account.asset === asset && !recipient.account.pubkey) - a2 = recipient.account; - else { - a2 = await db.Account.findOne({ - where: acc, - lock: transaction.LOCK.UPDATE, - transaction - }); - } - - if (a2) { - await a2.increment({ balance: amount }, { transaction }); - await a2.reload({ transaction }); - } else { - let name = asset.substr(0, 6); - let domain; - let ticker = asset.substr(0, 3).toUpperCase(); - let precision = 8; - - if (store.assets[asset]) { - ({ domain, ticker, precision, name } = store.assets[asset]); - } else { - const existing = await db.Account.findOne({ - where: { - asset - }, - order: [["id", "ASC"]], - limit: 1, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (existing) { - ({ domain, ticker, precision, name } = existing); - } - } - - acc = { ...acc, ...{ domain, ticker, precision, name } }; - acc.balance = amount; - acc.pending = 0; - acc.network = "liquid"; - a2 = await db.Account.create(acc, { transaction }); - } - - params = { - amount, - account_id: a2.id, - user_id: recipient.id, - rate: store.rates[recipient.currency], - currency: recipient.currency, - confirmed: true, - hash: `#${v4().substr(0, 6)} Payment from ${user.username}`, - memo, - network: "COINOS", - received: true, - with_id: user.id + let fee = 0; + let recipient; + + let t = async () => { + await rd.watch(`user:${user_id}`); + user = await g(`user:${user_id}`); + if (user.balance < amount) throw new Error("Insufficient funds"); + + user.balance -= amount; + let m = await rd.multi(); + await s(`user:${user.id}`, user); + + if (username && (recipient = await g(`user:${username}`))) { + let { payment_hash: h } = await ln.decode(payreq); + let invoice = payreq && (await g(`invoice:${h}`)); + + let t = async () => { + await rd.watch(`user:${recipient.id}`); + recipient = await g(`user:${recipient.id}`); + recipient.balance += amount; + let m = await rd.multi(); + await s(`user:${recipient.id}`, recipient); + if (!(await m.exec())) t(); }; + await t(); - if (invoice) { - let { tip } = invoice; - params.invoice_id = invoice.id; - if (tip) { - params.tip = tip; - params.amount -= tip; - } - - await invoice.increment({ received: amount }, { transaction }); - await invoice.reload({ transaction }); - } - - let p2 = await db.Payment.create(params, { transaction }); - - p2 = p2.get({ plain: true }); - p2.account = a2.get({ plain: true }); - if (invoice) p2.invoice = invoice.get({ plain: true }); - - emit(recipient.username, "payment", p2); - emit(recipient.username, "account", p2.account); - - l("received internal", recipient.username, amount); - notify(recipient, `Received ${amount} ${a2.ticker} sats`); - callWebhook(invoice, p2); + if (!(await m.exec())) t(); } - - params = { - amount: -amount, - account_id: account.id, + await t(); + + let p2 = { + id: v4(), + amount, + user_id: recipient.id, + rate: store.rates[recipient.currency], + currency: recipient.currency, memo, - user_id: user.id, - rate: store.rates[user.currency], - currency: user.currency, - confirmed: true, - with_id: recipient && recipient.id, - hash: `#${v4().substr(0, 6)} ${ - username ? `Payment to ${username}` : "Internal Transfer" - }`, - network: "COINOS" + type: "internal", + with_id: user.id, + created_at: Date.now() }; - if (!username) { - l("creating redeemable payment"); - params.redeemcode = v4(); - params.hash = `${hostname}/redeem/${params.redeemcode}`; - } - - payment = await db.Payment.create(params, { transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - - l("sent internal", user.username, -payment.amount); + if (invoice) { + let { tip } = invoice; + params.invoice_id = invoice.id; + if (tip) { + params.tip = tip; + params.amount -= tip; + } - emit(user.username, "payment", payment); - emit(user.username, "account", account); + invoice.received += amount; + await s(`invoice:${h}`, invoice); + } - return payment; - }); + if (invoice) p2.invoice = invoice; + await s(`payment:${p2.id}`, p2); + await rd.lPush(`${recipient.id}:payments`, p2.id); + + emit(recipient.username, "payment", p2); + + l("received internal", recipient.username, amount); + notify(recipient, `Received ${amount} sats`); + callWebhook(invoice, p2); + }; + + let p1 = { + id: v4(), + amount: -amount, + memo, + user_id: user.id, + rate: store.rates[user.currency], + currency: user.currency, + with_id: recipient && recipient.id, + hash: `#${v4().substr(0, 6)} ${ + username ? `Payment to ${username}` : "Internal Transfer" + }`, + type: "internal", + created_at: Date.now() + }; + + if (!username) { + l("creating redeemable payment"); + params.redeemcode = v4(); + params.hash = `${hostname}/redeem/${params.redeemcode}`; + } + + await s(`payment:${p1.id}`, p1); + await rd.lPush(`${user.id}:payments`, p1.id); + + l("sent internal", user.username, -p1.amount); + + emit(user.username, "payment", p1); + + return p1; }; diff --git a/routes/invoices.js b/routes/invoices.js index f7ab78cf..10a80b90 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -9,7 +9,6 @@ import ln from "$lib/ln"; app.get("/invoice", async ({ query: { id } }, res) => { let invoice = await g(`invoice:${id}`); invoice.user = await g(`user:${invoice.user_id}`); - console.log("INV", invoice) res.send(invoice); }); @@ -48,7 +47,8 @@ app.post( rate, tip, user_id, - text + text, + received: 0 }; l( diff --git a/routes/lightning/receive.js b/routes/lightning/receive.js index a37dc716..ffb0ad95 100644 --- a/routes/lightning/receive.js +++ b/routes/lightning/receive.js @@ -1,3 +1,4 @@ +import { v4 } from "uuid"; import store from "$lib/store"; import { emit } from "$lib/sockets"; import config from "$config"; @@ -35,7 +36,9 @@ let handle = async () => { let amount = parseInt(received) - tip; if (amount < 0) throw new Error("amount out of range"); + let id = v4(); let payment = { + id, user_id, hash, memo, @@ -44,7 +47,8 @@ let handle = async () => { secret, rate, tip, - invoice_id: invoice.id + invoice_id: invoice.id, + created_at: Date.now() }; total = amount + tip; @@ -64,6 +68,10 @@ let handle = async () => { await t(); payment.invoice = invoice; + + await s(`payment:${id}`, payment); + await rd.lPush(`${user.id}:payments`, id); + callWebhook(invoice, payment); emit(user.username, "payment", payment); diff --git a/routes/payments.js b/routes/payments.js index 7b094ccc..b328e763 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -1,6 +1,7 @@ import app from "$app"; import config from "$config"; import store from "$lib/store"; +import { g, rd } from "$lib/redis"; import { auth, adminAuth, optionalAuth } from "$lib/passport"; import fs from "fs"; @@ -22,7 +23,7 @@ app.post("/lightning/send", auth, lnRoutes.send); app.get( "/payments", auth, - async ({ uuid, query: { start, end, limit, offset } }, res) => { + async ({ user: { id }, query: { start, end, limit, offset } }, res) => { if (limit) limit = parseInt(limit); if (offset) offset = parseInt(offset); @@ -30,8 +31,10 @@ app.get( // if (start) where.createdAt[Op.gte] = new Date(parseInt(start)); // if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); - let payments = await redis.get(`${uuid}:payments`); - res.send({ payments, total }); + let payments = (await rd.lRange(`${id}:payments`, 0, -1)) || []; + payments = await Promise.all(payments.map(id => g(`payment:${id}`))); + payments = payments.filter(p => p); + res.send({ payments, total: payments.length }); } ); diff --git a/routes/send.js b/routes/send.js index 591b5ab7..3f71a496 100644 --- a/routes/send.js +++ b/routes/send.js @@ -8,6 +8,7 @@ export default async (req, res, next) => { let payment = await sendInternal(req.body, req.user); res.send(payment); } catch (e) { + console.log(e) err("problem sending internal payment", e.message); return res.code(500).send(e.message); } diff --git a/routes/users.js b/routes/users.js index 97ff9dd4..c0324803 100644 --- a/routes/users.js +++ b/routes/users.js @@ -84,72 +84,53 @@ app.post("/register", async (req, res) => { } }); -app.post("/disable2fa", auth, async (req, res) => { - try { - let { - user, - body: { token } - } = req; - - if (user.twofa && !authenticator.check(token, user.otpsecret)) { - return res.code(401).send("2fa required"); - } - - user.twofa = false; - await user.save(); - emit(user.username, "user", user); - emit(user.username, "otpsecret", user.otpsecret); - l("disabled 2fa", user.username); - res.send({}); - } catch (e) { - res.code(500).send("Problem disabling 2fa"); +app.post("/disable2fa", auth, async ({ user, body: { token } }, res) => { + let { id, twofa, username, otpsecret } = user; + if (twofa && !authenticator.check(token, otpsecret)) { + return res.code(401).send("2fa required"); } + + user.twofa = false; + await s(`user:${id}`, user); + emit(username, "user", user); + emit(username, "otpsecret", user.otpsecret); + l("disabled 2fa", username); + res.send({}); }); -app.post("/2fa", auth, async (req, res) => { - let { user } = req; - try { - const isValid = authenticator.check(req.body.token, req.user.otpsecret); - if (isValid) { - user.twofa = true; - user.save(); - emit(user.username, "user", req.user); - } else { - return res.code(500).send("Invalid token"); - } - } catch (e) { - err("error setting up 2fa", e); +app.post("/2fa", auth, async ({ user, body: { token } }, res) => { + let { id, otpsecret, username } = user; + const isValid = authenticator.check(token, otpsecret); + if (isValid) { + user.twofa = true; + await s(`user:${id}`, user); + emit(username, "user", user); + } else { + return res.code(500).send("Invalid token"); } - l("enabled 2fa", user.username); + l("enabled 2fa", username); res.send({}); }); -app.post("/user", auth, async (req, res) => { +app.post("/user", auth, async ({ user, body: { confirm, password, pin, newpin, username }}, res) => { try { let { user } = req; l("updating user", user.username); - let { confirm, password, pin, newpin, username } = req.body; - if (user.pin && !(pin === user.pin)) throw new Error("Pin required"); if (typeof newpin !== "undefined") user.pin = newpin; if (!user.pin) user.pin = null; let exists; - - if (username) - exists = await db.User.findOne({ - where: { username } - }); + if (username) exists = await g(`user:${username}`); let token; if (user.username !== username && exists) { err("username taken", username, user.username, exists.username); throw new Error("Username taken"); } else if (username) { - store.sockets[username] = store.sockets[user.username]; if (user.username !== username) l("changing username", user.username, username); user.username = username; @@ -195,99 +176,6 @@ app.post("/user", auth, async (req, res) => { } }); -app.post("/keys", auth, async (req, res) => { - const { key: hex } = req.body; - const key = await db.Key.create({ - user_id: req.user.id, - hex - }); - emit(req.user.username, "key", key); - res.send(key); -}); - -app.post("/keys/delete", auth, async (req, res) => { - const { hex } = req.body; - ( - await db.Key.findOne({ - where: { - user_id: req.user.id, - hex - } - }) - ).destroy(); -}); - -app.post("/updateSeeds", auth, async (req, res) => { - let { user } = req; - let { seeds } = req.body; - let keys = Object.keys(seeds); - - for (let i = 0; i < keys.length; i++) { - let id = keys[i]; - let seed = seeds[id]; - await db.Account.update( - { seed }, - { - where: { id, user_id: user.id } - } - ); - - const account = await db.Account.findOne({ - where: { id } - }); - - emit(user.username, "account", account); - } - - res.send({}); -}); - -app.post("/accounts/delete", auth, async (req, res) => { - const { id } = req.body; - const account = await db.Account.findOne({ where: { id } }); - if (account) await account.destroy(); - res.send({}); -}); - -app.post("/accounts", auth, async (req, res) => { - const { - name, - seed, - pubkey, - ticker, - precision, - path, - privkey, - network - } = req.body; - const { user } = req; - - let account = await db.Account.create({ - user_id: user.id, - asset: config.liquid.btcasset, - balance: 0, - pending: 0, - name, - ticker, - precision, - pubkey, - privkey, - seed, - path, - network - }); - - emit(user.username, "account", account); - - if (pubkey) user.index++; - user.account_id = account.id; - await user.save(); - user.account = account; - emit(user.username, "user", user); - - res.send(account); -}); - let login = async (req, res) => { try { const { params, sig, key, password, username, token: twofa } = req.body; @@ -337,7 +225,6 @@ let login = async (req, res) => { let token = jwt.sign(payload, config.jwt); res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); user = pick(user, whitelist); - console.log("SENDING", user, token) res.send({ user, token }); } catch (e) { console.log(e); -- cgit v1.2.3 From 91e847126c832efa25ab0bf8d6f3b7e537378fa7 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 16 Jan 2023 23:40:25 -0800 Subject: shuffle things around --- app.js | 38 ++- docker-compose.yml | 14 + index.js | 87 +++++-- lib/auth.js | 37 +++ lib/binance.js | 33 --- lib/db.js | 14 + lib/lightning.js | 0 lib/locations.js | 20 ++ lib/mail.js | 29 --- lib/nostr.js | 80 +++--- lib/passport.js | 79 ------ lib/rates.js | 87 +++---- lib/redis.js | 15 -- lib/register.js | 2 +- lib/send.js | 201 -------------- lib/sendInternal.js | 136 +++++----- lib/sockets.js | 93 +++---- lib/store.js | 1 - lib/upload.js | 36 +-- lib/utils.js | 2 +- routes/email.js | 24 ++ routes/events.js | 2 +- routes/invoices.js | 33 ++- routes/lightning/conversionFee.js | 12 - routes/lightning/index.js | 11 - routes/lightning/invoice.js | 25 -- routes/lightning/parse.js | 19 -- routes/lightning/query.js | 19 -- routes/lightning/receive.js | 92 ------- routes/lightning/send.js | 17 -- routes/locations.js | 28 +- routes/nostr.js | 161 ++++++++++++ routes/payments.js | 302 ++++++++++----------- routes/rates.js | 11 + routes/requests.js | 72 +++-- routes/send.js | 2 +- routes/users.js | 536 ++++++++++++++++++-------------------- 37 files changed, 1035 insertions(+), 1335 deletions(-) create mode 100644 lib/auth.js delete mode 100644 lib/binance.js create mode 100644 lib/db.js create mode 100644 lib/lightning.js create mode 100644 lib/locations.js delete mode 100644 lib/mail.js delete mode 100644 lib/passport.js delete mode 100644 lib/redis.js delete mode 100644 lib/send.js create mode 100644 routes/email.js delete mode 100644 routes/lightning/conversionFee.js delete mode 100644 routes/lightning/index.js delete mode 100644 routes/lightning/invoice.js delete mode 100644 routes/lightning/parse.js delete mode 100644 routes/lightning/query.js delete mode 100644 routes/lightning/receive.js delete mode 100644 routes/lightning/send.js create mode 100644 routes/nostr.js create mode 100644 routes/rates.js (limited to 'lib') diff --git a/app.js b/app.js index 89ba136b..af2bd59f 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,40 @@ +import config from "$config"; import fastify from "fastify"; +import fastifyWs from "@fastify/websocket"; +import fastifyMultipart from "@fastify/multipart"; +import fastifyStatic from "@fastify/static"; +import fastifyPassport from "@fastify/passport"; +import fastifySecureSession from "@fastify/secure-session"; + +import path from "path"; import pino from "pino"; -export default fastify({ logger: pino(), disableRequestLogging: true, maxParamLength: 500 }); +import { jwtStrategy } from "$lib/auth"; +import { socketServer } from "$lib/sockets"; + +const app = fastify({ + logger: pino(), + disableRequestLogging: true, + maxParamLength: 500 +}); + +app.register(fastifySecureSession, { + key: Buffer.from(config.jwt, "hex") +}); + +app.register(fastifyPassport.initialize()); +app.register(fastifyPassport.secureSession()); + +fastifyPassport.use("jwt", jwtStrategy); + +app.register(fastifyMultipart); + +app.register(fastifyStatic, { + root: path.join("/app/uploads"), + prefix: "/public/" +}); + +app.register(fastifyWs); +app.register(socketServer); + +export default app; diff --git a/docker-compose.yml b/docker-compose.yml index 83214b92..02432156 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,20 @@ services: volumes: - ./data/lightningb:/root/.lightning restart: always + db: + image: eqalpha/keydb + container_name: db + volumes: + - ./data/db:/data + restart: always + clc: + image: elementsproject/lightningd:latest + container_name: clc + environment: + LIGHTNINGD_NETWORK: regtest + volumes: + - ./data/lightningc:/root/.lightning + restart: always rd: image: eqalpha/keydb container_name: rd diff --git a/index.js b/index.js index 640f7389..7d4aa135 100644 --- a/index.js +++ b/index.js @@ -1,24 +1,71 @@ -import app from "./app.js"; -import { l, err } from "$lib/logging"; - -import "$lib/redis"; -import "$lib/sockets"; -import "$lib/passport"; -import "$lib/upload"; -import "$lib/rates"; -import "$lib/notifications"; -import "$lib/mail"; -import "$lib/nostr"; - -import "$routes/locations"; -import "$routes/invoices"; -import "$routes/payments"; -import "$routes/users"; -import "$routes/requests"; +import app from "$app"; +import { auth, optional } from "$lib/auth"; +import { getLocations } from "$lib/locations"; +import { getRates, sendRates } from "$lib/rates"; +import { sendHeartbeat } from "$lib/sockets"; + +import email from "$routes/email"; +import locations from "$routes/locations"; +import nostr from "$routes/nostr"; +import rates from "$routes/rates"; +import invoices from "$routes/invoices"; +import users from "$routes/users"; +import payments from "$routes/payments"; +import requests from "$routes/requests"; + +getLocations(); +getRates(); + +setInterval(sendRates, 1000); +setInterval(sendHeartbeat, 2000); + +app.post("/email", email.send); + +app.get("/rate", rates.last); +app.get("/rates", rates.index); + +app.get("/nostr.json", nostr.identities); +app.get("/:pubkey/followers", nostr.followers); +app.get("/:pubkey/follows", nostr.follows); +app.get("/:pubkey/notes", nostr.notes); +app.get("/event/:id", nostr.event); +app.post("/event", nostr.broadcast); + +app.get("/locations", locations.list); + +app.get("/invoice", invoices.get); +app.post("/invoice", optional, invoices.create); + +app.post("/send", auth, payments.send); +app.get("/payments", auth, payments.list); +app.get("/payments/:hash", auth, payments.get); +app.post("/lightning/send", auth, payments.sendLightning); + +app.get("/voucher/:id", payments.voucher); +app.post("/redeem", optional, payments.redeem); + +app.get("/me", auth, users.me); +app.get("/users/:key", users.get); +app.post("/register", users.create); +app.post("/disable2fa", users.disable2fa); +app.post("/2fa", auth, users.enable2fa); +app.post("/user", auth, users.update); +app.post("/upload/:type", auth, users.upload); + +app.post("/login", users.login); +app.post("/logout", optional, users.logout); + +app.post("/subscribe", auth, users.subscribe); +app.post("/password", auth, users.password); +app.post("/otpsecret", auth, users.otpsecret); +app.get("/contacts", auth, users.contacts); + +app.get("/request/:id", requests.get); +app.get("/requests", requests.list); +app.post("/requests", requests.create); +app.post("/requests/delete", requests.destroy); let host = process.env.HOST || "0.0.0.0"; let port = process.env.PORT || 3119; -app.listen({ host, port }, (e, h) => { - if (e) return err("Failed to start:", e.message); -}); +app.listen({ host, port }); diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 00000000..9720cd83 --- /dev/null +++ b/lib/auth.js @@ -0,0 +1,37 @@ +import fastifyPassport from "@fastify/passport"; +import { g } from "$lib/db"; +import config from "$config"; +import jwt from "passport-jwt"; + +export const optional = { + preValidation: fastifyPassport.authenticate( + "jwt", + { session: false }, + function(req, res, err, user, info) { + req.user = user; + } + ) +}; + +export const auth = { + preValidation: fastifyPassport.authenticate("jwt", { + authInfo: false, + session: false + }) +}; + +export const requirePin = async ({ body, user }) => { + if (!user || (user.pin && user.pin !== body.pin)) + throw new Error("Invalid pin"); +}; + +export const jwtStrategy = new jwt.Strategy( + { + jwtFromRequest: jwt.ExtractJwt.fromExtractors([ + jwt.ExtractJwt.fromAuthHeaderAsBearerToken(), + req => (req.cookies ? req.cookies.token : null) + ]), + secretOrKey: config.jwt + }, + async (payload, next) => next(null, await g(`user:${payload.id}`)) +); diff --git a/lib/binance.js b/lib/binance.js deleted file mode 100644 index e583d312..00000000 --- a/lib/binance.js +++ /dev/null @@ -1,33 +0,0 @@ -import app from "$app"; -import WebSocket from "ws"; -import { err } from "./logging"; -import store from "./store"; - -export default () => { - const binance = new WebSocket( - "wss://stream.binance.com:9443/ws/btcusdt@ticker" - ); - - binance.onmessage = async function(event) { - try { - let msg = JSON.parse(event.data); - - if (!store.fx) return; - - Object.keys(store.fx).map(symbol => { - store.rates[symbol] = msg.c * store.fx[symbol]; - }); - - store.last = msg.c; - } catch (e) { - err("binance message error", e.message); - } - }; - - binance.onerror = async function(error) { - console.log(error); - err("binance socket error", error.message); - }; - - return binance; -}; diff --git a/lib/db.js b/lib/db.js new file mode 100644 index 00000000..76653c81 --- /dev/null +++ b/lib/db.js @@ -0,0 +1,14 @@ +import { createClient } from "redis"; + +export const db = createClient({ + url: "redis://rd" +}); + +db.on("error", err => console.log("db error", err)); + +await db.connect(); + +export default db; + +export let g = async k => JSON.parse(await db.get(k)); +export let s = (k, v) => db.set(k, JSON.stringify(v)); diff --git a/lib/lightning.js b/lib/lightning.js new file mode 100644 index 00000000..e69de29b diff --git a/lib/locations.js b/lib/locations.js new file mode 100644 index 00000000..bd2cfb5e --- /dev/null +++ b/lib/locations.js @@ -0,0 +1,20 @@ +import got from "got"; +import { s } from "$lib/db"; + +export let getLocations = async () => { + try { + let locations = await got( + "https://api.btcmap.org/v2/elements?updated_since=2022-09-19" + ).json(); + + locations = locations.filter( + l => + l["osm_json"].tags && l["osm_json"].tags["payment:coinos"] === "yes" + ) + + + await s("locations", locations); + } catch (e) {} + + setTimeout(getLocations, 60000); +}; diff --git a/lib/mail.js b/lib/mail.js deleted file mode 100644 index 966ce578..00000000 --- a/lib/mail.js +++ /dev/null @@ -1,29 +0,0 @@ -import app from "$app"; -import config from "$config"; - -app.post("/email", async (req, res) => { - try { - try { - var postmark = await import("postmark"); - - var client = new postmark.ServerClient(config.postmark); - - await client.sendEmail({ - From: "support@coinos.io", - To: "support@coinos.io", - Subject: req.body.subject || "Email Signup", - HtmlBody: JSON.stringify(req.body), - TextBody: JSON.stringify(req.body), - MessageStream: "outbound" - }); - - res.send({ ok: true }); - } catch (e) { - console.log("problem sending email", e); - res.code(500).send(e.message); - } - } catch (e) { - console.log(e); - res.code(500).send(e.message); - } -}); diff --git a/lib/nostr.js b/lib/nostr.js index 08ed8b61..93370587 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -3,16 +3,15 @@ import config from "$config"; import { Relay, RelayPool } from "nostr"; import { broadcast } from "$lib/sockets"; import store from "$lib/store"; -import redis from "$lib/redis"; +import { g, s, db } from "$lib/db"; import { nada, wait } from "$lib/utils"; export let pool; try { pool = RelayPool(config.relays); -} catch(e) { - console.log(e) -} - +} catch (e) { + console.log(e); +} pool.on("open", relay => { if (relay.url.includes(config.relay)) coinos = relay; @@ -32,7 +31,7 @@ export let q = async ( let start = Date.now(); let seen = []; let rejected; - query.since = await redis.get(`since:${sub}`); + query.since = await g(`since:${sub}`); if (now() - query.since < since) return r(); let done = { [sub]: [] }; @@ -56,7 +55,7 @@ export let q = async ( clearTimeout(this.timer); delete timeouts[sub]; pool.unsubscribe(sub); - await redis.set(`since:${sub}`, now()); + await s(`since:${sub}`, now()); }, extend(ev) { if (ev) { @@ -93,7 +92,7 @@ export let q = async ( let seen = []; let getUser = async pubkey => - JSON.parse(await redis.get(`user:${pubkey}`)) || { + JSON.parse(await g(`user:${pubkey}`)) || { username: pubkey.substr(0, 6), pubkey, anon: true @@ -141,37 +140,38 @@ pool.on("event", async (relay, sub, ev) => { updated: ev.created_at }; - await redis.set(`user:${pubkey}`, JSON.stringify(user)); + await s(`user:${pubkey}`, JSON.stringify(user)); coinos.send(ev); } else if (sub.includes("follows")) { let pubkey = sub.split(":")[0]; - await redis.set(`${pubkey}:follows`, JSON.stringify(ev.tags)); + await s(`${pubkey}:follows`, JSON.stringify(ev.tags)); for (let f of ev.tags) { let [_, followPubkey] = f; - await redis.sAdd(`${followPubkey}:followers`, pubkey); + await db.sAdd(`${followPubkey}:followers`, pubkey); } coinos.send(ev); } else if (sub.includes("followers")) { let followed = sub.split(":")[0]; let { pubkey } = ev; - await redis.sAdd(`${followed}:followers`, pubkey); + await db.sAdd(`${followed}:followers`, pubkey); coinos.send(ev); } else if (sub.includes("notes")) { let pubkey = sub.split(":")[0]; - await redis.sAdd(pubkey, ev.id); - await redis.set(`ev:${ev.id}`, JSON.stringify(ev)); + await db.sAdd(pubkey, ev.id); + await s(`ev:${ev.id}`, JSON.stringify(ev)); } } catch (e) { console.log(e); } }); -app.get("/event/:id", async (req, res) => { +export default { +async event (req, res) { try { - let event = JSON.parse(await redis.get(`ev:${req.params.id}`)); + let event = JSON.parse(await g(`ev:${req.params.id}`)); let { pubkey } = event; - event.user = JSON.parse(await redis.get(`user:${pubkey}`)) || { + event.user = JSON.parse(await g(`user:${pubkey}`)) || { username: pubkey.substr(0, 6), pubkey, anon: true, @@ -183,13 +183,13 @@ app.get("/event/:id", async (req, res) => { } catch (e) { res.code(500).send("Problem fetching event"); } -}); +}, -app.get("/:pubkey/notes", async (req, res) => { +async notes (req, res) { try { let { pubkey } = req.params; - let user = JSON.parse(await redis.get(`user:${pubkey}`)); + let user = JSON.parse(await g(`user:${pubkey}`)); if (!user) { user = await db.User.findOne({ @@ -215,12 +215,12 @@ app.get("/:pubkey/notes", async (req, res) => { { since: 0 } ).catch(nada); - await redis.set(`user:${pubkey}`, JSON.stringify(user)); + await g(`user:${pubkey}`, JSON.stringify(user)); - let ids = await redis.sMembers(pubkey); + let ids = await db.sMembers(pubkey); let events = ids.length - ? (await redis.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) + ? (await db.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) : []; res.send(events.map(e => ({ ...e, user }))); @@ -228,15 +228,15 @@ app.get("/:pubkey/notes", async (req, res) => { console.log(e); res.code(500).send("problem fetching user events"); } -}); +}, -app.post("/event", async (req, res) => { +async broadcast (req, res) { let { event } = req.body; pool.send(["EVENT", event]); res.send(event); -}); +}, -app.get("/:pubkey/follows", async (req, res) => { +async follows (req, res) { try { let { pubkey } = req.params; let { tagsonly } = req.query; @@ -252,7 +252,7 @@ app.get("/:pubkey/follows", async (req, res) => { { timeout: 60000, eager: 60000 } ).catch(nada); - let tags = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; + let tags = JSON.parse(await g(`${pubkey}:follows`)) || []; if (tagsonly) return res.send(tags); let follows = []; @@ -265,7 +265,7 @@ app.get("/:pubkey/follows", async (req, res) => { authors: [pubkey] }).catch(nada); - let user = JSON.parse(await redis.get(`user:${pubkey}`)); + let user = JSON.parse(await g(`user:${pubkey}`)); if (!user) user = { @@ -287,14 +287,14 @@ app.get("/:pubkey/follows", async (req, res) => { } }); -app.get("/:pubkey/followers", async (req, res) => { +async (req, res) followers { try { let { pubkey } = req.params; let pubkeys = [ ...new Set([ ...(await got(`${config.nostr}/followers?pubkey=${pubkey}`).json()), - ...(await redis.sMembers(`${pubkey}:followers`)) + ...(await db.sMembers(`${pubkey}:followers`)) ]) ]; @@ -307,7 +307,7 @@ app.get("/:pubkey/followers", async (req, res) => { ).catch(nada); for (let pubkey of pubkeys) { - let user = JSON.parse(await redis.get(`user:${pubkey}`)); + let user = JSON.parse(await g(`user:${pubkey}`)); if (!user || !user.updated) { q(`${pubkey}:profile:f2`, { limit: 1, @@ -315,7 +315,7 @@ app.get("/:pubkey/followers", async (req, res) => { authors: [pubkey] }).catch(nada); - user = JSON.parse(await redis.get(`user:${pubkey}`)); + user = JSON.parse(await g(`user:${pubkey}`)); } if (!user) @@ -336,11 +336,15 @@ app.get("/:pubkey/followers", async (req, res) => { console.log(e); res.code(500).send(e && e.message); } -}); +}, -app.get('/nostr.json', async (req, res) => { +async identities (req, res) { res.send({ - names: { adam: "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8", asoltys: "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8" } + names: { + adam: "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8", + asoltys: + "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8" + } }); -}); - +} +} diff --git a/lib/passport.js b/lib/passport.js deleted file mode 100644 index 25dc0103..00000000 --- a/lib/passport.js +++ /dev/null @@ -1,79 +0,0 @@ -import app from "$app"; -import jwt from "passport-jwt"; -import fastifyPassport from "@fastify/passport"; -import fastifySecureSession from "@fastify/secure-session"; -import config from "$config"; -import fs from "fs"; -import path from "path"; -import { err } from "$lib/logging"; -import { g } from "$lib/redis"; - -app.register(fastifySecureSession, { - key: Buffer.from(config.jwt, "hex") -}); - -app.register(fastifyPassport.initialize()); -app.register(fastifyPassport.secureSession()); - -function cookieExtractor(req) { - if (req && req.cookies) { - return req.cookies["token"]; - } - return null; -} - -fastifyPassport.use( - "jwt", - new jwt.Strategy( - { - jwtFromRequest: jwt.ExtractJwt.fromExtractors([ - jwt.ExtractJwt.fromAuthHeaderAsBearerToken(), - cookieExtractor - ]), - secretOrKey: config.jwt - }, - async (payload, next) => { - try { - let { id } = payload; - let user = await g(`user:${id}`); - user.haspin = !!user.pin; - next(null, user); - } catch (e) { - console.log("payload", payload); - if (!e.message.includes("locked")) { - console.log(e); - err("problem logging in", e); - } - next(null, { username: "guest" }); - } - } - ) -); - -export const optionalAuth = { - preValidation: fastifyPassport.authenticate( - "jwt", - { session: false }, - function(req, res, err, user, info) { - req.user = user; - } - ) -}; - -export const adminAuth = { - preValidation: fastifyPassport.authenticate( - "jwt", - { session: false }, - function(req, res, err, user, info) { - if (!(user && user.admin)) return res.code(401).send("Unauthorized"); - req.user = user; - } - ) -}; - -export const auth = { - preValidation: fastifyPassport.authenticate("jwt", { - authInfo: false, - session: false - }) -}; diff --git a/lib/rates.js b/lib/rates.js index 8064a117..c7d6918c 100644 --- a/lib/rates.js +++ b/lib/rates.js @@ -1,69 +1,60 @@ -import app from "$app"; import got from "got"; import config from "$config"; -import fs from "fs"; -import binance from "./binance"; import { broadcast } from "./sockets"; import store from "./store"; import { err, warn } from "$lib/logging"; +import { g, s } from "$lib/db"; +import WebSocket from "ws"; -let b = binance(); -setInterval(() => { - b.terminate(); - b = binance(); -}, 360000); +let ws; +let connect = () => { + if (ws) ws.terminate(); + ws = new WebSocket("wss://stream.binance.com:9443/ws/btcusdt@ticker"); -let fetchRates = async () => { - let date, fx; + ws.onmessage = async function(event) { + try { + let msg = JSON.parse(event.data); - try { - ({ date, fx } = JSON.parse(fs.readFileSync("data/fx"))); - } catch (e) { - warn("Failed to read fx file"); - } + if (!store.fx) return; - let today = JSON.stringify(new Date()) - .replace(/T.*/, "") - .replace(/"/, ""); + Object.keys(store.fx).map(symbol => { + store.rates[symbol] = msg.c * store.fx[symbol]; + }); + + store.last = msg.c; + } catch (e) { + err("binance message error", e.message); + } + }; + + ws.onerror = async function(error) { + err("binance socket error", error.message); + }; + + setTimeout(connect, 360000); + return ws; +}; - if (date !== today) { +export let sendRates = () => broadcast("rate", store.last); +export let getRates = async () => { + connect(); + + let date, fx; + let rates = await g("rates"); + if (rates) ({ date, fx } = rates); + + if (Date.now() - date > 24 * 60 * 60 * 1000) { try { let { rates: fx } = await got( `https://api.exchangerate.host/latest?base=USD&v={(new Date()).toISOString().split('T')[0]}` ).json(); - fs.writeFileSync("data/fx", JSON.stringify({ date, fx })); + await s("rates", { date, fx }); } catch (e) { - try { - let { rates: fx } = await got( - `http://data.fixer.io/api/latest?access_key=${config.fixer}` - ).json(); - - let USD = fx["USD"]; - Object.keys(fx).map(k => { - fx[k] = parseFloat((fx[k] / fx["USD"]).toFixed(6)); - }); - - fs.writeFileSync("data/fx", JSON.stringify({ date, fx })); - } catch (e) { - err("error fetching rates", e.message); - } + err("error fetching rates", e.message); } } store.fx = fx; - setTimeout(fetchRates, 7200000); + setTimeout(getRates, 7200000); }; -fetchRates(); - -app.get("/rate", async (req, res, next) => { - res.send(store.last); -}); - -app.get("/rates", async (req, res, next) => { - res.send(store.rates); -}); - -setInterval(() => { - broadcast("rate", store.last); -}, 1000); diff --git a/lib/redis.js b/lib/redis.js deleted file mode 100644 index 6aec2af7..00000000 --- a/lib/redis.js +++ /dev/null @@ -1,15 +0,0 @@ -import { createClient } from "redis"; - -const redis = createClient({ - url: "redis://rd" -}); - -redis.on("error", err => console.log("redis error", err)); - -await redis.connect(); - -export default redis; - -export let g = async k => JSON.parse(await redis.get(k)); -export let s = (k, v) => redis.set(k, JSON.stringify(v)); -export let rd = redis; diff --git a/lib/register.js b/lib/register.js index 8ac5355f..63dccb38 100644 --- a/lib/register.js +++ b/lib/register.js @@ -4,7 +4,7 @@ import { authenticator } from "otplib"; import bcrypt from "bcrypt"; import { l } from "$lib/logging"; import { v4 } from "uuid"; -import { s, g } from "$lib/redis"; +import { s, g } from "$lib/db"; import { got } from "got"; export default async (user, ip, requireChallenge) => { diff --git a/lib/send.js b/lib/send.js deleted file mode 100644 index f378cfc1..00000000 --- a/lib/send.js +++ /dev/null @@ -1,201 +0,0 @@ -import config from "$config"; -import { emit } from "$lib/sockets"; -import store from "./store"; -import { - computeConversionFee, - conversionFeeReceiver -} from "../routes/lightning/conversionFee"; -import ln from "$lib/ln"; -import { l, warn, err } from "$lib/logging"; - -export default async (amount, memo, hash, user) => { - l("attempting lightning payment", user.username, amount, hash); - - // decode payreq with cl - if (!amount || payreq.satoshis > amount) amount = payreq.satoshis; - amount = parseInt(amount); - - if (store.seen.includes(hash)) { - warn("attempted to pay a paid invoice", user.username); - throw new Error("Invoice has been paid, can't pay again"); - } - - let error, fee, total, preimage, payment; - - let conversionFee = computeConversionFee(amount); - let conversionFeeDeduction; - await db.transaction(async transaction => { - l("starting lightning send transaction"); - let account; - if (user.account.asset === config.liquid.btcasset) - account = await db.Account.findOne({ - where: { - id: user.account.id, - pubkey: null - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - else - account = await db.Account.findOne({ - where: { - user_id: user.id, - asset: config.liquid.btcasset, - pubkey: null, - balance: { - [Op.gte]: amount - } - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - conversionFeeDeduction = Math.min(account.lightning_credits, conversionFee); - total = amount + conversionFee - conversionFeeDeduction; - - if (!account || account.balance < total) { - warn( - "user attempting to send more than they have", - user.id, - amount, - conversionFee - ); - throw new Error("Insufficient funds"); - } - - await account.decrement( - { balance: total, lightning_credits: conversionFeeDeduction }, - { transaction } - ); - await account.reload({ transaction }); - - l("debited account", account.id, total); - let tokens; - if (!payreq.satoshis) tokens = amount; - - let m; - let { msatoshi, msatoshi_sent, payment_preimage } = await ln.pay( - hash, - tokens ? `${tokens}sats` : undefined - ); - preimage = payment_preimage; - fee = parseInt((msatoshi_sent - msatoshi) / 1000); - - if (store.seen.includes(preimage)) { - warn("duplicate payment detected", preimage); - throw new Error("Duplicate payment detected"); - } - - store.seen.push(preimage); - store.seen.push(hash); - }); - - await db.transaction(async transaction => { - l("starting lightning fee transaction"); - - let account; - if (user.account.asset === config.liquid.btcasset) - account = await db.Account.findOne({ - where: { - id: user.account.id, - pubkey: null - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - else - account = await db.Account.findOne({ - where: { - user_id: user.id, - asset: config.liquid.btcasset, - pubkey: null, - balance: { - [Op.gte]: fee - } - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - l("deducting fee", fee); - await account.decrement({ balance: fee }, { transaction }); - await account.reload({ transaction }); - - if (conversionFeeDeduction) { - conversionFee -= conversionFeeDeduction; - } - - l("conversion fee", conversionFee); - - let receiverAccount = await db.Account.findOne({ - where: { - "$user.username$": conversionFeeReceiver - }, - include: [ - { - model: db.User, - as: "user" - } - ], - lock: transaction.LOCK.UPDATE, - transaction - }); - - let fee_payment; - let fee_payment_id = null; - if (conversionFee) { - await receiverAccount.increment( - { balance: conversionFee }, - { transaction } - ); - await receiverAccount.reload({ transaction }); - fee_payment = await db.Payment.create( - { - amount: conversionFee, - fee: 0, - memo: "Lightning conversion fee", - account_id: receiverAccount.id, - user_id: receiverAccount.user_id, - rate: store.rates[receiverAccount.user.currency], - currency: receiverAccount.user.currency, - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - fee_payment_id = fee_payment.id; - } - - payment = await db.Payment.create( - { - amount: -total, - account_id: account.id, - user_id: user.id, - hash, - memo, - preimage, - rate: store.rates[user.currency], - currency: user.currency, - confirmed: true, - network: "lightning", - fee, - fee_payment_id - }, - { transaction } - ); - - l("created payment record", payment.id); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - payment.fee_payment = fee_payment && fee_payment.get({ plain: true }); - - emit(user.username, "account", payment.account); - emit(user.username, "payment", payment); - - l("sent lightning", user.username, amount, total, fee); - }); - - return payment; -}; diff --git a/lib/sendInternal.js b/lib/sendInternal.js index f801c9cd..0ba0f0b4 100644 --- a/lib/sendInternal.js +++ b/lib/sendInternal.js @@ -7,12 +7,12 @@ import { notify } from "$lib/notifications"; import { l, err } from "$lib/logging"; import { fail } from "$lib/utils"; import ln from "$lib/ln"; -import { g, s, rd } from "$lib/redis"; +import { g, s, db } from "$lib/db"; const { HOSTNAME: hostname } = process.env; export default async ( - { amount, address, payreq, unconfidential, memo, username, tip }, + { amount, hash, memo, tip }, user ) => { amount = parseInt(amount); @@ -20,97 +20,77 @@ export default async ( if (!amount || amount < 0) fail("Amount must be greater than zero"); let fee = 0; - let recipient; + let invoice = await g(`invoice:${hash}`); let t = async () => { - await rd.watch(`user:${user_id}`); - user = await g(`user:${user_id}`); - if (user.balance < amount) throw new Error("Insufficient funds"); - + await db.watch(`user:${user.id}`); + user = await g(`user:${user.id}`); + if (user.balance < amount) fail("Insufficient funds"); user.balance -= amount; - let m = await rd.multi(); + let m = await db.multi(); await s(`user:${user.id}`, user); - if (username && (recipient = await g(`user:${username}`))) { - let { payment_hash: h } = await ln.decode(payreq); - let invoice = payreq && (await g(`invoice:${h}`)); - - let t = async () => { - await rd.watch(`user:${recipient.id}`); - recipient = await g(`user:${recipient.id}`); - recipient.balance += amount; - let m = await rd.multi(); - await s(`user:${recipient.id}`, recipient); - if (!(await m.exec())) t(); - }; - await t(); - - if (!(await m.exec())) t(); - } - await t(); - - let p2 = { - id: v4(), - amount, - user_id: recipient.id, - rate: store.rates[recipient.currency], - currency: recipient.currency, + let p1 = { + hash, + amount: -amount, memo, + uid: user.id, + rate: store.rates[user.currency], + currency: user.currency, type: "internal", - with_id: user.id, - created_at: Date.now() + created: Date.now() }; - if (invoice) { + await s(`payment:${hash}`, p1); + await db.lPush(`${user.id}:payments`, hash); + + l("sent internal", user.username, -p1.amount); + emit(user.id, "payment", p1); + + let { uid } = invoice; + if (uid !== user.id) { + let recipient = await g(`user:${uid}`); + + let u = async () => { + await db.watch(`user:${uid}`); + recipient = await g(`user:${uid}`); + recipient.balance += amount; + let m = await db.multi(); + await s(`user:${uid}`, recipient); + if (!(await m.exec())) u(); + }; + await u(); + + let p2 = { + hash, + amount, + uid: recipient.id, + rate: store.rates[recipient.currency], + currency: recipient.currency, + memo, + type: "internal", + with_id: user.id, + created: Date.now() + }; + let { tip } = invoice; - params.invoice_id = invoice.id; if (tip) { - params.tip = tip; - params.amount -= tip; + p2.tip = tip; + p2.amount -= tip; } invoice.received += amount; - await s(`invoice:${h}`, invoice); + await s(`invoice:${hash}`, invoice); + await s(`payment:${id}`, p2); + await db.lPush(`${recipient.id}:payments`, id); + + l("received internal", recipient.username, amount); + emit(recipient.username, "payment", p2); + notify(recipient, `Received ${amount} sats`); + callWebhook(invoice, p2); } - if (invoice) p2.invoice = invoice; - await s(`payment:${p2.id}`, p2); - await rd.lPush(`${recipient.id}:payments`, p2.id); - - emit(recipient.username, "payment", p2); - - l("received internal", recipient.username, amount); - notify(recipient, `Received ${amount} sats`); - callWebhook(invoice, p2); - }; - - let p1 = { - id: v4(), - amount: -amount, - memo, - user_id: user.id, - rate: store.rates[user.currency], - currency: user.currency, - with_id: recipient && recipient.id, - hash: `#${v4().substr(0, 6)} ${ - username ? `Payment to ${username}` : "Internal Transfer" - }`, - type: "internal", - created_at: Date.now() + if (!(await m.exec())) t(); }; - - if (!username) { - l("creating redeemable payment"); - params.redeemcode = v4(); - params.hash = `${hostname}/redeem/${params.redeemcode}`; - } - - await s(`payment:${p1.id}`, p1); - await rd.lPush(`${user.id}:payments`, p1.id); - - l("sent internal", user.username, -p1.amount); - - emit(user.username, "payment", p1); - - return p1; + await t(); }; diff --git a/lib/sockets.js b/lib/sockets.js index c94c29a0..7f655570 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -1,49 +1,42 @@ -import store from "$lib/store"; -import config from "$config"; import app from "$app"; +import config from "$config"; +import store from "$lib/store"; import jwt from "jsonwebtoken"; import { v4 } from "uuid"; -import WebSocket from "ws"; -import { differenceInSeconds, subSeconds } from "date-fns"; -import fastifyWs from "@fastify/websocket"; import whitelist from "./whitelist"; import { l, err, warn } from "$lib/logging"; -import { g } from "$lib/redis"; +import { g } from "$lib/db"; +import { pick } from "$lib/utils"; const code = 1000; - -const wss = new WebSocket.Server({ clientTracking: false, noServer: true }); - -const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); const all = {}; const subscriptions = []; const lastSeen = {}; +const users = {}; -export const emit = (uuid, type, data) => { - if (data.uuid !== undefined) data = pick(data, ...whitelist); - +export const emit = (uid, type, data) => { if (type === "payment" && data.amount > 0) { for (let i = subscriptions.length - 1; i >= 0; i--) { let s = subscriptions[i]; - if (s.invoice.id === data.invoice_id) { + if (s.hash === data.hash) { s.ws.send(JSON.stringify({ type, data })); } } } - if (!store.sockets[uuid]) return; - for (let id in store.sockets[uuid]) { - const ws = store.sockets[uuid][id]; + if (!store.sockets[uid]) return; + for (let id in store.sockets[uid]) { + const ws = store.sockets[uid][id]; ws.send(JSON.stringify({ type, data })); } }; export const broadcast = (type, data) => { const sent = []; - for (const uuid in store.sockets) { - for (const id in store.sockets[uuid]) { - const ws = store.sockets[uuid][id]; + for (const uid in store.sockets) { + for (const id in store.sockets[uid]) { + const ws = store.sockets[uid][id]; ws.send(JSON.stringify({ type, data })); sent.push(ws.id); } @@ -54,14 +47,10 @@ export const broadcast = (type, data) => { }); }; -function subtractSeconds(numOfSeconds, date = new Date()) { - date.setSeconds(date.getSeconds() - numOfSeconds); - return date; -} +export async function socketServer(app) { + let seconds = (n, d = new Date()) => d.setSeconds(d.getSeconds() - n) && d; -app.register(fastifyWs); -app.register(async function(app) { app.get("/ws", { websocket: true }, ({ socket: ws }, req) => { const id = v4(); ws.id = id; @@ -85,41 +74,42 @@ app.register(async function(app) { case "login": let token = data; - if (lastSeen[token] > subtractSeconds(5)) return; + if (lastSeen[token] > seconds(5)) return; lastSeen[token] = new Date(); try { if (!token || token === "null" || !jwt.decode(token)) return ws.close(code, `bad token ${token}`); - let { id: user_id } = jwt.decode(token); - if (!user_id) throw new Error("Invalid JWT token"); - let user = await g(`user:${user_id}`); + let { id: uid } = jwt.decode(token); + if (!uid) throw new Error("Invalid JWT token"); + let user = await g(`user:${uid}`); if (!user) throw new Error("User not found"); - user.haspin = !!user.pin; - if (!store.sockets[user_id]) store.sockets[user_id] = {}; - const existing = Object.keys(store.sockets[user_id]); + if (!store.sockets[uid]) store.sockets[uid] = {}; + const existing = Object.keys(store.sockets[uid]); if (existing.length > 4) { - const p = existing.find(sid => sid !== user_id); - store.sockets[user_id][p].send(JSON.stringify({ type: "logout" })); + const p = existing.find(sid => sid !== uid); + store.sockets[uid][p].send( + JSON.stringify({ type: "logout" }) + ); setTimeout(() => { - if (store.sockets[user_id][p]) - store.sockets[user_id][p].close(code, "too many sockets"); + if (store.sockets[uid][p]) + store.sockets[uid][p].close(code, "too many sockets"); }, 5000); } - store.sockets[user_id][id] = ws; - names[id] = user_id; + store.sockets[uid][id] = ws; + users[id] = uid; ws.user = user; ws.send( - JSON.stringify({ type: "login", data: pick(user, ...whitelist) }) + JSON.stringify({ type: "login", data: pick(user, whitelist) }) ); ws.send( JSON.stringify({ type: "version", data: config.clientVersion }) ); } catch (e) { - console.log(e) + console.log(e); err("caught socket error", e.message); if (e.message.includes("not found")) ws.send(JSON.stringify({ type: "logout" })); @@ -146,9 +136,9 @@ app.register(async function(app) { ws.on("close", function(code, reason) { try { - const uuid = names[id]; - if (store.sockets[uuid] && store.sockets[uuid][id]) { - delete store.sockets[uuid][id]; + const uid = users[id]; + if (store.sockets[uid] && store.sockets[uid][id]) { + delete store.sockets[uid][id]; } if (all[id]) delete all[id]; @@ -157,16 +147,15 @@ app.register(async function(app) { } }); }); -}); +} -const names = {}; -setInterval(() => { - for (const uuid in store.sockets) { - for (const id in store.sockets[uuid]) { - let ws = store.sockets[uuid][id]; +export let sendHeartbeat = () => { + for (const uid in store.sockets) { + for (const id in store.sockets[uid]) { + let ws = store.sockets[uid][id]; ws.beats++; - if (ws.beats > 10) ws.close(code, `${uuid} ${id} lost heartbeat`); + if (ws.beats > 10) ws.close(code, `${uid} ${id} lost heartbeat`); } } -}, 2000); +} diff --git a/lib/store.js b/lib/store.js index 6f0c1c97..bf71219e 100644 --- a/lib/store.js +++ b/lib/store.js @@ -2,7 +2,6 @@ export default { addresses: {}, challenge: {}, change: [], - exceptions: [], issuances: {}, logins: {}, nodes: [], diff --git a/lib/upload.js b/lib/upload.js index 95dfe21a..b37e0bf2 100644 --- a/lib/upload.js +++ b/lib/upload.js @@ -1,7 +1,3 @@ -import app from "$app"; -import { auth } from "./passport"; -import fastifyMultipart from "@fastify/multipart"; -import fastifyStatic from "@fastify/static"; import { fileTypeFromStream } from "file-type"; import pump from "pump"; import Clone from "readable-stream-clone"; @@ -9,21 +5,13 @@ import fs from "fs"; import path from "path"; import sharp from "sharp"; import { pipeline } from "stream"; +import { g, s } from "$lib/db"; -app.register(fastifyMultipart); - -app.register(fastifyStatic, { - root: path.join("/app/uploads"), - prefix: "/public/" -}); - -app.post("/upload/:type", auth, async function(req, res) { - try { - let { type } = req.params; +export default async ({ params: { type }, file, user }, res) => { if (!["banner", "profile"].includes(type)) throw new Error("invalid upload"); - let data = await req.file(); + let data = await file(); let s1 = new Clone(data.file); let s2 = new Clone(data.file); @@ -32,7 +20,7 @@ app.post("/upload/:type", auth, async function(req, res) { if (format !== "image" && !["jpg", "jpeg", "png"].includes(ext)) throw new Error("unsupported file type"); - let name = `${req.user.uuid}-${type}.webp`; + let name = `${user.id}-${type}.webp`; let path = `/app/uploads/${name}`; let size = type === "banner" ? 1920 : 240; @@ -43,12 +31,14 @@ app.post("/upload/:type", auth, async function(req, res) { await new Promise(r => pipeline(s2, t, fs.createWriteStream(path), r)); - req.user[type] = true; - await req.user.save(); + let r = async () => { + await db.watch(`user:${user.id}`); + user = await g(`user:${user.id}`); + user[type] = true; + let m = await db.multi(); + await s(`user:${user.id}`, user); + }; + await r(); res.send(name); - } catch (e) { - console.log(e); - return res.code(500).send(e); - } -}); +} diff --git a/lib/utils.js b/lib/utils.js index b556c46a..e53bf649 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,5 +1,5 @@ import store from "$lib/store"; -import { g } from "$lib/redis"; +import { g } from "$lib/db"; import config from "$config"; export const nada = () => {}; diff --git a/routes/email.js b/routes/email.js new file mode 100644 index 00000000..529307e6 --- /dev/null +++ b/routes/email.js @@ -0,0 +1,24 @@ +import config from "$config"; + +export default { + async send({ body }, res) { + try { + let postmark = await import("postmark"); + let client = new postmark.ServerClient(config.postmark); + + await client.sendEmail({ + From: "support@coinos.io", + To: "support@coinos.io", + Subject: body.subject || "Email Signup", + HtmlBody: JSON.stringify(body), + TextBody: JSON.stringify(body), + MessageStream: "outbound" + }); + + res.send({ ok: true }); + } catch (e) { + console.log("problem sending email", e); + res.code(500).send(e.message); + } + } +}; diff --git a/routes/events.js b/routes/events.js index c105dc40..cea82118 100644 --- a/routes/events.js +++ b/routes/events.js @@ -1,6 +1,6 @@ import app from "$app"; import db from "$db"; -import redis from "$lib/redis"; +import redis from "$lib/db"; import { coinos, pool, q } from "$lib/nostr"; import store from "$lib/store"; import { nada, wait } from "$lib/utils"; diff --git a/routes/invoices.js b/routes/invoices.js index 10a80b90..94b0d27d 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -1,21 +1,17 @@ -import app from "$app"; import store from "$lib/store"; -import { optionalAuth } from "$lib/passport"; import { l } from "$lib/logging"; import { emit } from "$lib/sockets"; -import { rd, g, s } from "$lib/redis"; +import { db, g, s } from "$lib/db"; import ln from "$lib/ln"; -app.get("/invoice", async ({ query: { id } }, res) => { - let invoice = await g(`invoice:${id}`); - invoice.user = await g(`user:${invoice.user_id}`); - res.send(invoice); -}); +export default { + async get({ query: { id } }, res) { + let invoice = await g(`invoice:${id}`); + invoice.user = await g(`user:${invoice.uid}`); + res.send(invoice); + }, -app.post( - "/invoice", - optionalAuth, - async ({ body: { invoice, user }, user: me }, res) => { + async create({ body: { invoice, user }, user: me }, res) { let { currency, tip, amount, rate, request_id } = invoice; if (amount < 0) throw new Error("amount out of range"); @@ -23,8 +19,8 @@ app.post( tip = tip || 0; if (!user) user = me; - let user_id = await g(`user:${user.username}`); - user = await g(`user:${user_id}`); + let uid = await g(`user:${user.username}`); + user = await g(`user:${uid}`); if (!user) throw new Error("user not provided"); @@ -46,9 +42,10 @@ app.post( currency, rate, tip, - user_id, + uid, text, - received: 0 + received: 0, + created: Date.now() }; l( @@ -61,7 +58,7 @@ app.post( ); await s(`invoice:${id}`, invoice); - await rd.lPush(`${user.id}:invoices`, id); + await db.lPush(`${uid}:invoices`, id); if (request_id) { let request = await g(`request:${request_id}`); @@ -74,4 +71,4 @@ app.post( res.send(invoice); } -); +}; diff --git a/routes/lightning/conversionFee.js b/routes/lightning/conversionFee.js deleted file mode 100644 index f6cf434c..00000000 --- a/routes/lightning/conversionFee.js +++ /dev/null @@ -1,12 +0,0 @@ -// Code for the conversion fee -// This is in a separate file so that the same function can be used by ./send and ./receive - -const conversionFeeMultiplier = 0.001; -export const conversionFeeReceiver = "coinosfees"; // account that receives the fees - -/** - * Returns the fee you have to pay for withdrawing 'amount' lightning BTC. - */ -export function computeConversionFee(amount) { - return Math.floor(amount * conversionFeeMultiplier); -} diff --git a/routes/lightning/index.js b/routes/lightning/index.js deleted file mode 100644 index ddc10bfe..00000000 --- a/routes/lightning/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import invoice from "./invoice"; -import query from "./query"; -import send from "./send"; -import parse from "./parse"; - -export default { - invoice, - query, - send, - parse -}; diff --git a/routes/lightning/invoice.js b/routes/lightning/invoice.js deleted file mode 100644 index ae974120..00000000 --- a/routes/lightning/invoice.js +++ /dev/null @@ -1,25 +0,0 @@ -import config from "$config"; -import { err } from "$lib/logging"; -import ln from "$lib/ln"; -import { v4 } from "uuid"; - -export default async (req, res) => { - let { amount, memo, tip } = req.body; - if (!tip) tip = 0; - let value = amount + tip; - - try { - if (!memo) memo = "coinos"; - const invoice = await ln.invoice( - value ? `${value}sat` : "any", - v4(), - memo, - 360 - ); - res.send({ text: invoice.bolt11 }); - } catch (e) { - console.log(e); - err("problem creating invoice", e.message); - res.code(500).send(e.message); - } -}; diff --git a/routes/lightning/parse.js b/routes/lightning/parse.js deleted file mode 100644 index 1b77019a..00000000 --- a/routes/lightning/parse.js +++ /dev/null @@ -1,19 +0,0 @@ -import ln from "$lib/ln"; -import store from "$lib/store"; - -export default async (req, res) => { - let { payreq } = req.body; - let hour = 1000 * 60 * 60; - let { last } = store.nodes; - let { nodes } = store; - - if (!last || last > Date.now() - hour) ({ nodes } = await ln.listnodes()); - store.nodes = nodes; - - let twoWeeksAgo = new Date(new Date().setDate(new Date().getDate() - 14)); - let { msatoshi, payee } = await ln.decodepay(payreq); - let node = nodes.find((n) => n.nodeid === payee); - let alias = node ? node.alias : payee.substr(0, 12); - - res.send({ alias, amount: Math.round(msatoshi / 1000) }); -}; diff --git a/routes/lightning/query.js b/routes/lightning/query.js deleted file mode 100644 index c5456a6f..00000000 --- a/routes/lightning/query.js +++ /dev/null @@ -1,19 +0,0 @@ -import { emit } from "$lib/sockets"; - -export default async (req, res) => { - const { payreq } = req.body; - - const invoice = await db.Invoice.findOne({ - include: { - attributes: ["username"], - model: db.User, - as: "user" - }, - where: { - text: payreq - } - }); - - if (invoice) emit(req.user.username, "to", invoice.user); - res.send({}); -}; diff --git a/routes/lightning/receive.js b/routes/lightning/receive.js deleted file mode 100644 index ffb0ad95..00000000 --- a/routes/lightning/receive.js +++ /dev/null @@ -1,92 +0,0 @@ -import { v4 } from "uuid"; -import store from "$lib/store"; -import { emit } from "$lib/sockets"; -import config from "$config"; -import { notify } from "$lib/notifications"; -import { callWebhook } from "$lib/webhooks"; -import { computeConversionFee } from "./conversionFee"; -import ln from "$lib/ln"; -import { l, err, warn } from "$lib/logging"; -import { g, s, rd } from "$lib/redis"; - -let handle = async () => { - let { - payment_hash, - bolt11: request, - pay_index, - status, - msatoshi_received, - payment_preimage: secret - } = await ln.waitanyinvoice(await g("pay_index")); - - await s("pay_index", pay_index); - - let received = Math.round(msatoshi_received / 1000); - - try { - l("incoming lightning payment", received); - - if (!secret) return; - let account, total, user; - - let invoice = await g(`invoice:${payment_hash}`); - if (!invoice) return warn("received lightning with no invoice", request); - - let { text: hash, currency, memo, rate, tip, user_id } = invoice; - let amount = parseInt(received) - tip; - if (amount < 0) throw new Error("amount out of range"); - - let id = v4(); - let payment = { - id, - user_id, - hash, - memo, - amount, - currency, - secret, - rate, - tip, - invoice_id: invoice.id, - created_at: Date.now() - }; - - total = amount + tip; - - invoice.status = "paid"; - invoice.received += total; - await s(`invoice:${payment_hash}`, invoice); - - let t = async () => { - await rd.watch(`user:${user_id}`); - user = await g(`user:${user_id}`); - user.balance += total; - let m = await rd.multi(); - await s(`user:${user.id}`, user); - if (!(await m.exec())) t(); - }; - await t(); - - payment.invoice = invoice; - - await s(`payment:${id}`, payment); - await rd.lPush(`${user.id}:payments`, id); - - callWebhook(invoice, payment); - - emit(user.username, "payment", payment); - notify(user, `Received ${total} SAT`); - - l("lightning payment received", user.username, payment.amount, payment.tip); - } catch (e) { - console.log(e); - err("problem receiving lightning payment", e.message); - } -}; - -let listen = async () => { - await handle(); - listen(); -}; - -listen(); diff --git a/routes/lightning/send.js b/routes/lightning/send.js deleted file mode 100644 index b41d5ed0..00000000 --- a/routes/lightning/send.js +++ /dev/null @@ -1,17 +0,0 @@ -import { err } from "$lib/logging"; -import send from "$lib/send"; -import { requirePin } from "$lib/utils"; - -export default async (req, res) => { - let { amount, route, memo, payreq } = req.body; - let { user } = req; - - try { - await requirePin(req); - res.send(await send(amount, memo, payreq, user)); - } catch (e) { - console.log(e); - err("problem sending lightning payment", user.username, payreq, e.message); - res.code(500).send(e.message); - } -}; diff --git a/routes/locations.js b/routes/locations.js index 1cebc883..e080a7b9 100644 --- a/routes/locations.js +++ b/routes/locations.js @@ -1,23 +1,7 @@ -import app from "$app"; -import redis from "$lib/redis"; -import got from "got"; +import { g } from "$lib/db"; -let locations = []; -app.get("/locations", async (req, res) => { - res.send({ locations }); -}); - -let getLocations = async () => { - try { - let r = await got( - "https://api.btcmap.org/v2/elements?updated_since=2022-09-19" - ).json(); - locations = r.filter( - l => l["osm_json"].tags && l["osm_json"].tags["payment:coinos"] === "yes" - ); - } catch(e) {} - - setTimeout(getLocations, 60000); -}; - -getLocations(); +export default { +async list(req, res) { + res.send({ locations: await g('locations') }); +} +} diff --git a/routes/nostr.js b/routes/nostr.js new file mode 100644 index 00000000..3d326e94 --- /dev/null +++ b/routes/nostr.js @@ -0,0 +1,161 @@ +export default { + async event({ params: { id } }, res) { + let event = JSON.parse(await g(`ev:${id}`)); + let { pubkey } = event; + + event.user = JSON.parse(await g(`user:${pubkey}`)) || { + username: pubkey.substr(0, 6), + pubkey, + anon: true, + follows: [], + followers: [] + }; + + res.send(event); + }, + + async notes({ params: { pubkey } }, res) { + let user = JSON.parse(await g(`user:${pubkey}`)); + + if (!user) { + user = await db.User.findOne({ + where: { pubkey } + }); + } + + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true, + follows: [], + followers: [] + }; + + let params = { + kinds: [1], + authors: [pubkey] + }, + opts = { since: 0 }; + + q(`${pubkey}:notes`, params, opts).catch(nada); + + await g(`user:${pubkey}`, JSON.stringify(user)); + + let ids = await db.sMembers(pubkey); + + let events = ids.length + ? (await db.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) + : []; + + res.send(events.map(e => ({ ...e, user }))); + }, + + async broadcast(req, res) { + let { event } = req.body; + pool.send(["EVENT", event]); + res.send(event); + }, + + async follows({ params: { pubkey }, query: { tagsOnly } }, res) { + let sub = `${pubkey}:follows`, + params = { + limit: 1, + kinds: [3], + authors: [pubkey] + }, + opts = { timeout: 60000, eager: 60000 }; + + q(sub, params, opts).catch(nada); + + let tags = JSON.parse(await g(`${pubkey}:follows`)) || []; + if (tagsonly) return res.send(tags); + + let follows = []; + for (let f of tags) { + let [_, pubkey] = f; + + q(`${pubkey}:profile:f1`, { + limit: 1, + kinds: [0], + authors: [pubkey] + }).catch(nada); + + let user = JSON.parse(await g(`user:${pubkey}`)); + + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; + + follows.push(user); + } + + follows = uniq(follows, e => e.pubkey); + follows.sort((a, b) => a.username.localeCompare(b.username)); + + res.send(follows); + }, + + async followers({ params: { pubkey}}, res) { + try { + let pubkeys = [ + ...new Set([ + ...(await got(`${config.nostr}/followers?pubkey=${pubkey}`).json()), + ...(await db.sMembers(`${pubkey}:followers`)) + ]) + ]; + + let followers = []; + + q( + `${pubkey}:followers`, + { kinds: [3], "#p": [pubkey] }, + { timeout: 60000, eager: 60000 } + ).catch(nada); + + for (let pubkey of pubkeys) { + let user = JSON.parse(await g(`user:${pubkey}`)); + if (!user || !user.updated) { + q(`${pubkey}:profile:f2`, { + limit: 1, + kinds: [0], + authors: [pubkey] + }).catch(nada); + + user = JSON.parse(await g(`user:${pubkey}`)); + } + + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; + + followers.push(user); + } + + followers = uniq(followers, e => e.pubkey); + followers.sort((a, b) => a.username.localeCompare(b.username)); + + res.send(followers); + } catch (e) { + console.log(e); + res.code(500).send(e && e.message); + } + }, + + async identities(req, res) { + res.send({ + names: { + adam: + "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8", + asoltys: + "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8" + } + }); + } +}; diff --git a/routes/payments.js b/routes/payments.js index b328e763..13c0cdb7 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -1,29 +1,19 @@ -import app from "$app"; -import config from "$config"; -import store from "$lib/store"; -import { g, rd } from "$lib/redis"; - -import { auth, adminAuth, optionalAuth } from "$lib/passport"; -import fs from "fs"; -import { join } from "path"; -import send from "./send"; -import { err, l, warn } from "$lib/logging"; -import { fail } from "$lib/utils"; import { emit } from "$lib/sockets"; +import { db, g, s } from "$lib/db"; +import { l, err, warn } from "$lib/logging"; +import { fail } from "$lib/utils"; +import ln from "$lib/ln"; +import sendInternal from "$lib/sendInternal"; +import { requirePin } from "$lib/auth"; -import lnRoutes from "./lightning/index"; -import "./lightning/receive"; - -app.post("/send", auth, send); - -app.post("/lightning/parse", lnRoutes.parse); -app.post("/lightning/query", auth, lnRoutes.query); -app.post("/lightning/send", auth, lnRoutes.send); +export default { + async send(req, res) { + await requirePin(req); + let payment = await sendInternal(req.body, req.user); + res.send(payment); + }, -app.get( - "/payments", - auth, - async ({ user: { id }, query: { start, end, limit, offset } }, res) => { + async list({ user: { id }, query: { start, end, limit, offset } }, res) { if (limit) limit = parseInt(limit); if (offset) offset = parseInt(offset); @@ -31,144 +21,134 @@ app.get( // if (start) where.createdAt[Op.gte] = new Date(parseInt(start)); // if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); - let payments = (await rd.lRange(`${id}:payments`, 0, -1)) || []; - payments = await Promise.all(payments.map(id => g(`payment:${id}`))); + let payments = (await db.lRange(`${id}:payments`, 0, -1)) || []; + payments = await Promise.all(payments.map(hash => g(`payment:${hash}`))); payments = payments.filter(p => p); res.send({ payments, total: payments.length }); + }, + + async voucher(req, res) { + try { + const { redeemcode } = req.params; + let payment = await g(`voucher:${id}`); + + payment = payment.get({ plain: true }); + payment.redeemer = payment["with"]; + + if (!payment) fail("invalid code"); + + res.send(payment); + } catch (e) { + res.code(500).send(e.message); + } + }, + + async get({ params: { hash } }, res) { + res.send(await g(`payment:${hash}`)); + }, + + async redeem({ body: { redeemcode } }, res) { + try { + await db.transaction(async transaction => { + if (!redeemcode) fail("no code provided"); + + let { user } = req; + + const source = await db.Payment.findOne({ + where: { + redeemcode: req.body.redeemcode + }, + include: { + model: db.Account, + as: "account" + }, + lock: transaction.LOCK.UPDATE, + transaction + }); + + l("redeeming", redeemcode); + + if (!source) fail("Invalid code"); + if (source.redeemed) fail("Voucher has already been redeemed"); + let { amount } = source; + amount = -amount; + + if (!user) { + const ip = + req.headers["x-forwarded-for"] || req.connection.remoteAddress; + + user = await register( + { + username: redeemcode.substr(0, 8), + password: "" + }, + ip, + false + ); + + let payload = { username: user.username }; + let token = jwt.sign(payload, config.jwt); + res.cookie("token", token, { + expires: new Date(Date.now() + 432000000) + }); + + return res.send({ user }); + } + + let account = await getAccount(source.account.asset, user, transaction); + let { hash, memo, confirmed, fee, network } = source; + + source.redeemed = true; + (source.with_id = user.id), await source.save({ transaction }); + + let payment = await db.Payment.create( + { + amount, + account_id: account.id, + user_id: user.id, + hash: "Voucher " + redeemcode, + memo, + rate: store.rates[user.currency], + currency: user.currency, + confirmed, + network, + received: true, + fee, + with_id: source.user_id + }, + { transaction } + ); + + await account.increment({ balance: amount }, { transaction }); + await account.reload({ transaction }); + + payment = payment.get({ plain: true }); + payment.account = account.get({ plain: true }); + emit(user.username, "payment", payment); + emit(user.username, "account", account); + + res.send({ payment }); + }); + } catch (e) { + console.log(e); + err("problem redeeming", e.message); + return res.code(500).send("There was a problem redeeming the voucher"); + } + }, + + async sendLightning({ body: { payreq } }, res) { + let hour = 1000 * 60 * 60; + let { last } = store.nodes; + let { nodes } = store; + + if (!last || last > Date.now() - hour) ({ nodes } = await ln.listnodes()); + store.nodes = nodes; + + let twoWeeksAgo = new Date(new Date().setDate(new Date().getDate() - 14)); + let { msatoshi, payee } = await ln.decodepay(payreq); + let node = nodes.find(n => n.nodeid === payee); + let alias = node ? node.alias : payee.substr(0, 12); + + res.send({ alias, amount: Math.round(msatoshi / 1000) }); } -); - -// app.get("/voucher/:redeemcode", async (req, res) => { -// try { -// const { redeemcode } = req.params; -// let payment = await db.Payment.findOne({ -// where: { -// redeemcode -// }, -// include: { all: true } -// }); -// -// payment = payment.get({ plain: true }); -// payment.redeemer = payment["with"]; -// -// if (!payment) fail("invalid code"); -// -// res.send(payment); -// } catch (e) { -// res.code(500).send(e.message); -// } -// }); - -// let redeeming = {}; -// app.post("/redeem", optionalAuth, async function(req, res) { -// const { redeemcode } = req.body; -// try { -// await db.transaction(async transaction => { -// if (redeeming[redeemcode]) fail("redemption in progress"); -// redeeming[redeemcode] = true; -// if (!redeemcode) fail("no code provided"); -// -// let { user } = req; -// -// const source = await db.Payment.findOne({ -// where: { -// redeemcode: req.body.redeemcode -// }, -// include: { -// model: db.Account, -// as: "account" -// }, -// lock: transaction.LOCK.UPDATE, -// transaction -// }); -// -// l("redeeming", redeemcode); -// -// if (!source) fail("Invalid code"); -// if (source.redeemed) fail("Voucher has already been redeemed"); -// let { amount } = source; -// amount = -amount; -// -// if (!user) { -// const ip = -// req.headers["x-forwarded-for"] || req.connection.remoteAddress; -// -// user = await register( -// { -// username: redeemcode.substr(0, 8), -// password: "" -// }, -// ip, -// false -// ); -// -// let payload = { username: user.username }; -// let token = jwt.sign(payload, config.jwt); -// res.cookie("token", token, { -// expires: new Date(Date.now() + 432000000) -// }); -// -// delete redeeming[redeemcode]; -// return res.send({ user }); -// } -// -// let account = await getAccount(source.account.asset, user, transaction); -// let { hash, memo, confirmed, fee, network } = source; -// -// source.redeemed = true; -// (source.with_id = user.id), await source.save({ transaction }); -// -// let payment = await db.Payment.create( -// { -// amount, -// account_id: account.id, -// user_id: user.id, -// hash: "Voucher " + redeemcode, -// memo, -// rate: store.rates[user.currency], -// currency: user.currency, -// confirmed, -// network, -// received: true, -// fee, -// with_id: source.user_id -// }, -// { transaction } -// ); -// -// await account.increment({ balance: amount }, { transaction }); -// await account.reload({ transaction }); -// -// payment = payment.get({ plain: true }); -// payment.account = account.get({ plain: true }); -// emit(user.username, "payment", payment); -// emit(user.username, "account", account); -// -// res.send({ payment }); -// }); -// } catch (e) { -// delete redeeming[redeemcode]; -// console.log(e); -// err("problem redeeming", e.message); -// return res.code(500).send("There was a problem redeeming the voucher"); -// } -// }); -// -// app.post("/checkRedeemCode", auth, async function(req, res) { -// const { redeemcode } = req.body; -// -// const payment = await db.Payment.findOne({ where: { redeemcode } }); -// res.send(payment); -// }); -// -// app.get("/payments/:hash", auth, async function(req, res) { -// try { -// let payment = await db.Payment.findOne({ -// where: { user_id: req.user.id, hash: req.params.hash } -// }); -// -// return payment.get({ plain: true }); -// } catch (e) { -// console.log(e); -// } -// }); +}; diff --git a/routes/rates.js b/routes/rates.js new file mode 100644 index 00000000..c36c7ae8 --- /dev/null +++ b/routes/rates.js @@ -0,0 +1,11 @@ +import store from "$lib/store"; + +export default { + async last(req, res) { + res.send(store.last); + }, + + async index(req, res) { + res.send(store.rates); + } +}; diff --git a/routes/requests.js b/routes/requests.js index bea8f7fd..f62c3131 100644 --- a/routes/requests.js +++ b/routes/requests.js @@ -1,38 +1,34 @@ -import app from "$app"; -import { auth } from "$lib/passport"; import { emit } from "$lib/sockets"; -import { g, rd } from "$lib/redis"; - -app.get("/request/:id", auth, async ({ params: { id } }, res) => { - try { - res.send({ request: await g(id) }); - } catch (e) { - console.log(e); - res.code(500).send(e.message); - } -}); - -app.get("/requests", auth, async ({ user: { uuid } }, res) => { - try { - const day = new Date(new Date().setDate(new Date().getDate() - 1)); - - let invoices = await rd.lRange(`${uuid}:invoices`, 0, -1); - let requests = await rd.lRange(`${uuid}:requests`, 0, -1); - - res.send({ invoices, requests }); - } catch (e) { - console.log(e); - res.code(500).send(e.message); - } -}); - -app.post( - "/requests", - auth, - async ( +import { g, db } from "$lib/db"; + +export default { + async get({ params: { id } }, res) { + try { + res.send({ request: await g(id) }); + } catch (e) { + console.log(e); + res.code(500).send(e.message); + } + }, + + async list({ user: { uuid } }, res) { + try { + const day = new Date(new Date().setDate(new Date().getDate() - 1)); + + let invoices = await db.lRange(`${uuid}:invoices`, 0, -1); + let requests = await db.lRange(`${uuid}:requests`, 0, -1); + + res.send({ invoices, requests }); + } catch (e) { + console.log(e); + res.code(500).send(e.message); + } + }, + + async create( { body: { recipient, ...params }, user: { username, profile } }, res - ) => { + ) { let { id: recipient_id } = await g(`user:${recipient}`); let request = { recipient_id, ...params }; @@ -44,14 +40,10 @@ app.post( emit(recipient, "request", request); res.send(request); - } -); + }, -app.post( - "/requests/delete", - auth, - async ({ body: { request_id }, user: { id } }, res) => { - await rd.lrem(`user:${id}:requests`, request_id); + async destroy({ body: { request_id }, user: { id } }, res) { + await db.lrem(`user:${id}:requests`, request_id); res.send(); } -); +}; diff --git a/routes/send.js b/routes/send.js index 3f71a496..1bd4910a 100644 --- a/routes/send.js +++ b/routes/send.js @@ -1,6 +1,6 @@ import { err } from "$lib/logging"; import sendInternal from "$lib/sendInternal"; -import { requirePin } from "$lib/utils"; +import { requirePin } from "$lib/auth"; export default async (req, res, next) => { try { diff --git a/routes/users.js b/routes/users.js index c0324803..53b02d02 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1,8 +1,7 @@ import app from "$app"; -import { g, s } from "$lib/redis"; +import { g, s } from "$lib/db"; import config from "$config"; import store from "$lib/store"; -import { auth, optionalAuth } from "$lib/passport"; import { nada, pick, uniq, wait } from "$lib/utils"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; @@ -13,310 +12,293 @@ import { emit } from "$lib/sockets"; import register from "$lib/register"; import { requirePin } from "$lib/utils"; import got from "got"; +import upload from "$lib/upload"; + +export default { + upload, + + async me({ user }, res) { + try { + user.balance = await g(`balance:${user.id}`); + res.send(pick(user, whitelist)); + } catch (e) { + console.log("problem fetching user", e); + res.code(500).send(e.message); + } + }, -app.get("/me", auth, async (req, res) => { - try { - res.send(pick(req.user, whitelist)); - } catch (e) { - console.log("problem fetching user", e); - res.code(500).send(e.message); - } -}); + async get({ params: { key } }, res) { + try { + if (key.startsWith("npub")) { + key = Buffer.from(fromWords(decode(key).words)).toString("hex"); + } -app.get("/users/:key", async ({ params: { key } }, res) => { - try { - if (key.startsWith("npub")) { - key = Buffer.from(fromWords(decode(key).words)).toString("hex"); - } + let user = await g(`user:${key}`); + if (typeof user === "string") { + user = await g(`user:${user}`); + } - let user = await g(`user:${key}`); - if (typeof user === "string") { - user = await g(`user:${user}`); - } + if (!user && key.length === 64) { + user = { + username: key.substr(0, 6), + pubkey: key, + anon: true + }; + } - if (!user && key.length === 64) { - user = { - username: key.substr(0, 6), - pubkey: key, - anon: true - }; + if (!user) return res.code(500).send("User not found"); + + let whitelist = [ + "username", + "banner", + "profile", + "address", + "currency", + "pubkey", + "display", + "prompt", + "id" + ]; + + res.send(pick(user, whitelist)); + } catch (e) { + console.log(e); + res.code(500).send(e.message); } + }, + + async create(req, res) { + try { + const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress; + let { cipher, pubkey, password, username, salt } = req.body.user; + + let user = { + cipher, + pubkey, + password, + username, + salt + }; - if (!user) return res.code(500).send("User not found"); - - let whitelist = [ - "username", - "banner", - "profile", - "address", - "currency", - "pubkey", - "display", - "prompt", - "id" - ]; - - res.send(pick(user, whitelist)); - } catch (e) { - console.log(e); - res.code(500).send(e.message); - } -}); - -app.post("/register", async (req, res) => { - try { - const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress; - let { cipher, pubkey, password, username, salt } = req.body.user; - - let user = { - cipher, - pubkey, - password, - username, - salt - }; - - user = await register(user, ip, false); - l("registered new user", username); - res.send(pick(user, whitelist)); - } catch (e) { - res.code(500).send(e.message); - } -}); + user = await register(user, ip, false); + l("registered new user", username); + res.send(pick(user, whitelist)); + } catch (e) { + res.code(500).send(e.message); + } + }, -app.post("/disable2fa", auth, async ({ user, body: { token } }, res) => { - let { id, twofa, username, otpsecret } = user; - if (twofa && !authenticator.check(token, otpsecret)) { - return res.code(401).send("2fa required"); - } + async disable2fa({ user, body: { token } }, res) { + let { id, twofa, username, otpsecret } = user; + if (twofa && !authenticator.check(token, otpsecret)) { + return res.code(401).send("2fa required"); + } - user.twofa = false; - await s(`user:${id}`, user); - emit(username, "user", user); - emit(username, "otpsecret", user.otpsecret); - l("disabled 2fa", username); - res.send({}); -}); - -app.post("/2fa", auth, async ({ user, body: { token } }, res) => { - let { id, otpsecret, username } = user; - const isValid = authenticator.check(token, otpsecret); - if (isValid) { - user.twofa = true; + user.twofa = false; await s(`user:${id}`, user); emit(username, "user", user); - } else { - return res.code(500).send("Invalid token"); - } - - l("enabled 2fa", username); - res.send({}); -}); - -app.post("/user", auth, async ({ user, body: { confirm, password, pin, newpin, username }}, res) => { - try { - let { user } = req; + emit(username, "otpsecret", user.otpsecret); + l("disabled 2fa", username); + res.send({}); + }, + + async enable2fa({ user, body: { token } }, res) { + let { id, otpsecret, username } = user; + const isValid = authenticator.check(token, otpsecret); + if (isValid) { + user.twofa = true; + await s(`user:${id}`, user); + emit(username, "user", user); + } else { + return res.code(500).send("Invalid token"); + } - l("updating user", user.username); + l("enabled 2fa", username); + res.send({}); + }, + + async update( + { user, body: { confirm, password, pin, newpin, username } }, + res + ) { + try { + let { user } = req; + + l("updating user", user.username); + + if (user.pin && !(pin === user.pin)) throw new Error("Pin required"); + if (typeof newpin !== "undefined") user.pin = newpin; + if (!user.pin) user.pin = null; + + let exists; + if (username) exists = await g(`user:${username}`); + + let token; + if (user.username !== username && exists) { + err("username taken", username, user.username, exists.username); + throw new Error("Username taken"); + } else if (username) { + if (user.username !== username) + l("changing username", user.username, username); + user.username = username; + } - if (user.pin && !(pin === user.pin)) throw new Error("Pin required"); - if (typeof newpin !== "undefined") user.pin = newpin; - if (!user.pin) user.pin = null; + let attributes = [ + "address", + "cipher", + "currencies", + "currency", + "display", + "email", + "fiat", + "locktime", + "prompt", + "pubkey", + "salt", + "seed", + "tokens", + "twofa", + "unit" + ]; + + for (let a of attributes) { + if (req.body[a]) user[a] = req.body[a]; + } - let exists; - if (username) exists = await g(`user:${username}`); + if (password && password === confirm) { + user.password = await bcrypt.hash(password, 1); + } - let token; - if (user.username !== username && exists) { - err("username taken", username, user.username, exists.username); - throw new Error("Username taken"); - } else if (username) { - if (user.username !== username) - l("changing username", user.username, username); - user.username = username; - } + await s(`user:${user.id}`, user); - let attributes = [ - "address", - "cipher", - "currencies", - "currency", - "display", - "email", - "fiat", - "locktime", - "prompt", - "pubkey", - "salt", - "seed", - "tokens", - "twofa", - "unit" - ]; - - for (let a of attributes) { - if (req.body[a]) user[a] = req.body[a]; - } + user.haspin = !!user.pin; - if (password && password === confirm) { - user.password = await bcrypt.hash(password, 1); + emit(user.username, "user", user); + res.send({ user, token }); + } catch (e) { + console.log(e); + err("error updating user", e.message); + res.code(500).send(e.message); } + }, + + async login(req, res) { + try { + const { params, sig, key, password, username, token: twofa } = req.body; + l("logging in", username); + + if (sig) { + const { callback } = params; + + try { + const url = `${callback}&sig=${sig}&key=${key}`; + const response = await got(url).json(); + res.send(response); + } catch (e) { + err("problem calling lnurl login", e.message); + res.code(500).send(e.message); + } + + return; + } - await user.save(); + let id = await g(`user:${username}`); + let user = await g(`user:${id}`); - user = user.get({ plain: true }); - user.haspin = !!user.pin; + if ( + !user || + (user.password && !(await bcrypt.compare(password, user.password))) + ) { + warn("invalid username or password attempt", username); + return res.code(401).send({}); + } - emit(user.username, "user", user); - res.send({ user, token }); - } catch (e) { - console.log(e); - err("error updating user", e.message); - res.code(500).send(e.message); - } -}); - -let login = async (req, res) => { - try { - const { params, sig, key, password, username, token: twofa } = req.body; - l("logging in", username); - - if (sig) { - const { callback } = params; - - try { - const url = `${callback}&sig=${sig}&key=${key}`; - const response = await got(url).json(); - res.send(response); - } catch (e) { - err("problem calling lnurl login", e.message); - res.code(500).send(e.message); + if ( + user.twofa && + (typeof twofa === "undefined" || + !authenticator.check(twofa, user.otpsecret)) + ) { + return res.code(401).send("2fa required"); } - return; + l( + "login", + username, + req.headers["x-forwarded-for"] || req.socket.remoteAddress + ); + + let payload = { username, id: user.id }; + let token = jwt.sign(payload, config.jwt); + res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); + user = pick(user, whitelist); + res.send({ user, token }); + } catch (e) { + console.log(e); + err("login error", e.message, req.socket.remoteAddress); + res.code(401).send({}); } + }, - let id = await g(`user:${username}`); - let user = await g(`user:${id}`); + async logout(req, res) { + let { subscription } = req.body; + if (!subscription) return res.send({}); - if ( - !user || - (user.password && !(await bcrypt.compare(password, user.password))) - ) { - warn("invalid username or password attempt", username); - return res.code(401).send({}); - } + const { username } = req.user; - if ( - user.twofa && - (typeof twofa === "undefined" || - !authenticator.check(twofa, user.otpsecret)) - ) { - return res.code(401).send("2fa required"); + if (username) { + l("logging out", username); + let i = req.user.subscriptions.findIndex( + s => JSON.stringify(s) === subscription + ); + if (i > -1) { + req.user.subscriptions.splice(i, 1); + } + await req.user.save(); + Object.keys(logins).map( + k => logins[k]["username"] === username && delete logins[k] + ); } - l( - "login", - username, - req.headers["x-forwarded-for"] || req.socket.remoteAddress - ); - - let payload = { username, id: user.id }; - let token = jwt.sign(payload, config.jwt); - res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); - user = pick(user, whitelist); - res.send({ user, token }); - } catch (e) { - console.log(e); - err("login error", e.message, req.socket.remoteAddress); - res.code(401).send({}); - } -}; - -app.post("/login", login); + res.send({}); + }, -app.post("/logout", optionalAuth, async (req, res) => { - let { subscription } = req.body; - if (!subscription) return res.send({}); - - const { username } = req.user; - - if (username) { - l("logging out", username); - let i = req.user.subscriptions.findIndex( - s => JSON.stringify(s) === subscription - ); - if (i > -1) { - req.user.subscriptions.splice(i, 1); + async subscribe({ body, user }, res) { + let { subscriptions } = user; + let { subscription } = body; + if (!subscriptions) subscriptions = []; + if ( + !subscriptions.find( + s => JSON.stringify(s) === JSON.stringify(subscription) + ) + ) + subscriptions.push(subscription); + user.subscriptions = subscriptions; + l("subscribing", user.username); + await user.save(); + res.sendStatus(201); + }, + + async password({ body: { password }, user }, res) { + if (!user.password) return res.send(true); + res.send(await bcrypt.compare(password, user.password)); + }, + + async otpsecret(req, res) { + try { + await requirePin(req); + let { otpsecret, username } = req.user; + res.send({ secret: otpsecret, username }); + } catch (e) { + res.code(500).send(e.message); } - await req.user.save(); - Object.keys(logins).map( - k => logins[k]["username"] === username && delete logins[k] + }, + + async contacts({ user: { uuid } }, res) { + let payments = (await g(`${uuid}:payments`)) || []; + res.send( + Promise.all( + [...new Set(payments.map(p => p.with_id))].map(id => g(`user:${id}`)) + ) ); } - - res.send({}); -}); - -app.get("/vapidPublicKey", function(req, res) { - res.send(config.vapid.publicKey); -}); - -app.post("/subscribe", auth, async function(req, res) { - let { subscriptions } = req.user; - let { subscription } = req.body; - if (!subscriptions) subscriptions = []; - if ( - !subscriptions.find(s => JSON.stringify(s) === JSON.stringify(subscription)) - ) - subscriptions.push(subscription); - req.user.subscriptions = subscriptions; - l("subscribing", req.user.username); - await req.user.save(); - res.sendStatus(201); -}); - -app.post("/password", auth, async function(req, res) { - const { user } = req; - const { password } = req.body; - - if (!user.password) return res.send(true); - res.send(await bcrypt.compare(password, user.password)); -}); - -app.get("/invoices", auth, async function(req, res) { - let invoices = await db.Invoice.findAll({ - where: { user_id: req.user.id } - }); - res.send(invoices); -}); - -app.post("/otpsecret", auth, async function(req, res) { - try { - await requirePin(req); - let { otpsecret, username } = req.user; - res.send({ secret: otpsecret, username }); - } catch (e) { - res.code(500).send(e.message); - } -}); - -app.get("/contacts", auth, async function({ user: { uuid } }, res) { - let payments = (await g(`${uuid}:payments`)) || []; - res.send( - Promise.all( - [...new Set(payments.map(p => p.with_id))].map(id => g(`user:${id}`)) - ) - ); -}); - -app.post("/checkPassword", auth, async function(req, res) { - try { - let { user } = req; - let result = await bcrypt.compare(req.body.password, user.password); - if (!result) throw new Error(); - res.send(result); - } catch (e) { - res.code(500).send("Invalid password"); - } -}); +}; -- cgit v1.2.3 From 220d6d52b8439a81b572a889d7012f826efc55c5 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 17 Jan 2023 09:17:17 -0800 Subject: fix rates --- index.js | 8 ++++---- lib/rates.js | 5 ++--- routes/requests.js | 6 +++--- 3 files changed, 9 insertions(+), 10 deletions(-) (limited to 'lib') diff --git a/index.js b/index.js index 7d4aa135..4a8fb01a 100644 --- a/index.js +++ b/index.js @@ -60,10 +60,10 @@ app.post("/password", auth, users.password); app.post("/otpsecret", auth, users.otpsecret); app.get("/contacts", auth, users.contacts); -app.get("/request/:id", requests.get); -app.get("/requests", requests.list); -app.post("/requests", requests.create); -app.post("/requests/delete", requests.destroy); +app.get("/request/:id", auth, requests.get); +app.get("/requests", auth, requests.list); +app.post("/requests", auth, requests.create); +app.post("/requests/delete", auth, requests.destroy); let host = process.env.HOST || "0.0.0.0"; let port = process.env.PORT || 3119; diff --git a/lib/rates.js b/lib/rates.js index c7d6918c..00e1bd88 100644 --- a/lib/rates.js +++ b/lib/rates.js @@ -15,8 +15,6 @@ let connect = () => { try { let msg = JSON.parse(event.data); - if (!store.fx) return; - Object.keys(store.fx).map(symbol => { store.rates[symbol] = msg.c * store.fx[symbol]; }); @@ -39,11 +37,12 @@ export let sendRates = () => broadcast("rate", store.last); export let getRates = async () => { connect(); - let date, fx; + let date = 0, fx; let rates = await g("rates"); if (rates) ({ date, fx } = rates); if (Date.now() - date > 24 * 60 * 60 * 1000) { + date = Date.now(); try { let { rates: fx } = await got( `https://api.exchangerate.host/latest?base=USD&v={(new Date()).toISOString().split('T')[0]}` diff --git a/routes/requests.js b/routes/requests.js index f62c3131..79af1fb6 100644 --- a/routes/requests.js +++ b/routes/requests.js @@ -11,12 +11,12 @@ export default { } }, - async list({ user: { uuid } }, res) { + async list({ user: { id } }, res) { try { const day = new Date(new Date().setDate(new Date().getDate() - 1)); - let invoices = await db.lRange(`${uuid}:invoices`, 0, -1); - let requests = await db.lRange(`${uuid}:requests`, 0, -1); + let invoices = await db.lRange(`${id}:invoices`, 0, -1); + let requests = await db.lRange(`${id}:requests`, 0, -1); res.send({ invoices, requests }); } catch (e) { -- cgit v1.2.3 From 46913d34e77f19c3dd7c105b1deec452348dd1db Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 17 Jan 2023 11:42:57 -0800 Subject: rename invoice id to hash --- index.js | 4 +++ lib/lightning.js | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ routes/invoices.js | 12 ++++---- 3 files changed, 92 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/index.js b/index.js index 4a8fb01a..ec0e4244 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,7 @@ import app from "$app"; import { auth, optional } from "$lib/auth"; + +import { listenForLightning } from "$lib/lightning"; import { getLocations } from "$lib/locations"; import { getRates, sendRates } from "$lib/rates"; import { sendHeartbeat } from "$lib/sockets"; @@ -13,6 +15,8 @@ import users from "$routes/users"; import payments from "$routes/payments"; import requests from "$routes/requests"; +listenForLightning(); + getLocations(); getRates(); diff --git a/lib/lightning.js b/lib/lightning.js index e69de29b..2a8e8053 100644 --- a/lib/lightning.js +++ b/lib/lightning.js @@ -0,0 +1,82 @@ +import { emit } from "$lib/sockets"; +import config from "$config"; +import { notify } from "$lib/notifications"; +import { callWebhook } from "$lib/webhooks"; +import ln from "$lib/ln"; +import { l, err, warn } from "$lib/logging"; +import { g, s, db } from "$lib/db"; + +export async function listenForLightning() { + let { + payment_hash, + bolt11: request, + pay_index, + status, + msatoshi_received, + payment_preimage: preimage + } = await ln.waitanyinvoice(await g("pay_index")); + + await s("pay_index", pay_index); + + let received = Math.round(msatoshi_received / 1000); + + try { + l("incoming lightning payment", received); + + if (!preimage) return; + + let invoice = await g(`invoice:${payment_hash}`); + if (!invoice) return warn("received lightning with no invoice", request); + + let { hash, currency, memo, rate, tip, uid } = invoice; + let amount = parseInt(received) - tip; + if (amount < 0) throw new Error("amount out of range"); + + let user = await g(`user:${uid}`); + + let payment = { + amount, + created: Date.now(), + currency, + fee: 0, + hash, + memo, + preimage, + rate, + tip, + type: "lightning", + uid + }; + + invoice.status = "paid"; + invoice.received += received; + await s(`invoice:${payment_hash}`, invoice); + + let t = async () => { + await db.watch(`balance:${uid}`); + let b = await g(`balance:${uid}`); + b += received; + let m = await db.multi(); + await s(`balance:${uid}`, b); + if (!(await m.exec())) t(); + }; + await t(); + + payment.invoice = invoice; + + await s(`payment:${hash}`, payment); + await db.lPush(`${uid}:payments`, hash); + + callWebhook(invoice, payment); + + emit(uid, "payment", payment); + notify(user, `Received ${received} SAT`); + + l("lightning payment received", user.username, payment.amount, payment.tip); + } catch (e) { + console.log(e); + err("problem receiving lightning payment", e.message); + } + + setTimeout(listenForLightning); +} diff --git a/routes/invoices.js b/routes/invoices.js index 94b0d27d..acd9457e 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -5,8 +5,8 @@ import { db, g, s } from "$lib/db"; import ln from "$lib/ln"; export default { - async get({ query: { id } }, res) { - let invoice = await g(`invoice:${id}`); + async get({ query: { hash } }, res) { + let invoice = await g(`invoice:${hash}`); invoice.user = await g(`user:${invoice.uid}`); res.send(invoice); }, @@ -28,7 +28,7 @@ export default { if (!rate) rate = store.rates[currency]; if (amount < 0) throw new Error("invalid amount"); - let { payment_hash: id, bolt11: text } = await ln.invoice( + let { payment_hash: hash, bolt11: text } = await ln.invoice( amount ? `${amount + tip}sat` : "any", new Date(), "", @@ -37,7 +37,7 @@ export default { invoice = { ...invoice, - id, + hash, amount, currency, rate, @@ -57,8 +57,8 @@ export default { `${text.substr(0, 8)}..${text.substr(-6)}` ); - await s(`invoice:${id}`, invoice); - await db.lPush(`${uid}:invoices`, id); + await s(`invoice:${hash}`, invoice); + await db.lPush(`${uid}:invoices`, hash); if (request_id) { let request = await g(`request:${request_id}`); -- cgit v1.2.3 From e918a818acf789a6619cbf4432f0c4713d4c53e6 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 17 Jan 2023 11:53:24 -0800 Subject: nostr cleanup --- index.js | 2 + lib/locations.js | 6 +- lib/nostr.js | 332 +++++++++++--------------------------------------- lib/rates.js | 3 +- lib/sendInternal.js | 5 +- lib/sockets.js | 8 +- lib/upload.js | 51 ++++---- routes/events.js | 1 - routes/locations.js | 8 +- routes/nostr.js | 32 +++-- routes/send.js | 2 +- sampleconfig/index.js | 2 +- 12 files changed, 128 insertions(+), 324 deletions(-) (limited to 'lib') diff --git a/index.js b/index.js index ec0e4244..9d291355 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ import app from "$app"; import { auth, optional } from "$lib/auth"; +import { fillPool } from "$lib/nostr"; import { listenForLightning } from "$lib/lightning"; import { getLocations } from "$lib/locations"; import { getRates, sendRates } from "$lib/rates"; @@ -15,6 +16,7 @@ import users from "$routes/users"; import payments from "$routes/payments"; import requests from "$routes/requests"; +fillPool(); listenForLightning(); getLocations(); diff --git a/lib/locations.js b/lib/locations.js index bd2cfb5e..39883555 100644 --- a/lib/locations.js +++ b/lib/locations.js @@ -8,10 +8,8 @@ export let getLocations = async () => { ).json(); locations = locations.filter( - l => - l["osm_json"].tags && l["osm_json"].tags["payment:coinos"] === "yes" - ) - + l => l["osm_json"].tags && l["osm_json"].tags["payment:coinos"] === "yes" + ); await s("locations", locations); } catch (e) {} diff --git a/lib/nostr.js b/lib/nostr.js index 93370587..d4dc2e67 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -7,16 +7,81 @@ import { g, s, db } from "$lib/db"; import { nada, wait } from "$lib/utils"; export let pool; -try { + +export let fillPool = () => { pool = RelayPool(config.relays); -} catch (e) { - console.log(e); -} + pool.on("open", relay => { + if (relay.url.includes(config.relay)) coinos = relay; + relay.subscribe("live", { limit: 1 }); + }); + + pool.on("event", async (relay, sub, ev) => { + if (timeouts[sub]) timeouts[sub].extend(ev); + try { + let content; + try { + content = JSON.parse(ev.content); + } catch (e) { + content = ev.content; + } + + if (sub === "live") { + let { pubkey } = ev; + + if (Math.abs(Math.floor(Date.now() / 1000) - ev.created_at) > 7200) + return; + + if (seen.includes(ev.id)) return; + seen.push(ev.id); + seen.length > 1000 && seen.shift(); + + if (coinos && ev.kind < 5) { + // coinos.send(["EVENT", ev]); + ev.user = await getUser(pubkey); + // broadcast("event", ev); + } + } else if (sub.includes("profile")) { + let { pubkey } = ev; + let user = await getUser(pubkey); + + if (user.updated > ev.created_at) return; -pool.on("open", relay => { - if (relay.url.includes(config.relay)) coinos = relay; - relay.subscribe("live", { limit: 1 }); -}); + if (content.name && user.username === pubkey.substr(0, 6)) + user.username = content.name; + + delete content.name; + + user = { + ...user, + ...content, + updated: ev.created_at + }; + + await s(`user:${pubkey}`, JSON.stringify(user)); + coinos.send(ev); + } else if (sub.includes("follows")) { + let pubkey = sub.split(":")[0]; + await s(`${pubkey}:follows`, JSON.stringify(ev.tags)); + for (let f of ev.tags) { + let [_, followPubkey] = f; + await db.sAdd(`${followPubkey}:followers`, pubkey); + } + coinos.send(ev); + } else if (sub.includes("followers")) { + let followed = sub.split(":")[0]; + let { pubkey } = ev; + await db.sAdd(`${followed}:followers`, pubkey); + coinos.send(ev); + } else if (sub.includes("notes")) { + let pubkey = sub.split(":")[0]; + await db.sAdd(pubkey, ev.id); + await s(`ev:${ev.id}`, JSON.stringify(ev)); + } + } catch (e) { + console.log(e); + } + }); +}; export let coinos; let now = () => Math.round(Date.now() / 1000); @@ -97,254 +162,3 @@ let getUser = async pubkey => pubkey, anon: true }; - -pool.on("event", async (relay, sub, ev) => { - if (timeouts[sub]) timeouts[sub].extend(ev); - try { - let content; - try { - content = JSON.parse(ev.content); - } catch (e) { - content = ev.content; - } - - if (sub === "live") { - let { pubkey } = ev; - - if (Math.abs(Math.floor(Date.now() / 1000) - ev.created_at) > 7200) - return; - - if (seen.includes(ev.id)) return; - seen.push(ev.id); - seen.length > 1000 && seen.shift(); - - if (coinos && ev.kind < 5) { - // coinos.send(["EVENT", ev]); - ev.user = await getUser(pubkey); - // broadcast("event", ev); - } - } else if (sub.includes("profile")) { - let { pubkey } = ev; - let user = await getUser(pubkey); - - if (user.updated > ev.created_at) return; - - if (content.name && user.username === pubkey.substr(0, 6)) - user.username = content.name; - - delete content.name; - - user = { - ...user, - ...content, - updated: ev.created_at - }; - - await s(`user:${pubkey}`, JSON.stringify(user)); - coinos.send(ev); - } else if (sub.includes("follows")) { - let pubkey = sub.split(":")[0]; - await s(`${pubkey}:follows`, JSON.stringify(ev.tags)); - for (let f of ev.tags) { - let [_, followPubkey] = f; - await db.sAdd(`${followPubkey}:followers`, pubkey); - } - coinos.send(ev); - } else if (sub.includes("followers")) { - let followed = sub.split(":")[0]; - let { pubkey } = ev; - await db.sAdd(`${followed}:followers`, pubkey); - coinos.send(ev); - } else if (sub.includes("notes")) { - let pubkey = sub.split(":")[0]; - await db.sAdd(pubkey, ev.id); - await s(`ev:${ev.id}`, JSON.stringify(ev)); - } - } catch (e) { - console.log(e); - } -}); - -export default { -async event (req, res) { - try { - let event = JSON.parse(await g(`ev:${req.params.id}`)); - let { pubkey } = event; - - event.user = JSON.parse(await g(`user:${pubkey}`)) || { - username: pubkey.substr(0, 6), - pubkey, - anon: true, - follows: [], - followers: [] - }; - - res.send(event); - } catch (e) { - res.code(500).send("Problem fetching event"); - } -}, - -async notes (req, res) { - try { - let { pubkey } = req.params; - - let user = JSON.parse(await g(`user:${pubkey}`)); - - if (!user) { - user = await db.User.findOne({ - where: { pubkey } - }); - } - - if (!user) - user = { - username: pubkey.substr(0, 6), - pubkey, - anon: true, - follows: [], - followers: [] - }; - - q( - `${pubkey}:notes`, - { - kinds: [1], - authors: [pubkey] - }, - { since: 0 } - ).catch(nada); - - await g(`user:${pubkey}`, JSON.stringify(user)); - - let ids = await db.sMembers(pubkey); - - let events = ids.length - ? (await db.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) - : []; - - res.send(events.map(e => ({ ...e, user }))); - } catch (e) { - console.log(e); - res.code(500).send("problem fetching user events"); - } -}, - -async broadcast (req, res) { - let { event } = req.body; - pool.send(["EVENT", event]); - res.send(event); -}, - -async follows (req, res) { - try { - let { pubkey } = req.params; - let { tagsonly } = req.query; - - let sub = `${pubkey}:follows`; - q( - sub, - { - limit: 1, - kinds: [3], - authors: [pubkey] - }, - { timeout: 60000, eager: 60000 } - ).catch(nada); - - let tags = JSON.parse(await g(`${pubkey}:follows`)) || []; - if (tagsonly) return res.send(tags); - - let follows = []; - for (let f of tags) { - let [_, pubkey] = f; - - q(`${pubkey}:profile:f1`, { - limit: 1, - kinds: [0], - authors: [pubkey] - }).catch(nada); - - let user = JSON.parse(await g(`user:${pubkey}`)); - - if (!user) - user = { - username: pubkey.substr(0, 6), - pubkey, - anon: true - }; - - follows.push(user); - } - - follows = uniq(follows, e => e.pubkey); - follows.sort((a, b) => a.username.localeCompare(b.username)); - - res.send(follows); - } catch (e) { - console.log(e); - res.code(500).send(e && e.message); - } -}); - -async (req, res) followers { - try { - let { pubkey } = req.params; - - let pubkeys = [ - ...new Set([ - ...(await got(`${config.nostr}/followers?pubkey=${pubkey}`).json()), - ...(await db.sMembers(`${pubkey}:followers`)) - ]) - ]; - - let followers = []; - - q( - `${pubkey}:followers`, - { kinds: [3], "#p": [pubkey] }, - { timeout: 60000, eager: 60000 } - ).catch(nada); - - for (let pubkey of pubkeys) { - let user = JSON.parse(await g(`user:${pubkey}`)); - if (!user || !user.updated) { - q(`${pubkey}:profile:f2`, { - limit: 1, - kinds: [0], - authors: [pubkey] - }).catch(nada); - - user = JSON.parse(await g(`user:${pubkey}`)); - } - - if (!user) - user = { - username: pubkey.substr(0, 6), - pubkey, - anon: true - }; - - followers.push(user); - } - - followers = uniq(followers, e => e.pubkey); - followers.sort((a, b) => a.username.localeCompare(b.username)); - - res.send(followers); - } catch (e) { - console.log(e); - res.code(500).send(e && e.message); - } -}, - -async identities (req, res) { - res.send({ - names: { - adam: "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8", - asoltys: - "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8" - } - }); -} -} diff --git a/lib/rates.js b/lib/rates.js index 00e1bd88..6f87c78e 100644 --- a/lib/rates.js +++ b/lib/rates.js @@ -37,7 +37,8 @@ export let sendRates = () => broadcast("rate", store.last); export let getRates = async () => { connect(); - let date = 0, fx; + let date = 0, + fx; let rates = await g("rates"); if (rates) ({ date, fx } = rates); diff --git a/lib/sendInternal.js b/lib/sendInternal.js index 0ba0f0b4..32e94aa2 100644 --- a/lib/sendInternal.js +++ b/lib/sendInternal.js @@ -11,10 +11,7 @@ import { g, s, db } from "$lib/db"; const { HOSTNAME: hostname } = process.env; -export default async ( - { amount, hash, memo, tip }, - user -) => { +export default async ({ amount, hash, memo, tip }, user) => { amount = parseInt(amount); if (!amount || amount < 0) fail("Amount must be greater than zero"); diff --git a/lib/sockets.js b/lib/sockets.js index 7f655570..e803d5b0 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -47,7 +47,6 @@ export const broadcast = (type, data) => { }); }; - export async function socketServer(app) { let seconds = (n, d = new Date()) => d.setSeconds(d.getSeconds() - n) && d; @@ -90,9 +89,7 @@ export async function socketServer(app) { const existing = Object.keys(store.sockets[uid]); if (existing.length > 4) { const p = existing.find(sid => sid !== uid); - store.sockets[uid][p].send( - JSON.stringify({ type: "logout" }) - ); + store.sockets[uid][p].send(JSON.stringify({ type: "logout" })); setTimeout(() => { if (store.sockets[uid][p]) store.sockets[uid][p].close(code, "too many sockets"); @@ -149,7 +146,6 @@ export async function socketServer(app) { }); } - export let sendHeartbeat = () => { for (const uid in store.sockets) { for (const id in store.sockets[uid]) { @@ -158,4 +154,4 @@ export let sendHeartbeat = () => { if (ws.beats > 10) ws.close(code, `${uid} ${id} lost heartbeat`); } } -} +}; diff --git a/lib/upload.js b/lib/upload.js index b37e0bf2..43cc35ce 100644 --- a/lib/upload.js +++ b/lib/upload.js @@ -8,37 +8,36 @@ import { pipeline } from "stream"; import { g, s } from "$lib/db"; export default async ({ params: { type }, file, user }, res) => { - if (!["banner", "profile"].includes(type)) - throw new Error("invalid upload"); + if (!["banner", "profile"].includes(type)) throw new Error("invalid upload"); - let data = await file(); - let s1 = new Clone(data.file); - let s2 = new Clone(data.file); + let data = await file(); + let s1 = new Clone(data.file); + let s2 = new Clone(data.file); - let [format, ext] = (await fileTypeFromStream(s1)).mime.split("/"); + let [format, ext] = (await fileTypeFromStream(s1)).mime.split("/"); - if (format !== "image" && !["jpg", "jpeg", "png"].includes(ext)) - throw new Error("unsupported file type"); + if (format !== "image" && !["jpg", "jpeg", "png"].includes(ext)) + throw new Error("unsupported file type"); - let name = `${user.id}-${type}.webp`; - let path = `/app/uploads/${name}`; - let size = type === "banner" ? 1920 : 240; + let name = `${user.id}-${type}.webp`; + let path = `/app/uploads/${name}`; + let size = type === "banner" ? 1920 : 240; - let t = sharp() - .rotate() - .resize(size) - .webp(); + let t = sharp() + .rotate() + .resize(size) + .webp(); - await new Promise(r => pipeline(s2, t, fs.createWriteStream(path), r)); + await new Promise(r => pipeline(s2, t, fs.createWriteStream(path), r)); - let r = async () => { - await db.watch(`user:${user.id}`); - user = await g(`user:${user.id}`); - user[type] = true; - let m = await db.multi(); - await s(`user:${user.id}`, user); - }; - await r(); + let r = async () => { + await db.watch(`user:${user.id}`); + user = await g(`user:${user.id}`); + user[type] = true; + let m = await db.multi(); + await s(`user:${user.id}`, user); + }; + await r(); - res.send(name); -} + res.send(name); +}; diff --git a/routes/events.js b/routes/events.js index cea82118..21ee37d8 100644 --- a/routes/events.js +++ b/routes/events.js @@ -4,4 +4,3 @@ import redis from "$lib/db"; import { coinos, pool, q } from "$lib/nostr"; import store from "$lib/store"; import { nada, wait } from "$lib/utils"; - diff --git a/routes/locations.js b/routes/locations.js index e080a7b9..fb4487f7 100644 --- a/routes/locations.js +++ b/routes/locations.js @@ -1,7 +1,7 @@ import { g } from "$lib/db"; export default { -async list(req, res) { - res.send({ locations: await g('locations') }); -} -} + async list(req, res) { + res.send({ locations: await g("locations") }); + } +}; diff --git a/routes/nostr.js b/routes/nostr.js index 3d326e94..35b40c07 100644 --- a/routes/nostr.js +++ b/routes/nostr.js @@ -1,9 +1,14 @@ +import { nada, uniq } from "$lib/utils"; +import config from "$config"; +import { g, s, db } from "$lib/db"; +import { q } from "$lib/nostr"; + export default { async event({ params: { id } }, res) { - let event = JSON.parse(await g(`ev:${id}`)); + let event = await g(`ev:${id}`); let { pubkey } = event; - event.user = JSON.parse(await g(`user:${pubkey}`)) || { + event.user = (await g(`user:${pubkey}`)) || { username: pubkey.substr(0, 6), pubkey, anon: true, @@ -15,13 +20,7 @@ export default { }, async notes({ params: { pubkey } }, res) { - let user = JSON.parse(await g(`user:${pubkey}`)); - - if (!user) { - user = await db.User.findOne({ - where: { pubkey } - }); - } + let user = await g(`user:${pubkey}`); if (!user) user = { @@ -40,7 +39,7 @@ export default { q(`${pubkey}:notes`, params, opts).catch(nada); - await g(`user:${pubkey}`, JSON.stringify(user)); + await s(`user:${pubkey}`, user); let ids = await db.sMembers(pubkey); @@ -57,7 +56,7 @@ export default { res.send(event); }, - async follows({ params: { pubkey }, query: { tagsOnly } }, res) { + async follows({ params: { pubkey }, query: { tagsonly } }, res) { let sub = `${pubkey}:follows`, params = { limit: 1, @@ -68,7 +67,7 @@ export default { q(sub, params, opts).catch(nada); - let tags = JSON.parse(await g(`${pubkey}:follows`)) || []; + let tags = (await g(`${pubkey}:follows`)) || []; if (tagsonly) return res.send(tags); let follows = []; @@ -81,7 +80,7 @@ export default { authors: [pubkey] }).catch(nada); - let user = JSON.parse(await g(`user:${pubkey}`)); + let user = await g(`user:${pubkey}`); if (!user) user = { @@ -99,11 +98,10 @@ export default { res.send(follows); }, - async followers({ params: { pubkey}}, res) { + async followers({ params: { pubkey } }, res) { try { let pubkeys = [ ...new Set([ - ...(await got(`${config.nostr}/followers?pubkey=${pubkey}`).json()), ...(await db.sMembers(`${pubkey}:followers`)) ]) ]; @@ -117,7 +115,7 @@ export default { ).catch(nada); for (let pubkey of pubkeys) { - let user = JSON.parse(await g(`user:${pubkey}`)); + let user = await g(`user:${pubkey}`); if (!user || !user.updated) { q(`${pubkey}:profile:f2`, { limit: 1, @@ -125,7 +123,7 @@ export default { authors: [pubkey] }).catch(nada); - user = JSON.parse(await g(`user:${pubkey}`)); + user = await g(`user:${pubkey}`); } if (!user) diff --git a/routes/send.js b/routes/send.js index 1bd4910a..4b954616 100644 --- a/routes/send.js +++ b/routes/send.js @@ -8,7 +8,7 @@ export default async (req, res, next) => { let payment = await sendInternal(req.body, req.user); res.send(payment); } catch (e) { - console.log(e) + console.log(e); err("problem sending internal payment", e.message); return res.code(500).send(e.message); } diff --git a/sampleconfig/index.js b/sampleconfig/index.js index 8683bfa6..8fcb0a6f 100644 --- a/sampleconfig/index.js +++ b/sampleconfig/index.js @@ -10,7 +10,7 @@ export default { username: "admin1", password: "123", network: "regtest", - port: 18443, + port: 18443 }, lightning: "/app/config/lightning/regtest/lightning-rpc" }; -- cgit v1.2.3 From 5ae4209579287c571559edcf00dce24754553aaf Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 17 Jan 2023 12:30:30 -0800 Subject: rename db --- docker-compose.yml | 2 +- lib/db.js | 17 +++++++-- lib/lightning.js | 12 +++--- lib/rates.js | 1 + lib/sockets.js | 1 + lib/utils.js | 2 +- routes/nostr.js | 2 +- routes/users.js | 107 ++++++++++++++++++++++++----------------------------- 8 files changed, 73 insertions(+), 71 deletions(-) (limited to 'lib') diff --git a/docker-compose.yml b/docker-compose.yml index 11047bf4..0555775d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: image: cs container_name: app depends_on: - - rd + - db - bc - cl command: "pnpm dev" diff --git a/lib/db.js b/lib/db.js index 76653c81..c21e224f 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,8 +1,6 @@ import { createClient } from "redis"; -export const db = createClient({ - url: "redis://rd" -}); +export const db = createClient({ url: "redis://db" }); db.on("error", err => console.log("db error", err)); @@ -12,3 +10,16 @@ export default db; export let g = async k => JSON.parse(await db.get(k)); export let s = (k, v) => db.set(k, JSON.stringify(v)); + +let retries = {}; +export let t = async (k, f) => { + retries[k] = retries[k] ? retries[k]++ : 0; + await db.watch(k); + let o = f(await g(k)); + let m = await db.multi(); + await s(k, o); + + if (retries[k] < 10) { + if (!(await m.exec())) setTimeout(() => t(k, f), 100); + } else throw new Error("could not obtain lock"); +}; diff --git a/lib/lightning.js b/lib/lightning.js index 2a8e8053..be5929b7 100644 --- a/lib/lightning.js +++ b/lib/lightning.js @@ -8,8 +8,8 @@ import { g, s, db } from "$lib/db"; export async function listenForLightning() { let { - payment_hash, - bolt11: request, + payment_hash: hash, + bolt11, pay_index, status, msatoshi_received, @@ -25,10 +25,10 @@ export async function listenForLightning() { if (!preimage) return; - let invoice = await g(`invoice:${payment_hash}`); - if (!invoice) return warn("received lightning with no invoice", request); + let invoice = await g(`invoice:${hash}`); + if (!invoice) return warn("received lightning with no invoice", bolt11); - let { hash, currency, memo, rate, tip, uid } = invoice; + let { currency, memo, rate, tip, uid } = invoice; let amount = parseInt(received) - tip; if (amount < 0) throw new Error("amount out of range"); @@ -50,7 +50,7 @@ export async function listenForLightning() { invoice.status = "paid"; invoice.received += received; - await s(`invoice:${payment_hash}`, invoice); + await s(`invoice:${hash}`, invoice); let t = async () => { await db.watch(`balance:${uid}`); diff --git a/lib/rates.js b/lib/rates.js index 6f87c78e..7cc24e6b 100644 --- a/lib/rates.js +++ b/lib/rates.js @@ -21,6 +21,7 @@ let connect = () => { store.last = msg.c; } catch (e) { + console.log(e) err("binance message error", e.message); } }; diff --git a/lib/sockets.js b/lib/sockets.js index e803d5b0..a7d392a9 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -83,6 +83,7 @@ export async function socketServer(app) { let { id: uid } = jwt.decode(token); if (!uid) throw new Error("Invalid JWT token"); let user = await g(`user:${uid}`); + console.log("uid", uid) if (!user) throw new Error("User not found"); if (!store.sockets[uid]) store.sockets[uid] = {}; diff --git a/lib/utils.js b/lib/utils.js index e53bf649..8d2648f2 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -26,4 +26,4 @@ export const SATS = 100000000; export const toSats = n => Math.round(n * SATS); export const uniq = (a, k) => [...new Map(a.map(x => [k(x), x])).values()]; -export const pick = (O, K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); +export const pick = (O, K) => K.reduce((o, k) => (typeof O[k] !== 'undefined' && (o[k] = O[k]), o), {}); diff --git a/routes/nostr.js b/routes/nostr.js index 35b40c07..1cf9d595 100644 --- a/routes/nostr.js +++ b/routes/nostr.js @@ -1,7 +1,7 @@ import { nada, uniq } from "$lib/utils"; import config from "$config"; import { g, s, db } from "$lib/db"; -import { q } from "$lib/nostr"; +import { pool, q } from "$lib/nostr"; export default { async event({ params: { id } }, res) { diff --git a/routes/users.js b/routes/users.js index 53b02d02..c5ac61b1 100644 --- a/routes/users.js +++ b/routes/users.js @@ -117,69 +117,58 @@ export default { res.send({}); }, - async update( - { user, body: { confirm, password, pin, newpin, username } }, - res - ) { - try { - let { user } = req; - - l("updating user", user.username); - - if (user.pin && !(pin === user.pin)) throw new Error("Pin required"); - if (typeof newpin !== "undefined") user.pin = newpin; - if (!user.pin) user.pin = null; - - let exists; - if (username) exists = await g(`user:${username}`); - - let token; - if (user.username !== username && exists) { - err("username taken", username, user.username, exists.username); - throw new Error("Username taken"); - } else if (username) { - if (user.username !== username) - l("changing username", user.username, username); - user.username = username; - } - - let attributes = [ - "address", - "cipher", - "currencies", - "currency", - "display", - "email", - "fiat", - "locktime", - "prompt", - "pubkey", - "salt", - "seed", - "tokens", - "twofa", - "unit" - ]; - - for (let a of attributes) { - if (req.body[a]) user[a] = req.body[a]; - } + async update({ user, body }, res) { + l("updating user", user.username); + + let { confirm, password, pin, newpin, username } = body; + + if (user.pin && !(pin === user.pin)) throw new Error("Pin required"); + if (typeof newpin !== "undefined") user.pin = newpin; + if (!user.pin) user.pin = null; + + let exists; + if (username) exists = await g(`user:${username}`); + + let token; + if (user.username !== username && exists) { + err("username taken", username, user.username, exists.username); + throw new Error("Username taken"); + } else if (username) { + if (user.username !== username) + l("changing username", user.username, username); + user.username = username; + } - if (password && password === confirm) { - user.password = await bcrypt.hash(password, 1); - } + let attributes = [ + "address", + "cipher", + "currencies", + "currency", + "display", + "email", + "fiat", + "locktime", + "prompt", + "pubkey", + "salt", + "seed", + "tokens", + "twofa", + ]; + + for (let a of attributes) { + if (body[a]) user[a] = body[a]; + } - await s(`user:${user.id}`, user); + if (password && password === confirm) { + user.password = await bcrypt.hash(password, 1); + } - user.haspin = !!user.pin; + user.haspin = !!user.pin; + await s(`user:${user.id}`, user); - emit(user.username, "user", user); - res.send({ user, token }); - } catch (e) { - console.log(e); - err("error updating user", e.message); - res.code(500).send(e.message); - } + emit(user.username, "user", user); + res.send({ user, token }); }, async login(req, res) { -- cgit v1.2.3 From e6cd93b51798992a01f4bfee61d24384d2fa4089 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 17 Jan 2023 12:30:41 -0800 Subject: prettier --- lib/rates.js | 2 +- lib/sockets.js | 2 +- lib/utils.js | 3 ++- routes/nostr.js | 4 +--- routes/users.js | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/rates.js b/lib/rates.js index 7cc24e6b..45c5977a 100644 --- a/lib/rates.js +++ b/lib/rates.js @@ -21,7 +21,7 @@ let connect = () => { store.last = msg.c; } catch (e) { - console.log(e) + console.log(e); err("binance message error", e.message); } }; diff --git a/lib/sockets.js b/lib/sockets.js index a7d392a9..1105f94d 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -83,7 +83,7 @@ export async function socketServer(app) { let { id: uid } = jwt.decode(token); if (!uid) throw new Error("Invalid JWT token"); let user = await g(`user:${uid}`); - console.log("uid", uid) + console.log("uid", uid); if (!user) throw new Error("User not found"); if (!store.sockets[uid]) store.sockets[uid] = {}; diff --git a/lib/utils.js b/lib/utils.js index 8d2648f2..92b34be5 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -26,4 +26,5 @@ export const SATS = 100000000; export const toSats = n => Math.round(n * SATS); export const uniq = (a, k) => [...new Map(a.map(x => [k(x), x])).values()]; -export const pick = (O, K) => K.reduce((o, k) => (typeof O[k] !== 'undefined' && (o[k] = O[k]), o), {}); +export const pick = (O, K) => + K.reduce((o, k) => (typeof O[k] !== "undefined" && (o[k] = O[k]), o), {}); diff --git a/routes/nostr.js b/routes/nostr.js index 1cf9d595..005a4be9 100644 --- a/routes/nostr.js +++ b/routes/nostr.js @@ -101,9 +101,7 @@ export default { async followers({ params: { pubkey } }, res) { try { let pubkeys = [ - ...new Set([ - ...(await db.sMembers(`${pubkey}:followers`)) - ]) + ...new Set([...(await db.sMembers(`${pubkey}:followers`))]) ]; let followers = []; diff --git a/routes/users.js b/routes/users.js index c5ac61b1..0358e548 100644 --- a/routes/users.js +++ b/routes/users.js @@ -153,7 +153,7 @@ export default { "salt", "seed", "tokens", - "twofa", + "twofa" ]; for (let a of attributes) { -- cgit v1.2.3 From 4b51fb8325acafac4e9231754dc58663f6431e1e Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 17 Jan 2023 12:33:17 -0800 Subject: rename app --- app.js | 40 ---------------------------------------- index.js | 2 +- lib/app.js | 40 ++++++++++++++++++++++++++++++++++++++++ lib/nostr.js | 1 - lib/sockets.js | 2 -- routes/events.js | 6 ------ routes/users.js | 1 - 7 files changed, 41 insertions(+), 51 deletions(-) delete mode 100644 app.js create mode 100644 lib/app.js delete mode 100644 routes/events.js (limited to 'lib') diff --git a/app.js b/app.js deleted file mode 100644 index af2bd59f..00000000 --- a/app.js +++ /dev/null @@ -1,40 +0,0 @@ -import config from "$config"; -import fastify from "fastify"; -import fastifyWs from "@fastify/websocket"; -import fastifyMultipart from "@fastify/multipart"; -import fastifyStatic from "@fastify/static"; -import fastifyPassport from "@fastify/passport"; -import fastifySecureSession from "@fastify/secure-session"; - -import path from "path"; -import pino from "pino"; - -import { jwtStrategy } from "$lib/auth"; -import { socketServer } from "$lib/sockets"; - -const app = fastify({ - logger: pino(), - disableRequestLogging: true, - maxParamLength: 500 -}); - -app.register(fastifySecureSession, { - key: Buffer.from(config.jwt, "hex") -}); - -app.register(fastifyPassport.initialize()); -app.register(fastifyPassport.secureSession()); - -fastifyPassport.use("jwt", jwtStrategy); - -app.register(fastifyMultipart); - -app.register(fastifyStatic, { - root: path.join("/app/uploads"), - prefix: "/public/" -}); - -app.register(fastifyWs); -app.register(socketServer); - -export default app; diff --git a/index.js b/index.js index 9d291355..ca33b646 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -import app from "$app"; +import app from "$lib/app"; import { auth, optional } from "$lib/auth"; import { fillPool } from "$lib/nostr"; diff --git a/lib/app.js b/lib/app.js new file mode 100644 index 00000000..af2bd59f --- /dev/null +++ b/lib/app.js @@ -0,0 +1,40 @@ +import config from "$config"; +import fastify from "fastify"; +import fastifyWs from "@fastify/websocket"; +import fastifyMultipart from "@fastify/multipart"; +import fastifyStatic from "@fastify/static"; +import fastifyPassport from "@fastify/passport"; +import fastifySecureSession from "@fastify/secure-session"; + +import path from "path"; +import pino from "pino"; + +import { jwtStrategy } from "$lib/auth"; +import { socketServer } from "$lib/sockets"; + +const app = fastify({ + logger: pino(), + disableRequestLogging: true, + maxParamLength: 500 +}); + +app.register(fastifySecureSession, { + key: Buffer.from(config.jwt, "hex") +}); + +app.register(fastifyPassport.initialize()); +app.register(fastifyPassport.secureSession()); + +fastifyPassport.use("jwt", jwtStrategy); + +app.register(fastifyMultipart); + +app.register(fastifyStatic, { + root: path.join("/app/uploads"), + prefix: "/public/" +}); + +app.register(fastifyWs); +app.register(socketServer); + +export default app; diff --git a/lib/nostr.js b/lib/nostr.js index d4dc2e67..23ea937b 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -1,4 +1,3 @@ -import app from "$app"; import config from "$config"; import { Relay, RelayPool } from "nostr"; import { broadcast } from "$lib/sockets"; diff --git a/lib/sockets.js b/lib/sockets.js index 1105f94d..234c647a 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -1,4 +1,3 @@ -import app from "$app"; import config from "$config"; import store from "$lib/store"; import jwt from "jsonwebtoken"; @@ -83,7 +82,6 @@ export async function socketServer(app) { let { id: uid } = jwt.decode(token); if (!uid) throw new Error("Invalid JWT token"); let user = await g(`user:${uid}`); - console.log("uid", uid); if (!user) throw new Error("User not found"); if (!store.sockets[uid]) store.sockets[uid] = {}; diff --git a/routes/events.js b/routes/events.js deleted file mode 100644 index 21ee37d8..00000000 --- a/routes/events.js +++ /dev/null @@ -1,6 +0,0 @@ -import app from "$app"; -import db from "$db"; -import redis from "$lib/db"; -import { coinos, pool, q } from "$lib/nostr"; -import store from "$lib/store"; -import { nada, wait } from "$lib/utils"; diff --git a/routes/users.js b/routes/users.js index 0358e548..49aeebd7 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1,4 +1,3 @@ -import app from "$app"; import { g, s } from "$lib/db"; import config from "$config"; import store from "$lib/store"; -- cgit v1.2.3 From 33229e38a910e26083175a3e272278745378114c Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 17 Jan 2023 16:05:00 -0800 Subject: redlock --- index.js | 2 ++ lib/db.js | 24 +++----------- lib/lightning.js | 69 +++++++++++++++++++++++++++++++++------- lib/nostr.js | 6 ++-- lib/sendInternal.js | 4 +-- package.json | 2 ++ pnpm-lock.yaml | 65 ++++++++++++++++++++++++++++++++++++++ routes/info.js | 90 ++++++++++------------------------------------------- routes/invoices.js | 2 +- routes/nostr.js | 6 ++-- routes/payments.js | 2 +- routes/requests.js | 4 +-- 12 files changed, 158 insertions(+), 118 deletions(-) (limited to 'lib') diff --git a/index.js b/index.js index ca33b646..fd365e1a 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ import { getRates, sendRates } from "$lib/rates"; import { sendHeartbeat } from "$lib/sockets"; import email from "$routes/email"; +import info from "$routes/info"; import locations from "$routes/locations"; import nostr from "$routes/nostr"; import rates from "$routes/rates"; @@ -25,6 +26,7 @@ getRates(); setInterval(sendRates, 1000); setInterval(sendHeartbeat, 2000); +app.get("/balances", info.balances); app.post("/email", email.send); app.get("/rate", rates.last); diff --git a/lib/db.js b/lib/db.js index c21e224f..fb99a756 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,25 +1,9 @@ -import { createClient } from "redis"; - -export const db = createClient({ url: "redis://db" }); - -db.on("error", err => console.log("db error", err)); - -await db.connect(); +import { Redis } from "ioredis"; +import Redlock from "redlock"; +export const db = new Redis("redis://db", { enableAutoPipelining: true }); export default db; export let g = async k => JSON.parse(await db.get(k)); export let s = (k, v) => db.set(k, JSON.stringify(v)); - -let retries = {}; -export let t = async (k, f) => { - retries[k] = retries[k] ? retries[k]++ : 0; - await db.watch(k); - let o = f(await g(k)); - let m = await db.multi(); - await s(k, o); - - if (retries[k] < 10) { - if (!(await m.exec())) setTimeout(() => t(k, f), 100); - } else throw new Error("could not obtain lock"); -}; +export let t = (f) => new Redlock([db]).using([db], 5000, f); diff --git a/lib/lightning.js b/lib/lightning.js index be5929b7..57667be2 100644 --- a/lib/lightning.js +++ b/lib/lightning.js @@ -4,7 +4,8 @@ import { notify } from "$lib/notifications"; import { callWebhook } from "$lib/webhooks"; import ln from "$lib/ln"; import { l, err, warn } from "$lib/logging"; -import { g, s, db } from "$lib/db"; +import { g, s, t, db } from "$lib/db"; +import Redlock from "redlock"; export async function listenForLightning() { let { @@ -50,22 +51,66 @@ export async function listenForLightning() { invoice.status = "paid"; invoice.received += received; - await s(`invoice:${hash}`, invoice); - let t = async () => { - await db.watch(`balance:${uid}`); - let b = await g(`balance:${uid}`); - b += received; - let m = await db.multi(); - await s(`balance:${uid}`, b); - if (!(await m.exec())) t(); - }; - await t(); + const redlock = new Redlock([db]); + + await s(`invoice:${hash}`, invoice); + console.log("BAL", await g(`balance:${uid}`)); + setTimeout(async () => { + let lock = await redlock.acquire(["b"], 5000); + try { + let bal = await g(`balance:${uid}`); + console.log("b bal", bal); + await new Promise(r => setTimeout(r, 50)); + + await s(`balance:${uid}`, bal + 100); + console.log("set b bal", bal + 100); + } finally { + await lock.release(); + } + // try { + // console.log("tweaking balance"); + // await s(`balance:${uid}`, 0); + // await new Promise(r => setTimeout(r, 5)); + // console.log("mwahaha", await g(`balance:${uid}`)); + // await new Promise(r => setTimeout(r, 600)); + // console.log("hmm", await g(`balance:${uid}`)); + // } catch (e) { + // console.log(e); + // } + }, 50); + + await new Promise(r => setTimeout(r, 5)); + + let lock = await redlock.acquire(["a"], 5000); + try { + let bal = await g(`balance:${uid}`); + console.log("a bal", bal, received); + await new Promise(r => setTimeout(r, 500)); + + await s(`balance:${uid}`, bal + received); + console.log("set a bal", bal + received); + } finally { + await lock.release(); + } + + // await t(async signal => { + // console.log("acquiring lock"); + // let bal = await g(`balance:${uid}`); + // console.log("bal", bal); + // await new Promise(r => setTimeout(r, 500)); + // + // if (signal.aborted) throw signal.error; + // + // await s(`balance:${uid}`, bal + received); + // console.log("set bal", bal + received); + // }); + console.log("done", await g(`balance:${uid}`)); payment.invoice = invoice; await s(`payment:${hash}`, payment); - await db.lPush(`${uid}:payments`, hash); + await db.lpush(`${uid}:payments`, hash); callWebhook(invoice, payment); diff --git a/lib/nostr.js b/lib/nostr.js index 23ea937b..c8accf85 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -63,17 +63,17 @@ export let fillPool = () => { await s(`${pubkey}:follows`, JSON.stringify(ev.tags)); for (let f of ev.tags) { let [_, followPubkey] = f; - await db.sAdd(`${followPubkey}:followers`, pubkey); + await db.sadd(`${followPubkey}:followers`, pubkey); } coinos.send(ev); } else if (sub.includes("followers")) { let followed = sub.split(":")[0]; let { pubkey } = ev; - await db.sAdd(`${followed}:followers`, pubkey); + await db.sadd(`${followed}:followers`, pubkey); coinos.send(ev); } else if (sub.includes("notes")) { let pubkey = sub.split(":")[0]; - await db.sAdd(pubkey, ev.id); + await db.sadd(pubkey, ev.id); await s(`ev:${ev.id}`, JSON.stringify(ev)); } } catch (e) { diff --git a/lib/sendInternal.js b/lib/sendInternal.js index 32e94aa2..6054f753 100644 --- a/lib/sendInternal.js +++ b/lib/sendInternal.js @@ -39,7 +39,7 @@ export default async ({ amount, hash, memo, tip }, user) => { }; await s(`payment:${hash}`, p1); - await db.lPush(`${user.id}:payments`, hash); + await db.lpush(`${user.id}:payments`, hash); l("sent internal", user.username, -p1.amount); emit(user.id, "payment", p1); @@ -79,7 +79,7 @@ export default async ({ amount, hash, memo, tip }, user) => { invoice.received += amount; await s(`invoice:${hash}`, invoice); await s(`payment:${id}`, p2); - await db.lPush(`${recipient.id}:payments`, id); + await db.lpush(`${recipient.id}:payments`, id); l("received internal", recipient.username, amount); emit(recipient.username, "payment", p2); diff --git a/package.json b/package.json index 2c1b7258..6f1c45f9 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "fastify": "^4.5.3", "file-type": "^18.0.0", "got": "^12.5.3", + "ioredis": "^5.2.5", "jsonwebtoken": "^8.5.1", "lnurl": "^0.24.2", "nodemon": "^2.0.7", @@ -32,6 +33,7 @@ "query-string": "^6.13.1", "readable-stream-clone": "^0.0.7", "redis": "^4.3.1", + "redlock": "5.0.0-beta.2", "sharp": "^0.31.1", "uuid": "8", "web-push": "^3.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c9f09dd..4c22c13f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,7 @@ specifiers: fastify: ^4.5.3 file-type: ^18.0.0 got: ^12.5.3 + ioredis: ^5.2.5 jsonwebtoken: ^8.5.1 lnurl: ^0.24.2 nodemon: ^2.0.7 @@ -27,6 +28,7 @@ specifiers: query-string: ^6.13.1 readable-stream-clone: ^0.0.7 redis: ^4.3.1 + redlock: 5.0.0-beta.2 sharp: ^0.31.1 uuid: '8' web-push: ^3.4.4 @@ -44,6 +46,7 @@ dependencies: fastify: 4.8.1 file-type: 18.0.0 got: 12.5.3 + ioredis: 5.2.5 jsonwebtoken: 8.5.1 lnurl: 0.24.2 nodemon: 2.0.20 @@ -58,6 +61,7 @@ dependencies: query-string: 6.14.1 readable-stream-clone: 0.0.7 redis: 4.3.1 + redlock: 5.0.0-beta.2 sharp: 0.31.1 uuid: 8.3.2 web-push: 3.5.0 @@ -175,6 +179,10 @@ packages: - utf-8-validate dev: false + /@ioredis/commands/1.2.0: + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + dev: false + /@mapbox/node-pre-gyp/1.0.10: resolution: {integrity: sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==} hasBin: true @@ -856,6 +864,11 @@ packages: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false + /denque/2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dev: false + /depd/2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1390,6 +1403,23 @@ packages: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} dev: false + /ioredis/5.2.5: + resolution: {integrity: sha512-7HKo/ClM2DGLRXdFq8ruS3Uuadensz4A76wPOU0adqlOqd1qkhoLPDaBhmVhUhNGpB+J65/bhLmNB8DDY99HJQ==} + engines: {node: '>=12.22.0'} + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.0 + debug: 4.3.4 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + /ip/2.0.0: resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} dev: false @@ -1544,10 +1574,18 @@ packages: - supports-color dev: false + /lodash.defaults/4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + dev: false + /lodash.includes/4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} dev: false + /lodash.isarguments/3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + dev: false + /lodash.isboolean/3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} dev: false @@ -1737,6 +1775,10 @@ packages: semver: 7.3.8 dev: false + /node-abort-controller/3.0.1: + resolution: {integrity: sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw==} + dev: false + /node-addon-api/2.0.2: resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} dev: false @@ -2129,6 +2171,18 @@ packages: engines: {node: '>= 12.13.0'} dev: false + /redis-errors/1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + dev: false + + /redis-parser/3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + dependencies: + redis-errors: 1.2.0 + dev: false + /redis/4.3.1: resolution: {integrity: sha512-cM7yFU5CA6zyCF7N/+SSTcSJQSRMEKN0k0Whhu6J7n9mmXRoXugfWDBo5iOzGwABmsWKSwGPTU5J4Bxbl+0mrA==} dependencies: @@ -2140,6 +2194,13 @@ packages: '@redis/time-series': 1.0.3_@redis+client@1.3.0 dev: false + /redlock/5.0.0-beta.2: + resolution: {integrity: sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw==} + engines: {node: '>=12'} + dependencies: + node-abort-controller: 3.0.1 + dev: false + /require-from-string/2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2403,6 +2464,10 @@ packages: engines: {node: '>= 10.x'} dev: false + /standard-as-callback/2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + dev: false + /statuses/2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} diff --git a/routes/info.js b/routes/info.js index eb42ca35..00a4915e 100644 --- a/routes/info.js +++ b/routes/info.js @@ -1,90 +1,32 @@ -import app from "$app"; -import db from "$db"; -import config from "$config"; -import store from "$lib/store"; - -import bc from "$lib/bitcoin"; -import lq from "$lib/liquid"; +import db from "$lib/db"; import ln from "$lib/ln"; -import sequelize from "@sequelize/core"; -import { SATS } from "$lib/utils"; - -const { Op } = sequelize; - -/** - * @api {get} /info Request server information - * @apiName GetInfo - * @apiGroup Info - * - * @apiSuccess {Object} fx Exchange rates relative to USD - fx.CUR is the value of one CUR in USD. - * @apiSuccess {String[]} networks Array of supported networks; possible values are "bitcoin", "liquid" and "lightning". - * @apiSuccess {String} clientVersion the current git commit of the ui - */ -app.get("/info", async (req, res, next) => { - const { clientVersion } = config; - - const info = { - fx: store.fx, - nodes: store.networks, - clientVersion - }; - - res.send(info); -}); +export default { + async balances(req, res) { + let lnchannel; + let lnwallet; -app.get("/balances", async (req, res, next) => { - try { - const accounts = await db.Account.findAll({ - where: { - pubkey: null - }, - attributes: [ - "asset", - [sequelize.fn("sum", sequelize.col("balance")), "total"] - ], - group: ["asset"] + const stream = db.scanStream({ + match: "balance:*", + count: 100 }); - let lnchannel; - let lnwallet; + let b = 0; + for await (let keys of stream) { + for (let k of keys) b += parseInt(await db.get(k)); + } + console.log(b) const funds = await ln.listfunds(); lnchannel = parseInt(funds.channels.reduce((a, b) => a + b.channel_sat, 0)); lnwallet = parseInt(funds.outputs.reduce((a, b) => a + b.value, 0)); - let bitcoin, bitcoind; - if (config.bitcoin) { - bitcoin = parseInt((await bc.getBalance()) * SATS); - bitcoind = await bc.getNetworkInfo(); - } - - let assets, liquid, elementsd; - if (config.liquid) { - assets = await lq.getBalance(); - liquid = parseInt(assets.bitcoin * SATS); - elementsd = await lq.getNetworkInfo(); - } - const info = { - bitcoind, - elementsd, - accounts, - assets, - bitcoin, - liquid, lnchannel, - lnwallet + lnwallet, + total: lnchannel + lnwallet }; - info.total = - parseInt(bitcoin) + - parseInt(liquid) + - parseInt(lnchannel) + - parseInt(lnwallet); - res.send(info); - } catch (e) { - console.log("problem getting balances", e); } -}); +}; diff --git a/routes/invoices.js b/routes/invoices.js index acd9457e..3e43c57c 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -58,7 +58,7 @@ export default { ); await s(`invoice:${hash}`, invoice); - await db.lPush(`${uid}:invoices`, hash); + await db.lpush(`${uid}:invoices`, hash); if (request_id) { let request = await g(`request:${request_id}`); diff --git a/routes/nostr.js b/routes/nostr.js index 005a4be9..bac04810 100644 --- a/routes/nostr.js +++ b/routes/nostr.js @@ -41,10 +41,10 @@ export default { await s(`user:${pubkey}`, user); - let ids = await db.sMembers(pubkey); + let ids = await db.smembers(pubkey); let events = ids.length - ? (await db.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) + ? (await db.mget(ids.map(k => "ev:" + k))).map(JSON.parse) : []; res.send(events.map(e => ({ ...e, user }))); @@ -101,7 +101,7 @@ export default { async followers({ params: { pubkey } }, res) { try { let pubkeys = [ - ...new Set([...(await db.sMembers(`${pubkey}:followers`))]) + ...new Set([...(await db.smembers(`${pubkey}:followers`))]) ]; let followers = []; diff --git a/routes/payments.js b/routes/payments.js index 13c0cdb7..22de79c3 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -21,7 +21,7 @@ export default { // if (start) where.createdAt[Op.gte] = new Date(parseInt(start)); // if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); - let payments = (await db.lRange(`${id}:payments`, 0, -1)) || []; + let payments = (await db.lrange(`${id}:payments`, 0, -1)) || []; payments = await Promise.all(payments.map(hash => g(`payment:${hash}`))); payments = payments.filter(p => p); res.send({ payments, total: payments.length }); diff --git a/routes/requests.js b/routes/requests.js index 79af1fb6..8db373dd 100644 --- a/routes/requests.js +++ b/routes/requests.js @@ -15,8 +15,8 @@ export default { try { const day = new Date(new Date().setDate(new Date().getDate() - 1)); - let invoices = await db.lRange(`${id}:invoices`, 0, -1); - let requests = await db.lRange(`${id}:requests`, 0, -1); + let invoices = await db.lrange(`${id}:invoices`, 0, -1); + let requests = await db.lrange(`${id}:requests`, 0, -1); res.send({ invoices, requests }); } catch (e) { -- cgit v1.2.3 From d62cc3b3db7d96ec48b71bb4adff99d134f934d7 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 17 Jan 2023 23:19:56 -0800 Subject: revert redlock --- lib/db.js | 39 +++++++++++++++++++--- lib/lightning.js | 62 ++--------------------------------- lib/nostr.js | 6 ++-- lib/sendInternal.js | 93 ----------------------------------------------------- lib/whitelist.js | 2 -- package.json | 2 -- pnpm-lock.yaml | 65 ------------------------------------- routes/invoices.js | 2 +- routes/nostr.js | 4 +-- routes/payments.js | 89 ++++++++++++++++++++++++++++++++++++++++++++++---- routes/requests.js | 4 +-- routes/send.js | 15 --------- routes/users.js | 1 + 13 files changed, 129 insertions(+), 255 deletions(-) delete mode 100644 lib/sendInternal.js delete mode 100644 routes/send.js (limited to 'lib') diff --git a/lib/db.js b/lib/db.js index fb99a756..1122038c 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,9 +1,40 @@ -import { Redis } from "ioredis"; -import Redlock from "redlock"; +import { sleep } from "$lib/utils"; +import { createClient } from "redis"; -export const db = new Redis("redis://db", { enableAutoPipelining: true }); +export const db = createClient({ url: "redis://db" }); +await db.connect(); export default db; export let g = async k => JSON.parse(await db.get(k)); export let s = (k, v) => db.set(k, JSON.stringify(v)); -export let t = (f) => new Redlock([db]).using([db], 5000, f); + +let retries = {}; +export let t = async (k, f) => { + let result; + try { + result = await db.executeIsolated(async db => { + await db.watch(k); + let v = await db.get(k); + await db + .multi() + .set(k, await f(v)) + .exec(); + }); + } catch (err) { + if (!err.message.includes("watch")) throw err; + + let r = retries[k] || 0; + retries[k] = r + 1; + + if (r < 10) { + await sleep(100); + await t(k, f); + } else { + delete retries[k]; + throw new Error("unable to obtain lock"); + } + } + + delete retries[k]; + return result; +}; diff --git a/lib/lightning.js b/lib/lightning.js index 57667be2..86dfdb2b 100644 --- a/lib/lightning.js +++ b/lib/lightning.js @@ -5,7 +5,6 @@ import { callWebhook } from "$lib/webhooks"; import ln from "$lib/ln"; import { l, err, warn } from "$lib/logging"; import { g, s, t, db } from "$lib/db"; -import Redlock from "redlock"; export async function listenForLightning() { let { @@ -18,6 +17,7 @@ export async function listenForLightning() { } = await ln.waitanyinvoice(await g("pay_index")); await s("pay_index", pay_index); + setTimeout(listenForLightning); let received = Math.round(msatoshi_received / 1000); @@ -52,65 +52,11 @@ export async function listenForLightning() { invoice.status = "paid"; invoice.received += received; - const redlock = new Redlock([db]); - await s(`invoice:${hash}`, invoice); - console.log("BAL", await g(`balance:${uid}`)); - setTimeout(async () => { - let lock = await redlock.acquire(["b"], 5000); - try { - let bal = await g(`balance:${uid}`); - console.log("b bal", bal); - await new Promise(r => setTimeout(r, 50)); - - await s(`balance:${uid}`, bal + 100); - console.log("set b bal", bal + 100); - } finally { - await lock.release(); - } - // try { - // console.log("tweaking balance"); - // await s(`balance:${uid}`, 0); - // await new Promise(r => setTimeout(r, 5)); - // console.log("mwahaha", await g(`balance:${uid}`)); - // await new Promise(r => setTimeout(r, 600)); - // console.log("hmm", await g(`balance:${uid}`)); - // } catch (e) { - // console.log(e); - // } - }, 50); - - await new Promise(r => setTimeout(r, 5)); - - let lock = await redlock.acquire(["a"], 5000); - try { - let bal = await g(`balance:${uid}`); - console.log("a bal", bal, received); - await new Promise(r => setTimeout(r, 500)); - - await s(`balance:${uid}`, bal + received); - console.log("set a bal", bal + received); - } finally { - await lock.release(); - } - - // await t(async signal => { - // console.log("acquiring lock"); - // let bal = await g(`balance:${uid}`); - // console.log("bal", bal); - // await new Promise(r => setTimeout(r, 500)); - // - // if (signal.aborted) throw signal.error; - // - // await s(`balance:${uid}`, bal + received); - // console.log("set bal", bal + received); - // }); - console.log("done", await g(`balance:${uid}`)); - + await db.incrBy(`balance:${uid}`, received); payment.invoice = invoice; - await s(`payment:${hash}`, payment); - await db.lpush(`${uid}:payments`, hash); + await db.lPush(`${uid}:payments`, hash); callWebhook(invoice, payment); @@ -122,6 +68,4 @@ export async function listenForLightning() { console.log(e); err("problem receiving lightning payment", e.message); } - - setTimeout(listenForLightning); } diff --git a/lib/nostr.js b/lib/nostr.js index c8accf85..23ea937b 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -63,17 +63,17 @@ export let fillPool = () => { await s(`${pubkey}:follows`, JSON.stringify(ev.tags)); for (let f of ev.tags) { let [_, followPubkey] = f; - await db.sadd(`${followPubkey}:followers`, pubkey); + await db.sAdd(`${followPubkey}:followers`, pubkey); } coinos.send(ev); } else if (sub.includes("followers")) { let followed = sub.split(":")[0]; let { pubkey } = ev; - await db.sadd(`${followed}:followers`, pubkey); + await db.sAdd(`${followed}:followers`, pubkey); coinos.send(ev); } else if (sub.includes("notes")) { let pubkey = sub.split(":")[0]; - await db.sadd(pubkey, ev.id); + await db.sAdd(pubkey, ev.id); await s(`ev:${ev.id}`, JSON.stringify(ev)); } } catch (e) { diff --git a/lib/sendInternal.js b/lib/sendInternal.js deleted file mode 100644 index 6054f753..00000000 --- a/lib/sendInternal.js +++ /dev/null @@ -1,93 +0,0 @@ -import config from "$config"; -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import { callWebhook } from "$lib/webhooks"; -import { v4 } from "uuid"; -import { notify } from "$lib/notifications"; -import { l, err } from "$lib/logging"; -import { fail } from "$lib/utils"; -import ln from "$lib/ln"; -import { g, s, db } from "$lib/db"; - -const { HOSTNAME: hostname } = process.env; - -export default async ({ amount, hash, memo, tip }, user) => { - amount = parseInt(amount); - - if (!amount || amount < 0) fail("Amount must be greater than zero"); - - let fee = 0; - let invoice = await g(`invoice:${hash}`); - - let t = async () => { - await db.watch(`user:${user.id}`); - user = await g(`user:${user.id}`); - if (user.balance < amount) fail("Insufficient funds"); - user.balance -= amount; - let m = await db.multi(); - await s(`user:${user.id}`, user); - - let p1 = { - hash, - amount: -amount, - memo, - uid: user.id, - rate: store.rates[user.currency], - currency: user.currency, - type: "internal", - created: Date.now() - }; - - await s(`payment:${hash}`, p1); - await db.lpush(`${user.id}:payments`, hash); - - l("sent internal", user.username, -p1.amount); - emit(user.id, "payment", p1); - - let { uid } = invoice; - if (uid !== user.id) { - let recipient = await g(`user:${uid}`); - - let u = async () => { - await db.watch(`user:${uid}`); - recipient = await g(`user:${uid}`); - recipient.balance += amount; - let m = await db.multi(); - await s(`user:${uid}`, recipient); - if (!(await m.exec())) u(); - }; - await u(); - - let p2 = { - hash, - amount, - uid: recipient.id, - rate: store.rates[recipient.currency], - currency: recipient.currency, - memo, - type: "internal", - with_id: user.id, - created: Date.now() - }; - - let { tip } = invoice; - if (tip) { - p2.tip = tip; - p2.amount -= tip; - } - - invoice.received += amount; - await s(`invoice:${hash}`, invoice); - await s(`payment:${id}`, p2); - await db.lpush(`${recipient.id}:payments`, id); - - l("received internal", recipient.username, amount); - emit(recipient.username, "payment", p2); - notify(recipient, `Received ${amount} sats`); - callWebhook(invoice, p2); - } - - if (!(await m.exec())) t(); - }; - await t(); -}; diff --git a/lib/whitelist.js b/lib/whitelist.js index 49cc9af3..77243855 100644 --- a/lib/whitelist.js +++ b/lib/whitelist.js @@ -22,10 +22,8 @@ export default [ "pubkey", "salt", "seed", - "symbol", "theme", "twofa", - "unit", "username", "verified" ]; diff --git a/package.json b/package.json index 6f1c45f9..2c1b7258 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "fastify": "^4.5.3", "file-type": "^18.0.0", "got": "^12.5.3", - "ioredis": "^5.2.5", "jsonwebtoken": "^8.5.1", "lnurl": "^0.24.2", "nodemon": "^2.0.7", @@ -33,7 +32,6 @@ "query-string": "^6.13.1", "readable-stream-clone": "^0.0.7", "redis": "^4.3.1", - "redlock": "5.0.0-beta.2", "sharp": "^0.31.1", "uuid": "8", "web-push": "^3.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c22c13f..5c9f09dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,7 +12,6 @@ specifiers: fastify: ^4.5.3 file-type: ^18.0.0 got: ^12.5.3 - ioredis: ^5.2.5 jsonwebtoken: ^8.5.1 lnurl: ^0.24.2 nodemon: ^2.0.7 @@ -28,7 +27,6 @@ specifiers: query-string: ^6.13.1 readable-stream-clone: ^0.0.7 redis: ^4.3.1 - redlock: 5.0.0-beta.2 sharp: ^0.31.1 uuid: '8' web-push: ^3.4.4 @@ -46,7 +44,6 @@ dependencies: fastify: 4.8.1 file-type: 18.0.0 got: 12.5.3 - ioredis: 5.2.5 jsonwebtoken: 8.5.1 lnurl: 0.24.2 nodemon: 2.0.20 @@ -61,7 +58,6 @@ dependencies: query-string: 6.14.1 readable-stream-clone: 0.0.7 redis: 4.3.1 - redlock: 5.0.0-beta.2 sharp: 0.31.1 uuid: 8.3.2 web-push: 3.5.0 @@ -179,10 +175,6 @@ packages: - utf-8-validate dev: false - /@ioredis/commands/1.2.0: - resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} - dev: false - /@mapbox/node-pre-gyp/1.0.10: resolution: {integrity: sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==} hasBin: true @@ -864,11 +856,6 @@ packages: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false - /denque/2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} - dev: false - /depd/2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1403,23 +1390,6 @@ packages: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} dev: false - /ioredis/5.2.5: - resolution: {integrity: sha512-7HKo/ClM2DGLRXdFq8ruS3Uuadensz4A76wPOU0adqlOqd1qkhoLPDaBhmVhUhNGpB+J65/bhLmNB8DDY99HJQ==} - engines: {node: '>=12.22.0'} - dependencies: - '@ioredis/commands': 1.2.0 - cluster-key-slot: 1.1.0 - debug: 4.3.4 - denque: 2.1.0 - lodash.defaults: 4.2.0 - lodash.isarguments: 3.1.0 - redis-errors: 1.2.0 - redis-parser: 3.0.0 - standard-as-callback: 2.1.0 - transitivePeerDependencies: - - supports-color - dev: false - /ip/2.0.0: resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} dev: false @@ -1574,18 +1544,10 @@ packages: - supports-color dev: false - /lodash.defaults/4.2.0: - resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - dev: false - /lodash.includes/4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} dev: false - /lodash.isarguments/3.1.0: - resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - dev: false - /lodash.isboolean/3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} dev: false @@ -1775,10 +1737,6 @@ packages: semver: 7.3.8 dev: false - /node-abort-controller/3.0.1: - resolution: {integrity: sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw==} - dev: false - /node-addon-api/2.0.2: resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} dev: false @@ -2171,18 +2129,6 @@ packages: engines: {node: '>= 12.13.0'} dev: false - /redis-errors/1.2.0: - resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} - engines: {node: '>=4'} - dev: false - - /redis-parser/3.0.0: - resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} - engines: {node: '>=4'} - dependencies: - redis-errors: 1.2.0 - dev: false - /redis/4.3.1: resolution: {integrity: sha512-cM7yFU5CA6zyCF7N/+SSTcSJQSRMEKN0k0Whhu6J7n9mmXRoXugfWDBo5iOzGwABmsWKSwGPTU5J4Bxbl+0mrA==} dependencies: @@ -2194,13 +2140,6 @@ packages: '@redis/time-series': 1.0.3_@redis+client@1.3.0 dev: false - /redlock/5.0.0-beta.2: - resolution: {integrity: sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw==} - engines: {node: '>=12'} - dependencies: - node-abort-controller: 3.0.1 - dev: false - /require-from-string/2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2464,10 +2403,6 @@ packages: engines: {node: '>= 10.x'} dev: false - /standard-as-callback/2.1.0: - resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - dev: false - /statuses/2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} diff --git a/routes/invoices.js b/routes/invoices.js index 3e43c57c..acd9457e 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -58,7 +58,7 @@ export default { ); await s(`invoice:${hash}`, invoice); - await db.lpush(`${uid}:invoices`, hash); + await db.lPush(`${uid}:invoices`, hash); if (request_id) { let request = await g(`request:${request_id}`); diff --git a/routes/nostr.js b/routes/nostr.js index bac04810..2900e949 100644 --- a/routes/nostr.js +++ b/routes/nostr.js @@ -41,7 +41,7 @@ export default { await s(`user:${pubkey}`, user); - let ids = await db.smembers(pubkey); + let ids = await db.sMembers(pubkey); let events = ids.length ? (await db.mget(ids.map(k => "ev:" + k))).map(JSON.parse) @@ -101,7 +101,7 @@ export default { async followers({ params: { pubkey } }, res) { try { let pubkeys = [ - ...new Set([...(await db.smembers(`${pubkey}:followers`))]) + ...new Set([...(await db.sMembers(`${pubkey}:followers`))]) ]; let followers = []; diff --git a/routes/payments.js b/routes/payments.js index 22de79c3..27c8c5de 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -1,16 +1,91 @@ +import config from "$config"; +import store from "$lib/store"; import { emit } from "$lib/sockets"; -import { db, g, s } from "$lib/db"; +import { v4 } from "uuid"; +import { notify } from "$lib/notifications"; +import { db, g, s, t } from "$lib/db"; import { l, err, warn } from "$lib/logging"; import { fail } from "$lib/utils"; import ln from "$lib/ln"; -import sendInternal from "$lib/sendInternal"; import { requirePin } from "$lib/auth"; +import { callWebhook } from "$lib/webhooks"; + +const { HOSTNAME: hostname } = process.env; export default { - async send(req, res) { - await requirePin(req); - let payment = await sendInternal(req.body, req.user); - res.send(payment); + async send({ body, user }, res) { + let { amount, hash, memo, tip } = body; + await requirePin({ body, user }); + if (!hash) fail("invoice hash required"); + + let { id: uid } = user; + amount = parseInt(amount); + + if (!amount || amount < 0) fail("Amount must be greater than zero"); + + await t(`balance:${uid}`, async (balance) => { + await new Promise(r => setTimeout(r, 100)); + if (balance < amount) fail("Insufficient funds"); + return balance - amount; + }); + + let fee = 0; + let invoice = await g(`invoice:${hash}`); + user = await g(`user:${uid}`); + + await s(`user:${uid}`, user); + + let p1 = { + hash, + amount: -amount, + memo, + uid, + rate: store.rates[user.currency], + currency: user.currency, + type: "internal", + created: Date.now() + }; + + await s(`payment:${hash}`, p1); + await db.lPush(`${uid}:payments`, hash); + + l("sent internal", user.username, -p1.amount); + emit(user.id, "payment", p1); + + ({ uid } = invoice); + if (uid !== user.id) { + let recipient = await g(`user:${uid}`); + + let p2 = { + hash, + amount, + uid: recipient.id, + rate: store.rates[recipient.currency], + currency: recipient.currency, + memo, + type: "internal", + with_id: user.id, + created: Date.now() + }; + + let { tip } = invoice; + if (tip) { + p2.tip = tip; + p2.amount -= tip; + } + + invoice.received += amount; + await s(`invoice:${hash}`, invoice); + await s(`payment:${hash}`, p2); + await db.lPush(`${recipient.id}:payments`, hash); + + l("received internal", recipient.username, amount); + emit(recipient.username, "payment", p2); + notify(recipient, `Received ${amount} sats`); + callWebhook(invoice, p2); + } + + res.send(p1); }, async list({ user: { id }, query: { start, end, limit, offset } }, res) { @@ -21,7 +96,7 @@ export default { // if (start) where.createdAt[Op.gte] = new Date(parseInt(start)); // if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); - let payments = (await db.lrange(`${id}:payments`, 0, -1)) || []; + let payments = (await db.lRange(`${id}:payments`, 0, -1)) || []; payments = await Promise.all(payments.map(hash => g(`payment:${hash}`))); payments = payments.filter(p => p); res.send({ payments, total: payments.length }); diff --git a/routes/requests.js b/routes/requests.js index 8db373dd..79af1fb6 100644 --- a/routes/requests.js +++ b/routes/requests.js @@ -15,8 +15,8 @@ export default { try { const day = new Date(new Date().setDate(new Date().getDate() - 1)); - let invoices = await db.lrange(`${id}:invoices`, 0, -1); - let requests = await db.lrange(`${id}:requests`, 0, -1); + let invoices = await db.lRange(`${id}:invoices`, 0, -1); + let requests = await db.lRange(`${id}:requests`, 0, -1); res.send({ invoices, requests }); } catch (e) { diff --git a/routes/send.js b/routes/send.js deleted file mode 100644 index 4b954616..00000000 --- a/routes/send.js +++ /dev/null @@ -1,15 +0,0 @@ -import { err } from "$lib/logging"; -import sendInternal from "$lib/sendInternal"; -import { requirePin } from "$lib/auth"; - -export default async (req, res, next) => { - try { - await requirePin(req); - let payment = await sendInternal(req.body, req.user); - res.send(payment); - } catch (e) { - console.log(e); - err("problem sending internal payment", e.message); - return res.code(500).send(e.message); - } -}; diff --git a/routes/users.js b/routes/users.js index 49aeebd7..57a423b8 100644 --- a/routes/users.js +++ b/routes/users.js @@ -19,6 +19,7 @@ export default { async me({ user }, res) { try { user.balance = await g(`balance:${user.id}`); + console.log("USER BAL", user.id, user.balance) res.send(pick(user, whitelist)); } catch (e) { console.log("problem fetching user", e); -- cgit v1.2.3 From ef3679051214a9b3047b68f236eac656edcf8d38 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 18 Jan 2023 23:14:48 -0800 Subject: bitcoin invoices --- index.js | 1 + lib/bitcoin.js | 2 +- lib/rpc.js | 26 ++++++++++++++++++++++++++ lib/utils.js | 9 +++++++++ routes/invoices.js | 38 ++++++++++++++++++++++++-------------- routes/payments.js | 7 ++++++- 6 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 lib/rpc.js (limited to 'lib') diff --git a/index.js b/index.js index 1ade59d8..6a34f543 100644 --- a/index.js +++ b/index.js @@ -50,6 +50,7 @@ app.get("/payments/:hash", auth, payments.get); app.post("/lightning/send", auth, payments.sendLightning); app.get("/pot/:name", payments.pot); app.post("/withdraw", auth, payments.withdraw); +app.post("/bitcoin", payments.bitcoin); app.get("/voucher/:id", payments.voucher); app.post("/redeem", optional, payments.redeem); diff --git a/lib/bitcoin.js b/lib/bitcoin.js index eda8b4b0..a811dc14 100644 --- a/lib/bitcoin.js +++ b/lib/bitcoin.js @@ -1,3 +1,3 @@ import config from "$config"; -import rpc from "./rpc"; +import rpc from "$lib/rpc"; export default rpc(config.bitcoin); diff --git a/lib/rpc.js b/lib/rpc.js new file mode 100644 index 00000000..b8a2d0dd --- /dev/null +++ b/lib/rpc.js @@ -0,0 +1,26 @@ +import got from "got"; + +export default ({ host, port, wallet, username, password }) => + new Proxy( + {}, + { + get: (target, prop) => (...params) => + ((method, ...params) => { + let url = `http://${host}:${port}/wallet/${wallet}`; + return got + .post(url, { + json: { + method, + params + }, + headers: { + authorization: `Basic ${Buffer.from( + `${username}:${password}` + ).toString("base64")}` + } + }) + .json() + .then(({ result }) => result); + })(prop.toLowerCase(), ...params) + } + ); diff --git a/lib/utils.js b/lib/utils.js index 92b34be5..e653f235 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -28,3 +28,12 @@ export const toSats = n => Math.round(n * SATS); export const uniq = (a, k) => [...new Map(a.map(x => [k(x), x])).values()]; export const pick = (O, K) => K.reduce((o, k) => (typeof O[k] !== "undefined" && (o[k] = O[k]), o), {}); + +export const bip21 = (address, { amount, memo, tip }) => { + let url = amount || memo ? `bitcoin:${address}?` : address; + if (amount) + url += `amount=${((amount + tip) / SATS).toFixed(8)}${memo ? "&" : ""}`; + if (memo) url += `message=${memo}`; + + return url; +}; diff --git a/routes/invoices.js b/routes/invoices.js index acd9457e..1287de88 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -2,6 +2,8 @@ import store from "$lib/store"; import { l } from "$lib/logging"; import { emit } from "$lib/sockets"; import { db, g, s } from "$lib/db"; +import { bip21, fail } from "$lib/utils"; +import bc from "$lib/bitcoin"; import ln from "$lib/ln"; export default { @@ -12,49 +14,57 @@ export default { }, async create({ body: { invoice, user }, user: me }, res) { - let { currency, tip, amount, rate, request_id } = invoice; + let { currency, tip, amount, rate, request_id, type } = invoice; - if (amount < 0) throw new Error("amount out of range"); - if (tip > 5 * amount || tip < 0) throw new Error("tip amount out of range"); + if (amount < 0) fail("amount out of range"); + if (tip > 5 * amount || tip < 0) fail("tip amount out of range"); tip = tip || 0; if (!user) user = me; let uid = await g(`user:${user.username}`); user = await g(`user:${uid}`); - if (!user) throw new Error("user not provided"); + if (!user) fail("user not provided"); if (!currency) currency = user.currency; if (!rate) rate = store.rates[currency]; - if (amount < 0) throw new Error("invalid amount"); + if (amount < 0) fail("invalid amount"); - let { payment_hash: hash, bolt11: text } = await ln.invoice( - amount ? `${amount + tip}sat` : "any", - new Date(), - "", - 3600 - ); + let hash, text; + if (type === "lightning") { + let amt = amount ? `${amount + tip}sat` : "any"; + let r = await ln.invoice(amt, new Date(), "", 3600); + + hash = r.payment_hash; + text = r.bolt11; + } else if (type === "bitcoin") { + hash = await bc.getNewAddress(); + text = bip21(hash, invoice); + } else { + fail("unrecognized type"); + } invoice = { ...invoice, - hash, amount, + hash, + text, currency, rate, tip, uid, - text, received: 0, created: Date.now() }; + l( "creating invoice", user.username, amount, tip, currency, - `${text.substr(0, 8)}..${text.substr(-6)}` + invoice.hash ); await s(`invoice:${hash}`, invoice); diff --git a/routes/payments.js b/routes/payments.js index ba701bef..fc506685 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -265,7 +265,7 @@ export default { }, async withdraw({ body: { name, amount }, user }, res) { - amount = parseInt(amount) + amount = parseInt(amount); await t(`pot:${name}`, async balance => { await new Promise(r => setTimeout(r, 100)); if (balance < amount) fail("Insufficient funds"); @@ -282,5 +282,10 @@ export default { await db.lPush(`pot:${name}:payments`, hash); res.send({ payment }); + }, + + async bitcoin({ body }, res) { + console.log(body); + res.send({}); } }; -- cgit v1.2.3 From 5264f702a2217fe8815cb5275aca14a672f6d4bb Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 19 Jan 2023 16:00:38 -0800 Subject: refactor payment types --- index.js | 9 +-- lib/lightning.js | 33 +------- lib/payments.js | 132 ++++++++++++++++++++++++++++++++ lib/utils.js | 2 +- routes/invoices.js | 16 ++-- routes/payments.js | 220 ++++++----------------------------------------------- 6 files changed, 166 insertions(+), 246 deletions(-) create mode 100644 lib/payments.js (limited to 'lib') diff --git a/index.js b/index.js index 6a34f543..7f97a20d 100644 --- a/index.js +++ b/index.js @@ -44,17 +44,14 @@ app.get("/locations", locations.list); app.get("/invoice", invoices.get); app.post("/invoice", optional, invoices.create); -app.post("/send", auth, payments.send); +app.post("/payments", auth, payments.create); app.get("/payments", auth, payments.list); app.get("/payments/:hash", auth, payments.get); -app.post("/lightning/send", auth, payments.sendLightning); +app.post("/query", auth, payments.query); app.get("/pot/:name", payments.pot); -app.post("/withdraw", auth, payments.withdraw); +app.post("/take", auth, payments.take); app.post("/bitcoin", payments.bitcoin); -app.get("/voucher/:id", payments.voucher); -app.post("/redeem", optional, payments.redeem); - app.get("/me", auth, users.me); app.get("/users/:key", users.get); app.post("/register", users.create); diff --git a/lib/lightning.js b/lib/lightning.js index 86dfdb2b..c8daf8c8 100644 --- a/lib/lightning.js +++ b/lib/lightning.js @@ -5,6 +5,7 @@ import { callWebhook } from "$lib/webhooks"; import ln from "$lib/ln"; import { l, err, warn } from "$lib/logging"; import { g, s, t, db } from "$lib/db"; +import { credit, types } from "$lib/payments"; export async function listenForLightning() { let { @@ -33,37 +34,7 @@ export async function listenForLightning() { let amount = parseInt(received) - tip; if (amount < 0) throw new Error("amount out of range"); - let user = await g(`user:${uid}`); - - let payment = { - amount, - created: Date.now(), - currency, - fee: 0, - hash, - memo, - preimage, - rate, - tip, - type: "lightning", - uid - }; - - invoice.status = "paid"; - invoice.received += received; - - await s(`invoice:${hash}`, invoice); - await db.incrBy(`balance:${uid}`, received); - payment.invoice = invoice; - await s(`payment:${hash}`, payment); - await db.lPush(`${uid}:payments`, hash); - - callWebhook(invoice, payment); - - emit(uid, "payment", payment); - notify(user, `Received ${received} SAT`); - - l("lightning payment received", user.username, payment.amount, payment.tip); + await credit(hash, amount, memo, preimage, types.lightning); } catch (e) { console.log(e); err("problem receiving lightning payment", e.message); diff --git a/lib/payments.js b/lib/payments.js new file mode 100644 index 00000000..e12f7858 --- /dev/null +++ b/lib/payments.js @@ -0,0 +1,132 @@ +import config from "$config"; +import store from "$lib/store"; +import { emit } from "$lib/sockets"; +import { v4 } from "uuid"; +import { notify } from "$lib/notifications"; +import { db, g, s, t } from "$lib/db"; +import { l, err } from "$lib/logging"; +import { fail } from "$lib/utils"; +import { callWebhook } from "$lib/webhooks"; + +export const types = { + internal: "internal", + bitcoin: "bitcoin", + lightning: "lightning", + pot: "pot" +}; + +export let debit = async ( + user, + amount, + memo, + to, + type = types.internal, + id = v4() +) => { + let { id: uid, currency, username } = user; + to = to && to.id; + + amount = parseInt(amount); + + if (!amount || amount < 0) fail("Amount must be greater than zero"); + + await t(`balance:${uid}`, async balance => { + await new Promise(r => setTimeout(r, 100)); + if (balance < amount) fail("Insufficient funds"); + return balance - amount; + }); + + let fee = 0; + + let p = { + id, + amount: -amount, + memo, + uid, + rate: store.rates[currency], + currency, + type, + to, + created: Date.now() + }; + + if (!to) delete p.to; + + await s(`payment:${hash}`, p); + await db.lPush(`${uid}:payments`, hash); + + l("sent internal", user.username, amount); + emit(user.id, "payment", p); + + return p; +}; + +export let credit = async (hash, amount, memo, from, type = "internal") => { + amount = parseInt(amount); + let invoice = await g(`invoice:${hash}`); + if (!invoice) return; + + let user = await g(`user:${invoice.uid}`); + + let { id: uid, currency, username } = user; + + let p = { + hash, + amount, + uid, + rate: store.rates[currency], + currency, + memo, + from, + type, + confirmed: true, + created: Date.now() + }; + + let { tip } = invoice; + if (tip) { + p.tip = tip; + p.amount -= tip; + } + + invoice.received += amount; + + let balance = "balance"; + if (type === types.bitcoin) { + let [txid, vout] = from.split(":").slice(-2); + p.id = `${txid}:${vout}`; + p.confirmed = false; + balance = "pending"; + } else { + p.id = hash; + } + + await db + .multi() + .set(`invoice:${hash}`, JSON.stringify(invoice)) + .set(`payment:${p.id}`, JSON.stringify(p)) + .lPush(`${uid}:payments`, p.id) + .incrBy(`${balance}:${uid}`, amount) + .exec(); + + l("received internal", username, amount); + emit(uid, "payment", p); + notify(user, `Received ${amount} sats`); + callWebhook(invoice, p); + + return p; +}; + +export let confirm = async (address, txid, vout) => { + let id = `payment:${txid}:${vout}`; + let p = await g(id); + p.confirmed = true; + console.log("P", p); + + await db + .multi() + .set(id, JSON.stringify(p)) + .decrBy(`pending:${p.uid}`, p.amount) + .incrBy(`balance:${p.uid}`, p.amount) + .exec(); +}; diff --git a/lib/utils.js b/lib/utils.js index e653f235..b65f179a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -23,7 +23,7 @@ export const fail = msg => { }; export const SATS = 100000000; -export const toSats = n => Math.round(n * SATS); +export const sats = n => Math.round(n * SATS); export const uniq = (a, k) => [...new Map(a.map(x => [k(x), x])).values()]; export const pick = (O, K) => diff --git a/routes/invoices.js b/routes/invoices.js index 1287de88..62106f3a 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -3,6 +3,8 @@ import { l } from "$lib/logging"; import { emit } from "$lib/sockets"; import { db, g, s } from "$lib/db"; import { bip21, fail } from "$lib/utils"; +import { types } from "$lib/payments"; + import bc from "$lib/bitcoin"; import ln from "$lib/ln"; @@ -31,13 +33,13 @@ export default { if (amount < 0) fail("invalid amount"); let hash, text; - if (type === "lightning") { + if (type === types.lightning) { let amt = amount ? `${amount + tip}sat` : "any"; let r = await ln.invoice(amt, new Date(), "", 3600); hash = r.payment_hash; text = r.bolt11; - } else if (type === "bitcoin") { + } else if (type === types.bitcoin) { hash = await bc.getNewAddress(); text = bip21(hash, invoice); } else { @@ -57,15 +59,7 @@ export default { created: Date.now() }; - - l( - "creating invoice", - user.username, - amount, - tip, - currency, - invoice.hash - ); + l("creating invoice", user.username, amount, tip, currency, invoice.hash); await s(`invoice:${hash}`, invoice); await db.lPush(`${uid}:invoices`, hash); diff --git a/routes/payments.js b/routes/payments.js index fc506685..d044f1e8 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -2,97 +2,19 @@ import config from "$config"; import store from "$lib/store"; import { emit } from "$lib/sockets"; import { v4 } from "uuid"; -import { notify } from "$lib/notifications"; import { db, g, s, t } from "$lib/db"; -import { l, err, warn } from "$lib/logging"; -import { fail } from "$lib/utils"; -import ln from "$lib/ln"; +import { l, err } from "$lib/logging"; +import { fail, sats } from "$lib/utils"; import { requirePin } from "$lib/auth"; -import { callWebhook } from "$lib/webhooks"; - -const { HOSTNAME: hostname } = process.env; - -let debit = async (user, amount, memo, to) => { - let { id: uid, currency, username } = user; - to = to && to.id; - - amount = parseInt(amount); - - if (!amount || amount < 0) fail("Amount must be greater than zero"); - - await t(`balance:${uid}`, async balance => { - await new Promise(r => setTimeout(r, 100)); - if (balance < amount) fail("Insufficient funds"); - return balance - amount; - }); - - let fee = 0; - - let hash = v4(); - - let p = { - hash, - amount: -amount, - memo, - uid, - rate: store.rates[currency], - currency, - type: "internal", - to, - created: Date.now() - }; - - if (!to) delete p.to; - - await s(`payment:${hash}`, p); - await db.lPush(`${uid}:payments`, hash); - - l("sent internal", user.username, amount); - emit(user.id, "payment", p); +import { debit, credit, confirm, types } from "$lib/payments"; - return p; -}; - -let credit = async (hash, amount, memo, from) => { - let invoice = await g(`invoice:${hash}`); - let user = await g(`user:${invoice.uid}`); - - let { id: uid, currency, username } = user; - from = from.id; - - let p = { - hash, - amount, - uid, - rate: store.rates[currency], - currency, - memo, - type: memo === "pot" ? "pot" : "internal", - from, - created: Date.now() - }; - - let { tip } = invoice; - if (tip) { - p.tip = tip; - p.amount -= tip; - } - - invoice.received += amount; - await s(`invoice:${hash}`, invoice); - await s(`payment:${hash}`, p); - await db.lPush(`${uid}:payments`, hash); - - l("received internal", username, amount); - emit(username, "payment", p); - notify(user, `Received ${amount} sats`); - callWebhook(invoice, p); +import bc from "$lib/bitcoin"; +import ln from "$lib/ln"; - return p; -}; +const { HOSTNAME: hostname } = process.env; export default { - async send({ body, user }, res) { + async create({ body, user }, res) { let { amount, hash, name, memo, tip } = body; await requirePin({ body, user }); @@ -101,7 +23,7 @@ export default { let p = await debit(user, amount, memo, recipient); if (hash) { - await credit(hash, amount, memo, user); + await credit(hash, amount, memo, user.id); } else { let pot = name || v4(); await db.incrBy(`pot:${pot}`, amount); @@ -126,116 +48,11 @@ export default { res.send({ payments, total: payments.length }); }, - async voucher(req, res) { - try { - const { redeemcode } = req.params; - let payment = await g(`voucher:${id}`); - - payment = payment.get({ plain: true }); - payment.redeemer = payment["with"]; - - if (!payment) fail("invalid code"); - - res.send(payment); - } catch (e) { - res.code(500).send(e.message); - } - }, - async get({ params: { hash } }, res) { res.send(await g(`payment:${hash}`)); }, - async redeem({ body: { redeemcode } }, res) { - try { - await db.transaction(async transaction => { - if (!redeemcode) fail("no code provided"); - - let { user } = req; - - const source = await db.Payment.findOne({ - where: { - redeemcode: req.body.redeemcode - }, - include: { - model: db.Account, - as: "account" - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - l("redeeming", redeemcode); - - if (!source) fail("Invalid code"); - if (source.redeemed) fail("Voucher has already been redeemed"); - let { amount } = source; - amount = -amount; - - if (!user) { - const ip = - req.headers["x-forwarded-for"] || req.connection.remoteAddress; - - user = await register( - { - username: redeemcode.substr(0, 8), - password: "" - }, - ip, - false - ); - - let payload = { username: user.username }; - let token = jwt.sign(payload, config.jwt); - res.cookie("token", token, { - expires: new Date(Date.now() + 432000000) - }); - - return res.send({ user }); - } - - let account = await getAccount(source.account.asset, user, transaction); - let { hash, memo, confirmed, fee, network } = source; - - source.redeemed = true; - (source.with_id = user.id), await source.save({ transaction }); - - let payment = await db.Payment.create( - { - amount, - account_id: account.id, - user_id: user.id, - hash: "Voucher " + redeemcode, - memo, - rate: store.rates[user.currency], - currency: user.currency, - confirmed, - network, - received: true, - fee, - with_id: source.user_id - }, - { transaction } - ); - - await account.increment({ balance: amount }, { transaction }); - await account.reload({ transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - emit(user.username, "payment", payment); - emit(user.username, "account", account); - - res.send({ payment }); - }); - } catch (e) { - console.log(e); - err("problem redeeming", e.message); - return res.code(500).send("There was a problem redeeming the voucher"); - } - }, - - async sendLightning({ body: { payreq } }, res) { + async query({ body: { payreq } }, res) { let hour = 1000 * 60 * 60; let { last } = store.nodes; let { nodes } = store; @@ -264,7 +81,7 @@ export default { res.send({ amount, payments }); }, - async withdraw({ body: { name, amount }, user }, res) { + async take({ body: { name, amount }, user }, res) { amount = parseInt(amount); await t(`pot:${name}`, async balance => { await new Promise(r => setTimeout(r, 100)); @@ -278,14 +95,23 @@ export default { received: 0 }); - let payment = await credit(hash, amount, "pot", { id: name }); - await db.lPush(`pot:${name}:payments`, hash); + let payment = await credit(hash, amount, "", name, types.pot); + await db.lPush(`${name}:payments`, hash); res.send({ payment }); }, - async bitcoin({ body }, res) { - console.log(body); + async bitcoin({ body: { txid, wallet } }, res) { + if (wallet === config.bitcoin.wallet) { + let { confirmations, details } = await bc.getTransaction(txid); + for (let { address, amount, vout } of details) { + if (confirmations > 0) { + await confirm(address, txid, vout); + } else { + await credit(address, sats(amount), "", `${txid}:${vout}`, types.bitcoin); + } + } + } res.send({}); } }; -- cgit v1.2.3 From 5dd47b9b2cb5a9359c57079205da68a675e10b45 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 20 Jan 2023 11:40:10 -0800 Subject: endpoint renaming --- docker-compose.yml | 8 ++++++++ index.js | 2 +- lib/payments.js | 29 ++++++++++++++++------------- routes/invoices.js | 4 ++++ routes/payments.js | 32 ++++++++++++++++++++++---------- 5 files changed, 51 insertions(+), 24 deletions(-) (limited to 'lib') diff --git a/docker-compose.yml b/docker-compose.yml index 3cf2b5aa..421b6a45 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,6 +48,14 @@ services: volumes: - ./data/lightningc:/root/.lightning restart: always + cld: + image: elementsproject/lightningd:latest + container_name: cld + environment: + LIGHTNINGD_NETWORK: regtest + volumes: + - ./data/lightningd:/root/.lightning + restart: always db: image: eqalpha/keydb container_name: db diff --git a/index.js b/index.js index 7f97a20d..31739f8a 100644 --- a/index.js +++ b/index.js @@ -47,7 +47,7 @@ app.post("/invoice", optional, invoices.create); app.post("/payments", auth, payments.create); app.get("/payments", auth, payments.list); app.get("/payments/:hash", auth, payments.get); -app.post("/query", auth, payments.query); +app.post("/parse", payments.parse); app.get("/pot/:name", payments.pot); app.post("/take", auth, payments.take); app.post("/bitcoin", payments.bitcoin); diff --git a/lib/payments.js b/lib/payments.js index e12f7858..af08cb84 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -16,15 +16,20 @@ export const types = { }; export let debit = async ( - user, + hash, amount, memo, - to, + user, type = types.internal, id = v4() ) => { + console.log("HASH", hash) + let ref; + let invoice = await g(`invoice:${hash}`); + if (invoice) ref = invoice.uid; + console.log("REF", ref) + let { id: uid, currency, username } = user; - to = to && to.id; amount = parseInt(amount); @@ -46,22 +51,20 @@ export let debit = async ( rate: store.rates[currency], currency, type, - to, + ref, created: Date.now() }; - if (!to) delete p.to; - - await s(`payment:${hash}`, p); - await db.lPush(`${uid}:payments`, hash); + await s(`payment:${id}`, p); + await db.lPush(`${uid}:payments`, id); - l("sent internal", user.username, amount); + l("sent", type, user.username, amount); emit(user.id, "payment", p); return p; }; -export let credit = async (hash, amount, memo, from, type = "internal") => { +export let credit = async (hash, amount, memo, ref, type = types.internal) => { amount = parseInt(amount); let invoice = await g(`invoice:${hash}`); if (!invoice) return; @@ -77,7 +80,7 @@ export let credit = async (hash, amount, memo, from, type = "internal") => { rate: store.rates[currency], currency, memo, - from, + ref, type, confirmed: true, created: Date.now() @@ -93,7 +96,7 @@ export let credit = async (hash, amount, memo, from, type = "internal") => { let balance = "balance"; if (type === types.bitcoin) { - let [txid, vout] = from.split(":").slice(-2); + let [txid, vout] = ref.split(":").slice(-2); p.id = `${txid}:${vout}`; p.confirmed = false; balance = "pending"; @@ -109,7 +112,7 @@ export let credit = async (hash, amount, memo, from, type = "internal") => { .incrBy(`${balance}:${uid}`, amount) .exec(); - l("received internal", username, amount); + l("received", type, username, amount); emit(uid, "payment", p); notify(user, `Received ${amount} sats`); callWebhook(invoice, p); diff --git a/routes/invoices.js b/routes/invoices.js index 62106f3a..8fbb25c1 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -4,6 +4,7 @@ import { emit } from "$lib/sockets"; import { db, g, s } from "$lib/db"; import { bip21, fail } from "$lib/utils"; import { types } from "$lib/payments"; +import { v4 } from "uuid"; import bc from "$lib/bitcoin"; import ln from "$lib/ln"; @@ -33,6 +34,7 @@ export default { if (amount < 0) fail("invalid amount"); let hash, text; + console.log("TYPE", type) if (type === types.lightning) { let amt = amount ? `${amount + tip}sat` : "any"; let r = await ln.invoice(amt, new Date(), "", 3600); @@ -42,6 +44,8 @@ export default { } else if (type === types.bitcoin) { hash = await bc.getNewAddress(); text = bip21(hash, invoice); + } else if (type === types.internal) { + hash = v4(); } else { fail("unrecognized type"); } diff --git a/routes/payments.js b/routes/payments.js index d044f1e8..b92a6f81 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -18,16 +18,14 @@ export default { let { amount, hash, name, memo, tip } = body; await requirePin({ body, user }); - let invoice, recipient; - - let p = await debit(user, amount, memo, recipient); + let p = await debit(hash, amount, memo, user); if (hash) { await credit(hash, amount, memo, user.id); } else { let pot = name || v4(); await db.incrBy(`pot:${pot}`, amount); - await db.lPush(`pot:${pot}:payments`, p.hash); + await db.lPush(`pot:${pot}:payments`, p.id); l("funded pot", pot); } @@ -43,8 +41,13 @@ export default { // if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); let payments = (await db.lRange(`${id}:payments`, 0, -1)) || []; - payments = await Promise.all(payments.map(hash => g(`payment:${hash}`))); - payments = payments.filter(p => p); + payments = await Promise.all( + payments.map(async id => { + let p = await g(`payment:${id}`); + if (p.type === types.internal) p.with = await g(`user:${p.ref}`); + return p; + }) + ); res.send({ payments, total: payments.length }); }, @@ -52,7 +55,7 @@ export default { res.send(await g(`payment:${hash}`)); }, - async query({ body: { payreq } }, res) { + async parse({ body: { payreq } }, res) { let hour = 1000 * 60 * 60; let { last } = store.nodes; let { nodes } = store; @@ -61,11 +64,14 @@ export default { store.nodes = nodes; let twoWeeksAgo = new Date(new Date().setDate(new Date().getDate() - 14)); - let { msatoshi, payee } = await ln.decodepay(payreq); + let decoded = await ln.decodepay(payreq); + let { msatoshi, payee, routes } = decoded; let node = nodes.find(n => n.nodeid === payee); let alias = node ? node.alias : payee.substr(0, 12); + let r = await ln.getroute(payee, msatoshi, 5); + console.log(routes, r); - res.send({ alias, amount: Math.round(msatoshi / 1000) }); + res.send({ alias, amount: Math.round(msatoshi / 1000), routes }); }, async pot({ params: { name } }, res) { @@ -108,7 +114,13 @@ export default { if (confirmations > 0) { await confirm(address, txid, vout); } else { - await credit(address, sats(amount), "", `${txid}:${vout}`, types.bitcoin); + await credit( + address, + sats(amount), + "", + `${txid}:${vout}`, + types.bitcoin + ); } } } -- cgit v1.2.3 From 2edcafb31e954628689e769c6926e3e48ddf84eb Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 22 Jan 2023 21:04:41 -0800 Subject: on chain send --- index.js | 3 ++ lib/payments.js | 9 ++--- lib/utils.js | 1 + routes/info.js | 2 +- routes/invoices.js | 1 - routes/payments.js | 112 ++++++++++++++++++++++++++++++++++++++++++++++++----- 6 files changed, 111 insertions(+), 17 deletions(-) (limited to 'lib') diff --git a/index.js b/index.js index 31739f8a..99058833 100644 --- a/index.js +++ b/index.js @@ -50,7 +50,10 @@ app.get("/payments/:hash", auth, payments.get); app.post("/parse", payments.parse); app.get("/pot/:name", payments.pot); app.post("/take", auth, payments.take); + app.post("/bitcoin", payments.bitcoin); +app.post("/bitcoin/fee", auth, payments.fee); +app.post("/bitcoin/send", auth, payments.send); app.get("/me", auth, users.me); app.get("/users/:key", users.get); diff --git a/lib/payments.js b/lib/payments.js index af08cb84..d25df227 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -18,16 +18,15 @@ export const types = { export let debit = async ( hash, amount, + fee = 0, memo, user, type = types.internal, id = v4() ) => { - console.log("HASH", hash) let ref; let invoice = await g(`invoice:${hash}`); if (invoice) ref = invoice.uid; - console.log("REF", ref) let { id: uid, currency, username } = user; @@ -41,13 +40,13 @@ export let debit = async ( return balance - amount; }); - let fee = 0; - let p = { id, amount: -amount, + fee, memo, uid, + confirmed: true, rate: store.rates[currency], currency, type, @@ -102,7 +101,7 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => { balance = "pending"; } else { p.id = hash; - } + } await db .multi() diff --git a/lib/utils.js b/lib/utils.js index b65f179a..e58f7d1f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -24,6 +24,7 @@ export const fail = msg => { export const SATS = 100000000; export const sats = n => Math.round(n * SATS); +export const btc = n => parseFloat((n / SATS).toFixed(8)); export const uniq = (a, k) => [...new Map(a.map(x => [k(x), x])).values()]; export const pick = (O, K) => diff --git a/routes/info.js b/routes/info.js index 00a4915e..017e473a 100644 --- a/routes/info.js +++ b/routes/info.js @@ -15,7 +15,7 @@ export default { for await (let keys of stream) { for (let k of keys) b += parseInt(await db.get(k)); } - console.log(b) + console.log(b); const funds = await ln.listfunds(); lnchannel = parseInt(funds.channels.reduce((a, b) => a + b.channel_sat, 0)); diff --git a/routes/invoices.js b/routes/invoices.js index 8fbb25c1..5da481e1 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -34,7 +34,6 @@ export default { if (amount < 0) fail("invalid amount"); let hash, text; - console.log("TYPE", type) if (type === types.lightning) { let amt = amount ? `${amount + tip}sat` : "any"; let r = await ln.invoice(amt, new Date(), "", 3600); diff --git a/routes/payments.js b/routes/payments.js index b92a6f81..80d8818f 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -4,7 +4,7 @@ import { emit } from "$lib/sockets"; import { v4 } from "uuid"; import { db, g, s, t } from "$lib/db"; import { l, err } from "$lib/logging"; -import { fail, sats } from "$lib/utils"; +import { fail, btc, sats } from "$lib/utils"; import { requirePin } from "$lib/auth"; import { debit, credit, confirm, types } from "$lib/payments"; @@ -15,15 +15,41 @@ const { HOSTNAME: hostname } = process.env; export default { async create({ body, user }, res) { - let { amount, hash, name, memo, tip } = body; - await requirePin({ body, user }); + let { amount, hash, maxfee, name, memo, payreq, tip } = body; + + amount = parseInt(amount); + maxfee = parseInt(maxfee); + tip = parseInt(tip); - let p = await debit(hash, amount, memo, user); + await requirePin({ body, user }); - if (hash) { + let p; + + if (payreq) { + p = await debit( + hash, + amount + maxfee, + maxfee, + memo, + user, + types.lightning + ); + + let r = await ln.pay(payreq); + + p.amount = -amount; + p.hash = r.payment_hash; + p.fee = r.msatoshi_sent - r.msatoshi; + p.preimage = r.payment_preimage; + + await s(`payment:${p.id}`, p); + await db.incrBy(`balance:${p.uid}`, maxfee - p.fee); + } else if (hash) { + p = await debit(hash, amount, 0, memo, user); await credit(hash, amount, memo, user.id); } else { let pot = name || v4(); + p = await debit(hash, amount, 0, memo, user); await db.incrBy(`pot:${pot}`, amount); await db.lPush(`pot:${pot}:payments`, p.id); l("funded pot", pot); @@ -65,13 +91,11 @@ export default { let twoWeeksAgo = new Date(new Date().setDate(new Date().getDate() - 14)); let decoded = await ln.decodepay(payreq); - let { msatoshi, payee, routes } = decoded; + let { msatoshi, payee } = decoded; let node = nodes.find(n => n.nodeid === payee); let alias = node ? node.alias : payee.substr(0, 12); - let r = await ln.getroute(payee, msatoshi, 5); - console.log(routes, r); - res.send({ alias, amount: Math.round(msatoshi / 1000), routes }); + res.send({ alias, amount: Math.round(msatoshi / 1000) }); }, async pot({ params: { name } }, res) { @@ -90,7 +114,6 @@ export default { async take({ body: { name, amount }, user }, res) { amount = parseInt(amount); await t(`pot:${name}`, async balance => { - await new Promise(r => setTimeout(r, 100)); if (balance < amount) fail("Insufficient funds"); return balance - amount; }); @@ -125,5 +148,74 @@ export default { } } res.send({}); + }, + + async fee(req, res) { + let { amount, address, feeRate, subtract } = req.body; + let subtractFeeFromOutputs = subtract ? [0] : []; + let replaceable = true; + let outs = [{ [address]: btc(amount) }]; + let count = await bc.getBlockCount(); + + let raw = await bc.createRawTransaction([], outs, 0, replaceable); + let tx = await bc.fundRawTransaction(raw, { + feeRate, + subtractFeeFromOutputs, + replaceable + }); + + if (config.bitcoin.walletpass) + await bc.walletPassphrase(config.bitcoin.walletpass, 300); + + let { hex } = await bc.signRawTransactionWithWallet(tx.hex); + let { vsize } = await bc.decodeRawTransaction(hex); + feeRate = Math.round((sats(tx.fee) * 1000) / vsize); + + res.send({ feeRate, tx }); + }, + + async send(req, res) { + await requirePin(req); + + let { user } = req; + let { address, memo, tx } = req.body; + let { hex, fee } = tx; + + if (config.bitcoin.walletpass) + await bc.walletPassphrase(config.bitcoin.walletpass, 300); + + ({ hex } = await bc.signRawTransactionWithWallet(hex)); + + let r = await bc.testMempoolAccept([hex]); + if (!r[0].allowed) fail("transaction rejected"); + + fee = sats(fee); + if (fee < 0) fail("fee cannot be negative"); + + tx = await bc.decodeRawTransaction(hex); + let { txid } = tx; + + let total = 0; + let change = 0; + + for (let { + scriptPubKey: { address }, + value + } of tx.vout) { + total += sats(value); + if ( + (await bc.getAddressInfo(address)).ismine && + !(await g(`invoice:${address}`)) + ) + change += sats(value); + } + + total = total - change + fee; + let amount = total - fee; + + await debit(txid, amount, fee, null, user, types.bitcoin, txid); + await bc.sendRawTransaction(hex); + + res.send({ txid }); } }; -- cgit v1.2.3 From f24aec1fb7dd4080d961c26ab5216fd2dfe5d971 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 24 Jan 2023 09:53:51 -0800 Subject: lnurl endpoints --- index.js | 3 +++ lib/payments.js | 1 - routes/payments.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/index.js b/index.js index 99058833..b2796203 100644 --- a/index.js +++ b/index.js @@ -50,6 +50,9 @@ app.get("/payments/:hash", auth, payments.get); app.post("/parse", payments.parse); app.get("/pot/:name", payments.pot); app.post("/take", auth, payments.take); +app.get("/encode", payments.encode); +app.get("/decode", payments.decode); +app.get("/lnurl/:code", payments.lnurl); app.post("/bitcoin", payments.bitcoin); app.post("/bitcoin/fee", auth, payments.fee); diff --git a/lib/payments.js b/lib/payments.js index d25df227..386d84d8 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -123,7 +123,6 @@ export let confirm = async (address, txid, vout) => { let id = `payment:${txid}:${vout}`; let p = await g(id); p.confirmed = true; - console.log("P", p); await db .multi() diff --git a/routes/payments.js b/routes/payments.js index 3ec196cd..53bb6e4d 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -7,6 +7,8 @@ import { l, err } from "$lib/logging"; import { fail, btc, sats } from "$lib/utils"; import { requirePin } from "$lib/auth"; import { debit, credit, confirm, types } from "$lib/payments"; +import { bech32 } from "bech32"; +import got from "got"; import bc from "$lib/bitcoin"; import ln from "$lib/ln"; @@ -217,5 +219,33 @@ export default { await bc.sendRawTransaction(hex); res.send({ txid }); + }, + + async encode({ query: { address } }, res) { + let [name, domain] = address.split("@"); + let url = `https://${domain}/.well-known/lnurlp/${name}`; + let r = await got(url).json(); + if (r.tag !== "payRequest") fail("not an ln address"); + let enc = bech32.encode("lnurl", bech32.toWords(Buffer.from(url)), 20000); + console.log(enc); + res.send(enc); + }, + + async decode({ query: { text } }, res) { + let url = Buffer.from( + bech32.fromWords(bech32.decode(text, 20000).words) + ).toString(); + + res.send(await got(url).json()); + }, + + async lnurl({ params: { code }, query: { amount } }, res) { + let { uid } = await g(`code:${code}`); + + let url = Buffer.from( + bech32.fromWords(bech32.decode(text, 20000).words) + ).toString(); + + res.send(await got(url).json()); } }; -- cgit v1.2.3 From 692315c0d4259a274322ca579a7b6118d033bf7a Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 24 Jan 2023 12:28:49 -0800 Subject: lnurl --- docker-compose.yml | 1 + index.js | 9 ++++--- lib/invoices.js | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++ routes/invoices.js | 75 +++--------------------------------------------------- routes/lnurl.js | 66 +++++++++++++++++++++++++++++++++++++++++++++++ routes/payments.js | 46 ++++++++++++++++++++++++++------- 6 files changed, 187 insertions(+), 83 deletions(-) create mode 100644 lib/invoices.js create mode 100644 routes/lnurl.js (limited to 'lib') diff --git a/docker-compose.yml b/docker-compose.yml index 421b6a45..70d12c24 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: environment: NODE_OPTIONS: "--es-module-specifier-resolution=node --experimental-modules --loader ./loader.js" NODE_ENV: development + URL: http://localhost:3119 ports: - '3119:3119' volumes: diff --git a/index.js b/index.js index b2796203..2c59b8e6 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,7 @@ import { sendHeartbeat } from "$lib/sockets"; import email from "$routes/email"; import info from "$routes/info"; import locations from "$routes/locations"; +import lnurl from "$routes/lnurl"; import nostr from "$routes/nostr"; import rates from "$routes/rates"; import invoices from "$routes/invoices"; @@ -50,9 +51,11 @@ app.get("/payments/:hash", auth, payments.get); app.post("/parse", payments.parse); app.get("/pot/:name", payments.pot); app.post("/take", auth, payments.take); -app.get("/encode", payments.encode); -app.get("/decode", payments.decode); -app.get("/lnurl/:code", payments.lnurl); + +app.get("/encode", lnurl.encode); +app.get("/decode", lnurl.decode); +app.get("/lnurlp/:username", lnurl.lnurlp); +app.get("/lnurl/:id", lnurl.lnurl); app.post("/bitcoin", payments.bitcoin); app.post("/bitcoin/fee", auth, payments.fee); diff --git a/lib/invoices.js b/lib/invoices.js new file mode 100644 index 00000000..7a45b1c0 --- /dev/null +++ b/lib/invoices.js @@ -0,0 +1,73 @@ +import store from "$lib/store"; +import { l } from "$lib/logging"; +import { emit } from "$lib/sockets"; +import { db, g, s } from "$lib/db"; +import { bip21, fail } from "$lib/utils"; +import { types } from "$lib/payments"; +import { v4 } from "uuid"; + +import bc from "$lib/bitcoin"; +import ln from "$lib/ln"; + +export let invoice = async ({ invoice, user, sender }) => { + let { currency, tip, amount, rate, request_id, type } = invoice; + + if (amount < 0) fail("amount out of range"); + if (tip > 5 * amount || tip < 0) fail("tip amount out of range"); + tip = tip || 0; + + if (!user) user = sender; + let uid = await g(`user:${user.username}`); + user = await g(`user:${uid}`); + + if (!user) fail("user not provided"); + + if (!currency) currency = user.currency; + if (!rate) rate = store.rates[currency]; + if (amount < 0) fail("invalid amount"); + + let hash, text; + if (type === types.lightning) { + let amt = amount ? `${amount + tip}sat` : "any"; + let r = await ln.invoice(amt, new Date(), "", 3600); + + hash = r.payment_hash; + text = r.bolt11; + } else if (type === types.bitcoin) { + hash = await bc.getNewAddress(); + text = bip21(hash, invoice); + } else if (type === types.internal) { + hash = v4(); + } else { + fail("unrecognized type"); + } + + invoice = { + ...invoice, + amount, + hash, + text, + currency, + rate, + tip, + uid, + received: 0, + created: Date.now() + }; + + l("creating invoice", user.username, amount, tip, currency, invoice.hash); + + await s(`invoice:${hash}`, invoice); + await db.lPush(`${uid}:invoices`, hash); + + if (request_id) { + let request = await g(`request:${request_id}`); + let { invoice_id: prev } = request; + request.invoice_id = invoice.id; + await s(`request:${request_id}`, request); + + if (!prev) emit(request.requester.username, "invoice", invoice); + } + + return invoice; +}; diff --git a/routes/invoices.js b/routes/invoices.js index 5da481e1..a4155d94 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -1,13 +1,5 @@ -import store from "$lib/store"; -import { l } from "$lib/logging"; -import { emit } from "$lib/sockets"; -import { db, g, s } from "$lib/db"; -import { bip21, fail } from "$lib/utils"; -import { types } from "$lib/payments"; -import { v4 } from "uuid"; - -import bc from "$lib/bitcoin"; -import ln from "$lib/ln"; +import { g, s } from "$lib/db"; +import { invoice } from "$lib/invoices"; export default { async get({ query: { hash } }, res) { @@ -16,66 +8,7 @@ export default { res.send(invoice); }, - async create({ body: { invoice, user }, user: me }, res) { - let { currency, tip, amount, rate, request_id, type } = invoice; - - if (amount < 0) fail("amount out of range"); - if (tip > 5 * amount || tip < 0) fail("tip amount out of range"); - tip = tip || 0; - - if (!user) user = me; - let uid = await g(`user:${user.username}`); - user = await g(`user:${uid}`); - - if (!user) fail("user not provided"); - - if (!currency) currency = user.currency; - if (!rate) rate = store.rates[currency]; - if (amount < 0) fail("invalid amount"); - - let hash, text; - if (type === types.lightning) { - let amt = amount ? `${amount + tip}sat` : "any"; - let r = await ln.invoice(amt, new Date(), "", 3600); - - hash = r.payment_hash; - text = r.bolt11; - } else if (type === types.bitcoin) { - hash = await bc.getNewAddress(); - text = bip21(hash, invoice); - } else if (type === types.internal) { - hash = v4(); - } else { - fail("unrecognized type"); - } - - invoice = { - ...invoice, - amount, - hash, - text, - currency, - rate, - tip, - uid, - received: 0, - created: Date.now() - }; - - l("creating invoice", user.username, amount, tip, currency, invoice.hash); - - await s(`invoice:${hash}`, invoice); - await db.lPush(`${uid}:invoices`, hash); - - if (request_id) { - let request = await g(`request:${request_id}`); - let { invoice_id: prev } = request; - request.invoice_id = invoice.id; - await s(`request:${request_id}`, request); - - if (!prev) emit(request.requester.username, "invoice", invoice); - } - - res.send(invoice); + async create({ body: { invoice, user }, user: sender }, res) { + res.send(await invoice({ invoice, user, sender })); } }; diff --git a/routes/lnurl.js b/routes/lnurl.js new file mode 100644 index 00000000..cb1b75c5 --- /dev/null +++ b/routes/lnurl.js @@ -0,0 +1,66 @@ +import { g, s } from "$lib/db"; +import { l } from "$lib/logging"; +import { fail } from "$lib/utils"; +import { v4 } from "uuid"; +import got from "got"; +import { invoice } from "$lib/invoices"; +import { bech32 } from "bech32"; + +export default { + async encode({ query: { address } }, res) { + let [name, domain] = address.split("@"); + let url = `https://${domain}/.well-known/lnurlp/${name}`; + let r = await got(url).json(); + if (r.tag !== "payRequest") fail("not an ln address"); + let enc = bech32.encode("lnurl", bech32.toWords(Buffer.from(url)), 20000); + res.send(enc); + }, + + async decode({ query: { text } }, res) { + let url = Buffer.from( + bech32.fromWords(bech32.decode(text, 20000).words) + ).toString(); + + res.send(await got(url).json()); + }, + + async lnurlp({ params: { username } }, res) { + let uid = await g(`user:${username}`); + if (!uid) fail("user not found"); + + let id = v4(); + await s(`lnurl:${id}`, uid); + let { URL } = process.env; + let host = URL.split("/").at(-1); + + res.send({ + metadata: [ + ["text/plain", `Paying ${username}@${host}`], + ["text/identifier", `${username}@${host}`] + ], + callback: `${URL}/lnurl/${id}`, + tag: "payRequest" + }); + }, + + async lnurl({ params: { id }, query: { amount } }, res) { + let pr = await g(`lnurlp:${id}`); + + if (!pr) { + let uid = await g(`lnurl:${id}`); + let user = await g(`user:${uid}`); + + ({ text: pr } = await invoice({ + invoice: { + amount, + type: types.lightning + }, + user + })); + + await s(`lnurlp:${id}`, pr); + } + + res.send({ pr }); + } +}; diff --git a/routes/payments.js b/routes/payments.js index 53bb6e4d..b80d54f6 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -8,13 +8,12 @@ import { fail, btc, sats } from "$lib/utils"; import { requirePin } from "$lib/auth"; import { debit, credit, confirm, types } from "$lib/payments"; import { bech32 } from "bech32"; +import { invoice } from "$lib/invoices"; import got from "got"; import bc from "$lib/bitcoin"; import ln from "$lib/ln"; -const { HOSTNAME: hostname } = process.env; - export default { async create({ body, user }, res) { let { amount, hash, maxfee, name, memo, payreq, tip } = body; @@ -227,7 +226,6 @@ export default { let r = await got(url).json(); if (r.tag !== "payRequest") fail("not an ln address"); let enc = bech32.encode("lnurl", bech32.toWords(Buffer.from(url)), 20000); - console.log(enc); res.send(enc); }, @@ -239,13 +237,43 @@ export default { res.send(await got(url).json()); }, - async lnurl({ params: { code }, query: { amount } }, res) { - let { uid } = await g(`code:${code}`); + async lnurlp({ params: { username } }, res) { + let uid = await g(`user:${username}`); + if (!uid) fail("user not found"); + + let id = v4(); + await s(`lnurl:${id}`, uid); + let { URL } = process.env; + let host = URL.split("/").at(-1); + + res.send({ + metadata: [ + ["text/plain", `Paying ${username}@${host}`], + ["text/identifier", `${username}@${host}`] + ], + callback: `${host}/lnurl/${id}`, + tag: "payRequest" + }); + }, - let url = Buffer.from( - bech32.fromWords(bech32.decode(text, 20000).words) - ).toString(); + async lnurl({ params: { id }, query: { amount } }, res) { + let pr = await g(`lnurlp:${id}`); - res.send(await got(url).json()); + if (!pr) { + let uid = await g(`lnurl:${id}`); + let user = await g(`user:${uid}`); + + ({ text: pr } = await invoice({ + invoice: { + amount, + type: types.lightning + }, + user + })); + + await s(`lnurlp:${id}`, pr); + } + + res.send({ pr }); } }; -- cgit v1.2.3 From adf35d736de2cd870e363abc28a0989158ea0b5b Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 24 Jan 2023 14:46:46 -0800 Subject: move lnurl routes --- lib/invoices.js | 2 +- routes/invoices.js | 4 ++-- routes/lnurl.js | 4 ++-- routes/payments.js | 59 ------------------------------------------------------ 4 files changed, 5 insertions(+), 64 deletions(-) (limited to 'lib') diff --git a/lib/invoices.js b/lib/invoices.js index 7a45b1c0..f17223aa 100644 --- a/lib/invoices.js +++ b/lib/invoices.js @@ -9,7 +9,7 @@ import { v4 } from "uuid"; import bc from "$lib/bitcoin"; import ln from "$lib/ln"; -export let invoice = async ({ invoice, user, sender }) => { +export let generate = async ({ invoice, user, sender }) => { let { currency, tip, amount, rate, request_id, type } = invoice; if (amount < 0) fail("amount out of range"); diff --git a/routes/invoices.js b/routes/invoices.js index a4155d94..d1ec5338 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -1,5 +1,5 @@ import { g, s } from "$lib/db"; -import { invoice } from "$lib/invoices"; +import { generate } from "$lib/invoices"; export default { async get({ query: { hash } }, res) { @@ -9,6 +9,6 @@ export default { }, async create({ body: { invoice, user }, user: sender }, res) { - res.send(await invoice({ invoice, user, sender })); + res.send(await generate({ invoice, user, sender })); } }; diff --git a/routes/lnurl.js b/routes/lnurl.js index cb1b75c5..63488033 100644 --- a/routes/lnurl.js +++ b/routes/lnurl.js @@ -3,7 +3,7 @@ import { l } from "$lib/logging"; import { fail } from "$lib/utils"; import { v4 } from "uuid"; import got from "got"; -import { invoice } from "$lib/invoices"; +import { generate } from "$lib/invoices"; import { bech32 } from "bech32"; export default { @@ -50,7 +50,7 @@ export default { let uid = await g(`lnurl:${id}`); let user = await g(`user:${uid}`); - ({ text: pr } = await invoice({ + ({ text: pr } = await generate({ invoice: { amount, type: types.lightning diff --git a/routes/payments.js b/routes/payments.js index b80d54f6..a060d90a 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -7,9 +7,6 @@ import { l, err } from "$lib/logging"; import { fail, btc, sats } from "$lib/utils"; import { requirePin } from "$lib/auth"; import { debit, credit, confirm, types } from "$lib/payments"; -import { bech32 } from "bech32"; -import { invoice } from "$lib/invoices"; -import got from "got"; import bc from "$lib/bitcoin"; import ln from "$lib/ln"; @@ -220,60 +217,4 @@ export default { res.send({ txid }); }, - async encode({ query: { address } }, res) { - let [name, domain] = address.split("@"); - let url = `https://${domain}/.well-known/lnurlp/${name}`; - let r = await got(url).json(); - if (r.tag !== "payRequest") fail("not an ln address"); - let enc = bech32.encode("lnurl", bech32.toWords(Buffer.from(url)), 20000); - res.send(enc); - }, - - async decode({ query: { text } }, res) { - let url = Buffer.from( - bech32.fromWords(bech32.decode(text, 20000).words) - ).toString(); - - res.send(await got(url).json()); - }, - - async lnurlp({ params: { username } }, res) { - let uid = await g(`user:${username}`); - if (!uid) fail("user not found"); - - let id = v4(); - await s(`lnurl:${id}`, uid); - let { URL } = process.env; - let host = URL.split("/").at(-1); - - res.send({ - metadata: [ - ["text/plain", `Paying ${username}@${host}`], - ["text/identifier", `${username}@${host}`] - ], - callback: `${host}/lnurl/${id}`, - tag: "payRequest" - }); - }, - - async lnurl({ params: { id }, query: { amount } }, res) { - let pr = await g(`lnurlp:${id}`); - - if (!pr) { - let uid = await g(`lnurl:${id}`); - let user = await g(`user:${uid}`); - - ({ text: pr } = await invoice({ - invoice: { - amount, - type: types.lightning - }, - user - })); - - await s(`lnurlp:${id}`, pr); - } - - res.send({ pr }); - } }; -- cgit v1.2.3 From b933716cc1fc4a2a785796fb91ed2e3a43c2557c Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 25 Jan 2023 11:09:13 -0800 Subject: sample config --- .gitignore | 1 - lib/register.js | 2 +- sampleconfig.js | 21 +++++++++++++++++++++ setup.sh | 1 + 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 sampleconfig.js (limited to 'lib') diff --git a/.gitignore b/.gitignore index 29bf5351..7bbd2455 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ node_modules/ .pnpm* -uploads data config.js diff --git a/lib/register.js b/lib/register.js index 63dccb38..90df18e2 100644 --- a/lib/register.js +++ b/lib/register.js @@ -25,7 +25,7 @@ export default async (user, ip, requireChallenge) => { } if (!config.ipxapi || ip.startsWith("127") || ip.startsWith("192")) { - user.currency = "CAD"; + user.currency = "USD"; } else { let countryCode = "US"; try { diff --git a/sampleconfig.js b/sampleconfig.js new file mode 100644 index 00000000..b82623bc --- /dev/null +++ b/sampleconfig.js @@ -0,0 +1,21 @@ +export default { + nostr: "ws://nostr:8080", + relays: [ + "ws://nostr:8080", + "wss://nostr-pub.wellorder.net", + "wss://brb.io", + "wss://nostr.v0l.io", + "wss://relay.nostr.bg", + "wss://nostr.orangepill.dev" + ], + jwt: "00d0ab4f7738a83feb37f661526512063c41e49278b7c32cba87314269a5788b", + bitcoin: { + host: "bc", + wallet: "coinos", + username: "admin1", + password: "123", + network: "regtest", + port: 18443, + }, + lightning: "/app/data/lightning/regtest/lightning-rpc" +}; diff --git a/setup.sh b/setup.sh index 1f527f88..c3e58161 100644 --- a/setup.sh +++ b/setup.sh @@ -1,3 +1,4 @@ +cp sampleconfig.js config.js cp -r sampledata data docker compose up -d docker run -it -v $(pwd):/app --entrypoint pnpm cs i -- cgit v1.2.3 From be20fa2178f75c35c541d7e77b3f1c0f68b41ac4 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 25 Jan 2023 11:12:17 -0800 Subject: default to usd --- lib/register.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/register.js b/lib/register.js index 90df18e2..e8b826e3 100644 --- a/lib/register.js +++ b/lib/register.js @@ -24,10 +24,10 @@ export default async (user, ip, requireChallenge) => { user.password = await bcrypt.hash(password, 1); } - if (!config.ipxapi || ip.startsWith("127") || ip.startsWith("192")) { - user.currency = "USD"; - } else { + user.currency = "USD"; + if (config.ipxapi) { let countryCode = "US"; + try { let r = await got(`https://ipxapi.com/api/ip?ip=${ip}`, { headers: { authorization: `Bearer ${config.ipxapi}` } -- cgit v1.2.3 From 74704611d9c5772491488852b88db7e458b95610 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 25 Jan 2023 11:47:06 -0800 Subject: mv uploads --- lib/app.js | 2 +- lib/upload.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/app.js b/lib/app.js index af2bd59f..4691786a 100644 --- a/lib/app.js +++ b/lib/app.js @@ -30,7 +30,7 @@ fastifyPassport.use("jwt", jwtStrategy); app.register(fastifyMultipart); app.register(fastifyStatic, { - root: path.join("/app/uploads"), + root: path.join("/app/data/uploads"), prefix: "/public/" }); diff --git a/lib/upload.js b/lib/upload.js index 43cc35ce..fbf9ce20 100644 --- a/lib/upload.js +++ b/lib/upload.js @@ -20,7 +20,7 @@ export default async ({ params: { type }, file, user }, res) => { throw new Error("unsupported file type"); let name = `${user.id}-${type}.webp`; - let path = `/app/uploads/${name}`; + let path = `/app/data/uploads/${name}`; let size = type === "banner" ? 1920 : 240; let t = sharp() -- cgit v1.2.3 From 143490db49b6d67e9ddb2c59ea92ae923d08911c Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 25 Jan 2023 20:38:10 +0000 Subject: db url in config --- lib/db.js | 3 ++- sampleconfig.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/db.js b/lib/db.js index 1122038c..acc4bff1 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,7 +1,8 @@ +import config from "$config" import { sleep } from "$lib/utils"; import { createClient } from "redis"; -export const db = createClient({ url: "redis://db" }); +export const db = createClient({ url: config.db }); await db.connect(); export default db; diff --git a/sampleconfig.js b/sampleconfig.js index b82623bc..9284866a 100644 --- a/sampleconfig.js +++ b/sampleconfig.js @@ -1,4 +1,5 @@ export default { + db: "redis://db", nostr: "ws://nostr:8080", relays: [ "ws://nostr:8080", -- cgit v1.2.3 From f0543b6d94081f43a265bb9f754a654df9c4268a Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 26 Jan 2023 11:26:05 -0800 Subject: sample notify script --- lib/db.js | 12 ++---------- lib/payments.js | 31 ++++++++++++++++++++++++------- lib/upload.js | 20 +++++++++----------- lib/whitelist.js | 1 + routes/payments.js | 12 ++++++++---- sampledata/bitcoin/bitcoin.conf | 1 + sampledata/bitcoin/transaction.sh | 2 ++ 7 files changed, 47 insertions(+), 32 deletions(-) create mode 100755 sampledata/bitcoin/transaction.sh (limited to 'lib') diff --git a/lib/db.js b/lib/db.js index acc4bff1..516db236 100644 --- a/lib/db.js +++ b/lib/db.js @@ -11,16 +11,9 @@ export let s = (k, v) => db.set(k, JSON.stringify(v)); let retries = {}; export let t = async (k, f) => { - let result; try { - result = await db.executeIsolated(async db => { - await db.watch(k); - let v = await db.get(k); - await db - .multi() - .set(k, await f(v)) - .exec(); - }); + await db.watch(k); + await f(await db.get(k), db); } catch (err) { if (!err.message.includes("watch")) throw err; @@ -37,5 +30,4 @@ export let t = async (k, f) => { } delete retries[k]; - return result; }; diff --git a/lib/payments.js b/lib/payments.js index 386d84d8..d0593447 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -34,16 +34,30 @@ export let debit = async ( if (!amount || amount < 0) fail("Amount must be greater than zero"); - await t(`balance:${uid}`, async balance => { - await new Promise(r => setTimeout(r, 100)); - if (balance < amount) fail("Insufficient funds"); - return balance - amount; + let ourfee = Math.round(amount * config.fee); + await t(`balance:${uid}`, async (balance, db) => { + await t(`credit:${type}:${uid}`, async (credit, db) => { + let covered = Math.min(credit, ourfee) || 0; + ourfee -= covered; + + if (balance < amount + ourfee) fail("Insufficient funds"); + await db + .multi() + .decrBy(`credit:${type}:${uid}`, covered) + .exec(); + }); + + await db + .multi() + .decrBy(`balance:${uid}`, amount + (ourfee - credit)) + .exec(); }); let p = { id, amount: -amount, fee, + ourfee, memo, uid, confirmed: true, @@ -103,9 +117,12 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => { p.id = hash; } - await db - .multi() - .set(`invoice:${hash}`, JSON.stringify(invoice)) + let m = await db.multi(); + + if ([types.bitcoin, types.lightning].includes(type)) + m.incrBy(`credit:${type}:${uid}`, Math.round(amount * config.fee)); + + m.set(`invoice:${hash}`, JSON.stringify(invoice)) .set(`payment:${p.id}`, JSON.stringify(p)) .lPush(`${uid}:payments`, p.id) .incrBy(`${balance}:${uid}`, amount) diff --git a/lib/upload.js b/lib/upload.js index fbf9ce20..5f5aebed 100644 --- a/lib/upload.js +++ b/lib/upload.js @@ -5,12 +5,16 @@ import fs from "fs"; import path from "path"; import sharp from "sharp"; import { pipeline } from "stream"; -import { g, s } from "$lib/db"; +import { g, s, db } from "$lib/db"; -export default async ({ params: { type }, file, user }, res) => { +export default async (req, res) => { + let { + params: { type }, + user + } = req; if (!["banner", "profile"].includes(type)) throw new Error("invalid upload"); - let data = await file(); + let data = await req.file(); let s1 = new Clone(data.file); let s2 = new Clone(data.file); @@ -30,14 +34,8 @@ export default async ({ params: { type }, file, user }, res) => { await new Promise(r => pipeline(s2, t, fs.createWriteStream(path), r)); - let r = async () => { - await db.watch(`user:${user.id}`); - user = await g(`user:${user.id}`); - user[type] = true; - let m = await db.multi(); - await s(`user:${user.id}`, user); - }; - await r(); + user[type] = true; + await s(`user:${user.id}`, user); res.send(name); }; diff --git a/lib/whitelist.js b/lib/whitelist.js index 77243855..850dffc0 100644 --- a/lib/whitelist.js +++ b/lib/whitelist.js @@ -14,6 +14,7 @@ export default [ "haspin", "id", "index", + "linked", "keys", "locktime", "payments", diff --git a/routes/payments.js b/routes/payments.js index a060d90a..ca32e969 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -111,9 +111,12 @@ export default { async take({ body: { name, amount }, user }, res) { amount = parseInt(amount); - await t(`pot:${name}`, async balance => { + await t(`pot:${name}`, async (balance, db) => { if (balance < amount) fail("Insufficient funds"); - return balance - amount; + await db + .multi() + .decrBy(`pot:${name}`, amount) + .exec(); }); let hash = v4(); @@ -211,10 +214,11 @@ export default { total = total - change + fee; let amount = total - fee; + if (change && !amount) fail("Cannot send to unregistered coinos address"); + await debit(txid, amount, fee, null, user, types.bitcoin, txid); await bc.sendRawTransaction(hex); res.send({ txid }); - }, - + } }; diff --git a/sampledata/bitcoin/bitcoin.conf b/sampledata/bitcoin/bitcoin.conf index 5da37e5a..3aa44186 100644 --- a/sampledata/bitcoin/bitcoin.conf +++ b/sampledata/bitcoin/bitcoin.conf @@ -16,6 +16,7 @@ rpcwallet=coinos wallet=coinos wallet=external +walletnotify=/home/bitcoin/.bitcoin/transaction.sh %s %w zmqpubrawblock=tcp://0.0.0.0:18506 zmqpubrawtx=tcp://0.0.0.0:18507 diff --git a/sampledata/bitcoin/transaction.sh b/sampledata/bitcoin/transaction.sh new file mode 100755 index 00000000..e44789d1 --- /dev/null +++ b/sampledata/bitcoin/transaction.sh @@ -0,0 +1,2 @@ +#!/bin/sh +wget -O- --post-data='{"txid": "'$1'", "wallet": "'$2'"}' --header='Content-Type:application/json' 'http://app:3119/bitcoin' -- cgit v1.2.3 From 283ffc9d8442ecbbc85c0c40167cc03f20f64a40 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 28 Jan 2023 00:54:08 -0800 Subject: parse ints --- lib/invoices.js | 4 +++- lib/upload.js | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/invoices.js b/lib/invoices.js index f17223aa..814e87d9 100644 --- a/lib/invoices.js +++ b/lib/invoices.js @@ -12,9 +12,11 @@ import ln from "$lib/ln"; export let generate = async ({ invoice, user, sender }) => { let { currency, tip, amount, rate, request_id, type } = invoice; + amount = parseInt(amount); + tip = parseInt(tip || 0); + if (amount < 0) fail("amount out of range"); if (tip > 5 * amount || tip < 0) fail("tip amount out of range"); - tip = tip || 0; if (!user) user = sender; let uid = await g(`user:${user.username}`); diff --git a/lib/upload.js b/lib/upload.js index 5f5aebed..2f8180a0 100644 --- a/lib/upload.js +++ b/lib/upload.js @@ -12,6 +12,7 @@ export default async (req, res) => { params: { type }, user } = req; + console.log("TYPE", type) if (!["banner", "profile"].includes(type)) throw new Error("invalid upload"); let data = await req.file(); -- cgit v1.2.3 From 2eb199b8609b0976b4800815edcf73aa3da4b464 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 31 Jan 2023 20:03:04 -0800 Subject: account migration --- lib/payments.js | 2 +- lib/register.js | 1 + lib/utils.js | 20 ++++++++++++ routes/payments.js | 32 +++++++++++-------- routes/users.js | 93 ++++++++++++++++++++++++++++++++++++++++++++---------- 5 files changed, 118 insertions(+), 30 deletions(-) (limited to 'lib') diff --git a/lib/payments.js b/lib/payments.js index d0593447..cc8747d7 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -34,7 +34,7 @@ export let debit = async ( if (!amount || amount < 0) fail("Amount must be greater than zero"); - let ourfee = Math.round(amount * config.fee); + let ourfee = type === types.internal ? 0 : Math.round(amount * config.fee); await t(`balance:${uid}`, async (balance, db) => { await t(`credit:${type}:${uid}`, async (credit, db) => { let covered = Math.min(credit, ourfee) || 0; diff --git a/lib/register.js b/lib/register.js index e8b826e3..a3ebfc86 100644 --- a/lib/register.js +++ b/lib/register.js @@ -44,6 +44,7 @@ export default async (user, ip, requireChallenge) => { user.fiat = false; user.otpsecret = authenticator.generateSecret(); user.balance = 0; + user.migrated = true; let d = ip.split("."); ip = ((+d[0] * 256 + +d[1]) * 256 + +d[2]) * 256 + +d[3]; diff --git a/lib/utils.js b/lib/utils.js index e58f7d1f..8291d173 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -38,3 +38,23 @@ export const bip21 = (address, { amount, memo, tip }) => { return url; }; + +export const fields = [ + "cipher", + "pubkey", + "password", + "username", + "salt", + "currency", + "currencies", + "fiat", + "otpsecret", + "balance", + "ip", + "pin", + "display", + "haspin", + "profile", + "prompt", + "banner" +]; diff --git a/routes/payments.js b/routes/payments.js index ca32e969..26708a44 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -58,21 +58,27 @@ export default { async list({ user: { id }, query: { start, end, limit, offset } }, res) { if (limit) limit = parseInt(limit); - if (offset) offset = parseInt(offset); - - // if (start || end) where.createdAt = {}; - // if (start) where.createdAt[Op.gte] = new Date(parseInt(start)); - // if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); + offset = parseInt(offset) || 0; let payments = (await db.lRange(`${id}:payments`, 0, -1)) || []; - payments = await Promise.all( - payments.map(async id => { - let p = await g(`payment:${id}`); - if (p.type === types.internal) p.with = await g(`user:${p.ref}`); - return p; - }) - ); - res.send({ payments, total: payments.length }); + payments = ( + await Promise.all( + payments.map(async id => { + let p = await g(`payment:${id}`); + if (p.created < start || p.created > end) return; + if (p.type === types.internal) p.with = await g(`user:${p.ref}`); + return p; + }) + ) + ) + .filter(p => p) + .sort((a, b) => b.created - a.created); + + let total = payments.length; + + if (limit) payments = payments.slice(offset, offset + limit); + + res.send({ payments, total }); }, async get({ params: { hash } }, res) { diff --git a/routes/users.js b/routes/users.js index 49aeebd7..4b5157ce 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1,7 +1,7 @@ -import { g, s } from "$lib/db"; +import { g, s, db } from "$lib/db"; import config from "$config"; import store from "$lib/store"; -import { nada, pick, uniq, wait } from "$lib/utils"; +import { fields, nada, pick, uniq, wait } from "$lib/utils"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { authenticator } from "otplib"; @@ -10,6 +10,10 @@ import { l, err, warn } from "$lib/logging"; import { emit } from "$lib/sockets"; import register from "$lib/register"; import { requirePin } from "$lib/utils"; +import { v4 } from "uuid"; +import { parseISO } from "date-fns"; +import { types } from "$lib/payments"; + import got from "got"; import upload from "$lib/upload"; @@ -172,27 +176,84 @@ export default { async login(req, res) { try { - const { params, sig, key, password, username, token: twofa } = req.body; + const { username, password, token: twofa } = req.body; l("logging in", username); - if (sig) { - const { callback } = params; + let uid = await g(`user:${username}`); + let user = await g(`user:${uid}`); + if (!(user && user.migrated)) { + let { classic } = config; try { - const url = `${callback}&sig=${sig}&key=${key}`; - const response = await got(url).json(); - res.send(response); + let { token } = await got + .post(`${classic}/login`, { json: { username, password } }) + .json(); + if (!token) fail(); + + user = await got(`${classic}/admin/migrate/${username}?zero=true`, { + headers: { authorization: `Bearer ${config.admin}` } + }).json(); + + let { balance } = user; + if (!user) fail(); + + uid = v4(); + user = { + ...pick(user, fields), + id: uid, + about: user.address, + migrated: true + }; + + await s(`user:${username}`, uid); + await s(`user:${uid}`, user); + await s(`balance:${uid}`, balance); + + let payments = await got(`${classic}/payments`, { + headers: { authorization: `Bearer ${token}` } + }).json(); + + for (let p of payments) { + let n = pick(p, ["amount", "fee", "confirmed", "rate", "currency"]); + n.id = v4(); + n.created = parseISO(p.createdAt).getTime(); + n.type = p.network; + if (!["bitcoin", "lightning"].includes(n.type)) + n.type = types.internal; + + if (n.type === types.internal) { + if (!p.with) continue; + + let id = await g(`user:${p.with.username}`); + let u = await g(`user:${id}`); + if (!u) { + u = await got(`${classic}/admin/migrate/${p.with.username}`, { + headers: { authorization: `Bearer ${config.admin}` } + }).json(); + + u = { id: v4(), about: u.address, ...pick(u, fields) }; + delete u.address; + + await s(`user:${p.with.username}`, u.id); + await s(`user:${u.id}`, u); + + l("added missing user", u.username); + } + + if (!(u && u.id)) continue; + n.ref = u.id; + } + + await s(`payment:${n.id}`, n); + await db.lPush(`${uid}:payments`, n.id); + } + + l("migrated user", user.username); } catch (e) { - err("problem calling lnurl login", e.message); - res.code(500).send(e.message); + console.log(e); } - - return; } - let id = await g(`user:${username}`); - let user = await g(`user:${id}`); - if ( !user || (user.password && !(await bcrypt.compare(password, user.password))) @@ -215,7 +276,7 @@ export default { req.headers["x-forwarded-for"] || req.socket.remoteAddress ); - let payload = { username, id: user.id }; + let payload = { username, id: uid }; let token = jwt.sign(payload, config.jwt); res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); user = pick(user, whitelist); -- cgit v1.2.3 From 1ae65a5a143d218e21828e297fd7d0ba9ab372bf Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 1 Feb 2023 11:07:37 -0800 Subject: classic invoices --- index.js | 3 ++- lib/payments.js | 8 ++++++-- routes/invoices.js | 28 ++++++++++++++++++++++++++-- routes/payments.js | 16 ++++++++++++++-- routes/users.js | 4 +++- 5 files changed, 51 insertions(+), 8 deletions(-) (limited to 'lib') diff --git a/index.js b/index.js index 2c59b8e6..ed4e0827 100644 --- a/index.js +++ b/index.js @@ -42,8 +42,9 @@ app.post("/event", nostr.broadcast); app.get("/locations", locations.list); -app.get("/invoice", invoices.get); +app.get("/invoice/:hash", invoices.get); app.post("/invoice", optional, invoices.create); +app.get("/invoice/classic/:username", invoices.classic); app.post("/payments", auth, payments.create); app.get("/payments", auth, payments.list); diff --git a/lib/payments.js b/lib/payments.js index cc8747d7..47cb11f4 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -12,7 +12,8 @@ export const types = { internal: "internal", bitcoin: "bitcoin", lightning: "lightning", - pot: "pot" + pot: "pot", + classic: "classic" }; export let debit = async ( @@ -34,7 +35,10 @@ export let debit = async ( if (!amount || amount < 0) fail("Amount must be greater than zero"); - let ourfee = type === types.internal ? 0 : Math.round(amount * config.fee); + let ourfee = [types.bitcoin, types.lightning].includes(type) + ? 0 + : Math.round(amount * config.fee); + await t(`balance:${uid}`, async (balance, db) => { await t(`credit:${type}:${uid}`, async (credit, db) => { let covered = Math.min(credit, ourfee) || 0; diff --git a/routes/invoices.js b/routes/invoices.js index d1ec5338..2ca3b006 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -1,14 +1,38 @@ import { g, s } from "$lib/db"; import { generate } from "$lib/invoices"; +import got from "got"; +import config from "$config"; export default { - async get({ query: { hash } }, res) { + async get({ params: { hash } }, res) { let invoice = await g(`invoice:${hash}`); - invoice.user = await g(`user:${invoice.uid}`); + + if (invoice) { + invoice.user = await g(`user:${invoice.uid}`); + } else { + invoice = await got(`${config.classic}/invoice/${hash}`).json(); + invoice.id = invoice.uuid; + invoice.classic = true; + invoice.user.username += "@classic"; + } + res.send(invoice); }, async create({ body: { invoice, user }, user: sender }, res) { res.send(await generate({ invoice, user, sender })); + }, + + async classic({ params: { username } }, res) { + let invoice = await got + .post(`${config.classic}/invoice`, { + json: { + invoice: { amount: 0, network: "lightning" }, + user: { username } + } + }) + .json(); + + res.send(invoice); } }; diff --git a/routes/payments.js b/routes/payments.js index 26708a44..c67e0f9b 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -7,13 +7,14 @@ import { l, err } from "$lib/logging"; import { fail, btc, sats } from "$lib/utils"; import { requirePin } from "$lib/auth"; import { debit, credit, confirm, types } from "$lib/payments"; +import got from "got"; import bc from "$lib/bitcoin"; import ln from "$lib/ln"; export default { async create({ body, user }, res) { - let { amount, hash, maxfee, name, memo, payreq, tip } = body; + let { amount, hash, maxfee, name, memo, payreq, tip, username } = body; amount = parseInt(amount); maxfee = parseInt(maxfee); @@ -23,7 +24,18 @@ export default { let p; - if (payreq) { + if (username.endsWith("@classic")) { + let { username: source } = user; + username = username.replace("@classic", ""); + p = await debit(hash, amount, 0, memo, user, types.classic); + + await got + .post(`${config.classic}/admin/credit`, { + json: { username, amount, source }, + headers: { authorization: `Bearer ${config.admin}` } + }) + .json(); + } else if (payreq) { p = await debit( hash, amount + maxfee, diff --git a/routes/users.js b/routes/users.js index 4b5157ce..9830b2fc 100644 --- a/routes/users.js +++ b/routes/users.js @@ -183,11 +183,14 @@ export default { let user = await g(`user:${uid}`); if (!(user && user.migrated)) { + uid = user ? user.id : v4(); + let { classic } = config; try { let { token } = await got .post(`${classic}/login`, { json: { username, password } }) .json(); + if (!token) fail(); user = await got(`${classic}/admin/migrate/${username}?zero=true`, { @@ -197,7 +200,6 @@ export default { let { balance } = user; if (!user) fail(); - uid = v4(); user = { ...pick(user, fields), id: uid, -- cgit v1.2.3 From fe0455ffe2d93eda5792ec1ef38348ef24aba2aa Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 1 Feb 2023 19:58:13 +0000 Subject: preserve user id --- lib/invoices.js | 2 +- routes/invoices.js | 1 + routes/users.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/invoices.js b/lib/invoices.js index 814e87d9..4a09f44b 100644 --- a/lib/invoices.js +++ b/lib/invoices.js @@ -12,7 +12,7 @@ import ln from "$lib/ln"; export let generate = async ({ invoice, user, sender }) => { let { currency, tip, amount, rate, request_id, type } = invoice; - amount = parseInt(amount); + amount = parseInt(amount || 0); tip = parseInt(tip || 0); if (amount < 0) fail("amount out of range"); diff --git a/routes/invoices.js b/routes/invoices.js index 2ca3b006..d856ef6b 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -13,6 +13,7 @@ export default { invoice = await got(`${config.classic}/invoice/${hash}`).json(); invoice.id = invoice.uuid; invoice.classic = true; + invoice.user.id = invoice.user.uuid; invoice.user.username += "@classic"; } diff --git a/routes/users.js b/routes/users.js index 0692ed23..e3264937 100644 --- a/routes/users.js +++ b/routes/users.js @@ -235,7 +235,7 @@ export default { headers: { authorization: `Bearer ${config.admin}` } }).json(); - u = { id: v4(), about: u.address, ...pick(u, fields) }; + u = { id: u.uuid, about: u.address, ...pick(u, fields) }; delete u.address; await s(`user:${p.with.username}`, u.id); -- cgit v1.2.3 From f07883cdae6da45b063c3b17843c75abf8128e4e Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 1 Feb 2023 16:19:12 -0800 Subject: socket reconnect logic --- lib/rates.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/rates.js b/lib/rates.js index 45c5977a..362aaa98 100644 --- a/lib/rates.js +++ b/lib/rates.js @@ -1,14 +1,18 @@ import got from "got"; import config from "$config"; -import { broadcast } from "./sockets"; -import store from "./store"; +import { broadcast } from "$lib/sockets"; +import store from "$lib/store"; import { err, warn } from "$lib/logging"; import { g, s } from "$lib/db"; +import { sleep } from "$lib/utils"; import WebSocket from "ws"; +let last; let ws; -let connect = () => { - if (ws) ws.terminate(); +let connect = async () => { + if (ws && ws.readyState === 1 && Date.now() - last < 5000) return; + if (ws) ws.terminate() && (await sleep(Math.round(Math.random() * 1000))); + ws = new WebSocket("wss://stream.binance.com:9443/ws/btcusdt@ticker"); ws.onmessage = async function(event) { @@ -20,6 +24,7 @@ let connect = () => { }); store.last = msg.c; + last = Date.now(); } catch (e) { console.log(e); err("binance message error", e.message); @@ -30,7 +35,6 @@ let connect = () => { err("binance socket error", error.message); }; - setTimeout(connect, 360000); return ws; }; @@ -57,5 +61,5 @@ export let getRates = async () => { store.fx = fx; - setTimeout(getRates, 7200000); + setTimeout(getRates, 30000); }; -- cgit v1.2.3 From 17a2a698de2a522f05d9ec813411b5a01b95234d Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 3 Feb 2023 09:58:47 -0800 Subject: contacts --- lib/db.js | 2 +- lib/lightning.js | 9 +++------ lib/nostr.js | 1 + lib/payments.js | 5 +++-- lib/register.js | 3 ++- lib/upload.js | 2 +- routes/payments.js | 2 +- routes/users.js | 39 +++++++++++++++++++++++++++++---------- 8 files changed, 41 insertions(+), 22 deletions(-) (limited to 'lib') diff --git a/lib/db.js b/lib/db.js index 516db236..86f8082e 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,4 +1,4 @@ -import config from "$config" +import config from "$config"; import { sleep } from "$lib/utils"; import { createClient } from "redis"; diff --git a/lib/lightning.js b/lib/lightning.js index c8daf8c8..2915b7c7 100644 --- a/lib/lightning.js +++ b/lib/lightning.js @@ -8,6 +8,7 @@ import { g, s, t, db } from "$lib/db"; import { credit, types } from "$lib/payments"; export async function listenForLightning() { + let event = await ln.waitanyinvoice(await g("pay_index")); let { payment_hash: hash, bolt11, @@ -15,7 +16,7 @@ export async function listenForLightning() { status, msatoshi_received, payment_preimage: preimage - } = await ln.waitanyinvoice(await g("pay_index")); + } = event; await s("pay_index", pay_index); setTimeout(listenForLightning); @@ -30,11 +31,7 @@ export async function listenForLightning() { let invoice = await g(`invoice:${hash}`); if (!invoice) return warn("received lightning with no invoice", bolt11); - let { currency, memo, rate, tip, uid } = invoice; - let amount = parseInt(received) - tip; - if (amount < 0) throw new Error("amount out of range"); - - await credit(hash, amount, memo, preimage, types.lightning); + await credit(hash, received, invoice.memo, preimage, types.lightning); } catch (e) { console.log(e); err("problem receiving lightning payment", e.message); diff --git a/lib/nostr.js b/lib/nostr.js index 23ea937b..065fafb0 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -15,6 +15,7 @@ export let fillPool = () => { }); pool.on("event", async (relay, sub, ev) => { + if (!coinos) return; if (timeouts[sub]) timeouts[sub].extend(ev); try { let content; diff --git a/lib/payments.js b/lib/payments.js index 47cb11f4..6fd595d4 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -36,8 +36,8 @@ export let debit = async ( if (!amount || amount < 0) fail("Amount must be greater than zero"); let ourfee = [types.bitcoin, types.lightning].includes(type) - ? 0 - : Math.round(amount * config.fee); + ? Math.round(amount * config.fee) + : 0; await t(`balance:${uid}`, async (balance, db) => { await t(`credit:${type}:${uid}`, async (credit, db) => { @@ -85,6 +85,7 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => { amount = parseInt(amount); let invoice = await g(`invoice:${hash}`); if (!invoice) return; + if (!memo) ({ memo } = invoice); let user = await g(`user:${invoice.uid}`); diff --git a/lib/register.js b/lib/register.js index a3ebfc86..a222a416 100644 --- a/lib/register.js +++ b/lib/register.js @@ -8,7 +8,7 @@ import { s, g } from "$lib/db"; import { got } from "got"; export default async (user, ip, requireChallenge) => { - let { password, username } = user; + let { password, pubkey, username } = user; l("registering", username); if (!username) throw new Error("Username required"); @@ -54,6 +54,7 @@ export default async (user, ip, requireChallenge) => { await s(`user:${id}`, user); await s(`user:${user.username}`, id); + await s(`user:${pubkey}`, id); l("new user", user.username); return user; diff --git a/lib/upload.js b/lib/upload.js index 2f8180a0..954e8d4d 100644 --- a/lib/upload.js +++ b/lib/upload.js @@ -12,7 +12,7 @@ export default async (req, res) => { params: { type }, user } = req; - console.log("TYPE", type) + console.log("TYPE", type); if (!["banner", "profile"].includes(type)) throw new Error("invalid upload"); let data = await req.file(); diff --git a/routes/payments.js b/routes/payments.js index 5c3944ce..970bc187 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -31,7 +31,7 @@ export default { await got .post(`${config.classic}/admin/credit`, { - json: { username, amount, source }, + json: { username, amount, source }, headers: { authorization: `Bearer ${config.admin}` } }) .json(); diff --git a/routes/users.js b/routes/users.js index e3264937..10cc358d 100644 --- a/routes/users.js +++ b/routes/users.js @@ -23,6 +23,7 @@ export default { async me({ user }, res) { try { user.balance = await g(`balance:${user.id}`); + user.prompt = !!user.prompt; res.send(pick(user, whitelist)); } catch (e) { console.log("problem fetching user", e); @@ -33,7 +34,9 @@ export default { async get({ params: { key } }, res) { try { if (key.startsWith("npub")) { - key = Buffer.from(fromWords(decode(key).words)).toString("hex"); + try { + key = Buffer.from(fromWords(decode(key).words)).toString("hex"); + } catch (e) {} } let user = await g(`user:${key}`); @@ -43,7 +46,9 @@ export default { if (!user && key.length === 64) { user = { - username: key.substr(0, 6), + currency: "USD", + username: key, + display: key.substr(0, 6), pubkey: key, anon: true }; @@ -63,6 +68,8 @@ export default { "id" ]; + user.prompt = !!user.prompt; + res.send(pick(user, whitelist)); } catch (e) { console.log(e); @@ -197,7 +204,7 @@ export default { headers: { authorization: `Bearer ${config.admin}` } }).json(); - let { balance } = user; + let { balance, pubkey } = user; if (!user) fail(); uid = user.uuid; @@ -209,6 +216,7 @@ export default { migrated: true }; + await s(`user:${pubkey}`, uid); await s(`user:${username}`, uid); await s(`user:${uid}`, user); await s(`balance:${uid}`, balance); @@ -238,6 +246,7 @@ export default { u = { id: u.uuid, about: u.address, ...pick(u, fields) }; delete u.address; + await s(`user:${u.pubkey}`, u.id); await s(`user:${p.with.username}`, u.id); await s(`user:${u.id}`, u); @@ -346,12 +355,22 @@ export default { } }, - async contacts({ user: { uuid } }, res) { - let payments = (await g(`${uuid}:payments`)) || []; - res.send( - Promise.all( - [...new Set(payments.map(p => p.with_id))].map(id => g(`user:${id}`)) - ) - ); + async contacts({ user: { id } }, res) { + let i = (await g(`${id}:cindex`)) || 0; + let payments = (await db.lRange(`${id}:payments`, i, -1)) || []; + await db.incrBy(`${id}:cindex`, payments.length); + + let contacts = (await g(`${id}:contacts`)) || []; + + for (let { ref } of ( + await Promise.all(payments.map(async id => await g(`payment:${id}`))) + ).filter(p => p.type === types.internal && p.ref)) { + !~contacts.findIndex(({ id }) => id === ref) && + contacts.push(await g(`user:${ref}`)); + } + + await s(`${id}:contacts`, contacts); + + res.send(contacts); } }; -- cgit v1.2.3