Embedded Wallet Integration
Use this page when you want one reference page for wiring the hosted embedded wallet into a web app.
Use This When
Section titled “Use This When”- you want the recommended React integration path
- you need to understand the smallest public wallet contract a dApp uses
- you want to connect a dApp, send a wallet-managed transaction intent for approval, and then submit it with
@thru/sdk
Choose The Right Package Layer
Section titled “Choose The Right Package Layer”| Entry point | Use it when | Avoid it when |
|---|---|---|
@thru/wallet/react | Your app already uses React and you want provider plus hooks. | You are not using React. |
@thru/wallet | You want a browser-side SDK without React. | You want React provider state or hooks. |
Install
Section titled “Install”For the recommended React path:
npm install @thru/wallet @thru/sdkFor a non-React integration:
npm install @thru/wallet @thru/sdkMinimal React Setup
Section titled “Minimal React Setup”Wrap the app with ThruProvider and point it at the hosted wallet iframe.
import { ThruProvider } from "@thru/wallet/react";
export function App({ children }: { children: React.ReactNode }) { return ( <ThruProvider config={{ iframeUrl: "https://wallet.thru.org/embedded", rpcUrl: "https://rpc.alphanet.thru.org", }} > {children} </ThruProvider> );}Minimal Connect Flow
Section titled “Minimal Connect Flow”connect() is the dApp entrypoint. The wallet resolves the request against the iframe, origin, and app metadata.
import { useWallet } from "@thru/wallet/react";
export function ConnectButton() { const { connect, isConnected, isConnecting } = useWallet();
if (isConnected) { return <button disabled>Wallet connected</button>; }
return ( <button onClick={() => connect({ metadata: { appId: window.location.origin, appName: "My Thru App", appUrl: window.location.origin, }, }) } disabled={isConnecting} > {isConnecting ? "Connecting..." : "Connect wallet"} </button> );}Minimal Sign-And-Submit Flow
Section titled “Minimal Sign-And-Submit Flow”Use signTransaction() with a transaction intent. The dApp supplies the program, instruction bytes, account addresses, and optional review metadata. The wallet approves the action, chooses the fee payer, orders accounts, fills transaction headers and nonces, signs the final transaction, and returns canonical raw transaction bytes encoded as base64.
instructionData is the base64-encoded program instruction payload.
import { useThru, useWallet } from "@thru/wallet/react";
function bytesToBase64(bytes: Uint8Array): string { let binary = ""; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary);}
function base64ToBytes(value: string): Uint8Array { const binary = atob(value); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes;}
export function SubmitSignedTransaction({ programAddress, instructionData, readWriteAddresses, readOnlyAddresses,}: { programAddress: string; instructionData: Uint8Array; readWriteAddresses?: string[]; readOnlyAddresses?: string[];}) { const { thru } = useThru(); const { wallet } = useWallet();
return ( <button onClick={async () => { if (!thru || !wallet) throw new Error("Wallet not ready");
const rawSignedBase64 = await wallet.signTransaction({ programAddress, instructionData: bytesToBase64(instructionData), readWriteAddresses, readOnlyAddresses, review: { appName: "My Thru App", programAddress, }, }); const signature = await thru.transactions.send(base64ToBytes(rawSignedBase64));
console.log("Submitted transaction", signature); }} > Sign and submit </button> );}Signing Context
Section titled “Signing Context”Call wallet.getSigningContext() when your UI needs to display the selected managed account or the current network signer. You do not need to use this response to build the final transaction wire payload; the wallet owns that step for signTransaction(intent).
The current embedded wallet contract returns a managed-fee-payer shape:
type ThruSigningContext = { mode: "managed_fee_payer"; selectedAccountPublicKey: string | null; feePayerPublicKey: string; signerPublicKey: string; acceptedInputEncodings: [ "signing_payload_base64", "raw_transaction_base64", ]; outputEncoding: "raw_transaction_base64";};Use it to answer two questions before signing:
- which managed account the user thinks they are acting as
- which public key actually signs and pays for network submission
What The dApp Owns
Section titled “What The dApp Owns”The dApp is responsible for:
- deciding when to call
connect() - building the program instruction payload
- passing a
ThruTransactionIntenttosignTransaction() - submitting the returned raw transaction bytes directly
- showing the right status while the wallet UI is open
The wallet is responsible for:
- presenting connection and approval UI
- unlocking with passkey if required
- selecting the current wallet account
- returning the current signing contract for the embedded environment
- choosing the fee payer and network signer
- ordering accounts, filling headers and nonces, and constructing the final wire transaction
- returning canonical raw transaction bytes after signing
Important Assumptions
Section titled “Important Assumptions”- the iframe URL must be a trusted wallet origin:
https://wallet.thru.orgor localhost during development signTransaction()expects a transaction intent withprogramAddressand base64instructionData- the wallet, not the dApp, owns fee payer selection and final transaction wire layout
- the browser wallet contract is intentionally narrow: connect, disconnect, account selection, transaction signing, and signing sessions
Open Next
Section titled “Open Next”- Approval and Signing to understand what happens after a dApp calls
connect()orsignTransaction() - Signing Sessions when repeated actions should use a temporary wallet-owned signer
- Troubleshooting if the request flow stalls or the transaction never appears on-chain