@fastnear/wallet-adapter
Version:
Wallet adapter implementations for Meteor Wallet and Near Mobile
513 lines • 17.8 kB
JavaScript
/* ⋈ 🏃🏻💨 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 { ed25519 } from "@noble/curves/ed25519.js";
import { secp256k1 } from "@noble/curves/secp256k1.js";
import {
base64ToBytes,
curveFromKey,
keyFromString,
privateKeyFromRandom,
publicKeyFromPrivate,
sha256
} from "@fastnear/utils";
import { createRpcFactory } from "./rpc.js";
import { TransportError, UserRejectedError } from "./errors.js";
import { createDefaultStorage, readJson, writeJson } from "./storage.js";
import { defaultPollingOptions, visibilityAwarePoll } from "./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 TransportError("INVALID_NETWORK", `Unsupported network: ${network}`);
}
return network;
}, "ensureNetwork");
const normalizeError = /* @__PURE__ */ __name((error, fallbackCode, fallbackMessage) => {
if (error instanceof TransportError || error instanceof UserRejectedError) return error;
if (error instanceof Error) return new TransportError(fallbackCode, error.message, { cause: error });
return new 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 = borshSerialize(signMessagePayloadSchema, {
tag: NEP413_TAG,
message,
nonce: Uint8Array.from(nonce),
recipient,
callbackUrl: callbackUrl ?? null
});
const hash = sha256(new Uint8Array(borshPayload));
const pk = keyFromString(publicKey);
const sig = base64ToBytes(signature);
if (curveFromKey(publicKey) === "secp256k1") {
const compactSig = sig.slice(0, 64);
const fullPk = new Uint8Array(65);
fullPk[0] = 4;
fullPk.set(pk, 1);
return secp256k1.verify(compactSig, hash, fullPk, { prehash: false });
}
return 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 readJson(this.storage, this.key, this.defaultState());
}
async set(state) {
await 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 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 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 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 TransportError("API_TIMEOUT", "Near Mobile backend request timed out", { cause: error });
}
if (error instanceof TransportError) throw error;
throw new 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 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 ?? 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 = createRpcFactory(options.getNetworkProviders);
const 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 visibilityAwarePoll(
() => api.getRequestStatus(id),
({ status }) => status === "pending",
polling
);
}, "awaitRequestStatus");
const awaitMessageStatus = /* @__PURE__ */ __name(async (id) => {
return 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 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 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: publicKeyFromPrivate(privateKey)
});
}
return accounts;
}, "getAccounts");
const signIn = /* @__PURE__ */ __name(async ({ network, contractId, methodNames = [], allowance }) => {
const net = ensureNetwork(network);
const privateKey = privateKeyFromRandom();
const publicKey = 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 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 = 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 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 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 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");
export {
createNearMobileAdapter
};
//# sourceMappingURL=near-mobile.js.map