Skip to main content

Overview

In the Bluvo state machine, errors are states, not exceptions. When something goes wrong, the flow transitions to an error state (e.g., withdraw:error2FA), and the hook exposes it as a boolean (e.g., flow.requires2FA). Your UI reacts to these booleans the same way it reacts to any other state. Every error state has a defined recovery action, submit a code, retry, adjust parameters, or cancel. The hook also provides detection helpers like hasAmountError and hasAddressError that inspect error messages to help you show targeted UI.
For the full list of states, booleans, and transitions, see the State Machine Reference. This page focuses on what to do when things go wrong.

Error Quick Reference

Connection Errors

Hook BooleanStateSeverityRecovery Action
isOAuthErroroauth:error or oauth:fatalVariesRetry startWithdrawalFlow() (if not fatal) or cancel()
isOAuthFataloauth:fatalFatalcancel(), exchange rejected the connection
isOAuthWindowBeenClosedByTheUseroauth:window_closed_by_userUser actionRetry startWithdrawalFlow() or cancel()
isWalletConnectionInvalidoauth:fatal or qrcode:fatalFatalcancel() and start over
isQRCodeErrorqrcode:error or qrcode:fatalVariesrefreshQRCode() (if not fatal) or cancel()
isQRCodeTimeoutqrcode:timeoutRecoverablerefreshQRCode()

Wallet Errors

Hook BooleanStateSeverityRecovery Action
isWalletErrorwallet:errorErrorcancel() and retry flow
hasWalletNotFoundErrorwallet:error + “not found”FatalWallet deleted, create new wallet ID
hasInvalidCredentialsErrorwallet:error + “invalid credential”FatalTokens revoked, user must re-authenticate

Quote Errors

Hook BooleanStateSeverityRecovery Action
isQuoteErrorquote:errorErrorRetry requestQuote() with corrected params
isQuoteExpiredquote:expiredExpiredCall requestQuote() again
hasAmountErrorquote:error or withdraw:fatalValidationShow min/max from quote.additionalInfo, let user adjust
hasAddressErrorquote:error or withdraw:fatalValidationPrompt user to check destination address
hasNetworkErrorquote:error or withdraw:fatalValidationShow supported networks, let user re-select

Withdrawal Errors

Hook BooleanStateSeverityRecovery Action
requires2FAwithdraw:error2FARequires inputsubmit2FA(code)
requires2FAMultiStepwithdraw:error2FAMultiStepRequires inputsubmit2FAMultiStep(stepType, code) / pollFaceVerification()
requiresSMSwithdraw:errorSMSRequires inputsubmit2FA(code) (SMS code)
requiresKYCwithdraw:errorKYCBlockingShow “Complete KYC on exchange” message
hasInsufficientBalancewithdraw:errorBalanceBlockingShow balance, let user adjust amount
canRetrywithdraw:retryingAuto-retryShow spinner, retry is automatic
isWithdrawBlockedwithdraw:blockedFatalShow reason from flow.error?.message
hasFatalErrorwithdraw:fatalFatalCheck requiresValid2FAMethod; show flow.error?.message
requiresValid2FAMethodwithdraw:fatal + valid methodsFatalShow supported 2FA methods from flow.valid2FAMethods

Exchange-Specific 2FA

2FA behavior is completely different between exchanges. Your UI must account for this, do not assume all exchanges use the same 2FA pattern. The hook booleans tell you exactly which path to render.
Auth Method2FA TypeHook Booleans That FireNotes
OAuthNoneNeither requires2FA nor requires2FAMultiStepNo 2FA for withdrawals. Hide all 2FA UI.
OAuthSingle-steprequires2FA onlyOne TOTP code → submit2FA(code). requires2FAMultiStep never fires.
QR CodeMulti-step MFArequires2FAMultiStep onlyStep-by-step verification. requires2FA never fires. See deep dive below.

Handling All Three in Your UI

// This single block handles all exchanges correctly
if (flow.requires2FA) {
  // single 2FA input
  return <SingleCodeInput onSubmit={(code) => flow.submit2FA(code)} />;
}

if (flow.requires2FAMultiStep) {
  // multi-step MFA
  return <MultiStepMFA flow={flow} />;
}

if (flow.isWithdrawProcessing) {
  // no 2FA, just processing
  return <Spinner label="Processing withdrawal..." />;
}

Multi-Step 2FA Deep Dive

When requires2FAMultiStep is true, the exchange requires multiple verification steps before confirming the withdrawal. This is currently used by Binance.

Step Types

Step TypeDescriptionVerification Method
GOOGLEGoogle Authenticator TOTPsubmit2FAMultiStep('GOOGLE', code)
EMAILEmail verification codesubmit2FAMultiStep('EMAIL', code)
SMSSMS verification codesubmit2FAMultiStep('SMS', code)
FACEBiometric face verification via QRpollFaceVerification(), user scans QR on mobile
ROAMING_FIDOPasskey / security keypollRoamingFidoVerification(), user verifies on device

Relation Types

The multiStep2FARelation field determines how steps combine:
  • AND, All required steps must be verified. Show all step inputs simultaneously.
  • OR, Any one required step is sufficient. Show options and let user pick.

Verification Source of Truth

Use mfa.verified (available as flow.mfaVerified) as the primary source of truth for whether a step is verified, not step.status. The mfa.verified object is updated by the server and reflects the actual verification state. The step status field may lag behind.
The hook provides convenience booleans that check mfa.verified first, falling back to step.status:
flow.isGoogleStepVerified  // mfa.verified.GOOGLE === true || step.status === 'success'
flow.isEmailStepVerified   // mfa.verified.EMAIL === true  || step.status === 'success'
flow.isFaceStepVerified    // mfa.verified.FACE === true   || step.status === 'success'
flow.isSmsStepVerified     // mfa.verified.SMS === true    || step.status === 'success'

Complete Flow

  1. Withdrawal execution triggers requires2FAMultiStep
  2. Check flow.multiStep2FASteps for required step types
  3. For each step, render the appropriate input:
    • GOOGLE / EMAIL / SMS, code input → submit2FAMultiStep(type, code)
    • FACE, show QR code from flow.faceQrCodeUrl → user scans → pollFaceVerification()
    • ROAMING_FIDO, prompt user → pollRoamingFidoVerification()
  4. After each submission, the server returns updated step statuses
  5. When flow.allMultiStep2FAStepsVerified becomes true, isReadyToConfirm fires
  6. Call flow.confirmWithdrawal() to finalize

FACE Verification

The FACE step requires the user to scan a QR code with their exchange mobile app and complete biometric verification:
if (flow.hasFaceStep && !flow.isFaceStepVerified) {
  return (
    <div>
      <p>Scan this QR code with your Binance app to verify your identity</p>
      <QRCode value={flow.faceQrCodeUrl} />
      {flow.faceQrCodeExpiresAt && (
        <p>Expires: {new Date(flow.faceQrCodeExpiresAt).toLocaleTimeString()}</p>
      )}
      <button onClick={() => flow.pollFaceVerification()}>
        I've completed verification
      </button>
    </div>
  );
}

Code Example

function MultiStepMFA({ flow }: { flow: UseBluvoFlowHook }) {
  const [googleCode, setGoogleCode] = React.useState("");
  const [emailCode, setEmailCode] = React.useState("");
  const [smsCode, setSmsCode] = React.useState("");

  // All steps verified, show confirm button
  if (flow.isReadyToConfirm) {
    return (
      <div>
        <p>All verification steps complete.</p>
        <button onClick={() => flow.confirmWithdrawal()}>
          Confirm Withdrawal
        </button>
      </div>
    );
  }

  const relation = flow.multiStep2FARelation; // 'AND' or 'OR'

  return (
    <div>
      <h3>Verify your identity {relation === 'AND' ? '(all required)' : '(any one)'}</h3>

      {flow.hasGoogleStep && !flow.isGoogleStepVerified && (
        <div>
          <label>Google Authenticator</label>
          <input value={googleCode} onChange={(e) => setGoogleCode(e.target.value)} />
          <button onClick={() => flow.submit2FAMultiStep('GOOGLE', googleCode)}>
            Verify
          </button>
        </div>
      )}
      {flow.isGoogleStepVerified && <p>Google Authenticator: Verified</p>}

      {flow.hasEmailStep && !flow.isEmailStepVerified && (
        <div>
          <label>Email Code</label>
          <input value={emailCode} onChange={(e) => setEmailCode(e.target.value)} />
          <button onClick={() => flow.submit2FAMultiStep('EMAIL', emailCode)}>
            Verify
          </button>
        </div>
      )}
      {flow.isEmailStepVerified && <p>Email: Verified</p>}

      {flow.hasSmsStep && !flow.isSmsStepVerified && (
        <div>
          <label>SMS Code</label>
          <input value={smsCode} onChange={(e) => setSmsCode(e.target.value)} />
          <button onClick={() => flow.submit2FAMultiStep('SMS', smsCode)}>
            Verify
          </button>
        </div>
      )}
      {flow.isSmsStepVerified && <p>SMS: Verified</p>}

      {flow.hasFaceStep && !flow.isFaceStepVerified && (
        <div>
          <p>Scan with your exchange app:</p>
          <img src={flow.faceQrCodeUrl} alt="Face verification QR" />
          <button onClick={() => flow.pollFaceVerification()}>
            I've completed face verification
          </button>
        </div>
      )}
      {flow.isFaceStepVerified && <p>Face Verification: Verified</p>}
    </div>
  );
}

Recovery Actions by Error State

The exchange (typically Coinbase) requires a TOTP code.Recovery: Call flow.submit2FA(code) with the user’s authenticator code.Track: flow.invalid2FAAttempts increments on each invalid submission. Consider showing a warning after 2-3 attempts.
if (flow.requires2FA) {
  return (
    <div>
      <input placeholder="Enter 2FA code" onChange={(e) => setCode(e.target.value)} />
      {flow.invalid2FAAttempts > 0 && (
        <p>Invalid code. Attempts: {flow.invalid2FAAttempts}</p>
      )}
      <button onClick={() => flow.submit2FA(code)}>Submit</button>
    </div>
  );
}
The exchange requires multiple verification steps.Recovery: Use flow.submit2FAMultiStep(stepType, code) for code-based steps and flow.pollFaceVerification() for biometric steps. Once flow.allMultiStep2FAStepsVerified is true, call flow.confirmWithdrawal().See the Multi-Step 2FA Deep Dive above for the complete implementation.
The exchange sent an SMS code to the user’s registered phone.Recovery: Call flow.submit2FA(code) with the SMS code the user received.
if (flow.requiresSMS) {
  return (
    <div>
      <p>Enter the SMS code sent to your phone</p>
      <input onChange={(e) => setCode(e.target.value)} />
      <button onClick={() => flow.submit2FA(code)}>Verify</button>
    </div>
  );
}
The exchange requires the user to complete identity verification before withdrawals are allowed.Recovery: This cannot be resolved via the SDK. Direct the user to complete KYC on the exchange’s website or app, then retry the withdrawal later.
if (flow.requiresKYC) {
  return (
    <div>
      <p>Your exchange account requires identity verification before withdrawals.</p>
      <p>Please complete KYC on the exchange, then try again.</p>
      <button onClick={() => flow.cancel()}>Close</button>
    </div>
  );
}
The withdrawal amount exceeds the available balance.Recovery: Show the user their current balance (from flow.walletBalances), let them adjust the amount, and request a new quote.
if (flow.hasInsufficientBalance) {
  return (
    <div>
      <p>Insufficient balance for this withdrawal.</p>
      <p>Available: {flow.walletBalances.find(b => b.asset === selectedAsset)?.balance}</p>
      <button onClick={() => flow.requestQuote({ asset, amount: newAmount, destinationAddress })}>
        Try Different Amount
      </button>
    </div>
  );
}
The exchange has blocked this withdrawal. This is non-recoverable via the SDK.Recovery: Display flow.error?.message which contains the reason from the exchange. The user may need to resolve account-level restrictions.
if (flow.isWithdrawBlocked) {
  return (
    <div>
      <p>Withdrawal blocked: {flow.error?.message}</p>
      <button onClick={() => flow.cancel()}>Close</button>
    </div>
  );
}
An unrecoverable error occurred. Check requiresValid2FAMethod for a special case where the user’s 2FA method isn’t supported.Recovery:
  • If flow.requiresValid2FAMethod is true: show flow.valid2FAMethods and instruct the user to enable a supported method on the exchange.
  • Otherwise: display flow.error?.message and offer flow.cancel().
if (flow.hasFatalError) {
  if (flow.requiresValid2FAMethod) {
    return (
      <div>
        <p>{flow.error?.message}</p>
        <p>Supported methods: {flow.valid2FAMethods?.join(', ')}</p>
      </div>
    );
  }
  return (
    <div>
      <p>Withdrawal failed: {flow.error?.message}</p>
      <button onClick={() => flow.cancel()}>Start Over</button>
    </div>
  );
}
The SDK is automatically retrying the withdrawal after a transient failure.Recovery: No action needed. Show a spinner. Track progress with flow.retryAttempts / flow.maxRetryAttempts.
if (flow.canRetry) {
  return (
    <div>
      <Spinner />
      <p>Retrying... ({flow.retryAttempts} / {flow.maxRetryAttempts})</p>
    </div>
  );
}
The quote’s TTL has elapsed and it can no longer be used for withdrawal.Recovery: Call flow.requestQuote() with the same parameters to get a fresh quote.
if (flow.isQuoteExpired) {
  return (
    <div>
      <p>Quote expired. Fetching a new one...</p>
      <button onClick={() => flow.requestQuote({ asset, amount, destinationAddress })}>
        Get New Quote
      </button>
    </div>
  );
}
Enable autoRefreshQuotation: true in hook options to automatically refresh quotes before they expire.
A transient failure occurred during the OAuth flow (network timeout, temporary exchange issue).Recovery: Call startWithdrawalFlow() again with the same exchange and wallet ID.
if (flow.isOAuthError && !flow.isOAuthFatal) {
  return (
    <div>
      <p>Connection failed: {flow.error?.message}</p>
      <button onClick={() => flow.startWithdrawalFlow({ exchange, walletId })}>
        Try Again
      </button>
    </div>
  );
}
The exchange permanently rejected the connection. This cannot be retried.Recovery: Call flow.cancel() and let the user start over, potentially with a different exchange.
if (flow.isWalletConnectionInvalid) {
  return (
    <div>
      <p>Connection rejected: {flow.error?.message}</p>
      <button onClick={() => flow.cancel()}>Start Over</button>
    </div>
  );
}

Next Steps

State Machine Reference

Full list of 35 states, transitions, hook booleans, and context data

OAuth2 Integration

Step-by-step implementation guide with React and Next.js

Code Samples

Full working examples for Next.js, React, and more

Encryption & Security

How Bluvo encrypts and isolates exchange credentials