From 0939e6192d5c89d158ed38082020a2c92eadd3aa Mon Sep 17 00:00:00 2001 From: Aere Network Date: Tue, 20 Oct 2026 12:53:50 +0300 Subject: [PATCH] 0.16.14: watchTransfersTo reads the block as the node serves it, so a block carrying a post-quantum transaction (type 0x50, AIP-20) no longer throws and a deposit made with one is credited; transfersToFromRawBlock exported and tested offline on the real transaction of block 19,900,028 --- README.md | 10 +++++-- package.json | 2 +- src/client.ts | 34 ++++++++++++++++----- src/test/transfers-raw-block.test.ts | 44 ++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 src/test/transfers-raw-block.test.ts diff --git a/README.md b/README.md index 499d895..330f191 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,14 @@ live AERE L1 (chain ID 2800): the DeFi, agentic-settlement, compliance, account-abstraction and post-quantum contract surface, the seedless hybrid PQC wallet, and AERE's native NIST post-quantum precompiles. -**SDK version:** 0.16.13 -**Last updated:** 2026-10-15 (network facts, state window, client selectors; the rest is as of 2026-07-16) +**SDK version:** 0.16.14 +**Last updated:** 2026-10-20 (deposit watching on post-quantum transactions; network facts, state window, client selectors 2026-10-15; the rest is as of 2026-07-16) + +**0.16.14, 2026-10-20: `watchTransfersTo` reads post-quantum transactions.** Until this release it asked ethers for the block with +full transactions; ethers does not know the post-quantum transaction type 0x50 (AIP-20), so on a block carrying one (the first on chain +2800 is block 19,900,028) the handler threw, every deposit of that block was missed, and a deposit made with a post-quantum transaction +was never credited. The block is now read as the node serves it (`eth_getBlockByNumber`), and the filter is the exported +`transfersToFromRawBlock(block, addr)`, tested offline on the real transaction of that block (`src/test/transfers-raw-block.test.ts`). **0.16.13, 2026-10-15: client selectors repaired.** Until this release about 130 hand-written function selectors in ten clients (RaaS, Sink, Agent and x402 settlement, state channels, compliance, lending, the compliant pool) did not match diff --git a/package.json b/package.json index 4272c62..00ff871 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aere/sdk", - "version": "0.16.13", + "version": "0.16.14", "description": "Official AERE Network SDK: typed ethers v6 clients for AERE L1 (chain ID 2800). 35+ typed clients over a 130+ contract address book, covering core DeFi, agentic x402 settlement, compliance, ERC-4337 account abstraction, t-of-n post-quantum custody, the 5 live NIST PQC precompiles (Falcon-512/1024, ML-DSA-44, SLH-DSA-128s, SHAKE256), and a seedless hybrid PQC wallet.", "type": "module", "main": "./dist/index.js", diff --git a/src/client.ts b/src/client.ts index c4e8153..a9de6b3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -242,19 +242,39 @@ export class AereClient { return () => { ws.removeAllListeners('block'); ws.destroy(); }; } - /** Watch native AERE deposits to a specific address. Useful for "credit user's neobank account when they receive AERE". */ + /** + * Watch native AERE deposits to a specific address. Useful for "credit user's neobank account when they receive AERE". + * The block is read as the node serves it (eth_getBlockByNumber), not decoded by ethers: ethers does not know the post-quantum + * transaction type 0x50 (AIP-20), so until 0.16.14 a block carrying one threw, every deposit in that block was missed, and a deposit + * made WITH a post-quantum transaction was never credited. + */ async watchTransfersTo(addr: string, handler: (tx: { hash: string; value: bigint; from: string; blockNumber: number }) => void, wsUrl?: string): Promise<() => void> { const ws = new WebSocketProvider(wsUrl ?? AERE_MAINNET.ws); const onBlock = async (blockNumber: number) => { - const block = await ws.getBlock(blockNumber, true); + const block = await ws.send('eth_getBlockByNumber', [ethers.toQuantity(blockNumber), true]); if (!block) return; - for (const tx of block.prefetchedTransactions ?? []) { - if (tx.to?.toLowerCase() === addr.toLowerCase() && tx.value > 0n) { - handler({ hash: tx.hash, value: tx.value, from: tx.from, blockNumber }); - } - } + for (const tx of transfersToFromRawBlock(block, addr)) handler({ ...tx, blockNumber }); }; ws.on('block', onBlock); return () => { ws.off('block', onBlock); ws.destroy(); }; } } + +/** + * The native AERE transfers to `addr` in a block as the node serves it (eth_getBlockByNumber with full transactions): every transaction + * type, the post-quantum type 0x50 (AIP-20) included, since only hash, from, to and value are read. A zero value or a contract creation + * (no `to`) is not a transfer. + */ +export function transfersToFromRawBlock( + block: { transactions?: Array<{ hash: string; from: string; to?: string | null; value?: string }> }, + addr: string, +): Array<{ hash: string; value: bigint; from: string }> { + const want = addr.toLowerCase(); + const out: Array<{ hash: string; value: bigint; from: string }> = []; + for (const tx of block.transactions ?? []) { + if (typeof tx !== 'object' || !tx.to || tx.to.toLowerCase() !== want) continue; + const value = BigInt(tx.value ?? '0x0'); + if (value > 0n) out.push({ hash: tx.hash, value, from: ethers.getAddress(tx.from) }); + } + return out; +} diff --git a/src/test/transfers-raw-block.test.ts b/src/test/transfers-raw-block.test.ts new file mode 100644 index 0000000..f75d443 --- /dev/null +++ b/src/test/transfers-raw-block.test.ts @@ -0,0 +1,44 @@ +// Offline tests for transfersToFromRawBlock (0.16.14): watchTransfersTo reads the block as the node serves it, so a block carrying the +// post-quantum transaction type 0x50 (AIP-20) no longer throws, and a deposit made WITH such a transaction is credited. +// The first fixture is the real transaction of chain 2800 block 19,900,028 (2026-09-23), trimmed to the fields the function reads; on that +// block ethers' getBlock(n, true) throws "missing r", which is what made every deposit of the block disappear before 0.16.14. +// +// Run: npm run build && node --test dist/test/transfers-raw-block.test.js + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { transfersToFromRawBlock } from '../client.js'; + +const REAL_0X50 = { hash: '0x7cb5bdaceb475a6fee881c6a68aadf69734e78c1fd5648865c4fdde243d602e5', from: '0xa5bd56abf32ab382678e311c12fe14b736153f0c', + to: '0xa5bd56abf32ab382678e311c12fe14b736153f0c', value: '0x0', type: '0x50', algId: '0x2', nonce: '0x0' }; +const USER = '0x1111111111111111111111111111111111111111'; +const OTHER = '0x2222222222222222222222222222222222222222'; + +test('the real 0x50 block of 2800 is read without throwing; its self-transfer of zero is not a deposit', () => { + assert.deepEqual(transfersToFromRawBlock({ transactions: [REAL_0X50] }, REAL_0X50.to), []); +}); + +test('a deposit made WITH a post-quantum transaction (type 0x50) is credited, with its exact value and a checksummed sender', () => { + const tx = { ...REAL_0X50, hash: '0x' + 'ab'.repeat(32), to: USER, value: '0xde0b6b3a7640000' }; + const got = transfersToFromRawBlock({ transactions: [tx] }, USER.toUpperCase().replace('0X', '0x')); + assert.equal(got.length, 1); + assert.equal(got[0].value, 10n ** 18n); + assert.equal(got[0].from, '0xa5bD56abf32Ab382678e311C12FE14b736153f0c'); +}); + +test('legacy and EIP-1559 deposits to the address are credited; other recipients, zero values and contract creations are not', () => { + const txs = [ + { hash: '0x01', from: OTHER, to: USER, value: '0x5', type: '0x0' }, + { hash: '0x02', from: OTHER, to: USER, value: '0x7', type: '0x2' }, + { hash: '0x03', from: USER, to: OTHER, value: '0x9', type: '0x2' }, + { hash: '0x04', from: OTHER, to: USER, value: '0x0', type: '0x2' }, + { hash: '0x05', from: OTHER, to: null, value: '0x9', type: '0x2' }, + ]; + const got = transfersToFromRawBlock({ transactions: txs }, USER); + assert.deepEqual(got.map((x) => [x.hash, x.value]), [['0x01', 5n], ['0x02', 7n]]); +}); + +test('a block with only transaction hashes (no full transactions) or none at all gives nothing, never throws', () => { + assert.deepEqual(transfersToFromRawBlock({ transactions: ['0x01' as unknown as never] }, USER), []); + assert.deepEqual(transfersToFromRawBlock({}, USER), []); +});