Skip to content

Commit

Permalink
feat: added blockchain.com api, fix multichain endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
0x31 committed Mar 2, 2022
1 parent 417e8c1 commit ad95456
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 41 deletions.
187 changes: 187 additions & 0 deletions src/common/apis/blockchain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import axios from "axios";
import { URLSearchParams } from "url";

import { sortUTXOs, UTXO } from "../../lib/utxo";
import { DEFAULT_TIMEOUT } from "./timeout";

export enum BlockchainNetwork {
Bitcoin = "btc",
BitcoinCash = "bch",
BitcoinTestnet = "btc-testnet",
BitcoinCashTestnet = "bch-testnet",
}

interface BlockchainTransaction {
txid: string; // "550b293355f5274e513c65f846311fd5817d13bcfcd492ab94ff2725ba94f21e"
size: number; // 124
version: number; // 1
locktime: number; // 0
fee: number; // 0
inputs: [
{
coinbase: boolean; // true
txid: string; // "0000000000000000000000000000000000000000000000000000000000000000"
output: number; // 4294967295
sigscript: string; // "03e3b9162f696d2f"
sequence: number; // 4294967295
pkscript: null;
value: null;
address: null;
witness: unknown[];
}
];
outputs: [
{
address: string; // "bchtest:qp7k5sm9dcmvse2rgmkj2ktylm9fgqcnv5kp2hrs0h"
pkscript: string; // "76a9147d6a43656e36c8654346ed255964feca9403136588ac"
value: number; // 39062500
spent: boolean; // false
spender: null;
},
{
address: null;
pkscript: string; // "6a14883805620000000000000000faee4177fe240000"
value: number; // 0
spent: boolean; // false
spender: null;
}
];
block: {
height?: number; //1489379
position?: number; // 0
mempool?: number;
};
deleted: boolean; // false
time: number; // 1646186011
rbf: boolean; // false
weight: number; // 496
}

const fetchLatestBlock = async (
network: BlockchainNetwork
): Promise<number> => {
const statsUrl = `https://api.blockchain.info/haskoin-store/${network}/block/best?notx=true`;
const statsResponse = (await axios.get<{ height: number }>(statsUrl)).data;
return statsResponse.height;
};

const fetchUTXO =
(network: BlockchainNetwork) =>
async (txHash: string, vOut: number): Promise<UTXO> => {
const url = `https://api.blockchain.info/haskoin-store/${network}/transaction/${txHash}`;

const response = (
await axios.get<BlockchainTransaction>(`${url}`, {
timeout: DEFAULT_TIMEOUT,
})
).data;

const confirmations =
!response.block || !response.block.height
? 0
: Math.max(
(await fetchLatestBlock(network)) -
response.block.height +
1,
0
);

return {
txHash,
vOut,
amount: response.outputs[vOut].value,
confirmations,
};
};

const fetchUTXOs =
(network: BlockchainNetwork) =>
async (
address: string,
confirmations: number,
limit: number = 25,
offset: number = 0
): Promise<readonly UTXO[]> =>
fetchTXs(network)(address, confirmations, limit, offset, true);

const fetchTXs =
(network: BlockchainNetwork) =>
async (
address: string,
confirmations: number = 0,
limit: number = 25,
offset: number = 0,
onlyUnspent: boolean = false
): Promise<readonly UTXO[]> => {
const url = `https://api.blockchain.info/haskoin-store/${network}/address/${address}/transactions/full?limit=${limit}&offset=${offset}`;
const response = (
await axios.get<BlockchainTransaction[]>(url, {
timeout: DEFAULT_TIMEOUT,
})
).data;

let latestBlock: number | undefined;

const received: UTXO[] = [];

for (const tx of response) {
latestBlock = latestBlock || (await fetchLatestBlock(network));
const txConfirmations =
tx.block && tx.block.height
? Math.max(latestBlock - tx.block.height + 1, 0)
: 0;
for (let i = 0; i < tx.outputs.length; i++) {
const vout = tx.outputs[i];
if (
vout.address === address &&
// If the onlyUnspent flag is true, check that the tx is unspent.
(!onlyUnspent || vout.spent === false)
) {
received.push({
txHash: tx.txid,
amount: vout.value,
vOut: i,
confirmations: txConfirmations,
});
}
}
}

return received
.filter(
(utxo) =>
confirmations === 0 || utxo.confirmations >= confirmations
)
.sort(sortUTXOs);
};

export const broadcastTransaction =
(network: BlockchainNetwork) =>
async (txHex: string): Promise<string> => {
if (network !== BlockchainNetwork.Bitcoin) {
throw new Error(
`Broadcasting ${network} transactions not supported by endpoint.`
);
}
const url = `https://blockchain.info/pushtx`;

const params = new URLSearchParams();
params.append("tx", txHex);

const response = await axios.post(url, params, {
timeout: DEFAULT_TIMEOUT,
});
if ((response.data as any).error) {
throw new Error((response.data as any).error);
}
// TODO
return response.data;
};

export const Blockchain = {
networks: BlockchainNetwork,
fetchUTXO,
fetchUTXOs,
broadcastTransaction,
fetchTXs,
};
54 changes: 29 additions & 25 deletions src/common/apis/jsonrpc.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,38 @@
import axios from "axios";

import { DEFAULT_TIMEOUT } from "./timeout";

export const MULTICHAIN_URLS = {
BTC: "https://multichain.renproject.io/mainnet/bitcoind",
BTCTEST: "https://multichain-staging.renproject.io/testnet/bitcoind",
ZEC: "https://multichain.renproject.io/mainnet/zcashd",
ZECTEST: "https://multichain-staging.renproject.io/testnet/zcashd",
BCH: "https://multichain.renproject.io/mainnet/bitcoincashd",
BCHTEST: "https://multichain-staging.renproject.io/testnet/bitcoincashd",
BTC: "https://multichain-web-proxy.herokuapp.com/multichain-bitcoin",
BTCTEST:
"https://multichain-web-proxy.herokuapp.com/multichain-bitcoin-testnet",
ZEC: "https://multichain-web-proxy.herokuapp.com/multichain-zcash",
ZECTEST:
"https://multichain-web-proxy.herokuapp.com/multichain-zcash-testnet",
BCH: "https://multichain-web-proxy.herokuapp.com/multichain-bitcoincash",
BCHTEST:
"https://multichain-web-proxy.herokuapp.com/multichain-bitcoincash-testnet",
};

const broadcastTransaction = (url: string) => async (
txHex: string
): Promise<string> => {
const response = await axios.post<{
result: string;
error: null;
id: string | number;
}>(
url,
{
jsonrpc: "1.0",
id: "67",
method: "sendrawtransaction",
params: [txHex],
},
{ timeout: DEFAULT_TIMEOUT }
);
return response.data.result;
};
const broadcastTransaction =
(url: string) =>
async (txHex: string): Promise<string> => {
const response = await axios.post<{
result: string;
error: null;
id: string | number;
}>(
url,
{
jsonrpc: "1.0",
id: "67",
method: "sendrawtransaction",
params: [txHex],
},
{ timeout: DEFAULT_TIMEOUT }
);
return response.data.result;
};

export const JSONRPC = {
broadcastTransaction,
Expand Down
3 changes: 1 addition & 2 deletions src/common/libraries/bitgoUtxoLib.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as bitcoin from "bitgo-utxo-lib";

import BigNumber from "bignumber.js";
import * as bitcoin from "bitgo-utxo-lib";

import { UTXO } from "../../lib/utxo";

Expand Down
49 changes: 45 additions & 4 deletions src/handlers/BCH/BCHHandler.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import * as bitcoin from "bitgo-utxo-lib";

import { toCashAddress, toLegacyAddress } from "bchaddrjs";
import BigNumber from "bignumber.js";
import * as bitcoin from "bitgo-utxo-lib";
import { List } from "immutable";

import { BitcoinDotCom } from "../../common/apis/bitcoinDotCom";
import { Blockchain, BlockchainNetwork } from "../../common/apis/blockchain";
import { Blockchair } from "../../common/apis/blockchair";
import { Insight } from "../../common/apis/insight";
import { JSONRPC, MULTICHAIN_URLS } from "../../common/apis/jsonrpc";
import { BitgoUTXOLib } from "../../common/libraries/bitgoUtxoLib";
import { subscribeToConfirmations } from "../../lib/confirmations";
import { newPromiEvent, PromiEvent } from "../../lib/promiEvent";
import { fallback, retryNTimes } from "../../lib/retry";
import { UTXO } from "../../lib/utxo";
import { Asset, Handler } from "../../types/types";
import { JSONRPC, MULTICHAIN_URLS } from "../../common/apis/jsonrpc";

const testnetInsight = "https://api.bitcore.io/api/BCH/testnet/";
const mainnetInsight = "https://api.bitcore.io/api/BCH/mainnet/";

interface AddressOptions {}
interface BalanceOptions extends AddressOptions {
Expand All @@ -34,6 +38,17 @@ const toCashAddr = (legacyAddress: string) => {

export const _apiFallbacks = {
fetchUTXO: (testnet: boolean, txHash: string, vOut: number) => [
() =>
Blockchain.fetchUTXO(
testnet
? BlockchainNetwork.BitcoinCashTestnet
: BlockchainNetwork.BitcoinCash
)(txHash, vOut),
() =>
Insight.fetchUTXO(testnet ? testnetInsight : mainnetInsight)(
txHash,
vOut
),
() => BitcoinDotCom.fetchUTXO(testnet)(txHash, vOut),
testnet
? undefined
Expand All @@ -45,6 +60,17 @@ export const _apiFallbacks = {
],

fetchUTXOs: (testnet: boolean, address: string, confirmations: number) => [
() =>
Blockchain.fetchUTXOs(
testnet
? BlockchainNetwork.BitcoinCashTestnet
: BlockchainNetwork.BitcoinCash
)(address, confirmations),
() =>
Insight.fetchUTXOs(testnet ? testnetInsight : mainnetInsight)(
address,
confirmations
),
() => BitcoinDotCom.fetchUTXOs(testnet)(address, confirmations),
testnet
? undefined
Expand All @@ -60,7 +86,18 @@ export const _apiFallbacks = {
address: string,
confirmations: number = 0
) => [
() => BitcoinDotCom.fetchTXs(testnet)(address, confirmations),
() =>
Blockchain.fetchTXs(
testnet
? BlockchainNetwork.BitcoinCashTestnet
: BlockchainNetwork.BitcoinCash
)(address, confirmations),
() =>
Insight.fetchTXs(testnet ? testnetInsight : mainnetInsight)(
address,
confirmations
),
// () => BitcoinDotCom.fetchTXs(testnet)(address, confirmations),
testnet
? undefined
: () =>
Expand All @@ -75,6 +112,10 @@ export const _apiFallbacks = {
JSONRPC.broadcastTransaction(
testnet ? MULTICHAIN_URLS.BCHTEST : MULTICHAIN_URLS.BCH
)(hex),
() =>
Insight.broadcastTransaction(
testnet ? testnetInsight : mainnetInsight
)(hex),
() => BitcoinDotCom.broadcastTransaction(testnet)(hex),
testnet
? undefined
Expand Down
Loading

0 comments on commit ad95456

Please sign in to comment.