Skip to content

Commit

Permalink
Portfolio allocation (#3660)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mattupham authored Aug 5, 2024
1 parent c373c76 commit a904c95
Show file tree
Hide file tree
Showing 32 changed files with 807 additions and 19 deletions.
1 change: 1 addition & 0 deletions packages/server/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ module.exports = {
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname",
],
maxWorkers: 1,
};
1 change: 1 addition & 0 deletions packages/server/src/queries/complex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Original file line number Diff line number Diff line change
@@ -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",
},
]);
});
});
});
157 changes: 157 additions & 0 deletions packages/server/src/queries/complex/portfolio/allocation.ts
Original file line number Diff line number Diff line change
@@ -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<GetAllocationResponse> {
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,
};
}
1 change: 1 addition & 0 deletions packages/server/src/queries/complex/portfolio/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./allocation";
1 change: 1 addition & 0 deletions packages/server/src/queries/data-services/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "../sidecar/allocation";
export * from "./earn";
export * from "./filtered-pools";
export * from "./market-cap";
Expand Down
Loading

0 comments on commit a904c95

Please sign in to comment.