Support for Litecoin and Dogecoin added

This commit is contained in:
2021-01-17 18:48:47 +01:00
parent fc71aed660
commit 881b350252
14 changed files with 538 additions and 34 deletions

45
package-lock.json generated
View File

@@ -279,6 +279,14 @@
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" "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": { "accepts": {
"version": "1.3.7", "version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", "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", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" "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": { "express": {
"version": "4.17.1", "version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", "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", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" "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": { "is-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz",
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==" "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": { "node-gyp-build": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
@@ -1755,6 +1778,18 @@
"once": "^1.3.1" "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": { "qs": {
"version": "6.7.0", "version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
@@ -2201,6 +2236,16 @@
"yn": "3.1.1" "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": { "type-is": {
"version": "1.6.18", "version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",

View File

@@ -31,6 +31,7 @@
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"mongoose": "^5.11.8", "mongoose": "^5.11.8",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"pusher": "^4.0.2",
"socket.io": "^2.3.0", "socket.io": "^2.3.0",
"ts-node": "^9.1.1", "ts-node": "^9.1.1",
"typescript": "^4.1.3", "typescript": "^4.1.3",

View File

@@ -2,7 +2,6 @@ import * as bodyParser from 'body-parser';
import * as cors from 'cors'; import * as cors from 'cors';
import { config as dconfig } from 'dotenv'; import { config as dconfig } from 'dotenv';
import * as express from 'express'; import * as express from 'express';
import * as rpc from 'jayson';
import * as mongoose from 'mongoose'; import * as mongoose from 'mongoose';
import * as winston from 'winston'; import * as winston from 'winston';
import * as socketio from 'socket.io'; 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 JWT_SECRET = process.env.JWT_SECRET || "";
export const INVOICE_SECRET = process.env.INVOICE_SECRET || ""; export const INVOICE_SECRET = process.env.INVOICE_SECRET || "";
export let rpcClient: rpc.HttpClient | undefined = undefined;
export let invoiceManager: InvoiceManager | undefined = undefined; export let invoiceManager: InvoiceManager | undefined = undefined;
export let socketManager: SocketManager | undefined = undefined; export let socketManager: SocketManager | undefined = undefined;
export let providerManager: ProviderManager = 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}`); logger.info(`HTTP server started on port ${config.http.host}:${config.http.port}`);
}); });
rpcClient = rpc.Client.http({
port: 18332,
auth: 'admin:admin'
});
} }
run(); run();

View File

@@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import got from 'got'; import got from 'got';
import { config } from '../../config'; 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 { randomString } from '../helper/crypto';
import { CryptoUnits, decimalPlaces, FiatUnits, findCryptoBySymbol, PaymentStatus, roundNumber } from '../helper/types'; import { CryptoUnits, decimalPlaces, FiatUnits, findCryptoBySymbol, PaymentStatus, roundNumber } from '../helper/types';
import { ICart, IInvoice, IPaymentMethod } from '../models/invoice/invoice.interface'; import { ICart, IInvoice, IPaymentMethod } from '../models/invoice/invoice.interface';

View File

@@ -1,5 +1,4 @@
import { IInvoice } from '../models/invoice/invoice.interface'; import { IInvoice } from '../models/invoice/invoice.interface';
import { InvoiceManager } from './invoiceManager';
import { CryptoUnits } from './types'; import { CryptoUnits } from './types';
/** /**
@@ -16,14 +15,16 @@ export abstract class BackendProvider {
abstract readonly AUTHOR: string; 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. * 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. * Generate a new address to receive new funds.
@@ -42,7 +43,7 @@ export abstract class BackendProvider {
* @param rawTx Raw transcation * @param rawTx Raw transcation
* @returns See https://developer.bitcoin.org/reference/rpc/decoderawtransaction.html for reference * @returns See https://developer.bitcoin.org/reference/rpc/decoderawtransaction.html for reference
*/ */
abstract decodeRawTransaction(rawTx: string): Promise<IRawTransaction>; //abstract decodeRawTransaction(rawTx: string): Promise<IRawTransaction>;
/** /**
* Send funds to a specific address. * Send funds to a specific address.

View File

@@ -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. * 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<void> { async validatePayment(invoice: IInvoice, tx: string): Promise<void> {
if (invoice.dueBy.getTime() < Date.now()) { if (invoice.dueBy.getTime() < Date.now() && invoice.status <= PaymentStatus.PENDING && invoice.status >= PaymentStatus.REQUESTED) {
invoice.status = PaymentStatus.TOOLATE; invoice.status = PaymentStatus.TOOLATE;
await invoice.save(); await invoice.save();

View File

@@ -32,20 +32,40 @@ export class ProviderManager {
const absolutePath = join(this.providerFilePath, file); const absolutePath = join(this.providerFilePath, file);
const providerModule = require(absolutePath); const providerModule = require(absolutePath);
const provider = new providerModule.Provider() as BackendProvider; const provider = new providerModule.Provider() as BackendProvider;
if (this.cryptoProvider.has(provider.CRYPTO)) { provider.CRYPTO.forEach(crypto => {
logger.warn(`Provider ${provider.NAME} will be ignored since there is already another provider active for ${provider.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; return;
} }
this.cryptoProvider.set(provider.CRYPTO, provider); logger.info(`Loaded provider "${provider.NAME}" by ${provider.AUTHOR} (${provider.VERSION}) for ${provider.CRYPTO.join(', ')}`);
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}`);
}); });
} }
/**
* 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);
}
} }

View File

@@ -1,6 +1,7 @@
import { Subscriber } from 'zeromq'; 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 { IInvoice } from '../../models/invoice/invoice.interface';
import { BackendProvider, IRawTransaction, ITransaction, ITransactionList } from '../backendProvider'; import { BackendProvider, IRawTransaction, ITransaction, ITransactionList } from '../backendProvider';
import { CryptoUnits, PaymentStatus } from '../types'; import { CryptoUnits, PaymentStatus } from '../types';
@@ -8,27 +9,36 @@ import { CryptoUnits, PaymentStatus } from '../types';
export class Provider implements BackendProvider { export class Provider implements BackendProvider {
private sock: Subscriber; private sock: Subscriber;
private rpcClient: rpc.HttpClient;
NAME = 'Bitcoin Core'; NAME = 'Bitcoin Core';
DESCRIPTION = 'This provider communicates with the Bitcoin Core application.'; DESCRIPTION = 'This provider communicates with the Bitcoin Core application.';
AUTHOR = 'LibrePay Team'; AUTHOR = 'LibrePay Team';
VERSION = '0.1'; VERSION = '0.1';
CRYPTO = CryptoUnits.BITCOIN; CRYPTO = [CryptoUnits.BITCOIN];
onEnable() { onEnable() {
this.sock = new Subscriber(); this.sock = new Subscriber();
this.sock.connect('tcp://127.0.0.1:29000'); this.sock.connect('tcp://127.0.0.1:29000');
this.sock.subscribe('rawtx'); this.sock.subscribe('rawtx');
this.rpcClient = rpc.Client.http({
port: 18332,
auth: 'admin:admin'
});
this.listener(); this.listener();
this.watchConfirmations(); this.watchConfirmations();
return true;
//logger.info('The Bitcoin Core backend is now available!'); //logger.info('The Bitcoin Core backend is now available!');
} }
async getNewAddress(): Promise<string> { async getNewAddress(): Promise<string> {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
rpcClient.request('getnewaddress', ['', 'bech32'], async (err, message) => { this.rpcClient.request('getnewaddress', ['', 'bech32'], async (err, message) => {
if (err) { if (err) {
reject(err); reject(err);
return; return;
@@ -41,7 +51,7 @@ export class Provider implements BackendProvider {
async getTransaction(txId: string): Promise<ITransaction> { async getTransaction(txId: string): Promise<ITransaction> {
return new Promise<ITransaction>((resolve, reject) => { return new Promise<ITransaction>((resolve, reject) => {
rpcClient.request('gettransaction', [txId], (err, message) => { this.rpcClient.request('gettransaction', [txId], (err, message) => {
if (err) { if (err) {
reject(err); reject(err);
return; return;
@@ -52,9 +62,9 @@ export class Provider implements BackendProvider {
}); });
} }
async decodeRawTransaction(rawTx: string): Promise<IRawTransaction> { private async decodeRawTransaction(rawTx: string): Promise<IRawTransaction> {
return new Promise<IRawTransaction>((resolve, reject) => { return new Promise<IRawTransaction>((resolve, reject) => {
rpcClient.request('decoderawtransaction', [rawTx], (err, decoded) => { this.rpcClient.request('decoderawtransaction', [rawTx], (err, decoded) => {
if (err) { if (err) {
reject(err); reject(err);
return; return;
@@ -72,7 +82,7 @@ export class Provider implements BackendProvider {
commentTo?: string, commentTo?: string,
subtractFeeFromAmount?: boolean): Promise<string> { subtractFeeFromAmount?: boolean): Promise<string> {
return new Promise<string>((resolve, reject) => { return new Promise<string>((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) { if (err) {
reject(err); reject(err);
return; return;
@@ -84,7 +94,6 @@ export class Provider implements BackendProvider {
} }
async listener() { async listener() {
logger.info('Now listing for incoming transaction to any invoices ...');
for await (const [topic, msg] of this.sock) { for await (const [topic, msg] of this.sock) {
const rawtx = msg.toString('hex'); const rawtx = msg.toString('hex');
const tx = await this.decodeRawTransaction(rawtx); const tx = await this.decodeRawTransaction(rawtx);
@@ -92,7 +101,7 @@ export class Provider implements BackendProvider {
tx.vout.forEach(output => { tx.vout.forEach(output => {
// Loop over each output and check if the address of one matches the one of an invoice. // 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 if (output.scriptPubKey.addresses === undefined) return; // Sometimes (weird) transaction don't have any addresses
logger.debug(`${output.scriptPubKey.addresses} <-> ${invoice.receiveAddress}`); logger.debug(`${output.scriptPubKey.addresses} <-> ${invoice.receiveAddress}`);
@@ -112,7 +121,7 @@ export class Provider implements BackendProvider {
async watchConfirmations() { async watchConfirmations() {
setInterval(() => { setInterval(() => {
invoiceManager.getUnconfirmedTransactions().forEach(async invoice => { invoiceManager.getUnconfirmedTransactions().filter(item => { return item.paymentMethod === CryptoUnits.BITCOIN }).forEach(async invoice => {
if (invoice.transcationHash.length === 0) return; if (invoice.transcationHash.length === 0) return;
const transcation = invoice.transcationHash; 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.status === PaymentStatus.DONE || invoice.status === PaymentStatus.CANCELLED) return;
if (invoice.paymentMethod !== CryptoUnits.BITCOIN) 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) { if (err) {
logger.error(`There was an error while getting transcations of address ${invoice.receiveAddress}: ${err.message}`); logger.error(`There was an error while getting transcations of address ${invoice.receiveAddress}: ${err.message}`);
return; return;

View File

@@ -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.
}
}

View File

@@ -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<string> {
return new Promise<string>((resolve, reject) => {
this.rpcClient.request('getnewaddress', [''], async (err, message) => {
if (err) {
reject(err);
return;
}
resolve(message.result);
});
});
}
async getTransaction(txId: string): Promise<ITransaction> {
return new Promise<ITransaction>((resolve, reject) => {
this.rpcClient.request('gettransaction', [txId], (err, message) => {
if (err) {
reject(err);
return;
}
resolve(message.result);
});
});
}
private async decodeRawTransaction(rawTx: string): Promise<IRawTransaction> {
return new Promise<IRawTransaction>((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<string> {
return new Promise<string>((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);
});
});
}
}

View File

@@ -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<string> {
return new Promise<string>((resolve, reject) => {
this.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) => {
this.rpcClient.request('gettransaction', [txId], (err, message) => {
if (err) {
reject(err);
return;
}
resolve(message.result);
});
});
}
private async decodeRawTransaction(rawTx: string): Promise<IRawTransaction> {
return new Promise<IRawTransaction>((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<string> {
return new Promise<string>((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);
});
});
}
}

View File

@@ -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<string> {
return new Promise<string>((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<ITransaction> {
}
}

View File

@@ -28,6 +28,9 @@ export interface IInvoice extends Document {
// 1Kss3e9iPB9vTgWJJZ1SZNkkFKcFJXPz9t // 1Kss3e9iPB9vTgWJJZ1SZNkkFKcFJXPz9t
receiveAddress?: string; receiveAddress?: string;
/** This payment ID is **only available if Monero has been used**. */
paymentId?: string;
// Is set when invoice got paid // Is set when invoice got paid
// 3b38c3a215d4e7981e1516b2dcbf76fca58911274d5d55b3d615274d6e10f2c1 // 3b38c3a215d4e7981e1516b2dcbf76fca58911274d5d55b3d615274d6e10f2c1
transcationHash?: string; transcationHash?: string;

View File

@@ -23,6 +23,7 @@ const schemaInvoice = new Schema({
paymentMethods: [{ type: schemaPaymentMethods, required: true }], paymentMethods: [{ type: schemaPaymentMethods, required: true }],
paymentMethod: { type: String, enum: Object.values(CryptoUnits), required: false }, paymentMethod: { type: String, enum: Object.values(CryptoUnits), required: false },
receiveAddress: { type: String, required: false }, receiveAddress: { type: String, required: false },
paymentId: { type: String, required: false },
transcationHash: { type: String, required: false }, transcationHash: { type: String, required: false },
cart: [{ type: schemaCart, required: false }], cart: [{ type: schemaCart, required: false }],
totalPrice: { type: Number, required: false }, totalPrice: { type: Number, required: false },