@perawallet/connect

JavaScript SDK for integrating Pera Wallet to web applications
391,726 70
Install
npm install @perawallet/connect
Latest Version:1.9.0
License:ISC
Last Updated:Oct 19, 2026
Links: GitHub  ·  npm
Maintainer: yasincaliskan

Pera Connect Cover Image

@perawallet/connect

JavaScript SDK for integrating Pera Wallet to web applications. For more detailed information, please check our Pera Connect Docs. Using use-wallet to support several wallets? See Using with use-wallet.

Getting Started

Learn how to integrate with your JavaScript application

Learn how to Sign Transactions

Try it out using CodeSandbox

Example Applications

Quick Start

Let's start with installing @perawallet/connect

# pnpm
pnpm add @perawallet/connect

# npm
npm install @perawallet/connect

# yarn
yarn add @perawallet/connect

Subscribe to disconnect once, next to the instance rather than inside a connect callback: on() keeps every handler you pass it until you unsubscribe, so registering a fresh one per connect leaves the old ones running too.

// Once, alongside the peraWallet instance — not inside connect()/reconnectSession()
useEffect(() => {
  // Fires for every transport (mobile, extension) when the wallet ends the
  // session on its side. Returns the unsubscribe function.
  const unsubscribe = peraWallet.on("disconnect", handleDisconnectWalletClick);

  // Releases the `window.pera` subscriptions this instance holds; the wallet
  // session itself is untouched.
  return () => {
    unsubscribe();
    peraWallet.dispose();
  };
}, []);
// Connect handler
peraWallet
  .connect()
  .then((newAccounts) => {
    setAccountAddress(newAccounts[0]);
  })
  .catch((error) => {
    // You MUST handle the reject because once the user closes the modal, peraWallet.connect() promise will be rejected.
    // For the async/await syntax you MUST use try/catch
    if (error?.data?.type !== "CONNECT_MODAL_CLOSED") {
      // log the necessary errors
    }
  });

If you don't want the user's account information to be lost by the dApp when the user closes the browser with user’s wallet connected to the dApp, you need to handle the reconnect session status. You can do this in the following way.

// On the every page refresh
peraWallet.reconnectSession().then((accounts) => {
  if (accounts.length) {
    setAccountAddress(accounts[0]);
  }
});

After that you can sign transaction with this way

// Single Transaction
try {
  const signedTxn = await peraWallet.signTransaction([singleTxnGroups]);
} catch (error) {
  console.log("Couldn't sign Opt-in txns", error);
}

Using with use-wallet

If your dApp uses use-wallet v5, add Pera with @perawallet/use-wallet-pera. Pera maintains that adapter, and it pins the version of this SDK it was tested against.

pnpm add @perawallet/use-wallet-pera
import {WalletManager} from "@txnlab/use-wallet";
import {pera} from "@perawallet/use-wallet-pera";

const manager = new WalletManager({wallets: [pera()]});

pera() accepts the same options as new PeraWalletConnect(). Moving from TxnLab's @txnlab/use-wallet-pera only means changing the import: the wallet id is still "pera", so persisted sessions carry over.

getEmptySignatures() is what the adapter will use to report account types for use-wallet 5.1.

Options

option default value
chainId 4160 416001, 416002, 416003 , 4160 optional
shouldShowSignTxnToast true boolean optional
compactMode false boolean optional
shouldPreferExtension true boolean optional
algod public nodes {mainnet?, testnet?: algosdk.Algodv2} optional
chainId

Determines which Algorand network your dApp uses.

MainNet: 416001

TestNet: 416002

BetaNet: 416003

All Networks: 4160

shouldShowSignTxnToast

It's enabled by default but in some cases, you may not need the toast message (e.g. you already have signing guidance for users). To disable it, use the shouldShowSignTxnToast option.

compactMode

It offers a compact UI optimized for smaller screens, with a minimum resolution of 400x400 pixels.

shouldPreferExtension

When the Pera browser extension is installed, the connect modal lists "Connect with Pera Extension" first and pre-selects it. Set this to false to leave the extension out of the modal and only offer the QR code and Pera Web options. See Browser extension below.

algod

Connect reads account state from an Algorand node in two places: resolveArc60Signer, and the auth-address lookup behind signData verification. By default those reads go to the public AlgoNode endpoints (mainnet-api.algonode.cloud, testnet-api.algonode.cloud), which need no API token.

That default is a third party on a shared free tier. If your dApp reads often, supply your own client — otherwise a rate limit surfaces as SIGN_DATA_AUTH_ADDR_LOOKUP_FAILED. Pass algosdk.Algodv2 instances to keep these reads on your own infrastructure, with whatever token, headers, or HTTP client your provider needs. A network you leave out keeps the public default.

import algosdk from "algosdk";
import {PeraWalletConnect} from "@perawallet/connect";

const peraWallet = new PeraWalletConnect({
  chainId: 416001,
  algod: {
    mainnet: new algosdk.Algodv2(process.env.ALGOD_TOKEN, "https://my-node.example.com", 443)
  }
});

The client is used as you built it, so anything algosdk.Algodv2 supports works — a tokenless node (new algosdk.Algodv2("", "http://localhost", 4001)), a provider that wants its own header (new algosdk.Algodv2({"X-API-Key": key}, server, "")), or a custom BaseHTTPClient.

Methods

PeraWalletConnect.connect(): Promise<string[]>

Starts the initial connection flow and returns the array of account addresses.

PeraWalletConnect.reconnectSession(): Promise<string[]>

Reconnects to the wallet if there is any active connection and returns the array of account addresses.

PeraWalletConnect.disconnect(): Promise<void | undefined>

Disconnects from the wallet and resets the related storage items. Pera's session state lives only under its own localStorage keys (PeraWallet.Wallet and PeraWallet.WalletConnect), so disconnecting never removes another wallet's WalletConnect session. A session stored under the shared walletconnect key by earlier versions is migrated to PeraWallet.WalletConnect automatically the first time PeraWalletConnect is instantiated.

PeraWalletConnect.platform: PeraWalletPlatformType

Returns the platform of the active session. Possible responses: mobile | web | extension | null

PeraWalletConnect.isExtensionAvailable(): Promise<boolean>

Resolves with whether the Pera browser extension's provider (window.pera) is present on the page. See Browser extension.

PeraWalletConnect.on(event, handler): () => void

Subscribes to session events and returns the unsubscribe function.

  • "disconnect": the wallet ended the session on its side. With the extension this happens when the user revokes your site from the extension's Connections screen; with Pera mobile when the WalletConnect session is killed from the app. The SDK has already cleared its session state when the handler runs. It does not fire for teardown the SDK itself starts — your own disconnect() call, or the session connect() replaces.
  • "networkChanged": the extension wallet switched network, on a session owned by the extension. The handler receives {network: "mainnet" | "testnet" | "betanet"}.

Handlers stay subscribed until you call the returned function, so register them once per instance rather than on every connect.

const unsubscribe = peraWallet.on("disconnect", () => setAccountAddress(null));
PeraWalletConnect.dispose(): void

Releases everything the instance holds — the window.pera subscriptions and any WalletConnect connector — without touching the wallet session. Call it when the component owning the instance unmounts; under React StrictMode or hot reload, an undisposed instance keeps listening and swallows events the live one should handle. Use disconnect() to end the session itself.

PeraWalletConnect.isConnected: boolean

Checks if there's any active session regardless of platform. Possible responses: true | false

PeraWalletConnect.isPeraDiscoverBrowser: boolean

Checks if it is on Pera Discover Browser. Possible responses: true | false

PeraWalletConnect.signTransaction(txGroups: SignerTransaction[][], signerAddress?: string): Promise<Uint8Array[]>

Starts the sign process and returns the signed transaction in Uint8Array

PeraWalletConnect.transactionSigner: TransactionSigner

An algosdk TransactionSigner backed by the current wallet connection, so Pera can be plugged straight into AtomicTransactionComposer (or anything else that accepts a signer). The whole group is sent to the wallet in a single request; transactions not assigned to Pera are shown to the user but not signed. The same function instance is returned on every access, which is what lets the composer batch every Pera-signed transaction into one prompt. The signer carries only the transactions themselves; if you need authAddr, msig or a per-transaction message, call signTransaction directly.

import algosdk from "algosdk";

const peraWallet = new PeraWalletConnect();
const [sender] = await peraWallet.reconnectSession();

const algod = new algosdk.Algodv2("", "https://testnet-api.algonode.cloud", "");
const suggestedParams = await algod.getTransactionParams().do();
const atc = new algosdk.AtomicTransactionComposer();

atc.addTransaction({
  signer: peraWallet.transactionSigner,
  txn: algosdk.makePaymentTxnWithSuggestedParamsFromObject({
    sender,
    receiver: sender,
    amount: 0,
    suggestedParams
  })
});

const result = await atc.execute(algod, 4);
PeraWalletConnect.signData(data: PeraWalletArbitraryData[], signer: string, verifySignature?: boolean): Promise<Uint8Array[]>

Starts the signing process for arbitrary data signing and returns the signed data in Uint8Array. The wallet signs each item with the "MX" prefix, exactly like algosdk.signBytes, so each signature verifies with algosdk.verifyBytes against the account that signed it, or with verifySignature below. signer should be a valid Algorand address that exists in the user's wallet.

Parameters:

  • data: Array of arbitrary data to sign
  • signer: Algorand address that will sign the data
  • verifySignature (optional): If true, automatically detects if the account is rekeyed (has authAddr) and uses the authAddr as the signer. After signing, verifies each signature against the original data. Defaults to false.

Note: When verifySignature is true, the function will:

  1. Fetch account information from the Algorand network
  2. Check if the account has an authAddr (rekeyed account)
  3. Automatically use the authAddr as the signer if it exists, otherwise use the provided signer address
  4. Verify each signature after signing using the verifySignature method (see below)
// Basic usage
const signedData: Uint8Array[] = await peraWallet.signData([
  {
    data: new Uint8Array(Buffer.from(`timestamp//${Date.now()}`)),
    message: "Timestamp confirmation"
  },
  {
    data: new Uint8Array(Buffer.from(`agent//${navigator.userAgent}`)),
    message: "User agent confirmation"
  }
], "SAHBJDRHHRR72JHTWSXZR5VHQQUVC7S757TJZI656FWSDO3TZZWV3IGJV4");

// With signature verification (automatically handles rekeyed accounts)
const verifiedSignedData: Uint8Array[] = await peraWallet.signData([
  {
    data: new Uint8Array(Buffer.from(`timestamp//${Date.now()}`)),
    message: "Timestamp confirmation"
  }
], "SAHBJDRHHRR72JHTWSXZR5VHQQUVC7S757TJZI656FWSDO3TZZWV3IGJV4", true);
PeraWalletConnect.verifySignature(data: Uint8Array, signature: Uint8Array, signerAddress: string): boolean

Verifies a signature against the provided data and signer address. This method can be used independently to verify signatures returned from signData or other sources. When signData is called with verifySignature: true, it uses this method internally to verify the signatures.

Parameters:

  • data: The original data that was signed (as Uint8Array)
  • signature: The signature to verify (as Uint8Array)
  • signerAddress: The Algorand address that should have signed the data

Returns: true if the signature is valid, false otherwise.

Note: This method automatically prefixes the data with "MX" (bytes [77, 88]) before verification to be consistent with algosdk.verifyBytes function. This ensures compatibility with Algorand's standard signature verification format. The data passed to this method should be the original data without the "MX" prefix, as the prefix is added internally.

// Verify a signature independently
const isValid = peraWallet.verifySignature(
  originalData,
  signature,
  "SAHBJDRHHRR72JHTWSXZR5VHQQUVC7S757TJZI656FWSDO3TZZWV3IGJV4"
);

if (isValid) {
  console.log("Signature is valid!");
} else {
  console.log("Signature verification failed!");
}
PeraWalletConnect.signArc60Data(payload: PeraWalletArc60SignData, metadata: SignMetadata, verifySignature?: boolean): Promise<PeraWalletArc60SignDataResponse>

Signs an ARC-60 payload (for example a Sign-In With Algorand request) with the Pera mobile wallet or the Pera extension. payload.signer is the public key that must sign, payload.domain must match your page origin, and metadata is {scope: ScopeType.AUTH, encoding: "base64"}. With verifySignature: true the returned signature is checked against payload.signer before resolving. For rekeyed accounts see resolveArc60Signer below.

payload.scheme names the signing scheme, as proposed for ARC-60 in algorandfoundation/ARCs#494. Leave it out for ed25519, which is what every request used before schemes existed. Those requests go to the wallet unchanged.

Falcon-1024 accounts

Pera's quantum accounts sign with scheme: "falcon-1024-det", the same way as use-wallet and Lute:

  • A Falcon address is a hash of its public key, so signer must be the key itself. Take it from the account's empty signature (pqsig.pk).
  • The SIWA payload's type must also be "falcon-1024-det". The wallet rejects a scheme that doesn't match it, or that the account can't sign with.
  • connect sends the key as base64, with the scheme beside it. Pera versions that can't sign Falcon ARC-60 yet reject the request.
  • The response keeps your signer and scheme, and signature is a compressed Falcon signature.
  • connect can only verify ed25519 signatures. With verifySignature: true, a Falcon request throws SIGN_DATA_VERIFICATION_UNSUPPORTED before the wallet is asked. Verify it in your app instead.
import algosdk from "algosdk"; // 3.7 or later, for post-quantum accounts
import {verifyCompressed} from "falcon-1024";

const emptySignatures = await peraWallet.getEmptySignatures("testnet");
const signer = algosdk
  .msgpackRawDecodeAsMap(Buffer.from(emptySignatures[accountAddress], "base64"))
  .get("pqsig")
  ?.get("pk"); // undefined for an account that isn't Falcon

// The other SIWA fields as in the resolveArc60Signer example below.
const siwa = {...siwaFields, account_address: accountAddress, type: "falcon-1024-det"};
const data = new TextEncoder().encode(JSON.stringify(siwa, Object.keys(siwa).sort()));
const sha256 = async (bytes: Uint8Array) =>
  new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
const authenticatorData = await sha256(new TextEncoder().encode(siwa.domain));

const response = await peraWallet.signArc60Data(
  {
    data: Buffer.from(data).toString("base64"),
    signer,
    domain: siwa.domain,
    authenticatorData,
    scheme: "falcon-1024-det"
  },
  {scope: ScopeType.AUTH, encoding: "base64"}
);

// The key must hash to the account (or its auth address), and the signature
// must cover sha256(data) || sha256(authenticatorData).
const {address} = algosdk.addressFromPQKey(algosdk.FALCON_1024_SCHEME, response.signer);
const signedBytes = new Uint8Array([...(await sha256(data)), ...(await sha256(authenticatorData))]);
const ok =
  address.toString() === accountAddress &&
  verifyCompressed(response.signer, response.signature, signedBytes);
PeraWalletConnect.resolveArc60Signer(accountAddress: string, network?: PeraWalletNetwork): Promise<PeraWalletArc60SignerResolution>

Resolves who has to sign an ARC-60 request for accountAddress. An ARC-60 signature is verified against the signer public key, and Pera never substitutes another key. If the account is rekeyed, its own key no longer controls it, so the request must name the on-chain auth address as signer while the SIWA payload keeps the account as account_address. Pera refuses a rekeyed account that names itself as signer, and it can only sign when the user's wallet holds the auth address as a key-bearing or Ledger account (a watch-only or multisig auth address cannot sign ARC-60).

Rekeys are per network, and the wallet checks them on the network it is currently connected to, which connect cannot observe:

  • With chainId: 416001 or 416002 the wallet only serves the session while it is on that network, so network can be omitted; passing a different one throws SIGN_DATA_NETWORK_MISMATCH. This is the recommended setup.
  • With an all-networks session (4160, the default) network is required (SIGN_DATA_NETWORK_REQUIRED otherwise) and must be the network the user's wallet is on; if it is not, the wallet rejects the sign-in.
  • Betanet sessions (416003) and values other than "mainnet" / "testnet" throw SIGN_DATA_NETWORK_UNSUPPORTED.

Returns: {accountAddress, signerAddress, signer, isRekeyed, network}. signer is signerAddress as a public key, ready for PeraWalletArc60SignData.signer.

It resolves ed25519 signers only. A Falcon account's key can't be read from its address, so see Falcon-1024 accounts instead.

Throws: SIGN_DATA_INVALID_ADDRESS for a malformed address, the network errors above, and SIGN_DATA_AUTH_ADDR_LOOKUP_FAILED when the account lookup fails (the cause is in error.data.detail), instead of assuming the account is not rekeyed.

The auth address is read from a public node by default. Use the algod option to read it from your own node instead — recommended for anything beyond light traffic.

import {PeraWalletConnect, ScopeType} from "@perawallet/connect";

// Pin the session to one network so the wallet and this lookup agree.
const peraWallet = new PeraWalletConnect({chainId: 416002});
const [accountAddress] = await peraWallet.connect();

const {signer, signerAddress} = await peraWallet.resolveArc60Signer(accountAddress);

const siwa = {
  account_address: accountAddress, // the account being authenticated
  chain_id: "283",
  domain: window.location.host,
  nonce: crypto.randomUUID(),
  type: "ed25519",
  uri: window.location.origin,
  version: "1"
};

const response = await peraWallet.signArc60Data(
  {
    data: Buffer.from(JSON.stringify(siwa, Object.keys(siwa).sort())).toString("base64"),
    signer, // the auth address when rekeyed, the account itself otherwise
    domain: siwa.domain,
    authenticatorData: new Uint8Array(
      await crypto.subtle.digest("SHA-256", new TextEncoder().encode(siwa.domain))
    )
  },
  {scope: ScopeType.AUTH, encoding: "base64"},
  true
);

// Send `response` and `signerAddress` to your backend. A verifier checks the
// signature against `signerAddress`, then checks on chain that `signerAddress`
// is the current auth address of `account_address`, or, when the two are equal,
// that `account_address` has no auth address (it is not rekeyed).
PeraWalletConnect.getEmptySignatures(network?: PeraNetwork): Promise<Record<string, string>>

Returns each connected account's empty signature, as defined by use-wallet: base64 of a SignedTransaction without its txn field. It tells you the account type (ed25519, multisig, logic sig or post-quantum) and lets you simulate with correctly shaped signatures. Accounts missing from the result are unknown.

const accounts = await peraWallet.connect();
const emptySignatures = await peraWallet.getEmptySignatures("testnet");
// {"<address>": "<base64>"}
  • Pera Mobile and the Pera extension answer it without prompting the user. Pera Web, and extension versions that don't support it yet, resolve {}.
  • Call it right after a fresh connect(), not after reconnectSession(): a value must come from the wallet just now.
  • network ("mainnet", "testnet" or "betanet") defaults to the network your chainId option pins. With the default chainId (4160, any network), Pera Mobile needs it; the extension answers for the network it is on.
  • If Pera Mobile doesn't answer within 30 seconds it rejects with EMPTY_SIGNATURES_TIMEOUT. That happens with Pera versions that don't support it yet, or when iOS suspends Pera after the user returns to the browser. Treat it as "unknown" and don't block your UI on it.

It rejects with these error.data.type values:

error.data.type when
SESSION_DISCONNECTED no Pera session is connected
EMPTY_SIGNATURES_NETWORK_REQUIRED Pera Mobile only: network is missing and the session allows any network (chainId 4160)
EMPTY_SIGNATURES_NETWORK_UNSUPPORTED network isn't "mainnet", "testnet" or "betanet"
EMPTY_SIGNATURES_NETWORK_MISMATCH extension only: the wallet is on a different network than network
EMPTY_SIGNATURES_TIMEOUT Pera didn't answer within 30 seconds
EMPTY_SIGNATURES error.data.detail.reason is "wallet-error", "session-changed" or "invalid-result"

Customizing Style

You can override the z-index using the .pera-wallet-modal class so that the modal does not conflict with another component on your application.

.pera-wallet-modal {
  // The default value of z-index is 10. You can lower and raise it as much as you want.
  z-index: 11;
}

Your app name on Pera Wallet

By default, the connect wallet drawer on Pera Wallet gets the app name from document.title.

In some cases, you may want to customize it. You can achieve this by adding a meta tag to your HTML between the head tag.

<meta name="name" content="My dApp" />

WalletConnect

@perawallet/connect talks to the Pera mobile app over WalletConnect v1, through @perawallet/walletconnect, a v1 client that Pera maintains. WalletConnect v2 is not supported, and the Pera mobile app only accepts v1 pairing URIs. Sessions run through Pera-hosted bridge servers, so no WalletConnect Cloud project ID is needed.

Browser extension (window.pera)

The Pera browser extension injects a provider at window.pera on every https page (and http://localhost) before any page script runs. When it is present, @perawallet/connect uses it directly instead of WalletConnect: the connect modal lists "Connect with Pera Extension" first and pre-selects it, and connect(), reconnectSession(), signTransaction(), signData(), signArc60Data() and disconnect() all go through the provider. Pages without the extension behave exactly as before, and users can still pick the QR code or Pera Web options from the same modal.

The SDK detects the provider synchronously when it is constructed, so no probe or handshake delays the connect modal. Call peraWallet.isExtensionAvailable() if you want to know yourself, or getPeraProvider() to reach the provider directly. Pass shouldPreferExtension: false to keep the extension out of the modal.

connect() needs a user gesture

The extension only opens its approval window for a first-time connection while the page has transient user activation (a click or key press within the last few seconds). The SDK's extension button calls window.pera.connect() synchronously from its click handler, so the normal flow works out of the box. If you build your own UI on top of window.pera, call connect() directly inside your click handler, before any await; a connect without activation fails with code -32001 and no approval window.

Once your origin is approved, reconnectSession() resolves silently with the wallet's current accounts on every page load. If the user has revoked the site, it resolves with [] just like any other missing session. Any other failure — a wallet on a network your chainId does not allow, or the extension's service worker restarting mid page-load — rejects with the cause at error.data.type and leaves the approval in place, so a later reload picks the session back up.

Networks

With a specific chainId (416001, 416002, 416003) the SDK asks the extension for that network, and connect() rejects with CONNECT_NETWORK_MISMATCH when the wallet is on a different one. With the default 4160 the wallet connects on whatever network it is on. Subscribe to peraWallet.on("networkChanged", ...) to follow later switches.

Errors

Provider errors are mapped onto the SDK's PeraWalletConnectError types:

extension code meaning error.data.type
-32002 user rejected CONNECT_CANCELLED, SIGN_TXN_CANCELLED, SIGN_DATA_CANCELLED
-32003 network mismatch CONNECT_NETWORK_MISMATCH, SIGN_TXN_NETWORK_MISMATCH, SIGN_DATA_NETWORK_MISMATCH
-32001 not connected / no user gesture SESSION_CONNECT on connect, SESSION_DISCONNECTED when signing, [] from reconnect
-32004 the wallet did not answer in time MESSAGE_NOT_RECEIVED

The wallet applies its own approval timeout (about five minutes); the SDK adds none, so users can take their time. The original provider error is attached as error.data.detail.

Using window.pera directly

The provider's types are exported for dApps that talk to it themselves:

import {getPeraProvider, PERA_PROVIDER_ERROR_CODES, type PeraProvider} from "@perawallet/connect";

const pera = getPeraProvider(); // PeraProvider | null, also typed as window.pera

Contributing

All contributions are welcomed! To get more information about the details, please read the contribution guide first.

Version History

Version Released License
1.9.0 ISC
1.8.0 ISC
1.7.0 ISC
1.6.1 ISC
1.6.0 ISC
Pre-releases (11)
Version Released License
1.8.0-beta.0 ISC
1.7.0-beta.2 ISC
1.7.0-beta.1 ISC
1.6.1-beta.0 ISC
1.6.0-beta.7 ISC