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

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