feat: start working on monero implementation

feat: monero files and config

feat: monero interface
feat: monero rpc digest request

feat: add monero to wallet wizard splash

fix: tarball unzipping

fix: monero files

fix: monero zipped folder path

fix: undefined variable

chore: xmr stagenet

fix: prune-blockchain flag

fix: monero wallet arguments

chore: rpc-bind-port on monero wallet

chore: monero wallet directory

fix: fetch digest request
fix: monero authentication
fix: monero address creation

fix: monero config port

fix: wallet creation
fix: wallet rpc daemon login

fix: get monero funding

fix: unauthorized issue with multiple requests on Promise.all
fix: generate funding addresses
fix: destination address balance for XMR cashout
fix: transaction creation

feat: transaction recommended mixin and ring size

fix: monero wallet error handling

fix: error handling

fix: monero wallet error
feat: guide to add new cryptos

chore: small code shortcuts

fix: crypto implementation guide

chore: add XMR to exchange files
This commit is contained in:
Sérgio Salgado 2021-05-06 17:11:24 +01:00
parent 9ec871e163
commit c0808e9bd7
11 changed files with 445 additions and 14 deletions

54
CRYPTO_README.md Normal file
View file

@ -0,0 +1,54 @@
## Adding a new cryptocurrency to a Lamassu ATM
### Structure
In order to install new coins onto a Lamassu system, there are three points to pay attention:
- **The blockchain daemon:** This is a file which will need to be running on the lamassu-server and communicates with the blockchain network. This generally has an integrated wallet, but it may occur for the daemon and the wallet to be completely seperate processes (e.g. XMR). This manager is currently built into the lamassu-server project, but in the future, it will be moved to either lamassu-coins or a new library;
- **The wallet plugin:** This uses the capabilities of the RPC (Remote Procedure Call) API built into the blockchain daemon and wallet to create a linking API to standardize with the Lamassu ecosystem. It is built into the lamassu-server project;
- **The coin constants:** This has all the information about an implemented coin, including its code, unit scale, daemon RPC ports and all other information to make lamassu-server, lamassu-admin-server and lamassu-machine know about the supported coins. It is built into the lamassu-coins package;
I'll be using XMR as example for all the steps in this guide.
#### Blockchain
Steps to implement a daemon:
- Create a file in `lamassu-server/lib/blockchain/<name_of_coin>.js`;
- Go to `lamassu-server/lib/blockchain/common.js` and add a new entry to the `BINARIES` object. Each entry has two mandatory fields (`url` and `dir`), and an optional one (`files`).
- To get the `url` needed to download the blockchain daemon, you need to access the releases of the daemon of the coin you're working with. For example, for XMR, the daemon can be found in their GitHub releases (https://github.com/monero-project/monero-gui/releases). Get the URL for the Linux 64-bit distribution and note the extension of the file, which will most likely be `.tar.gz` or `.tar.bz2`. For `.tar.bz2`, the coin you're working with needs to be added to the following snippet of code, responsible for the extraction of the downloaded file (`common.js`):
```
coinRec.cryptoCode === 'XMR'
? es(`tar -xf ${downloadFile}`)
: es(`tar -xzf ${downloadFile}`)
```
- To get the `dir`, simply download the file, extract it, and take note of the folder inside the zipped file and the path towards the actual files you want. In XMR's case, `dir` = `monero-x86_64-linux-gnu-v0.17.2.0`, but for BTC it is `bitcoin-0.20.1/bin`
- Inside the directory specified in the `dir` field, there can be multiple files inside. In that case, you want to specity the `files` field. This is a multi-dimensional array, where each entry contains a pair of [<file_in_the_downloaded_folder>, <name_with_with_the_file_is_saved_in_the_server>].
```
[
['monerod', 'monerod'],
['monero-wallet-rpc', 'monero-wallet-rpc']
]
```
This means that the `monerod` found inside the distribution folder will be saved as `monerod` on the server. Same for the `monero-wallet-rpc`.
- Go to `lamassu-server/lib/blockchain/install.js` and add a new entry on the `PLUGINS` object. This entry must import the file created in step 1.
- Go to the file created in step one and import the object (which isn't created right now) containing all the information needed of a coin `const coinRec = utils.getCryptoCurrency('<coin_code>')`.
- The coin blockchain plugin contains two functions: `setup` and `buildConfig`.
- The build config has all the required flags to operate the downloaded daemon, and each coin has their particular set of flags and options, so that specification won't be covered here.
- The setup function has a similar structure in any coin, and the differences between them is generally related to how a daemon is ran.
#### Wallet plugin
Steps to implement a wallet plugin:
- Create a file in `lamassu-server/lib/plugins/wallet/<name_of_daemon>/<name_of_daemon>.js`
- The wallet plugin serves as a middleware between the RPC calls supported by each daemon, and the processes ran inside the lamassu-server ecosystem. This includes address creation, balance lookup, making transactions, etc. As such, this file needs to export the following functions:
- `balance`: Responds with the amount of usable balance the operator wallet has;
- `sendCoins`: Responsible for creating a transaction and responds with an object containing the fee of the transaction and the transactionID;
- `newAddress`: Generates a new address for the operator wallet. Used for machine transactions and funding page;
- `getStatus`: Responsible for getting the status of a cash-out transaction (transaction from an operator address to a client address).
- `newFunding`: Creates the response to the funding page, with the amount of balance the operator has, the pending balance and a new funding address;
- `cryptoNetwork`: Responds with the crypto network the wallet is operating in, based on the port of the RPC calls used.
#### Coin utils
Steps to work on lamassu-coins:
- Create a new object on `lamassu-coins/config/consts.js` containing all the information relative to a coin. If you're using a wallet built into the daemon, use BTC as template. Otherwise, if the wallet process is separated from the daemon, use XMR as template;
- Create a new file on `lamassu-coins/plugins/<coin_code>.js`. This file should handle URL parsing and address validation. Despite most coins in lamassu-coins operating on base58 or bech32 validation, this validation can implemented on some variation of the existing algorithms, as is the case with XMR. When this happens, the implementation of this variation needs to be created from scratch. With the validator created, the machine should be able to recognize a valid address. To test this out, simply edit the `lamassu-machine/device-config.json` file with the new coin address to validate.

View file

@ -48,6 +48,11 @@ const BINARIES = {
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v23.1.0/bitcoin-cash-node-23.1.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-cash-node-23.1.0/bin',
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
},
XMR: {
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.17.2.0.tar.bz2',
dir: 'monero-x86_64-linux-gnu-v0.17.2.0',
files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']]
}
}
@ -71,11 +76,24 @@ function es (cmd) {
return res.toString()
}
function writeSupervisorConfig (coinRec, cmd) {
function writeSupervisorConfig (coinRec, cmd, walletCmd = '') {
if (isInstalledSoftware(coinRec)) return
const blockchain = coinRec.code
if (!_.isNil(coinRec.wallet)) {
const supervisorConfigWallet = `[program:${blockchain}-wallet]
command=nice ${walletCmd}
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/${blockchain}-wallet.err.log
stdout_logfile=/var/log/supervisor/${blockchain}-wallet.out.log
environment=HOME="/root"
`
writeFile(`/etc/supervisor/conf.d/${coinRec.code}-wallet.conf`, supervisorConfigWallet)
}
const supervisorConfig = `[program:${blockchain}]
command=nice ${cmd}
autostart=true
@ -89,7 +107,10 @@ environment=HOME="/root"
}
function isInstalledSoftware (coinRec) {
return fs.existsSync(`/etc/supervisor/conf.d/${coinRec.code}.conf`)
return !_.isNil(coinRec.wallet)
? fs.existsSync(`/etc/supervisor/conf.d/${coinRec.code}.conf`)
&& fs.existsSync(`/etc/supervisor/conf.d/${coinRec.code}-wallet.conf`)
: fs.existsSync(`/etc/supervisor/conf.d/${coinRec.code}.conf`)
}
function fetchAndInstall (coinRec) {
@ -104,7 +125,9 @@ function fetchAndInstall (coinRec) {
const binDir = requiresUpdate ? binaries.defaultDir : binaries.dir
es(`wget -q ${url}`)
es(`tar -xzf ${downloadFile}`)
coinRec.cryptoCode === 'XMR'
? es(`tar -xf ${downloadFile}`)
: es(`tar -xzf ${downloadFile}`)
if (_.isEmpty(binaries.files)) {
es(`sudo cp ${binDir}/* /usr/local/bin`)

View file

@ -23,7 +23,8 @@ const PLUGINS = {
DASH: require('./dash.js'),
ETH: require('./ethereum.js'),
LTC: require('./litecoin.js'),
ZEC: require('./zcash.js')
ZEC: require('./zcash.js'),
XMR: require('./monero.js')
}
module.exports = {run}

30
lib/blockchain/monero.js Normal file
View file

@ -0,0 +1,30 @@
const path = require('path')
const { utils } = require('lamassu-coins')
const common = require('./common')
module.exports = {setup}
const coinRec = utils.getCryptoCurrency('XMR')
function setup (dataDir) {
common.firewall([coinRec.defaultPort])
const auth = `lamassuserver:${common.randomPass()}`
const config = buildConfig(auth)
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
const cmd = `/usr/local/bin/${coinRec.daemon} --data-dir ${dataDir} --config-file ${dataDir}/${coinRec.configFile}`
const walletCmd = `/usr/local/bin/${coinRec.wallet} --stagenet --rpc-login ${auth} --daemon-host 127.0.0.1 --daemon-port 38081 --trusted-daemon --daemon-login ${auth} --rpc-bind-port 38083 --wallet-dir ${dataDir}/wallets`
common.writeSupervisorConfig(coinRec, cmd, walletCmd)
}
function buildConfig (auth) {
return `rpc-login=${auth}
stagenet=1
restricted-rpc=1
db-sync-mode=safe
out-peers=20
in-peers=20
prune-blockchain=1
`
}

View file

@ -3,7 +3,7 @@ const _ = require('lodash/fp')
const { ALL } = require('../../plugins/common/ccxt')
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT } = COINS
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, XMR } = COINS
const { bitpay, coinbase, itbit, bitstamp, kraken, binanceus, cex, ftx } = ALL
const TICKER = 'ticker'
@ -32,6 +32,7 @@ const ALL_ACCOUNTS = [
{ code: 'zcashd', display: 'zcashd', class: WALLET, cryptos: [ZEC] },
{ code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] },
{ code: 'dashd', display: 'dashd', class: WALLET, cryptos: [DASH] },
{ code: 'monerod', display: 'monerod', class: WALLET, cryptos: [XMR] },
{ code: 'bitcoincashd', display: 'bitcoincashd', class: WALLET, cryptos: [BCH] },
{ code: 'bitgo', display: 'BitGo', class: WALLET, cryptos: [BTC, ZEC, LTC, BCH, DASH] },
{ code: 'bitstamp', display: 'Bitstamp', class: EXCHANGE, cryptos: bitstamp.CRYPTO },
@ -47,7 +48,7 @@ const ALL_ACCOUNTS = [
{ code: 'mock-id-verify', display: 'Mock ID verifier', class: ID_VERIFIER, dev: true },
{ code: 'twilio', display: 'Twilio', class: SMS },
{ code: 'mailgun', display: 'Mailgun', class: EMAIL },
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: [BTC, ZEC, LTC, DASH, BCH, ETH] },
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: [BTC, ZEC, LTC, DASH, BCH, ETH, XMR] },
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },
{ code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: ALL_CRYPTOS, dev: true }
]

View file

@ -8,7 +8,7 @@ const binanceus = require('../exchange/binanceus')
const cex = require('../exchange/cex')
const ftx = require('../exchange/ftx')
const bitpay = require('../ticker/bitpay')
const { BTC, BCH, DASH, ETH, LTC, ZEC } = COINS
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, XMR } = COINS
const ALL = {
cex: cex,
@ -19,7 +19,7 @@ const ALL = {
itbit: itbit,
bitpay: bitpay,
coinbase: {
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH],
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, XMR],
FIAT: 'ALL_CURRENCIES'
}
}

View file

@ -3,8 +3,9 @@ const axios = require('axios')
const uuid = require('uuid')
const fs = require('fs')
const _ = require('lodash/fp')
const request = require('request-promise')
module.exports = {fetch, parseConf}
module.exports = {fetch, fetchDigest, parseConf}
function fetch (account = {}, method, params) {
params = _.defaultTo([], params)
@ -41,6 +42,40 @@ function fetch (account = {}, method, params) {
})
}
function fetchDigest(account = {}, method, params = []) {
return Promise.resolve(true)
.then(() => {
if (_.isNil(account.port))
throw new Error('port attribute required for jsonRpc')
const headers = {
'Content-Type': 'application/json'
}
const dataString = `{"jsonrpc":"2.0","id":"${uuid.v4()}","method":"${method}","params":${JSON.stringify(params)}}`
const options = {
url: `http://localhost:${account.port}/json_rpc`,
method: 'POST',
headers,
body: dataString,
forever: true,
auth: {
user: account.username,
pass: account.password,
sendImmediately: false
}
}
return request(options)
})
.then((res) => {
const r = JSON.parse(res)
if (r.error) throw r.error
return r.result
})
}
function split (str) {
const i = str.indexOf('=')
if (i === -1) return []

View file

@ -4,8 +4,8 @@ const { ORDER_TYPES } = require('./consts')
const { COINS } = require('lamassu-coins')
const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT]
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, XMR } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, XMR]
const FIAT = ['USD', 'EUR']
const AMOUNT_PRECISION = 6
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']

View file

@ -0,0 +1,196 @@
const fs = require('fs')
const path = require('path')
const _ = require('lodash/fp')
const jsonRpc = require('../../common/json-rpc')
const { utils } = require('lamassu-coins')
const blockchainUtils = require('../../../coin-utils')
const BN = require('../../../bn')
const E = require('../../../error')
const { logger } = require('../../../blockchain/common')
const cryptoRec = utils.getCryptoCurrency('XMR')
const configPath = utils.configPath(cryptoRec, blockchainUtils.blockchainDir())
const walletDir = path.resolve(utils.cryptoDir(cryptoRec, blockchainUtils.blockchainDir()), 'wallets')
const unitScale = cryptoRec.unitScale
const config = jsonRpc.parseConf(configPath)
const rpcConfig = {
username: config['rpc-login'].split(':')[0],
password: config['rpc-login'].split(':')[1],
port: cryptoRec.walletPort || cryptoRec.defaultPort
}
function fetch (method, params) {
return jsonRpc.fetchDigest(rpcConfig, method, params)
}
function handleError (error) {
switch(error.code) {
case -13:
{
if (fs.existsSync(path.resolve(walletDir, 'Wallet')) && fs.existsSync(path.resolve(walletDir, 'Wallet.keys')) && fs.existsSync(path.resolve(walletDir, 'Wallet.address.txt'))) {
logger.debug('Found wallet! Opening wallet...')
return openWallet()
}
logger.debug('Couldn\'t find wallet! Creating...')
return createWallet()
}
case -21:
throw new Error('Wallet already exists!')
case -17:
throw new E.InsufficientFundsError()
case -37:
throw new E.InsufficientFundsError()
default:
throw new Error(
_.join(' ', [
'json-rpc::got error:',
JSON.stringify(_.get('message', error, '')),
JSON.stringify(_.get('response.data.error', error, ''))
])
)
}
}
function openWallet () {
return fetch('open_wallet', { filename: 'Wallet', password: rpcConfig.password })
}
function createWallet () {
return fetch('create_wallet', { filename: 'Wallet', password: rpcConfig.password, language: 'English' })
.then(() => new Promise(() => setTimeout(() => openWallet(), 3000)))
.then(() => fetch('auto_refresh'))
}
function checkCryptoCode (cryptoCode) {
if (cryptoCode !== 'XMR') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
return Promise.resolve()
}
function refreshWallet () {
return fetch('refresh')
}
function accountBalance (cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => refreshWallet())
.then(() => fetch('get_balance', { account_index: 0, address_indices: [0] }))
.then(res => {
return BN(res.unlocked_balance).shift(unitScale).round()
})
.catch(err => handleError(err))
}
function balance (account, cryptoCode) {
return accountBalance(cryptoCode)
}
function sendCoins (account, address, cryptoAtoms, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => refreshWallet())
.then(() => fetch('transfer_split', {
destinations: [{ amount: cryptoAtoms, address }],
account_index: 0,
subaddr_indices: [],
priority: 0,
mixin: 6,
ring_size: 7,
unlock_time: 0,
get_tx_hex: false,
new_algorithm: false,
get_tx_metadata: false
}))
.then(res => ({
fee: BN(res.fee_list[0]).abs(),
txid: res.tx_hash_list[0]
}))
.catch(err => handleError(err))
}
function newAddress (account, info) {
return checkCryptoCode(info.cryptoCode)
.then(() => fetch('create_address', { account_index: 0 }))
.then(res => res.address)
.catch(err => handleError(err))
}
function addressBalance (address, confirmations) {
return fetch('get_address_index', { address: address })
.then(addressRes => fetch('get_balance', { account_index: addressRes.index.major, address_indices: [addressRes.index.minor] }))
.then(res => BN(res.unlocked_balance))
.catch(err => handleError(err))
}
function confirmedBalance (address, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => refreshWallet())
.then(() => addressBalance(address, 1))
.catch(err => handleError(err))
}
function pendingBalance (address, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => refreshWallet())
.then(() => addressBalance(address, 0))
.catch(err => handleError(err))
}
function getStatus (account, toAddress, requested, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => refreshWallet())
.then(() => confirmedBalance(toAddress, cryptoCode))
.then(confirmed => {
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
return pendingBalance(toAddress, cryptoCode)
.then(pending => {
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
return { receivedCryptoAtoms: pending, status: 'notSeen' }
})
})
.catch(err => handleError(err))
}
function newFunding (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => refreshWallet())
.then(() => fetch('get_balance', { account_index: 0, address_indices: [0] }))
.then(balanceRes => Promise.all([
balanceRes,
fetch('create_address', { account_index: 0 })
]))
.then(([balanceRes, addressRes]) => ({
fundingPendingBalance: BN(balanceRes.balance).sub(balanceRes.unlocked_balance),
fundingConfirmedBalance: BN(balanceRes.unlocked_balance),
fundingAddress: addressRes.address
}))
.catch(err => handleError(err))
}
function cryptoNetwork (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => {
switch(parseInt(rpcConfig.port, 10)) {
case 18083:
return 'main'
case 28083:
return 'test'
case 38083:
return 'stage'
default:
return ''
}
})
}
module.exports = {
balance,
sendCoins,
newAddress,
getStatus,
newFunding,
cryptoNetwork
}

96
package-lock.json generated
View file

@ -10931,6 +10931,11 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
@ -10939,6 +10944,77 @@
"number-is-nan": "^1.0.0"
}
},
"keccak": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz",
"integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==",
"requires": {
"node-addon-api": "^2.0.0",
"node-gyp-build": "^4.2.0"
}
},
"keccak256": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/keccak256/-/keccak256-1.0.2.tgz",
"integrity": "sha512-f2EncSgmHmmQOkgxZ+/f2VaWTNkFL6f39VIrpoX+p8cEXJVyyCs/3h9GNz/ViHgwchxvv7oG5mjT2Tk4ZqInag==",
"requires": {
"bn.js": "^4.11.8",
"keccak": "^3.0.1"
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
"requires": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1",
"safe-buffer": "^5.1.2"
}
},
"merkle-lib": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/merkle-lib/-/merkle-lib-2.0.10.tgz",
"integrity": "sha1-grjbrnXieneFOItz+ddyXQ9vMyY="
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
"node-addon-api": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz",
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="
},
"node-gyp-build": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
"integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg=="
},
"pushdata-bitcoin": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz",
"integrity": "sha1-FZMdPNlnreUiBvUjqnMxrvfUOvc=",
"requires": {
"bitcoin-ops": "^1.3.0"
}
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@ -10949,6 +11025,11 @@
"strip-ansi": "^3.0.0"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
@ -18081,11 +18162,21 @@
}
}
},
"request-promise": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz",
"integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==",
"requires": {
"bluebird": "^3.5.0",
"request-promise-core": "1.1.4",
"stealthy-require": "^1.1.1",
"tough-cookie": "^2.3.3"
}
},
"request-promise-core": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz",
"integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==",
"dev": true,
"requires": {
"lodash": "^4.17.19"
}
@ -19805,8 +19896,7 @@
"stealthy-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
"dev": true
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
},
"stellar-base": {
"version": "5.3.0",

View file

@ -64,6 +64,7 @@
"pify": "^3.0.0",
"pretty-ms": "^2.1.0",
"promise-sequential": "^1.1.1",
"request-promise": "^4.2.6",
"semver": "^7.1.3",
"serve-static": "^1.12.4",
"socket.io": "^2.0.3",