Skip to content

Commit 18cb8a2

Browse files
authored
feat: make cart handle new chains gracefully (#2372)
1 parent 15d15ed commit 18cb8a2

File tree

8 files changed

+101
-53
lines changed

8 files changed

+101
-53
lines changed

packages/grant-explorer/src/cartStore.test.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// cartStore.test.ts
2-
31
import { useCartStorage } from "./store";
42
import { ChainId } from "common";
53
import {
@@ -67,9 +65,9 @@ describe("useCartStorage Zustand store", () => {
6765
const chainId: ChainId = ChainId.MAINNET; // Mock ChainId
6866
const payoutToken: VotingToken = votingTokensMap[ChainId.MAINNET][0];
6967

70-
useCartStorage.getState().setPayoutTokenForChain(chainId, payoutToken);
68+
useCartStorage.getState().setVotingTokenForChain(chainId, payoutToken);
7169

72-
expect(useCartStorage.getState().chainToPayoutToken[chainId]).toEqual(
70+
expect(useCartStorage.getState().chainToVotingToken[chainId]).toEqual(
7371
payoutToken
7472
);
7573
});
@@ -119,18 +117,18 @@ describe("useCartStorage Zustand store", () => {
119117
expect(useCartStorage.getState().projects).toEqual(initialProjects);
120118
});
121119

122-
test("should override payout token for a specific chain", () => {
120+
test("should override voting token for a specific chain", () => {
123121
const chainId: ChainId = ChainId.MAINNET; // Mock ChainId
124-
const initialPayoutToken: VotingToken = votingTokensMap[ChainId.MAINNET][0];
125-
const newPayoutToken: VotingToken = votingTokensMap[ChainId.MAINNET][1];
122+
const initialVotingToken: VotingToken = votingTokensMap[ChainId.MAINNET][0];
123+
const newVotingToken: VotingToken = votingTokensMap[ChainId.MAINNET][1];
126124

127125
useCartStorage
128126
.getState()
129-
.setPayoutTokenForChain(chainId, initialPayoutToken);
130-
useCartStorage.getState().setPayoutTokenForChain(chainId, newPayoutToken);
127+
.setVotingTokenForChain(chainId, initialVotingToken);
128+
useCartStorage.getState().setVotingTokenForChain(chainId, newVotingToken);
131129

132-
expect(useCartStorage.getState().chainToPayoutToken[chainId]).toEqual(
133-
newPayoutToken
130+
expect(useCartStorage.getState().chainToVotingToken[chainId]).toEqual(
131+
newVotingToken
134132
);
135133
});
136134

@@ -181,16 +179,16 @@ describe("useCartStorage Zustand store", () => {
181179
const payoutToken: VotingToken = votingTokensMap[ChainId.MAINNET][0];
182180

183181
const initialChainToPayoutToken = {
184-
...useCartStorage.getState().chainToPayoutToken,
182+
...useCartStorage.getState().chainToVotingToken,
185183
};
186184

187185
useCartStorage
188186
.getState()
189187
// @ts-expect-error We purposefully pass a wrong ChainId here
190-
.setPayoutTokenForChain(nonExistingChainId, payoutToken);
188+
.setVotingTokenForChain(nonExistingChainId, payoutToken);
191189

192-
// The state should remain unchanged unchanged.
193-
expect(useCartStorage.getState().chainToPayoutToken).toEqual(
190+
// The state should remain unchanged.
191+
expect(useCartStorage.getState().chainToVotingToken).toEqual(
194192
initialChainToPayoutToken
195193
);
196194
});

packages/grant-explorer/src/checkoutStore.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ export const useCheckoutStore = create<CheckoutState>()(
114114
[chain: number]: CartProject[];
115115
};
116116

117-
const payoutTokens = useCartStorage.getState().chainToPayoutToken;
117+
const getVotingTokenForChain =
118+
useCartStorage.getState().getVotingTokenForChain;
118119

119120
const totalDonationPerChain = Object.fromEntries(
120121
Object.entries(projectsByChain).map(([key, value]) => [
@@ -126,7 +127,7 @@ export const useCheckoutStore = create<CheckoutState>()(
126127
acc +
127128
parseUnits(
128129
amount ? amount : "0",
129-
payoutTokens[Number(key) as ChainId].decimal
130+
getVotingTokenForChain(Number(key) as ChainId).decimal
130131
),
131132
0n
132133
),
@@ -149,7 +150,7 @@ export const useCheckoutStore = create<CheckoutState>()(
149150
const wc = await getWalletClient({
150151
chainId,
151152
})!;
152-
const token = payoutTokens[chainId];
153+
const token = getVotingTokenForChain(chainId);
153154

154155
let sig;
155156
let nonce;

packages/grant-explorer/src/features/api/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ const ARBITRUM_TOKENS: VotingToken[] = [
312312
const ARBITRUM_GOERLI_TOKENS: VotingToken[] = [
313313
{
314314
name: "ETH",
315-
chainId: ChainId.PGN,
315+
chainId: ChainId.ARBITRUM_GOERLI,
316316
address: zeroAddress,
317317
decimal: 18,
318318
logo: TokenNamesAndLogos["ETH"],

packages/grant-explorer/src/features/round/ViewCartPage/CartWithProjects.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export function CartWithProjects({ cart, chainId }: Props) {
2525

2626
const [fixedDonation, setFixedDonation] = useState("");
2727

28-
const { chainToPayoutToken, setPayoutTokenForChain } = useCartStorage();
29-
const selectedPayoutToken = chainToPayoutToken[chainId];
28+
const { getVotingTokenForChain, setVotingTokenForChain } = useCartStorage();
29+
const selectedPayoutToken = getVotingTokenForChain(chainId);
3030
const payoutTokenOptions: VotingToken[] = getVotingTokenOptions(
3131
Number(chainId)
3232
).filter((p) => p.canVote);
@@ -42,7 +42,7 @@ export function CartWithProjects({ cart, chainId }: Props) {
4242
/** The payout token data (like permit version etc.) might've changed since the user last visited the page
4343
* Refresh it to update, default to the first payout token if the previous token was deleted */
4444
useEffect(() => {
45-
setPayoutTokenForChain(
45+
setVotingTokenForChain(
4646
chainId,
4747
getVotingTokenOptions(chainId).find(
4848
(token) => token.address === selectedPayoutToken.address
@@ -83,7 +83,7 @@ export function CartWithProjects({ cart, chainId }: Props) {
8383
<PayoutTokenDropdown
8484
selectedPayoutToken={selectedPayoutToken}
8585
setSelectedPayoutToken={(token) => {
86-
setPayoutTokenForChain(chainId, token);
86+
setVotingTokenForChain(chainId, token);
8787
}}
8888
payoutTokenOptions={payoutTokenOptions}
8989
/>

packages/grant-explorer/src/features/round/ViewCartPage/ChainConfirmationModalBody.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ export function ChainConfirmationModalBody({
3232
}
3333
};
3434

35-
const payoutTokens = useCartStorage((state) => state.chainToPayoutToken);
35+
const getVotingTokenForChain = useCartStorage(
36+
(state) => state.getVotingTokenForChain
37+
);
3638
return (
3739
<>
3840
<p className="text-sm text-grey-400">
@@ -43,7 +45,9 @@ export function ChainConfirmationModalBody({
4345
{Object.keys(projectsByChain).map((chainId, index) => (
4446
<ChainSummary
4547
chainId={Number(chainId) as ChainId}
46-
selectedPayoutToken={payoutTokens[Number(chainId) as ChainId]}
48+
selectedPayoutToken={getVotingTokenForChain(
49+
Number(chainId) as ChainId
50+
)}
4751
totalDonation={totalDonationsPerChain[Number(chainId)]}
4852
checked={chainIdsBeingCheckedOut.includes(Number(chainId))}
4953
onChange={(checked) =>

packages/grant-explorer/src/features/round/ViewCartPage/SummaryContainer.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import { formatUnits, parseUnits } from "viem";
2222
import { useConnectModal } from "@rainbow-me/rainbowkit";
2323

2424
export function SummaryContainer() {
25-
const { projects, chainToPayoutToken: payoutTokens } = useCartStorage();
25+
const { projects, getVotingTokenForChain, chainToVotingToken } =
26+
useCartStorage();
2627
const { checkout, voteStatus, chainsToCheckout } = useCheckoutStore();
2728

2829
const { openConnectModal } = useConnectModal();
@@ -83,13 +84,15 @@ export function SummaryContainer() {
8384
acc +
8485
parseUnits(
8586
amount ? amount : "0",
86-
payoutTokens[Number(key) as ChainId]?.decimal
87+
getVotingTokenForChain(Number(key) as ChainId).decimal
8788
),
8889
0n
8990
),
9091
])
9192
);
92-
}, [payoutTokens, projectsByChain]);
93+
/* NB: we want to update the totalDonationsPerChain value based on chainToVotingToken */
94+
// eslint-disable-next-line react-hooks/exhaustive-deps
95+
}, [getVotingTokenForChain, chainToVotingToken, projectsByChain]);
9396

9497
const navigate = useNavigate();
9598
const { address, isConnected } = useAccount();
@@ -247,13 +250,13 @@ export function SummaryContainer() {
247250
Object.keys(totalDonationsPerChain).map((chainId) =>
248251
getTokenPrice(
249252
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
250-
payoutTokens[Number(chainId) as ChainId].redstoneTokenId!
253+
getVotingTokenForChain(Number(chainId) as ChainId).redstoneTokenId!
251254
).then((price) => {
252255
return (
253256
Number(
254257
formatUnits(
255258
totalDonationsPerChain[chainId],
256-
payoutTokens[Number(chainId) as ChainId].decimal
259+
getVotingTokenForChain(Number(chainId) as ChainId).decimal
257260
)
258261
) * Number(price)
259262
);
@@ -284,7 +287,9 @@ export function SummaryContainer() {
284287
<Summary
285288
key={chainId}
286289
chainId={Number(chainId) as ChainId}
287-
selectedPayoutToken={payoutTokens[Number(chainId) as ChainId]}
290+
selectedPayoutToken={getVotingTokenForChain(
291+
Number(chainId) as ChainId
292+
)}
288293
totalDonation={totalDonationsPerChain[chainId]}
289294
/>
290295
))}

packages/grant-explorer/src/store.ts

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { create } from "zustand";
33
import { persist } from "zustand/middleware";
44
import { CartProject, VotingToken } from "./features/api/types";
55
import { votingTokensMap } from "./features/api/utils";
6+
import { zeroAddress } from "viem";
67

78
interface CartState {
89
projects: CartProject[];
@@ -11,19 +12,38 @@ interface CartState {
1112
remove: (grantApplicationId: string) => void;
1213
updateDonationsForChain: (chainId: ChainId, amount: string) => void;
1314
updateDonationAmount: (grantApplicationId: string, amount: string) => void;
14-
chainToPayoutToken: Record<ChainId, VotingToken>;
15-
setPayoutTokenForChain: (chainId: ChainId, payoutToken: VotingToken) => void;
15+
chainToVotingToken: Record<ChainId, VotingToken>;
16+
getVotingTokenForChain: (chainId: ChainId) => VotingToken;
17+
setVotingTokenForChain: (chainId: ChainId, votingToken: VotingToken) => void;
1618
}
1719

18-
const ethOnlyPayoutTokens = Object.fromEntries(
19-
Object.entries(votingTokensMap).map(
20-
([key, value]) =>
21-
[
22-
Number(key) as ChainId,
23-
value.find((token) => token.defaultForVoting && token.canVote) ??
24-
value[0],
25-
] as [ChainId, VotingToken]
26-
)
20+
/**
21+
* Consumes an array of voting tokens and returns the default one.
22+
* If there's no default one, return the first one.
23+
* If the array is empty,
24+
* return the native token for the chain (Although this should never happen)
25+
* */
26+
function getDefaultVotingToken(votingTokens: VotingToken[], chainId: ChainId) {
27+
return (
28+
votingTokens.find((token) => token.defaultForVoting && token.canVote) ??
29+
votingTokens[0] ?? {
30+
chainId,
31+
canVote: true,
32+
defaultForVoting: true,
33+
decimal: 18,
34+
name: "Native Token",
35+
address: zeroAddress,
36+
}
37+
);
38+
}
39+
40+
const defaultVotingTokens = Object.fromEntries(
41+
Object.entries(votingTokensMap).map(([key, value]) => {
42+
return [
43+
Number(key) as ChainId,
44+
getDefaultVotingToken(value, Number(key) as ChainId),
45+
] as [ChainId, VotingToken];
46+
})
2747
) as Record<ChainId, VotingToken>;
2848

2949
export const useCartStorage = create<CartState>()(
@@ -77,8 +97,29 @@ export const useCartStorage = create<CartState>()(
7797
});
7898
}
7999
},
80-
chainToPayoutToken: ethOnlyPayoutTokens,
81-
setPayoutTokenForChain: (chainId: ChainId, payoutToken: VotingToken) => {
100+
chainToVotingToken: defaultVotingTokens,
101+
getVotingTokenForChain: (chainId: ChainId) => {
102+
const tokenFromStore = get().chainToVotingToken[chainId];
103+
if (!tokenFromStore) {
104+
const defaultToken = getDefaultVotingToken(
105+
votingTokensMap[chainId],
106+
chainId
107+
);
108+
console.log(
109+
"no token for chain",
110+
chainId,
111+
" defaulting to ",
112+
defaultToken,
113+
" and setting it as the default token for that chain"
114+
);
115+
116+
get().setVotingTokenForChain(chainId, defaultToken);
117+
return defaultToken;
118+
} else {
119+
return tokenFromStore;
120+
}
121+
},
122+
setVotingTokenForChain: (chainId: ChainId, payoutToken: VotingToken) => {
82123
if (!Object.values(ChainId).includes(chainId)) {
83124
console.warn(
84125
"Tried setting payoutToken",
@@ -93,15 +134,14 @@ export const useCartStorage = create<CartState>()(
93134
}
94135

95136
set({
96-
chainToPayoutToken: {
97-
...get().chainToPayoutToken,
137+
chainToVotingToken: {
138+
...get().chainToVotingToken,
98139
[chainId]: payoutToken,
99140
},
100141
});
101142
},
102143
}),
103144
{
104-
/*This is the localStorage key. Change this whenever the shape of the stores objects changes. append a v1, v2. etc. */
105145
name: "cart-storage",
106146
version: 3,
107147
}

pnpm-lock.yaml

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)