Skip to content

Commit a27aaa0

Browse files
authored
Merge branch 'main' into feat/sunset-merit
2 parents 72174d2 + 0aa46a1 commit a27aaa0

14 files changed

Lines changed: 384 additions & 27 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@
4242
"dependencies": {
4343
"@aave-dao/aave-address-book": "^4.55.6",
4444
"@aave/contract-helpers": "1.38.0",
45-
"@aave/graphql": "0.12.0",
45+
"@aave/graphql": "0.13.0",
4646
"@aave/math-utils": "1.38.0",
47-
"@aave/react": "0.9.1",
47+
"@aave/react": "0.10.0",
4848
"@amplitude/analytics-browser": "^2.13.0",
4949
"@cowprotocol/cow-sdk": "7.3.4",
5050
"@cowprotocol/sdk-ethers-v5-adapter": "0.3.5",

pages/sgho.page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ const SGhoVaultWithdrawModal = dynamic(() =>
3737
(module) => module.SGhoVaultWithdrawModal
3838
)
3939
);
40+
const StkGhoMigrateModal = dynamic(() =>
41+
import('../src/components/transactions/StkGhoMigrate/StkGhoMigrateModal').then(
42+
(module) => module.StkGhoMigrateModal
43+
)
44+
);
4045

4146
export default function SavingsGho() {
4247
const [trackEvent, currentMarket, setCurrentMarket] = useRootStore(
@@ -91,6 +96,7 @@ SavingsGho.getLayout = function getLayout(page: React.ReactElement) {
9196
<StakeRewardClaimModal />
9297
<SGhoVaultDepositModal />
9398
<SGhoVaultWithdrawModal />
99+
<StkGhoMigrateModal />
94100
{/** End of modals */}
95101
</MainLayout>
96102
);
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { evmAddress, useStkGhoMigrate } from '@aave/react';
2+
import { useSendTransaction } from '@aave/react/viem';
3+
import { Trans } from '@lingui/macro';
4+
import { BoxProps } from '@mui/material';
5+
import { useQueryClient } from '@tanstack/react-query';
6+
import { errAsync } from 'neverthrow';
7+
import React, { useEffect } from 'react';
8+
import { oracles, stakedTokens } from 'src/hooks/stake/common';
9+
import { useModalContext } from 'src/hooks/useModal';
10+
import { useSavingsMarketData } from 'src/hooks/useSavingsMarketData';
11+
import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
12+
import { useSGhoVaultContext } from 'src/modules/sGho/SGhoVaultContext';
13+
import { useRootStore } from 'src/store/root';
14+
import { queryKeysFactory } from 'src/ui-config/queries';
15+
import { wagmiConfig } from 'src/ui-config/wagmiConfig';
16+
import { useWalletClient } from 'wagmi';
17+
import { waitForTransactionReceipt } from 'wagmi/actions';
18+
import { useShallow } from 'zustand/shallow';
19+
20+
import { TxActionsWrapper } from '../TxActionsWrapper';
21+
22+
// Static recommendation: migrate redeems the full stkGHO position and deposits
23+
// it into the sGHO vault. No approval step exists (the migrator holds the
24+
// stkGHO claim-helper role and redeems on the user's behalf), so this is a safe
25+
// upper bound for the single migrate() call.
26+
const STK_GHO_MIGRATE_GAS_LIMIT = 250_000;
27+
28+
export interface StkGhoMigrateActionsProps extends BoxProps {
29+
isWrongNetwork: boolean;
30+
blocked: boolean;
31+
}
32+
33+
export const StkGhoMigrateActions = React.memo(
34+
({ isWrongNetwork, blocked, sx, ...props }: StkGhoMigrateActionsProps) => {
35+
const { currentAccount } = useWeb3Context();
36+
const { chainId: targetChainId, sdkChainId } = useSavingsMarketData();
37+
const { mainTxState, setMainTxState, setTxError, setGasLimit } = useModalContext();
38+
const { refresh } = useSGhoVaultContext();
39+
const queryClient = useQueryClient();
40+
const [user, marketData] = useRootStore(
41+
useShallow((state) => [state.account, state.currentMarketData])
42+
);
43+
44+
const { data: walletClient } = useWalletClient();
45+
const [migrate] = useStkGhoMigrate();
46+
const [sendTransaction] = useSendTransaction(walletClient);
47+
48+
useEffect(() => {
49+
setGasLimit(STK_GHO_MIGRATE_GAS_LIMIT.toString());
50+
}, [setGasLimit]);
51+
52+
const action = async () => {
53+
if (!currentAccount || !walletClient) return;
54+
setMainTxState({ loading: true });
55+
setTxError(undefined);
56+
57+
const result = await migrate({
58+
user: evmAddress(currentAccount),
59+
chainId: sdkChainId,
60+
}).andThen((plan) => {
61+
switch (plan.__typename) {
62+
case 'TransactionRequest':
63+
return sendTransaction(plan);
64+
case 'InsufficientBalanceError':
65+
return errAsync(new Error('No stkGHO balance available to migrate.'));
66+
case 'ApprovalRequired':
67+
return errAsync(
68+
new Error('Unexpected migration plan; expected a transaction request.')
69+
);
70+
}
71+
});
72+
73+
if (result.isErr()) {
74+
setMainTxState({ loading: false });
75+
setTxError({
76+
blocking: true,
77+
actionBlocked: true,
78+
rawError: result.error as Error,
79+
error: <span>{(result.error as Error).message}</span>,
80+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
81+
txAction: 0 as any,
82+
});
83+
return;
84+
}
85+
86+
const submittedTxHash = result.value;
87+
setMainTxState({ loading: true, txHash: submittedTxHash });
88+
89+
// Wait for the receipt on the connected chain (works on forks too).
90+
try {
91+
await waitForTransactionReceipt(wagmiConfig, {
92+
hash: submittedTxHash as `0x${string}`,
93+
chainId: targetChainId,
94+
});
95+
} catch (e) {
96+
console.warn('waitForTransactionReceipt failed', e);
97+
}
98+
99+
// Refresh the sGHO vault cache (new shares) and invalidate the stkGHO
100+
// position + pool balances so both panels reflect the migration.
101+
refresh();
102+
await new Promise((resolve) => setTimeout(resolve, 1000));
103+
104+
queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool });
105+
queryClient.invalidateQueries({
106+
queryKey: queryKeysFactory.userStakeUiData(user, marketData, stakedTokens, oracles),
107+
});
108+
109+
setMainTxState({ loading: false, success: true, txHash: submittedTxHash });
110+
};
111+
112+
return (
113+
<TxActionsWrapper
114+
requiresApproval={false}
115+
preparingTransactions={false}
116+
mainTxState={mainTxState}
117+
isWrongNetwork={isWrongNetwork}
118+
handleAction={action}
119+
symbol="stkGHO"
120+
actionText={<Trans>Proceed with migration</Trans>}
121+
actionInProgressText={<Trans>Migrating</Trans>}
122+
sx={sx}
123+
blocked={blocked}
124+
{...props}
125+
/>
126+
);
127+
}
128+
);
129+
130+
StkGhoMigrateActions.displayName = 'StkGhoMigrateActions';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Trans } from '@lingui/macro';
2+
import { BasicModal } from 'src/components/primitives/BasicModal';
3+
import { ModalContextType, ModalType, useModalContext } from 'src/hooks/useModal';
4+
5+
import { ModalWrapper } from '../FlowCommons/ModalWrapper';
6+
import { StkGhoMigrateModalContent } from './StkGhoMigrateModalContent';
7+
8+
export const StkGhoMigrateModal = () => {
9+
const { type, close, args } = useModalContext() as ModalContextType<{
10+
underlyingAsset: string;
11+
}>;
12+
13+
return (
14+
<BasicModal open={type === ModalType.StkGhoMigrate} setOpen={close}>
15+
<ModalWrapper
16+
title={<Trans>Migrate stkGHO to sGHO</Trans>}
17+
underlyingAsset={args.underlyingAsset}
18+
hideTitleSymbol
19+
>
20+
{() => <StkGhoMigrateModalContent />}
21+
</ModalWrapper>
22+
</BasicModal>
23+
);
24+
};
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Stake } from '@aave/contract-helpers';
2+
import { bigDecimal, useSghoVaultPreviewDeposit } from '@aave/react';
3+
import { Trans } from '@lingui/macro';
4+
import { formatEther } from 'ethers/lib/utils';
5+
import { useRef } from 'react';
6+
import { useUserStakeUiData } from 'src/hooks/stake/useUserStakeUiData';
7+
import { useModalContext } from 'src/hooks/useModal';
8+
import { useSavingsMarketData } from 'src/hooks/useSavingsMarketData';
9+
import { useSGhoVaultContext } from 'src/modules/sGho/SGhoVaultContext';
10+
import { useRootStore } from 'src/store/root';
11+
12+
import { useWeb3Context } from '../../../libs/hooks/useWeb3Context';
13+
import { TxErrorView } from '../FlowCommons/Error';
14+
import { TxSuccessView } from '../FlowCommons/Success';
15+
import { DetailsNumberLineWithSub, TxModalDetails } from '../FlowCommons/TxModalDetails';
16+
import { StkGhoMigrateActions } from './StkGhoMigrateActions';
17+
18+
export const StkGhoMigrateModalContent = () => {
19+
const { chainId: connectedChainId } = useWeb3Context();
20+
const { chainId: targetChainId, sdkChainId } = useSavingsMarketData();
21+
const { mainTxState, txError, gasLimit } = useModalContext();
22+
23+
const currentMarketData = useRootStore((store) => store.currentMarketData);
24+
const { data: stakeUserResult } = useUserStakeUiData(currentMarketData, Stake.gho);
25+
26+
// stkGHO is redeemable 1:1 to GHO, so the migrated GHO amount equals the
27+
// user's full stkGHO position. We use it to preview the sGHO shares minted.
28+
const stkGhoBalance = formatEther(stakeUserResult?.[0]?.stakeTokenRedeemableAmount || '0');
29+
30+
const previewAmount = +stkGhoBalance > 0 ? stkGhoBalance : '0';
31+
const { data: previewShares, loading: previewFetching } = useSghoVaultPreviewDeposit({
32+
amount: bigDecimal(previewAmount),
33+
chainId: sdkChainId,
34+
});
35+
36+
// USD pricing from the sGHO vault. stkGHO is 1:1 to GHO, so it's priced at the
37+
// GHO rate (`usdPerToken`). sGHO shares appreciate (not 1:1 to GHO), so they're
38+
// priced at the vault share price = totalAssetsUSD / totalSupply.
39+
const { vault } = useSGhoVaultContext();
40+
const ghoUsdPerToken = +(vault?.totalAssets?.usdPerToken ?? '1');
41+
const totalAssetsUsd = +(vault?.totalAssets?.usd ?? '0');
42+
const totalSupply = +(vault?.totalSupply?.value ?? '0');
43+
const sghoUsdPerShare =
44+
totalSupply > 0 && totalAssetsUsd > 0 ? totalAssetsUsd / totalSupply : ghoUsdPerToken;
45+
46+
const stkGhoUSD = (+stkGhoBalance * ghoUsdPerToken).toString();
47+
const sghoUSD = (+(previewShares?.value ?? '0') * sghoUsdPerShare).toString();
48+
49+
const isWrongNetwork = connectedChainId !== targetChainId;
50+
51+
// Snapshot the received shares at submit time — once the tx mines the Actions
52+
// invalidate the stake data, so `stkGhoBalance` (and thus `previewShares`)
53+
// refetches to 0 and would otherwise blank the success view.
54+
const receivedSharesRef = useRef<string | null>(null);
55+
if (mainTxState.txHash && receivedSharesRef.current === null) {
56+
receivedSharesRef.current = previewShares?.value ?? '0';
57+
}
58+
if (!mainTxState.txHash && !mainTxState.success && receivedSharesRef.current !== null) {
59+
receivedSharesRef.current = null;
60+
}
61+
62+
if (txError && txError.blocking) return <TxErrorView txError={txError} />;
63+
if (mainTxState.success) {
64+
return (
65+
<TxSuccessView
66+
action={<Trans>received</Trans>}
67+
amount={receivedSharesRef.current ?? previewShares?.value ?? '0'}
68+
symbol="sGHO"
69+
/>
70+
);
71+
}
72+
73+
return (
74+
<>
75+
<TxModalDetails gasLimit={gasLimit} chainId={targetChainId}>
76+
<DetailsNumberLineWithSub
77+
description={<Trans>Migrating</Trans>}
78+
futureValue={stkGhoBalance}
79+
futureValueUSD={stkGhoUSD}
80+
symbol="stkGHO"
81+
/>
82+
<DetailsNumberLineWithSub
83+
description={<Trans>You&apos;ll receive</Trans>}
84+
futureValue={previewShares?.value ?? '0'}
85+
futureValueUSD={sghoUSD}
86+
symbol="sGHO"
87+
loading={previewFetching}
88+
/>
89+
</TxModalDetails>
90+
91+
<StkGhoMigrateActions
92+
isWrongNetwork={isWrongNetwork}
93+
blocked={+stkGhoBalance <= 0}
94+
sx={{ mt: '48px' }}
95+
/>
96+
</>
97+
);
98+
};

src/hooks/compliance/service-compliance.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export type ComplianceCheckResponse = {
1111

1212
export const checkCompliance = async (address: string): Promise<ComplianceCheckResponse> => {
1313
try {
14-
const res = await fetch(`/api/preflight-compliance?address=${encodeURIComponent(address)}`);
14+
// NOTE: trailing slash is required because next.config.js sets `trailingSlash: true`.
15+
// Without it Next issues a 308 redirect that loops -> ERR_TOO_MANY_REDIRECTS.
16+
const res = await fetch(`/api/preflight-compliance/?address=${encodeURIComponent(address)}`);
1517
const data = await res.json();
1618

1719
if (res.ok) {

src/hooks/useModal.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export enum ModalType {
3939
SavingsGhoWithdraw,
4040
SGhoVaultDeposit,
4141
SGhoVaultWithdraw,
42+
StkGhoMigrate,
4243
CancelCowOrder,
4344

4445
// Swaps
@@ -151,6 +152,7 @@ export interface ModalContextType<T extends ModalArgsType> {
151152
openSavingsGhoWithdraw: () => void;
152153
openSGhoVaultDeposit: () => void;
153154
openSGhoVaultWithdraw: () => void;
155+
openStkGhoMigrate: () => void;
154156
openCancelCowOrder: (
155157
transaction: TransactionHistoryItem<SwapActionFields[ActionName.Swap]>
156158
) => void;
@@ -439,6 +441,11 @@ export const ModalContextProvider: React.FC<PropsWithChildren> = ({ children })
439441
setType(ModalType.SGhoVaultWithdraw);
440442
setArgs({ underlyingAsset: AaveV3Ethereum.ASSETS.GHO.UNDERLYING.toLowerCase() });
441443
},
444+
openStkGhoMigrate: () => {
445+
trackEvent(GENERAL.OPEN_MODAL, { modal: 'stkGHO to sGHO Migration' });
446+
setType(ModalType.StkGhoMigrate);
447+
setArgs({ underlyingAsset: AaveV3Ethereum.ASSETS.GHO.UNDERLYING.toLowerCase() });
448+
},
442449
openCancelCowOrder: (transaction) => {
443450
trackEvent(GENERAL.OPEN_MODAL, {
444451
modal: 'Cancel CoW Order',

src/locales/en/messages.po

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,6 +1997,10 @@ msgstr "Can't validate the wallet address. Try again."
19971997
msgid "GHO balance"
19981998
msgstr "GHO balance"
19991999

2000+
#: src/components/transactions/StkGhoMigrate/StkGhoMigrateModalContent.tsx
2001+
msgid "received"
2002+
msgstr "received"
2003+
20002004
#: pages/dashboard.page.tsx
20012005
#: src/components/transactions/Borrow/BorrowModal.tsx
20022006
#: src/modules/dashboard/lists/BorrowAssetsList/BorrowAssetsListItem.tsx
@@ -2344,6 +2348,7 @@ msgstr "VIEW"
23442348

23452349
#: src/components/transactions/SGhoVault/SGhoVaultDepositModalContent.tsx
23462350
#: src/components/transactions/SGhoVault/SGhoVaultWithdrawModalContent.tsx
2351+
#: src/components/transactions/StkGhoMigrate/StkGhoMigrateModalContent.tsx
23472352
msgid "You'll receive"
23482353
msgstr "You'll receive"
23492354

@@ -3182,6 +3187,10 @@ msgstr "To repay on behalf of a user an explicit amount to repay is needed"
31823187
msgid "Repayment amount to reach {0}% utilization"
31833188
msgstr "Repayment amount to reach {0}% utilization"
31843189

3190+
#: src/components/transactions/StkGhoMigrate/StkGhoMigrateActions.tsx
3191+
msgid "Proceed with migration"
3192+
msgstr "Proceed with migration"
3193+
31853194
#: src/modules/umbrella/UmbrellaModalContent.tsx
31863195
msgid "You can not stake this amount because it will cause collateral call"
31873196
msgstr "You can not stake this amount because it will cause collateral call"
@@ -3392,6 +3401,10 @@ msgstr "GHO yield with instant withdraws."
33923401
msgid "Both"
33933402
msgstr "Both"
33943403

3404+
#: src/components/transactions/StkGhoMigrate/StkGhoMigrateModal.tsx
3405+
msgid "Migrate stkGHO to sGHO"
3406+
msgstr "Migrate stkGHO to sGHO"
3407+
33953408
#: src/components/SecondsToString.tsx
33963409
msgid "{h}h"
33973410
msgstr "{h}h"
@@ -3891,6 +3904,8 @@ msgstr "Tip: Try improving your order parameters"
38913904

38923905
#: src/components/transactions/MigrateV3/MigrateV3Actions.tsx
38933906
#: src/components/transactions/StakingMigrate/StakingMigrateActions.tsx
3907+
#: src/components/transactions/StkGhoMigrate/StkGhoMigrateActions.tsx
3908+
#: src/components/transactions/StkGhoMigrate/StkGhoMigrateModalContent.tsx
38943909
msgid "Migrating"
38953910
msgstr "Migrating"
38963911

@@ -4138,6 +4153,7 @@ msgstr "Flashloan is disabled for this asset, hence this position cannot be migr
41384153
#: src/components/transactions/StakingMigrate/StakingMigrateActions.tsx
41394154
#: src/modules/markets/Gho/GhoBanner.tsx
41404155
#: src/modules/markets/Gho/GhoBanner.tsx
4156+
#: src/modules/stkGho/StkGhoDepositRow.tsx
41414157
msgid "Migrate"
41424158
msgstr "Migrate"
41434159

0 commit comments

Comments
 (0)