From ec1837b792ebff6d499382ec71a659946d52d0ae Mon Sep 17 00:00:00 2001 From: mfbz Date: Tue, 15 Jul 2025 18:53:21 +0200 Subject: [PATCH 01/12] Added crossvm hook to transfer tokens from evm to cadence --- .../cards/cross-vm-receive-token-card.tsx | 148 ++++++++++++ packages/demo/src/components/container.tsx | 2 + packages/demo/src/constants.ts | 3 + packages/kit/src/hooks/index.ts | 1 + .../src/hooks/useCrossVmReceiveToken.test.ts | 160 ++++++++++++ .../kit/src/hooks/useCrossVmReceiveToken.ts | 227 ++++++++++++++++++ 6 files changed, 541 insertions(+) create mode 100644 packages/demo/src/components/cards/cross-vm-receive-token-card.tsx create mode 100644 packages/kit/src/hooks/useCrossVmReceiveToken.test.ts create mode 100644 packages/kit/src/hooks/useCrossVmReceiveToken.ts diff --git a/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx b/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx new file mode 100644 index 000000000..2b6507816 --- /dev/null +++ b/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx @@ -0,0 +1,148 @@ +import {useCrossVmReceiveToken, useFlowConfig} from "@onflow/kit" +import {useState, useMemo} from "react" +import {getContractAddress} from "../../constants" + +export function CrossVmReceiveTokenCard() { + const config = useFlowConfig() + const currentNetwork = config.flowNetwork || "emulator" + const [vaultIdentifier, setVaultIdentifier] = useState("") + const [amount, setAmount] = useState("1000000000000000000") // 1 token (18 decimals for EVM) + + const { + receiveToken, + isPending, + data: transactionId, + error, + } = useCrossVmReceiveToken() + + const isNetworkSupported = currentNetwork === "testnet" + + const clickTokenData = useMemo(() => { + if (currentNetwork !== "testnet") return null + + const clickTokenAddress = getContractAddress("ClickToken", currentNetwork) + return { + name: "ClickToken", + vaultIdentifier: `A.${clickTokenAddress.replace("0x", "")}.EVMVMBridgedToken_a7cf2260e501952c71189d04fad17c704dfb36e6.Vault`, + amount: "1000000000000000000", // 1 ClickToken (18 decimals for EVM) + } + }, [currentNetwork]) + + // Set default vault identifier when network changes + useMemo(() => { + if (clickTokenData && !vaultIdentifier) { + setVaultIdentifier(clickTokenData.vaultIdentifier) + } + }, [clickTokenData, vaultIdentifier]) + + const handleReceiveToken = () => { + receiveToken({ + vaultIdentifier, + amount, + }) + } + + if (!isNetworkSupported) { + return ( +
+

+ useCrossVmReceiveToken +

+
+

+ Network not supported: This feature is only + available on testnet (with ClickToken ERC20). Current network:{" "} + {currentNetwork} +

+
+
+ ) + } + + return ( +
+

+ useCrossVmReceiveToken +

+
+ + + setVaultIdentifier(e.target.value)} + placeholder={ + clickTokenData + ? clickTokenData.vaultIdentifier + : "e.g., A.dfc20aee650fcbdf.EVMVMBridgedToken_a7cf2260e501952c71189d04fad17c704dfb36e6.Vault" + } + className="p-3 border-2 border-[#00EF8B] rounded-md text-sm text-black bg-white + outline-none transition-colors duration-200 ease-in-out w-full mb-4 font-mono" + /> + + + setAmount(e.target.value)} + placeholder="e.g., 100000000000000000 (0.1 token with 18 decimals)" + className="p-3 border-2 border-[#00EF8B] rounded-md text-sm text-black bg-white + outline-none transition-colors duration-200 ease-in-out w-full mb-4 font-mono" + /> + + +
+ +
+

Transaction Status:

+ + {isPending && ( +

Receiving tokens from EVM...

+ )} + + {error && ( +
+ Error: {error.message} +
+ )} + + {transactionId && !isPending && !error && ( +
+

+ Tokens received successfully! +

+

+ Transaction ID: {transactionId} +

+
+ )} + + {!transactionId && !isPending && !error && ( +
+

+ Click "Receive Token" to bridge tokens from EVM to Cadence +

+
+ )} +
+
+ ) +} diff --git a/packages/demo/src/components/container.tsx b/packages/demo/src/components/container.tsx index b82bfdcc7..3a6b38b2d 100644 --- a/packages/demo/src/components/container.tsx +++ b/packages/demo/src/components/container.tsx @@ -9,6 +9,7 @@ import {FlowQueryCard} from "./cards/flow-query-card" import {FlowQueryRawCard} from "./cards/flow-query-raw-card" import {FlowRevertibleRandomCard} from "./cards/flow-revertible-random-card" import {FlowTransactionStatusCard} from "./cards/flow-transaction-status-card" +import {CrossVmReceiveTokenCard} from "./cards/cross-vm-receive-token-card" import {KitConnectCard} from "./kits/kit-connect-card" import {KitTransactionButtonCard} from "./kits/kit-transaction-button-card" import {KitTransactionDialogCard} from "./kits/kit-transaction-dialog-card" @@ -28,6 +29,7 @@ export function Container() { +

Components

diff --git a/packages/demo/src/constants.ts b/packages/demo/src/constants.ts index 85759b509..2469ef575 100644 --- a/packages/demo/src/constants.ts +++ b/packages/demo/src/constants.ts @@ -21,6 +21,9 @@ export const CONTRACT_ADDRESSES: Record> = { testnet: "0x7e60df042a9c0868", mainnet: "0x1654653399040a61", }, + ClickToken: { + testnet: "0xdfc20aee650fcbdf", + }, } // Helper function to get contract address for current network diff --git a/packages/kit/src/hooks/index.ts b/packages/kit/src/hooks/index.ts index 78a5cf89e..139d80f43 100644 --- a/packages/kit/src/hooks/index.ts +++ b/packages/kit/src/hooks/index.ts @@ -15,4 +15,5 @@ export {useFlowTransaction} from "./useFlowTransaction" export {useFlowTransactionStatus} from "./useFlowTransactionStatus" export {useCrossVmSpendNft} from "./useCrossVmSpendNft" export {useCrossVmSpendToken} from "./useCrossVmSpendToken" +export {useCrossVmReceiveToken} from "./useCrossVmReceiveToken" export {useCrossVmTransactionStatus} from "./useCrossVmTransactionStatus" diff --git a/packages/kit/src/hooks/useCrossVmReceiveToken.test.ts b/packages/kit/src/hooks/useCrossVmReceiveToken.test.ts new file mode 100644 index 000000000..f9ba6b292 --- /dev/null +++ b/packages/kit/src/hooks/useCrossVmReceiveToken.test.ts @@ -0,0 +1,160 @@ +import {renderHook, act, waitFor} from "@testing-library/react" +import * as fcl from "@onflow/fcl" +import {FlowProvider} from "../provider" +import { + getCrossVmReceiveTokenTransaction, + useCrossVmReceiveToken, +} from "./useCrossVmReceiveToken" +import {useFlowChainId} from "./useFlowChainId" + +jest.mock("@onflow/fcl", () => require("../__mocks__/fcl").default) +jest.mock("./useFlowChainId", () => ({ + useFlowChainId: jest.fn(), +})) + +describe("useCrossVmReceiveToken", () => { + const mockTxId = "0x123" + const mockTxResult = { + events: [ + { + type: "TransactionExecuted", + data: { + hash: ["1", "2", "3"], + errorCode: "0", + errorMessage: "", + }, + }, + ], + } + + beforeEach(() => { + jest.clearAllMocks() + jest.mocked(useFlowChainId).mockReturnValue({ + data: "testnet", + isLoading: false, + } as any) + }) + + describe("getCrossVmReceiveTokenTransaction", () => { + it("should return correct cadence for mainnet", () => { + const result = getCrossVmReceiveTokenTransaction("mainnet") + expect(result).toContain("import EVM from 0xe467b9dd11fa00df") + }) + + it("should return correct cadence for testnet", () => { + const result = getCrossVmReceiveTokenTransaction("testnet") + expect(result).toContain("import EVM from 0x8c5303eaa26202d6") + }) + + it("should throw error for unsupported chain", () => { + expect(() => getCrossVmReceiveTokenTransaction("unsupported")).toThrow( + "Unsupported chain: unsupported" + ) + }) + }) + + describe("useCrossVmReceiveToken", () => { + test("should handle successful transaction", async () => { + jest.mocked(fcl.mutate).mockResolvedValue(mockTxId) + jest.mocked(fcl.tx).mockReturnValue({ + onceExecuted: jest.fn().mockResolvedValue(mockTxResult), + } as any) + + let result: any + let rerender: any + await act(async () => { + ;({result, rerender} = renderHook(useCrossVmReceiveToken, { + wrapper: FlowProvider, + })) + }) + + await act(async () => { + await result.current.receiveToken({ + vaultIdentifier: "A.dfc20aee650fcbdf.ClickToken.Vault", + amount: "1000000000000000000", + }) + rerender() + }) + + await waitFor(() => result.current.isPending === false) + + expect(result.current.isError).toBe(false) + expect(result.current.data).toBe(mockTxId) + }) + + it("should handle missing chain ID", async () => { + ;(useFlowChainId as jest.Mock).mockReturnValue({ + data: null, + isLoading: false, + }) + + let hookResult: any + + await act(async () => { + const {result} = renderHook(() => useCrossVmReceiveToken(), { + wrapper: FlowProvider, + }) + hookResult = result + }) + + await act(async () => { + await hookResult.current.receiveToken({ + vaultIdentifier: "A.dfc20aee650fcbdf.ClickToken.Vault", + amount: "1000000000000000000", + }) + }) + + await waitFor(() => expect(hookResult.current.isError).toBe(true)) + expect(hookResult.current.error?.message).toBe("No current chain found") + }) + + it("should handle loading chain ID", async () => { + ;(useFlowChainId as jest.Mock).mockReturnValue({ + data: null, + isLoading: true, + }) + + let hookResult: any + + await act(async () => { + const {result} = renderHook(() => useCrossVmReceiveToken(), { + wrapper: FlowProvider, + }) + hookResult = result + }) + + await act(async () => { + await hookResult.current.receiveToken({ + vaultIdentifier: "A.dfc20aee650fcbdf.ClickToken.Vault", + amount: "1000000000000000000", + }) + }) + + await waitFor(() => expect(hookResult.current.isError).toBe(true)) + expect(hookResult.current.error?.message).toBe("No current chain found") + }) + + it("should handle mutation error", async () => { + ;(fcl.mutate as jest.Mock).mockRejectedValue(new Error("Mutation failed")) + + let hookResult: any + + await act(async () => { + const {result} = renderHook(() => useCrossVmReceiveToken(), { + wrapper: FlowProvider, + }) + hookResult = result + }) + + await act(async () => { + await hookResult.current.receiveToken({ + vaultIdentifier: "A.dfc20aee650fcbdf.ClickToken.Vault", + amount: "1000000000000000000", + }) + }) + + await waitFor(() => expect(hookResult.current.isError).toBe(true)) + expect(hookResult.current.error?.message).toBe("Mutation failed") + }) + }) +}) diff --git a/packages/kit/src/hooks/useCrossVmReceiveToken.ts b/packages/kit/src/hooks/useCrossVmReceiveToken.ts new file mode 100644 index 000000000..fddb27381 --- /dev/null +++ b/packages/kit/src/hooks/useCrossVmReceiveToken.ts @@ -0,0 +1,227 @@ +import * as fcl from "@onflow/fcl" +import { + UseMutateAsyncFunction, + UseMutateFunction, + useMutation, + UseMutationOptions, + UseMutationResult, +} from "@tanstack/react-query" +import {useFlowChainId} from "./useFlowChainId" +import {useFlowQueryClient} from "../provider/FlowQueryClient" +import {CONTRACT_ADDRESSES} from "../constants" + +export interface UseCrossVmReceiveTokenArgs { + mutation?: Omit< + UseMutationOptions, + "mutationFn" + > +} + +export interface UseCrossVmReceiveTokenMutateArgs { + vaultIdentifier: string + amount: string +} + +export interface UseCrossVmReceiveTokenResult + extends Omit, "mutate" | "mutateAsync"> { + receiveToken: UseMutateFunction< + string, + Error, + UseCrossVmReceiveTokenMutateArgs + > + receiveTokenAsync: UseMutateAsyncFunction< + string, + Error, + UseCrossVmReceiveTokenMutateArgs + > +} + +// Takes a chain id and returns the cadence tx with addresses set +export const getCrossVmReceiveTokenTransaction = (chainId: string) => { + const contractAddresses = + CONTRACT_ADDRESSES[chainId as keyof typeof CONTRACT_ADDRESSES] + if (!contractAddresses) { + throw new Error(`Unsupported chain: ${chainId}`) + } + + return ` +import FungibleToken from ${contractAddresses.FungibleToken} +import ViewResolver from ${contractAddresses.ViewResolver} +import FungibleTokenMetadataViews from ${contractAddresses.FungibleTokenMetadataViews} +import FlowToken from ${contractAddresses.FlowToken} + +import ScopedFTProviders from ${contractAddresses.ScopedFTProviders} + +import EVM from ${contractAddresses.EVM} + +import FlowEVMBridge from ${contractAddresses.FlowEVMBridge} +import FlowEVMBridgeConfig from ${contractAddresses.FlowEVMBridgeConfig} +import FlowEVMBridgeUtils from ${contractAddresses.FlowEVMBridgeUtils} + +/// This transaction bridges fungible tokens from EVM to Cadence assuming it has already been onboarded to the +/// FlowEVMBridge. +/// +/// NOTE: The ERC20 must have first been onboarded to the bridge. This can be checked via the method +/// FlowEVMBridge.evmAddressRequiresOnboarding(address: self.evmContractAddress) +/// +/// @param vaultIdentifier: The Cadence type identifier of the FungibleToken Vault to bridge +/// - e.g. vault.getType().identifier +/// @param amount: The amount of tokens to bridge from EVM +/// +transaction(vaultIdentifier: String, amount: UInt256) { + + let vaultType: Type + let receiver: &{FungibleToken.Vault} + let scopedProvider: @ScopedFTProviders.ScopedFTProvider + let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount + + prepare(signer: auth(BorrowValue, CopyValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) { + + /* --- Reference the signer's CadenceOwnedAccount --- */ + // + // Borrow a reference to the signer's COA + self.coa = signer.storage.borrow(from: /storage/evm) + ?? panic("Could not borrow COA signer's account at path /storage/evm") + + /* --- Construct the Vault type --- */ + // + // Construct the Vault type from the provided identifier + self.vaultType = CompositeType(vaultIdentifier) + ?? panic("Could not construct Vault type from identifier: ".concat(vaultIdentifier)) + // Parse the Vault identifier into its components + let tokenContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: self.vaultType) + ?? panic("Could not get contract address from identifier: ".concat(vaultIdentifier)) + let tokenContractName = FlowEVMBridgeUtils.getContractName(fromType: self.vaultType) + ?? panic("Could not get contract name from identifier: ".concat(vaultIdentifier)) + + /* --- Reference the signer's Vault --- */ + // + // Borrow a reference to the FungibleToken Vault, configuring if necessary + let viewResolver = getAccount(tokenContractAddress).contracts.borrow<&{ViewResolver}>(name: tokenContractName) + ?? panic("Could not borrow ViewResolver from FungibleToken contract with name" + .concat(tokenContractName).concat(" and address ") + .concat(tokenContractAddress.toString())) + let vaultData = viewResolver.resolveContractView( + resourceType: self.vaultType, + viewType: Type() + ) as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Could not resolve FTVaultData view for Vault type ".concat(self.vaultType.identifier)) + // If the vault does not exist, create it and publish according to the contract's defined configuration + if signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) == nil { + signer.storage.save(<-vaultData.createEmptyVault(), to: vaultData.storagePath) + + signer.capabilities.unpublish(vaultData.receiverPath) + signer.capabilities.unpublish(vaultData.metadataPath) + + let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) + let metadataCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) + + signer.capabilities.publish(receiverCap, at: vaultData.receiverPath) + signer.capabilities.publish(metadataCap, at: vaultData.metadataPath) + } + self.receiver = signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) + ?? panic("Could not borrow FungibleToken Vault from storage path ".concat(vaultData.storagePath.toString())) + + /* --- Configure a ScopedFTProvider --- */ + // + // Set a cap on the withdrawable bridge fee + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction + ) + + // Issue and store bridge-dedicated Provider Capability in storage if necessary + if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil { + let providerCap = signer.capabilities.storage.issue( + /storage/flowTokenVault + ) + signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath) + } + // Copy the stored Provider capability and create a ScopedFTProvider + let providerCapCopy = signer.storage.copy>( + from: FlowEVMBridgeConfig.providerCapabilityStoragePath + ) ?? panic("Invalid FungibleToken Provider Capability found in storage at path " + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) + let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( + provider: providerCapCopy, + filters: [ providerFilter ], + expiration: getCurrentBlock().timestamp + 1.0 + ) + } + + execute { + // Execute the bridge request + let vault: @{FungibleToken.Vault} <- self.coa.withdrawTokens( + type: self.vaultType, + amount: amount, + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + // Ensure the bridged vault is the correct type + assert( + vault.getType() == self.vaultType, + message: "Bridged vault type mismatch - requested: ".concat(self.vaultType.identifier) + .concat(", received: ").concat(vault.getType().identifier) + ) + // Deposit the bridged token into the signer's vault + self.receiver.deposit(from: <-vault) + // Destroy the ScopedFTProvider + destroy self.scopedProvider + } +} +` +} + +/** + * Hook to receive a cross-VM FT transaction from EVM to Cadence. This function will + * withdraw tokens from the signer's COA in EVM and deposit them into their Cadence vault. + * + * @returns The mutation object used to send the transaction. + */ +export function useCrossVmReceiveToken({ + mutation: mutationOptions = {}, +}: UseCrossVmReceiveTokenArgs = {}): UseCrossVmReceiveTokenResult { + const chainId = useFlowChainId() + const cadenceTx = chainId.data + ? getCrossVmReceiveTokenTransaction(chainId.data) + : null + + const queryClient = useFlowQueryClient() + const mutation = useMutation( + { + mutationFn: async ({ + vaultIdentifier, + amount, + }: UseCrossVmReceiveTokenMutateArgs) => { + if (!cadenceTx) { + throw new Error("No current chain found") + } + + const txId = await fcl.mutate({ + cadence: cadenceTx, + args: (arg, t) => [ + arg(vaultIdentifier, t.String), + arg(amount, t.UInt256), + ], + limit: 9999, + }) + + return txId + }, + retry: false, + ...mutationOptions, + }, + queryClient + ) + + const { + mutate: receiveToken, + mutateAsync: receiveTokenAsync, + ...rest + } = mutation + + return { + receiveToken, + receiveTokenAsync, + ...rest, + } +} From 98619db3daa48e059ed02f7c0c6c394e13293c5e Mon Sep 17 00:00:00 2001 From: mfbz Date: Tue, 15 Jul 2025 18:53:59 +0200 Subject: [PATCH 02/12] Added changeset --- .changeset/sharp-donkeys-fly.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/sharp-donkeys-fly.md diff --git a/.changeset/sharp-donkeys-fly.md b/.changeset/sharp-donkeys-fly.md new file mode 100644 index 000000000..ef277174f --- /dev/null +++ b/.changeset/sharp-donkeys-fly.md @@ -0,0 +1,9 @@ +--- +"@onflow/fcl-core": minor +"@onflow/fcl-wc": minor +"@onflow/demo": minor +"@onflow/kit": minor +"@onflow/sdk": minor +--- + +Added crossvm hook to transfer tokens from evm to cadence From 6d7884eec5667299d4487db24977be2248da7c60 Mon Sep 17 00:00:00 2001 From: mfbz Date: Tue, 15 Jul 2025 19:32:58 +0200 Subject: [PATCH 03/12] Minor text fix --- .../demo/src/components/cards/cross-vm-receive-token-card.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx b/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx index 2b6507816..cb3df8d6f 100644 --- a/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx +++ b/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx @@ -51,8 +51,7 @@ export function CrossVmReceiveTokenCard() {

Network not supported: This feature is only - available on testnet (with ClickToken ERC20). Current network:{" "} - {currentNetwork} + available on testnet.

From 199a166d38221e8375f3a4df66dc91ad3a84fcca Mon Sep 17 00:00:00 2001 From: mfbz Date: Fri, 15 Aug 2025 22:23:09 +0200 Subject: [PATCH 04/12] Fixed wrong import --- .../demo/src/components/cards/cross-vm-receive-token-card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx b/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx index cb3df8d6f..e0c049188 100644 --- a/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx +++ b/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx @@ -1,4 +1,4 @@ -import {useCrossVmReceiveToken, useFlowConfig} from "@onflow/kit" +import {useCrossVmReceiveToken, useFlowConfig} from "@onflow/react-sdk" import {useState, useMemo} from "react" import {getContractAddress} from "../../constants" From 3ed88cc8178206e393580a8ec301407972aee7fc Mon Sep 17 00:00:00 2001 From: mfbz Date: Fri, 15 Aug 2025 22:32:14 +0200 Subject: [PATCH 05/12] Fixed changeset --- .changeset/chilly-kangaroos-train.md | 6 ++++++ .changeset/sharp-donkeys-fly.md | 9 --------- 2 files changed, 6 insertions(+), 9 deletions(-) create mode 100644 .changeset/chilly-kangaroos-train.md delete mode 100644 .changeset/sharp-donkeys-fly.md diff --git a/.changeset/chilly-kangaroos-train.md b/.changeset/chilly-kangaroos-train.md new file mode 100644 index 000000000..04c27364a --- /dev/null +++ b/.changeset/chilly-kangaroos-train.md @@ -0,0 +1,6 @@ +--- +"@onflow/react-sdk": minor +"@onflow/demo": minor +--- + +Added crossvm hook to transfer tokens from evm to cadence diff --git a/.changeset/sharp-donkeys-fly.md b/.changeset/sharp-donkeys-fly.md deleted file mode 100644 index ef277174f..000000000 --- a/.changeset/sharp-donkeys-fly.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@onflow/fcl-core": minor -"@onflow/fcl-wc": minor -"@onflow/demo": minor -"@onflow/kit": minor -"@onflow/sdk": minor ---- - -Added crossvm hook to transfer tokens from evm to cadence From 9a14d98c33ac7df5b83c07335bf3d8581e80f6ca Mon Sep 17 00:00:00 2001 From: mfbz Date: Fri, 15 Aug 2025 22:49:17 +0200 Subject: [PATCH 06/12] Fixed failing tests --- .../react-sdk/src/hooks/useCrossVmReceiveToken.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/react-sdk/src/hooks/useCrossVmReceiveToken.test.ts b/packages/react-sdk/src/hooks/useCrossVmReceiveToken.test.ts index f9ba6b292..7dda30138 100644 --- a/packages/react-sdk/src/hooks/useCrossVmReceiveToken.test.ts +++ b/packages/react-sdk/src/hooks/useCrossVmReceiveToken.test.ts @@ -6,13 +6,19 @@ import { useCrossVmReceiveToken, } from "./useCrossVmReceiveToken" import {useFlowChainId} from "./useFlowChainId" +import {createMockFclInstance, MockFclInstance} from "../__mocks__/flow-client" jest.mock("@onflow/fcl", () => require("../__mocks__/fcl").default) jest.mock("./useFlowChainId", () => ({ useFlowChainId: jest.fn(), })) +jest.mock("./useFlowClient", () => ({ + useFlowClient: jest.fn(), +})) describe("useCrossVmReceiveToken", () => { + let mockFcl: MockFclInstance + const mockTxId = "0x123" const mockTxResult = { events: [ @@ -33,6 +39,9 @@ describe("useCrossVmReceiveToken", () => { data: "testnet", isLoading: false, } as any) + + mockFcl = createMockFclInstance() + jest.mocked(fcl.createFlowClient).mockReturnValue(mockFcl.mockFclInstance) }) describe("getCrossVmReceiveTokenTransaction", () => { From 240fcf8e4b8dc7920238b2cfc24c83b485db29de Mon Sep 17 00:00:00 2001 From: mfbz Date: Thu, 2 Oct 2025 17:02:34 +0200 Subject: [PATCH 07/12] Renamed hook --- packages/react-sdk/src/hooks/index.ts | 2 +- ....test.ts => useBridgeTokenFromEvm.test.ts} | 34 +++++++++--------- ...ceiveToken.ts => useBridgeTokenFromEvm.ts} | 36 +++++++++---------- 3 files changed, 36 insertions(+), 36 deletions(-) rename packages/react-sdk/src/hooks/{useCrossVmReceiveToken.test.ts => useBridgeTokenFromEvm.test.ts} (81%) rename packages/react-sdk/src/hooks/{useCrossVmReceiveToken.ts => useBridgeTokenFromEvm.ts} (90%) diff --git a/packages/react-sdk/src/hooks/index.ts b/packages/react-sdk/src/hooks/index.ts index 96a9a5b44..760040ab6 100644 --- a/packages/react-sdk/src/hooks/index.ts +++ b/packages/react-sdk/src/hooks/index.ts @@ -15,5 +15,5 @@ export {useFlowTransaction} from "./useFlowTransaction" export {useFlowTransactionStatus} from "./useFlowTransactionStatus" export {useCrossVmSpendNft} from "./useCrossVmSpendNft" export {useCrossVmSpendToken} from "./useCrossVmSpendToken" -export {useCrossVmReceiveToken} from "./useCrossVmReceiveToken" +export {useBridgeTokenFromEvm} from "./useBridgeTokenFromEvm" export {useCrossVmTransactionStatus} from "./useCrossVmTransactionStatus" diff --git a/packages/react-sdk/src/hooks/useCrossVmReceiveToken.test.ts b/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.test.ts similarity index 81% rename from packages/react-sdk/src/hooks/useCrossVmReceiveToken.test.ts rename to packages/react-sdk/src/hooks/useBridgeTokenFromEvm.test.ts index 7dda30138..9ee790933 100644 --- a/packages/react-sdk/src/hooks/useCrossVmReceiveToken.test.ts +++ b/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.test.ts @@ -2,9 +2,9 @@ import {renderHook, act, waitFor} from "@testing-library/react" import * as fcl from "@onflow/fcl" import {FlowProvider} from "../provider" import { - getCrossVmReceiveTokenTransaction, - useCrossVmReceiveToken, -} from "./useCrossVmReceiveToken" + getBridgeTokenFromEvmTransaction, + useBridgeTokenFromEvm, +} from "./useBridgeTokenFromEvm" import {useFlowChainId} from "./useFlowChainId" import {createMockFclInstance, MockFclInstance} from "../__mocks__/flow-client" @@ -16,7 +16,7 @@ jest.mock("./useFlowClient", () => ({ useFlowClient: jest.fn(), })) -describe("useCrossVmReceiveToken", () => { +describe("useBridgeTokenFromEvm", () => { let mockFcl: MockFclInstance const mockTxId = "0x123" @@ -44,25 +44,25 @@ describe("useCrossVmReceiveToken", () => { jest.mocked(fcl.createFlowClient).mockReturnValue(mockFcl.mockFclInstance) }) - describe("getCrossVmReceiveTokenTransaction", () => { + describe("getBridgeTokenFromEvmTransaction", () => { it("should return correct cadence for mainnet", () => { - const result = getCrossVmReceiveTokenTransaction("mainnet") + const result = getBridgeTokenFromEvmTransaction("mainnet") expect(result).toContain("import EVM from 0xe467b9dd11fa00df") }) it("should return correct cadence for testnet", () => { - const result = getCrossVmReceiveTokenTransaction("testnet") + const result = getBridgeTokenFromEvmTransaction("testnet") expect(result).toContain("import EVM from 0x8c5303eaa26202d6") }) it("should throw error for unsupported chain", () => { - expect(() => getCrossVmReceiveTokenTransaction("unsupported")).toThrow( + expect(() => getBridgeTokenFromEvmTransaction("unsupported")).toThrow( "Unsupported chain: unsupported" ) }) }) - describe("useCrossVmReceiveToken", () => { + describe("useBridgeTokenFromEvmTx", () => { test("should handle successful transaction", async () => { jest.mocked(fcl.mutate).mockResolvedValue(mockTxId) jest.mocked(fcl.tx).mockReturnValue({ @@ -72,13 +72,13 @@ describe("useCrossVmReceiveToken", () => { let result: any let rerender: any await act(async () => { - ;({result, rerender} = renderHook(useCrossVmReceiveToken, { + ;({result, rerender} = renderHook(useBridgeTokenFromEvm, { wrapper: FlowProvider, })) }) await act(async () => { - await result.current.receiveToken({ + await result.current.bridgeTokenFromEvm({ vaultIdentifier: "A.dfc20aee650fcbdf.ClickToken.Vault", amount: "1000000000000000000", }) @@ -100,14 +100,14 @@ describe("useCrossVmReceiveToken", () => { let hookResult: any await act(async () => { - const {result} = renderHook(() => useCrossVmReceiveToken(), { + const {result} = renderHook(() => useBridgeTokenFromEvm(), { wrapper: FlowProvider, }) hookResult = result }) await act(async () => { - await hookResult.current.receiveToken({ + await hookResult.current.bridgeTokenFromEvm({ vaultIdentifier: "A.dfc20aee650fcbdf.ClickToken.Vault", amount: "1000000000000000000", }) @@ -126,14 +126,14 @@ describe("useCrossVmReceiveToken", () => { let hookResult: any await act(async () => { - const {result} = renderHook(() => useCrossVmReceiveToken(), { + const {result} = renderHook(() => useBridgeTokenFromEvm(), { wrapper: FlowProvider, }) hookResult = result }) await act(async () => { - await hookResult.current.receiveToken({ + await hookResult.current.bridgeTokenFromEvm({ vaultIdentifier: "A.dfc20aee650fcbdf.ClickToken.Vault", amount: "1000000000000000000", }) @@ -149,14 +149,14 @@ describe("useCrossVmReceiveToken", () => { let hookResult: any await act(async () => { - const {result} = renderHook(() => useCrossVmReceiveToken(), { + const {result} = renderHook(() => useBridgeTokenFromEvm(), { wrapper: FlowProvider, }) hookResult = result }) await act(async () => { - await hookResult.current.receiveToken({ + await hookResult.current.bridgeTokenFromEvm({ vaultIdentifier: "A.dfc20aee650fcbdf.ClickToken.Vault", amount: "1000000000000000000", }) diff --git a/packages/react-sdk/src/hooks/useCrossVmReceiveToken.ts b/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.ts similarity index 90% rename from packages/react-sdk/src/hooks/useCrossVmReceiveToken.ts rename to packages/react-sdk/src/hooks/useBridgeTokenFromEvm.ts index fddb27381..d11ad4bc3 100644 --- a/packages/react-sdk/src/hooks/useCrossVmReceiveToken.ts +++ b/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.ts @@ -10,34 +10,34 @@ import {useFlowChainId} from "./useFlowChainId" import {useFlowQueryClient} from "../provider/FlowQueryClient" import {CONTRACT_ADDRESSES} from "../constants" -export interface UseCrossVmReceiveTokenArgs { +export interface UseBridgeTokenFromEvmArgs { mutation?: Omit< - UseMutationOptions, + UseMutationOptions, "mutationFn" > } -export interface UseCrossVmReceiveTokenMutateArgs { +export interface UseBridgeTokenFromEvmMutateArgs { vaultIdentifier: string amount: string } -export interface UseCrossVmReceiveTokenResult +export interface UseBridgeTokenFromEvmResult extends Omit, "mutate" | "mutateAsync"> { - receiveToken: UseMutateFunction< + bridgeTokenFromEvm: UseMutateFunction< string, Error, - UseCrossVmReceiveTokenMutateArgs + UseBridgeTokenFromEvmMutateArgs > - receiveTokenAsync: UseMutateAsyncFunction< + bridgeTokenFromEvmAsync: UseMutateAsyncFunction< string, Error, - UseCrossVmReceiveTokenMutateArgs + UseBridgeTokenFromEvmMutateArgs > } // Takes a chain id and returns the cadence tx with addresses set -export const getCrossVmReceiveTokenTransaction = (chainId: string) => { +export const getBridgeTokenFromEvmTransaction = (chainId: string) => { const contractAddresses = CONTRACT_ADDRESSES[chainId as keyof typeof CONTRACT_ADDRESSES] if (!contractAddresses) { @@ -172,17 +172,17 @@ transaction(vaultIdentifier: String, amount: UInt256) { } /** - * Hook to receive a cross-VM FT transaction from EVM to Cadence. This function will + * Hook to bridge fungible tokens from Flow EVM to Cadence. This function will * withdraw tokens from the signer's COA in EVM and deposit them into their Cadence vault. * * @returns The mutation object used to send the transaction. */ -export function useCrossVmReceiveToken({ +export function useBridgeTokenFromEvm({ mutation: mutationOptions = {}, -}: UseCrossVmReceiveTokenArgs = {}): UseCrossVmReceiveTokenResult { +}: UseBridgeTokenFromEvmArgs = {}): UseBridgeTokenFromEvmResult { const chainId = useFlowChainId() const cadenceTx = chainId.data - ? getCrossVmReceiveTokenTransaction(chainId.data) + ? getBridgeTokenFromEvmTransaction(chainId.data) : null const queryClient = useFlowQueryClient() @@ -191,7 +191,7 @@ export function useCrossVmReceiveToken({ mutationFn: async ({ vaultIdentifier, amount, - }: UseCrossVmReceiveTokenMutateArgs) => { + }: UseBridgeTokenFromEvmMutateArgs) => { if (!cadenceTx) { throw new Error("No current chain found") } @@ -214,14 +214,14 @@ export function useCrossVmReceiveToken({ ) const { - mutate: receiveToken, - mutateAsync: receiveTokenAsync, + mutate: bridgeTokenFromEvm, + mutateAsync: bridgeTokenFromEvmAsync, ...rest } = mutation return { - receiveToken, - receiveTokenAsync, + bridgeTokenFromEvm, + bridgeTokenFromEvmAsync, ...rest, } } From 70a3d793d1d975b725fc615f1ecc350e727e983d Mon Sep 17 00:00:00 2001 From: mfbz Date: Thu, 2 Oct 2025 17:10:32 +0200 Subject: [PATCH 08/12] Updated changeset --- .changeset/chilly-kangaroos-train.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/chilly-kangaroos-train.md b/.changeset/chilly-kangaroos-train.md index 04c27364a..657519551 100644 --- a/.changeset/chilly-kangaroos-train.md +++ b/.changeset/chilly-kangaroos-train.md @@ -3,4 +3,4 @@ "@onflow/demo": minor --- -Added crossvm hook to transfer tokens from evm to cadence +Added `useBridgeTokenFromEvm` hook for bridging fungible tokens from Flow EVM to Cadence. This hook withdraws tokens from the signer's Cadence-Owned Account (COA) in EVM and deposits them into their Cadence vault, automatically configuring the vault if needed. From 64902ce79d59746b78eea3c68ef6ad4b47519558 Mon Sep 17 00:00:00 2001 From: mfbz Date: Thu, 2 Oct 2025 18:29:18 +0200 Subject: [PATCH 09/12] Fixed tests and added playground cards --- .../cards/cross-vm-receive-token-card.tsx | 147 ------------ .../demo/src/components/content-section.tsx | 4 + .../demo/src/components/content-sidebar.tsx | 12 + .../use-bridge-token-from-evm-card.tsx | 218 ++++++++++++++++++ .../use-cross-vm-spend-token-card.tsx | 173 ++++++++++++++ packages/demo/src/constants.ts | 3 - .../src/hooks/useBridgeTokenFromEvm.test.ts | 9 +- .../src/hooks/useBridgeTokenFromEvm.ts | 9 +- 8 files changed, 416 insertions(+), 159 deletions(-) delete mode 100644 packages/demo/src/components/cards/cross-vm-receive-token-card.tsx create mode 100644 packages/demo/src/components/hook-cards/use-bridge-token-from-evm-card.tsx create mode 100644 packages/demo/src/components/hook-cards/use-cross-vm-spend-token-card.tsx diff --git a/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx b/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx deleted file mode 100644 index e0c049188..000000000 --- a/packages/demo/src/components/cards/cross-vm-receive-token-card.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import {useCrossVmReceiveToken, useFlowConfig} from "@onflow/react-sdk" -import {useState, useMemo} from "react" -import {getContractAddress} from "../../constants" - -export function CrossVmReceiveTokenCard() { - const config = useFlowConfig() - const currentNetwork = config.flowNetwork || "emulator" - const [vaultIdentifier, setVaultIdentifier] = useState("") - const [amount, setAmount] = useState("1000000000000000000") // 1 token (18 decimals for EVM) - - const { - receiveToken, - isPending, - data: transactionId, - error, - } = useCrossVmReceiveToken() - - const isNetworkSupported = currentNetwork === "testnet" - - const clickTokenData = useMemo(() => { - if (currentNetwork !== "testnet") return null - - const clickTokenAddress = getContractAddress("ClickToken", currentNetwork) - return { - name: "ClickToken", - vaultIdentifier: `A.${clickTokenAddress.replace("0x", "")}.EVMVMBridgedToken_a7cf2260e501952c71189d04fad17c704dfb36e6.Vault`, - amount: "1000000000000000000", // 1 ClickToken (18 decimals for EVM) - } - }, [currentNetwork]) - - // Set default vault identifier when network changes - useMemo(() => { - if (clickTokenData && !vaultIdentifier) { - setVaultIdentifier(clickTokenData.vaultIdentifier) - } - }, [clickTokenData, vaultIdentifier]) - - const handleReceiveToken = () => { - receiveToken({ - vaultIdentifier, - amount, - }) - } - - if (!isNetworkSupported) { - return ( -
-

- useCrossVmReceiveToken -

-
-

- Network not supported: This feature is only - available on testnet. -

-
-
- ) - } - - return ( -
-

- useCrossVmReceiveToken -

-
- - - setVaultIdentifier(e.target.value)} - placeholder={ - clickTokenData - ? clickTokenData.vaultIdentifier - : "e.g., A.dfc20aee650fcbdf.EVMVMBridgedToken_a7cf2260e501952c71189d04fad17c704dfb36e6.Vault" - } - className="p-3 border-2 border-[#00EF8B] rounded-md text-sm text-black bg-white - outline-none transition-colors duration-200 ease-in-out w-full mb-4 font-mono" - /> - - - setAmount(e.target.value)} - placeholder="e.g., 100000000000000000 (0.1 token with 18 decimals)" - className="p-3 border-2 border-[#00EF8B] rounded-md text-sm text-black bg-white - outline-none transition-colors duration-200 ease-in-out w-full mb-4 font-mono" - /> - - -
- -
-

Transaction Status:

- - {isPending && ( -

Receiving tokens from EVM...

- )} - - {error && ( -
- Error: {error.message} -
- )} - - {transactionId && !isPending && !error && ( -
-

- Tokens received successfully! -

-

- Transaction ID: {transactionId} -

-
- )} - - {!transactionId && !isPending && !error && ( -
-

- Click "Receive Token" to bridge tokens from EVM to Cadence -

-
- )} -
-
- ) -} diff --git a/packages/demo/src/components/content-section.tsx b/packages/demo/src/components/content-section.tsx index 75928a2e5..5029da598 100644 --- a/packages/demo/src/components/content-section.tsx +++ b/packages/demo/src/components/content-section.tsx @@ -12,6 +12,8 @@ import {UseFlowMutateCard} from "./hook-cards/use-flow-mutate-card" import {UseFlowEventsCard} from "./hook-cards/use-flow-events-card" import {UseFlowTransactionStatusCard} from "./hook-cards/use-flow-transaction-status-card" import {UseFlowRevertibleRandomCard} from "./hook-cards/use-flow-revertible-random-card" +import {UseBridgeTokenFromEvmCard} from "./hook-cards/use-bridge-token-from-evm-card" +import {UseCrossVmSpendTokenCard} from "./hook-cards/use-cross-vm-spend-token-card" // Import setup cards import {InstallationCard} from "./setup-cards/installation-card" @@ -85,6 +87,8 @@ export function ContentSection() { + +
diff --git a/packages/demo/src/components/content-sidebar.tsx b/packages/demo/src/components/content-sidebar.tsx index 29d88da5e..de9784939 100644 --- a/packages/demo/src/components/content-sidebar.tsx +++ b/packages/demo/src/components/content-sidebar.tsx @@ -110,6 +110,18 @@ const sidebarItems: SidebarItem[] = [ category: "hooks", description: "Track transaction status", }, + { + id: "hook-bridge-token-from-evm", + label: "Bridge Token from EVM", + category: "hooks", + description: "Bridge tokens from EVM to Cadence", + }, + { + id: "hook-cross-vm-spend-token", + label: "Bridge Token to EVM", + category: "hooks", + description: "Bridge tokens from Cadence to EVM", + }, // Advanced section { diff --git a/packages/demo/src/components/hook-cards/use-bridge-token-from-evm-card.tsx b/packages/demo/src/components/hook-cards/use-bridge-token-from-evm-card.tsx new file mode 100644 index 000000000..d1b81cfb1 --- /dev/null +++ b/packages/demo/src/components/hook-cards/use-bridge-token-from-evm-card.tsx @@ -0,0 +1,218 @@ +import {useBridgeTokenFromEvm, useFlowConfig} from "@onflow/react-sdk" +import {useState, useMemo} from "react" +import {useDarkMode} from "../flow-provider-wrapper" +import {DemoCard} from "../ui/demo-card" +import {ResultsSection} from "../ui/results-section" +import {getContractAddress} from "../../constants" +import {PlusGridIcon} from "../ui/plus-grid" + +const IMPLEMENTATION_CODE = `import { useBridgeTokenFromEvm } from "@onflow/react-sdk" + +const { + bridgeTokenFromEvm, + isPending, + error, + data: txId +} = useBridgeTokenFromEvm() + +bridgeTokenFromEvm({ + vaultIdentifier: "A.dfc20aee650fcbdf.EVMVMBridgedToken_xxx.Vault", + amount: "1000000000000000000" +})` + +export function UseBridgeTokenFromEvmCard() { + const {darkMode} = useDarkMode() + const config = useFlowConfig() + const currentNetwork = config.flowNetwork || "emulator" + const [vaultIdentifier, setVaultIdentifier] = useState("") + const [amount, setAmount] = useState("1") // User-friendly amount + const [decimals, setDecimals] = useState("18") // ERC20 decimals + + const { + bridgeTokenFromEvm, + isPending, + data: transactionId, + error, + } = useBridgeTokenFromEvm() + + const clickTokenData = useMemo(() => { + if (currentNetwork !== "testnet") return null + + const clickTokenAddress = getContractAddress("ClickToken", currentNetwork) + return { + name: "ClickToken", + vaultIdentifier: `A.${clickTokenAddress.replace("0x", "")}.EVMVMBridgedToken_a7cf2260e501952c71189d04fad17c704dfb36e6.Vault`, + amount: "1", // 1 ClickToken + decimals: "18", // ClickToken has 18 decimals + } + }, [currentNetwork]) + + // Set default vault identifier when network changes to testnet + useMemo(() => { + if (clickTokenData && !vaultIdentifier) { + setVaultIdentifier(clickTokenData.vaultIdentifier) + setAmount(clickTokenData.amount) + setDecimals(clickTokenData.decimals) + } + }, [clickTokenData, vaultIdentifier]) + + // Convert user-friendly amount to Wei (UInt256) + const convertToWei = (value: string, tokenDecimals: string): string => { + try { + // Remove any non-numeric characters except decimal point + const cleaned = value.replace(/[^\d.]/g, "") + if (!cleaned || cleaned === ".") return "0" + + const decimalPlaces = parseInt(tokenDecimals) || 18 + const [whole = "0", fraction = ""] = cleaned.split(".") + + // Pad or truncate fraction to match decimals + const paddedFraction = fraction.padEnd(decimalPlaces, "0").slice(0, decimalPlaces) + + // Combine whole and fraction parts + const weiValue = (whole || "0") + paddedFraction + + // Remove leading zeros but keep at least one digit + const result = weiValue.replace(/^0+/, "") || "0" + + return result + } catch { + return "0" + } + } + + const handleBridgeToken = () => { + const weiAmount = convertToWei(amount, decimals) + console.log("Bridging token:", { + amount, + decimals, + weiAmount, + vaultIdentifier, + }) + bridgeTokenFromEvm({ + vaultIdentifier, + amount: weiAmount, + }) + } + + return ( + +
+ {clickTokenData && ( +
+ +

+ Note: Example prefilled with ClickToken (ERC20) + vault identifier for testnet +

+
+ )} + +
+ + setVaultIdentifier(e.target.value)} + placeholder={ + clickTokenData + ? clickTokenData.vaultIdentifier + : "e.g., A.dfc20aee650fcbdf.EVMVMBridgedToken_xxx.Vault" + } + className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ + darkMode + ? "bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50" + : "bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50" + } outline-none`} + /> +
+ +
+ + setAmount(e.target.value)} + placeholder="e.g., 1 or 1.5" + className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ + darkMode + ? "bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50" + : "bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50" + } outline-none`} + /> +
+ +
+ + setDecimals(e.target.value)} + placeholder="e.g., 18" + className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ + darkMode + ? "bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50" + : "bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50" + } outline-none`} + /> +

+ ERC20 decimals (typically 18). Amount will be converted to Wei automatically. +

+
+ +
+ +
+ + +
+
+ ) +} diff --git a/packages/demo/src/components/hook-cards/use-cross-vm-spend-token-card.tsx b/packages/demo/src/components/hook-cards/use-cross-vm-spend-token-card.tsx new file mode 100644 index 000000000..adc8537f7 --- /dev/null +++ b/packages/demo/src/components/hook-cards/use-cross-vm-spend-token-card.tsx @@ -0,0 +1,173 @@ +import {useCrossVmSpendToken, useFlowConfig} from "@onflow/react-sdk" +import {useState, useMemo} from "react" +import {useDarkMode} from "../flow-provider-wrapper" +import {DemoCard} from "../ui/demo-card" +import {ResultsSection} from "../ui/results-section" +import {getContractAddress} from "../../constants" +import {PlusGridIcon} from "../ui/plus-grid" + +const IMPLEMENTATION_CODE = `import { useCrossVmSpendToken } from "@onflow/react-sdk" + +const { + spendToken, + isPending, + error, + data: txId +} = useCrossVmSpendToken() + +spendToken({ + vaultIdentifier: "A.dfc20aee650fcbdf.EVMVMBridgedToken_xxx.Vault", + amount: "1.0", + calls: [] +})` + +export function UseCrossVmSpendTokenCard() { + const {darkMode} = useDarkMode() + const config = useFlowConfig() + const currentNetwork = config.flowNetwork || "emulator" + const [vaultIdentifier, setVaultIdentifier] = useState("") + const [amount, setAmount] = useState("1") // UFix64 in Cadence (8 decimals) + + const {spendToken, isPending, data: transactionId, error} = useCrossVmSpendToken() + + const clickTokenData = useMemo(() => { + if (currentNetwork !== "testnet") return null + + const clickTokenAddress = getContractAddress("ClickToken", currentNetwork) + return { + name: "ClickToken", + vaultIdentifier: `A.${clickTokenAddress.replace("0x", "")}.EVMVMBridgedToken_a7cf2260e501952c71189d04fad17c704dfb36e6.Vault`, + amount: "1", // 1 ClickToken (UFix64 with 8 decimals) + } + }, [currentNetwork]) + + // Set default vault identifier when network changes to testnet + useMemo(() => { + if (clickTokenData && !vaultIdentifier) { + setVaultIdentifier(clickTokenData.vaultIdentifier) + setAmount(clickTokenData.amount) + } + }, [clickTokenData, vaultIdentifier]) + + // Normalize amount to ensure it has a decimal point for UFix64 + const normalizeAmount = (value: string): string => { + // Remove any non-numeric characters except decimal point + const cleaned = value.replace(/[^\d.]/g, "") + // If it's just a number without decimal, add .0 + if (cleaned && !cleaned.includes(".")) { + return `${cleaned}.0` + } + return cleaned + } + + const handleSpendToken = () => { + const normalizedAmount = normalizeAmount(amount) + spendToken({ + vaultIdentifier, + amount: normalizedAmount, + calls: [], // No EVM calls, just bridging + }) + } + + return ( + +
+ {clickTokenData && ( +
+ +

+ Note: Example prefilled with ClickToken (ERC20) + vault identifier for testnet +

+
+ )} + +
+ + setVaultIdentifier(e.target.value)} + placeholder={ + clickTokenData + ? clickTokenData.vaultIdentifier + : "e.g., A.dfc20aee650fcbdf.EVMVMBridgedToken_xxx.Vault" + } + className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ + darkMode + ? "bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50" + : "bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50" + } outline-none`} + /> +
+ +
+ + setAmount(e.target.value)} + placeholder="e.g., 1 or 1.5" + className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ + darkMode + ? "bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50" + : "bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50" + } outline-none`} + /> +

+ Enter amount as a number (e.g., "1" or "1.5"). Will be converted to UFix64 format. +

+
+ +
+ +
+ + +
+
+ ) +} diff --git a/packages/demo/src/constants.ts b/packages/demo/src/constants.ts index 3c44342bb..af84a73f5 100644 --- a/packages/demo/src/constants.ts +++ b/packages/demo/src/constants.ts @@ -22,10 +22,7 @@ export const CONTRACT_ADDRESSES: Record> = { mainnet: "0x1654653399040a61", }, ClickToken: { - local: "0xdfc20aee650fcbdf", - emulator: "0xdfc20aee650fcbdf", testnet: "0xdfc20aee650fcbdf", - mainnet: "0xdfc20aee650fcbdf", }, FungibleToken: { local: "0xee82856bf20e2aa6", diff --git a/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.test.ts b/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.test.ts index 9ee790933..9e031082b 100644 --- a/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.test.ts +++ b/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.test.ts @@ -12,9 +12,6 @@ jest.mock("@onflow/fcl", () => require("../__mocks__/fcl").default) jest.mock("./useFlowChainId", () => ({ useFlowChainId: jest.fn(), })) -jest.mock("./useFlowClient", () => ({ - useFlowClient: jest.fn(), -})) describe("useBridgeTokenFromEvm", () => { let mockFcl: MockFclInstance @@ -64,8 +61,8 @@ describe("useBridgeTokenFromEvm", () => { describe("useBridgeTokenFromEvmTx", () => { test("should handle successful transaction", async () => { - jest.mocked(fcl.mutate).mockResolvedValue(mockTxId) - jest.mocked(fcl.tx).mockReturnValue({ + mockFcl.mockFclInstance.mutate.mockResolvedValue(mockTxId) + mockFcl.mockTx.mockReturnValue({ onceExecuted: jest.fn().mockResolvedValue(mockTxResult), } as any) @@ -144,7 +141,7 @@ describe("useBridgeTokenFromEvm", () => { }) it("should handle mutation error", async () => { - ;(fcl.mutate as jest.Mock).mockRejectedValue(new Error("Mutation failed")) + mockFcl.mockFclInstance.mutate.mockRejectedValue(new Error("Mutation failed")) let hookResult: any diff --git a/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.ts b/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.ts index d11ad4bc3..c23549230 100644 --- a/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.ts +++ b/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.ts @@ -1,4 +1,3 @@ -import * as fcl from "@onflow/fcl" import { UseMutateAsyncFunction, UseMutateFunction, @@ -6,15 +5,17 @@ import { UseMutationOptions, UseMutationResult, } from "@tanstack/react-query" -import {useFlowChainId} from "./useFlowChainId" -import {useFlowQueryClient} from "../provider/FlowQueryClient" import {CONTRACT_ADDRESSES} from "../constants" +import {useFlowQueryClient} from "../provider/FlowQueryClient" +import {useFlowChainId} from "./useFlowChainId" +import {useFlowClient} from "./useFlowClient" export interface UseBridgeTokenFromEvmArgs { mutation?: Omit< UseMutationOptions, "mutationFn" > + flowClient?: ReturnType } export interface UseBridgeTokenFromEvmMutateArgs { @@ -179,6 +180,7 @@ transaction(vaultIdentifier: String, amount: UInt256) { */ export function useBridgeTokenFromEvm({ mutation: mutationOptions = {}, + flowClient, }: UseBridgeTokenFromEvmArgs = {}): UseBridgeTokenFromEvmResult { const chainId = useFlowChainId() const cadenceTx = chainId.data @@ -186,6 +188,7 @@ export function useBridgeTokenFromEvm({ : null const queryClient = useFlowQueryClient() + const fcl = useFlowClient({flowClient}) const mutation = useMutation( { mutationFn: async ({ From 1ddad2c43388e2f0285323ab48c8b8946f42fdc0 Mon Sep 17 00:00:00 2001 From: mfbz Date: Thu, 2 Oct 2025 18:29:56 +0200 Subject: [PATCH 10/12] Prettier fix --- .../use-bridge-token-from-evm-card.tsx | 48 ++++++++++++------- .../use-cross-vm-spend-token-card.tsx | 40 +++++++++++----- .../src/hooks/useBridgeTokenFromEvm.test.ts | 4 +- 3 files changed, 61 insertions(+), 31 deletions(-) diff --git a/packages/demo/src/components/hook-cards/use-bridge-token-from-evm-card.tsx b/packages/demo/src/components/hook-cards/use-bridge-token-from-evm-card.tsx index d1b81cfb1..a365c0b22 100644 --- a/packages/demo/src/components/hook-cards/use-bridge-token-from-evm-card.tsx +++ b/packages/demo/src/components/hook-cards/use-bridge-token-from-evm-card.tsx @@ -67,7 +67,9 @@ export function UseBridgeTokenFromEvmCard() { const [whole = "0", fraction = ""] = cleaned.split(".") // Pad or truncate fraction to match decimals - const paddedFraction = fraction.padEnd(decimalPlaces, "0").slice(0, decimalPlaces) + const paddedFraction = fraction + .padEnd(decimalPlaces, "0") + .slice(0, decimalPlaces) // Combine whole and fraction parts const weiValue = (whole || "0") + paddedFraction @@ -107,7 +109,7 @@ export function UseBridgeTokenFromEvmCard() { {clickTokenData && (
@@ -156,11 +161,14 @@ export function UseBridgeTokenFromEvmCard() { value={amount} onChange={e => setAmount(e.target.value)} placeholder="e.g., 1 or 1.5" - className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ + className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 + ${ darkMode - ? "bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50" - : "bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50" - } outline-none`} + ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 + focus:border-flow-primary/50` + : `bg-white border-black/10 text-black placeholder-gray-400 + focus:border-flow-primary/50` + } outline-none`} />
@@ -175,14 +183,20 @@ export function UseBridgeTokenFromEvmCard() { value={decimals} onChange={e => setDecimals(e.target.value)} placeholder="e.g., 18" - className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ + className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 + ${ darkMode - ? "bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50" - : "bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50" - } outline-none`} + ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 + focus:border-flow-primary/50` + : `bg-white border-black/10 text-black placeholder-gray-400 + focus:border-flow-primary/50` + } outline-none`} /> -

- ERC20 decimals (typically 18). Amount will be converted to Wei automatically. +

+ ERC20 decimals (typically 18). Amount will be converted to Wei + automatically.

@@ -194,7 +208,7 @@ export function UseBridgeTokenFromEvmCard() { isPending || !vaultIdentifier || !amount ? "bg-gray-300 text-gray-500 cursor-not-allowed" : "bg-flow-primary text-black hover:bg-flow-primary/80" - }`} + }`} > {isPending ? "Bridging..." : "Bridge Token from EVM"} diff --git a/packages/demo/src/components/hook-cards/use-cross-vm-spend-token-card.tsx b/packages/demo/src/components/hook-cards/use-cross-vm-spend-token-card.tsx index adc8537f7..15e1d9532 100644 --- a/packages/demo/src/components/hook-cards/use-cross-vm-spend-token-card.tsx +++ b/packages/demo/src/components/hook-cards/use-cross-vm-spend-token-card.tsx @@ -28,7 +28,12 @@ export function UseCrossVmSpendTokenCard() { const [vaultIdentifier, setVaultIdentifier] = useState("") const [amount, setAmount] = useState("1") // UFix64 in Cadence (8 decimals) - const {spendToken, isPending, data: transactionId, error} = useCrossVmSpendToken() + const { + spendToken, + isPending, + data: transactionId, + error, + } = useCrossVmSpendToken() const clickTokenData = useMemo(() => { if (currentNetwork !== "testnet") return null @@ -81,7 +86,7 @@ export function UseCrossVmSpendTokenCard() { {clickTokenData && (
@@ -130,14 +138,20 @@ export function UseCrossVmSpendTokenCard() { value={amount} onChange={e => setAmount(e.target.value)} placeholder="e.g., 1 or 1.5" - className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ + className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 + ${ darkMode - ? "bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50" - : "bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50" - } outline-none`} + ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 + focus:border-flow-primary/50` + : `bg-white border-black/10 text-black placeholder-gray-400 + focus:border-flow-primary/50` + } outline-none`} /> -

- Enter amount as a number (e.g., "1" or "1.5"). Will be converted to UFix64 format. +

+ Enter amount as a number (e.g., "1" or "1.5"). Will be converted to + UFix64 format.

@@ -149,7 +163,7 @@ export function UseCrossVmSpendTokenCard() { isPending || !vaultIdentifier || !amount ? "bg-gray-300 text-gray-500 cursor-not-allowed" : "bg-flow-primary text-black hover:bg-flow-primary/80" - }`} + }`} > {isPending ? "Bridging..." : "Bridge Token to EVM"} diff --git a/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.test.ts b/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.test.ts index 9e031082b..6b86068db 100644 --- a/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.test.ts +++ b/packages/react-sdk/src/hooks/useBridgeTokenFromEvm.test.ts @@ -141,7 +141,9 @@ describe("useBridgeTokenFromEvm", () => { }) it("should handle mutation error", async () => { - mockFcl.mockFclInstance.mutate.mockRejectedValue(new Error("Mutation failed")) + mockFcl.mockFclInstance.mutate.mockRejectedValue( + new Error("Mutation failed") + ) let hookResult: any From 7060446e290ab7515153b0ad0f388d044d814251 Mon Sep 17 00:00:00 2001 From: mfbz Date: Mon, 20 Oct 2025 22:04:31 +0200 Subject: [PATCH 11/12] Renamed crossvm token hooks --- .changeset/chilly-kangaroos-train.md | 2 +- .../demo/src/components/content-section.tsx | 8 +- .../demo/src/components/content-sidebar.tsx | 4 +- ...e-cross-vm-bridge-token-from-evm-card.tsx} | 24 +- ...use-cross-vm-bridge-token-to-evm-card.tsx} | 28 +- packages/react-sdk/src/hooks/index.ts | 3 +- ...s => useCrossVmBridgeTokenFromEvm.test.ts} | 34 +-- ...Evm.ts => useCrossVmBridgeTokenFromEvm.ts} | 34 +-- .../hooks/useCrossVmBridgeTokenToEvm.test.ts | 183 ++++++++++++ .../src/hooks/useCrossVmBridgeTokenToEvm.ts | 279 ++++++++++++++++++ .../src/hooks/useCrossVmSpendToken.ts | 3 + 11 files changed, 534 insertions(+), 68 deletions(-) rename packages/demo/src/components/hook-cards/{use-bridge-token-from-evm-card.tsx => use-cross-vm-bridge-token-from-evm-card.tsx} (93%) rename packages/demo/src/components/hook-cards/{use-cross-vm-spend-token-card.tsx => use-cross-vm-bridge-token-to-evm-card.tsx} (90%) rename packages/react-sdk/src/hooks/{useBridgeTokenFromEvm.test.ts => useCrossVmBridgeTokenFromEvm.test.ts} (79%) rename packages/react-sdk/src/hooks/{useBridgeTokenFromEvm.ts => useCrossVmBridgeTokenFromEvm.ts} (90%) create mode 100644 packages/react-sdk/src/hooks/useCrossVmBridgeTokenToEvm.test.ts create mode 100644 packages/react-sdk/src/hooks/useCrossVmBridgeTokenToEvm.ts diff --git a/.changeset/chilly-kangaroos-train.md b/.changeset/chilly-kangaroos-train.md index 657519551..3a18cb772 100644 --- a/.changeset/chilly-kangaroos-train.md +++ b/.changeset/chilly-kangaroos-train.md @@ -3,4 +3,4 @@ "@onflow/demo": minor --- -Added `useBridgeTokenFromEvm` hook for bridging fungible tokens from Flow EVM to Cadence. This hook withdraws tokens from the signer's Cadence-Owned Account (COA) in EVM and deposits them into their Cadence vault, automatically configuring the vault if needed. +Added `useCrossVmBridgeTokenFromEvm` hook for bridging fungible tokens from Flow EVM to Cadence. This hook withdraws tokens from the signer's Cadence-Owned Account (COA) in EVM and deposits them into their Cadence vault, automatically configuring the vault if needed. diff --git a/packages/demo/src/components/content-section.tsx b/packages/demo/src/components/content-section.tsx index a71df17e1..3671346b5 100644 --- a/packages/demo/src/components/content-section.tsx +++ b/packages/demo/src/components/content-section.tsx @@ -12,8 +12,8 @@ import {UseFlowMutateCard} from "./hook-cards/use-flow-mutate-card" import {UseFlowEventsCard} from "./hook-cards/use-flow-events-card" import {UseFlowTransactionStatusCard} from "./hook-cards/use-flow-transaction-status-card" import {UseFlowRevertibleRandomCard} from "./hook-cards/use-flow-revertible-random-card" -import {UseBridgeTokenFromEvmCard} from "./hook-cards/use-bridge-token-from-evm-card" -import {UseCrossVmSpendTokenCard} from "./hook-cards/use-cross-vm-spend-token-card" +import {UseCrossVmBridgeTokenFromEvmCard} from "./hook-cards/use-cross-vm-bridge-token-from-evm-card" +import {UseCrossVmBridgeTokenToEvmCard} from "./hook-cards/use-cross-vm-bridge-token-to-evm-card" import {UseFlowNftMetadataCard} from "./hook-cards/use-flow-nft-metadata-card" // Import setup cards @@ -88,8 +88,8 @@ export function ContentSection() { - - + + diff --git a/packages/demo/src/components/content-sidebar.tsx b/packages/demo/src/components/content-sidebar.tsx index 49b064491..832a2e492 100644 --- a/packages/demo/src/components/content-sidebar.tsx +++ b/packages/demo/src/components/content-sidebar.tsx @@ -111,13 +111,13 @@ const sidebarItems: SidebarItem[] = [ description: "Track transaction status", }, { - id: "usebridgetokenfromevm", + id: "usecrossvmbridgetokenfromevm", label: "Bridge Token from EVM", category: "hooks", description: "Bridge tokens from EVM to Cadence", }, { - id: "usecrossvmspendtoken", + id: "usecrossvmbridgetokentoevm", label: "Bridge Token to EVM", category: "hooks", description: "Bridge tokens from Cadence to EVM", diff --git a/packages/demo/src/components/hook-cards/use-bridge-token-from-evm-card.tsx b/packages/demo/src/components/hook-cards/use-cross-vm-bridge-token-from-evm-card.tsx similarity index 93% rename from packages/demo/src/components/hook-cards/use-bridge-token-from-evm-card.tsx rename to packages/demo/src/components/hook-cards/use-cross-vm-bridge-token-from-evm-card.tsx index 3c6bb3a6a..74886c941 100644 --- a/packages/demo/src/components/hook-cards/use-bridge-token-from-evm-card.tsx +++ b/packages/demo/src/components/hook-cards/use-cross-vm-bridge-token-from-evm-card.tsx @@ -1,4 +1,4 @@ -import {useBridgeTokenFromEvm, useFlowConfig} from "@onflow/react-sdk" +import {useCrossVmBridgeTokenFromEvm, useFlowConfig} from "@onflow/react-sdk" import {useState, useMemo} from "react" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard} from "../ui/demo-card" @@ -6,21 +6,21 @@ import {ResultsSection} from "../ui/results-section" import {getContractAddress} from "../../constants" import {PlusGridIcon} from "../ui/plus-grid" -const IMPLEMENTATION_CODE = `import { useBridgeTokenFromEvm } from "@onflow/react-sdk" +const IMPLEMENTATION_CODE = `import { useCrossVmBridgeTokenFromEvm } from "@onflow/react-sdk" const { - bridgeTokenFromEvm, + crossVmBridgeTokenFromEvm, isPending, error, data: txId -} = useBridgeTokenFromEvm() +} = useCrossVmBridgeTokenFromEvm() -bridgeTokenFromEvm({ +crossVmBridgeTokenFromEvm({ vaultIdentifier: "A.dfc20aee650fcbdf.EVMVMBridgedToken_xxx.Vault", amount: "1000000000000000000" })` -export function UseBridgeTokenFromEvmCard() { +export function UseCrossVmBridgeTokenFromEvmCard() { const {darkMode} = useDarkMode() const config = useFlowConfig() const currentNetwork = config.flowNetwork || "emulator" @@ -29,11 +29,11 @@ export function UseBridgeTokenFromEvmCard() { const [decimals, setDecimals] = useState("18") // ERC20 decimals const { - bridgeTokenFromEvm, + crossVmBridgeTokenFromEvm, isPending, data: transactionId, error, - } = useBridgeTokenFromEvm() + } = useCrossVmBridgeTokenFromEvm() const clickTokenData = useMemo(() => { if (currentNetwork !== "testnet") return null @@ -91,7 +91,7 @@ export function UseBridgeTokenFromEvmCard() { weiAmount, vaultIdentifier, }) - bridgeTokenFromEvm({ + crossVmBridgeTokenFromEvm({ vaultIdentifier, amount: weiAmount, }) @@ -99,11 +99,11 @@ export function UseBridgeTokenFromEvmCard() { return (
{clickTokenData && ( diff --git a/packages/demo/src/components/hook-cards/use-cross-vm-spend-token-card.tsx b/packages/demo/src/components/hook-cards/use-cross-vm-bridge-token-to-evm-card.tsx similarity index 90% rename from packages/demo/src/components/hook-cards/use-cross-vm-spend-token-card.tsx rename to packages/demo/src/components/hook-cards/use-cross-vm-bridge-token-to-evm-card.tsx index 5556742f4..84dff1bcf 100644 --- a/packages/demo/src/components/hook-cards/use-cross-vm-spend-token-card.tsx +++ b/packages/demo/src/components/hook-cards/use-cross-vm-bridge-token-to-evm-card.tsx @@ -1,4 +1,4 @@ -import {useCrossVmSpendToken, useFlowConfig} from "@onflow/react-sdk" +import {useCrossVmBridgeTokenToEvm, useFlowConfig} from "@onflow/react-sdk" import {useState, useMemo} from "react" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard} from "../ui/demo-card" @@ -6,22 +6,22 @@ import {ResultsSection} from "../ui/results-section" import {getContractAddress} from "../../constants" import {PlusGridIcon} from "../ui/plus-grid" -const IMPLEMENTATION_CODE = `import { useCrossVmSpendToken } from "@onflow/react-sdk" +const IMPLEMENTATION_CODE = `import { useCrossVmBridgeTokenToEvm } from "@onflow/react-sdk" const { - spendToken, + crossVmBridgeTokenToEvm, isPending, error, data: txId -} = useCrossVmSpendToken() +} = useCrossVmBridgeTokenToEvm() -spendToken({ +crossVmBridgeTokenToEvm({ vaultIdentifier: "A.dfc20aee650fcbdf.EVMVMBridgedToken_xxx.Vault", amount: "1.0", calls: [] })` -export function UseCrossVmSpendTokenCard() { +export function UseCrossVmBridgeTokenToEvmCard() { const {darkMode} = useDarkMode() const config = useFlowConfig() const currentNetwork = config.flowNetwork || "emulator" @@ -29,11 +29,11 @@ export function UseCrossVmSpendTokenCard() { const [amount, setAmount] = useState("1") // UFix64 in Cadence (8 decimals) const { - spendToken, + crossVmBridgeTokenToEvm, isPending, data: transactionId, error, - } = useCrossVmSpendToken() + } = useCrossVmBridgeTokenToEvm() const clickTokenData = useMemo(() => { if (currentNetwork !== "testnet") return null @@ -65,9 +65,9 @@ export function UseCrossVmSpendTokenCard() { return cleaned } - const handleSpendToken = () => { + const handleBridgeToken = () => { const normalizedAmount = normalizeAmount(amount) - spendToken({ + crossVmBridgeTokenToEvm({ vaultIdentifier, amount: normalizedAmount, calls: [], // No EVM calls, just bridging @@ -76,11 +76,11 @@ export function UseCrossVmSpendTokenCard() { return (
{clickTokenData && ( @@ -157,7 +157,7 @@ export function UseCrossVmSpendTokenCard() {