Configuration
Every field of the widget's config object, what it changes, and what the widget refuses at mount.
The widget takes one object, config. integrator is the only required field. A configuration
mistake, such as an empty endpoint or a network without a sponsor, stops the widget when it
mounts, with a message that starts with Mavna widget configuration:, rather than later in front of
your users.
Network and endpoints
integrator (required). A name for your integration.
network. "testnet" or "mainnet": the network the widget starts on. The default is
"testnet".
rpcUrl. The Stellar RPC endpoint for network. The widget reads EVM chains through the
connected wallet, never through this endpoint. On the test network the default is the public
testnet endpoint; on the main network it is the first entry of PUBLIC_MAINNET_RPC_URLS in
@mavna/stellar, https://soroban-rpc.mainnet.stellar.gateway.fm, a public node run by a third
party. An empty string is a configuration error.
sponsorUrl. The sponsor service you run for network. On the test network the
default is http://localhost:8787. On the main network there is no default and the widget refuses
to mount without one. An empty string is a configuration error.
networks. The networks the network toggle offers, network among them. The default is
[network], and the toggle then shows the other network as unavailable. Switching starts the form
over on the other network, and the toggle does nothing while a transfer runs.
endpoints. Endpoints per network:
{ testnet?: { rpcUrl?, sponsorUrl?, arrivalContract? }, mainnet?: { ... } }. A value here wins
over the top-level field for the same network. Every network in networks other than network
needs its own sponsorUrl here, because a sponsor serves one network; without it the widget
refuses to mount.
arrivalContract. The Mavna Arrival Contract on network.
With one, the LayerZero rail also offers routes that convert on arrival. The test network defaults
to the deployed contract, recorded as ARRIVAL_CONTRACTS in @mavna/adapter-layerzero. The main
network has no default.
What the widget offers
rails. The rails to race and offer, by display name: "Circle", "Axelar", "LayerZero",
"NEAR Intents". The default is all four.
chains. { allow?, deny? }: lists of chain slugs that narrow the chains the picker offers.
They only remove chains; they cannot add one. The slugs are listed in
Rails and networks.
disabledSources. Rails you have taken out of rotation, as comma-separated id:reason pairs
with adapter ids, for example "cctp:Circle is paused". A disabled rail appears as declined with
your reason. See Turn off a rail.
routePriority. "best-received" (the default) or "fastest": how routes are ranked when the
widget opens.
fee. An integrator fee: { bps, recipient, recipientName }, with bps an integer from 0 to
1000. The fee is taken only on routes that convert on arrival, where the Arrival Contract pays it
out and the review lists it as Integrator fee. When you set one, the widget logs a warning
once in the browser console that says so.
Starting values
fromChain. The chain the form starts on, by slug. A chain the picker does not offer for the
asset being sent is a configuration error.
fromAsset. "USDC" (the default) or "USDT0": what the form starts out sending. USDT0 is
offered only while the LayerZero rail is on.
wanted. What the recipient ends up holding: "USDC" or "XLM" when sending USDC, and
"USDT0", "USDC" or "XLM" when sending USDT0. "USDT0" requires fromAsset: "USDT0".
fromAmount. The amount the form starts with.
toAddress. The recipient, prefilled. With requiredUI: ["destination"] your user cannot
change it.
What the widget shows
hiddenUI. Controls to remove: "activity", "settings", "networkToggle",
"railToggles", "poweredBy".
requiredUI. Controls to lock: "destination" fixes the recipient to toAddress.
menuLinks. Links for the widget's header menu (the ... button):
{ label, href, external? }. With external: true the link opens in a new tab. An empty or absent
list hides the menu.
variant, appearance and theme are covered in Layouts and
Theming.
Integration
keyPrefix. A prefix for the transfer history the widget keeps in the browser, so two
integrations on one website keep separate lists. The history is also kept separately per network.
walletConfig. Use your app's own wallet connection instead of the widget's:
onConnect: called when your user presses the widget's connect button with no wallet connected, in place of the widget's own wallet menu.evm: an EVM connection your app already holds.stellar: your own Stellar signer.
onEvent. A function that receives the widget's events.
The type
The declarations below are copied from packages/widget/src/config.ts when this site is built.
/**
* The widget's public configuration, in the shape an integrator writes.
*
* Modelled on the LI.FI widget: one object, drop it on `<MavnaWidget>`, and the
* widget quotes, routes and (by default) manages wallets itself. Every field maps
* to a real capability of the engine or the flow underneath, so nothing here
* promises something the code cannot honour.
*
* Kept JSON-serialisable except the event and wallet hooks, so a future iframe
* build can pass the rest of the config over postMessage without a rewrite.
*/
export type Variant = "compact" | "wide" | "drawer";
export type Appearance = "light" | "dark" | "system";
/** UI the integrator can remove. Each maps to a real control in the flow. */
export type HiddenUIElement =
| "activity"
| "settings"
| "networkToggle"
| "railToggles"
| "poweredBy";
/** UI the integrator can lock. `destination` fixes the recipient to `toAddress`. */
export type RequiredUIElement = "destination";
/**
* What the widget reports back to the host. Every event is backed by a real signal
* in the flow: a completed quote, a route the user picked, a wallet that connected,
* a transfer moving through its steps, or one that finished or failed.
*/
export type MavnaWidgetEvent =
| { readonly type: "routesAvailable"; readonly count: number; readonly declined: number }
| { readonly type: "routeSelected"; readonly source: string; readonly receives: number }
| {
readonly type: "walletConnected";
readonly ecosystem: "evm" | "stellar";
readonly address: string;
}
| { readonly type: "transferStarted" }
| { readonly type: "transferStepUpdated"; readonly id: string; readonly phase: string }
| { readonly type: "transferCompleted" }
| { readonly type: "transferFailed"; readonly reason: string }
/**
* The transfer is on its way and the widget stopped watching it: it outlived
* the widget's patience, or the widget went away. Not a failure; the money is
* in flight and can be checked on or finished from Activity.
*/
| { readonly type: "transferStillMoving"; readonly reason: string }
/**
* A conversion on arrival could not meet the minimum, so what was sent
* arrived as it came: not a success, but the money is in the account.
*/
| {
readonly type: "transferDeliveredAsCame";
readonly reason: string;
readonly asset: string;
readonly baseUnits: string;
}
/**
* The rail says the transfer executed on Stellar but can no longer prove what
* arrived. It has ended: not a failure, and not still moving. The Stellar
* transaction is given for checking it.
*/
| {
readonly type: "transferDeliveredUnverified";
readonly reason: string;
/** The Stellar transaction the rail reported. */
readonly stellarTxHash: string;
readonly explorerUrl: string;
};
/**
* Opt out of the widget's own wallet management. Provide an already-connected EVM
* `Connection` and/or a Stellar signer, and the widget uses them instead of
* discovering wallets itself; `onConnect` replaces the built-in connect UI with
* the host's own.
*/
export interface WalletConfig {
readonly onConnect?: () => void;
readonly evm?: Connection;
readonly stellar?: StellarWalletApi;
}
/** Colour and type overrides, applied as CSS custom properties on the root. */
export interface WidgetTheme {
readonly tokens?: Readonly<Record<string, string>>;
readonly font?: string;
readonly fontMono?: string;
readonly radius?: string;
readonly radiusLg?: string;
}
export interface MavnaWidgetConfig {
/** Required. Identifies the integrator, and attributes an integrator fee. */
readonly integrator: string;
/**
* The network the widget starts on. Default testnet. Mainnet's default node
* is a public third-party one; set `rpcUrl` to your own provider.
*/
readonly network?: NetworkId;
/**
* The networks the network toggle switches between, `network` among them.
* Default only `network`. Every network besides `network` needs its own
* `sponsorUrl` in `endpoints`, since a sponsor serves one network.
*/
readonly networks?: readonly NetworkId[];
/**
* Endpoints per network. For `network` itself, `rpcUrl` and `sponsorUrl`
* below work too; a value here wins.
*/
readonly endpoints?: Partial<Record<NetworkId, NetworkEndpoints>>;
readonly variant?: Variant;
readonly appearance?: Appearance;
readonly theme?: WidgetTheme;
/** Stellar RPC endpoint. EVM reads go through the connected wallet, never here. */
readonly rpcUrl?: string;
/** A sponsor the integrator runs; defaults to Mavna's local dev sponsor. */
readonly sponsorUrl?: string;
/**
* The Mavna Arrival Contract on `network`, which lets USDT0 routes convert on
* arrival with one signature. Defaults to the deployed one (testnet only); a
* network without one says so on its LayerZero routes. Other networks set
* theirs in `endpoints`.
*/
readonly arrivalContract?: string;
/**
* How routes are ranked when several arrive, and the flow's initial priority.
* The two the flow exposes: most received, or fastest.
*/
readonly routePriority?: "best-received" | "fastest";
/**
* An integrator fee, taken from what the user receives. bps is an integer in
* [0, 1000]; recipient and recipientName are both required when set.
*/
readonly fee?: IntegratorFeeConfig;
/** Rails an operator has taken out of rotation (`parseDisabledSources` format). */
readonly disabledSources?: string;
// ---- form initialisation ----
/** The chain the form starts on, by its Mavna slug (e.g. "ethereum"). */
readonly fromChain?: string;
/**
* What the recipient arrives holding: USDC or XLM when sending USDC, and
* USDT0, USDC or XLM when sending USDT0 (anything but USDT0 is converted).
*/
readonly wanted?: Wanted;
/** What the form starts out sending. Defaults to USDC. */
readonly fromAsset?: SendAsset;
/** The amount the form starts with. */
readonly fromAmount?: string;
/** The recipient's Stellar address, pre-filled (locked with requiredUI). */
readonly toAddress?: string;
// ---- narrowing what the widget offers ----
/** Chains the picker offers, filtered over the network's real chain list. */
readonly chains?: { readonly allow?: readonly string[]; readonly deny?: readonly string[] };
/** The rails to race and toggle. Defaults to all four. */
readonly rails?: readonly RailName[];
readonly hiddenUI?: readonly HiddenUIElement[];
readonly requiredUI?: readonly RequiredUIElement[];
// ---- integration ----
/** Links for the header overflow ("...") menu. Empty or absent hides the menu.
The integrator's own destinations (its site, docs, socials), kept as plain
data so the config stays serialisable. */
readonly menuLinks?: readonly MavnaMenuLink[];
/** Namespaces this widget's local transfer history, so two integrators on one
origin do not share it. */
readonly keyPrefix?: string;
readonly walletConfig?: WalletConfig;
readonly onEvent?: (event: MavnaWidgetEvent) => void;
}