From a53782c0474bfd7170b8658d18647895c516e911 Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 4 Aug 2024 00:40:25 -0400 Subject: [PATCH 1/8] fix: frontend bug breaking weighted LP (#3667) --- packages/pools/src/weighted.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pools/src/weighted.ts b/packages/pools/src/weighted.ts index c2473bc21f..805fb53aa3 100644 --- a/packages/pools/src/weighted.ts +++ b/packages/pools/src/weighted.ts @@ -133,7 +133,7 @@ export class WeightedPool implements SharePool, RoutablePool { /** LBP pool */ get smoothWeightChange(): SmoothWeightChangeParams | undefined { - if (this.raw.pool_params.smooth_weight_change_params !== null) { + if (this.raw.pool_params.smooth_weight_change_params != null) { const { start_time, duration, From d57d4deb458761e724fc305eb9617059377a0dd0 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Sun, 4 Aug 2024 18:10:35 -0400 Subject: [PATCH 2/8] Deposit/Withdraw fixes (#3662) * improve check for loading * cleanup * catch no route found error in skip * make get timeout height use chain ID --- packages/bridge/src/axelar/index.ts | 2 +- packages/bridge/src/ibc/index.ts | 2 +- packages/bridge/src/interface.ts | 5 ++--- packages/bridge/src/skip/index.ts | 5 +++-- packages/bridge/src/squid/index.ts | 4 +++- .../src/queries/complex/get-timeout-height.ts | 5 +---- packages/utils/src/chain-utils.ts | 4 ++++ .../bridge/use-bridges-supported-assets.ts | 6 +----- packages/web/server/api/routers/bridge-transfer.ts | 14 ++++---------- 9 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/bridge/src/axelar/index.ts b/packages/bridge/src/axelar/index.ts index 512af6bbff..967b2e3299 100644 --- a/packages/bridge/src/axelar/index.ts +++ b/packages/bridge/src/axelar/index.ts @@ -568,7 +568,7 @@ export class AxelarBridgeProvider implements BridgeProvider { }); const timeoutHeight = await this.ctx.getTimeoutHeight({ - destinationAddress: depositAddress, + chainId: toChain.chainId.toString(), }); const ibcAsset = getAssetFromAssetList({ diff --git a/packages/bridge/src/ibc/index.ts b/packages/bridge/src/ibc/index.ts index ccfe2234a0..b11b1b1a70 100644 --- a/packages/bridge/src/ibc/index.ts +++ b/packages/bridge/src/ibc/index.ts @@ -146,7 +146,7 @@ export class IbcBridgeProvider implements BridgeProvider { const { sourceChannel, sourcePort, address } = this.getIbcSource(params); const timeoutHeight = await this.ctx.getTimeoutHeight({ - destinationAddress: params.toAddress, + chainId: params.toChain.chainId.toString(), }); const { typeUrl, value: msg } = cosmosMsgOpts.ibcTransfer.messageComposer({ diff --git a/packages/bridge/src/interface.ts b/packages/bridge/src/interface.ts index c44225388c..9b0d898922 100644 --- a/packages/bridge/src/interface.ts +++ b/packages/bridge/src/interface.ts @@ -12,9 +12,8 @@ export interface BridgeProviderContext { assetLists: AssetList[]; chainList: Chain[]; - /** Provides current timeout height for a chain of the ID - * parsed from the bech32 config of the given destinationAddress. */ - getTimeoutHeight(params: { destinationAddress: string }): Promise<{ + /** Provides current timeout height for a chain of given chainId. */ + getTimeoutHeight(params: { chainId: string }): Promise<{ revisionNumber: string | undefined; revisionHeight: string; }>; diff --git a/packages/bridge/src/skip/index.ts b/packages/bridge/src/skip/index.ts index a92a631afa..40df3b1738 100644 --- a/packages/bridge/src/skip/index.ts +++ b/packages/bridge/src/skip/index.ts @@ -151,7 +151,8 @@ export class SkipBridgeProvider implements BridgeProvider { if ( msg.includes( "no single-tx routes found, to enable multi-tx routes set allow_multi_tx to true" - ) + ) || + msg.includes("no routes found") ) { throw new BridgeQuoteError({ bridgeId: SkipBridgeProvider.ID, @@ -493,7 +494,7 @@ export class SkipBridgeProvider implements BridgeProvider { // is an ibc transfer const timeoutHeight = await this.ctx.getTimeoutHeight({ - destinationAddress: messageData.receiver, + chainId: messageData.chain_id, }); const { typeUrl, value } = cosmosMsgOpts.ibcTransfer.messageComposer({ diff --git a/packages/bridge/src/squid/index.ts b/packages/bridge/src/squid/index.ts index a84abf10e2..e473f1537f 100644 --- a/packages/bridge/src/squid/index.ts +++ b/packages/bridge/src/squid/index.ts @@ -271,6 +271,7 @@ export class SquidBridgeProvider implements BridgeProvider { : await this.createCosmosTransaction( transactionRequest.data, fromAddress, + toChain.chainId.toString(), { denom: fromAsset.address, amount: fromAmount } // TODO: uncomment when we're able to find a way to get gas limit from Squid // or get it ourselves @@ -494,6 +495,7 @@ export class SquidBridgeProvider implements BridgeProvider { async createCosmosTransaction( data: string, fromAddress: string, + toChainId: string, fromCoin: { denom: string; amount: string; @@ -532,7 +534,7 @@ export class SquidBridgeProvider implements BridgeProvider { }; const timeoutHeight = await this.ctx.getTimeoutHeight({ - destinationAddress: ibcData.msg.receiver, + chainId: toChainId, }); const { typeUrl, value: msg } = diff --git a/packages/server/src/queries/complex/get-timeout-height.ts b/packages/server/src/queries/complex/get-timeout-height.ts index 68b55a8560..bee9cf0ef6 100644 --- a/packages/server/src/queries/complex/get-timeout-height.ts +++ b/packages/server/src/queries/complex/get-timeout-height.ts @@ -7,16 +7,13 @@ import { queryRPCStatus } from "../../queries/cosmos"; export async function getTimeoutHeight({ chainList, chainId, - destinationAddress, }: { chainList: Chain[]; - chainId?: string; - destinationAddress?: string; + chainId: string; }) { const destinationCosmosChain = getChain({ chainList, chainId, - destinationAddress, }); if (!destinationCosmosChain) { diff --git a/packages/utils/src/chain-utils.ts b/packages/utils/src/chain-utils.ts index 9760837c2b..8809bb43ef 100644 --- a/packages/utils/src/chain-utils.ts +++ b/packages/utils/src/chain-utils.ts @@ -8,6 +8,10 @@ export function getChain({ }: { chainId?: string; chainName?: string; + /** + * WARNING: bech32 prefix may be the same across different chains, + * retulting in the use of an unintended chain. + */ destinationAddress?: string; chainList: Chain[]; }): Chain | undefined { diff --git a/packages/web/components/bridge/use-bridges-supported-assets.ts b/packages/web/components/bridge/use-bridges-supported-assets.ts index 9c3f8a5481..a0f35fb6f6 100644 --- a/packages/web/components/bridge/use-bridges-supported-assets.ts +++ b/packages/web/components/bridge/use-bridges-supported-assets.ts @@ -72,11 +72,7 @@ export const useBridgesSupportedAssets = ({ ); const isLoading = useMemo( - () => - supportedAssetsResults.some( - (data): data is NonNullable> => - !isNil(data) && data.isLoading - ), + () => supportedAssetsResults.some((data) => isNil(data) || data.isLoading), [supportedAssetsResults] ); diff --git a/packages/web/server/api/routers/bridge-transfer.ts b/packages/web/server/api/routers/bridge-transfer.ts index 224c798911..8b1fb06522 100644 --- a/packages/web/server/api/routers/bridge-transfer.ts +++ b/packages/web/server/api/routers/bridge-transfer.ts @@ -78,8 +78,7 @@ export const bridgeTransferRouter = createTRPCRouter({ ...ctx, env: IS_TESTNET ? "testnet" : "mainnet", cache: lruCache, - getTimeoutHeight: ({ destinationAddress }) => - getTimeoutHeight({ ...ctx, destinationAddress }), + getTimeoutHeight: (params) => getTimeoutHeight({ ...ctx, ...params }), } ); @@ -330,8 +329,7 @@ export const bridgeTransferRouter = createTRPCRouter({ ...ctx, env: IS_TESTNET ? "testnet" : "mainnet", cache: lruCache, - getTimeoutHeight: ({ destinationAddress }) => - getTimeoutHeight({ ...ctx, destinationAddress }), + getTimeoutHeight: (params) => getTimeoutHeight({ ...ctx, ...params }), } ); @@ -455,9 +453,7 @@ export const bridgeTransferRouter = createTRPCRouter({ ...ctx, env: IS_TESTNET ? "testnet" : "mainnet", cache: lruCache, - getTimeoutHeight: ({ destinationAddress }) => - // passes testnet chains if IS_TESTNET - getTimeoutHeight({ ...ctx, destinationAddress }), + getTimeoutHeight: (params) => getTimeoutHeight({ ...ctx, ...params }), } ); @@ -498,9 +494,7 @@ export const bridgeTransferRouter = createTRPCRouter({ ...ctx, env: IS_TESTNET ? "testnet" : "mainnet", cache: lruCache, - getTimeoutHeight: ({ destinationAddress }) => - // passes testnet chains if IS_TESTNET - getTimeoutHeight({ ...ctx, destinationAddress }), + getTimeoutHeight: (params) => getTimeoutHeight({ ...ctx, ...params }), } ); From 01d9d2ccd8c7a893b43e28432999aaece8d10900 Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Mon, 5 Aug 2024 15:38:43 +0200 Subject: [PATCH 3/8] fix: :bug: fix hidden alloyed composition (#3670) --- packages/web/pages/assets/[denom].tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/pages/assets/[denom].tsx b/packages/web/pages/assets/[denom].tsx index 9b397ce9ff..74b6bc16bb 100644 --- a/packages/web/pages/assets/[denom].tsx +++ b/packages/web/pages/assets/[denom].tsx @@ -179,7 +179,7 @@ const AssetInfoView: FunctionComponent = observer( {SwapTool_} - {asset.areTransfersDisabled && asset.contract ? ( + {!asset.areTransfersDisabled && asset.contract ? ( = observer(
{SwapTool_}
- {asset.areTransfersDisabled && asset.contract ? ( + {!asset.areTransfersDisabled && asset.contract ? ( Date: Mon, 5 Aug 2024 11:38:58 -0400 Subject: [PATCH 4/8] (Deposit/Withdraw) Fix Skip timeout height chain ID (#3672) * fix chain ID * fix test --- .../skip/__tests__/skip-bridge-provider.spec.ts | 1 + packages/bridge/src/skip/index.ts | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/bridge/src/skip/__tests__/skip-bridge-provider.spec.ts b/packages/bridge/src/skip/__tests__/skip-bridge-provider.spec.ts index ed8d673339..56c03585b4 100644 --- a/packages/bridge/src/skip/__tests__/skip-bridge-provider.spec.ts +++ b/packages/bridge/src/skip/__tests__/skip-bridge-provider.spec.ts @@ -339,6 +339,7 @@ describe("SkipBridgeProvider", () => { const txRequest = (await provider.createTransaction( "1", + "osmosis-1", "0xabc", messages )) as EvmBridgeTransactionRequest; diff --git a/packages/bridge/src/skip/index.ts b/packages/bridge/src/skip/index.ts index 40df3b1738..68c48901a1 100644 --- a/packages/bridge/src/skip/index.ts +++ b/packages/bridge/src/skip/index.ts @@ -212,6 +212,7 @@ export class SkipBridgeProvider implements BridgeProvider { const transactionRequest = await this.createTransaction( fromChain.chainId.toString(), + toChain.chainId.toString(), fromAddress as Address, msgs ); @@ -439,26 +440,31 @@ export class SkipBridgeProvider implements BridgeProvider { } async createTransaction( - chainID: string, + fromChainId: string, + toChainId: string, address: Address, messages: SkipMsg[] ) { for (const message of messages) { if ("evm_tx" in message) { return await this.createEvmTransaction( - chainID, + fromChainId, address, message.evm_tx ); } if ("multi_chain_msg" in message) { - return await this.createCosmosTransaction(message.multi_chain_msg); + return await this.createCosmosTransaction( + toChainId, + message.multi_chain_msg + ); } } } async createCosmosTransaction( + toChainId: string, message: SkipMultiChainMsg ): Promise { const messageData = JSON.parse(message.msg); @@ -494,7 +500,7 @@ export class SkipBridgeProvider implements BridgeProvider { // is an ibc transfer const timeoutHeight = await this.ctx.getTimeoutHeight({ - chainId: messageData.chain_id, + chainId: toChainId, }); const { typeUrl, value } = cosmosMsgOpts.ibcTransfer.messageComposer({ From c373c768b4c6e5f36835be8ea1f016410f8af2d1 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Mon, 5 Aug 2024 11:48:23 -0400 Subject: [PATCH 5/8] log asset selected from query params (#3675) --- packages/web/components/bridge/immersive-bridge.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/web/components/bridge/immersive-bridge.tsx b/packages/web/components/bridge/immersive-bridge.tsx index 27fc1577eb..39b38816e3 100644 --- a/packages/web/components/bridge/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive-bridge.tsx @@ -66,8 +66,14 @@ export const ImmersiveBridgeFlow = ({ if (!isNil(selectedAssetDenom) && !isVisible) { setIsVisible(true); setStep(ImmersiveBridgeScreen.Amount); + logEvent([ + EventName.DepositWithdraw.assetSelected, + { + tokenName: selectedAssetDenom, + }, + ]); } - }, [direction, selectedAssetDenom, isVisible, setDirection]); + }, [direction, selectedAssetDenom, isVisible, setDirection, logEvent]); const [fiatRampParams, setFiatRampParams] = useState<{ fiatRampKey: FiatRampKey; From a904c950e94ace61cbc4a3f4f0ad43100ae9d743 Mon Sep 17 00:00:00 2001 From: Matt Upham <30577966+mattupham@users.noreply.github.com> Date: Mon, 5 Aug 2024 08:52:01 -0700 Subject: [PATCH 6/8] Portfolio allocation (#3660) * Add getAllocation procedure * add base ui * Render all to frontend * Add assets * add other * Add get assets * Add available * Add temp tabs * add types for allocation * Add tabs * Update classnames * Lint issue * Add ion * Clean up styles * Add open / close * Add address for allocation * Update colors * Fix error * Update types * type 2 * fix lint * Move alloction * Clean up tabs * Update URL * Clean up addresses * Clean up comments * Add translations * Add translations * add pooled * Update useMemo * Fix ion color * Remove cachified * Remove color * Update to fiatValue * Migrate all to RatePretty and PricePretty * Convert to Dec and PricePretty * Refactor with RatePretty and PricePretty * Update * Remove log * Add spec * Update tests * Update tests * Moved to sidecar folder * Update to local router * Clean up * Add neglible percent * Extract color class * Update sort * Update allocation limit * Top coins results * Add allocation limit * Update allocation --- packages/server/jest.config.js | 1 + packages/server/src/queries/complex/index.ts | 1 + .../portfolio/__tests__/allocation.spec.ts | 187 ++++++++++++++++++ .../queries/complex/portfolio/allocation.ts | 157 +++++++++++++++ .../src/queries/complex/portfolio/index.ts | 1 + .../server/src/queries/data-services/index.ts | 1 + .../server/src/queries/sidecar/allocation.ts | 45 +++++ packages/trpc/src/index.ts | 1 + packages/trpc/src/portfolio.ts | 20 ++ .../web/components/complex/portfolio-page.tsx | 18 ++ .../complex/portfolio/allocation-tabs.tsx | 66 +++++++ .../complex/portfolio/allocation.tsx | 135 +++++++++++++ .../web/components/complex/portfolio/types.ts | 1 + .../transactions/transaction-utils.tsx | 3 +- packages/web/localizations/de.json | 11 +- packages/web/localizations/en.json | 11 +- packages/web/localizations/es.json | 11 +- packages/web/localizations/fa.json | 11 +- packages/web/localizations/fr.json | 11 +- packages/web/localizations/gu.json | 11 +- packages/web/localizations/hi.json | 11 +- packages/web/localizations/ja.json | 11 +- packages/web/localizations/ko.json | 11 +- packages/web/localizations/pl.json | 11 +- packages/web/localizations/pt-br.json | 11 +- packages/web/localizations/ro.json | 11 +- packages/web/localizations/ru.json | 11 +- packages/web/localizations/tr.json | 11 +- packages/web/localizations/zh-cn.json | 11 +- packages/web/localizations/zh-hk.json | 11 +- packages/web/localizations/zh-tw.json | 11 +- packages/web/server/api/local-router.ts | 2 + 32 files changed, 807 insertions(+), 19 deletions(-) create mode 100644 packages/server/src/queries/complex/portfolio/__tests__/allocation.spec.ts create mode 100644 packages/server/src/queries/complex/portfolio/allocation.ts create mode 100644 packages/server/src/queries/complex/portfolio/index.ts create mode 100644 packages/server/src/queries/sidecar/allocation.ts create mode 100644 packages/trpc/src/portfolio.ts create mode 100644 packages/web/components/complex/portfolio/allocation-tabs.tsx create mode 100644 packages/web/components/complex/portfolio/allocation.tsx create mode 100644 packages/web/components/complex/portfolio/types.ts diff --git a/packages/server/jest.config.js b/packages/server/jest.config.js index ef54106983..d1bb8b8f71 100644 --- a/packages/server/jest.config.js +++ b/packages/server/jest.config.js @@ -23,4 +23,5 @@ module.exports = { "jest-watch-typeahead/filename", "jest-watch-typeahead/testname", ], + maxWorkers: 1, }; diff --git a/packages/server/src/queries/complex/index.ts b/packages/server/src/queries/complex/index.ts index 3aabb6274c..72dccfe885 100644 --- a/packages/server/src/queries/complex/index.ts +++ b/packages/server/src/queries/complex/index.ts @@ -6,6 +6,7 @@ export * from "./earn"; export * from "./get-timeout-height"; export * from "./osmosis"; export * from "./pools"; +export * from "./portfolio"; export * from "./staking"; export * from "./swap-routers"; export * from "./transactions"; diff --git a/packages/server/src/queries/complex/portfolio/__tests__/allocation.spec.ts b/packages/server/src/queries/complex/portfolio/__tests__/allocation.spec.ts new file mode 100644 index 0000000000..4f061b41a0 --- /dev/null +++ b/packages/server/src/queries/complex/portfolio/__tests__/allocation.spec.ts @@ -0,0 +1,187 @@ +import { AssetLists as assetLists } from "../../../../queries/__tests__/mock-asset-lists"; +import { AllocationResponse } from "../../../sidecar/allocation"; +import { calculatePercentAndFiatValues, getAll } from "../allocation"; + +const MOCK_DATA: AllocationResponse = { + categories: { + "in-locks": { + capitalization: "5.000000000000000000", + is_best_effort: false, + }, + pooled: { + capitalization: "5.000000000000000000", + is_best_effort: false, + }, + staked: { + capitalization: "5.000000000000000000", + is_best_effort: false, + }, + "total-assets": { + capitalization: "60.000000000000000000", + account_coins_result: [ + { + coin: { + denom: "factory/osmo1pfyxruwvtwk00y8z06dh2lqjdj82ldvy74wzm3/WOSMO", + amount: "789", + }, + cap_value: "10.000000000000000000", + }, + { + coin: { + denom: + "factory/osmo1rckme96ptawr4zwexxj5g5gej9s2dmud8r2t9j0k0prn5mch5g4snzzwjv/sail", + amount: "456", + }, + cap_value: "20.000000000000000000", + }, + { + coin: { + denom: + "ibc/7ED954CFFFC06EE8419387F3FC688837FF64EF264DE14219935F724EEEDBF8D3", + amount: "123", + }, + cap_value: "30.000000000000000000", + }, + ], + is_best_effort: false, + }, + "unclaimed-rewards": { + capitalization: "5.000000000000000000", + is_best_effort: false, + }, + unstaking: { + capitalization: "5.000000000000000000", + is_best_effort: false, + }, + "user-balances": { + capitalization: "10.000000000000000000", + account_coins_result: [ + { + coin: { + denom: "factory/osmo1pfyxruwvtwk00y8z06dh2lqjdj82ldvy74wzm3/WOSMO", + amount: "789", + }, + cap_value: "10.000000000000000000", + }, + { + coin: { + denom: + "factory/osmo1rckme96ptawr4zwexxj5g5gej9s2dmud8r2t9j0k0prn5mch5g4snzzwjv/sail", + amount: "456", + }, + cap_value: "20.000000000000000000", + }, + ], + is_best_effort: false, + }, + }, +}; + +describe("Allocation Functions", () => { + describe("getAll", () => { + it("should calculate the correct allocation percentages and fiat values", () => { + const result = getAll(MOCK_DATA.categories).map((allocation) => ({ + ...allocation, + percentage: allocation.percentage.toString(), + fiatValue: allocation.fiatValue.toString(), + })); + + expect(result).toEqual([ + { + key: "available", + percentage: "33.333%", + fiatValue: "$10", + }, + { + key: "staked", + percentage: "16.666%", + fiatValue: "$5", + }, + { + key: "unstaking", + percentage: "16.666%", + fiatValue: "$5", + }, + { + key: "unclaimedRewards", + percentage: "16.666%", + fiatValue: "$5", + }, + { + key: "pooled", + percentage: "16.666%", + fiatValue: "$5", + }, + ]); + }); + }); + + describe("calculatePercentAndFiatValues", () => { + it("should calculate the correct asset percentages and fiat values", async () => { + const result = await calculatePercentAndFiatValues( + MOCK_DATA.categories, + assetLists, + "total-assets", + 5 + ).map((allocation) => ({ + ...allocation, + percentage: allocation.percentage.toString(), + fiatValue: allocation.fiatValue.toString(), + })); + + expect(result).toEqual([ + { + key: "CTK", + percentage: "50%", + fiatValue: "$30", + }, + { + key: "SAIL", + percentage: "33.333%", + fiatValue: "$20", + }, + { + key: "WOSMO", + percentage: "16.666%", + fiatValue: "$10", + }, + { + key: "Other", + percentage: "0%", + fiatValue: "$0", + }, + ]); + }); + + it("should calculate the correct asset percentages and fiat values", async () => { + const result = await calculatePercentAndFiatValues( + MOCK_DATA.categories, + assetLists, + "user-balances", + 5 + ).map((allocation) => ({ + ...allocation, + percentage: allocation.percentage.toString(), + fiatValue: allocation.fiatValue.toString(), + })); + + expect(result).toEqual([ + { + key: "SAIL", + percentage: "200%", + fiatValue: "$20", + }, + { + key: "WOSMO", + percentage: "100%", + fiatValue: "$10", + }, + { + key: "Other", + percentage: "0%", + fiatValue: "$0", + }, + ]); + }); + }); +}); diff --git a/packages/server/src/queries/complex/portfolio/allocation.ts b/packages/server/src/queries/complex/portfolio/allocation.ts new file mode 100644 index 0000000000..34211ed718 --- /dev/null +++ b/packages/server/src/queries/complex/portfolio/allocation.ts @@ -0,0 +1,157 @@ +import { CoinPretty, PricePretty } from "@keplr-wallet/unit"; +import { Dec, RatePretty } from "@keplr-wallet/unit"; +import { AssetList } from "@osmosis-labs/types"; +import { sort } from "@osmosis-labs/utils"; + +import { DEFAULT_VS_CURRENCY } from "../../../queries/complex/assets/config"; +import { queryAllocation } from "../../../queries/data-services"; +import { Categories } from "../../../queries/data-services"; +import { AccountCoinsResult } from "../../../queries/data-services"; +import { getAsset } from "../assets"; + +interface FormattedAllocation { + key: string; + percentage: RatePretty; + fiatValue: PricePretty; + asset?: CoinPretty; +} + +export interface GetAllocationResponse { + all: FormattedAllocation[]; + assets: FormattedAllocation[]; + available: FormattedAllocation[]; +} + +export function getAll(categories: Categories): FormattedAllocation[] { + const userBalancesCap = new Dec(categories["user-balances"].capitalization); + const stakedCap = new Dec(categories["staked"].capitalization); + const unstakingCap = new Dec(categories["unstaking"].capitalization); + const unclaimedRewardsCap = new Dec( + categories["unclaimed-rewards"].capitalization + ); + const pooledCap = new Dec(categories["pooled"].capitalization); + + const totalCap = userBalancesCap + .add(stakedCap) + .add(unstakingCap) + .add(unclaimedRewardsCap) + .add(pooledCap); + + return [ + { + key: "available", + percentage: new RatePretty(userBalancesCap.quo(totalCap)), + fiatValue: new PricePretty(DEFAULT_VS_CURRENCY, userBalancesCap), + }, + { + key: "staked", + percentage: new RatePretty(stakedCap.quo(totalCap)), + fiatValue: new PricePretty(DEFAULT_VS_CURRENCY, stakedCap), + }, + { + key: "unstaking", + percentage: new RatePretty(unstakingCap.quo(totalCap)), + fiatValue: new PricePretty(DEFAULT_VS_CURRENCY, unstakingCap), + }, + { + key: "unclaimedRewards", + percentage: new RatePretty(unclaimedRewardsCap.quo(totalCap)), + fiatValue: new PricePretty(DEFAULT_VS_CURRENCY, unclaimedRewardsCap), + }, + { + key: "pooled", + percentage: new RatePretty(pooledCap.quo(totalCap)), + fiatValue: new PricePretty(DEFAULT_VS_CURRENCY, pooledCap), + }, + ]; +} + +export function calculatePercentAndFiatValues( + categories: Categories, + assetLists: AssetList[], + category: "total-assets" | "user-balances", + allocationLimit +) { + const totalAssets = categories[category]; + const totalCap = new Dec(totalAssets.capitalization); + + const sortedAccountCoinsResults = sort( + totalAssets?.account_coins_result || [], + "cap_value", + "asc" + ); + + const topCoinsResults = sortedAccountCoinsResults.slice(0, allocationLimit); + + const assets: FormattedAllocation[] = topCoinsResults.map( + (asset: AccountCoinsResult) => { + const assetFromAssetLists = getAsset({ + assetLists, + anyDenom: asset.coin.denom, + }); + + return { + key: assetFromAssetLists.coinDenom, + percentage: new RatePretty(new Dec(asset.cap_value).quo(totalCap)), + fiatValue: new PricePretty( + DEFAULT_VS_CURRENCY, + new Dec(asset.cap_value) + ), + }; + } + ); + + const otherAssets = sortedAccountCoinsResults.slice(allocationLimit); + + const otherAmount = otherAssets.reduce( + (sum: Dec, asset: AccountCoinsResult) => sum.add(new Dec(asset.cap_value)), + new Dec(0) + ); + + const otherPercentage = new RatePretty(otherAmount).quo(totalCap); + + const other: FormattedAllocation = { + key: "Other", + percentage: otherPercentage, + fiatValue: new PricePretty(DEFAULT_VS_CURRENCY, otherAmount), + }; + + return [...assets, other]; +} + +export async function getAllocation({ + address, + assetLists, + allocationLimit = 5, +}: { + address: string; + assetLists: AssetList[]; + allocationLimit?: number; +}): Promise { + const data = await queryAllocation({ + address, + }); + + const categories = data.categories; + + const all = getAll(categories); + const assets = calculatePercentAndFiatValues( + categories, + assetLists, + "total-assets", + allocationLimit + ); + + const available = calculatePercentAndFiatValues( + categories, + assetLists, + "user-balances", + allocationLimit + ); + + return { + all, + assets, + available, + }; +} diff --git a/packages/server/src/queries/complex/portfolio/index.ts b/packages/server/src/queries/complex/portfolio/index.ts new file mode 100644 index 0000000000..8c741c990c --- /dev/null +++ b/packages/server/src/queries/complex/portfolio/index.ts @@ -0,0 +1 @@ +export * from "./allocation"; diff --git a/packages/server/src/queries/data-services/index.ts b/packages/server/src/queries/data-services/index.ts index d929431f5d..31295a31bf 100644 --- a/packages/server/src/queries/data-services/index.ts +++ b/packages/server/src/queries/data-services/index.ts @@ -1,3 +1,4 @@ +export * from "../sidecar/allocation"; export * from "./earn"; export * from "./filtered-pools"; export * from "./market-cap"; diff --git a/packages/server/src/queries/sidecar/allocation.ts b/packages/server/src/queries/sidecar/allocation.ts new file mode 100644 index 0000000000..5fae475503 --- /dev/null +++ b/packages/server/src/queries/sidecar/allocation.ts @@ -0,0 +1,45 @@ +import { apiClient } from "@osmosis-labs/utils"; + +import { SIDECAR_BASE_URL } from "../../env"; + +interface Coin { + denom: string; + amount: string; +} + +export interface AccountCoinsResult { + coin: Coin; + cap_value: string; +} +export interface Category { + capitalization: string; + is_best_effort: boolean; + account_coins_result?: AccountCoinsResult[]; +} + +export interface Categories { + "in-locks": Category; + pooled: Category; + staked: Category; + "total-assets": Category; + "unclaimed-rewards": Category; + unstaking: Category; + "user-balances": Category; +} + +export interface AllocationResponse { + categories: Categories; +} + +export async function queryAllocation({ + address, +}: { + address: string; +}): Promise { + const url = new URL( + `passthrough/portfolio-assets/${address}`, + SIDECAR_BASE_URL + ); + + return apiClient(url.toString()); +} diff --git a/packages/trpc/src/index.ts b/packages/trpc/src/index.ts index d556a04d45..8b8e389bbe 100644 --- a/packages/trpc/src/index.ts +++ b/packages/trpc/src/index.ts @@ -10,6 +10,7 @@ export * from "./one-click-trading"; export * from "./parameter-types"; export * from "./params"; export * from "./pools"; +export * from "./portfolio"; export * from "./staking"; export * from "./swap"; export * from "./transactions"; diff --git a/packages/trpc/src/portfolio.ts b/packages/trpc/src/portfolio.ts new file mode 100644 index 0000000000..b07f9125fe --- /dev/null +++ b/packages/trpc/src/portfolio.ts @@ -0,0 +1,20 @@ +import { getAllocation } from "@osmosis-labs/server"; +import { z } from "zod"; + +import { createTRPCRouter, publicProcedure } from "./api"; + +export const portfolioRouter = createTRPCRouter({ + getAllocation: publicProcedure + .input( + z.object({ + address: z.string(), + }) + ) + .query(async ({ input: { address }, ctx }) => { + const res = await getAllocation({ + address, + assetLists: ctx.assetLists, + }); + return res; + }), +}); diff --git a/packages/web/components/complex/portfolio-page.tsx b/packages/web/components/complex/portfolio-page.tsx index 431d2d8ab4..2a94fef07b 100644 --- a/packages/web/components/complex/portfolio-page.tsx +++ b/packages/web/components/complex/portfolio-page.tsx @@ -7,6 +7,7 @@ import { useRouter } from "next/router"; import { FunctionComponent, useCallback } from "react"; import { Icon } from "~/components/assets"; +import { Allocation } from "~/components/complex/portfolio/allocation"; import { AssetBalancesTable } from "~/components/table/asset-balances"; import { useDimension, @@ -51,6 +52,17 @@ export const PortfolioPage: FunctionComponent = () => { }, } ); + + const { data: allocation, isLoading: isLoadingAllocation } = + api.local.portfolio.getAllocation.useQuery( + { + address: wallet?.address ?? "", + }, + { + enabled: Boolean(wallet?.isWalletConnected && wallet?.address), + } + ); + const userHasNoAssets = totalValue && totalValue.toDec().isZero(); const [overviewRef, { height: overviewHeight }] = @@ -137,6 +149,11 @@ export const PortfolioPage: FunctionComponent = () => { )} +
+ {!isLoadingAllocation && !userHasNoAssets && ( + + )} +
); }; @@ -374,6 +391,7 @@ function useUserPositionsData(address: string | undefined) { }, } ); + const hasPositions = Boolean(positions?.length); const { data: allMyPoolDetails, isLoading: isLoadingMyPoolDetails } = diff --git a/packages/web/components/complex/portfolio/allocation-tabs.tsx b/packages/web/components/complex/portfolio/allocation-tabs.tsx new file mode 100644 index 0000000000..664d4ab9fd --- /dev/null +++ b/packages/web/components/complex/portfolio/allocation-tabs.tsx @@ -0,0 +1,66 @@ +import classNames from "classnames"; +import { FunctionComponent, useMemo } from "react"; + +import { useTranslation } from "~/hooks"; + +import { AllocationOptions } from "./types"; +export interface AllocationTabProps { + setTab: (tab: AllocationOptions) => void; + activeTab: AllocationOptions; +} + +export const AllocationTabs: FunctionComponent = ({ + setTab, + activeTab, +}) => { + const { t } = useTranslation(); + + const tabs = useMemo( + () => + [ + { + label: t("portfolio.all"), + value: "all", + }, + { + label: t("portfolio.assets"), + value: "assets", + }, + { + label: t("portfolio.available"), + value: "available", + }, + ] as { label: string; value: AllocationOptions }[], + [t] + ); + + return ( +
+ {tabs.map((tab) => { + const isActive = activeTab === tab.value; + return ( + + ); + })} +
+ ); +}; diff --git a/packages/web/components/complex/portfolio/allocation.tsx b/packages/web/components/complex/portfolio/allocation.tsx new file mode 100644 index 0000000000..44932418c6 --- /dev/null +++ b/packages/web/components/complex/portfolio/allocation.tsx @@ -0,0 +1,135 @@ +import { Dec } from "@keplr-wallet/unit"; +import { GetAllocationResponse } from "@osmosis-labs/server"; +import classNames from "classnames"; +import { FunctionComponent, useState } from "react"; + +import { Icon } from "~/components/assets"; +import { AllocationTabs } from "~/components/complex/portfolio/allocation-tabs"; +import { AllocationOptions } from "~/components/complex/portfolio/types"; +import { displayFiatPrice } from "~/components/transactions/transaction-utils"; +import { MultiLanguageT } from "~/hooks"; +import { useTranslation } from "~/hooks"; + +const COLORS: Record = { + all: [ + "bg-wosmongton-500", + "bg-ammelia-400", + "bg-osmoverse-500", + "bg-bullish-500", + "bg-ion-500", + ], + assets: [ + "bg-[#9C01D4]", + "bg-[#E9983D]", + "bg-[#2775CA]", + "bg-[#424667]", + "bg-[#009393]", + "bg-osmoverse-500", + ], + available: [ + "bg-[#9C01D4]", + "bg-[#E9983D]", + "bg-[#2775CA]", + "bg-[#424667]", + "bg-[#009393]", + "bg-osmoverse-500", + ], +}; + +const getTranslation = (key: string, t: MultiLanguageT): string => { + const translationMap: Record = { + available: t("portfolio.available"), + staked: t("portfolio.staked"), + unstaking: t("portfolio.unstaking"), + unclaimedRewards: t("portfolio.unclaimedRewards"), + pooled: t("portfolio.pooled"), + other: t("portfolio.other"), + }; + + return translationMap[key] || key; +}; + +export const Allocation: FunctionComponent<{ + allocation?: GetAllocationResponse; +}> = ({ allocation }) => { + const [selectedOption, setSelectedOption] = + useState("all"); + + const [isOpen, setIsOpen] = useState(true); + + const { t } = useTranslation(); + + if (!allocation) return null; + + const selectedList = allocation[selectedOption]; + + return ( +
+
setIsOpen(!isOpen)} + > +
{t("portfolio.allocation")}
+ +
+ {isOpen && ( + <> +
+ setSelectedOption(option)} + activeTab={selectedOption} + /> +
+
+ {selectedList.map(({ key, percentage }, index) => { + const isNegligiblePercent = percentage.toDec().lt(new Dec(0.01)); + + const width = isNegligiblePercent + ? "0.1%" + : percentage.toString(); + + const colorClass = + COLORS[selectedOption][index % COLORS[selectedOption].length]; + + return ( +
+ ); + })} +
+
+ {selectedList.map(({ key, percentage, fiatValue }, index) => { + const colorClass = + COLORS[selectedOption][index % COLORS[selectedOption].length]; + return ( +
+
+
+ {getTranslation(key, t)} + + {percentage.maxDecimals(0).toString()} + +
+
{displayFiatPrice(fiatValue, "", t)}
+
+ ); + })} +
+ + )} +
+ ); +}; diff --git a/packages/web/components/complex/portfolio/types.ts b/packages/web/components/complex/portfolio/types.ts new file mode 100644 index 0000000000..59f3d7b919 --- /dev/null +++ b/packages/web/components/complex/portfolio/types.ts @@ -0,0 +1 @@ +export type AllocationOptions = "all" | "assets" | "available"; diff --git a/packages/web/components/transactions/transaction-utils.tsx b/packages/web/components/transactions/transaction-utils.tsx index a53da6fda8..929ab8a4c0 100644 --- a/packages/web/components/transactions/transaction-utils.tsx +++ b/packages/web/components/transactions/transaction-utils.tsx @@ -53,8 +53,7 @@ export const displayFiatPrice = ( prefix: "-" | "+" | "", t: MultiLanguageT ): string => { - if (value === undefined || value?.toDec().equals(new Dec(0))) - return t("transactions.noPriceData"); + if (value === undefined) return t("transactions.noPriceData"); const decValue = value.toDec(); const symbol = value.symbol; diff --git a/packages/web/localizations/de.json b/packages/web/localizations/de.json index 582a6d5849..d5c07b8afb 100644 --- a/packages/web/localizations/de.json +++ b/packages/web/localizations/de.json @@ -325,7 +325,16 @@ "noAssets": "Sie haben keine Guthaben auf {osmosis}", "getStarted": "Zahlen Sie Guthaben ein, um loszulegen", "noPositions": "Sie haben keine Positionen", - "unlockPotential": "Schöpfen Sie Ihr volles Ertragspotenzial aus und nutzen Sie Ihr Vermögen mit Liquiditätspools" + "unlockPotential": "Schöpfen Sie Ihr volles Ertragspotenzial aus und nutzen Sie Ihr Vermögen mit Liquiditätspools", + "allocation": "Zuweisung", + "available": "Verfügbar", + "staked": "Abgesteckt", + "unstaking": "Aufheben der Absteckung", + "unclaimedRewards": "Nicht beanspruchte Belohnungen", + "pooled": "Gepoolt", + "other": "Andere", + "all": "Alle", + "assets": "Vermögenswerte" }, "buyTokens": "Kaufen Sie Token", "components": { diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index add7d050c2..adb56301e1 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -325,7 +325,16 @@ "noAssets": "You have no balances on {osmosis}", "getStarted": "Deposit assets to get started", "noPositions": "You have no positions", - "unlockPotential": "Unlock your full earning potential and put your assets to work with liquidity pools" + "unlockPotential": "Unlock your full earning potential and put your assets to work with liquidity pools", + "allocation": "Allocation", + "available": "Available", + "staked": "Staked", + "unstaking": "Unstaking", + "unclaimedRewards": "Unclaimed rewards", + "pooled": "Pooled", + "other": "Other", + "all": "All", + "assets": "Assets" }, "buyTokens": "Buy tokens", "components": { diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index c9cb860718..c36b8b25ee 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -325,7 +325,16 @@ "noAssets": "No tienes saldos en {osmosis}", "getStarted": "Deposite activos para comenzar", "noPositions": "no tienes posiciones", - "unlockPotential": "Libere todo su potencial de ganancias y ponga sus activos a trabajar con fondos de liquidez" + "unlockPotential": "Libere todo su potencial de ganancias y ponga sus activos a trabajar con fondos de liquidez", + "allocation": "Asignación", + "available": "Disponible", + "staked": "apostado", + "unstaking": "Desestacar", + "unclaimedRewards": "Recompensas no reclamadas", + "pooled": "agrupado", + "other": "Otro", + "all": "Todo", + "assets": "Activos" }, "buyTokens": "Comprar fichas", "components": { diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index 7d4c8e8131..b3ff03005c 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -325,7 +325,16 @@ "noAssets": "هیچ موجودی در {osmosis} ندارید", "getStarted": "برای شروع، دارایی ها را سپرده گذاری کنید", "noPositions": "شما هیچ مقامی ندارید", - "unlockPotential": "پتانسیل درآمد کامل خود را باز کنید و دارایی های خود را با استخرهای نقدینگی کار کنید" + "unlockPotential": "پتانسیل درآمد کامل خود را باز کنید و دارایی های خود را با استخرهای نقدینگی کار کنید", + "allocation": "تخصیص", + "available": "در دسترس", + "staked": "شرط بندی شده است", + "unstaking": "رها کردن", + "unclaimedRewards": "پاداش های بی ادعا", + "pooled": "جمع شده", + "other": "دیگر", + "all": "همه", + "assets": "دارایی های" }, "buyTokens": "خرید توکن", "components": { diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index 2dfc9009d7..4a93f9bc7e 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -325,7 +325,16 @@ "noAssets": "Vous n'avez aucun solde sur {osmosis}", "getStarted": "Déposez des actifs pour commencer", "noPositions": "Vous n'avez aucun poste", - "unlockPotential": "Libérez tout votre potentiel de revenus et faites fructifier vos actifs grâce aux pools de liquidités" + "unlockPotential": "Libérez tout votre potentiel de revenus et faites fructifier vos actifs grâce aux pools de liquidités", + "allocation": "Allocation", + "available": "Disponible", + "staked": "Jalonné", + "unstaking": "Détachement", + "unclaimedRewards": "Récompenses non réclamées", + "pooled": "Regroupé", + "other": "Autre", + "all": "Tous", + "assets": "Actifs" }, "buyTokens": "Acheter jetons", "components": { diff --git a/packages/web/localizations/gu.json b/packages/web/localizations/gu.json index 4111e421e1..7e1b52ba9a 100644 --- a/packages/web/localizations/gu.json +++ b/packages/web/localizations/gu.json @@ -325,7 +325,16 @@ "noAssets": "તમારી પાસે {osmosis} પર કોઈ બેલેન્સ નથી", "getStarted": "પ્રારંભ કરવા માટે અસ્કયામતો જમા કરો", "noPositions": "તમારી પાસે કોઈ હોદ્દા નથી", - "unlockPotential": "તમારી સંપૂર્ણ કમાણીની સંભાવનાને અનલૉક કરો અને તમારી સંપત્તિઓને લિક્વિડિટી પૂલ સાથે કામ કરવા માટે મૂકો" + "unlockPotential": "તમારી સંપૂર્ણ કમાણીની સંભાવનાને અનલૉક કરો અને તમારી સંપત્તિઓને લિક્વિડિટી પૂલ સાથે કામ કરવા માટે મૂકો", + "allocation": "ફાળવણી", + "available": "ઉપલબ્ધ છે", + "staked": "દાવ પર લગાવ્યો", + "unstaking": "અનસ્ટેકિંગ", + "unclaimedRewards": "દાવા વગરના પુરસ્કારો", + "pooled": "પૂલ", + "other": "અન્ય", + "all": "બધા", + "assets": "અસ્કયામતો" }, "buyTokens": "ટોકન્સ ખરીદો", "components": { diff --git a/packages/web/localizations/hi.json b/packages/web/localizations/hi.json index b84d183055..729d99d699 100644 --- a/packages/web/localizations/hi.json +++ b/packages/web/localizations/hi.json @@ -325,7 +325,16 @@ "noAssets": "आपके पास {osmosis} पर कोई शेष राशि नहीं है", "getStarted": "आरंभ करने के लिए संपत्ति जमा करें", "noPositions": "आपके पास कोई पद नहीं है", - "unlockPotential": "अपनी पूरी कमाई क्षमता को अनलॉक करें और अपनी परिसंपत्तियों को तरलता पूल के साथ काम में लगाएं" + "unlockPotential": "अपनी पूरी कमाई क्षमता को अनलॉक करें और अपनी परिसंपत्तियों को तरलता पूल के साथ काम में लगाएं", + "allocation": "आवंटन", + "available": "उपलब्ध", + "staked": "दांव पर लगा दिया", + "unstaking": "अनस्टेकिंग", + "unclaimedRewards": "दावा न किए गए पुरस्कार", + "pooled": "जमा", + "other": "अन्य", + "all": "सभी", + "assets": "संपत्ति" }, "buyTokens": "टोकन खरीदें", "components": { diff --git a/packages/web/localizations/ja.json b/packages/web/localizations/ja.json index d2756372f5..8c2e6b07dc 100644 --- a/packages/web/localizations/ja.json +++ b/packages/web/localizations/ja.json @@ -325,7 +325,16 @@ "noAssets": "{osmosis}に残高がありません", "getStarted": "始めるには資産を預けてください", "noPositions": "ポジションがありません", - "unlockPotential": "流動性プールを活用して収益の可能性を最大限に引き出し、資産を有効活用しましょう" + "unlockPotential": "流動性プールを活用して収益の可能性を最大限に引き出し、資産を有効活用しましょう", + "allocation": "割り当て", + "available": "利用可能", + "staked": "賭けられた", + "unstaking": "ステーキング解除", + "unclaimedRewards": "未請求の報酬", + "pooled": "プール", + "other": "他の", + "all": "全て", + "assets": "資産" }, "buyTokens": "トークンを購入する", "components": { diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index d668903097..9453f32e53 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -325,7 +325,16 @@ "noAssets": "{osmosis} 에 잔액이 없습니다.", "getStarted": "시작하려면 자산을 예치하세요", "noPositions": "직책이 없습니다.", - "unlockPotential": "귀하의 수익 잠재력을 최대한 활용하고 자산을 유동성 풀과 함께 활용하세요" + "unlockPotential": "귀하의 수익 잠재력을 최대한 활용하고 자산을 유동성 풀과 함께 활용하세요", + "allocation": "배당", + "available": "사용 가능", + "staked": "스테이킹됨", + "unstaking": "언스테이킹", + "unclaimedRewards": "청구하지 않은 보상", + "pooled": "풀링됨", + "other": "다른", + "all": "모두", + "assets": "자산" }, "buyTokens": "토큰 구매", "components": { diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index 30dc23a193..d791d0fa8a 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -325,7 +325,16 @@ "noAssets": "Nie masz salda na {osmosis}", "getStarted": "Aby rozpocząć, zdeponuj środki", "noPositions": "Nie masz żadnych stanowisk", - "unlockPotential": "Odblokuj swój pełny potencjał zarobkowy i wykorzystaj swoje aktywa dzięki pulom płynności" + "unlockPotential": "Odblokuj swój pełny potencjał zarobkowy i wykorzystaj swoje aktywa dzięki pulom płynności", + "allocation": "Przydział", + "available": "Dostępny", + "staked": "Postawione", + "unstaking": "Odstawienie", + "unclaimedRewards": "Nieodebrane nagrody", + "pooled": "Połączone", + "other": "Inny", + "all": "Wszystko", + "assets": "Aktywa" }, "buyTokens": "Kup tokeny", "components": { diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index 50d40248c0..00901a42e9 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -325,7 +325,16 @@ "noAssets": "Você não tem saldos em {osmosis}", "getStarted": "Deposite ativos para começar", "noPositions": "Você não tem posições", - "unlockPotential": "Desbloqueie todo o seu potencial de ganhos e coloque seus ativos para trabalhar com pools de liquidez" + "unlockPotential": "Desbloqueie todo o seu potencial de ganhos e coloque seus ativos para trabalhar com pools de liquidez", + "allocation": "Alocação", + "available": "Disponível", + "staked": "Apostado", + "unstaking": "Desempilhar", + "unclaimedRewards": "Recompensas não reclamadas", + "pooled": "Agrupados", + "other": "Outro", + "all": "Todos", + "assets": "Ativos" }, "buyTokens": "Comprar tokens", "components": { diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index 79e4a4def6..8239cf8e62 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -325,7 +325,16 @@ "noAssets": "Nu aveți solduri pe {osmosis}", "getStarted": "Depuneți active pentru a începe", "noPositions": "Nu ai posturi", - "unlockPotential": "Deblocați-vă întregul potențial de câștig și puneți-vă activele în funcțiune cu fonduri de lichiditate" + "unlockPotential": "Deblocați-vă întregul potențial de câștig și puneți-vă activele în funcțiune cu fonduri de lichiditate", + "allocation": "Alocare", + "available": "Disponibil", + "staked": "Mizat", + "unstaking": "Dezlegarea", + "unclaimedRewards": "Recompense nerevendicate", + "pooled": "Adunate", + "other": "Alte", + "all": "Toate", + "assets": "Active" }, "buyTokens": "Cumpărați jetoane", "components": { diff --git a/packages/web/localizations/ru.json b/packages/web/localizations/ru.json index ca9e3a7d71..d38464415d 100644 --- a/packages/web/localizations/ru.json +++ b/packages/web/localizations/ru.json @@ -325,7 +325,16 @@ "noAssets": "У вас нет остатков по {osmosis}", "getStarted": "Внесите активы, чтобы начать", "noPositions": "У вас нет должностей", - "unlockPotential": "Раскройте весь свой потенциал заработка и заставьте свои активы работать с пулами ликвидности" + "unlockPotential": "Раскройте весь свой потенциал заработка и заставьте свои активы работать с пулами ликвидности", + "allocation": "Распределение", + "available": "Доступный", + "staked": "Ставка", + "unstaking": "Снять ставку", + "unclaimedRewards": "Невостребованные награды", + "pooled": "Объединенный", + "other": "Другой", + "all": "Все", + "assets": "Ресурсы" }, "buyTokens": "Купить токены", "components": { diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index ffe40237cc..c651f1ec76 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -325,7 +325,16 @@ "noAssets": "{osmosis} üzerinde bakiyeniz yok", "getStarted": "Başlamak için varlık yatırın", "noPositions": "Hiç pozisyonunuz yok", - "unlockPotential": "Tüm kazanç potansiyelinizi ortaya çıkarın ve varlıklarınızı likidite havuzlarıyla çalıştırın" + "unlockPotential": "Tüm kazanç potansiyelinizi ortaya çıkarın ve varlıklarınızı likidite havuzlarıyla çalıştırın", + "allocation": "Paylaştırma", + "available": "Mevcut", + "staked": "Bahis", + "unstaking": "Stake kaldırma", + "unclaimedRewards": "Talep edilmeyen ödüller", + "pooled": "havuzlanmış", + "other": "Diğer", + "all": "Tüm", + "assets": "Varlıklar" }, "buyTokens": "jeton satın al", "components": { diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index e31c0d5a83..2c191fc338 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -325,7 +325,16 @@ "noAssets": "您在{osmosis}上没有余额", "getStarted": "存入资产即可开始", "noPositions": "您没有职位", - "unlockPotential": "释放您的全部盈利潜力,利用流动资金池来运作您的资产" + "unlockPotential": "释放您的全部盈利潜力,利用流动资金池来运作您的资产", + "allocation": "分配", + "available": "可用的", + "staked": "已质押", + "unstaking": "取消质押", + "unclaimedRewards": "未领取的奖励", + "pooled": "池化", + "other": "其他", + "all": "全部", + "assets": "资产" }, "buyTokens": "购买代币", "components": { diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index 1b75ccd5a2..be21c92f08 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -325,7 +325,16 @@ "noAssets": "您{osmosis}沒有餘額", "getStarted": "存入資產即可開始", "noPositions": "您沒有職位", - "unlockPotential": "釋放您的全部盈利潛力,並將您的資產與流動性池結合起來" + "unlockPotential": "釋放您的全部盈利潛力,並將您的資產與流動性池結合起來", + "allocation": "分配", + "available": "可用的", + "staked": "質押", + "unstaking": "取消質押", + "unclaimedRewards": "無人認領的獎勵", + "pooled": "匯集", + "other": "其他", + "all": "全部", + "assets": "資產" }, "buyTokens": "購買代幣", "components": { diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index 42b510f1d9..409d4ce9e3 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -325,7 +325,16 @@ "noAssets": "您{osmosis}沒有餘額", "getStarted": "存入資產即可開始", "noPositions": "您沒有職位", - "unlockPotential": "釋放您的全部盈利潛力,並將您的資產與流動性池結合起來" + "unlockPotential": "釋放您的全部盈利潛力,並將您的資產與流動性池結合起來", + "allocation": "分配", + "available": "可用的", + "staked": "質押", + "unstaking": "取消質押", + "unclaimedRewards": "無人認領的獎勵", + "pooled": "匯集", + "other": "其他", + "all": "全部", + "assets": "資產" }, "buyTokens": "購買代幣", "components": { diff --git a/packages/web/server/api/local-router.ts b/packages/web/server/api/local-router.ts index c978947a59..ab067d113b 100644 --- a/packages/web/server/api/local-router.ts +++ b/packages/web/server/api/local-router.ts @@ -4,6 +4,7 @@ import { concentratedLiquidityRouter, createTRPCRouter, oneClickTradingRouter, + portfolioRouter, swapRouter, } from "@osmosis-labs/trpc"; @@ -19,4 +20,5 @@ export const localRouter = createTRPCRouter({ oneClickTrading: oneClickTradingRouter, cms: cmsRouter, bridgeTransfer: localBridgeTransferRouter, + portfolio: portfolioRouter, }); From 580ffdc0bbcf021b74c83012e7cb3ec72a31eea0 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Mon, 5 Aug 2024 12:45:28 -0400 Subject: [PATCH 7/8] Jonator/fixes (#3676) * fix param * add invariant --- .../server/src/queries/complex/portfolio/allocation.ts | 2 +- packages/web/server/api/routers/bridge-transfer.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/server/src/queries/complex/portfolio/allocation.ts b/packages/server/src/queries/complex/portfolio/allocation.ts index 34211ed718..662000c5c7 100644 --- a/packages/server/src/queries/complex/portfolio/allocation.ts +++ b/packages/server/src/queries/complex/portfolio/allocation.ts @@ -70,7 +70,7 @@ export function calculatePercentAndFiatValues( categories: Categories, assetLists: AssetList[], category: "total-assets" | "user-balances", - allocationLimit + allocationLimit = 5 ) { const totalAssets = categories[category]; const totalCap = new Dec(totalAssets.capitalization); diff --git a/packages/web/server/api/routers/bridge-transfer.ts b/packages/web/server/api/routers/bridge-transfer.ts index 8b1fb06522..752f7f3f9d 100644 --- a/packages/web/server/api/routers/bridge-transfer.ts +++ b/packages/web/server/api/routers/bridge-transfer.ts @@ -96,6 +96,14 @@ export const bridgeTransferRouter = createTRPCRouter({ /** If the bridge takes longer than 10 seconds to respond, we should timeout that quote. */ const quote = await timeout(quoteFn, 10 * 1000)(); + // Basic circuit breaker to validate some invariants + // from input + given quote + if (input.fromAsset.address !== quote.input.address) { + throw new Error( + `Invalid quote: Expected fromAsset address ${input.fromAsset.address} but got ${quote.input.address} in quote` + ); + } + /** * Since transfer fee is deducted from input amount, * we overwrite the transfer fee asset to be the input From 22b353aa562516f7038e41905e7a7f89ac7ba0c2 Mon Sep 17 00:00:00 2001 From: Matt Upham <30577966+mattupham@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:38:10 -0700 Subject: [PATCH 8/8] Portfolio performance chart (#3601) * Migrate to tRPC * Clean up tRPC * Add chart types * Fix type issue * Add skeleton * Add formatted performance value * Clean up value calculation * Clean up percent * Add formatting for price change display * Add chart colors * Update chart config * Add comment * clean up imports * Add resize button * Add support for 1h * Clean up color logic * Migrate chart to asset overview * Update dates * remove log * Add simple error * Set defaults * clean up * Remove address * Fix imports * migrate to account store * Update numia key refrence * set last value * temp * comment out resize button temporarily * Cleanup * Clean up ts issues * Update date logic * Remove log --- packages/server/src/env.ts | 1 + .../src/queries/complex/portfolio/index.ts | 1 + .../queries/complex/portfolio/portfolio.ts | 49 ++ .../server/src/queries/data-services/index.ts | 1 + .../data-services/portfolio-over-time.ts | 27 ++ .../src/queries/data-services/staking-apr.ts | 4 +- .../src/queries/data-services/transactions.ts | 2 +- packages/trpc/src/portfolio.ts | 20 + packages/web/components/assets/price.tsx | 24 +- .../web/components/chart/historical-chart.tsx | 67 ++- .../web/components/complex/portfolio-page.tsx | 420 ------------------ .../complex/portfolio/assets-overview.tsx | 216 +++++++++ .../portfolio/get-started-with-osmosis.tsx | 31 ++ .../complex/portfolio/historical-chart.tsx | 93 ++++ .../historical-range-button-group.tsx | 38 ++ .../complex/portfolio/performance.tsx | 25 ++ .../complex/portfolio/portfolio-page.tsx | 156 +++++++ .../web/components/complex/portfolio/types.ts | 7 + .../complex/portfolio/user-positions.tsx | 121 +++++ .../user-zero-balance-table-splash.tsx | 48 ++ .../portfolio/wallet-disconnected-splash.tsx | 15 + .../transaction-content.tsx | 107 +++++ packages/web/pages/portfolio/index.tsx | 2 +- packages/web/public/icons/sprite.svg | 69 ++- 24 files changed, 1085 insertions(+), 459 deletions(-) create mode 100644 packages/server/src/queries/complex/portfolio/portfolio.ts create mode 100644 packages/server/src/queries/data-services/portfolio-over-time.ts delete mode 100644 packages/web/components/complex/portfolio-page.tsx create mode 100644 packages/web/components/complex/portfolio/assets-overview.tsx create mode 100644 packages/web/components/complex/portfolio/get-started-with-osmosis.tsx create mode 100644 packages/web/components/complex/portfolio/historical-chart.tsx create mode 100644 packages/web/components/complex/portfolio/historical-range-button-group.tsx create mode 100644 packages/web/components/complex/portfolio/performance.tsx create mode 100644 packages/web/components/complex/portfolio/portfolio-page.tsx create mode 100644 packages/web/components/complex/portfolio/user-positions.tsx create mode 100644 packages/web/components/complex/portfolio/user-zero-balance-table-splash.tsx create mode 100644 packages/web/components/complex/portfolio/wallet-disconnected-splash.tsx create mode 100644 packages/web/components/transactions/transaction-details/transaction-content.tsx diff --git a/packages/server/src/env.ts b/packages/server/src/env.ts index bfa5aed48e..4f7339e67f 100644 --- a/packages/server/src/env.ts +++ b/packages/server/src/env.ts @@ -25,6 +25,7 @@ export const INDEXER_DATA_URL = export const NUMIA_BASE_URL = process.env.NEXT_PUBLIC_NUMIA_BASE_URL ?? "https://public-osmosis-api.numia.xyz"; +export const NUMIA_API_KEY = process.env.NUMIA_API_KEY; // sqs export const SIDECAR_BASE_URL = diff --git a/packages/server/src/queries/complex/portfolio/index.ts b/packages/server/src/queries/complex/portfolio/index.ts index 8c741c990c..0e7d69859a 100644 --- a/packages/server/src/queries/complex/portfolio/index.ts +++ b/packages/server/src/queries/complex/portfolio/index.ts @@ -1 +1,2 @@ export * from "./allocation"; +export * from "./portfolio"; diff --git a/packages/server/src/queries/complex/portfolio/portfolio.ts b/packages/server/src/queries/complex/portfolio/portfolio.ts new file mode 100644 index 0000000000..2468be6112 --- /dev/null +++ b/packages/server/src/queries/complex/portfolio/portfolio.ts @@ -0,0 +1,49 @@ +import cachified, { CacheEntry } from "cachified"; +import { LRUCache } from "lru-cache"; + +import { queryPortfolioOverTime } from "../../../queries/data-services"; +import { DEFAULT_LRU_OPTIONS } from "../../../utils/cache"; + +export type Range = "1d" | "7d" | "1mo" | "1y" | "all"; + +const transactionsCache = new LRUCache(DEFAULT_LRU_OPTIONS); + +export interface ChartPortfolioOverTimeResponse { + time: number; + value: number; +} + +export async function getPortfolioOverTime({ + address, + range, +}: { + address: string; + range: Range; +}): Promise { + return await cachified({ + cache: transactionsCache, + ttl: 1000 * 60, // 60 seconds + key: `portfolio-over-time-${address}-range-${range}`, + getFreshValue: async () => { + const data = await queryPortfolioOverTime({ + address, + range, + }); + + // sort data by timestamp in ascending order for chart + const sortedData = data?.sort( + (a, b) => + new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() + ); + + // map data to time and value for chart + const mappedData = sortedData.map((d) => ({ + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#get_the_number_of_seconds_since_the_ecmascript_epoch + time: Math.floor(new Date(d.timestamp).getTime() / 1000), // convert to seconds + value: d.usd, + })); + + return mappedData; + }, + }); +} diff --git a/packages/server/src/queries/data-services/index.ts b/packages/server/src/queries/data-services/index.ts index 31295a31bf..48bdbfd25f 100644 --- a/packages/server/src/queries/data-services/index.ts +++ b/packages/server/src/queries/data-services/index.ts @@ -4,6 +4,7 @@ export * from "./filtered-pools"; export * from "./market-cap"; export * from "./pool-aprs"; export * from "./pools-fees"; +export * from "./portfolio-over-time"; export * from "./position-performance"; export * from "./price-range-apr"; export * from "./staking-apr"; diff --git a/packages/server/src/queries/data-services/portfolio-over-time.ts b/packages/server/src/queries/data-services/portfolio-over-time.ts new file mode 100644 index 0000000000..c4f769ac6a --- /dev/null +++ b/packages/server/src/queries/data-services/portfolio-over-time.ts @@ -0,0 +1,27 @@ +import { apiClient } from "@osmosis-labs/utils"; + +import { NUMIA_BASE_URL } from "../../env"; + +export interface PortfolioOverTimeResponse { + timestamp: string; + usd: number; +} + +export async function queryPortfolioOverTime({ + address, + range, +}: { + address: string; + range: string; +}): Promise { + const url = new URL("/users/portfolio/over_time", NUMIA_BASE_URL); + + url.searchParams.append("address", address); + url.searchParams.append("range", range); + + const headers = { + Authorization: `Bearer ${process.env.NUMIA_API_KEY}`, + }; + + return apiClient(url.toString(), { headers }); +} diff --git a/packages/server/src/queries/data-services/staking-apr.ts b/packages/server/src/queries/data-services/staking-apr.ts index f5421a52e8..b9b31e6016 100644 --- a/packages/server/src/queries/data-services/staking-apr.ts +++ b/packages/server/src/queries/data-services/staking-apr.ts @@ -1,6 +1,6 @@ import { apiClient } from "@osmosis-labs/utils"; -import { NUMIA_BASE_URL } from "../../env"; +import { NUMIA_API_KEY, NUMIA_BASE_URL } from "../../env"; interface StakingAprResponse { labels: string; @@ -20,7 +20,7 @@ export async function queryStakingApr({ url.searchParams.append("end_date", endDate); const headers = { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_NUMIA_API_KEY}`, + Authorization: `Bearer ${NUMIA_API_KEY}`, }; return apiClient(url.toString(), { headers }); diff --git a/packages/server/src/queries/data-services/transactions.ts b/packages/server/src/queries/data-services/transactions.ts index 855bc715ce..6deea79409 100644 --- a/packages/server/src/queries/data-services/transactions.ts +++ b/packages/server/src/queries/data-services/transactions.ts @@ -94,7 +94,7 @@ export async function queryTransactions({ url.searchParams.append("pageSize", pageSize); const headers = { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_NUMIA_API_KEY}`, + Authorization: `Bearer ${process.env.NUMIA_API_KEY}`, }; return apiClient(url.toString(), { headers }); diff --git a/packages/trpc/src/portfolio.ts b/packages/trpc/src/portfolio.ts index b07f9125fe..868baede4d 100644 --- a/packages/trpc/src/portfolio.ts +++ b/packages/trpc/src/portfolio.ts @@ -1,9 +1,29 @@ +import { getPortfolioOverTime } from "@osmosis-labs/server"; import { getAllocation } from "@osmosis-labs/server"; +import { ChartPortfolioOverTimeResponse } from "@osmosis-labs/server/src/queries/complex/portfolio/portfolio"; import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "./api"; export const portfolioRouter = createTRPCRouter({ + getPortfolioOverTime: publicProcedure + .input( + z.object({ + address: z.string(), + range: z.enum(["1d", "7d", "1mo", "1y", "all"]), + }) + ) + .query( + async ({ + input: { address, range }, + }): Promise => { + const res = await getPortfolioOverTime({ + address, + range, + }); + return res; + } + ), getAllocation: publicProcedure .input( z.object({ diff --git a/packages/web/components/assets/price.tsx b/packages/web/components/assets/price.tsx index 6d6f4ec139..626c9c8903 100644 --- a/packages/web/components/assets/price.tsx +++ b/packages/web/components/assets/price.tsx @@ -1,4 +1,4 @@ -import { RatePretty } from "@keplr-wallet/unit"; +import { PricePretty, RatePretty } from "@keplr-wallet/unit"; import { CommonPriceChartTimeFrame } from "@osmosis-labs/server"; import classNames from "classnames"; import { FunctionComponent, useMemo } from "react"; @@ -16,14 +16,26 @@ export const PriceChange: FunctionComponent< { priceChange: RatePretty; overrideTextClasses?: string; + value?: PricePretty; } & CustomClasses -> = ({ priceChange, overrideTextClasses = "body1", className }) => { +> = ({ priceChange, overrideTextClasses = "body1", className, value }) => { const isBullish = priceChange.toDec().isPositive(); const isBearish = priceChange.toDec().isNegative(); const isFlat = !isBullish && !isBearish; // remove negative symbol since we're using arrows - if (isBearish) priceChange = priceChange.mul(new RatePretty(-1)); + if (isBearish) { + priceChange = priceChange.mul(new RatePretty(-1)); + value = value?.mul(new RatePretty(-1)); + } + + const priceChangeDisplay = priceChange + .maxDecimals(1) + .inequalitySymbol(false) + .toString(); + + const formattedPriceChangeDisplay = + value !== undefined ? `(${priceChangeDisplay})` : priceChangeDisplay; return (
@@ -53,9 +65,9 @@ export const PriceChange: FunctionComponent< overrideTextClasses )} > - {isFlat - ? "-" - : priceChange.maxDecimals(1).inequalitySymbol(false).toString()} + {value !== undefined ? value.toString() + " " : null} + + {isFlat ? "-" : formattedPriceChangeDisplay}
); diff --git a/packages/web/components/chart/historical-chart.tsx b/packages/web/components/chart/historical-chart.tsx index 64bdf15eec..425db481cb 100644 --- a/packages/web/components/chart/historical-chart.tsx +++ b/packages/web/components/chart/historical-chart.tsx @@ -18,33 +18,62 @@ import { getDecimalCount } from "~/utils/number"; import { Chart } from "./light-weight-charts/chart"; -const seriesOpt: DeepPartial = { - lineColor: theme.colors.wosmongton[300], - lineWidth: 2, - lineType: LineType.Curved, - topColor: theme.colors.osmoverse[700], - bottomColor: theme.colors.osmoverse[850], - priceLineVisible: false, - lastValueVisible: false, - priceScaleId: "right", - crosshairMarkerBorderWidth: 4, - crosshairMarkerBorderColor: theme.colors.osmoverse[900], - crosshairMarkerRadius: 4, - priceFormat: { - type: "custom", - formatter: priceFormatter, - minMove: 0.0000000001, - }, +const getSeriesOpt = (config: Style): DeepPartial => { + let lineColor, topColor, bottomColor, crosshairMarkerBorderColor; + + switch (config) { + case "bullish": + lineColor = theme.colors.bullish[500]; + topColor = `${theme.colors.bullish[500]}33`; // 20% opacity + bottomColor = `${theme.colors.bullish[500]}00`; // 0% opacity + crosshairMarkerBorderColor = theme.colors.bullish[500]; + break; + case "bearish": + lineColor = theme.colors.rust[500]; + topColor = `${theme.colors.rust[500]}33`; // 20% opacity + bottomColor = `${theme.colors.rust[500]}00`; // 0% opacity + crosshairMarkerBorderColor = theme.colors.rust[500]; + break; + case "neutral": + default: + lineColor = theme.colors.wosmongton[300]; + topColor = theme.colors.osmoverse[700]; + bottomColor = theme.colors.osmoverse[850]; + crosshairMarkerBorderColor = theme.colors.osmoverse[900]; + break; + } + + return { + lineColor, + lineWidth: 2, + lineType: LineType.Curved, + topColor, + bottomColor, + priceLineVisible: false, + lastValueVisible: false, + priceScaleId: "right", + crosshairMarkerBorderWidth: 4, + crosshairMarkerBorderColor, + crosshairMarkerRadius: 4, + priceFormat: { + type: "custom", + formatter: priceFormatter, + minMove: 0.0000000001, + }, + }; }; +type Style = "bullish" | "bearish" | "neutral"; + interface HistoricalChartProps { data: AreaData