From 881b350252a6d59999280ca0b82b8eec01d3cca1 Mon Sep 17 00:00:00 2001 From: Mondei1 Date: Sun, 17 Jan 2021 18:48:47 +0100 Subject: [PATCH] Support for Litecoin and Dogecoin added --- package-lock.json | 45 +++++++ package.json | 1 + src/app.ts | 7 +- src/controllers/invoice.ts | 2 +- src/helper/backendProvider.ts | 11 +- src/helper/invoiceManager.ts | 2 +- src/helper/providerManager.ts | 40 ++++-- src/helper/providers/bitcoinCore.ts | 31 +++-- src/helper/providers/blockio.js | 37 ++++++ src/helper/providers/dogecoinCore.ts | 156 ++++++++++++++++++++++++ src/helper/providers/litecoinCore.ts | 151 +++++++++++++++++++++++ src/helper/providers/moneroCLI.js | 85 +++++++++++++ src/models/invoice/invoice.interface.ts | 3 + src/models/invoice/invoice.schema.ts | 1 + 14 files changed, 538 insertions(+), 34 deletions(-) create mode 100644 src/helper/providers/blockio.js create mode 100644 src/helper/providers/dogecoinCore.ts create mode 100644 src/helper/providers/litecoinCore.ts create mode 100644 src/helper/providers/moneroCLI.js diff --git a/package-lock.json b/package-lock.json index eaa5c81..f534ed6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -279,6 +279,14 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -873,6 +881,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -1155,6 +1168,11 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, + "is-base64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-base64/-/is-base64-1.1.0.tgz", + "integrity": "sha512-Nlhg7Z2dVC4/PTvIFkgVVNvPHSO2eR/Yd0XzhGiXCXEvWnptXlXa/clQ8aePPiMuxEGcWfzWbGw2Fe3d+Y3v1g==" + }, "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -1553,6 +1571,11 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==" }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "node-gyp-build": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", @@ -1755,6 +1778,18 @@ "once": "^1.3.1" } }, + "pusher": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pusher/-/pusher-4.0.2.tgz", + "integrity": "sha512-11kmKP7WZFKLs11XX14Ma+/TJg8TdW3cY/FLPkSQBFNOkXnFEdLEM6YPprzQNPIhQ05KjLS+1XR33AvuveZBRA==", + "requires": { + "abort-controller": "^3.0.0", + "is-base64": "^1.1.0", + "node-fetch": "^2.6.1", + "tweetnacl": "^1.0.0", + "tweetnacl-util": "^0.15.0" + } + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -2201,6 +2236,16 @@ "yn": "3.1.1" } }, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "tweetnacl-util": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/package.json b/package.json index 68d6d29..4c929c3 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "jsonwebtoken": "^8.5.1", "mongoose": "^5.11.8", "mysql": "^2.18.1", + "pusher": "^4.0.2", "socket.io": "^2.3.0", "ts-node": "^9.1.1", "typescript": "^4.1.3", diff --git a/src/app.ts b/src/app.ts index 39ce229..8904ae3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,7 +2,6 @@ import * as bodyParser from 'body-parser'; import * as cors from 'cors'; import { config as dconfig } from 'dotenv'; import * as express from 'express'; -import * as rpc from 'jayson'; import * as mongoose from 'mongoose'; import * as winston from 'winston'; import * as socketio from 'socket.io'; @@ -26,7 +25,6 @@ export const MONGO_URI = process.env.MONGO_URI || ""; 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 invoiceManager: InvoiceManager | undefined = undefined; export let socketManager: SocketManager | undefined = undefined; export let providerManager: ProviderManager = undefined; @@ -135,10 +133,7 @@ async function run() { logger.info(`HTTP server started on port ${config.http.host}:${config.http.port}`); }); - rpcClient = rpc.Client.http({ - port: 18332, - auth: 'admin:admin' - }); + } run(); \ No newline at end of file diff --git a/src/controllers/invoice.ts b/src/controllers/invoice.ts index 04b70f7..acf5bbd 100644 --- a/src/controllers/invoice.ts +++ b/src/controllers/invoice.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import got from 'got'; import { config } from '../../config'; -import { invoiceManager, INVOICE_SECRET, logger, providerManager, rpcClient } from '../app'; +import { invoiceManager, INVOICE_SECRET, logger, providerManager } from '../app'; import { randomString } from '../helper/crypto'; import { CryptoUnits, decimalPlaces, FiatUnits, findCryptoBySymbol, PaymentStatus, roundNumber } from '../helper/types'; import { ICart, IInvoice, IPaymentMethod } from '../models/invoice/invoice.interface'; diff --git a/src/helper/backendProvider.ts b/src/helper/backendProvider.ts index 2444c26..c0e7a55 100644 --- a/src/helper/backendProvider.ts +++ b/src/helper/backendProvider.ts @@ -1,5 +1,4 @@ import { IInvoice } from '../models/invoice/invoice.interface'; -import { InvoiceManager } from './invoiceManager'; import { CryptoUnits } from './types'; /** @@ -16,14 +15,16 @@ export abstract class BackendProvider { abstract readonly AUTHOR: string; /** - * The cryptocurrency that this providers supports. + * The cryptocurrencies that this providers supports. */ - abstract readonly CRYPTO: CryptoUnits; + abstract readonly CRYPTO: CryptoUnits[]; /** * This function gets called when this provider gets activated. + * + * @returns If `false` is returned, then the provider failed to initialize. */ - abstract onEnable(): void; + abstract onEnable(): boolean; /** * Generate a new address to receive new funds. @@ -42,7 +43,7 @@ export abstract class BackendProvider { * @param rawTx Raw transcation * @returns See https://developer.bitcoin.org/reference/rpc/decoderawtransaction.html for reference */ - abstract decodeRawTransaction(rawTx: string): Promise; + //abstract decodeRawTransaction(rawTx: string): Promise; /** * Send funds to a specific address. diff --git a/src/helper/invoiceManager.ts b/src/helper/invoiceManager.ts index 0d77d32..223ce55 100644 --- a/src/helper/invoiceManager.ts +++ b/src/helper/invoiceManager.ts @@ -148,7 +148,7 @@ export class InvoiceManager { * This method checks if a payment has been made in time and that the right amount was sent. */ async validatePayment(invoice: IInvoice, tx: string): Promise { - if (invoice.dueBy.getTime() < Date.now()) { + if (invoice.dueBy.getTime() < Date.now() && invoice.status <= PaymentStatus.PENDING && invoice.status >= PaymentStatus.REQUESTED) { invoice.status = PaymentStatus.TOOLATE; await invoice.save(); diff --git a/src/helper/providerManager.ts b/src/helper/providerManager.ts index 59b6004..985c0e4 100644 --- a/src/helper/providerManager.ts +++ b/src/helper/providerManager.ts @@ -32,20 +32,40 @@ export class ProviderManager { const absolutePath = join(this.providerFilePath, file); const providerModule = require(absolutePath); const provider = new providerModule.Provider() as BackendProvider; - - if (this.cryptoProvider.has(provider.CRYPTO)) { - logger.warn(`Provider ${provider.NAME} will be ignored since there is already another provider active for ${provider.CRYPTO}!`); + + provider.CRYPTO.forEach(crypto => { + if (this.cryptoProvider.has(crypto)) { + logger.warn(`Provider ${provider.NAME} will be ignored since there is already another provider active for ${provider.CRYPTO}!`); + return; + } + + this.cryptoProvider.set(crypto, provider); + config.payment.methods.push(crypto); + }); + + // Execute onEnable() function of this provider + const startUp = provider.onEnable(); + if (!startUp) { + logger.error(`Provider "${provider.NAME}" by ${provider.AUTHOR} (${provider.VERSION}) failed to start! (check previous logs)`); return; } - this.cryptoProvider.set(provider.CRYPTO, provider); - config.payment.methods.push(provider.CRYPTO); - - // Execute onEnable() function of this provider - provider.onEnable(); - - logger.info(`Loaded provider ${provider.NAME} by ${provider.AUTHOR} (${provider.VERSION}) for ${provider.CRYPTO}`); + logger.info(`Loaded provider "${provider.NAME}" by ${provider.AUTHOR} (${provider.VERSION}) for ${provider.CRYPTO.join(', ')}`); }); } + /** + * This provider will be no longer be used. + */ + disable(providerFor: CryptoUnits) { + if (!this.cryptoProvider.has(providerFor)) { + return; + } + + const provider = this.getProvider(providerFor); + logger.warn(`Provider "${provider.NAME}" will be disabled ...`); + + this.cryptoProvider.delete(providerFor); + } + } \ No newline at end of file diff --git a/src/helper/providers/bitcoinCore.ts b/src/helper/providers/bitcoinCore.ts index 6016508..3ace3eb 100644 --- a/src/helper/providers/bitcoinCore.ts +++ b/src/helper/providers/bitcoinCore.ts @@ -1,6 +1,7 @@ import { Subscriber } from 'zeromq'; -import { invoiceManager, logger, rpcClient } from '../../app'; +import * as rpc from 'jayson'; +import { invoiceManager, logger } from '../../app'; import { IInvoice } from '../../models/invoice/invoice.interface'; import { BackendProvider, IRawTransaction, ITransaction, ITransactionList } from '../backendProvider'; import { CryptoUnits, PaymentStatus } from '../types'; @@ -8,27 +9,36 @@ import { CryptoUnits, PaymentStatus } from '../types'; export class Provider implements BackendProvider { private sock: Subscriber; + private rpcClient: rpc.HttpClient; NAME = 'Bitcoin Core'; DESCRIPTION = 'This provider communicates with the Bitcoin Core application.'; AUTHOR = 'LibrePay Team'; VERSION = '0.1'; - CRYPTO = CryptoUnits.BITCOIN; + CRYPTO = [CryptoUnits.BITCOIN]; onEnable() { this.sock = new Subscriber(); this.sock.connect('tcp://127.0.0.1:29000'); this.sock.subscribe('rawtx'); + + this.rpcClient = rpc.Client.http({ + port: 18332, + auth: 'admin:admin' + }); + this.listener(); this.watchConfirmations(); + return true; + //logger.info('The Bitcoin Core backend is now available!'); } async getNewAddress(): Promise { return new Promise((resolve, reject) => { - rpcClient.request('getnewaddress', ['', 'bech32'], async (err, message) => { + this.rpcClient.request('getnewaddress', ['', 'bech32'], async (err, message) => { if (err) { reject(err); return; @@ -41,7 +51,7 @@ export class Provider implements BackendProvider { async getTransaction(txId: string): Promise { return new Promise((resolve, reject) => { - rpcClient.request('gettransaction', [txId], (err, message) => { + this.rpcClient.request('gettransaction', [txId], (err, message) => { if (err) { reject(err); return; @@ -52,9 +62,9 @@ export class Provider implements BackendProvider { }); } - async decodeRawTransaction(rawTx: string): Promise { + private async decodeRawTransaction(rawTx: string): Promise { return new Promise((resolve, reject) => { - rpcClient.request('decoderawtransaction', [rawTx], (err, decoded) => { + this.rpcClient.request('decoderawtransaction', [rawTx], (err, decoded) => { if (err) { reject(err); return; @@ -72,7 +82,7 @@ export class Provider implements BackendProvider { commentTo?: string, subtractFeeFromAmount?: boolean): Promise { return new Promise((resolve, reject) => { - rpcClient.request('sendtoaddress', [recipient, amount, comment, commentTo, subtractFeeFromAmount], (err, decoded) => { + this.rpcClient.request('sendtoaddress', [recipient, amount, comment, commentTo, subtractFeeFromAmount], (err, decoded) => { if (err) { reject(err); return; @@ -84,7 +94,6 @@ export class Provider implements BackendProvider { } async listener() { - 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); @@ -92,7 +101,7 @@ export class Provider implements BackendProvider { tx.vout.forEach(output => { // Loop over each output and check if the address of one matches the one of an invoice. - invoiceManager.getPendingInvoices().forEach(async invoice => { + invoiceManager.getPendingInvoices().filter(item => { return item.paymentMethod === CryptoUnits.BITCOIN }).forEach(async invoice => { if (output.scriptPubKey.addresses === undefined) return; // Sometimes (weird) transaction don't have any addresses logger.debug(`${output.scriptPubKey.addresses} <-> ${invoice.receiveAddress}`); @@ -112,7 +121,7 @@ export class Provider implements BackendProvider { async watchConfirmations() { setInterval(() => { - invoiceManager.getUnconfirmedTransactions().forEach(async invoice => { + invoiceManager.getUnconfirmedTransactions().filter(item => { return item.paymentMethod === CryptoUnits.BITCOIN }).forEach(async invoice => { if (invoice.transcationHash.length === 0) return; const transcation = invoice.transcationHash; @@ -126,7 +135,7 @@ export class Provider implements BackendProvider { if (invoice.status === PaymentStatus.DONE || invoice.status === PaymentStatus.CANCELLED) return; if (invoice.paymentMethod !== CryptoUnits.BITCOIN) return; - rpcClient.request('listreceivedbyaddress', [0, false, false, invoice.receiveAddress], async (err, message) => { + this.rpcClient.request('listreceivedbyaddress', [0, false, false, invoice.receiveAddress], async (err, message) => { if (err) { logger.error(`There was an error while getting transcations of address ${invoice.receiveAddress}: ${err.message}`); return; diff --git a/src/helper/providers/blockio.js b/src/helper/providers/blockio.js new file mode 100644 index 0000000..8af0977 --- /dev/null +++ b/src/helper/providers/blockio.js @@ -0,0 +1,37 @@ +import got from "got/dist/source"; +import { logger } from "../../app"; +import { BackendProvider } from "../backendProvider"; +import { CryptoUnits } from "../types"; +import * as Pusher from "pusher" + +export class Provider implements BackendProvider { + + NAME = 'Block.io'; + DESCRIPTION = 'This provider communicates with Block.io and sochain1.com to manage your online wallet.'; + AUTHOR = 'LibrePay Team'; + VERSION = '0.1'; + CRYPTO = CryptoUnits.DOGECOIN; + + onEnable() { + if (process.env.BLOCKIO_DOGECOIN_API_KEY === undefined) { + logger.error(`Enviroment variable BLOCKIO_DOGECOIN_API_KEY is required but not set!`); + return false; + } + + return true; + } + + async listener() { + const pusher = new Pusher({ + host: 'slanger1.sochain.com', + port: '443', + encrypted: true, + appId: 'e9f5cc20074501ca7395', + key: '', + secret: '' + }); + + let ticker = pusher. + } + +} \ No newline at end of file diff --git a/src/helper/providers/dogecoinCore.ts b/src/helper/providers/dogecoinCore.ts new file mode 100644 index 0000000..92617f7 --- /dev/null +++ b/src/helper/providers/dogecoinCore.ts @@ -0,0 +1,156 @@ +import { Subscriber } from 'zeromq'; + +import * as rpc from 'jayson'; +import { invoiceManager, logger } from '../../app'; +import { IInvoice } from '../../models/invoice/invoice.interface'; +import { BackendProvider, IRawTransaction, ITransaction, ITransactionDetails, ITransactionList } from '../backendProvider'; +import { CryptoUnits, PaymentStatus } from '../types'; + +export class Provider implements BackendProvider { + + private sock: Subscriber; + private rpcClient: rpc.HttpClient; + + NAME = 'Dogecoin Core'; + DESCRIPTION = 'This provider communicates with the Bitcoin Core application.'; + AUTHOR = 'LibrePay Team'; + VERSION = '0.1'; + CRYPTO = [CryptoUnits.DOGECOIN]; + + onEnable() { + this.sock = new Subscriber(); + this.sock.connect('tcp://127.0.0.1:30000'); + this.sock.subscribe('rawtx'); + + + this.rpcClient = rpc.Client.http({ + port: 22556, + auth: 'admin:admin' + }); + + this.listener(); + this.watchConfirmations(); + + return true; + + //logger.info('The Bitcoin Core backend is now available!'); + } + + async getNewAddress(): Promise { + return new Promise((resolve, reject) => { + this.rpcClient.request('getnewaddress', [''], async (err, message) => { + if (err) { + reject(err); + return; + } + + resolve(message.result); + }); + }); + } + + async getTransaction(txId: string): Promise { + return new Promise((resolve, reject) => { + this.rpcClient.request('gettransaction', [txId], (err, message) => { + if (err) { + reject(err); + return; + } + + resolve(message.result); + }); + }); + } + + private async decodeRawTransaction(rawTx: string): Promise { + return new Promise((resolve, reject) => { + this.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 { + return new Promise((resolve, reject) => { + this.rpcClient.request('sendtoaddress', [recipient, amount, comment, commentTo, subtractFeeFromAmount], (err, decoded) => { + if (err) { + reject(err); + return; + } + + resolve(decoded.result.txid); + }); + }); + } + + async listener() { + 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. + invoiceManager.getPendingInvoices().filter(item => { return item.paymentMethod === CryptoUnits.DOGECOIN }).forEach(async invoice => { + if (output.scriptPubKey.addresses === undefined) return; // Sometimes (weird) transaction don't have any addresses + + logger.debug(`${output.scriptPubKey.addresses} <-> ${invoice.receiveAddress}`); + // 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 + invoiceManager.validatePayment(invoice, tx.txid); + } + }) + }); + + } + } + + async watchConfirmations() { + setInterval(() => { + invoiceManager.getUnconfirmedTransactions().filter(item => { return item.paymentMethod === CryptoUnits.DOGECOIN }).forEach(async invoice => { + if (invoice.transcationHash.length === 0) return; + const transcation = invoice.transcationHash; + + const tx = await this.getTransaction(transcation); + invoiceManager.setConfirmationCount(invoice, tx.confirmations); + }); + }, 2_000); + } + + async validateInvoice(invoice: IInvoice) { + if (invoice.status === PaymentStatus.DONE || invoice.status === PaymentStatus.CANCELLED) return; + if (invoice.paymentMethod !== CryptoUnits.DOGECOIN) return; + + this.rpcClient.request('listreceivedbyaddress', [0, false, false], async (err, message) => { + if (err) { + logger.error(`There was an error while getting transcations of address ${invoice.receiveAddress}: ${err.message}`); + return; + } + + // Unfortunately we have to search the map manually. + const res = (message.result as ITransactionList[]).find(item => { + return item.address === invoice.receiveAddress; + }) as ITransactionList; + if (res === undefined) return; + + res.txids.forEach(async tx => { + invoiceManager.validatePayment(invoice, tx); + }); + }); + } +} + diff --git a/src/helper/providers/litecoinCore.ts b/src/helper/providers/litecoinCore.ts new file mode 100644 index 0000000..bd18752 --- /dev/null +++ b/src/helper/providers/litecoinCore.ts @@ -0,0 +1,151 @@ +import { Subscriber } from 'zeromq'; + +import * as rpc from 'jayson'; +import { invoiceManager, logger } from '../../app'; +import { IInvoice } from '../../models/invoice/invoice.interface'; +import { BackendProvider, IRawTransaction, ITransaction, ITransactionList } from '../backendProvider'; +import { CryptoUnits, PaymentStatus } from '../types'; + +export class Provider implements BackendProvider { + + private sock: Subscriber; + private rpcClient: rpc.HttpClient; + + NAME = 'Litecoin Core'; + DESCRIPTION = 'This provider communicates with the Litecoin Core application.'; + AUTHOR = 'LibrePay Team'; + VERSION = '0.1'; + CRYPTO = [CryptoUnits.LITECOIN]; + + onEnable() { + this.sock = new Subscriber(); + this.sock.connect('tcp://127.0.0.1:40000'); + this.sock.subscribe('rawtx'); + + + this.rpcClient = rpc.Client.http({ + port: 22557, + auth: 'admin:admin' + }); + + this.listener(); + this.watchConfirmations(); + + return true; + } + + async getNewAddress(): Promise { + return new Promise((resolve, reject) => { + this.rpcClient.request('getnewaddress', ['', 'bech32'], async (err, message) => { + if (err) { + reject(err); + return; + } + + resolve(message.result); + }); + }); + } + + async getTransaction(txId: string): Promise { + return new Promise((resolve, reject) => { + this.rpcClient.request('gettransaction', [txId], (err, message) => { + if (err) { + reject(err); + return; + } + + resolve(message.result); + }); + }); + } + + private async decodeRawTransaction(rawTx: string): Promise { + return new Promise((resolve, reject) => { + this.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 { + return new Promise((resolve, reject) => { + this.rpcClient.request('sendtoaddress', [recipient, amount, comment, commentTo, subtractFeeFromAmount], (err, decoded) => { + if (err) { + reject(err); + return; + } + + resolve(decoded.result.txid); + }); + }); + } + + async listener() { + 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. + invoiceManager.getPendingInvoices().filter(item => { return item.paymentMethod === CryptoUnits.LITECOIN }).forEach(async invoice => { + if (output.scriptPubKey.addresses === undefined) return; // Sometimes (weird) transaction don't have any addresses + + logger.debug(`${output.scriptPubKey.addresses} <-> ${invoice.receiveAddress}`); + // 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 + invoiceManager.validatePayment(invoice, tx.txid); + } + }) + }); + + } + } + + async watchConfirmations() { + setInterval(() => { + invoiceManager.getUnconfirmedTransactions().filter(item => { return item.paymentMethod === CryptoUnits.LITECOIN }).forEach(async invoice => { + if (invoice.transcationHash.length === 0) return; + const transcation = invoice.transcationHash; + + const tx = await this.getTransaction(transcation); + invoiceManager.setConfirmationCount(invoice, tx.confirmations); + }); + }, 2_000); + } + + async validateInvoice(invoice: IInvoice) { + if (invoice.status === PaymentStatus.DONE || invoice.status === PaymentStatus.CANCELLED) return; + if (invoice.paymentMethod !== CryptoUnits.LITECOIN) return; + + this.rpcClient.request('listreceivedbyaddress', [0, false, false, invoice.receiveAddress], async (err, message) => { + if (err) { + logger.error(`There was an error while getting transcations of address ${invoice.receiveAddress}: ${err.message}`); + return; + } + + const res = message.result[0] as ITransactionList; + if (res === undefined) return; + + res.txids.forEach(async tx => { + invoiceManager.validatePayment(invoice, tx); + }); + }); + } +} + diff --git a/src/helper/providers/moneroCLI.js b/src/helper/providers/moneroCLI.js new file mode 100644 index 0000000..abbe058 --- /dev/null +++ b/src/helper/providers/moneroCLI.js @@ -0,0 +1,85 @@ +import * as rpc from 'jayson'; +import { Subscriber } from 'zeromq'; + +import { logger, providerManager } from '../../app'; +import { BackendProvider, ITransaction } from '../backendProvider'; +import { CryptoUnits } from '../types'; + +export class Provider implements BackendProvider { + + private sock: Subscriber; + private rpcClient: rpc.HttpClient; + + NAME = 'Monero CLI'; + DESCRIPTION = 'This provider queries the Monero daemon running on your computer'; + AUTHOR = 'LibrePay Team'; + VERSION = '1.0'; + CRYPTO = CryptoUnits.MONERO; + + onEnable() { + logger.info('Monero CLI provider is now availabe!'); + + if (process.env.MONERO_WALLET_PASSWORD === undefined) { + logger.error(`Enviroment variable MONERO_WALLET_PASSWORD is required but not set!`); + return false; + } + + if (process.env.MONERO_WALLET_NAME === undefined) { + logger.error(`Enviroment variable MONERO_WALLET_FILEPATH is required but not set!`); + return false; + } + + if (process.env.MONERO_RPC_ADDRESS === undefined) { + logger.error(`Enviroment variable MONERO_RPC_ADDRESS is required but not set!`); + return false; + } + + this.rpcClient = rpc.Client.http({ + port: 18082, + version: 2, + auth: 'admin:admin' + }); + this.rpcClient.request('open_wallet', { + filename: process.env.MONERO_WALLET_NAME, + password: process.env.MONERO_WALLET_PASSWORD + }, (err, message) => { + if (err) { + logger.error(`Failed to open Monero wallet: ${err.message}\nMaybe a wrong password or path?`); + providerManager.disable(this.CRYPTO); + + return; + } + console.log(message); + }); + + this.listener(); + + return true; + } + + listener() { + this.sock = new Subscriber(); + this.sock.connect(process.env.MONERO_RPC_ADDRESS); + this.sock.subscribe('rawtx'); + } + + // Since we can safely use the same address everytime, we just need to return a address + // with an integrated payment id. + getNewAddress(): Promise { + return new Promise((resolve, reject) => { + const account_index = Number(process.env.MONERO_WALLET_ACCOUNT_INDEX) || 0; + this.rpcClient.request('make_integrated_address', {}, async (err, message) => { + if (err) { + reject(err); + return; + } + + resolve(message.result.integrated_address); + }); + }); + } + + async getTransaction(txId: string): Promise { + + } +} \ No newline at end of file diff --git a/src/models/invoice/invoice.interface.ts b/src/models/invoice/invoice.interface.ts index a8ddcc9..c3f5a79 100644 --- a/src/models/invoice/invoice.interface.ts +++ b/src/models/invoice/invoice.interface.ts @@ -28,6 +28,9 @@ export interface IInvoice extends Document { // 1Kss3e9iPB9vTgWJJZ1SZNkkFKcFJXPz9t receiveAddress?: string; + /** This payment ID is **only available if Monero has been used**. */ + paymentId?: string; + // Is set when invoice got paid // 3b38c3a215d4e7981e1516b2dcbf76fca58911274d5d55b3d615274d6e10f2c1 transcationHash?: string; diff --git a/src/models/invoice/invoice.schema.ts b/src/models/invoice/invoice.schema.ts index 161c48a..fc29f47 100644 --- a/src/models/invoice/invoice.schema.ts +++ b/src/models/invoice/invoice.schema.ts @@ -23,6 +23,7 @@ const schemaInvoice = new Schema({ paymentMethods: [{ type: schemaPaymentMethods, required: true }], paymentMethod: { type: String, enum: Object.values(CryptoUnits), required: false }, receiveAddress: { type: String, required: false }, + paymentId: { type: String, required: false }, transcationHash: { type: String, required: false }, cart: [{ type: schemaCart, required: false }], totalPrice: { type: Number, required: false },