Skip to content

Connect a site ​

Your server does not receive the passkey. The page loads the wallet script. The user confirms in UTN Wallet. Your server checks the signature or the transaction hash on the RPC.

The coin on this network is T-RAIN. On mainnet the coin name is RAIN and the RPC URL is different. The script calls stay the same.

Load the script ​

html
<script
  src="https://wallet.utn.network/sdk/utn-wallet.js"
  data-wallet="https://wallet.utn.network"
  data-rpc="https://rpc.utn.network"
></script>

data-wallet is optional. If you omit it, the script uses window.UTN_CONFIG.wallet from /utn-config.js, then the origin it was served from. data-rpc overrides the RPC the same way. data-deployer overrides the deployer address used when a transaction creates a contract.

The script sets window.utn (an EIP-1193 provider) and window.UtnWallet. It does not replace window.ethereum. It also announces itself over EIP-6963, so a site that lists wallets can show UTN Wallet next to MetaMask.

Call request from a click. On a phone the wallet opens as a tab. On a desktop it opens as a popup (420 by 720). If the browser blocks the window, the page shows Open UTN Wallet. The script posts the request to POST https://wallet.utn.network/bridge/req and waits on GET https://wallet.utn.network/bridge/res/{sid}.

Ask for the account ​

js
const [account] = await window.utn.request({
  method: "eth_requestAccounts",
});

account is the smart-account address.

Sign in ​

wallet_connect opens one window and can return a Sign-In with Ethereum message and signature.

js
const nonce = crypto.randomUUID().replace(/-/g, "").slice(0, 17);
const result = await window.utn.request({
  method: "wallet_connect",
  params: [{
    capabilities: {
      signInWithEthereum: {
        nonce,
        chainId: "0x135288b",
      },
    },
  }],
});

const signedIn = result.accounts[0];
const address = signedIn.address;
const message = signedIn.capabilities.signInWithEthereum.message;
const signature = signedIn.capabilities.signInWithEthereum.signature;

Send address, message, and signature to your server. Reject a nonce you have already accepted.

personal_sign signs text you choose:

js
const signature = await window.utn.request({
  method: "personal_sign",
  params: [`Sign in to example.com\nNonce: ${nonce}`, account],
});

That proves control of the account. It does not submit a transaction and it does not spend T-RAIN.

eth_signTypedData_v4 is implemented. The script rejects eth_sign, eth_signTransaction, eth_signTypedData, eth_signTypedData_v3, and eth_sendRawTransaction.

Check the signature ​

personal_sign returns a passkey signature. Check it with UtnSignatures.isValidSignature(account, hash, signature), where hash is the EIP-191 hash of the exact message (including line breaks). UtnAccount does not implement EIP-1271, so ecrecover on the account address will not succeed.

/utn-config.js names this contract as privacySignatures. wallet/app/js/networks.js names the same address as signatures: 0x90d3e1668768b54372a96b8a8f7f0c9b5a3c793b. On 8 Oct 2026, factory() on that address returned the passkey factory from GET /relay/info, and factory2() returned that same payload's factory2. A testnet reset can move this contract. Read eth_getCode before you rely on an address.

js
import { createPublicClient, http, hashMessage } from "viem";

const client = createPublicClient({
  transport: http("https://rpc.utn.network"),
});

const ok = await client.readContract({
  address: "0x90d3e1668768b54372a96b8a8f7f0c9b5a3c793b",
  abi: [{
    type: "function",
    name: "isValidSignature",
    stateMutability: "view",
    inputs: [
      { name: "account", type: "address" },
      { name: "hash", type: "bytes32" },
      { name: "signature", type: "bytes" },
    ],
    outputs: [{ type: "bool" }],
  }],
  functionName: "isValidSignature",
  args: [process.env.ACCOUNT, hashMessage(process.env.MESSAGE), process.env.SIGNATURE],
});

if (!ok) throw new Error("Signature rejected");

The source of that contract was not verified on Scan, so this check identifies it by factory() and factory2(), not by a published ABI match.

Send a transaction ​

js
const hash = await window.utn.request({
  method: "eth_sendTransaction",
  params: [{
    from: account,
    to: "0xCONTRACT",
    data: "0x",
    value: "0x0",
  }],
});

The user confirms with the passkey. The script submits through the relay. hash is the transaction hash. A deployment omits to. The constructor sees msg.sender as the deployer contract; pass the creating account as a constructor argument if the contract needs it.

eth_getTransactionReceipt on this provider reports from as the smart account. For a deployment it reports to as null and contractAddress as the new contract.

Reads (eth_call, eth_chainId, eth_getBalance, and other eth_, net_, and web3_ methods the script does not handle itself) go to the RPC.

The relay, the bridge, and the connect window are listed on Endpoints.

Testnet coin T-RAIN · chain 20261003