@pafi-dev/issuer 0.5.6 → 0.5.7

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.cjs CHANGED
@@ -1300,6 +1300,20 @@ var PTRedeemHandler = class {
1300
1300
  redeemLockDurationMs;
1301
1301
  signatureDeadlineSeconds;
1302
1302
  now;
1303
+ /**
1304
+ * Per-user in-flight nonce guard (single-process only).
1305
+ *
1306
+ * Prevents two concurrent requests from reading the same on-chain
1307
+ * burnRequestNonce before either has completed, which would produce two
1308
+ * signed UserOps with the same nonce — only one succeeds on-chain; the
1309
+ * other leaves an orphaned pending credit and a wasted signer call.
1310
+ *
1311
+ * NOTE: This guard is effective only within a single Node.js process. For
1312
+ * multi-instance deployments (k8s, PM2 cluster), enforce mutual exclusion
1313
+ * via a distributed lock (Redis SETNX / Postgres advisory lock) keyed on
1314
+ * `(userAddress, pointTokenAddress)` BEFORE calling `handle()`.
1315
+ */
1316
+ inFlightNonces = /* @__PURE__ */ new Map();
1303
1317
  constructor(config) {
1304
1318
  if (!config.ledger.reservePendingCredit) {
1305
1319
  throw new PTRedeemError(
@@ -1352,6 +1366,27 @@ var PTRedeemHandler = class {
1352
1366
  `failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
1353
1367
  );
1354
1368
  }
1369
+ const userKey = (0, import_viem7.getAddress)(request.userAddress).toLowerCase();
1370
+ let userNonces = this.inFlightNonces.get(userKey);
1371
+ if (!userNonces) {
1372
+ userNonces = /* @__PURE__ */ new Set();
1373
+ this.inFlightNonces.set(userKey, userNonces);
1374
+ }
1375
+ if (userNonces.has(burnNonce)) {
1376
+ throw new PTRedeemError(
1377
+ "NONCE_IN_FLIGHT",
1378
+ `A burn request for nonce ${burnNonce} is already in progress for ${request.userAddress}. Retry after the current request completes.`
1379
+ );
1380
+ }
1381
+ userNonces.add(burnNonce);
1382
+ try {
1383
+ return await this._handleAfterNonceLock(request, burnNonce);
1384
+ } finally {
1385
+ userNonces.delete(burnNonce);
1386
+ if (userNonces.size === 0) this.inFlightNonces.delete(userKey);
1387
+ }
1388
+ }
1389
+ async _handleAfterNonceLock(request, burnNonce) {
1355
1390
  const onChainBalance = await (0, import_core4.getPointTokenBalance)(
1356
1391
  this.provider,
1357
1392
  this.pointTokenAddress,
@@ -1858,6 +1893,40 @@ var PafiBackendClient = class {
1858
1893
  }
1859
1894
  throw lastError;
1860
1895
  }
1896
+ async relayUserOperation(request) {
1897
+ const fetchFn = this.config.fetchImpl ?? fetch;
1898
+ const url = `${this.config.url}/bundler/relay`;
1899
+ let response;
1900
+ try {
1901
+ response = await fetchFn(url, {
1902
+ method: "POST",
1903
+ headers: {
1904
+ "Content-Type": "application/json",
1905
+ Authorization: `Bearer ${this.config.apiKey}`,
1906
+ "X-Issuer-Id": this.config.issuerId
1907
+ },
1908
+ body: JSON.stringify(request)
1909
+ });
1910
+ } catch (err) {
1911
+ throw new PafiBackendError(
1912
+ "NETWORK_ERROR",
1913
+ `Network error: ${err instanceof Error ? err.message : String(err)}`,
1914
+ 0
1915
+ );
1916
+ }
1917
+ const text = await response.text();
1918
+ let json = {};
1919
+ try {
1920
+ json = JSON.parse(text);
1921
+ } catch {
1922
+ }
1923
+ if (!response.ok) {
1924
+ const code = json.code ?? "INTERNAL_ERROR";
1925
+ const message = json.message ?? `HTTP ${response.status}`;
1926
+ throw new PafiBackendError(code, message, response.status, json);
1927
+ }
1928
+ return { userOpHash: json.userOpHash };
1929
+ }
1861
1930
  async _doRequest(request) {
1862
1931
  const fetchFn = this.config.fetchImpl ?? fetch;
1863
1932
  const url = `${this.config.url}/paymaster/sponsor`;