> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Fireblocks Flow Flutter guide

> End-to-end guide for completing a Fireblocks Flow payment with the Flutter SDK.

<Note>
  This is an enterprise-only feature. Please [contact us](https://www.dynamic.xyz/book-a-call) to enable.
</Note>

Fireblocks Flow lets your users pay from any supported wallet, exchange, or deposit address and have the funds settle in the token and chain you configure. This guide covers the full client-side flow with the Dynamic Flutter SDK.

## Prerequisites

Before starting a flow on the client, create it from your backend. The flow creation endpoint is server-side only, authenticated by an API token with `flow.write` scope — this is where the amount, currency, settlement, and destination are fixed.

```bash theme={"system"}
# Create a flow from your backend (returns a flowId)
curl --request POST \
  --url https://app.dynamicauth.com/api/v0/server/<YOUR_ENVIRONMENT_ID>/flow/payment \
  --header 'Authorization: Bearer <YOUR_API_TOKEN>' \
  --header 'Content-Type: application/json' \
  --data '{
    "amount": "25.00",
    "currency": "USD",
    "settlementConfig": {
      "strategy": "cheapest",
      "settlements": [
        {
          "chainName": "EVM",
          "tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
          "chainId": "1",
          "symbol": "USDC",
          "tokenDecimals": 18
        }
      ]
    },
    "destinationConfig": {
      "destinations": [
        {
          "chainName": "EVM",
          "type": "address",
          "identifier": "0xYourSettlementAddress"
        }
      ]
    },
    "memo": {
      "description": "Payment for order #1234"
    }
  }'
```

The response includes `flow.id` — pass this as the `flowId` to your Flutter app when calling the SDK methods below.

<Note>
  **Optional — collect your own fee.** Add a `feeConfig` to the create body to take a percentage of each swap to your own EVM wallet(s). Recipient addresses must be EVM (`0x…`). See [Fee collection and claiming](/docs/overview/fireblocks-flow-api#fee-collection-and-claiming) for the config shape, how to check balances, and how to claim accrued fees.
</Note>

## Installation

The `dynamic_sdk_fireblocks_flow` package is available from Dynamic Flutter SDK `1.14.0` onward. Add it to your `pubspec.yaml`:

```yaml theme={"system"}
dependencies:
  dynamic_sdk: ^1.14.0
  dynamic_sdk_fireblocks_flow: ^1.14.0
```

For EVM sources you also need `dynamic_sdk_web3dart`:

```yaml theme={"system"}
dependencies:
  dynamic_sdk_web3dart: ^1.14.0
```

For Solana sources you need `dynamic_sdk_solana`:

```yaml theme={"system"}
dependencies:
  dynamic_sdk_solana: ^1.14.0
```

Then run:

```bash theme={"system"}
flutter pub get
```

## Supported chains

The Flutter SDK supports the following source chains:

* BTC
* EVM
* SOL
* SUI
* TRON

<Note>
  TRON cross-chain swaps currently route through intent-based bridges and may incur higher gas costs relative to the transfer amount.
</Note>

## Overview

The client-side flow follows these steps:

1. **Attach** the source wallet or deposit address with `attachSource`.
2. **Quote** the conversion with `getQuote`.
3. **Prepare** the unsigned source-chain payload with `prepareSigning`.
4. **Sign and broadcast** the payload with the source wallet's own signer.
5. **Record** the resulting transaction hash with `broadcast`.
6. **Poll** for completion with `getFlow`.

`attachSource` returns a one-time session token (`dft_…`). The SDK persists it to secure storage keyed by `flowId` and adds it automatically to every subsequent call. You never need to handle it directly.

## Resume a flow from its current state

A `flowId` represents one execution attempt. On page load, retry, or reconnect, call `getFlow` before calling a mutation. Do not unconditionally call `attachSource` again.

| `executionState`                    | Valid next action                                                                                            |
| :---------------------------------- | :----------------------------------------------------------------------------------------------------------- |
| `initiated`                         | Attach a source or cancel.                                                                                   |
| `source_attached`                   | Attach a different source, get a quote, record an exchange transfer, or cancel.                              |
| `quoted`                            | Re-quote, attach a different source, prepare signing, or cancel.                                             |
| `signing`                           | Continue the in-progress signing operation. If it cannot resume, re-quote, attach another source, or cancel. |
| `broadcasted`                       | Poll or process webhooks. Do not attach another source or cancel.                                            |
| `source_confirmed`                  | Poll or process webhooks until settlement completes or fails. Do not call execution mutations.               |
| `cancelled`, `expired`, or `failed` | Create a new flow for another attempt. These execution states do not allow any further transitions.          |

<Warning>
  `source_confirmed` means source execution is finished, not necessarily that settlement is finished. Keep tracking `settlementState` until it reaches `completed` or `failed`.
</Warning>

## Full example

```dart theme={"system"}
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:dynamic_sdk_fireblocks_flow/dynamic_sdk_fireblocks_flow.dart';
import 'package:flutter/material.dart';

// Initialized earlier in main()
// DynamicSDK.init(
//   props: ClientProps(
//     environmentId: 'YOUR_ENVIRONMENT_ID',
//     appName: 'Your App',
//     appOrigin: 'https://your-app.com',
//   ),
// );

Future<void> runFlow({
  required String flowId,
  required BaseWallet wallet,
}) async {
  final flow = DynamicSDK.instance.fireblocksFlow;

  // Step 1: attach the source wallet
  final attached = await flow.attachSource(
    flowId: flowId,
    request: AttachSourceRequest(
      sourceType: FlowSourceType.wallet,
      fromAddress: wallet.address,
      fromChainId: '1',
      fromChainName: ChainEnum.evm,
    ),
  );

  // Step 2: get a quote. Quotes expire 60 seconds after creation.
  final quoted = await flow.getQuote(
    flowId: flowId,
    request: const GetQuoteRequest(
      fromTokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
    ),
  );

  print('Quote: ${quoted.quote}');

  // Step 3: prepare the unsigned source-chain payload
  final prepared = await flow.prepareSigning(flowId: flowId);

  // Step 4: sign and broadcast with the source wallet's signer
  // (see chain-specific examples below)
  final txHash = await signAndBroadcast(prepared.signingPayload, wallet);

  // Step 5: record the broadcast
  await flow.broadcast(
    flowId: flowId,
    request: BroadcastRequest(txHash: txHash),
  );

  // Step 6: poll for settlement
  var status = await flow.getFlow(flowId: flowId);
  while (
    status.settlementState != FlowSettlementState.completed &&
    status.settlementState != FlowSettlementState.failed
  ) {
    await Future.delayed(const Duration(seconds: 3));
    status = await flow.getFlow(flowId: flowId);
  }

  print('Final settlement state: ${status.settlementState}');
}
```

`flowId` and `wallet` come from your app state. `wallet` is a `BaseWallet` returned by the Dynamic SDK.

## Chain-specific signing

Step 4 (sign and broadcast) is not an API call. You sign `prepared.signingPayload` with the source wallet's own signer and submit the result to the chain. The `SigningPayload` exposes one chain-specific field:

* `EVM` → `evmTransaction` (and optional `evmApproval`)
* `SOL` / `SUI` → `serializedTransaction` (base64)
* `BTC` → `psbt` (base64-encoded unsigned PSBT)
* `TRON` → `tronTransaction`

### EVM source

```dart theme={"system"}
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:dynamic_sdk_web3dart/dynamic_sdk_web3dart.dart';
import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart';

Future<String> signAndBroadcast(
  SigningPayload payload,
  BaseWallet wallet,
) async {
  final evmTx = payload.evmTransaction;
  if (evmTx == null) {
    throw Exception('No EVM transaction in signing payload');
  }

  final transaction = Transaction(
    from: EthereumAddress.fromHex(wallet.address),
    to: EthereumAddress.fromHex(evmTx.to),
    data: evmTx.data.isEmpty ? null : hexToBytes(evmTx.data),
    value: EtherAmount.inWei(BigInt.parse(evmTx.value)),
    maxGas: evmTx.gasLimit != null ? int.tryParse(evmTx.gasLimit!) : null,
  );

  final txHash = await DynamicSDK.instance.web3dart.sendTransaction(
    transaction: transaction,
    wallet: wallet,
  );

  return txHash;
}
```

If `evmApproval` is present, submit the ERC-20 approval transaction first and wait for it to be mined before signing the main `evmTransaction`.

### Solana source

```dart theme={"system"}
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:dynamic_sdk_solana/dynamic_sdk_solana.dart';

Future<String> signAndBroadcast(
  SigningPayload payload,
  BaseWallet wallet,
) async {
  final serialized = payload.serializedTransaction;
  if (serialized == null) {
    throw Exception('No serialized transaction in signing payload');
  }

  final signer = DynamicSDK.instance.solana.createSigner(wallet: wallet);
  final txHash = await signer.signAndSendEncodedTransaction(
    base64Transaction: serialized,
  );

  return txHash;
}
```

### Bitcoin source

```dart theme={"system"}
import 'dart:convert';

import 'package:dynamic_sdk/dynamic_sdk.dart';

Future<String> signAndBroadcast(
  SigningPayload payload,
  BaseWallet wallet,
) async {
  final psbtBase64 = payload.psbt;
  if (psbtBase64 == null) {
    throw Exception('No PSBT in signing payload');
  }

  final psbtBytes = base64.decode(psbtBase64);

  // Sign the PSBT with your Bitcoin signer library, then finalize and
  // broadcast the resulting transaction to the Bitcoin network.
  // This step is not provided by the Dynamic SDK.
  final txHash = await signPsbtAndBroadcast(psbtBytes, wallet);

  return txHash;
}
```

### TRON source

```dart theme={"system"}
import 'package:dynamic_sdk/dynamic_sdk.dart';

Future<String> signAndBroadcast(
  SigningPayload payload,
  BaseWallet wallet,
) async {
  final tronTx = payload.tronTransaction;
  if (tronTx == null) {
    throw Exception('No TRON transaction in signing payload');
  }

  // Sign the raw transaction with your TRON signer and broadcast it to the
  // TRON network. This step is not provided by the Dynamic SDK.
  final txHash = await signTronTransactionAndBroadcast(tronTx, wallet);

  return txHash;
}
```

## Deposit address source

With a deposit address flow the user sends funds directly to a generated address — no wallet connection or on-chain signing required from your app. Works for BTC, SOL, EVM, and TRON.

<Steps>
  <Step title="Attach a deposit address source">
    ```dart theme={"system"}
    await flow.attachSource(
      flowId: flowId,
      request: const AttachSourceRequest(
        sourceType: FlowSourceType.depositAddress,
        fromChainId: '1',
        fromChainName: ChainEnum.evm,
        refundAddress: '0xRefundAddress',
      ),
    );
    ```
  </Step>

  <Step title="Get a quote — response includes the deposit address">
    ```dart theme={"system"}
    final quoted = await flow.getQuote(
      flowId: flowId,
      request: const GetQuoteRequest(
        fromTokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
      ),
    );

    final depositAddress = quoted.quote.depositAddress;
    final rawAmount = quoted.quote.fromAmount;

    // Show the address and amount to the user as a QR code or copy string.
    ```
  </Step>

  <Step title="Poll until the transfer is detected">
    The backend detects the inbound transfer automatically. Poll `getFlow` until `executionState` leaves `quoted`.

    ```dart theme={"system"}
    var status = await flow.getFlow(flowId: flowId);
    while (status.executionState == FlowExecutionState.quoted) {
      await Future.delayed(const Duration(seconds: 3));
      status = await flow.getFlow(flowId: flowId);
    }

    print('Final execution state: ${status.executionState}');
    ```
  </Step>
</Steps>

<Note>
  There is no signing step. Once the user sends funds to `depositAddress`, the transfer is detected automatically and the flow advances to `source_confirmed`.
</Note>

## Resuming after an app restart

The session token returned by `attachSource` is persisted to secure storage automatically. To resume tracking an in-flight flow after the app was killed, call `getFlow` — it needs no session token:

```dart theme={"system"}
final status = await DynamicSDK.instance.fireblocksFlow.getFlow(
  flowId: flowId,
);
```

If a later call throws `FireblocksFlowException` with `sessionTokenInvalid == true`, the stored token has expired or was rejected and has already been removed. Call `attachSource` again to obtain a new token.

## Error handling

All Fireblocks Flow methods throw `FireblocksFlowException` on failure. The exception exposes `message`, `statusCode`, and `sessionTokenInvalid`. Use `sessionTokenInvalid` to decide whether to re-run `attachSource`.

```dart theme={"system"}
try {
  final quoted = await flow.getQuote(
    flowId: flowId,
    request: const GetQuoteRequest(fromTokenAddress: '0x...'),
  );
} on FireblocksFlowException catch (e) {
  if (e.sessionTokenInvalid) {
    // Token removed from storage; re-attach
    await flow.attachSource(
      flowId: flowId,
      request: AttachSourceRequest(
        sourceType: FlowSourceType.wallet,
        fromAddress: wallet.address,
        fromChainId: '1',
        fromChainName: ChainEnum.evm,
      ),
    );
  } else {
    print('Flow error: ${e.message}');
  }
}
```

### Common errors

| Status | Error                                                                                    | Cause                                                                | Fix                                                                     |
| :----- | :--------------------------------------------------------------------------------------- | :------------------------------------------------------------------- | :---------------------------------------------------------------------- |
| `400`  | `fromAddress, fromChainId, and fromChainName are required for wallet sources`            | Missing source fields when `sourceType` is `wallet`.                 | Pass `fromAddress`, `fromChainId`, and `fromChainName` from the wallet. |
| `400`  | `fromChainId, fromChainName, and refundAddress are required for deposit_address sources` | Missing chain or refund fields for deposit-address sources.          | Pass `fromChainId`, `fromChainName`, and `refundAddress`.               |
| `400`  | `fromAddress is not accepted for deposit_address sources; use refundAddress instead`     | `fromAddress` was passed with `depositAddress` source.               | Remove `fromAddress` and use `refundAddress`.                           |
| `403`  | `Flow is blocked by sanctions`                                                           | The source address failed sanctions screening (wallet sources only). | Use a different source wallet.                                          |
| `404`  | `Flow not found`                                                                         | The `flowId` does not exist or the environment ID is wrong.          | Verify the `flowId` and environment.                                    |
| `409`  | State transition error                                                                   | The endpoint was called from an invalid flow state.                  | Call `getFlow` and resume from the current `executionState`.            |
| `422`  | `Quote has expired; request a new quote before signing`                                  | The quote is older than 60 seconds.                                  | Call `getQuote` again, then retry `prepareSigning`.                     |
| `422`  | `Insufficient balance ...`                                                               | The wallet does not have enough tokens or gas.                       | Fund the wallet or use a smaller amount.                                |

## Polling for status

After `broadcast`, poll `getFlow` to track execution and settlement. Stop when `settlementState` reaches `completed` or `failed`, or when `executionState` reaches a terminal state (`cancelled`, `expired`, `failed`).

```dart theme={"system"}
Future<FlowStatus> pollUntilDone(FireblocksFlow flow, String flowId) async {
  const terminalExecution = [
    FlowExecutionState.cancelled,
    FlowExecutionState.expired,
    FlowExecutionState.failed,
  ];
  const terminalSettlement = [
    FlowSettlementState.completed,
    FlowSettlementState.failed,
  ];

  Future<FlowStatus> poll() async {
    final status = await flow.getFlow(flowId: flowId);

    if (terminalExecution.contains(status.executionState) ||
        terminalSettlement.contains(status.settlementState)) {
      return status;
    }

    await Future.delayed(const Duration(seconds: 3));
    return poll();
  }

  return poll();
}
```

## Related

* [`/overview/fireblocks-flow`](/docs/overview/fireblocks-flow) — Product overview and concepts
* [`/overview/fireblocks-flow-api`](/docs/overview/fireblocks-flow-api) — Full HTTP API reference
* [`/flutter/sdk-reference/overview`](/docs/flutter/sdk-reference/overview) — Flutter SDK reference
* [`/flutter/web3dart`](/docs/flutter/web3dart) — EVM transactions with `dynamic_sdk_web3dart`
* [`/flutter/solana`](/docs/flutter/solana) — Solana transactions with `dynamic_sdk_solana`
