Skip to content

Commit

Permalink
Merge pull request #4872 from leather-wallet/release/ledger-fixes
Browse files Browse the repository at this point in the history
Release/ledger fixes
  • Loading branch information
fbwoolf authored Jan 26, 2024
2 parents ff1e138 + 07729b5 commit 2c369e7
Show file tree
Hide file tree
Showing 81 changed files with 1,476 additions and 808 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module.exports = {
// methods, such as implicit use of signed transactions
'deprecation/deprecation': 'warn',
'no-console': ['error'],
'no-duplicate-imports': ['error'],
'prefer-const': [
'error',
{
Expand Down
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,14 +251,14 @@
"@sentry/webpack-plugin": "2.10.2",
"@stacks/connect-react": "22.2.0",
"@stacks/stacks-blockchain-api-types": "6.3.4",
"@storybook/addon-essentials": "7.6.7",
"@storybook/addon-interactions": "7.6.7",
"@storybook/addon-links": "7.6.7",
"@storybook/addon-essentials": "7.6.10",
"@storybook/addon-interactions": "7.6.10",
"@storybook/addon-links": "7.6.10",
"@storybook/addon-onboarding": "1.0.10",
"@storybook/blocks": "7.6.7",
"@storybook/react": "7.6.7",
"@storybook/react-webpack5": "7.6.7",
"@storybook/test": "7.6.7",
"@storybook/blocks": "7.6.10",
"@storybook/react": "7.6.10",
"@storybook/react-webpack5": "7.6.10",
"@storybook/test": "7.6.10",
"@types/argon2-browser": "1.18.2",
"@types/chroma-js": "2.4.1",
"@types/chrome": "0.0.246",
Expand Down Expand Up @@ -319,7 +319,7 @@
"react-refresh": "0.14.0",
"schema-inspector": "2.0.2",
"speed-measure-webpack-plugin": "1.5.0",
"storybook": "7.6.7",
"storybook": "7.6.10",
"stream-browserify": "3.0.0",
"svg-url-loader": "8.0.0",
"ts-node": "10.9.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { fetchWalletConfig, generateWallet } from '@stacks/wallet-sdk';
import { connectToGaiaHubWithConfig, getHubInfo } from '@stacks/wallet-sdk';
import {
connectToGaiaHubWithConfig,
fetchWalletConfig,
generateWallet,
getHubInfo,
} from '@stacks/wallet-sdk';

import { gaiaUrl as gaiaHubUrl } from '@shared/constants';

Expand Down
3 changes: 1 addition & 2 deletions src/app/common/hooks/use-bitcoin-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import {
import { Money, createMoneyFromDecimal } from '@shared/models/money.model';
import { RouteUrls } from '@shared/route-urls';
import { BitcoinContractResponseStatus } from '@shared/rpc/methods/accept-bitcoin-contract';
import { makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
import { makeRpcErrorResponse, makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';

import { sendAcceptedBitcoinContractOfferToProtocolWallet } from '@app/query/bitcoin/contract/send-accepted-bitcoin-contract-offer';
import {
Expand Down
11 changes: 11 additions & 0 deletions src/app/common/hooks/use-brc20-tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useGetBrc20TokensQuery } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.query';

export function useBrc20Tokens() {
const { data: allBrc20TokensResponse } = useGetBrc20TokensQuery();
const brc20Tokens = allBrc20TokensResponse?.pages
.flatMap(page => page.brc20Tokens)
.filter(token => token.length > 0)
.flatMap(token => token);

return brc20Tokens;
}
14 changes: 7 additions & 7 deletions src/app/common/hooks/use-event-listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,32 @@ const isBrowser = checkIsBrowser();
*
* @param event the event name
* @param handler the event handler function to execute
* @param doc the dom environment to execute against (defaults to `document`)
* @param element the dom environment to execute against (defaults to `document`)
* @param options the event listener options
*/
export function useEventListener(
event: keyof WindowEventMap,
handler: (event: any) => void,
doc: Document | null = isBrowser ? document : null,
element: Document | null = isBrowser ? document : null,
options?: AddEventListener[2]
) {
const savedHandler = useLatestRef(handler);

useEffect(() => {
if (!doc) return;
if (!element) return;

const listener = (event: any) => {
savedHandler.current(event);
};

doc.addEventListener(event, listener, options);
element.addEventListener(event, listener, options);

return () => {
doc.removeEventListener(event, listener, options);
element.removeEventListener(event, listener, options);
};
}, [event, doc, options, savedHandler]);
}, [event, element, options, savedHandler]);

return () => {
doc?.removeEventListener(event, savedHandler.current, options);
element?.removeEventListener(event, savedHandler.current, options);
};
}
2 changes: 1 addition & 1 deletion src/app/common/theme-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function ThemeSwitcherProvider({ children }: ThemeSwitcherProviderProps)
return (
<ThemeContext.Provider value={{ theme, userSelectedTheme, setUserSelectedTheme }}>
<RadixTheme appearance={theme}>
<RadixTooltip.Provider>{children}</RadixTooltip.Provider>
<RadixTooltip.Provider delayDuration={300}>{children}</RadixTooltip.Provider>
</RadixTheme>
</ThemeContext.Provider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe(determineUtxosForSpend.name, () => {
describe('sorting algorithm (biggest first and no dust)', () => {
test('that it filters out dust utxos', () => {
const result = generate10kSpendWithTestData('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m');
const hasDust = result.orderedUtxos.some(utxo => utxo.value <= BTC_P2WPKH_DUST_AMOUNT);
const hasDust = result.filteredUtxos.some(utxo => utxo.value <= BTC_P2WPKH_DUST_AMOUNT);
expect(hasDust).toBeFalsy();
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { getAddressInfo, validate } from 'bitcoin-address-validation';

import { BTC_P2WPKH_DUST_AMOUNT } from '@shared/constants';
import { validate } from 'bitcoin-address-validation';

import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client';

import { BtcSizeFeeEstimator } from '../fees/btc-size-fee-estimator';
import { filterUneconomicalUtxos, getSizeInfo } from '../utils';

export interface DetermineUtxosForSpendArgs {
amount: number;
Expand All @@ -20,17 +18,12 @@ export function determineUtxosForSpendAll({
utxos,
}: DetermineUtxosForSpendArgs) {
if (!validate(recipient)) throw new Error('Cannot calculate spend of invalid address type');
const filteredUtxos = filterUneconomicalUtxos({ utxos, feeRate, address: recipient });

const addressInfo = getAddressInfo(recipient);

const txSizer = new BtcSizeFeeEstimator();

const filteredUtxos = utxos.filter(utxo => utxo.value >= BTC_P2WPKH_DUST_AMOUNT);

const sizeInfo = txSizer.calcTxSize({
input_script: 'p2wpkh',
input_count: filteredUtxos.length,
[addressInfo.type + '_output_count']: 1,
const sizeInfo = getSizeInfo({
inputLength: filteredUtxos.length,
outputLength: 1,
recipient,
});

// Fee has already been deducted from the amount with send all
Expand All @@ -54,25 +47,23 @@ export function determineUtxosForSpend({
}: DetermineUtxosForSpendArgs) {
if (!validate(recipient)) throw new Error('Cannot calculate spend of invalid address type');

const addressInfo = getAddressInfo(recipient);

const orderedUtxos = utxos
.filter(utxo => utxo.value >= BTC_P2WPKH_DUST_AMOUNT)
.sort((a, b) => b.value - a.value);
const orderedUtxos = utxos.sort((a, b) => b.value - a.value);

const txSizer = new BtcSizeFeeEstimator();
const filteredUtxos = filterUneconomicalUtxos({
utxos: orderedUtxos,
feeRate,
address: recipient,
});

const neededUtxos = [];
let sum = 0n;
let sizeInfo = null;

for (const utxo of orderedUtxos) {
sizeInfo = txSizer.calcTxSize({
// Only p2wpkh is supported by the wallet
input_script: 'p2wpkh',
input_count: neededUtxos.length,
// From the address of the recipient, we infer the output type
[addressInfo.type + '_output_count']: 2,
for (const utxo of filteredUtxos) {
sizeInfo = getSizeInfo({
inputLength: neededUtxos.length,
outputLength: 2,
recipient,
});
if (sum >= BigInt(amount) + BigInt(Math.ceil(sizeInfo.txVBytes * feeRate))) break;

Expand All @@ -92,7 +83,7 @@ export function determineUtxosForSpend({
];

return {
orderedUtxos,
filteredUtxos,
inputs: neededUtxos,
outputs,
size: sizeInfo.txVBytes,
Expand Down
143 changes: 143 additions & 0 deletions src/app/common/transactions/bitcoin/fees/bitcoin-fees.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import BigNumber from 'bignumber.js';
import { sha256 } from 'bitcoinjs-lib/src/crypto';

import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client';

import { filterUneconomicalUtxos } from '../utils';
import { calculateMaxBitcoinSpend } from './calculate-max-bitcoin-spend';

function generateTxId(value: number): UtxoResponseItem {
const buffer = Buffer.from(Math.random().toString());
return {
txid: sha256(sha256(buffer)).toString(),
vout: 0,
status: {
confirmed: true,
block_height: 2568495,
block_hash: '000000000000008622fafce4a5388861b252d534f819d0f7cb5d4f2c5f9c1638',
block_time: 1703787327,
},
value,
};
}

function generateTransactions(values: number[]) {
return values.map(val => generateTxId(val));
}

function generateAverageFee(value: number) {
return {
hourFee: BigNumber(value / 2),
halfHourFee: BigNumber(value),
fastestFee: BigNumber(value * 2),
};
}

describe(calculateMaxBitcoinSpend.name, () => {
const utxos = generateTransactions([600, 600, 1200, 1200, 10000, 10000, 25000, 40000, 50000000]);

test('with 1 sat/vb fee', () => {
const fee = 1;
const maxBitcoinSpend = calculateMaxBitcoinSpend({
address: '',
utxos,
fetchedFeeRates: generateAverageFee(fee),
});
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50087948);
});

test('with 5 sat/vb fee', () => {
const fee = 5;
const maxBitcoinSpend = calculateMaxBitcoinSpend({
address: '',
utxos,
fetchedFeeRates: generateAverageFee(fee),
});
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50085342);
});

test('with 30 sat/vb fee', () => {
const fee = 30;
const maxBitcoinSpend = calculateMaxBitcoinSpend({
address: '',
utxos,
fetchedFeeRates: generateAverageFee(fee),
});
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50073585);
});

test('with 100 sat/vb fee', () => {
const fee = 100;
const maxBitcoinSpend = calculateMaxBitcoinSpend({
address: '',
utxos,
fetchedFeeRates: generateAverageFee(fee),
});
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50046950);
});

test('with 400 sat/vb fee', () => {
const fee = 400;
const maxBitcoinSpend = calculateMaxBitcoinSpend({
address: '',
utxos,
fetchedFeeRates: generateAverageFee(fee),
});
expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(49969100);
});
});

describe(filterUneconomicalUtxos.name, () => {
const utxos = generateTransactions([600, 600, 1200, 1200, 10000, 10000, 25000, 40000, 50000000]);

test('with 1 sat/vb fee', () => {
const fee = 1;
const filteredUtxos = filterUneconomicalUtxos({
address: '',
utxos,
feeRate: fee,
});

expect(filteredUtxos.length).toEqual(9);
});

test('with 10 sat/vb fee', () => {
const fee = 10;
const filteredUtxos = filterUneconomicalUtxos({
address: '',
utxos,
feeRate: fee,
});
expect(filteredUtxos.length).toEqual(7);
});

test('with 30 sat/vb fee', () => {
const fee = 30;
const filteredUtxos = filterUneconomicalUtxos({
address: '',
utxos,
feeRate: fee,
});
expect(filteredUtxos.length).toEqual(5);
});

test('with 200 sat/vb fee', () => {
const fee = 200;
const filteredUtxos = filterUneconomicalUtxos({
address: '',
utxos,
feeRate: fee,
});
expect(filteredUtxos.length).toEqual(3);
});

test('with 400 sat/vb fee', () => {
const fee = 400;
const filteredUtxos = filterUneconomicalUtxos({
address: '',
utxos,
feeRate: fee,
});
expect(filteredUtxos.length).toEqual(2);
});
});
Loading

0 comments on commit 2c369e7

Please sign in to comment.