diff --git a/data/currencies.json b/data/currencies.json
index 6261e780..2c88f30c 100644
--- a/data/currencies.json
+++ b/data/currencies.json
@@ -2419,158 +2419,5 @@
"Minor unit": 2,
"Fund": "",
"": ""
- },
- {
- "ENTITY": "ZZ01_Bond Markets Unit European_EURCO",
- "Currency": "Bond Markets Unit European Composite Unit (EURCO)",
- "Alphabetic Code": "XBA",
- "Numeric Code": 955,
- "Minor unit": "N.A.",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "ZZ02_Bond Markets Unit European_EMU-6",
- "Currency": "Bond Markets Unit European Monetary Unit (E.M.U.-6)",
- "Alphabetic Code": "XBB",
- "Numeric Code": 956,
- "Minor unit": "N.A.",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "ZZ03_Bond Markets Unit European_EUA-9",
- "Currency": "Bond Markets Unit European Unit of Account 9 (E.U.A.-9)",
- "Alphabetic Code": "XBC",
- "Numeric Code": 957,
- "Minor unit": "N.A.",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "ZZ04_Bond Markets Unit European_EUA-17",
- "Currency": "Bond Markets Unit European Unit of Account 17 (E.U.A.-17)",
- "Alphabetic Code": "XBD",
- "Numeric Code": 958,
- "Minor unit": "N.A.",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "ZZ06_Testing_Code",
- "Currency": "Codes specifically reserved for testing purposes",
- "Alphabetic Code": "XTS",
- "Numeric Code": 963,
- "Minor unit": "N.A.",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "ZZ07_No_Currency",
- "Currency": "The codes assigned for transactions where no currency is involved",
- "Alphabetic Code": "XXX",
- "Numeric Code": 999,
- "Minor unit": "N.A.",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "ZZ08_Gold",
- "Currency": "Gold",
- "Alphabetic Code": "XAU",
- "Numeric Code": 959,
- "Minor unit": "N.A.",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "ZZ09_Palladium",
- "Currency": "Palladium",
- "Alphabetic Code": "XPD",
- "Numeric Code": 964,
- "Minor unit": "N.A.",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "ZZ10_Platinum",
- "Currency": "Platinum",
- "Alphabetic Code": "XPT",
- "Numeric Code": 962,
- "Minor unit": "N.A.",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "ZZ11_Silver",
- "Currency": "Silver",
- "Alphabetic Code": "XAG",
- "Numeric Code": 961,
- "Minor unit": "N.A.",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "",
- "Currency": "",
- "Alphabetic Code": "",
- "Numeric Code": "",
- "Minor unit": "",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "",
- "Currency": "",
- "Alphabetic Code": "",
- "Numeric Code": "",
- "Minor unit": "",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "",
- "Currency": "",
- "Alphabetic Code": "",
- "Numeric Code": "",
- "Minor unit": "",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "",
- "Currency": "",
- "Alphabetic Code": "",
- "Numeric Code": "",
- "Minor unit": "",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "",
- "Currency": "",
- "Alphabetic Code": "",
- "Numeric Code": "",
- "Minor unit": "",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "",
- "Currency": "",
- "Alphabetic Code": "",
- "Numeric Code": "",
- "Minor unit": "",
- "Fund": "",
- "": ""
- },
- {
- "ENTITY": "",
- "Currency": "",
- "Alphabetic Code": "",
- "Numeric Code": "",
- "Minor unit": "",
- "Fund": "",
- "": ""
}
]
diff --git a/lib/blockchain/common.js b/lib/blockchain/common.js
index 383aa293..854a0ffb 100644
--- a/lib/blockchain/common.js
+++ b/lib/blockchain/common.js
@@ -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']]
}
}
diff --git a/lib/blockexplorers/mempool.space.js b/lib/blockexplorers/mempool.space.js
new file mode 100644
index 00000000..389d6727
--- /dev/null
+++ b/lib/blockexplorers/mempool.space.js
@@ -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 }
\ No newline at end of file
diff --git a/lib/compliance-triggers.js b/lib/compliance-triggers.js
index da4191a3..dcbe6ab2 100644
--- a/lib/compliance-triggers.js
+++ b/lib/compliance-triggers.js
@@ -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 }
\ No newline at end of file
+const AUTH_METHODS = {
+ SMS: 'SMS',
+ EMAIL: 'EMAIL'
+}
+
+module.exports = { getBackwardsCompatibleTriggers, hasSanctions, maxDaysThreshold, getCashLimit, hasPhone, hasFacephoto, hasIdScan, AUTH_METHODS }
\ No newline at end of file
diff --git a/lib/customers.js b/lib/customers.js
index e855070d..371e5634 100644
--- a/lib/customers.js
+++ b/lib/customers.js
@@ -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
}
diff --git a/lib/email.js b/lib/email.js
index 2d96ea18..9c809b9b 100644
--- a/lib/email.js
+++ b/lib/email.js
@@ -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}
diff --git a/lib/graphql/resolvers.js b/lib/graphql/resolvers.js
index 9ca1c68f..047e4c96 100644
--- a/lib/graphql/resolvers.js
+++ b/lib/graphql/resolvers.js
@@ -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,
}),
diff --git a/lib/graphql/types.js b/lib/graphql/types.js
index ee9e9d7d..6e0ff03b 100644
--- a/lib/graphql/types.js
+++ b/lib/graphql/types.js
@@ -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
diff --git a/lib/new-admin/config/accounts.js b/lib/new-admin/config/accounts.js
index 199b96bc..5626e052 100644
--- a/lib/new-admin/config/accounts.js
+++ b/lib/new-admin/config/accounts.js
@@ -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 },
diff --git a/lib/new-admin/config/index.js b/lib/new-admin/config/index.js
index 589de2b4..d64a6246 100644
--- a/lib/new-admin/config/index.js
+++ b/lib/new-admin/config/index.js
@@ -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 => {
diff --git a/lib/new-admin/filters.js b/lib/new-admin/filters.js
index d9b6c32d..89d67434 100644
--- a/lib/new-admin/filters.js
+++ b/lib/new-admin/filters.js
@@ -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
diff --git a/lib/new-admin/graphql/resolvers/customer.resolver.js b/lib/new-admin/graphql/resolvers/customer.resolver.js
index 19b54f79..93695b7e 100644
--- a/lib/new-admin/graphql/resolvers/customer.resolver.js
+++ b/lib/new-admin/graphql/resolvers/customer.resolver.js
@@ -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()
},
diff --git a/lib/new-admin/graphql/types/customer.type.js b/lib/new-admin/graphql/types/customer.type.js
index 5af0c458..d238fda6 100644
--- a/lib/new-admin/graphql/types/customer.type.js
+++ b/lib/new-admin/graphql/types/customer.type.js
@@ -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
}
diff --git a/lib/new-admin/graphql/types/transaction.type.js b/lib/new-admin/graphql/types/transaction.type.js
index b66e88cc..8c43f49e 100644
--- a/lib/new-admin/graphql/types/transaction.type.js
+++ b/lib/new-admin/graphql/types/transaction.type.js
@@ -33,6 +33,7 @@ const typeDef = gql`
rawTickerPrice: String
isPaperWallet: Boolean
customerPhone: String
+ customerEmail: String
customerIdCardDataNumber: String
customerIdCardDataExpiration: Date
customerIdCardData: JSONObject
diff --git a/lib/new-admin/services/transactions.js b/lib/new-admin/services/transactions.js
index d5edb9e9..733fcad9 100644
--- a/lib/new-admin/services/transactions.js
+++ b/lib/new-admin/services/transactions.js
@@ -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,
diff --git a/lib/new-config-manager.js b/lib/new-config-manager.js
index 65c7966d..efb84450 100644
--- a/lib/new-config-manager.js
+++ b/lib/new-config-manager.js
@@ -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,
}
diff --git a/lib/new-settings-loader.js b/lib/new-settings-loader.js
index 697a385d..670ebc99 100644
--- a/lib/new-settings-loader.js
+++ b/lib/new-settings-loader.js
@@ -26,7 +26,8 @@ const SECRET_FIELDS = [
'twilio.authToken',
'telnyx.apiKey',
'vonage.apiSecret',
- 'galoy.walletId'
+ 'galoy.walletId',
+ 'galoy.apiSecret'
]
/*
diff --git a/lib/plugins.js b/lib/plugins.js
index 1539badb..e10532ba 100644
--- a/lib/plugins.js
+++ b/lib/plugins.js
@@ -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
}
}
diff --git a/lib/plugins/email/mailgun/mailgun.js b/lib/plugins/email/mailgun/mailgun.js
index 357baa20..2bd32151 100644
--- a/lib/plugins/email/mailgun/mailgun.js
+++ b/lib/plugins/email/mailgun/mailgun.js
@@ -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
}
diff --git a/lib/plugins/email/mock-email/mock-email.js b/lib/plugins/email/mock-email/mock-email.js
new file mode 100644
index 00000000..219e7dea
--- /dev/null
+++ b/lib/plugins/email/mock-email/mock-email.js
@@ -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
+}
diff --git a/lib/plugins/ticker/ccxt.js b/lib/plugins/ticker/ccxt.js
index 080b2f18..336ef8a3 100644
--- a/lib/plugins/ticker/ccxt.js
+++ b/lib/plugins/ticker/ccxt.js
@@ -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)
}
diff --git a/lib/plugins/wallet/bitcoind/bitcoind.js b/lib/plugins/wallet/bitcoind/bitcoind.js
index 803f4115..ad979a5c 100644
--- a/lib/plugins/wallet/bitcoind/bitcoind.js
+++ b/lib/plugins/wallet/bitcoind/bitcoind.js
@@ -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,
diff --git a/lib/plugins/wallet/galoy/galoy.js b/lib/plugins/wallet/galoy/galoy.js
index d9d34521..0299aab0 100644
--- a/lib/plugins/wallet/galoy/galoy.js
+++ b/lib/plugins/wallet/galoy/galoy.js
@@ -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
}
diff --git a/lib/route-helpers.js b/lib/route-helpers.js
index b382b056..91cf5d5f 100644
--- a/lib/route-helpers.js
+++ b/lib/route-helpers.js
@@ -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
}
diff --git a/lib/routes.js b/lib/routes.js
index 726e53fd..d56e5243 100644
--- a/lib/routes.js
+++ b/lib/routes.js
@@ -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)
diff --git a/lib/routes/customerRoutes.js b/lib/routes/customerRoutes.js
index 0f880a10..3e7c4a98 100644
--- a/lib/routes/customerRoutes.js
+++ b/lib/routes/customerRoutes.js
@@ -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
diff --git a/lib/routes/probeLnRoutes.js b/lib/routes/probeLnRoutes.js
new file mode 100644
index 00000000..62cf127e
--- /dev/null
+++ b/lib/routes/probeLnRoutes.js
@@ -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
\ No newline at end of file
diff --git a/lib/routes/txRoutes.js b/lib/routes/txRoutes.js
index cfda6428..a3261147 100644
--- a/lib/routes/txRoutes.js
+++ b/lib/routes/txRoutes.js
@@ -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 }
diff --git a/lib/wallet.js b/lib/wallet.js
index 24f6b5c2..0597b3ed 100644
--- a/lib/wallet.js
+++ b/lib/wallet.js
@@ -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
}
diff --git a/migrations/1700123461281-customer-email.js b/migrations/1700123461281-customer-email.js
new file mode 100644
index 00000000..58cd358d
--- /dev/null
+++ b/migrations/1700123461281-customer-email.js
@@ -0,0 +1,14 @@
+const db = require('./db')
+
+exports.up = function (next) {
+ let sql = [
+ 'ALTER TABLE customers ADD COLUMN email text unique',
+ 'ALTER TABLE customers ADD COLUMN email_at timestamptz',
+ ]
+
+ db.multi(sql, next)
+}
+
+exports.down = function (next) {
+ next()
+}
diff --git a/migrations/1700123461282-customer-auth-advanced-trigger.js b/migrations/1700123461282-customer-auth-advanced-trigger.js
new file mode 100644
index 00000000..02e61f3f
--- /dev/null
+++ b/migrations/1700123461282-customer-auth-advanced-trigger.js
@@ -0,0 +1,18 @@
+const { migrationSaveConfig } = require('../lib/new-settings-loader')
+
+exports.up = function (next) {
+ const triggersDefault = {
+ triggersConfig_customerAuthentication: 'SMS',
+ }
+
+ return migrationSaveConfig(triggersDefault)
+ .then(() => next())
+ .catch(err => {
+ console.log(err.message)
+ return next(err)
+ })
+}
+
+exports.down = function (next) {
+ next()
+}
diff --git a/migrations/1700123461283-phone-on-tx.js b/migrations/1700123461283-phone-on-tx.js
new file mode 100644
index 00000000..1e8833f1
--- /dev/null
+++ b/migrations/1700123461283-phone-on-tx.js
@@ -0,0 +1,14 @@
+const db = require('./db')
+
+exports.up = function (next) {
+ let sql = [
+ 'ALTER TABLE cash_in_txs ADD COLUMN email text',
+ 'ALTER TABLE cash_out_txs ADD COLUMN email text',
+ ]
+
+ db.multi(sql, next)
+}
+
+exports.down = function (next) {
+ next()
+}
diff --git a/new-lamassu-admin/src/pages/Customers/CustomerData.js b/new-lamassu-admin/src/pages/Customers/CustomerData.js
index 4029814a..a2aa9a81 100644
--- a/new-lamassu-admin/src/pages/Customers/CustomerData.js
+++ b/new-lamassu-admin/src/pages/Customers/CustomerData.js
@@ -101,6 +101,7 @@ const CustomerData = ({
)
const phone = R.path(['phone'])(customer)
+ const email = R.path(['email'])(customer)
const smsData = R.path(['subscriberInfo'])(customer)
const isEven = elem => elem % 2 === 0
@@ -134,6 +135,9 @@ const CustomerData = ({
idCardPhoto: {
idCardPhoto: null
},
+ email: {
+ email
+ },
smsData: {
phoneNumber: getFormattedPhone(phone, locale.country)
}
@@ -201,6 +205,19 @@ const CustomerData = ({
hasAdditionalData: !R.isNil(smsData) && !R.isEmpty(smsData),
editable: false
},
+ {
+ title: 'Email',
+ fields: customerDataElements.email,
+ titleIcon: