SPB Git

Repository Public

aptos.ts 25.2 KB 330 lines typescript

1// Aptos adapter: two independent REST full nodes (n.rpc). Everything that feeds the signature (sequence number, authentication2// key, balance) is read from both at the same ledger version and must agree; the exact transaction is then simulated on both3// and must succeed and show the expected credit, or the transfer is refused.4// Identity: chain id 1 (node and on-chain 0x1::chain_id resource), a pinned ledger checkpoint, and the same block hash on both5// sources for the same recent height. Public full nodes prune history (about two to three weeks are kept), so they cannot show6// the genesis transaction; the keyless Aptos Labs archive node can, and when it answers it must show the pinned genesis hash7// and the same recent block as both sources (an unreachable archive does not block transfers, a contradicting one does).8import {9  APTOS_MAX_GAS_AMOUNT, APTOS_MIN_GAS_UNIT_PRICE, aptosAptStore, aptosDecodeAddress, aptosEncodeTransfer, aptosRawBytes, aptosSimulationTx, aptosTxid,10  formatUnits, type NetworkDef,11} from '@spw/core';12import { UserError } from '../config.ts';13import { hostOf, http, type Adapter, type Deposit, type Health, type Prepared, type Tracked } from './rpc.ts';1415interface Head { chain: number; version: bigint; height: bigint; epoch: bigint; timeSec: number }16interface Event { type: string; data: Record<string, unknown> }17interface Tx {18  type: string; hash: string; version?: string; success?: boolean; vm_status?: string; gas_used?: string; timestamp?: string;19  sender?: string; sequence_number?: string; max_gas_amount?: string; gas_unit_price?: string; expiration_timestamp_secs?: string;20  payload?: { type?: string; function?: string; type_arguments?: unknown[]; arguments?: unknown[] }; events?: Event[];21}2223/** approval window; the transaction itself stays valid a little longer so it can still be included after a late approval */24const TTL_SEC = 120, VALIDITY_SEC = 180;25/** ledger state the sources must have reached: a chain that claims chain id 1 below this point is not the main network (read 2026-11-10) */26const CHECKPOINT = { height: 1_175_000_000n, version: 7_990_000_000n, epoch: 17_954n };27/** gas price bid bounds in octas per unit: the network minimum, and ten times it */28const PRICE_CAP = 1_000n;29/** highest fee this adapter proposes (0.1 APT, the default policy ceiling) */30const FEE_CEILING = 10_000_000n;31/** gas limit of the first, estimating simulation; margin applied to the gas it used; smallest limit proposed */32const SIM_GAS = 20_000n, GAS_FLOOR = 200n;33const MAX_CLOCK_SKEW_SEC = 60, STALE_SEC = 120, IDENTITY_TTL_MS = 600_000;34/** a "not included" verdict needs every source's ledger time to be this far past the expiration */35const EXPIRY_MARGIN_SEC = 20;36/** full-history node (Aptos Labs, keyless): genesis anchor, and reading deposits older than the sources keep */37const ARCHIVE = 'https://archive.mainnet.aptoslabs.com/v1';38/** hash of the genesis transaction (ledger version 0) of the main network, read from the archive node on 2026-11-10 */39const GENESIS_TX = '0xf650d76ea0a3176f0412b7e6bea5eb6fbf1d7adb0c39ab18e378adefb9247309';40const APT_COIN = '0x1::aptos_coin::AptosCoin', APT_FA = '0x' + '0'.repeat(63) + 'a';41const TRANSFER_FN = '0x1::aptos_account::transfer';42const BCS_TX = 'application/x.aptos.signed_transaction+bcs';43const PAGE = 100, FIRST_PAGE = 50, MAX_PAGES = 3;4445class ApiError extends Error { constructor(public status: number, public apiCode: string, message: string) { super(message); } }46async function read<T>(r: Response): Promise<T> {47  const j = (await r.json().catch(() => null)) as (T & { error_code?: string; message?: string; vm_error_code?: number | null }) | null;48  if (!r.ok || j === null) throw new ApiError(r.status, String(j?.error_code ?? ''), `aptos http ${r.status} ${j?.error_code ?? ''} ${String(j?.message ?? '').slice(0, 160)}`.trim());49  return j;50}51const get = async <T>(base: string, path: string): Promise<T> => read<T>(await http(base + path, { headers: { accept: 'application/json' } }));52const postJson = async <T>(url: string, body: unknown): Promise<T> => read<T>(await http(url, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' }, body: JSON.stringify(body) }));53const postBcs = async <T>(url: string, body: Uint8Array): Promise<T> => read<T>(await http(url, { method: 'POST', headers: { 'content-type': BCS_TX, accept: 'application/json' }, body: body as BodyInit }));54const uint = (v: unknown, what: string): bigint => {55  if (typeof v === 'number' ? !Number.isSafeInteger(v) || v < 0 : typeof v !== 'string' || !/^\d{1,20}$/.test(v)) throw new Error(`aptos: bad ${what}`);56  return BigInt(v as string | number);57};58const hash32 = (v: unknown, what: string): string => { if (typeof v !== 'string' || !/^0x[0-9a-f]{64}$/.test(v)) throw new Error(`aptos: bad ${what}`); return v; };59/** the REST API prints account addresses without leading zeros: restore the canonical 64-digit form before comparing */60const canon = (v: unknown): string => (typeof v === 'string' && /^0x[0-9a-f]{1,64}$/.test(v) ? '0x' + v.slice(2).padStart(64, '0') : '');61const big = (a: bigint, b: bigint) => (a > b ? a : b), small = (a: bigint, b: bigint) => (a < b ? a : b);6263export function aptosAdapter(n: NetworkDef): Adapter {64  const sources = n.rpc;65  const apt = (v: bigint) => `${formatUnits(v, n.native.decimals)} ${n.native.symbol}`;66  const any = async <T>(f: (u: string) => Promise<T>, urls: string[] = sources): Promise<T> => { let last: unknown; for (const u of urls) { try { return await f(u); } catch (e) { last = e; } } throw last; };67  /** every source must answer */68  const all = async <T>(f: (u: string) => Promise<T>, what: string): Promise<T[]> => {69    const rs = await Promise.allSettled(sources.map(f));70    if (sources.length < 2 || rs.some((r) => r.status === 'rejected')) throw new UserError('rpc_degraded', `${n.name} data source unavailable (${what}); transfer refused`, 503);71    return rs.map((r) => (r as PromiseFulfilledResult<T>).value);72  };73  const agree = <T>(vals: T[], what: string): T => {74    for (const v of vals) if (JSON.stringify(v) !== JSON.stringify(vals[0])) throw new UserError('rpc_disagreement', `${n.name} data sources disagree (${what}); transfer refused`, 503);75    return vals[0]!;76  };77  /** ledger position of one source; verifies the chain id and the pinned checkpoint on every call */78  const head = async (u: string): Promise<Head> => {79    const r = await get<Record<string, unknown>>(u, '');80    const h: Head = { chain: r.chain_id as number, version: uint(r.ledger_version, 'ledger version'), height: uint(r.block_height, 'block height'), epoch: uint(r.epoch, 'epoch'),81      timeSec: Number(uint(r.ledger_timestamp, 'ledger timestamp') / 1_000_000n) };82    if (h.chain !== Number(n.chainRef) || h.chain !== 1) throw new Error('aptos: wrong chain id');83    if (h.height < CHECKPOINT.height || h.version < CHECKPOINT.version || h.epoch < CHECKPOINT.epoch) throw new Error('aptos: ledger is behind the pinned checkpoint');84    return h;85  };86  /** APT balance in octas: the framework view covers both the legacy CoinStore and the fungible-asset store */87  const balance = async (u: string, addr: string, version?: bigint): Promise<bigint> => {88    const r = await postJson<unknown[]>(`${u}/view${version === undefined ? '' : `?ledger_version=${version}`}`, { function: '0x1::coin::balance', type_arguments: [APT_COIN], arguments: [addr] });89    if (!Array.isArray(r) || r.length !== 1) throw new Error('aptos: bad balance answer');90    return uint(r[0], 'balance');91  };92  /** an account that was never created on-chain has sequence number 0 and its address as authentication key */93  const account = async (u: string, addr: string, version: bigint): Promise<{ sequence: string; authKey: string }> => {94    try {95      const a = await get<{ sequence_number: unknown; authentication_key: unknown }>(u, `/accounts/${addr}?ledger_version=${version}`);96      return { sequence: uint(a.sequence_number, 'sequence number').toString(), authKey: hash32(canon(a.authentication_key), 'authentication key') };97    } catch (e) { if (e instanceof ApiError && e.status === 404 && e.apiCode === 'account_not_found') return { sequence: '0', authKey: addr }; throw e; }98  };99  /** transaction by hash (the caller checks the record); null only for the node's own "not found" answer */100  const byHash = async (u: string, txid: string): Promise<Tx | null> => {101    try {102      return await get<Tx>(u, `/transactions/by_hash/${txid}`);103    } catch (e) { if (e instanceof ApiError && e.status === 404 && e.apiCode === 'transaction_not_found') return null; throw e; }104  };105  const simulate = async (u: string, rawTx: Uint8Array): Promise<Tx> => {106    const body = aptosSimulationTx(rawTx);107    const r = await postBcs<Tx[]>(`${u}/transactions/simulate`, body);108    if (!Array.isArray(r) || r.length !== 1 || typeof r[0]?.success !== 'boolean' || typeof r[0].vm_status !== 'string') throw new Error('aptos: bad simulation answer');109    if (r[0].hash !== aptosTxid(body)) throw new Error('aptos: simulation answered for another transaction');110    return r[0];111  };112  /** octas credited to `addr` by the events of one transaction: fungible-asset deposits into its APT store, or legacy coin deposits */113  const credited = (events: Event[] | undefined, addr: string, store: string): { idx: number; amount: bigint }[] => (events ?? []).flatMap((e, idx) => {114    const d = e?.data ?? {};115    const hit = (e?.type === '0x1::fungible_asset::Deposit' && canon(d.store) === store) || (e?.type === '0x1::coin::CoinDeposit' && canon(d.account) === addr && d.coin_type === APT_COIN);116    return hit ? [{ idx, amount: uint(d.amount, 'event amount') }] : [];117  });118119  let identity = false, identityAt = 0;120  /** on-chain chain id on every source, and the same block hash for the same recent height */121  const blockAt = async (u: string, height: bigint): Promise<string> => {122    const b = await get<Record<string, unknown>>(u, `/blocks/by_height/${height}`);123    return [hash32(b.block_hash, 'block hash'), uint(b.first_version, 'version'), uint(b.last_version, 'version'), uint(b.block_timestamp, 'timestamp')].join('|');124  };125  const verifyIdentity = async (heads: Head[]): Promise<void> => {126    const height = heads.map((h) => h.height).reduce(small) - 5n;127    const [seen, anchor] = await Promise.all([128      Promise.all(sources.map(async (u) => {129        const c = await get<{ data?: { id?: unknown } }>(u, '/accounts/0x1/resource/0x1::chain_id::ChainId');130        if (c.data?.id !== 1) throw new Error('aptos: on-chain chain id mismatch');131        return blockAt(u, height);132      })),133      (async () => {134        try { return { genesis: (await get<Tx>(ARCHIVE, '/transactions/by_version/0')).hash, block: await blockAt(ARCHIVE, height) }; }135        catch { return null; } // archive unreachable: the two-source checks stand alone136      })(),137    ]);138    if (seen.length < 2 || seen.some((s) => s !== seen[0])) throw new Error('aptos: sources are not on the same ledger');139    if (anchor && (anchor.genesis !== GENESIS_TX || anchor.block !== seen[0])) throw new Error('aptos: sources are not on the ledger that starts at the pinned genesis');140  };141142  return {143    async health(): Promise<Health> {144      const probes = await Promise.all(sources.map(async (url) => {145        const t = Date.now();146        try {147          const h = await head(url);148          if (Date.now() / 1000 - h.timeSec > STALE_SEC) throw new Error('stale');149          return { url: hostOf(url), ok: true, ms: Date.now() - t, h: h as Head | null };150        } catch { return { url: hostOf(url), ok: false, ms: null as number | null, h: null as Head | null }; }151      }));152      const up = probes.length >= 2 && probes.every((e) => e.ok);153      if (up && (!identity || Date.now() - identityAt > IDENTITY_TTL_MS)) {154        try { await verifyIdentity(probes.map((p) => p.h!)); identity = true; identityAt = Date.now(); } catch { identity = false; }155      }156      const heights = probes.flatMap((p) => (p.h ? [Number(p.h.height)] : []));157      return { network: n.id, ok: up && identity, readable: probes.some((e) => e.ok), identityVerified: identity, height: heights.length ? Math.max(...heights) : null,158        endpoints: probes.map(({ h: _h, ...e }) => e), checkedAt: new Date().toISOString() };159    },160161    async balances(addr) {162      aptosDecodeAddress(addr);163      return { native: await any((u) => balance(u, addr)) };164    },165166    async prepare(d): Promise<Prepared> {167      if (d.asset !== 'native') throw new UserError('unsupported_asset', 'only APT can be sent on Aptos in this version');168      let sender: Uint8Array, recipient: Uint8Array;169      try { sender = aptosDecodeAddress(d.from); recipient = aptosDecodeAddress(d.to); } catch (e) { throw new UserError('bad_address', (e as Error).message); }170      if (d.from === d.to) throw new UserError('bad_address', 'destination is this wallet');171      if (d.amount <= 0n || d.amount >= 1n << 64n) throw new UserError('bad_amount', 'amount must be greater than zero');172      if (d.memo !== undefined) throw new UserError('bad_memo', 'Aptos transfers carry no memo');173      const warnings: string[] = [];174175      // ledger position of both sources, then the account state of both at the same ledger version176      const heads = await all(head, 'ledger position');177      const now = Math.floor(Date.now() / 1000);178      for (const h of heads) if (Math.abs(now - h.timeSec) > MAX_CLOCK_SKEW_SEC) throw new UserError('rpc_degraded', `${n.name} ledger time and server clock differ; transfer refused`, 503);179      const version = heads.map((h) => h.version).reduce(small);180      const snaps = await all(async (u) => {181        const [acct, bal, gas] = await Promise.all([account(u, d.from, version), balance(u, d.from, version), get<{ gas_estimate: unknown }>(u, '/estimate_gas_price')]);182        return { state: { ...acct, balance: bal.toString() }, gasPrice: uint(gas.gas_estimate, 'gas estimate') };183      }, 'account state');184      const state = agree(snaps.map((s) => s.state), 'source account');185      if (state.authKey !== d.from) throw new UserError('unsupported_account', 'the authentication key of this Aptos account was rotated; it cannot send from this wallet');186      const bal = BigInt(state.balance), sequence = BigInt(state.sequence);187188      // gas price: the larger of the two estimates, never below the network minimum, capped189      const want = big(snaps.map((s) => s.gasPrice).reduce(big), APTOS_MIN_GAS_UNIT_PRICE);190      if (want > PRICE_CAP) warnings.push('network_congested');191      const gasPrice = small(want, PRICE_CAP);192193      const short = () => new UserError('insufficient_funds', `insufficient ${n.native.symbol}: ${apt(bal)} available for amount plus network fee`);194      if (bal <= d.amount) throw short();195      const expiration = BigInt(now + VALIDITY_SEC);196      const encode = (maxGas: bigint) => aptosEncodeTransfer({ sender, recipient, amount: d.amount, sequence, maxGas, gasPrice, expiration });197      const store = aptosAptStore(d.to);198      /** simulate on both sources; both must execute the transfer and show the exact credit to the recipient */199      const run = async (maxGas: bigint, what: string): Promise<bigint> => {200        const rawTx = encode(maxGas);201        const sims = await all((u) => simulate(u, rawTx), what);202        for (const s of sims) {203          if (!s.success) {204            const vm = s.vm_status ?? '';205            if (/INSUFFICIENT_BALANCE|EINSUFFICIENT_BALANCE|ECOIN_STORE_NOT_PUBLISHED/.test(vm)) throw short();206            if (/SEQUENCE_NUMBER/.test(vm)) throw new UserError('pending_tx', 'this account has a transaction in progress; try again in a moment', 409);207            throw new UserError('simulation_failed', `the network would reject this transfer: ${vm.slice(0, 160)}`);208          }209          const args = s.payload?.arguments;210          const echo = [canon(s.sender), s.sequence_number, s.max_gas_amount, s.gas_unit_price, s.expiration_timestamp_secs, s.payload?.function, JSON.stringify(s.payload?.type_arguments), JSON.stringify(Array.isArray(args) && args.length === 2 ? [canon(args[0]), args[1]] : null)];211          const sent = [d.from, sequence.toString(), maxGas.toString(), gasPrice.toString(), expiration.toString(), TRANSFER_FN, '[]', JSON.stringify([d.to, d.amount.toString()])];212          if (JSON.stringify(echo) !== JSON.stringify(sent)) throw new UserError('rpc_disagreement', `${n.name} simulated a different transaction; transfer refused`, 503);213          const credit = credited(s.events, d.to, store);214          if (credit.length !== 1 || credit[0]!.amount !== d.amount) throw new UserError('simulation_failed', 'the simulation does not credit the recipient with the exact amount; transfer refused');215        }216        const used = sims.map((s) => uint(s.gas_used, 'gas used')).reduce(big);217        if (used <= 0n || used > maxGas) throw new UserError('rpc_degraded', `${n.name} simulation returned an implausible gas figure; transfer refused`, 503);218        return used;219      };220221      // first pass: how much gas the transfer needs (a recipient without an APT store costs a one-time storage fee)222      const affordable = (bal - d.amount) / gasPrice;223      if (affordable < 1n) throw short();224      const estimate = await run(small(SIM_GAS, affordable), 'simulation');225      const maxGas = big(estimate + estimate / 2n + 1n, GAS_FLOOR);226      const maxFee = maxGas * gasPrice;227      if (maxGas > APTOS_MAX_GAS_AMOUNT || maxFee > FEE_CEILING) throw new UserError('fee_too_high', `the network fee for this transfer (up to ${apt(maxFee)}) is above the wallet limit`);228      if (bal < d.amount + maxFee) throw short();229      // second pass: the exact transaction that will be signed230      const used = await run(maxGas, 'simulation of the exact transaction');231232      return {233        section: { x: { sequence_number: sequence.toString(), max_gas_amount: maxGas.toString(), gas_unit_price: gasPrice.toString(), expiration_timestamp_secs: expiration.toString() } },234        maxFee, feeEstimate: used * gasPrice, warnings, ttlSec: TTL_SEC,235      };236    },237238    async broadcast(raw) {239      const bytes = aptosRawBytes(raw), txid = aptosTxid(bytes);240      const rs = await Promise.allSettled(sources.map(async (u) => {241        const r = await postBcs<{ hash?: string }>(`${u}/transactions`, bytes);242        if (r.hash !== txid) throw new Error('aptos node answered for another transaction');243      }));244      if (rs.some((r) => r.status === 'fulfilled')) return;245      // "already known": a re-submission after inclusion is refused (sequence number too old); the ledger is the judge246      const known = await Promise.allSettled(sources.map((u) => byHash(u, txid)));247      if (known.some((k) => k.status === 'fulfilled' && k.value?.hash === txid)) return;248      throw new Error(String((rs[0] as PromiseRejectedResult).reason?.message));249    },250251    async track(p): Promise<Tracked> {252      const exp = Number(p.intent?.x?.expiration_timestamp_secs);253      if (!Number.isSafeInteger(exp) || exp <= 0) throw new Error('intent has no expiration');254      hash32(p.txid, 'transaction id');255      // per source: ledger position FIRST, then the lookup — a miss is then known to cover the ledger up to that position256      const rs = await Promise.allSettled(sources.map(async (u) => { const h = await head(u); return { h, tx: await byHash(u, p.txid) }; }));257      const up = rs.flatMap((r) => (r.status === 'fulfilled' ? [r.value] : []));258      const allUp = up.length === sources.length && up.length >= 2;259      for (const s of up) if (s.tx !== null && (s.tx.hash !== p.txid || typeof s.tx.type !== 'string')) throw new Error('aptos: bad transaction record');260      const committed = up.filter((s) => s.tx !== null && s.tx.type !== 'pending_transaction');261      for (const s of committed) if (s.tx!.type !== 'user_transaction' || typeof s.tx!.success !== 'boolean') throw new Error('aptos: bad transaction record');262      if (committed.length > 0) {263        const version = uint(committed[0]!.tx!.version, 'version'), good = committed[0]!.tx!.success!;264        if (committed.some((s) => uint(s.tx!.version, 'version') !== version || s.tx!.success !== good)) throw new Error('data sources disagree on the transaction result');265        const blockRef = version.toString();266        // a committed transaction is final on Aptos (BFT); every reachable source must show it before we say so267        if (committed.length < up.length) return { state: 'confirming', confirmations: 1, blockRef };268        if (good) return { state: 'finalized', confirmations: Math.max(1, Number(up.map((s) => s.h.version).reduce(big) - version) + 1), blockRef };269        // committed but aborted (gas charged, nothing transferred): only final once every source is reachable and agrees270        if (!allUp) return { state: 'confirming', confirmations: 1, blockRef };271        return { state: 'failed', confirmations: 0, blockRef, error: `committed but the transfer aborted (${String(committed[0]!.tx!.vm_status).slice(0, 120)}); only the network fee was charged, no funds moved` };272      }273      // a transaction can only be committed in a block whose time is before its expiration274      if (allUp && up.every((s) => s.tx === null && s.h.timeSec > exp + EXPIRY_MARGIN_SEC))275        return { state: 'failed', confirmations: 0, error: 'not included before its expiration; no funds moved' };276      return { state: 'broadcast', confirmations: 0, rebroadcast: Date.now() / 1000 < exp };277    },278279    // Full nodes have no per-account index of incoming transfers. Discovery uses the keyless Aptos Labs indexer (GraphQL, same280    // host as rpc[0]); the indexer is never trusted: each transaction it names is read from a full node (the other source281    // first) and only the credit shown by that node's events is recorded.282    async deposits(addr, cursor) {283      aptosDecodeAddress(addr);284      const index = sources.find((u) => /(^|\.)aptoslabs\.com$/.test(new URL(u).hostname));285      const store = aptosAptStore(addr);286      return any(async (u) => {287        const latest = Number((await head(u)).version); // read before the listing: every version up to here is final and readable on this node288        if (!index) return { items: [], cursor: cursor ?? 0, finalizedHeight: latest };289        const versions = new Set<number>();290        let bound = latest, from = cursor, cut = false;291        for (let page = 0; page < (cursor === null ? 1 : MAX_PAGES); page++) {292          const where = `owner_address:{_eq:"${addr}"}, asset_type:{_in:["${APT_FA}","${APT_COIN}"]}, type:{_in:["0x1::fungible_asset::Deposit","0x1::coin::CoinDeposit"]}, is_transaction_success:{_eq:true}`293            + (from === null ? '' : `, transaction_version:{_gt:${from}}`);294          const q = `{ processor_status(where:{processor:{_eq:"fungible_asset_processor"}}) { last_success_version } `295            + `fungible_asset_activities(limit:${from === null ? FIRST_PAGE : PAGE}, where:{${where}}, order_by:{transaction_version:${from === null ? 'desc' : 'asc'}}) { transaction_version } }`;296          const r = await postJson<{ data?: { processor_status?: { last_success_version: unknown }[]; fungible_asset_activities?: { transaction_version: unknown }[] }; errors?: unknown }>(`${index}/graphql`, { query: q });297          const rows = r.data?.fungible_asset_activities, status = r.data?.processor_status;298          if (r.errors || !Array.isArray(rows) || !Array.isArray(status) || status.length !== 1) throw new Error('aptos: history index unavailable');299          // nothing beyond what the index has fully processed, nor beyond what the verifying node holds, is consumed in this pass300          bound = Math.min(bound, Number(uint(status[0]!.last_success_version, 'index position')));301          const vs = rows.map((x) => Number(uint(x.transaction_version, 'version')));302          for (const v of vs) if (v <= bound) versions.add(v);303          cut = from !== null && vs.length >= PAGE;304          if (!cut) break;305          from = Math.max(...vs);306          if (from >= bound) { cut = false; break; }307        }308        const items: Deposit[] = [];309        /** null when this node has pruned the version */310        const byVersion = async (node: string, v: number): Promise<Tx | null> => {311          try {312            const t = await get<Tx>(node, `/transactions/by_version/${v}`);313            if (uint(t.version, 'version') !== BigInt(v)) throw new Error('aptos: bad transaction record');314            return t;315          } catch (e) { if (e instanceof ApiError && e.status === 410 && e.apiCode === 'version_pruned') return null; throw e; }316        };317        for (const v of [...versions].sort((a, b) => a - b)) {318          const t = await byVersion(u, v) ?? await byVersion(ARCHIVE, v).catch((e) => { if (cursor === null) return null; throw e; });319          if (t === null) { if (cursor === null) continue; throw new Error('aptos: deposit transaction no longer readable'); } // history only on a first scan320          if (t.type !== 'user_transaction' || t.success !== true || canon(t.sender) === addr) continue;321          const time = Number(uint(t.timestamp, 'timestamp') / 1_000_000n);322          for (const c of credited(t.events, addr, store)) if (c.amount > 0n) items.push({ txid: hash32(t.hash, 'hash'), idx: c.idx, asset: 'native', amount: c.amount, from: hash32(canon(t.sender), 'sender'), block: v, time });323        }324        // more to read: resume next time after the last version fully covered325        return { items, cursor: cut ? Math.max(cursor ?? 0, from ?? 0) : Math.max(cursor ?? 0, bound), finalizedHeight: latest };326      }, index ? [...sources.filter((s) => s !== index), index] : sources);327    },328  };329}330
Files main
Menu

Theme