@pafi-dev/issuer 0.22.0 → 0.23.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/dist/index.d.cts CHANGED
@@ -967,12 +967,37 @@ interface BurnEvent {
967
967
  * number it is about to process so the caller can write it to Redis /
968
968
  * Postgres / a file. The SDK does not own persistence because every
969
969
  * issuer has their own storage stack.
970
+ *
971
+ * **Per-token keying (audit finding H-05).** When multiple PointTokens
972
+ * are wired through a single `createIssuerService` call, each
973
+ * `PointIndexer` MUST get its own cursor — otherwise token A advances
974
+ * the shared cursor past token B's events and token B's mints are
975
+ * never finalized (off-chain balance never deducts → on-chain PT
976
+ * supply for token B exceeds off-chain backing). Combined with the
977
+ * H-04 monotonic-save fix this becomes a catastrophic-and-stable
978
+ * state where token B's cursor can never catch up. Implementations
979
+ * SHOULD expose `forKey(key)` returning a derived store keyed under
980
+ * a distinct namespace; the SDK factory calls it once per token.
981
+ *
982
+ * When `forKey` is absent, the SDK falls back to the bare store and
983
+ * emits a runtime warning if more than one token is configured. New
984
+ * implementations are strongly encouraged to add `forKey`.
970
985
  */
971
986
  interface IIndexerCursorStore {
972
987
  /** Return the last persisted cursor (`undefined` on first run). */
973
988
  load(): Promise<bigint | undefined>;
974
989
  /** Persist a new cursor value. Called after each successful batch. */
975
990
  save(blockNumber: bigint): Promise<void>;
991
+ /**
992
+ * Return a derived store keyed under `key`. The returned store is
993
+ * an independent `IIndexerCursorStore` so the same persistence
994
+ * backend can serve N indexers with N distinct cursors.
995
+ *
996
+ * Optional for backwards compatibility, but REQUIRED in practice
997
+ * whenever more than one PointToken is wired through the SDK
998
+ * factory (see H-05).
999
+ */
1000
+ forKey?(key: string): IIndexerCursorStore;
976
1001
  }
977
1002
  /**
978
1003
  * No-op cursor store. Useful when the caller wants to drive the cursor
@@ -980,49 +1005,19 @@ interface IIndexerCursorStore {
980
1005
  */
981
1006
  declare class InMemoryCursorStore implements IIndexerCursorStore {
982
1007
  private cursor;
1008
+ /**
1009
+ * Child stores keyed by `forKey()`. Each child has its own cursor
1010
+ * (the H-05 fix), so a single InMemoryCursorStore can back N
1011
+ * PointIndexers in tests / single-process callers.
1012
+ */
1013
+ private readonly children;
983
1014
  load(): Promise<bigint | undefined>;
984
1015
  save(blockNumber: bigint): Promise<void>;
1016
+ forKey(key: string): IIndexerCursorStore;
985
1017
  }
986
- /**
987
- * Lock handle returned by a successful `ISingletonLock.acquire()`.
988
- *
989
- * The holder is the leader for the keyed indexer; non-holders MUST NOT
990
- * call `indexer.start()` (else they race against the leader's polling
991
- * loop and last-writer-wins cursor save corrupts replay state).
992
- *
993
- * `release()` is called by the leader on graceful shutdown. If the
994
- * leader crashes without calling release, the lock implementation
995
- * MUST drop the lock automatically (e.g. Postgres advisory locks
996
- * auto-release on connection close).
997
- */
998
1018
  interface SingletonLockHandle {
999
1019
  release(): Promise<void>;
1000
1020
  }
1001
- /**
1002
- * Leader-election primitive for indexer singletons.
1003
- *
1004
- * **Why this exists (audit finding H-04):** the `BurnIndexer` and
1005
- * `PointIndexer` classes are stateful polling loops with a cursor
1006
- * stored in an external store. Running them on multiple replicas
1007
- * simultaneously causes:
1008
- *
1009
- * 1. Double-credit — both replicas see the same Transfer event
1010
- * and call `ledger.resolveCreditByBurnTx` twice.
1011
- * 2. Cursor rewind — last-writer-wins `save()` causes the newer
1012
- * cursor to be overwritten by a lagging replica's older value,
1013
- * causing the next poll to replay events.
1014
- * 3. Skipped blocks — chunked range reads can leave gaps when two
1015
- * replicas race-advance the cursor.
1016
- *
1017
- * **Adoption:** pass `singletonLock` into `IssuerServiceConfig.indexer`
1018
- * (or wire it directly in your provider). The factory will only call
1019
- * `indexer.start()` on the indexers it successfully acquires a lock
1020
- * for; the rest stay idle and take over instantly on lock release.
1021
- *
1022
- * **Implementations:** `makePostgresSingletonLock(dataSource)` (recommended
1023
- * — uses `pg_try_advisory_lock`, auto-releases on connection close).
1024
- * You can also bring your own: Redis SETNX with TTL, etcd lease, etc.
1025
- */
1026
1021
  interface ISingletonLock {
1027
1022
  /**
1028
1023
  * Attempt to acquire the lock for `key`. Returns a handle on success
@@ -1242,7 +1237,7 @@ declare class BurnIndexer {
1242
1237
  private readonly provider;
1243
1238
  /**
1244
1239
  * The PointToken this indexer watches. Exposed so callers can key
1245
- * leader-election locks / cursor stores by token (audit H-04 fix).
1240
+ * leader-election locks / cursor stores by token
1246
1241
  */
1247
1242
  readonly pointTokenAddress: Address;
1248
1243
  private readonly ledger;
package/dist/index.d.ts CHANGED
@@ -967,12 +967,37 @@ interface BurnEvent {
967
967
  * number it is about to process so the caller can write it to Redis /
968
968
  * Postgres / a file. The SDK does not own persistence because every
969
969
  * issuer has their own storage stack.
970
+ *
971
+ * **Per-token keying (audit finding H-05).** When multiple PointTokens
972
+ * are wired through a single `createIssuerService` call, each
973
+ * `PointIndexer` MUST get its own cursor — otherwise token A advances
974
+ * the shared cursor past token B's events and token B's mints are
975
+ * never finalized (off-chain balance never deducts → on-chain PT
976
+ * supply for token B exceeds off-chain backing). Combined with the
977
+ * H-04 monotonic-save fix this becomes a catastrophic-and-stable
978
+ * state where token B's cursor can never catch up. Implementations
979
+ * SHOULD expose `forKey(key)` returning a derived store keyed under
980
+ * a distinct namespace; the SDK factory calls it once per token.
981
+ *
982
+ * When `forKey` is absent, the SDK falls back to the bare store and
983
+ * emits a runtime warning if more than one token is configured. New
984
+ * implementations are strongly encouraged to add `forKey`.
970
985
  */
971
986
  interface IIndexerCursorStore {
972
987
  /** Return the last persisted cursor (`undefined` on first run). */
973
988
  load(): Promise<bigint | undefined>;
974
989
  /** Persist a new cursor value. Called after each successful batch. */
975
990
  save(blockNumber: bigint): Promise<void>;
991
+ /**
992
+ * Return a derived store keyed under `key`. The returned store is
993
+ * an independent `IIndexerCursorStore` so the same persistence
994
+ * backend can serve N indexers with N distinct cursors.
995
+ *
996
+ * Optional for backwards compatibility, but REQUIRED in practice
997
+ * whenever more than one PointToken is wired through the SDK
998
+ * factory (see H-05).
999
+ */
1000
+ forKey?(key: string): IIndexerCursorStore;
976
1001
  }
977
1002
  /**
978
1003
  * No-op cursor store. Useful when the caller wants to drive the cursor
@@ -980,49 +1005,19 @@ interface IIndexerCursorStore {
980
1005
  */
981
1006
  declare class InMemoryCursorStore implements IIndexerCursorStore {
982
1007
  private cursor;
1008
+ /**
1009
+ * Child stores keyed by `forKey()`. Each child has its own cursor
1010
+ * (the H-05 fix), so a single InMemoryCursorStore can back N
1011
+ * PointIndexers in tests / single-process callers.
1012
+ */
1013
+ private readonly children;
983
1014
  load(): Promise<bigint | undefined>;
984
1015
  save(blockNumber: bigint): Promise<void>;
1016
+ forKey(key: string): IIndexerCursorStore;
985
1017
  }
986
- /**
987
- * Lock handle returned by a successful `ISingletonLock.acquire()`.
988
- *
989
- * The holder is the leader for the keyed indexer; non-holders MUST NOT
990
- * call `indexer.start()` (else they race against the leader's polling
991
- * loop and last-writer-wins cursor save corrupts replay state).
992
- *
993
- * `release()` is called by the leader on graceful shutdown. If the
994
- * leader crashes without calling release, the lock implementation
995
- * MUST drop the lock automatically (e.g. Postgres advisory locks
996
- * auto-release on connection close).
997
- */
998
1018
  interface SingletonLockHandle {
999
1019
  release(): Promise<void>;
1000
1020
  }
1001
- /**
1002
- * Leader-election primitive for indexer singletons.
1003
- *
1004
- * **Why this exists (audit finding H-04):** the `BurnIndexer` and
1005
- * `PointIndexer` classes are stateful polling loops with a cursor
1006
- * stored in an external store. Running them on multiple replicas
1007
- * simultaneously causes:
1008
- *
1009
- * 1. Double-credit — both replicas see the same Transfer event
1010
- * and call `ledger.resolveCreditByBurnTx` twice.
1011
- * 2. Cursor rewind — last-writer-wins `save()` causes the newer
1012
- * cursor to be overwritten by a lagging replica's older value,
1013
- * causing the next poll to replay events.
1014
- * 3. Skipped blocks — chunked range reads can leave gaps when two
1015
- * replicas race-advance the cursor.
1016
- *
1017
- * **Adoption:** pass `singletonLock` into `IssuerServiceConfig.indexer`
1018
- * (or wire it directly in your provider). The factory will only call
1019
- * `indexer.start()` on the indexers it successfully acquires a lock
1020
- * for; the rest stay idle and take over instantly on lock release.
1021
- *
1022
- * **Implementations:** `makePostgresSingletonLock(dataSource)` (recommended
1023
- * — uses `pg_try_advisory_lock`, auto-releases on connection close).
1024
- * You can also bring your own: Redis SETNX with TTL, etcd lease, etc.
1025
- */
1026
1021
  interface ISingletonLock {
1027
1022
  /**
1028
1023
  * Attempt to acquire the lock for `key`. Returns a handle on success
@@ -1242,7 +1237,7 @@ declare class BurnIndexer {
1242
1237
  private readonly provider;
1243
1238
  /**
1244
1239
  * The PointToken this indexer watches. Exposed so callers can key
1245
- * leader-election locks / cursor stores by token (audit H-04 fix).
1240
+ * leader-election locks / cursor stores by token
1246
1241
  */
1247
1242
  readonly pointTokenAddress: Address;
1248
1243
  private readonly ledger;
package/dist/index.js CHANGED
@@ -1122,14 +1122,28 @@ function createPafiEstimatorClient(config) {
1122
1122
  }
1123
1123
 
1124
1124
  // src/indexer/types.ts
1125
- var InMemoryCursorStore = class {
1125
+ var InMemoryCursorStore = class _InMemoryCursorStore {
1126
1126
  cursor;
1127
+ /**
1128
+ * Child stores keyed by `forKey()`. Each child has its own cursor
1129
+ * (the H-05 fix), so a single InMemoryCursorStore can back N
1130
+ * PointIndexers in tests / single-process callers.
1131
+ */
1132
+ children = /* @__PURE__ */ new Map();
1127
1133
  async load() {
1128
1134
  return this.cursor;
1129
1135
  }
1130
1136
  async save(blockNumber) {
1131
1137
  this.cursor = blockNumber;
1132
1138
  }
1139
+ forKey(key) {
1140
+ let child = this.children.get(key);
1141
+ if (!child) {
1142
+ child = new _InMemoryCursorStore();
1143
+ this.children.set(key, child);
1144
+ }
1145
+ return child;
1146
+ }
1133
1147
  };
1134
1148
 
1135
1149
  // src/indexer/pointIndexer.ts
@@ -1393,7 +1407,7 @@ var BurnIndexer = class {
1393
1407
  provider;
1394
1408
  /**
1395
1409
  * The PointToken this indexer watches. Exposed so callers can key
1396
- * leader-election locks / cursor stores by token (audit H-04 fix).
1410
+ * leader-election locks / cursor stores by token
1397
1411
  */
1398
1412
  pointTokenAddress;
1399
1413
  ledger;
@@ -4567,6 +4581,13 @@ async function createIssuerService(config) {
4567
4581
  const sdkWrapperAddress = getContractAddresses7(config.chainId).mintFeeWrapper;
4568
4582
  const wrapperOverride = config.indexer?.mintFeeWrapperAddress;
4569
4583
  const resolvedWrapperAddress = wrapperOverride !== void 0 ? wrapperOverride : sdkWrapperAddress;
4584
+ const baseCursorStore = config.indexer?.cursorStore;
4585
+ const sharedCursorWithMultipleTokens = baseCursorStore !== void 0 && typeof baseCursorStore.forKey !== "function" && tokenAddresses.length > 1;
4586
+ if (sharedCursorWithMultipleTokens) {
4587
+ console.warn(
4588
+ `[@pafi-dev/issuer] cursorStore lacks forKey() and ${tokenAddresses.length} PointTokens are configured. All PointIndexers will share one cursor row, causing token-skipping (audit finding H-05). Implement IIndexerCursorStore.forKey to return per-token derived stores. This permissive path will be removed in a future major release.`
4589
+ );
4590
+ }
4570
4591
  const indexers = /* @__PURE__ */ new Map();
4571
4592
  for (const tokenAddress of tokenAddresses) {
4572
4593
  const indexerConfig = {
@@ -4580,8 +4601,10 @@ async function createIssuerService(config) {
4580
4601
  if (config.indexer?.fromBlock !== void 0) {
4581
4602
  indexerConfig.fromBlock = config.indexer.fromBlock;
4582
4603
  }
4583
- if (config.indexer?.cursorStore) {
4584
- indexerConfig.cursorStore = config.indexer.cursorStore;
4604
+ if (baseCursorStore) {
4605
+ indexerConfig.cursorStore = typeof baseCursorStore.forKey === "function" ? baseCursorStore.forKey(
4606
+ `point-indexer:${tokenAddress.toLowerCase()}`
4607
+ ) : baseCursorStore;
4585
4608
  }
4586
4609
  if (config.indexer?.confirmations !== void 0) {
4587
4610
  indexerConfig.confirmations = config.indexer.confirmations;
@@ -4882,7 +4905,7 @@ var MemoryRedemptionHistoryStore = class {
4882
4905
  };
4883
4906
 
4884
4907
  // src/index.ts
4885
- var PAFI_ISSUER_SDK_VERSION = true ? "0.22.0" : "dev";
4908
+ var PAFI_ISSUER_SDK_VERSION = true ? "0.23.0" : "dev";
4886
4909
  export {
4887
4910
  AdapterMisconfiguredError,
4888
4911
  AuthError,