@fastnear/wallet-adapter
Version:
Wallet adapter implementations for Meteor Wallet and Near Mobile
529 lines • 19.2 kB
JavaScript
/* ⋈ 🏃🏻💨 FastNear Wallet Adapters - CJS (@fastnear/wallet-adapter version 1.2.0) */
/* https://www.npmjs.com/package/@fastnear/wallet-adapter/v/1.2.0 */
;
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