Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 16 additions & 18 deletions packages/payments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,16 @@ const client = createPaymentsClient({

const session = await client.createSession({
kind: "crypto",
destination: "eip155:747:0xRecipient", // CAIP-10 format
currency: "USDC", // Symbol, EVM address, or Cadence vault identifier
destination: "eip155:747:0xRecipient", // CAIP-10: Flow EVM address
currency: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", // USDC - EVM address or Cadence vault identifier
amount: "100.0", // Human-readable decimal format
sourceChain: "eip155:1", // CAIP-2: source chain
sourceCurrency: "USDC",
sourceChain: "eip155:1", // CAIP-2: Ethereum mainnet
sourceCurrency: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // USDC on Ethereum
})

// session.instructions contains provider-specific funding instructions
console.log(session.instructions.address) // e.g., deposit address
// session.instructions.address contains a unique deposit address
// User sends USDC on Ethereum to this address, Relay bridges it to Flow EVM
console.log(session.instructions.address)
```

## Core Concepts
Expand All @@ -52,10 +53,10 @@ Describes what the user wants to fund:
interface CryptoFundingIntent {
kind: "crypto"
destination: string // CAIP-10 account identifier
currency: string // Token symbol, EVM address, or Cadence vault ID
currency: string // EVM address (0x...) or Cadence vault ID (A.xxx.Token.Vault)
amount?: string // Human-readable amount (e.g., "100.50")
sourceChain: string // CAIP-2 chain identifier
sourceCurrency: string // Source token identifier
sourceCurrency: string // Source token EVM address
}
```

Expand Down Expand Up @@ -89,25 +90,22 @@ interface FundingProvider {

## Token Formats

The client accepts multiple token identifier formats:
The client accepts two token identifier formats:

**1. Symbols:**
**1. EVM Addresses (0x + 40 hex):**
```ts
currency: "USDC"
currency: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" // USDC on Ethereum
```

**2. EVM Addresses (0x + 40 hex):**
**2. Cadence Vault Identifiers:**
```ts
currency: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
```

**3. Cadence Vault Identifiers:**
```ts
currency: "A.1654653399040a61.FlowToken.Vault"
currency: "A.1654653399040a61.FlowToken.Vault" // FlowToken
```

When you provide a **Cadence vault identifier**, the client queries the **Flow EVM Bridge** to automatically convert the vault ID to the corresponding EVM address before passing to providers.

**Note:** Token symbols (e.g., "USDC", "USDF") are NOT supported. You must provide the full EVM address or Cadence vault identifier.

## Development

```bash
Expand Down
10 changes: 4 additions & 6 deletions packages/payments/src/bridge-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@
import type {createFlowClientCore} from "@onflow/fcl-core"
import {getContracts} from "@onflow/config"
import flowJSON from "../flow.json"
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"

type FlowNetwork = "emulator" | "testnet" | "mainnet"

interface BridgeQueryOptions {
flowClient: ReturnType<typeof createFlowClientCore>
}
Expand All @@ -22,11 +21,10 @@ async function resolveCadence(
flowClient: ReturnType<typeof createFlowClientCore>,
cadence: string
): Promise<string> {
const chainId = await flowClient.getChainId()
const n = chainId.replace(/^flow-/, "").toLowerCase()
const network: FlowNetwork = n === "local" ? "emulator" : (n as FlowNetwork)
const chainId = (await flowClient.getChainId()) as FlowNetwork
const network = chainId === "local" ? "emulator" : chainId

const contracts = getContracts(flowJSON, network)
const contracts = getContracts(flowJSON, network) as Record<string, string>
return cadence.replace(/import\s+"(\w+)"/g, (match, name) =>
contracts[name] ? `import ${name} from 0x${contracts[name]}` : match
)
Expand Down
26 changes: 17 additions & 9 deletions packages/payments/src/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {createPaymentsClient} from "./client"
import {FundingProvider, FundingIntent, FundingSession} from "./types"
import {
FundingProvider,
FundingProviderFactory,
FundingIntent,
FundingSession,
} from "./types"

describe("createPaymentsClient", () => {
const mockFlowClient = {
Expand All @@ -15,7 +20,7 @@ describe("createPaymentsClient", () => {
}

const client = createPaymentsClient({
providers: [mockProvider],
providers: [({flowClient}) => mockProvider],
flowClient: mockFlowClient,
})
expect(client.createSession).toBeInstanceOf(Function)
Expand All @@ -36,7 +41,7 @@ describe("createPaymentsClient", () => {
}

const client = createPaymentsClient({
providers: [mockProvider],
providers: [({flowClient}) => mockProvider],
flowClient: mockFlowClient,
})
const intent: FundingIntent = {
Expand Down Expand Up @@ -74,7 +79,10 @@ describe("createPaymentsClient", () => {
}

const client = createPaymentsClient({
providers: [failingProvider, workingProvider],
providers: [
({flowClient}) => failingProvider,
({flowClient}) => workingProvider,
],
flowClient: mockFlowClient,
})
const intent: FundingIntent = {
Expand Down Expand Up @@ -106,7 +114,7 @@ describe("createPaymentsClient", () => {
}

const client = createPaymentsClient({
providers: [provider1, provider2],
providers: [() => provider1, () => provider2],
flowClient: mockFlowClient,
})
const intent: FundingIntent = {
Expand Down Expand Up @@ -169,7 +177,7 @@ describe("createPaymentsClient", () => {
}

const client = createPaymentsClient({
providers: [provider1, provider2],
providers: [({flowClient}) => provider1, ({flowClient}) => provider2],
flowClient: mockFlowClient,
})
const intent: FundingIntent = {
Expand Down Expand Up @@ -202,7 +210,7 @@ describe("createPaymentsClient", () => {
}

const client = createPaymentsClient({
providers: [fiatProvider],
providers: [({flowClient}) => fiatProvider],
flowClient: mockFlowClient,
})
const intent: FundingIntent = {
Expand Down Expand Up @@ -246,7 +254,7 @@ describe("createPaymentsClient", () => {
}

const client = createPaymentsClient({
providers: [mockProvider],
providers: [() => mockProvider],
flowClient: mockFlowClient,
})
const intent: FundingIntent = {
Expand Down Expand Up @@ -280,7 +288,7 @@ describe("createPaymentsClient", () => {
}

const client = createPaymentsClient({
providers: [mockProvider],
providers: [() => mockProvider],
flowClient: mockFlowClient,
})
const intent: FundingIntent = {
Expand Down
18 changes: 14 additions & 4 deletions packages/payments/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type {FundingIntent, FundingSession, FundingProvider} from "./types"
import type {
FundingIntent,
FundingSession,
FundingProvider,
FundingProviderFactory,
} from "./types"
import type {createFlowClientCore} from "@onflow/fcl-core"
import {ADDRESS_PATTERN} from "./constants"
import {getEvmAddressFromVaultType} from "./bridge-service"
Expand All @@ -19,8 +24,8 @@ export interface PaymentsClient {
* Configuration for creating a payments client
*/
export interface PaymentsClientConfig {
/** Array of funding providers to use (in priority order) */
providers: FundingProvider[]
/** Array of funding provider factories to use (in priority order) */
providers: FundingProviderFactory[]
/** Flow client (FCL Core or SDK) for Cadence vault ID conversion */
flowClient: ReturnType<typeof createFlowClientCore>
}
Expand Down Expand Up @@ -97,6 +102,11 @@ async function convertCadenceCurrencies(
export function createPaymentsClient(
config: PaymentsClientConfig
): PaymentsClient {
// Initialize providers by passing flowClient to each factory
const providers: FundingProvider[] = config.providers.map(factory =>
factory({flowClient: config.flowClient})
)

return {
async createSession(intent) {
// Convert Cadence vault identifiers to EVM addresses
Expand All @@ -106,7 +116,7 @@ export function createPaymentsClient(
)

const providerErrors: {id?: string; error: any}[] = []
for (const provider of config.providers) {
for (const provider of providers) {
try {
return await provider.startSession(processedIntent)
} catch (err) {
Expand Down
11 changes: 11 additions & 0 deletions packages/payments/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,14 @@ export const ADDRESS_PATTERN = {
/** Cadence vault identifier format: A.{address}.{contract}.Vault */
CADENCE_VAULT: /^A\.[a-fA-F0-9]+\.[A-Za-z0-9_]+\.Vault$/,
} as const

export type FlowNetwork = "local" | "testnet" | "mainnet"

/**
* Flow EVM chain IDs mapped from Flow network names
*/
export const FLOW_EVM_CHAIN_IDS: Record<FlowNetwork, number> = {
mainnet: 747,
testnet: 545,
local: 646,
}
7 changes: 2 additions & 5 deletions packages/payments/src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
// Provider implementations are added in separate PRs
// e.g., export {relayProvider} from "./relay"

// Placeholder export - providers will be added here
export {}
export {relayProvider} from "./relay"
export type {RelayConfig} from "./relay"
Loading