Major api changes
- Remove of confirmation listener in providers - Random selector has been capped down to 32 characters - Monero is now semi-supported
This commit is contained in:
@@ -32,10 +32,9 @@ export let providerManager: ProviderManager = undefined;
|
||||
export let logger: winston.Logger;
|
||||
|
||||
async function run() {
|
||||
const { combine, timestamp, label, printf, prettyPrint } = winston.format;
|
||||
const { combine, timestamp, printf, prettyPrint } = winston.format;
|
||||
|
||||
const myFormat = printf(({ level, message, label, timestamp }) => {
|
||||
if (label !== undefined) return `${timestamp} ${level} (${label}) ${message}`;
|
||||
const myFormat = printf(({ level, message, timestamp }) => {
|
||||
return `${timestamp} ${level} ${message}`;
|
||||
});
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export async function createInvoice(req: Request, res: Response) {
|
||||
const dueBy = new Date(Date.now() + 1000 * 60 * 15);
|
||||
|
||||
Invoice.create({
|
||||
selector: randomString(128),
|
||||
selector: randomString(32),
|
||||
paymentMethods,
|
||||
successUrl,
|
||||
cancelUrl,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { invoiceManager, providerManager } from '../app';
|
||||
import { IInvoice } from '../models/invoice/invoice.interface';
|
||||
import { CryptoUnits } from './types';
|
||||
|
||||
@@ -34,9 +35,10 @@ export abstract class BackendProvider {
|
||||
/**
|
||||
* Get a transaction from the blockchain.
|
||||
* @param txId Hash of the transcation you're looking for.
|
||||
* @param context Invoice for context (required to calculate correct amount)
|
||||
* @returns See https://developer.bitcoin.org/reference/rpc/gettransaction.html for reference
|
||||
*/
|
||||
abstract getTransaction(txId: string): Promise<ITransaction>;
|
||||
abstract getTransaction(txId: string, context?: IInvoice): Promise<ITransaction | null>;
|
||||
|
||||
/**
|
||||
* Decode a raw transcation that was broadcasted in the network.
|
||||
@@ -66,11 +68,6 @@ export abstract class BackendProvider {
|
||||
*/
|
||||
abstract listener(): void;
|
||||
|
||||
/**
|
||||
* Keep track of unconfirmed transactions.
|
||||
*/
|
||||
abstract watchConfirmations(): void;
|
||||
|
||||
/**
|
||||
* Provided is an array with pending invoices that have to be check.
|
||||
*
|
||||
@@ -93,21 +90,21 @@ export interface ITransactionDetails {
|
||||
}
|
||||
|
||||
export interface ITransaction {
|
||||
amount: number;
|
||||
fee: number;
|
||||
id: string;
|
||||
blockhash: string;
|
||||
amount: number; // Total transaction amount
|
||||
fee?: number;
|
||||
confirmations: number;
|
||||
time: number; // Unix timestamp
|
||||
details: ITransactionDetails[];
|
||||
hex: string;
|
||||
time: number; // Unix timestamp
|
||||
details?: ITransactionDetails[]; // In-/and Outputs of an transaction
|
||||
}
|
||||
|
||||
// Special interface for RPC call `listreceivedbyaddress`
|
||||
export interface ITransactionList {
|
||||
address: string;
|
||||
amount: number;
|
||||
address: string; // Address that performed that action
|
||||
amount: number; // Amount that got transfered
|
||||
confirmation: number;
|
||||
label: string;
|
||||
txids: string[];
|
||||
txids?: string[];
|
||||
}
|
||||
|
||||
export interface IRawTransaction {
|
||||
|
||||
@@ -21,15 +21,24 @@ export class InvoiceManager {
|
||||
logger.info(`There are ${invoices.length} invoices that are pending or unconfirmed`);
|
||||
|
||||
invoices.forEach(invoice => {
|
||||
if (invoice.status === PaymentStatus.DONE || invoice.status === PaymentStatus.CANCELLED) {
|
||||
this.removeInvoice(invoice);
|
||||
return;
|
||||
}
|
||||
|
||||
if (invoice.status === PaymentStatus.PENDING) { this.pendingInvoices.push(invoice); }
|
||||
if (invoice.status === PaymentStatus.UNCONFIRMED) { this.unconfirmedTranscations.push(invoice); }
|
||||
|
||||
providerManager.getProvider(invoice.paymentMethod).validateInvoice(invoice);
|
||||
});
|
||||
});
|
||||
|
||||
this.expireScheduler();
|
||||
this.watchConfirmations();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is basicly close all invoices that have not been paid in time.
|
||||
* This function will basicly close all invoices that have not been paid in time.
|
||||
*/
|
||||
private expireScheduler() {
|
||||
setInterval(async () => {
|
||||
@@ -144,8 +153,26 @@ export class InvoiceManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This mehtod, once started, will check every n-seconds if the confirmation
|
||||
* count of one unconfirmed transcation has changed.
|
||||
*/
|
||||
async watchConfirmations() {
|
||||
setInterval(() => {
|
||||
this.unconfirmedTranscations.forEach(async invoice => {
|
||||
const transcation = invoice.transcationHash;
|
||||
|
||||
const provider = providerManager.getProvider(invoice.paymentMethod);
|
||||
const tx = await provider.getTransaction(transcation);
|
||||
this.setConfirmationCount(invoice, tx.confirmations);
|
||||
});
|
||||
}, 2_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks if a payment has been made in time and that the right amount was sent.
|
||||
*
|
||||
* **Only issue this method in the moment the payment has been made.**
|
||||
*/
|
||||
async validatePayment(invoice: IInvoice, tx: string): Promise<void> {
|
||||
if (invoice.dueBy.getTime() < Date.now() && invoice.status <= PaymentStatus.PENDING && invoice.status >= PaymentStatus.REQUESTED) {
|
||||
@@ -155,16 +182,13 @@ export class InvoiceManager {
|
||||
return; // Payment is too late
|
||||
}
|
||||
|
||||
const txInfo = await providerManager.getProvider(invoice.paymentMethod).getTransaction(tx);
|
||||
const receivedTranscation = txInfo.details.find(detail => {
|
||||
return (detail.address == invoice.receiveAddress && detail.amount > 0); // We only want receiving transactions
|
||||
});
|
||||
const txInfo = await providerManager.getProvider(invoice.paymentMethod).getTransaction(tx, invoice);
|
||||
|
||||
const price = this.getPriceByInvoice(invoice);
|
||||
if (price === undefined) return;
|
||||
if (price === 0) return;
|
||||
|
||||
// Transaction sent enough funds
|
||||
if (receivedTranscation.amount == price || receivedTranscation.amount > price) {
|
||||
// Sent enough funds
|
||||
if (txInfo.amount == price || txInfo.amount > price) {
|
||||
invoice.transcationHash = tx;
|
||||
await invoice.save();
|
||||
|
||||
@@ -176,40 +200,12 @@ export class InvoiceManager {
|
||||
* know who the original sender was. Therefore if a customer sent not the right amount, he/she
|
||||
* should contact the support of the shop.
|
||||
*/
|
||||
logger.warning(`Transaction (${tx}) did not sent requested funds. (sent: ${receivedTranscation.amount} BTC, requested: ${price} BTC)`);
|
||||
logger.warning(`Transaction (${tx}) did not sent requested funds. (sent: ${txInfo.amount}, requested: ${price})`);
|
||||
invoice.status = PaymentStatus.TOOLITTLE;
|
||||
|
||||
await invoice.save();
|
||||
|
||||
return;
|
||||
|
||||
// This is dead code and only exists because I'm yet unsure what to do with such payments.
|
||||
let sendBack = receivedTranscation.amount;
|
||||
|
||||
// If the amount was too much, mark invoice as paid and try to send remaining funds back.
|
||||
if (receivedTranscation.amount > price) {
|
||||
sendBack = price - txInfo.amount;
|
||||
|
||||
// Sent amount was too much but technically the bill is paid (will get saved in upgradeInvoice)
|
||||
invoice.transcationHash = tx;
|
||||
|
||||
this.upgradeInvoice(invoice);
|
||||
}
|
||||
|
||||
// We only have one input, we can be sure that the sender will receive the funds
|
||||
if (txInfo.details.length === 1) {
|
||||
if (txInfo.details[0].address.length !== 1) return;
|
||||
|
||||
const txBack = await providerManager.getProvider(invoice.paymentMethod).sendToAddress(txInfo.details[0].address[0], receivedTranscation.amount, null, null, true);
|
||||
logger.info(`Sent ${receivedTranscation.amount} ${invoice.paymentMethod} back to ${txInfo.details[0].address[0]}: ${txBack}`);
|
||||
} else {
|
||||
// If we cannot send the funds back, save transaction id and mark invoice as failed.
|
||||
invoice.transcationHash = tx;
|
||||
invoice.status = PaymentStatus.TOOLITTLE;
|
||||
this.removeInvoice(invoice);
|
||||
|
||||
await invoice.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { config } from '../../config';
|
||||
import { invoiceManager, logger } from '../app';
|
||||
import { invoiceManager, logger, providerManager } from '../app';
|
||||
import { BackendProvider } from './backendProvider';
|
||||
import { CryptoUnits } from './types';
|
||||
|
||||
@@ -57,15 +57,16 @@ export class ProviderManager {
|
||||
/**
|
||||
* 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);
|
||||
disable(name: string) {
|
||||
this.cryptoProvider.forEach(provider => {
|
||||
if (provider.NAME === name) {
|
||||
// Disable all coins that are supported by this provider.
|
||||
provider.CRYPTO.forEach(crypto => {
|
||||
this.cryptoProvider.delete(crypto);
|
||||
});
|
||||
logger.warning(`Provider "${provider.NAME}" is now disabled.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,7 @@ 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 { BackendProvider, IRawTransaction, ITransaction, ITransactionDetails, ITransactionList } from '../backendProvider';
|
||||
import { CryptoUnits, PaymentStatus } from '../types';
|
||||
|
||||
export class Provider implements BackendProvider {
|
||||
@@ -29,11 +29,8 @@ export class Provider implements BackendProvider {
|
||||
});
|
||||
|
||||
this.listener();
|
||||
this.watchConfirmations();
|
||||
|
||||
return true;
|
||||
|
||||
//logger.info('The Bitcoin Core backend is now available!');
|
||||
}
|
||||
|
||||
async getNewAddress(): Promise<string> {
|
||||
@@ -49,7 +46,7 @@ export class Provider implements BackendProvider {
|
||||
});
|
||||
}
|
||||
|
||||
async getTransaction(txId: string): Promise<ITransaction> {
|
||||
async getTransaction(txId: string, context?: IInvoice): Promise<ITransaction> {
|
||||
return new Promise<ITransaction>((resolve, reject) => {
|
||||
this.rpcClient.request('gettransaction', [txId], (err, message) => {
|
||||
if (err) {
|
||||
@@ -57,7 +54,28 @@ export class Provider implements BackendProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(message.result);
|
||||
// Calculate received funds
|
||||
const details: ITransactionDetails[] = message.result.details;
|
||||
let amount = 0;
|
||||
|
||||
if (context !== undefined) {
|
||||
for (let i = 0; i < details.length; i++) {
|
||||
if (details[i].category == 'receive' && details[i].address == context.receiveAddress) {
|
||||
amount += details[i].amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ret: ITransaction = {
|
||||
id: message.result.txid,
|
||||
amount,
|
||||
blockhash: message.result.blockhash,
|
||||
confirmations: message.result.confirmations,
|
||||
time: message.result.time,
|
||||
fee: message.result.fee
|
||||
}
|
||||
|
||||
resolve(ret);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -107,7 +125,6 @@ export class Provider implements BackendProvider {
|
||||
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
|
||||
@@ -119,20 +136,7 @@ export class Provider implements BackendProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async watchConfirmations() {
|
||||
setInterval(() => {
|
||||
invoiceManager.getUnconfirmedTransactions().filter(item => { return item.paymentMethod === CryptoUnits.BITCOIN }).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.BITCOIN) return;
|
||||
|
||||
this.rpcClient.request('listreceivedbyaddress', [0, false, false, invoice.receiveAddress], async (err, message) => {
|
||||
|
||||
@@ -29,7 +29,6 @@ export class Provider implements BackendProvider {
|
||||
});
|
||||
|
||||
this.listener();
|
||||
this.watchConfirmations();
|
||||
|
||||
return true;
|
||||
|
||||
@@ -49,7 +48,7 @@ export class Provider implements BackendProvider {
|
||||
});
|
||||
}
|
||||
|
||||
async getTransaction(txId: string): Promise<ITransaction> {
|
||||
async getTransaction(txId: string, context?: IInvoice): Promise<ITransaction> {
|
||||
return new Promise<ITransaction>((resolve, reject) => {
|
||||
this.rpcClient.request('gettransaction', [txId], (err, message) => {
|
||||
if (err) {
|
||||
@@ -57,7 +56,26 @@ export class Provider implements BackendProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(message.result);
|
||||
// Calculate received funds
|
||||
const details: ITransactionDetails[] = message.result.details;
|
||||
let amount = 0;
|
||||
|
||||
details.forEach(detail => {
|
||||
if (detail.category === 'receive' && detail.address === context.receiveAddress) {
|
||||
amount += detail.amount;
|
||||
}
|
||||
})
|
||||
|
||||
const ret: ITransaction = {
|
||||
id: message.result.txid,
|
||||
amount,
|
||||
blockhash: message.result.blockhash,
|
||||
confirmations: message.result.confirmations,
|
||||
time: message.result.time,
|
||||
fee: message.result.fee
|
||||
}
|
||||
|
||||
resolve(ret);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -119,20 +137,7 @@ export class Provider implements BackendProvider {
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { BackendProvider, IRawTransaction, ITransaction, ITransactionDetails, ITransactionList } from '../backendProvider';
|
||||
import { CryptoUnits, PaymentStatus } from '../types';
|
||||
|
||||
export class Provider implements BackendProvider {
|
||||
@@ -29,7 +29,6 @@ export class Provider implements BackendProvider {
|
||||
});
|
||||
|
||||
this.listener();
|
||||
this.watchConfirmations();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -47,7 +46,7 @@ export class Provider implements BackendProvider {
|
||||
});
|
||||
}
|
||||
|
||||
async getTransaction(txId: string): Promise<ITransaction> {
|
||||
async getTransaction(txId: string, context?: IInvoice): Promise<ITransaction> {
|
||||
return new Promise<ITransaction>((resolve, reject) => {
|
||||
this.rpcClient.request('gettransaction', [txId], (err, message) => {
|
||||
if (err) {
|
||||
@@ -55,7 +54,26 @@ export class Provider implements BackendProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(message.result);
|
||||
// Calculate received funds
|
||||
const details: ITransactionDetails[] = message.result.details;
|
||||
let amount = 0;
|
||||
|
||||
details.forEach(detail => {
|
||||
if (detail.category === 'receive' && detail.address === context.receiveAddress) {
|
||||
amount += detail.amount;
|
||||
}
|
||||
})
|
||||
|
||||
const ret: ITransaction = {
|
||||
id: message.result.txid,
|
||||
amount,
|
||||
blockhash: message.result.blockhash,
|
||||
confirmations: message.result.confirmations,
|
||||
time: message.result.time,
|
||||
fee: message.result.fee
|
||||
}
|
||||
|
||||
resolve(ret);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -117,18 +135,6 @@ export class Provider implements BackendProvider {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
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> {
|
||||
|
||||
}
|
||||
}
|
||||
217
src/helper/providers/moneroCLI.ts
Normal file
217
src/helper/providers/moneroCLI.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import * as rpc from 'jayson';
|
||||
import { Subscriber } from 'zeromq';
|
||||
|
||||
import { invoiceManager, logger } from '../../app';
|
||||
import { IInvoice } from '../../models/invoice/invoice.interface';
|
||||
import { Invoice } from '../../models/invoice/invoice.model';
|
||||
import { BackendProvider, ITransaction } from '../backendProvider';
|
||||
import { CryptoUnits } from '../types';
|
||||
|
||||
export class Provider implements BackendProvider {
|
||||
|
||||
private sock: Subscriber;
|
||||
private rpcClient: rpc.HttpClient;
|
||||
|
||||
NAME = 'Monero RPC Wallet';
|
||||
DESCRIPTION = 'This provider queries the Monero daemon running on your computer';
|
||||
AUTHOR = 'LibrePay Team';
|
||||
VERSION = '0.1';
|
||||
CRYPTO = [CryptoUnits.MONERO];
|
||||
|
||||
onEnable() {
|
||||
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_ZMQ_ADDRESS === undefined) {
|
||||
logger.error(`Enviroment variable MONERO_ZMQ_ADDRESS is required but not set!`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.rpcClient = rpc.Client.http({
|
||||
path: '/json_rpc',
|
||||
port: 38085
|
||||
});
|
||||
this.rpcClient.request('open_wallet', {
|
||||
filename: process.env.MONERO_WALLET_NAME,
|
||||
password: process.env.MONERO_WALLET_PASSWORD
|
||||
}, (err, message) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
logger.error(`Failed to open Monero wallet: ${err}\nMaybe a wrong password or path?`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
this.listener();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async listener() {
|
||||
// Since we can't really use the ZeroMQ interface, we have to query every n-seconds.
|
||||
// Technically there is a ZeroMQ interface but there is almost no to zero documentation for it.
|
||||
setInterval(() => {
|
||||
invoiceManager.getPendingInvoices().forEach(async invoice => {
|
||||
if (invoice.paymentMethod !== CryptoUnits.MONERO) return;
|
||||
|
||||
const tx = await this.getPaymentById(((await this.splitAddress(invoice.receiveAddress)).paymentId));
|
||||
if (tx === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Transcation for invoice ${invoice.id} received!`);
|
||||
invoiceManager.validatePayment(invoice, tx.id);
|
||||
});
|
||||
}, 5_000);
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns If a payment has not been made yet, `null` will be returned.
|
||||
*/
|
||||
async getTransaction(txid: string, context?: IInvoice): Promise<ITransaction | null> {
|
||||
return new Promise<ITransaction>(async (resolve, reject) => {
|
||||
// We're still missing the confirmation count, since we don't get it with this function.
|
||||
this.rpcClient.request('get_transfer_by_txid', { txid }, async (err, message) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentTransaction = message.result.transfer;
|
||||
if (paymentTransaction === undefined) {
|
||||
console.log(message)
|
||||
logger.warning(`Tried to get transfer by txid but failed: ${message}`);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Renaming properties to make them fit into interface.
|
||||
const ret: ITransaction = {
|
||||
id: paymentTransaction.txid,
|
||||
blockhash: paymentTransaction.txid,
|
||||
amount: this.decimalToFloat(paymentTransaction.amount),
|
||||
confirmations: paymentTransaction.confirmations,
|
||||
time: paymentTransaction.timestamp,
|
||||
fee: paymentTransaction.fee
|
||||
};
|
||||
resolve(ret);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sendToAddress(
|
||||
recipient: string,
|
||||
amount: number,
|
||||
comment?: string,
|
||||
commentTo?: string,
|
||||
subtractFeeFromAmount?: boolean): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const account_index = Number(process.env.MONERO_WALLET_ACCOUNT_INDEX) || 0;
|
||||
this.rpcClient.request('transfer', { destinations: [{ amount, address: recipient }] }, async (err, message) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`[Monero] Transaction has been made: ${message.result.tx_hash}`);
|
||||
resolve(message.result.tx_hash);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async validateInvoice(invoice: IInvoice) {
|
||||
if (invoice.paymentMethod !== CryptoUnits.MONERO) return;
|
||||
|
||||
const split = await this.splitAddress(invoice);
|
||||
if (split === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transaction = await this.getPaymentById(split.paymentId);
|
||||
|
||||
if (transaction === null) {
|
||||
return; // Transaction has not been yet made.
|
||||
}
|
||||
|
||||
invoiceManager.validatePayment(invoice, transaction.blockhash);
|
||||
/*this.rpcClient.request('get_payments', { payment_id }, async (err, message) => {
|
||||
if (err) {
|
||||
logger.error(`[Monero] There was an error while gettings payments of ${payment_id}: ${err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const payment = message.result.payments[0];
|
||||
invoiceManager.validatePayment(invoice, payment.tx_hash);
|
||||
});*/
|
||||
}
|
||||
|
||||
private getPaymentById(payment_id: string): Promise<ITransaction> {
|
||||
return new Promise(resolve => {
|
||||
this.rpcClient.request('get_payments', { payment_id: payment_id }, async (err, message) => {
|
||||
if (err) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(payment_id, message);
|
||||
|
||||
// The payment has not been made yet
|
||||
if (message.result.payments === undefined) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(await this.getTransaction(message.result.payments[0].tx_hash));
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will take the full receive address and will return the payment id and orignal address.
|
||||
* @returns Will return `null` if input was invalid.
|
||||
*/
|
||||
private splitAddress(context: IInvoice | string): Promise<{ address: string, paymentId: string } | null> {
|
||||
return new Promise(resolve => {
|
||||
const address = typeof(context) === 'string' ? context : context.receiveAddress;
|
||||
this.rpcClient.request('split_integrated_address', { integrated_address: address }, async (err, message) => {
|
||||
if (err) {
|
||||
logger.error(`[Monero] There was an error while splitting the address ${address}: ${err}`);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
resolve({ paymentId: message.result.payment_id, address: message.result.standard_address });
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* When querying the Monero RPC wallet we get full decimals back instead of floats. Maybe because
|
||||
* floats can be a hussle sometimes. Anyway, we have to convert them back into the original format.
|
||||
*/
|
||||
private decimalToFloat(int: number) {
|
||||
return int / 1000000000000;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ const schemaPaymentMethods = new Schema({
|
||||
}, { _id: false });
|
||||
|
||||
const schemaInvoice = new Schema({
|
||||
selector: { type: String, length: 128, required: true },
|
||||
selector: { type: String, length: 32, required: true },
|
||||
paymentMethods: [{ type: schemaPaymentMethods, required: true }],
|
||||
paymentMethod: { type: String, enum: Object.values(CryptoUnits), required: false },
|
||||
receiveAddress: { type: String, required: false },
|
||||
|
||||
Reference in New Issue
Block a user