diff --git a/lib/coinatmradar/new-coinatmradar.js b/lib/coinatmradar/new-coinatmradar.js
new file mode 100644
index 00000000..5c590d11
--- /dev/null
+++ b/lib/coinatmradar/new-coinatmradar.js
@@ -0,0 +1,178 @@
+const axios = require('axios')
+const _ = require('lodash/fp')
+const hkdf = require('futoin-hkdf')
+
+const pify = require('pify')
+const fs = pify(require('fs'))
+
+const db = require('../db')
+const mnemonicHelpers = require('../mnemonic-helpers')
+const configManager = require('../config-manager')
+const options = require('../options')
+const logger = require('../logger')
+const plugins = require('../plugins')
+
+const TIMEOUT = 10000
+const MAX_CONTENT_LENGTH = 2000
+
+// How long a machine can be down before it's considered offline
+const STALE_INTERVAL = '2 minutes'
+
+module.exports = { update, mapRecord }
+
+function mapCoin (info, deviceId, settings, cryptoCode) {
+ const config = info.config
+ const rates = plugins(settings, deviceId).buildRates(info.rates)[cryptoCode] || { cashIn: null, cashOut: null }
+ const cryptoConfig = configManager.scoped(cryptoCode, deviceId, config)
+ const unscoped = configManager.unscoped(config)
+ const showCommissions = unscoped.coinAtmRadar.sendCommissions
+
+ const cashInFee = showCommissions ? cryptoConfig.cashInCommission / 100 : null
+ const cashOutFee = showCommissions ? cryptoConfig.cashOutCommission / 100 : null
+ const cashInRate = showCommissions ? _.invoke('cashIn.toNumber', rates) : null
+ const cashOutRate = showCommissions ? _.invoke('cashOut.toNumber', rates) : null
+
+ return {
+ cryptoCode,
+ cashInFee,
+ cashOutFee,
+ cashInRate,
+ cashOutRate
+ }
+}
+
+function mapIdentification (info, deviceId) {
+ const machineConfig = configManager.machineScoped(deviceId, info.config)
+
+ return {
+ isPhone: machineConfig.smsVerificationActive,
+ isPalmVein: false,
+ isPhoto: false,
+ isIdDocScan: machineConfig.idCardDataVerificationActive,
+ isFingerprint: false
+ }
+}
+
+function mapMachine (info, settings, machineRow) {
+ const deviceId = machineRow.device_id
+ const config = info.config
+ const unscoped = configManager.unscoped(config)
+ const machineConfig = configManager.machineScoped(deviceId, config)
+
+ const lastOnline = machineRow.last_online.toISOString()
+ const status = machineRow.stale ? 'online' : 'offline'
+ const showSupportedCryptocurrencies =
+ unscoped.coinAtmRadar.sendSupportedCryptocurrencies
+ const showSupportedFiat =
+ unscoped.coinAtmRadar.sendSupportedFiat
+ const showSupportedBuySellDirection =
+ unscoped.coinAtmRadar.sendSupportedBuySellDirection
+ const showLimitsAndVerification =
+ unscoped.coinAtmRadar.sendLimitsAndVerification
+
+ const cashLimit = showLimitsAndVerification ? (
+ machineConfig.hardLimitVerificationActive
+ ? machineConfig.hardLimitVerificationThreshold
+ : Infinity ) : null
+
+ const cryptoCurrencies = machineConfig.cryptoCurrencies
+ const cashInEnabled = showSupportedBuySellDirection ? true : null
+ const cashOutEnabled = showSupportedBuySellDirection
+ ? machineConfig.cashOutEnabled
+ : null
+ const fiat = showSupportedFiat ? machineConfig.fiatCurrency : null
+ const identification = mapIdentification(info, deviceId)
+ const coins = showSupportedCryptocurrencies ?
+ _.map(_.partial(mapCoin, [info, deviceId, settings]), cryptoCurrencies)
+ : null
+
+ return {
+ machineId: deviceId,
+ address: {
+ streetAddress: null,
+ city: null,
+ region: null,
+ postalCode: null,
+ country: null
+ },
+ location: {
+ name: null,
+ url: null,
+ phone: null
+ },
+ status,
+ lastOnline,
+ cashIn: cashInEnabled,
+ cashOut: cashOutEnabled,
+ manufacturer: 'lamassu',
+ cashInTxLimit: cashLimit,
+ cashOutTxLimit: cashLimit,
+ cashInDailyLimit: cashLimit,
+ cashOutDailyLimit: cashLimit,
+ fiatCurrency: fiat,
+ identification,
+ coins
+ }
+}
+
+function getMachines (info, settings) {
+ const sql = `select device_id, last_online, now() - last_online < $1 as stale from devices
+ where display=TRUE and
+ paired=TRUE
+ order by created`
+
+ return db.any(sql, [STALE_INTERVAL])
+ .then(_.map(_.partial(mapMachine, [info, settings])))
+}
+
+function sendRadar (data) {
+ const url = _.get(['coinAtmRadar', 'url'], options)
+
+ if (_.isEmpty(url)) {
+ return Promise.reject(new Error('Missing coinAtmRadar url!'))
+ }
+
+ const config = {
+ url,
+ method: 'post',
+ data,
+ timeout: TIMEOUT,
+ maxContentLength: MAX_CONTENT_LENGTH
+ }
+
+ console.log('%j', data)
+
+ return axios(config)
+ .then(r => console.log(r.status))
+}
+
+function mapRecord (info, settings) {
+ const timestamp = new Date().toISOString()
+ return Promise.all([getMachines(info, settings), fs.readFile(options.mnemonicPath, 'utf8')])
+ .then(([machines, mnemonic]) => {
+ return {
+ operatorId: computeOperatorId(mnemonicHelpers.toEntropyBuffer(mnemonic)),
+ operator: {
+ name: null,
+ phone: null,
+ email: null
+ },
+ timestamp,
+ machines
+ }
+ })
+}
+
+function update (info, settings) {
+ const config = configManager.unscoped(info.config)
+
+ if (!config.coinAtmRadar.active) return Promise.resolve()
+
+ return mapRecord(info, settings)
+ .then(sendRadar)
+ .catch(err => logger.error(`Failure to update CoinATMRadar`, err))
+}
+
+function computeOperatorId (masterSeed) {
+ return hkdf(masterSeed, 16, { salt: 'lamassu-server-salt', info: 'operator-id' }).toString('hex')
+}
diff --git a/new-lamassu-admin/src/components/booleanPropertiesTable/BooleanPropertiesTable.js b/new-lamassu-admin/src/components/booleanPropertiesTable/BooleanPropertiesTable.js
new file mode 100644
index 00000000..306e5efe
--- /dev/null
+++ b/new-lamassu-admin/src/components/booleanPropertiesTable/BooleanPropertiesTable.js
@@ -0,0 +1,112 @@
+import { makeStyles } from '@material-ui/core/styles'
+import React, { useState, memo } from 'react'
+
+import { H4 } from 'src/components/typography'
+import { Link } from 'src/components/buttons'
+import { RadioGroup } from 'src/components/inputs'
+import { Table, TableBody, TableRow, TableCell } from 'src/components/table'
+import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
+import { ReactComponent as EditIconDisabled } from 'src/styling/icons/action/edit/disabled.svg'
+import { ReactComponent as TrueIcon } from 'src/styling/icons/table/true.svg'
+import { ReactComponent as FalseIcon } from 'src/styling/icons/table/false.svg'
+
+import { booleanPropertiesTableStyles } from './BooleanPropertiesTable.styles'
+
+const useStyles = makeStyles(booleanPropertiesTableStyles)
+
+const BooleanPropertiesTable = memo(
+ ({ title, disabled, data, elements, save }) => {
+ const [editing, setEditing] = useState(false)
+ const [radioGroupValues, setRadioGroupValues] = useState(elements)
+
+ const classes = useStyles()
+
+ const innerSave = () => {
+ radioGroupValues.forEach(element => {
+ data[element.name] = element.value
+ })
+
+ save(data)
+ setEditing(false)
+ }
+
+ const innerCancel = () => {
+ setEditing(false)
+ }
+
+ const handleRadioButtons = (elementName, newValue) => {
+ setRadioGroupValues(
+ radioGroupValues.map(element =>
+ element.name === elementName
+ ? { ...element, value: newValue }
+ : element
+ )
+ )
+ }
+
+ const radioButtonOptions = [
+ { label: 'Yes', value: true },
+ { label: 'No', value: false }
+ ]
+
+ if (!elements || radioGroupValues?.length === 0) return null
+
+ return (
+
+
+
{title}
+ {editing ? (
+
+
+ Cancel
+
+
+ Save
+
+
+ ) : (
+
+ setEditing(true)}>
+ {disabled ? : }
+
+
+ )}
+
+
+
+ {radioGroupValues &&
+ radioGroupValues.map((element, idx) => (
+
+
+ {element.display}
+ {editing ? (
+
+ handleRadioButtons(
+ element.name,
+ event.target.value === 'true'
+ )
+ }
+ className={classes.radioButtons}
+ />
+ ) : element.value ? (
+
+ ) : (
+
+ )}
+
+
+ ))}
+
+
+
+ )
+ }
+)
+
+export default BooleanPropertiesTable
diff --git a/new-lamassu-admin/src/components/booleanPropertiesTable/BooleanPropertiesTable.styles.js b/new-lamassu-admin/src/components/booleanPropertiesTable/BooleanPropertiesTable.styles.js
new file mode 100644
index 00000000..18a1d49d
--- /dev/null
+++ b/new-lamassu-admin/src/components/booleanPropertiesTable/BooleanPropertiesTable.styles.js
@@ -0,0 +1,68 @@
+import baseStyles from 'src/pages/Logs.styles'
+import { tableCellColor, zircon } from 'src/styling/variables'
+
+const { fillColumn } = baseStyles
+
+const booleanPropertiesTableStyles = {
+ booleanPropertiesTableWrapper: {
+ display: 'flex',
+ flexDirection: 'column',
+ width: 396
+ },
+ tableRow: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ '&:nth-child(even)': {
+ backgroundColor: tableCellColor
+ },
+ '&:nth-child(odd)': {
+ backgroundColor: zircon
+ },
+ boxShadow: '0 0 0 0 rgba(0, 0, 0, 0)'
+ },
+ tableCell: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ width: '100%',
+ height: 32,
+ padding: [[5, 14, 5, 20]]
+ },
+ transparentButton: {
+ '& > *': {
+ margin: 'auto 12px'
+ },
+ '& button': {
+ border: 'none',
+ backgroundColor: 'transparent',
+ cursor: 'pointer'
+ }
+ },
+ rowWrapper: {
+ display: 'flex',
+ alignItems: 'center',
+ position: 'relative',
+ flex: 'wrap'
+ },
+ rightAligned: {
+ display: 'flex',
+ position: 'absolute',
+ right: 0
+ },
+ radioButtons: {
+ display: 'flex',
+ flexDirection: 'row',
+ marginRight: -15
+ },
+ rightLink: {
+ marginLeft: '20px'
+ },
+ fillColumn,
+ popoverContent: {
+ width: 272,
+ padding: [[10, 15]]
+ }
+}
+
+export { booleanPropertiesTableStyles }
diff --git a/new-lamassu-admin/src/components/booleanPropertiesTable/index.js b/new-lamassu-admin/src/components/booleanPropertiesTable/index.js
new file mode 100644
index 00000000..3112ae66
--- /dev/null
+++ b/new-lamassu-admin/src/components/booleanPropertiesTable/index.js
@@ -0,0 +1,3 @@
+import BooleanPropertiesTable from './BooleanPropertiesTable'
+
+export { BooleanPropertiesTable }
diff --git a/new-lamassu-admin/src/pages/OperatorInfo/CoinATMRadar/CoinATMRadar.js b/new-lamassu-admin/src/pages/OperatorInfo/CoinATMRadar/CoinATMRadar.js
new file mode 100644
index 00000000..02bffbc1
--- /dev/null
+++ b/new-lamassu-admin/src/pages/OperatorInfo/CoinATMRadar/CoinATMRadar.js
@@ -0,0 +1,193 @@
+import { makeStyles } from '@material-ui/core/styles'
+import React, { useState, memo } from 'react'
+import { useQuery, useMutation } from '@apollo/react-hooks'
+import { gql } from 'apollo-boost'
+
+import { BooleanPropertiesTable } from 'src/components/booleanPropertiesTable'
+import { H4, P, Label2 } from 'src/components/typography'
+import { Button } from 'src/components/buttons'
+import Popper from 'src/components/Popper'
+import { Switch } from 'src/components/inputs'
+import { ReactComponent as HelpIcon } from 'src/styling/icons/action/help/zodiac.svg'
+
+import { mainStyles } from './CoinATMRadar.styles'
+
+const useStyles = makeStyles(mainStyles)
+
+const initialValues = {
+ active: false,
+ // location: false,
+ commissions: false,
+ supportedCryptocurrencies: false,
+ supportedFiat: false,
+ supportedBuySellDirection: false,
+ limitsAndVerification: false
+ // operatorName: false,
+ // operatorPhoneNumber: false,
+ // operatorEmail: false
+}
+
+const GET_CONFIG = gql`
+ {
+ config
+ }
+`
+
+const SAVE_CONFIG = gql`
+ mutation Save($config: JSONObject) {
+ saveConfig(config: $config)
+ }
+`
+
+const CoinATMRadar = memo(() => {
+ const [helpPopperAnchorEl, setHelpPopperAnchorEl] = useState(null)
+ const [coinAtmRadarConfig, setCoinAtmRadarConfig] = useState(null)
+
+ const classes = useStyles()
+
+ // TODO: treat errors on useMutation and useQuery
+ const [saveConfig] = useMutation(SAVE_CONFIG, {
+ onCompleted: configResponse =>
+ setCoinAtmRadarConfig(configResponse.saveConfig.coinAtmRadar)
+ })
+ useQuery(GET_CONFIG, {
+ onCompleted: configResponse => {
+ setCoinAtmRadarConfig(
+ configResponse?.config?.coinAtmRadar ?? initialValues
+ )
+ }
+ })
+
+ const save = it => saveConfig({ variables: { config: { coinAtmRadar: it } } })
+
+ const handleOpenHelpPopper = event => {
+ setHelpPopperAnchorEl(helpPopperAnchorEl ? null : event.currentTarget)
+ }
+
+ const handleCloseHelpPopper = () => {
+ setHelpPopperAnchorEl(null)
+ }
+
+ const helpPopperOpen = Boolean(helpPopperAnchorEl)
+
+ if (!coinAtmRadarConfig) return null
+
+ return (
+ <>
+
+
+
Coin ATM Radar share settings
+
+
+
+
+
+
+ For details on configuring this panel, please read the
+ relevant knowledgebase article{' '}
+
+ here
+
+ .
+
+
+
+
+
+
+
+
Share information?
+
+
+ save({
+ active: event.target.checked
+ })
+ }
+ />
+
+
{coinAtmRadarConfig.active ? 'Yes' : 'No'}
+
+
+ {/* */}
+ >
+ )
+})
+
+export default CoinATMRadar
diff --git a/new-lamassu-admin/src/pages/OperatorInfo/CoinATMRadar/CoinATMRadar.styles.js b/new-lamassu-admin/src/pages/OperatorInfo/CoinATMRadar/CoinATMRadar.styles.js
new file mode 100644
index 00000000..ab0302d2
--- /dev/null
+++ b/new-lamassu-admin/src/pages/OperatorInfo/CoinATMRadar/CoinATMRadar.styles.js
@@ -0,0 +1,31 @@
+import baseStyles from 'src/pages/Logs.styles'
+import { booleanPropertiesTableStyles } from 'src/components/booleanPropertiesTable/BooleanPropertiesTable.styles'
+
+const { button } = baseStyles
+const { rowWrapper, rightAligned } = booleanPropertiesTableStyles
+
+const mainStyles = {
+ button,
+ transparentButton: {
+ '& > *': {
+ margin: 'auto 15px'
+ },
+ '& button': {
+ border: 'none',
+ backgroundColor: 'transparent',
+ cursor: 'pointer'
+ }
+ },
+ rowWrapper,
+ switchWrapper: {
+ display: 'flex',
+ marginLeft: 120
+ },
+ rightAligned,
+ popoverContent: {
+ width: 272,
+ padding: [[10, 15]]
+ }
+}
+
+export { mainStyles }
diff --git a/new-lamassu-admin/src/pages/OperatorInfo/CoinATMRadar/index.js b/new-lamassu-admin/src/pages/OperatorInfo/CoinATMRadar/index.js
new file mode 100644
index 00000000..8fe804d8
--- /dev/null
+++ b/new-lamassu-admin/src/pages/OperatorInfo/CoinATMRadar/index.js
@@ -0,0 +1,3 @@
+import CoinATMRadar from './CoinATMRadar'
+
+export default CoinATMRadar
diff --git a/new-lamassu-admin/src/pages/OperatorInfo/OperatorInfo.js b/new-lamassu-admin/src/pages/OperatorInfo/OperatorInfo.js
index ec31ff8d..2e7c45b8 100644
--- a/new-lamassu-admin/src/pages/OperatorInfo/OperatorInfo.js
+++ b/new-lamassu-admin/src/pages/OperatorInfo/OperatorInfo.js
@@ -7,6 +7,7 @@ import Title from 'src/components/Title'
import logsStyles from '../Logs.styles'
+import CoinAtmRadar from './CoinATMRadar'
import ContactInfo from './ContactInfo'
const localStyles = {
@@ -49,6 +50,7 @@ const OperatorInfo = () => {
/>
{isSelected(CONTACT_INFORMATION) && }
+ {isSelected(COIN_ATM_RADAR) && }
>
diff --git a/new-lamassu-admin/src/styling/icons/action/edit/disabled.svg b/new-lamassu-admin/src/styling/icons/action/edit/disabled.svg
index 6b10746a..19ba8772 100644
--- a/new-lamassu-admin/src/styling/icons/action/edit/disabled.svg
+++ b/new-lamassu-admin/src/styling/icons/action/edit/disabled.svg
@@ -1,17 +1,10 @@
-
+
icon/action/edit/disabled
Created with Sketch.
-
-
-
-
-
-
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/new-lamassu-admin/src/styling/icons/action/edit/white.svg b/new-lamassu-admin/src/styling/icons/action/edit/white.svg
index bf023ef6..a4a16c85 100644
--- a/new-lamassu-admin/src/styling/icons/action/edit/white.svg
+++ b/new-lamassu-admin/src/styling/icons/action/edit/white.svg
@@ -1,17 +1,10 @@
-
+
icon/action/edit/white
Created with Sketch.
-
-
-
-
-
-
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/new-lamassu-admin/src/styling/icons/table/false.svg b/new-lamassu-admin/src/styling/icons/table/false.svg
new file mode 100644
index 00000000..e06867c2
--- /dev/null
+++ b/new-lamassu-admin/src/styling/icons/table/false.svg
@@ -0,0 +1,12 @@
+
+
+
+ icon/table/false
+ Created with Sketch.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/new-lamassu-admin/src/styling/icons/table/true.svg b/new-lamassu-admin/src/styling/icons/table/true.svg
new file mode 100644
index 00000000..d805af9d
--- /dev/null
+++ b/new-lamassu-admin/src/styling/icons/table/true.svg
@@ -0,0 +1,9 @@
+
+
+
+ icon/table/true
+ Created with Sketch.
+
+
+
+
\ No newline at end of file