Migrate plugins to lib directory

This commit is contained in:
Josh Harvey 2017-04-15 19:12:29 +03:00
parent 09b29bba56
commit e7ab8223c2
27 changed files with 869 additions and 858 deletions

View file

@ -1,13 +1,12 @@
const configManager = require('./config-manager')
const ph = require('./plugin-helper')
function sendMessage (settings, rec) {
return Promise.resolve()
.then(() => {
const pluginCode = configManager.unscoped(settings.config).email
if (!pluginCode) throw new Error('No email plugin defined')
const plugin = ph.load(ph.EMAIL, pluginCode)
const account = settings.accounts[pluginCode]
const plugin = require('lamassu-' + pluginCode)
return plugin.sendMessage(account, rec)
})

View file

@ -1,11 +1,5 @@
const configManager = require('./config-manager')
function noExchangeError (cryptoCode) {
const err = new Error('No exchange plugin defined for: ' + cryptoCode)
err.name = 'NoExchangeError'
return err
}
const ph = require('./plugin-helper')
function lookupExchange (settings, cryptoCode) {
return configManager.cryptoScoped(cryptoCode, settings.config).exchange
@ -15,9 +9,8 @@ function fetchExchange (settings, cryptoCode) {
return Promise.resolve()
.then(() => {
const plugin = lookupExchange(settings, cryptoCode)
if (!plugin) throw noExchangeError(cryptoCode)
const exchange = ph.load(ph.EXCHANGE, plugin)
const account = settings.accounts[plugin]
const exchange = require('lamassu-' + plugin)
return {exchange, account}
})

23
lib/plugin-helper.js Normal file
View file

@ -0,0 +1,23 @@
const _ = require('lodash/fp')
module.exports = {
load,
TICKER: 'ticker',
EXCHANGE: 'exchange',
WALLET: 'wallet',
SMS: 'sms',
EMAIL: 'email'
}
function load (type, pluginCode) {
const me = module.exports
if (!_.includes(type, [me.TICKER, me.EXCHANGE, me.WALLET, me.SMS, me.EMAIL])) {
throw new Error(`Unallowed plugin type: ${type}`)
}
if (pluginCode.search(/[a-z0-9\-]/) === -1) {
throw new Error(`Unallowed plugin name: ${pluginCode}`)
}
return require(`./plugins/${type}/${pluginCode}/${pluginCode}`)
}

View file

@ -366,7 +366,6 @@ function plugins (settings, deviceId) {
return executeTradeForType(tradeEntry)
.catch(err => {
tradesQueues[market].push(tradeEntry)
if (err.name === 'NoExchangeError') return logger.debug(err.message)
if (err.name === 'orderTooSmall') return logger.debug(err.message)
logger.error(err)
})

View file

@ -0,0 +1,81 @@
'use strict'
const querystring = require('querystring')
const axios = require('axios')
const crypto = require('crypto')
const _ = require('lodash')
const API_ENDPOINT = 'https://www.bitstamp.net/api/v2'
let counter = -1
let lastTimestamp = Date.now()
function pad (num) {
const asString = num.toString(10)
if (num < 10) return '00' + asString
if (num < 100) return '0' + asString
return asString
}
function generateNonce () {
const timestamp = Date.now()
if (timestamp !== lastTimestamp) counter = -1
lastTimestamp = timestamp
counter = (counter + 1) % 1000
return timestamp.toString(10) + pad(counter)
}
function authRequest (config, path, data) {
if (!config.key || !config.secret || !config.clientId) {
const err = new Error('Must provide key, secret and client ID')
return Promise.reject(err)
}
data = data || {}
const nonce = generateNonce()
const msg = [nonce, config.clientId, config.key].join('')
const signature = crypto
.createHmac('sha256', Buffer.from(config.secret))
.update(msg)
.digest('hex')
.toUpperCase()
_.merge(data, {
key: config.key,
signature: signature,
nonce: nonce
})
return request(path, 'POST', data)
}
function buildMarket (fiatCode, cryptoCode) {
if (cryptoCode !== 'BTC') throw new Error('Unsupported crypto: ' + cryptoCode)
if (fiatCode === 'USD') return 'btcusd'
if (fiatCode === 'EUR') return 'btceur'
throw new Error('Unsupported fiat: ' + fiatCode)
}
function request (path, method, data) {
const options = {
method: method,
url: API_ENDPOINT + path + '/',
headers: {
'User-Agent': 'Mozilla/4.0 (compatible; Lamassu client)',
'Content-Type': 'application/x-www-form-urlencoded'
}
}
if (data) options.data = querystring.stringify(data)
return axios(options)
.then(r => r.data)
}
module.exports = {
authRequest,
request,
buildMarket
}

View file

@ -0,0 +1,17 @@
var BigNumber = require('bignumber.js')
var TEN = new BigNumber(10)
var UNIT_SCALES = {
BTC: 8,
ETH: 18
}
function unitScale (cryptoCoin) {
return UNIT_SCALES[cryptoCoin]
}
exports.toUnit = function toUnit (cryptoAtoms, cryptoCoin) {
var scale = TEN.pow(unitScale(cryptoCoin))
return cryptoAtoms.div(scale)
}

View file

@ -0,0 +1,23 @@
const Mailjet = require('node-mailjet')
const NAME = 'Mailjet'
function sendMessage (account, rec) {
const mailjet = Mailjet.connect(account.apiKey, account.apiSecret)
const sendEmail = mailjet.post('send')
const emailData = {
FromEmail: account.fromEmail,
FromName: 'Lamassu Server',
Subject: rec.email.subject,
'Text-part': rec.email.body,
Recipients: [{'Email': account.toEmail}]
}
return sendEmail.request(emailData)
}
module.exports = {
NAME,
sendMessage
}

View file

@ -0,0 +1,34 @@
{
"code": "mailjet",
"display": "Mailjet",
"fields": [
{
"code": "apiKey",
"display": "API key",
"fieldType": "string",
"required": true,
"value": ""
},
{
"code": "apiSecret",
"display": "API secret",
"fieldType": "password",
"required": true,
"value": ""
},
{
"code": "fromEmail",
"display": "From email",
"fieldType": "string",
"required": true,
"value": ""
},
{
"code": "toEmail",
"display": "To email",
"fieldType": "string",
"required": true,
"value": ""
}
]
}

View file

@ -0,0 +1,44 @@
const common = require('../common/bitstamp')
const SATOSHI_SHIFT = 8
function buy (account, cryptoAtoms, fiatCode, cryptoCode) {
return trade('buy', account, cryptoAtoms, fiatCode, cryptoCode)
}
function sell (account, cryptoAtoms, fiatCode, cryptoCode) {
return trade('sell', account, cryptoAtoms, fiatCode, cryptoCode)
}
function handleErrors (data) {
if (!data.reason || !data.reason.__all__) return data
const err = new Error(data.reason.__all__[0])
if (data.reason.__all__[0].indexOf('Minimum order size is') === 0) {
err.name = 'orderTooSmall'
}
throw err
}
function trade (type, account, cryptoAtoms, fiatCode, cryptoCode) {
try {
const market = common.buildMarket(fiatCode, cryptoCode)
const options = {amount: cryptoAtoms.shift(-SATOSHI_SHIFT).toFixed(8)}
return common.authRequest(account, '/' + type + '/market/' + market, options)
.catch(e => {
if (e.response) handleErrors(e.response.data)
throw e
})
.then(handleErrors)
} catch (e) {
return Promise.reject(e)
}
}
module.exports = {
buy,
sell
}

View file

@ -0,0 +1,27 @@
{
"code": "bitstamp",
"display": "Bitstamp",
"fields": [
{
"code": "clientId",
"display": "Client ID",
"fieldType": "string",
"required": true,
"value": ""
},
{
"code": "key",
"display": "API key",
"fieldType": "string",
"required": true,
"value": ""
},
{
"code": "secret",
"display": "API secret",
"fieldType": "password",
"required": true,
"value": ""
}
]
}

View file

@ -0,0 +1,54 @@
const Kraken = require('kraken-api')
const coinmath = require('../common/kraken')
var PAIRS = {
BTC: {
USD: 'XXBTZUSD',
EUR: 'XXBTZEUR'
},
ETH: {
USD: 'XETHZUSD',
EUR: 'XETHZEUR'
}
}
module.exports = {buy, sell}
function buy (account, cryptoAtoms, fiatCode, cryptoCode) {
return trade(account, 'buy', cryptoAtoms, fiatCode, cryptoCode)
}
function sell (account, cryptoAtoms, fiatCode, cryptoCode) {
return trade(account, 'sell', cryptoAtoms, fiatCode, cryptoCode)
}
function trade (account, type, cryptoAtoms, fiatCode, cryptoCode) {
const kraken = new Kraken(account.key, account.secret)
const amount = coinmath.toUnit(cryptoAtoms, cryptoCode)
if (amount.lte('0.01')) {
const err = new Error('Order size too small')
err.name = 'orderTooSmall'
return Promise.reject(err)
}
const amountStr = amount.toFixed(6)
const pair = PAIRS[cryptoCode][fiatCode]
var orderInfo = {
pair: pair,
type: type,
ordertype: 'market',
volume: amountStr,
expiretm: '+60'
}
kraken.api('AddOrder', orderInfo, function (error, response) {
if (error) {
// TODO: handle: EOrder:Order minimum not met (volume too low)
return Promise.reject(error)
} else {
return Promise.resolve()
}
})
}

View file

@ -0,0 +1,14 @@
module.exports = {
buy,
sell
}
function buy (account, cryptoAtoms, fiatCode, cryptoCode) {
console.log('[mock] buying %s %s for %s', cryptoAtoms.toString(), cryptoCode, fiatCode)
return Promise.resolve()
}
function sell (account, cryptoAtoms, fiatCode, cryptoCode) {
console.log('[mock] selling %s %s for %s', cryptoAtoms.toString(), cryptoCode, fiatCode)
return Promise.resolve()
}

View file

@ -0,0 +1,6 @@
exports.NAME = 'MockSMS'
exports.sendMessage = function sendMessage (account, rec) {
console.log('Sending SMS: %j', rec)
return Promise.resolve()
}

View file

@ -0,0 +1,34 @@
{
"code": "twilio",
"display": "Twilio",
"fields": [
{
"code": "accountSid",
"display": "Account SID",
"fieldType": "string",
"required": true,
"value": ""
},
{
"code": "authToken",
"display": "Auth token",
"fieldType": "password",
"required": true,
"value": ""
},
{
"code": "fromNumber",
"display": "From number",
"fieldType": "string",
"required": true,
"value": ""
},
{
"code": "toNumber",
"display": "To number",
"fieldType": "string",
"required": true,
"value": ""
}
]
}

View file

@ -0,0 +1,33 @@
const Client = require('twilio')
const _ = require('lodash/fp')
const NAME = 'Twilio'
const BAD_NUMBER_CODES = [21201, 21202, 21211, 21214, 21216, 21217, 21219, 21408,
21610, 21612, 21614, 21608]
function sendMessage (account, rec) {
const client = Client(account.accountSid, account.authToken)
const body = rec.sms.body
const _toNumber = rec.sms.toNumber || account.toNumber
return client.sendMessage({
body: body,
to: _toNumber,
from: account.fromNumber
})
.catch(err => {
if (_.includes(err.code, BAD_NUMBER_CODES)) {
const badNumberError = new Error(err.message)
badNumberError.name = 'BadNumberError'
throw badNumberError
}
throw new Error(err.message)
})
}
module.exports = {
NAME,
sendMessage
}

View file

@ -0,0 +1,25 @@
const axios = require('axios')
const BN = require('../../../bn')
function ticker (account, fiatCode, cryptoCode) {
if (cryptoCode !== 'BTC') {
return Promise.reject('Unsupported crypto: ' + cryptoCode)
}
return axios.get('https://bitpay.com/api/rates/' + fiatCode)
.then(r => {
const data = r.data
const price = BN(data.rate)
return {
rates: {
ask: price,
bid: price
}
}
})
}
module.exports = {
ticker,
name: 'BitPay'
}

View file

@ -0,0 +1,26 @@
const BN = require('../../../bn')
const common = require('../common/bitstamp')
function ticker (account, fiatCode, cryptoCode) {
return Promise.resolve()
.then(() => {
if (cryptoCode !== 'BTC') {
throw new Error('Unsupported crypto: ' + cryptoCode)
}
})
.then(() => {
const market = common.buildMarket(fiatCode, cryptoCode)
return common.request('/ticker/' + market, 'GET')
})
.then(r => ({
rates: {
ask: BN(r.ask),
bid: BN(r.bid)
}
}))
}
module.exports = {
ticker
}

View file

@ -0,0 +1,60 @@
const axios = require('axios')
const _ = require('lodash/fp')
const BN = require('../../../bn')
exports.NAME = 'Kraken'
exports.SUPPORTED_MODULES = ['ticker']
const PAIRS = {
BTC: {
USD: 'XXBTZUSD',
EUR: 'XXBTZEUR'
},
ETH: {
USD: 'XETHZUSD',
EUR: 'XETHZEUR'
}
}
function findCurrency (fxRates, fiatCode) {
const rates = _.find(_.matchesProperty('code', fiatCode), fxRates)
if (!rates || !rates.rate) throw new Error(`Unsupported currency: ${fiatCode}`)
return BN(rates.rate)
}
exports.ticker = function ticker (account, fiatCode, cryptoCode) {
if (fiatCode === 'USD' || fiatCode === 'EUR') {
return getCurrencyRates(fiatCode, cryptoCode)
}
return axios.get('https://bitpay.com/api/rates')
.then(response => {
const fxRates = response.data
const usdRate = findCurrency(fxRates, 'USD')
const fxRate = findCurrency(fxRates, fiatCode).div(usdRate)
return getCurrencyRates('USD', cryptoCode)
.then(res => ({
rates: {
ask: res.rates.ask.times(fxRate),
bid: res.rates.bid.times(fxRate)
}
}))
})
}
function getCurrencyRates (fiatCode, cryptoCode) {
const pair = PAIRS[cryptoCode][fiatCode]
return axios.get('https://api.kraken.com/0/public/Ticker?pair=' + pair)
.then(function (response) {
const rates = response.data.result[pair]
return {
rates: {
ask: BN(rates.a[0]),
bid: BN(rates.b[0])
}
}
})
}

View file

@ -0,0 +1,146 @@
const path = require('path')
const os = require('os')
const RpcClient = require('bitcoind-rpc')
const fs = require('fs')
const pify = require('pify')
const BN = require('../../../bn')
const NAME = 'Bitcoind'
const SATOSHI_SHIFT = 8
const configPath = path.resolve(os.homedir(), '.bitcoin', 'bitcoin.conf')
const pluginConfig = {
account: '',
bitcoindConfigurationPath: configPath
}
function initRpc () {
const bitcoindConf = parseConf(pluginConfig.bitcoindConfigurationPath)
const rpcConfig = {
protocol: 'http',
user: bitcoindConf.rpcuser,
pass: bitcoindConf.rpcpassword
}
return new RpcClient(rpcConfig)
}
function richError (msg, name) {
const err = new Error(msg)
err.name = name
return err
}
/*
* initialize RpcClient
*/
function parseConf (confPath) {
const conf = fs.readFileSync(confPath)
const lines = conf.toString().split('\n')
const res = {}
for (let i = 0; i < lines.length; i++) {
const keyVal = lines[i].split('=')
// skip when value is empty
if (!keyVal[1]) continue
res[keyVal[0]] = keyVal[1]
}
return res
}
function checkCryptoCode (cryptoCode) {
if (cryptoCode !== 'BTC') throw new Error('Unsupported crypto: ' + cryptoCode)
}
// We want a balance that includes all spends (0 conf) but only deposits that
// have at least 1 confirmation. getbalance does this for us automatically.
function balance (account, cryptoCode) {
return new Promise((resolve, reject) => {
checkCryptoCode(cryptoCode)
const rpc = initRpc()
rpc.getBalance(pluginConfig.account, 1, (err, result) => {
if (err) return reject(err)
if (result.error) {
return reject(richError(result.error, 'bitcoindError'))
}
resolve(BN(result.result).shift(SATOSHI_SHIFT).round())
})
})
}
function sendCoins (account, address, cryptoAtoms, cryptoCode) {
const rpc = initRpc()
const confirmations = 1
const bitcoins = cryptoAtoms.shift(-SATOSHI_SHIFT).toFixed(8)
return new Promise((resolve, reject) => {
checkCryptoCode(cryptoCode)
rpc.sendFrom(pluginConfig.account, address, bitcoins, confirmations, (err, result) => {
if (err) {
if (err.code === -6) {
return reject(richError('Insufficient funds', 'InsufficientFunds'))
}
if (err instanceof Error) {
return reject(err)
}
return reject(richError(err.message, 'bitcoindError'))
}
resolve(result.result)
})
})
}
function newAddress (account, cryptoCode, info) {
return new Promise((resolve, reject) => {
checkCryptoCode(cryptoCode)
const rpc = initRpc()
rpc.getNewAddress((err, result) => {
if (err) return reject(err)
resolve(result.result)
})
})
}
function addressBalance (address, confs) {
const rpc = initRpc()
return pify(rpc.getReceivedByAddress.bind(rpc))(address, confs)
.then(r => BN(r.result).shift(SATOSHI_SHIFT).round())
}
const confirmedBalance = address => addressBalance(address, 1)
const pendingBalance = address => addressBalance(address, 0)
function getStatus (account, toAddress, requested, cryptoCode) {
return Promise.resolve()
.then(() => checkCryptoCode(cryptoCode))
.then(() => confirmedBalance(toAddress))
.then(confirmed => {
if (confirmed.gte(requested)) return {status: 'confirmed'}
return pendingBalance(toAddress)
.then(pending => {
if (pending.gte(requested)) return {status: 'authorized'}
if (pending.gt(0)) return {status: 'insufficientFunds'}
return {status: 'notSeen'}
})
})
}
module.exports = {
NAME,
balance,
sendCoins,
newAddress,
getStatus
}

View file

@ -0,0 +1,91 @@
const BitGo = require('bitgo')
const BN = require('../../../bn')
const pjson = require('../../../package.json')
const userAgent = 'Lamassu-Server/' + pjson.version
const NAME = 'BitGo'
function buildBitgo (account) {
return new BitGo.BitGo({accessToken: account.token, env: 'prod', userAgent: userAgent})
}
function getWallet (account) {
const bitgo = buildBitgo(account)
return bitgo.wallets().get({ id: account.walletId })
}
function checkCryptoCode (cryptoCode) {
if (cryptoCode !== 'BTC') {
return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
}
return Promise.resolve()
}
function sendCoins (account, address, cryptoAtoms, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => getWallet(account))
.then(wallet => {
const params = {
address: address,
amount: cryptoAtoms.toNumber(),
walletPassphrase: account.walletPassphrase
}
return wallet.sendCoins(params)
})
.then(result => {
return result.hash
})
.catch(err => {
if (err.message === 'Insufficient funds') {
err.name = 'InsufficientFunds'
}
throw err
})
}
function balance (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => getWallet(account))
.then(wallet => BN(wallet.wallet.spendableConfirmedBalance))
}
function newAddress (account, cryptoCode, info) {
return checkCryptoCode(cryptoCode)
.then(() => getWallet(account))
.then(wallet => {
return wallet.createAddress()
.then(result => {
const address = result.address
// If a label was provided, set the label
if (info.label) {
return wallet.setLabel({ address: address, label: info.label })
.then(() => address)
}
return address
})
})
}
function getStatus (account, toAddress, requested, cryptoCode) {
const bitgo = buildBitgo(account)
return checkCryptoCode(cryptoCode)
.then(() => bitgo.blockchain().getAddress({address: toAddress}))
.then(rec => {
if (rec.balance === 0) return {status: 'notSeen'}
if (requested.gt(rec.balance)) return {status: 'insufficientFunds'}
if (requested.gt(rec.confirmedBalance)) return {status: 'authorized'}
return {status: 'confirmed'}
})
}
module.exports = {
NAME,
balance,
sendCoins,
newAddress,
getStatus
}

View file

@ -0,0 +1,30 @@
{
"code": "bitgo",
"display": "BitGo",
"fields": [
{
"code": "token",
"display": "API token",
"fieldType": "string",
"secret": true,
"required": true,
"value": ""
},
{
"code": "walletId",
"display": "Wallet ID",
"fieldType": "string",
"secret": false,
"required": true,
"value": ""
},
{
"code": "walletPassphrase",
"display": "Wallet passphrase",
"fieldType": "string",
"secret": true,
"required": true,
"value": ""
}
]
}

View file

@ -0,0 +1,53 @@
const BN = require('../../../bn')
const NAME = 'FakeWallet'
const SECONDS = 1000
const UNSEEN_TIME = 6 * SECONDS
const PUBLISH_TIME = 12 * SECONDS
const AUTHORIZE_TIME = 60 * SECONDS
let t0
function balance (account, cryptoCode) {
return Promise.resolve()
.then(() => {
if (cryptoCode === 'BTC') return BN(1e8 * 10)
if (cryptoCode === 'ETH') return BN(1e18 * 10)
throw new Error('Unsupported crypto: ' + cryptoCode)
})
}
function sendCoins (account, toAddress, cryptoAtoms, cryptoCode) {
return new Promise(resolve => {
setTimeout(() => {
console.log('[%s] DEBUG: Mock wallet sending %s cryptoAtoms to %s',
cryptoCode, cryptoAtoms.toString(), toAddress)
resolve('<txHash>')
}, 2000)
})
}
function newAddress () {
t0 = Date.now()
return Promise.resolve('<Fake address, don\'t send>')
}
function getStatus (account, toAddress, cryptoAtoms, cryptoCode) {
const elapsed = Date.now() - t0
if (elapsed < UNSEEN_TIME) return Promise.resolve({status: 'notSeen'})
if (elapsed < PUBLISH_TIME) return Promise.resolve({status: 'published'})
if (elapsed < AUTHORIZE_TIME) return Promise.resolve({status: 'authorized'})
console.log('[%s] DEBUG: Mock wallet has confirmed transaction', cryptoCode)
return Promise.resolve({status: 'confirmed'})
}
module.exports = {
NAME,
balance,
sendCoins,
newAddress,
getStatus
}

View file

@ -1,13 +1,12 @@
const configManager = require('./config-manager')
const ph = require('./plugin-helper')
function sendMessage (settings, rec) {
return Promise.resolve()
.then(() => {
const pluginCode = configManager.unscoped(settings.config).sms
if (!pluginCode) throw new Error('No sms plugin defined')
const plugin = ph.load(ph.SMS, pluginCode)
const account = settings.accounts[pluginCode]
const plugin = require('lamassu-' + pluginCode)
return plugin.sendMessage(account, rec)
})

View file

@ -1,5 +1,6 @@
const mem = require('mem')
const configManager = require('./config-manager')
const ph = require('./plugin-helper')
const FETCH_INTERVAL = 10000
function getRates (settings, fiatCode, cryptoCode) {
@ -8,10 +9,8 @@ function getRates (settings, fiatCode, cryptoCode) {
const config = settings.config
const plugin = configManager.cryptoScoped(cryptoCode, config).ticker
if (!plugin) throw new Error('No ticker plugin defined')
const account = settings.accounts[plugin]
const ticker = require('lamassu-' + plugin)
const ticker = ph.load(ph.TICKER, plugin)
return ticker.ticker(account, fiatCode, cryptoCode)
.then(r => ({

View file

@ -6,6 +6,7 @@ const configManager = require('./config-manager')
const pify = require('pify')
const fs = pify(require('fs'))
const options = require('./options')
const ph = require('./plugin-helper')
const FETCH_INTERVAL = 5000
const INSUFFICIENT_FUNDS_CODE = 570
@ -29,8 +30,8 @@ function fetchWallet (settings, cryptoCode) {
.then(hex => {
const masterSeed = Buffer.from(hex.trim(), 'hex')
const plugin = configManager.cryptoScoped(cryptoCode, settings.config).wallet
const wallet = ph.load(ph.WALLET, plugin)
const account = settings.accounts[plugin]
const wallet = require('lamassu-' + plugin)
return {wallet, account: _.set('seed', computeSeed(masterSeed), account)}
})

View file

@ -6,8 +6,11 @@
"license": "Unlicense",
"author": "Lamassu (https://lamassu.is)",
"dependencies": {
"axios": "^0.16.1",
"base-x": "^3.0.0",
"bignumber.js": "^4.0.1",
"bip39": "^2.3.0",
"bitcoind-rpc": "^0.7.0",
"body-parser": "^1.15.1",
"cookie-parser": "^1.4.3",
"express": "^4.13.4",
@ -15,16 +18,6 @@
"express-rate-limit": "^2.6.0",
"got": "^6.6.3",
"helmet": "^3.1.0",
"lamassu-bitcoind": "lamassu/lamassu-bitcoind#alpha",
"lamassu-bitgo": "lamassu/lamassu-bitgo#alpha",
"lamassu-bitpay": "lamassu/lamassu-bitpay#alpha",
"lamassu-bitstamp": "lamassu/lamassu-bitstamp#alpha",
"lamassu-kraken": "^1.1.1",
"lamassu-mailjet": "lamassu/lamassu-mailjet",
"lamassu-mock-id-verify": "lamassu/lamassu-mock-id-verify",
"lamassu-mock-sms": "lamassu/lamassu-mock-sms",
"lamassu-mock-wallet": "lamassu/lamassu-mock-wallet",
"lamassu-twilio": "lamassu/lamassu-twilio",
"lodash": "^4.17.2",
"mem": "^1.1.0",
"migrate": "^0.2.2",
@ -32,6 +25,7 @@
"moment": "^2.17.0",
"morgan": "^1.7.0",
"node-hkdf-sync": "^1.0.0",
"node-mailjet": "^3.0.6",
"numeral": "^2.0.3",
"pg": "^6.1.2",
"pg-native": "latest",

862
yarn.lock

File diff suppressed because it is too large Load diff