Skip to content

Commit 56feba3

Browse files
fix: value
1 parent 47ae026 commit 56feba3

30 files changed

+489
-204
lines changed

apps/docs/md/dependencies.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
This document rational behind opionated dependencies
44

55
- `graphql-request` is used with @tanstack query as lightweight fetching. Data are not normalized
6-
- it is optional to combine with [client from the graph](https://thegraph.com/docs/en/querying/querying-from-an-application/#graph-client) with block tracking, cross-chain subgraphs handling etc.
6+
- it is optional to combine with [client from the graph](https://thegraph.com/docs/en/querying/querying-from-an-application/#graph-client) with block tracking, multi-chain subgraphs handling etc.
77
- `@graphprotocol/client-cli` is used for artifact generating
88

9-
- jotai over zustand
10-
- Jotai is preferred although zustand is being used in scaffold. Firstly it is used by shadcn, and it [doesn't use a single store as in zustand](https://zustand.docs.pmnd.rs/getting-started/comparison#jotai)
9+
- nanostore over jotai over zustand
10+
- Nanostore is tiny and framework agnostic, recommended by [astro](https://docs.astro.build/en/recipes/sharing-state-islands/), will be used for more basic use cases
11+
- Jotai more mature and is preferred to zustand although zustand is being used in scaffold. Firstly it is used by shadcn, and it [doesn't use a single store as in zustand](https://zustand.docs.pmnd.rs/getting-started/comparison#jotai)
1112

1213
- vitest over jest
1314
- used by shadcn, scaffold and generally author find less issues for typscript setup as in jest.

apps/storybook/src/stories/token/TokenBalanceChart.stories.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
asCaip19Id,
3-
groupCrosschainTokens,
4-
} from "@geist/domain/token/cross-chain";
3+
groupMultichainToken,
4+
} from "@geist/domain/token/multi-chain";
55
import { asTokenBalanceEntries } from "@geist/domain/token/token";
66
import {
77
PRICE_DATA_SNAPSHOT,
@@ -24,11 +24,33 @@ export default meta;
2424

2525
type Story = StoryObj<typeof meta>;
2626

27-
export const ByCrosschainToken: Story = {
27+
// separate abstraction
28+
// similar to how Elasticsearch query handles aggs https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
29+
// agg by chain, token
30+
31+
// useful:
32+
// e.g. USDC amount distribution by each chain
33+
// e.g. value distribution of agg of multiple tokens by each chain
34+
// e.g. value distribution by each token of a chain
35+
36+
// not useful
37+
// e.g. multiple non-stable token amount by each chain
38+
39+
export const TokenAmountByMultichain: Story = {
2840
args: {
41+
group: "chain",
42+
// dataByGroup: {}
43+
2944
tokenBalances: asTokenBalanceEntries(
30-
groupCrosschainTokens(TOKEN_BALANCES_MULTIPLE_STABLECOINS),
45+
groupMultichainToken(TOKEN_BALANCES_MULTIPLE_STABLECOINS),
3146
PRICE_DATA_SNAPSHOT,
3247
),
3348
},
3449
};
50+
51+
// export const TokenValueByMultichain: Story = {
52+
// args: {
53+
// ...TokenAmountByMultichain.args,
54+
// group: 'chain',
55+
// },
56+
// };

apps/storybook/src/stories/token/TokenBalanceTable.stories.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
asCaip19Id,
3-
groupCrosschainTokens,
4-
} from "@geist/domain/token/cross-chain";
3+
groupMultichainToken,
4+
} from "@geist/domain/token/multi-chain";
55
import { asTokenBalanceEntries } from "@geist/domain/token/token";
66
import {
77
PRICE_DATA_SNAPSHOT,
@@ -27,7 +27,7 @@ type Story = StoryObj<typeof meta>;
2727
export const CrossChain: Story = {
2828
args: {
2929
tokenBalances: asTokenBalanceEntries(
30-
groupCrosschainTokens(TOKEN_BALANCES_MULTIPLE_STABLECOINS),
30+
groupMultichainToken(TOKEN_BALANCES_MULTIPLE_STABLECOINS),
3131
PRICE_DATA_SNAPSHOT,
3232
),
3333
},

apps/storybook/src/stories/token/TokenPriceChartWithFeed.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
asCaip19Id,
3-
groupCrosschainTokens,
4-
} from "@geist/domain/token/cross-chain";
3+
groupMultichainToken,
4+
} from "@geist/domain/token/multi-chain";
55
import {
66
PRICE_DATA_FEED,
77
PRICE_DATA_SNAPSHOT,

packages/domain/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"vitest": "^2.1.8"
2727
},
2828
"dependencies": {
29+
"nanostores": "0.11.3",
2930
"@ethereumjs/rlp": "^5.0.2",
3031
"@hookform/resolvers": "^3.9.0",
3132
"@ipld/dag-ucan": "^3.4.0",

packages/domain/src/amount.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
11
import { describe, expect, test } from "vitest";
2-
import { formatUnitsWithLocale } from "./amount";
2+
import { formatNumberWithLocale, formatUnitsWithLocale } from "./amount";
33

44
describe("amount", () => {
5+
describe("#formatNumberWithLocale", () => {
6+
test("format non currency", () => {
7+
const result = formatNumberWithLocale({
8+
value: 1234.567,
9+
locale: new Intl.Locale("en-US"),
10+
});
11+
expect(result).toBe("1,234.57");
12+
});
13+
14+
test("format currency", () => {
15+
const result = formatNumberWithLocale({
16+
value: 1234.567,
17+
locale: new Intl.Locale("en-US"),
18+
formatOptions: {
19+
style: "currency",
20+
},
21+
});
22+
expect(result).toBe("$1,234.57");
23+
});
24+
});
525
describe("formatUnitsWithLocale", () => {
6-
test("formats number with locale options", () => {
26+
test("format units with locale options", () => {
727
const result = formatUnitsWithLocale({
828
value: 1234567890000n,
929
exponent: 9,

packages/domain/src/amount.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { formatUnits } from "viem";
22

33
/**
4-
* compared to formatUnits from viem
4+
*
5+
* Prefer passing around explicit bigint, decimals for financial values
6+
* which also result in more isomorphic code
7+
*
8+
*
9+
* Compared to `formatUnits` from viem
510
* besides expotent handling, also take control of decimals displayed and locale concern
611
*/
712

@@ -21,14 +26,35 @@ export const formatUnitsWithLocale = ({
2126
}
2227
const e = Math.pow(10, exponent);
2328

29+
return formatNumberWithLocale({
30+
value: Number(value) / e,
31+
locale,
32+
formatOptions,
33+
});
34+
};
35+
36+
/**
37+
* Reasonable defaults for formatting cryptocurrency values
38+
* Could use native number.toLocaleString() otherwise
39+
*/
40+
41+
export const formatNumberWithLocale = ({
42+
value,
43+
locale,
44+
formatOptions = {},
45+
}: {
46+
value: number;
47+
locale?: Intl.Locale;
48+
formatOptions?: Intl.NumberFormatOptions;
49+
}) => {
2450
const currency =
2551
formatOptions?.style === "currency"
2652
? formatOptions?.currency || "USD"
2753
: undefined;
2854

29-
return (Number(value) / e).toLocaleString(locale, {
30-
...formatOptions,
55+
return value.toLocaleString(locale, {
3156
currency,
3257
maximumFractionDigits: formatOptions?.maximumFractionDigits ?? 2,
58+
...formatOptions,
3359
});
3460
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
2+
import { aggregateBySymbol, tokenBalanceStore } from "./aggregate";
3+
4+
import { atom, cleanStores, keepMount } from "nanostores";
5+
import {
6+
PRICE_DATA_SNAPSHOT,
7+
TOKEN_BALANCES_MULTIPLE_STABLECOINS,
8+
} from "./token-balance.fixture";
9+
import type { TokenPriceEntry } from "./token-price-entry";
10+
11+
let { $tokenBalances, $priceData } = tokenBalanceStore();
12+
13+
afterEach(() => {
14+
cleanStores($tokenBalances);
15+
cleanStores($priceData);
16+
});
17+
18+
describe("aggregate", () => {
19+
beforeEach(() => {
20+
keepMount($tokenBalances);
21+
keepMount($priceData);
22+
});
23+
24+
it("#aggregateBySymbol with no price", () => {
25+
$tokenBalances.set([
26+
...$tokenBalances.get(),
27+
...TOKEN_BALANCES_MULTIPLE_STABLECOINS,
28+
]);
29+
30+
const $aggregate = aggregateBySymbol($tokenBalances);
31+
32+
expect($aggregate.get().get()["OP"].agg).toEqual({
33+
amount: 555555n,
34+
value: 0n,
35+
});
36+
});
37+
38+
// TODO fix race condition
39+
it("#aggregateBySymbol with price", () => {
40+
$tokenBalances.set([
41+
...$tokenBalances.get(),
42+
...TOKEN_BALANCES_MULTIPLE_STABLECOINS,
43+
]);
44+
45+
$priceData.set([...PRICE_DATA_SNAPSHOT]);
46+
47+
const $aggregate = aggregateBySymbol($tokenBalances, $priceData);
48+
49+
expect($aggregate.get().get()["OP"].agg).toEqual({
50+
amount: 1111110n,
51+
value: 2333331000000000000000000n,
52+
});
53+
});
54+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Extract logic from presentation component to store
3+
* we want a structure flexible enough to observe for price change
4+
* such that value is reactive to price and amount and aggregates reactive its components
5+
*/
6+
7+
import { groupMultichainToken, withValue } from "./multi-chain";
8+
import type { TokenBalanceEntry } from "./token-balance-entry";
9+
10+
import { type Atom, atom, computed, deepMap } from "nanostores";
11+
import type { TokenPriceEntry } from "./token-price-entry";
12+
13+
export const tokenBalanceStore = () => {
14+
const $tokenBalances = atom<TokenBalanceEntry[]>([]);
15+
const $priceData = atom<TokenPriceEntry[]>([]);
16+
17+
return {
18+
$tokenBalances,
19+
$priceData,
20+
};
21+
};
22+
23+
export const aggregateBySymbol = (
24+
$tokenBalances: Atom<TokenBalanceEntry[]>,
25+
$priceData?: Atom<TokenPriceEntry[]>,
26+
) => {
27+
return computed(
28+
[$tokenBalances, $priceData || atom<TokenPriceEntry[]>([])],
29+
(
30+
tokenBalances: TokenBalanceEntry[],
31+
$priceData: TokenPriceEntry[] = [],
32+
) => {
33+
const bySymbol = groupMultichainToken(tokenBalances);
34+
35+
return Object.entries(bySymbol).reduce((acc, [symbol, tokenInfo]) => {
36+
const { tokenBalances } = tokenInfo;
37+
38+
const tokenBalancesWithValue = tokenBalances.map((tokenBalance) =>
39+
withValue(tokenBalance, $priceData),
40+
);
41+
42+
// TODO if empty price data
43+
console.log("tokenBalancesWithValue", tokenBalancesWithValue);
44+
const agg = tokenBalancesWithValue.reduce(
45+
(acc, tokenBalanceWithValue) => {
46+
return {
47+
amount: acc.amount + tokenBalanceWithValue.amount,
48+
value: acc.value + (tokenBalanceWithValue.value || 0n),
49+
};
50+
},
51+
{
52+
amount: 0n,
53+
value: 0n,
54+
},
55+
);
56+
console.log("agg", agg);
57+
acc.setKey(symbol, {
58+
tokenBalances: tokenBalancesWithValue,
59+
agg,
60+
});
61+
return acc;
62+
}, deepMap<Record<string, unknown>>({}));
63+
},
64+
);
65+
};

packages/domain/src/token/cross-chain.test.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)