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:
2021-01-24 20:00:08 +01:00
parent 881b350252
commit 868a408a9d
11 changed files with 344 additions and 204 deletions

View File

@@ -32,10 +32,9 @@ export let providerManager: ProviderManager = undefined;
export let logger: winston.Logger; export let logger: winston.Logger;
async function run() { 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 }) => { const myFormat = printf(({ level, message, timestamp }) => {
if (label !== undefined) return `${timestamp} ${level} (${label}) ${message}`;
return `${timestamp} ${level} ${message}`; return `${timestamp} ${level} ${message}`;
}); });

View File

@@ -90,7 +90,7 @@ export async function createInvoice(req: Request, res: Response) {
const dueBy = new Date(Date.now() + 1000 * 60 * 15); const dueBy = new Date(Date.now() + 1000 * 60 * 15);
Invoice.create({ Invoice.create({
selector: randomString(128), selector: randomString(32),
paymentMethods, paymentMethods,
successUrl, successUrl,
cancelUrl, cancelUrl,

View File

@@ -1,3 +1,4 @@
import { invoiceManager, providerManager } from '../app';
import { IInvoice } from '../models/invoice/invoice.interface'; import { IInvoice } from '../models/invoice/invoice.interface';
import { CryptoUnits } from './types'; import { CryptoUnits } from './types';
@@ -34,9 +35,10 @@ export abstract class BackendProvider {
/** /**
* Get a transaction from the blockchain. * Get a transaction from the blockchain.
* @param txId Hash of the transcation you're looking for. * @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 * @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. * Decode a raw transcation that was broadcasted in the network.
@@ -66,11 +68,6 @@ export abstract class BackendProvider {
*/ */
abstract listener(): void; abstract listener(): void;
/**
* Keep track of unconfirmed transactions.
*/
abstract watchConfirmations(): void;
/** /**
* Provided is an array with pending invoices that have to be check. * Provided is an array with pending invoices that have to be check.
* *
@@ -93,21 +90,21 @@ export interface ITransactionDetails {
} }
export interface ITransaction { export interface ITransaction {
amount: number; id: string;
fee: number; blockhash: string;
amount: number; // Total transaction amount
fee?: number;
confirmations: number; confirmations: number;
time: number; // Unix timestamp time: number; // Unix timestamp
details: ITransactionDetails[]; details?: ITransactionDetails[]; // In-/and Outputs of an transaction
hex: string;
} }
// Special interface for RPC call `listreceivedbyaddress` // Special interface for RPC call `listreceivedbyaddress`
export interface ITransactionList { export interface ITransactionList {
address: string; address: string; // Address that performed that action
amount: number; amount: number; // Amount that got transfered
confirmation: number; confirmation: number;
label: string; txids?: string[];
txids: string[];
} }
export interface IRawTransaction { export interface IRawTransaction {

View File

@@ -21,15 +21,24 @@ export class InvoiceManager {
logger.info(`There are ${invoices.length} invoices that are pending or unconfirmed`); logger.info(`There are ${invoices.length} invoices that are pending or unconfirmed`);
invoices.forEach(invoice => { 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); providerManager.getProvider(invoice.paymentMethod).validateInvoice(invoice);
}); });
}); });
this.expireScheduler(); 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() { private expireScheduler() {
setInterval(async () => { 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. * 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> { async validatePayment(invoice: IInvoice, tx: string): Promise<void> {
if (invoice.dueBy.getTime() < Date.now() && invoice.status <= PaymentStatus.PENDING && invoice.status >= PaymentStatus.REQUESTED) { 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 return; // Payment is too late
} }
const txInfo = await providerManager.getProvider(invoice.paymentMethod).getTransaction(tx); const txInfo = await providerManager.getProvider(invoice.paymentMethod).getTransaction(tx, invoice);
const receivedTranscation = txInfo.details.find(detail => {
return (detail.address == invoice.receiveAddress && detail.amount > 0); // We only want receiving transactions
});
const price = this.getPriceByInvoice(invoice); const price = this.getPriceByInvoice(invoice);
if (price === undefined) return; if (price === 0) return;
// Transaction sent enough funds // Sent enough funds
if (receivedTranscation.amount == price || receivedTranscation.amount > price) { if (txInfo.amount == price || txInfo.amount > price) {
invoice.transcationHash = tx; invoice.transcationHash = tx;
await invoice.save(); 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 * know who the original sender was. Therefore if a customer sent not the right amount, he/she
* should contact the support of the shop. * 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; invoice.status = PaymentStatus.TOOLITTLE;
await invoice.save(); await invoice.save();
return; 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();
}
} }
} }
} }

View File

@@ -1,7 +1,7 @@
import { readdirSync } from 'fs'; import { readdirSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { config } from '../../config'; import { config } from '../../config';
import { invoiceManager, logger } from '../app'; import { invoiceManager, logger, providerManager } from '../app';
import { BackendProvider } from './backendProvider'; import { BackendProvider } from './backendProvider';
import { CryptoUnits } from './types'; import { CryptoUnits } from './types';
@@ -57,15 +57,16 @@ export class ProviderManager {
/** /**
* This provider will be no longer be used. * This provider will be no longer be used.
*/ */
disable(providerFor: CryptoUnits) { disable(name: string) {
if (!this.cryptoProvider.has(providerFor)) { this.cryptoProvider.forEach(provider => {
return; 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.`);
} }
});
const provider = this.getProvider(providerFor);
logger.warn(`Provider "${provider.NAME}" will be disabled ...`);
this.cryptoProvider.delete(providerFor);
} }
} }

View File

@@ -3,7 +3,7 @@ import { Subscriber } from 'zeromq';
import * as rpc from 'jayson'; import * as rpc from 'jayson';
import { invoiceManager, logger } from '../../app'; 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, ITransactionDetails, ITransactionList } from '../backendProvider';
import { CryptoUnits, PaymentStatus } from '../types'; import { CryptoUnits, PaymentStatus } from '../types';
export class Provider implements BackendProvider { export class Provider implements BackendProvider {
@@ -29,11 +29,8 @@ export class Provider implements BackendProvider {
}); });
this.listener(); this.listener();
this.watchConfirmations();
return true; return true;
//logger.info('The Bitcoin Core backend is now available!');
} }
async getNewAddress(): Promise<string> { 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) => { return new Promise<ITransaction>((resolve, reject) => {
this.rpcClient.request('gettransaction', [txId], (err, message) => { this.rpcClient.request('gettransaction', [txId], (err, message) => {
if (err) { if (err) {
@@ -57,7 +54,28 @@ export class Provider implements BackendProvider {
return; 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}`); logger.debug(`${output.scriptPubKey.addresses} <-> ${invoice.receiveAddress}`);
// We found our transaction (https://developer.bitcoin.org/reference/rpc/decoderawtransaction.html) // We found our transaction (https://developer.bitcoin.org/reference/rpc/decoderawtransaction.html)
if (output.scriptPubKey.addresses.indexOf(invoice.receiveAddress) !== -1) { 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})`); logger.info(`Transcation for invoice ${invoice.id} received! (${tx.hash})`);
// Change state in database // 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) { async validateInvoice(invoice: IInvoice) {
if (invoice.status === PaymentStatus.DONE || invoice.status === PaymentStatus.CANCELLED) return;
if (invoice.paymentMethod !== CryptoUnits.BITCOIN) return; if (invoice.paymentMethod !== CryptoUnits.BITCOIN) return;
this.rpcClient.request('listreceivedbyaddress', [0, false, false, invoice.receiveAddress], async (err, message) => { this.rpcClient.request('listreceivedbyaddress', [0, false, false, invoice.receiveAddress], async (err, message) => {

View File

@@ -29,7 +29,6 @@ export class Provider implements BackendProvider {
}); });
this.listener(); this.listener();
this.watchConfirmations();
return true; 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) => { return new Promise<ITransaction>((resolve, reject) => {
this.rpcClient.request('gettransaction', [txId], (err, message) => { this.rpcClient.request('gettransaction', [txId], (err, message) => {
if (err) { if (err) {
@@ -57,7 +56,26 @@ export class Provider implements BackendProvider {
return; 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) { async validateInvoice(invoice: IInvoice) {
if (invoice.status === PaymentStatus.DONE || invoice.status === PaymentStatus.CANCELLED) return;
if (invoice.paymentMethod !== CryptoUnits.DOGECOIN) return; if (invoice.paymentMethod !== CryptoUnits.DOGECOIN) return;
this.rpcClient.request('listreceivedbyaddress', [0, false, false], async (err, message) => { this.rpcClient.request('listreceivedbyaddress', [0, false, false], async (err, message) => {

View File

@@ -3,7 +3,7 @@ import { Subscriber } from 'zeromq';
import * as rpc from 'jayson'; import * as rpc from 'jayson';
import { invoiceManager, logger } from '../../app'; 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, ITransactionDetails, ITransactionList } from '../backendProvider';
import { CryptoUnits, PaymentStatus } from '../types'; import { CryptoUnits, PaymentStatus } from '../types';
export class Provider implements BackendProvider { export class Provider implements BackendProvider {
@@ -29,7 +29,6 @@ export class Provider implements BackendProvider {
}); });
this.listener(); this.listener();
this.watchConfirmations();
return true; 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) => { return new Promise<ITransaction>((resolve, reject) => {
this.rpcClient.request('gettransaction', [txId], (err, message) => { this.rpcClient.request('gettransaction', [txId], (err, message) => {
if (err) { if (err) {
@@ -55,7 +54,26 @@ export class Provider implements BackendProvider {
return; 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) { async validateInvoice(invoice: IInvoice) {
if (invoice.status === PaymentStatus.DONE || invoice.status === PaymentStatus.CANCELLED) return; if (invoice.status === PaymentStatus.DONE || invoice.status === PaymentStatus.CANCELLED) return;
if (invoice.paymentMethod !== CryptoUnits.LITECOIN) return; if (invoice.paymentMethod !== CryptoUnits.LITECOIN) return;

View File

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

View 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;
}
}

View File

@@ -19,7 +19,7 @@ const schemaPaymentMethods = new Schema({
}, { _id: false }); }, { _id: false });
const schemaInvoice = new Schema({ const schemaInvoice = new Schema({
selector: { type: String, length: 128, required: true }, selector: { type: String, length: 32, required: true },
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 },