Merge pull request #1647 from RafaelTaranto/chore/merge-9-into-10-20240206

Chore/merge 9 into 10 20240206
This commit is contained in:
Rafael Taranto 2024-02-06 08:56:07 +00:00 committed by GitHub
commit 97cdb97099
52 changed files with 4794 additions and 6007 deletions

View file

@ -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": "",
"": ""
}
]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 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: <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',
titleIcon: <EditIcon className={classes.editIcon} />,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,7 +31,8 @@ const ThirdPartyProvider = () => {
}
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 = [
@ -46,14 +47,30 @@ const ThirdPartyProvider = () => {
valueProp: 'code',
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 (
<EditableTable
name="thirdParty"
initialValues={{ sms: data.sms ?? 'twilio' }}
data={R.of({ sms: data.sms ?? 'twilio' })}
initialValues={values}
data={R.of(values)}
error={error?.message}
enableEdit
editWidth={174}

View file

@ -1,7 +1,10 @@
import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
import {
SecretInput,
TextInput,
Autocomplete
} from 'src/components/inputs/formik'
import { secretTest } from './helper'
@ -11,26 +14,49 @@ export default {
title: 'Galoy (Wallet)',
elements: [
{
code: 'apiKey',
display: 'API Key',
component: TextInputFormik,
face: true,
long: true
code: 'apiSecret',
display: 'API Secret',
component: SecretInput
},
{
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',
display: 'Wallet ID',
component: SecretInputFormik
component: SecretInput
}
],
getValidationSchema: account => {
return Yup.object().shape({
apiKey: Yup.string('The API key must be a string')
.max(200, 'The API key is too long')
.required('The API key is required'),
apiSecret: Yup.string('The API Secret must be a string')
.max(200, 'The API Secret is too long')
.test(secretTest(account?.apiSecret)),
walletId: Yup.string('The wallet id must be a string')
.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 =>
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 = '') =>
coinUtils.formatCryptoAddress(cryptoCode, address).replace(/(.{5})/g, '$1 ')
@ -129,6 +140,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
.minus(cashInFee)
.toFixed(2, 1) // ROUND_DOWN
const crypto = getCryptoAmount(tx)
const cryptoFee = getCryptoFeeAmount(tx)
const exchangeRate = BigNumber(fiat)
.div(crypto)
.toFixed(2, 1) // ROUND_DOWN
@ -369,6 +381,12 @@ const DetailsRow = ({ it: tx, timezone }) => {
)}
</div>
</div>
{tx.txClass === 'cashIn' && (
<div className={classes.blockFee}>
<Label>Network Fee</Label>
{cryptoFee} {tx.fiatCode}
</div>
)}
<div className={classes.sessionId}>
<Label>Session ID</Label>
<CopyToClipboard>{tx.id}</CopyToClipboard>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -94,6 +94,21 @@ const getDefaultSettings = () => {
labelProp: 'display',
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 = [
{
expirationTime: 'Forever',
automation: 'Automatic'
automation: 'Automatic',
customerAuth: 'SMS'
}
]

View file

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

View file

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

View file

@ -28,10 +28,13 @@ const displayName = ({
isAnonymous,
customerName,
customerIdCardData,
customerPhone
customerPhone,
customerEmail
}) =>
isAnonymous
? 'Anonymous'
: customerName || R.defaultTo(customerPhone, formatName(customerIdCardData))
: customerName ||
customerEmail ||
R.defaultTo(customerPhone, formatName(customerIdCardData))
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",
"author": "Lamassu (https://lamassu.is)",
"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/tx": "^3.5.1",
"@graphql-tools/merge": "^6.2.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",
"@simplewebauthn/server": "^3.0.0",
"@vonage/auth": "^1.5.0",
@ -24,13 +30,6 @@
"bchaddrjs": "^0.3.0",
"bignumber.js": "9.0.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",
"compression": "^1.7.4",
"connect-pg-simple": "^6.2.1",
@ -40,6 +39,7 @@
"dataloader": "^2.0.0",
"date-fns": "^2.26.0",
"date-fns-tz": "^1.1.6",
"dateformat": "^3.0.3",
"dotenv": "^16.0.0",
"ethereumjs-tx": "^1.3.3",
"ethereumjs-util": "^5.2.0",