Skip to main content
Use this page when you want one reference page for wiring the hosted embedded wallet into a web app.

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, inspect the wallet signing contract, sign a transaction, and then submit it with @thru/thru-sdk

Choose The Right Package Layer

PackageUse it whenAvoid it when
@thru/react-sdkYour app already uses React and you want provider plus hooks.You are not using React.
@thru/browser-sdkYou want a simple browser-side SDK without React.You need raw iframe lifecycle control.
@thru/embedded-providerYou are building a custom host around the iframe and postMessage layer.You just need normal dApp integration.

Install

For the recommended React path:
npm install @thru/react-sdk @thru/thru-sdk
For a non-React integration:
npm install @thru/browser-sdk @thru/thru-sdk

Minimal React Setup

Wrap the app with ThruProvider and point it at the hosted wallet iframe.
import { ThruProvider } from "@thru/react-sdk";

export function App({ children }: { children: React.ReactNode }) {
  return (
    <ThruProvider
      config={{
        iframeUrl: "https://wallet.thru.org/embedded",
        rpcUrl: "https://grpc-web.alphanet.thruput.org",
      }}
    >
      {children}
    </ThruProvider>
  );
}

Minimal Connect Flow

connect() is the dApp entrypoint. The wallet resolves the request against the iframe, origin, and app metadata.
import { useWallet } from "@thru/react-sdk";

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

Use getSigningContext() before you build the transaction. The selected wallet account is the managed account the user sees, but the actual fee payer and signer can be the embedded manager profile. signTransaction() accepts either:
  • signing payload bytes from transaction.toWireForSigning()
  • raw transaction bytes from transaction.toWire()
It always returns canonical raw transaction bytes encoded as base64, ready for direct submission.
import { useThru, useWallet } from "@thru/react-sdk";

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({
  transaction,
}: {
  transaction: { toWireForSigning(): Uint8Array; toWire(): Uint8Array };
}) {
  const { thru } = useThru();
  const { wallet } = useWallet();

  return (
    <button
      onClick={async () => {
        if (!thru || !wallet) throw new Error("Wallet not ready");

        const signingContext = await wallet.getSigningContext();

        console.log("Managed account", signingContext.selectedAccountPublicKey);
        console.log("Network signer", signingContext.signerPublicKey);
        console.log("Fee payer", signingContext.feePayerPublicKey);

        // Build the transaction with signingContext.feePayerPublicKey rather than
        // assuming the selected managed account is also the network signer.
        const signingPayloadBase64 = bytesToBase64(transaction.toWireForSigning());
        const rawSignedBase64 = await wallet.signTransaction(signingPayloadBase64);
        const signature = await thru.transactions.send(base64ToBytes(rawSignedBase64));

        console.log("Submitted transaction", signature);
      }}
    >
      Sign and submit
    </button>
  );
}

Signing Context

Call wallet.getSigningContext() before building transactions that need exact signer or fee-payer information. 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

The dApp is responsible for:
  • deciding when to call connect()
  • calling getSigningContext() before building transactions that depend on signer or fee-payer identity
  • building the unsigned transaction bytes with the correct fee payer
  • calling signTransaction() with a base64 payload
  • 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
  • returning canonical raw transaction bytes after signing

Important Assumptions

  • the iframe URL must be a trusted wallet origin: https://wallet.thru.org or localhost during development
  • signTransaction() expects a non-empty base64 payload in one of the accepted encodings
  • getSigningContext().feePayerPublicKey is the source of truth for the network fee payer
  • the wallet contract is intentionally narrow: connect, disconnect, account selection, and transaction signing

Open Next

  • Approval and Signing to understand what happens after a dApp calls connect() or signTransaction()
  • Troubleshooting if the request flow stalls or the transaction never appears on-chain