Merge remote-tracking branch 'origin/release-9.0' into chore/merge-9-into-10-20240206

This commit is contained in:
Rafael Taranto 2024-02-06 08:51:09 +00:00
commit 35e40f4528
52 changed files with 4794 additions and 6007 deletions

View file

@ -30,22 +30,22 @@ const BINARIES = {
BTC: {
defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz',
defaultDir: 'bitcoin-0.20.1/bin',
url: 'https://bitcoincore.org/bin/bitcoin-core-25.0/bitcoin-25.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-25.0/bin'
url: 'https://bitcoincore.org/bin/bitcoin-core-26.0/bitcoin-26.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-26.0/bin'
},
ETH: {
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.13.1-3f40e65c.tar.gz',
dir: 'geth-linux-amd64-1.13.1-3f40e65c'
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.13.5-916d6a44.tar.gz',
dir: 'geth-linux-amd64-1.13.5-916d6a44'
},
ZEC: {
url: 'https://z.cash/downloads/zcash-5.6.1-linux64-debian-bullseye.tar.gz',
dir: 'zcash-5.6.1/bin'
url: 'https://download.z.cash/downloads/zcash-5.7.0-linux64-debian-bullseye.tar.gz',
dir: 'zcash-5.7.0/bin'
},
DASH: {
defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz',
defaultDir: 'dashcore-18.1.0/bin',
url: 'https://github.com/dashpay/dash/releases/download/v19.3.0/dashcore-19.3.0-x86_64-linux-gnu.tar.gz',
dir: 'dashcore-19.3.0/bin'
url: 'https://github.com/dashpay/dash/releases/download/v20.0.2/dashcore-20.0.2-x86_64-linux-gnu.tar.gz',
dir: 'dashcore-20.0.2/bin'
},
LTC: {
defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
@ -54,13 +54,13 @@ const BINARIES = {
dir: 'litecoin-0.21.2.2/bin'
},
BCH: {
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v26.1.0/bitcoin-cash-node-26.1.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-cash-node-26.1.0/bin',
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v27.0.0/bitcoin-cash-node-27.0.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-cash-node-27.0.0/bin',
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
},
XMR: {
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.2.2.tar.bz2',
dir: 'monero-x86_64-linux-gnu-v0.18.2.2',
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.1.tar.bz2',
dir: 'monero-x86_64-linux-gnu-v0.18.3.1',
files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']]
}
}

View file

@ -0,0 +1,8 @@
const axios = require("axios");
const getSatBEstimateFee = () => {
return axios.get('https://mempool.space/api/v1/fees/recommended')
.then(r => r.data.hourFee)
}
module.exports = { getSatBEstimateFee }

View file

@ -26,4 +26,9 @@ const hasPhone = hasRequirement('sms')
const hasFacephoto = hasRequirement('facephoto')
const hasIdScan = hasRequirement('idCardData')
module.exports = { getBackwardsCompatibleTriggers, hasSanctions, maxDaysThreshold, getCashLimit, hasPhone, hasFacephoto, hasIdScan }
const AUTH_METHODS = {
SMS: 'SMS',
EMAIL: 'EMAIL'
}
module.exports = { getBackwardsCompatibleTriggers, hasSanctions, maxDaysThreshold, getCashLimit, hasPhone, hasFacephoto, hasIdScan, AUTH_METHODS }

View file

@ -6,13 +6,10 @@ const makeDir = require('make-dir')
const path = require('path')
const fs = require('fs')
const util = require('util')
const { sub, differenceInHours } = require('date-fns/fp')
const db = require('./db')
const BN = require('./bn')
const anonymous = require('../lib/constants').anonymousCustomer
const complianceOverrides = require('./compliance_overrides')
const users = require('./users')
const writeFile = util.promisify(fs.writeFile)
const notifierQueries = require('./notifier/queries')
const notifierUtils = require('./notifier/utils')
@ -43,6 +40,12 @@ function add (customer) {
.then(camelize)
}
function addWithEmail (customer) {
const sql = 'insert into customers (id, email, email_at) values ($1, $2, now()) returning *'
return db.one(sql, [uuid.v4(), customer.email])
.then(camelize)
}
/**
* Get single customer by phone
* Phone numbers are unique per customer
@ -60,6 +63,12 @@ function get (phone) {
.then(camelize)
}
function getWithEmail (email) {
const sql = 'select * from customers where email=$1'
return db.oneOrNone(sql, [email])
.then(camelize)
}
/**
* Update customer record
*
@ -308,7 +317,7 @@ const updateSubscriberData = (customerId, data, userToken) => {
*
* Used for the machine.
*/
function getById (id, userToken) {
function getById (id) {
const sql = 'select * from customers where id=$1'
return db.oneOrNone(sql, [id])
.then(assignCustomerData)
@ -349,6 +358,7 @@ function camelizeDeep (customer) {
function getComplianceTypes () {
return [
'sms',
'email',
'id_card_data',
'id_card_photo',
'front_camera',
@ -478,11 +488,11 @@ function batch () {
* @returns {array} Array of customers with it's transactions aggregations
*/
function getCustomersList (phone = null, name = null, address = null, id = null) {
function getCustomersList (phone = null, name = null, address = null, id = null, email = null) {
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
phone, email, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, GREATEST(created, last_transaction, last_data_provided) AS last_active, fiat AS last_tx_fiat,
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, notes, is_test_customer
@ -491,9 +501,9 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
greatest(0, date_part('day', c.suspended_until - NOW())) AS days_suspended,
c.suspended_until > NOW() AS is_suspended,
c.front_camera_path, c.front_camera_override,
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
c.phone, c.email, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
GREATEST(c.phone_at, c.id_card_data_at, c.front_camera_at, c.id_card_photo_at, c.us_ssn_at) AS last_data_provided,
GREATEST(c.phone_at, c.email_at, c.id_card_data_at, c.front_camera_at, c.id_card_photo_at, c.us_ssn_at) AS last_data_provided,
c.sanctions_at, c.sanctions_override, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes,
row_number() OVER (partition by c.id order by t.created desc) AS rn,
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs,
@ -519,8 +529,9 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
AND ($5 IS NULL OR CONCAT(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') = $5 OR id_card_data::json->>'firstName' = $5 OR id_card_data::json->>'lastName' = $5)
AND ($6 IS NULL OR id_card_data::json->>'address' = $6)
AND ($7 IS NULL OR id_card_data::json->>'documentNumber' = $7)
AND ($8 IS NULL OR email = $8)
limit $3`
return db.any(sql, [ passableErrorCodes, anonymous.uuid, NUM_RESULTS, phone, name, address, id ])
return db.any(sql, [ passableErrorCodes, anonymous.uuid, NUM_RESULTS, phone, name, address, id, email ])
.then(customers => Promise.all(_.map(customer =>
getCustomInfoRequestsData(customer)
.then(camelizeDeep), customers)
@ -540,7 +551,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
function getCustomerById (id) {
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override,
phone, phone_at, phone_override, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration,
phone, phone_at, email, email_at, phone_override, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_at, id_card_photo_override, us_ssn_at, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, LEAST(created, last_transaction) AS last_active, fiat AS last_tx_fiat,
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, subscriber_info_at, custom_fields, notes, is_test_customer
@ -549,7 +560,7 @@ function getCustomerById (id) {
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
c.suspended_until > now() AS is_suspended,
c.front_camera_path, c.front_camera_override, c.front_camera_at,
c.phone, c.phone_at, c.phone_override, c.sms_override, c.id_card_data, c.id_card_data_at, c.id_card_data_override, c.id_card_data_expiration,
c.phone, c.phone_at, c.email, c.email_at, c.phone_override, c.sms_override, c.id_card_data, c.id_card_data_at, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_at, c.id_card_photo_override, c.us_ssn, c.us_ssn_at, c.us_ssn_override, c.sanctions,
c.sanctions_at, c.sanctions_override, c.subscriber_info, c.subscriber_info_at, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes,
row_number() OVER (PARTITION BY c.id ORDER BY t.created DESC) AS rn,
@ -912,7 +923,9 @@ function disableTestCustomer (customerId) {
module.exports = {
add,
addWithEmail,
get,
getWithEmail,
batch,
getCustomersList,
getCustomerById,
@ -930,7 +943,5 @@ module.exports = {
updateEditedPhoto,
updateTxCustomerPhoto,
enableTestCustomer,
disableTestCustomer,
selectLatestData,
getEditedData
disableTestCustomer
}

View file

@ -3,7 +3,7 @@ const ph = require('./plugin-helper')
function sendMessage (settings, rec) {
return Promise.resolve()
.then(() => {
const pluginCode = 'mailgun'
const pluginCode = settings.config.notifications_thirdParty_email
const plugin = ph.load(ph.EMAIL, pluginCode)
const account = settings.accounts[pluginCode]
@ -11,4 +11,15 @@ function sendMessage (settings, rec) {
})
}
module.exports = {sendMessage}
function sendCustomerMessage (settings, rec) {
return Promise.resolve()
.then(() => {
const pluginCode = settings.config.notifications_thirdParty_email
const plugin = ph.load(ph.EMAIL, pluginCode)
const account = settings.accounts[pluginCode]
return plugin.sendMessage(account, rec)
})
}
module.exports = {sendMessage, sendCustomerMessage}

View file

@ -117,6 +117,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
configManager.getReceipt(settings.config),
!!configManager.getCashOut(deviceId, settings.config).active,
getMachine(deviceId, currentConfigVersion),
configManager.getCustomerAuthenticationMethod(settings.config)
])
.then(([
enablePaperWalletOnly,
@ -128,6 +129,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
receiptInfo,
twoWayMode,
{ numberOfCassettes, numberOfRecyclers },
customerAuthentication,
]) =>
(currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ?
null :
@ -144,6 +146,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
},
machineInfo: { deviceId, deviceName, numberOfCassettes, numberOfRecyclers },
twoWayMode,
customerAuthentication,
speedtestFiles,
urlsToPing,
}),

View file

@ -125,6 +125,11 @@ type Terms {
details: TermsDetails
}
enum CustomerAuthentication {
EMAIL
SMS
}
type StaticConfig {
configVersion: Int!
@ -134,6 +139,7 @@ type StaticConfig {
serverVersion: String!
timezone: Int!
twoWayMode: Boolean!
customerAuthentication: CustomerAuthentication!
localeInfo: LocaleInfo!
operatorInfo: OperatorInfo

View file

@ -53,6 +53,7 @@ const ALL_ACCOUNTS = [
{ code: 'telnyx', display: 'Telnyx', class: SMS },
{ code: 'vonage', display: 'Vonage', class: SMS },
{ code: 'mailgun', display: 'Mailgun', class: EMAIL },
{ code: 'mock-email', display: 'Mock Email', class: EMAIL, dev: true },
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },
{ code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: ALL_CRYPTOS, dev: true },

View file

@ -17,7 +17,7 @@ function massageCurrencies (currencies) {
const codeToRec = code => _.find(_.matchesProperty('code', code), mapped)
const top5 = _.map(codeToRec, top5Codes)
const raw = _.uniqBy(_.get('code'), _.concat(top5, mapped))
return raw.filter(r => r.code !== '' && r.code[0] !== 'X' && r.display.indexOf('(') === -1)
return raw.filter(r => r.code !== '' && r.display.indexOf('(') === -1)
}
const mapLanguage = lang => {

View file

@ -31,6 +31,7 @@ function transaction () {
function customer () {
const sql = `SELECT DISTINCT * FROM (
SELECT 'phone' AS type, phone AS value FROM customers WHERE phone IS NOT NULL UNION
SELECT 'email' AS type, email AS value FROM customers WHERE email IS NOT NULL UNION
SELECT 'name' AS type, id_card_data::json->>'firstName' AS value FROM customers WHERE id_card_data::json->>'firstName' IS NOT NULL AND id_card_data::json->>'lastName' IS NULL UNION
SELECT 'name' AS type, id_card_data::json->>'lastName' AS value FROM customers WHERE id_card_data::json->>'firstName' IS NULL AND id_card_data::json->>'lastName' IS NOT NULL UNION
SELECT 'name' AS type, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') AS value FROM customers WHERE id_card_data::json->>'firstName' IS NOT NULL AND id_card_data::json->>'lastName' IS NOT NULL UNION

View file

@ -10,7 +10,7 @@ const resolvers = {
isAnonymous: parent => (parent.customerId === anonymous.uuid)
},
Query: {
customers: (...[, { phone, name, address, id }]) => customers.getCustomersList(phone, name, address, id),
customers: (...[, { phone, email, name, address, id }]) => customers.getCustomersList(phone, name, address, id, email),
customer: (...[, { customerId }]) => customers.getCustomerById(customerId),
customerFilters: () => filters.customer()
},

View file

@ -12,6 +12,7 @@ const typeDef = gql`
frontCameraAt: Date
frontCameraOverride: String
phone: String
email: String
isAnonymous: Boolean
smsOverride: String
idCardData: JSONObject
@ -92,7 +93,7 @@ const typeDef = gql`
}
type Query {
customers(phone: String, name: String, address: String, id: String): [Customer] @auth
customers(phone: String, name: String, email: String, address: String, id: String): [Customer] @auth
customer(customerId: ID!): Customer @auth
customerFilters: [Filter] @auth
}

View file

@ -33,6 +33,7 @@ const typeDef = gql`
rawTickerPrice: String
isPaperWallet: Boolean
customerPhone: String
customerEmail: String
customerIdCardDataNumber: String
customerIdCardDataExpiration: Date
customerIdCardData: JSONObject

View file

@ -54,6 +54,7 @@ function batch (
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
c.phone AS customer_phone,
c.email AS customer_email,
c.id_card_data_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data,
@ -86,6 +87,7 @@ function batch (
txs.*,
actions.tx_hash,
c.phone AS customer_phone,
c.email AS customer_email,
c.id_card_data_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data,
@ -159,7 +161,7 @@ function advancedBatch (data) {
'denominationRecycler1', 'denominationRecycler2', 'denominationRecycler3', 'denominationRecycler4', 'denominationRecycler5', 'denominationRecycler6',
'errorCode', 'customerId', 'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms',
'discount', 'txHash', 'customerPhone', 'customerIdCardDataNumber',
'discount', 'txHash', 'customerPhone', 'customerEmail', 'customerIdCardDataNumber',
'customerIdCardDataExpiration', 'customerIdCardData', 'customerName', 'sendTime',
'customerFrontCameraPath', 'customerIdCardPhotoPath', 'expired', 'machineName', 'walletScore']
@ -174,8 +176,8 @@ function advancedBatch (data) {
}
function simplifiedBatch (data) {
const fields = ['txClass', 'id', 'created', 'machineName',
'cryptoCode', 'cryptoAtoms', 'fiat', 'fiatCode', 'phone', 'toAddress',
const fields = ['txClass', 'id', 'created', 'machineName', 'fee',
'cryptoCode', 'cryptoAtoms', 'fiat', 'fiatCode', 'phone', 'email', 'toAddress',
'txHash', 'dispense', 'error', 'status', 'fiatProfit', 'cryptoAmount']
const addSimplifiedFields = _.map(it => ({
@ -236,6 +238,7 @@ function getCustomerTransactionsBatch (ids) {
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
c.phone AS customer_phone,
c.email AS customer_email,
c.id_card_data_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data,
@ -254,6 +257,7 @@ function getCustomerTransactionsBatch (ids) {
txs.*,
actions.tx_hash,
c.phone AS customer_phone,
c.email AS customer_email,
c.id_card_data_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data,
@ -282,6 +286,7 @@ function single (txId) {
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
c.phone AS customer_phone,
c.email AS customer_email,
c.id_card_data_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data,
@ -299,6 +304,7 @@ function single (txId) {
txs.*,
actions.tx_hash,
c.phone AS customer_phone,
c.email AS customer_email,
c.id_card_data_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data,

View file

@ -1,3 +1,5 @@
const {AUTH_METHODS} = require('./compliance-triggers')
const _ = require('lodash/fp')
const { validate } = require('uuid')
@ -120,6 +122,10 @@ const getGlobalNotifications = config => getNotifications(null, null, config)
const getTriggers = _.get('triggers')
function getCustomerAuthenticationMethod(config) {
return _.get('triggersConfig_customerAuthentication')(config)
}
/* `customInfoRequests` is the result of a call to `getCustomInfoRequests` */
const getTriggersAutomation = (customInfoRequests, config, oldFormat = false) => {
return customInfoRequests
@ -193,4 +199,5 @@ module.exports = {
getCryptosFromWalletNamespace,
getCryptoUnits,
setTermsConditions,
getCustomerAuthenticationMethod,
}

View file

@ -26,7 +26,8 @@ const SECRET_FIELDS = [
'twilio.authToken',
'telnyx.apiKey',
'vonage.apiSecret',
'galoy.walletId'
'galoy.walletId',
'galoy.apiSecret'
]
/*

View file

@ -203,7 +203,7 @@ function plugins (settings, deviceId) {
it => cashOutConfig[`recycler${it}`],
_.range(1, numberOfRecyclers+1)
)
if (counts.length !== denominations.length)
throw new Error('Denominations and respective counts do not match!')
@ -771,7 +771,7 @@ function plugins (settings, deviceId) {
fiatCode
}
: null
const recycler1Alert = device.numberOfRecyclers >= 1 && isUnitLow(device.cashUnits.recycler1, getCashUnitCapacity(device.model, 'recycler'), notifications.fillingPercentageRecycler1)
? {
code: 'LOW_RECYCLER_STACKER',
@ -917,7 +917,7 @@ function plugins (settings, deviceId) {
}
function getPhoneCode (phone) {
const code = settings.config.notifications_thirdParty_sms === 'mock-sms'
const code = settings.config.notifications_thirdParty_sms === 'mock-sms'
? '123'
: randomCode()
@ -933,6 +933,23 @@ function plugins (settings, deviceId) {
})
}
function getEmailCode (toEmail) {
const code = settings.config.notifications_thirdParty_email === 'mock-email'
? '123'
: randomCode()
const rec = {
email: {
toEmail,
subject: 'Your cryptomat code',
body: `Your cryptomat code: ${code}`
}
}
return email.sendCustomerMessage(settings, rec)
.then(() => code)
}
function sweepHdRow (row) {
const txId = row.id
const cryptoCode = row.crypto_code
@ -999,6 +1016,10 @@ function plugins (settings, deviceId) {
return walletScoring.isWalletScoringEnabled(settings, tx.cryptoCode)
}
function probeLN (cryptoCode, address) {
return wallet.probeLN(settings, cryptoCode, address)
}
return {
getRates,
recordPing,
@ -1012,6 +1033,7 @@ function plugins (settings, deviceId) {
isZeroConf,
getStatus,
getPhoneCode,
getEmailCode,
executeTrades,
pong,
clearOldLogs,
@ -1031,6 +1053,7 @@ function plugins (settings, deviceId) {
getTransactionHash,
getInputAddresses,
isWalletScoringEnabled,
probeLN,
buildAvailableUnits
}
}

View file

@ -4,10 +4,25 @@ const NAME = 'Mailgun'
function sendMessage ({apiKey, domain, fromEmail, toEmail}, rec) {
const mailgun = Mailgun({apiKey, domain})
const to = rec.email.toEmail ?? toEmail
const emailData = {
from: `Lamassu Server ${fromEmail}`,
to: toEmail,
to,
subject: rec.email.subject,
text: rec.email.body
}
return mailgun.messages().send(emailData)
}
function sendCustomerMessage ({apiKey, domain, fromEmail}, rec) {
const mailgun = Mailgun({apiKey, domain})
const to = rec.email.toEmail
const emailData = {
from: fromEmail,
to,
subject: rec.email.subject,
text: rec.email.body
}
@ -17,5 +32,6 @@ function sendMessage ({apiKey, domain, fromEmail, toEmail}, rec) {
module.exports = {
NAME,
sendMessage
sendMessage,
sendCustomerMessage
}

View file

@ -0,0 +1,15 @@
const NAME = 'mock-email'
function sendMessage (settings, rec) {
console.log('sending email', rec)
}
function sendCustomerMessage(settings, rec) {
console.log('sending email', rec)
}
module.exports = {
NAME,
sendMessage,
sendCustomerMessage
}

View file

@ -8,6 +8,14 @@ const RETRIES = 2
const tickerObjects = {}
// This is probably fixed on upstream ccxt
// but we need to udpate node to get on the latest version
const sanityCheckRates = (ask, bid, tickerName) => {
if (new BN(0).eq(ask) || new BN(0).eq(bid)) {
throw new Error(`Failure fetching rates for ${tickerName}`)
}
}
function ticker (fiatCode, cryptoCode, tickerName) {
if (!tickerObjects[tickerName]) {
tickerObjects[tickerName] = new ccxt[tickerName]({
@ -45,12 +53,15 @@ function getCurrencyRates (ticker, fiatCode, cryptoCode) {
}
const symbol = buildMarket(fiatCode, cryptoCode, ticker.id)
return ticker.fetchTicker(symbol)
.then(res => ({
rates: {
ask: new BN(res.ask),
bid: new BN(res.bid)
.then(res => {
sanityCheckRates(res.ask, res.bid, cryptoCode)
return {
rates: {
ask: new BN(res.ask),
bid: new BN(res.bid)
}
}
}))
})
} catch (e) {
return Promise.reject(e)
}

View file

@ -1,5 +1,6 @@
const _ = require('lodash/fp')
const jsonRpc = require('../../common/json-rpc')
const { getSatBEstimateFee } = require('../../../blockexplorers/mempool.space')
const BN = require('../../../bn')
const E = require('../../../error')
@ -56,20 +57,28 @@ function balance (account, cryptoCode, settings, operatorId) {
}
function estimateFee () {
return fetch('estimatesmartfee', [6, 'unset'])
.then(result => BN(result.feerate))
.catch(() => {})
return getSatBEstimateFee()
.then(result => BN(result))
.catch(err => {
logger.error('failure estimating fes', err)
})
}
function calculateFeeDiscount (feeMultiplier) {
function calculateFeeDiscount (feeMultiplier = 1, unitScale) {
// 0 makes bitcoind do automatic fee selection
const AUTOMATIC_FEE = isDevMode() ? 0.01 : 0
if (!feeMultiplier || feeMultiplier.eq(1)) return AUTOMATIC_FEE
const AUTOMATIC_FEE = 0
return estimateFee()
.then(estimatedFee => {
if (!estimatedFee) return AUTOMATIC_FEE
const newFee = estimatedFee.times(feeMultiplier)
if (newFee.lt(0.00001) || newFee.gt(0.1)) return AUTOMATIC_FEE
if (!estimatedFee) {
logger.info('failure estimating fee, using bitcoind automatic fee selection')
return AUTOMATIC_FEE
}
// transform from sat/vB to BTC/kvB and apply the multipler
const newFee = estimatedFee.shiftedBy(-unitScale+3).times(feeMultiplier)
if (newFee.lt(0.00001) || newFee.gt(0.1)) {
logger.info('fee outside safety parameters, defaulting to automatic fee selection')
return AUTOMATIC_FEE
}
return newFee.toFixed(8)
})
}
@ -79,7 +88,7 @@ function sendCoins (account, tx, settings, operatorId, feeMultiplier) {
const coins = cryptoAtoms.shiftedBy(-unitScale).toFixed(8)
return checkCryptoCode(cryptoCode)
.then(() => calculateFeeDiscount(feeMultiplier))
.then(() => calculateFeeDiscount(feeMultiplier, unitScale))
.then(newFee => fetch('settxfee', [newFee]))
.then(() => fetch('sendtoaddress', [toAddress, coins]))
.then((txId) => fetch('gettransaction', [txId]))
@ -95,7 +104,7 @@ function sendCoins (account, tx, settings, operatorId, feeMultiplier) {
function sendCoinsBatch (account, txs, cryptoCode, feeMultiplier) {
return checkCryptoCode(cryptoCode)
.then(() => calculateFeeDiscount(feeMultiplier))
.then(() => calculateFeeDiscount(feeMultiplier, unitScale))
.then(newFee => fetch('settxfee', [newFee]))
.then(() => _.reduce((acc, value) => ({
...acc,
@ -207,7 +216,6 @@ module.exports = {
newFunding,
cryptoNetwork,
fetchRBF,
estimateFee,
sendCoinsBatch,
checkBlockchainStatus,
getTxHashesByAddress,

View file

@ -1,25 +1,20 @@
const _ = require('lodash/fp')
const invoice = require('@node-lightning/invoice')
const axios = require('axios')
const { utils: coinUtils } = require('@lamassu/coins')
const NAME = 'LN'
const SUPPORTED_COINS = ['LN', 'BTC']
const TX_PENDING = 'PENDING'
const TX_SUCCESS = 'SUCCESS'
const URI = 'https://api.staging.galoy.io/graphql'
const BN = require('../../../bn')
function request (graphqlQuery, token) {
function request (graphqlQuery, token, endpoint) {
const headers = {
'content-type': 'application/json',
'Authorization': `Bearer ${token}`
}
return axios({
method: 'post',
url: URI,
url: endpoint,
headers: headers,
data: graphqlQuery
})
@ -27,6 +22,9 @@ function request (graphqlQuery, token) {
if (r.error) throw r.error
return r.data
})
.catch(err => {
throw new Error(err)
})
}
function checkCryptoCode (cryptoCode) {
@ -37,62 +35,60 @@ function checkCryptoCode (cryptoCode) {
return Promise.resolve()
}
function getGaloyAccount (token) {
function getTransactionsByAddress (token, endpoint, walletId, address) {
const accountInfo = {
'operationName': 'me',
'query': `query me {
me {
defaultAccount {
defaultWalletId
wallets {
id
walletCurrency
balance
transactions {
transactionsByAddress (address: "${address}") {
edges {
node {
direction
id
settlementAmount
settlementFee
status
initiationVia {
... on InitiationViaIntraLedger {
counterPartyUsername
counterPartyWalletId
}
... on InitiationViaLn {
paymentHash
}
... on InitiationViaOnChain {
address
}
}
settlementVia {
... on SettlementViaIntraLedger {
counterPartyUsername
counterPartyWalletId
}
... on SettlementViaLn {
preImage
}
... on SettlementViaOnChain {
transactionHash
}
}
}
}
}
}
}
id
}
}`,
'variables': {}
}
return request(accountInfo, token)
return request(accountInfo, token, endpoint)
.then(r => {
return r.data.me.defaultAccount
return _.find(it => it.id === walletId, r.data.me.defaultAccount.wallets).transactionsByAddress
})
.catch(err => {
throw new Error(err)
})
}
function getGaloyWallet (token, endpoint, walletId) {
const accountInfo = {
'operationName': 'me',
'query': `query me {
me {
defaultAccount {
wallets {
id
walletCurrency
balance
}
}
}
}`,
'variables': {}
}
return request(accountInfo, token, endpoint)
.then(r => {
return _.find(it => it.id === walletId, r.data.me.defaultAccount.wallets)
})
.catch(err => {
throw new Error(err)
})
}
@ -100,7 +96,7 @@ function isLightning (address) {
return address.substr(0, 2) === 'ln'
}
function sendFundsOnChain (walletId, address, cryptoAtoms, token) {
function sendFundsOnChain (walletId, address, cryptoAtoms, token, endpoint) {
const sendOnChain = {
'operationName': 'onChainPaymentSend',
'query': `mutation onChainPaymentSend($input: OnChainPaymentSendInput!) {
@ -114,17 +110,17 @@ function sendFundsOnChain (walletId, address, cryptoAtoms, token) {
}`,
'variables': { 'input': { 'address': `${address}`, 'amount': `${cryptoAtoms}`, 'walletId': `${walletId}` } }
}
return request(sendOnChain, token)
return request(sendOnChain, token, endpoint)
.then(result => {
return result.data.onChainPaymentSend
})
}
function sendFundsLN (walletId, invoice, token) {
const sendLN = {
'operationName': 'lnInvoicePaymentSend',
'query': `mutation lnInvoicePaymentSend($input: LnInvoicePaymentInput!) {
lnInvoicePaymentSend(input: $input) {
function sendFundsLN (walletId, invoice, cryptoAtoms, token, endpoint) {
const sendLnNoAmount = {
'operationName': 'lnNoAmountInvoicePaymentSend',
'query': `mutation lnNoAmountInvoicePaymentSend($input: LnNoAmountInvoicePaymentInput!) {
lnNoAmountInvoicePaymentSend(input: $input) {
errors {
message
path
@ -132,29 +128,38 @@ function sendFundsLN (walletId, invoice, token) {
status
}
}`,
'variables': { 'input': { 'paymentRequest': `${invoice}`, 'walletId': `${walletId}` } }
'variables': { 'input': { 'paymentRequest': `${invoice}`, 'walletId': `${walletId}`, 'amount': `${cryptoAtoms}` } }
}
return request(sendLN, token)
.then(result => {
return result.data.lnInvoicePaymentSend
})
return request(sendLnNoAmount, token, endpoint).then(result => result.data.lnNoAmountInvoicePaymentSend)
}
function sendProbeRequest (walletId, invoice, cryptoAtoms, token, endpoint) {
const sendProbeNoAmount = {
'operationName': 'lnNoAmountInvoiceFeeProbe',
'query': `mutation lnNoAmountInvoiceFeeProbe($input: LnNoAmountInvoiceFeeProbeInput!) {
lnNoAmountInvoiceFeeProbe(input: $input) {
amount
errors {
message
path
}
}
}`,
'variables': { 'input': { 'paymentRequest': `${invoice}`, 'walletId': `${walletId}`, 'amount': `${cryptoAtoms}` } }
}
return request(sendProbeNoAmount, token, endpoint).then(result => result.data.lnNoAmountInvoiceFeeProbe)
}
function sendCoins (account, tx, settings, operatorId) {
const { toAddress, cryptoAtoms, cryptoCode } = tx
const externalCryptoCode = coinUtils.getEquivalentCode(cryptoCode)
return checkCryptoCode(cryptoCode)
.then(() => getGaloyAccount(account.apiKey))
.then(galoyAccount => {
const wallet = _.head(
_.filter(wallet => wallet.walletCurrency === externalCryptoCode &&
wallet.id === galoyAccount.defaultWalletId &&
wallet.id === account.walletId)(galoyAccount.wallets)
)
.then(() => getGaloyWallet(account.apiSecret, account.endpoint, account.walletId))
.then(wallet => {
if (isLightning(toAddress)) {
return sendFundsLN(wallet.id, toAddress, account.apiKey)
return sendFundsLN(wallet.id, toAddress, cryptoAtoms, account.apiSecret, account.endpoint)
}
return sendFundsOnChain(wallet.id, toAddress, cryptoAtoms, account.apiKey)
return sendFundsOnChain(wallet.id, toAddress, cryptoAtoms, account.apiSecret, account.endpoint)
})
.then(result => {
switch (result.status) {
@ -172,7 +177,17 @@ function sendCoins (account, tx, settings, operatorId) {
})
}
function newOnChainAddress (walletId, token) {
function probeLN (account, cryptoCode, invoice) {
const probeHardLimits = [100, 500, 1000]
const promises = probeHardLimits.map(limit => {
return sendProbeRequest(account.walletId, invoice, limit, account.apiSecret, account.endpoint)
.then(r => _.isEmpty(r.errors))
})
return Promise.all(promises)
.then(results => _.zipObject(probeHardLimits, results))
}
function newOnChainAddress (walletId, token, endpoint) {
const createOnChainAddress = {
'operationName': 'onChainAddressCreate',
'query': `mutation onChainAddressCreate($input: OnChainAddressCreateInput!) {
@ -186,13 +201,13 @@ function newOnChainAddress (walletId, token) {
}`,
'variables': { 'input': { 'walletId': `${walletId}` } }
}
return request(createOnChainAddress, token)
return request(createOnChainAddress, token, endpoint)
.then(result => {
return result.data.onChainAddressCreate.address
})
}
function newInvoice (walletId, cryptoAtoms, token) {
function newInvoice (walletId, cryptoAtoms, token, endpoint) {
const createInvoice = {
'operationName': 'lnInvoiceCreate',
'query': `mutation lnInvoiceCreate($input: LnInvoiceCreateInput!) {
@ -208,42 +223,28 @@ function newInvoice (walletId, cryptoAtoms, token) {
}`,
'variables': { 'input': { 'walletId': `${walletId}`, 'amount': `${cryptoAtoms}` } }
}
return request(createInvoice, token)
return request(createInvoice, token, endpoint)
.then(result => {
return result.data.lnInvoiceCreate.invoice.paymentRequest
})
}
function balance (account, cryptoCode, settings, operatorId) {
const externalCryptoCode = coinUtils.getEquivalentCode(cryptoCode)
return checkCryptoCode(cryptoCode)
.then(() => getGaloyAccount(account.apiKey))
.then(galoyAccount => {
// account has a list of wallets, should we consider the balance of each one?
// for now we'll get the first BTC wallet that matches the defaultWalletId
const wallet = _.head(
_.filter(
wallet => wallet.walletCurrency === externalCryptoCode &&
wallet.id === account.walletId)(galoyAccount.wallets)
)
.then(() => getGaloyWallet(account.apiSecret, account.endpoint, account.walletId))
.then(wallet => {
return new BN(wallet.balance || 0)
})
}
function newAddress (account, info, tx, settings, operatorId) {
const { cryptoAtoms, cryptoCode } = tx
const externalCryptoCode = coinUtils.getEquivalentCode(cryptoCode)
return checkCryptoCode(cryptoCode)
.then(() => getGaloyAccount(account.apiKey))
.then(galoyAccount => {
const wallet = _.head(
_.filter(wallet => wallet.walletCurrency === externalCryptoCode &&
wallet.id === galoyAccount.defaultWalletId &&
wallet.id === account.walletId)(galoyAccount.wallets)
)
.then(() => getGaloyWallet(account.apiSecret, account.endpoint, account.walletId))
.then(wallet => {
const promises = [
newOnChainAddress(wallet.id, account.apiKey),
newInvoice(wallet.id, cryptoAtoms, account.apiKey)
newOnChainAddress(wallet.id, account.apiSecret, account.endpoint),
newInvoice(wallet.id, cryptoAtoms, account.apiSecret, account.endpoint)
]
return Promise.all(promises)
})
@ -254,31 +255,29 @@ function newAddress (account, info, tx, settings, operatorId) {
function getStatus (account, tx, requested, settings, operatorId) {
const { toAddress, cryptoAtoms, cryptoCode } = tx
const mapStatus = tx => {
if (!tx) return 'notSeen'
if (tx.node.status === TX_PENDING) return 'authorized'
if (tx.node.status === TX_SUCCESS) return 'confirmed'
return 'notSeen'
}
const getBalance = _.reduce((acc, value) => {
acc[value.node.status] = acc[value.node.status].plus(new BN(value.node.settlementAmount))
return acc
}, { SUCCESS: new BN(0), PENDING: new BN(0), FAILURE: new BN(0) })
const externalCryptoCode = coinUtils.getEquivalentCode(cryptoCode)
const address = coinUtils.parseUrl(toAddress)
return checkCryptoCode(cryptoCode)
.then(() => getGaloyAccount(account.apiKey))
.then(galoyAccount => {
const wallet = _.head(
_.filter(wallet => wallet.walletCurrency === externalCryptoCode &&
wallet.id === galoyAccount.defaultWalletId &&
wallet.id === account.walletId)(galoyAccount.wallets)
)
const transactions = wallet.transactions.edges
.then(() => {
const address = coinUtils.parseUrl(cryptoCode, account.environment, toAddress, false)
// Consider all LN transactions successful
if (isLightning(address)) {
const paymentHash = invoice.decode(address).paymentHash.toString('hex')
const transaction = _.head(_.filter(tx => tx.node.initiationVia.paymentHash === paymentHash && tx.node.direction === 'RECEIVE')(transactions))
return { receivedCryptoAtoms: cryptoAtoms, status: mapStatus(transaction) }
return { receivedCryptoAtoms: cryptoAtoms, status: 'confirmed' }
}
// On-chain tx
const transaction = _.head(_.filter(tx => tx.node.initiationVia.address === address)(transactions))
return { receivedCryptoAtoms: cryptoAtoms, status: mapStatus(transaction) }
// On-chain and intra-ledger transactions
return getTransactionsByAddress(account.apiSecret, account.endpoint, account.walletId, address)
.then(transactions => {
const txEdges = transactions.edges
const { SUCCESS: confirmed, PENDING: pending } = getBalance(txEdges)
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
return { receivedCryptoAtoms: pending, status: 'notSeen' }
})
})
}
@ -286,23 +285,15 @@ function newFunding (account, cryptoCode, settings, operatorId) {
const externalCryptoCode = coinUtils.getEquivalentCode(cryptoCode)
// Regular BTC address
return checkCryptoCode(cryptoCode)
.then(() => getGaloyAccount(account.apiKey))
.then(galoyAccount => {
const wallet = _.head(
_.filter(wallet => wallet.walletCurrency === externalCryptoCode &&
wallet.id === galoyAccount.defaultWalletId &&
wallet.id === account.walletId)(galoyAccount.wallets)
)
const pendingBalance = _.sumBy(tx => {
if (tx.node.status === TX_PENDING) return tx.node.settlementAmount
return 0
})(wallet.transactions.edges)
return newOnChainAddress(wallet.id, account.apiKey)
.then(onChainAddress => [onChainAddress, wallet.balance, pendingBalance])
.then(() => getGaloyWallet(account.apiSecret, account.endpoint, account.walletId))
.then(wallet => {
return newOnChainAddress(wallet.id, account.apiSecret, account.endpoint)
.then(onChainAddress => [onChainAddress, wallet.balance])
})
.then(([onChainAddress, balance, pendingBalance]) => {
.then(([onChainAddress, balance]) => {
return {
fundingPendingBalance: new BN(pendingBalance),
// with the old api is not possible to get pending balance
fundingPendingBalance: new BN(0),
fundingConfirmedBalance: new BN(balance),
fundingAddress: onChainAddress
}
@ -327,5 +318,6 @@ module.exports = {
getStatus,
newFunding,
cryptoNetwork,
checkBlockchainStatus
checkBlockchainStatus,
probeLN
}

View file

@ -45,12 +45,12 @@ function toCashOutTx (row) {
return _.set('direction', 'cashOut', newObj)
}
function fetchPhoneTx (phone) {
function fetchEmailOrPhoneTx (data, type) {
const sql = `select * from cash_out_txs
where phone=$1 and dispense=$2
where ${type === 'email' ? 'email' : 'phone'}=$1 and dispense=$2
and (extract(epoch from (now() - created))) * 1000 < $3`
const values = [phone, false, TRANSACTION_EXPIRATION]
const values = [data, false, TRANSACTION_EXPIRATION]
return db.any(sql, values)
.then(_.map(toCashOutTx))
@ -72,6 +72,13 @@ function fetchPhoneTx (phone) {
throw httpError('No transactions', 404)
})
}
function fetchEmailTx (email) {
return fetchEmailOrPhoneTx(email, 'email')
}
function fetchPhoneTx (phone) {
return fetchEmailOrPhoneTx(phone, 'phone')
}
function fetchStatusTx (txId, status) {
const sql = 'select * from cash_out_txs where id=$1'
@ -88,6 +95,7 @@ function fetchStatusTx (txId, status) {
module.exports = {
stateChange,
fetchPhoneTx,
fetchEmailTx,
fetchStatusTx,
httpError
}

View file

@ -30,6 +30,7 @@ const { router: txRoutes } = require('./routes/txRoutes')
const verifyUserRoutes = require('./routes/verifyUserRoutes')
const verifyTxRoutes = require('./routes/verifyTxRoutes')
const verifyPromoCodeRoutes = require('./routes/verifyPromoCodeRoutes')
const probeRoutes = require('./routes/probeLnRoutes')
const graphQLServer = require('./graphql/server')
@ -77,7 +78,10 @@ app.use('/verify_user', verifyUserRoutes)
app.use('/verify_transaction', verifyTxRoutes)
app.use('/verify_promo_code', verifyPromoCodeRoutes)
// BACKWARDS_COMPATIBILITY 9.0
// machines before 9.0 still use the phone_code route
app.use('/phone_code', phoneCodeRoutes)
app.use('/customer', customerRoutes)
app.use('/tx', txRoutes)
@ -85,6 +89,8 @@ app.use('/tx', txRoutes)
app.use('/logs', logsRoutes)
app.use('/units', unitsRoutes)
app.use('/probe', probeRoutes)
graphQLServer.applyMiddleware({ app })
app.use(errorHandler)

View file

@ -20,6 +20,9 @@ const machineLoader = require('../machine-loader')
const { loadLatestConfig } = require('../new-settings-loader')
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
const T = require('../time')
const plugins = require('../plugins')
const Tx = require('../tx')
const loyalty = require('../loyalty')
function updateCustomerCustomInfoRequest (customerId, patch, req, res) {
if (_.isNil(patch.data)) {
@ -185,6 +188,70 @@ function sendSmsReceipt (req, res, next) {
})
}
function addOrUpdateCustomer (customerData, config, isEmailAuth) {
const triggers = configManager.getTriggers(config)
const maxDaysThreshold = complianceTriggers.maxDaysThreshold(triggers)
const customerKey = isEmailAuth ? customerData.email : customerData.phone
const getFunc = isEmailAuth ? customers.getWithEmail : customers.get
const addFunction = isEmailAuth ? customers.addWithEmail : customers.add
return getFunc(customerKey)
.then(customer => {
if (customer) return customer
return addFunction(customerData)
})
.then(customer => customers.getById(customer.id))
.then(customer => {
return Tx.customerHistory(customer.id, maxDaysThreshold)
.then(result => {
customer.txHistory = result
return customer
})
})
.then(customer => {
return loyalty.getCustomerActiveIndividualDiscount(customer.id)
.then(discount => ({ ...customer, discount }))
})
}
function getOrAddCustomerPhone (req, res, next) {
const customerData = req.body
const pi = plugins(req.settings, req.deviceId)
const phone = req.body.phone
return pi.getPhoneCode(phone)
.then(code => {
return addOrUpdateCustomer(customerData, req.settings.config, false)
.then(customer => respond(req, res, { code, customer }))
})
.catch(err => {
if (err.name === 'BadNumberError') throw httpError('Bad number', 401)
throw err
})
.catch(next)
}
function getOrAddCustomerEmail (req, res, next) {
const customerData = req.body
const pi = plugins(req.settings, req.deviceId)
const email = req.body.email
return pi.getEmailCode(email)
.then(code => {
return addOrUpdateCustomer(customerData, req.settings.config, true)
.then(customer => respond(req, res, { code, customer }))
})
.catch(err => {
if (err.name === 'BadNumberError') throw httpError('Bad number', 401)
throw err
})
.catch(next)
}
router.patch('/:id', updateCustomer)
router.patch('/:id/sanctions', triggerSanctions)
router.patch('/:id/block', triggerBlock)
@ -192,5 +259,7 @@ router.patch('/:id/suspend', triggerSuspend)
router.patch('/:id/photos/idcarddata', updateIdCardData)
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
router.post('/:id/smsreceipt', sendSmsReceipt)
router.post('/phone_code', getOrAddCustomerPhone)
router.post('/email_code', getOrAddCustomerEmail)
module.exports = router

View file

@ -0,0 +1,20 @@
const express = require('express')
const router = express.Router()
const plugins = require('../plugins')
const settingsLoader = require('../new-settings-loader')
function probe (req, res, next) {
// TODO: why req.settings is undefined?
settingsLoader.loadLatest()
.then(settings => {
const pi = plugins(settings, req.deviceId)
return pi.probeLN('LN', req.body.address)
.then(r => res.status(200).send({ hardLimits: r }))
.catch(next)
})
}
router.get('/', probe)
module.exports = router

View file

@ -66,8 +66,19 @@ function getPhoneTx (req, res, next) {
return next(httpError('Not Found', 404))
}
function getEmailTx (req, res, next) {
if (req.query.email) {
return helpers.fetchEmailTx(req.query.email)
.then(r => res.json(r))
.catch(next)
}
return next(httpError('Not Found', 404))
}
router.post('/', postTx)
router.get('/:id', getTx)
router.get('/', getPhoneTx)
router.get('/', getEmailTx)
module.exports = { postTx, getTx, getPhoneTx, router }
module.exports = { postTx, getTx, getPhoneTx, getEmailTx, router }

View file

@ -62,6 +62,13 @@ function _balance (settings, cryptoCode) {
})
}
function probeLN (settings, cryptoCode, address) {
return fetchWallet(settings, cryptoCode).then(r => {
if (!r.wallet.probeLN) return null
return r.wallet.probeLN(r.account, cryptoCode, address)
})
}
function sendCoins (settings, tx) {
return fetchWallet(settings, tx.cryptoCode)
.then(r => {
@ -299,5 +306,6 @@ module.exports = {
newFunding,
cryptoNetwork,
supportsBatching,
checkBlockchainStatus
checkBlockchainStatus,
probeLN
}