Merge remote-tracking branch 'origin/release-8.1' into chore/merge-8.1-into-8.6-2023-10-22

This commit is contained in:
Rafael Taranto 2023-10-22 00:57:33 +01:00
commit 416744baf3
31 changed files with 3327 additions and 2533 deletions

View file

@ -11,9 +11,9 @@ const argv = require('minimist')(process.argv.slice(2))
const got = require('got')
const morgan = require('morgan')
const helmet = require('helmet')
const WebSocket = require('ws')
// const WebSocket = require('ws')
const http = require('http')
const SocketIo = require('socket.io')
// const SocketIo = require('socket.io')
const makeDir = require('make-dir')
const _ = require('lodash/fp')

View file

@ -3,7 +3,6 @@ const pgp = require('pg-promise')()
const db = require('../db')
const E = require('../error')
const socket = require('../socket-client')
const logger = require('../logger')
const helper = require('./cash-out-helper')
@ -81,7 +80,7 @@ function preProcess (t, oldTx, newTx, pi) {
if (hasError || hasDispenseOccurred) {
return cashOutActions.logDispense(t, updatedTx)
.then(updateCassettes(t, updatedTx))
.then(it => updateCassettes(t, updatedTx).then(() => it) )
.then((t) => {
pi.notifyOperator(updatedTx, { isRedemption: true })
.catch((err) => logger.error('Failure sending transaction notification', err))
@ -125,7 +124,6 @@ function updateCassettes (t, tx) {
values.push(tx.deviceId)
return t.one(sql, values)
.then(r => socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId})))
}
function wasJustAuthorized (oldTx, newTx, isZeroConf) {

View file

@ -3,7 +3,7 @@ const _ = require('lodash/fp')
const { ALL } = require('../../plugins/common/ccxt')
const { BTC, BCH, DASH, ETH, LTC, USDT, ZEC, XMR, TRX, USDT_TRON } = COINS
const { BTC, BCH, DASH, ETH, LTC, USDT, ZEC, XMR, LN, TRX, USDT_TRON } = COINS
const { bitpay, coinbase, itbit, bitstamp, kraken, binanceus, cex, binance } = ALL
const TICKER = 'ticker'
@ -37,6 +37,7 @@ const ALL_ACCOUNTS = [
{ code: 'monerod', display: 'monerod', class: WALLET, cryptos: [XMR] },
{ code: 'bitcoincashd', display: 'bitcoincashd', class: WALLET, cryptos: [BCH] },
{ code: 'bitgo', display: 'BitGo', class: WALLET, cryptos: [BTC, ZEC, LTC, BCH, DASH] },
{ code: 'galoy', display: 'Galoy', class: WALLET, cryptos: [LN] },
{ code: 'bitstamp', display: 'Bitstamp', class: EXCHANGE, cryptos: bitstamp.CRYPTO },
{ code: 'itbit', display: 'itBit', class: EXCHANGE, cryptos: itbit.CRYPTO },
{ code: 'kraken', display: 'Kraken', class: EXCHANGE, cryptos: kraken.CRYPTO },

View file

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

View file

@ -1,7 +1,6 @@
const _ = require('lodash/fp')
const crypto = require('crypto')
const { utils: coinUtils } = require('@lamassu/coins')
const numeral = require('numeral')
const prettyMs = require('pretty-ms')
const {
@ -140,8 +139,9 @@ const buildTransactionMessage = (tx, rec, highValueTx, machineName, customer) =>
}, highValueTx]
}
function formatCurrency (num, code) {
return numeral(num).format('0,0.00') + ' ' + code
function formatCurrency (num = 0, code) {
const formattedNumber = Number(num).toLocaleString(undefined, {maximumFractionDigits:2, minimumFractionDigits:2})
return `${formattedNumber} ${code}`
}
function formatAge (age, settings) {

View file

@ -1,7 +1,6 @@
const _ = require('lodash/fp')
const crypto = require('crypto')
const pgp = require('pg-promise')()
const dateFormat = require('dateformat')
const { getTimezoneOffset } = require('date-fns-tz')
const { millisecondsToMinutes } = require('date-fns/fp')
@ -471,7 +470,8 @@ function plugins (settings, deviceId) {
logger.debug('notifyConfirmation')
const phone = tx.phone
const timestamp = dateFormat(new Date(), 'UTC:HH:MM Z')
const timestamp = `${(new Date()).toISOString().substring(11, 19)} UTC`
return sms.getSms(CASH_OUT_DISPENSE_READY, phone, { timestamp })
.then(smsObj => {
const rec = {
@ -961,13 +961,13 @@ function plugins (settings, deviceId) {
? '123'
: randomCode()
const timestamp = dateFormat(new Date(), 'UTC:HH:MM Z')
const timestamp = `${(new Date()).toISOString().substring(11, 19)} UTC`
return sms.getSms(CONFIRMATION_CODE, phone, { code, timestamp })
.then(smsObj => {
const rec = {
sms: smsObj
}
return sms.sendMessage(settings, rec)
.then(() => code)
})

View file

@ -1,5 +1,6 @@
const { COINS } = require('@lamassu/coins')
const _ = require('lodash/fp')
const { utils: coinUtils } = require('@lamassu/coins')
const kraken = require('../exchange/kraken')
const bitstamp = require('../exchange/bitstamp')
@ -10,7 +11,7 @@ const bitpay = require('../ticker/bitpay')
const binance = require('../exchange/binance')
const logger = require('../../logger')
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, TRX, USDT_TRON } = COINS
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, TRX, USDT_TRON, LN } = COINS
const ALL = {
cex: cex,
@ -20,7 +21,7 @@ const ALL = {
itbit: itbit,
bitpay: bitpay,
coinbase: {
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, TRX],
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, TRX, LN],
FIAT: 'ALL_CURRENCIES'
},
binance: binance

View file

@ -4,8 +4,8 @@ const _ = require('lodash/fp')
const { ORDER_TYPES } = require('./consts')
const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, BCH, XMR, ETH, LTC, ZEC } = COINS
const CRYPTO = [BTC, ETH, LTC, ZEC, BCH, XMR]
const { BTC, BCH, XMR, ETH, LTC, ZEC, LN } = COINS
const CRYPTO = [BTC, ETH, LTC, ZEC, BCH, XMR, LN]
const FIAT = ['USD', 'EUR']
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']

View file

@ -4,8 +4,8 @@ const _ = require('lodash/fp')
const { ORDER_TYPES } = require('./consts')
const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, USDT_TRON } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON]
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, USDT_TRON, LN } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, LN]
const FIAT = ['USD']
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']

View file

@ -4,8 +4,8 @@ const _ = require('lodash/fp')
const { ORDER_TYPES } = require('./consts')
const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, ETH, LTC, BCH, USDT } = COINS
const CRYPTO = [BTC, ETH, LTC, BCH, USDT ]
const { BTC, ETH, LTC, BCH, USDT, LN } = COINS
const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN]
const FIAT = ['USD', 'EUR']
const AMOUNT_PRECISION = 8
const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId']

View file

@ -4,8 +4,8 @@ const _ = require('lodash/fp')
const { ORDER_TYPES } = require('./consts')
const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, BCH, DASH, ETH, LTC, USDT, TRX, USDT_TRON } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, BCH, USDT, TRX, USDT_TRON]
const { BTC, BCH, DASH, ETH, LTC, USDT, TRX, USDT_TRON, LN } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, BCH, USDT, TRX, USDT_TRON, LN]
const FIAT = ['USD', 'EUR']
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']

View file

@ -4,8 +4,8 @@ const { ORDER_TYPES } = require('./consts')
const { COINS } = require('@lamassu/coins')
const ORDER_TYPE = ORDER_TYPES.LIMIT
const { BTC, ETH, USDT } = COINS
const CRYPTO = [BTC, ETH, USDT]
const { BTC, ETH, USDT, LN } = COINS
const CRYPTO = [BTC, ETH, USDT, LN]
const FIAT = ['USD']
const AMOUNT_PRECISION = 4
const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId']

View file

@ -4,8 +4,8 @@ const { ORDER_TYPES } = require('./consts')
const { COINS } = require('@lamassu/coins')
const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT, TRX, USDT_TRON } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT, TRX, USDT_TRON]
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT, TRX, USDT_TRON, LN } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT, TRX, USDT_TRON, LN]
const FIAT = ['USD', 'EUR']
const AMOUNT_PRECISION = 6
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']

View file

@ -2,9 +2,9 @@ const axios = require('axios')
const { COINS } = require('@lamassu/coins')
const BN = require('../../bn')
const { BTC, BCH } = COINS
const { BTC, BCH, LN } = COINS
const CRYPTO = [BTC, BCH]
const CRYPTO = [BTC, BCH, LN]
const FIAT = 'ALL_CURRENCIES'
function ticker (fiatCode, cryptoCode) {

View file

@ -0,0 +1,331 @@
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) {
const headers = {
'content-type': 'application/json',
'Authorization': `Bearer ${token}`
}
return axios({
method: 'post',
url: URI,
headers: headers,
data: graphqlQuery
})
.then(r => {
if (r.error) throw r.error
return r.data
})
}
function checkCryptoCode (cryptoCode) {
if (!SUPPORTED_COINS.includes(cryptoCode)) {
return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
}
return Promise.resolve()
}
function getGaloyAccount (token) {
const accountInfo = {
'operationName': 'me',
'query': `query me {
me {
defaultAccount {
defaultWalletId
wallets {
id
walletCurrency
balance
transactions {
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)
.then(r => {
return r.data.me.defaultAccount
})
}
function isLightning (address) {
return address.substr(0, 2) === 'ln'
}
function sendFundsOnChain (walletId, address, cryptoAtoms, token) {
const sendOnChain = {
'operationName': 'onChainPaymentSend',
'query': `mutation onChainPaymentSend($input: OnChainPaymentSendInput!) {
onChainPaymentSend(input: $input) {
errors {
message
path
}
status
}
}`,
'variables': { 'input': { 'address': `${address}`, 'amount': `${cryptoAtoms}`, 'walletId': `${walletId}` } }
}
return request(sendOnChain, token)
.then(result => {
return result.data.onChainPaymentSend
})
}
function sendFundsLN (walletId, invoice, token) {
const sendLN = {
'operationName': 'lnInvoicePaymentSend',
'query': `mutation lnInvoicePaymentSend($input: LnInvoicePaymentInput!) {
lnInvoicePaymentSend(input: $input) {
errors {
message
path
}
status
}
}`,
'variables': { 'input': { 'paymentRequest': `${invoice}`, 'walletId': `${walletId}` } }
}
return request(sendLN, token)
.then(result => {
return result.data.lnInvoicePaymentSend
})
}
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)
)
if (isLightning(toAddress)) {
return sendFundsLN(wallet.id, toAddress, account.apiKey)
}
return sendFundsOnChain(wallet.id, toAddress, cryptoAtoms, account.apiKey)
})
.then(result => {
switch (result.status) {
case 'ALREADY_PAID':
throw new Error('Transaction already exists!')
case 'FAILURE':
throw new Error('Transaction failed!')
case 'SUCCESS':
return '<galoy transaction>'
case 'PENDING':
return '<galoy transaction>'
default:
throw new Error(`Transaction failed: ${_.head(result.errors).message}`)
}
})
}
function newOnChainAddress (walletId, token) {
const createOnChainAddress = {
'operationName': 'onChainAddressCreate',
'query': `mutation onChainAddressCreate($input: OnChainAddressCreateInput!) {
onChainAddressCreate(input: $input) {
address
errors {
message
path
}
}
}`,
'variables': { 'input': { 'walletId': `${walletId}` } }
}
return request(createOnChainAddress, token)
.then(result => {
return result.data.onChainAddressCreate.address
})
}
function newInvoice (walletId, cryptoAtoms, token) {
const createInvoice = {
'operationName': 'lnInvoiceCreate',
'query': `mutation lnInvoiceCreate($input: LnInvoiceCreateInput!) {
lnInvoiceCreate(input: $input) {
errors {
message
path
}
invoice {
paymentRequest
}
}
}`,
'variables': { 'input': { 'walletId': `${walletId}`, 'amount': `${cryptoAtoms}` } }
}
return request(createInvoice, token)
.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)
)
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)
)
const promises = [
newOnChainAddress(wallet.id, account.apiKey),
newInvoice(wallet.id, cryptoAtoms, account.apiKey)
]
return Promise.all(promises)
})
.then(([onChainAddress, invoice]) => {
return `bitcoin:${onChainAddress}?amount=${cryptoAtoms}&lightning=${invoice}`
})
}
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 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
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) }
}
// On-chain tx
const transaction = _.head(_.filter(tx => tx.node.initiationVia.address === address)(transactions))
return { receivedCryptoAtoms: cryptoAtoms, status: mapStatus(transaction) }
})
}
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(([onChainAddress, balance, pendingBalance]) => {
return {
fundingPendingBalance: new BN(pendingBalance),
fundingConfirmedBalance: new BN(balance),
fundingAddress: onChainAddress
}
})
}
function cryptoNetwork (account, cryptoCode, settings, operatorId) {
return checkCryptoCode(cryptoCode)
.then(() => account.environment === 'test' ? 'test' : 'main')
}
function checkBlockchainStatus (cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => Promise.resolve('ready'))
}
module.exports = {
NAME,
balance,
sendCoins,
newAddress,
getStatus,
newFunding,
cryptoNetwork,
checkBlockchainStatus
}

View file

@ -1,6 +1,3 @@
const dateFormat = require('dateformat')
const ph = require('./plugin-helper')
const { utils: coinUtils } = require('@lamassu/coins')
const _ = require('lodash/fp')
@ -104,7 +101,8 @@ function formatSmsReceipt (data, options) {
message = message.concat(`Address: ${data.address}\n`)
}
const timestamp = dateFormat(new Date(), 'UTC:HH:MM Z')
const timestamp = `${(new Date()).toISOString().substring(11, 19)} UTC`
const postReceiptSmsPromise = getSms(RECEIPT, data.customerPhone, { timestamp })
return Promise.all([smsNotices.getSMSNotice(RECEIPT), postReceiptSmsPromise])

View file

@ -1,9 +0,0 @@
const io = require('socket.io-client')
const socket = io('http://localhost:3060')
module.exports = {emit}
function emit (msg) {
socket.emit('message', msg)
}

View file

@ -1,6 +1,7 @@
const { utils: coinUtils } = require('@lamassu/coins')
const _ = require('lodash/fp')
const mem = require('mem')
const configManager = require('./new-config-manager')
const logger = require('./logger')
const lastRate = {}
@ -36,16 +37,15 @@ function _getRates (settings, fiatCode, cryptoCode) {
})
}
function buildTicker (fiatCode, _cryptoCode, tickerName) {
const fiatPeggedEquivalent = _.includes(fiatCode, _.keys(PEGGED_FIAT_CURRENCIES))
function buildTicker (fiatCode, cryptoCode, tickerName) {
fiatCode = _.includes(fiatCode, _.keys(PEGGED_FIAT_CURRENCIES))
? PEGGED_FIAT_CURRENCIES[fiatCode]
: fiatCode
cryptoCode = coinUtils.getEquivalentCode(cryptoCode)
const cryptoCode = coinUtils.getEquivalentCode(_cryptoCode)
if (tickerName === 'bitpay') return bitpay.ticker(fiatPeggedEquivalent, cryptoCode)
if (tickerName === 'mock-ticker') return mockTicker.ticker(fiatPeggedEquivalent, cryptoCode)
return ccxt.ticker(fiatPeggedEquivalent, cryptoCode, tickerName)
if (tickerName === 'bitpay') return bitpay.ticker(fiatCode, cryptoCode)
if (tickerName === 'mock-ticker') return mockTicker.ticker(fiatCode, cryptoCode)
return ccxt.ticker(fiatCode, cryptoCode, tickerName)
}
const getRates = mem(_getRates, {