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

@ -2419,158 +2419,5 @@
"Minor unit": 2, "Minor unit": 2,
"Fund": "", "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": "",
"": ""
} }
] ]

View file

@ -30,22 +30,22 @@ const BINARIES = {
BTC: { BTC: {
defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz', 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', defaultDir: 'bitcoin-0.20.1/bin',
url: 'https://bitcoincore.org/bin/bitcoin-core-25.0/bitcoin-25.0-x86_64-linux-gnu.tar.gz', url: 'https://bitcoincore.org/bin/bitcoin-core-26.0/bitcoin-26.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-25.0/bin' dir: 'bitcoin-26.0/bin'
}, },
ETH: { ETH: {
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.13.1-3f40e65c.tar.gz', url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.13.5-916d6a44.tar.gz',
dir: 'geth-linux-amd64-1.13.1-3f40e65c' dir: 'geth-linux-amd64-1.13.5-916d6a44'
}, },
ZEC: { ZEC: {
url: 'https://z.cash/downloads/zcash-5.6.1-linux64-debian-bullseye.tar.gz', url: 'https://download.z.cash/downloads/zcash-5.7.0-linux64-debian-bullseye.tar.gz',
dir: 'zcash-5.6.1/bin' dir: 'zcash-5.7.0/bin'
}, },
DASH: { DASH: {
defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz', 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', 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', url: 'https://github.com/dashpay/dash/releases/download/v20.0.2/dashcore-20.0.2-x86_64-linux-gnu.tar.gz',
dir: 'dashcore-19.3.0/bin' dir: 'dashcore-20.0.2/bin'
}, },
LTC: { LTC: {
defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz', 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' dir: 'litecoin-0.21.2.2/bin'
}, },
BCH: { 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', 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-26.1.0/bin', dir: 'bitcoin-cash-node-27.0.0/bin',
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']] files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
}, },
XMR: { XMR: {
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.2.2.tar.bz2', url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.1.tar.bz2',
dir: 'monero-x86_64-linux-gnu-v0.18.2.2', dir: 'monero-x86_64-linux-gnu-v0.18.3.1',
files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']] 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 hasFacephoto = hasRequirement('facephoto')
const hasIdScan = hasRequirement('idCardData') 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 path = require('path')
const fs = require('fs') const fs = require('fs')
const util = require('util') const util = require('util')
const { sub, differenceInHours } = require('date-fns/fp')
const db = require('./db') const db = require('./db')
const BN = require('./bn')
const anonymous = require('../lib/constants').anonymousCustomer const anonymous = require('../lib/constants').anonymousCustomer
const complianceOverrides = require('./compliance_overrides') const complianceOverrides = require('./compliance_overrides')
const users = require('./users')
const writeFile = util.promisify(fs.writeFile) const writeFile = util.promisify(fs.writeFile)
const notifierQueries = require('./notifier/queries') const notifierQueries = require('./notifier/queries')
const notifierUtils = require('./notifier/utils') const notifierUtils = require('./notifier/utils')
@ -43,6 +40,12 @@ function add (customer) {
.then(camelize) .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 * Get single customer by phone
* Phone numbers are unique per customer * Phone numbers are unique per customer
@ -60,6 +63,12 @@ function get (phone) {
.then(camelize) .then(camelize)
} }
function getWithEmail (email) {
const sql = 'select * from customers where email=$1'
return db.oneOrNone(sql, [email])
.then(camelize)
}
/** /**
* Update customer record * Update customer record
* *
@ -308,7 +317,7 @@ const updateSubscriberData = (customerId, data, userToken) => {
* *
* Used for the machine. * Used for the machine.
*/ */
function getById (id, userToken) { function getById (id) {
const sql = 'select * from customers where id=$1' const sql = 'select * from customers where id=$1'
return db.oneOrNone(sql, [id]) return db.oneOrNone(sql, [id])
.then(assignCustomerData) .then(assignCustomerData)
@ -349,6 +358,7 @@ function camelizeDeep (customer) {
function getComplianceTypes () { function getComplianceTypes () {
return [ return [
'sms', 'sms',
'email',
'id_card_data', 'id_card_data',
'id_card_photo', 'id_card_photo',
'front_camera', 'front_camera',
@ -478,11 +488,11 @@ function batch () {
* @returns {array} Array of customers with it's transactions aggregations * @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 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, 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, 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, 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 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, greatest(0, date_part('day', c.suspended_until - NOW())) AS days_suspended,
c.suspended_until > NOW() AS is_suspended, c.suspended_until > NOW() AS is_suspended,
c.front_camera_path, c.front_camera_override, 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, 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, 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, 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, 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 ($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 ($6 IS NULL OR id_card_data::json->>'address' = $6)
AND ($7 IS NULL OR id_card_data::json->>'documentNumber' = $7) AND ($7 IS NULL OR id_card_data::json->>'documentNumber' = $7)
AND ($8 IS NULL OR email = $8)
limit $3` 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 => .then(customers => Promise.all(_.map(customer =>
getCustomInfoRequestsData(customer) getCustomInfoRequestsData(customer)
.then(camelizeDeep), customers) .then(camelizeDeep), customers)
@ -540,7 +551,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
function getCustomerById (id) { function getCustomerById (id) {
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',') 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, 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, 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, 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 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, greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
c.suspended_until > now() AS is_suspended, c.suspended_until > now() AS is_suspended,
c.front_camera_path, c.front_camera_override, c.front_camera_at, 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.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, 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, row_number() OVER (PARTITION BY c.id ORDER BY t.created DESC) AS rn,
@ -912,7 +923,9 @@ function disableTestCustomer (customerId) {
module.exports = { module.exports = {
add, add,
addWithEmail,
get, get,
getWithEmail,
batch, batch,
getCustomersList, getCustomersList,
getCustomerById, getCustomerById,
@ -930,7 +943,5 @@ module.exports = {
updateEditedPhoto, updateEditedPhoto,
updateTxCustomerPhoto, updateTxCustomerPhoto,
enableTestCustomer, enableTestCustomer,
disableTestCustomer, disableTestCustomer
selectLatestData,
getEditedData
} }

View file

@ -3,7 +3,7 @@ const ph = require('./plugin-helper')
function sendMessage (settings, rec) { function sendMessage (settings, rec) {
return Promise.resolve() return Promise.resolve()
.then(() => { .then(() => {
const pluginCode = 'mailgun' const pluginCode = settings.config.notifications_thirdParty_email
const plugin = ph.load(ph.EMAIL, pluginCode) const plugin = ph.load(ph.EMAIL, pluginCode)
const account = settings.accounts[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.getReceipt(settings.config),
!!configManager.getCashOut(deviceId, settings.config).active, !!configManager.getCashOut(deviceId, settings.config).active,
getMachine(deviceId, currentConfigVersion), getMachine(deviceId, currentConfigVersion),
configManager.getCustomerAuthenticationMethod(settings.config)
]) ])
.then(([ .then(([
enablePaperWalletOnly, enablePaperWalletOnly,
@ -128,6 +129,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
receiptInfo, receiptInfo,
twoWayMode, twoWayMode,
{ numberOfCassettes, numberOfRecyclers }, { numberOfCassettes, numberOfRecyclers },
customerAuthentication,
]) => ]) =>
(currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ? (currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ?
null : null :
@ -144,6 +146,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
}, },
machineInfo: { deviceId, deviceName, numberOfCassettes, numberOfRecyclers }, machineInfo: { deviceId, deviceName, numberOfCassettes, numberOfRecyclers },
twoWayMode, twoWayMode,
customerAuthentication,
speedtestFiles, speedtestFiles,
urlsToPing, urlsToPing,
}), }),

View file

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

View file

@ -53,6 +53,7 @@ const ALL_ACCOUNTS = [
{ code: 'telnyx', display: 'Telnyx', class: SMS }, { code: 'telnyx', display: 'Telnyx', class: SMS },
{ code: 'vonage', display: 'Vonage', class: SMS }, { code: 'vonage', display: 'Vonage', class: SMS },
{ code: 'mailgun', display: 'Mailgun', class: EMAIL }, { 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: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] }, { 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 }, { 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 codeToRec = code => _.find(_.matchesProperty('code', code), mapped)
const top5 = _.map(codeToRec, top5Codes) const top5 = _.map(codeToRec, top5Codes)
const raw = _.uniqBy(_.get('code'), _.concat(top5, mapped)) 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 => { const mapLanguage = lang => {

View file

@ -31,6 +31,7 @@ function transaction () {
function customer () { function customer () {
const sql = `SELECT DISTINCT * FROM ( const sql = `SELECT DISTINCT * FROM (
SELECT 'phone' AS type, phone AS value FROM customers WHERE phone IS NOT NULL UNION 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->>'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, 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 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) isAnonymous: parent => (parent.customerId === anonymous.uuid)
}, },
Query: { 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), customer: (...[, { customerId }]) => customers.getCustomerById(customerId),
customerFilters: () => filters.customer() customerFilters: () => filters.customer()
}, },

View file

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

View file

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

View file

@ -54,6 +54,7 @@ function batch (
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*, const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
c.phone AS customer_phone, 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_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration, c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data, c.id_card_data AS customer_id_card_data,
@ -86,6 +87,7 @@ function batch (
txs.*, txs.*,
actions.tx_hash, actions.tx_hash,
c.phone AS customer_phone, 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_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration, c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data, c.id_card_data AS customer_id_card_data,
@ -159,7 +161,7 @@ function advancedBatch (data) {
'denominationRecycler1', 'denominationRecycler2', 'denominationRecycler3', 'denominationRecycler4', 'denominationRecycler5', 'denominationRecycler6', 'denominationRecycler1', 'denominationRecycler2', 'denominationRecycler3', 'denominationRecycler4', 'denominationRecycler5', 'denominationRecycler6',
'errorCode', 'customerId', 'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address', 'errorCode', 'customerId', 'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms', 'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms',
'discount', 'txHash', 'customerPhone', 'customerIdCardDataNumber', 'discount', 'txHash', 'customerPhone', 'customerEmail', 'customerIdCardDataNumber',
'customerIdCardDataExpiration', 'customerIdCardData', 'customerName', 'sendTime', 'customerIdCardDataExpiration', 'customerIdCardData', 'customerName', 'sendTime',
'customerFrontCameraPath', 'customerIdCardPhotoPath', 'expired', 'machineName', 'walletScore'] 'customerFrontCameraPath', 'customerIdCardPhotoPath', 'expired', 'machineName', 'walletScore']
@ -174,8 +176,8 @@ function advancedBatch (data) {
} }
function simplifiedBatch (data) { function simplifiedBatch (data) {
const fields = ['txClass', 'id', 'created', 'machineName', const fields = ['txClass', 'id', 'created', 'machineName', 'fee',
'cryptoCode', 'cryptoAtoms', 'fiat', 'fiatCode', 'phone', 'toAddress', 'cryptoCode', 'cryptoAtoms', 'fiat', 'fiatCode', 'phone', 'email', 'toAddress',
'txHash', 'dispense', 'error', 'status', 'fiatProfit', 'cryptoAmount'] 'txHash', 'dispense', 'error', 'status', 'fiatProfit', 'cryptoAmount']
const addSimplifiedFields = _.map(it => ({ const addSimplifiedFields = _.map(it => ({
@ -236,6 +238,7 @@ function getCustomerTransactionsBatch (ids) {
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*, const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
c.phone AS customer_phone, 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_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration, c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data, c.id_card_data AS customer_id_card_data,
@ -254,6 +257,7 @@ function getCustomerTransactionsBatch (ids) {
txs.*, txs.*,
actions.tx_hash, actions.tx_hash,
c.phone AS customer_phone, 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_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration, c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data, c.id_card_data AS customer_id_card_data,
@ -282,6 +286,7 @@ function single (txId) {
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*, const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
c.phone AS customer_phone, 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_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration, c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data, c.id_card_data AS customer_id_card_data,
@ -299,6 +304,7 @@ function single (txId) {
txs.*, txs.*,
actions.tx_hash, actions.tx_hash,
c.phone AS customer_phone, 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_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration, c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data, 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 _ = require('lodash/fp')
const { validate } = require('uuid') const { validate } = require('uuid')
@ -120,6 +122,10 @@ const getGlobalNotifications = config => getNotifications(null, null, config)
const getTriggers = _.get('triggers') const getTriggers = _.get('triggers')
function getCustomerAuthenticationMethod(config) {
return _.get('triggersConfig_customerAuthentication')(config)
}
/* `customInfoRequests` is the result of a call to `getCustomInfoRequests` */ /* `customInfoRequests` is the result of a call to `getCustomInfoRequests` */
const getTriggersAutomation = (customInfoRequests, config, oldFormat = false) => { const getTriggersAutomation = (customInfoRequests, config, oldFormat = false) => {
return customInfoRequests return customInfoRequests
@ -193,4 +199,5 @@ module.exports = {
getCryptosFromWalletNamespace, getCryptosFromWalletNamespace,
getCryptoUnits, getCryptoUnits,
setTermsConditions, setTermsConditions,
getCustomerAuthenticationMethod,
} }

View file

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

View file

@ -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) { function sweepHdRow (row) {
const txId = row.id const txId = row.id
const cryptoCode = row.crypto_code const cryptoCode = row.crypto_code
@ -999,6 +1016,10 @@ function plugins (settings, deviceId) {
return walletScoring.isWalletScoringEnabled(settings, tx.cryptoCode) return walletScoring.isWalletScoringEnabled(settings, tx.cryptoCode)
} }
function probeLN (cryptoCode, address) {
return wallet.probeLN(settings, cryptoCode, address)
}
return { return {
getRates, getRates,
recordPing, recordPing,
@ -1012,6 +1033,7 @@ function plugins (settings, deviceId) {
isZeroConf, isZeroConf,
getStatus, getStatus,
getPhoneCode, getPhoneCode,
getEmailCode,
executeTrades, executeTrades,
pong, pong,
clearOldLogs, clearOldLogs,
@ -1031,6 +1053,7 @@ function plugins (settings, deviceId) {
getTransactionHash, getTransactionHash,
getInputAddresses, getInputAddresses,
isWalletScoringEnabled, isWalletScoringEnabled,
probeLN,
buildAvailableUnits buildAvailableUnits
} }
} }

View file

@ -4,10 +4,25 @@ const NAME = 'Mailgun'
function sendMessage ({apiKey, domain, fromEmail, toEmail}, rec) { function sendMessage ({apiKey, domain, fromEmail, toEmail}, rec) {
const mailgun = Mailgun({apiKey, domain}) const mailgun = Mailgun({apiKey, domain})
const to = rec.email.toEmail ?? toEmail
const emailData = { const emailData = {
from: `Lamassu Server ${fromEmail}`, 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, subject: rec.email.subject,
text: rec.email.body text: rec.email.body
} }
@ -17,5 +32,6 @@ function sendMessage ({apiKey, domain, fromEmail, toEmail}, rec) {
module.exports = { module.exports = {
NAME, 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 = {} 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) { function ticker (fiatCode, cryptoCode, tickerName) {
if (!tickerObjects[tickerName]) { if (!tickerObjects[tickerName]) {
tickerObjects[tickerName] = new ccxt[tickerName]({ tickerObjects[tickerName] = new ccxt[tickerName]({
@ -45,12 +53,15 @@ function getCurrencyRates (ticker, fiatCode, cryptoCode) {
} }
const symbol = buildMarket(fiatCode, cryptoCode, ticker.id) const symbol = buildMarket(fiatCode, cryptoCode, ticker.id)
return ticker.fetchTicker(symbol) return ticker.fetchTicker(symbol)
.then(res => ({ .then(res => {
rates: { sanityCheckRates(res.ask, res.bid, cryptoCode)
ask: new BN(res.ask), return {
bid: new BN(res.bid) rates: {
ask: new BN(res.ask),
bid: new BN(res.bid)
}
} }
})) })
} catch (e) { } catch (e) {
return Promise.reject(e) return Promise.reject(e)
} }

View file

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

View file

@ -1,25 +1,20 @@
const _ = require('lodash/fp') const _ = require('lodash/fp')
const invoice = require('@node-lightning/invoice')
const axios = require('axios') const axios = require('axios')
const { utils: coinUtils } = require('@lamassu/coins') const { utils: coinUtils } = require('@lamassu/coins')
const NAME = 'LN' const NAME = 'LN'
const SUPPORTED_COINS = ['LN', 'BTC'] 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') const BN = require('../../../bn')
function request (graphqlQuery, token) { function request (graphqlQuery, token, endpoint) {
const headers = { const headers = {
'content-type': 'application/json', 'content-type': 'application/json',
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
return axios({ return axios({
method: 'post', method: 'post',
url: URI, url: endpoint,
headers: headers, headers: headers,
data: graphqlQuery data: graphqlQuery
}) })
@ -27,6 +22,9 @@ function request (graphqlQuery, token) {
if (r.error) throw r.error if (r.error) throw r.error
return r.data return r.data
}) })
.catch(err => {
throw new Error(err)
})
} }
function checkCryptoCode (cryptoCode) { function checkCryptoCode (cryptoCode) {
@ -37,62 +35,60 @@ function checkCryptoCode (cryptoCode) {
return Promise.resolve() return Promise.resolve()
} }
function getGaloyAccount (token) { function getTransactionsByAddress (token, endpoint, walletId, address) {
const accountInfo = { const accountInfo = {
'operationName': 'me', 'operationName': 'me',
'query': `query me { 'query': `query me {
me { me {
defaultAccount { defaultAccount {
defaultWalletId
wallets { wallets {
id id
walletCurrency transactionsByAddress (address: "${address}") {
balance
transactions {
edges { edges {
node { node {
direction direction
id
settlementAmount settlementAmount
settlementFee
status 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': {} 'variables': {}
} }
return request(accountInfo, token) return request(accountInfo, token, endpoint)
.then(r => { .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' return address.substr(0, 2) === 'ln'
} }
function sendFundsOnChain (walletId, address, cryptoAtoms, token) { function sendFundsOnChain (walletId, address, cryptoAtoms, token, endpoint) {
const sendOnChain = { const sendOnChain = {
'operationName': 'onChainPaymentSend', 'operationName': 'onChainPaymentSend',
'query': `mutation onChainPaymentSend($input: OnChainPaymentSendInput!) { 'query': `mutation onChainPaymentSend($input: OnChainPaymentSendInput!) {
@ -114,17 +110,17 @@ function sendFundsOnChain (walletId, address, cryptoAtoms, token) {
}`, }`,
'variables': { 'input': { 'address': `${address}`, 'amount': `${cryptoAtoms}`, 'walletId': `${walletId}` } } 'variables': { 'input': { 'address': `${address}`, 'amount': `${cryptoAtoms}`, 'walletId': `${walletId}` } }
} }
return request(sendOnChain, token) return request(sendOnChain, token, endpoint)
.then(result => { .then(result => {
return result.data.onChainPaymentSend return result.data.onChainPaymentSend
}) })
} }
function sendFundsLN (walletId, invoice, token) { function sendFundsLN (walletId, invoice, cryptoAtoms, token, endpoint) {
const sendLN = { const sendLnNoAmount = {
'operationName': 'lnInvoicePaymentSend', 'operationName': 'lnNoAmountInvoicePaymentSend',
'query': `mutation lnInvoicePaymentSend($input: LnInvoicePaymentInput!) { 'query': `mutation lnNoAmountInvoicePaymentSend($input: LnNoAmountInvoicePaymentInput!) {
lnInvoicePaymentSend(input: $input) { lnNoAmountInvoicePaymentSend(input: $input) {
errors { errors {
message message
path path
@ -132,29 +128,38 @@ function sendFundsLN (walletId, invoice, token) {
status status
} }
}`, }`,
'variables': { 'input': { 'paymentRequest': `${invoice}`, 'walletId': `${walletId}` } } 'variables': { 'input': { 'paymentRequest': `${invoice}`, 'walletId': `${walletId}`, 'amount': `${cryptoAtoms}` } }
} }
return request(sendLN, token) return request(sendLnNoAmount, token, endpoint).then(result => result.data.lnNoAmountInvoicePaymentSend)
.then(result => { }
return result.data.lnInvoicePaymentSend
}) 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) { function sendCoins (account, tx, settings, operatorId) {
const { toAddress, cryptoAtoms, cryptoCode } = tx const { toAddress, cryptoAtoms, cryptoCode } = tx
const externalCryptoCode = coinUtils.getEquivalentCode(cryptoCode) const externalCryptoCode = coinUtils.getEquivalentCode(cryptoCode)
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => getGaloyAccount(account.apiKey)) .then(() => getGaloyWallet(account.apiSecret, account.endpoint, account.walletId))
.then(galoyAccount => { .then(wallet => {
const wallet = _.head(
_.filter(wallet => wallet.walletCurrency === externalCryptoCode &&
wallet.id === galoyAccount.defaultWalletId &&
wallet.id === account.walletId)(galoyAccount.wallets)
)
if (isLightning(toAddress)) { 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 => { .then(result => {
switch (result.status) { 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 = { const createOnChainAddress = {
'operationName': 'onChainAddressCreate', 'operationName': 'onChainAddressCreate',
'query': `mutation onChainAddressCreate($input: OnChainAddressCreateInput!) { 'query': `mutation onChainAddressCreate($input: OnChainAddressCreateInput!) {
@ -186,13 +201,13 @@ function newOnChainAddress (walletId, token) {
}`, }`,
'variables': { 'input': { 'walletId': `${walletId}` } } 'variables': { 'input': { 'walletId': `${walletId}` } }
} }
return request(createOnChainAddress, token) return request(createOnChainAddress, token, endpoint)
.then(result => { .then(result => {
return result.data.onChainAddressCreate.address return result.data.onChainAddressCreate.address
}) })
} }
function newInvoice (walletId, cryptoAtoms, token) { function newInvoice (walletId, cryptoAtoms, token, endpoint) {
const createInvoice = { const createInvoice = {
'operationName': 'lnInvoiceCreate', 'operationName': 'lnInvoiceCreate',
'query': `mutation lnInvoiceCreate($input: LnInvoiceCreateInput!) { 'query': `mutation lnInvoiceCreate($input: LnInvoiceCreateInput!) {
@ -208,42 +223,28 @@ function newInvoice (walletId, cryptoAtoms, token) {
}`, }`,
'variables': { 'input': { 'walletId': `${walletId}`, 'amount': `${cryptoAtoms}` } } 'variables': { 'input': { 'walletId': `${walletId}`, 'amount': `${cryptoAtoms}` } }
} }
return request(createInvoice, token) return request(createInvoice, token, endpoint)
.then(result => { .then(result => {
return result.data.lnInvoiceCreate.invoice.paymentRequest return result.data.lnInvoiceCreate.invoice.paymentRequest
}) })
} }
function balance (account, cryptoCode, settings, operatorId) { function balance (account, cryptoCode, settings, operatorId) {
const externalCryptoCode = coinUtils.getEquivalentCode(cryptoCode)
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => getGaloyAccount(account.apiKey)) .then(() => getGaloyWallet(account.apiSecret, account.endpoint, account.walletId))
.then(galoyAccount => { .then(wallet => {
// 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)
)
return new BN(wallet.balance || 0) return new BN(wallet.balance || 0)
}) })
} }
function newAddress (account, info, tx, settings, operatorId) { function newAddress (account, info, tx, settings, operatorId) {
const { cryptoAtoms, cryptoCode } = tx const { cryptoAtoms, cryptoCode } = tx
const externalCryptoCode = coinUtils.getEquivalentCode(cryptoCode)
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => getGaloyAccount(account.apiKey)) .then(() => getGaloyWallet(account.apiSecret, account.endpoint, account.walletId))
.then(galoyAccount => { .then(wallet => {
const wallet = _.head(
_.filter(wallet => wallet.walletCurrency === externalCryptoCode &&
wallet.id === galoyAccount.defaultWalletId &&
wallet.id === account.walletId)(galoyAccount.wallets)
)
const promises = [ const promises = [
newOnChainAddress(wallet.id, account.apiKey), newOnChainAddress(wallet.id, account.apiSecret, account.endpoint),
newInvoice(wallet.id, cryptoAtoms, account.apiKey) newInvoice(wallet.id, cryptoAtoms, account.apiSecret, account.endpoint)
] ]
return Promise.all(promises) return Promise.all(promises)
}) })
@ -254,31 +255,29 @@ function newAddress (account, info, tx, settings, operatorId) {
function getStatus (account, tx, requested, settings, operatorId) { function getStatus (account, tx, requested, settings, operatorId) {
const { toAddress, cryptoAtoms, cryptoCode } = tx const { toAddress, cryptoAtoms, cryptoCode } = tx
const mapStatus = tx => { const getBalance = _.reduce((acc, value) => {
if (!tx) return 'notSeen' acc[value.node.status] = acc[value.node.status].plus(new BN(value.node.settlementAmount))
if (tx.node.status === TX_PENDING) return 'authorized' return acc
if (tx.node.status === TX_SUCCESS) return 'confirmed' }, { SUCCESS: new BN(0), PENDING: new BN(0), FAILURE: new BN(0) })
return 'notSeen'
}
const externalCryptoCode = coinUtils.getEquivalentCode(cryptoCode) const externalCryptoCode = coinUtils.getEquivalentCode(cryptoCode)
const address = coinUtils.parseUrl(toAddress)
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => getGaloyAccount(account.apiKey)) .then(() => {
.then(galoyAccount => { const address = coinUtils.parseUrl(cryptoCode, account.environment, toAddress, false)
const wallet = _.head( // Consider all LN transactions successful
_.filter(wallet => wallet.walletCurrency === externalCryptoCode &&
wallet.id === galoyAccount.defaultWalletId &&
wallet.id === account.walletId)(galoyAccount.wallets)
)
const transactions = wallet.transactions.edges
if (isLightning(address)) { if (isLightning(address)) {
const paymentHash = invoice.decode(address).paymentHash.toString('hex') return { receivedCryptoAtoms: cryptoAtoms, status: 'confirmed' }
const transaction = _.head(_.filter(tx => tx.node.initiationVia.paymentHash === paymentHash && tx.node.direction === 'RECEIVE')(transactions))
return { receivedCryptoAtoms: cryptoAtoms, status: mapStatus(transaction) }
} }
// On-chain tx // On-chain and intra-ledger transactions
const transaction = _.head(_.filter(tx => tx.node.initiationVia.address === address)(transactions)) return getTransactionsByAddress(account.apiSecret, account.endpoint, account.walletId, address)
return { receivedCryptoAtoms: cryptoAtoms, status: mapStatus(transaction) } .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) const externalCryptoCode = coinUtils.getEquivalentCode(cryptoCode)
// Regular BTC address // Regular BTC address
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => getGaloyAccount(account.apiKey)) .then(() => getGaloyWallet(account.apiSecret, account.endpoint, account.walletId))
.then(galoyAccount => { .then(wallet => {
const wallet = _.head( return newOnChainAddress(wallet.id, account.apiSecret, account.endpoint)
_.filter(wallet => wallet.walletCurrency === externalCryptoCode && .then(onChainAddress => [onChainAddress, wallet.balance])
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(([onChainAddress, balance, pendingBalance]) => { .then(([onChainAddress, balance]) => {
return { return {
fundingPendingBalance: new BN(pendingBalance), // with the old api is not possible to get pending balance
fundingPendingBalance: new BN(0),
fundingConfirmedBalance: new BN(balance), fundingConfirmedBalance: new BN(balance),
fundingAddress: onChainAddress fundingAddress: onChainAddress
} }
@ -327,5 +318,6 @@ module.exports = {
getStatus, getStatus,
newFunding, newFunding,
cryptoNetwork, cryptoNetwork,
checkBlockchainStatus checkBlockchainStatus,
probeLN
} }

View file

@ -45,12 +45,12 @@ function toCashOutTx (row) {
return _.set('direction', 'cashOut', newObj) return _.set('direction', 'cashOut', newObj)
} }
function fetchPhoneTx (phone) { function fetchEmailOrPhoneTx (data, type) {
const sql = `select * from cash_out_txs 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` 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) return db.any(sql, values)
.then(_.map(toCashOutTx)) .then(_.map(toCashOutTx))
@ -72,6 +72,13 @@ function fetchPhoneTx (phone) {
throw httpError('No transactions', 404) throw httpError('No transactions', 404)
}) })
} }
function fetchEmailTx (email) {
return fetchEmailOrPhoneTx(email, 'email')
}
function fetchPhoneTx (phone) {
return fetchEmailOrPhoneTx(phone, 'phone')
}
function fetchStatusTx (txId, status) { function fetchStatusTx (txId, status) {
const sql = 'select * from cash_out_txs where id=$1' const sql = 'select * from cash_out_txs where id=$1'
@ -88,6 +95,7 @@ function fetchStatusTx (txId, status) {
module.exports = { module.exports = {
stateChange, stateChange,
fetchPhoneTx, fetchPhoneTx,
fetchEmailTx,
fetchStatusTx, fetchStatusTx,
httpError httpError
} }

View file

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

View file

@ -20,6 +20,9 @@ const machineLoader = require('../machine-loader')
const { loadLatestConfig } = require('../new-settings-loader') const { loadLatestConfig } = require('../new-settings-loader')
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests') const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
const T = require('../time') const T = require('../time')
const plugins = require('../plugins')
const Tx = require('../tx')
const loyalty = require('../loyalty')
function updateCustomerCustomInfoRequest (customerId, patch, req, res) { function updateCustomerCustomInfoRequest (customerId, patch, req, res) {
if (_.isNil(patch.data)) { 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', updateCustomer)
router.patch('/:id/sanctions', triggerSanctions) router.patch('/:id/sanctions', triggerSanctions)
router.patch('/:id/block', triggerBlock) router.patch('/:id/block', triggerBlock)
@ -192,5 +259,7 @@ router.patch('/:id/suspend', triggerSuspend)
router.patch('/:id/photos/idcarddata', updateIdCardData) router.patch('/:id/photos/idcarddata', updateIdCardData)
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto) router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
router.post('/:id/smsreceipt', sendSmsReceipt) router.post('/:id/smsreceipt', sendSmsReceipt)
router.post('/phone_code', getOrAddCustomerPhone)
router.post('/email_code', getOrAddCustomerEmail)
module.exports = router 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)) 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.post('/', postTx)
router.get('/:id', getTx) router.get('/:id', getTx)
router.get('/', getPhoneTx) 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) { function sendCoins (settings, tx) {
return fetchWallet(settings, tx.cryptoCode) return fetchWallet(settings, tx.cryptoCode)
.then(r => { .then(r => {
@ -299,5 +306,6 @@ module.exports = {
newFunding, newFunding,
cryptoNetwork, cryptoNetwork,
supportsBatching, supportsBatching,
checkBlockchainStatus checkBlockchainStatus,
probeLN
} }

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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()
}

View file

@ -101,6 +101,7 @@ const CustomerData = ({
) )
const phone = R.path(['phone'])(customer) const phone = R.path(['phone'])(customer)
const email = R.path(['email'])(customer)
const smsData = R.path(['subscriberInfo'])(customer) const smsData = R.path(['subscriberInfo'])(customer)
const isEven = elem => elem % 2 === 0 const isEven = elem => elem % 2 === 0
@ -134,6 +135,9 @@ const CustomerData = ({
idCardPhoto: { idCardPhoto: {
idCardPhoto: null idCardPhoto: null
}, },
email: {
email
},
smsData: { smsData: {
phoneNumber: getFormattedPhone(phone, locale.country) phoneNumber: getFormattedPhone(phone, locale.country)
} }
@ -201,6 +205,19 @@ const CustomerData = ({
hasAdditionalData: !R.isNil(smsData) && !R.isEmpty(smsData), hasAdditionalData: !R.isNil(smsData) && !R.isEmpty(smsData),
editable: false editable: false
}, },
{
title: 'Email',
fields: customerDataElements.email,
titleIcon: <CardIcon className={classes.cardIcon} />,
// state: R.path(['emailOverride'])(customer),
// authorize: () => updateCustomer({ emailOverride: OVERRIDE_AUTHORIZED }),
// reject: () => updateCustomer({ emailOverride: OVERRIDE_REJECTED }),
save: values => editCustomer(values),
deleteEditedData: () => deleteEditedData({ email: null }),
initialValues: initialValues.email,
isAvailable: !R.isNil(customer.email),
editable: false
},
{ {
title: 'Name', title: 'Name',
titleIcon: <EditIcon className={classes.editIcon} />, titleIcon: <EditIcon className={classes.editIcon} />,

View file

@ -57,6 +57,7 @@ const GET_CUSTOMER = gql`
frontCameraAt frontCameraAt
frontCameraOverride frontCameraOverride
phone phone
email
isAnonymous isAnonymous
smsOverride smsOverride
idCardData idCardData
@ -132,6 +133,7 @@ const SET_CUSTOMER = gql`
frontCameraPath frontCameraPath
frontCameraOverride frontCameraOverride
phone phone
email
smsOverride smsOverride
idCardData idCardData
idCardDataOverride idCardDataOverride
@ -516,6 +518,8 @@ const CustomerProfile = memo(() => {
})) ?? [] })) ?? []
const classes = useStyles() const classes = useStyles()
const email = R.path(['email'])(customerData)
const phone = R.path(['phone'])(customerData)
return ( return (
<> <>
@ -532,10 +536,9 @@ const CustomerProfile = memo(() => {
<Label2 noMargin className={classes.labelLink}> <Label2 noMargin className={classes.labelLink}>
{name.length {name.length
? name ? name
: getFormattedPhone( : email?.length
R.path(['phone'])(customerData), ? email
locale.country : getFormattedPhone(phone, locale.country)}
)}
</Label2> </Label2>
</Breadcrumbs> </Breadcrumbs>
<div className={classes.panels}> <div className={classes.panels}>

View file

@ -31,14 +31,22 @@ const GET_CUSTOMERS = gql`
query configAndCustomers( query configAndCustomers(
$phone: String $phone: String
$name: String $name: String
$email: String
$address: String $address: String
$id: String $id: String
) { ) {
config config
customers(phone: $phone, name: $name, address: $address, id: $id) { customers(
phone: $phone
email: $email
name: $name
address: $address
id: $id
) {
id id
idCardData idCardData
phone phone
email
totalTxs totalTxs
totalSpent totalSpent
lastActive lastActive
@ -154,6 +162,7 @@ const Customers = () => {
setVariables({ setVariables({
phone: filtersObject.phone, phone: filtersObject.phone,
name: filtersObject.name, name: filtersObject.name,
email: filtersObject.email,
address: filtersObject.address, address: filtersObject.address,
id: filtersObject.id id: filtersObject.id
}) })
@ -173,6 +182,7 @@ const Customers = () => {
setVariables({ setVariables({
phone: filtersObject.phone, phone: filtersObject.phone,
name: filtersObject.name, name: filtersObject.name,
email: filtersObject.email,
address: filtersObject.address, address: filtersObject.address,
id: filtersObject.id id: filtersObject.id
}) })
@ -187,6 +197,7 @@ const Customers = () => {
setVariables({ setVariables({
phone: filtersObject.phone, phone: filtersObject.phone,
name: filtersObject.name, name: filtersObject.name,
email: filtersObject.email,
address: filtersObject.address, address: filtersObject.address,
id: filtersObject.id id: filtersObject.id
}) })

View file

@ -25,9 +25,10 @@ const CustomersList = ({
const elements = [ const elements = [
{ {
header: 'Phone', header: 'Phone/email',
width: 199, width: 199,
view: it => getFormattedPhone(it.phone, locale.country) view: it => `${getFormattedPhone(it.phone, locale.country) || ''}
${it.email || ''}`
}, },
{ {
header: 'Name', header: 'Name',

View file

@ -17,6 +17,9 @@ const CustomerDetails = memo(({ customer, photosData, locale, timezone }) => {
const idNumber = R.path(['idCardData', 'documentNumber'])(customer) const idNumber = R.path(['idCardData', 'documentNumber'])(customer)
const usSsn = R.path(['usSsn'])(customer) const usSsn = R.path(['usSsn'])(customer)
const name = getName(customer)
const email = R.path(['email'])(customer)
const phone = R.path(['phone'])(customer)
const elements = [ const elements = [
{ {
@ -40,7 +43,12 @@ const CustomerDetails = memo(({ customer, photosData, locale, timezone }) => {
value: usSsn value: usSsn
}) })
const name = getName(customer) if (email)
elements.push({
header: 'Email',
size: 190,
value: email
})
return ( return (
<Box display="flex"> <Box display="flex">
@ -51,7 +59,9 @@ const CustomerDetails = memo(({ customer, photosData, locale, timezone }) => {
<H2 noMargin> <H2 noMargin>
{name.length {name.length
? name ? name
: getFormattedPhone(R.path(['phone'])(customer), locale.country)} : email?.length
? email
: getFormattedPhone(phone, locale.country)}
</H2> </H2>
</div> </div>
<Box display="flex" mt="auto"> <Box display="flex" mt="auto">

View file

@ -59,7 +59,7 @@ const ID_CARD_DATA = 'idCardData'
const getAuthorizedStatus = (it, triggers, customRequests) => { const getAuthorizedStatus = (it, triggers, customRequests) => {
const fields = R.concat( const fields = R.concat(
['frontCamera', 'idCardData', 'idCardPhoto', 'usSsn', 'sanctions'], ['frontCamera', 'idCardData', 'idCardPhoto', 'email', 'usSsn', 'sanctions'],
R.map(ite => ite.id, customRequests) R.map(ite => ite.id, customRequests)
) )
const fieldsWithPathSuffix = ['frontCamera', 'idCardPhoto'] const fieldsWithPathSuffix = ['frontCamera', 'idCardPhoto']
@ -165,6 +165,7 @@ const requirementOptions = [
{ display: 'ID card image', code: 'idCardPhoto' }, { display: 'ID card image', code: 'idCardPhoto' },
{ display: 'ID data', code: 'idCardData' }, { display: 'ID data', code: 'idCardData' },
{ display: 'US SSN', code: 'usSsn' }, { display: 'US SSN', code: 'usSsn' },
{ display: 'Email', code: 'email' },
{ display: 'Customer camera', code: 'frontCamera' } { display: 'Customer camera', code: 'frontCamera' }
] ]
@ -430,6 +431,15 @@ const customerDataElements = {
editable: true editable: true
} }
], ],
email: [
{
name: 'email',
label: 'Email',
component: TextInput,
size: 190,
editable: false
}
],
idCardPhoto: [{ name: 'idCardPhoto' }], idCardPhoto: [{ name: 'idCardPhoto' }],
frontCamera: [{ name: 'frontCamera' }] frontCamera: [{ name: 'frontCamera' }]
} }
@ -460,6 +470,9 @@ const customerDataSchemas = {
}), }),
frontCamera: Yup.object().shape({ frontCamera: Yup.object().shape({
frontCamera: Yup.mixed().required() frontCamera: Yup.mixed().required()
}),
email: Yup.object().shape({
email: Yup.string().required()
}) })
} }
@ -486,6 +499,13 @@ const requirementElements = {
initialValues: { usSsn: '' }, initialValues: { usSsn: '' },
saveType: 'customerData' saveType: 'customerData'
}, },
email: {
schema: customerDataSchemas.email,
options: customerDataElements.email,
Component: ManualDataEntry,
initialValues: { email: '' },
saveType: 'customerData'
},
idCardPhoto: { idCardPhoto: {
schema: customerDataSchemas.idCardPhoto, schema: customerDataSchemas.idCardPhoto,
options: customerDataElements.idCardPhoto, options: customerDataElements.idCardPhoto,

View file

@ -31,7 +31,8 @@ const ThirdPartyProvider = () => {
} }
const ThirdPartySchema = Yup.object().shape({ const ThirdPartySchema = Yup.object().shape({
sms: Yup.string('The sms must be a string').required('The sms is required') sms: Yup.string('SMS must be a string').required('SMS is required'),
email: Yup.string('Email must be a string').required('Email is required')
}) })
const elements = [ const elements = [
@ -46,14 +47,30 @@ const ThirdPartyProvider = () => {
valueProp: 'code', valueProp: 'code',
labelProp: 'display' labelProp: 'display'
} }
},
{
name: 'email',
size: 'sm',
view: getDisplayName('email'),
width: 175,
input: Autocomplete,
inputProps: {
options: filterOptions('email'),
valueProp: 'code',
labelProp: 'display'
}
} }
] ]
const values = {
sms: data.sms ?? 'twilio',
email: data.email ?? 'mailgun'
}
return ( return (
<EditableTable <EditableTable
name="thirdParty" name="thirdParty"
initialValues={{ sms: data.sms ?? 'twilio' }} initialValues={values}
data={R.of({ sms: data.sms ?? 'twilio' })} data={R.of(values)}
error={error?.message} error={error?.message}
enableEdit enableEdit
editWidth={174} editWidth={174}

View file

@ -1,7 +1,10 @@
import * as Yup from 'yup' import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput' import {
import TextInputFormik from 'src/components/inputs/formik/TextInput' SecretInput,
TextInput,
Autocomplete
} from 'src/components/inputs/formik'
import { secretTest } from './helper' import { secretTest } from './helper'
@ -11,26 +14,49 @@ export default {
title: 'Galoy (Wallet)', title: 'Galoy (Wallet)',
elements: [ elements: [
{ {
code: 'apiKey', code: 'apiSecret',
display: 'API Key', display: 'API Secret',
component: TextInputFormik, component: SecretInput
face: true, },
long: true {
code: 'environment',
display: 'Environment',
component: Autocomplete,
inputProps: {
options: [
{ code: 'main', display: 'prod' },
{ code: 'test', display: 'test' }
],
labelProp: 'display',
valueProp: 'code'
},
face: true
},
{
code: 'endpoint',
display: 'Endpoint',
component: TextInput
}, },
{ {
code: 'walletId', code: 'walletId',
display: 'Wallet ID', display: 'Wallet ID',
component: SecretInputFormik component: SecretInput
} }
], ],
getValidationSchema: account => { getValidationSchema: account => {
return Yup.object().shape({ return Yup.object().shape({
apiKey: Yup.string('The API key must be a string') apiSecret: Yup.string('The API Secret must be a string')
.max(200, 'The API key is too long') .max(200, 'The API Secret is too long')
.required('The API key is required'), .test(secretTest(account?.apiSecret)),
walletId: Yup.string('The wallet id must be a string') walletId: Yup.string('The wallet id must be a string')
.max(100, 'The wallet id is too long') .max(100, 'The wallet id is too long')
.test(secretTest(account?.walletId)) .test(secretTest(account?.walletId)),
environment: Yup.string('The environment must be a string')
.matches(/(main|test)/)
.required('The environment is required'),
endpoint: Yup.string('The endpoint must be a string')
.max(100, 'The endpoint is too long')
.required('The endpoint is required')
}) })
} }
} }

View file

@ -89,6 +89,17 @@ const CANCEL_CASH_IN_TRANSACTION = gql`
const getCryptoAmount = tx => const getCryptoAmount = tx =>
coinUtils.toUnit(new BigNumber(tx.cryptoAtoms), tx.cryptoCode).toNumber() coinUtils.toUnit(new BigNumber(tx.cryptoAtoms), tx.cryptoCode).toNumber()
const getCryptoFeeAmount = tx => {
const feeAmount = coinUtils
.toUnit(new BigNumber(tx.fee), tx.cryptoCode)
.toNumber()
return new BigNumber(feeAmount)
.times(tx.rawTickerPrice)
.toNumber()
.toFixed(2, 1)
}
const formatAddress = (cryptoCode = '', address = '') => const formatAddress = (cryptoCode = '', address = '') =>
coinUtils.formatCryptoAddress(cryptoCode, address).replace(/(.{5})/g, '$1 ') coinUtils.formatCryptoAddress(cryptoCode, address).replace(/(.{5})/g, '$1 ')
@ -129,6 +140,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
.minus(cashInFee) .minus(cashInFee)
.toFixed(2, 1) // ROUND_DOWN .toFixed(2, 1) // ROUND_DOWN
const crypto = getCryptoAmount(tx) const crypto = getCryptoAmount(tx)
const cryptoFee = getCryptoFeeAmount(tx)
const exchangeRate = BigNumber(fiat) const exchangeRate = BigNumber(fiat)
.div(crypto) .div(crypto)
.toFixed(2, 1) // ROUND_DOWN .toFixed(2, 1) // ROUND_DOWN
@ -369,6 +381,12 @@ const DetailsRow = ({ it: tx, timezone }) => {
)} )}
</div> </div>
</div> </div>
{tx.txClass === 'cashIn' && (
<div className={classes.blockFee}>
<Label>Network Fee</Label>
{cryptoFee} {tx.fiatCode}
</div>
)}
<div className={classes.sessionId}> <div className={classes.sessionId}>
<Label>Session ID</Label> <Label>Session ID</Label>
<CopyToClipboard>{tx.id}</CopyToClipboard> <CopyToClipboard>{tx.id}</CopyToClipboard>

View file

@ -90,6 +90,9 @@ export default {
transactionId: { transactionId: {
width: 280 width: 280
}, },
blockFee: {
width: 140
},
sessionId: { sessionId: {
width: 215 width: 215
}, },

View file

@ -104,6 +104,7 @@ const GET_TRANSACTIONS = gql`
errorCode errorCode
deviceId deviceId
fiat fiat
fee
cashInFee cashInFee
fiatCode fiatCode
cryptoAtoms cryptoAtoms
@ -116,6 +117,7 @@ const GET_TRANSACTIONS = gql`
customerFrontCameraPath customerFrontCameraPath
txCustomerPhotoPath txCustomerPhotoPath
customerPhone customerPhone
customerEmail
discount discount
customerId customerId
isAnonymous isAnonymous

View file

@ -28,7 +28,8 @@ const TriggerView = ({
config, config,
toggleWizard, toggleWizard,
addNewTriger, addNewTriger,
customInfoRequests customInfoRequests,
emailAuth
}) => { }) => {
const currency = R.path(['fiatCurrency'])( const currency = R.path(['fiatCurrency'])(
fromNamespace(namespaces.LOCALE)(config) fromNamespace(namespaces.LOCALE)(config)
@ -77,6 +78,7 @@ const TriggerView = ({
save={add} save={add}
onClose={() => toggleWizard(true)} onClose={() => toggleWizard(true)}
customInfoRequests={customInfoRequests} customInfoRequests={customInfoRequests}
emailAuth={emailAuth}
/> />
)} )}
{R.isEmpty(triggers) && ( {R.isEmpty(triggers) && (

View file

@ -58,7 +58,7 @@ const GET_CUSTOM_REQUESTS = gql`
const Triggers = () => { const Triggers = () => {
const classes = useStyles() const classes = useStyles()
const [wizardType, setWizard] = useState(false) const [wizardType, setWizard] = useState(false)
const { data, loading: configLoading } = useQuery(GET_CONFIG) const { data, loading: configLoading, refetch } = useQuery(GET_CONFIG)
const { data: customInfoReqData, loading: customInfoLoading } = useQuery( const { data: customInfoReqData, loading: customInfoLoading } = useQuery(
GET_CUSTOM_REQUESTS GET_CUSTOM_REQUESTS
) )
@ -72,6 +72,8 @@ const Triggers = () => {
const enabledCustomInfoRequests = R.filter(R.propEq('enabled', true))( const enabledCustomInfoRequests = R.filter(R.propEq('enabled', true))(
customInfoRequests customInfoRequests
) )
const emailAuth =
data?.config?.triggersConfig_customerAuthentication === 'EMAIL'
const triggers = fromServer(data?.config?.triggers ?? []) const triggers = fromServer(data?.config?.triggers ?? [])
const complianceConfig = const complianceConfig =
@ -141,6 +143,7 @@ const Triggers = () => {
inverseIcon: ReverseSettingsIcon, inverseIcon: ReverseSettingsIcon,
forceDisable: !(subMenu === 'advancedSettings'), forceDisable: !(subMenu === 'advancedSettings'),
toggle: show => { toggle: show => {
refetch()
setSubMenu(show ? 'advancedSettings' : false) setSubMenu(show ? 'advancedSettings' : false)
} }
}, },
@ -150,6 +153,7 @@ const Triggers = () => {
inverseIcon: ReverseCustomInfoIcon, inverseIcon: ReverseCustomInfoIcon,
forceDisable: !(subMenu === 'customInfoRequests'), forceDisable: !(subMenu === 'customInfoRequests'),
toggle: show => { toggle: show => {
refetch()
setSubMenu(show ? 'customInfoRequests' : false) setSubMenu(show ? 'customInfoRequests' : false)
} }
} }
@ -216,6 +220,7 @@ const Triggers = () => {
toggleWizard={toggleWizard('newTrigger')} toggleWizard={toggleWizard('newTrigger')}
addNewTriger={addNewTriger} addNewTriger={addNewTriger}
customInfoRequests={enabledCustomInfoRequests} customInfoRequests={enabledCustomInfoRequests}
emailAuth={emailAuth}
/> />
)} )}
{!loading && subMenu === 'advancedSettings' && ( {!loading && subMenu === 'advancedSettings' && (

View file

@ -48,14 +48,14 @@ const styles = {
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const getStep = (step, currency, customInfoRequests) => { const getStep = (step, currency, customInfoRequests, emailAuth) => {
switch (step) { switch (step) {
// case 1: // case 1:
// return txDirection // return txDirection
case 1: case 1:
return type(currency) return type(currency)
case 2: case 2:
return requirements(customInfoRequests) return requirements(customInfoRequests, emailAuth)
default: default:
return Fragment return Fragment
} }
@ -138,6 +138,8 @@ const getTypeText = (config, currency, classes) => {
const getRequirementText = (config, classes) => { const getRequirementText = (config, classes) => {
switch (config.requirement?.requirement) { switch (config.requirement?.requirement) {
case 'email':
return <>asked to enter code provided through email verification</>
case 'sms': case 'sms':
return <>asked to enter code provided through SMS verification</> return <>asked to enter code provided through SMS verification</>
case 'idCardPhoto': case 'idCardPhoto':
@ -202,7 +204,14 @@ const GetValues = ({ setValues }) => {
return null return null
} }
const Wizard = ({ onClose, save, error, currency, customInfoRequests }) => { const Wizard = ({
onClose,
save,
error,
currency,
customInfoRequests,
emailAuth
}) => {
const classes = useStyles() const classes = useStyles()
const [liveValues, setLiveValues] = useState({}) const [liveValues, setLiveValues] = useState({})
@ -211,7 +220,7 @@ const Wizard = ({ onClose, save, error, currency, customInfoRequests }) => {
}) })
const isLastStep = step === LAST_STEP const isLastStep = step === LAST_STEP
const stepOptions = getStep(step, currency, customInfoRequests) const stepOptions = getStep(step, currency, customInfoRequests, emailAuth)
const onContinue = async it => { const onContinue = async it => {
const newConfig = R.merge(config, stepOptions.schema.cast(it)) const newConfig = R.merge(config, stepOptions.schema.cast(it))

View file

@ -94,6 +94,21 @@ const getDefaultSettings = () => {
labelProp: 'display', labelProp: 'display',
valueProp: 'code' valueProp: 'code'
} }
},
{
name: 'customerAuthentication',
header: 'Customer Auth',
width: 196,
size: 'sm',
input: Autocomplete,
inputProps: {
options: [
{ code: 'SMS', display: 'SMS' },
{ code: 'EMAIL', display: 'EMAIL' }
],
labelProp: 'display',
valueProp: 'code'
}
} }
] ]
} }
@ -144,7 +159,8 @@ const getOverrides = customInfoRequests => {
const defaults = [ const defaults = [
{ {
expirationTime: 'Forever', expirationTime: 'Forever',
automation: 'Automatic' automation: 'Automatic',
customerAuth: 'SMS'
} }
] ]

View file

@ -522,6 +522,7 @@ const requirementSchema = Yup.object()
const requirementOptions = [ const requirementOptions = [
{ display: 'SMS verification', code: 'sms' }, { display: 'SMS verification', code: 'sms' },
{ display: 'Email verification', code: 'email' },
{ display: 'ID card image', code: 'idCardPhoto' }, { display: 'ID card image', code: 'idCardPhoto' },
{ display: 'ID data', code: 'idCardData' }, { display: 'ID data', code: 'idCardData' },
{ display: 'Customer camera', code: 'facephoto' }, { display: 'Customer camera', code: 'facephoto' },
@ -544,7 +545,7 @@ const hasCustomRequirementError = (errors, touched, values) =>
(!values.requirement?.customInfoRequestId || (!values.requirement?.customInfoRequestId ||
!R.isNil(values.requirement?.customInfoRequestId)) !R.isNil(values.requirement?.customInfoRequestId))
const Requirement = ({ customInfoRequests }) => { const Requirement = ({ customInfoRequests, emailAuth }) => {
const classes = useStyles() const classes = useStyles()
const { const {
touched, touched,
@ -567,9 +568,11 @@ const Requirement = ({ customInfoRequests }) => {
display: 'Custom information requirement', display: 'Custom information requirement',
code: 'custom' code: 'custom'
} }
const itemToRemove = emailAuth ? 'sms' : 'email'
const reqOptions = requirementOptions.filter(it => it.code !== itemToRemove)
const options = enableCustomRequirement const options = enableCustomRequirement
? [...requirementOptions, customInfoOption] ? [...reqOptions, customInfoOption]
: [...requirementOptions] : [...reqOptions]
const titleClass = { const titleClass = {
[classes.error]: [classes.error]:
(!!errors.requirement && !isSuspend && !isCustom) || (!!errors.requirement && !isSuspend && !isCustom) ||
@ -621,11 +624,11 @@ const Requirement = ({ customInfoRequests }) => {
) )
} }
const requirements = customInfoRequests => ({ const requirements = (customInfoRequests, emailAuth) => ({
schema: requirementSchema, schema: requirementSchema,
options: requirementOptions, options: requirementOptions,
Component: Requirement, Component: Requirement,
props: { customInfoRequests }, props: { customInfoRequests, emailAuth },
hasRequirementError: hasRequirementError, hasRequirementError: hasRequirementError,
hasCustomRequirementError: hasCustomRequirementError, hasCustomRequirementError: hasCustomRequirementError,
initialValues: { initialValues: {

View file

@ -408,6 +408,17 @@ export default {
}, },
polymer: false polymer: false
}, },
XCD: {
thickness: 0x0c,
lengths: {
5: [0x9b, 0x87],
10: [0x9b, 0x87],
20: [0x9b, 0x87],
50: [0x9b, 0x87],
100: [0x9b, 0x87]
},
polymer: true
},
ZAR: { ZAR: {
thickness: 0x0c, thickness: 0x0c,
lengths: { lengths: {

View file

@ -28,10 +28,13 @@ const displayName = ({
isAnonymous, isAnonymous,
customerName, customerName,
customerIdCardData, customerIdCardData,
customerPhone customerPhone,
customerEmail
}) => }) =>
isAnonymous isAnonymous
? 'Anonymous' ? 'Anonymous'
: customerName || R.defaultTo(customerPhone, formatName(customerIdCardData)) : customerName ||
customerEmail ||
R.defaultTo(customerPhone, formatName(customerIdCardData))
export { displayName, formatFullName, formatName } export { displayName, formatFullName, formatName }

9710
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,11 +6,17 @@
"license": "./LICENSE", "license": "./LICENSE",
"author": "Lamassu (https://lamassu.is)", "author": "Lamassu (https://lamassu.is)",
"dependencies": { "dependencies": {
"@bitgo/sdk-api": "1.33.0",
"@bitgo/sdk-coin-bch": "1.5.22",
"@bitgo/sdk-coin-btc": "1.7.22",
"@bitgo/sdk-coin-dash": "1.5.22",
"@bitgo/sdk-coin-ltc": "2.2.22",
"@bitgo/sdk-coin-zec": "1.5.22",
"@ethereumjs/common": "^2.6.4", "@ethereumjs/common": "^2.6.4",
"@ethereumjs/tx": "^3.5.1", "@ethereumjs/tx": "^3.5.1",
"@graphql-tools/merge": "^6.2.5", "@graphql-tools/merge": "^6.2.5",
"@haensl/subset-sum": "^3.0.5", "@haensl/subset-sum": "^3.0.5",
"@lamassu/coins": "v1.4.0-beta.4", "@lamassu/coins": "v1.4.3",
"@node-lightning/invoice": "0.28.0", "@node-lightning/invoice": "0.28.0",
"@simplewebauthn/server": "^3.0.0", "@simplewebauthn/server": "^3.0.0",
"@vonage/auth": "^1.5.0", "@vonage/auth": "^1.5.0",
@ -24,13 +30,6 @@
"bchaddrjs": "^0.3.0", "bchaddrjs": "^0.3.0",
"bignumber.js": "9.0.1", "bignumber.js": "9.0.1",
"bip39": "^2.3.1", "bip39": "^2.3.1",
"@bitgo/sdk-api": "1.21.0",
"@bitgo/sdk-coin-bch": "1.5.10",
"@bitgo/sdk-coin-btc": "1.7.10",
"@bitgo/sdk-coin-dash": "1.5.10",
"@bitgo/sdk-coin-ltc": "2.2.10",
"@bitgo/sdk-coin-zec": "1.5.10",
"@bitgo/utxo-lib": "9.13.0",
"ccxt": "2.9.16", "ccxt": "2.9.16",
"compression": "^1.7.4", "compression": "^1.7.4",
"connect-pg-simple": "^6.2.1", "connect-pg-simple": "^6.2.1",
@ -40,6 +39,7 @@
"dataloader": "^2.0.0", "dataloader": "^2.0.0",
"date-fns": "^2.26.0", "date-fns": "^2.26.0",
"date-fns-tz": "^1.1.6", "date-fns-tz": "^1.1.6",
"dateformat": "^3.0.3",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"ethereumjs-tx": "^1.3.3", "ethereumjs-tx": "^1.3.3",
"ethereumjs-util": "^5.2.0", "ethereumjs-util": "^5.2.0",