import {WalletTransport, ConnectOptions} from "../WalletTransport"; import {buildArc60WireParams, buildArc60SignDataResponse} from "../arc60Wire"; import {Arc0027Client, Arc0027RequestError} from "./arc0027Client"; import {DiscoverResult, ARC0027_ERROR_CODES} from "./arc0027Types"; import {isArc60OriginMismatch} from "./originBinding"; import PeraWalletConnectError from "../../util/PeraWalletConnectError"; import { PeraWalletArbitraryData, PeraWalletArc60SignData, PeraWalletArc60SignDataResponse, SignMetadata, PeraWalletTransaction } from "../../util/model/peraWalletModels"; import {AlgorandChainIDs} from "../../util/peraWalletTypes"; import {base64ToUint8Array} from "../../util/transaction/transactionUtils"; import { saveWalletDetailsToStorage, resetWalletDetailsFromStorage } from "../../util/storage/storageUtils"; function mapError(error: unknown, context: "connect" | "sign"): PeraWalletConnectError { if (error instanceof Arc0027RequestError) { if (error.code === ARC0027_ERROR_CODES.MethodCanceledError) { return new PeraWalletConnectError( {type: context === "connect" ? "CONNECT_MODAL_CLOSED" : "SIGN_TXN_CANCELLED"}, error.message ); } if (error.code === ARC0027_ERROR_CODES.UnauthorizedSignerError) { return new PeraWalletConnectError({type: "SESSION_RECONNECT"}, error.message); } } return new PeraWalletConnectError( {type: "SIGN_TRANSACTIONS", detail: error}, (error as Error)?.message || "ARC-0027 request failed" ); } export class ExtensionTransport implements WalletTransport { readonly platform = "extension" as const; private client: Arc0027Client; constructor(client: Arc0027Client = new Arc0027Client()) { this.client = client; } static discover( client: Arc0027Client = new Arc0027Client() ): Promise { return client.discover(); } async connect(_opts?: ConnectOptions): Promise { try { const result = await this.client.request("enable", {}); const accounts = ((result.accounts as {address: string}[]) || []).map( (item) => item.address ); saveWalletDetailsToStorage(accounts, "pera-wallet-extension"); return accounts; } catch (error) { throw mapError(error, "connect"); } } async reconnect(): Promise { // The extension is the source of truth for per-origin permission. Confirm // it is still installed; the next real call re-checks the grant (4100 → // caller disconnects). Return whatever is stored. const info = await this.client.discover(); if (!info) { await resetWalletDetailsFromStorage(); return []; } // Accounts are read from storage by the orchestrator; nothing to re-fetch. return []; } async disconnect(): Promise { try { await this.client.request("disable", {}); } catch { // Best-effort; clear local state regardless. } await resetWalletDetailsFromStorage(); } async signTransaction( txns: PeraWalletTransaction[], _signerAddress?: string ): Promise { try { const result = await this.client.request("sign_transactions", {txns}); const stxns = (result.stxns as (string | null)[]) || []; return stxns .filter((value): value is string => typeof value === "string") .map(base64ToUint8Array); } catch (error) { throw mapError(error, "sign"); } } signData( _data: PeraWalletArbitraryData[], _signer: string, _chainId: AlgorandChainIDs ): Promise { return Promise.reject( new PeraWalletConnectError( {type: "EXTENSION_UNSUPPORTED_OPERATION"}, "Arbitrary-data signing is not supported by the Pera extension; use signArc60Data, or connect via Pera mobile / Pera Web" ) ); } async signArc60Data( payload: PeraWalletArc60SignData, metadata: SignMetadata ): Promise { // Fast client-side pre-check mirroring the extension's SIWA origin binding. if (isArc60OriginMismatch(payload.domain, window.location.origin)) { throw new PeraWalletConnectError( {type: "SIGN_DATA_DOMAIN_MISMATCH"}, `ARC-60 domain "${payload.domain}" does not match the page origin "${window.location.origin}"` ); } const wireParams = buildArc60WireParams(payload, metadata); try { const result = await this.client.request("sign_message", wireParams); const signature = base64ToUint8Array(result.signature as string); return buildArc60SignDataResponse(payload, signature); } catch (error) { throw mapError(error, "sign"); } } }