diff --git a/packages/bridge/package.json b/packages/bridge/package.json index d5e4225513..30b14a7ac1 100644 --- a/packages/bridge/package.json +++ b/packages/bridge/package.json @@ -40,7 +40,7 @@ "launchdarkly-node-client-sdk": "^3.3.0", "long": "^5.2.3", "lru-cache": "^10.0.1", - "nomic-bitcoin": "^4.2.0", + "nomic-bitcoin": "^5.0.0-pre.0", "viem": "^2.21.19", "zod": "^3.22.4" }, diff --git a/packages/bridge/src/nomic/index.ts b/packages/bridge/src/nomic/index.ts index aa67f7d939..cfdab75f4b 100644 --- a/packages/bridge/src/nomic/index.ts +++ b/packages/bridge/src/nomic/index.ts @@ -1,17 +1,27 @@ import type { Registry } from "@cosmjs/proto-signing"; import { Dec, RatePretty } from "@keplr-wallet/unit"; import { getRouteTokenOutGivenIn } from "@osmosis-labs/server"; -import { estimateGasFee, getSwapMessages } from "@osmosis-labs/tx"; +import { + estimateGasFee, + getSwapMessages, + makeSkipIbcHookSwapMemo, + SkipSwapIbcHookContractAddress, +} from "@osmosis-labs/tx"; import { IbcTransferMethod } from "@osmosis-labs/types"; import { deriveCosmosAddress, + getAllBtcMinimalDenom, + getnBTCMinimalDenom, isCosmosAddressValid, timeout, } from "@osmosis-labs/utils"; import { + BaseDepositOptions, buildDestination, + DepositInfo, generateDepositAddressIbc, getPendingDeposits, + IbcDepositOptions, } from "nomic-bitcoin"; import { BridgeQuoteError } from "../errors"; @@ -40,13 +50,15 @@ export class NomicBridgeProvider implements BridgeProvider { readonly relayers: string[]; readonly nBTCMinimalDenom: string; + readonly allBtcMinimalDenom: string | undefined; protected protoRegistry: Registry | null = null; constructor(protected readonly ctx: BridgeProviderContext) { - this.nBTCMinimalDenom = - this.ctx.env === "mainnet" - ? "ibc/75345531D87BD90BF108BE7240BD721CB2CB0A1F16D4EBA71B09EC3C43E15C8F" // nBTC - : "ibc/72D483F0FD4229DBF3ACC78E648F0399C4ACADDFDBCDD9FE791FEE4443343422"; // Testnet nBTC + this.allBtcMinimalDenom = getAllBtcMinimalDenom({ env: this.ctx.env }); + this.nBTCMinimalDenom = getnBTCMinimalDenom({ + env: this.ctx.env, + }); + this.relayers = this.ctx.env === "testnet" ? ["https://testnet-relayer.nomic.io:8443"] @@ -111,7 +123,9 @@ export class NomicBridgeProvider implements BridgeProvider { | Awaited> | undefined; - if (fromAsset.address !== this.nBTCMinimalDenom) { + if ( + fromAsset.address.toLowerCase() === this.allBtcMinimalDenom?.toLowerCase() + ) { swapRoute = await getRouteTokenOutGivenIn({ assetLists: this.ctx.assetLists, tokenInAmount: fromAmount, @@ -264,6 +278,7 @@ export class NomicBridgeProvider implements BridgeProvider { async getDepositAddress({ fromChain, toAddress, + toAsset, }: GetDepositAddressParams): Promise { if (!isCosmosAddressValid({ address: toAddress, bech32Prefix: "osmo" })) { throw new BridgeQuoteError({ @@ -287,11 +302,11 @@ export class NomicBridgeProvider implements BridgeProvider { }); } - const transferMethod = nomicBtc.transferMethods.find( + const nomicIbcTransferMethod = nomicBtc.transferMethods.find( (method): method is IbcTransferMethod => method.type === "ibc" ); - if (!transferMethod) { + if (!nomicIbcTransferMethod) { throw new BridgeQuoteError({ bridgeId: "Nomic", errorType: "UnsupportedQuoteError", @@ -307,11 +322,52 @@ export class NomicBridgeProvider implements BridgeProvider { }); } + const nomicChain = this.ctx.chainList.find( + ({ chain_name }) => + chain_name === nomicIbcTransferMethod.counterparty.chainName + ); + + if (!nomicChain) { + throw new BridgeQuoteError({ + bridgeId: "Nomic", + errorType: "UnsupportedQuoteError", + message: "Nomic chain not found in chain list.", + }); + } + + const userWantsAllBtc = + this.allBtcMinimalDenom && toAsset.address === this.allBtcMinimalDenom; + + const now = Date.now(); + const timeoutTimestampFiveDaysFromNow = + Number(now + 86_400 * 5 * 1_000 - (now % (60 * 60 * 1_000))) * 1_000_000; + const swapMemo = userWantsAllBtc + ? makeSkipIbcHookSwapMemo({ + denomIn: this.nBTCMinimalDenom, + denomOut: + this.ctx.env === "mainnet" ? this.allBtcMinimalDenom : "uosmo", + env: this.ctx.env, + minAmountOut: "1", + poolId: + this.ctx.env === "mainnet" + ? "1868" // nBTC/allBTC pool on Osmosis + : "663", // nBTC/osmo pool on Osmosis. Since there's no alloyed btc in testnet, we'll use these pool instead + receiverOsmoAddress: toAddress, + timeoutTimestamp: timeoutTimestampFiveDaysFromNow, + }) + : undefined; + const depositInfo = await generateDepositAddressIbc({ relayers: this.relayers, - channel: transferMethod.counterparty.channelId, // IBC channel ID on Nomic + channel: nomicIbcTransferMethod.counterparty.channelId, // IBC channel ID on Nomic bitcoinNetwork: this.ctx.env === "testnet" ? "testnet" : "bitcoin", - receiver: toAddress, + sender: deriveCosmosAddress({ + address: toAddress, + desiredBech32Prefix: nomicChain.bech32_prefix, + }), + receiver: + userWantsAllBtc && swapMemo ? swapMemo.wasm.contract : toAddress, + ...(swapMemo ? { memo: JSON.stringify(swapMemo) } : {}), }); if (depositInfo.code === 1) { @@ -387,18 +443,31 @@ export class NomicBridgeProvider implements BridgeProvider { const isNomicBtc = assetListAsset.coinMinimalDenom.toLowerCase() === this.nBTCMinimalDenom.toLowerCase(); + const isAllBtc = + this.allBtcMinimalDenom && + assetListAsset.coinMinimalDenom.toLowerCase() === + this.allBtcMinimalDenom.toLowerCase(); - const nomicWithdrawAmountEnabled = await getLaunchDarklyFlagValue({ + const nomicWithdrawEnabled = await getLaunchDarklyFlagValue({ key: "nomicWithdrawAmount", }); if (bitcoinCounterparty || isNomicBtc) { + let transferTypes: BridgeSupportedAsset["transferTypes"] = []; + + if (direction === "withdraw" && nomicWithdrawEnabled) { + transferTypes = ["quote"]; + } else if (direction === "deposit" && (isNomicBtc || isAllBtc)) { + transferTypes = ["deposit-address"]; + } + + if (transferTypes.length === 0) { + return []; + } + return [ { - transferTypes: - direction === "withdraw" && nomicWithdrawAmountEnabled - ? ["quote"] - : ["deposit-address"], + transferTypes, chainId: "bitcoin", chainName: "Bitcoin", chainType: "bitcoin", @@ -437,10 +506,43 @@ export class NomicBridgeProvider implements BridgeProvider { async getPendingDeposits({ address }: { address: string }) { try { - const pendingDeposits = await timeout( - () => getPendingDeposits(this.relayers, address), - 10000 - )(); + const [pendingDeposits, skipSwapPendingDeposits] = await Promise.all([ + timeout(() => getPendingDeposits(this.relayers, address), 10000)(), + /** + * We need to check all deposits to skip contract since we set the receiver to the skip contract address. + * So, we need to filter out any deposits that are not intended for the user. + */ + timeout( + () => + getPendingDeposits(this.relayers, SkipSwapIbcHookContractAddress), + 10000 + )(), + ]); + + /** + * Filter out deposits that are not intended for the user + */ + const filteredSkipSwapPendingDeposits = skipSwapPendingDeposits + .filter((deposit) => { + try { + if (!("dest" in deposit)) return false; + const dest = deposit.dest as { + data: BaseDepositOptions & IbcDepositOptions; + }; + const memo = JSON.parse(dest.data.memo ?? "{}"); + return ( + memo.wasm.msg.swap_and_action.post_swap_action.transfer + .to_address === address + ); + } catch (error) { + console.error("Error parsing memo:", error); + return false; + } + }) + .map((deposit) => ({ + ...deposit, + __type: "contract-deposit" as const, + })); const nomicBtc = this.ctx.assetLists .flatMap(({ assets }) => assets) @@ -452,7 +554,12 @@ export class NomicBridgeProvider implements BridgeProvider { throw new Error("Nomic Bitcoin asset not found in asset list."); } - return pendingDeposits.map((deposit) => ({ + const deposits = [ + ...pendingDeposits, + ...filteredSkipSwapPendingDeposits, + ] as (DepositInfo & { __type?: "contract-deposit" })[]; + + return deposits.map((deposit) => ({ transactionId: deposit.txid, amount: deposit.amount, confirmations: deposit.confirmations, @@ -460,16 +567,16 @@ export class NomicBridgeProvider implements BridgeProvider { address: nomicBtc.coinMinimalDenom, amount: ((deposit.minerFeeRate ?? 0) * 1e8).toString(), decimals: 8, - // TODO: Handle case when we can receive allBTC - denom: nomicBtc.symbol, + denom: + deposit.__type === "contract-deposit" ? "BTC" : nomicBtc.symbol, coinGeckoId: nomicBtc.coingeckoId, }, providerFee: { address: nomicBtc.coinMinimalDenom, amount: ((deposit.bridgeFeeRate ?? 0) * deposit.amount).toString(), decimals: 8, - // TODO: Handle case when we can receive allBTC - denom: nomicBtc.symbol, + denom: + deposit.__type === "contract-deposit" ? "BTC" : nomicBtc.symbol, coinGeckoId: nomicBtc.coingeckoId, }, })); diff --git a/packages/server/src/queries/complex/portfolio/__tests__/assets.spec.ts b/packages/server/src/queries/complex/portfolio/__tests__/assets.spec.ts index afb8b16a13..74efc0d074 100644 --- a/packages/server/src/queries/complex/portfolio/__tests__/assets.spec.ts +++ b/packages/server/src/queries/complex/portfolio/__tests__/assets.spec.ts @@ -262,16 +262,19 @@ describe("Has Asset Variants", () => { denom: "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", // USDC amount: "0.2", + fiatValue: "0.2", }, { denom: "factory/osmo1rckme96ptawr4zwexxj5g5gej9s2dmud8r2t9j0k0prn5mch5g4snzzwjv/sail", // SAIL amount: "0.2", + fiatValue: "0.2", }, { denom: "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", // ETH.axl <- this is the variant amount: "0.2", + fiatValue: "0.2", }, ]; @@ -284,11 +287,13 @@ describe("Has Asset Variants", () => { { denom: "uosmo", // OSMO amount: "0.2", + fiatValue: "0.2", }, { denom: "factory/osmo1rckme96ptawr4zwexxj5g5gej9s2dmud8r2t9j0k0prn5mch5g4snzzwjv/sail", // SAIL amount: "0.2", + fiatValue: "0.2", }, ]; @@ -297,7 +302,11 @@ describe("Has Asset Variants", () => { }); it("should return empty array when user has no asset variants", () => { - const userCoinMinimalDenoms: { denom: string; amount: string }[] = []; + const userCoinMinimalDenoms: { + denom: string; + amount: string; + fiatValue: string; + }[] = []; const result = checkAssetVariants(userCoinMinimalDenoms, assetLists); expect(result.length).toBe(0); diff --git a/packages/server/src/queries/complex/portfolio/assets.ts b/packages/server/src/queries/complex/portfolio/assets.ts index 0656321440..fadd377815 100644 --- a/packages/server/src/queries/complex/portfolio/assets.ts +++ b/packages/server/src/queries/complex/portfolio/assets.ts @@ -5,7 +5,6 @@ import { sort } from "@osmosis-labs/utils"; import { captureIfError } from "../../../utils"; import { Categories, queryPortfolioAssets } from "../../sidecar"; -import { AccountCoinsResultDec } from "../../sidecar/portfolio-assets"; import { getAsset } from "../assets"; import { DEFAULT_VS_CURRENCY } from "../assets/config"; @@ -19,6 +18,7 @@ export interface Allocation { export interface AssetVariant { name: string; amount: CoinPretty; + fiatValue: PricePretty; canonicalAsset: MinimalAsset; } @@ -86,21 +86,21 @@ export function calculatePercentAndFiatValues( const account_coins_result = (totalAssets?.account_coins_result || []).map( (asset) => ({ - ...asset, - cap_value: new Dec(asset.cap_value), + coin: asset.coin, + fiatValue: new Dec(asset.cap_value), }) ); const sortedAccountCoinsResults = sort( account_coins_result || [], - "cap_value", + "fiatValue", "desc" ); const topCoinsResults = sortedAccountCoinsResults.slice(0, allocationLimit); const assets: Allocation[] = topCoinsResults - .map((asset: AccountCoinsResultDec) => { + .map((asset) => { const assetFromAssetLists = captureIfError(() => getAsset({ assetLists, @@ -116,8 +116,8 @@ export function calculatePercentAndFiatValues( key: assetFromAssetLists.coinDenom, percentage: totalCap.isZero() ? new RatePretty(0) - : new RatePretty(asset.cap_value.quo(totalCap)), - fiatValue: new PricePretty(DEFAULT_VS_CURRENCY, asset.cap_value), + : new RatePretty(asset.fiatValue.quo(totalCap)), + fiatValue: new PricePretty(DEFAULT_VS_CURRENCY, asset.fiatValue), asset: new CoinPretty(assetFromAssetLists, asset.coin.amount), }; }) @@ -126,7 +126,7 @@ export function calculatePercentAndFiatValues( const otherAssets = sortedAccountCoinsResults.slice(allocationLimit); const otherAmount = otherAssets.reduce( - (sum: Dec, asset: AccountCoinsResultDec) => sum.add(asset.cap_value), + (sum: Dec, asset) => sum.add(asset.fiatValue), new Dec(0) ); @@ -195,6 +195,7 @@ export async function getPortfolioAssets({ categories["user-balances"]?.account_coins_result?.map((result) => ({ denom: result.coin.denom, amount: result.coin.amount, // Assuming amount is stored in result.coin.amount + fiatValue: result.cap_value, })) ?? []; // check for asset variants, alloys and canonical assets such as USDC @@ -210,13 +211,13 @@ export async function getPortfolioAssets({ } export function checkAssetVariants( - userBalanceDenoms: { denom: string; amount: string }[], + userBalanceDenoms: { denom: string; amount: string; fiatValue: string }[], assetLists: AssetList[] ): AssetVariant[] { const assetListAssets = assetLists.flatMap((list) => list.assets); return userBalanceDenoms - .map(({ denom, amount }) => { + .map(({ denom, amount, fiatValue }) => { const asset = assetListAssets.find( (asset) => asset.coinMinimalDenom === denom ); @@ -240,6 +241,7 @@ export function checkAssetVariants( return { name: userAsset.coinName, amount: new CoinPretty(userAsset, amount), + fiatValue: new PricePretty(DEFAULT_VS_CURRENCY, new Dec(fiatValue)), canonicalAsset: canonicalAsset, }; } diff --git a/packages/server/src/queries/sidecar/portfolio-assets.ts b/packages/server/src/queries/sidecar/portfolio-assets.ts index 7104e64f39..eb7323ef63 100644 --- a/packages/server/src/queries/sidecar/portfolio-assets.ts +++ b/packages/server/src/queries/sidecar/portfolio-assets.ts @@ -1,4 +1,3 @@ -import { Dec } from "@keplr-wallet/unit"; import { apiClient } from "@osmosis-labs/utils"; import { SIDECAR_BASE_URL } from "../../env"; @@ -13,10 +12,6 @@ export interface AccountCoinsResult { cap_value: string; } -export interface AccountCoinsResultDec { - coin: Coin; - cap_value: Dec; -} export interface Category { capitalization: string; is_best_effort: boolean; diff --git a/packages/stores/src/account/base.ts b/packages/stores/src/account/base.ts index bd72913f15..b09fc7b4ac 100644 --- a/packages/stores/src/account/base.ts +++ b/packages/stores/src/account/base.ts @@ -1032,12 +1032,12 @@ export class AccountStore[] = []> { } if (memo === "") { - // If the memo is empty, set it to "FE" so we know it originated from the frontend for + // If the memo is empty, set it to "OsmosisFE" so we know it originated from the frontend for // QA purposes. - memo = "FE"; + memo = "OsmosisFE"; } else { - // Otherwise, tack on "FE" to the end of the memo. - memo += " \nFE"; + // Otherwise, tack on "OsmosisFE" to the end of the memo. + memo += " \nOsmosisFE"; } const [ @@ -1230,12 +1230,12 @@ export class AccountStore[] = []> { pubkey.typeUrl = pubKeyTypeUrl; if (memo === "") { - // If the memo is empty, set it to "FE" so we know it originated from the frontend for + // If the memo is empty, set it to "OsmosisFE" so we know it originated from the frontend for // QA purposes. - memo = "FE"; + memo = "OsmosisFE"; } else { - // Otherwise, tack on "FE" to the end of the memo. - memo += " \nFE"; + // Otherwise, tack on "OsmosisFE" to the end of the memo. + memo += " \nOsmosisFE"; } const txBodyEncodeObject = { diff --git a/packages/trpc/src/assets.ts b/packages/trpc/src/assets.ts index a70ee7930c..8df816c203 100644 --- a/packages/trpc/src/assets.ts +++ b/packages/trpc/src/assets.ts @@ -22,13 +22,20 @@ import { getPoolAssetPairHistoricalPrice, getUpcomingAssets, getUserAssetsTotal, + IS_TESTNET, mapGetAssetsWithUserBalances, mapGetMarketAssets, maybeCachePaginatedItems, TimeDuration, TimeFrame, } from "@osmosis-labs/server"; -import { compareCommon, isNil, sort } from "@osmosis-labs/utils"; +import { + compareCommon, + getAllBtcMinimalDenom, + getnBTCMinimalDenom, + isNil, + sort, +} from "@osmosis-labs/utils"; import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "./api"; @@ -107,15 +114,55 @@ export const assetsRouter = createTRPCRouter({ limit, }) ), - getCanonicalAssetWithVariants: publicProcedure - .input( - z.object({ - findMinDenomOrSymbol: z.string(), - }) - ) - .query(async ({ input: { findMinDenomOrSymbol }, ctx }) => - getAssetWithVariants({ ...ctx, anyDenom: findMinDenomOrSymbol }) - ), + + getBridgeAssetWithVariants: publicProcedure + .input(z.object({ findMinDenomOrSymbol: z.string() })) + .query(async ({ input: { findMinDenomOrSymbol }, ctx }) => { + const canonicalAssetWithVariants = getAssetWithVariants({ + ...ctx, + anyDenom: findMinDenomOrSymbol, + }); + + /** + * Manually include Nomic and BTC assets in the list. + * These assets are not variants of each other and cannot be automatically linked + * from our asset list. We do this to display them in the receive asset dropdown + * and enable conversion to alloy or native. + */ + if ( + canonicalAssetWithVariants[0].coinMinimalDenom.toLowerCase() === + getnBTCMinimalDenom({ + env: IS_TESTNET ? "testnet" : "mainnet", + }).toLowerCase() + ) { + const allBtcAsset = getAsset({ + ...ctx, + anyDenom: + getAllBtcMinimalDenom({ + env: IS_TESTNET ? "testnet" : "mainnet", + }) ?? "", + }); + if (allBtcAsset) canonicalAssetWithVariants.push(allBtcAsset); + } + + if ( + canonicalAssetWithVariants[0].coinMinimalDenom.toLowerCase() === + ( + getAllBtcMinimalDenom({ env: IS_TESTNET ? "testnet" : "mainnet" }) ?? + "" + ).toLowerCase() + ) { + const nBTCAsset = getAsset({ + ...ctx, + anyDenom: getnBTCMinimalDenom({ + env: IS_TESTNET ? "testnet" : "mainnet", + }), + }); + if (nBTCAsset) canonicalAssetWithVariants.push(nBTCAsset); + } + + return canonicalAssetWithVariants; + }), getAssetPrice: publicProcedure .input( z.object({ diff --git a/packages/tx/src/message-composers/complex/swap.ts b/packages/tx/src/message-composers/complex/swap.ts index bf5a63746f..78ad23ed13 100644 --- a/packages/tx/src/message-composers/complex/swap.ts +++ b/packages/tx/src/message-composers/complex/swap.ts @@ -246,3 +246,66 @@ export async function getSwapMessages({ throw new Error(`Unsupported quote type ${quoteType}`); } + +export const SkipSwapIbcHookContractAddress = + "osmo1vkdakqqg5htq5c3wy2kj2geq536q665xdexrtjuwqckpads2c2nsvhhcyv"; + +export function makeSkipIbcHookSwapMemo({ + denomIn, + denomOut, + minAmountOut: amountOut, + poolId, + receiverOsmoAddress, + env, + timeoutTimestamp, +}: { + denomIn: string; + denomOut: string; + minAmountOut: string; + poolId: string; + timeoutTimestamp: number; + receiverOsmoAddress: string; + env: "testnet" | "mainnet"; +}) { + return { + wasm: { + contract: + env === "testnet" + ? // osmosis-1 and osmo-test-5 share the same contract address + SkipSwapIbcHookContractAddress // https://celatone.osmosis.zone/osmo-test-5/contracts/osmo1vkdakqqg5htq5c3wy2kj2geq536q665xdexrtjuwqckpads2c2nsvhhcyv + : SkipSwapIbcHookContractAddress, // https://celatone.osmosis.zone/osmo-1/contracts/osmo1vkdakqqg5htq5c3wy2kj2geq536q665xdexrtjuwqckpads2c2nsvhhcyv + msg: { + swap_and_action: { + user_swap: { + swap_exact_asset_in: { + swap_venue_name: + env === "testnet" + ? "testnet-osmosis-poolmanager" + : "osmosis-poolmanager", + operations: [ + { + denom_in: denomIn, + denom_out: denomOut, + pool: poolId, + }, + ], + }, + }, + timeout_timestamp: timeoutTimestamp, + min_asset: { + native: { + denom: denomOut, + amount: amountOut, + }, + }, + post_swap_action: { + transfer: { + to_address: receiverOsmoAddress, + }, + }, + affiliates: [], + }, + }, + }, + }; +} diff --git a/packages/types/src/feature-flags-types.ts b/packages/types/src/feature-flags-types.ts index 986737bfb6..34dafa8661 100644 --- a/packages/types/src/feature-flags-types.ts +++ b/packages/types/src/feature-flags-types.ts @@ -22,4 +22,5 @@ export type AvailableFlags = | "sqsActiveOrders" | "alloyedAssets" | "bridgeDepositAddress" - | "nomicWithdrawAmount"; + | "nomicWithdrawAmount" + | "swapToolTopGainers"; diff --git a/packages/utils/src/bitcoin.ts b/packages/utils/src/bitcoin.ts index 6baf706d74..b72303af94 100644 --- a/packages/utils/src/bitcoin.ts +++ b/packages/utils/src/bitcoin.ts @@ -21,3 +21,23 @@ export const getBitcoinExplorerUrl = ({ ? BitcoinTestnetExplorerUrl.replace("{txHash}", txHash) : BitcoinMainnetExplorerUrl.replace("{txHash}", txHash); }; + +export const getAllBtcMinimalDenom = ({ + env, +}: { + env: "mainnet" | "testnet"; +}) => { + return env === "mainnet" + ? "factory/osmo1z6r6qdknhgsc0zeracktgpcxf43j6sekq07nw8sxduc9lg0qjjlqfu25e3/alloyed/allBTC" + : undefined; // No testnet allBTC for now. +}; + +export const getnBTCMinimalDenom = ({ + env, +}: { + env: "mainnet" | "testnet"; +}) => { + return env === "mainnet" + ? "ibc/75345531D87BD90BF108BE7240BD721CB2CB0A1F16D4EBA71B09EC3C43E15C8F" // nBTC + : "ibc/72D483F0FD4229DBF3ACC78E648F0399C4ACADDFDBCDD9FE791FEE4443343422"; // Testnet nBTC +}; diff --git a/packages/web/components/assets/highlights-categories.tsx b/packages/web/components/assets/highlights-categories.tsx index 5c001aa727..0ca1797176 100644 --- a/packages/web/components/assets/highlights-categories.tsx +++ b/packages/web/components/assets/highlights-categories.tsx @@ -94,7 +94,7 @@ const HighlightsGrid: FunctionComponent = ({ ); }; -function highlightPrice24hChangeAsset(asset: PriceChange24hAsset) { +export function highlightPrice24hChangeAsset(asset: PriceChange24hAsset) { return { asset: { ...asset, @@ -129,7 +129,7 @@ function highlightUpcomingReleaseAsset(asset: UpcomingReleaseAsset) { }; } -const AssetHighlights: FunctionComponent< +export const AssetHighlights: FunctionComponent< { title: string; subtitle?: string; @@ -145,6 +145,7 @@ const AssetHighlights: FunctionComponent< isLoading?: boolean; disableLinking?: boolean; highlight: Highlight; + onClickAsset?: (asset: HighlightAsset) => void; } & CustomClasses > = ({ title, @@ -154,6 +155,7 @@ const AssetHighlights: FunctionComponent< isLoading = false, className, highlight, + onClickAsset, }) => { const { t } = useTranslation(); @@ -192,6 +194,7 @@ const AssetHighlights: FunctionComponent< asset={asset} extraInfo={extraInfo} highlight={highlight} + onClick={onClickAsset} /> ))} @@ -201,21 +204,21 @@ const AssetHighlights: FunctionComponent< ); }; +type HighlightAsset = { + coinDenom: string; + coinName: string; + coinImageUrl?: string; + href?: string; + externalLink?: boolean; +}; + const AssetHighlightRow: FunctionComponent<{ - asset: { - coinDenom: string; - coinName: string; - coinImageUrl?: string; - href?: string; - externalLink?: boolean; - }; + asset: HighlightAsset; extraInfo: ReactNode; highlight: Highlight; -}> = ({ - asset: { coinDenom, coinName, coinImageUrl, href, externalLink }, - extraInfo, - highlight, -}) => { + onClick?: (asset: HighlightAsset) => void; +}> = ({ asset, extraInfo, highlight, onClick }) => { + const { coinDenom, coinName, coinImageUrl, href, externalLink } = asset; const { logEvent } = useAmplitudeAnalytics(); const AssetContent = ( @@ -234,7 +237,10 @@ const AssetHighlightRow: FunctionComponent<{ ); return !href ? ( -
+
onClick?.(asset)} + > {AssetContent}
) : ( @@ -245,6 +251,7 @@ const AssetHighlightRow: FunctionComponent<{ className="-mx-2 flex items-center justify-between gap-4 rounded-lg p-2 transition-colors duration-200 ease-in-out hover:cursor-pointer hover:bg-osmoverse-850" onClick={() => { logEvent([EventName.Assets.assetClicked, { coinDenom, highlight }]); + onClick?.(asset); }} > {AssetContent} diff --git a/packages/web/components/bridge/amount-and-review-screen.tsx b/packages/web/components/bridge/amount-and-review-screen.tsx index 02ac700a81..77982cd28b 100644 --- a/packages/web/components/bridge/amount-and-review-screen.tsx +++ b/packages/web/components/bridge/amount-and-review-screen.tsx @@ -102,7 +102,7 @@ export const AmountAndReviewScreen = observer( : toChainCosmosAccount?.walletInfo.name; const { data: assetsInOsmosis, isLoading: isLoadingAssetsInOsmosis } = - api.edge.assets.getCanonicalAssetWithVariants.useQuery( + api.edge.assets.getBridgeAssetWithVariants.useQuery( { findMinDenomOrSymbol: selectedAssetDenom ?? "", }, diff --git a/packages/web/components/bridge/amount-screen.tsx b/packages/web/components/bridge/amount-screen.tsx index ba7fb8f1c2..783aef9911 100644 --- a/packages/web/components/bridge/amount-screen.tsx +++ b/packages/web/components/bridge/amount-screen.tsx @@ -2,10 +2,6 @@ import { Disclosure, DisclosureButton, DisclosurePanel, - Menu, - MenuButton, - MenuItem, - MenuItems, } from "@headlessui/react"; import { IntPretty } from "@keplr-wallet/unit"; import { MinimalAsset } from "@osmosis-labs/types"; @@ -26,6 +22,7 @@ import { import { useMeasure } from "react-use"; import { Icon } from "~/components/assets"; +import { BridgeReceiveAssetDropdown } from "~/components/bridge/bridge-receive-asset-dropdown"; import { DepositAddressScreen } from "~/components/bridge/deposit-address-screen"; import { CryptoFiatInput } from "~/components/control/crypto-fiat-input"; import { SkeletonLoader, Spinner } from "~/components/loaders"; @@ -34,7 +31,6 @@ import { ScreenManager, useScreenManager, } from "~/components/screen-manager"; -import { Tooltip } from "~/components/tooltip"; import { Button } from "~/components/ui/button"; import { EventName } from "~/config"; import { EthereumChainIds } from "~/config/wagmi"; @@ -770,6 +766,25 @@ export const AmountScreen = observer(
); + const assetDropdown = fromAsset && + toAsset && + assetsInOsmosis && + shouldShowAssetDropdown && + !isLoading && ( + + ); + + console.log(fromAsset); + if ( featureFlags.bridgeDepositAddress && !quote.enabled && @@ -786,6 +801,7 @@ export const AmountScreen = observer( canonicalAsset={canonicalAsset} direction={direction} chainSelection={chainSelection} + assetDropdown={assetDropdown} fromChain={fromChain} toChain={toChain} fromAsset={fromAsset} @@ -1085,218 +1101,7 @@ export const AmountScreen = observer( )} - {fromAsset && - toAsset && - assetsInOsmosis && - shouldShowAssetDropdown && - !isLoading && ( - - {({ open }) => ( -
- -
- -

- {t("transfer.receiveAsset")} -

-

- {direction === "deposit" - ? t("transfer.depositAssetDescription") - : t("transfer.withdrawAssetDescription")} -

-
- } - enablePropagation - > -
- - {t("transfer.receiveAsset")} - - address)[0], - } - )} - id="generate-stars" - width={24} - /> -
- - -
- - {toAsset?.denom} - - -
-
- - - - {direction === "deposit" ? ( - <> - {Object.keys(fromAsset.supportedVariants).map( - (variantCoinMinimalDenom, index) => { - const asset = assetsInOsmosis.find( - (asset) => - asset.coinMinimalDenom === - variantCoinMinimalDenom - )!; - - const onClick = () => { - logEvent([ - EventName.DepositWithdraw.variantSelected, - ]); - setToAsset({ - chainType: "cosmos", - address: asset.coinMinimalDenom, - decimals: asset.coinDecimals, - chainId: accountStore.osmosisChainId, - denom: asset.coinDenom, - // Can be left empty because for deposits we don't rely on the supported variants within the destination asset - supportedVariants: {}, - transferTypes: [], - }); - }; - - // Show all as 'deposit as' for now - const isConvert = - false ?? - asset.coinMinimalDenom === - asset.variantGroupKey; - const isSelected = - toAsset?.address === asset.coinMinimalDenom; - - const isCanonicalAsset = index === 0; - - return ( - - - - ); - } - )} - - ) : ( - <> - {counterpartySupportedAssetsByChainId[ - toAsset.chainId - ].map((asset, index, assets) => { - const onClick = () => { - setToAsset(asset); - }; - - const isSelected = - toAsset?.address === asset.address; - - const isCanonicalAsset = index === 0; - const representativeAsset = - assetsInOsmosis.find( - (a) => - a.coinMinimalDenom === asset.address || - asset.denom === a.coinDenom - ) ?? assetsInOsmosis[0]; - - /** - * If the denom/symbol is the same as the root variant, - * there's no visible difference in the dropdown. - * So if it's the same, we reveal the address as subtext. - */ - const revealAddress = - assets[0].denom === asset.denom; - - return ( - - - - ); - })} - - )} - - - )} -
- )} + {assetDropdown} {fromChain && ( void; + assetsInOsmosis: MinimalAsset[]; + counterpartySupportedAssetsByChainId: Record; +} + +export const BridgeReceiveAssetDropdown: FunctionComponent = + observer( + ({ + direction, + fromAsset, + toAsset, + setToAsset, + assetsInOsmosis, + counterpartySupportedAssetsByChainId, + }) => { + const { logEvent } = useAmplitudeAnalytics(); + const { accountStore } = useStore(); + const { t } = useTranslation(); + + return ( + + {({ open }) => ( +
+ +
+ +

+ {t("transfer.receiveAsset")} +

+

+ {direction === "deposit" + ? t("transfer.depositAssetDescription") + : t("transfer.withdrawAssetDescription")} +

+
+ } + enablePropagation + > +
+ + {t("transfer.receiveAsset")} + + address)[0], + })} + id="generate-stars" + width={24} + /> +
+ + +
+ + {toAsset?.denom} + + +
+
+ + + + {direction === "deposit" ? ( + <> + {Object.keys(fromAsset.supportedVariants).map( + (variantCoinMinimalDenom, index) => { + const asset = assetsInOsmosis.find( + (asset) => + asset.coinMinimalDenom === variantCoinMinimalDenom + )!; + + const onClick = () => { + logEvent([EventName.DepositWithdraw.variantSelected]); + setToAsset({ + chainType: "cosmos", + address: asset.coinMinimalDenom, + decimals: asset.coinDecimals, + chainId: accountStore.osmosisChainId, + denom: asset.coinDenom, + supportedVariants: {}, + transferTypes: [], + }); + }; + + const isConvert = + false ?? + asset.coinMinimalDenom === asset.variantGroupKey; + const isSelected = + toAsset?.address === asset.coinMinimalDenom; + + const isCanonicalAsset = index === 0; + + return ( + + + + ); + } + )} + + ) : ( + <> + {counterpartySupportedAssetsByChainId[toAsset.chainId].map( + (asset, index, assets) => { + const onClick = () => { + setToAsset(asset); + }; + + const isSelected = toAsset?.address === asset.address; + + const isCanonicalAsset = index === 0; + const representativeAsset = + assetsInOsmosis.find( + (a) => + a.coinMinimalDenom === asset.address || + asset.denom === a.coinDenom + ) ?? assetsInOsmosis[0]; + + const revealAddress = assets[0].denom === asset.denom; + + return ( + + + + ); + } + )} + + )} + + + )} +
+ ); + } + ); diff --git a/packages/web/components/bridge/deposit-address-screen.tsx b/packages/web/components/bridge/deposit-address-screen.tsx index 5de0536b9b..a0964e0b9a 100644 --- a/packages/web/components/bridge/deposit-address-screen.tsx +++ b/packages/web/components/bridge/deposit-address-screen.tsx @@ -44,6 +44,7 @@ interface DepositAddressScreenProps { direction: "deposit" | "withdraw"; canonicalAsset: MinimalAsset; chainSelection: React.ReactNode; + assetDropdown: React.ReactNode; fromChain: BridgeChainWithDisplayInfo; toChain: BridgeChainWithDisplayInfo; fromAsset: BridgeAsset; @@ -56,6 +57,7 @@ export const DepositAddressScreen = observer( direction, canonicalAsset, chainSelection, + assetDropdown, fromChain, bridge, toChain, @@ -87,6 +89,8 @@ export const DepositAddressScreen = observer( enabled: !!osmosisAddress, refetchOnWindowFocus: false, useErrorBoundary: true, + cacheTime: 0, + staleTime: 0, } ); @@ -324,18 +328,13 @@ export const DepositAddressScreen = observer( ) : ( <> - {t("transfer.receiveAsset")}}> - {/* TODO: Remove this conditional when alloyed BTC is supported */} -

- {bridge === "Nomic" ? "nBTC" : toAsset.denom} -

-
+
{assetDropdown}
{t("transfer.minimumDeposit")}}> -

+

{data?.depositData?.minimumDeposit.amount.toString()}{" "} {data?.depositData?.minimumDeposit.fiatValue ? ( <> @@ -369,7 +368,7 @@ const DepositInfoRow: FunctionComponent<{ }> = ({ label, children }) => { return (

-

{label}

+

{label}

{children}
); @@ -414,13 +413,15 @@ const TransferDetails: FunctionComponent<{

) : open ? ( -

{t("transfer.transferDetails")}

+

+ {t("transfer.transferDetails")} +

) : null} {!isLoading && depositData?.estimatedTime && !open && (
-

+

{t(depositData?.estimatedTime)}

@@ -436,7 +437,7 @@ const TransferDetails: FunctionComponent<{
{!open && totalFees && ( - + {!showTotalFeeIneqSymbol && "~"}{" "} {totalFees .inequalitySymbol(showTotalFeeIneqSymbol) diff --git a/packages/web/components/bridge/review-screen.tsx b/packages/web/components/bridge/review-screen.tsx index 0c7775ab60..e8b7d4e3f5 100644 --- a/packages/web/components/bridge/review-screen.tsx +++ b/packages/web/components/bridge/review-screen.tsx @@ -82,7 +82,7 @@ export const ReviewScreen: FunctionComponent = ({ const { t } = useTranslation(); const { data: assetsInOsmosis } = - api.edge.assets.getCanonicalAssetWithVariants.useQuery( + api.edge.assets.getBridgeAssetWithVariants.useQuery( { findMinDenomOrSymbol: selectedDenom!, }, diff --git a/packages/web/components/cards/my-position/__tests__/expanded.spec.tsx b/packages/web/components/cards/my-position/__tests__/expanded.spec.tsx new file mode 100644 index 0000000000..fb612ed5a7 --- /dev/null +++ b/packages/web/components/cards/my-position/__tests__/expanded.spec.tsx @@ -0,0 +1,115 @@ +import { CoinPretty, Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; +import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; +import { render, screen } from "@testing-library/react"; +import React from "react"; + +import { AssetsInfo } from "../expanded"; + +jest.mock("~/hooks", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe("AssetsInfo", () => { + it("renders with assets and total value", () => { + const assets = [ + new CoinPretty( + { coinDenom: "ATOM", coinDecimals: 6, coinMinimalDenom: "uatom" }, + new Dec(100).mul(DecUtils.getTenExponentN(6)) + ), + new CoinPretty( + { coinDenom: "OSMO", coinDecimals: 6, coinMinimalDenom: "uosmo" }, + new Dec(200).mul(DecUtils.getTenExponentN(6)) + ), + new CoinPretty( + { coinDenom: "PEPE", coinDecimals: 6, coinMinimalDenom: "upepe" }, + new Dec(200.002).mul(DecUtils.getTenExponentN(6)) + ), + new CoinPretty( + { coinDenom: "USDC", coinDecimals: 6, coinMinimalDenom: "uusdc" }, + new Dec(200.02).mul(DecUtils.getTenExponentN(6)) + ), + ]; + const totalValue = new PricePretty(DEFAULT_VS_CURRENCY, "300"); + + render( + + ); + + expect(screen.getByText("Test Title")).toBeInTheDocument(); + expect(screen.getByText("100 ATOM")).toBeInTheDocument(); + expect(screen.getByText("200 OSMO")).toBeInTheDocument(); + expect(screen.getByText("200.002 PEPE")).toBeInTheDocument(); + expect(screen.getByText("200.02 USDC")).toBeInTheDocument(); + expect(screen.getByText("($300)")).toBeInTheDocument(); + }); + + it("should handle assets with very small decimals", () => { + const assets = [ + new CoinPretty( + { coinDenom: "BTC", coinDecimals: 8, coinMinimalDenom: "sat" }, + new Dec(0.02123451).mul(DecUtils.getTenExponentN(8)) + ), + new CoinPretty( + { coinDenom: "BTC2", coinDecimals: 8, coinMinimalDenom: "sat" }, + new Dec(0.00000001).mul(DecUtils.getTenExponentN(8)) + ), + new CoinPretty( + { coinDenom: "BTC3", coinDecimals: 8, coinMinimalDenom: "sat" }, + new Dec(0.62345678).mul(DecUtils.getTenExponentN(8)) + ), + ]; + const totalValue = new PricePretty(DEFAULT_VS_CURRENCY, "300"); + + render( + + ); + + expect(screen.getByText("0.021234 BTC")).toBeInTheDocument(); + expect(screen.getByText("0.00000001 BTC2")).toBeInTheDocument(); + expect(screen.getByText("0.623456 BTC3")).toBeInTheDocument(); + expect(screen.getByText("($300)")).toBeInTheDocument(); + }); + + it("renders with empty text when no assets", () => { + render( + + ); + + expect(screen.getByText("Test Title")).toBeInTheDocument(); + expect( + screen.getByText("clPositions.checkBackForRewards") + ).toBeInTheDocument(); + }); + + it("renders without total value when not provided", () => { + const assets = [ + new CoinPretty( + { coinDenom: "ATOM", coinDecimals: 6, coinMinimalDenom: "uatom" }, + "100" + ), + ]; + + render( + + ); + + expect(screen.queryByText("(100 USD)")).not.toBeInTheDocument(); + }); +}); diff --git a/packages/web/components/cards/my-position/expanded.tsx b/packages/web/components/cards/my-position/expanded.tsx index 57b24a6c6c..0f4aad7e0c 100644 --- a/packages/web/components/cards/my-position/expanded.tsx +++ b/packages/web/components/cards/my-position/expanded.tsx @@ -444,7 +444,7 @@ const PositionButton: FunctionComponent> = ( ); }; -const AssetsInfo: FunctionComponent< +export const AssetsInfo: FunctionComponent< { title: string; assets?: CoinPretty[]; @@ -479,7 +479,7 @@ const AssetsInfo: FunctionComponent< )}
- {formatPretty(asset, { maxDecimals: 2 })} + {formatPretty(asset, { maxDecimals: 6 })}
))} diff --git a/packages/web/components/nomic/nomic-pending-transfers.tsx b/packages/web/components/nomic/nomic-pending-transfers.tsx index 8a583c2c26..09d2fd5ef2 100644 --- a/packages/web/components/nomic/nomic-pending-transfers.tsx +++ b/packages/web/components/nomic/nomic-pending-transfers.tsx @@ -364,7 +364,9 @@ const TransactionDetailsModal = ({

{t("transfer.nomic.asset")}

-

nBTC

+

+ {depositData.networkFee.amount.denom} +

diff --git a/packages/web/components/table/asset-info.tsx b/packages/web/components/table/asset-info.tsx index ad6a0eceef..45883ce1e3 100644 --- a/packages/web/components/table/asset-info.tsx +++ b/packages/web/components/table/asset-info.tsx @@ -19,6 +19,7 @@ import { useMemo, useState, } from "react"; +import { useMount } from "react-use"; import { HighlightsCategories } from "~/components/assets/highlights-categories"; import { AssetCell } from "~/components/table/cells/asset"; @@ -39,6 +40,7 @@ import { UnverifiedAssetsState } from "~/stores/user-settings"; import { theme } from "~/tailwind.config"; import { formatPretty } from "~/utils/formatter"; import { api, RouterInputs, RouterOutputs } from "~/utils/trpc"; +import { removeQueryParam } from "~/utils/url"; import { AssetCategoriesSelectors } from "../assets/categories"; import { HistoricalPriceSparkline, PriceChange } from "../assets/price"; @@ -66,8 +68,6 @@ export const AssetsInfoTable: FunctionComponent<{ const { t } = useTranslation(); const { logEvent } = useAmplitudeAnalytics(); - // State - // category const [selectedCategory, setCategory] = useState(); const selectCategory = useCallback( @@ -97,6 +97,15 @@ export const AssetsInfoTable: FunctionComponent<{ [selectedCategory] ); + useMount(() => { + const urlParams = new URLSearchParams(window.location.search); + const category = urlParams.get("category"); + if (category) { + setCategory(category); + removeQueryParam("category"); + } + }); + // search const [searchQuery, setSearchQuery] = useState(); const onSearchInput = useCallback((input: string) => { diff --git a/packages/web/config/analytics-events.ts b/packages/web/config/analytics-events.ts index bef4462ef1..24f2f6a510 100644 --- a/packages/web/config/analytics-events.ts +++ b/packages/web/config/analytics-events.ts @@ -82,6 +82,7 @@ export type EventProperties = { section: string; tokenIn: string; tokenOut: string; + token: string; option: string; numberOfValidators: number; validatorNames: string[]; @@ -114,6 +115,7 @@ export const EventName = { swapCompleted: "Swap: Swap completed", swapFailed: "Swap: Swap failed", dropdownAssetSelected: "Swap: Dropdown asset selected", + checkTopGainers: "Swap: Check Top Gainers", }, // Events in Sidebar UI Sidebar: { diff --git a/packages/web/hooks/use-convert-variant.ts b/packages/web/hooks/use-convert-variant.ts index 6697ef1f2c..b6c77bc3fc 100644 --- a/packages/web/hooks/use-convert-variant.ts +++ b/packages/web/hooks/use-convert-variant.ts @@ -126,6 +126,7 @@ export function useConvertVariant( { fromToken: variant.amount.currency.coinDenom, toToken: variant.canonicalAsset.coinDenom, + valueUsd: Number(variant.fiatValue.toDec().toString()), }, ]); resolve(); @@ -140,10 +141,10 @@ export function useConvertVariant( }), [ variant, + account?.address, quote, accountStore, variantTransactionIdentifier, - account?.address, logEvent, ] ); diff --git a/packages/web/hooks/use-feature-flags.ts b/packages/web/hooks/use-feature-flags.ts index 85addd6368..f4a5079ec3 100644 --- a/packages/web/hooks/use-feature-flags.ts +++ b/packages/web/hooks/use-feature-flags.ts @@ -29,6 +29,7 @@ const defaultFlags: Record = { alloyedAssets: false, bridgeDepositAddress: false, nomicWithdrawAmount: false, + swapToolTopGainers: false, }; export function useFeatureFlags() { diff --git a/packages/web/next-env.d.ts b/packages/web/next-env.d.ts index a4a7b3f5cf..4f11a03dc6 100644 --- a/packages/web/next-env.d.ts +++ b/packages/web/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/packages/web/pages/index.tsx b/packages/web/pages/index.tsx index 17be86468c..2547cd34ee 100644 --- a/packages/web/pages/index.tsx +++ b/packages/web/pages/index.tsx @@ -1,14 +1,23 @@ import dayjs from "dayjs"; import { observer } from "mobx-react-lite"; import Image from "next/image"; +import { useRouter } from "next/router"; import { useLocalStorage } from "react-use"; import { AdBanners } from "~/components/ad-banner"; +import { + AssetHighlights, + highlightPrice24hChangeAsset, +} from "~/components/assets/highlights-categories"; import { ClientOnly } from "~/components/client-only"; import { ErrorBoundary } from "~/components/error/error-boundary"; import { TradeTool } from "~/components/trade-tool"; import { EventName } from "~/config"; -import { useAmplitudeAnalytics, useFeatureFlags } from "~/hooks"; +import { + useAmplitudeAnalytics, + useFeatureFlags, + useTranslation, +} from "~/hooks"; import { api } from "~/utils/trpc"; export const SwapPreviousTradeKey = "swap-previous-trade"; @@ -52,6 +61,7 @@ const Home = () => { setPreviousTrade={setPreviousTrade} /> + {featureFlags.swapToolTopGainers && }

@@ -59,6 +69,35 @@ const Home = () => { ); }; +const TopGainers = () => { + const { t } = useTranslation(); + const router = useRouter(); + const { logEvent } = useAmplitudeAnalytics(); + + const { data: topGainerAssets, isLoading: isTopGainerAssetsLoading } = + api.edge.assets.getTopGainerAssets.useQuery({ + topN: 4, + }); + + return ( + { + logEvent([EventName.Swap.checkTopGainers, { token: "All" }]); + router.push(`/assets?category=topGainers`); + }} + onClickAsset={(asset) => { + logEvent([EventName.Swap.checkTopGainers, { token: asset.coinDenom }]); + }} + highlight="topGainers" + /> + ); +}; + const SwapAdsBanner = observer(() => { const { data, isLoading } = api.local.cms.getSwapAdBanners.useQuery( undefined, diff --git a/yarn.lock b/yarn.lock index c6f2313b3d..706aaff503 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17309,10 +17309,10 @@ nodemailer@^6.9.14: resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.14.tgz#845fda981f9fd5ac264f4446af908a7c78027f75" integrity sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA== -nomic-bitcoin@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/nomic-bitcoin/-/nomic-bitcoin-4.2.0.tgz#85794508e200fc14541e6b0b1a4302b3ee147f93" - integrity sha512-ChAkC4JbIfGXWzrGn8Aw+EIJcPzpGhC9cokXOlJhnKz5XGAzuE540kJai6PU+eJPRX1sB8Vg80ac4YjZuZw56Q== +nomic-bitcoin@^5.0.0-pre.0: + version "5.0.0-pre.0" + resolved "https://registry.yarnpkg.com/nomic-bitcoin/-/nomic-bitcoin-5.0.0-pre.0.tgz#e99f798d0ab0fcef83bfefd57bd26f77572f25ac" + integrity sha512-h/WbHEw5tlkfV6LxtFf/BKKUnf64LBY+4DaYtEuNsRTeqvFZbY8KWWb0BCx0b5MngumSmrge+0sGViWkud88nA== dependencies: "@cosmjs/encoding" "^0.31.1" bitcoinjs-lib "^6.1.3"