@ninsei-labs/sdk 0.51.2 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 0.52.0 — 2026-10-26
2
+
3
+ THE NEXT CONTRACT VERSION IS WIRED IN.
4
+
5
+ - `arbitrum-sepolia` moves to factory `0x69b5a6878b542960af4d239ecc60ad1c9e3e6932`, cashier
6
+ `0x05b7433f66f2b75cc7ece9ec00af0fb810f9c9e0`, registry
7
+ `0x4707a6684b654fbd74372ec624efc0f825af0537` and implementation code hash
8
+ `0xc6cde36301b6551a6ca8bd2c47fbd5ffd01ffcdf22d2f6909be2b3b2dcf360b4`
9
+ (deployment block `323014007`).
10
+ - `OrderCreated` now carries `xmrAmount`. `findSwaps` reads it from the current event and reports
11
+ `xmrAmountReason: "event"`; orders from the previous event keep the clone word-18 fallback.
12
+ - The previous event topic is still accepted, so an old factory remains searchable.
13
+
1
14
  ## 0.51.2 — 2026-10-22
2
15
 
3
16
  - The withdrawal window falls back to the full scan when `scanTxs` or the balance read fails, and reports
@@ -363,35 +363,22 @@ export const CHAINS = [
363
363
  explorerName: "arbiscan (sepolia)",
364
364
  gasUsd: 0.01,
365
365
  routeType: "atomic",
366
- // THE DEMO'S MAIN NETWORK: the Ninsei escrow factory and the interface's fee cashier live here; the
367
- // ArrakisFeeRouter is gone. The factory is a FACT, not an intention:
368
- // 0x3eE25f12758ec28e5b3045d16132D89Cd0bf341F (height 321430239, transaction
369
- // 0x61da6d34c77dee1e1ada04dbbe9cbb1c86e5297acbc5a5b08320df20a2cf3ef6, code 2049 bytes, keccak256
370
- // 0xf490764213ae9569f9ef19a174e237f4a22c3ff6613756852571672ae7dea2a8); every order is an EIP-1167 clone and
371
- // the factory carries the pair (implementation(), implementationCodeHash()). The fee cashier is the second CREATE
372
- // of the same pass - 0x1123A3875775Da25FA181Af0d91EB278E94a5dfb (height 321430248, code 582 bytes, keccak256
373
- // 0xf5cb00c7974e6fab3f04dbd2cdc24976249dc4d55de2fa31524bce60205a1877). This release did NOT deploy a maker
374
- // registry (DEPLOY_REGISTRY=false): the registry is a separate contract, and the node must name an existing one
375
- // in RFQ_REGISTRY_ADDRESS until a fresh one is deployed and its keys are registered.
376
- // `npm run check:deployed -- --address=0x3eE25f12758ec28e5b3045d16132D89Cd0bf341F`).
377
- // PREVIOUS (the factory before claimTip): 0x1029D6bC53f6b9919F8ec6Ac922327F7e5A3DdF2 (height 318033203,
378
- // transaction 0x0f82c556f056f2dcaba5c12da75a749c257dfb1ccbf0eb6962a4e7f0119d166d, code 4226 bytes, keccak256
379
- // 0x4b515f600c641bffde054b3a98f4e26bd43d2dd2b2d8e60e8cfe36ee03686dd9). PREVIOUS (the ArrakisFeeRouter of
380
- // the previous rules, now removed): 0xaEB1c0455C76551dcEb2E6B1245B65F0458b0aE8 (height 314323871, code 8223
381
- // bytes, keccak256 0x90f26f641b48a2a630463195d4429b0818d29187563e09711a40811fcf08a0ba).
382
- // PREVIOUS (ceiling 3%, no early refund): 0xEa619bDC78989627093B7830990367B2ECE52CE5 (height 314309038,
383
- // code 8143 bytes). PREVIOUS live (router with a 10% ceiling): 0x9f856cd9c14fd9493961473b804039b09a4012ef
384
- // (height 313597131, code 8723 bytes). PREVIOUS before it (factory without a fee):
385
- // 0xcb54af9ec11344d448c7900ccb08530a23783259 (height 312222006, code 6035 bytes), and earlier ones still.
386
- // mode: "live" - the form sends REAL transactions: order creation TOGETHER WITH THE MONEY in one transaction
387
- // (createOrderAndFund; the escrow has no separate funding function - an order is born funded, see FACTORY_METHODS
388
- // in www/js/evm/factory.js and ESCROW_METHODS in escrow.js). The old lock(sealedHalf) and hashlock are gone: the
389
- // half is not sent but revealed by presentation. Switching back to "simulated" is one edit, to show a demo with
390
- // no money movement. cashier: the INTERFACE's fee cashier, named in quotes and now deployed (the second CREATE of
391
- // the same pass). implementationCodeHash: the escrow implementation the factory carries, checked against the chain.
392
- // deploymentBlock: the height the factory was created at; a log search for its orders starts here, not at zero,
393
- // because public RPCs cap a log query at about 10M blocks and would refuse a scan from genesis.
394
- escrow: { address: "0x3eE25f12758ec28e5b3045d16132D89Cd0bf341F", mode: "live", deploymentBlock: 321430239, cashier: "0x1123A3875775Da25FA181Af0d91EB278E94a5dfb", implementationCodeHash: "0xf97af2b4eed8c646d82bce001e52ff03c065d43127073f8ee52271b65c35d659" },
366
+ // THE DEMO'S MAIN NETWORK: the Ninsei escrow factory and the interface's fee cashier live here. The next
367
+ // contract version (contracts-testnet-2026.10.26) is a FACT, not an intention:
368
+ // factory 0x69b5a6878b542960af4d239ecc60ad1c9e3e6932 (height 323014007,
369
+ // transaction 0xbe8244da0cb1429edc92d18c7c8b5ba1c6b17c42a5806609d930a6496e918408);
370
+ // cashier 0x05b7433f66f2b75cc7ece9ec00af0fb810f9c9e0;
371
+ // registry 0x4707a6684b654fbd74372ec624efc0f825af0537 (this release deploys it);
372
+ // implementation 0x2E0214c6242fB794a4cC861D609Ecee544735170, codehash
373
+ // 0xc6cde36301b6551a6ca8bd2c47fbd5ffd01ffcdf22d2f6909be2b3b2dcf360b4.
374
+ // `OrderCreated` now carries xmrAmount; the SDK reads it from the event and keeps the clone fallback for the
375
+ // previous event. PREVIOUS factory before this version: 0x3eE25f12758ec28e5b3045d16132D89Cd0bf341F
376
+ // (height 321430239, cashier 0x1123A3875775Da25FA181Af0d91EB278E94a5dfb).
377
+ // mode: "live" - order creation TOGETHER WITH THE MONEY in one transaction (createOrderAndFund). cashier: the
378
+ // INTERFACE's fee cashier, named in quotes. implementationCodeHash: the escrow implementation the factory
379
+ // carries, checked against the chain. deploymentBlock: the height the factory was created at; a log search for
380
+ // its orders starts here, not at zero, because public RPCs cap a log query at about 10M blocks.
381
+ escrow: { address: "0x69b5a6878b542960af4d239ecc60ad1c9e3e6932", mode: "live", deploymentBlock: 323014007, cashier: "0x05b7433f66f2b75cc7ece9ec00af0fb810f9c9e0", implementationCodeHash: "0xc6cde36301b6551a6ca8bd2c47fbd5ffd01ffcdf22d2f6909be2b3b2dcf360b4" },
395
382
  tokens: [
396
383
  // Test USDC (Circle): decimals 6 and symbol "USDC" verified on chain by the same node.
397
384
  { symbol: "USDC", address: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", decimals: 6, usd: 1, cls: "ar-token-usdc" },
@@ -22,11 +22,18 @@ export const FACTORY_METHODS = {
22
22
  createOrderAndFund: "0x3a504547",
23
23
  };
24
24
 
25
- // The OrderCreated event topic. It grew the registry address, the provider and the signing key:
26
- // cast keccak "OrderCreated(address,address,address,bytes32,bytes32,bytes32,bytes32,bytes32,uint256,uint64,uint64,bytes32,address,address,address)"
25
+ // The OrderCreated event topic. It grew the registry address, the provider and the signing key, and now carries
26
+ // `xmrAmount` in bulk (#32):
27
+ // cast keccak "OrderCreated(address,address,address,bytes32,bytes32,bytes32,bytes32,bytes32,uint256,uint256,uint64,uint64,bytes32,address,address,address)"
27
28
  // The topic lives in TWO places and both must match; a check pins them.
28
29
  export const ORDER_CREATED_TOPIC =
30
+ "0x09add13cca47b227c377447295bfaa00ef3169bfed0f5513d06f8be04af78aad";
31
+
32
+ // THE PREVIOUS EVENT, KEPT FOR ORDERS ALREADY ON CHAIN. It has no `xmrAmount`, so those orders still fall back to
33
+ // reading word 18 of the clone's code. A new order uses the topic above.
34
+ export const ORDER_CREATED_TOPIC_V1 =
29
35
  "0xa22cdabaed3cce4f51dde71626a108ffe9c7bc4216fd14d5e719f892e2fd8ab0";
36
+ export const ORDER_CREATED_TOPICS = Object.freeze([ORDER_CREATED_TOPIC, ORDER_CREATED_TOPIC_V1]);
30
37
 
31
38
  // QUOTE FIELDS IN CONTRACT ORDER. The order is part of the ABI: a swap gives another call. One list for both
32
39
  // functions, so they cannot diverge.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninsei-labs/sdk",
3
- "version": "0.51.2",
3
+ "version": "0.52.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -333,10 +333,11 @@ export interface FoundSwap {
333
333
  readonly payToken: string | null;
334
334
  /** Why `payToken` is absent. `not-on-chain` means the order event and clone carry no token; do not infer one. */
335
335
  readonly payTokenReason: "not-on-chain";
336
- /** The XMR side of the order in atomic units, read from the clone's condition word 18; null when no code reader
337
- * was passed or the contract is not the expected clone. */
336
+ /** The XMR side of the order in atomic units. The current factory emits it in `OrderCreated`; for an older event
337
+ * it is read from the clone's condition word 18, and null when no code reader was passed or the contract is not
338
+ * the expected clone. */
338
339
  readonly xmrAmount: string | null;
339
- readonly xmrAmountReason: "no-code-reader" | "code" | "not-clone" | null;
340
+ readonly xmrAmountReason: "event" | "no-code-reader" | "code" | "not-clone" | null;
340
341
  readonly index: number;
341
342
  readonly side: "locker" | "claimer";
342
343
  /** The Monero height a withdrawal may start from, derived from the order's block time. Null when the caller did
@@ -17,7 +17,7 @@ import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from "@scure/b
17
17
  import { wordlist } from "@scure/bip39/wordlists/english.js";
18
18
  import { createHalves } from "../engine/atomic/halves.js";
19
19
  import { halfCommitmentHex } from "../engine/atomic/orderHalfCommit.js";
20
- import { ORDER_CREATED_TOPIC } from "../engine/evm/factory.js";
20
+ import { ORDER_CREATED_TOPIC, ORDER_CREATED_TOPICS } from "../engine/evm/factory.js";
21
21
  import { SdkError } from "./errors.mjs";
22
22
  import { probeMoneroNode } from "./probes.mjs";
23
23
  import { ed25519, keccak256, randomBytes, secp256k1 } from "./primitives.mjs";
@@ -359,10 +359,15 @@ function topicAddress(address) {
359
359
 
360
360
  function decodeOrderLog(log) {
361
361
  const topics = Array.isArray(log && log.topics) ? log.topics : [];
362
- if (topics.length < 4 || String(topics[0]).toLowerCase() !== ORDER_CREATED_TOPIC) return null;
362
+ const topic0 = String(topics[0] || "").toLowerCase();
363
+ if (topics.length < 4 || !ORDER_CREATED_TOPICS.includes(topic0)) return null;
363
364
  const data = String(log.data || "").replace(/^0x/, "");
364
- if (data.length < 64 * 2) return null;
365
+ const modern = topic0 === ORDER_CREATED_TOPIC;
366
+ if (data.length < 64 * (modern ? 13 : 12)) return null;
365
367
  const word = (i) => "0x" + data.slice(i * 64, (i + 1) * 64).toLowerCase();
368
+ // THE FIELD OFFSETS MOVED WHEN `xmrAmount` JOINED THE EVENT. The old topic has no XMR amount; its orders fall
369
+ // back to the clone read, and a caller can still tell why from `xmrAmountReason`.
370
+ const shift = modern ? 1 : 0;
366
371
  return {
367
372
  escrow: "0x" + String(topics[1]).slice(26).toLowerCase(),
368
373
  locker: "0x" + String(topics[2]).slice(26).toLowerCase(),
@@ -373,12 +378,13 @@ function decodeOrderLog(log) {
373
378
  edPointClaimer: word(3),
374
379
  edViewPointLocker: word(4),
375
380
  amount: BigInt(word(5)).toString(),
376
- readyBy: Number(BigInt(word(6))),
377
- t1: Number(BigInt(word(7))),
378
- salt: word(8),
379
- registry: "0x" + data.slice(9 * 64 + 24, 10 * 64),
380
- provider: "0x" + data.slice(10 * 64 + 24, 11 * 64),
381
- quoteKey: "0x" + data.slice(11 * 64 + 24, 12 * 64),
381
+ xmrAmount: modern ? BigInt(word(6)).toString() : null,
382
+ readyBy: Number(BigInt(word(6 + shift))),
383
+ t1: Number(BigInt(word(7 + shift))),
384
+ salt: word(8 + shift),
385
+ registry: "0x" + data.slice((9 + shift) * 64 + 24, (10 + shift) * 64),
386
+ provider: "0x" + data.slice((10 + shift) * 64 + 24, (11 + shift) * 64),
387
+ quoteKey: "0x" + data.slice((11 + shift) * 64 + 24, (12 + shift) * 64),
382
388
  transactionHash: log.transactionHash || null,
383
389
  blockNumber: log.blockNumber != null ? Number(log.blockNumber) : null,
384
390
  logIndex: log.logIndex != null ? Number(log.logIndex) : null,
@@ -431,7 +437,7 @@ async function readLogs(evmLogs, request) {
431
437
  export async function recoveryIndexTaken({ mnemonic, chainId, index, factory, evmLogs, fromBlock = 0, toBlock = "latest" } = {}) {
432
438
  if (typeof factory !== "string" || !ADDRESS_RE.test(factory)) throw bad("factory");
433
439
  const own = takerHalves({ mnemonic, chainId, index });
434
- const logs = await readLogs(evmLogs, { address: factory, topics: [ORDER_CREATED_TOPIC], fromBlock, toBlock });
440
+ const logs = await readLogs(evmLogs, { address: factory, topics: [ORDER_CREATED_TOPICS], fromBlock, toBlock });
435
441
  return logs.map(decodeOrderLog).filter(Boolean).some((order) =>
436
442
  order.commitHalfLocker.toLowerCase() === own.spendCommitment.toLowerCase() ||
437
443
  order.commitHalfClaimer.toLowerCase() === own.spendCommitment.toLowerCase());
@@ -581,8 +587,8 @@ export async function findSwaps({ mnemonic, chainId, factory, address = null, ev
581
587
  if (address) {
582
588
  const padded = topicAddress(address);
583
589
  const [asLocker, asClaimer] = await Promise.all([
584
- readLogs(evmLogs, { ...base, topics: [ORDER_CREATED_TOPIC, null, padded, null] }),
585
- readLogs(evmLogs, { ...base, topics: [ORDER_CREATED_TOPIC, null, null, padded] }),
590
+ readLogs(evmLogs, { ...base, topics: [ORDER_CREATED_TOPICS, null, padded, null] }),
591
+ readLogs(evmLogs, { ...base, topics: [ORDER_CREATED_TOPICS, null, null, padded] }),
586
592
  ]);
587
593
  const seen = new Set();
588
594
  for (const log of asLocker.concat(asClaimer)) {
@@ -592,7 +598,7 @@ export async function findSwaps({ mnemonic, chainId, factory, address = null, ev
592
598
  logs.push(log);
593
599
  }
594
600
  } else {
595
- logs = await readLogs(evmLogs, { ...base, topics: [ORDER_CREATED_TOPIC] });
601
+ logs = await readLogs(evmLogs, { ...base, topics: [ORDER_CREATED_TOPICS] });
596
602
  }
597
603
  const orders = logs.map(decodeOrderLog).filter(Boolean);
598
604
  const found = [];
@@ -637,9 +643,14 @@ export async function findSwaps({ mnemonic, chainId, factory, address = null, ev
637
643
  for (const order of found) {
638
644
  Object.assign(order, await recoveredStartHeight(order, { evmBlockTime, moneroTip: tip, moneroBlockTimestamp: header, cache, readersReason }));
639
645
  }
640
- // THE XMR AMOUNT IS IN THE CLONE, NOT IN THE LOG. A caller without a code reader gets null and a named reason;
641
- // an unreadable or differently-shaped contract does the same. This is data, not a withdrawal verdict.
646
+ // THE XMR AMOUNT COMES FROM THE EVENT IN THE CURRENT FACTORY. The previous event has no field; those orders fall
647
+ // back to the clone read, where word 18 is read only from a clone of the exact shape. This is data, not a
648
+ // withdrawal verdict.
642
649
  for (const order of found) {
650
+ if (order.xmrAmount !== null && order.xmrAmount !== undefined) {
651
+ order.xmrAmountReason = "event";
652
+ continue;
653
+ }
643
654
  if (typeof evmCode !== "function") {
644
655
  order.xmrAmount = null;
645
656
  order.xmrAmountReason = "no-code-reader";
package/src/version.mjs CHANGED
@@ -6,4 +6,4 @@
6
6
  // tools/sync-version.mjs. Do not edit it by hand - set the version in package.json and run `npm run sync:version`.
7
7
  // The `version` check in tools/check.mjs refuses a mismatch, and refuses a runtime read of package.json here.
8
8
 
9
- export const SDK_VERSION = "0.51.2";
9
+ export const SDK_VERSION = "0.52.0";