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;
|
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}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 });
|
}, { _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 },
|
||||||
|
|||||||
Reference in New Issue
Block a user