From ebf6325074047d95079a1a92d1de841d498da71e Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 09:00:30 -0800 Subject: [PATCH 01/15] Add Crypto React SDK UI Integration --- .../react-sdk/src/components/FundContent.tsx | 324 ++++++++++++++---- packages/react-sdk/src/hooks/index.ts | 1 + .../src/hooks/useFundingCapabilities.ts | 107 ++++++ 3 files changed, 367 insertions(+), 65 deletions(-) create mode 100644 packages/react-sdk/src/hooks/useFundingCapabilities.ts diff --git a/packages/react-sdk/src/components/FundContent.tsx b/packages/react-sdk/src/components/FundContent.tsx index 6b5e58220..65b63449b 100644 --- a/packages/react-sdk/src/components/FundContent.tsx +++ b/packages/react-sdk/src/components/FundContent.tsx @@ -1,4 +1,4 @@ -import React, {useState} from "react" +import React, {useState, useEffect} from "react" import {Tab, TabGroup, TabList, TabPanels} from "./internal/Tabs" import {TabPanel} from "@headlessui/react" import {Input} from "./internal/Input" @@ -11,31 +11,135 @@ import { } from "./internal/Listbox" import {QRCode} from "./internal/QRCode" import {Address} from "./internal/Address" +import {useFund} from "../hooks/useFund" +import {useFundingCapabilities} from "../hooks/useFundingCapabilities" +import {relayProvider, CryptoProviderCapability} from "@onflow/payments" +import {useFlowCurrentUser} from "../hooks/useFlowCurrentUser" +import {useFlowChainId} from "../hooks/useFlowChainId" +import * as viemChains from "viem/chains" -const tokens = [ - {id: 1, name: "USDC"}, - {id: 2, name: "FLOW"}, -] +// Helper to get chain name from CAIP-2 ID +const getChainName = (caipId: string): string => { + // Extract chain ID from CAIP-2 format (e.g., "eip155:1" -> 1) + const parts = caipId.split(":") + if (parts.length !== 2) return caipId -const chains = [ - {id: 1, name: "Flow"}, - {id: 2, name: "Ethereum"}, -] + const chainId = parseInt(parts[1]) + if (isNaN(chainId)) return caipId -const PLACEHOLDER_ADDRESS = "0x1a2b3c4d5e6f7890abcdef1234567890" + // Find matching chain in viem's chain definitions + const chain = Object.values(viemChains).find(c => c?.id === chainId) + return chain?.name || caipId +} export const FundContent: React.FC = () => { const [amount, setAmount] = useState("") + const [selectedTabIndex, setSelectedTabIndex] = useState(0) + + const {user} = useFlowCurrentUser() + const {data: chainId} = useFlowChainId() + + const providers = [relayProvider()] + + // Fetch available funding capabilities + const { + data: capabilities, + isLoading: isLoadingCapabilities, + error: capabilitiesError, + } = useFundingCapabilities({ + providers, + }) + + // Extract crypto capabilities + const cryptoCapability = capabilities?.find( + c => c.type === "crypto" + ) as CryptoProviderCapability + + // Build tokens list from capabilities + const tokens = (cryptoCapability?.currencies || []).map( + (currency, index) => ({ + id: index + 1, + name: currency, + address: currency, + }) + ) + + // Build chains list from capabilities + const chains = (cryptoCapability?.sourceChains || []).map( + (caipId, index) => ({ + id: index + 1, + name: getChainName(caipId), + caipId, + }) + ) + const [selectedToken, setSelectedToken] = useState(tokens[0]) const [selectedChain, setSelectedChain] = useState(chains[0]) + // Update selections when capabilities load + useEffect(() => { + if (tokens.length > 0 && !selectedToken) { + setSelectedToken(tokens[0]) + } + if (chains.length > 0 && !selectedChain) { + setSelectedChain(chains[0]) + } + }, [tokens, chains]) + + // Initialize useFund hook with relay provider + const { + mutate: createSession, + data: session, + isPending, + error, + } = useFund({ + providers, + }) + + // Create funding session when crypto tab is selected and user is authenticated + useEffect(() => { + if ( + selectedTabIndex === 1 && + user?.addr && + chainId && + selectedToken && + selectedChain + ) { + // User's Flow address as destination (in CAIP-10 format) + const destination = `eip155:${chainId}:${user.addr}` + + createSession({ + kind: "crypto", + destination, + currency: selectedToken.address, + sourceChain: selectedChain.caipId, + sourceCurrency: selectedToken.address, + amount: amount || undefined, + }) + } + }, [ + selectedTabIndex, + selectedToken, + selectedChain, + amount, + user?.addr, + chainId, + createSession, + ]) + + // Get the deposit address from the session + const depositAddress = + session && session.kind === "crypto" + ? session.instructions.address + : undefined + return (

Fund Your Account

- + {({selected}) => ( @@ -106,65 +210,155 @@ export const FundContent: React.FC = () => {
-
-
- - - {({open}) => ( -
- {selectedToken.name} - {open && ( - - {tokens.map(token => ( - - {token.name} - - ))} - - )} -
- )} -
+ {!user?.addr && ( +
+

+ Please connect your wallet to generate a deposit address +

-
- - - {({open}) => ( -
- {selectedChain.name} - {open && ( - - {chains.map(chain => ( - - {chain.name} - - ))} - + )} + + {isLoadingCapabilities && ( +
+

+ Loading available funding options... +

+
+ )} + + {capabilitiesError && ( +
+

+ Failed to load funding options: {capabilitiesError.message} +

+
+ )} + + {error && ( +
+

+ {error.message} +

+
+ )} + + {!isLoadingCapabilities && + tokens.length > 0 && + chains.length > 0 && ( +
+
+ + + {({open}) => ( +
+ + {selectedToken?.name || "Select token"} + + {open && ( + + {tokens.map(token => ( + + {token.name} + + ))} + + )} +
)} -
- )} - + +
+
+ + + {({open}) => ( +
+ + {selectedChain?.name || "Select chain"} + + {open && ( + + {chains.map(chain => ( + + {chain.name} + + ))} + + )} +
+ )} +
+
+
+ )} + + {isPending && ( +
+
+

+ Generating deposit address... +

-
+ )} -
- -
+ {depositAddress && !isPending && ( + <> +
+ +
-
+
+ +
+

+ Send {selectedToken?.name || "tokens"} from{" "} + {selectedChain?.name || "source chain"} to this address. + Funds will be automatically bridged to your Flow account. +

+
+ + )}
diff --git a/packages/react-sdk/src/hooks/index.ts b/packages/react-sdk/src/hooks/index.ts index 507a5a10b..a455ab85e 100644 --- a/packages/react-sdk/src/hooks/index.ts +++ b/packages/react-sdk/src/hooks/index.ts @@ -32,3 +32,4 @@ export {useFlowScheduledTransaction} from "./useFlowScheduledTransaction" export {useFlowScheduledTransactionSetup} from "./useFlowScheduledTransactionSetup" export {useFlowScheduledTransactionCancel} from "./useFlowScheduledTransactionCancel" export {useFund} from "./useFund" +export {useFundingCapabilities} from "./useFundingCapabilities" diff --git a/packages/react-sdk/src/hooks/useFundingCapabilities.ts b/packages/react-sdk/src/hooks/useFundingCapabilities.ts new file mode 100644 index 000000000..91a228c8c --- /dev/null +++ b/packages/react-sdk/src/hooks/useFundingCapabilities.ts @@ -0,0 +1,107 @@ +import {useQuery, UseQueryOptions, UseQueryResult} from "@tanstack/react-query" +import {useCallback, useMemo} from "react" +import {useFlowQueryClient} from "../provider/FlowQueryClient" +import {useFlowClient} from "./useFlowClient" +import { + createPaymentsClient, + FundingProviderFactory, + ProviderCapability, +} from "@onflow/payments" + +/** + * Arguments for the useFundingCapabilities hook. + */ +export interface UseFundingCapabilitiesArgs { + /** Array of funding provider factories to query */ + providers: FundingProviderFactory[] + /** Optional React Query options */ + query?: Omit< + UseQueryOptions, + "queryKey" | "queryFn" + > + /** Optional Flow client override */ + flowClient?: ReturnType +} + +/** + * useFundingCapabilities + * + * Fetches the capabilities (supported chains, currencies, etc.) from funding providers. + * Use this to dynamically populate UI with available funding options. + * + * @param args.providers - Array of funding provider factories + * @param args.query - Optional React Query options + * @param args.flowClient - Optional Flow client override + * + * @example + * ```tsx + * import { useFundingCapabilities } from "@onflow/react-sdk" + * import { relayProvider } from "@onflow/payments" + * + * function FundingOptions() { + * const { data: capabilities, isLoading } = useFundingCapabilities({ + * providers: [relayProvider()], + * }) + * + * if (isLoading) return
Loading...
+ * + * const cryptoCapability = capabilities?.find(c => c.type === "crypto") + * const chains = cryptoCapability?.sourceChains || [] + * const currencies = cryptoCapability?.currencies || [] + * + * return ( + *
+ *

Supported Chains:

+ *
    {chains.map(chain =>
  • {chain}
  • )}
+ *

Supported Currencies:

+ *
    {currencies.map(currency =>
  • {currency}
  • )}
+ *
+ * ) + * } + * ``` + */ +export function useFundingCapabilities({ + providers, + query: queryOptions = {}, + flowClient, +}: UseFundingCapabilitiesArgs): UseQueryResult { + const queryClient = useFlowQueryClient() + const fcl = useFlowClient({flowClient}) + + const paymentsClient = useMemo( + () => + createPaymentsClient({ + providers, + flowClient: fcl, + }), + [providers, fcl] + ) + + const fetchCapabilities = useCallback(async () => { + // Get all provider instances from the client + // We need to access the internal providers array + // Since it's not exposed, we'll need to call getCapabilities on the first provider + // This is a limitation - we should enhance the payments client to expose providers or aggregated capabilities + + const allCapabilities: ProviderCapability[] = [] + + // Create provider instances manually to call getCapabilities + for (const factory of providers) { + const provider = factory({flowClient: fcl}) + const capabilities = await provider.getCapabilities() + allCapabilities.push(...capabilities) + } + + return allCapabilities + }, [providers, fcl]) + + return useQuery( + { + queryKey: ["fundingCapabilities", providers.length], + queryFn: fetchCapabilities, + staleTime: 5 * 60 * 1000, // 5 minutes - capabilities don't change often + ...queryOptions, + }, + queryClient + ) +} From 51d205e42d4e7236f35cc579e7a17adc95abdf4c Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 09:13:01 -0800 Subject: [PATCH 02/15] Swtich to COA for now --- .../cadence/scripts/get-coa-address.cdc | 18 ++++++++++++ packages/payments/src/bridge-service.ts | 15 ++++++++++ packages/payments/src/providers/relay.ts | 28 +++++++++++-------- 3 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 packages/payments/cadence/scripts/get-coa-address.cdc diff --git a/packages/payments/cadence/scripts/get-coa-address.cdc b/packages/payments/cadence/scripts/get-coa-address.cdc new file mode 100644 index 000000000..2c45a38b0 --- /dev/null +++ b/packages/payments/cadence/scripts/get-coa-address.cdc @@ -0,0 +1,18 @@ +import "EVM" + +/// Get the EVM address for a Cadence account's COA (Cadence Owned Account) +/// Returns the EVM address as a hex string, or nil if no COA exists +access(all) fun main(address: Address): String? { + let account = getAccount(address) + + // Try to borrow the COA from the account's storage + let coaRef = account.capabilities.borrow<&EVM.CadenceOwnedAccount>( + /public/evm + ) + + if let coa = coaRef { + return coa.address().toString() + } + + return nil +} diff --git a/packages/payments/src/bridge-service.ts b/packages/payments/src/bridge-service.ts index 231d984bf..8f542e0f9 100644 --- a/packages/payments/src/bridge-service.ts +++ b/packages/payments/src/bridge-service.ts @@ -11,6 +11,7 @@ import type {FlowNetwork} from "./constants" import GET_EVM_ADDRESS_SCRIPT from "../cadence/scripts/get-evm-address-from-vault.cdc" import GET_VAULT_TYPE_SCRIPT from "../cadence/scripts/get-vault-type-from-evm.cdc" import GET_TOKEN_DECIMALS_SCRIPT from "../cadence/scripts/get-token-decimals.cdc" +import GET_COA_ADDRESS_SCRIPT from "../cadence/scripts/get-coa-address.cdc" interface BridgeQueryOptions { flowClient: ReturnType @@ -71,3 +72,17 @@ export async function getTokenDecimals({ }) return Number(result) } + +/** + * Get the COA (Cadence Owned Account) EVM address for a Cadence account + */ +export async function getCoaAddress({ + flowClient, + cadenceAddress, +}: BridgeQueryOptions & {cadenceAddress: string}): Promise { + const result = await flowClient.query({ + cadence: await resolveCadence(flowClient, GET_COA_ADDRESS_SCRIPT), + args: (arg: any, t: any) => [arg(cadenceAddress, t.Address)], + }) + return result || null +} diff --git a/packages/payments/src/providers/relay.ts b/packages/payments/src/providers/relay.ts index fc0060935..e63b5aa83 100644 --- a/packages/payments/src/providers/relay.ts +++ b/packages/payments/src/providers/relay.ts @@ -11,6 +11,7 @@ import type {createFlowClientCore} from "@onflow/fcl-core" import {parseCAIP2, parseCAIP10} from "../utils/caip" import {isEvmAddress, isCadenceAddress} from "../utils/address" import {getFlowEvmChainId} from "../utils/network" +import {getCoaAddress} from "../bridge-service" /** * Configuration for the Relay funding provider @@ -224,22 +225,25 @@ export function relayProvider( destination.chainId ) - // Detect if destination is Cadence (needs bridging after EVM funding) + // Detect if destination is Cadence and convert to COA EVM address const isCadenceDestination = isCadenceAddress(destination.address) let actualDestination = destination.address - // TODO: For Cadence destinations, we need to: - // 1. Determine the user's COA (Cadence Owned Account) EVM address - // 2. Fund the COA instead - // 3. Return instructions for the user to bridge COA -> Cadence - // For now, we reject Cadence destinations until this is implemented if (isCadenceDestination) { - throw new Error( - `Cadence destination detected: ${destination.address}. ` + - `Automatic Cadence routing is not yet implemented. ` + - `Please provide the COA (Cadence Owned Account) EVM address instead. ` + - `Future versions will automatically route funds through the COA and provide bridging instructions.` - ) + // Fetch the user's COA (Cadence Owned Account) EVM address + const coaAddress = await getCoaAddress({ + flowClient, + cadenceAddress: destination.address, + }) + + if (!coaAddress) { + throw new Error( + `No COA (Cadence Owned Account) found for ${destination.address}. ` + + `Please ensure the account has a COA set up at /public/evm.` + ) + } + + actualDestination = coaAddress } if (!isEvmAddress(actualDestination)) { From 8cfd10b84244797d5dd84f77a97fbcdfb2582c2f Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 09:17:24 -0800 Subject: [PATCH 03/15] select from source chain --- .../react-sdk/src/components/FundContent.tsx | 71 ++++++++++--------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/packages/react-sdk/src/components/FundContent.tsx b/packages/react-sdk/src/components/FundContent.tsx index 65b63449b..0158a5d1a 100644 --- a/packages/react-sdk/src/components/FundContent.tsx +++ b/packages/react-sdk/src/components/FundContent.tsx @@ -55,8 +55,8 @@ export const FundContent: React.FC = () => { c => c.type === "crypto" ) as CryptoProviderCapability - // Build tokens list from capabilities - const tokens = (cryptoCapability?.currencies || []).map( + // Build SOURCE tokens list (what user can send FROM) + const sourceTokens = (cryptoCapability?.sourceCurrencies || []).map( (currency, index) => ({ id: index + 1, name: currency, @@ -64,8 +64,8 @@ export const FundContent: React.FC = () => { }) ) - // Build chains list from capabilities - const chains = (cryptoCapability?.sourceChains || []).map( + // Build SOURCE chains list (where user can send FROM) + const sourceChains = (cryptoCapability?.sourceChains || []).map( (caipId, index) => ({ id: index + 1, name: getChainName(caipId), @@ -73,18 +73,22 @@ export const FundContent: React.FC = () => { }) ) - const [selectedToken, setSelectedToken] = useState(tokens[0]) - const [selectedChain, setSelectedChain] = useState(chains[0]) + const [selectedSourceToken, setSelectedSourceToken] = useState( + sourceTokens[0] + ) + const [selectedSourceChain, setSelectedSourceChain] = useState( + sourceChains[0] + ) // Update selections when capabilities load useEffect(() => { - if (tokens.length > 0 && !selectedToken) { - setSelectedToken(tokens[0]) + if (sourceTokens.length > 0 && !selectedSourceToken) { + setSelectedSourceToken(sourceTokens[0]) } - if (chains.length > 0 && !selectedChain) { - setSelectedChain(chains[0]) + if (sourceChains.length > 0 && !selectedSourceChain) { + setSelectedSourceChain(sourceChains[0]) } - }, [tokens, chains]) + }, [sourceTokens, sourceChains]) // Initialize useFund hook with relay provider const { @@ -102,8 +106,8 @@ export const FundContent: React.FC = () => { selectedTabIndex === 1 && user?.addr && chainId && - selectedToken && - selectedChain + selectedSourceToken && + selectedSourceChain ) { // User's Flow address as destination (in CAIP-10 format) const destination = `eip155:${chainId}:${user.addr}` @@ -111,16 +115,16 @@ export const FundContent: React.FC = () => { createSession({ kind: "crypto", destination, - currency: selectedToken.address, - sourceChain: selectedChain.caipId, - sourceCurrency: selectedToken.address, + currency: selectedSourceToken.address, // Destination currency (will be same token on Flow) + sourceChain: selectedSourceChain.caipId, + sourceCurrency: selectedSourceToken.address, amount: amount || undefined, }) } }, [ selectedTabIndex, - selectedToken, - selectedChain, + selectedSourceToken, + selectedSourceChain, amount, user?.addr, chainId, @@ -255,29 +259,29 @@ export const FundContent: React.FC = () => { )} {!isLoadingCapabilities && - tokens.length > 0 && - chains.length > 0 && ( + sourceTokens.length > 0 && + sourceChains.length > 0 && (
{({open}) => (
- {selectedToken?.name || "Select token"} + {selectedSourceToken?.name || "Select token"} {open && ( - {tokens.map(token => ( + {sourceTokens.map(token => ( {token.name} @@ -293,21 +297,21 @@ export const FundContent: React.FC = () => { className="flow-text-xs flow-font-medium flow-text-slate-500 dark:flow-text-slate-400 flow-uppercase flow-tracking-wide" > - Source Chain + From Chain {({open}) => (
- {selectedChain?.name || "Select chain"} + {selectedSourceChain?.name || "Select chain"} {open && ( - {chains.map(chain => ( + {sourceChains.map(chain => ( {chain.name} @@ -352,9 +356,10 @@ export const FundContent: React.FC = () => { flow-border-blue-200 dark:flow-border-blue-800 flow-p-4" >

- Send {selectedToken?.name || "tokens"} from{" "} - {selectedChain?.name || "source chain"} to this address. - Funds will be automatically bridged to your Flow account. + Send {selectedSourceToken?.name || "tokens"} from{" "} + {selectedSourceChain?.name || "any chain"} to this + address. Funds will be automatically bridged to your Flow + account.

From 9dde57a5c77e2b39929bf2b5452b6d486492896b Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 09:20:54 -0800 Subject: [PATCH 04/15] fix coa query --- packages/payments/cadence/scripts/get-coa-address.cdc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/payments/cadence/scripts/get-coa-address.cdc b/packages/payments/cadence/scripts/get-coa-address.cdc index 2c45a38b0..4c3826188 100644 --- a/packages/payments/cadence/scripts/get-coa-address.cdc +++ b/packages/payments/cadence/scripts/get-coa-address.cdc @@ -1,7 +1,7 @@ import "EVM" /// Get the EVM address for a Cadence account's COA (Cadence Owned Account) -/// Returns the EVM address as a hex string, or nil if no COA exists +/// Returns the EVM address as a hex string with 0x prefix, or nil if no COA exists access(all) fun main(address: Address): String? { let account = getAccount(address) @@ -11,7 +11,9 @@ access(all) fun main(address: Address): String? { ) if let coa = coaRef { - return coa.address().toString() + // Get the address bytes and convert to hex string with 0x prefix + let addressBytes = coa.address().bytes + return "0x".concat(String.encodeHex(addressBytes)) } return nil From 0587a6090a76304df42713b56b0302beda7deda0 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 09:40:10 -0800 Subject: [PATCH 05/15] fix currency lookup --- packages/payments/src/providers/relay.ts | 15 +++- packages/payments/src/types.ts | 8 ++- .../react-sdk/src/components/FundContent.tsx | 68 +++++++++++++------ 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/packages/payments/src/providers/relay.ts b/packages/payments/src/providers/relay.ts index e63b5aa83..0be1aed5d 100644 --- a/packages/payments/src/providers/relay.ts +++ b/packages/payments/src/providers/relay.ts @@ -195,8 +195,21 @@ export function relayProvider( { type: "crypto", sourceChains: supportedChains, - sourceCurrencies: flowCurrenciesArray, currencies: flowCurrenciesArray, + // Query chain-specific source currencies dynamically + getCurrenciesForChain: async (sourceChain: string) => { + try { + const {chainId} = parseCAIP2(sourceChain) + const currencies = await getRelayCurrencies(apiUrl, chainId) + return currencies.map(c => c.address) + } catch (error) { + console.error( + `Failed to fetch currencies for chain ${sourceChain}:`, + error + ) + return [] + } + }, }, ] } catch (error) { diff --git a/packages/payments/src/types.ts b/packages/payments/src/types.ts index f4fabb7ad..af1f60e8e 100644 --- a/packages/payments/src/types.ts +++ b/packages/payments/src/types.ts @@ -106,8 +106,12 @@ export interface CryptoProviderCapability extends BaseProviderCapability { type: "crypto" /** List of supported source chains in CAIP-2 format (e.g., `["eip155:1", "eip155:137"]`) */ sourceChains?: string[] - /** List of supported source currencies */ - sourceCurrencies?: string[] + /** + * Function to get currencies available on a specific source chain + * @param sourceChain - Source chain in CAIP-2 format (e.g., "eip155:1") + * @returns Promise resolving to array of currency addresses available on that chain + */ + getCurrenciesForChain: (sourceChain: string) => Promise } /** diff --git a/packages/react-sdk/src/components/FundContent.tsx b/packages/react-sdk/src/components/FundContent.tsx index 0158a5d1a..e2b4cefe7 100644 --- a/packages/react-sdk/src/components/FundContent.tsx +++ b/packages/react-sdk/src/components/FundContent.tsx @@ -16,6 +16,8 @@ import {useFundingCapabilities} from "../hooks/useFundingCapabilities" import {relayProvider, CryptoProviderCapability} from "@onflow/payments" import {useFlowCurrentUser} from "../hooks/useFlowCurrentUser" import {useFlowChainId} from "../hooks/useFlowChainId" +import {useQuery} from "@tanstack/react-query" +import {useFlowQueryClient} from "../provider/FlowQueryClient" import * as viemChains from "viem/chains" // Helper to get chain name from CAIP-2 ID @@ -55,15 +57,6 @@ export const FundContent: React.FC = () => { c => c.type === "crypto" ) as CryptoProviderCapability - // Build SOURCE tokens list (what user can send FROM) - const sourceTokens = (cryptoCapability?.sourceCurrencies || []).map( - (currency, index) => ({ - id: index + 1, - name: currency, - address: currency, - }) - ) - // Build SOURCE chains list (where user can send FROM) const sourceChains = (cryptoCapability?.sourceChains || []).map( (caipId, index) => ({ @@ -73,22 +66,56 @@ export const FundContent: React.FC = () => { }) ) - const [selectedSourceToken, setSelectedSourceToken] = useState( - sourceTokens[0] - ) const [selectedSourceChain, setSelectedSourceChain] = useState( sourceChains[0] ) + const [selectedSourceToken, setSelectedSourceToken] = useState<{ + id: number + name: string + address: string + } | null>(null) - // Update selections when capabilities load + // Update chain selection when capabilities load useEffect(() => { - if (sourceTokens.length > 0 && !selectedSourceToken) { - setSelectedSourceToken(sourceTokens[0]) - } if (sourceChains.length > 0 && !selectedSourceChain) { setSelectedSourceChain(sourceChains[0]) } - }, [sourceTokens, sourceChains]) + }, [sourceChains]) + + // Fetch currencies for the selected source chain + const queryClient = useFlowQueryClient() + const {data: chainCurrencies, isLoading: isLoadingCurrencies} = useQuery( + { + queryKey: ["chainCurrencies", selectedSourceChain?.caipId], + queryFn: async () => { + if (!selectedSourceChain || !cryptoCapability?.getCurrenciesForChain) { + return [] + } + return await cryptoCapability.getCurrenciesForChain( + selectedSourceChain.caipId + ) + }, + enabled: !!selectedSourceChain && !!cryptoCapability, + staleTime: 5 * 60 * 1000, + }, + queryClient + ) + + // Build token list from chain-specific currencies + const sourceTokens = (chainCurrencies || []).map((address, index) => ({ + id: index + 1, + name: address, + address: address, + })) + + // Update token selection when currencies load or chain changes + useEffect(() => { + if (sourceTokens.length > 0) { + setSelectedSourceToken(sourceTokens[0]) + } else { + setSelectedSourceToken(null) + } + }, [selectedSourceChain, chainCurrencies]) // Initialize useFund hook with relay provider const { @@ -225,13 +252,15 @@ export const FundContent: React.FC = () => {
)} - {isLoadingCapabilities && ( + {(isLoadingCapabilities || isLoadingCurrencies) && (

- Loading available funding options... + {isLoadingCapabilities + ? "Loading available funding options..." + : "Loading available tokens..."}

)} @@ -259,6 +288,7 @@ export const FundContent: React.FC = () => { )} {!isLoadingCapabilities && + !isLoadingCurrencies && sourceTokens.length > 0 && sourceChains.length > 0 && (
From a5b1509f1ffb5304618c8a0f052d1e66a1849f0c Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 09:42:04 -0800 Subject: [PATCH 06/15] Fix COA address --- packages/payments/cadence/scripts/get-coa-address.cdc | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/payments/cadence/scripts/get-coa-address.cdc b/packages/payments/cadence/scripts/get-coa-address.cdc index 4c3826188..389f5d5f1 100644 --- a/packages/payments/cadence/scripts/get-coa-address.cdc +++ b/packages/payments/cadence/scripts/get-coa-address.cdc @@ -11,9 +11,14 @@ access(all) fun main(address: Address): String? { ) if let coa = coaRef { - // Get the address bytes and convert to hex string with 0x prefix + // Get the address bytes - it's a fixed-size array [UInt8; 20] + // Convert to variable-size array for String.encodeHex let addressBytes = coa.address().bytes - return "0x".concat(String.encodeHex(addressBytes)) + let variableBytes: [UInt8] = [] + for byte in addressBytes { + variableBytes.append(byte) + } + return "0x".concat(String.encodeHex(variableBytes)) } return nil From e8729d51f6b9cb99babe6cf9dd93ce4b4dae90c9 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 09:53:16 -0800 Subject: [PATCH 07/15] Fix currencies query --- package-lock.json | 3 +++ packages/demo/package.json | 1 + packages/demo/src/main.tsx | 4 ++++ packages/demo/vite.config.ts | 16 ++++++++++++++++ packages/payments/src/providers/relay.ts | 16 ++++++++++++---- 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8364b65c6..14925f2cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13504,6 +13504,8 @@ }, "node_modules/buffer": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -31707,6 +31709,7 @@ "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.4.1", "autoprefixer": "^10.4.21", + "buffer": "^6.0.3", "concurrently": "^8.0.1", "cross-env": "^7.0.3", "eslint": "^9.25.0", diff --git a/packages/demo/package.json b/packages/demo/package.json index 1b1d5a279..8b36db20f 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -34,6 +34,7 @@ "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.4.1", "autoprefixer": "^10.4.21", + "buffer": "^6.0.3", "concurrently": "^8.0.1", "cross-env": "^7.0.3", "eslint": "^9.25.0", diff --git a/packages/demo/src/main.tsx b/packages/demo/src/main.tsx index 17bf16756..7fcd3e849 100644 --- a/packages/demo/src/main.tsx +++ b/packages/demo/src/main.tsx @@ -1,7 +1,11 @@ import {StrictMode} from "react" import {createRoot} from "react-dom/client" +import {Buffer} from "buffer" import {App} from "./app.tsx" +// Polyfill Buffer for FCL in browser +globalThis.Buffer = Buffer + createRoot(document.getElementById("root")!).render( diff --git a/packages/demo/vite.config.ts b/packages/demo/vite.config.ts index 29ac14a6d..a2d8981ad 100644 --- a/packages/demo/vite.config.ts +++ b/packages/demo/vite.config.ts @@ -7,6 +7,22 @@ export default defineConfig({ server: { allowedHosts: true, }, + define: { + // Polyfill Buffer for browser + global: "globalThis", + }, + resolve: { + alias: { + buffer: "buffer", + }, + }, + optimizeDeps: { + esbuildOptions: { + define: { + global: "globalThis", + }, + }, + }, build: { chunkSizeWarningLimit: 1000, rollupOptions: { diff --git a/packages/payments/src/providers/relay.ts b/packages/payments/src/providers/relay.ts index 0be1aed5d..718ed0a03 100644 --- a/packages/payments/src/providers/relay.ts +++ b/packages/payments/src/providers/relay.ts @@ -436,8 +436,16 @@ async function getRelayCurrencies( apiUrl: string, chainId: number | string ): Promise { - const response = await fetch(`${apiUrl}/currencies?chainId=${chainId}`, { - method: "GET", + const response = await fetch(`${apiUrl}/currencies/v2`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + chainIds: [typeof chainId === "string" ? parseInt(chainId) : chainId], + verified: true, + depositAddressOnly: true, + }), }) if (!response.ok) { @@ -447,6 +455,6 @@ async function getRelayCurrencies( } const data = await response.json() - // Response might be array or object with currencies property - return Array.isArray(data) ? data : data.currencies || data.data || [] + // Response is an array of currency objects + return Array.isArray(data) ? data : [] } From 723511e387b483f419bf08cb7cca45cff02a171a Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 10:09:54 -0800 Subject: [PATCH 08/15] add metadata to ui --- packages/payments/src/providers/relay.ts | 8 ++- packages/payments/src/types.ts | 20 +++++- .../react-sdk/src/components/FundContent.tsx | 68 ++++++++++++++----- 3 files changed, 76 insertions(+), 20 deletions(-) diff --git a/packages/payments/src/providers/relay.ts b/packages/payments/src/providers/relay.ts index 718ed0a03..61e48920e 100644 --- a/packages/payments/src/providers/relay.ts +++ b/packages/payments/src/providers/relay.ts @@ -201,7 +201,13 @@ export function relayProvider( try { const {chainId} = parseCAIP2(sourceChain) const currencies = await getRelayCurrencies(apiUrl, chainId) - return currencies.map(c => c.address) + return currencies.map(c => ({ + address: c.address, + symbol: c.symbol, + name: c.name, + decimals: c.decimals, + logoURI: c.metadata?.logoURI, + })) } catch (error) { console.error( `Failed to fetch currencies for chain ${sourceChain}:`, diff --git a/packages/payments/src/types.ts b/packages/payments/src/types.ts index af1f60e8e..a3291b82f 100644 --- a/packages/payments/src/types.ts +++ b/packages/payments/src/types.ts @@ -98,6 +98,22 @@ export interface BaseProviderCapability { maxAmount?: string } +/** + * Currency metadata returned by providers + */ +export interface CurrencyMetadata { + /** Token contract address */ + address: string + /** Token symbol (e.g., "USDC", "WETH") */ + symbol: string + /** Token name (e.g., "USD Coin") */ + name: string + /** Number of decimals */ + decimals: number + /** Optional logo URI */ + logoURI?: string +} + /** * Capabilities for a crypto funding provider */ @@ -109,9 +125,9 @@ export interface CryptoProviderCapability extends BaseProviderCapability { /** * Function to get currencies available on a specific source chain * @param sourceChain - Source chain in CAIP-2 format (e.g., "eip155:1") - * @returns Promise resolving to array of currency addresses available on that chain + * @returns Promise resolving to array of currency metadata */ - getCurrenciesForChain: (sourceChain: string) => Promise + getCurrenciesForChain: (sourceChain: string) => Promise } /** diff --git a/packages/react-sdk/src/components/FundContent.tsx b/packages/react-sdk/src/components/FundContent.tsx index e2b4cefe7..8e872e26a 100644 --- a/packages/react-sdk/src/components/FundContent.tsx +++ b/packages/react-sdk/src/components/FundContent.tsx @@ -13,7 +13,11 @@ import {QRCode} from "./internal/QRCode" import {Address} from "./internal/Address" import {useFund} from "../hooks/useFund" import {useFundingCapabilities} from "../hooks/useFundingCapabilities" -import {relayProvider, CryptoProviderCapability} from "@onflow/payments" +import { + relayProvider, + CryptoProviderCapability, + CurrencyMetadata, +} from "@onflow/payments" import {useFlowCurrentUser} from "../hooks/useFlowCurrentUser" import {useFlowChainId} from "../hooks/useFlowChainId" import {useQuery} from "@tanstack/react-query" @@ -57,6 +61,10 @@ export const FundContent: React.FC = () => { c => c.type === "crypto" ) as CryptoProviderCapability + // Get destination currencies (what can be received on Flow) + // These are typically FLOW, USDF, WFLOW, etc. + const destinationCurrencies = cryptoCapability?.currencies || [] + // Build SOURCE chains list (where user can send FROM) const sourceChains = (cryptoCapability?.sourceChains || []).map( (caipId, index) => ({ @@ -69,11 +77,8 @@ export const FundContent: React.FC = () => { const [selectedSourceChain, setSelectedSourceChain] = useState( sourceChains[0] ) - const [selectedSourceToken, setSelectedSourceToken] = useState<{ - id: number - name: string - address: string - } | null>(null) + const [selectedSourceToken, setSelectedSourceToken] = + useState(null) // Update chain selection when capabilities load useEffect(() => { @@ -101,12 +106,8 @@ export const FundContent: React.FC = () => { queryClient ) - // Build token list from chain-specific currencies - const sourceTokens = (chainCurrencies || []).map((address, index) => ({ - id: index + 1, - name: address, - address: address, - })) + // Use currency metadata directly + const sourceTokens = chainCurrencies || [] // Update token selection when currencies load or chain changes useEffect(() => { @@ -142,7 +143,9 @@ export const FundContent: React.FC = () => { createSession({ kind: "crypto", destination, - currency: selectedSourceToken.address, // Destination currency (will be same token on Flow) + // Use first available destination currency (e.g., FLOW, USDF) + // Relay will automatically bridge source token to this + currency: destinationCurrencies[0] || selectedSourceToken.address, sourceChain: selectedSourceChain.caipId, sourceCurrency: selectedSourceToken.address, amount: amount || undefined, @@ -307,13 +310,44 @@ export const FundContent: React.FC = () => { {({open}) => (
- {selectedSourceToken?.name || "Select token"} +
+ {selectedSourceToken?.logoURI && ( + {selectedSourceToken.symbol} + )} + + {selectedSourceToken?.symbol || + "Select token"} + +
{open && ( {sourceTokens.map(token => ( - - {token.name} + +
+ {token.logoURI && ( + {token.symbol} + )} +
+ + {token.symbol} + + + {token.name} + +
+
))}
@@ -386,7 +420,7 @@ export const FundContent: React.FC = () => { flow-border-blue-200 dark:flow-border-blue-800 flow-p-4" >

- Send {selectedSourceToken?.name || "tokens"} from{" "} + Send {selectedSourceToken?.symbol || "tokens"} from{" "} {selectedSourceChain?.name || "any chain"} to this address. Funds will be automatically bridged to your Flow account. From c6aff522283205ecae23fa0f2f28c429b5eea66c Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 10:22:49 -0800 Subject: [PATCH 09/15] Improve client abstraction --- packages/react-sdk/src/core/context.ts | 5 ++ packages/react-sdk/src/hooks/index.ts | 1 + packages/react-sdk/src/hooks/useFund.ts | 46 +++++-------- .../src/hooks/useFundingCapabilities.ts | 69 ++++++++++--------- .../react-sdk/src/hooks/usePaymentsClient.ts | 10 +++ .../react-sdk/src/provider/FlowProvider.tsx | 39 ++++++++--- 6 files changed, 100 insertions(+), 70 deletions(-) create mode 100644 packages/react-sdk/src/hooks/usePaymentsClient.ts diff --git a/packages/react-sdk/src/core/context.ts b/packages/react-sdk/src/core/context.ts index efb8a18c2..8b192ce16 100644 --- a/packages/react-sdk/src/core/context.ts +++ b/packages/react-sdk/src/core/context.ts @@ -1,6 +1,7 @@ import {createContext} from "react" import {FlowNetwork} from "./types" import {createFlowClient} from "@onflow/fcl" +import type {PaymentsClient} from "@onflow/payments" export type FlowConfig = { accessNodeUrl?: string @@ -25,3 +26,7 @@ export const FlowConfigContext = createContext({}) export const FlowClientContext = createContext | null>(null) + +export const PaymentsClientContext = createContext( + undefined +) diff --git a/packages/react-sdk/src/hooks/index.ts b/packages/react-sdk/src/hooks/index.ts index a455ab85e..556fbae0a 100644 --- a/packages/react-sdk/src/hooks/index.ts +++ b/packages/react-sdk/src/hooks/index.ts @@ -33,3 +33,4 @@ export {useFlowScheduledTransactionSetup} from "./useFlowScheduledTransactionSet export {useFlowScheduledTransactionCancel} from "./useFlowScheduledTransactionCancel" export {useFund} from "./useFund" export {useFundingCapabilities} from "./useFundingCapabilities" +export {usePaymentsClient} from "./usePaymentsClient" diff --git a/packages/react-sdk/src/hooks/useFund.ts b/packages/react-sdk/src/hooks/useFund.ts index 8e31d0b7d..e722e02af 100644 --- a/packages/react-sdk/src/hooks/useFund.ts +++ b/packages/react-sdk/src/hooks/useFund.ts @@ -3,22 +3,18 @@ import { UseMutationResult, UseMutationOptions, } from "@tanstack/react-query" -import {useCallback, useMemo} from "react" +import {useCallback} from "react" import {useFlowQueryClient} from "../provider/FlowQueryClient" import {useFlowClient} from "./useFlowClient" -import { - createPaymentsClient, - FundingIntent, - FundingSession, - FundingProviderFactory, -} from "@onflow/payments" +import {usePaymentsClient} from "./usePaymentsClient" +import {FundingIntent, FundingSession, PaymentsClient} from "@onflow/payments" /** * Arguments for the useFund hook. */ export interface UseFundArgs { - /** Array of funding provider factories to use (in priority order) */ - providers: FundingProviderFactory[] + /** Optional payments client (uses context if not provided) */ + paymentsClient?: PaymentsClient /** Optional React Query mutation settings (e.g., `onSuccess`, `onError`, `retry`) */ mutation?: Omit< UseMutationOptions, @@ -34,28 +30,25 @@ export interface UseFundArgs { * Creates a funding session via the payments client and returns a React Query mutation. * Use this hook to initiate crypto or fiat funding flows. * - * @param args.providers - Array of funding providers (e.g., `[relayProvider()]`) + * @param args.paymentsClient - Optional payments client (uses context if not provided) * @param args.mutation - Optional React Query mutation options * @param args.flowClient - Optional Flow client override * * @example * ```tsx * import { useFund } from "@onflow/react-sdk" - * import { relayProvider } from "@onflow/payments" * * function FundButton() { - * const { mutateAsync: fund, isPending } = useFund({ - * providers: [relayProvider()], - * }) + * const { mutateAsync: fund, isPending } = useFund() * * const handleFund = async () => { * const session = await fund({ * kind: "crypto", * destination: "eip155:747:0xRecipient", - * currency: "USDC", + * currency: "0xUSDC", * amount: "100", * sourceChain: "eip155:1", - * sourceCurrency: "USDC", + * sourceCurrency: "0xUSDC", * }) * console.log("Deposit to:", session.instructions.address) * } @@ -65,24 +58,21 @@ export interface UseFundArgs { * ``` */ export function useFund({ - providers, + paymentsClient: _paymentsClient, mutation: mutationOptions = {}, flowClient, -}: UseFundArgs): UseMutationResult { +}: UseFundArgs = {}): UseMutationResult { const queryClient = useFlowQueryClient() - const fcl = useFlowClient({flowClient}) - - const paymentsClient = useMemo( - () => - createPaymentsClient({ - providers, - flowClient: fcl, - }), - [providers, fcl] - ) + const contextPaymentsClient = usePaymentsClient() + const paymentsClient = _paymentsClient || contextPaymentsClient const mutationFn = useCallback( async (intent: FundingIntent) => { + if (!paymentsClient) { + throw new Error( + "No payments client available. Configure fundingProviders in FlowProvider or pass paymentsClient to useFund." + ) + } return paymentsClient.createSession(intent) }, [paymentsClient] diff --git a/packages/react-sdk/src/hooks/useFundingCapabilities.ts b/packages/react-sdk/src/hooks/useFundingCapabilities.ts index 91a228c8c..5298e8a6c 100644 --- a/packages/react-sdk/src/hooks/useFundingCapabilities.ts +++ b/packages/react-sdk/src/hooks/useFundingCapabilities.ts @@ -1,19 +1,17 @@ import {useQuery, UseQueryOptions, UseQueryResult} from "@tanstack/react-query" -import {useCallback, useMemo} from "react" +import {useCallback} from "react" import {useFlowQueryClient} from "../provider/FlowQueryClient" import {useFlowClient} from "./useFlowClient" -import { - createPaymentsClient, - FundingProviderFactory, - ProviderCapability, -} from "@onflow/payments" +import {usePaymentsClient} from "./usePaymentsClient" +import {useFlowChainId} from "./useFlowChainId" +import {ProviderCapability, PaymentsClient} from "@onflow/payments" /** * Arguments for the useFundingCapabilities hook. */ export interface UseFundingCapabilitiesArgs { - /** Array of funding provider factories to query */ - providers: FundingProviderFactory[] + /** Optional payments client (uses context if not provided) */ + paymentsClient?: PaymentsClient /** Optional React Query options */ query?: Omit< UseQueryOptions, @@ -29,19 +27,16 @@ export interface UseFundingCapabilitiesArgs { * Fetches the capabilities (supported chains, currencies, etc.) from funding providers. * Use this to dynamically populate UI with available funding options. * - * @param args.providers - Array of funding provider factories + * @param args.paymentsClient - Optional payments client (uses context if not provided) * @param args.query - Optional React Query options * @param args.flowClient - Optional Flow client override * * @example * ```tsx * import { useFundingCapabilities } from "@onflow/react-sdk" - * import { relayProvider } from "@onflow/payments" * * function FundingOptions() { - * const { data: capabilities, isLoading } = useFundingCapabilities({ - * providers: [relayProvider()], - * }) + * const { data: capabilities, isLoading } = useFundingCapabilities() * * if (isLoading) return

Loading...
* @@ -61,44 +56,52 @@ export interface UseFundingCapabilitiesArgs { * ``` */ export function useFundingCapabilities({ - providers, + paymentsClient: _paymentsClient, query: queryOptions = {}, flowClient, -}: UseFundingCapabilitiesArgs): UseQueryResult { +}: UseFundingCapabilitiesArgs = {}): UseQueryResult< + ProviderCapability[], + Error +> { const queryClient = useFlowQueryClient() - const fcl = useFlowClient({flowClient}) + const contextPaymentsClient = usePaymentsClient() + const paymentsClient = _paymentsClient || contextPaymentsClient + const {data: chainId} = useFlowChainId() - const paymentsClient = useMemo( - () => - createPaymentsClient({ - providers, - flowClient: fcl, - }), - [providers, fcl] - ) + // Use chainId in query key for proper cache invalidation when network switches + const chainIdForKey = chainId || "unknown" const fetchCapabilities = useCallback(async () => { - // Get all provider instances from the client - // We need to access the internal providers array - // Since it's not exposed, we'll need to call getCapabilities on the first provider - // This is a limitation - we should enhance the payments client to expose providers or aggregated capabilities + if (!paymentsClient) { + throw new Error( + "No payments client available. Configure fundingProviders in FlowProvider or pass paymentsClient to useFundingCapabilities." + ) + } + // Access providers from the payments client and get their capabilities const allCapabilities: ProviderCapability[] = [] - // Create provider instances manually to call getCapabilities - for (const factory of providers) { - const provider = factory({flowClient: fcl}) + // TODO: Expose providers array from PaymentsClient in the spec + // For now, we access them via type assertion + const providers = (paymentsClient as any).providers || [] + + for (const provider of providers) { const capabilities = await provider.getCapabilities() allCapabilities.push(...capabilities) } return allCapabilities - }, [providers, fcl]) + }, [paymentsClient]) return useQuery( { - queryKey: ["fundingCapabilities", providers.length], + queryKey: [ + "fundingCapabilities", + paymentsClient ? "configured" : "none", + chainIdForKey, + ], queryFn: fetchCapabilities, + enabled: !!paymentsClient, staleTime: 5 * 60 * 1000, // 5 minutes - capabilities don't change often ...queryOptions, }, diff --git a/packages/react-sdk/src/hooks/usePaymentsClient.ts b/packages/react-sdk/src/hooks/usePaymentsClient.ts new file mode 100644 index 000000000..dc5f9575a --- /dev/null +++ b/packages/react-sdk/src/hooks/usePaymentsClient.ts @@ -0,0 +1,10 @@ +import {useContext} from "react" +import {PaymentsClientContext} from "../core/context" + +/** + * Hook to access the PaymentsClient from FlowProvider context + * @returns The PaymentsClient instance or undefined if not configured + */ +export function usePaymentsClient() { + return useContext(PaymentsClientContext) +} diff --git a/packages/react-sdk/src/provider/FlowProvider.tsx b/packages/react-sdk/src/provider/FlowProvider.tsx index 388af9af1..e1c038b28 100644 --- a/packages/react-sdk/src/provider/FlowProvider.tsx +++ b/packages/react-sdk/src/provider/FlowProvider.tsx @@ -1,8 +1,17 @@ import React, {useState, PropsWithChildren, useMemo, useEffect} from "react" -import {FlowClientContext, FlowConfig, FlowConfigContext} from "../core/context" +import { + FlowClientContext, + FlowConfig, + FlowConfigContext, + PaymentsClientContext, +} from "../core/context" import {DefaultOptions, QueryClient} from "@tanstack/react-query" import {FlowQueryClientProvider} from "./FlowQueryClient" import {createFlowClient} from "@onflow/fcl" +import { + createPaymentsClient, + FundingProviderFactory, +} from "@onflow/payments" import {ThemeProvider, Theme} from "../core/theme" import {GlobalTransactionProvider} from "./GlobalTransactionProvider" import tailwindStyles from "../styles/tailwind.css" @@ -17,6 +26,7 @@ interface FlowProviderProps { flowJson?: Record theme?: Partial colorMode?: ColorMode + fundingProviders?: FundingProviderFactory[] } const defaultQueryOptions: DefaultOptions = { @@ -37,6 +47,7 @@ export function FlowProvider({ theme: customTheme, children, colorMode = "system", + fundingProviders = [], }: PropsWithChildren) { const [queryClient] = useState( () => _queryClient ?? new QueryClient({defaultOptions: defaultQueryOptions}) @@ -65,6 +76,14 @@ export function FlowProvider({ }) }, [_flowClient, initialConfig, flowJson]) + const paymentsClient = useMemo(() => { + if (fundingProviders.length === 0) return undefined + return createPaymentsClient({ + providers: fundingProviders, + flowClient, + }) + }, [fundingProviders, flowClient]) + // Helper function to get initial dark mode value const getInitialDarkMode = (colorMode: ColorMode): boolean => { if (colorMode === "dark") return true @@ -106,14 +125,16 @@ export function FlowProvider({ - - - - - {children} - - - + + + + + + {children} + + + + From f6d03808efec4c8710e19945644b6db6d309479f Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 10:24:58 -0800 Subject: [PATCH 10/15] fix fundcontent --- .../react-sdk/src/components/FundContent.tsx | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/packages/react-sdk/src/components/FundContent.tsx b/packages/react-sdk/src/components/FundContent.tsx index 8e872e26a..932a03f35 100644 --- a/packages/react-sdk/src/components/FundContent.tsx +++ b/packages/react-sdk/src/components/FundContent.tsx @@ -13,11 +13,7 @@ import {QRCode} from "./internal/QRCode" import {Address} from "./internal/Address" import {useFund} from "../hooks/useFund" import {useFundingCapabilities} from "../hooks/useFundingCapabilities" -import { - relayProvider, - CryptoProviderCapability, - CurrencyMetadata, -} from "@onflow/payments" +import {CryptoProviderCapability, CurrencyMetadata} from "@onflow/payments" import {useFlowCurrentUser} from "../hooks/useFlowCurrentUser" import {useFlowChainId} from "../hooks/useFlowChainId" import {useQuery} from "@tanstack/react-query" @@ -45,16 +41,12 @@ export const FundContent: React.FC = () => { const {user} = useFlowCurrentUser() const {data: chainId} = useFlowChainId() - const providers = [relayProvider()] - // Fetch available funding capabilities const { data: capabilities, isLoading: isLoadingCapabilities, error: capabilitiesError, - } = useFundingCapabilities({ - providers, - }) + } = useFundingCapabilities() // Extract crypto capabilities const cryptoCapability = capabilities?.find( @@ -119,14 +111,7 @@ export const FundContent: React.FC = () => { }, [selectedSourceChain, chainCurrencies]) // Initialize useFund hook with relay provider - const { - mutate: createSession, - data: session, - isPending, - error, - } = useFund({ - providers, - }) + const {mutate: createSession, data: session, isPending, error} = useFund({}) // Create funding session when crypto tab is selected and user is authenticated useEffect(() => { From 453a6825b9ab24bed365a31df33440fb10d38a41 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 10:31:47 -0800 Subject: [PATCH 11/15] Fix build --- packages/payments/src/providers/relay.test.ts | 171 ++++++++-------- packages/payments/src/providers/relay.ts | 4 +- packages/react-sdk/src/hooks/useFund.test.ts | 183 +++++++----------- .../react-sdk/src/provider/FlowProvider.tsx | 5 +- 4 files changed, 155 insertions(+), 208 deletions(-) diff --git a/packages/payments/src/providers/relay.test.ts b/packages/payments/src/providers/relay.test.ts index 2eeaeb0d8..237c5eaa8 100644 --- a/packages/payments/src/providers/relay.test.ts +++ b/packages/payments/src/providers/relay.test.ts @@ -6,6 +6,8 @@ describe("relayProvider", () => { const mockFlowClient = { getChainId: jest.fn().mockResolvedValue("mainnet"), + query: jest.fn(), + config: jest.fn(), } as any beforeEach(() => { @@ -81,8 +83,7 @@ describe("relayProvider", () => { expect(cryptoCap.sourceChains).toContain("eip155:1") expect(cryptoCap.sourceChains).toContain("eip155:8453") expect(cryptoCap.sourceChains).toContain("eip155:747") - expect(cryptoCap.sourceCurrencies).toContain("USDC") - expect(cryptoCap.currencies).toContain("USDC") + expect(cryptoCap.currencies).toContain("0x...") } }) @@ -93,13 +94,13 @@ describe("relayProvider", () => { id: 1, depositEnabled: true, disabled: false, - erc20Currencies: [{symbol: "USDC", supportsBridging: true}], + erc20Currencies: [{symbol: "USDC", address: "0x...", supportsBridging: true}], }, { id: 999, depositEnabled: false, // Not enabled disabled: false, - erc20Currencies: [{symbol: "USDC", supportsBridging: true}], + erc20Currencies: [{symbol: "USDC", address: "0x...", supportsBridging: true}], }, ], } @@ -150,37 +151,26 @@ describe("relayProvider", () => { ).rejects.toThrow("Fiat not supported") }) - it("should reject Cadence destinations", async () => { + it("should reject Cadence destinations without COA", async () => { + // Mock flowClient to simulate no COA found + ;(mockFlowClient.query as jest.Mock).mockResolvedValueOnce(null) + const providerFactory = relayProvider() const provider = providerFactory({flowClient: mockFlowClient}) const intent: CryptoFundingIntent = { kind: "crypto", destination: "eip155:747:0x8c5303eaa26202d6", // Cadence address (16 hex chars) - currency: "USDC", + currency: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // Use EVM address sourceChain: "eip155:1", - sourceCurrency: "USDC", + sourceCurrency: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", } await expect(provider.startSession(intent)).rejects.toThrow( - "Cadence destination detected" + /No COA.*found/ ) }) it("should reject symbol-based currency identifiers", async () => { - // Mock currencies API (required even though we reject symbols) - fetchSpy.mockResolvedValueOnce({ - ok: true, - json: async () => ({ - currencies: [ - { - symbol: "USDC", - address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - decimals: 6, - }, - ], - }), - }) - const providerFactory = relayProvider() const provider = providerFactory({flowClient: mockFlowClient}) const intent: CryptoFundingIntent = { @@ -192,6 +182,7 @@ describe("relayProvider", () => { sourceCurrency: "USDC", // Symbol not supported } + // Should reject immediately without any API calls await expect(provider.startSession(intent)).rejects.toThrow( /Invalid currency format/ ) @@ -199,30 +190,28 @@ describe("relayProvider", () => { it("should create session with explicit addresses", async () => { // Mock currencies API for decimal lookup (even with addresses, we need decimals) + // First call: for source currency on origin chain (chain 1) fetchSpy .mockResolvedValueOnce({ ok: true, - json: async () => ({ - currencies: [ - { - symbol: "USDC", - address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - decimals: 6, - }, - ], - }), + json: async () => [ + { + symbol: "USDC", + address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + decimals: 6, + }, + ], }) + // Second call: for destination currency on destination chain (chain 8453) .mockResolvedValueOnce({ ok: true, - json: async () => ({ - currencies: [ - { - symbol: "USDC", - address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", - decimals: 6, - }, - ], - }), + json: async () => [ + { + symbol: "USDC", + address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + decimals: 6, + }, + ], }) // Mock quote API .mockResolvedValueOnce({ @@ -259,31 +248,27 @@ describe("relayProvider", () => { }) it("should throw if deposit address not found in response", async () => { - // Mock currencies API + // Mock currencies API (returns array directly) fetchSpy .mockResolvedValueOnce({ ok: true, - json: async () => ({ - currencies: [ - { - symbol: "USDC", - address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - decimals: 6, - }, - ], - }), + json: async () => [ + { + symbol: "USDC", + address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + decimals: 6, + }, + ], }) .mockResolvedValueOnce({ ok: true, - json: async () => ({ - currencies: [ - { - symbol: "USDC", - address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", - decimals: 6, - }, - ], - }), + json: async () => [ + { + symbol: "USDC", + address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + decimals: 6, + }, + ], }) // Mock quote API with no deposit address .mockResolvedValueOnce({ @@ -314,6 +299,8 @@ describe("relayProvider", () => { const providerFactory = relayProvider() const provider = providerFactory({flowClient: mockFlowClient}) + let currenciesCallCount = 0 + // Mock Relay API responses fetchSpy.mockImplementation((url: string | Request | URL) => { const urlString = url.toString() @@ -322,40 +309,43 @@ describe("relayProvider", () => { return Promise.resolve({ ok: true, json: () => - Promise.resolve([ - { - id: "1", - name: "Ethereum", - depositEnabled: true, - erc20Currencies: [ - { - symbol: "USDC", - address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - decimals: 6, - }, - ], - }, - { - id: "747", - name: "Flow EVM", - depositEnabled: true, - erc20Currencies: [ - { - symbol: "FLOW", - address: "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e", - decimals: 18, - }, - ], - }, - ]), + Promise.resolve({ + chains: [ + { + id: 1, + name: "Ethereum", + depositEnabled: true, + erc20Currencies: [ + { + symbol: "USDC", + address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + decimals: 6, + }, + ], + }, + { + id: 747, + name: "Flow EVM", + depositEnabled: true, + erc20Currencies: [ + { + symbol: "FLOW", + address: "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e", + decimals: 18, + }, + ], + }, + ], + }), } as Response) } if (urlString.includes("/currencies")) { - const urlObj = new URL(urlString) - const chainId = urlObj.searchParams.get("chainId") - - if (chainId === "1") { + // currencies/v2 is called twice - once for origin, once for destination + currenciesCallCount++ + + if (currenciesCallCount === 1) { + // First call: origin chain (Ethereum, chain 1) return Promise.resolve({ ok: true, json: () => @@ -367,7 +357,8 @@ describe("relayProvider", () => { }, ]), } as Response) - } else if (chainId === "747") { + } else { + // Second call: destination chain (Flow EVM, chain 747) return Promise.resolve({ ok: true, json: () => diff --git a/packages/payments/src/providers/relay.ts b/packages/payments/src/providers/relay.ts index 61e48920e..c1245282f 100644 --- a/packages/payments/src/providers/relay.ts +++ b/packages/payments/src/providers/relay.ts @@ -174,14 +174,14 @@ export function relayProvider( if (chain.erc20Currencies) { chain.erc20Currencies.forEach(currency => { if (currency.supportsBridging) { - flowCurrencies.add(currency.symbol) + flowCurrencies.add(currency.address) } }) } // Also check featured tokens if (chain.featuredTokens) { chain.featuredTokens.forEach(token => { - flowCurrencies.add(token.symbol) + flowCurrencies.add(token.address) }) } } diff --git a/packages/react-sdk/src/hooks/useFund.test.ts b/packages/react-sdk/src/hooks/useFund.test.ts index 25cece0da..53b5f28bb 100644 --- a/packages/react-sdk/src/hooks/useFund.test.ts +++ b/packages/react-sdk/src/hooks/useFund.test.ts @@ -1,136 +1,95 @@ -import {renderHook} from "@testing-library/react" -import * as fcl from "@onflow/fcl" -import {FlowProvider} from "../provider" +import {renderHook, waitFor} from "@testing-library/react" import {useFund} from "./useFund" -import {createMockFclInstance, MockFclInstance} from "../__mocks__/flow-client" -import { - FundingProviderFactory, - FundingSession, - CryptoFundingIntent, -} from "@onflow/payments" - -jest.mock("@onflow/fcl", () => require("../__mocks__/fcl").default) +import {FundingIntent, FundingSession, PaymentsClient} from "@onflow/payments" +import {FlowProvider} from "../provider/FlowProvider" describe("useFund", () => { - let mockFcl: MockFclInstance + const mockSession: FundingSession = { + provider: "test-provider", + instructions: { + kind: "crypto", + address: "0xTestDepositAddress", + chain: "eip155:1", + currency: "0xUSDC", + }, + } + + const mockIntent: FundingIntent = { + kind: "crypto", + destination: "eip155:747:0xRecipient", + currency: "0xUSDC", + amount: "100", + sourceChain: "eip155:1", + sourceCurrency: "0xUSDC", + } + + // Create mock payments client + const createMockPaymentsClient = (shouldSucceed: boolean): PaymentsClient => { + return { + createSession: jest + .fn() + .mockResolvedValue( + shouldSucceed + ? mockSession + : Promise.reject(new Error("Provider failed")) + ), + } as any + } beforeEach(() => { - mockFcl = createMockFclInstance() - // Override getChainId to return numeric chain ID (payments client expects "747", not "mainnet") - mockFcl.mockFclInstance.getChainId = jest.fn().mockResolvedValue("747") - jest.mocked(fcl.createFlowClient).mockReturnValue(mockFcl.mockFclInstance) - }) - - afterEach(() => { jest.clearAllMocks() }) - const createMockProviderFactory = - (session: FundingSession): FundingProviderFactory => - () => ({ - id: "mock-provider", - getCapabilities: jest.fn().mockResolvedValue([{type: "crypto"}]), - startSession: jest.fn().mockResolvedValue(session), - }) + it("should successfully create a funding session", async () => { + const mockClient = createMockPaymentsClient(true) - test("creates a funding session and returns it", async () => { - const mockSession: FundingSession = { - id: "session-123", - providerId: "mock-provider", - kind: "crypto", - instructions: { - address: "0x1234567890123456789012345678901234567890", - }, - } - - const startSessionSpy = jest.fn().mockResolvedValue(mockSession) - const mockProviderFactory = (): FundingProviderFactory => () => ({ - id: "mock-provider", - getCapabilities: jest.fn().mockResolvedValue([{type: "crypto"}]), - startSession: startSessionSpy, + const {result} = renderHook(() => useFund({paymentsClient: mockClient}), { + wrapper: FlowProvider, }) - const {result} = renderHook( - () => - useFund({ - providers: [mockProviderFactory()], - }), - { - wrapper: FlowProvider, - } - ) - - const intent: CryptoFundingIntent = { - kind: "crypto", - destination: "eip155:747:0xRecipient", - currency: "USDC", - amount: "100", - sourceChain: "eip155:1", - sourceCurrency: "USDC", - } + // Trigger the mutation + result.current.mutateAsync(mockIntent) - const session = await result.current.mutateAsync(intent) + await waitFor(() => { + expect(result.current.isSuccess).toBe(true) + }) - expect(session).toEqual(mockSession) - expect(startSessionSpy).toHaveBeenCalledWith(intent) + expect(result.current.data).toEqual(mockSession) + expect(mockClient.createSession).toHaveBeenCalledWith(mockIntent) }) - test("handles provider error", async () => { + it("should handle funding session errors", async () => { const error = new Error("Provider failed") - const mockProviderFactory: FundingProviderFactory = () => ({ - id: "failing-provider", - getCapabilities: jest.fn().mockResolvedValue([{type: "crypto"}]), - startSession: jest.fn().mockRejectedValue(error), + const mockClient = createMockPaymentsClient(false) + + const {result} = renderHook(() => useFund({paymentsClient: mockClient}), { + wrapper: FlowProvider, }) - const {result} = renderHook( - () => - useFund({ - providers: [mockProviderFactory], - }), - { - wrapper: FlowProvider, - } - ) + // Trigger the mutation + result.current.mutate(mockIntent) - const intent: CryptoFundingIntent = { - kind: "crypto", - destination: "eip155:747:0xRecipient", - currency: "USDC", - sourceChain: "eip155:1", - sourceCurrency: "USDC", - } - - await expect(result.current.mutateAsync(intent)).rejects.toThrow( - "Failed to create session: no provider could handle the request" - ) + await waitFor(() => { + expect(result.current.isError).toBe(true) + }) + + expect(result.current.error?.message).toContain("Provider failed") }) - test("returns mutation state properties", () => { - const mockSession: FundingSession = { - id: "session-123", - providerId: "mock-provider", - kind: "crypto", - instructions: {address: "0x123"}, - } - - const mockProviderFactory = createMockProviderFactory(mockSession) - - const {result} = renderHook( - () => - useFund({ - providers: [mockProviderFactory], - }), - { - wrapper: FlowProvider, - } - ) + it("should throw error when no payments client is provided", async () => { + const {result} = renderHook(() => useFund(), { + wrapper: FlowProvider, + }) + + // Trigger the mutation + result.current.mutate(mockIntent) - // Check that mutation properties are present - expect(result.current.mutate).toBeInstanceOf(Function) - expect(result.current.mutateAsync).toBeInstanceOf(Function) - expect(result.current.isPending).toBe(false) - expect(result.current.isError).toBe(false) - expect(result.current.isSuccess).toBe(false) + await waitFor(() => { + expect(result.current.isError).toBe(true) + }) + + expect(result.current.error?.message).toContain( + "No payments client available" + ) }) }) diff --git a/packages/react-sdk/src/provider/FlowProvider.tsx b/packages/react-sdk/src/provider/FlowProvider.tsx index e1c038b28..e35b064eb 100644 --- a/packages/react-sdk/src/provider/FlowProvider.tsx +++ b/packages/react-sdk/src/provider/FlowProvider.tsx @@ -8,10 +8,7 @@ import { import {DefaultOptions, QueryClient} from "@tanstack/react-query" import {FlowQueryClientProvider} from "./FlowQueryClient" import {createFlowClient} from "@onflow/fcl" -import { - createPaymentsClient, - FundingProviderFactory, -} from "@onflow/payments" +import {createPaymentsClient, FundingProviderFactory} from "@onflow/payments" import {ThemeProvider, Theme} from "../core/theme" import {GlobalTransactionProvider} from "./GlobalTransactionProvider" import tailwindStyles from "../styles/tailwind.css" From a5f5e0b4ea9108ac197dc20df7b461d2d381df4f Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 10:39:47 -0800 Subject: [PATCH 12/15] debug --- packages/payments/src/providers/relay.test.ts | 10 +++-- packages/payments/src/providers/relay.ts | 43 ++++++++++++++++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/packages/payments/src/providers/relay.test.ts b/packages/payments/src/providers/relay.test.ts index 237c5eaa8..c3a3e62f3 100644 --- a/packages/payments/src/providers/relay.test.ts +++ b/packages/payments/src/providers/relay.test.ts @@ -94,13 +94,17 @@ describe("relayProvider", () => { id: 1, depositEnabled: true, disabled: false, - erc20Currencies: [{symbol: "USDC", address: "0x...", supportsBridging: true}], + erc20Currencies: [ + {symbol: "USDC", address: "0x...", supportsBridging: true}, + ], }, { id: 999, depositEnabled: false, // Not enabled disabled: false, - erc20Currencies: [{symbol: "USDC", address: "0x...", supportsBridging: true}], + erc20Currencies: [ + {symbol: "USDC", address: "0x...", supportsBridging: true}, + ], }, ], } @@ -343,7 +347,7 @@ describe("relayProvider", () => { if (urlString.includes("/currencies")) { // currencies/v2 is called twice - once for origin, once for destination currenciesCallCount++ - + if (currenciesCallCount === 1) { // First call: origin chain (Ethereum, chain 1) return Promise.resolve({ diff --git a/packages/payments/src/providers/relay.ts b/packages/payments/src/providers/relay.ts index c1245282f..df4f5d25f 100644 --- a/packages/payments/src/providers/relay.ts +++ b/packages/payments/src/providers/relay.ts @@ -170,18 +170,50 @@ export function relayProvider( const isFlowEVM = chain.id === flowEvmChainId if (isFlowEVM) { + console.log("[Relay Provider] Flow EVM chain found:", chain) // Add ERC20 currencies from Flow if (chain.erc20Currencies) { chain.erc20Currencies.forEach(currency => { - if (currency.supportsBridging) { + console.log( + "[Relay Provider] Checking currency:", + currency.symbol, + "address:", + currency.address, + "supportsBridging:", + currency.supportsBridging + ) + // Only add currencies that support bridging AND have an address + if (currency.supportsBridging && currency.address) { + console.log( + "[Relay Provider] ✓ Adding currency:", + currency.symbol, + currency.address + ) flowCurrencies.add(currency.address) + } else if (!currency.address) { + console.warn( + "[Relay Provider] Currency missing address:", + currency + ) } }) } // Also check featured tokens if (chain.featuredTokens) { chain.featuredTokens.forEach(token => { - flowCurrencies.add(token.address) + if (token.address) { + console.log( + "[Relay Provider] Adding featured token:", + token.symbol, + token.address + ) + flowCurrencies.add(token.address) + } else { + console.warn( + "[Relay Provider] Featured token missing address:", + token + ) + } }) } } @@ -190,6 +222,13 @@ export function relayProvider( // sourceCurrencies should match destination currencies // You can only bridge tokens that exist on Flow const flowCurrenciesArray = Array.from(flowCurrencies) + console.log( + "[Relay Provider] Final currencies array:", + flowCurrenciesArray + ) + + console.log("[Relay Provider] Supported chains:", supportedChains) + console.log("[Relay Provider] Flow EVM chain ID:", flowEvmChainId) return [ { From 6e10e7e1bedfbdd02ef14ee1fe3527996932656c Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 10:55:01 -0800 Subject: [PATCH 13/15] Fix capabilities bug --- .../src/components/flow-provider-wrapper.tsx | 2 ++ packages/payments/src/client.ts | 14 ++++++++ packages/payments/src/providers/relay.ts | 36 ------------------- .../src/hooks/useFundingCapabilities.ts | 15 ++------ 4 files changed, 18 insertions(+), 49 deletions(-) diff --git a/packages/demo/src/components/flow-provider-wrapper.tsx b/packages/demo/src/components/flow-provider-wrapper.tsx index abd9bc52e..81053f087 100644 --- a/packages/demo/src/components/flow-provider-wrapper.tsx +++ b/packages/demo/src/components/flow-provider-wrapper.tsx @@ -1,5 +1,6 @@ import * as fcl from "@onflow/fcl" import {FlowProvider, type FlowNetwork} from "@onflow/react-sdk" +import {relayProvider} from "@onflow/payments" import React, {createContext, useCallback, useContext, useState} from "react" // Dark mode context @@ -151,6 +152,7 @@ function FlowProviderInner({ flowNetwork: currentNetwork, }} colorMode={darkMode ? "dark" : "light"} + fundingProviders={[relayProvider()]} > {children} diff --git a/packages/payments/src/client.ts b/packages/payments/src/client.ts index 1a2494b52..ca0704a9f 100644 --- a/packages/payments/src/client.ts +++ b/packages/payments/src/client.ts @@ -3,6 +3,7 @@ import type { FundingSession, FundingProvider, FundingProviderFactory, + ProviderCapability, } from "./types" import type {createFlowClientCore} from "@onflow/fcl-core" import {ADDRESS_PATTERN} from "./constants" @@ -18,6 +19,11 @@ export interface PaymentsClient { * @returns Promise resolving to a funding session with instructions */ createSession(intent: FundingIntent): Promise + /** + * Get capabilities from all configured providers + * @returns Promise resolving to an array of provider capabilities + */ + getCapabilities(): Promise } /** @@ -134,5 +140,13 @@ export function createPaymentsClient( `Failed to create session: no provider could handle the request. Errors: ${errorDetails}` ) }, + async getCapabilities() { + const allCapabilities: ProviderCapability[] = [] + for (const provider of providers) { + const capabilities = await provider.getCapabilities() + allCapabilities.push(...capabilities) + } + return allCapabilities + }, } } diff --git a/packages/payments/src/providers/relay.ts b/packages/payments/src/providers/relay.ts index df4f5d25f..d1f5a531c 100644 --- a/packages/payments/src/providers/relay.ts +++ b/packages/payments/src/providers/relay.ts @@ -170,31 +170,12 @@ export function relayProvider( const isFlowEVM = chain.id === flowEvmChainId if (isFlowEVM) { - console.log("[Relay Provider] Flow EVM chain found:", chain) // Add ERC20 currencies from Flow if (chain.erc20Currencies) { chain.erc20Currencies.forEach(currency => { - console.log( - "[Relay Provider] Checking currency:", - currency.symbol, - "address:", - currency.address, - "supportsBridging:", - currency.supportsBridging - ) // Only add currencies that support bridging AND have an address if (currency.supportsBridging && currency.address) { - console.log( - "[Relay Provider] ✓ Adding currency:", - currency.symbol, - currency.address - ) flowCurrencies.add(currency.address) - } else if (!currency.address) { - console.warn( - "[Relay Provider] Currency missing address:", - currency - ) } }) } @@ -202,17 +183,7 @@ export function relayProvider( if (chain.featuredTokens) { chain.featuredTokens.forEach(token => { if (token.address) { - console.log( - "[Relay Provider] Adding featured token:", - token.symbol, - token.address - ) flowCurrencies.add(token.address) - } else { - console.warn( - "[Relay Provider] Featured token missing address:", - token - ) } }) } @@ -222,13 +193,6 @@ export function relayProvider( // sourceCurrencies should match destination currencies // You can only bridge tokens that exist on Flow const flowCurrenciesArray = Array.from(flowCurrencies) - console.log( - "[Relay Provider] Final currencies array:", - flowCurrenciesArray - ) - - console.log("[Relay Provider] Supported chains:", supportedChains) - console.log("[Relay Provider] Flow EVM chain ID:", flowEvmChainId) return [ { diff --git a/packages/react-sdk/src/hooks/useFundingCapabilities.ts b/packages/react-sdk/src/hooks/useFundingCapabilities.ts index 5298e8a6c..5cd7d5c30 100644 --- a/packages/react-sdk/src/hooks/useFundingCapabilities.ts +++ b/packages/react-sdk/src/hooks/useFundingCapabilities.ts @@ -78,19 +78,8 @@ export function useFundingCapabilities({ ) } - // Access providers from the payments client and get their capabilities - const allCapabilities: ProviderCapability[] = [] - - // TODO: Expose providers array from PaymentsClient in the spec - // For now, we access them via type assertion - const providers = (paymentsClient as any).providers || [] - - for (const provider of providers) { - const capabilities = await provider.getCapabilities() - allCapabilities.push(...capabilities) - } - - return allCapabilities + // Use the getCapabilities method from the payments client + return await paymentsClient.getCapabilities() }, [paymentsClient]) return useQuery( From f231d8259ccec915bad38cdecf2eb4cb3d289752 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 11 Dec 2025 14:02:41 -0800 Subject: [PATCH 14/15] Add working whitelist --- packages/payments/src/providers/relay.ts | 45 ++++++++---- .../react-sdk/src/components/FundContent.tsx | 71 ++++++++++++++++--- 2 files changed, 92 insertions(+), 24 deletions(-) diff --git a/packages/payments/src/providers/relay.ts b/packages/payments/src/providers/relay.ts index d1f5a531c..7422049c2 100644 --- a/packages/payments/src/providers/relay.ts +++ b/packages/payments/src/providers/relay.ts @@ -253,19 +253,29 @@ export function relayProvider( if (isCadenceDestination) { // Fetch the user's COA (Cadence Owned Account) EVM address - const coaAddress = await getCoaAddress({ - flowClient, - cadenceAddress: destination.address, - }) + try { + const coaAddress = await getCoaAddress({ + flowClient, + cadenceAddress: destination.address, + }) + + if (!coaAddress) { + throw new Error( + `No COA (Cadence Owned Account) found for Cadence address ${destination.address}. ` + + `Please ensure your Flow account has a COA set up. ` + + `Alternatively, connect using your Flow EVM address directly.` + ) + } - if (!coaAddress) { + actualDestination = coaAddress + } catch (error) { throw new Error( - `No COA (Cadence Owned Account) found for ${destination.address}. ` + - `Please ensure the account has a COA set up at /public/evm.` + `Failed to get COA for Cadence address ${destination.address}: ${ + error instanceof Error ? error.message : String(error) + }. ` + + `Try connecting with your Flow EVM address instead, or ensure your account has a COA set up.` ) } - - actualDestination = coaAddress } if (!isEvmAddress(actualDestination)) { @@ -289,19 +299,24 @@ export function relayProvider( cryptoIntent.currency ) - // Convert human-readable amount to base units if provided - const amountInBaseUnits = cryptoIntent.amount - ? toBaseUnits(cryptoIntent.amount, originCurrency.decimals) - : undefined + // Convert human-readable amount to base units + // For deposit address mode, if no amount is specified, use a default that covers fees + // The actual amount sent by the user can be different for deposit addresses + // Use 1.0 (~$1 for stablecoins) which is enough to cover fees but not hit liquidity limits + const amountForQuote = cryptoIntent.amount || "1.0" + const amountInBaseUnits = toBaseUnits( + amountForQuote, + originCurrency.decimals + ) // Call Relay API with deposit address mode const quote = await callRelayQuote(apiUrl, { - user: destination.address, + user: actualDestination, originChainId: parseInt(originChainId), destinationChainId: parseInt(destinationChainId), originCurrency: originCurrency.address, destinationCurrency: destinationCurrency.address, - recipient: destination.address, + recipient: actualDestination, amount: amountInBaseUnits, tradeType: DEPOSIT_ADDRESS_TRADE_TYPE, // Deposit addresses only work with EXACT_INPUT useDepositAddress: true, diff --git a/packages/react-sdk/src/components/FundContent.tsx b/packages/react-sdk/src/components/FundContent.tsx index 932a03f35..6ebdd52dc 100644 --- a/packages/react-sdk/src/components/FundContent.tsx +++ b/packages/react-sdk/src/components/FundContent.tsx @@ -34,6 +34,18 @@ const getChainName = (caipId: string): string => { return chain?.name || caipId } +// Helper to convert Flow network name to Flow EVM chain ID +const getFlowEvmChainId = (network: string): number => { + switch (network) { + case "mainnet": + return 747 // Flow EVM Mainnet + case "testnet": + return 545 // Flow EVM Testnet + default: + return 747 // Default to mainnet + } +} + export const FundContent: React.FC = () => { const [amount, setAmount] = useState("") const [selectedTabIndex, setSelectedTabIndex] = useState(0) @@ -53,10 +65,6 @@ export const FundContent: React.FC = () => { c => c.type === "crypto" ) as CryptoProviderCapability - // Get destination currencies (what can be received on Flow) - // These are typically FLOW, USDF, WFLOW, etc. - const destinationCurrencies = cryptoCapability?.currencies || [] - // Build SOURCE chains list (where user can send FROM) const sourceChains = (cryptoCapability?.sourceChains || []).map( (caipId, index) => ({ @@ -88,9 +96,26 @@ export const FundContent: React.FC = () => { if (!selectedSourceChain || !cryptoCapability?.getCurrenciesForChain) { return [] } - return await cryptoCapability.getCurrenciesForChain( + const currencies = await cryptoCapability.getCurrenciesForChain( selectedSourceChain.caipId ) + + // Whitelist of tokens that reliably support Relay deposit addresses + // These have been tested and confirmed to work with Flow EVM as destination + const SUPPORTED_TOKENS = new Set([ + // USDC (works on all chains) + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // Ethereum + "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", // Base + "0xaf88d065e77c8cc2239327c5edb3a432268e5831", // Arbitrum + "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", // Polygon + // USDT (works on Base, Polygon) + "0xfde4c96c8593536e31f229ea8f37b2ada2699bb2", // Base + "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", // Polygon + ]) + + return currencies.filter(c => + SUPPORTED_TOKENS.has(c.address.toLowerCase()) + ) }, enabled: !!selectedSourceChain && !!cryptoCapability, staleTime: 5 * 60 * 1000, @@ -101,6 +126,30 @@ export const FundContent: React.FC = () => { // Use currency metadata directly const sourceTokens = chainCurrencies || [] + // Fetch destination currencies for Flow EVM + const {data: destinationCurrencies} = useQuery( + { + queryKey: ["destinationCurrencies", chainId], + queryFn: async () => { + if (!chainId || !cryptoCapability?.getCurrenciesForChain) { + return [] + } + // Fetch currencies available on Flow EVM (destination chain) + const flowEvmChainId = getFlowEvmChainId(chainId) + const currencies = await cryptoCapability.getCurrenciesForChain( + `eip155:${flowEvmChainId}` + ) + // Filter out native tokens (zero address) - Relay only supports ERC20 for deposit addresses + return currencies.filter( + c => c.address !== "0x0000000000000000000000000000000000000000" + ) + }, + enabled: !!chainId && !!cryptoCapability, + staleTime: 5 * 60 * 1000, + }, + queryClient + ) + // Update token selection when currencies load or chain changes useEffect(() => { if (sourceTokens.length > 0) { @@ -120,17 +169,20 @@ export const FundContent: React.FC = () => { user?.addr && chainId && selectedSourceToken && - selectedSourceChain + selectedSourceChain && + destinationCurrencies && + destinationCurrencies.length > 0 ) { // User's Flow address as destination (in CAIP-10 format) - const destination = `eip155:${chainId}:${user.addr}` + const flowEvmChainId = getFlowEvmChainId(chainId) + const destination = `eip155:${flowEvmChainId}:${user.addr}` createSession({ kind: "crypto", destination, - // Use first available destination currency (e.g., FLOW, USDF) + // Use first available destination currency (e.g., USDC on Flow EVM) // Relay will automatically bridge source token to this - currency: destinationCurrencies[0] || selectedSourceToken.address, + currency: destinationCurrencies[0].address, sourceChain: selectedSourceChain.caipId, sourceCurrency: selectedSourceToken.address, amount: amount || undefined, @@ -140,6 +192,7 @@ export const FundContent: React.FC = () => { selectedTabIndex, selectedSourceToken, selectedSourceChain, + destinationCurrencies, amount, user?.addr, chainId, From d9c687b7463018daccf305eac6932a5903664c6e Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 16 Dec 2025 08:30:01 -0800 Subject: [PATCH 15/15] fix build --- packages/react-sdk/src/components/FundContent.tsx | 10 ++++++---- packages/react-sdk/src/index.ts | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/react-sdk/src/components/FundContent.tsx b/packages/react-sdk/src/components/FundContent.tsx index 6ebdd52dc..f0ebbf1b8 100644 --- a/packages/react-sdk/src/components/FundContent.tsx +++ b/packages/react-sdk/src/components/FundContent.tsx @@ -11,11 +11,13 @@ import { } from "./internal/Listbox" import {QRCode} from "./internal/QRCode" import {Address} from "./internal/Address" -import {useFund} from "../hooks/useFund" -import {useFundingCapabilities} from "../hooks/useFundingCapabilities" +import { + useFund, + useFundingCapabilities, + useFlowCurrentUser, + useFlowChainId, +} from "@onflow/react-core" import {CryptoProviderCapability, CurrencyMetadata} from "@onflow/payments" -import {useFlowCurrentUser} from "../hooks/useFlowCurrentUser" -import {useFlowChainId} from "../hooks/useFlowChainId" import {useQuery} from "@tanstack/react-query" import {useFlowQueryClient} from "../provider/FlowQueryClient" import * as viemChains from "viem/chains" diff --git a/packages/react-sdk/src/index.ts b/packages/react-sdk/src/index.ts index c77bd166a..9bb01c758 100644 --- a/packages/react-sdk/src/index.ts +++ b/packages/react-sdk/src/index.ts @@ -33,6 +33,9 @@ export { useFlowScheduledTransactionCancel, ScheduledTransactionPriority, ScheduledTransactionStatus, + useFund, + useFundingCapabilities, + usePaymentsClient, } from "@onflow/react-core" // Re-export types from hooks