Merge pull request #3626 from hirosystems/release/monday-release

Release/monday release
This commit is contained in:
kyranjamie
2023-05-01 17:24:46 +02:00
committed by GitHub
30 changed files with 257 additions and 88 deletions

49
.github/workflows/ordinals-checker.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Ordinals API test
on:
pull_request:
schedule:
# https://crontab.guru
- cron: '0 * * * *'
env:
CI: true
jobs:
test-ordinals:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
- name: Install dependencies
run: yarn
- name: Get installed Playwright version
id: playwright-version
run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'])")" >> $GITHUB_ENV
- name: Cache playwright binaries
uses: actions/cache@v3
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
- name: Install Playwright deps
run: yarn playwright install chrome
if: steps.playwright-cache.outputs.cache-hit != 'true'
- name: Run Playwright tests
run: yarn playwright test tests/specs/ordinals/ordinals.spec.ts
- name: Discord notification
if: failure()
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_ALERT_CHANNEL }}
uses: Ilshidur/action-discord@master
with:
args: "Something funky's up with the OrdAPI.xyz. Wallet team engineer to investigate. See `ordinals-checker.yml Github Action."

View File

@@ -1,5 +1,5 @@
import { fibonacciGenerator } from '../math/fibonacci';
import { createCounter } from '../utils/counter';
import { fibonacciGenerator } from '../utils/fibonacci';
const numOfEmptyAccountsToCheck = 20;

View File

@@ -0,0 +1,26 @@
import BigNumber from 'bignumber.js';
import { sumNumbers } from './helpers';
const cases = [
{
sums: [1, 2, 3],
expectedResult: new BigNumber(6),
},
{
sums: [0.1, 0.2],
expectedResult: new BigNumber(0.3),
},
{
sums: [Number.MAX_SAFE_INTEGER, 1],
expectedResult: new BigNumber('9007199254740992'),
},
];
describe(sumNumbers.name, () => {
describe.each(cases)('Sum example', ({ sums, expectedResult }) => {
test('sum of ' + sums.toString(), () => {
expect(sumNumbers(sums).toString()).toEqual(expectedResult.toString());
});
});
});

View File

@@ -0,0 +1,25 @@
import BigNumber from 'bignumber.js';
export function initBigNumber(num: string | number | BigNumber) {
return BigNumber.isBigNumber(num) ? num : new BigNumber(num);
}
export function sumNumbers(nums: number[]) {
return nums.reduce((acc, num) => acc.plus(num), new BigNumber(0));
}
function isMultipleOf(multiple: number) {
return (num: number) => num % multiple === 0;
}
export function isEven(num: number) {
return isMultipleOf(2)(num);
}
export function countDecimals(num: string | number | BigNumber) {
const LARGE_NUMBER_OF_DECIMALS = 100;
BigNumber.config({ DECIMAL_PLACES: LARGE_NUMBER_OF_DECIMALS });
const amount = initBigNumber(num);
const decimals = amount.toString(10).split('.')[1];
return decimals ? decimals.length : 0;
}

View File

@@ -3,7 +3,7 @@ import BigNumber from 'bignumber.js';
import { BTC_DECIMALS, STX_DECIMALS } from '@shared/constants';
import { Money } from '@shared/models/money.model';
import { initBigNumber } from '../utils';
import { initBigNumber } from '../math/helpers';
function fractionalUnitToUnit(decimals: number) {
return (unit: number | string | BigNumber) => {

View File

@@ -4,8 +4,9 @@ import { c32addressDecode } from 'c32check';
import { NetworkConfiguration, STX_DECIMALS } from '@shared/constants';
import { abbreviateNumber, initBigNumber } from '@app/common/utils';
import { abbreviateNumber } from '@app/common/utils';
import { initBigNumber } from './math/helpers';
import { microStxToStx } from './money/unit-conversion';
export const stacksValue = ({

View File

@@ -1,4 +1,5 @@
import { createNullArrayOfLength, sumNumbers } from '@app/common/utils';
import { sumNumbers } from '@app/common/math/helpers';
import { createNullArrayOfLength } from '@app/common/utils';
import { determineUtxosForSpend } from './local-coin-selection';

View File

@@ -1,9 +1,7 @@
import BigNumber from 'bignumber.js';
import { getTicker, sumNumbers } from '@app/common/utils';
import { getTicker } from '@app/common/utils';
import { extractPhraseFromString } from '@app/common/utils';
import { countDecimals } from './utils';
import { countDecimals } from './math/helpers';
describe(countDecimals.name, () => {
test('that it returns 0 when given an integer', () => expect(countDecimals(100)).toEqual(0));
@@ -107,26 +105,3 @@ describe(extractPhraseFromString.name, () => {
expect(key).toEqual(SECRET_KEY_COPIED_FROM_V3_FORMATTED);
});
});
const cases = [
{
sums: [1, 2, 3],
expectedResult: new BigNumber(6),
},
{
sums: [0.1, 0.2],
expectedResult: new BigNumber(0.3),
},
{
sums: [Number.MAX_SAFE_INTEGER, 1],
expectedResult: new BigNumber('9007199254740992'),
},
];
describe(sumNumbers.name, () => {
describe.each(cases)('Sum example', ({ sums, expectedResult }) => {
test('sum of ' + sums.toString(), () => {
expect(sumNumbers(sums).toString()).toEqual(expectedResult.toString());
});
});
});

View File

@@ -7,7 +7,6 @@ import {
PostCondition,
deserializePostCondition,
} from '@stacks/transactions';
import BigNumber from 'bignumber.js';
import { toUnicode } from 'punycode';
import { BitcoinNetworkModes, KEBAB_REGEX, NetworkModes } from '@shared/constants';
@@ -88,14 +87,6 @@ export function truncateString(str: string, maxLength: number) {
return str.slice(0, maxLength) + '…';
}
function isMultipleOf(multiple: number) {
return (num: number) => num % multiple === 0;
}
export function isEven(num: number) {
return isMultipleOf(2)(num);
}
function getLetters(string: string, offset = 1) {
return string.slice(0, offset);
}
@@ -251,18 +242,6 @@ export function with0x(value: string): string {
return !value.startsWith('0x') ? `0x${value}` : value;
}
export function initBigNumber(num: string | number | BigNumber) {
return BigNumber.isBigNumber(num) ? num : new BigNumber(num);
}
export function countDecimals(num: string | number | BigNumber) {
const LARGE_NUMBER_OF_DECIMALS = 100;
BigNumber.config({ DECIMAL_PLACES: LARGE_NUMBER_OF_DECIMALS });
const amount = initBigNumber(num);
const decimals = amount.toString(10).split('.')[1];
return decimals ? decimals.length : 0;
}
export function pullContractIdFromIdentity(identifier: string) {
return identifier.split('::')[0];
}
@@ -314,10 +293,6 @@ export function bitcoinNetworkModeToCoreNetworkMode(mode: BitcoinNetworkModes) {
return bitcoinNetworkToCoreNetworkMap[mode];
}
export function sumNumbers(nums: number[]) {
return nums.reduce((acc, num) => acc.plus(num), new BigNumber(0));
}
export function logAndThrow(msg: string, args: any[] = []) {
logger.error(msg, ...args);
throw new Error(msg);

View File

@@ -4,6 +4,7 @@ import * as yup from 'yup';
import { Money } from '@shared/models/money.model';
import { isNumber } from '@shared/utils';
import { countDecimals } from '@app/common/math/helpers';
import { subtractMoney } from '@app/common/money/format-money';
import {
btcToSat,
@@ -18,7 +19,6 @@ import {
formatPrecisionError,
} from '../../error-formatters';
import { FormErrorMessages } from '../../error-messages';
import { countDecimals } from '../../utils';
import { currencyAmountValidator, stxAmountPrecisionValidator } from './currency-validators';
const minSpendAmountInSats = 6000;

View File

@@ -5,7 +5,7 @@ import { isNumber } from '@shared/utils';
import { formatErrorWithSymbol } from '@app/common/error-formatters';
import { FormErrorMessages } from '@app/common/error-messages';
import { countDecimals } from '@app/common/utils';
import { countDecimals } from '@app/common/math/helpers';
export function currencyAmountValidator() {
return yup

View File

@@ -1,4 +1,4 @@
import { isEven } from '@app/common/utils';
import { isEven } from '@app/common/math/helpers';
import { AddressDisplayerLayout } from './address-displayer.layout';

View File

@@ -1,5 +1,6 @@
import { Stack, Text } from '@stacks/ui';
import { LoadingSpinner } from '../loading-spinner';
import { FeesCard } from './components/fees-card';
import { useBitcoinFeesList } from './use-bitcoin-fees-list';
@@ -9,8 +10,19 @@ interface BitcoinFeesListProps {
recipient: string;
}
export function BitcoinFeesList({ amount, onChooseFee, recipient }: BitcoinFeesListProps) {
const { feesList } = useBitcoinFeesList({ amount, recipient });
const { feesList, isLoading } = useBitcoinFeesList({ amount, recipient });
if (isLoading) return <LoadingSpinner />;
if (!feesList.length) {
return (
<Stack alignItems="center" spacing="extra-loose" width="100%">
<Text color="#74777D" fontSize="14px" textAlign="center">
Unable to fetch fees. Please try again later.
</Text>
</Stack>
);
}
return (
<Stack alignItems="center" spacing="extra-loose" width="100%">
<Stack spacing="base" width="100%">

View File

@@ -8,7 +8,7 @@ import { btcToSat } from '@app/common/money/unit-conversion';
import { determineUtxosForSpend } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection';
import { useGetUtxosByAddressQuery } from '@app/query/bitcoin/address/utxos-by-address.query';
import { BtcFeeType, btcTxTimeMap } from '@app/query/bitcoin/bitcoin-client';
import { useBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.hooks';
import { useAverageBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.hooks';
import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks';
import { useCurrentBtcNativeSegwitAccountAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
@@ -21,7 +21,7 @@ export function useBitcoinFeesList({ amount, recipient }: UseBitcoinFeesListArgs
const { data: utxos } = useGetUtxosByAddressQuery(currentAccountBtcAddress);
const btcMarketData = useCryptoCurrencyMarketData('BTC');
const { data: feeRate } = useBitcoinFeeRate();
const { avgApiFeeRates: feeRate, isLoading } = useAverageBitcoinFeeRate();
const feesList = useMemo(() => {
function getFiatFeeValue(fee: number) {
@@ -37,21 +37,21 @@ export function useBitcoinFeesList({ amount, recipient }: UseBitcoinFeesListArgs
utxos,
recipient,
amount: satAmount,
feeRate: feeRate.fastestFee,
feeRate: feeRate.fastestFee.toNumber(),
});
const { fee: standartFeeValue } = determineUtxosForSpend({
utxos,
recipient,
amount: satAmount,
feeRate: feeRate.halfHourFee,
feeRate: feeRate.halfHourFee.toNumber(),
});
const { fee: lowFeeValue } = determineUtxosForSpend({
utxos,
recipient,
amount: satAmount,
feeRate: feeRate.economyFee,
feeRate: feeRate.hourFee.toNumber(),
});
return [
@@ -61,7 +61,7 @@ export function useBitcoinFeesList({ amount, recipient }: UseBitcoinFeesListArgs
btcValue: formatMoneyPadded(createMoney(highFeeValue, 'BTC')),
time: btcTxTimeMap.fastestFee,
fiatValue: getFiatFeeValue(highFeeValue),
feeRate: feeRate.fastestFee,
feeRate: feeRate.fastestFee.toNumber(),
},
{
label: BtcFeeType.Standard,
@@ -69,7 +69,7 @@ export function useBitcoinFeesList({ amount, recipient }: UseBitcoinFeesListArgs
btcValue: formatMoneyPadded(createMoney(standartFeeValue, 'BTC')),
time: btcTxTimeMap.halfHourFee,
fiatValue: getFiatFeeValue(standartFeeValue),
feeRate: feeRate.halfHourFee,
feeRate: feeRate.halfHourFee.toNumber(),
},
{
label: BtcFeeType.Low,
@@ -77,12 +77,13 @@ export function useBitcoinFeesList({ amount, recipient }: UseBitcoinFeesListArgs
btcValue: formatMoneyPadded(createMoney(lowFeeValue, 'BTC')),
time: btcTxTimeMap.economyFee,
fiatValue: getFiatFeeValue(lowFeeValue),
feeRate: feeRate.economyFee,
feeRate: feeRate.hourFee.toNumber(),
},
];
}, [feeRate, btcMarketData, utxos, recipient, amount]);
return {
feesList,
isLoading,
};
}

View File

@@ -2,8 +2,8 @@ import { truncateMiddle } from '@stacks/ui-utils';
import { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import { sumNumbers } from '@app/common/math/helpers';
import { satToBtc } from '@app/common/money/unit-conversion';
import { sumNumbers } from '@app/common/utils';
export const getBitcoinTxCaption = (transaction?: BitcoinTransaction) =>
transaction ? truncateMiddle(transaction.txid, 4) : '';

View File

@@ -1,6 +1,6 @@
import { useIsFetching } from '@tanstack/react-query';
import { sumNumbers } from '@app/common/utils';
import { sumNumbers } from '@app/common/math/helpers';
import { QueryPrefixes } from '@app/query/query-prefixes';
function areAnyQueriesFetching(...args: number[]) {

View File

@@ -4,8 +4,8 @@ import * as btc from '@scure/btc-signer';
import { Money, createMoney } from '@shared/models/money.model';
import { sumNumbers } from '@app/common/math/helpers';
import { BtcSizeFeeEstimator } from '@app/common/transactions/bitcoin/fees/btc-size-fee-estimator';
import { sumNumbers } from '@app/common/utils';
import { useCurrentTaprootAccountUninscribedUtxos } from '@app/query/bitcoin/balance/bitcoin-balances.query';
import { useBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.hooks';
import { getNumberOfInscriptionOnUtxo } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query';

View File

@@ -1,4 +1,4 @@
import { sumNumbers } from '@app/common/utils';
import { sumNumbers } from '@app/common/math/helpers';
import { selectInscriptionTransferCoins } from './select-inscription-coins';

View File

@@ -1,8 +1,8 @@
import { BTC_P2WPKH_DUST_AMOUNT } from '@shared/constants';
import { isDefined } from '@shared/utils';
import { sumNumbers } from '@app/common/math/helpers';
import { BtcSizeFeeEstimator } from '@app/common/transactions/bitcoin/fees/btc-size-fee-estimator';
import { sumNumbers } from '@app/common/utils';
import { createCounter } from '@app/common/utils/counter';
import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client';
import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query';

View File

@@ -51,7 +51,7 @@ export function SendInscriptionForm() {
try {
const numInscriptionsOnUtxo = await getNumberOfInscriptionOnUtxo(utxo.txid, utxo.vout);
if (numInscriptionsOnUtxo !== 1) {
if (numInscriptionsOnUtxo > 1) {
setShowError('Sending inscription from utxo with multiple inscriptions is unsupported');
return;
}

View File

@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { createMoney } from '@shared/models/money.model';
import { sumNumbers } from '@app/common/utils';
import { sumNumbers } from '@app/common/math/helpers';
import { useCurrentBtcNativeSegwitAccountAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useGetBitcoinTransactionsByAddressQuery } from './transactions-by-address.query';

View File

@@ -5,7 +5,7 @@ import BigNumber from 'bignumber.js';
import { createMoney } from '@shared/models/money.model';
import { isDefined } from '@shared/utils';
import { sumNumbers } from '@app/common/utils';
import { sumNumbers } from '@app/common/math/helpers';
import { useCurrentBtcNativeSegwitAccountAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { createBitcoinCryptoCurrencyAssetTypeWrapper } from '../address/address.utils';

View File

@@ -1,9 +1,61 @@
import BigNumber from 'bignumber.js';
import { logger } from '@shared/logger';
import { useGetBitcoinFeeEstimatesQuery } from './fee-estimates.query';
import { calculateMeanAverage } from '@app/common/math/calculate-averages';
import {
useGetBitcoinAllFeeEstimatesQuery,
useGetBitcoinMempoolApiFeeEstimatesQuery,
} from './fee-estimates.query';
export function useBitcoinFeeRate() {
return useGetBitcoinFeeEstimatesQuery({
return useGetBitcoinMempoolApiFeeEstimatesQuery({
onError: err => logger.error('Error getting bitcoin fee estimates', { err }),
});
}
export function useAverageBitcoinFeeRate() {
const { data: avgApiFeeRates, isLoading } = useGetBitcoinAllFeeEstimatesQuery({
onError: err => logger.error('Error getting all apis bitcoin fee estimates', { err }),
select: resp => {
if (resp[0].status === 'rejected' && resp[1].status === 'rejected') {
return null;
}
let mempoolApiFeeRates = null;
if (resp[0].status === 'fulfilled') {
mempoolApiFeeRates = resp[0].value;
}
let earnApiFeeRates = null;
if (resp[1].status === 'fulfilled') {
earnApiFeeRates = resp[1].value;
}
const fastestFees = [
new BigNumber(mempoolApiFeeRates?.fastestFee ?? 0),
new BigNumber(earnApiFeeRates?.fastestFee ?? 0),
].filter(fee => fee.isGreaterThan(0));
const halfHourFees = [
new BigNumber(mempoolApiFeeRates?.halfHourFee ?? 0),
new BigNumber(earnApiFeeRates?.halfHourFee ?? 0),
].filter(fee => fee.isGreaterThan(0));
const hourFees = [
new BigNumber(mempoolApiFeeRates?.hourFee ?? 0),
new BigNumber(earnApiFeeRates?.hourFee ?? 0),
].filter(fee => fee.isGreaterThan(0));
// zero values for cases when one api is down
return {
fastestFee: calculateMeanAverage(fastestFees),
halfHourFee: calculateMeanAverage(halfHourFees),
hourFee: calculateMeanAverage(hourFees),
};
},
});
return { isLoading, avgApiFeeRates };
}

View File

@@ -5,25 +5,54 @@ import { useBitcoinClient } from '@app/store/common/api-clients.hooks';
import { BitcoinClient } from '../bitcoin-client';
function fetchBitcoinFeeEstimates(client: BitcoinClient) {
// Mempool api
function fetchMempoolApiBitcoinFeeEstimates(client: BitcoinClient) {
return async () => {
return client.feeEstimatesApi.getFeeEstimatesFromMempoolSpaceApi();
};
}
type FetchBitcoinFeeEstimatesResp = Awaited<
ReturnType<ReturnType<typeof fetchBitcoinFeeEstimates>>
type FetchMempoolApiBitcoinFeeEstimatesResp = Awaited<
ReturnType<ReturnType<typeof fetchMempoolApiBitcoinFeeEstimates>>
>;
// https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch06.asciidoc#transaction-fees
// Possible alt api if needed: https://bitcoinfees.earn.com/api
export function useGetBitcoinFeeEstimatesQuery<T extends unknown = FetchBitcoinFeeEstimatesResp>(
options?: AppUseQueryConfig<FetchBitcoinFeeEstimatesResp, T>
) {
export function useGetBitcoinMempoolApiFeeEstimatesQuery<
T extends unknown = FetchMempoolApiBitcoinFeeEstimatesResp
>(options?: AppUseQueryConfig<FetchMempoolApiBitcoinFeeEstimatesResp, T>) {
const client = useBitcoinClient();
return useQuery({
queryKey: ['bitcoin-fee-estimates'],
queryFn: fetchBitcoinFeeEstimates(client),
queryKey: ['mempool-api-bitcoin-fee-estimates'],
queryFn: fetchMempoolApiBitcoinFeeEstimates(client),
staleTime: 1000 * 60,
refetchOnWindowFocus: false,
refetchOnMount: false,
...options,
});
}
// Earn api
function fetchAllBitcoinFeeEstimates(client: BitcoinClient) {
return async () => {
return Promise.allSettled([
client.feeEstimatesApi.getFeeEstimatesFromMempoolSpaceApi(),
client.feeEstimatesApi.getFeeEstimatesFromEarnApi(),
]);
};
}
type FetchAllBitcoinFeeEstimatesResp = Awaited<
ReturnType<ReturnType<typeof fetchAllBitcoinFeeEstimates>>
>;
export function useGetBitcoinAllFeeEstimatesQuery<
T extends unknown = FetchAllBitcoinFeeEstimatesResp
>(options?: AppUseQueryConfig<FetchAllBitcoinFeeEstimatesResp, T>) {
const client = useBitcoinClient();
return useQuery({
queryKey: ['all-bitcoin-fee-estimates'],
queryFn: fetchAllBitcoinFeeEstimates(client),
staleTime: 1000 * 60,
refetchOnWindowFocus: false,
refetchOnMount: false,

View File

@@ -30,7 +30,9 @@ export type OrdApiInscriptionTxOutput = Prettify<yup.InferType<typeof ordApiGetT
export async function getNumberOfInscriptionOnUtxo(id: string, index: number) {
const resp = await fetchOrdinalsAwareUtxo(id, index);
return resp.all_inscriptions?.length ?? 0;
if (resp.all_inscriptions) return resp.all_inscriptions.length;
if (resp.inscriptions) return 1;
return 0;
}
async function fetchOrdinalsAwareUtxo(

View File

@@ -6,7 +6,7 @@ import { CryptoCurrencies } from '@shared/models/currencies.model';
import { MarketData, createMarketData, createMarketPair } from '@shared/models/market.model';
import { createMoney, currencyDecimalsMap } from '@shared/models/money.model';
import { calculateMeanAverage } from '@app/common/calculate-averages';
import { calculateMeanAverage } from '@app/common/math/calculate-averages';
import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money';
import {

View File

@@ -0,0 +1,21 @@
import { test } from '@playwright/test';
import { getNumberOfInscriptionOnUtxo } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query';
test.describe(getNumberOfInscriptionOnUtxo.name, () => {
test('should return 3 in case of 3 inscriptions', async () => {
const resp = await getNumberOfInscriptionOnUtxo(
'aa24aecb0e60afa43b646c5a61fee76aebdbbf85b8f85a4aa429f9d0c52c9623',
0
);
test.expect(resp).toBe(3);
});
test('should return 0 in case of this random address', async () => {
const resp = await getNumberOfInscriptionOnUtxo(
'75d11f43163ca0c3a1656e124a63bc08da267c0f8454aa5244ef7346839dc5d5',
0
);
test.expect(resp).toBe(0);
});
});