> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metalntwx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect a browser wallet

> Connect an EIP-1193 wallet and request user-approved Metal transactions

> [!NOTE]
>
> **What you'll be able to do**
>
> * Connect a user's browser wallet to Metal Testnet.
> * Request signatures for transactions or typed data.
> * Submit an approved transaction.

A browser wallet holds the user's signing authority. The application prepares
the network and transaction details, then requests wallet approval. Keep
connection and payment actions behind explicit user controls, and invalidate
pending actions whenever the selected account or network changes.

## Prepare the application

Use an existing browser application with TypeScript and an
[EIP-1193 provider](https://eips.ethereum.org/EIPS/eip-1193). Let the user select a
provider when several wallets are installed.

Install the public dependencies for the flows below:

```bash theme={null}
pnpm add viem@2 @metalntwx/metal-ts@0.3.2 idb@8
```

Obtain an endpoint through [Connect to Metal](https://docs.metalntwx.com/build/prepare/connect-to-metal.md).
A browser endpoint URL is visible to the browser's user. Configure it for that
application's allowed origins and usage limits, and keep administrative
credentials on the server. For testing, use a dedicated test account funded through
[Fund a test account](https://docs.metalntwx.com/build/prepare/fund-a-test-account.md).

Define the network with the full configured endpoint URL:

```typescript theme={null}
import {
  createPublicClient, createWalletClient, custom, defineChain, http,
  type EIP1193Provider
} from "viem"

export function metalChain(rpcUrl: string) {
  if (new URL(rpcUrl).protocol !== "https:") throw new Error("Use the HTTPS endpoint")
  return defineChain({
    id: 63825,
    name: "Metal Testnet",
    nativeCurrency: { name: "USD Zero", symbol: "USDZ", decimals: 18 },
    rpcUrls: { default: { http: [rpcUrl] } },
    blockExplorers: { default: { name: "Metal Explorer", url: "https://testnet.metalntwx.com" } }
  })
}
```

Wallet schemas require `nativeCurrency` display metadata. Metal charges the
default gas token for standard Ethereum transactions and exposes its
balance through `eth_getBalance`. Set transaction `value` to `0n`. Token payments
call the MIP20 contract's transfer function.

## Connect from a user action

Call `connectWallet(provider, rpcUrl)` from a Connect button. Show failures in the
application without automatically opening another wallet prompt. Only the
wallet's unknown-chain error `4902` triggers the add-network request.

```typescript theme={null}
export async function connectWallet(provider: EIP1193Provider, rpcUrl: string) {
  const chain = metalChain(rpcUrl)
  const publicClient = createPublicClient({ chain, transport: http(rpcUrl) })
  if (await publicClient.getChainId() !== chain.id) throw new Error("RPC is on another network")

  await provider.request({ method: "eth_requestAccounts" })
  try {
    await provider.request({
      method: "wallet_switchEthereumChain", params: [{ chainId: "0xf951" }]
    })
  } catch (error) {
    if ((error as { code?: number }).code !== 4902) throw error
    await provider.request({
      method: "wallet_addEthereumChain",
      params: [{
        chainId: "0xf951", chainName: chain.name,
        nativeCurrency: chain.nativeCurrency,
        rpcUrls: [rpcUrl], blockExplorerUrls: [chain.blockExplorers.default.url]
      }]
    })
    await provider.request({
      method: "wallet_switchEthereumChain", params: [{ chainId: "0xf951" }]
    })
  }

  const session = { valid: true }
  const invalidate = () => { session.valid = false }
  provider.on("accountsChanged", invalidate)
  provider.on("chainChanged", invalidate)
  provider.on("disconnect", invalidate)
  const dispose = () => {
    invalidate()
    provider.removeListener("accountsChanged", invalidate)
    provider.removeListener("chainChanged", invalidate)
    provider.removeListener("disconnect", invalidate)
  }

  try {
    const accounts = await provider.request({ method: "eth_accounts" })
    const account = accounts[0]
    if (!account) throw new Error("No account selected")
    const walletClient = createWalletClient({ account, chain, transport: custom(provider) })
    const assertCurrent = async () => {
      const current = await provider.request({ method: "eth_accounts" })
      const chainId = await walletClient.getChainId()
      if (!session.valid || current[0]?.toLowerCase() !== account.toLowerCase() || chainId !== chain.id) {
        throw new Error("Wallet changed. Reconnect before continuing")
      }
    }
    await assertCurrent()
    return { account, chain, publicClient, walletClient, session, assertCurrent, dispose }
  } catch (error) {
    dispose()
    throw error
  }
}
```

Disable payment controls when `session.valid` becomes false and reconnect through
another explicit user action. Call `dispose()` when replacing the connection or
unmounting the UI. Check `assertCurrent()` before requesting a signature and
again before broadcasting bytes held by the application.

## Prepare the payment for approval

Resolve `token`, `recipient`, and `amount` using
[Direct payment](https://docs.metalntwx.com/build/payments/direct-payment.md). Reject invalid
addresses, nonpositive amounts, excess decimal precision, and amounts beyond
`uint256`. Read token metadata and balances through the public client.

Use [Estimate and record fees](https://docs.metalntwx.com/build/prepare/estimate-and-record-fees.md) to obtain a
successful estimate, apply headroom, and verify affordability. The signing
account needs the transfer amount in the token being sent. The signing account's
balance in the gas token must cover the maximum gas cost. If those are the same
token, account for both debits together.

Show the user the network, token address and symbol, recipient, amount, gas
token, and maximum gas cost before opening the approval request. Keep the
application operation ID and expected payment fields in durable application storage. Disable
repeat submission while the request is awaiting approval or reconciliation.

## Submit a standard wallet transaction

A standard wallet transaction uses the network's default gas token. Supply
`gasLimit`, `maxFeePerGas`, and `maxPriorityFeePerGas` from the fee procedure.
Estimate without fee fields first, because wallet-added fee fields can trigger
a zero-allowance estimate failure on this network.

```typescript theme={null}
import { erc20Abi } from "viem"

await connection.assertCurrent()
const hash = await connection.walletClient.writeContract({
  address: token,
  abi: erc20Abi,
  functionName: "transfer",
  args: [recipient, amount],
  gas: gasLimit,
  maxFeePerGas,
  maxPriorityFeePerGas
})
```

Here `connection` is the result of `connectWallet`. The nonpayable token call
omits `value`, so the transaction carries zero native value. The wallet handles signing
and broadcast, and returns a transaction hash. Save that hash against the
existing operation immediately, even if a subsequent wallet event invalidates
the connection. Verify the result through the public client independently of
the wallet's current network.

`eth_sendTransaction` does not expose signed bytes before broadcast.
If the wallet submits but its response is lost, the application may have no
transaction hash. Mark the operation uncertain, inspect the wallet's activity
and the signing account's transaction history, and identify the exact call before
retrying.

## Sign typed data before broadcasting

For a selected MIP20 gas token, `signMip20Contract` asks the wallet to sign Metal's
EIP-712 transaction data and returns the encoded `0x7f` envelope without sending
it. The signature authorizes that exact transaction, including chain, nonce,
call, and fee fields.

Create a durable browser store with `idb`, which wraps IndexedDB:

```typescript theme={null}
import { openDB } from "idb"

const attempts = await openDB("metal-payment-attempts", 1, {
  upgrade(database) {
    database.createObjectStore("attempts", { keyPath: "id" })
  }
})
```

Before requesting the signature, add an attempt containing a fresh application
ID and the immutable expected payment. `expectedPayment` contains the chain,
token, sender, recipient, amount, gas token, and fee bounds. Encode bigint fields
as decimal strings when sending this record to the server.

```typescript theme={null}
import { signMip20Contract } from "@metalntwx/metal-ts"

const id = crypto.randomUUID()
await attempts.add("attempts", { id, expectedPayment, state: "awaiting_signature" })
await connection.assertCurrent()
const signed = await signMip20Contract({
  publicClient: connection.publicClient,
  walletClient: connection.walletClient,
  address: token,
  abi: erc20Abi,
  functionName: "transfer",
  args: [recipient, amount],
  gasToken,
  gasLimit,
  gasFallback: gasLimit,
  maxFeePerGas
})
await attempts.put("attempts", {
  id, expectedPayment, state: "signed", hash: signed.txHash, raw: signed.raw
})
await connection.assertCurrent()
const hash = await signed.send()
if (hash !== signed.txHash) throw new Error("Unexpected transaction hash")
await attempts.put("attempts", {
  id, expectedPayment, state: "submitted", hash, raw: signed.raw
})
```

The helper uses `maxFeePerGas` for both its maximum and priority fee. Passing the
successful estimate as `gasLimit` avoids silently proceeding after an estimation
failure. See [TypeScript SDK](https://docs.metalntwx.com/reference/typescript-sdk.md#signmip20contract) for the
exact transaction fields.

Serialize signing for each account and reconcile an unresolved attempt before
creating another. Browser storage is local to the profile and can be cleared.
For recovery across devices, save the attempt to the service's durable store
and wait for its acknowledgement before broadcasting.

After a reload, read the saved attempt. Verify its hash first. If submission is
still uncertain, use the identical `raw` bytes as described in
[Recover an uncertain submission](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md#recover-an-uncertain-submission).

## Verify and display the result

A returned hash means submission, not payment completion. Wait for the receipt,
check execution, and match the exact token, sender, recipient, and amount in its
`Transfer` event. Verify the canonical containing block and finality through
[Best practices in processing payments](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md).
Update the order once from that verified result.

| Condition | Application behavior |
| - | - |
| No selected provider | Explain how to open or select a compatible wallet |
| `4001`, user refusal | End the attempt without another automatic prompt |
| `4100`, unauthorized account | Return to an explicit connection request |
| `4200`, unsupported method | Explain which signing method the chosen flow needs |
| `4900` or `4901`, disconnected | Pause signing and reconnect before another request |
| Account or chain changes | Invalidate the connection and reconcile any existing attempt |
| RPC rejects broadcast | Preserve the attempt and classify the admission error |
| Transaction reverts | Record the failure and fee, without recording a recipient credit |
| Finality is delayed | Keep the payment pending and continue bounded read retries |

Test refusal, unknown chain, account changes during a signature, disconnect,
and an uncertain broadcast with a controlled provider fixture. Then exercise
consent and one finalized payment with a clean real-wallet profile. A provider
fixture establishes application behavior but does not establish wallet support.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.