Skip to content

Commit

Permalink
feat: add fetchUTXO
Browse files Browse the repository at this point in the history
  • Loading branch information
0x31 committed Jul 13, 2020
1 parent 0435336 commit e543cd2
Show file tree
Hide file tree
Showing 16 changed files with 335 additions and 84 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"fix:tslint": "tslint --fix --project .",
"test": "run-s build test:*",
"test:lint": "tslint --project .",
"test:unit": "nyc --silent ava -T 100s --verbose",
"test:unit": "(. ./.env); nyc --silent ava -T 100s --verbose",
"lint": "yarn test:lint",
"lint:strict": "yarn lint && prettier \"src/**/*.ts\" --list-different --tab-width 4",
"watch": "yarn clean && yarn build:main -- -w",
Expand Down
82 changes: 75 additions & 7 deletions src/common/apis/bitcoinDotCom.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,81 @@
import axios from "axios";

import { fixValues, sortUTXOs, UTXO } from "../../lib/utxo";
import { fixUTXO, fixUTXOs, fixValue, sortUTXOs, UTXO } from "../../lib/utxo";
import { DEFAULT_TIMEOUT } from "./timeout";

export interface ScriptSig {
hex: string;
asm: string;
}

export interface Vin {
txid: string; // "4f72fce028ff1e99459393232e8bf8a430815ec5cea8700676c101b02be3649c",
vout: number; // 1,
sequence: number; // 4294967295,
n: number; // 0,
scriptSig: ScriptSig;
value: number; // 7223963,
legacyAddress: string; // "1D4NXvNvjucShZeyLsDzYz1ky2W8gYKQH7",
cashAddress: string; // "bitcoincash:qzzyfwmnz3dlld7svwzn53xzr6ycz5kwavpd9uqf4l"
}

export interface ScriptPubKey {
hex: string; // "76a91427bad06158841621ed33eb91efdb1cc4af4996cb88ac",
asm: string; // "OP_DUP OP_HASH160 27bad06158841621ed33eb91efdb1cc4af4996cb OP_EQUALVERIFY OP_CHECKSIG",
addresses: string[]; // ["14d59YbC2D9W9y5kozabCDhxkY3eDQq7B3"],
type: string; // "pubkeyhash",
cashAddrs: string[]; // ["bitcoincash:qqnm45rptzzpvg0dx04erm7mrnz27jvkevaf3ys3c5"]
}

export interface Vout {
value: string; // "0.04159505",
n: number; // 0,
scriptPubKey: ScriptPubKey;
spentTxId: string; // "9fa7bc86ad4729cbd5c182a8cd5cfc5eb457608fe430ce673f33ca52bfb1a187",
spentIndex: number; // 1,
spentHeight: number; // 617875
}

export interface QueryTransaction {
vin: Vin[];
vout: Vout[];
txid: string; // "03e29b07bb98b1e964296289dadb2fb034cb52e178cc306d20cc9ddc951d2a31",
version: number; // 1,
locktime: number; // 0,
blockhash: string; // "0000000000000000011c71094699e3ba47c43da76d775cf5eb5fbea1787fafb5",
blockheight: number; // 616200,
confirmations: number; // 27433,
time: number; // 1578054427,
blocktime: number; // 1578054427,
firstSeenTime: number; // 1578054360,
valueOut: number; // 0.07213963,
size: number; // 226,
valueIn: number; // 0.07223963,
fees: number; // 0.0001
}

const endpoint = (testnet: boolean) => testnet ? "https://trest.bitcoin.com/v2/" : "https://rest.bitcoin.com/v2/";

const fetchConfirmations = (testnet: boolean) => async (txid: string): Promise<number> => {
const url = `${endpoint(testnet).replace(/\/$/, "")}/transaction/details/${txid}`;
const fetchUTXO = (testnet: boolean) => async (txHash: string, vOut: number): Promise<UTXO> => {
const url = `${endpoint(testnet).replace(/\/$/, "")}/transaction/details/${txHash}`;

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

const utxo = response.data;

return fixUTXO({
txHash,
amount: parseFloat(utxo.vout[vOut].value),
// script_hex: utxo.scriptPubKey,
vOut,
confirmations: utxo.confirmations,
}, 8);
};

const fetchConfirmations = (testnet: boolean) => async (txHash: string): Promise<number> => {
const url = `${endpoint(testnet).replace(/\/$/, "")}/transaction/details/${txHash}`;

const tx = (await axios.get<{
txid: string, // 'cacec549d9f1f67e9835889a2ce3fc0d593bd78d63f63f45e4c28a59e004667d',
version: number, // 4,
locktime: number, // 0,
Expand All @@ -22,9 +89,9 @@ const fetchConfirmations = (testnet: boolean) => async (txid: string): Promise<n
size: number, // 211,
valueIn: number, // 225.45789926,
fees: number, // 0.0001,
}>(`${url}`, { timeout: DEFAULT_TIMEOUT });
}>(`${url}`, { timeout: DEFAULT_TIMEOUT })).data;

return response.data.confirmations;
return tx.confirmations;
};

const fetchUTXOs = (testnet: boolean) => async (address: string, confirmations: number): Promise<readonly UTXO[]> => {
Expand All @@ -41,7 +108,7 @@ const fetchUTXOs = (testnet: boolean) => async (address: string, confirmations:
ts: number,
}>
}>(url, { timeout: DEFAULT_TIMEOUT });
return fixValues(response.data.utxos.map(utxo => ({
return fixUTXOs(response.data.utxos.map(utxo => ({
txHash: utxo.txid,
amount: utxo.amount,
// script_hex: utxo.scriptPubKey,
Expand All @@ -66,6 +133,7 @@ export const broadcastTransaction = (testnet: boolean) => async (txHex: string):
};

export const BitcoinDotCom = {
fetchUTXO,
fetchUTXOs,
fetchConfirmations,
broadcastTransaction,
Expand Down
68 changes: 58 additions & 10 deletions src/common/apis/blockchair.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,76 @@
import axios from "axios";
import BigNumber from "bignumber.js";

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

const fetchConfirmations = (network: string) => async (txid: string): Promise<number> => {
const url = `https://api.blockchair.com/${network}/dashboards/transaction/${txid}`;
const fetchUTXO = (network: string) => async (txHash: string, vOut: number): Promise<UTXO> => {
const url = `https://api.blockchair.com/${network}/dashboards/transaction/${txHash}`;

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

const txBlock = response.data[txid].transaction.block_id;
if (!response.data[txHash]) {
throw new Error(`Transaction not found.`);
}

const tx = response.data[txHash];
const txBlock = tx.transaction.block_id;

let latestBlock = response.context.state;
if (latestBlock === 0) {
const statsUrl = `https://api.blockchair.com/${network}/stats`;
const statsResponse = (await axios.get(statsUrl)).data;
latestBlock = statsResponse.data.blocks - 1;
}

const confirmations = txBlock === -1 ? 0 : Math.max(latestBlock - txBlock + 1, 0);

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

const fetchConfirmations = (network: string) => async (txHash: string): Promise<number> => {
const url = `https://api.blockchair.com/${network}/dashboards/transaction/${txHash}`;

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

if (!response.data[txHash]) {
throw new Error(`Transaction not found.`);
}

return txBlock === -1 ? 0 : response.context.state - txBlock + 1;
const txBlock = response.data[txHash].transaction.block_id;

let latestBlock = response.context.state;
if (latestBlock === 0) {
const statsUrl = `https://api.blockchair.com/${network}/stats`;
const statsResponse = (await axios.get(statsUrl)).data;
latestBlock = statsResponse.data.blocks - 1;
}

return txBlock === -1 ? 0 : Math.max(latestBlock - txBlock + 1, 0);
};

const fetchUTXOs = (network: string) => async (address: string, confirmations: number): Promise<readonly UTXO[]> => {
const url = `https://api.blockchair.com/${network}/dashboards/address/${address}?limit=0,100`;
const response = (await axios.get<AddressResponse>(url, { timeout: DEFAULT_TIMEOUT })).data;
return fixValues(response.data[address].utxo.map(utxo => ({

let latestBlock = response.context.state;
if (latestBlock === 0) {
const statsUrl = `https://api.blockchair.com/${network}/stats`;
const statsResponse = (await axios.get(statsUrl)).data;
latestBlock = statsResponse.data.blocks - 1;
}

return response.data[address].utxo.map(utxo => ({
txHash: utxo.transaction_hash,
amount: new BigNumber(utxo.value).div(100000000).toNumber(),
amount: utxo.value,
vOut: utxo.index,
confirmations: utxo.block_id === -1 ? 0 : response.context.state - utxo.block_id + 1,
confirmations: utxo.block_id === -1 ? 0 : latestBlock - utxo.block_id + 1,
}))
.filter(utxo => confirmations === 0 || utxo.confirmations >= confirmations), 8)
.filter(utxo => confirmations === 0 || utxo.confirmations >= confirmations)
.sort(sortUTXOs);
};

Expand Down Expand Up @@ -53,6 +100,7 @@ enum Networks {

export const Blockchair = {
networks: Networks,
fetchUTXO,
fetchUTXOs,
fetchConfirmations,
broadcastTransaction,
Expand Down
44 changes: 25 additions & 19 deletions src/common/apis/blockstream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ interface BlockstreamUTXO<vout = number> {
};
txid: string;
value: number;
vout: vout;
vout: vout; // vout is a number for utxos, or an array of utxos for a tx
};

interface BlockstreamDetailsUTXO extends BlockstreamUTXO<Array<{
interface BlockstreamTX extends BlockstreamUTXO<Array<{
scriptpubkey: string,
scriptpubkey_asm: string,
scriptpubkey_type: string,
scriptpubkey_address: string,
value: number,
value: number, // e.g. 1034439
}>> {
locktime: number;
vin: Array<{
Expand All @@ -40,32 +40,37 @@ interface BlockstreamDetailsUTXO extends BlockstreamUTXO<Array<{
fee: number,
}

const fetchUTXO = (testnet: boolean) => async (txHash: string, vOut: number): Promise<UTXO> => {
const apiUrl = `https://blockstream.info/${testnet ? "testnet/" : ""}api`;

const utxo = (await axios.get<BlockstreamTX>(`${apiUrl}/tx/${txHash}`, { timeout: DEFAULT_TIMEOUT })).data;

const heightResponse = (await axios.get<string>(`${apiUrl}/blocks/tip/height`, { timeout: DEFAULT_TIMEOUT })).data;

const fetchConfirmations = (testnet: boolean) => async (txid: string): Promise<number> => {
const confirmations = utxo.status.confirmed ? Math.max(1 + parseInt(heightResponse, 10) - utxo.status.block_height, 0) : 0;

return {
txHash,
amount: utxo.vout[vOut].value,
vOut,
confirmations,
};
};

const fetchConfirmations = (testnet: boolean) => async (txHash: string): Promise<number> => {
const apiUrl = `https://blockstream.info/${testnet ? "testnet/" : ""}api`;

const response = await axios.get<BlockstreamDetailsUTXO>(`${apiUrl}/tx/${txid}`, { timeout: DEFAULT_TIMEOUT });
const utxo = (await axios.get<BlockstreamTX>(`${apiUrl}/tx/${txHash}`, { timeout: DEFAULT_TIMEOUT })).data;

const heightResponse = () => axios.get<string>(`${apiUrl}/blocks/tip/height`, { timeout: DEFAULT_TIMEOUT });

const utxo = response.data;
return utxo.status.confirmed ? 1 + parseInt((await heightResponse()).data, 10) - utxo.status.block_height : 0;
return utxo.status.confirmed ? Math.max(1 + parseInt((await heightResponse()).data, 10) - utxo.status.block_height, 0) : 0;
};

const fetchUTXOs = (testnet: boolean) => async (address: string, confirmations: number): Promise<readonly UTXO[]> => {
const apiUrl = `https://blockstream.info/${testnet ? "testnet/" : ""}api`;

const response = await axios.get<ReadonlyArray<{
status: {
confirmed: boolean,
block_height: number,
block_hash: string,
block_time: number,
};
txid: string,
value: number,
vout: number,
}>>(`${apiUrl}/address/${address}/utxo`, { timeout: DEFAULT_TIMEOUT });
const response = await axios.get<ReadonlyArray<BlockstreamUTXO>>(`${apiUrl}/address/${address}/utxo`, { timeout: DEFAULT_TIMEOUT });

const heightResponse = await axios.get<string>(`${apiUrl}/blocks/tip/height`, { timeout: DEFAULT_TIMEOUT });

Expand All @@ -86,7 +91,8 @@ const broadcastTransaction = (testnet: boolean) => async (txHex: string): Promis
};

export const Blockstream = {
fetchConfirmations,
fetchUTXO,
fetchUTXOs,
fetchConfirmations,
broadcastTransaction,
}
Loading

0 comments on commit e543cd2

Please sign in to comment.