Skip to content

Commit

Permalink
feat(ui): Add Wormhole transfer hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
wormat committed Oct 13, 2022
1 parent f11f9da commit c00e40f
Show file tree
Hide file tree
Showing 6 changed files with 468 additions and 0 deletions.
1 change: 1 addition & 0 deletions apps/ui/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from "./solana";
export * from "./swim";
export * from "./utils";
export * from "./wallets";
export * from "./wormhole";
1 change: 1 addition & 0 deletions apps/ui/src/hooks/wormhole/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useWormholeTransfer";
141 changes: 141 additions & 0 deletions apps/ui/src/hooks/wormhole/useTransferEvmToEvmMutation.ts
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 apps/ui/src/hooks/wormhole/useTransferEvmToSolanaMutation.ts
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,
});
}
},
);
};
Loading

0 comments on commit c00e40f

Please sign in to comment.