-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): Add Wormhole transfer hooks
- Loading branch information
Showing
6 changed files
with
468 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./useWormholeTransfer"; |
141 changes: 141 additions & 0 deletions
141
apps/ui/src/hooks/wormhole/useTransferEvmToEvmMutation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { | ||
getEmitterAddressEth, | ||
parseSequenceFromLogEth, | ||
} from "@certusone/wormhole-sdk"; | ||
import { isEvmEcosystemId } from "@swim-io/evm"; | ||
import { findOrThrow, humanToAtomic } from "@swim-io/utils"; | ||
import { useMutation } from "react-query"; | ||
import shallow from "zustand/shallow.js"; | ||
|
||
import { ECOSYSTEM_LIST, Protocol, getWormholeRetries } from "../../config"; | ||
import { selectConfig } from "../../core/selectors"; | ||
import { useEnvironment } from "../../core/store"; | ||
import type { WormholeTransfer } from "../../models"; | ||
import { | ||
formatWormholeAddress, | ||
getSignedVaaWithRetry, | ||
getWrappedTokenInfoFromNativeDetails, | ||
} from "../../models"; | ||
import { useWallets } from "../crossEcosystem"; | ||
import { useGetEvmClient } from "../evm"; | ||
|
||
export const useTransferEvmToEvmMutation = () => { | ||
const { chains, wormhole } = useEnvironment(selectConfig, shallow); | ||
const getEvmClient = useGetEvmClient(); | ||
const wallets = useWallets(); | ||
|
||
return useMutation( | ||
async ({ | ||
interactionId, | ||
value, | ||
sourceDetails, | ||
targetDetails, | ||
nativeDetails, | ||
onTxResult, | ||
}: WormholeTransfer) => { | ||
if (!wormhole) { | ||
throw new Error("No Wormhole RPC configured"); | ||
} | ||
|
||
const [sourceEcosystem, targetEcosystem] = [ | ||
sourceDetails.chainId, | ||
targetDetails.chainId, | ||
].map((chainId) => | ||
findOrThrow( | ||
ECOSYSTEM_LIST, | ||
(ecosystem) => ecosystem.wormholeChainId === chainId, | ||
), | ||
); | ||
const sourceEcosystemId = sourceEcosystem.id; | ||
if (!isEvmEcosystemId(sourceEcosystemId)) { | ||
throw new Error("Invalid source chain"); | ||
} | ||
const targetEcosystemId = targetEcosystem.id; | ||
if (!isEvmEcosystemId(targetEcosystemId)) { | ||
throw new Error("Invalid target chain"); | ||
} | ||
const sourceChain = findOrThrow( | ||
chains[Protocol.Evm], | ||
(chain) => chain.ecosystem === sourceEcosystemId, | ||
); | ||
const targetChain = findOrThrow( | ||
chains[Protocol.Evm], | ||
(chain) => chain.ecosystem === targetEcosystemId, | ||
); | ||
|
||
const evmWallet = wallets[sourceEcosystemId].wallet; | ||
if (evmWallet === null) { | ||
throw new Error("Missing EVM wallet"); | ||
} | ||
const evmWalletAddress = evmWallet.address; | ||
if (evmWalletAddress === null) { | ||
throw new Error("Missing EVM wallet address"); | ||
} | ||
|
||
const sourceClient = getEvmClient(sourceEcosystemId); | ||
const targetClient = getEvmClient(targetEcosystemId); | ||
|
||
await evmWallet.switchNetwork(sourceChain.chainId); | ||
// Process transfer if transfer txId does not exist | ||
const { approvalResponses, transferResponse } = | ||
await sourceClient.initiateWormholeTransfer({ | ||
atomicAmount: humanToAtomic(value, sourceDetails.decimals).toString(), | ||
interactionId, | ||
sourceAddress: sourceDetails.address, | ||
targetAddress: formatWormholeAddress(Protocol.Evm, evmWalletAddress), | ||
targetChainId: targetDetails.chainId, | ||
wallet: evmWallet, | ||
wrappedTokenInfo: getWrappedTokenInfoFromNativeDetails( | ||
sourceDetails.chainId, | ||
nativeDetails, | ||
), | ||
}); | ||
|
||
approvalResponses.forEach((response) => | ||
onTxResult({ | ||
chainId: sourceDetails.chainId, | ||
txId: response.hash, | ||
}), | ||
); | ||
|
||
const transferTxReceipt = await sourceClient.getTxReceiptOrThrow( | ||
transferResponse, | ||
); | ||
onTxResult({ | ||
chainId: sourceDetails.chainId, | ||
txId: transferTxReceipt.transactionHash, | ||
}); | ||
const sequence = parseSequenceFromLogEth( | ||
transferTxReceipt, | ||
sourceChain.wormhole.bridge, | ||
); | ||
const retries = getWormholeRetries(sourceDetails.chainId); | ||
const { vaaBytes: vaa } = await getSignedVaaWithRetry( | ||
[...wormhole.rpcUrls], | ||
sourceDetails.chainId, | ||
getEmitterAddressEth(sourceChain.wormhole.portal), | ||
sequence, | ||
undefined, | ||
undefined, | ||
retries, | ||
); | ||
|
||
await evmWallet.switchNetwork(targetChain.chainId); | ||
const redeemResponse = await targetClient.completeWormholeTransfer({ | ||
interactionId, | ||
vaa, | ||
wallet: evmWallet, | ||
}); | ||
if (redeemResponse === null) { | ||
throw new Error( | ||
`Transaction not found: (unlock/mint on ${targetEcosystemId})`, | ||
); | ||
} | ||
const evmReceipt = await targetClient.getTxReceiptOrThrow(redeemResponse); | ||
onTxResult({ | ||
chainId: targetDetails.chainId, | ||
txId: evmReceipt.transactionHash, | ||
}); | ||
}, | ||
); | ||
}; |
155 changes: 155 additions & 0 deletions
155
apps/ui/src/hooks/wormhole/useTransferEvmToSolanaMutation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import { | ||
getEmitterAddressEth, | ||
parseSequenceFromLogEth, | ||
} from "@certusone/wormhole-sdk"; | ||
import { Keypair } from "@solana/web3.js"; | ||
import { isEvmEcosystemId } from "@swim-io/evm"; | ||
import { SOLANA_ECOSYSTEM_ID, solana } from "@swim-io/solana"; | ||
import { findOrThrow, humanToAtomic } from "@swim-io/utils"; | ||
import { WormholeChainId } from "@swim-io/wormhole"; | ||
import { useMutation, useQueryClient } from "react-query"; | ||
import shallow from "zustand/shallow.js"; | ||
|
||
import { ECOSYSTEM_LIST, Protocol, getWormholeRetries } from "../../config"; | ||
import { selectConfig } from "../../core/selectors"; | ||
import { useEnvironment } from "../../core/store"; | ||
import type { WormholeTransfer } from "../../models"; | ||
import { | ||
findOrCreateSplTokenAccount, | ||
formatWormholeAddress, | ||
getSignedVaaWithRetry, | ||
getWrappedTokenInfoFromNativeDetails, | ||
} from "../../models"; | ||
import { useWallets } from "../crossEcosystem"; | ||
import { useGetEvmClient } from "../evm"; | ||
import { useSolanaClient, useSplTokenAccountsQuery } from "../solana"; | ||
|
||
export const useTransferEvmToSolanaMutation = () => { | ||
const queryClient = useQueryClient(); | ||
const { env } = useEnvironment(); | ||
const { chains, wormhole } = useEnvironment(selectConfig, shallow); | ||
const getEvmClient = useGetEvmClient(); | ||
const solanaClient = useSolanaClient(); | ||
const wallets = useWallets(); | ||
const solanaWallet = wallets[SOLANA_ECOSYSTEM_ID].wallet; | ||
const { data: splTokenAccounts = [] } = useSplTokenAccountsQuery(); | ||
|
||
return useMutation( | ||
async ({ | ||
interactionId, | ||
value, | ||
sourceDetails, | ||
targetDetails, | ||
nativeDetails, | ||
onTxResult, | ||
}: WormholeTransfer) => { | ||
if (!wormhole) { | ||
throw new Error("No Wormhole RPC configured"); | ||
} | ||
if (!solanaWallet) { | ||
throw new Error("No Solana wallet"); | ||
} | ||
if (targetDetails.chainId !== WormholeChainId.Solana) { | ||
throw new Error("Invalid target chain"); | ||
} | ||
|
||
const evmEcosystem = findOrThrow( | ||
ECOSYSTEM_LIST, | ||
(ecosystem) => ecosystem.wormholeChainId === sourceDetails.chainId, | ||
); | ||
const evmEcosystemId = evmEcosystem.id; | ||
if (!isEvmEcosystemId(evmEcosystemId)) { | ||
throw new Error("Invalid EVM chain"); | ||
} | ||
const evmChain = findOrThrow( | ||
chains[Protocol.Evm], | ||
({ ecosystem }) => ecosystem === evmEcosystemId, | ||
); | ||
const evmClient = getEvmClient(evmEcosystemId); | ||
const evmWallet = wallets[evmEcosystemId].wallet; | ||
if (!evmWallet) { | ||
throw new Error("Missing EVM wallet"); | ||
} | ||
|
||
const { tokenAccount, creationTxId } = await findOrCreateSplTokenAccount({ | ||
env, | ||
solanaClient, | ||
wallet: solanaWallet, | ||
queryClient, | ||
splTokenMintAddress: targetDetails.address, | ||
splTokenAccounts, | ||
}); | ||
const splTokenAccountAddress = tokenAccount.address.toBase58(); | ||
if (creationTxId) { | ||
onTxResult({ | ||
chainId: targetDetails.chainId, | ||
txId: creationTxId, | ||
}); | ||
} | ||
|
||
await evmWallet.switchNetwork(evmChain.chainId); | ||
// Process transfer if transfer txId does not exist | ||
const { approvalResponses, transferResponse } = | ||
await evmClient.initiateWormholeTransfer({ | ||
atomicAmount: humanToAtomic(value, sourceDetails.decimals).toString(), | ||
interactionId, | ||
sourceAddress: sourceDetails.address, | ||
targetAddress: formatWormholeAddress( | ||
Protocol.Solana, | ||
splTokenAccountAddress, | ||
), | ||
targetChainId: solana.wormholeChainId, | ||
wallet: evmWallet, | ||
wrappedTokenInfo: getWrappedTokenInfoFromNativeDetails( | ||
sourceDetails.chainId, | ||
nativeDetails, | ||
), | ||
}); | ||
|
||
approvalResponses.forEach((response) => | ||
onTxResult({ | ||
chainId: sourceDetails.chainId, | ||
txId: response.hash, | ||
}), | ||
); | ||
|
||
const transferTxReceipt = await evmClient.getTxReceiptOrThrow( | ||
transferResponse, | ||
); | ||
onTxResult({ | ||
chainId: sourceDetails.chainId, | ||
txId: transferTxReceipt.transactionHash, | ||
}); | ||
const sequence = parseSequenceFromLogEth( | ||
transferTxReceipt, | ||
evmChain.wormhole.bridge, | ||
); | ||
|
||
const auxiliarySigner = Keypair.generate(); | ||
const retries = getWormholeRetries(evmEcosystem.wormholeChainId); | ||
const { vaaBytes: vaa } = await getSignedVaaWithRetry( | ||
[...wormhole.rpcUrls], | ||
evmEcosystem.wormholeChainId, | ||
getEmitterAddressEth(evmChain.wormhole.portal), | ||
sequence, | ||
undefined, | ||
undefined, | ||
retries, | ||
); | ||
const unlockSplTokenTxIdsGenerator = | ||
solanaClient.generateCompleteWormholeTransferTxIds({ | ||
interactionId, | ||
vaa, | ||
wallet: solanaWallet, | ||
auxiliarySigner, | ||
}); | ||
|
||
for await (const txId of unlockSplTokenTxIdsGenerator) { | ||
onTxResult({ | ||
chainId: targetDetails.chainId, | ||
txId, | ||
}); | ||
} | ||
}, | ||
); | ||
}; |
Oops, something went wrong.