Initial work on BitGo integration. Functions as expected.
This commit is contained in:
parent
06ae67f1db
commit
d4715695ad
|
@ -8,6 +8,7 @@ import click
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask.cli import with_appcontext
|
from flask.cli import with_appcontext
|
||||||
from werkzeug.exceptions import MethodNotAllowed, NotFound
|
from werkzeug.exceptions import MethodNotAllowed, NotFound
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||||
PROJECT_ROOT = os.path.join(HERE, os.pardir)
|
PROJECT_ROOT = os.path.join(HERE, os.pardir)
|
||||||
|
@ -167,6 +168,15 @@ def reset_db_chain_data():
|
||||||
|
|
||||||
# Commit state
|
# Commit state
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
# Attempt to reset contribution ID sequence, psql specific. Don't fail out
|
||||||
|
# if this messes up though, just warn them.
|
||||||
|
try:
|
||||||
|
db.engine.execute(text('ALTER SEQUENCE proposal_contribution_id_seq RESTART WITH 1'))
|
||||||
|
except e:
|
||||||
|
print(e)
|
||||||
|
print('Failed to reset contribution id sequence, see above error. Continuing anyway.')
|
||||||
|
|
||||||
print('Successfully wiped chain-dependent db state!')
|
print('Successfully wiped chain-dependent db state!')
|
||||||
print(f'* Deleted {p_count} proposals and their linked entities')
|
print(f'* Deleted {p_count} proposals and their linked entities')
|
||||||
print(f'* Deleted {t_count} tasks')
|
print(f'* Deleted {t_count} tasks')
|
||||||
|
|
|
@ -14,13 +14,26 @@ MINIMUM_BLOCK_CONFIRMATIONS="6"
|
||||||
API_SECRET_HASH=""
|
API_SECRET_HASH=""
|
||||||
API_SECRET_KEY=""
|
API_SECRET_KEY=""
|
||||||
|
|
||||||
|
############################ ADDRESS DERIVATION ############################
|
||||||
|
# You should only set one OR the other. The former will generate addresses #
|
||||||
|
# using Bip32 address derivation. The latter uses BitGo's API. If you set #
|
||||||
|
# both, BitGo takes precedence. API key should ONLY HAVE VIEW ACCESS! #
|
||||||
|
############################################################################
|
||||||
|
|
||||||
|
# BITGO_WALLET_ID=""
|
||||||
|
# BITGO_ACCESS_TOKEN=""
|
||||||
|
|
||||||
|
### OR ###
|
||||||
|
|
||||||
|
# BIP32_XPUB=""
|
||||||
|
|
||||||
|
############################################################################
|
||||||
|
|
||||||
|
|
||||||
# Addresses, run `yarn genaddress` to get sprout information
|
# Addresses, run `yarn genaddress` to get sprout information
|
||||||
SPROUT_ADDRESS=""
|
SPROUT_ADDRESS=""
|
||||||
SPROUT_VIEWKEY=""
|
SPROUT_VIEWKEY=""
|
||||||
|
|
||||||
# extended public seed
|
|
||||||
BIP32_XPUB=""
|
|
||||||
|
|
||||||
# Block heights to fall back on for starting our scan
|
# Block heights to fall back on for starting our scan
|
||||||
MAINNET_START_BLOCK="464000"
|
MAINNET_START_BLOCK="464000"
|
||||||
TESTNET_START_BLOCK="390000"
|
TESTNET_START_BLOCK="390000"
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
"@types/dotenv": "^6.1.0",
|
"@types/dotenv": "^6.1.0",
|
||||||
"@types/ws": "^6.0.1",
|
"@types/ws": "^6.0.1",
|
||||||
"axios": "0.18.0",
|
"axios": "0.18.0",
|
||||||
|
"bitgo": "4.48.1",
|
||||||
"body-parser": "1.18.3",
|
"body-parser": "1.18.3",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"dotenv": "^6.1.0",
|
"dotenv": "^6.1.0",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import node from '../node';
|
import node from '../node';
|
||||||
|
import { extractErrMessage } from '../util';
|
||||||
|
|
||||||
async function printAddressAndKey() {
|
async function printAddressAndKey() {
|
||||||
try {
|
try {
|
||||||
|
@ -9,11 +10,7 @@ async function printAddressAndKey() {
|
||||||
console.log(`SPROUT_ADDRESS="${address}"`);
|
console.log(`SPROUT_ADDRESS="${address}"`);
|
||||||
console.log(`SPROUT_VIEWKEY="${viewkey}"\n`);
|
console.log(`SPROUT_VIEWKEY="${viewkey}"\n`);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
if (err.response && err.response.data) {
|
console.error(extractErrMessage(err));
|
||||||
console.error(err.response.data);
|
|
||||||
} else {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { BitGo, Wallet } from 'bitgo';
|
||||||
|
import bitcore from "zcash-bitcore-lib";
|
||||||
|
import env from './env';
|
||||||
|
import log from './log';
|
||||||
|
import { getNetwork } from './node';
|
||||||
|
|
||||||
|
let bitgoWallet: Wallet;
|
||||||
|
|
||||||
|
export async function initBitGo() {
|
||||||
|
if (!env.BITGO_ACCESS_TOKEN || !env.BITGO_WALLET_ID) {
|
||||||
|
log.info('BITGO environment variables not set, nooping initBitGo');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that we're on mainnet
|
||||||
|
const network = getNetwork();
|
||||||
|
if (network !== bitcore.Networks.mainnet) {
|
||||||
|
throw new Error(`BitGo cannot be used on anything but mainnet, connected node is ${network}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bitgo = new BitGo({
|
||||||
|
env: 'prod', // Non-prod ZEC is not supported
|
||||||
|
accessToken: env.BITGO_ACCESS_TOKEN,
|
||||||
|
});
|
||||||
|
bitgoWallet = await bitgo.coin('zec').wallets().get({ id: env.BITGO_WALLET_ID });
|
||||||
|
log.info(`Initialized BitGo wallet "${bitgoWallet.label()}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getContributionAddress(id: number) {
|
||||||
|
if (!bitgoWallet) {
|
||||||
|
throw new Error('Must run initBitGo before getContributionAddress');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to fetch first
|
||||||
|
const label = `Contribution #${id}`;
|
||||||
|
const res = await bitgoWallet.addresses({ labelContains: label });
|
||||||
|
if (res.addresses.length) {
|
||||||
|
if (res.addresses.length > 1) {
|
||||||
|
log.warn(`Contribution ${id} has ${res.addresses.length} associated with it. Using the first one (${res.addresses[0].address})`);
|
||||||
|
}
|
||||||
|
return res.addresses[0].address;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new one otherwise
|
||||||
|
const createRes = await bitgoWallet.createAddress({ label });
|
||||||
|
log.info(`Generate new address for contribution ${id}`);
|
||||||
|
return createRes.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateLabel(id: number) {
|
||||||
|
return `Contribution #${id}`;
|
||||||
|
}
|
|
@ -18,9 +18,13 @@ const DEFAULTS = {
|
||||||
ZCASH_NODE_PASSWORD: "",
|
ZCASH_NODE_PASSWORD: "",
|
||||||
MINIMUM_BLOCK_CONFIRMATIONS: "6",
|
MINIMUM_BLOCK_CONFIRMATIONS: "6",
|
||||||
|
|
||||||
|
BITGO_WALLET_ID: "",
|
||||||
|
BITGO_ACCESS_TOKEN: "",
|
||||||
|
|
||||||
|
BIP32_XPUB: "",
|
||||||
|
|
||||||
SPROUT_ADDRESS: "",
|
SPROUT_ADDRESS: "",
|
||||||
SPROUT_VIEWKEY: "",
|
SPROUT_VIEWKEY: "",
|
||||||
BIP32_XPUB: "",
|
|
||||||
|
|
||||||
MAINNET_START_BLOCK: "464000",
|
MAINNET_START_BLOCK: "464000",
|
||||||
TESTNET_START_BLOCK: "390000",
|
TESTNET_START_BLOCK: "390000",
|
||||||
|
@ -28,21 +32,39 @@ const DEFAULTS = {
|
||||||
SENTRY_DSN: "",
|
SENTRY_DSN: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const OPTIONAL: { [key: string]: undefined | boolean } = {
|
||||||
|
BITGO_WALLET_ID: true,
|
||||||
|
BITGO_ACCESS_TOKEN: true,
|
||||||
|
BIP32_XPUB: true,
|
||||||
|
// NOTE: Remove these from optional when sapling is ready
|
||||||
|
SPROUT_ADDRESS: true,
|
||||||
|
SPROUT_VIEWKEY: true,
|
||||||
|
}
|
||||||
|
|
||||||
type CustomEnvironment = typeof DEFAULTS;
|
type CustomEnvironment = typeof DEFAULTS;
|
||||||
|
|
||||||
// ignore when testing
|
// ignore when testing
|
||||||
if (process.env.NODE_ENV !== "test") {
|
if (process.env.NODE_ENV !== "test") {
|
||||||
|
// Set environment variables, throw on missing required ones
|
||||||
Object.entries(DEFAULTS).forEach(([k, v]) => {
|
Object.entries(DEFAULTS).forEach(([k, v]) => {
|
||||||
if (!process.env[k]) {
|
if (!process.env[k]) {
|
||||||
const defVal = (DEFAULTS as any)[k];
|
const defVal = (DEFAULTS as any)[k];
|
||||||
if (defVal) {
|
if (defVal) {
|
||||||
console.info(`Using default environment variable ${k}="${defVal}"`);
|
console.info(`Using default environment variable ${k}="${defVal}"`);
|
||||||
process.env[k] = defVal;
|
process.env[k] = defVal;
|
||||||
} else {
|
} else if (!OPTIONAL[k]) {
|
||||||
throw new Error(`Missing required environment variable ${k}`);
|
throw new Error(`Missing required environment variable ${k}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ensure we have either xpub or bitgo, and warn if we have both
|
||||||
|
if (!process.env.BIP32_XPUB && (!process.env.BITGO_WALLET_ID || !process.env.BITGO_ACCESS_TOKEN)) {
|
||||||
|
throw new Error('Either BIP32_XPUB or BITGO_* environment variables required, missing both');
|
||||||
|
}
|
||||||
|
if (process.env.BIP32_XPUB && process.env.BITGO_WALLET_ID) {
|
||||||
|
console.info('BIP32_XPUB and BITGO environment variables set, BIP32_XPUB will be ignored');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (process.env as any) as CustomEnvironment;
|
export default (process.env as any) as CustomEnvironment;
|
||||||
|
|
|
@ -2,6 +2,8 @@ import * as Sentry from "@sentry/node";
|
||||||
import * as Webhooks from "./webhooks";
|
import * as Webhooks from "./webhooks";
|
||||||
import * as RestServer from "./server";
|
import * as RestServer from "./server";
|
||||||
import { initNode } from "./node";
|
import { initNode } from "./node";
|
||||||
|
import { initBitGo } from "./bitgo";
|
||||||
|
import { extractErrMessage } from "./util";
|
||||||
import env from "./env";
|
import env from "./env";
|
||||||
import log from "./log";
|
import log from "./log";
|
||||||
|
|
||||||
|
@ -15,6 +17,7 @@ async function start() {
|
||||||
|
|
||||||
log.info("============== Starting services ==============");
|
log.info("============== Starting services ==============");
|
||||||
await initNode();
|
await initNode();
|
||||||
|
await initBitGo();
|
||||||
await RestServer.start();
|
await RestServer.start();
|
||||||
Webhooks.start();
|
Webhooks.start();
|
||||||
log.info("===============================================");
|
log.info("===============================================");
|
||||||
|
@ -28,4 +31,8 @@ process.on("SIGINT", () => {
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
start();
|
start().catch(err => {
|
||||||
|
Sentry.captureException(err);
|
||||||
|
log.error(`Unexpected error while starting blockchain watcher: ${extractErrMessage(err)}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
|
@ -3,6 +3,7 @@ import bitcore from "zcash-bitcore-lib";
|
||||||
import { captureException } from "@sentry/node";
|
import { captureException } from "@sentry/node";
|
||||||
import env from "./env";
|
import env from "./env";
|
||||||
import log from "./log";
|
import log from "./log";
|
||||||
|
import { extractErrMessage } from "./util";
|
||||||
|
|
||||||
export interface BlockChainInfo {
|
export interface BlockChainInfo {
|
||||||
chain: string;
|
chain: string;
|
||||||
|
@ -166,31 +167,29 @@ export async function initNode() {
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
captureException(err);
|
captureException(err);
|
||||||
log.error(err.response ? err.response.data : err);
|
log.error(extractErrMessage(err));
|
||||||
log.error(
|
log.error(`Failed to connect to zcash node with the following credentials: ${JSON.stringify(rpcOptions, null, 2)}`);
|
||||||
"Failed to connect to zcash node with the following credentials:\r\n",
|
|
||||||
rpcOptions
|
|
||||||
);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if sprout address is readable
|
// Check if sprout address is readable
|
||||||
try {
|
// NOTE: Replace with sapling when ready
|
||||||
if (!env.SPROUT_ADDRESS) {
|
// try {
|
||||||
console.error("Missing SPROUT_ADDRESS environment variable, exiting");
|
// if (!env.SPROUT_ADDRESS) {
|
||||||
process.exit(1);
|
// console.error("Missing SPROUT_ADDRESS environment variable, exiting");
|
||||||
}
|
// process.exit(1);
|
||||||
await node.z_getbalance(env.SPROUT_ADDRESS as string);
|
// }
|
||||||
} catch (err) {
|
// await node.z_getbalance(env.SPROUT_ADDRESS as string);
|
||||||
if (!env.SPROUT_VIEWKEY) {
|
// } catch (err) {
|
||||||
log.error(
|
// if (!env.SPROUT_VIEWKEY) {
|
||||||
"Unable to view SPROUT_ADDRESS and missing SPROUT_VIEWKEY environment variable, exiting"
|
// log.error(
|
||||||
);
|
// "Unable to view SPROUT_ADDRESS and missing SPROUT_VIEWKEY environment variable, exiting"
|
||||||
process.exit(1);
|
// );
|
||||||
}
|
// process.exit(1);
|
||||||
await node.z_importviewingkey(env.SPROUT_VIEWKEY as string);
|
// }
|
||||||
await node.z_getbalance(env.SPROUT_ADDRESS as string);
|
// await node.z_importviewingkey(env.SPROUT_VIEWKEY as string);
|
||||||
}
|
// await node.z_getbalance(env.SPROUT_ADDRESS as string);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNetwork() {
|
export function getNetwork() {
|
||||||
|
@ -210,23 +209,21 @@ export async function getBootstrapBlockHeight(txid: string | undefined) {
|
||||||
block.height - parseInt(env.MINIMUM_BLOCK_CONFIRMATIONS, 10);
|
block.height - parseInt(env.MINIMUM_BLOCK_CONFIRMATIONS, 10);
|
||||||
return height.toString();
|
return height.toString();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
log.warn(`Attempted to get block height for tx ${txid} but failed with the following error: ${extractErrMessage(err)}`);
|
||||||
`Attempted to get block height for tx ${txid} but failed with the following error:\n`,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
console.warn("Falling back to hard-coded starter blocks");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we can't find the latest tx block, fall back to when the grant
|
// If we can't find the latest tx block, fall back to when the grant
|
||||||
// system first launched, and scan from there.
|
// system first launched, and scan from there. Regtest or unknown networks
|
||||||
|
// start from the bottom.
|
||||||
const net = getNetwork();
|
const net = getNetwork();
|
||||||
|
let height = "0";
|
||||||
if (net === bitcore.Networks.mainnet) {
|
if (net === bitcore.Networks.mainnet) {
|
||||||
return env.MAINNET_START_BLOCK;
|
height = env.MAINNET_START_BLOCK;
|
||||||
} else if (net === bitcore.Networks.testnet && !net.regtestEnabled) {
|
} else if (net === bitcore.Networks.testnet && !net.regtestEnabled) {
|
||||||
return env.TESTNET_START_BLOCK;
|
height = env.TESTNET_START_BLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regtest or otherwise unknown networks should start at the bottom
|
log.info(`Falling back to hard-coded starter block height ${height}`);
|
||||||
return "0";
|
return height;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
} from '../store';
|
} from '../store';
|
||||||
import env from '../env';
|
import env from '../env';
|
||||||
import node, { getBootstrapBlockHeight } from '../node';
|
import node, { getBootstrapBlockHeight } from '../node';
|
||||||
import { makeContributionMemo } from '../util';
|
import { makeContributionMemo, extractErrMessage } from '../util';
|
||||||
import log from '../log';
|
import log from '../log';
|
||||||
|
|
||||||
// Configure server
|
// Configure server
|
||||||
|
@ -29,8 +29,16 @@ app.use(authMiddleware);
|
||||||
// Routes
|
// Routes
|
||||||
app.post('/bootstrap', async (req, res) => {
|
app.post('/bootstrap', async (req, res) => {
|
||||||
const { pendingContributions, latestTxId } = req.body;
|
const { pendingContributions, latestTxId } = req.body;
|
||||||
const info = await node.getblockchaininfo();
|
|
||||||
const startHeight = await getBootstrapBlockHeight(latestTxId);
|
let info;
|
||||||
|
let startHeight;
|
||||||
|
try {
|
||||||
|
info = await node.getblockchaininfo();
|
||||||
|
startHeight = await getBootstrapBlockHeight(latestTxId);
|
||||||
|
} catch(err) {
|
||||||
|
log.error(`Unknown node error during bootstrap: ${extractErrMessage(err)}`);
|
||||||
|
return res.status(500).json({ error: 'Unknown zcash node error' });
|
||||||
|
}
|
||||||
|
|
||||||
console.info('Bootstrapping watcher!');
|
console.info('Bootstrapping watcher!');
|
||||||
console.info(' * Start height:', startHeight);
|
console.info(' * Start height:', startHeight);
|
||||||
|
@ -39,11 +47,18 @@ app.post('/bootstrap', async (req, res) => {
|
||||||
console.info('Generating addresses to watch for each contribution...');
|
console.info('Generating addresses to watch for each contribution...');
|
||||||
|
|
||||||
// Running generate address on each will add each contribution to redux state
|
// Running generate address on each will add each contribution to redux state
|
||||||
pendingContributions.forEach((c: any) => {
|
try {
|
||||||
store.dispatch(generateAddresses(c.id));
|
const dispatchers = pendingContributions.map(async (c: any) => {
|
||||||
});
|
const action = await generateAddresses(c.id);
|
||||||
console.info(`Done! Generated ${pendingContributions.length} addresses.`);
|
store.dispatch(action);
|
||||||
store.dispatch(setStartingBlockHeight(startHeight));
|
});
|
||||||
|
await Promise.all(dispatchers);
|
||||||
|
console.info(`Done! Generated ${pendingContributions.length} addresses.`);
|
||||||
|
store.dispatch(setStartingBlockHeight(startHeight));
|
||||||
|
} catch(err) {
|
||||||
|
log.error(`Unknown error during bootstrap address generation: ${extractErrMessage(err)}`);
|
||||||
|
return res.status(500).json({ error: 'Failed to generate addresses for contributions' });
|
||||||
|
}
|
||||||
|
|
||||||
// Send back some basic info about where the chain is at
|
// Send back some basic info about where the chain is at
|
||||||
res.json({
|
res.json({
|
||||||
|
@ -54,20 +69,29 @@ app.post('/bootstrap', async (req, res) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/contribution/addresses', (req, res) => {
|
app.get('/contribution/addresses', async (req, res) => {
|
||||||
const { contributionId } = req.query;
|
const { contributionId } = req.query;
|
||||||
let addresses = getAddressesByContributionId(store.getState(), contributionId)
|
let addresses = getAddressesByContributionId(store.getState(), contributionId)
|
||||||
if (!addresses) {
|
if (!addresses) {
|
||||||
const action = generateAddresses(req.query.contributionId);
|
try {
|
||||||
addresses = action.payload.addresses;
|
const action = await generateAddresses(contributionId);
|
||||||
store.dispatch(action);
|
addresses = action.payload.addresses;
|
||||||
|
store.dispatch(action);
|
||||||
|
} catch(err) {
|
||||||
|
log.error(`Unknown error during address generation for contribution ${contributionId}: ${extractErrMessage(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addresses) {
|
||||||
|
res.json({
|
||||||
|
data: {
|
||||||
|
...addresses,
|
||||||
|
memo: makeContributionMemo(contributionId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ error: 'Failed to generate addresses' });
|
||||||
}
|
}
|
||||||
res.json({
|
|
||||||
data: {
|
|
||||||
...addresses,
|
|
||||||
memo: makeContributionMemo(contributionId),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,7 +120,7 @@ app.post('/contribution/disclosure', async (req, res) => {
|
||||||
return res.status(400).json({ error: err.response.data.error.message });
|
return res.status(400).json({ error: err.response.data.error.message });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.error('Unknown node error:', err.response ? err.response.data : err);
|
log.error(`Unknown node error during disclosure: ${extractErrMessage(err)}`);
|
||||||
return res.status(500).json({ error: 'Unknown zcash node error' });
|
return res.status(500).json({ error: 'Unknown zcash node error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { captureException } from "@sentry/node";
|
import { captureException } from "@sentry/node";
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import log from "../../log";
|
import log from "../../log";
|
||||||
|
import { extractErrMessage } from "../../util";
|
||||||
|
|
||||||
export default function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
|
export default function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
|
||||||
// Non-error responses, or something else handled & responded
|
// Non-error responses, or something else handled & responded
|
||||||
|
@ -9,7 +10,7 @@ export default function errorHandler(err: Error, req: Request, res: Response, ne
|
||||||
}
|
}
|
||||||
|
|
||||||
captureException(err);
|
captureException(err);
|
||||||
log.error(`Uncaught ${err.name} exception at ${req.method} ${req.path}: ${err.message}`);
|
log.error(`Uncaught ${err.name} exception at ${req.method} ${req.path}: ${extractErrMessage(err)}`);
|
||||||
log.debug(`Query: ${JSON.stringify(req.query, null, 2)}`);
|
log.debug(`Query: ${JSON.stringify(req.query, null, 2)}`);
|
||||||
log.debug(`Body: ${JSON.stringify(req.body, null, 2)}`);
|
log.debug(`Body: ${JSON.stringify(req.body, null, 2)}`);
|
||||||
log.debug(`Full stacktrace:\n${err.stack}`);
|
log.debug(`Full stacktrace:\n${err.stack}`);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type, { AddressCollection } from './types';
|
import type, { AddressCollection } from './types';
|
||||||
import { deriveTransparentAddress } from '../util';
|
import { deriveTransparentAddress } from '../util';
|
||||||
import { getNetwork } from '../node';
|
import { getNetwork } from '../node';
|
||||||
|
import { getContributionAddress } from '../bitgo';
|
||||||
import env from '../env';
|
import env from '../env';
|
||||||
|
|
||||||
export function setStartingBlockHeight(height: string | number) {
|
export function setStartingBlockHeight(height: string | number) {
|
||||||
|
@ -10,10 +11,16 @@ export function setStartingBlockHeight(height: string | number) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateAddresses(contributionId: number) {
|
export async function generateAddresses(contributionId: number) {
|
||||||
// 2^31 is the maximum number of BIP32 addresses
|
let transparent;
|
||||||
|
if (env.BITGO_WALLET_ID) {
|
||||||
|
transparent = await getContributionAddress(contributionId);
|
||||||
|
} else {
|
||||||
|
transparent = deriveTransparentAddress(contributionId, getNetwork());
|
||||||
|
}
|
||||||
|
|
||||||
const addresses: AddressCollection = {
|
const addresses: AddressCollection = {
|
||||||
transparent: deriveTransparentAddress(contributionId, getNetwork()),
|
transparent,
|
||||||
sprout: env.SPROUT_ADDRESS,
|
sprout: env.SPROUT_ADDRESS,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -73,3 +73,15 @@ export function sleep(ms: number) {
|
||||||
setTimeout(resolve, ms);
|
setTimeout(resolve, ms);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// They come in all shapes and sizes, and nested data can get truncated as
|
||||||
|
// [object Object], so try to extract the best parts available.
|
||||||
|
export function extractErrMessage(err: any) {
|
||||||
|
if (err.response && err.response.data) {
|
||||||
|
if (err.response.data.error && err.response.data.error.message) {
|
||||||
|
return err.response.data.error.message;
|
||||||
|
}
|
||||||
|
return JSON.stringify(err.response.data, null, 2);
|
||||||
|
}
|
||||||
|
return err.message || err.toString();
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import { Notifier } from "./notifiers/notifier";
|
||||||
import node from "../node";
|
import node from "../node";
|
||||||
import env from "../env";
|
import env from "../env";
|
||||||
import { store } from "../store";
|
import { store } from "../store";
|
||||||
import { sleep } from "../util";
|
import { sleep, extractErrMessage } from "../util";
|
||||||
import log from "../log";
|
import log from "../log";
|
||||||
|
|
||||||
let blockScanTimeout: any = null;
|
let blockScanTimeout: any = null;
|
||||||
|
@ -64,8 +64,7 @@ async function scanBlock(height: number) {
|
||||||
notifiers.forEach(n => n.onNewBlock && n.onNewBlock(block));
|
notifiers.forEach(n => n.onNewBlock && n.onNewBlock(block));
|
||||||
consecutiveBlockFailures = 0;
|
consecutiveBlockFailures = 0;
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
log.warn(err.response ? err.response.data : err);
|
log.warn(`Failed to fetch block ${height}: ${extractErrMessage(err)}`);
|
||||||
log.warn(`Failed to fetch block ${height}, see above error`);
|
|
||||||
consecutiveBlockFailures++;
|
consecutiveBlockFailures++;
|
||||||
// If we fail a certain number of times, it's reasonable to
|
// If we fail a certain number of times, it's reasonable to
|
||||||
// assume that the blockchain is down, and we should just quit.
|
// assume that the blockchain is down, and we should just quit.
|
||||||
|
@ -94,8 +93,7 @@ async function requestBootstrap() {
|
||||||
log.debug('Requesting bootstrap from backend...');
|
log.debug('Requesting bootstrap from backend...');
|
||||||
await send('/blockchain/bootstrap', 'GET');
|
await send('/blockchain/bootstrap', 'GET');
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
log.error(err.response ? err.response.data : err);
|
log.error(`Request for bootstrap failed: ${extractErrMessage(err)}`);
|
||||||
log.error('Request for bootstrap failed, see above for details');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,7 +124,6 @@ const send: Send = (route, method, payload) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
captureException(err);
|
captureException(err);
|
||||||
const errMsg = err.response ? `Response: ${JSON.stringify(err.response.data, null, 2)}` : err.message;
|
log.error(`Webhook server request to ${method} ${route} failed: ${extractErrMessage(err)}`);
|
||||||
log.error(`Webhook server request to ${method} ${route} failed: ${errMsg}`);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
} from "../../../store";
|
} from "../../../store";
|
||||||
import env from "../../../env";
|
import env from "../../../env";
|
||||||
import log from "../../../log";
|
import log from "../../../log";
|
||||||
import { getContributionIdFromMemo, decodeHexMemo, toBaseUnit } from "../../../util";
|
import { getContributionIdFromMemo, decodeHexMemo, toBaseUnit, extractErrMessage } from "../../../util";
|
||||||
|
|
||||||
interface ContributionConfirmationPayload {
|
interface ContributionConfirmationPayload {
|
||||||
to: string;
|
to: string;
|
||||||
|
@ -26,8 +26,9 @@ export default class ContributionNotifier implements Notifier {
|
||||||
|
|
||||||
onNewBlock = (block: BlockWithTransactions) => {
|
onNewBlock = (block: BlockWithTransactions) => {
|
||||||
this.checkBlockForTransparentPayments(block);
|
this.checkBlockForTransparentPayments(block);
|
||||||
this.checkForMemoPayments();
|
// NOTE: Re-enable when sapling is ready
|
||||||
this.checkDisclosuresForPayment(block);
|
// this.checkForMemoPayments();
|
||||||
|
// this.checkDisclosuresForPayment(block);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerSend = (sm: Send) => (this.send = sm);
|
registerSend = (sm: Send) => (this.send = sm);
|
||||||
|
@ -91,7 +92,7 @@ export default class ContributionNotifier implements Notifier {
|
||||||
captureException(err);
|
captureException(err);
|
||||||
log.error(
|
log.error(
|
||||||
'Failed to check sprout address for memo payments:\n',
|
'Failed to check sprout address for memo payments:\n',
|
||||||
err.response ? err.response.data : err,
|
extractErrMessage(err),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -131,9 +132,9 @@ export default class ContributionNotifier implements Notifier {
|
||||||
store.dispatch(confirmPaymentDisclosure(contributionId, disclosure));
|
store.dispatch(confirmPaymentDisclosure(contributionId, disclosure));
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
captureException(err);
|
captureException(err);
|
||||||
log.error(
|
log.warn(
|
||||||
'Encountered an error while checking disclosure:\n',
|
'Encountered an error while checking disclosure:\n',
|
||||||
err.response ? err.response.data : err,
|
extractErrMessage(err),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -50,7 +50,8 @@
|
||||||
// https://www.npmjs.com/package/ts-node#help-my-types-are-missing
|
// https://www.npmjs.com/package/ts-node#help-my-types-are-missing
|
||||||
"paths": {
|
"paths": {
|
||||||
"stdrpc": ["types/stdrpc"],
|
"stdrpc": ["types/stdrpc"],
|
||||||
"zcash-bitcore-lib": ["types/zcash-bitcore-lib"]
|
"zcash-bitcore-lib": ["types/zcash-bitcore-lib"],
|
||||||
|
"bitgo": ["types/bitgo"]
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Source Map Options */
|
/* Source Map Options */
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
// Adapted from documentation here: https://www.bitgo.com/api/v2/?javascript
|
||||||
|
// Far from exhaustive, only functions used are properly typed.
|
||||||
|
|
||||||
|
declare module 'bitgo' {
|
||||||
|
// Wallet
|
||||||
|
interface CreateAddressOptions {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetAddressesOptions {
|
||||||
|
labelContains?: string;
|
||||||
|
limit?: number;
|
||||||
|
mine?: boolean;
|
||||||
|
prevId?: string;
|
||||||
|
chains?: number[];
|
||||||
|
sort?: 1 | -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddressInfo {
|
||||||
|
id: string;
|
||||||
|
address: string;
|
||||||
|
chain: number;
|
||||||
|
index: number;
|
||||||
|
coin: string;
|
||||||
|
lastNonce: number;
|
||||||
|
wallet: string;
|
||||||
|
label: string;
|
||||||
|
addressType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetAddressResponse {
|
||||||
|
coin: string;
|
||||||
|
totalAddressCount: number;
|
||||||
|
pendingAddressCount: number;
|
||||||
|
addresses: AddressInfo[];
|
||||||
|
nextBatchPrevId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Wallet {
|
||||||
|
id(): string;
|
||||||
|
label(): string;
|
||||||
|
createAddress(options?: CreateAddressOptions): Promise<AddressInfo>;
|
||||||
|
addresses(options?: GetAddressesOptions): Promise<GetAddressResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallets
|
||||||
|
interface GetWalletOptions {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Wallets {
|
||||||
|
get(options: GetWalletOptions): Promise<Wallet>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseCoin
|
||||||
|
export class BaseCoin {
|
||||||
|
wallets(): Wallets;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitGo
|
||||||
|
interface BitGoOptions {
|
||||||
|
env: 'test' | 'prod';
|
||||||
|
accessToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BitGo {
|
||||||
|
constructor(options: BitGoOptions);
|
||||||
|
coin(coin: string): BaseCoin;
|
||||||
|
}
|
||||||
|
}
|
1430
blockchain/yarn.lock
1430
blockchain/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue