New abstract structure

Events for invoices get emitted in rooms
This commit is contained in:
2020-12-28 19:04:13 +01:00
parent 7f8ae69e2e
commit 0b3502d81f
14 changed files with 467 additions and 229 deletions

View File

@@ -1,10 +1,7 @@
# Socket events
## Invoice status
### Requests
* `subscribe` - Subscribe to a invoices progress. Returns `true` if successful.
* `selector` - Your selector
In order to receive updates about the a specific invoice, **you have to join the room with the selector.**
### Events
* `status` - Status changed (see PaymentStatus enum)
* `confirmationUpdate` - When there is a new confirmation on the transaction

View File

@@ -1,3 +1,4 @@
import { CryptoUnits } from './src/helper/types';
/**
* Here you can change various settings like database credentials, http settings and more.
*
@@ -18,6 +19,14 @@ export const config: IConfig = {
transcations: {
// If a payment has been made and its value is this amount less, it would be still accepted.
acceptMargin: 0.00000001
},
payment: {
// This is a list of cryptocurrencies that you want to accpet.
methods: [
CryptoUnits.BITCOIN,
CryptoUnits.DOGECOIN,
CryptoUnits.ETHEREUM
]
}
}
/**
@@ -40,5 +49,8 @@ export interface IConfig {
},
transcations: {
acceptMargin: number
},
payment: {
methods: CryptoUnits[];
}
}

View File

@@ -6,15 +6,17 @@ import * as rpc from 'jayson';
import * as mongoose from 'mongoose';
import * as winston from 'winston';
import * as socketio from 'socket.io';
import { resolve } from 'path';
import { Server } from 'http';
import { config } from '../config';
import { hashPassword, randomPepper, randomString } from './helper/crypto';
import { InvoiceScheduler } from './helper/invoiceScheduler';
import { InvoiceManager } from './helper/invoiceManager';
import { User } from './models/user/user.model';
import { invoiceRouter } from './routes/invoice';
import { userRouter } from './routes/user';
import { SocketManager } from './helper/socketio';
import { ProviderManager } from './helper/providerManager';
// Load .env
dconfig({ debug: true, encoding: 'UTF-8' });
@@ -25,7 +27,7 @@ export const JWT_SECRET = process.env.JWT_SECRET || "";
export const INVOICE_SECRET = process.env.INVOICE_SECRET || "";
export let rpcClient: rpc.HttpClient | undefined = undefined;
export let invoiceScheduler: InvoiceScheduler | undefined = undefined;
export let invoiceScheduler: InvoiceManager | undefined = undefined;
export let socketManager: SocketManager | undefined = undefined;
export let logger: winston.Logger;
@@ -106,7 +108,10 @@ async function run() {
logger.debug("At least one admin user already exists, skip.");
}
invoiceScheduler = new InvoiceScheduler();
const providerManager = new ProviderManager(resolve('./src/helper/providers'));
providerManager.scan();
invoiceScheduler = new InvoiceManager();
const app = express();
const http = new Server(app);

View File

@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import got from 'got';
import { config } from '../../config';
import { invoiceScheduler, INVOICE_SECRET, rpcClient } from '../app';
import { randomString } from '../helper/crypto';
@@ -62,53 +63,55 @@ export async function createInvoice(req: Request, res: Response) {
return;
}
rpcClient.request('getnewaddress', ['', 'bech32'], async (err, response) => {
if (err) throw err;
// Get price
// Convert coin symbol to full text in order to query Coin Gecko. eg.: ['btc', 'xmr'] => ['bitcoin', 'monero']
let cgFormat = [];
// Get price
// Convert coin symbol to full text in order to query Coin Gecko. eg.: ['btc', 'xmr'] => ['bitcoin', 'monero']
let cgFormat = [];
paymentMethodsRaw.forEach(coin => {
const crypto = findCryptoBySymbol(coin);
paymentMethodsRaw.forEach(coin => {
const crypto = findCryptoBySymbol(coin);
if (crypto !== undefined) {
cgFormat.push(crypto.toLowerCase());
}
});
if (crypto !== undefined) {
cgFormat.push(crypto.toLowerCase());
}
});
const request = await got.get(`https://api.coingecko.com/api/v3/simple/price?ids=${cgFormat.join(',')}&vs_currencies=${currency.toLowerCase()}`, {
responseType: 'json'
});
const request = await got.get(`https://api.coingecko.com/api/v3/simple/price?ids=${cgFormat.join(',')}&vs_currencies=${currency.toLowerCase()}`, {
responseType: 'json'
});
// Calulate total price, if cart is provided
if (cart !== undefined && totalPrice === undefined) {
totalPrice = calculateCart(cart);
}
// Calulate total price, if cart is provided
if (cart !== undefined && totalPrice === undefined) {
totalPrice = calculateCart(cart);
let paymentMethods: IPaymentMethod[] = [];
config.payment.methods.forEach(coin => {
paymentMethods.push({ method: CryptoUnits[coin.toUpperCase()], amount: totalPrice / Number(request.body[coin][currency.toLowerCase()]) });
});
const dueBy = new Date(Date.now() + 1000 * 60 * 60);
Invoice.create({
selector: randomString(128),
paymentMethods,
successUrl,
cancelUrl,
cart,
currency,
totalPrice,
dueBy
}, (error, invoice: IInvoice) => {
if (error) {
res.status(500).send({error: error.message});
return;
}
let paymentMethods: IPaymentMethod[] = [];
Object.keys(request.body).forEach(coin => {
paymentMethods.push({ method: CryptoUnits[coin.toUpperCase()], amount: totalPrice / Number(request.body[coin][currency.toLowerCase()]) });
});
Invoice.create({
selector: randomString(128),
paymentMethods: paymentMethods,
successUrl,
cancelUrl,
cart,
currency,
totalPrice,
dueBy: 60,
receiveAddress: response.result
}, (error, invoice: IInvoice) => {
if (error) {
res.status(500).send({error: error.message});
return;
}
invoiceScheduler.addInvoice(invoice);
res.status(200).send({ id: invoice.selector });
//invoiceScheduler.addInvoice(invoice);
//res.status(200).send({ id: invoice.selector });
res.status(200).send({
methods: paymentMethods,
selector: invoice.selector,
expireDate: invoice.dueBy
});
});
@@ -167,7 +170,7 @@ export async function getInvoice(req: Request, res: Response) {
}
// DELETE /invoice/:selector
export async function cancelPaymnet(req: Request, res: Response) {
export async function cancelInvoice(req: Request, res: Response) {
const selector = req.params.selector;
// If an id is provided
@@ -183,3 +186,7 @@ export async function cancelPaymnet(req: Request, res: Response) {
return;
}
}
export async function getPaymentMethods(req: Request, res: Response) {
res.status(200).send({ methods: config.payment.methods });
}

View File

@@ -0,0 +1,112 @@
import { InvoiceManager } from './invoiceManager';
import { CryptoUnits } from './types';
/**
* This backend provider class is required to write your own backends.
*
* *By default LibrePay supports Bitcoin Core.*
*/
export abstract class BackendProvider {
invoiceManager: InvoiceManager = null;
constructor (invoiceManager: InvoiceManager) {
this.invoiceManager = invoiceManager;
}
/* Provider information */
abstract readonly NAME: string;
abstract readonly DESCRIPTION: string;
abstract readonly VERSION: string;
abstract readonly AUTHOR: string;
/**
* The cryptocurrency that this providers supports.
*/
abstract readonly CRYPTO: CryptoUnits;
/**
* This function gets called when this provider gets activated.
*/
abstract onEnable(): void;
/**
* Generate a new address to receive new funds.
*/
abstract getNewAddress(): Promise<string>;
/**
* Get a transaction from the blockchain.
* @param txId Hash of the transcation you're looking for.
* @returns See https://developer.bitcoin.org/reference/rpc/gettransaction.html for reference
*/
abstract getTransaction(txId: string): Promise<ITransaction>;
/**
* Decode a raw transcation that was broadcasted in the network.
* @param rawTx Raw transcation
* @returns See https://developer.bitcoin.org/reference/rpc/decoderawtransaction.html for reference
*/
abstract decodeRawTransaction(rawTx: string): Promise<IRawTransaction>;
/**
* Send funds to a specific address.
* @param recipient Address of the recipient
* @param amount Amount of coins to transfer
* @param comment Comment what this transaction is about
* @param commentTo Comment on who is receiving it
* @param subtractFeeFromAmount The fee will be deducted from the amount being sent
* @returns The transcation id
*/
abstract sendToAddress(
recipient: string,
amount: number,
comment?: string,
commentTo?: string,
subtractFeeFromAmount?: boolean): Promise<string>;
/**
* Wait for new transactions by the network.
*/
abstract listener(): void;
/**
* Keep track of unconfirmed transactions.
*/
abstract watchConfirmations(): void;
}
export interface ITransaction {
amount: number;
fee: number;
confirmations: number;
time: number; // Unix timestamp
details: {
address: string;
category: 'send' | 'receive' | 'generate' | 'immature' | 'orphan'
vout: number;
fee: number;
abandoned: boolean
}[];
hex: string;
}
export interface IRawTransaction {
txid: string;
hash: string;
size: number;
vsize: number;
weight: number;
version: number;
vin: {
txid: string;
vout: number;
}[];
vout: {
value: number;
n: number;
scriptPubKey: {
addresses: string[];
}
}[];
}

View File

@@ -0,0 +1,85 @@
import { IInvoice } from "../models/invoice/invoice.interface";
import { Subscriber } from 'zeromq';
import { logger, rpcClient, socketManager } from "../app";
import { invoiceRouter } from "../routes/invoice";
import { Invoice } from "../models/invoice/invoice.model";
import { CryptoUnits, PaymentStatus } from "./types";
import { config } from "../../config";
/**
* This invoice manager keeps track of the status of each transaction.
*/
export class InvoiceManager {
private pendingInvoices: IInvoice[];
private unconfirmedTranscations: IInvoice[];
private knownConfirmations: Map<string, number>; // Invoice id / confirmation count
constructor() {
this.unconfirmedTranscations = [];
this.pendingInvoices = [];
this.knownConfirmations = new Map<string, number>();
// Get all pending transcations
Invoice.find({ status: PaymentStatus.PENDING }).then(invoices => {
this.pendingInvoices = invoices;
});
// Get all unconfirmed transactions
Invoice.find({ status: PaymentStatus.UNCONFIRMED }).then(invoices => {
this.unconfirmedTranscations = invoices;
});
this.watchConfirmations();
}
/**
* This will add `invoice` to the pending list.
*/
addInvoice(invoice: IInvoice) {
logger.info(`A new invoice has been created: ${invoice.id}`)
this.pendingInvoices.push(invoice);
}
removeInvoice(invoice: IInvoice) {
this.unconfirmedTranscations.splice(this.unconfirmedTranscations.indexOf(invoice), 1);
}
/**
* Upgrade a pending invoice up to an unconfirmed invoice.
*/
upgradeInvoice(invoice: IInvoice) {
const target = this.pendingInvoices.find(item => { return item.id = invoice.id });
if (target !== undefined) {
this.pendingInvoices.push(invoice);
this.pendingInvoices.splice(this.pendingInvoices.indexOf(invoice), 1);
}
}
getPendingInvoices() {
return this.pendingInvoices;
}
getUnconfirmedTransactions() {
return this.unconfirmedTranscations;
}
hasConfirmationChanged(invoice: IInvoice, confirmations: number) {
return this.knownConfirmations.get(invoice.id) !== confirmations;
}
getConfirmationCount(invoice: IInvoice) {
return this.knownConfirmations.get(invoice.id);
}
setConfirmationCount(invoice: IInvoice, count: number) {
socketManager.emitInvoiceEvent(invoice, 'confirmationUpdate', { count });
return this.knownConfirmations.set(invoice.id, count);
}
/**
* This functions loops over each unconfirmed transaction to check if it reached "trusted" threshold.
*/
private watchConfirmations() {
}
}

View File

@@ -1,134 +0,0 @@
import { IInvoice } from "../models/invoice/invoice.interface";
import { Subscriber } from 'zeromq';
import { logger, rpcClient, socketManager } from "../app";
import { invoiceRouter } from "../routes/invoice";
import { Invoice } from "../models/invoice/invoice.model";
import { CryptoUnits, PaymentStatus } from "./types";
import { config } from "../../config";
export class InvoiceScheduler {
private pendingInvoices: IInvoice[];
private unconfirmedTranscations: IInvoice[];
private knownConfirmations: Map<string, number>; // Invoice id / confirmation cound
private sock: Subscriber;
constructor() {
this.unconfirmedTranscations = [];
this.pendingInvoices = [];
this.knownConfirmations = new Map<string, number>();
// Get all pending transcations
Invoice.find({ status: PaymentStatus.PENDING }).then(invoices => {
this.pendingInvoices = invoices;
});
// Get all unconfirmed transactions
Invoice.find({ status: PaymentStatus.UNCONFIRMED }).then(invoices => {
this.unconfirmedTranscations = invoices;
});
this.sock = new Subscriber();
this.sock.connect('tcp://127.0.0.1:29000');
this.listen();
this.watchConfirmations();
}
addInvoice(invoice: IInvoice) {
logger.info(`A new invoice has been created: ${invoice.id}`)
this.pendingInvoices.push(invoice);
}
/**
* This function waits for Bitcoin Core to respond with raw TX.
*/
private async listen() {
this.sock.subscribe('rawtx');
logger.info('Now listing for incoming transaction to any invoices ...');
for await (const [topic, msg] of this.sock) {
const rawtx = msg.toString('hex');
//logger.debug(`New tx: ${rawtx}`);
rpcClient.request('decoderawtransaction', [rawtx], (err, decoded) => {
if (err) {
logger.error(`Error while decoding raw tx: ${err.message}`);
return;
}
decoded.result.vout.forEach(output => {
// Loop over each output and check if the address of one matches the one of an invoice.
this.pendingInvoices.forEach(invoice => {
if (output.scriptPubKey.addresses === undefined) return; // Sometimes (weird) transaction don't have any addresses
// We found our transaction (https://developer.bitcoin.org/reference/rpc/decoderawtransaction.html)
if (output.scriptPubKey.addresses.indexOf(invoice.receiveAddress) !== -1) {
invoice.paid += output.value;
logger.info(`Transcation for invoice ${invoice.id} received! (${decoded.result.hash})`);
// Change state in database
const price = invoice.paymentMethods.find((item) => { return item.method === CryptoUnits.BITCOIN }).amount;
if (invoice.paid < price - config.transcations.acceptMargin) {
const left = price - output.value;
invoice.status = PaymentStatus.PARTIALLY;
invoice.save();
logger.info(`Transcation for invoice ${invoice.id} received but there are still ${left} BTC missing (${decoded.result.hash})`);
} else {
invoice.status = PaymentStatus.UNCONFIRMED;
invoice.transcationHashes.push(decoded.result.txid);
invoice.save();
// Push to array & remove from pending
this.unconfirmedTranscations.push(invoice);
this.pendingInvoices.splice(this.pendingInvoices.indexOf(invoice), 1);
}
}
})
});
});
}
}
/**
* This functions loops over each unconfirmed transaction to check if it reached "trusted" threshold.
*/
private watchConfirmations() {
setInterval(() => {
this.unconfirmedTranscations.forEach(invoice => {
if (invoice.transcationHashes.length === 0) return;
let trustworthy = true; // Will be true if all transactions are above threshold.
for (let i = 0; i < invoice.transcationHashes.length; i++) {
const transcation = invoice.transcationHashes[i];
rpcClient.request('gettransaction', [transcation], (err, message) => {
if (err) {
logger.error(`Error while fetching confirmation state of ${transcation}: ${err.message}`);
trustworthy = false;
return;
}
if (this.knownConfirmations.get(invoice.id) != message.result.confirmations) {
this.knownConfirmations.set(invoice.id, message.result.confirmations);
socketManager.getSocketByInvoice(invoice).emit('confirmationUpdate', { count: Number(message.result.confirmations) });
}
if (Number(message.result.confirmations) > 0) {
logger.info(`Transaction (${transcation}) has reached more then 2 confirmations and can now be trusted!`);
this.unconfirmedTranscations.splice(this.unconfirmedTranscations.indexOf(invoice), 1);
} else {
trustworthy = false;
logger.debug(`Transcation (${transcation}) has not reached his threshold yet.`);
}
});
}
if (trustworthy) {
invoice.status = PaymentStatus.DONE;
invoice.save(); // This will trigger a post save hook that will notify the user.
}
});
}, 2_000);
}
}

View File

@@ -0,0 +1,20 @@
import { readdirSync } from 'fs';
export class ProviderManager {
providerFilePath: string;
constructor(filePath: string) {
this.providerFilePath = filePath;
}
scan() {
const getDirectories = () =>
readdirSync(this.providerFilePath, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name)
console.log(getDirectories());
}
}

View File

@@ -0,0 +1,154 @@
import { Socket, Subscriber } from "zeromq";
import { config } from "../../../config";
import { logger, rpcClient } from "../../app";
import { BackendProvider, ITransaction, IRawTransaction } from "../backendProvider";
import { InvoiceManager } from "../invoiceManager";
import { CryptoUnits, PaymentStatus } from "../types";
export class BitcoinCore implements BackendProvider {
invoiceManager: InvoiceManager;
private sock: Subscriber;
NAME = 'Bitcoin Core';
DESCRIPTION = 'This provider communicates with the Bitcoin Core application.';
AUTHOR = 'LibrePay Team';
VERSION = '0.1';
CRYPTO = CryptoUnits.BITCOIN;
onEnable() {
logger.info('The Bitcoin Core backend is now available!');
}
async getNewAddress(): Promise<string> {
return new Promise<string>((resolve, reject) => {
rpcClient.request('getnewaddress', ['', 'bech32'], async (err, message) => {
if (err) {
reject(err);
return;
}
resolve(message.result);
});
});
}
async getTransaction(txId: string): Promise<ITransaction> {
return new Promise<ITransaction>((resolve, reject) => {
rpcClient.request('gettransaction', [txId], (err, message) => {
if (err) {
reject(err);
return;
}
resolve(message.result);
});
});
}
async decodeRawTransaction(rawTx: string): Promise<IRawTransaction> {
return new Promise<IRawTransaction>((resolve, reject) => {
rpcClient.request('decoderawtransaction', [rawTx], (err, decoded) => {
if (err) {
reject(err);
return;
}
resolve(decoded.result);
});
});
}
async sendToAddress(
recipient: string,
amount: number,
comment?: string,
commentTo?: string,
subtractFeeFromAmount?: boolean): Promise<string> {
return new Promise<string>((resolve, reject) => {
rpcClient.request('sendtoaddress', [recipient, amount, comment, commentTo, subtractFeeFromAmount], (err, decoded) => {
if (err) {
reject(err);
return;
}
resolve(decoded.result.txid);
});
});
}
async listener() {
this.sock = new Subscriber();
this.sock.connect('tcp://127.0.0.1:29000');
this.sock.subscribe('rawtx');
logger.info('Now listing for incoming transaction to any invoices ...');
for await (const [topic, msg] of this.sock) {
const rawtx = msg.toString('hex');
const tx = await this.decodeRawTransaction(rawtx);
tx.vout.forEach(output => {
// Loop over each output and check if the address of one matches the one of an invoice.
this.invoiceManager.getPendingInvoices().forEach(async invoice => {
if (output.scriptPubKey.addresses === undefined) return; // Sometimes (weird) transaction don't have any addresses
// We found our transaction (https://developer.bitcoin.org/reference/rpc/decoderawtransaction.html)
if (output.scriptPubKey.addresses.indexOf(invoice.receiveAddress) !== -1) {
const senderAddress = output.scriptPubKey.addresses[output.scriptPubKey.addresses.indexOf(invoice.receiveAddress)];
logger.info(`Transcation for invoice ${invoice.id} received! (${tx.hash})`);
// Change state in database
const price = invoice.paymentMethods.find((item) => { return item.method === CryptoUnits.BITCOIN }).amount;
if (output.value < price - config.transcations.acceptMargin) {
const left = price - output.value;
logger.info(`Transcation for invoice ${invoice.id} received but there are ${left} BTC missing (${tx.hash}).`);
const txBack = await this.sendToAddress(senderAddress, output.value, null, null, true);
logger.info(`Sent ${output.value} BTC back to ${senderAddress}`);
} else {
invoice.status = PaymentStatus.UNCONFIRMED;
invoice.transcationHashes = tx.txid;
invoice.save();
this.invoiceManager.upgradeInvoice(invoice);
}
}
})
});
}
}
async watchConfirmations() {
setInterval(() => {
this.invoiceManager.getUnconfirmedTransactions().forEach(async invoice => {
if (invoice.transcationHashes.length === 0) return;
let trustworthy = true; // Will be true if all transactions are above threshold.
for (let i = 0; i < invoice.transcationHashes.length; i++) {
const transcation = invoice.transcationHashes[i];
const tx = await this.getTransaction(transcation);
if (this.invoiceManager.hasConfirmationChanged(invoice, tx.confirmations)) {
this.invoiceManager.setConfirmationCount(invoice, tx.confirmations);
}
if (Number(tx.confirmations) > 0) {
logger.info(`Transaction (${transcation}) has reached more then 2 confirmations and can now be trusted!`);
this.invoiceManager.removeInvoice(invoice);
} else {
trustworthy = false;
logger.debug(`Transcation (${transcation}) has not reached his threshold yet.`);
}
}
if (trustworthy) {
invoice.status = PaymentStatus.DONE;
invoice.save(); // This will trigger a post save hook that will notify the user.
}
});
}, 2_000);
}
}

View File

@@ -7,15 +7,8 @@ import { PaymentStatus } from "./types";
export class SocketManager {
io: Server;
private socketInvoice: Map<string, string>; // Socket ID / _id
private idSocket: Map<string, Socket>; // Socket ID / Socket
private invoiceSocket: Map<string, Socket>; // _id / Socket
constructor(io: Server) {
this.io = io;
this.socketInvoice = new Map<string, string>();
this.idSocket = new Map<string, Socket>();
this.invoiceSocket = new Map<string, Socket>();
this.listen();
}
@@ -23,8 +16,6 @@ export class SocketManager {
console.log("Listen");
this.io.on('connection', (socket: Socket) => {
this.idSocket.set(socket.id, socket);
// The frontend sends his selector, then pick _id and put it in `socketInvoice` map.
// Return `true` if successful and `false` if not.
socket.on('subscribe', async data => {
@@ -36,25 +27,13 @@ export class SocketManager {
}
logger.info(`Socket ${socket.id} has subscribed to invoice ${invoice.id} (${PaymentStatus[invoice.status]})`);
this.socketInvoice.set(socket.id, invoice.id);
this.invoiceSocket.set(invoice.id, socket);
socket.emit('subscribe', true);
}
});
});
}
getSocketById(id: string) {
return this.idSocket.get(id);
}
async getInvoiceBySocket(socketId: string) {
const invoiceId = this.socketInvoice.get(socketId);
return await Invoice.findById(invoiceId);
}
getSocketByInvoice(invoice: IInvoice) {
return this.invoiceSocket.get(invoice.id);
emitInvoiceEvent(invoice: IInvoice, event: string, data: any) {
logger.debug(`Broadcast ${data} to room ${invoice.selector}`);
this.io.to(invoice.selector).emit(event, data);
}
}

View File

@@ -20,6 +20,11 @@ export enum FiatUnits {
}
export enum PaymentStatus {
/**
* The invoice has been requested but the payment method has to be choosen.
*/
REQUESTED = -1,
/**
* The payment has not been yet started. The user did not initiated the transfer.
*/

View File

@@ -17,29 +17,26 @@ export interface IInvoice extends Document {
selector: string;
// Available payment methods
// [{ method: 'btc', amount: 0.0000105 }]
// { method: 'btc', amount: 0.0000105 }
paymentMethods: IPaymentMethod[];
// This is the method choosen by the user
paymentMethod?: CryptoUnits;
// Will be created as soon as the user picked one options
// 1Kss3e9iPB9vTgWJJZ1SZNkkFKcFJXPz9t
receiveAddress: string;
paidWith?: CryptoUnits;
// Already paid amount, in case that not the entire amount was paid with once.
// 0.000013
paid?: number;
receiveAddress?: string;
// Is set when invoice got paid
// 3b38c3a215d4e7981e1516b2dcbf76fca58911274d5d55b3d615274d6e10f2c1
transcationHashes?: string[];
transcationHashes?: string;
cart?: ICart[];
totalPrice?: number;
currency: FiatUnits;
// Time in minutes the user has to pay.
// Time left = (createdAt + dueBy) - Date.now() / 1000
dueBy: number;
// Datetime the user has to pay.
dueBy: Date;
status?: PaymentStatus;

View File

@@ -20,15 +20,14 @@ const schemaPaymentMethods = new Schema({
const schemaInvoice = new Schema({
selector: { type: String, length: 128, required: true },
paymentMethods: [{ type: schemaPaymentMethods, required: true }],
receiveAddress: { type: String, required: true },
paidWith: { type: String, enum: CryptoUnits },
paid: { type: Number, default: 0 },
transcationHashes: [{ type: String, required: false }],
paymentMethod: { type: String, enum: Object.values(CryptoUnits), required: false },
receiveAddress: { type: String, required: false },
transcationHashes: { type: String, required: false },
cart: [{ type: schemaCart, required: false }],
totalPrice: { type: Number, required: false },
currency: { type: String, enum: Object.values(FiatUnits), required: true },
dueBy: { type: Number, required: true },
status: { type: Number, enum: Object.values(PaymentStatus), default: PaymentStatus.PENDING },
dueBy: { type: Date, required: true },
status: { type: Number, enum: Object.values(PaymentStatus), default: PaymentStatus.REQUESTED },
email: { type: String, required: false },
successUrl: { type: String, match: urlRegex, required: false },
cancelUrl: { type: String, match: urlRegex, required: false }
@@ -62,8 +61,7 @@ schemaInvoice.post('validate', function (doc, next) {
schemaInvoice.post('save', function(doc, next) {
let self = this as IInvoice;
if (socketManager.getSocketByInvoice(self) === undefined) return;
socketManager.getSocketByInvoice(self).emit('status', self.status);
socketManager.emitInvoiceEvent(self, 'status', self.status);
next();
})

View File

@@ -1,9 +1,10 @@
import { Router } from "express";
import { createInvoice, getInvoice } from "../controllers/invoice";
import { createInvoice, getInvoice, getPaymentMethods } from "../controllers/invoice";
import { MW_User } from "../controllers/user";
const invoiceRouter = Router()
invoiceRouter.get('/paymentmethods', getPaymentMethods);
invoiceRouter.get('/:selector', getInvoice);
invoiceRouter.get('/', MW_User, getInvoice);
invoiceRouter.post('/', MW_User, createInvoice);