feat: add bill math cassette-agnostic solution
fix: multiple generic fixes related with the recyclers fix: slight UI data changes
This commit is contained in:
parent
2d010fc359
commit
f3ab63766e
15 changed files with 173 additions and 353 deletions
247
lib/bill-math.js
247
lib/bill-math.js
|
|
@ -1,225 +1,46 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const uuid = require('uuid')
|
const sumService = require('@haensl/subset-sum')
|
||||||
|
|
||||||
const MAX_AMOUNT_OF_SOLUTIONS = 10000
|
const getSolution = (units, amount) => {
|
||||||
const MAX_BRUTEFORCE_ITERATIONS = 10000000
|
const billList = _.reduce(
|
||||||
|
(acc, value) => {
|
||||||
|
acc.push(..._.times(_.constant(value.denomination), value.count))
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
units
|
||||||
|
)
|
||||||
|
|
||||||
function newSolution(cassettes, c0, c1, c2, c3, shouldFlip) {
|
const solver = sumService.subsetSum(billList, amount.toNumber())
|
||||||
return [
|
const solution = _.countBy(Math.floor, solver.next().value)
|
||||||
{
|
return _.reduce(
|
||||||
provisioned: shouldFlip ? cassettes[0].count - c0 : c0,
|
(acc, value) => {
|
||||||
denomination: cassettes[0].denomination
|
acc.push({ denomination: _.toNumber(value), provisioned: solution[value] })
|
||||||
|
return acc
|
||||||
},
|
},
|
||||||
{
|
[],
|
||||||
provisioned: shouldFlip ? cassettes[1].count - c1 : c1,
|
_.keys(solution)
|
||||||
denomination: cassettes[1].denomination
|
)
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: shouldFlip ? cassettes[2].count - c2 : c2,
|
|
||||||
denomination: cassettes[2].denomination
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: shouldFlip ? cassettes[3].count - c3 : c3,
|
|
||||||
denomination: cassettes[3].denomination
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeCassettes(cassettes) {
|
const solutionToOriginalUnits = (solution, units) => {
|
||||||
const map = {}
|
const billsLeft = _.clone(_.fromPairs(_.map(it => [it.denomination, it.provisioned])(solution)))
|
||||||
|
return _.reduce(
|
||||||
_.forEach(it => {
|
(acc, value) => {
|
||||||
if (!map[it.denomination]) {
|
const unit = units[value]
|
||||||
map[it.denomination] = 0
|
const billsToAssign = _.clamp(0, unit.count)(_.isNaN(billsLeft[unit.denomination]) || _.isNil(billsLeft[unit.denomination]) ? 0 : billsLeft[unit.denomination])
|
||||||
}
|
acc.push({ name: unit.name, denomination: unit.denomination, provisioned: billsToAssign })
|
||||||
map[it.denomination] += it.count
|
billsLeft[unit.denomination] -= billsToAssign
|
||||||
}, cassettes)
|
return acc
|
||||||
|
},
|
||||||
return _.map(it => ({ denomination: it, count: map[it] }), _.keys(map))
|
[],
|
||||||
}
|
_.range(0, _.size(units))
|
||||||
|
)
|
||||||
function unmergeCassettes(cassettes, output) {
|
|
||||||
const map = {}
|
|
||||||
|
|
||||||
_.forEach(it => {
|
|
||||||
if (!map[it.denomination]) {
|
|
||||||
map[it.denomination] = 0
|
|
||||||
}
|
|
||||||
map[it.denomination] += it.provisioned
|
|
||||||
}, output)
|
|
||||||
|
|
||||||
const response = []
|
|
||||||
_.forEach(it => {
|
|
||||||
const value = {
|
|
||||||
denomination: it.denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
}
|
|
||||||
|
|
||||||
const amountNeeded = map[it.denomination]
|
|
||||||
if (!amountNeeded) {
|
|
||||||
return response.push({ provisioned: 0, ...value })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (amountNeeded < it.count) {
|
|
||||||
map[it.denomination] = 0
|
|
||||||
return response.push({ provisioned: amountNeeded, ...value })
|
|
||||||
}
|
|
||||||
|
|
||||||
map[it.denomination] -= it.count
|
|
||||||
return response.push({ provisioned: it.count, ...value })
|
|
||||||
}, cassettes)
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeChangeDuo(cassettes, amount) {
|
|
||||||
// Initialize empty cassettes in case of undefined, due to same denomination across all cassettes results in a single merged cassette
|
|
||||||
const small = cassettes[0] ?? { denomination: 0, count: 0 }
|
|
||||||
const large = cassettes[1] ?? { denomination: 0, count: 0 }
|
|
||||||
|
|
||||||
const largeDenom = large.denomination
|
|
||||||
const smallDenom = small.denomination
|
|
||||||
const largeBills = Math.min(large.count, Math.floor(amount / largeDenom))
|
|
||||||
const amountNum = amount.toNumber()
|
|
||||||
|
|
||||||
for (let i = largeBills; i >= 0; i--) {
|
|
||||||
const remainder = amountNum - largeDenom * i
|
|
||||||
|
|
||||||
if (remainder % smallDenom !== 0) continue
|
|
||||||
const smallCount = remainder / smallDenom
|
|
||||||
if (smallCount > small.count) continue
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
provisioned: smallCount,
|
|
||||||
denomination: small.denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
},
|
|
||||||
{ provisioned: i, denomination: largeDenom, id: uuid.v4() }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeChange(outCassettes, amount) {
|
function makeChange(outCassettes, amount) {
|
||||||
const available = _.reduce(
|
const solution = getSolution(outCassettes, amount)
|
||||||
(res, val) => res + val.count * val.denomination,
|
return solutionToOriginalUnits(solution, outCassettes)
|
||||||
0,
|
|
||||||
outCassettes
|
|
||||||
)
|
|
||||||
|
|
||||||
if (available < amount) {
|
|
||||||
console.log(`Tried to dispense more than was available for amount ${amount.toNumber()} with cassettes ${JSON.stringify(outCassettes)}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const cassettes = mergeCassettes(outCassettes)
|
|
||||||
const result =
|
|
||||||
_.size(cassettes) >= 3
|
|
||||||
? makeChangeDynamic(cassettes, amount, available)
|
|
||||||
: makeChangeDuo(cassettes, amount)
|
|
||||||
|
|
||||||
if (!result.length) return null
|
|
||||||
return unmergeCassettes(outCassettes, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeChangeDynamicBruteForce(outCassettes, amount, available) {
|
|
||||||
const solutions = []
|
|
||||||
let x = 0
|
|
||||||
|
|
||||||
const shouldFlip = amount > _.max(_.map(it => it.denomination * it.count, outCassettes))
|
|
||||||
const amountNum = shouldFlip ? available - amount : amount
|
|
||||||
|
|
||||||
const cassettes = shouldFlip ? _.reverse(outCassettes) : outCassettes
|
|
||||||
const { denomination: denomination0, count: count0 } = cassettes[0]
|
|
||||||
const { denomination: denomination1, count: count1 } = cassettes[1]
|
|
||||||
const { denomination: denomination2, count: count2 } = cassettes[2]
|
|
||||||
const { denomination: denomination3, count: count3 } = cassettes[3]
|
|
||||||
|
|
||||||
const startTime = new Date().getTime()
|
|
||||||
|
|
||||||
loop1: for (let i = 0; i <= count0; i++) {
|
|
||||||
const firstSum = i * denomination0
|
|
||||||
|
|
||||||
for (let j = 0; j <= count1; j++) {
|
|
||||||
const secondSum = firstSum + j * denomination1
|
|
||||||
if (secondSum > amountNum) break
|
|
||||||
|
|
||||||
if (secondSum === amountNum) {
|
|
||||||
solutions.push(newSolution(cassettes, i, j, 0, 0, shouldFlip))
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let k = 0; k <= count2; k++) {
|
|
||||||
const thirdSum = secondSum + k * denomination2
|
|
||||||
if (thirdSum > amountNum) break
|
|
||||||
|
|
||||||
if (denomination2 === 0) break
|
|
||||||
|
|
||||||
if (thirdSum === amountNum) {
|
|
||||||
solutions.push(newSolution(cassettes, i, j, k, 0, shouldFlip))
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let l = 0; l <= count3; l++) {
|
|
||||||
if ((x > MAX_AMOUNT_OF_SOLUTIONS && solutions.length >= 1) || x > MAX_BRUTEFORCE_ITERATIONS) break loop1
|
|
||||||
x++
|
|
||||||
const fourthSum = thirdSum + l * denomination3
|
|
||||||
if (fourthSum > amountNum) break
|
|
||||||
|
|
||||||
if (denomination3 === 0) break
|
|
||||||
|
|
||||||
if (fourthSum === amountNum) {
|
|
||||||
solutions.push(newSolution(cassettes, i, j, k, l, shouldFlip))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const endTime = new Date().getTime()
|
|
||||||
|
|
||||||
console.log(`Exiting bruteforce after ${x} tries. Took ${endTime - startTime} ms`)
|
|
||||||
return solutions
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeChangeDynamic(cassettes, amount, available) {
|
|
||||||
while (_.size(cassettes) < 4) {
|
|
||||||
cassettes.push({ denomination: 0, count: 0 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const amountNum = amount.toNumber()
|
|
||||||
|
|
||||||
const solutions = makeChangeDynamicBruteForce(cassettes, amountNum, available)
|
|
||||||
|
|
||||||
const sortedSolutions = _.sortBy(it => {
|
|
||||||
const arr = []
|
|
||||||
|
|
||||||
for (let la = 0; la < 4; la++) {
|
|
||||||
arr.push(cassettes[la].count - it[la].provisioned)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arr.length < 2) return Infinity
|
|
||||||
return _.max(arr) - _.min(arr)
|
|
||||||
}, solutions)
|
|
||||||
|
|
||||||
const cleanSolution = _.filter(
|
|
||||||
it => it.denomination > 0,
|
|
||||||
_.head(sortedSolutions)
|
|
||||||
)
|
|
||||||
|
|
||||||
const response = cleanSolution
|
|
||||||
|
|
||||||
// Final sanity check
|
|
||||||
let total = 0
|
|
||||||
_.forEach(it => {
|
|
||||||
total += it.provisioned * it.denomination
|
|
||||||
}, response)
|
|
||||||
|
|
||||||
if (total === amountNum) return response
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Failed to find a solution for ${amountNum} with cassettes ${JSON.stringify(cassettes)}`
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { makeChange }
|
module.exports = { makeChange }
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,11 @@ function mapDispense (tx) {
|
||||||
const res = {}
|
const res = {}
|
||||||
|
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
res[`provisioned_${it + 1}`] = bills[it].provisioned
|
const suffix = bills[it].name.replace(/cassette|stacker/gi, '')
|
||||||
res[`denomination_${it + 1}`] = bills[it].denomination
|
res[`provisioned_${suffix}`] = bills[it].provisioned
|
||||||
res[`dispensed_${it + 1}`] = bills[it].dispensed
|
res[`denomination_${suffix}`] = bills[it].denomination
|
||||||
res[`rejected_${it + 1}`] = bills[it].rejected
|
res[`dispensed_${suffix}`] = bills[it].dispensed
|
||||||
|
res[`rejected_${suffix}`] = bills[it].rejected
|
||||||
}, _.times(_.identity(), _.size(bills)))
|
}, _.times(_.identity(), _.size(bills)))
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,32 @@ case
|
||||||
else 'Pending'
|
else 'Pending'
|
||||||
end`
|
end`
|
||||||
|
|
||||||
|
const MAX_CASSETTES = 4
|
||||||
|
const MAX_STACKERS = 3
|
||||||
|
|
||||||
|
const BILL_FIELDS = [
|
||||||
|
'denomination1',
|
||||||
|
'denomination2',
|
||||||
|
'denomination3',
|
||||||
|
'denomination4',
|
||||||
|
'denomination1f',
|
||||||
|
'denomination1r',
|
||||||
|
'denomination2f',
|
||||||
|
'denomination2r',
|
||||||
|
'denomination3f',
|
||||||
|
'denomination3r',
|
||||||
|
'provisioned1',
|
||||||
|
'provisioned2',
|
||||||
|
'provisioned3',
|
||||||
|
'provisioned4',
|
||||||
|
'provisioned1f',
|
||||||
|
'provisioned1r',
|
||||||
|
'provisioned2f',
|
||||||
|
'provisioned2r',
|
||||||
|
'provisioned3f',
|
||||||
|
'provisioned3r'
|
||||||
|
]
|
||||||
|
|
||||||
module.exports = { redeemableTxs, toObj, toDb, REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES }
|
module.exports = { redeemableTxs, toObj, toDb, REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES }
|
||||||
|
|
||||||
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
||||||
|
|
@ -43,23 +69,37 @@ function convertBigNumFields (obj) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertField (key) {
|
function convertField (key) {
|
||||||
return _.snakeCase(key)
|
return _.includes('denomination', key) || _.includes('provisioned', key) ? key : _.snakeCase(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDbBills (tx) {
|
function addDbBills (tx) {
|
||||||
const bills = tx.bills
|
const bills = tx.bills
|
||||||
if (_.isEmpty(bills)) return tx
|
if (_.isEmpty(bills)) return tx
|
||||||
|
|
||||||
const billsObj = {
|
const billFields = _.map(it => _.replace(/(denomination|provisioned)/g, '$1_')(it), BILL_FIELDS)
|
||||||
provisioned1: bills[0]?.provisioned ?? 0,
|
|
||||||
provisioned2: bills[1]?.provisioned ?? 0,
|
const billsObj = _.flow(
|
||||||
provisioned3: bills[2]?.provisioned ?? 0,
|
_.reduce(
|
||||||
provisioned4: bills[3]?.provisioned ?? 0,
|
(acc, value) => {
|
||||||
denomination1: bills[0]?.denomination ?? 0,
|
const suffix = value.name.replace(/cassette|stacker/gi, '')
|
||||||
denomination2: bills[1]?.denomination ?? 0,
|
return {
|
||||||
denomination3: bills[2]?.denomination ?? 0,
|
...acc,
|
||||||
denomination4: bills[3]?.denomination ?? 0
|
[`provisioned_${suffix}`]: value.provisioned,
|
||||||
}
|
[`denomination_${suffix}`]: value.denomination
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
it => {
|
||||||
|
const missingKeys = _.reduce(
|
||||||
|
(acc, value) => {
|
||||||
|
return _.assign({ [value]: 0 })(acc)
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)(_.difference(billFields, _.keys(it)))
|
||||||
|
return _.assign(missingKeys, it)
|
||||||
|
}
|
||||||
|
)(bills)
|
||||||
|
|
||||||
return _.assign(tx, billsObj)
|
return _.assign(tx, billsObj)
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +118,7 @@ function toObj (row) {
|
||||||
let newObj = {}
|
let newObj = {}
|
||||||
|
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
const objKey = _.camelCase(key)
|
const objKey = key.match(/denomination|provisioned/g) ? key.replace(/_/g, '') : _.camelCase(key)
|
||||||
if (key === 'received_crypto_atoms' && row[key]) {
|
if (key === 'received_crypto_atoms' && row[key]) {
|
||||||
newObj[objKey] = new BN(row[key])
|
newObj[objKey] = new BN(row[key])
|
||||||
return
|
return
|
||||||
|
|
@ -93,35 +133,28 @@ function toObj (row) {
|
||||||
|
|
||||||
newObj.direction = 'cashOut'
|
newObj.direction = 'cashOut'
|
||||||
|
|
||||||
const billFields = ['denomination1', 'denomination2', 'denomination3', 'denomination4', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4']
|
if (_.every(_.isNil, _.at(BILL_FIELDS, newObj))) return newObj
|
||||||
|
if (_.some(_.isNil, _.at(BILL_FIELDS, newObj))) throw new Error('Missing cassette values')
|
||||||
|
|
||||||
if (_.every(_.isNil, _.at(billFields, newObj))) return newObj
|
const billFieldsArr = _.concat(
|
||||||
if (_.some(_.isNil, _.at(billFields, newObj))) throw new Error('Missing cassette values')
|
_.map(it => ({ name: `cassette${it + 1}`, denomination: newObj[`denomination${it + 1}`], provisioned: newObj[`provisioned${it + 1}`] }))(_.range(0, MAX_CASSETTES)),
|
||||||
|
_.reduce(
|
||||||
const billFieldsArr = [
|
(acc, value) => {
|
||||||
{
|
acc.push(
|
||||||
denomination: newObj.denomination1,
|
{ name: `stacker${value + 1}f`, denomination: newObj[`denomination${value + 1}f`], provisioned: newObj[`provisioned${value + 1}f`] },
|
||||||
provisioned: newObj.provisioned1
|
{ name: `stacker${value + 1}r`, denomination: newObj[`denomination${value + 1}r`], provisioned: newObj[`provisioned${value + 1}r`] }
|
||||||
},
|
)
|
||||||
{
|
return acc
|
||||||
denomination: newObj.denomination2,
|
},
|
||||||
provisioned: newObj.provisioned2
|
[]
|
||||||
},
|
)(_.range(0, MAX_STACKERS))
|
||||||
{
|
)
|
||||||
denomination: newObj.denomination3,
|
|
||||||
provisioned: newObj.provisioned3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
denomination: newObj.denomination4,
|
|
||||||
provisioned: newObj.provisioned4
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// There can't be bills with denomination === 0.
|
// There can't be bills with denomination === 0.
|
||||||
// If a bill has denomination === 0, then that cassette is not set and should be filtered out.
|
// If a bill has denomination === 0, then that cassette is not set and should be filtered out.
|
||||||
const bills = _.filter(it => it.denomination > 0, billFieldsArr)
|
const bills = _.filter(it => it.denomination > 0, billFieldsArr)
|
||||||
|
|
||||||
return _.set('bills', bills, _.omit(billFields, newObj))
|
return _.set('bills', bills, _.omit(BILL_FIELDS, newObj))
|
||||||
}
|
}
|
||||||
|
|
||||||
function redeemableTxs (deviceId) {
|
function redeemableTxs (deviceId) {
|
||||||
|
|
@ -129,7 +162,10 @@ function redeemableTxs (deviceId) {
|
||||||
where device_id=$1
|
where device_id=$1
|
||||||
and redeem=$2
|
and redeem=$2
|
||||||
and dispense=$3
|
and dispense=$3
|
||||||
and provisioned_1 is not null
|
and (
|
||||||
|
provisioned_1 is not null or provisioned_2 is not null or provisioned_3 is not null or provisioned_4 is not null or
|
||||||
|
provisioned_1f is not null or provisioned_1r is not null or provisioned_2f is not null or provisioned_2r is not null or provisioned_3f is not null or provisioned_3r is not null
|
||||||
|
)
|
||||||
and extract(epoch from (now() - greatest(created, confirmed_at))) < $4`
|
and extract(epoch from (now() - greatest(created, confirmed_at))) < $4`
|
||||||
|
|
||||||
return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE])
|
return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE])
|
||||||
|
|
|
||||||
|
|
@ -56,14 +56,15 @@ function postProcess (txVector, justAuthorized, pi) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) {
|
if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) {
|
||||||
return pi.buildAvailableCassettes(newTx.id)
|
return pi.buildAvailableUnits(newTx.id)
|
||||||
.then(cassettes => {
|
.then(_units => {
|
||||||
|
const units = _.concat(_units.cassettes, _units.stackers)
|
||||||
logger.silly('Computing bills to dispense:', {
|
logger.silly('Computing bills to dispense:', {
|
||||||
txId: newTx.id,
|
txId: newTx.id,
|
||||||
cassettes: cassettes.cassettes,
|
units: units,
|
||||||
fiat: newTx.fiat
|
fiat: newTx.fiat
|
||||||
})
|
})
|
||||||
const bills = billMath.makeChange(cassettes.cassettes, newTx.fiat)
|
const bills = billMath.makeChange(units, newTx.fiat)
|
||||||
logger.silly('Bills to dispense:', JSON.stringify(bills))
|
logger.silly('Bills to dispense:', JSON.stringify(bills))
|
||||||
|
|
||||||
if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE)
|
if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE)
|
||||||
|
|
@ -73,8 +74,9 @@ function postProcess (txVector, justAuthorized, pi) {
|
||||||
const rec = {}
|
const rec = {}
|
||||||
|
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
rec[`provisioned_${it + 1}`] = bills[it].provisioned
|
const suffix = bills[it].name.replace(/cassette|stacker/gi, '')
|
||||||
rec[`denomination_${it + 1}`] = bills[it].denomination
|
rec[`provisioned_${suffix}`] = bills[it].provisioned
|
||||||
|
rec[`denomination_${suffix}`] = bills[it].denomination
|
||||||
}, _.times(_.identity(), _.size(bills)))
|
}, _.times(_.identity(), _.size(bills)))
|
||||||
|
|
||||||
return cashOutActions.logAction(db, 'provisionNotes', rec, newTx)
|
return cashOutActions.logAction(db, 'provisionNotes', rec, newTx)
|
||||||
|
|
|
||||||
|
|
@ -165,11 +165,13 @@ type DynamicCoinValues {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PhysicalCassette {
|
type PhysicalCassette {
|
||||||
|
name: String!
|
||||||
denomination: Int!
|
denomination: Int!
|
||||||
count: Int!
|
count: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
type PhysicalStacker {
|
type PhysicalStacker {
|
||||||
|
name: String!
|
||||||
denomination: Int!
|
denomination: Int!
|
||||||
count: Int!
|
count: Int!
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -153,9 +153,11 @@ function advancedBatch (data) {
|
||||||
'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount',
|
'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount',
|
||||||
'dispense', 'notified', 'redeem', 'phone', 'error',
|
'dispense', 'notified', 'redeem', 'phone', 'error',
|
||||||
'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout',
|
'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout',
|
||||||
'dispenseConfirmed', 'provisioned1', 'provisioned2',
|
'dispenseConfirmed', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4',
|
||||||
'denomination1', 'denomination2', 'errorCode', 'customerId',
|
'provisioned1f', 'provisioned1r', 'provisioned2f', 'provisioned2r', 'provisioned3f', 'provisioned3r',
|
||||||
'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
|
'denomination1', 'denomination2', 'denomination3', 'denomination4',
|
||||||
|
'denomination1f', 'denomination1r', 'denomination2f', 'denomination2r', 'denomination3f', 'denomination3r',
|
||||||
|
'errorCode', 'customerId', 'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
|
||||||
'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms',
|
'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms',
|
||||||
'discount', 'txHash', 'customerPhone', 'customerIdCardDataNumber',
|
'discount', 'txHash', 'customerPhone', 'customerIdCardDataNumber',
|
||||||
'customerIdCardDataExpiration', 'customerIdCardData', 'customerName', 'sendTime',
|
'customerIdCardDataExpiration', 'customerIdCardData', 'customerName', 'sendTime',
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ const CA_PATH = process.env.CA_PATH
|
||||||
// A machine on an older version (no multicassette code) could be paired with a server with multicassette code.
|
// A machine on an older version (no multicassette code) could be paired with a server with multicassette code.
|
||||||
// This makes sure that the server stores a default value
|
// This makes sure that the server stores a default value
|
||||||
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
||||||
|
const DEFAULT_NUMBER_OF_STACKERS = 0
|
||||||
|
|
||||||
function pullToken (token) {
|
function pullToken (token) {
|
||||||
const sql = `delete from pairing_tokens
|
const sql = `delete from pairing_tokens
|
||||||
|
|
@ -36,16 +37,16 @@ function unpair (deviceId) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pair (token, deviceId, machineModel, numOfCassettes = DEFAULT_NUMBER_OF_CASSETTES) {
|
function pair (token, deviceId, machineModel, numOfCassettes = DEFAULT_NUMBER_OF_CASSETTES, numOfStackers = DEFAULT_NUMBER_OF_STACKERS) {
|
||||||
return pullToken(token)
|
return pullToken(token)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (r.expired) return false
|
if (r.expired) return false
|
||||||
|
|
||||||
const insertSql = `insert into devices (device_id, name, number_of_cassettes) values ($1, $2, $3)
|
const insertSql = `insert into devices (device_id, name, number_of_cassettes, number_of_stackers) values ($1, $2, $3)
|
||||||
on conflict (device_id)
|
on conflict (device_id)
|
||||||
do update set paired=TRUE, display=TRUE`
|
do update set paired=TRUE, display=TRUE`
|
||||||
|
|
||||||
return db.none(insertSql, [deviceId, r.name, numOfCassettes])
|
return db.none(insertSql, [deviceId, r.name, numOfCassettes, numOfStackers])
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
const sumTxs = (sum, tx) => {
|
const sumTxs = (sum, tx) => {
|
||||||
// cash-out-helper sends 0 as fallback value, need to filter it out as there are no '0' denominations
|
// cash-out-helper sends 0 as fallback value, need to filter it out as there are no '0' denominations
|
||||||
const bills = _.filter(it => it.denomination > 0, tx.bills)
|
const bills = _.filter(it => _.includes('cassette', it.name) && it.denomination > 0, tx.bills)
|
||||||
const sameDenominations = a => a[0]?.denomination === a[1]?.denomination
|
const sameDenominations = a => a[0]?.denomination === a[1]?.denomination
|
||||||
|
|
||||||
const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills))
|
const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills))
|
||||||
|
|
@ -139,6 +139,7 @@ function plugins (settings, deviceId) {
|
||||||
const computedCassettes = []
|
const computedCassettes = []
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
computedCassettes.push({
|
computedCassettes.push({
|
||||||
|
name: cassettes[it].name,
|
||||||
denomination: cassettes[it].denomination,
|
denomination: cassettes[it].denomination,
|
||||||
count: counts[it]
|
count: counts[it]
|
||||||
})
|
})
|
||||||
|
|
@ -152,7 +153,7 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
const sumTxs = (sum, tx) => {
|
const sumTxs = (sum, tx) => {
|
||||||
// cash-out-helper sends 0 as fallback value, need to filter it out as there are no '0' denominations
|
// cash-out-helper sends 0 as fallback value, need to filter it out as there are no '0' denominations
|
||||||
const bills = _.filter(it => it.denomination > 0, tx.bills)
|
const bills = _.filter(it => _.includes('stacker', it.name) && it.denomination > 0, tx.bills)
|
||||||
const sameDenominations = a => a[0]?.denomination === a[1]?.denomination
|
const sameDenominations = a => a[0]?.denomination === a[1]?.denomination
|
||||||
|
|
||||||
const doDenominationsMatch = _.every(sameDenominations, _.zip(stackers, bills))
|
const doDenominationsMatch = _.every(sameDenominations, _.zip(stackers, bills))
|
||||||
|
|
@ -175,6 +176,7 @@ function plugins (settings, deviceId) {
|
||||||
const computedStackers = []
|
const computedStackers = []
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
computedStackers.push({
|
computedStackers.push({
|
||||||
|
name: stackers[it].name,
|
||||||
denomination: stackers[it].denomination,
|
denomination: stackers[it].denomination,
|
||||||
count: counts[it]
|
count: counts[it]
|
||||||
})
|
})
|
||||||
|
|
@ -210,6 +212,7 @@ function plugins (settings, deviceId) {
|
||||||
const cassettes = []
|
const cassettes = []
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
cassettes.push({
|
cassettes.push({
|
||||||
|
name: `cassette${it + 1}`,
|
||||||
denomination: parseInt(denominations[it], 10),
|
denomination: parseInt(denominations[it], 10),
|
||||||
count: parseInt(counts[it], 10)
|
count: parseInt(counts[it], 10)
|
||||||
})
|
})
|
||||||
|
|
@ -241,10 +244,10 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
const denominations = []
|
const denominations = []
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
denominations.push(cashOutConfig[`stacker${it + 1}f`], cashOutConfig[`stacker${it + 1}r`])
|
denominations.push([cashOutConfig[`stacker${it + 1}f`], cashOutConfig[`stacker${it + 1}r`]])
|
||||||
}, _.times(_.identity(), _stackers.numberOfStackers))
|
}, _.times(_.identity(), _stackers.numberOfStackers))
|
||||||
|
|
||||||
const virtualStackers = [Math.max(...denominations) * 2]
|
const virtualStackers = [Math.max(..._.flatten(denominations)) * 2]
|
||||||
|
|
||||||
const counts = _stackers.counts
|
const counts = _stackers.counts
|
||||||
|
|
||||||
|
|
@ -255,10 +258,16 @@ function plugins (settings, deviceId) {
|
||||||
const stackers = []
|
const stackers = []
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
stackers.push({
|
stackers.push({
|
||||||
denomination: parseInt(denominations[it], 10),
|
name: `stacker${it + 1}f`,
|
||||||
count: parseInt(counts[it], 10)
|
denomination: parseInt(denominations[it][0], 10),
|
||||||
|
count: parseInt(counts[it][0], 10)
|
||||||
})
|
})
|
||||||
}, _.times(_.identity(), _stackers.numberOfStackers * 2))
|
stackers.push({
|
||||||
|
name: `stacker${it + 1}r`,
|
||||||
|
denomination: parseInt(denominations[it][1], 10),
|
||||||
|
count: parseInt(counts[it][1], 10)
|
||||||
|
})
|
||||||
|
}, _.times(_.identity(), _stackers.numberOfStackers))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return {
|
return {
|
||||||
|
|
@ -275,6 +284,11 @@ function plugins (settings, deviceId) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildAvailableUnits (excludeTxId) {
|
||||||
|
return Promise.all([buildAvailableCassettes(excludeTxId), buildAvailableStackers(excludeTxId)])
|
||||||
|
.then(([cassettes, stackers]) => ({ cassettes: cassettes.cassettes, stackers: stackers.stackers }))
|
||||||
|
}
|
||||||
|
|
||||||
function fetchCurrentConfigVersion () {
|
function fetchCurrentConfigVersion () {
|
||||||
const sql = `select id from user_config
|
const sql = `select id from user_config
|
||||||
where type=$1
|
where type=$1
|
||||||
|
|
@ -1051,7 +1065,6 @@ function plugins (settings, deviceId) {
|
||||||
sendMessage,
|
sendMessage,
|
||||||
checkBalances,
|
checkBalances,
|
||||||
getMachineNames,
|
getMachineNames,
|
||||||
buildAvailableCassettes,
|
|
||||||
buy,
|
buy,
|
||||||
sell,
|
sell,
|
||||||
getNotificationConfig,
|
getNotificationConfig,
|
||||||
|
|
@ -1062,7 +1075,8 @@ function plugins (settings, deviceId) {
|
||||||
isValidWalletScore,
|
isValidWalletScore,
|
||||||
getTransactionHash,
|
getTransactionHash,
|
||||||
getInputAddresses,
|
getInputAddresses,
|
||||||
isWalletScoringEnabled
|
isWalletScoringEnabled,
|
||||||
|
buildAvailableUnits
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ exports.stackerCounts = function stackerCounts (deviceId) {
|
||||||
.then(row => {
|
.then(row => {
|
||||||
const counts = []
|
const counts = []
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
counts.push(row[`stacker${it + 1}f`], row[`stacker${it + 1}r`])
|
counts.push([row[`stacker${it + 1}f`], row[`stacker${it + 1}r`]])
|
||||||
}, _.times(_.identity(), row.number_of_stackers))
|
}, _.times(_.identity(), row.number_of_stackers))
|
||||||
|
|
||||||
return { numberOfStackers: row.number_of_stackers, counts }
|
return { numberOfStackers: row.number_of_stackers, counts }
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,9 @@ function pair (req, res, next) {
|
||||||
const deviceId = req.deviceId
|
const deviceId = req.deviceId
|
||||||
const model = req.query.model
|
const model = req.query.model
|
||||||
const numOfCassettes = req.query.numOfCassettes
|
const numOfCassettes = req.query.numOfCassettes
|
||||||
|
const numOfStackers = req.query.numOfStackers
|
||||||
|
|
||||||
return pairing.pair(token, deviceId, model, numOfCassettes)
|
return pairing.pair(token, deviceId, model, numOfCassettes, numOfStackers)
|
||||||
.then(isValid => {
|
.then(isValid => {
|
||||||
if (isValid) return res.json({ status: 'paired' })
|
if (isValid) return res.json({ status: 'paired' })
|
||||||
throw httpError('Pairing failed')
|
throw httpError('Pairing failed')
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ exports.up = function (next) {
|
||||||
ADD COLUMN stacker2f INTEGER NOT NULL DEFAULT 0,
|
ADD COLUMN stacker2f INTEGER NOT NULL DEFAULT 0,
|
||||||
ADD COLUMN stacker2r INTEGER NOT NULL DEFAULT 0,
|
ADD COLUMN stacker2r INTEGER NOT NULL DEFAULT 0,
|
||||||
ADD COLUMN stacker3f INTEGER NOT NULL DEFAULT 0,
|
ADD COLUMN stacker3f INTEGER NOT NULL DEFAULT 0,
|
||||||
ADD COLUMN stacker3r INTEGER NOT NULL DEFAULT 0
|
ADD COLUMN stacker3r INTEGER NOT NULL DEFAULT 0,
|
||||||
ADD COLUMN number_of_stackers INTEGER NOT NULL DEFAULT 0`,
|
ADD COLUMN number_of_stackers INTEGER NOT NULL DEFAULT 0`,
|
||||||
`ALTER TABLE cash_out_txs
|
`ALTER TABLE cash_out_txs
|
||||||
ADD COLUMN provisioned_1f INTEGER,
|
ADD COLUMN provisioned_1f INTEGER,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { DialogActions, makeStyles, Box } from '@material-ui/core'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import * as Yup from 'yup'
|
|
||||||
|
|
||||||
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
|
|
@ -28,76 +27,6 @@ import helper from './helper'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const ValidationSchema = Yup.object().shape({
|
|
||||||
name: Yup.string().required(),
|
|
||||||
cashbox: Yup.number()
|
|
||||||
.label('Cash box')
|
|
||||||
.required()
|
|
||||||
.integer()
|
|
||||||
.min(0)
|
|
||||||
.max(1000),
|
|
||||||
cassette1: Yup.number()
|
|
||||||
.label('Cassette 1')
|
|
||||||
.required()
|
|
||||||
.integer()
|
|
||||||
.min(0)
|
|
||||||
.max(500),
|
|
||||||
cassette2: Yup.number()
|
|
||||||
.label('Cassette 2')
|
|
||||||
.required()
|
|
||||||
.integer()
|
|
||||||
.min(0)
|
|
||||||
.max(500),
|
|
||||||
cassette3: Yup.number()
|
|
||||||
.label('Cassette 3')
|
|
||||||
.required()
|
|
||||||
.integer()
|
|
||||||
.min(0)
|
|
||||||
.max(500),
|
|
||||||
cassette4: Yup.number()
|
|
||||||
.label('Cassette 4')
|
|
||||||
.required()
|
|
||||||
.integer()
|
|
||||||
.min(0)
|
|
||||||
.max(500),
|
|
||||||
stacker1f: Yup.number()
|
|
||||||
.label('Stacker 1F')
|
|
||||||
.required('Required')
|
|
||||||
.integer()
|
|
||||||
.min(0)
|
|
||||||
.max(60),
|
|
||||||
stacker1r: Yup.number()
|
|
||||||
.label('Stacker 1R')
|
|
||||||
.required('Required')
|
|
||||||
.integer()
|
|
||||||
.min(0)
|
|
||||||
.max(60),
|
|
||||||
stacker2f: Yup.number()
|
|
||||||
.label('Stacker 2F')
|
|
||||||
.required('Required')
|
|
||||||
.integer()
|
|
||||||
.min(0)
|
|
||||||
.max(60),
|
|
||||||
stacker2r: Yup.number()
|
|
||||||
.label('Stacker 2R')
|
|
||||||
.required('Required')
|
|
||||||
.integer()
|
|
||||||
.min(0)
|
|
||||||
.max(60),
|
|
||||||
stacker3f: Yup.number()
|
|
||||||
.label('Stacker 3F')
|
|
||||||
.required('Required')
|
|
||||||
.integer()
|
|
||||||
.min(0)
|
|
||||||
.max(60),
|
|
||||||
stacker3r: Yup.number()
|
|
||||||
.label('Stacker 3R')
|
|
||||||
.required('Required')
|
|
||||||
.integer()
|
|
||||||
.min(0)
|
|
||||||
.max(60)
|
|
||||||
})
|
|
||||||
|
|
||||||
const GET_MACHINES_AND_CONFIG = gql`
|
const GET_MACHINES_AND_CONFIG = gql`
|
||||||
query getData($billFilters: JSONObject) {
|
query getData($billFilters: JSONObject) {
|
||||||
machines {
|
machines {
|
||||||
|
|
@ -327,7 +256,6 @@ const CashCassettes = () => {
|
||||||
stripeWhen={isCashOutDisabled}
|
stripeWhen={isCashOutDisabled}
|
||||||
elements={nonStackerElements}
|
elements={nonStackerElements}
|
||||||
data={nonStackerMachines}
|
data={nonStackerMachines}
|
||||||
validationSchema={ValidationSchema}
|
|
||||||
tbodyWrapperClass={classes.tBody}
|
tbodyWrapperClass={classes.tBody}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -337,7 +265,6 @@ const CashCassettes = () => {
|
||||||
stripeWhen={isCashOutDisabled}
|
stripeWhen={isCashOutDisabled}
|
||||||
elements={stackerElements}
|
elements={stackerElements}
|
||||||
data={stackerMachines}
|
data={stackerMachines}
|
||||||
validationSchema={ValidationSchema}
|
|
||||||
tbodyWrapperClass={classes.tBody}
|
tbodyWrapperClass={classes.tBody}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { CashOut, CashIn } from 'src/components/inputs/cashbox/Cashbox'
|
||||||
import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik'
|
import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik'
|
||||||
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
||||||
import { fromNamespace } from 'src/utils/config'
|
import { fromNamespace } from 'src/utils/config'
|
||||||
|
import { cashUnitCapacity } from 'src/utils/machine'
|
||||||
|
|
||||||
const widthsByCashUnits = {
|
const widthsByCashUnits = {
|
||||||
2: {
|
2: {
|
||||||
|
|
@ -121,12 +122,13 @@ const getElements = (
|
||||||
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
|
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
|
||||||
stripe: true,
|
stripe: true,
|
||||||
doubleHeader: 'Cash-out',
|
doubleHeader: 'Cash-out',
|
||||||
view: (_, { id, cashUnits }) => (
|
view: (_, { id, model, cashUnits }) => (
|
||||||
<CashOut
|
<CashOut
|
||||||
className={classes.cashbox}
|
className={classes.cashbox}
|
||||||
denomination={getCashoutSettings(id)?.[`cassette${it}`]}
|
denomination={getCashoutSettings(id)?.[`cassette${it}`]}
|
||||||
currency={{ code: fiatCurrency }}
|
currency={{ code: fiatCurrency }}
|
||||||
notes={cashUnits[`cassette${it}`]}
|
notes={cashUnits[`cassette${it}`]}
|
||||||
|
capacity={cashUnitCapacity[model].cassette}
|
||||||
width={
|
width={
|
||||||
widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph
|
widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph
|
||||||
}
|
}
|
||||||
|
|
@ -158,12 +160,13 @@ const getElements = (
|
||||||
header: `Stacker ${it}F`,
|
header: `Stacker ${it}F`,
|
||||||
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
|
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
|
||||||
stripe: true,
|
stripe: true,
|
||||||
view: (_, { id, cashUnits }) => (
|
view: (_, { id, model, cashUnits }) => (
|
||||||
<CashOut
|
<CashOut
|
||||||
className={classes.cashbox}
|
className={classes.cashbox}
|
||||||
denomination={getCashoutSettings(id)?.[`stacker${it}f`]}
|
denomination={getCashoutSettings(id)?.[`stacker${it}f`]}
|
||||||
currency={{ code: fiatCurrency }}
|
currency={{ code: fiatCurrency }}
|
||||||
notes={cashUnits[`stacker${it}f`]}
|
notes={cashUnits[`stacker${it}f`]}
|
||||||
|
capacity={cashUnitCapacity[model].stacker}
|
||||||
width={
|
width={
|
||||||
widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph
|
widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph
|
||||||
}
|
}
|
||||||
|
|
@ -186,12 +189,13 @@ const getElements = (
|
||||||
header: `Stacker ${it}R`,
|
header: `Stacker ${it}R`,
|
||||||
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
|
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
|
||||||
stripe: true,
|
stripe: true,
|
||||||
view: (_, { id, cashUnits }) => (
|
view: (_, { id, model, cashUnits }) => (
|
||||||
<CashOut
|
<CashOut
|
||||||
className={classes.cashbox}
|
className={classes.cashbox}
|
||||||
denomination={getCashoutSettings(id)?.[`stacker${it}r`]}
|
denomination={getCashoutSettings(id)?.[`stacker${it}r`]}
|
||||||
currency={{ code: fiatCurrency }}
|
currency={{ code: fiatCurrency }}
|
||||||
notes={cashUnits[`stacker${it}r`]}
|
notes={cashUnits[`stacker${it}r`]}
|
||||||
|
capacity={cashUnitCapacity[model].stacker}
|
||||||
width={
|
width={
|
||||||
widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph
|
widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph
|
||||||
}
|
}
|
||||||
|
|
|
||||||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -6079,6 +6079,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@haensl/subset-sum": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@haensl/subset-sum/-/subset-sum-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-ySEbozvn6tzZNemM+3Sm2ZBkALuwzTQnhlIhA6Sw5Ja55QOPeEtZJMtR+TqHCvxdhfP61I9XxXpqZVlyvgvcqw==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.11.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@hapi/hoek": {
|
"@hapi/hoek": {
|
||||||
"version": "9.3.0",
|
"version": "9.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"@ethereumjs/common": "^2.6.4",
|
"@ethereumjs/common": "^2.6.4",
|
||||||
"@ethereumjs/tx": "^3.5.1",
|
"@ethereumjs/tx": "^3.5.1",
|
||||||
"@graphql-tools/merge": "^6.2.5",
|
"@graphql-tools/merge": "^6.2.5",
|
||||||
|
"@haensl/subset-sum": "^3.0.5",
|
||||||
"@lamassu/coins": "1.3.0",
|
"@lamassu/coins": "1.3.0",
|
||||||
"@simplewebauthn/server": "^3.0.0",
|
"@simplewebauthn/server": "^3.0.0",
|
||||||
"apollo-server-express": "2.25.1",
|
"apollo-server-express": "2.25.1",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue