Support for Litecoin and Dogecoin added
This commit is contained in:
45
package-lock.json
generated
45
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
@@ -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';
|
||||
|
||||
@@ -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<IRawTransaction>;
|
||||
//abstract decodeRawTransaction(rawTx: string): Promise<IRawTransaction>;
|
||||
|
||||
/**
|
||||
* Send funds to a specific address.
|
||||
|
||||
@@ -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<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;
|
||||
await invoice.save();
|
||||
|
||||
|
||||
@@ -33,19 +33,39 @@ export class ProviderManager {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<string> {
|
||||
return new Promise<string>((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<ITransaction> {
|
||||
return new Promise<ITransaction>((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<IRawTransaction> {
|
||||
private async decodeRawTransaction(rawTx: string): Promise<IRawTransaction> {
|
||||
return new Promise<IRawTransaction>((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<string> {
|
||||
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) {
|
||||
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;
|
||||
|
||||
37
src/helper/providers/blockio.js
Normal file
37
src/helper/providers/blockio.js
Normal 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.
|
||||
}
|
||||
|
||||
}
|
||||
156
src/helper/providers/dogecoinCore.ts
Normal file
156
src/helper/providers/dogecoinCore.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
151
src/helper/providers/litecoinCore.ts
Normal file
151
src/helper/providers/litecoinCore.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
85
src/helper/providers/moneroCLI.js
Normal file
85
src/helper/providers/moneroCLI.js
Normal 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> {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user