committed by
GitHub
58 changed files with 2152 additions and 781 deletions
@ -0,0 +1,38 @@ |
|||||
|
--- |
||||
|
name: Bug report |
||||
|
about: Create a report to help us improve |
||||
|
title: '' |
||||
|
labels: '' |
||||
|
assignees: '' |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Describe the bug** |
||||
|
A clear and concise description of what the bug is. |
||||
|
|
||||
|
**To Reproduce** |
||||
|
Steps to reproduce the behavior: |
||||
|
1. Go to '...' |
||||
|
2. Click on '....' |
||||
|
3. Scroll down to '....' |
||||
|
4. See error |
||||
|
|
||||
|
**Expected behavior** |
||||
|
A clear and concise description of what you expected to happen. |
||||
|
|
||||
|
**Screenshots** |
||||
|
If applicable, add screenshots to help explain your problem. |
||||
|
Do not submit screenshots containing PII (logins, passwords, keys, IP addresses, etc) |
||||
|
|
||||
|
**Desktop (please complete the following information):** |
||||
|
- OS: [e.g. Ubuntu vX.X] |
||||
|
- Dojo Version [e.g. 22] |
||||
|
- Dojo Advanced Setups used (external bitcoind, bitcoind exposed to external apps, etc) |
||||
|
|
||||
|
**Smartphone (please complete the following information):** |
||||
|
- Device: [e.g. Samsung Galaxy X] |
||||
|
- OS: [e.g. AndroidX.X] |
||||
|
- Wallet Version [e.g. X.X.X] |
||||
|
|
||||
|
**Additional context** |
||||
|
Add any other context about the problem here. |
@ -0,0 +1,20 @@ |
|||||
|
--- |
||||
|
name: Feature request |
||||
|
about: Suggest an idea for this project |
||||
|
title: '' |
||||
|
labels: '' |
||||
|
assignees: '' |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Is your feature request related to a problem? Please describe.** |
||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] |
||||
|
|
||||
|
**Describe the solution you'd like** |
||||
|
A clear and concise description of what you want to happen. |
||||
|
|
||||
|
**Describe alternatives you've considered** |
||||
|
A clear and concise description of any alternative solutions or features you've considered. |
||||
|
|
||||
|
**Additional context** |
||||
|
Add any other context or screenshots about the feature request here. |
@ -0,0 +1,23 @@ |
|||||
|
--- |
||||
|
name: Support Request |
||||
|
about: Describe your issue |
||||
|
title: '' |
||||
|
labels: '' |
||||
|
assignees: '' |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Describe the issue** |
||||
|
A clear and concise description of what the issue is. |
||||
|
|
||||
|
**Screenshots** |
||||
|
If applicable, add screenshots to help explain your problem. |
||||
|
Do not submit screenshots containing PII (logins, passwords, keys, IP addresses, etc) |
||||
|
|
||||
|
**Desktop (please complete the following information):** |
||||
|
- OS: [e.g. Ubuntu vX.X] |
||||
|
- Dojo Version [e.g. 22] |
||||
|
- Dojo Advanced Setups used (external bitcoind, bitcoind exposed to external apps, etc) |
||||
|
|
||||
|
**Additional context** |
||||
|
Add any other context about the problem here. |
@ -0,0 +1,34 @@ |
|||||
|
######################################### |
||||
|
# CONFIGURATION OF TOR CONTAINER |
||||
|
######################################### |
||||
|
|
||||
|
# |
||||
|
# USE TOR BRIDGES |
||||
|
# |
||||
|
# To get Tor bridges head over to https://bridges.torproject.org and click on |
||||
|
# Get bridges, then you will see a form with "Advanced Options" header |
||||
|
# leave the Pluggable Transport as obfs4 and click on Get Bridges button |
||||
|
# solve the captcah, you will get the bridge addresses (usually 3) |
||||
|
# |
||||
|
# Then, set TOR_USE_BRIDGES to "on" and initialize the TOR_BRIDGE_n options |
||||
|
# with the 3 lines generated by the online tool. |
||||
|
# |
||||
|
# For instance, if the first line generated by the tool is: |
||||
|
# obfs4 24.106.248.94:65531 B9EFBC5... cert=yrX... iat-mode=0 |
||||
|
# You will have to set: |
||||
|
# TOR_BRIDGE_1=obfs4 24.106.248.94:65531 B9EFBC5... cert=yrX... iat-mode=0 |
||||
|
# |
||||
|
|
||||
|
|
||||
|
# Activate the use of Tor bridges |
||||
|
# Value: on | off |
||||
|
TOR_USE_BRIDGES=off |
||||
|
|
||||
|
# Bridge 1 |
||||
|
TOR_BRIDGE_1=ToBeDefined |
||||
|
|
||||
|
# Bridge 2 |
||||
|
TOR_BRIDGE_2=ToBeDefined |
||||
|
|
||||
|
# Bridge 3 |
||||
|
TOR_BRIDGE_3=ToBeDefined |
@ -1,44 +0,0 @@ |
|||||
## Tor opens a socks proxy on port 9050 by default -- even if you don't |
|
||||
## configure one below. Set "SocksPort 0" if you plan to run Tor only |
|
||||
## as a relay, and not make any local application connections yourself. |
|
||||
|
|
||||
# Socks is only available from dojonet |
|
||||
SocksPort 172.28.1.4:9050 |
|
||||
|
|
||||
## Entry policies to allow/deny SOCKS requests based on IP address. |
|
||||
## First entry that matches wins. If no SocksPolicy is set, we accept |
|
||||
## all (and only) requests that reach a SocksPort. Untrusted users who |
|
||||
## can access your SocksPort may be able to learn about the connections |
|
||||
## you make. |
|
||||
|
|
||||
# Socks is only available from dojonet |
|
||||
SocksPolicy accept 172.28.0.0/16 |
|
||||
SocksPolicy reject * |
|
||||
|
|
||||
## The directory for keeping all the keys/etc. By default, we store |
|
||||
## things in $HOME/.tor on Unix, and in Application Data\tor on Windows. |
|
||||
|
|
||||
DataDirectory /var/lib/tor/.tor |
|
||||
DataDirectoryGroupReadable 1 |
|
||||
|
|
||||
|
|
||||
############### This section is just for location-hidden services ### |
|
||||
|
|
||||
## Once you have configured a hidden service, you can look at the |
|
||||
## contents of the file ".../hidden_service/hostname" for the address |
|
||||
## to tell people. |
|
||||
## HiddenServicePort x y:z says to redirect requests on port x to the |
|
||||
## address y:z. |
|
||||
|
|
||||
HiddenServiceDir /var/lib/tor/hsv2dojo |
|
||||
HiddenServiceVersion 2 |
|
||||
HiddenServicePort 80 172.29.1.3:80 |
|
||||
|
|
||||
HiddenServiceDir /var/lib/tor/hsv3dojo |
|
||||
HiddenServiceVersion 3 |
|
||||
HiddenServicePort 80 172.29.1.3:80 |
|
||||
|
|
||||
HiddenServiceDir /var/lib/tor/hsv2bitcoind |
|
||||
HiddenServiceVersion 2 |
|
||||
HiddenServicePort 8333 172.28.1.5:8333 |
|
||||
HiddenServiceDirGroupReadable 1 |
|
@ -0,0 +1,224 @@ |
|||||
|
/*! |
||||
|
* lib/indexer_rpc/rpc-client.js |
||||
|
* Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved. |
||||
|
*/ |
||||
|
'use strict' |
||||
|
|
||||
|
const net = require('net') |
||||
|
const makeConcurrent = require('make-concurrent') |
||||
|
const network = require('../bitcoin/network') |
||||
|
const keys = require('../../keys')[network.key] |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* RPC client for an indexer |
||||
|
* following the Electrum protocol |
||||
|
*/ |
||||
|
class RpcClient { |
||||
|
|
||||
|
/** |
||||
|
* Constructor |
||||
|
*/ |
||||
|
constructor(opts) { |
||||
|
this._opts = { |
||||
|
host: keys.indexer.localIndexer.host, |
||||
|
port: keys.indexer.localIndexer.port, |
||||
|
concurrency: Infinity, |
||||
|
timeout: 10000 |
||||
|
} |
||||
|
|
||||
|
if (this._opts.concurrency !== Infinity) { |
||||
|
this._call = makeConcurrent( |
||||
|
this._call.bind(this), |
||||
|
{concurrency: this._opts.concurrency} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set an option |
||||
|
* @param {string} key |
||||
|
* @param {*} value |
||||
|
* @return {RpcClient} |
||||
|
*/ |
||||
|
set(key, value) { |
||||
|
this._opts[key] = value |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get an option |
||||
|
* @param {string} key |
||||
|
* @return {*} |
||||
|
*/ |
||||
|
get(key) { |
||||
|
return this._opts[key] |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if an error returned by the wrapper |
||||
|
* is a connection error. |
||||
|
* @param {string} err - error message |
||||
|
* @returns {boolean} returns true if message related to a connection error |
||||
|
*/ |
||||
|
static isConnectionError(err) { |
||||
|
if (typeof err != 'string') |
||||
|
return false |
||||
|
|
||||
|
const isTimeoutError = (err.indexOf('connect ETIMEDOUT') != -1) |
||||
|
const isConnRejected = (err.indexOf('Connection Rejected') != -1) |
||||
|
|
||||
|
return (isTimeoutError || isConnRejected) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if the rpc api is ready to process requests |
||||
|
* @returns {Promise} |
||||
|
*/ |
||||
|
static async waitForIndexerRpcApi(opts) { |
||||
|
let client = new RpcClient(opts) |
||||
|
|
||||
|
try { |
||||
|
await client.sendRequest('server.version', 'dojo', ['1.0', '1.4']) |
||||
|
} catch(e) { |
||||
|
client = null |
||||
|
Logger.info('Indexer RPC API is still unreachable. New attempt in 20s.') |
||||
|
return util.delay(20000).then(() => { |
||||
|
return RpcClient.waitForIndexerRpcApi() |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Send multiple requests (batch mode) |
||||
|
* @param {Object[]} batch - array of objects {method: ..., params: ...} |
||||
|
* @return {Promise} |
||||
|
*/ |
||||
|
async sendBatch(batch) { |
||||
|
return this._call(batch, true) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Send multiple requests (flood mode) |
||||
|
* @param {Object[]} batch - array of objects {method: ..., params: ...} |
||||
|
* @return {Promise} |
||||
|
*/ |
||||
|
async sendRequests(batch) { |
||||
|
return this._call(batch, false) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Send a request |
||||
|
* @param {string} method - called method |
||||
|
* @return {Promise} |
||||
|
*/ |
||||
|
async sendRequest(method, ...params) { |
||||
|
const batch = [{method: method, params: params}] |
||||
|
const ret = await this._call(batch, false) |
||||
|
return ret[0] |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Send requests (internal method) |
||||
|
* @param {Object[]} data - array of objects {method: ..., params: ...} |
||||
|
* @returns {Promise} |
||||
|
*/ |
||||
|
async _call(data, batched) { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
let methodId = 0 |
||||
|
let requests = [] |
||||
|
let responses = [] |
||||
|
let response = '' |
||||
|
|
||||
|
const requestErrorMsg = `Indexer JSON-RPC: host=${this._opts.host} port=${this._opts.port}:` |
||||
|
|
||||
|
// Prepare an array of requests
|
||||
|
requests = data.map(req => { |
||||
|
return JSON.stringify({ |
||||
|
jsonrpc: '2.0', |
||||
|
method: req.method, |
||||
|
params: req.params || [], |
||||
|
id: methodId++ |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
// If batch mode
|
||||
|
// send a single batched request
|
||||
|
if (batched) |
||||
|
requests = [`[${requests.join(',')}]`] |
||||
|
|
||||
|
// Initialize the connection
|
||||
|
const conn = net.Socket() |
||||
|
conn.setTimeout(this._opts.timeout) |
||||
|
conn.setEncoding('utf8') |
||||
|
conn.setKeepAlive(true, 0) |
||||
|
conn.setNoDelay(true) |
||||
|
|
||||
|
conn.on('connect', () => { |
||||
|
// Send the requests
|
||||
|
for (let request of requests) |
||||
|
conn.write(request + '\n') |
||||
|
}) |
||||
|
|
||||
|
conn.on('timeout', () => { |
||||
|
const e = new Error('ETIMEDOUT') |
||||
|
e.errorno = 'ETIMEDOUT' |
||||
|
e.code = 'ETIMEDOUT' |
||||
|
e.connect = false |
||||
|
conn.emit('error', e) |
||||
|
}) |
||||
|
|
||||
|
conn.on('data', chunk => { |
||||
|
// Process the received chunk char by char
|
||||
|
for (let c of chunk) { |
||||
|
response += c |
||||
|
// Detect the end of a response
|
||||
|
if (c == '\n') { |
||||
|
try { |
||||
|
// Parse the response
|
||||
|
let parsed = JSON.parse(response) |
||||
|
if (parsed.error) |
||||
|
throw new Error(JSON.stringify(parsed.error)) |
||||
|
// Add the parsed reponse to the array of responses
|
||||
|
if (batched) { |
||||
|
responses = parsed.map(p => { return {idxAddr: p.id, txs: p.result} }) |
||||
|
} else { |
||||
|
responses.push({idxAddr: parsed.id, txs: parsed.result}) |
||||
|
} |
||||
|
// Reset the response
|
||||
|
response = '' |
||||
|
// If all responses have been received
|
||||
|
// close the connection
|
||||
|
if (responses.length == data.length) |
||||
|
conn.end() |
||||
|
} catch (err) { |
||||
|
reject( |
||||
|
new Error(`${requestErrorMsg} Error Parsing JSON: ${err.message}, data: ${response}`) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
conn.on('end', e => { |
||||
|
// Connection closed
|
||||
|
// we can return the responses
|
||||
|
resolve(responses) |
||||
|
}) |
||||
|
|
||||
|
conn.on('error', e => { |
||||
|
reject(new Error(`${requestErrorMsg} Request error: ${e}`)) |
||||
|
}) |
||||
|
|
||||
|
// Connect to the RPC API
|
||||
|
conn.connect({ |
||||
|
host: this._opts.host, |
||||
|
port: this._opts.port |
||||
|
}) |
||||
|
|
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
module.exports = RpcClient |
@ -1,179 +0,0 @@ |
|||||
/*! |
|
||||
* lib/remote-importer\btccom-wrapper.js |
|
||||
* Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved. |
|
||||
*/ |
|
||||
'use strict' |
|
||||
|
|
||||
const rp = require('request-promise-native') |
|
||||
const addrHelper = require('../bitcoin/addresses-helper') |
|
||||
const util = require('../util') |
|
||||
const Logger = require('../logger') |
|
||||
const network = require('../bitcoin/network') |
|
||||
const keys = require('../../keys')[network.key] |
|
||||
const Wrapper = require('./wrapper') |
|
||||
|
|
||||
|
|
||||
/** |
|
||||
* Wrapper for the btc.com block explorer APIs |
|
||||
*/ |
|
||||
class BtcComWrapper extends Wrapper { |
|
||||
|
|
||||
/** |
|
||||
* Constructor |
|
||||
*/ |
|
||||
constructor(url) { |
|
||||
super(url, keys.explorers.socks5Proxy) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Send a GET request to the API |
|
||||
* @param {string} route |
|
||||
* @returns {Promise} |
|
||||
*/ |
|
||||
async _get(route) { |
|
||||
const params = { |
|
||||
url: `${this.base}${route}`, |
|
||||
method: 'GET', |
|
||||
json: true, |
|
||||
timeout: 15000 |
|
||||
} |
|
||||
|
|
||||
// Sets socks proxy agent if required
|
|
||||
if (keys.explorers.socks5Proxy != null) |
|
||||
params['agent'] = this.socksProxyAgent |
|
||||
|
|
||||
return rp(params) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get a page of transactions related to a given address |
|
||||
* @param {string} address - bitcoin address |
|
||||
* @param {integer} page - page index |
|
||||
* @returns {Promise} |
|
||||
*/ |
|
||||
async _getTxsForAddress(address, page) { |
|
||||
const uri = `/address/${address}/tx?page=${page}&verbose=1` |
|
||||
const results = await this._get(uri) |
|
||||
return results.data.list.map(tx => tx.hash) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Retrieve information for a given address |
|
||||
* @param {string} address - bitcoin address |
|
||||
* @param {boolean} filterAddr - True if an upper bound should be used |
|
||||
* for #transactions associated to the address, False otherwise |
|
||||
* @returns {Promise} returns an object |
|
||||
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>} |
|
||||
*/ |
|
||||
async getAddress(address, filterAddr) { |
|
||||
const reqAddr = addrHelper.isBech32(address) |
|
||||
? addrHelper.getScriptHashFromBech32(address) |
|
||||
: address |
|
||||
|
|
||||
const uri = `/address/${reqAddr}` |
|
||||
const result = await this._get(uri) |
|
||||
|
|
||||
const ret = { |
|
||||
address: address, |
|
||||
ntx: result.data.tx_count, |
|
||||
txids: [] |
|
||||
} |
|
||||
|
|
||||
// Check if we should filter this address
|
|
||||
if (filterAddr && ret.ntx > keys.addrFilterThreshold) { |
|
||||
Logger.info(` import of ${ret.address} rejected (too many transactions - ${ret.ntx})`) |
|
||||
return ret |
|
||||
} |
|
||||
|
|
||||
const nbPagesApi = Math.ceil(ret.ntx / BtcComWrapper.NB_TXS_PER_PAGE) |
|
||||
const nbPages = Math.min(20, nbPagesApi) |
|
||||
|
|
||||
const aPages = new Array(nbPages) |
|
||||
const listPages = Array.from(aPages, (val, idx) => idx + 1) |
|
||||
|
|
||||
const results = await util.seriesCall(listPages, idx => { |
|
||||
return this._getTxsForAddress(reqAddr, idx) |
|
||||
}) |
|
||||
|
|
||||
for (let txids of results) |
|
||||
ret.txids = ret.txids.concat(txids) |
|
||||
|
|
||||
return ret |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Retrieve information for a given list of addresses |
|
||||
* @param {string[]} addresses - array of bitcoin addresses |
|
||||
* @param {boolean} filterAddr - True if an upper bound should be used |
|
||||
* for #transactions associated to the address, False otherwise |
|
||||
* @returns {Promise} returns an array of objects |
|
||||
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>} |
|
||||
*/ |
|
||||
async getAddresses(addresses, filterAddr) { |
|
||||
const ret = [] |
|
||||
const reqAddresses = [] |
|
||||
const xlatedBech32Addr = {} |
|
||||
|
|
||||
for (let a of addresses) { |
|
||||
if (addrHelper.isBech32(a)) { |
|
||||
const scriptHash = addrHelper.getScriptHashFromBech32(a) |
|
||||
reqAddresses.push(scriptHash) |
|
||||
xlatedBech32Addr[scriptHash] = a |
|
||||
} else { |
|
||||
reqAddresses.push(a) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Send a batch request for all the addresses
|
|
||||
const strReqAddresses = reqAddresses.join(',') |
|
||||
const uri = `/address/${strReqAddresses}` |
|
||||
const results = await this._get(uri) |
|
||||
|
|
||||
const foundAddresses = Array.isArray(results.data) |
|
||||
? results.data |
|
||||
: [results.data] |
|
||||
|
|
||||
for (let a of foundAddresses) { |
|
||||
if (a && a.tx_count > 0) { |
|
||||
// Translate bech32 address
|
|
||||
const address = xlatedBech32Addr.hasOwnProperty(a.address) |
|
||||
? xlatedBech32Addr[a.address] |
|
||||
: a.address |
|
||||
|
|
||||
if (a.tx_count <= 2) { |
|
||||
// Less than 3 transactions for this address
|
|
||||
// all good
|
|
||||
const retAddr = { |
|
||||
address: address, |
|
||||
ntx: a.tx_count, |
|
||||
txids: [] |
|
||||
} |
|
||||
|
|
||||
retAddr.txids = (a.tx_count == 1) |
|
||||
? [a.first_tx] |
|
||||
: [a.first_tx, a.last_tx] |
|
||||
|
|
||||
ret.push(retAddr) |
|
||||
|
|
||||
} else { |
|
||||
// More than 2 transactions for this address
|
|
||||
// We need more requests to the API
|
|
||||
if (filterAddr && a.tx_count > keys.addrFilterThreshold) { |
|
||||
Logger.info(` import of ${address} rejected (too many transactions - ${a.tx_count})`) |
|
||||
} else { |
|
||||
const retAddr = await this.getAddress(address) |
|
||||
ret.push(retAddr) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return ret |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
|
|
||||
// BTC.COM acepts a max of 50txs per page
|
|
||||
BtcComWrapper.NB_TXS_PER_PAGE = 50 |
|
||||
|
|
||||
module.exports = BtcComWrapper |
|
@ -0,0 +1,131 @@ |
|||||
|
/*! |
||||
|
* lib/remote-importer\esplora-wrapper.js |
||||
|
* Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved. |
||||
|
*/ |
||||
|
'use strict' |
||||
|
|
||||
|
const rp = require('request-promise-native') |
||||
|
const addrHelper = require('../bitcoin/addresses-helper') |
||||
|
const util = require('../util') |
||||
|
const Logger = require('../logger') |
||||
|
const network = require('../bitcoin/network') |
||||
|
const keys = require('../../keys')[network.key] |
||||
|
const Wrapper = require('./wrapper') |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* Wrapper for the esplora block explorer APIs |
||||
|
*/ |
||||
|
class EsploraWrapper extends Wrapper { |
||||
|
|
||||
|
/** |
||||
|
* Constructor |
||||
|
*/ |
||||
|
constructor(url) { |
||||
|
super(url, keys.indexer.socks5Proxy) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Send a GET request to the API |
||||
|
* @param {string} route |
||||
|
* @returns {Promise} |
||||
|
*/ |
||||
|
async _get(route) { |
||||
|
const params = { |
||||
|
url: `${this.base}${route}`, |
||||
|
method: 'GET', |
||||
|
json: true, |
||||
|
timeout: 15000 |
||||
|
} |
||||
|
|
||||
|
// Sets socks proxy agent if required
|
||||
|
if (keys.indexer.socks5Proxy != null) |
||||
|
params['agent'] = this.socksProxyAgent |
||||
|
|
||||
|
return rp(params) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get a page of transactions related to a given address |
||||
|
* @param {string} address - bitcoin address |
||||
|
* @param {string} lastSeenTxid - last seen txid |
||||
|
* (see https://github.com/Blockstream/esplora/blob/master/API.md)
|
||||
|
* @returns {Promise} |
||||
|
*/ |
||||
|
async _getTxsForAddress(address, lastSeenTxid) { |
||||
|
let uri = `/api/address/${address}/txs` |
||||
|
if (lastSeenTxid) |
||||
|
uri = uri + `/chain/${lastSeenTxid}` |
||||
|
|
||||
|
const results = await this._get(uri) |
||||
|
return results.map(tx => tx.txid) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Retrieve information for a given address |
||||
|
* @param {string} address - bitcoin address |
||||
|
* @param {boolean} filterAddr - True if an upper bound should be used |
||||
|
* for #transactions associated to the address, False otherwise |
||||
|
* @returns {Promise} returns an object |
||||
|
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>} |
||||
|
*/ |
||||
|
async getAddress(address, filterAddr) { |
||||
|
const ret = { |
||||
|
address: address, |
||||
|
ntx: 0, |
||||
|
txids: [] |
||||
|
} |
||||
|
|
||||
|
let lastSeenTxid = null |
||||
|
|
||||
|
while (true) { |
||||
|
const txids = await this._getTxsForAddress(address, lastSeenTxid) |
||||
|
|
||||
|
if (txids.length == 0) |
||||
|
// we have all the transactions
|
||||
|
return ret |
||||
|
|
||||
|
ret.txids = ret.txids.concat(txids) |
||||
|
ret.ntx += ret.txids.length |
||||
|
|
||||
|
if (txids.length < EsploraWrapper.NB_TXS_PER_PAGE) { |
||||
|
// we have all the transactions
|
||||
|
return ret |
||||
|
} else if (filterAddr && ret.ntx > keys.addrFilterThreshold) { |
||||
|
// we have too many transactions
|
||||
|
Logger.info(` import of ${ret.address} rejected (too many transactions - ${ret.ntx})`) |
||||
|
ret.txids = [] |
||||
|
ret.ntx = 0 |
||||
|
return ret |
||||
|
} else { |
||||
|
// we need a new iteration
|
||||
|
lastSeenTxid = txids[txids.length-1] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Retrieve information for a given list of addresses |
||||
|
* @param {string[]} addresses - array of bitcoin addresses |
||||
|
* @param {boolean} filterAddr - True if an upper bound should be used |
||||
|
* for #transactions associated to the address, False otherwise |
||||
|
* @returns {Promise} returns an array of objects |
||||
|
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>} |
||||
|
*/ |
||||
|
async getAddresses(addresses, filterAddr) { |
||||
|
const ret = [] |
||||
|
|
||||
|
for (let a of addresses) { |
||||
|
const retAddr = await this.getAddress(a, filterAddr) |
||||
|
ret.push(retAddr) |
||||
|
} |
||||
|
|
||||
|
return ret |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
// Esplora returns a max of 25 txs per page
|
||||
|
EsploraWrapper.NB_TXS_PER_PAGE = 25 |
||||
|
|
||||
|
module.exports = EsploraWrapper |
@ -1,90 +0,0 @@ |
|||||
/*! |
|
||||
* lib/remote-importer/insight-wrapper.js |
|
||||
* Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved. |
|
||||
*/ |
|
||||
'use strict' |
|
||||
|
|
||||
const rp = require('request-promise-native') |
|
||||
const Logger = require('../logger') |
|
||||
const network = require('../bitcoin/network') |
|
||||
const keys = require('../../keys')[network.key] |
|
||||
const Wrapper = require('./wrapper') |
|
||||
|
|
||||
|
|
||||
/** |
|
||||
* Wrapper for the Insight block explorer APIs |
|
||||
*/ |
|
||||
class InsightWrapper extends Wrapper { |
|
||||
|
|
||||
/** |
|
||||
* Constructor |
|
||||
*/ |
|
||||
constructor(url) { |
|
||||
super(url, keys.explorers.socks5Proxy) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Send a GET request to the API |
|
||||
* @param {string} route |
|
||||
* @returns {Promise} |
|
||||
*/ |
|
||||
async _get(route) { |
|
||||
const params = { |
|
||||
url: `${this.base}${route}`, |
|
||||
method: 'GET', |
|
||||
json: true, |
|
||||
timeout: 15000 |
|
||||
} |
|
||||
|
|
||||
// Sets socks proxy agent if required
|
|
||||
if (keys.explorers.socks5Proxy != null) |
|
||||
params['agent'] = this.socksProxyAgent |
|
||||
|
|
||||
return rp(params) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Retrieve information for a given address |
|
||||
* @param {string} address - bitcoin address |
|
||||
* @param {boolean} filterAddr - True if an upper bound should be used |
|
||||
* for #transactions associated to the address, False otherwise |
|
||||
* @returns {Promise} returns an object |
|
||||
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>} |
|
||||
*/ |
|
||||
async getAddress(address, filterAddr) { |
|
||||
const uri = `/addr/${address}` |
|
||||
// Param filterAddr isn't used for insight
|
|
||||
const result = await this._get(uri) |
|
||||
|
|
||||
const ret = { |
|
||||
address: result.addrStr, |
|
||||
txids: [], |
|
||||
ntx: result.txApperances |
|
||||
} |
|
||||
|
|
||||
// Check if we should filter this address
|
|
||||
if (filterAddr && ret.ntx > keys.addrFilterThreshold) { |
|
||||
Logger.info(` import of ${ret.address} rejected (too many transactions - ${ret.ntx})`) |
|
||||
return ret |
|
||||
} |
|
||||
|
|
||||
ret.txids = result.transactions |
|
||||
return ret |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Retrieve information for a given list of addresses |
|
||||
* @param {string} addresses - array of bitcoin addresses |
|
||||
* @param {boolean} filterAddr - True if an upper bound should be used |
|
||||
* for #transactions associated to the address, False otherwise |
|
||||
* @returns {Promise} returns an array of objects |
|
||||
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>} |
|
||||
*/ |
|
||||
async getAddresses(addresses, filterAddr) { |
|
||||
// Not implemented for this api
|
|
||||
throw "Not implemented" |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
|
|
||||
module.exports = InsightWrapper |
|
@ -0,0 +1,143 @@ |
|||||
|
/*! |
||||
|
* lib/remote-importer/local-indexer-wrapper.js |
||||
|
* Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved. |
||||
|
*/ |
||||
|
'use strict' |
||||
|
|
||||
|
const bitcoin = require('bitcoinjs-lib') |
||||
|
const Logger = require('../logger') |
||||
|
const util = require('../util') |
||||
|
const network = require('../bitcoin/network') |
||||
|
const activeNet = network.network |
||||
|
const keys = require('../../keys')[network.key] |
||||
|
const RpcClient = require('../indexer-rpc/rpc-client') |
||||
|
const Wrapper = require('./wrapper') |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* Wrapper for a local indexer |
||||
|
* Currently supports indexers |
||||
|
* providing a RPC API compliant |
||||
|
* with a subset of the electrum protocol |
||||
|
*/ |
||||
|
class LocalIndexerWrapper extends Wrapper { |
||||
|
|
||||
|
/** |
||||
|
* Constructor |
||||
|
*/ |
||||
|
constructor() { |
||||
|
super(null, null) |
||||
|
// RPC client
|
||||
|
this.client = new RpcClient() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Translate a bitcoin address into a script hash |
||||
|
* (@see https://electrumx.readthedocs.io/en/latest/protocol-basics.html#script-hashes)
|
||||
|
* @param {string} address - bitcoin address |
||||
|
* @returns {string} returns the script hash associated to the address |
||||
|
*/ |
||||
|
_getScriptHash(address) { |
||||
|
const bScriptPubKey = bitcoin.address.toOutputScript(address, activeNet) |
||||
|
const bScriptHash = bitcoin.crypto.sha256(bScriptPubKey) |
||||
|
return util.reverseBuffer(bScriptHash).toString('hex') |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Retrieve information for a given address |
||||
|
* @param {string} address - bitcoin address |
||||
|
* @param {boolean} filterAddr - True if an upper bound should be used |
||||
|
* for #transactions associated to the address, False otherwise |
||||
|
* @returns {Promise} returns an object |
||||
|
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>} |
||||
|
*/ |
||||
|
async getAddress(address, filterAddr) { |
||||
|
const ret = { |
||||
|
address: address, |
||||
|
ntx: 0, |
||||
|
txids: [] |
||||
|
} |
||||
|
|
||||
|
const scriptHash = this._getScriptHash(address) |
||||
|
|
||||
|
const results = await this.client.sendRequest( |
||||
|
LocalIndexerWrapper.GET_HISTORY_RPC_CMD, |
||||
|
scriptHash |
||||
|
) |
||||
|
|
||||
|
for (let r of results.txs) { |
||||
|
ret.txids.push(r.tx_hash) |
||||
|
ret.ntx++ |
||||
|
} |
||||
|
|
||||
|
if (filterAddr && ret.ntx > keys.addrFilterThreshold) { |
||||
|
Logger.info(` import of ${address} rejected (too many transactions - ${ret.ntx})`) |
||||
|
return { |
||||
|
address: address, |
||||
|
ntx: 0, |
||||
|
txids: [] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ret |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Retrieve information for a given list of addresses |
||||
|
* @param {string} addresses - array of bitcoin addresses |
||||
|
* @param {boolean} filterAddr - True if an upper bound should be used |
||||
|
* for #transactions associated to the address, False otherwise |
||||
|
* @returns {Promise} returns an array of objects |
||||
|
* { address: <bitcoin_address>, txids: <txids>, ntx: <total_nb_txs>} |
||||
|
*/ |
||||
|
async getAddresses(addresses, filterAddr) { |
||||
|
const ret = {} |
||||
|
|
||||
|
// Build an array of script hashes
|
||||
|
const scriptHashes = addresses.map(a => this._getScriptHash(a)) |
||||
|
|
||||
|
// Build an array of commands
|
||||
|
const commands = scriptHashes.map(s => { |
||||
|
return { |
||||
|
method: LocalIndexerWrapper.GET_HISTORY_RPC_CMD, |
||||
|
params: [s] |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// Send the requests
|
||||
|
const results = (keys.indexer.localIndexer.batchRequests == 'active') |
||||
|
? await this.client.sendBatch(commands) |
||||
|
: await this.client.sendRequests(commands) |
||||
|
|
||||
|
for (let r of results) { |
||||
|
const addr = addresses[r.idxAddr] |
||||
|
const txids = r.txs.map(t => t.tx_hash) |
||||
|
|
||||
|
ret[addr] = { |
||||
|
address: addr, |
||||
|
ntx: txids.length, |
||||
|
txids: txids |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const aRet = Object.values(ret) |
||||
|
|
||||
|
for (let i in aRet) { |
||||
|
if (filterAddr && aRet[i].ntx > keys.addrFilterThreshold) { |
||||
|
Logger.info(` import of ${aRet[i].address} rejected (too many transactions - ${aRet[i].ntx})`) |
||||
|
aRet.splice(i, 1) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return aRet |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get history RPC command (Electrum protocol) |
||||
|
*/ |
||||
|
LocalIndexerWrapper.GET_HISTORY_RPC_CMD = 'blockchain.scripthash.get_history' |
||||
|
|
||||
|
|
||||
|
module.exports = LocalIndexerWrapper |
File diff suppressed because it is too large
@ -0,0 +1,79 @@ |
|||||
|
/*! |
||||
|
* tracker/tracker-rest-api.js |
||||
|
* Copyright (c) 2016-2019, Samourai Wallet (CC BY-NC-ND 4.0 License). |
||||
|
*/ |
||||
|
'use strict' |
||||
|
|
||||
|
const qs = require('querystring') |
||||
|
const validator = require('validator') |
||||
|
const bodyParser = require('body-parser') |
||||
|
const Logger = require('../lib/logger') |
||||
|
const errors = require('../lib/errors') |
||||
|
const authMgr = require('../lib/auth/authorizations-manager') |
||||
|
const HttpServer = require('../lib/http-server/http-server') |
||||
|
const network = require('../lib/bitcoin/network') |
||||
|
const keys = require('../keys')[network.key] |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* Tracker API endpoints |
||||
|
*/ |
||||
|
class TrackerRestApi { |
||||
|
|
||||
|
/** |
||||
|
* Constructor |
||||
|
* @param {pushtx.HttpServer} httpServer - HTTP server |
||||
|
* @param {tracker.Tracker} tracker - tracker |
||||
|
*/ |
||||
|
constructor(httpServer, tracker) { |
||||
|
this.httpServer = httpServer |
||||
|
this.tracker = tracker |
||||
|
|
||||
|
const urlencodedParser = bodyParser.urlencoded({ extended: true }) |
||||
|
|
||||
|
// Establish routes. Proxy server strips /pushtx
|
||||
|
this.httpServer.app.get( |
||||
|
`/${keys.prefixes.support}/rescan`, |
||||
|
authMgr.checkHasAdminProfile.bind(authMgr), |
||||
|
this.getBlocksRescan.bind(this), |
||||
|
HttpServer.sendAuthError |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Rescan a range of blocks |
||||
|
*/ |
||||
|
async getBlocksRescan(req, res) { |
||||
|
// Check request arguments
|
||||
|
if (!req.query) |
||||
|
return HttpServer.sendError(res, errors.body.INVDATA) |
||||
|
|
||||
|
if (!req.query.fromHeight || !validator.isInt(req.query.fromHeight)) |
||||
|
return HttpServer.sendError(res, errors.body.INVDATA) |
||||
|
|
||||
|
if (req.query.toHeight && !validator.isInt(req.query.toHeight)) |
||||
|
return HttpServer.sendError(res, errors.body.INVDATA) |
||||
|
|
||||
|
// Retrieve the request arguments
|
||||
|
const fromHeight = parseInt(req.query.fromHeight) |
||||
|
const toHeight = req.query.toHeight ? parseInt(req.query.toHeight) : fromHeight |
||||
|
|
||||
|
if (req.query.toHeight && (toHeight < fromHeight)) |
||||
|
return HttpServer.sendError(res, errors.body.INVDATA) |
||||
|
|
||||
|
try { |
||||
|
await this.tracker.blockchainProcessor.rescanBlocks(fromHeight, toHeight) |
||||
|
const ret = { |
||||
|
status: 'Rescan complete', |
||||
|
fromHeight: fromHeight, |
||||
|
toHeight: toHeight |
||||
|
} |
||||
|
HttpServer.sendRawData(res, JSON.stringify(ret, null, 2)) |
||||
|
} catch(e) { |
||||
|
return HttpServer.sendError(res, e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
module.exports = TrackerRestApi |
Loading…
Reference in new issue