Repository Public
1// NEAR adapter against two scripted JSON-RPC sources, the archival node and the transfers index (no network):2// agreement rules, refusals, fee/reserve arithmetic, tracking verdicts, broadcast outcomes and deposit re-verification.3import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';4import { sha256 } from '@noble/hashes/sha2.js';5import { base58 } from '@scure/base';6import { NETWORKS, nearFeeCeiling } from '@spw/core';7import type { Adapter } from '../src/chains/rpc.ts';89const n = NETWORKS.near;10const [A, B] = n.rpc as [string, string];11const ARCHIVAL = 'https://archival-rpc.mainnet.fastnear.com', INDEX = 'https://transfers.main.fastnear.com/v0/transfers';12const h = (label: string) => base58.encode(sha256(new TextEncoder().encode(label)));13const hex = (label: string) => Buffer.from(sha256(new TextEncoder().encode(label))).toString('hex');14const FROM = hex('from'), TO_IMPLICIT = hex('to'), TO = 'alice.near';15const FROM_KEY = 'ed25519:' + base58.encode(Buffer.from(FROM, 'hex'));16const TIP = 223_990_000, NONCE = 223_000_000_000_007;17const blockHash = (height: number) => h(`block-${height}`);18const ONE = 10n ** 24n;19/** the blocks the scripted nodes know by hash */20const HEIGHT_OF = new Map<string, number>([...Array.from({ length: 800 }, (_, k) => TIP - k), TIP - 86_400, TIP - 90_000].map((height) => [blockHash(height), height]));2122type Handler = (params: any, url: string) => unknown;23class RpcErr { constructor(public cause: string, public data: unknown = '') {} }24const unknownTx = () => { throw new RpcErr('UNKNOWN_TRANSACTION'); };25const FEES = { execution: 108059500000, send_not_sir: 108059500000, send_sir: 108059500000 };26const protocol = (over: Record<string, unknown> = {}, costs: Record<string, unknown> = {}) => ({27 transaction_validity_period: 86400, ...over,28 runtime_config: { storage_amount_per_byte: '10000000000000000000', min_gas_purchase_price: '1000000000', account_creation_charge: '7000000000000000000000', ...(over.runtime ?? {}) as object,29 transaction_costs: { action_receipt_creation_config: FEES, action_creation_config: {30 transfer_cost: { execution: 115123062500, send_not_sir: 115123062500, send_sir: 115123062500 },31 create_account_cost: { execution: 7200000000000, send_not_sir: 500000000000, send_sir: 500000000000 },32 add_key_cost: { full_access_cost: { execution: 101765125000, send_not_sir: 101765125000, send_sir: 101765125000 } }, ...costs } } },33});3435interface World { accounts: Record<string, { amount: string; locked: string; storage_usage: number } | undefined>; calls: { url: string; method: string; params: any }[]; tips: Record<string, number> }36/** A healthy pair of sources at the same final block; individual tests override methods per source. */37function world(over: Partial<Record<string, Handler>> = {}, perUrl: Record<string, Partial<Record<string, Handler | 'down'>>> = {}, index: (body: any) => unknown = () => ({ transfers: [], resume_token: null })): World {38 const w: World = {39 accounts: { [FROM]: { amount: (3n * ONE).toString(), locked: '0', storage_usage: 182 }, [TO]: { amount: '1', locked: '0', storage_usage: 5000 } },40 calls: [], tips: { [A]: TIP, [B]: TIP, [ARCHIVAL]: TIP },41 };42 const header = (height: number) => ({ header: { height, hash: blockHash(height), timestamp_nanosec: String(BigInt(Date.now() - (TIP - height) * 600) * 1_000_000n), gas_price: '100000000' } });43 const base: Record<string, Handler> = {44 status: (_p, url) => ({ chain_id: 'mainnet', genesis_hash: n.chainRef, sync_info: { latest_block_height: w.tips[url]! + 2, latest_block_time: new Date().toISOString(), syncing: false } }),45 block: (p, url) => {46 if (p.finality === 'final') return header(w.tips[url]!);47 if (typeof p.block_id === 'number') { if (p.block_id > w.tips[url]!) throw new RpcErr('UNKNOWN_BLOCK'); return header(p.block_id); }48 const height = HEIGHT_OF.get(p.block_id);49 if (height === undefined) throw new RpcErr('UNKNOWN_BLOCK');50 return header(height);51 },52 query: (p) => {53 if (p.request_type === 'view_account') { const a = w.accounts[p.account_id]; if (!a) throw new RpcErr('UNKNOWN_ACCOUNT'); return { ...a, code_hash: '1'.repeat(32), block_height: TIP, block_hash: blockHash(TIP) }; }54 if (p.request_type === 'view_access_key') return p.account_id === FROM && p.public_key === FROM_KEY && w.accounts[FROM] ? { nonce: NONCE, permission: 'FullAccess' } : { error: `access key ${p.public_key} does not exist while viewing`, logs: [] };55 throw new Error('unexpected query');56 },57 EXPERIMENTAL_protocol_config: () => protocol(),58 tx: unknownTx, EXPERIMENTAL_tx_status: unknownTx,59 send_tx: () => ({ final_execution_status: 'INCLUDED' }),60 ...over,61 };62 vi.stubGlobal('fetch', async (url: string, init: RequestInit) => {63 const body = JSON.parse(String(init.body));64 if (url === INDEX) { w.calls.push({ url, method: 'index', params: body }); const r = index(body); if (r === 'down') throw new TypeError('fetch failed'); return new Response(JSON.stringify(r), { status: 200 }); }65 const { method, params } = body as { method: string; params: any };66 w.calls.push({ url, method, params });67 const f = perUrl[url]?.[method] ?? base[method];68 if (f === 'down' || !f) throw new TypeError('fetch failed');69 try { return new Response(JSON.stringify({ jsonrpc: '2.0', id: 1, result: f(params, url) }), { status: 200 }); }70 catch (e) {71 if (!(e instanceof RpcErr)) throw e;72 return new Response(JSON.stringify({ jsonrpc: '2.0', id: 1, error: { name: 'HANDLER_ERROR', cause: { name: e.cause, info: {} }, code: -32000, message: 'Server error', data: e.data } }), { status: e.cause === 'UNKNOWN_BLOCK' ? 422 : 200 });73 }74 });75 return w;76}7778let make: () => Adapter;79beforeAll(async () => {80 Object.assign(process.env, { SPW_ORIGIN: 'https://wallet.test', SPW_RP_ID: 'wallet.test', DATABASE_URL: 'postgres://unused', SPW_SIGNER_SECRET: 'x'.repeat(48), SPW_BROADCAST: 'off', SPW_WORKERS: 'off' });81 const { nearAdapter } = await import('../src/chains/near.ts');82 make = () => nearAdapter(n);83});84afterEach(() => vi.unstubAllGlobals());85const req = { asset: 'native', from: FROM, to: TO, amount: ONE };86const code = async (p: Promise<unknown>) => p.then(() => 'ok', (e: { code?: string; message: string }) => e.code ?? `error:${e.message}`);8788describe('near adapter: health and identity', () => {89 it('is ok only when both sources are up, synced and on mainnet (chain id + genesis hash)', async () => {90 world();91 expect(await make().health()).toMatchObject({ network: 'near', ok: true, readable: true, identityVerified: true, height: TIP + 2 });92 world({}, { [B]: { status: 'down' } });93 expect(await make().health()).toMatchObject({ ok: false, readable: true, identityVerified: false });94 world({}, { [B]: { status: () => ({ chain_id: 'testnet', genesis_hash: n.chainRef, sync_info: { latest_block_height: 5, latest_block_time: new Date().toISOString(), syncing: false } }) } });95 expect(await make().health()).toMatchObject({ ok: false, readable: true, identityVerified: false });96 world({}, { [A]: { status: () => ({ chain_id: 'mainnet', genesis_hash: h('other genesis'), sync_info: { latest_block_height: 5, latest_block_time: new Date().toISOString(), syncing: false } }) } });97 expect(await make().health()).toMatchObject({ ok: false, identityVerified: false });98 world({}, { [A]: { status: () => ({ chain_id: 'mainnet', genesis_hash: n.chainRef, sync_info: { latest_block_height: 5, latest_block_time: new Date().toISOString(), syncing: true } }) } });99 expect(await make().health()).toMatchObject({ ok: false });100 world({}, { [A]: { status: () => ({ chain_id: 'mainnet', genesis_hash: n.chainRef, sync_info: { latest_block_height: 5, latest_block_time: new Date(Date.now() - 600_000).toISOString(), syncing: false } }) } });101 expect(await make().health()).toMatchObject({ ok: false }); // stale102 world({}, { [A]: { status: 'down' }, [B]: { status: 'down' } });103 expect(await make().health()).toMatchObject({ ok: false, readable: false, height: null });104 });105 it('balances: exact yoctoNEAR; an account that does not exist holds nothing; failover to the second source', async () => {106 const w = world();107 w.accounts[FROM] = { amount: '123456789012345678901234567', locked: '0', storage_usage: 182 };108 expect(await make().balances(FROM)).toEqual({ native: 123456789012345678901234567n });109 expect(await make().balances(TO_IMPLICIT)).toEqual({ native: 0n });110 world({}, { [A]: { query: 'down' } });111 expect(await make().balances(FROM)).toEqual({ native: 3n * ONE });112 world({}, { [A]: { query: 'down' }, [B]: { query: 'down' } });113 await expect(make().balances(FROM)).rejects.toThrow();114 });115});116117describe('near adapter: prepare', () => {118 it('returns nonce + 1 and the agreed final block hash; the fee ceiling is the signer bound', async () => {119 const w = world();120 const p = await make().prepare(req);121 expect(p.section).toEqual({ x: { nonce: String(NONCE + 1), blockHash: blockHash(TIP) } });122 expect(p.maxFee).toBe(nearFeeCeiling(TO));123 expect(p.maxFee).toBe(2_000_000_000_000_000_000_000n);124 expect(p.feeEstimate).toBe(446_365_125_000n * 100_000_000n); // send + execution gas at the block price125 expect([p.warnings, p.ttlSec, p.aux]).toEqual([[], 120, undefined]);126 // every state read is pinned to the agreed block, on both sources127 const queries = w.calls.filter((c) => c.method === 'query');128 expect(queries).toHaveLength(6);129 expect(queries.every((c) => c.params.block_id === blockHash(TIP) && c.params.finality === undefined)).toBe(true);130 expect(new Set(queries.map((c) => c.url))).toEqual(new Set([A, B]));131 expect(w.calls.some((c) => c.method === 'send_tx')).toBe(false);132 });133 it('uses the older final block when the sources are a few blocks apart, after checking it is canonical on both', async () => {134 const w = world();135 w.tips[B] = TIP - 7;136 expect((await make().prepare(req)).section).toEqual({ x: { nonce: String(NONCE + 1), blockHash: blockHash(TIP - 7) } });137 expect(w.calls.filter((c) => c.method === 'block' && c.params.block_id === TIP - 7).map((c) => c.url).sort()).toEqual([A, B].sort());138 w.tips[B] = TIP - 101; // too far apart139 expect(await code(make().prepare(req))).toBe('rpc_disagreement');140 });141 it('a transfer that creates an implicit account: warning, higher ceiling, creation charge in the estimate', async () => {142 world();143 const p = await make().prepare({ ...req, to: TO_IMPLICIT });144 expect(p.warnings).toEqual(['near_creates_account']);145 expect(p.maxFee).toBe(20_000_000_000_000_000_000_000n);146 // (send + exec) × price + (0.007 NEAR − create_account execution at the block price)147 expect(p.feeEstimate).toBe(8_349_895_375_000n * 100_000_000n + (7_000_000_000_000_000_000_000n - 7_200_000_000_000n * 100_000_000n));148 const w = world();149 w.accounts[TO_IMPLICIT] = { amount: '5', locked: '0', storage_usage: 182 };150 const q = await make().prepare({ ...req, to: TO_IMPLICIT });151 expect([q.warnings, q.feeEstimate, q.maxFee]).toEqual([[], 8_349_895_375_000n * 100_000_000n, 20_000_000_000_000_000_000_000n]);152 });153 it('refuses a named destination that does not exist, and every malformed or unsupported request', async () => {154 world();155 expect(await code(make().prepare({ ...req, to: 'nobody-here.near' }))).toBe('bad_destination');156 expect(await code(make().prepare({ ...req, to: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu' }))).toBe('bad_destination'); // an address of another chain is not a NEAR account157 for (const to of ['Alice.near', '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', 'a', 'system', TO_IMPLICIT.toUpperCase(), 'alice..near']) expect(await code(make().prepare({ ...req, to })), to).toBe('bad_address');158 expect(await code(make().prepare({ ...req, to: FROM }))).toBe('bad_address');159 expect(await code(make().prepare({ ...req, from: 'bob.near' }))).toBe('bad_address');160 expect(await code(make().prepare({ ...req, asset: 'usdt.tether-token.near' }))).toBe('unsupported_asset');161 expect(await code(make().prepare({ ...req, amount: 0n }))).toBe('bad_amount');162 expect(await code(make().prepare({ ...req, memo: 'hi' }))).toBe('bad_memo');163 });164 it('fails closed when a source is down, on another chain, stale, or disagrees on anything', async () => {165 const cases: [string, Partial<Record<string, Handler | 'down'>>, string][] = [166 ['status down', { status: 'down' }, 'rpc_degraded'],167 ['testnet', { status: () => ({ chain_id: 'testnet', genesis_hash: h('testnet'), sync_info: { latest_block_height: 9, latest_block_time: new Date().toISOString(), syncing: false } }) }, 'rpc_degraded'],168 ['block down', { block: 'down' }, 'rpc_degraded'],169 ['query down', { query: 'down' }, 'rpc_degraded'],170 ['protocol config down', { EXPERIMENTAL_protocol_config: 'down' }, 'rpc_degraded'],171 ['rate limited', { query: () => { throw new Error('boom'); } }, 'rpc_degraded'],172 ['another block at the same height', { block: (p) => ({ header: { height: p.finality ? TIP : p.block_id, hash: h('fork'), timestamp_nanosec: String(BigInt(Date.now()) * 1_000_000n), gas_price: '100000000' } }) }, 'rpc_disagreement'],173 ['another nonce', { query: (p) => (p.request_type === 'view_access_key' ? { nonce: NONCE + 1, permission: 'FullAccess' } : { amount: (3n * ONE).toString(), locked: '0', storage_usage: p.account_id === FROM ? 182 : 5000 }) }, 'rpc_disagreement'],174 ['another balance', { query: (p) => (p.request_type === 'view_access_key' ? { nonce: NONCE, permission: 'FullAccess' } : { amount: (4n * ONE).toString(), locked: '0', storage_usage: p.account_id === FROM ? 182 : 5000 }) }, 'rpc_disagreement'],175 ['destination missing on one source', { query: (p) => { if (p.request_type === 'view_access_key') return { nonce: NONCE, permission: 'FullAccess' }; if (p.account_id === TO) throw new RpcErr('UNKNOWN_ACCOUNT'); return { amount: (3n * ONE).toString(), locked: '0', storage_usage: 182 }; } }, 'rpc_disagreement'],176 ['other fee parameters', { EXPERIMENTAL_protocol_config: () => protocol({}, { transfer_cost: { execution: 115123062501, send_not_sir: 115123062500, send_sir: 1 } }) }, 'rpc_disagreement'],177 ['malformed balance', { query: (p) => (p.request_type === 'view_access_key' ? { nonce: NONCE, permission: 'FullAccess' } : { amount: '1e24', locked: '0', storage_usage: 182 }) }, 'rpc_degraded'],178 ['nonce beyond exact integers', { query: (p) => (p.request_type === 'view_access_key' ? { nonce: 2 ** 60, permission: 'FullAccess' } : { amount: (3n * ONE).toString(), locked: '0', storage_usage: p.account_id === FROM ? 182 : 5000 }) }, 'rpc_degraded'],179 ];180 for (const [name, o, want] of cases) for (const src of [A, B]) { world({}, { [src]: o }); expect(await code(make().prepare(req)), `${name} on ${src}`).toBe(want); }181 // both sources stale in the same way: agreement is not enough182 world({ block: (p) => ({ header: { height: p.finality ? TIP : p.block_id, hash: blockHash(TIP), timestamp_nanosec: String(BigInt(Date.now() - 200_000) * 1_000_000n), gas_price: '100000000' } }) });183 expect(await code(make().prepare(req))).toBe('rpc_degraded');184 });185 it('refuses when the live protocol parameters leave the bounds the signer enforces', async () => {186 const outside: [string, Handler][] = [187 ['gas above the bound', () => protocol({}, { transfer_cost: { execution: 900_000_000_000, send_not_sir: 115123062500, send_sir: 1 } })],188 ['minimum purchase price above the price cap', () => protocol({ runtime: { min_gas_purchase_price: '2000000001' } })],189 ['another validity period', () => protocol({ transaction_validity_period: 43200 })],190 ['missing parameter', () => { const p = protocol(); delete (p.runtime_config as any).min_gas_purchase_price; return p; }],191 ];192 for (const [name, f] of outside) { world({ EXPERIMENTAL_protocol_config: f }); expect(await code(make().prepare(req)), name).toBe('rpc_degraded'); }193 world({ block: (p) => ({ header: { height: p.finality ? TIP : p.block_id, hash: blockHash(TIP), timestamp_nanosec: String(BigInt(Date.now()) * 1_000_000n), gas_price: '2000000001' } }) });194 expect(await code(make().prepare(req))).toBe('rpc_degraded'); // a gas price above the protocol cap means the pinned cap is wrong195 });196 it('account state: not activated, key missing or restricted', async () => {197 const w = world();198 delete w.accounts[FROM];199 expect(await code(make().prepare(req))).toBe('insufficient_funds');200 world({ query: (p) => (p.request_type === 'view_access_key' ? { error: 'access key does not exist while viewing', logs: [] } : { amount: (3n * ONE).toString(), locked: '0', storage_usage: 182 }) });201 expect(await code(make().prepare(req))).toBe('unsupported_account');202 world({ query: (p) => (p.request_type === 'view_access_key' ? { nonce: NONCE, permission: { FunctionCall: { allowance: null, receiver_id: 'x.near', method_names: [] } } } : { amount: (3n * ONE).toString(), locked: '0', storage_usage: 182 }) });203 expect(await code(make().prepare(req))).toBe('unsupported_account');204 });205 it('balance must cover amount + upfront gas purchase (+ storage when the account is not a zero-balance account), to the yoctoNEAR', async () => {206 // named destination at 10^8: burnt 223182562500 gas at 2×10^8 (headroom) + 223182562500 gas bought at 10^9207 const upfront = 223_182_562_500n * 200_000_000n + 223_182_562_500n * 1_000_000_000n;208 const w = world();209 w.accounts[FROM] = { amount: (ONE + upfront).toString(), locked: '0', storage_usage: 182 };210 expect(await code(make().prepare(req))).toBe('ok'); // a small account can be emptied (zero-balance account)211 w.accounts[FROM] = { amount: (ONE + upfront - 1n).toString(), locked: '0', storage_usage: 182 };212 expect(await code(make().prepare(req))).toBe('insufficient_funds');213 // 771 bytes: 771 × 10^19 yoctoNEAR must remain214 const reserve = 771n * 10_000_000_000_000_000_000n;215 w.accounts[FROM] = { amount: (ONE + upfront + reserve).toString(), locked: '0', storage_usage: 771 };216 expect(await code(make().prepare(req))).toBe('ok');217 w.accounts[FROM] = { amount: (ONE + upfront + reserve - 1n).toString(), locked: '0', storage_usage: 771 };218 expect(await code(make().prepare(req))).toBe('insufficient_funds');219 w.accounts[FROM] = { amount: (ONE + upfront).toString(), locked: reserve.toString(), storage_usage: 771 }; // a locked (staked) balance counts toward storage220 expect(await code(make().prepare(req))).toBe('ok');221 w.accounts[FROM] = { amount: (ONE + upfront).toString(), locked: '0', storage_usage: 770 };222 expect(await code(make().prepare(req))).toBe('ok');223 // creating an implicit account buys far more gas upfront224 const upfrontImplicit = 824_947_687_500n * 200_000_000n + 7_524_947_687_500n * 1_000_000_000n;225 w.accounts[FROM] = { amount: (ONE + upfrontImplicit - 1n).toString(), locked: '0', storage_usage: 182 };226 expect(await code(make().prepare({ ...req, to: TO_IMPLICIT }))).toBe('insufficient_funds');227 w.accounts[FROM] = { amount: (ONE + upfrontImplicit).toString(), locked: '0', storage_usage: 182 };228 expect(await code(make().prepare({ ...req, to: TO_IMPLICIT }))).toBe('ok');229 });230});231232const AMOUNT = '1000000000000000000000000';233const txView = (o: { txid: string; final?: string; status?: object; to?: string; amount?: string; from?: string; receipts?: unknown[]; outcomes?: unknown[] }) => ({234 final_execution_status: o.final ?? 'FINAL', status: o.status ?? { SuccessValue: '' },235 transaction: { hash: o.txid, signer_id: o.from ?? FROM, receiver_id: o.to ?? TO, nonce: NONCE + 1, actions: [{ Transfer: { deposit: o.amount ?? AMOUNT } }] },236 transaction_outcome: { block_hash: blockHash(TIP - 3), id: o.txid, outcome: { status: { SuccessReceiptId: h('r') } } },237 receipts: o.receipts, receipts_outcome: o.outcomes ?? [],238});239const TXID = h('tx-1');240const tracked = (over: Record<string, unknown> = {}) => ({ txid: TXID, intent: { from: FROM, to: TO, amount: AMOUNT, createdAt: Math.floor(Date.now() / 1000) - 30, x: { nonce: String(NONCE + 1), blockHash: blockHash(TIP - 20) } },241 blockRef: null as string | null, broadcastAt: Date.now() / 1000 - 20, ...over });242243describe('near adapter: track', () => {244 it('finalized only when every reachable source shows a final success; a hash alone is never success', async () => {245 world({ tx: () => txView({ txid: TXID }) });246 expect(await make().track(tracked())).toEqual({ state: 'finalized', confirmations: n.minConf, blockRef: blockHash(TIP - 3) });247 world({ tx: () => txView({ txid: TXID }) }, { [B]: { tx: unknownTx } });248 expect(await make().track(tracked())).toMatchObject({ state: 'confirming', confirmations: 1 });249 for (const final of ['NONE', 'INCLUDED', 'EXECUTED_OPTIMISTIC', 'INCLUDED_FINAL', 'EXECUTED']) {250 world({ tx: () => txView({ txid: TXID, final }) });251 expect((await make().track(tracked())).state, final).toBe('confirming');252 }253 world({ tx: () => txView({ txid: TXID, final: 'FINAL', status: { Unknown: null } }) });254 expect((await make().track(tracked())).state).toBe('confirming');255 world({ tx: () => txView({ txid: TXID }) }, { [B]: { tx: () => txView({ txid: TXID, final: 'EXECUTED_OPTIMISTIC' }) } });256 expect((await make().track(tracked())).state).toBe('confirming');257 world({ tx: () => txView({ txid: TXID }) }, { [B]: { tx: 'down', block: 'down' } }); // one source reachable and final: success may be shown258 expect((await make().track(tracked())).state).toBe('finalized');259 });260 it('failed when the transfer was included and failed, on both sources', async () => {261 const failure = { Failure: { ActionError: { index: 0, kind: { AccountDoesNotExist: { account_id: TO } } } } };262 world({ tx: () => txView({ txid: TXID, status: failure }) });263 const t = await make().track(tracked());264 expect(t).toMatchObject({ state: 'failed', confirmations: 0, blockRef: blockHash(TIP - 3) });265 expect(t.error).toMatch(/AccountDoesNotExist/);266 world({ tx: () => txView({ txid: TXID, status: failure }) }, { [B]: { tx: 'down' } });267 expect((await make().track(tracked())).state).toBe('confirming'); // a failure verdict needs both sources268 world({ tx: () => txView({ txid: TXID, status: failure }) }, { [B]: { tx: () => txView({ txid: TXID }) } });269 expect((await make().track(tracked())).state).toBe('confirming'); // contradictory results are never announced270 });271 it('a record that is not the approved transfer is not accepted', async () => {272 for (const bad of [txView({ txid: TXID, to: 'mallory.near' }), txView({ txid: TXID, amount: '1' }), txView({ txid: h('other') }), txView({ txid: TXID, from: TO_IMPLICIT })]) {273 world({ tx: () => bad });274 expect((await make().track(tracked())).state).toBe('broadcast');275 }276 });277 it('unknown transaction: stays pending while it can still be included', async () => {278 world();279 expect(await make().track(tracked())).toEqual({ state: 'broadcast', confirmations: 0, rebroadcast: true });280 world({}, { [B]: { tx: 'down' } });281 expect(await make().track(tracked({ intent: { ...tracked().intent, x: { nonce: String(NONCE + 1), blockHash: blockHash(TIP - 90_000) } } }))).toMatchObject({ state: 'broadcast' }); // never "failed" on one source282 world();283 expect(await make().track(tracked({ blockRef: blockHash(TIP - 3) }))).toMatchObject({ state: 'reorged', rebroadcast: true }); // it had been seen in a block284 });285 it('failed only when the nonce is still unused at a final block past the validity period, on both sources', async () => {286 const expired = () => tracked({ intent: { ...tracked().intent, x: { nonce: String(NONCE + 1), blockHash: blockHash(TIP - 90_000) } }, broadcastAt: Date.now() / 1000 - 60_000 });287 world();288 expect(await make().track(expired())).toMatchObject({ state: 'failed', confirmations: 0, error: expect.stringMatching(/no funds moved/) });289 const w = world();290 w.tips[B] = TIP - 3500; // the second source is not yet past reference + 86 400 + margin291 expect((await make().track({ ...expired(), txid: h('tx-2') })).state).toBe('broadcast');292 world();293 expect((await make().track({ ...tracked({ intent: { ...tracked().intent, x: { nonce: String(NONCE + 1), blockHash: blockHash(TIP - 86_400) } } }), txid: h('tx-3') })).state).toBe('broadcast'); // exactly at the limit: still valid294 world({}, { [A]: { query: 'down' } });295 expect((await make().track({ ...expired(), txid: h('tx-4') })).state).toBe('broadcast'); // nonce unreadable: no verdict296 });297 it('nonce already used: asks the archival node; included → final result, otherwise replaced (never before two minutes)', async () => {298 const used = (over: Record<string, unknown> = {}) => tracked({ intent: { ...tracked().intent, x: { nonce: String(NONCE), blockHash: blockHash(TIP - 20) } }, broadcastAt: Date.now() / 1000 - 600, ...over });299 world({}, { [ARCHIVAL]: { tx: () => txView({ txid: TXID }) } });300 expect(await make().track(used())).toMatchObject({ state: 'finalized', blockRef: blockHash(TIP - 3) });301 world({}, { [ARCHIVAL]: { tx: () => txView({ txid: TXID, status: { Failure: { ActionError: {} } } }) } });302 expect((await make().track(used())).state).toBe('failed');303 world({}, { [ARCHIVAL]: { tx: () => txView({ txid: TXID, final: 'EXECUTED' }) } });304 expect((await make().track(used())).state).toBe('confirming');305 world();306 expect(await make().track(used())).toMatchObject({ state: 'replaced', error: expect.stringMatching(/nonce was used/) });307 world();308 expect((await make().track(used({ blockRef: blockHash(TIP - 3) }))).state).toBe('failed'); // same verdict, legal transition from "reorged"309 world();310 expect((await make().track(used({ broadcastAt: Date.now() / 1000 - 60 }))).state).toBe('broadcast'); // too early to conclude311 world({}, { [ARCHIVAL]: { tx: 'down' } });312 expect(await make().track({ ...used(), txid: h('tx-5') })).toMatchObject({ state: 'broadcast', rebroadcast: false }); // archival unreachable: no verdict313 });314 it('rejects an intent without NEAR parameters', async () => {315 world();316 await expect(make().track(tracked({ intent: { from: FROM, to: TO, amount: AMOUNT, x: {} } }))).rejects.toThrow();317 await expect(make().track(tracked({ intent: { from: 'bob.near', to: TO, amount: AMOUNT, x: tracked().intent.x } }))).rejects.toThrow();318 });319});320321describe('near adapter: broadcast', () => {322 // a syntactically valid SignedTransaction for FROM (the adapter derives the hash and the signer from the bytes)323 const body = Buffer.concat([Buffer.from([64, 0, 0, 0]), Buffer.from(FROM), Buffer.from([0]), Buffer.from(FROM, 'hex'), Buffer.alloc(8, 1), Buffer.from([10, 0, 0, 0]), Buffer.from('alice.near'),324 Buffer.alloc(32, 7), Buffer.from([1, 0, 0, 0, 3]), Buffer.alloc(16, 2)]);325 const RAW = Buffer.concat([body, Buffer.from([0]), Buffer.alloc(64, 9)]).toString('base64'), RAW_TXID = base58.encode(sha256(body));326 it('relays the exact bytes to both sources and waits for inclusion', async () => {327 const w = world();328 await make().broadcast(RAW);329 const sent = w.calls.filter((c) => c.method === 'send_tx');330 expect(sent.map((c) => c.url).sort()).toEqual([A, B].sort());331 expect(sent.every((c) => c.params.signed_tx_base64 === RAW && c.params.wait_until === 'INCLUDED')).toBe(true);332 });333 it('one source accepting is enough; a node timeout after validation counts as accepted', async () => {334 world({}, { [A]: { send_tx: 'down' } });335 await expect(make().broadcast(RAW)).resolves.toBeUndefined();336 world({ send_tx: () => { throw new RpcErr('TIMEOUT_ERROR'); } });337 await expect(make().broadcast(RAW)).resolves.toBeUndefined();338 });339 it('reports the network refusal; "already known" is success when the chain has the transaction', async () => {340 const invalid = () => { throw new RpcErr('INVALID_TRANSACTION', { TxExecutionError: { InvalidTxError: { InvalidNonce: { ak_nonce: 9, tx_nonce: 8 } } } }); };341 world({ send_tx: invalid });342 await expect(make().broadcast(RAW)).rejects.toThrow(/rejected by the network: INVALID_TRANSACTION.*InvalidNonce/);343 const w = world({ send_tx: invalid, tx: (p) => txView({ txid: p.tx_hash }) });344 await expect(make().broadcast(RAW)).resolves.toBeUndefined();345 expect(w.calls.find((c) => c.method === 'tx')!.params).toEqual({ tx_hash: RAW_TXID, sender_account_id: FROM, wait_until: 'NONE' });346 world({}, { [A]: { send_tx: 'down' }, [B]: { send_tx: 'down' } });347 await expect(make().broadcast(RAW)).rejects.toThrow();348 await expect(make().broadcast('AAAA')).rejects.toThrow(/not a NEAR signed transaction/);349 });350});351352describe('near adapter: deposits', () => {353 const ME = FROM, SENDER = TO_IMPLICIT;354 const row = (k: number, over: Record<string, unknown> = {}) => ({ block_height: String(TIP - 500 - k), transaction_id: h(`dtx-${k}`), receipt_id: h(`drc-${k}`), signer_id: SENDER, account_id: ME, asset_id: 'native:near',355 amount: '999', transfer_type: 'NativeTransfer', ...over });356 const receipt = (k: number, over: Record<string, unknown> = {}, actions: unknown[] = [{ Transfer: { deposit: String(BigInt(k + 1) * ONE + 7n) } }]) =>357 ({ predecessor_id: SENDER, receiver_id: ME, receipt_id: h(`drc-${k}`), receipt: { Action: { actions } }, ...over });358 const outcome = (k: number, status: object = { SuccessValue: '' }, executor = ME) => ({ id: h(`drc-${k}`), block_hash: blockHash(TIP - 100 - k), outcome: { executor_id: executor, status } });359 const status = (views: Record<string, unknown>): Handler => (p) => { const v = views[p.tx_hash]; if (!v) throw new RpcErr('UNKNOWN_TRANSACTION'); return v; };360 const view = (k: number, receipts: unknown[], outcomes: unknown[], final = 'FINAL') => ({ ...txView({ txid: h(`dtx-${k}`), from: SENDER, to: ME, receipts, outcomes, final }) });361362 it('reports only transfers that RPC confirms: amount, sender and block come from the node, not from the index', async () => {363 const views = {364 [h('dtx-0')]: view(0, [receipt(0), { ...receipt(0), receipt_id: h('refund'), predecessor_id: 'system', receiver_id: SENDER }], [outcome(0)]),365 [h('dtx-1')]: view(1, [receipt(1, { predecessor_id: 'system' })], [outcome(1)]), // gas refund366 [h('dtx-2')]: view(2, [receipt(2)], [outcome(2, { Failure: { ActionError: {} } })]), // failed receipt367 [h('dtx-3')]: view(3, [receipt(3, { receiver_id: 'someone-else.near' })], [outcome(3, undefined, 'someone-else.near')]), // not to this account368 [h('dtx-4')]: view(4, [receipt(4, {}, [{ FunctionCall: { method_name: 'x', args: '', gas: 1, deposit: '5' } }])], [outcome(4)]), // not a plain transfer369 [h('dtx-5')]: view(5, [receipt(5, { predecessor_id: ME })], [outcome(5)]), // from the account itself370 [h('dtx-6')]: view(6, [receipt(6, {}, [{ Transfer: { deposit: '5' } }, { Transfer: { deposit: '6' } }])], [outcome(6)]), // two transfers in one receipt371 [h('dtx-7')]: view(7, [{ ...receipt(7), receipt_id: h('other') }], [outcome(7)]), // the indexed receipt is not in the transaction372 // dtx-8: unknown to every node373 };374 const w = world({ EXPERIMENTAL_tx_status: status(views) }, {}, () => ({ transfers: [0, 1, 2, 3, 4, 5, 6, 7, 8].map((k) => row(k)).concat([row(0), row(9, { account_id: 'x.near' }), row(10, { asset_id: 'usdt.tether-token.near' }), row(11, { transaction_id: null })] as never), resume_token: null }));375 const d = await make().deposits(ME, null);376 expect(d.items).toEqual([377 { txid: h('dtx-0'), idx: 0, asset: 'native', amount: ONE + 7n, from: SENDER, block: TIP - 100, time: expect.any(Number) },378 { txid: h('dtx-6'), idx: 0, asset: 'native', amount: 11n, from: SENDER, block: TIP - 106, time: expect.any(Number) },379 ]);380 expect([d.cursor, d.finalizedHeight]).toEqual([TIP - 3000, TIP]);381 expect(w.calls.find((c) => c.method === 'index')!.params).toEqual({ account_id: ME, direction: 'receiver', asset_id: 'native:near', ignore_system: true, desc: true, limit: 20 });382 });383 it('a transfer that is not final yet, or that no source can check, is not reported and the cursor waits for it', async () => {384 const pendingView = { [h('dtx-0')]: view(0, [receipt(0)], [outcome(0)], 'EXECUTED_OPTIMISTIC') };385 const old = { block_height: String(TIP - 4000) }; // older than the window that is re-read anyway386 world({ EXPERIMENTAL_tx_status: status(pendingView) }, {}, () => ({ transfers: [row(0, old)], resume_token: null }));387 const a = make();388 const d = await a.deposits(ME, TIP - 5000);389 expect([d.items, d.cursor]).toEqual([[], TIP - 4001]); // just below the unverified row390 world({ EXPERIMENTAL_tx_status: status({ [h('dtx-0')]: view(0, [receipt(0)], [outcome(0)]) }) }, {}, () => ({ transfers: [row(0, old)], resume_token: null }));391 const e = await a.deposits(ME, d.cursor);392 expect(e.items.map((i) => i.txid)).toEqual([h('dtx-0')]);393 expect(e.cursor).toBe(TIP - 3000);394 world({ EXPERIMENTAL_tx_status: 'down' as never }, {}, () => ({ transfers: [row(1, old)], resume_token: null }));395 expect(await make().deposits(ME, TIP - 5000)).toMatchObject({ items: [], cursor: TIP - 4001 });396 // a recent unverified row is inside the re-read window: the cursor advances normally and the row is read again next time397 world({ EXPERIMENTAL_tx_status: status(pendingView) }, {}, () => ({ transfers: [row(0)], resume_token: null }));398 expect(await make().deposits(ME, TIP - 5000)).toMatchObject({ items: [], cursor: TIP - 3000 });399 // a row that still cannot be checked after about three days no longer holds the cursor400 world({ EXPERIMENTAL_tx_status: 'down' as never }, {}, () => ({ transfers: [row(2, { block_height: String(TIP - 430_001) })], resume_token: null }));401 expect(await make().deposits(ME, TIP - 500_000)).toMatchObject({ items: [], cursor: TIP - 3000 });402 });403 it('reads only rows above the cursor, pages through the index, and never skips when there is more than one scan can read', async () => {404 const all = Array.from({ length: 250 }, (_, k) => row(k));405 const views = Object.fromEntries(all.map((_, k) => [h(`dtx-${k}`), view(k, [receipt(k)], [outcome(k)])]));406 const page = (b: any) => { const from = Number(b.resume_token ?? 0); return { transfers: all.slice(from, from + 100), resume_token: from + 100 < all.length ? String(from + 100) : null }; };407 const w = world({ EXPERIMENTAL_tx_status: status(views) }, {}, page);408 const d = await make().deposits(ME, TIP - 500 - 120); // rows 0..119 are above the cursor409 expect(d.items).toHaveLength(120);410 expect(w.calls.filter((c) => c.method === 'index')).toHaveLength(2);411 expect(d.cursor).toBe(TIP - 620); // a cursor never moves backwards412 world({}, {}, () => ({ transfers: [], resume_token: null }));413 expect((await make().deposits(ME, TIP - 10)).cursor).toBe(TIP - 10);414 // endless index: the cursor is held415 world({ EXPERIMENTAL_tx_status: unknownTx }, {}, (b) => ({ transfers: [row(Number(b.resume_token ?? 0))], resume_token: String(Number(b.resume_token ?? 0) + 1) }));416 expect((await make().deposits(ME, 5)).cursor).toBe(5);417 });418 it('fails (to be retried) when the index or every RPC source is unreachable', async () => {419 world({}, {}, () => 'down');420 await expect(make().deposits(ME, null)).rejects.toThrow();421 world({}, { [A]: { block: 'down' }, [B]: { block: 'down' } });422 await expect(make().deposits(ME, null)).rejects.toThrow();423 });424});425