Skip to content

Commit b5acf3e

Browse files
committed
NDEV-830. Upgraded Wormhole portal for Neon transfer
1 parent 42ecd2b commit b5acf3e

File tree

8 files changed

+240
-55
lines changed

8 files changed

+240
-55
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# dependencies
44
/node_modules
5+
.idea
56
/.pnp
67
.pnp.js
78

@@ -24,4 +25,4 @@ yarn-error.log*
2425

2526
# ethereum contracts
2627
/contracts
27-
/src/ethers-contracts
28+
/src/ethers-contracts

src/components/FeeMethodSelector.tsx

+86-45
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
CHAIN_ID_ACALA,
33
CHAIN_ID_KARURA,
4+
CHAIN_ID_NEON,
45
CHAIN_ID_TERRA,
56
hexToNativeAssetString,
67
isEVMChain,
@@ -32,6 +33,7 @@ import {
3233
} from "../store/selectors";
3334
import { setRelayerFee, setUseRelayer } from "../store/transferSlice";
3435
import { CHAINS_BY_ID, getDefaultNativeCurrencySymbol } from "../utils/consts";
36+
import { useNeonRelayerInfo } from "../hooks/useNeonRelayerInfo";
3537

3638
const useStyles = makeStyles((theme) => ({
3739
feeSelectorContainer: {
@@ -107,6 +109,11 @@ function FeeMethodSelector() {
107109
vaaNormalizedAmount,
108110
originChain ? hexToNativeAssetString(originAsset, originChain) : undefined
109111
);
112+
const neonRelayerInfo = useNeonRelayerInfo(
113+
targetChain,
114+
vaaNormalizedAmount,
115+
originChain ? hexToNativeAssetString(originAsset, originChain) : undefined
116+
);
110117
const sourceChain = useSelector(selectTransferSourceChain);
111118
const dispatch = useDispatch();
112119
const relayerSelected = !!useSelector(selectTransferUseRelayer);
@@ -123,13 +130,23 @@ function FeeMethodSelector() {
123130
targetChain === CHAIN_ID_ACALA || targetChain === CHAIN_ID_KARURA;
124131
const acalaRelayerEligible = acalaRelayerInfo.data?.shouldRelay;
125132

133+
const targetIsNeon = targetChain === CHAIN_ID_NEON;
134+
const neonRelayerEligible = neonRelayerInfo.data?.shouldRelay;
135+
126136
const chooseAcalaRelayer = useCallback(() => {
127137
if (targetIsAcala && acalaRelayerEligible) {
128138
dispatch(setUseRelayer(true));
129139
dispatch(setRelayerFee(undefined));
130140
}
131141
}, [dispatch, targetIsAcala, acalaRelayerEligible]);
132142

143+
const chooseNeonRelayer = useCallback(() => {
144+
if (targetIsNeon && neonRelayerEligible) {
145+
dispatch(setUseRelayer(true));
146+
dispatch(setRelayerFee(undefined));
147+
}
148+
}, [dispatch, targetIsNeon, neonRelayerEligible]);
149+
133150
const chooseRelayer = useCallback(() => {
134151
if (relayerEligible) {
135152
dispatch(setUseRelayer(true));
@@ -149,6 +166,12 @@ function FeeMethodSelector() {
149166
} else {
150167
chooseManual();
151168
}
169+
} else if (targetIsNeon) {
170+
if (neonRelayerEligible) {
171+
chooseNeonRelayer();
172+
} else {
173+
chooseManual();
174+
}
152175
} else if (relayerInfo.data?.isRelayable === true) {
153176
chooseRelayer();
154177
} else if (relayerInfo.data?.isRelayable === false) {
@@ -162,53 +185,67 @@ function FeeMethodSelector() {
162185
targetIsAcala,
163186
acalaRelayerEligible,
164187
chooseAcalaRelayer,
188+
targetIsNeon,
189+
neonRelayerEligible,
190+
chooseNeonRelayer,
165191
]);
166192

167-
const acalaRelayerContent = (
168-
<Card
169-
className={
170-
classes.optionCardBase +
171-
" " +
172-
(relayerSelected ? classes.optionCardSelected : "") +
173-
" " +
174-
(acalaRelayerEligible ? classes.optionCardSelectable : "")
175-
}
176-
onClick={chooseAcalaRelayer}
177-
>
178-
<div className={classes.alignCenterContainer}>
179-
<Checkbox
180-
checked={relayerSelected}
181-
disabled={!acalaRelayerEligible}
182-
onClick={chooseAcalaRelayer}
183-
className={classes.inlineBlock}
184-
/>
185-
<div className={clsx(classes.inlineBlock, classes.alignLeft)}>
186-
{acalaRelayerEligible ? (
187-
<div>
188-
<Typography variant="body1">
189-
{CHAINS_BY_ID[targetChain].name}
190-
</Typography>
191-
<Typography variant="body2" color="textSecondary">
192-
{CHAINS_BY_ID[targetChain].name} pays gas for you &#127881;
193-
</Typography>
194-
</div>
195-
) : (
196-
<>
197-
<Typography color="textSecondary" variant="body2">
198-
{"Automatic redeem is unavailable for this token."}
199-
</Typography>
200-
<div />
201-
</>
202-
)}
193+
const relayerContentFactory = (relayerEligible: any, chooseRelayer: any) => {
194+
return (
195+
<Card
196+
className={
197+
classes.optionCardBase +
198+
" " +
199+
(relayerSelected ? classes.optionCardSelected : "") +
200+
" " +
201+
(relayerEligible ? classes.optionCardSelectable : "")
202+
}
203+
onClick={chooseRelayer}
204+
>
205+
<div className={classes.alignCenterContainer}>
206+
<Checkbox
207+
checked={relayerSelected}
208+
disabled={!relayerEligible}
209+
onClick={chooseRelayer}
210+
className={classes.inlineBlock}
211+
/>
212+
<div className={clsx(classes.inlineBlock, classes.alignLeft)}>
213+
{relayerEligible ? (
214+
<>
215+
<Typography variant="body1">
216+
{CHAINS_BY_ID[targetChain].name}
217+
</Typography>
218+
<Typography variant="body2" color="textSecondary">
219+
{CHAINS_BY_ID[targetChain].name} pays gas for you &#127881;
220+
</Typography>
221+
</>
222+
) : (
223+
<>
224+
<Typography color="textSecondary" variant="body2">
225+
{"Automatic redeem is unavailable for this token."}
226+
</Typography>
227+
<div />
228+
</>
229+
)}
230+
</div>
203231
</div>
204-
</div>
205-
{acalaRelayerEligible ? (
206-
<>
207-
<div></div>
208-
<div></div>
209-
</>
210-
) : null}
211-
</Card>
232+
{relayerEligible ? (
233+
<>
234+
<div></div>
235+
<div></div>
236+
</>
237+
) : null}
238+
</Card>
239+
);
240+
};
241+
242+
const acalaRelayerContent = relayerContentFactory(
243+
acalaRelayerEligible,
244+
chooseAcalaRelayer
245+
);
246+
const neonRelayerContent = relayerContentFactory(
247+
neonRelayerEligible,
248+
chooseNeonRelayer
212249
);
213250

214251
const relayerContent = (
@@ -323,7 +360,11 @@ function FeeMethodSelector() {
323360
>
324361
How would you like to pay the target chain fees?
325362
</Typography>
326-
{targetIsAcala ? acalaRelayerContent : relayerContent}
363+
{targetIsAcala
364+
? acalaRelayerContent
365+
: targetIsNeon
366+
? neonRelayerContent
367+
: relayerContent}
327368
{manualRedeemContent}
328369
</div>
329370
);

src/components/Transfer/Redeem.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ function Redeem() {
9292
}, [useRelayer]);
9393
const targetChain = useSelector(selectTransferTargetChain);
9494
const targetIsAcala =
95-
targetChain === CHAIN_ID_ACALA || targetChain === CHAIN_ID_KARURA;
95+
targetChain === CHAIN_ID_ACALA ||
96+
targetChain === CHAIN_ID_KARURA ||
97+
targetChain === CHAIN_ID_NEON;
9698
const targetAsset = useSelector(selectTransferTargetAsset);
9799
const isRecovery = useSelector(selectTransferIsRecovery);
98100
const { isTransferCompletedLoading, isTransferCompleted } =
@@ -273,7 +275,6 @@ function Redeem() {
273275
</ButtonWithLoader>
274276
<WaitingForWalletMessage />
275277
</>
276-
277278
{useRelayer && !isTransferCompleted ? (
278279
<div className={classes.centered}>
279280
<Button

src/hooks/useHandleRedeem.tsx

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import {
2-
ChainId,
32
CHAIN_ID_ALGORAND,
43
CHAIN_ID_APTOS,
54
CHAIN_ID_INJECTIVE,
65
CHAIN_ID_KLAYTN,
76
CHAIN_ID_NEAR,
87
CHAIN_ID_SOLANA,
98
CHAIN_ID_XPLA,
9+
ChainId,
10+
CHAINS,
1011
isEVMChain,
1112
isTerraChain,
1213
postVaaSolanaWithRetry,
@@ -68,9 +69,10 @@ import {
6869
getTokenBridgeAddressForChain,
6970
MAX_VAA_UPLOAD_RETRIES_SOLANA,
7071
NEAR_TOKEN_BRIDGE_ACCOUNT,
71-
SOLANA_HOST,
72+
NEON_RELAY_URL,
7273
SOL_BRIDGE_ADDRESS,
7374
SOL_TOKEN_BRIDGE_ADDRESS,
75+
SOLANA_HOST,
7476
} from "../utils/consts";
7577
import { broadcastInjectiveTx } from "../utils/injective";
7678
import {
@@ -530,13 +532,22 @@ export function useHandleRedeem() {
530532
injAddress,
531533
]);
532534

535+
const getUrl = (targetChain: ChainId): string => {
536+
switch (targetChain) {
537+
case CHAINS.neon:
538+
return NEON_RELAY_URL;
539+
default:
540+
return ACALA_RELAY_URL;
541+
}
542+
};
543+
533544
const handleAcalaRelayerRedeemClick = useCallback(async () => {
534545
if (!signedVAA) return;
535546

536547
dispatch(setIsRedeeming(true));
537548

538549
try {
539-
const res = await axios.post(ACALA_RELAY_URL, {
550+
const res = await axios.post(getUrl(targetChain), {
540551
targetChain,
541552
signedVAA: uint8ArrayToHex(signedVAA),
542553
});

src/hooks/useNeonRelayerInfo.ts

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { ChainId } from "@certusone/wormhole-sdk";
2+
import { CHAIN_ID_NEON } from "@certusone/wormhole-sdk/lib/cjs/utils/consts";
3+
import axios from "axios";
4+
import { useEffect, useState } from "react";
5+
import { useDispatch, useSelector } from "react-redux";
6+
import {
7+
DataWrapper,
8+
errorDataWrapper,
9+
fetchDataWrapper,
10+
getEmptyDataWrapper,
11+
receiveDataWrapper,
12+
} from "../store/helpers";
13+
import { selectNeonRelayerInfo } from "../store/selectors";
14+
import {
15+
errorNeonRelayerInfo,
16+
fetchNeonRelayerInfo,
17+
receiveNeonRelayerInfo,
18+
setNeonRelayerInfo,
19+
} from "../store/transferSlice";
20+
import { NEON_RELAYER_URL, NEON_SHOULD_RELAY_URL } from "../utils/consts";
21+
22+
export interface NeonRelayerInfo {
23+
shouldRelay: boolean;
24+
msg: string;
25+
}
26+
27+
export const useNeonRelayerInfo = (
28+
targetChain: ChainId,
29+
vaaNormalizedAmount: string | undefined,
30+
originAsset: string | undefined,
31+
useStore: boolean = true
32+
) => {
33+
// within flow, update the store
34+
const dispatch = useDispatch();
35+
// within recover, use internal state
36+
const [state, setState] = useState<DataWrapper<NeonRelayerInfo>>(
37+
getEmptyDataWrapper()
38+
);
39+
useEffect(() => {
40+
let cancelled = false;
41+
if (
42+
!NEON_RELAYER_URL ||
43+
!targetChain ||
44+
targetChain !== CHAIN_ID_NEON ||
45+
!vaaNormalizedAmount ||
46+
!originAsset
47+
) {
48+
useStore
49+
? dispatch(setNeonRelayerInfo())
50+
: setState(getEmptyDataWrapper());
51+
return;
52+
}
53+
useStore ? dispatch(fetchNeonRelayerInfo()) : setState(fetchDataWrapper());
54+
(async () => {
55+
try {
56+
const result = await axios.get(NEON_SHOULD_RELAY_URL, {
57+
params: {
58+
targetChain,
59+
originAsset,
60+
amount: vaaNormalizedAmount,
61+
},
62+
});
63+
if (!cancelled) {
64+
useStore
65+
? dispatch(receiveNeonRelayerInfo(result.data))
66+
: setState(receiveDataWrapper(result.data));
67+
}
68+
} catch (e) {
69+
if (!cancelled) {
70+
useStore
71+
? dispatch(
72+
errorNeonRelayerInfo(
73+
"Failed to retrieve the Neon relayer info."
74+
)
75+
)
76+
: setState(
77+
errorDataWrapper("Failed to retrieve the Neon relayer info.")
78+
);
79+
}
80+
}
81+
})();
82+
return () => {
83+
cancelled = true;
84+
};
85+
}, [targetChain, vaaNormalizedAmount, originAsset, dispatch, useStore]);
86+
const neonRelayerInfoFromStore = useSelector(selectNeonRelayerInfo);
87+
return useStore ? neonRelayerInfoFromStore : state;
88+
};

src/store/selectors.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
CHAIN_ID_ACALA,
33
CHAIN_ID_KARURA,
4+
CHAIN_ID_NEON,
45
CHAIN_ID_SOLANA,
56
isEVMChain,
67
} from "@certusone/wormhole-sdk";
@@ -296,14 +297,16 @@ export const selectTransferTargetError = (state: RootState) => {
296297
state.transfer.relayerFee === undefined &&
297298
// Acala offers relaying without a fee for qualified tokens
298299
state.transfer.targetChain !== CHAIN_ID_ACALA &&
299-
state.transfer.targetChain !== CHAIN_ID_KARURA
300+
state.transfer.targetChain !== CHAIN_ID_KARURA &&
301+
state.transfer.targetChain !== CHAIN_ID_NEON
300302
) {
301303
return "Invalid relayer fee.";
302304
}
303305
if (
304306
state.transfer.useRelayer &&
305307
(state.transfer.targetChain === CHAIN_ID_ACALA ||
306-
state.transfer.targetChain === CHAIN_ID_KARURA) &&
308+
state.transfer.targetChain === CHAIN_ID_KARURA ||
309+
state.transfer.targetChain === CHAIN_ID_NEON) &&
307310
!state.transfer.acalaRelayerInfo.data?.shouldRelay
308311
) {
309312
return "Token is ineligible for relay.";
@@ -357,6 +360,8 @@ export const selectTransferRelayerFee = (state: RootState) =>
357360
state.transfer.relayerFee;
358361
export const selectAcalaRelayerInfo = (state: RootState) =>
359362
state.transfer.acalaRelayerInfo;
363+
export const selectNeonRelayerInfo = (state: RootState) =>
364+
state.transfer.neonRelayerInfo;
360365
export const selectSolanaTokenMap = (state: RootState) => {
361366
return state.tokens.solanaTokenMap;
362367
};

0 commit comments

Comments
 (0)