duly / web/lib/near.js · raw
// Minimal NEAR wire format shared by the browser and the Worker.// Only what Duly needs: base58, borsh for transactions and NEP-366 delegate// actions, and a strict decoder the relayer uses on untrusted bytes.const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';const INDEX = Object.fromEntries([...ALPHABET].map((c, i) => [c, i]));export function b58encode(bytes) {  let n = 0n;  for (const b of bytes) n = (n << 8n) | BigInt(b);  let out = '';  while (n > 0n) {    out = ALPHABET[Number(n % 58n)] + out;    n /= 58n;  }  for (const b of bytes) {    if (b !== 0) break;    out = '1' + out;  }  return out;}export function b58decode(s) {  let n = 0n;  for (const c of s) {    if (!(c in INDEX)) throw new Error('Bad base58');    n = n * 58n + BigInt(INDEX[c]);  }  const out = [];  while (n > 0n) {    out.unshift(Number(n & 255n));    n >>= 8n;  }  for (const c of s) {    if (c !== '1') break;    out.unshift(0);  }  return Uint8Array.from(out);}export function hex(bytes) {  return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');}export function unhex(s) {  if (!/^([0-9a-f]{2})*$/.test(s)) throw new Error('Bad hex');  return Uint8Array.from(s.match(/../g) || [], (x) => parseInt(x, 16));}export function b64encode(bytes) {  let s = '';  for (let i = 0; i < bytes.length; i += 0x8000) {    s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));  }  return btoa(s);}export function b64decode(s) {  const bin = atob(s);  const out = new Uint8Array(bin.length);  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);  return out;}export function concat(...parts) {  const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));  let o = 0;  for (const p of parts) {    out.set(p, o);    o += p.length;  }  return out;}export async function sha256(bytes) {  return new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));}const te = new TextEncoder();const td = new TextDecoder('utf-8', { fatal: true });class Writer {  constructor() {    this.parts = [];  }  u8(v) {    this.parts.push(Uint8Array.of(v));    return this;  }  u32(v) {    const b = new Uint8Array(4);    new DataView(b.buffer).setUint32(0, v, true);    this.parts.push(b);    return this;  }  u64(v) {    const b = new Uint8Array(8);    new DataView(b.buffer).setBigUint64(0, BigInt(v), true);    this.parts.push(b);    return this;  }  u128(v) {    const n = BigInt(v);    return this.u64(n & 0xffffffffffffffffn).u64(n >> 64n);  }  fixed(bytes) {    this.parts.push(bytes);    return this;  }  bytes(bytes) {    return this.u32(bytes.length).fixed(bytes);  }  string(s) {    return this.bytes(te.encode(s));  }  done() {    return concat(...this.parts);  }}class Reader {  constructor(bytes) {    this.b = bytes;    this.dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);    this.o = 0;  }  need(n) {    if (this.o + n > this.b.length) throw new Error('Truncated');  }  u8() {    this.need(1);    return this.b[this.o++];  }  u32() {    this.need(4);    const v = this.dv.getUint32(this.o, true);    this.o += 4;    return v;  }  u64() {    this.need(8);    const v = this.dv.getBigUint64(this.o, true);    this.o += 8;    return v;  }  u128() {    const lo = this.u64();    const hi = this.u64();    return (hi << 64n) | lo;  }  fixed(n) {    this.need(n);    const v = this.b.slice(this.o, this.o + n);    this.o += n;    return v;  }  bytes(max) {    const n = this.u32();    if (n > max) throw new Error('Too long');    return this.fixed(n);  }  string(max) {    return td.decode(this.bytes(max));  }  end() {    if (this.o !== this.b.length) throw new Error('Trailing bytes');  }}const ED25519 = 0;function writePublicKey(w, pub) {  return w.u8(ED25519).fixed(pub);}function writeSignature(w, sig) {  return w.u8(ED25519).fixed(sig);}function writeAction(w, a) {  switch (a.type) {    case 'FunctionCall':      return w        .u8(2)        .string(a.method)        .bytes(a.args)        .u64(a.gas)        .u128(a.deposit);    case 'Transfer':      return w.u8(3).u128(a.deposit);    case 'CreateAccount':      return w.u8(0);    case 'DeployContract':      return w.u8(1).bytes(a.code);    case 'AddFunctionCallKey': {      writePublicKey(w.u8(5), a.publicKey).u64(0).u8(0).u8(1).u128(a.allowance).string(a.receiverId);      w.u32(a.methods.length);      for (const m of a.methods) w.string(m);      return w;    }    case 'AddFullAccessKey':      return writePublicKey(w.u8(5), a.publicKey).u64(0).u8(1);    case 'DeleteKey':      return writePublicKey(w.u8(6), a.publicKey);    case 'Delegate':      writeDelegate(w.u8(8), a.delegate);      return writeSignature(w, a.signature);    default:      throw new Error('Unsupported action');  }}function writeDelegate(w, d) {  w.string(d.senderId).string(d.receiverId).u32(d.actions.length);  for (const a of d.actions) writeAction(w, a);  w.u64(d.nonce).u64(d.maxBlockHeight);  return writePublicKey(w, d.publicKey);}export function encodeDelegate(d) {  return writeDelegate(new Writer(), d).done();}/** NEP-461 prefix (2^30 + 366) keeps a delegate signature from being a valid transaction signature. */const DELEGATE_PREFIX = new Writer().u32(1073742190).done();export async function delegateHash(d) {  return sha256(concat(DELEGATE_PREFIX, encodeDelegate(d)));}export function encodeSignedDelegate(d, signature) {  const w = new Writer();  writeDelegate(w, d);  return writeSignature(w, signature).done();}export function encodeTransaction(t) {  const w = new Writer();  w.string(t.signerId);  writePublicKey(w, t.publicKey);  w.u64(t.nonce).string(t.receiverId).fixed(t.blockHash).u32(t.actions.length);  for (const a of t.actions) writeAction(w, a);  return w.done();}export function encodeSignedTransaction(txBytes, signature) {  const w = new Writer();  w.fixed(txBytes);  return writeSignature(w, signature).done();}export const MAX_ARGS_BYTES = 2048;const MAX_NAME = 64;const MAX_METHOD = 64;/** * Strict decoder for a SignedDelegateAction from an untrusted client. It accepts * exactly one shape: one FunctionCall, ed25519 only, no trailing bytes. * Anything else throws, including nested delegates, transfers and key changes. */export function decodeSignedDelegate(bytes) {  const r = new Reader(bytes);  const senderId = r.string(MAX_NAME);  const receiverId = r.string(MAX_NAME);  const count = r.u32();  if (count !== 1) throw new Error('Exactly one action is allowed');  if (r.u8() !== 2) throw new Error('Only function calls are allowed');  const method = r.string(MAX_METHOD);  const args = r.bytes(MAX_ARGS_BYTES);  const gas = r.u64();  const deposit = r.u128();  const nonce = r.u64();  const maxBlockHeight = r.u64();  if (r.u8() !== ED25519) throw new Error('Only ed25519 keys are allowed');  const publicKey = r.fixed(32);  if (r.u8() !== ED25519) throw new Error('Only ed25519 signatures are allowed');  const signature = r.fixed(64);  r.end();  return {    delegate: {      senderId,      receiverId,      actions: [{ type: 'FunctionCall', method, args, gas, deposit }],      nonce,      maxBlockHeight,      publicKey,    },    signature,  };}export function publicKeyString(pub) {  return 'ed25519:' + b58encode(pub);}export function implicitAccount(pub) {  return hex(pub);}export function toYocto(near) {  const [whole, frac = ''] = String(near).split('.');  return BigInt(whole + frac.padEnd(24, '0').slice(0, 24));}export const TGAS = 1_000_000_000_000n;// ------------------------------------------------------------------ RPCexport class RpcError extends Error {  constructor(message, detail) {    super(message);    this.detail = detail;  }}export async function rpc(urls, method, params, { fetchImpl = fetch, timeoutMs = 20000 } = {}) {  const list = Array.isArray(urls) ? urls : [urls];  let last;  for (const url of list) {    try {      const res = await fetchImpl(url, {        method: 'POST',        headers: { 'content-type': 'application/json' },        body: JSON.stringify({ jsonrpc: '2.0', id: 'duly', method, params }),        signal: AbortSignal.timeout(timeoutMs),      });      if (!res.ok) throw new Error(`RPC HTTP ${res.status}`);      const j = await res.json();      if (j.error) throw new RpcError(j.error.message || 'RPC error', j.error);      return j.result;    } catch (e) {      last = e;      if (e instanceof RpcError && e.detail?.name !== 'INTERNAL_ERROR') throw e;    }  }  throw last;}export async function viewCall(urls, contract, method, args = {}, opts) {  const r = await rpc(    urls,    'query',    {      request_type: 'call_function',      finality: 'optimistic',      account_id: contract,      method_name: method,      args_base64: b64encode(te.encode(JSON.stringify(args))),    },    opts,  );  if (r.error) throw new RpcError(r.error);  return JSON.parse(new TextDecoder().decode(Uint8Array.from(r.result)));}export async function accessKeyNonce(urls, account, publicKey, opts) {  const r = await rpc(    urls,    'query',    {      request_type: 'view_access_key',      finality: 'optimistic',      account_id: account,      public_key: publicKey,    },    opts,  );  if (r.error) return null;  return { nonce: BigInt(r.nonce), blockHash: r.block_hash, blockHeight: BigInt(r.block_height), permission: r.permission };}export async function accountExists(urls, account, opts) {  try {    const r = await rpc(      urls,      'query',      { request_type: 'view_account', finality: 'optimistic', account_id: account },      opts,    );    return !r.error;  } catch (e) {    if (/does not exist|UNKNOWN_ACCOUNT/i.test(`${e.message} ${JSON.stringify(e.detail || {})}`)) return false;    throw e;  }}/** First failure inside a final outcome, or null when every receipt succeeded. */export function outcomeFailure(outcome) {  const all = [outcome.transaction_outcome, ...(outcome.receipts_outcome || [])];  for (const o of all) {    const s = o?.outcome?.status;    if (s && s.Failure) return s.Failure;  }  return null;}/** Plain-language text for a contract panic inside a failure object. */export function failureText(f) {  const text = JSON.stringify(f);  const m = text.match(/Smart contract panicked: ([^"\\]+)/);  return m ? m[1] : text.slice(0, 200);}/** Duly events logged by any receipt of this transaction, in order. */export function outcomeEvents(outcome, standard = 'duly') {  const out = [];  for (const r of outcome?.receipts_outcome || []) {    for (const line of r.outcome?.logs || []) {      if (!line.startsWith('EVENT_JSON:')) continue;      try {        const e = JSON.parse(line.slice('EVENT_JSON:'.length));        if (e.standard === standard) out.push({ event: e.event, data: e.data?.[0] });      } catch {        // a malformed log is ignored      }    }  }  return out;}/** JSON value the last receipt returned, if any. */export function outcomeValue(outcome) {  const v = outcome?.status?.SuccessValue;  if (!v) return null;  try {    return JSON.parse(new TextDecoder().decode(b64decode(v)));  } catch {    return null;  }}