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

This commit is contained in:
Aere Network 2026-10-20 12:53:50 +03:00
parent 7c64a252e5
commit 0939e6192d
4 changed files with 80 additions and 10 deletions

View File

@ -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 account-abstraction and post-quantum contract surface, the seedless hybrid PQC
wallet, and AERE's native NIST post-quantum precompiles. wallet, and AERE's native NIST post-quantum precompiles.
**SDK version:** 0.16.13 **SDK version:** 0.16.14
**Last updated:** 2026-10-15 (network facts, state window, client selectors; the rest is as of 2026-07-16) **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 **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 clients (RaaS, Sink, Agent and x402 settlement, state channels, compliance, lending, the compliant pool) did not match

View File

@ -1,6 +1,6 @@
{ {
"name": "@aere/sdk", "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.", "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", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@ -242,19 +242,39 @@ export class AereClient {
return () => { ws.removeAllListeners('block'); ws.destroy(); }; 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> { 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 ws = new WebSocketProvider(wsUrl ?? AERE_MAINNET.ws);
const onBlock = async (blockNumber: number) => { 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; if (!block) return;
for (const tx of block.prefetchedTransactions ?? []) { for (const tx of transfersToFromRawBlock(block, addr)) handler({ ...tx, blockNumber });
if (tx.to?.toLowerCase() === addr.toLowerCase() && tx.value > 0n) {
handler({ hash: tx.hash, value: tx.value, from: tx.from, blockNumber });
}
}
}; };
ws.on('block', onBlock); ws.on('block', onBlock);
return () => { ws.off('block', onBlock); ws.destroy(); }; 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;
}

View File

@ -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), []);
});