UNPKG

@fastnear/wallet-adapter

Version:

Wallet adapter implementations for Meteor Wallet and Near Mobile

529 lines 19.2 kB
/* ⋈ 🏃🏻💨 FastNear Wallet Adapters - CJS (@fastnear/wallet-adapter version 1.2.0) */ /* https://www.npmjs.com/package/@fastnear/wallet-adapter/v/1.2.0 */ "use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var near_mobile_exports = {}; __export(near_mobile_exports, { createNearMobileAdapter: () => createNearMobileAdapter }); module.exports = __toCommonJS(near_mobile_exports); var import_borsh = require("@fastnear/borsh"); var import_ed25519 = require("@noble/curves/ed25519.js"); var import_secp256k1 = require("@noble/curves/secp256k1.js"); var import_utils = require("@fastnear/utils"); var import_rpc = require("./rpc.js"); var import_errors = require("./errors.js"); var import_storage = require("./storage.js"); var import_polling = require("./polling.js"); const DEFAULT_SIGNER_BACKEND_URL = "https://near-mobile-signer-backend_production.peersyst.tech"; const DEFAULT_NEAR_MOBILE_WALLET_URL = "near-mobile-wallet://sign"; const SESSION_KEY = "session"; const NEP413_TAG = 2147484061; const ensureNetwork = /* @__PURE__ */ __name((network) => { if (network !== "mainnet" && network !== "testnet") { throw new import_errors.TransportError("INVALID_NETWORK", `Unsupported network: ${network}`); } return network; }, "ensureNetwork"); const normalizeError = /* @__PURE__ */ __name((error, fallbackCode, fallbackMessage) => { if (error instanceof import_errors.TransportError || error instanceof import_errors.UserRejectedError) return error; if (error instanceof Error) return new import_errors.TransportError(fallbackCode, error.message, { cause: error }); return new import_errors.TransportError(fallbackCode, fallbackMessage, { details: error }); }, "normalizeError"); const signMessagePayloadSchema = { struct: { tag: "u32", message: "string", nonce: { array: { type: "u8", len: 32 } }, recipient: "string", callbackUrl: { option: "string" } } }; const verifyNep413Signature = /* @__PURE__ */ __name(({ publicKey, signature, message, nonce, recipient, callbackUrl }) => { const borshPayload = (0, import_borsh.serialize)(signMessagePayloadSchema, { tag: NEP413_TAG, message, nonce: Uint8Array.from(nonce), recipient, callbackUrl: callbackUrl ?? null }); const hash = (0, import_utils.sha256)(new Uint8Array(borshPayload)); const pk = (0, import_utils.keyFromString)(publicKey); const sig = (0, import_utils.base64ToBytes)(signature); if ((0, import_utils.curveFromKey)(publicKey) === "secp256k1") { const compactSig = sig.slice(0, 64); const fullPk = new Uint8Array(65); fullPk[0] = 4; fullPk.set(pk, 1); return import_secp256k1.secp256k1.verify(compactSig, hash, fullPk, { prehash: false }); } return import_ed25519.ed25519.verify(sig, hash, pk); }, "verifyNep413Signature"); class SessionRepository { static { __name(this, "SessionRepository"); } storage; key; constructor(storage, key = SESSION_KEY) { this.storage = storage; this.key = key; } defaultState() { return { mainnet: { activeAccount: null, accounts: {} }, testnet: { activeAccount: null, accounts: {} } }; } async get() { return (0, import_storage.readJson)(this.storage, this.key, this.defaultState()); } async set(state) { await (0, import_storage.writeJson)(this.storage, this.key, state); } async getKey(network, accountId) { const state = await this.get(); const key = state[network]?.accounts[accountId]; if (key == null) { throw new import_errors.TransportError("ACCOUNT_KEY_NOT_FOUND", "Account key not found in session storage"); } return key; } async setKey(network, accountId, privateKey) { const state = await this.get(); state[network].accounts[accountId] = privateKey; await this.set(state); } async removeKey(network, accountId) { const state = await this.get(); if (state[network].activeAccount === accountId) { state[network].activeAccount = null; } delete state[network].accounts[accountId]; await this.set(state); } async getActiveAccount(network) { const state = await this.get(); return state[network].activeAccount ?? null; } async setActiveAccount(network, accountId) { const state = await this.get(); const exists = Object.prototype.hasOwnProperty.call(state[network].accounts, accountId); if (!exists) { throw new import_errors.TransportError("INVALID_ACCOUNT_ID", "Cannot set active account that does not exist in session storage"); } state[network].activeAccount = accountId; await this.set(state); } async getAccounts(network) { const state = await this.get(); return Object.keys(state[network].accounts); } } class NearMobileApiClient { static { __name(this, "NearMobileApiClient"); } backendUrl; fetcher; constructor(backendUrl, fetcher) { this.backendUrl = backendUrl.replace(/\/$/, ""); this.fetcher = fetcher ?? fetch; } async request(path, init = {}, timeoutMs = 3e4) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const response = await this.fetcher(`${this.backendUrl}${path}`, { ...init, headers: { "Content-Type": "application/json", ...init.headers ?? {} }, signal: controller.signal }); if (!response.ok) { const text = await response.text().catch(() => "Unknown API error"); throw new import_errors.TransportError("API_HTTP_ERROR", `Near Mobile backend request failed (${response.status}): ${text}`); } if (response.status === 204) { return void 0; } return await response.json(); } catch (error) { if (controller.signal.aborted) { throw new import_errors.TransportError("API_TIMEOUT", "Near Mobile backend request timed out", { cause: error }); } if (error instanceof import_errors.TransportError) throw error; throw new import_errors.TransportError("API_NETWORK_ERROR", "Near Mobile backend request failed", { cause: error }); } finally { clearTimeout(timeout); } } async createRequest(network, transactions, metadata) { return this.request("/api/signer-request", { method: "POST", body: JSON.stringify({ network, transactions, dAppMetadata: metadata }) }); } async getRequestStatus(id) { return this.request(`/api/signer-request/${id}/status`, { method: "GET" }); } async getRequest(id) { return this.request(`/api/signer-request/${id}`, { method: "GET" }); } async rejectRequest(id) { await this.request(`/api/signer-request/${id}/reject`, { method: "POST" }); } async createSignMessageRequest(network, message, receiver, nonce, callbackUrl, metadata) { return this.request("/api/signer-request/message", { method: "POST", body: JSON.stringify({ network, message, receiver, nonce, callbackUrl, receiverMetadata: metadata }) }); } async getSignMessageRequest(id) { return this.request(`/api/signer-request/message/${id}`, { method: "GET" }); } async rejectSignMessageRequest(id) { await this.request(`/api/signer-request/message/${id}/reject`, { method: "POST" }); } } const normalizeTransactions = /* @__PURE__ */ __name((signerId, transactions) => { return transactions.map((tx) => { const useSigner = tx.signerId ?? signerId; if (useSigner == null) throw new import_errors.TransportError("MISSING_SIGNER_ID", "Missing signer id for transaction"); return { signerId: useSigner, receiverId: tx.receiverId, actions: tx.actions }; }); }, "normalizeTransactions"); const createNearMobileAdapter = /* @__PURE__ */ __name((options = {}) => { const storage = options.storage ?? (0, import_storage.createDefaultStorage)(); const session = new SessionRepository(storage); const backendUrl = options.signerBackendUrl ?? DEFAULT_SIGNER_BACKEND_URL; const nearMobileWalletUrl = options.nearMobileWalletUrl ?? DEFAULT_NEAR_MOBILE_WALLET_URL; const api = new NearMobileApiClient(backendUrl, options.fetcher); const rpcForNetwork = (0, import_rpc.createRpcFactory)(options.getNetworkProviders); const polling = { ...import_polling.defaultPollingOptions, ...options.polling ?? {} }; const emitError = /* @__PURE__ */ __name((error) => options.onError?.(error), "emitError"); const emitRequested = /* @__PURE__ */ __name((payload) => { options.onRequested?.({ ...payload, requestUrl: `${nearMobileWalletUrl}/${payload.kind}/${payload.id}` }); }, "emitRequested"); const awaitRequestStatus = /* @__PURE__ */ __name(async (id) => { return (0, import_polling.visibilityAwarePoll)( () => api.getRequestStatus(id), ({ status }) => status === "pending", polling ); }, "awaitRequestStatus"); const awaitMessageStatus = /* @__PURE__ */ __name(async (id) => { return (0, import_polling.visibilityAwarePoll)( () => api.getSignMessageRequest(id), ({ status, response }) => status === "pending" && response == null, polling ); }, "awaitMessageStatus"); const handleRejectedStatus = /* @__PURE__ */ __name((status, message) => { if (status === "approved") { options.onApproved?.(); return; } if (status === "rejected") { throw new import_errors.UserRejectedError("USER_REJECTED", message); } }, "handleRejectedStatus"); const ensureFullAccessKey = /* @__PURE__ */ __name(async (network, accountId, publicKey) => { const rpc = rpcForNetwork(network); const accessKey = await rpc.query({ request_type: "view_access_key", finality: "optimistic", account_id: accountId, public_key: publicKey }); if (accessKey?.permission !== "FullAccess") { throw new import_errors.TransportError("INVALID_ACCESS_KEY", "Signer key is not a full access key"); } }, "ensureFullAccessKey"); const getAccounts = /* @__PURE__ */ __name(async (network) => { const net = ensureNetwork(network); const accountIds = await session.getAccounts(net); const accounts = []; for (const accountId of accountIds) { const privateKey = await session.getKey(net, accountId); accounts.push({ accountId, publicKey: (0, import_utils.publicKeyFromPrivate)(privateKey) }); } return accounts; }, "getAccounts"); const signIn = /* @__PURE__ */ __name(async ({ network, contractId, methodNames = [], allowance }) => { const net = ensureNetwork(network); const privateKey = (0, import_utils.privateKeyFromRandom)(); const publicKey = (0, import_utils.publicKeyFromPrivate)(privateKey); const permission = contractId != null ? { receiverId: contractId, methodNames, ...allowance ? { allowance } : {} } : "FullAccess"; const { id, network: responseNetwork, requests } = await api.createRequest( net, [ { actions: [ { type: "AddKey", params: { publicKey, accessKey: { permission } } } ] } ], options.metadata ); emitRequested({ id, kind: "request", network: responseNetwork, request: requests, close: /* @__PURE__ */ __name(async () => api.rejectRequest(id), "close") }); const { status } = await awaitRequestStatus(id); handleRejectedStatus(status, "User rejected Near Mobile sign-in"); const request = await api.getRequest(id); if (request.signerAccountId == null) { throw new import_errors.TransportError("REQUEST_NOT_SIGNED", "Signer request was approved but did not return signer account id"); } await session.setKey(net, request.signerAccountId, privateKey); await session.setActiveAccount(net, request.signerAccountId); options.onSuccess?.(); return getAccounts(net); }, "signIn"); const signOut = /* @__PURE__ */ __name(async ({ network }) => { const net = ensureNetwork(network); const activeAccount = await session.getActiveAccount(net); if (activeAccount == null) return; const privateKey = await session.getKey(net, activeAccount); const publicKey = (0, import_utils.publicKeyFromPrivate)(privateKey); const { id, network: responseNetwork, requests } = await api.createRequest( net, [ { signerId: activeAccount, receiverId: activeAccount, actions: [ { type: "DeleteKey", params: { publicKey } } ] } ], options.metadata ); emitRequested({ id, kind: "request", network: responseNetwork, request: requests, close: /* @__PURE__ */ __name(async () => api.rejectRequest(id), "close") }); const { status } = await awaitRequestStatus(id); handleRejectedStatus(status, "User rejected Near Mobile sign-out"); await session.removeKey(net, activeAccount); options.onSuccess?.(); }, "signOut"); const signAndSendTransactions = /* @__PURE__ */ __name(async ({ network, signerId, transactions }) => { const net = ensureNetwork(network); const activeAccount = signerId ?? await session.getActiveAccount(net) ?? void 0; const normalizedTransactions = normalizeTransactions(activeAccount, transactions); const { id, network: responseNetwork, requests } = await api.createRequest(net, normalizedTransactions, options.metadata); emitRequested({ id, kind: "request", network: responseNetwork, request: requests, close: /* @__PURE__ */ __name(async () => api.rejectRequest(id), "close") }); const { status } = await awaitRequestStatus(id); handleRejectedStatus(status, "User rejected Near Mobile transaction signing"); const request = await api.getRequest(id); if (!request.txHash || request.txHash.length === 0) { throw new import_errors.TransportError("REQUEST_NOT_SIGNED", "Near Mobile request did not include transaction hashes"); } const requestSigner = request.signerAccountId ?? normalizedTransactions[0].signerId; const rpc = rpcForNetwork(net); const outcomes = []; for (const hash of request.txHash) { outcomes.push(await rpc.txStatus(hash, requestSigner, "EXECUTED_OPTIMISTIC")); } options.onSuccess?.(); return outcomes; }, "signAndSendTransactions"); const signAndSendTransaction = /* @__PURE__ */ __name(async ({ network, signerId, receiverId, actions }) => { const outcomes = await signAndSendTransactions({ network, signerId, transactions: [{ receiverId, actions, signerId }] }); return outcomes[0]; }, "signAndSendTransaction"); const signMessage = /* @__PURE__ */ __name(async ({ network, message, nonce, recipient, callbackUrl }) => { const net = ensureNetwork(network); const { id, network: responseNetwork } = await api.createSignMessageRequest( net, message, recipient, Array.from(nonce), callbackUrl, options.metadata ); emitRequested({ id, kind: "message", network: responseNetwork, request: { message, nonce, recipient, callbackUrl }, close: /* @__PURE__ */ __name(async () => api.rejectSignMessageRequest(id), "close") }); const result = await awaitMessageStatus(id); handleRejectedStatus(result.status, "User rejected Near Mobile message signing"); if (result.response == null) { throw new import_errors.TransportError("NO_SIGNATURE", "Near Mobile message request was approved without a signature"); } const { accountId, publicKey, signature } = result.response; const isValidSignature = verifyNep413Signature({ publicKey, signature, message, nonce, recipient, callbackUrl }); if (!isValidSignature) { throw new import_errors.TransportError("INVALID_SIGNATURE", "Near Mobile returned an invalid message signature"); } await ensureFullAccessKey(net, accountId, publicKey); options.onSuccess?.(); return { accountId, publicKey, signature }; }, "signMessage"); return { async signIn(params) { try { return await signIn(params); } catch (error) { const normalized = normalizeError(error, "SIGN_IN_FAILED", "Near Mobile sign-in failed"); emitError(normalized); throw normalized; } }, async signOut({ network }) { try { return await signOut({ network }); } catch (error) { const normalized = normalizeError(error, "SIGN_OUT_FAILED", "Near Mobile sign-out failed"); emitError(normalized); throw normalized; } }, async getAccounts({ network }) { try { return await getAccounts(network); } catch (error) { const normalized = normalizeError(error, "GET_ACCOUNTS_FAILED", "Near Mobile getAccounts failed"); emitError(normalized); throw normalized; } }, async signMessage(params) { try { return await signMessage(params); } catch (error) { const normalized = normalizeError(error, "SIGN_MESSAGE_FAILED", "Near Mobile signMessage failed"); emitError(normalized); throw normalized; } }, async signAndSendTransaction(params) { try { return await signAndSendTransaction(params); } catch (error) { const normalized = normalizeError(error, "SIGN_TX_FAILED", "Near Mobile signAndSendTransaction failed"); emitError(normalized); throw normalized; } }, async signAndSendTransactions(params) { try { return await signAndSendTransactions(params); } catch (error) { const normalized = normalizeError(error, "SIGN_TXS_FAILED", "Near Mobile signAndSendTransactions failed"); emitError(normalized); throw normalized; } } }; }, "createNearMobileAdapter"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { createNearMobileAdapter }); //# sourceMappingURL=near-mobile.cjs.map