Migrate plugins to lib directory
This commit is contained in:
parent
09b29bba56
commit
e7ab8223c2
27 changed files with 869 additions and 858 deletions
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
23
lib/plugin-helper.js
Normal 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}`)
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
81
lib/plugins/common/bitstamp.js
Normal file
81
lib/plugins/common/bitstamp.js
Normal 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
|
||||
}
|
||||
17
lib/plugins/common/kraken.js
Normal file
17
lib/plugins/common/kraken.js
Normal 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)
|
||||
}
|
||||
23
lib/plugins/email/mailjet/mailjet.js
Normal file
23
lib/plugins/email/mailjet/mailjet.js
Normal 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
|
||||
}
|
||||
34
lib/plugins/email/mailjet/schema.json
Normal file
34
lib/plugins/email/mailjet/schema.json
Normal 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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
44
lib/plugins/exchange/bitstamp/bitstamp.js
Normal file
44
lib/plugins/exchange/bitstamp/bitstamp.js
Normal 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
|
||||
}
|
||||
27
lib/plugins/exchange/bitstamp/schema.json
Normal file
27
lib/plugins/exchange/bitstamp/schema.json
Normal 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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
54
lib/plugins/exchange/kraken/kraken.js
Normal file
54
lib/plugins/exchange/kraken/kraken.js
Normal 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
14
lib/plugins/exchange/mock-exchange/mock-exchange.js
Normal file
14
lib/plugins/exchange/mock-exchange/mock-exchange.js
Normal 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()
|
||||
}
|
||||
6
lib/plugins/sms/mock-sms/mock-sms.js
Normal file
6
lib/plugins/sms/mock-sms/mock-sms.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
exports.NAME = 'MockSMS'
|
||||
|
||||
exports.sendMessage = function sendMessage (account, rec) {
|
||||
console.log('Sending SMS: %j', rec)
|
||||
return Promise.resolve()
|
||||
}
|
||||
34
lib/plugins/sms/twilio/schema.json
Normal file
34
lib/plugins/sms/twilio/schema.json
Normal 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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
33
lib/plugins/sms/twilio/twilio.js
Normal file
33
lib/plugins/sms/twilio/twilio.js
Normal 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
|
||||
}
|
||||
25
lib/plugins/ticker/bitpay/bitpay.js
Normal file
25
lib/plugins/ticker/bitpay/bitpay.js
Normal 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'
|
||||
}
|
||||
26
lib/plugins/ticker/bitstamp/bitstamp.js
Normal file
26
lib/plugins/ticker/bitstamp/bitstamp.js
Normal 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
|
||||
}
|
||||
|
||||
60
lib/plugins/ticker/kraken/kraken.js
Normal file
60
lib/plugins/ticker/kraken/kraken.js
Normal 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])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
146
lib/plugins/wallet/bitcoind/bitcoind.js
Normal file
146
lib/plugins/wallet/bitcoind/bitcoind.js
Normal 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
|
||||
}
|
||||
91
lib/plugins/wallet/bitgo/bitgo.js
Normal file
91
lib/plugins/wallet/bitgo/bitgo.js
Normal 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
|
||||
}
|
||||
30
lib/plugins/wallet/bitgo/schema.json
Normal file
30
lib/plugins/wallet/bitgo/schema.json
Normal 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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
53
lib/plugins/wallet/mock-wallet/mock-wallet.js
Normal file
53
lib/plugins/wallet/mock-wallet/mock-wallet.js
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 => ({
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
})
|
||||
|
|
|
|||
14
package.json
14
package.json
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue