Dynamic SDK Fireblocks Flow

Client-side implementation of the Dynamic Fireblocks Flow API — drives a payment/deposit/withdraw flow through its client-side steps: attachSource, getQuote, prepareSigning, broadcast, and getFlow.

The Flow session token (dft_...) returned by attachSource is persisted automatically to secure storage (flutter_secure_storage), keyed by flow ID, and reused for every subsequent call on that flow — including after the app is killed and restarted.

Deliberately out of scope:

  • Create Flow (POST /server/{environmentId}/flow/{mode}) requires an Authorization: Bearer dyn_... API token with flow.write scope. That's a backend-only credential — create the flow from your server, then hand the resulting flowId to the client.
  • Sign and Broadcast (step 5) isn't an API call. Sign the signingPayload returned by prepareSigning with the source wallet's own signer (see the dynamic_sdk/dynamic_sdk_web3dart/dynamic_sdk_solana packages), submit it to the chain yourself, then report the resulting hash via broadcast.

Full Example

1. Initialize the SDK

// main.dart
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:flutter/material.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  DynamicSDK.init(
    props: ClientProps(
      environmentId: 'YOUR_ENVIRONMENT_ID',
      appName: 'Your App Name',
      redirectUrl: 'yourapp://',
    ),
  );
  runApp(const MyApp());
}

2. Drive a flow created by your backend

Your backend creates the flow (POST /server/{environmentId}/flow/{mode}) and gives the client a flowId. From there:

import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:dynamic_sdk_fireblocks_flow/dynamic_sdk_fireblocks_flow.dart';

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

  // Step 2 -- attach the paying wallet. The session token this returns is
  // persisted to secure storage automatically; you never handle it directly.
  final attached = await flow.attachSource(
    flowId: flowId,
    request: AttachSourceRequest(
      sourceType: FlowSourceType.wallet,
      fromAddress: wallet.address,
      fromChainId: '1',
      fromChainName: ChainEnum.evm,
    ),
  );

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

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

  // Step 5 -- sign `prepared.signingPayload` with the source wallet's own
  // signer and broadcast it to the chain yourself (this package does not do
  // this step). For an EVM source, for example, map the fields of
  // `prepared.signingPayload.evmTransaction` onto a web3dart `Transaction`
  // and send it with `DynamicSDK.instance.web3dart.sendTransaction(...)`;
  // for SOL/SUI, sign+send `serializedTransaction` with `dynamic_sdk_solana`
  // (or the SUI signer); for BTC, sign the `psbt`.
  // `signAndBroadcast` is your own app code -- not part of this package.
  final txHash = await signAndBroadcast(prepared.signingPayload, wallet);

  // Step 6 -- report the resulting transaction hash.
  await flow.broadcast(
    flowId: flowId,
    request: BroadcastRequest(txHash: txHash),
  );

  // Step 7 -- poll until settled. This call needs no session token, so it
  // also works after an app restart to resume tracking an in-flight flow.
  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);
  }
}

3. Resuming after an app restart

Because the session token is persisted per flowId, tracking an in-flight flow after the app was killed just means calling getFlow() again — no re-attach needed unless the token has actually expired:

final status = await DynamicSDK.instance.fireblocksFlow.getFlow(flowId: flowId);

If a later call (getQuote, prepareSigning, broadcast) throws a FireblocksFlowException with sessionTokenInvalid == true, the stored token has already been removed — call attachSource() again to obtain a new one.