Freighter is a browser extension that manages Stellar key pairs and signs transactions without ever exposing the private key to the web page. StellarKraal uses Freighter to sign XDR transactions returned by the backend before they are submitted to the Stellar network.
This guide covers:
- How the
freighterClient.tswrapper works - The connect, disconnect, and sign-transaction flow
- The
getTestApi()mock used in testing - Network switching and mismatch detection
Install the Freighter browser extension from the official site: https://www.freighter.app/
Freighter is available for Chrome, Brave, Firefox, and Edge.
The frontend depends on @stellar/freighter-api (npm package). This is already listed as a
dependency in frontend/package.json.
frontend/src/lib/freighterClient.ts is a thin abstraction over @stellar/freighter-api that:
- Delegates every call to the real Freighter extension in production.
- Falls back to
window.__STELLARKRAAL_E2E__(a test mock) when that property is set.
This design means tests and E2E runners never need the browser extension.
Returns whether the Freighter extension is installed and reachable in the current browser. A
result of { isConnected: false } means the extension is not installed (or the API is
unreachable); it does not mean the user has no account selected.
import { isConnected } from "@/lib/freighterClient";
const { isConnected: installed } = await isConnected();
if (!installed) {
// prompt user to install Freighter
}Returns whether the current page has been granted permission to read the user's public key. The
permission is granted once via setAllowed() and persists until the user revokes it.
import { isAllowed } from "@/lib/freighterClient";
const { isAllowed: permitted } = await isAllowed();Opens the Freighter permission prompt asking the user to grant this origin access to their public
key. Must be called before getAddress() if the page has not been allowed yet.
import { setAllowed } from "@/lib/freighterClient";
await setAllowed(); // triggers the Freighter popupReturns the public key (G⦠address) of the currently selected Freighter account.
import { getAddress } from "@/lib/freighterClient";
const { address } = await getAddress();
// address === "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN"Passes an unsigned XDR transaction to Freighter for the user to review and sign. Returns the signed XDR string.
import { signTransaction } from "@/lib/freighterClient";
const { signedTxXdr } = await signTransaction(unsignedXdr, {
networkPassphrase: "Test SDF Network ; September 2015",
address: "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
});Options:
| Option | Type | Description |
|---|---|---|
network |
string | Alias for networkPassphrase (accepted for backwards compatibility) |
networkPassphrase |
string | Full Stellar network passphrase. Use this for explicit network identification. |
address |
string | The signing account's public key. Freighter will warn if it does not match the active account. |
The React hook frontend/src/hooks/useWallet.ts encapsulates the full wallet lifecycle and is
the primary integration point for UI components.
import { useWallet } from "@/hooks/useWallet";
function MyComponent() {
const { address, freighterInstalled, connecting, error, connect, disconnect } = useWallet();
// ...
}| Field | Type | Description |
|---|---|---|
address |
string | null |
The connected Stellar public key, or null when disconnected |
freighterInstalled |
boolean | null |
true = extension detected, false = not found, null = still detecting |
connecting |
boolean |
true while the connect flow is in progress |
error |
string | null |
Error message from the last failed operation, or null |
Runs the full connection flow:
- Calls
setAllowed()β opens the Freighter permission prompt if not already granted. - Calls
getAddress()β retrieves the public key. - Persists the address to
localStorageunder the keystellarkraal_wallet.
On subsequent page loads the hook restores the persisted address automatically after verifying
the extension still reports isAllowed().
await connect();
// address is now populated if the user approved the promptClears address from state and removes the persisted value from localStorage. Does not
revoke the Freighter permission β the user must do that manually from the extension.
disconnect();
// address === nullAfter obtaining an unsigned XDR from the backend, pass it to signTransaction:
import { signTransaction } from "@/lib/freighterClient";
import { useWallet } from "@/hooks/useWallet";
function LoanRequestButton({ xdr }: { xdr: string }) {
const { address } = useWallet();
async function handleSign() {
const { signedTxXdr } = await signTransaction(xdr, {
networkPassphrase: process.env.NEXT_PUBLIC_NETWORK === "mainnet"
? "Public Global Stellar Network ; September 2015"
: "Test SDF Network ; September 2015",
address: address ?? undefined,
});
// Submit signedTxXdr to the Stellar network or backend
}
return <button onClick={handleSign}>Sign & Submit</button>;
}freighterClient.ts checks for window.__STELLARKRAAL_E2E__ before calling the real Freighter
API. If the property is set, its methods take precedence.
type FreighterTestApi = Partial<{
isConnected: () => Promise<{ isConnected: boolean }>;
isAllowed: () => Promise<{ isAllowed: boolean }>;
setAllowed: () => Promise<{ isAllowed: boolean }>;
getAddress: () => Promise<{ address: string }>;
signTransaction: (
xdr: string,
opts?: { network?: string }
) => Promise<{ signedTxXdr: string }>;
}>;
// Also available:
// submitSignedXdr?: (signedXdr: string) => Promise<string> | string;All fields are optional β only the methods you set are overridden.
beforeEach(() => {
// install the mock before importing modules that use freighterClient
Object.defineProperty(window, "__STELLARKRAAL_E2E__", {
value: {
isConnected: async () => ({ isConnected: true }),
isAllowed: async () => ({ isAllowed: true }),
setAllowed: async () => ({ isAllowed: true }),
getAddress: async () => ({ address: "GTEST1234567890" }),
signTransaction: async (xdr: string) => ({
signedTxXdr: "SIGNED_" + xdr,
}),
},
writable: true,
});
});
afterEach(() => {
// clean up
delete (window as any).__STELLARKRAAL_E2E__;
});In the Playwright config or a fixture, set the mock before navigating:
await page.addInitScript(() => {
window.__STELLARKRAAL_E2E__ = {
isConnected: async () => ({ isConnected: true }),
isAllowed: async () => ({ isAllowed: true }),
setAllowed: async () => ({ isAllowed: true }),
getAddress: async () => ({ address: "GPLAYWRIGHTTEST1234567890" }),
signTransaction: async (xdr) => ({ signedTxXdr: "SIGNED_" + xdr }),
submitSignedXdr: async (signed) => "TX_HASH_MOCK",
};
});This allows E2E scenarios to test the full wallet-connected flow without requiring the Freighter extension to be installed in the test browser.
frontend/src/hooks/useNetworkMismatch.ts compares the wallet's active network to the
app-configured network (NEXT_PUBLIC_NETWORK).
import { useNetworkMismatch } from "@/hooks/useNetworkMismatch";
function NetworkWarning({ walletAddress }: { walletAddress: string | null }) {
const mismatch = useNetworkMismatch(walletAddress);
if (mismatch) {
return (
<div role="alert">
Your Freighter wallet is connected to a different network than this app.
Please switch your wallet network to match <code>{process.env.NEXT_PUBLIC_NETWORK}</code>.
</div>
);
}
return null;
}- The hook calls
getNetworkDetails()from@stellar/freighter-apiwhenwalletAddressbecomes non-null. - It compares
result.network.toLowerCase()toprocess.env.NEXT_PUBLIC_NETWORK.toLowerCase(). - Returns
true(mismatch) if the strings differ. - Returns
falsewhen the wallet address isnull(not connected) or when the networks match.
NEXT_PUBLIC_NETWORK value |
Freighter network value |
Description |
|---|---|---|
testnet |
TESTNET |
Stellar test network |
mainnet |
PUBLIC |
Stellar public network |
local |
(sandbox-specific) | Local Soroban sandbox |
The comparison is case-insensitive so testnet matches TESTNET.
Instruct the user to open Freighter and switch the active network. Freighter allows multiple network profiles and the active one can be changed from the extension popup's settings. The mismatch state updates automatically on the next render cycle after the wallet is reconnected.
| Symptom | Cause | Resolution |
|---|---|---|
freighterInstalled === false |
Extension not installed | Install Freighter from freighter.app |
connect() shows no popup |
Permission already granted | setAllowed() is a no-op if permission is already held; getAddress() will succeed immediately |
signTransaction rejected |
User dismissed the Freighter popup | Catch the error and show a user-friendly message |
signedTxXdr rejected by network |
Wrong networkPassphrase |
Verify the passphrase matches the network in your .env |
| Network mismatch warning shown | Wallet on wrong network | Switch network in Freighter extension settings |
window.__STELLARKRAAL_E2E__ is not defined in tests |
Mock not installed | Set the mock in beforeEach before importing the module under test |
- Freighter API npm package
- Freighter Developer Docs
- Stellar Developer Docs β Transaction Signing
- API Quickstart β how to obtain unsigned XDR from the backend
- XDR Transaction Building Guide
- E2E Tests Guide