Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"bech32": "^2.0.0",
"bignumber.js": "^9.1.2",
"bitcoinjs-lib": "^6.1.6",
"coinselect": "^3.1.13",
"cosmjs-types": "^0.9.0",
"hi-base32": "^0.5.1",
"js-sha512": "^0.8.0",
Expand All @@ -56,5 +57,10 @@
"ts-node": "^10.4.0",
"tsconfig-paths": "^4.2.0",
"typescript": "^5"
},
"pnpm": {
"patchedDependencies": {
"coinselect@3.1.13": "patches/coinselect@3.1.13.patch"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have some context about the need for this patch? A link or anything.
Not mandatory just in case, could be useful for future reference.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put those links as context in the description of the PR:

with basic UTXO selection using bitcoinjs coinselect.
⚠️ Known issues:
Because the lib is only published in NPM without its types (bitcoinjs/coinselect#77 (comment)), I've patched the lib using their own definitions.

But your comment makes me think that I should put that context closer to the codebase itself. A recommendation?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose to add the following documentation right next to the patch.

}
}
}
43 changes: 43 additions & 0 deletions patches/coinselect@3.1.13.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 0000000..20cc98a
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1,25 @@
+export interface UTXO {
+ txid: string | Buffer;
+ vout: number;
+ value: number;
+ nonWitnessUtxo?: Buffer;
+ witnessUtxo?: {
+ script: Buffer;
+ value: number;
+ };
+}
+export interface Target {
+ address?: string;
+ value: number;
+}
+export interface SelectedUTXO {
+ inputs?: UTXO[];
+ outputs?: Target[];
+ fee: number;
+}
+
+export default function coinSelect(
+ utxos: UTXO[],
+ outputs: Target[],
+ feeRate: number
+): SelectedUTXO;
diff --git a/package.json b/package.json
index 6cdef00..337bf4f 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"utils.js"
],
"main": "index.js",
+ "types": "index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/bitcoinjs/coinselect.git"
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 2 additions & 11 deletions src/app/api/schemaCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const positiveBigintSchema = z
.or(toNumber)
.transform((data) => BigInt(data))
.openapi({
type: "string"
type: "string",
});

const recipientSchema = z.object({
Expand Down Expand Up @@ -204,7 +204,7 @@ const transactionDataSchema = z
case ChainFeature.TRANSACTIONS_TO_MANY:
return [TransactionMode.TRANSFER_TO_MANY];
default:
return []; // filter out features that don't concern transac
return []; // filter out features that don't concern transactions
}
});
// idea of how we could have generic errors for features, for all chains
Expand All @@ -220,15 +220,6 @@ const transactionDataSchema = z
// async validation of addresses
.superRefine(async (data, ctx) => {
if (data.mode === TransactionMode.TRANSFER_TO_MANY) {
if (data.chainId != ChainId.BITCOIN && data.chainId != ChainId.BITCOIN_TESTNET) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `TransferToMany is not supported for ${data.chainId}`,
fatal: true,
});
return z.NEVER;
}

const validSender = await getService(data.chainId).validateAddress(data.sender);
if (!validSender) {
ctx.addIssue({
Expand Down
1 change: 0 additions & 1 deletion src/app/api/transaction/txSchemaCommon.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { z } from "zod";
import { transactionDataSchema } from "../schemaCommon";
import { SchemaObject } from "node_modules/zod-openapi/lib-types/openapi3-ts/dist/oas31";

const newTransactionSchema = z
.object({
Expand Down
77 changes: 74 additions & 3 deletions src/app/families/bitcoin/backend/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const queryParams = new URLSearchParams({
key: BLOCKCHAIR_API_KEY,
});

export async function fetchXpubBalances(chainId: ChainId, xpub: Xpub) {
export async function fetchXpubData(chainId: ChainId, xpub: Xpub) {
const explorerURL = CHAINS[chainId].backends.explorerURL;

const url = `/dashboards/xpub/${xpub}?${queryParams.toString()}`;
Expand Down Expand Up @@ -45,8 +45,8 @@ export async function fetchAddressesBalances(chainId: ChainId, addresses: string
return result.data;
}

export async function pushTransaction(chaindId: ChainId, signedTransaction: string): Promise<string> {
const explorerURL = CHAINS[chaindId].backends.explorerURL;
export async function pushTransaction(chainId: ChainId, signedTransaction: string): Promise<string> {
const explorerURL = CHAINS[chainId].backends.explorerURL;

queryParams.set("data", signedTransaction);
const url = `/push/transaction?${queryParams.toString()}`;
Expand All @@ -64,4 +64,75 @@ export async function pushTransaction(chaindId: ChainId, signedTransaction: stri
return responseBody.data.transaction_hash;
}

export async function fetchFeePerByte(chainId: ChainId): Promise<number> {
const explorerURL = CHAINS[chainId].backends.explorerURL;

const url = `/stats?${queryParams.toString()}`;

const response = await fetch(explorerURL + url);

// TODO: add zod validation
const responseBody = await response.json();

if (responseBody.context.code !== 200) {
throw new Error(`fetchNetworkInformations - received invalid response: ${responseBody.context.error}`);
}

return responseBody.data.suggested_transaction_fee_per_byte_sat;
}

export async function fetchTxMetadata(
chainId: ChainId,
txHash: string
): Promise<{
[key: string]: {
has_witness: boolean;
outputs: [
{
index: number;
script_hex: string;
// ... other fields are not used for now
}
];
// ... other fields are not used for now
};
}> {
const explorerURL = CHAINS[chainId].backends.explorerURL;
const url = `/dashboards/transaction/${txHash}?${queryParams.toString()}`;

const response = await fetch(explorerURL + url);

const responseBody = await response.json();

if (responseBody.context.code !== 200) {
throw new Error(`fetchRawTxMetadata - received invalid response: ${responseBody.context.error}`);
}

return responseBody.data;
}

export async function fetchRawTxMetadata(
chainId: ChainId,
txHash: string
): Promise<{
[key: string]: {
// key is tx_hash
raw_transaction: string;
/// ... other fields are not used for now
};
}> {
const explorerURL = CHAINS[chainId].backends.explorerURL;
const url = `/raw/transaction/${txHash}?${queryParams.toString()}`;

const response = await fetch(explorerURL + url);

const responseBody = await response.json();

if (responseBody.context.code !== 200) {
throw new Error(`fetchRawTxMetadata - received invalid response: ${responseBody.context.error}`);
}

return responseBody.data;
}

export function fetchAccountAddresses(pubKey: string) {}
82 changes: 52 additions & 30 deletions src/app/families/bitcoin/backend/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import { UTXO } from "coinselect";
import z from "zod";

// Blockchair responses SUB-MODELS
const UtxosSchema = z.object({
block_id: z.number(),
transaction_hash: z.string(),
index: z.number(),
value: z.number(),
address: z.string(),
});
const UtxosSchema = z
.object({
block_id: z.number(),
transaction_hash: z.string(),
index: z.number(),
value: z.number(),
address: z.string(),
})
.transform((data) => {
return {
/**
* add fields for compatibility with coinselect @see {@link UTXO}
*/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nitpicking)
Single-line comment when possible, for more compact code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wanted to have the @see {@link UTXO} picked up as JsDoc. And VS code only parses them when between /** .... */ 🙈

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realised it could be done in one line 🤦

txid: data.transaction_hash,
vout: data.index,
...data,
};
});

export type UtxoWithMetadata = z.infer<typeof UtxosSchema>;

const AddressSchema = z.object({
type: z.string(),
Expand Down Expand Up @@ -69,17 +83,21 @@ export const requestContextSchema = z.object({
rows: z.number().optional(),
market_price_usd: z.number().optional(),
total_rows: z.number().optional(),
api: z.object({
version: z.string(),
last_major_update: z.string().optional(),
next_major_update: z.string().optional(),
documentation: z.string().optional(),
notice: z.string().optional(),
}).optional(),
cache: z.object({
live: z.boolean(),
until: z.string().optional(),
}).optional(),
api: z
.object({
version: z.string(),
last_major_update: z.string().optional(),
next_major_update: z.string().optional(),
documentation: z.string().optional(),
notice: z.string().optional(),
})
.optional(),
cache: z
.object({
live: z.boolean(),
until: z.string().optional(),
})
.optional(),
request_cost: z.number(),
});

Expand Down Expand Up @@ -185,7 +203,8 @@ export const requestContextSchema = z.object({
}
*/
export const XpubDashboardData = z.record(
z.string(), z.object({
z.string(),
z.object({
xpub: z.object({
address_count: z.number(),
balance: z.number().pipe(z.coerce.bigint()),
Expand All @@ -203,9 +222,12 @@ export const XpubDashboardData = z.record(
transaction_count: z.number(),
}),
addresses: z.record(
z.string(), AddressSchema.and(z.object({
path: z.string(),
}))
z.string(),
AddressSchema.and(
z.object({
path: z.string(),
})
)
),
transactions: z.array(z.string()),
utxo: UtxosSchema.array(),
Expand All @@ -219,7 +241,7 @@ export const XpubDashboardResponse = z.object({
z.object({
checked: z.array(z.string()),
})
)
),
});

export const AddressesDashboardResponse = z.object({
Expand All @@ -230,13 +252,13 @@ export const AddressesDashboardResponse = z.object({
received_usd: true,
spent_usd: true,
}),
addresses: z.record(
z.string(), AddressSchema
),
addresses: z.record(z.string(), AddressSchema),
transactions: z.array(z.string()),
utxo: UtxosSchema.and(z.object({
address: z.string(),
})).array(),
utxo: UtxosSchema.and(
z.object({
address: z.string(),
})
).array(),
}),
context: requestContextSchema
context: requestContextSchema,
});
Loading