npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pafi-dev/issuer

v0.43.0

Published

Issuer backend API and services for the PAFI point token system

Readme

@pafi-dev/issuer

npm License: Apache-2.0

Backend SDK for PAFI issuer servers — claim, redeem, perp deposit, mobile prepare/submit, EIP-7702 delegation, status polling, redemption restriction enforcement, and the IssuerApiAdapter that thins issuer controllers to one line per endpoint.

Server-only. Pulls in signer wallets, HTTP clients, ledger interfaces. Don't bundle into a browser app — use @pafi-dev/trading (FE swap/quote) or @pafi-dev/core (primitives).


v0.20.x — Bundler-driven gas estimate

v0.20.0 — operator fee gasUnits now come from a Pimlico bundler estimate cached server-side at PAFI's sponsor-relayer (POST /v1/estimate-gas-fee). Eliminates the hardcoded SCENARIO_GAS_UNITS table at the consumer layer.

  • New: FeeManagerConfig.bundlerClient?: BundlerEstimatorClient. When set, feeManager.estimateGasFee({ partialUserOp, scenario, contractAddress }) fetches gas units from the wired estimator instead of running the legacy gasUnits × premium math. Backwards-compatible — omit bundlerClient and the legacy hardcoded path runs.
  • New: createPafiEstimatorClient({ baseUrl, apiKey, issuerId }) HTTP adapter — POSTs to PAFI sponsor-relayer's /v1/estimate-gas-fee. Pimlico key never touches issuer infra; the bundler call happens server-side at PAFI.
  • New: RelayService.previewMintUserOp / previewBurnUserOp — build a dummy partial UserOp (placeholder 65-byte sig, no HSM call) for the bundler estimate. PTClaimHandler / PTRedeemHandler wire these automatically.
  • Changed: DEFAULT_PREMIUM_BPS 12_000 → 10_000 (100%, no double-pad). PAFI sponsor-relayer applies its own 110% premium upstream; adding another SDK-side pad would over-charge users by ~21% vs. actual gas.
  • Changed: PTClaimHandler now passes the FeeManager-computed feeAmount explicitly into RelayService.prepareMint, so the in-batch PT transfer matches the value returned in the response. Pre-v0.20 the handler relied on RelayService.resolveFee falling back to quoteOperatorFeePt (which uses the legacy 120% premium) — causing a ~20% mismatch between displayed fee and actual transfer.

v0.21.0 — bug fix follow-up to v0.20: closes the resolveFee mismatch in PTClaimHandler (above). Strongly recommended over v0.20.0 for any new consumer wiring.

v0.15.x — Uniswap V3 migration (breaking)

v0.15.0 — initial V3 migration:

  • createSubgraphPoolsProvider returns the Uniswap V3 PoolKey shape ({ token0, token1, fee }) — sourced from @pafi-dev/core. Any consumer typed against an older shape with extra fields will see compile-time type errors and must be updated.
  • New onError?: (err: Error) => void option — forward recoverable errors (network, GraphQL, parse) to your observability stack. Throws inside the callback are swallowed so the provider stays total.
  • feeTier is range-checked (uint24, < 1_000_000) before building the PoolKey; invalid values are skipped with a console.error + onError invocation.
  • Subgraph endpoint default → …/pafi-subgraph-v4 (the active PAFI Base-mainnet subgraph; see @pafi-dev/core for the constant).
  • Bumps peer-deps on @pafi-dev/core to ^0.13.0 (V3 types + ABIs).

v0.15.1 — documentation-only cleanup: refreshed JSDoc comments in nativePtQuoter, feeManager, relayService to match the V3 implementation. No behavior change.

v0.15.2POINT_TOKEN_V2_ABIPOINT_TOKEN_ABI rename across core+issuer (canonical name, no version suffix). Breaking export name change; the back-compat alias was removed. Requires @pafi-dev/core ^0.14.0.

Requirements

  • Node.js ≥ 18
  • TypeScript ≥ 5.0
  • viem ^2.0.0 (peer)
  • @pafi-dev/core ^0.14.0 (transitive — re-exported)

Installation

pnpm add @pafi-dev/issuer @pafi-dev/core viem
# Optional ledger backend:
pnpm add @pafi-dev/issuer-postgres typeorm pg

What's in this package

@pafi-dev/issuer
├── handlers           — PTClaimHandler, PTRedeemHandler, PerpDepositHandler
├── api/IssuerApiAdapter
│                      — single class, every endpoint is one line in controller
├── api/handleMobilePrepare/Submit
│                      — mobile claim/redeem orchestrators
├── api/handleClaimStatus/handleRedeemStatus
│                      — polling helpers w/ bundler-receipt fallback
├── api/handleDelegateSubmit
│                      — EIP-7702 delegation submit (empty-batch + auth)
├── api/createSdkErrorMapper
│                      — framework-agnostic error → HTTP status mapper
├── ledger             — IPointLedger interface + InMemoryCursorStore
├── userop-store       — IPendingUserOpStore + MemoryPendingUserOpStore
├── pafi-backend       — sponsor-relayer client + relay/paymaster helpers
├── auth               — ISessionStore, AuthService (SIWE), MemorySessionStore
├── relay              — RelayService (wrapper-aware), FeeManager
├── pools              — createSubgraphPoolsProvider, NativePtQuoter
├── policy             — IPolicyEngine + DefaultPolicyEngine
├── indexer            — PointIndexer (wrapper + direct mode), BurnIndexer
├── issuer-state       — IssuerStateValidator (7-field struct + oracle.tokenCaps)
├── redemption         — RedemptionService (per-issuer policy enforcement)
└── errors             — PafiSdkError base class for typed errors

Architecture

HTTP request (NestJS / Fastify / Express)
    ↓
Auth guard            ← issuer-specific (SIWE / Privy / NextAuth)
    ↓
Controller            ← thin: routing + DTO + auth context
    ↓
IssuerApiAdapter      ← orchestrates flows
    ↓
PTClaimHandler        ← signs MintForRequest, locks balance, builds UserOp
                        (routes to wrapper.mintWithFee when wrapper configured)
PTRedeemHandler       ← signs BurnRequest, reserves credit, builds UserOp
PerpDepositHandler    ← Orderly Vault via PAFI Relay
    ↓
IPointLedger          ← your DB impl (Postgres recommended)
RelayService          ← UserOp builders (auto-branches direct vs wrapper)
PafiBackendClient     ← sponsor-relayer proxy

Wrapper-mediated mint flow

[FE / mobile] → POST /claim/prepare
    ↓
[gg56] IssuerApiAdapter.claimPrepare()
    ↓
[gg56] PTClaimHandler:
    1. Lock off-chain balance (gross amount)
    2. Pre-validate via IssuerStateValidator (oracle cap check)
    3. Sign MintForRequest EIP-712 with receiver = wrapper
    4. Build UserOp callData:
       BatchExecute([
         { mintFeeWrapper, mintWithFee(pt, user, gross, deadline, sig) },
         { pointToken, transfer(pafiFeeRecipient, operatorFeePT) }
       ])
    ↓
[gg56] PafiBackendClient.requestSponsorship() → sponsor-relayer
    ↓
[sponsor-relayer] decode calldata, recognize MINT_WITH_FEE (0x5284d08b),
                  validate intent, forward to Pimlico paymaster
    ↓
[gg56] returns { userOp, typedData, userOpHash, sponsored: true|false,
                 typedDataFallback, userOpHashFallback }
    ↓
[FE / mobile] sign typedData via signTypedData_v4
    ↓
[FE / mobile] POST /claim/submit { lockId, signature, variant }
    ↓
[gg56] forwards signed UserOp to Pimlico bundler → on-chain mint
    ↓
[chain] wrapper.mintWithFee → PointToken.mint(gross to wrapper) →
        wrapper splits fee to recipients → transfers net to user
    ↓
[gg56] PointIndexer (wrapper mode) catches MintWithFee event →
       deduct off-chain balance, lock → MINTED

Quick start (NestJS)

Minimal reference at examples/nestjs-issuer/ — full working backend.

1. Wire IssuerApiAdapter

import { Provider } from "@nestjs/common";
import {
  IssuerApiAdapter,
  IssuerStateValidator,
  PTClaimHandler,
  PTRedeemHandler,
  PerpDepositHandler,
  MemoryPendingUserOpStore,
  type IssuerService,
} from "@pafi-dev/issuer";
import { PostgresPointLedger } from "@pafi-dev/issuer-postgres";
import { getContractAddresses } from "@pafi-dev/core";

export const issuerApiAdapterProvider: Provider = {
  provide: ISSUER_API_ADAPTER,
  useFactory: (issuerService, provider, walletClient, dataSource, config) => {
    const ledger = new PostgresPointLedger(dataSource);
    const { issuerRegistry, batchExecutor, usdc } = getContractAddresses(8453);
    const chainId = config.get<number>("CHAIN_ID");
    const pointToken = config.get<`0x${string}`>("POINT_TOKEN_ADDRESS");

    return new IssuerApiAdapter({
      issuerService,
      ledger,
      provider,
      issuerSignerWallet: walletClient,
      pafiIssuerId: config.get("PAFI_ISSUER_ID"),

      ptClaimHandler: new PTClaimHandler({
        ledger,
        relayService: issuerService.relay,
        provider,
        issuerSignerWallet: walletClient,
        pointTokenDomainName: config.get("POINT_TOKEN_DOMAIN_NAME"),
        feeService: issuerService.fee,
        issuerStateValidator: new IssuerStateValidator(provider, issuerRegistry),
        // mintFeeWrapperAddress: optional override; SDK auto-resolves from chainId
      }),

      ptRedeemHandler: new PTRedeemHandler({
        ledger,
        relayService: issuerService.relay,
        provider,
        pointTokenAddress: pointToken,
        batchExecutorAddress: batchExecutor,
        chainId,
        domain: { name: config.get("POINT_TOKEN_DOMAIN_NAME"), verifyingContract: pointToken },
        burnerSignerWallet: walletClient,
        feeService: issuerService.fee,
      }),

      perpHandler: new PerpDepositHandler({
        provider, feeService: issuerService.fee, pointTokenAddress: pointToken,
      }),

      pendingUserOpStore: new MemoryPendingUserOpStore(),
      pafiBackendClient,  // optional — sponsor-relayer client for sponsored flows
    });
  },
  inject: [...],
};

Skip handlers you don't need — adapter throws clear " not wired" if the corresponding method is called.

2. Slim controller

@Controller()
export class IssuerController {
  constructor(@Inject(ISSUER_API_ADAPTER) private api: IssuerApiAdapter) {}

  @Post("claim/prepare")
  @UseGuards(JwtGuard)
  async claimPrepare(@User() user: AuthContext, @Body() body) {
    const [aaNonce, mintRequestNonce] = await Promise.all([
      fetchAaNonce(this.provider, user.userAddress),
      fetchMintRequestNonce(this.provider, body.pointTokenAddress, user.userAddress),
    ]);
    return wrap(() => this.api.claimPrepare({
      authenticatedAddress: user.userAddress,
      chainId: body.chainId,
      pointTokenAddress: body.pointTokenAddress,
      amount: BigInt(body.amount),
      aaNonce, mintRequestNonce,
    }));
  }
}

3. Wire error mapper

import { createSdkErrorMapper, type SdkErrorBody } from "@pafi-dev/issuer";

const sdkErrorMapper = createSdkErrorMapper({
  notFound:           (b) => new NotFoundException(b),
  forbidden:          (b) => new ForbiddenException(b),
  unprocessable:      (b) => new UnprocessableEntityException(b),
  serviceUnavailable: (b) => new ServiceUnavailableException(b),
});

async function wrap<T>(fn: () => Promise<T>): Promise<T> {
  try { return await fn(); }
  catch (err) { sdkErrorMapper(err); }
}

IssuerApiAdapter methods

| Method | HTTP route | Notes | | --- | --- | --- | | pools(authedAddr, chainId, pointToken) | GET /pools | | | user(authedAddr, chainId, userAddr, pointToken) | GET /user | | | config(chainId) | GET /config | Includes mintFeeBpsByToken + contracts.mintFeeWrapper | | claim({ ...nonces }) | POST /claim (web — sync calls[]) | | | redeem({ amount, aaNonce, ... }) | POST /redeem | | | perpDeposit({ amount, brokerId, aaNonce, ... }) | POST /perp-deposit | | | claimPrepare(...) / claimSubmit(...) | Mobile claim flow | wrapper-aware | | redeemPrepare(...) / redeemSubmit(...) | Mobile redeem flow | | | claimStatus(authedAddr, lockId) / redeemStatus(...) | Status polling | | | delegateStatus(authedAddr, chainId) | EIP-7702 — check delegation | | | delegatePrepare(authedAddr, chainId) | EIP-7702 — build auth hash | | | delegateSubmit({ authSig, ... }) | EIP-7702 — relay empty-batch | |


Redemption restriction (v0.10+)

Per-issuer policy enforced at /redeem/prepare time. Fetched from PAFI issuer-api with 5-min cache + fail-open default.

import { RedemptionService, PolicyProvider } from "@pafi-dev/issuer";
import { PostgresRedemptionHistoryStore } from "@pafi-dev/issuer-postgres";

const redemption = new RedemptionService({
  policyProvider: new PolicyProvider({
    chainId: 8453,
    issuerId: "gg56",
    apiKey: process.env.PAFI_API_KEY,
  }),
  historyStore: new PostgresRedemptionHistoryStore(dataSource),
});

Wire to createIssuerService({ redemption: {...} }). Adapter exposes /redemption/preview + /redemption/evaluate automatically.


Mobile prepare/submit flow

// 1. mobile → POST /claim/prepare → backend runs:
const prepared = await api.claimPrepare({
  authenticatedAddress, chainId, pointTokenAddress, amount, aaNonce, mintRequestNonce,
});
// returns: {
//   lockId, userOpHash, typedData,
//   userOpHashFallback, typedDataFallback,
//   sponsored,           // true if paymaster signed; false → use fallback
//   needsDelegation,     // true if user EOA not delegated yet
//   feeAmount, signatureDeadline, expiresInSeconds,
// }

// 2. mobile signs typedData via Privy/MetaMask signTypedData_v4
//    (NOT personal_sign — Pimlico Simple7702Account does raw ecrecover)
//    If `sponsored: false`, sign `typedDataFallback` instead

// 3. mobile → POST /claim/submit:
const result = await api.claimSubmit({
  authenticatedAddress, lockId, signature,
  variant: prepared.sponsored ? "sponsored" : "fallback",
});
// returns: { userOpHash } — bundler hash for status polling

handleMobileSubmit enforces ownership: entry.sender === authenticatedAddress. Lock has 15-min TTL.


PointIndexer modes

// Default — auto-resolves wrapper from chainId
const service = createIssuerService({
  chainId: 8453,
  // ...
  indexer: { autoStart: true, pollIntervalMs: 5000 },
});

// Indexer listens to:
//   - mode wrapper:  MintFeeWrapper.MintWithFee filtered by pointToken
//                    (when wrapper is configured at chainId)
//   - mode direct:   PointToken.Transfer(0x0 → user) (legacy / no wrapper)

Override for fork tests:

indexer: { mintFeeWrapperAddress: "0x000...dead" }  // force direct mode

Error classes

All SDK errors inherit PafiSdkError. Subclasses + HTTP mapping:

| Error | httpStatus | safeToRetry | | --- | --- | --- | | PendingUserOpNotFoundError | not_found | false | | PendingUserOpForbiddenError | forbidden | false | | LockNotFoundError | not_found | false | | IssuerStateError (ISSUER_NOT_REGISTERED / ISSUER_INACTIVE / MINT_CAP_EXCEEDED) | unprocessable | true if cap exceeded | | PerpDepositError | unprocessable | true if RELAY_FEE_EXCEEDS_AMOUNT | | PTClaimError, PTRedeemError | unprocessable | false | | BundlerNotConfiguredError | service_unavailable | false | | BundlerRejectedError | unprocessable | false | | RedemptionPolicyError (REDEMPTION_DENIED / RATE_LIMIT_EXCEEDED / ...) | unprocessable | varies |


References

License

Apache-2.0