// Network selection, chain reads and the gasless call path. import { NETWORKS } from './lib/config.js'; import { TGAS, accessKeyNonce, b64encode, delegateHash, encodeSignedDelegate, rpc, viewCall, } from './lib/near.js'; export const isTest = location.pathname === '/test' || location.pathname.startsWith('/test/'); export const NET = NETWORKS[isTest ? 'testnet' : 'mainnet']; export const base = isTest ? '/test' : ''; export const view = (method, args = {}) => viewCall(NET.rpc, NET.contract, method, args); function netParam(path) { return `${path}${path.includes('?') ? '&' : '?'}net=${NET.id}`; } export async function apiGet(path) { const res = await fetch(netParam(path)); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || 'The service is not responding'); return data; } export async function apiPost(path, body) { const res = await fetch(netParam(path), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || 'The service is not responding'); return data; } /** Make sure a new account exists on-chain before its first signature. */ export async function ensureAccount(signer) { const ak = await accessKeyNonce(NET.rpc, signer.account, signer.publicKeyString).catch(() => null); if (ak) return ak; await apiPost('/api/onboard', { publicKey: signer.publicKeyString }); for (let i = 0; i < 12; i++) { const found = await accessKeyNonce(NET.rpc, signer.account, signer.publicKeyString).catch(() => null); if (found) return found; await new Promise((r) => setTimeout(r, 700)); } throw new Error('The new account has not appeared yet. Please try again.'); } /** * Sign a contract call and hand it to the relayer, which pays the gas. * The merchant holds no NEAR at any point. */ export async function gasless(signer, method, args, { deposit = 0n, tgas = 30n } = {}) { const ak = await ensureAccount(signer); const delegate = { senderId: signer.account, receiverId: NET.contract, actions: [{ type: 'FunctionCall', method, args: new TextEncoder().encode(JSON.stringify(args)), gas: tgas * TGAS, deposit }], nonce: ak.nonce + 1n, maxBlockHeight: ak.blockHeight + 120n, publicKey: signer.publicKey, }; const signature = await signer.sign(await delegateHash(delegate)); const res = await apiPost('/api/relay', { signedDelegate: b64encode(encodeSignedDelegate(delegate, signature)) }); if (!res.ok) throw new Error(res.error || 'The network refused this action'); return res; } /** Sign a short statement that proves this browser holds the account key. */ export async function proof(signer, purpose) { const at = Date.now(); const sig = await signer.sign(new TextEncoder().encode(`duly:${purpose}:${signer.account}:${at}`)); return { account: signer.account, publicKey: signer.publicKeyString, signature: b64encode(sig), at }; } /** False if the deployed contract code is not the code this site was built for. */ export async function contractVerified() { if (!NET.codeHash) return isTest; const a = await rpc(NET.rpc, 'query', { request_type: 'view_account', finality: 'final', account_id: NET.contract }).catch(() => null); return a?.code_hash === NET.codeHash; } export const txUrl = (hash) => `${NET.explorer}/txns/${hash}`; export const accountUrl = (a) => `${NET.explorer}/address/${a}`; export { rpc };