Skip to main content
The headless core gives you Trustware’s routing and transaction logic without the prebuilt widget. You build your own UI and the SDK handles route construction, quotes, wallet calls, settlement, and transaction submission.

When to use this pattern

Choose this pattern when:
  • you want full control over the deposit UI
  • your design system requires a custom amount entry or confirmation flow
  • you want to embed deposit logic into an existing interface without a floating widget
  • you need a flow the widget does not provide, such as withdrawing from an embedded wallet
  • you need to deposit into a contract on the destination chain, such as depositing into a vault destination
If you want built-in wallet selection, token picker, amount entry, and confirmation screens, use the drop-in widget instead.

Setup

The headless core still requires TrustwareProvider for config context. Mount it once near the root of your app; you do not need to render TrustwareWidget.
Then import the core in any component or hook:

Wallet setup

You have two options for attaching a wallet to the headless core.

Bring your own wallet

Adapt an existing Wagmi wallet client into Trustware using useWallet:

Let Trustware detect wallets

If you do not manage wallet state, call autoDetect once at startup:

Core operations

1. Build a route

buildRoute constructs a route. Use this when you want to inspect route details before asking the user to confirm.
BuildRouteBody shape: (? indicates optional)
hooks.postHook executes a contract call on the destination chain once routed funds arrive, instead of paying the destination token to toAddress. It requires SDK 1.1.10 or later, is fully optional, and omitting hooks leaves route behavior unchanged. See PostHookRequest for the field reference and vault destinations for the end-to-end flow.

2. Inspect route details

The returned BuildRouteResult contains exchange rate information you can display to the user before they confirm.
BuildRouteResult shape:
The txReq type is not exported under its own name. Refer to it as BuildRouteResult["txReq"] rather than importing a named type.

3. Sign and send a route

sendRouteTransaction switches the wallet to the route’s chain if needed, grants any ERC-20 allowance the route requires, and then sends the route transaction. It returns the source transaction hash.
On EVM routes, allowance handling is automatic. When the route response includes route.execution.approvals, the SDK reads the current allowance, submits an approval for the exact amount when one is missing, waits for that approval to confirm, and only then requests the route signature. Plan your UI around two consequences:
  • The user can see more than one wallet prompt for a single deposit. An ERC-20 source asset usually means an approval prompt followed by the deposit prompt.
  • Each approval must confirm on chain before the route signature is requested, so the call can stay pending for a minute or more.
Every approval the route lists is for the exact amount required, never an unlimited allowance. An ordinary EOA route processes all of them. If you sign transactions yourself, process route.execution.approvals in the same order: read, approve, wait, then send.
A route carrying valid gas sponsorship is the exception. sendRouteTransaction skips the listed approvals in that case, because the sponsored path handles the allowance itself: it grants the bridge allowance inside the user operation, and it pulls the source tokens through a one-time unlimited Permit2 approval that it requests separately. Neither is a route.execution.approvals entry. That path is smart-account execution, so do not pass a sponsored route to sendRouteTransaction with a standard EIP-1193 wallet: it sends a plain transaction and the approval is skipped with nothing to replace it. Use sendRouteAsUserOperation() instead, as Paymasters describes.

4. Run the full flow

runTopUp handles route construction, wallet approval, transaction submission, and status polling in a single call. Use this when you want the SDK to orchestrate the full deposit path. Only fromAmount is required; every other field is optional and falls back to the corresponding value in your TrustwareConfigOptions.routes config when omitted.
Full signature:

Working with chains and tokens

The same chain and token discovery the widget uses internally is available to your own UI through three helpers on the Trustware facade. All three respect the active TrustwareProvider config and reuse its cache.

Trustware.useChains()

React hook that returns the supported chain list, split into popular and other groups, with loading and error state. Use it to drive your own chain selector.

Trustware.useTokens(chainId)

React hook that returns the token list for a given chain with built-in pagination and search. Pass null to skip fetching.

Trustware.validateAddressForChain(address, chain)

Synchronous helper that returns { isValid: boolean; error?: string } for the given destination address against the rules of the selected chain (EVM checksum, Solana base58, Bitcoin, Cosmos prefixes, etc.). Use it for live form validation.

Scanning wallet balances

Three helpers read balances. Trustware.getBalances(chainRef, address) covers a single chain and is the one to use when you already know which chain you care about, as in embedded wallet withdrawals. The other two scan every chain compatible with the address format.

Trustware.getBalancesByAddress(address, options)

Resolves once with one WalletAddressBalanceWrapper per chain. Pass stream: true to have the SDK consume the streamed scan and merge the chunks for you. That changes the transport, not the timing: the promise still settles after the last chain reports, so use getBalancesByAddressStream below if you want to render progressively.
Read partial before you tell a user their wallet is empty: a chain that timed out and a chain with no holdings both come back as no rows. See BalanceStreamSummary for when the callback fires and what each field means.

Trustware.getBalancesByAddressStream(address, options)

Async generator that yields each chain’s balances as it lands, so you can paint a wallet that fills in rather than a spinner that sits.
Each yielded chunk holds only the chains that just reported, and chains report in completion order rather than list order, so merge into a map keyed by chain_id instead of appending. Aborting signal does not end the scan. The abort surfaces as a stream failure, so the SDK falls back and re-runs the scan against the buffered endpoint, which the signal does not reach. Stop consuming the generator if you need the results dropped.
Streaming is on by default from SDK 1.1.12 through features.balanceStreaming. Where a backend has streaming disabled, the request is aborted, or the runtime has no readable response body (React Native, most notably), the generator emits balance_stream_fallback and yields the buffered result as a single chunk. Setting the flag to false does the same thing without the event. Either way this code path still works, so it does not need a variant. See BalanceStreamOptions and GET /balances.

Lifecycle callbacks

TrustwareConfigOptions exposes three optional callbacks you can pass alongside your routes config to react to SDK activity:
  • onEvent fires for every lifecycle event the SDK emits. It receives the full TrustwareEvent discriminated union, which you can narrow by type (transaction_started, transaction_success, wallet_connected, etc.).
  • onSuccess fires once when a deposit settles on the destination chain. It receives the resolved Transaction with destTxHash populated.
  • onError fires for any TrustwareError thrown during a route or transaction operation. Use it as a single place to log errors or push them into your UI state. RateLimitError extends native Error (not TrustwareError) and does not flow through this callback; handle it with the imperative try/catch pattern below.
onError runs in addition to any try/catch you wrap around individual core calls; see Error handling below for the imperative pattern. See lifecycle events for the full list of event types.

Error handling

Wrap core calls in try/catch. Import RateLimitError to handle rate-limiting specifically:
The SDK already waited out the limit on the server’s own schedule before throwing, so do not retry immediately. See Rate limiting and retry configuration for what the two retriesExhausted cases mean.

When to use the widget instead

If you want built-in wallet selection, token picker, amount input, and processing/success screens, the widget patterns require significantly less code. The headless core is best when your UI requirements cannot be met by the prebuilt widget.