UNPKG

@fastnear/wallet-adapter

Version:

Wallet adapter implementations for Meteor Wallet and Near Mobile

414 lines 16.6 kB
/* ⋈ 🏃🏻💨 FastNear Wallet Adapters - ESM (@fastnear/wallet-adapter version 1.2.0) */ /* https://www.npmjs.com/package/@fastnear/wallet-adapter/v/1.2.0 */ var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); import { serialize as borshSerialize } from "@fastnear/borsh"; import { privateKeyFromRandom, publicKeyFromPrivate, bytesToBase64, mapTransaction, SCHEMA } from "@fastnear/utils"; import { connectorActionsToFastnearActions } from "./actions.js"; import { createRpcFactory } from "./rpc.js"; import { TransportError, UserRejectedError } from "./errors.js"; import { createDefaultStorage, readJson, writeJson } from "./storage.js"; const METEOR_DEFAULT_WALLET_BASE = "https://wallet.meteorwallet.app"; const METEOR_CONNECTION_PING_MS = 450; const METEOR_POPUP_WIDTH = 390; const METEOR_POPUP_HEIGHT = 650; const LEGACY_AUTH_KEY_SUFFIX = "_meteor_wallet_auth_key"; const randomUid = /* @__PURE__ */ __name(() => { if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { return crypto.randomUUID(); } return `meteor-${Date.now()}-${Math.floor(Math.random() * 1e5)}`; }, "randomUid"); const popupFeatures = /* @__PURE__ */ __name(() => { if (typeof window === "undefined" || window.top == null) { return `popup=1,width=${METEOR_POPUP_WIDTH},height=${METEOR_POPUP_HEIGHT}`; } const y = window.top.outerHeight / 2 + window.top.screenY - METEOR_POPUP_HEIGHT / 2; const x = window.top.outerWidth / 2 + window.top.screenX - METEOR_POPUP_WIDTH / 2; return `popup=1,width=${METEOR_POPUP_WIDTH},height=${METEOR_POPUP_HEIGHT},top=${y},left=${x}`; }, "popupFeatures"); const isUserRejectedTag = /* @__PURE__ */ __name((tag) => { return tag === "USER_CANCELLED" || tag === "WINDOW_CLOSED" || tag === "INCOMPLETE_ACTION"; }, "isUserRejectedTag"); const mapMeteorError = /* @__PURE__ */ __name((message, endTags) => { const tags = endTags ?? []; const lastTag = tags[tags.length - 1]; if (isUserRejectedTag(lastTag)) { return new UserRejectedError(lastTag ?? "USER_REJECTED", message, { details: { endTags: tags } }); } if (lastTag === "POPUP_WINDOW_OPEN_FAILED" || lastTag === "POPUP_WINDOW_REFUSED") { return new TransportError(lastTag, message, { details: { endTags: tags } }); } return new TransportError(lastTag ?? "METEOR_ACTION_FAILED", message, { details: { endTags: tags } }); }, "mapMeteorError"); const ensureNetwork = /* @__PURE__ */ __name((network) => { if (network !== "mainnet" && network !== "testnet") { throw new TransportError("INVALID_NETWORK", `Unsupported network: ${network}`); } return network; }, "ensureNetwork"); const toMeteorTxPayload = /* @__PURE__ */ __name((tx) => { const encoded = borshSerialize(SCHEMA.Transaction, mapTransaction(tx)); return bytesToBase64(new Uint8Array(encoded)); }, "toMeteorTxPayload"); const normalizeActionError = /* @__PURE__ */ __name((error) => { if (error instanceof TransportError || error instanceof UserRejectedError) return error; if (error instanceof Error) return new TransportError("METEOR_ACTION_FAILED", error.message, { cause: error }); return new TransportError("METEOR_ACTION_FAILED", "Meteor action failed", { details: error }); }, "normalizeActionError"); const createMeteorAdapter = /* @__PURE__ */ __name((options = {}) => { const storage = options.storage ?? createDefaultStorage(); const walletBaseUrl = options.walletBaseUrl ?? METEOR_DEFAULT_WALLET_BASE; const appKeyPrefix = options.appKeyPrefix ?? "near_app"; const openWindow = options.openWindow ?? ((url, name, features) => { if (typeof window === "undefined") return null; return window.open(url, name ?? "MeteorWallet", features ?? popupFeatures()); }); const rpcForNetwork = createRpcFactory(options.getNetworkProviders); const walletOrigin = new URL(walletBaseUrl).origin; const authStorageKey = /* @__PURE__ */ __name((network) => `${appKeyPrefix}${LEGACY_AUTH_KEY_SUFFIX}:${network}`, "authStorageKey"); const legacyAuthKey = `${appKeyPrefix}${LEGACY_AUTH_KEY_SUFFIX}`; let extensionListenerAttached = false; let activeConnection = null; const loadAuth = /* @__PURE__ */ __name(async (network) => { const keyed = await readJson(storage, authStorageKey(network), { allKeys: [] }); if (keyed.accountId || (keyed.allKeys?.length ?? 0) > 0) return keyed; return readJson(storage, legacyAuthKey, { allKeys: [] }); }, "loadAuth"); const saveAuth = /* @__PURE__ */ __name(async (network, state) => { await writeJson(storage, authStorageKey(network), state); await writeJson(storage, legacyAuthKey, state); }, "saveAuth"); const clearAuth = /* @__PURE__ */ __name(async (network) => { await storage.remove(authStorageKey(network)); }, "clearAuth"); const cleanupConnection = /* @__PURE__ */ __name(() => { if (activeConnection == null) return; if (activeConnection.interval != null) clearInterval(activeConnection.interval); activeConnection.cleanupFns.forEach((fn) => fn()); activeConnection.cleanupFns = []; activeConnection.popup?.close?.(); activeConnection = null; }, "cleanupConnection"); const sendConnectionMessage = /* @__PURE__ */ __name((connection) => { const payload = { uid: connection.uid, actionType: connection.actionType, status: connection.status, network: connection.network, endTags: [] }; if (connection.status === "initializing") payload.inputs = connection.inputs; if (connection.extension != null) { connection.extension.sendMessageData(payload); return; } if (connection.popup?.postMessage == null) return; try { connection.popup.postMessage(payload, connection.walletOrigin); } catch { connection.popup.postMessage(payload); } }, "sendConnectionMessage"); const closeWithError = /* @__PURE__ */ __name((error) => { if (activeConnection == null) return; const reject = activeConnection.reject; cleanupConnection(); reject(error); }, "closeWithError"); const closeWithSuccess = /* @__PURE__ */ __name((payload) => { if (activeConnection == null) return; const resolve = activeConnection.resolve; cleanupConnection(); resolve(payload); }, "closeWithSuccess"); const handleMeteorResponse = /* @__PURE__ */ __name((raw) => { const data = raw; if (activeConnection == null) return; if (data.uid !== activeConnection.uid) return; if (data.status == null) return; if (data.status === "attempting_reconnect") { activeConnection.status = "initializing"; sendConnectionMessage(activeConnection); return; } if (data.status === "connected" && activeConnection.status === "initializing") { activeConnection.status = "connected"; return; } if (data.status === "closed_success") { closeWithSuccess(data.payload); return; } if (data.status === "closed_fail") { closeWithError(mapMeteorError(data.message ?? "Meteor action failed", data.endTags)); return; } if (data.status === "closed_window") { closeWithError( new UserRejectedError( "WINDOW_CLOSED", data.message ?? "User closed the wallet window", { details: { endTags: data.endTags ?? ["WINDOW_CLOSED"] } } ) ); return; } if (data.status === "disconnected") { closeWithError(new TransportError("DISCONNECTED", "Meteor wallet transport disconnected")); } }, "handleMeteorResponse"); const attachExtensionListenerIfNeeded = /* @__PURE__ */ __name((extension) => { if (extension == null || extensionListenerAttached) return; extension.addMessageDataListener((message) => handleMeteorResponse(message)); extensionListenerAttached = true; }, "attachExtensionListenerIfNeeded"); const connectAndWaitForResponse = /* @__PURE__ */ __name(async (network, actionType, inputs) => { if (activeConnection != null) { activeConnection.reject( new TransportError("NEW_ACTION_STARTED", "A new action was started before the previous action completed") ); cleanupConnection(); } const uid = randomUid(); const extension = options.getExtensionBridge?.(); attachExtensionListenerIfNeeded(extension); let popup; const cleanupFns = []; if (extension == null) { const url = new URL(`${walletBaseUrl}/connect/${network}/${actionType}`); url.searchParams.set("source", "wpm"); url.searchParams.set("connectionUid", uid); popup = openWindow(url.toString(), "MeteorWallet", popupFeatures()) ?? void 0; if (popup == null) { throw new TransportError("POPUP_WINDOW_OPEN_FAILED", "Couldn't open popup window to complete wallet action"); } if (popup.windowIdPromise != null) { const popupId = await popup.windowIdPromise; if (popupId == null) { throw new TransportError("POPUP_WINDOW_OPEN_FAILED", "Couldn't open popup window to complete wallet action"); } } if (typeof window !== "undefined") { const listener = /* @__PURE__ */ __name((event) => handleMeteorResponse(event.data), "listener"); window.addEventListener("message", listener); cleanupFns.push(() => window.removeEventListener("message", listener)); } } return new Promise((resolve, reject) => { const connection = { uid, network, actionType, status: "initializing", inputs, popup, extension, walletOrigin, cleanupFns, resolve, reject }; activeConnection = connection; sendConnectionMessage(connection); connection.interval = setInterval(() => { if (activeConnection == null) return; if (activeConnection.popup != null && activeConnection.popup.closed) { closeWithError( new UserRejectedError( "WINDOW_CLOSED", "User closed the wallet window before completing the action", { details: { endTags: ["INCOMPLETE_ACTION", "WINDOW_CLOSED"] } } ) ); return; } sendConnectionMessage(activeConnection); }, METEOR_CONNECTION_PING_MS); }); }, "connectAndWaitForResponse"); const findSignerPublicKey = /* @__PURE__ */ __name(async (network, accountId, preferredKeys) => { const rpc = rpcForNetwork(network); for (const key of preferredKeys) { try { await rpc.query({ request_type: "view_access_key", finality: "optimistic", account_id: accountId, public_key: key }); return key; } catch { } } const accessKeyList = await rpc.query({ request_type: "view_access_key_list", finality: "optimistic", account_id: accountId }); if (!accessKeyList.keys?.length) { throw new TransportError("NO_ACCESS_KEYS", `No access keys found for account ${accountId}`); } return accessKeyList.keys[0].public_key; }, "findSignerPublicKey"); const prepareMeteorTransactions = /* @__PURE__ */ __name(async (network, signerId, preferredKeys, transactions) => { const rpc = rpcForNetwork(network); const block = await rpc.block({ finality: "final" }); const publicKey = await findSignerPublicKey(network, signerId, preferredKeys); const accessKey = await rpc.query({ request_type: "view_access_key", finality: "optimistic", account_id: signerId, public_key: publicKey }); return transactions.map((tx, index) => ({ signerId, publicKey, nonce: BigInt(accessKey.nonce) + BigInt(index + 1), receiverId: tx.receiverId, blockHash: block.header.hash, actions: connectorActionsToFastnearActions(tx.actions) })); }, "prepareMeteorTransactions"); const getAccountsForNetwork = /* @__PURE__ */ __name(async (network) => { const auth = await loadAuth(network); if (!auth.accountId) return []; const publicKey = auth.signedInContract?.public_key ?? auth.allKeys?.[0] ?? ""; return [{ accountId: auth.accountId, publicKey }]; }, "getAccountsForNetwork"); const signIn = /* @__PURE__ */ __name(async ({ network, contractId, methodNames }) => { const net = ensureNetwork(network); const generatedKey = privateKeyFromRandom(); const generatedPublicKey = publicKeyFromPrivate(generatedKey); const inputs = { type: methodNames && methodNames.length > 0 ? "SELECTED_METHODS" : "ALL_METHODS", contract_id: contractId, methods: methodNames ?? [], public_key: generatedPublicKey }; const response = await connectAndWaitForResponse( net, "login", inputs ).catch((error) => { throw normalizeActionError(error); }); const accountId = response.accountId ?? response.account_id; if (accountId == null) { throw new TransportError("INVALID_LOGIN_RESPONSE", "Meteor login response did not contain an account id", { details: response }); } await saveAuth(net, { accountId, allKeys: response.allKeys ?? [], signedInContract: contractId ? { contract_id: contractId, public_key: generatedPublicKey } : void 0 }); return getAccountsForNetwork(net); }, "signIn"); const signOut = /* @__PURE__ */ __name(async ({ network }) => { const net = ensureNetwork(network); const auth = await loadAuth(net); if (!auth.accountId) return; if (auth.signedInContract != null) { await connectAndWaitForResponse(net, "logout", { accountId: auth.accountId, contractInfo: auth.signedInContract }).catch((error) => { throw normalizeActionError(error); }); } await clearAuth(net); await saveAuth(net, { allKeys: [] }); }, "signOut"); const verifyOwner = /* @__PURE__ */ __name(async ({ network, message, accountId }) => { const net = ensureNetwork(network); const auth = await loadAuth(net); const useAccountId = accountId ?? auth.accountId; return connectAndWaitForResponse(net, "verify_owner", { accountId: useAccountId, message }).catch((error) => { throw normalizeActionError(error); }); }, "verifyOwner"); const signMessage = /* @__PURE__ */ __name(async ({ network, message, nonce, recipient, callbackUrl, state, accountId }) => { const net = ensureNetwork(network); const auth = await loadAuth(net); const useAccountId = accountId ?? auth.accountId; const response = await connectAndWaitForResponse( net, "sign_message", { message, nonce, recipient, callbackUrl: callbackUrl ?? options.getLocation?.(), state, accountId: useAccountId } ).catch((error) => { throw normalizeActionError(error); }); return { ...response, state }; }, "signMessage"); const signAndSendTransactions = /* @__PURE__ */ __name(async ({ network, signerId, transactions }) => { const net = ensureNetwork(network); const auth = await loadAuth(net); const useSigner = signerId ?? auth.accountId; if (useSigner == null) throw new TransportError("NOT_SIGNED_IN", "Wallet is not signed in"); const prepared = await prepareMeteorTransactions(net, useSigner, auth.allKeys ?? [], transactions); const serialized = prepared.map(toMeteorTxPayload).join(","); const response = await connectAndWaitForResponse(net, "sign", { transactions: serialized }).catch((error) => { throw normalizeActionError(error); }); if (Array.isArray(response?.executionOutcomes)) { return response.executionOutcomes; } if (Array.isArray(response)) return response; return [response]; }, "signAndSendTransactions"); const signAndSendTransaction = /* @__PURE__ */ __name(async ({ network, signerId, receiverId, actions }) => { const result = await signAndSendTransactions({ network, signerId, transactions: [{ receiverId, actions }] }); return result[0]; }, "signAndSendTransaction"); return { signIn, signOut, getAccounts: /* @__PURE__ */ __name(({ network }) => getAccountsForNetwork(ensureNetwork(network)), "getAccounts"), verifyOwner, signMessage, signAndSendTransaction, signAndSendTransactions }; }, "createMeteorAdapter"); export { createMeteorAdapter }; //# sourceMappingURL=meteor.js.map