duly / web/api.js · raw
// 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 };