UNPKG

windkit

Version:

A protocol for connecting Vexanium DApps to the Wind wallet, enabling secure communication and transaction signing.

199 lines (179 loc) 6.23 kB
import { Base64u, IdentityProof, SigningRequest } from "@wharfkit/signing-request"; import { Int64, PermissionLevel, Serializer } from "@wharfkit/antelope"; import { Peer } from "peerjs"; import zlib from "pako"; import { WalletSession } from "./WalletSession.js"; /** * Handles communication with the signaling server * and manages secure DApp ↔ Wallet sessions. */ export class WindConnector { #peer; #peerOptions = {}; #peerId; #listeners = new Map(); #session = new Map(); #identityArgs = {}; constructor() { this.#peerOptions.config = { iceServers: [ { urls: "stun:stun.l.google.com:19302" }, { urls: "stun:stun1.l.google.com:3478" }, { urls: "stun:stun.relay.metered.ca:80" }, { urls: "turn:asia.relay.metered.ca:80", username: "b66cd40a117bddb5cde924ab", credential: "4jRmuTehVCZ2a/S+" } ], sdpSemantics: "unified-plan" }; this.#identityArgs.scope = "vexanium"; this.#identityArgs.chainId = WalletSession.ChainID; this.#loadSession(); } /** * Adds an additional STUN or TURN server to the peer connection. * @param {RTCIceServer} server */ addIceServer(server) { this.#peerOptions.config.iceServers.push(server); } /** * Sets the signaling server address and port. * @param {string} host * @param {number} [port] */ setServer(host, port) { this.#peerOptions.host = host; if (port) this.#peerOptions.port = port; } /** * Registers a listener for specific peer events. * Supported events: `open`, `close`, `disconnected`, `error`, `session` * @param {string} event * @param {Function} func */ on(event, func) { this.#listeners.set(event, func); } /** * Connects to the signaling server. * The wallet's response can be captured using the `session` event listener. * * @see on * @see createLoginRequest */ async connect() { if (!this.#peerId) throw new Error("Peer ID is not set"); this.#peer = new Peer(this.#peerId, this.#peerOptions); this.#peer.on("connection", this.#onConnection.bind(this)); this.#listeners.forEach((func, key) => { this.#peer.on(key, func); }); } disconnect() { this.#peer.disconnect(); } destroy() { this.#peer.destroy(); } reconnect() { this.#peer.reconnect(); } isDisconnected() { return this.#peer.disconnected; } isDestroyed() { return this.#peer.destroyed; } /** * Creates a VSR (Vexanium Signing Request) for login. * @param {string} name - Application name * @param {string} icon - Application icon URL * @returns {string} Encoded VSR string for use in QR codes or query URLs */ createLoginRequest(name, icon) { const session = this.#getLastSession(); if (session) { const [actor, perm] = session.permission.split("@"); this.#identityArgs.account = actor; this.#identityArgs.permission = perm; this.#peerId = session.peerId; } else { // Generate a new peer ID this.#peerId = `VEX-${window.crypto.randomUUID()}`; } let req = SigningRequest.identity(this.#identityArgs, { zlib }); req.setInfoKey("pi", this.#peerId); req.setInfoKey("na", name); req.setInfoKey("ic", icon); req.setInfoKey("do", window.location.origin); if (session) { req.setInfoKey("exp", Int64.from(session.exp)); req.setInfoKey("sig", session.signature); } return req.encode(true, false, "vsr:"); } #getLastSession() { const domain = window.location.origin; let current = this.#session.get(domain); if (current && current.exp < Date.now()) { // Expired this.#session.delete(domain); current = null; } return current; } #addSession(permission, exp, signature) { const domain = window.location.origin; let current = this.#session.get(domain); if (current) { current.permission = permission; current.exp = exp; current.signature = signature; } else { current = { permission, exp, signature, domain, peerId: this.#peerId }; } this.#session.set(domain, current); } #saveSession() { const data = Array.from(this.#session.values()); sessionStorage.setItem("session", JSON.stringify(data)); } #loadSession() { const raw = sessionStorage.getItem("session"); if (raw) { let data = JSON.parse(raw); data.forEach(it => { this.#session.set(it.domain, it); }); } } /** * Handles incoming peer connections from the wallet. * @param {DataConnection} conn * @private */ #onConnection(conn) { conn.once("data", payload => { if (payload.code === 'LOGIN_OK') { const auth = Base64u.decode(payload.result.auth); const proof = Serializer.decode({ data: auth, type: IdentityProof }); const session = new WalletSession(conn); session.permissionLevel = proof.signer; // Store session this.#addSession(proof.signer.toString(), payload.result.exp, payload.result.signature); this.#saveSession(); const func = this.#listeners.get("session"); if (func) func(session, proof); } else if (payload.code === 'RE_LOGIN_OK') { const session = new WalletSession(conn); session.permissionLevel = PermissionLevel.from(payload.result.permission); const func = this.#listeners.get("session"); if (func) func(session); // Only one parameter for re-login } }); } }