Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -27,6 +27,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 @@ -52,5 +53,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"
}
}
}
66 changes: 66 additions & 0 deletions patches/coinselect@3.1.13.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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,48 @@
+// Declare the main coinselect module
+declare module "coinselect" {
+ export interface UTXO {
+ txid: string | Buffer;
+ vout: number;
+ value: number;
+ nonWitnessUtxo?: Buffer;
+ witnessUtxo?: {
+ script: Buffer;
+ value: number;
+ };
+ }
+
+ export type Target =
+ | {
+ address: string;
+ value: number;
+ }
+ | {
+ address: string;
+ }
+ | {
+ value: number;
+ };
+
+ export interface SelectedUTXO {
+ inputs?: UTXO[];
+ outputs?: Target[];
+ fee: number;
+ }
+
+ export default function coinSelect(
+ utxos: UTXO[],
+ outputs: Target[],
+ feeRate: number
+ ): SelectedUTXO;
+}
+
+// Declare the coinselect/split module
+declare module "coinselect/split.js" {
+ import { UTXO, Target, SelectedUTXO } from "coinselect";
+
+ export default function split(
+ 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"
40 changes: 40 additions & 0 deletions patches/how_to.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Why this patch

The library `coinselect.js` is manipulating important and low level objects, it seems important to make sure we don't just send any information to it.
Because the lib is only published in NPM without its types ([because the maintainer doesnt trust NPM](https://github.com/bitcoinjs/coinselect/pull/77#issuecomment-1676430774)), I've patched the lib using their own type definition [their own definitions](https://github.com/bitcoinjs/coinselect/blob/master/index.d.ts), modified to actually reflect the lib's bahaviour.


# How to maintain the patch

Create a temporary copy of the library to patch
```bash
pnpm patch
```

You now have access to a similar folder as: `/private/var/folders/yn/k6gkp3pd7fq4dn71mw1yb8qh0000gn/T/5191b8b733d6bbfd169476c53b6a123b` that I'll refer to as `temp_lib`

Open it on vscode
```bash
code "/temp_lib"
```

Do all the changes that you need.
And then run
```
diff -Naur node_modules/.pnpm/coinselect@3.1.13/node_modules/coinselect/ /temp_lib
```

And copy the changes to `patches/coinselect@3.1.13.patch`, just taking the lines with `+/-` preceeded by the ones with `@@`.

You can now your patch with
```bash
pnpm install --force
```

If all is good, `patches/coinselect@3.1.13.patch` can be commited as any other file.

# Why use `diff` manually and not use `pmpm patch-commit temp_lib` ?

For some reason, it seems pnpm 8 doesn't handle well newly created files (and not just modified ones). This should be better in [pnpm 9](https://github.com/pnpm/pnpm/issues/5686#issuecomment-2272406668)


13 changes: 13 additions & 0 deletions pnpm-lock.yaml

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

39 changes: 3 additions & 36 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 @@ -211,7 +211,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 @@ -223,40 +223,7 @@ const transactionDataSchema = z
options: supportedTransactionModes,
};
}
)
// 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({
code: z.ZodIssueCode.custom,
message: `Sender address ${data.sender} is invalid for ${data.chainId}`,
path: ["sender"],
});
}
for (const recipient of data.recipients) {
const validRecipient = await getService(data.chainId).validateAddress(recipient.address);
if (!validRecipient) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Recipient address ${recipient.address} is invalid for ${data.chainId}`,
path: ["recipients", "address"],
});
}
}
}
// TODO: add all of prevalidateTransactionCommon here?
});
);

const errorMsgSchema = z.object({
message: z.string(),
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
9 changes: 5 additions & 4 deletions src/app/config/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const FEATURES: { [key in ChainFamily]: ChainFeature[] } = {
[ChainFamily.BITCOIN]: [
ChainFeature.BALANCES_NATIVE,
ChainFeature.TRANSACTIONS_TO_MANY,
ChainFeature.TRANSACTIONS_NATIVE,
],
};

Expand Down Expand Up @@ -421,10 +422,10 @@ const CHAINS: { [key in ChainId]: Chain } = {
nodeURL: "",
explorerURL: "https://api.blockchair.com/bitcoin",
},
params: {
confirmations: 6,
},
nativeId: "bitcoin",
params: {
confirmations: 6,
},
nativeId: "bitcoin",
supportedFeatures: FEATURES[ChainFamily.BITCOIN],
},
[ChainId.BITCOIN_TESTNET]: {
Expand Down
75 changes: 73 additions & 2 deletions src/app/families/bitcoin/backend/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { ChainId } from "~/app/model/types_WIP";
import { Xpub } from "../types";
import {
BlockchairAddressesResponse,
TransactionBroadcastStatusSchema,
BlockchairXpubResponse,
TransactionBroadcastStatusSchema,
} from "./types";

const queryParams = new URLSearchParams({
Expand Down Expand Up @@ -50,7 +50,7 @@ export async function fetchAddressesDashboard(chainId: ChainId, addresses: strin
return result.data;
}

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

queryParams.set("data", signedTransaction);
Expand All @@ -69,3 +69,74 @@ export async function postTransaction(chaindId: ChainId, signedTransaction: stri

return result.data.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;
}
Loading