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

sdk-test-core

v0.1.0

Published

EIP-712 signing, contract interaction, and Relay calldata for the point token system

Readme

@pafi/core

The core TypeScript SDK for the PAFI point token system. Provides EIP-712 signing and verification, EIP-4361 (Sign-In with Ethereum) helpers, contract ABIs, Relay calldata encoding, on-chain quoting, and swap building.

@pafi/core is HTTP-client-free on purpose. It covers only the primitives that need a signer or a provider (or neither) — never the HTTP wire. The HTTP contract (routes, request/response types) is owned by @pafi/issuer as the single source of truth, and frontends import those types type-only so browser bundles never pull in server code like jose or node:crypto.

| Runs on | Purpose | |---|---| | Frontend (web + mobile) | Sign EIP-712 messages, build calldata, quote swaps, construct login messages | | Issuer backend | Verify EIP-712 signatures, decode calldata, read on-chain nonces / minter status |

Installation

pnpm add @pafi/core viem

viem ^2.0.0 is a peer dependency and must be installed alongside this package.


Quick Start

import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { PafiSDK } from "@pafi/core";

const account = privateKeyToAccount("0x...");

const sdk = new PafiSDK({
  chainId: 8453,
  rpcUrl: "https://base-mainnet.g.alchemy.com/v2/...",
  signer: createWalletClient({
    account,
    transport: http("https://base-mainnet.g.alchemy.com/v2/..."),
  }),
  pointTokenAddress: "0xPointToken...",
  relayContractAddress: "0xRelay...",
});

// Fetch on-chain nonce then sign a mint request
const nonce = await sdk.getMintRequestNonce(account.address);

const sig = await sdk.signMintRequest({
  to: account.address,
  amount: 1_000_000n,
  nonce,
  deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
});

console.log(sig.serialized);

Sub-path Imports

Every module is available both from the root entry and via its own sub-path export. Use sub-paths to reduce bundle size in tree-shaking-aware environments.

| Sub-path | Contents | |---|---| | @pafi/core | All exports (PafiSDK class + everything below) | | @pafi/core/eip712 | buildMintRequestTypedData, buildReceiverConsentTypedData, signMintRequest, verifyMintRequest, signReceiverConsent, verifyReceiverConsent, buildDomain | | @pafi/core/relay | encodeMintAndSwap, decodeMintAndSwap, buildRelaySwapParams, encodeExtData, decodeExtData | | @pafi/core/contract | getMintRequestNonce, getReceiverConsentNonce, isMinter, getTokenName, getIssuer, isActiveIssuer, verifyMintCap, getPointTokenIssuer, getFeeBasisPoints, getSlippageBasisPoints, getUsdt | | @pafi/core/quoting | quoteExactInput, quoteExactInputSingle, quoteBestRoute, findBestQuote, buildAllPaths, combineRoutes | | @pafi/core/swap | checkAllowance, buildErc20ApprovalCalldata, buildPermit2ApprovalCalldata, buildUniversalRouterExecuteArgs, buildSwapFromQuote, buildV4SwapInput | | @pafi/core/auth | createLoginMessage, parseLoginMessage, verifyLoginMessage (EIP-4361) | | @pafi/core/abi | All contract ABIs |


API Reference

PafiSDK Class

The PafiSDK class is a convenience wrapper around all pure functions. All methods delegate to the same pure functions exported from the sub-path modules.

Constructor

import { PafiSDK } from "@pafi/core";

const sdk = new PafiSDK(config: PafiSDKConfig);
interface PafiSDKConfig {
  chainId?: number;
  pointTokenAddress?: Address;
  relayContractAddress?: Address;
  signer?: WalletClient;       // viem WalletClient
  provider?: PublicClient;     // viem PublicClient (takes precedence over rpcUrl)
  rpcUrl?: string;             // creates a PublicClient automatically if provider is omitted
}

All fields are optional at construction time. Missing fields are validated lazily when the relevant method is called, throwing a ConfigurationError if a required field is absent.

Setters

sdk.setPointTokenAddress(address: Address): void
sdk.setRelayContractAddress(address: Address): void
sdk.setSigner(signer: WalletClient): void
sdk.setProvider(provider: PublicClient): void

Domain

// Resolves the EIP-712 domain by reading the token name on-chain.
// Requires provider, pointTokenAddress, and chainId.
await sdk.getDomain(): Promise<PointTokenDomainConfig>

EIP-712 Signing

Pure functions (from @pafi/core/eip712)

import {
  buildDomain,
  signMintRequest,
  verifyMintRequest,
  signReceiverConsent,
  verifyReceiverConsent,
} from "@pafi/core/eip712";
buildDomain
function buildDomain(config: PointTokenDomainConfig): {
  name: string;
  version: "1";
  chainId: number;
  verifyingContract: Address;
}

Constructs the EIP-712 domain object. Version is always "1".

signMintRequest
async function signMintRequest(
  walletClient: WalletClient,
  domain: PointTokenDomainConfig,
  message: MintRequest,
): Promise<EIP712Signature>

Signs a MintRequest struct with the connected wallet.

verifyMintRequest
async function verifyMintRequest(
  domain: PointTokenDomainConfig,
  message: MintRequest,
  signature: Hex,
  expectedMinter: Address,
): Promise<SignatureVerification>

Recovers the signer from a MintRequest signature and compares against expectedMinter.

signReceiverConsent
async function signReceiverConsent(
  walletClient: WalletClient,
  domain: PointTokenDomainConfig,
  message: ReceiverConsent,
): Promise<EIP712Signature>

Signs a ReceiverConsent struct.

verifyReceiverConsent
async function verifyReceiverConsent(
  domain: PointTokenDomainConfig,
  message: ReceiverConsent,
  signature: Hex,
  expectedReceiver: Address,
): Promise<SignatureVerification>

Recovers and validates a ReceiverConsent signature.

PafiSDK methods

await sdk.signMintRequest(message: MintRequest): Promise<EIP712Signature>
await sdk.verifyMintRequest(message: MintRequest, signature: Hex, expectedMinter: Address): Promise<SignatureVerification>
await sdk.signReceiverConsent(message: ReceiverConsent): Promise<EIP712Signature>
await sdk.verifyReceiverConsent(message: ReceiverConsent, signature: Hex, expectedReceiver: Address): Promise<SignatureVerification>

All four methods automatically resolve the domain from on-chain state via getDomain().


Relay Calldata

Pure functions (from @pafi/core/relay)

import {
  encodeMintAndSwap,
  decodeMintAndSwap,
  encodeMintAndSwapV2,
  decodeMintAndSwapV2,
} from "@pafi/core/relay";
V1 (Relay.sol)
// ABI-encode a mintAndSwap(MintParams, SwapParams) call for Relay.sol
function encodeMintAndSwap(mint: MintParams, swap: SwapParams): Hex

// Decode calldata produced by encodeMintAndSwap
function decodeMintAndSwap(calldata: Hex): { mint: MintParams; swap: SwapParams }
V2 (RelayV2.sol)
// ABI-encode a mintAndSwap(MintParams, SwapParams) call for RelayV2.sol
function encodeMintAndSwapV2(mint: MintParams, swap: SwapParams): Hex

// Decode calldata produced by encodeMintAndSwapV2
function decodeMintAndSwapV2(calldata: Hex): { mint: MintParams; swap: SwapParams }

Example

import { encodeMintAndSwap } from "@pafi/core/relay";

const calldata = encodeMintAndSwap(
  {
    pointToken: "0xPointToken...",
    receiver: "0xReceiver...",
    amount: 1_000_000n,
    deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
    minterSig: "0x...",
    receiverSig: "0x...",
  },
  {
    path: [
      {
        intermediateCurrency: "0xUSDT...",
        fee: 500,
        tickSpacing: 10,
        hooks: "0x0000000000000000000000000000000000000000",
        hookData: "0x",
      },
    ],
    deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
  },
);

PafiSDK methods

sdk.encodeMintAndSwap(mint: MintParams, swap: SwapParams): Hex
sdk.decodeMintAndSwap(calldata: Hex): { mint: MintParams; swap: SwapParams }
sdk.encodeExtData(minAmountOut: bigint, feeInUsdt: bigint): Hex
sdk.buildRelaySwapParams({ quote, minAmountOut, feeInUsdt, deadline }): { swapParams, extData }

Contract Reads

PointToken (from @pafi/core/contract)

import {
  getMintRequestNonce,
  getReceiverConsentNonce,
  isMinter,
  getTokenName,
  getPointTokenIssuerAddress,
} from "@pafi/core/contract";

| Function | Signature | Description | |---|---|---| | getMintRequestNonce | (client, pointToken, receiver) => Promise<bigint> | Current MintRequest nonce for receiver | | getReceiverConsentNonce | (client, pointToken, receiver) => Promise<bigint> | Current ReceiverConsent nonce for receiver | | isMinter | (client, pointToken, account) => Promise<boolean> | Returns true if account has the minter role | | getTokenName | (client, pointToken) => Promise<string> | ERC-20 name() — used as the EIP-712 domain name | | getPointTokenIssuerAddress | (client, pointToken) => Promise<Address> | Issuer address registered on the token |

IssuerRegistry

import { getIssuer, isActiveIssuer } from "@pafi/core/contract";

| Function | Signature | Description | |---|---|---| | getIssuer | (client, registryAddress, issuer) => Promise<Issuer> | Full Issuer struct for the given address | | isActiveIssuer | (client, registryAddress, issuer) => Promise<boolean> | Whether the issuer is currently active |

MintingOracle

import { verifyMintCap, getPointTokenIssuer } from "@pafi/core/contract";

| Function | Signature | Description | |---|---|---| | verifyMintCap | (client, oracleAddress, issuer, amount) => Promise<void> | Reverts if amount would exceed the issuer's mint cap | | getPointTokenIssuer | (client, oracleAddress, pointToken) => Promise<Address> | Maps a point token address back to its issuer |

Relay helpers

import { getFeeBasisPoints, getSlippageBasisPoints, getUsdt } from "@pafi/core/contract";

| Function | Signature | Description | |---|---|---| | getFeeBasisPoints | (client, relayAddress) => Promise<bigint> | Protocol fee in basis points | | getSlippageBasisPoints | (client, relayAddress) => Promise<bigint> | Slippage tolerance in basis points | | getUsdt | (client, relayAddress) => Promise<Address> | USDT address used by the Relay contract |

PafiSDK methods

await sdk.getMintRequestNonce(receiver: Address): Promise<bigint>
await sdk.getReceiverConsentNonce(receiver: Address): Promise<bigint>

PafiSDK quoting + swap methods

// Quote — finds the best route via multicall
await sdk.findBestQuote(tokenIn, tokenOut, amount, pools?, quoterAddress?): Promise<BestQuote>

// Build UniversalRouter execute args from a quote
sdk.buildSwapFromQuote({ quote, currencyIn, currencyOut, amountIn, minAmountOut }): { commands, inputs }

// Build Relay SwapParams + extData from a quote
sdk.buildRelaySwapParams({ quote, minAmountOut, feeInUsdt, deadline }): { swapParams, extData }

// Encode extData for ReceiverConsent signing
sdk.encodeExtData(minAmountOut, feeInUsdt): Hex

Quoting

Pure functions (from @pafi/core/quoting)

import {
  quoteExactInput,
  quoteExactInputSingle,
  quoteBestRoute,
  findBestQuote,
  buildAllPaths,
  combineRoutes,
} from "@pafi/core/quoting";
quoteExactInput
async function quoteExactInput(
  client: PublicClient,
  quoterAddress: Address,
  exactCurrency: Address,
  path: PathKey[],
  exactAmount: bigint,
): Promise<QuoteResult>

Quotes a multi-hop exact-input swap against the V4 quoter.

quoteExactInputSingle
async function quoteExactInputSingle(
  client: PublicClient,
  quoterAddress: Address,
  poolKey: PoolKey,
  zeroForOne: boolean,
  exactAmount: bigint,
  hookData: Hex,
): Promise<{ amountOut: bigint; gasEstimate: bigint }>

Quotes a single-hop swap given an explicit pool key and swap direction.

quoteBestRoute
async function quoteBestRoute(
  client: PublicClient,
  quoterAddress: Address,
  exactCurrency: Address,
  routes: PathKey[][],
  exactAmount: bigint,
): Promise<BestQuote>

Quotes multiple routes via a single multicall RPC call and returns the one with the highest amountOut. Routes that fail (e.g. pool does not exist) are silently dropped. Throws if no routes succeed.

findBestQuote
async function findBestQuote(
  client: PublicClient,
  chainId: number,
  tokenIn: Address,
  tokenOut: Address,
  exactAmount: bigint,
  pools?: PoolKey[],
  quoterAddress?: Address,
  maxHops?: number,
): Promise<BestQuote>

One-call quoting helper. Merges caller-provided pools with COMMON_POOLS[chainId], builds all possible paths via buildAllPaths, then quotes them all via multicall. Uses V4_QUOTER_ADDRESSES[chainId] unless quoterAddress is provided.

buildAllPaths
function buildAllPaths(
  pools: PoolKey[],
  tokenIn: Address,
  tokenOut: Address,
  maxHops?: number,
): PathKey[][]

Builds all possible swap paths from tokenIn to tokenOut using DFS through the given pools. Each pool is used at most once per path. Returns an array of PathKey[] routes (up to maxHops, default 3).

combineRoutes
function combineRoutes(
  chainId: number,
  pointTokenAddress: Address,
): PoolKey[]

Merges POINT_TOKEN_POOLS[chainId][pointTokenAddress] and COMMON_POOLS[chainId] into a single pool list, with point token pools listed first.

Example

import { findBestQuote } from "@pafi/core/quoting";

// Provide only your token-specific pools — COMMON_POOLS are merged automatically
const pointTokenPools = [
  { currency0: USDC, currency1: pointToken, fee: 3000, tickSpacing: 60, hooks: ZERO },
];

const { bestRoute, allRoutes } = await findBestQuote(
  client,
  8453,            // Base chain ID — auto-resolves quoter address
  pointToken,
  USDC,
  1_000_000n,
  pointTokenPools, // merged with COMMON_POOLS[8453]
);

console.log(bestRoute.amountOut, bestRoute.gasEstimate);
console.log(`Found ${allRoutes.length} valid routes`);

Swap Building

Pure functions (from @pafi/core/swap)

import {
  checkAllowance,
  buildErc20ApprovalCalldata,
  buildPermit2ApprovalCalldata,
  buildUniversalRouterExecuteArgs,
  buildV4SwapInput,
} from "@pafi/core/swap";
checkAllowance
async function checkAllowance(
  client: PublicClient,
  token: Address,
  owner: Address,
  spender: Address,
): Promise<bigint>

Reads the ERC-20 allowance of spender on behalf of owner.

buildErc20ApprovalCalldata
function buildErc20ApprovalCalldata(spender: Address, amount: bigint): Hex

Encodes ERC20.approve(spender, amount).

buildPermit2ApprovalCalldata
function buildPermit2ApprovalCalldata(
  token: Address,
  spender: Address,
  amount: bigint,
  expiration: number,
): Hex

Encodes Permit2.approve(token, spender, amount, expiration).

buildUniversalRouterExecuteArgs
function buildUniversalRouterExecuteArgs(
  currencyIn: Address,
  path: PathKey[],
  amountIn: bigint,
  minAmountOut: bigint,
  outputCurrency: Address,
): { commands: Hex; inputs: Hex[] }

Builds the commands and inputs arguments for UniversalRouter.execute. The command sequence is V4_SWAP (0x10). Actions encoded inside the payload are SWAP_EXACT_IN → SETTLE_ALL → TAKE_ALL.

buildV4SwapInput
function buildV4SwapInput(
  currencyIn: Address,
  path: PathKey[],
  amountIn: bigint,
  minAmountOut: bigint,
  outputCurrency: Address,
): Hex

Encodes only the V4_SWAP command payload (inputs[0]). Use buildUniversalRouterExecuteArgs unless you need the raw payload for custom command composition.

buildSwapFromQuote
function buildSwapFromQuote(params: {
  quote: QuoteResult;
  currencyIn: Address;
  currencyOut: Address;
  amountIn: bigint;
  minAmountOut: bigint;
}): { commands: Hex; inputs: Hex[] }

Build UniversalRouter execute args directly from a QuoteResult (returned by findBestQuote or quoteBestRoute). The caller provides minAmountOut after applying their own slippage tolerance.

Example

import { findBestQuote } from "@pafi/core/quoting";
import { buildSwapFromQuote } from "@pafi/core/swap";

// 1. Quote
const { bestRoute } = await findBestQuote(client, 8453, tokenIn, USDC, amountIn, myPools);

// 2. Build swap calldata (apply slippage externally)
const minReceive = bestRoute.amountOut * 99n / 100n; // 1% slippage
const { commands, inputs } = buildSwapFromQuote({
  quote: bestRoute,
  currencyIn: tokenIn,
  currencyOut: USDC,
  amountIn,
  minAmountOut: minReceive,
});

// 3. Execute via UniversalRouter

Auth helpers (EIP-4361 — Sign-In With Ethereum)

Pure functions (from @pafi/core/auth)

import {
  createLoginMessage,
  parseLoginMessage,
  verifyLoginMessage,
} from "@pafi/core/auth";

These are stateless helpers — they build, parse, and verify EIP-4361 login messages without touching the network. The frontend uses them to construct the message the wallet signs; the backend uses verifyLoginMessage inside its own AuthService (see @pafi/issuer).

function createLoginMessage(params: {
  domain: string;           // e.g. "app.example.com"
  address: Address;         // user's wallet
  chainId: number;
  nonce: string;            // from issuer backend /auth/nonce
  uri: string;              // e.g. "https://app.example.com"
  statement?: string;       // human-readable sign-in message
  version?: string;         // defaults to "1"
  issuedAt?: Date;
  expirationTime?: Date;
  notBefore?: Date;
  requestId?: string;
}): string

function parseLoginMessage(message: string): LoginMessageParams

async function verifyLoginMessage(
  message: string,
  signature: Hex,
): Promise<{ valid: boolean; address: Address }>

PafiSDK helpers

// Build a login message for the current signer + chain
await sdk.createLoginMessage(params: Omit<LoginMessageParams, "address" | "chainId">): Promise<string>

// Sign a login message with the current signer (personal_sign)
await sdk.signLoginMessage(message: string): Promise<Hex>

The SDK does not post the signed message for you — the frontend calls its own fetch() against the issuer backend:

// 1. Fetch nonce from the issuer backend
const { nonce } = await fetch(`${ISSUER_URL}/auth/nonce`).then(r => r.json());

// 2. Build + sign the login message
const message = await sdk.createLoginMessage({
  domain: "app.example.com",
  uri: "https://app.example.com",
  nonce,
});
const signature = await sdk.signLoginMessage(message);

// 3. POST to the issuer backend, receive a JWT
const { token, userAddress } = await fetch(`${ISSUER_URL}/auth/login`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ message, signature }),
}).then(r => r.json());

// 4. Store the JWT and attach it to subsequent protected requests
// (handled by the frontend's own state management — @pafi/core does
// not manage tokens)

Why isn't there an HTTP client? The HTTP contract (routes, request/response shapes) is owned by @pafi/issuer. Frontends import those types type-only and build their own fetch() calls. This keeps the frontend bundle free of server-side dependencies like jose and node:crypto, and ensures the protocol has exactly one source of truth.


ABIs

All ABIs are available from @pafi/core/abi or from the root import.

import {
  pointTokenAbi,
  relayAbi,
  relayV2Abi,
  issuerRegistryAbi,
  pointTokenFactoryAbi,
  mintingOracleAbi,
  erc20Abi,
  universalRouterAbi,
  permit2Abi,
  v4QuoterAbi,
} from "@pafi/core/abi";

| Export | Contract | |---|---| | pointTokenAbi | PointToken ERC-20 with minting and nonce functions | | relayAbi | Relay.sol — mintAndSwap, fee and slippage reads | | relayV2Abi | RelayV2.sol — same interface, new deployment | | issuerRegistryAbi | IssuerRegistry — getIssuer, isActiveIssuer | | pointTokenFactoryAbi | PointTokenFactory | | mintingOracleAbi | MintingOracle — verifyMintCap, pointTokenToIssuer | | erc20Abi | Standard ERC-20 (allowance, approve, transfer, balanceOf) | | universalRouterAbi | Uniswap UniversalRouter — execute | | permit2Abi | Uniswap Permit2 — approve | | v4QuoterAbi | Uniswap V4 Quoter — quoteExactInput, quoteExactInputSingle |


Constants

import {
  SUPPORTED_CHAINS,
  COMMON_TOKENS,
  COMMON_POOLS,
  POINT_TOKEN_POOLS,
  mintRequestTypes,
  receiverConsentTypes,
} from "@pafi/core";

| Constant | Type | Description | |---|---|---| | SUPPORTED_CHAINS | Record<number, ChainConfig> | Chain ID to chain metadata map | | COMMON_TOKENS | Record<number, Record<string, Address>> | Chain ID to { symbol → address } token map | | COMMON_POOLS | Record<number, PoolKey[]> | Chain ID to common pool list (e.g. stablecoin pairs) | | POINT_TOKEN_POOLS | Record<number, Record<Address, PoolKey[]>> | Chain ID to per-point-token pool list | | mintRequestTypes | const | EIP-712 typed data types for MintRequest | | receiverConsentTypes | const | EIP-712 typed data types for ReceiverConsent |

COMMON_POOLS and POINT_TOKEN_POOLS are consumed by combineRoutes to assemble candidate routes for quoting.


Errors

All errors extend PafiSDKError, which extends Error.

import { PafiSDKError, ConfigurationError, SigningError, ApiError } from "@pafi/core";

| Class | Thrown when | |---|---| | PafiSDKError | Base class — not thrown directly | | ConfigurationError | A required field (pointTokenAddress, signer, provider, etc.) is not set when a method requires it | | SigningError | A signing operation fails | | ApiError | Generic HTTP error helper for callers writing their own fetch() against the issuer backend; carries an optional status |

Example

import { ConfigurationError, SigningError } from "@pafi/core";

try {
  await sdk.signMintRequest(message);
} catch (err) {
  if (err instanceof ConfigurationError) {
    console.error(`SDK misconfigured: ${err.message}`);
  } else if (err instanceof SigningError) {
    console.error(`Signing failed: ${err.message}`);
  }
}

EIP-712 Type Reference

The PointToken contract is the authoritative EIP-712 domain contract.

Domain

name: <token name from ERC-20 name()>
version: "1"
chainId: <network chain ID>
verifyingContract: <PointToken address>

MintRequest

MintRequest(address to, uint256 amount, uint256 nonce, uint256 deadline)

| Field | Type | Description | |---|---|---| | to | address | Recipient of the minted tokens | | amount | uint256 | Token amount to mint | | nonce | uint256 | Anti-replay nonce from mintRequestNonces(to) | | deadline | uint256 | Unix timestamp after which the signature expires |

The minter (issuer signer) signs this struct. The nonce must match pointToken.mintRequestNonces(to) at the time of execution.

ReceiverConsent

ReceiverConsent(address onBehalfOf, address originalReceiver, uint256 amount, uint256 nonce, uint256 deadline)

| Field | Type | Description | |---|---|---| | onBehalfOf | address | Address whose points are being redirected | | originalReceiver | address | The original recipient who is granting consent | | amount | uint256 | Token amount involved in the consent | | nonce | uint256 | Anti-replay nonce from receiverConsentNonces(originalReceiver) | | deadline | uint256 | Unix timestamp after which the signature expires |

The original receiver signs this struct to authorize minting on behalf of another address.


PointToken Mint Paths

The Relay contract supports three mint paths depending on which signatures are provided:

| Path | Signer | Receiver consent required | Description | |---|---|---|---| | 1 | Minter (issuer signer) | No | Issuer mints directly to receiver. Only the minterSig is validated. | | 2 | Minter + Receiver | Yes | Issuer mints on behalf of receiver, who has pre-signed a ReceiverConsent. Both signatures are validated. | | 3 | Minter + Relayer | No | Issuer authorizes a relayer to execute the mint-and-swap. The relayer submits the transaction. |

In all paths, the MintParams.minterSig is a MintRequest EIP-712 signature. Path 2 additionally requires MintParams.receiverSig, which is a ReceiverConsent EIP-712 signature.


Type Reference

Core types

interface MintRequest {
  to: Address;
  amount: bigint;
  nonce: bigint;
  deadline: bigint;
}

interface ReceiverConsent {
  onBehalfOf: Address;
  originalReceiver: Address;
  amount: bigint;
  nonce: bigint;
  deadline: bigint;
}

interface EIP712Signature {
  v: number;
  r: Hex;
  s: Hex;
  serialized: Hex;
}

interface SignatureVerification {
  isValid: boolean;
  recoveredAddress: Address;
}

interface PointTokenDomainConfig {
  name: string;
  verifyingContract: Address;
  chainId: number;
}

Relay types

interface MintParams {
  pointToken: Address;
  receiver: Address;
  amount: bigint;
  deadline: bigint;
  minterSig: Hex;
  receiverSig: Hex;
}

interface PathKey {
  intermediateCurrency: Address;
  fee: number;
  tickSpacing: number;
  hooks: Address;
  hookData: Hex;
}

interface SwapParams {
  path: PathKey[];
  deadline: bigint;
}

Pool and issuer types

interface PoolKey {
  currency0: Address;
  currency1: Address;
  fee: number;
  tickSpacing: number;
  hooks: Address;
}

interface Issuer {
  issuerAddress: Address;
  signerAddress: Address;
  name: string;
  symbol: string;
  declaredTotalSupply: bigint;
  capBasisPoints: number;
  active: boolean;
  pointToken: Address;
  mintingOracle: Address;
}

Quoting types

interface QuoteResult {
  amountOut: bigint;
  gasEstimate: bigint;
  path: PathKey[];
}

interface BestQuote {
  bestRoute: QuoteResult;
  allRoutes: QuoteResult[];
}

Integration Guide

Flow 1: Direct swap via UniversalRouter

Use this when a user wants to swap tokens directly (not through the PAFI Relay).

import { PafiSDK } from "@pafi/core";
import { UNIVERSAL_ROUTER_ADDRESSES } from "@pafi/core";
import { universalRouterAbi } from "@pafi/core/abi";

const sdk = new PafiSDK({
  chainId: 8453,
  rpcUrl: "https://base-mainnet.g.alchemy.com/v2/...",
  signer: walletClient,
});

// 1. Quote — discovers all routes and picks the best via multicall
const { bestRoute, allRoutes } = await sdk.findBestQuote(
  tokenIn,               // e.g. WETH address
  USDC,                  // output token
  parseEther("1"),       // input amount
  myPools,               // optional: your own pools (merged with COMMON_POOLS)
);

console.log(`Best route: ${formatUnits(bestRoute.amountOut, 6)} USDC`);
console.log(`Found ${allRoutes.length} valid routes`);

// 2. Build swap calldata — caller decides minAmountOut (slippage)
const minReceive = bestRoute.amountOut * 99n / 100n; // 1% slippage
const { commands, inputs } = sdk.buildSwapFromQuote({
  quote: bestRoute,
  currencyIn: tokenIn,
  currencyOut: USDC,
  amountIn: parseEther("1"),
  minAmountOut: minReceive,
});

// 3. Approve (one-time): token → Permit2 → UniversalRouter
//    ERC20.approve(PERMIT2, MAX)
//    Permit2.approve(token, UNIVERSAL_ROUTER, MAX, MAX_EXPIRY)

// 4. Execute swap
const deadline = BigInt(Math.floor(Date.now() / 1000) + 1800);
await walletClient.writeContract({
  address: UNIVERSAL_ROUTER_ADDRESSES[8453],
  abi: universalRouterAbi,
  functionName: "execute",
  args: [commands, inputs, deadline],
});

Flow 2: Mint-and-swap via Relay (the PAFI cash-out flow)

The frontend signs, the issuer backend submits. This is the primary PAFI flow.

Frontend (signs + sends to backend)

The SDK builds the EIP-712 typed data object. You can sign it with any signer:

  • viem WalletClient — use sdk.signReceiverConsent() directly
  • Privy / WalletConnect / external — use sdk.buildReceiverConsentTypedData() to get the typed data, then pass it to the external signer's signTypedData
import { PafiSDK } from "@pafi/core";

const sdk = new PafiSDK({
  chainId: 8453,
  rpcUrl: "https://base-mainnet.g.alchemy.com/v2/...",
  pointTokenAddress: "0xPointToken...",
  // signer is optional — not needed if using Privy/external signer
});

// 1. Quote
const { bestRoute } = await sdk.findBestQuote(
  pointTokenAddress, USDC, amount, pointTokenPools,
);

// 2. Set swap terms
const minReceive = bestRoute.amountOut * 99n / 100n;
const feeInUsdt = 1_000_000n; // 1 USDC relayer fee
const extData = sdk.encodeExtData(minReceive, feeInUsdt);

// 3. Read nonce + build typed data
const nonce = await sdk.getReceiverConsentNonce(userAddress);
const deadline = BigInt(Math.floor(Date.now() / 1000) + 1800);
const consentMessage = {
  onBehalfOf: relayAddress,
  originalReceiver: userAddress,
  amount,
  nonce,
  deadline,
  extData,
};

// 4a. Sign with Privy (or any external signer)
const typedData = await sdk.buildReceiverConsentTypedData(consentMessage);
const receiverSig = await privy.signTypedData(typedData);
// receiverSig is a 0x-prefixed hex string (65 bytes)

// 4b. Or sign with viem WalletClient (if signer is set on SDK)
// const { serialized: receiverSig } = await sdk.signReceiverConsent(consentMessage);

// 5. Send to issuer backend (HTTP — not part of @pafi/core)
await fetch(`${ISSUER_URL}/claim-and-swap`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    receiver: userAddress,
    amount: amount.toString(),
    deadline: deadline.toString(),
    receiverSig,
    path: bestRoute.path,
    minAmountOut: minReceive.toString(),
    feeInUsdt: feeInUsdt.toString(),
  }),
});

Issuer backend (signs + submits tx)

The backend can sign with a viem WalletClient, or use buildMintRequestTypedData to get the typed data for an external signer (HSM/KMS).

import { PafiSDK } from "@pafi/core";
import { relayAbi } from "@pafi/core/abi";

const sdk = new PafiSDK({
  chainId: 8453,
  rpcUrl: "...",
  pointTokenAddress: "0xPointToken...",
  // signer is optional — not needed if using HSM/KMS via buildMintRequestTypedData
});

// 1. Issuer signs MintRequest
const mintNonce = await sdk.getMintRequestNonce(receiver);
const mintMessage = { to: receiver, amount, nonce: mintNonce, deadline };

// Option A: Sign with viem WalletClient (set signer on SDK)
// const { serialized: minterSig } = await sdk.signMintRequest(mintMessage);

// Option B: Build typed data for external signer (HSM/KMS/Privy)
const typedData = await sdk.buildMintRequestTypedData(mintMessage);
const minterSig = await externalSigner.signTypedData(typedData);

// 2. Build Relay params from the frontend's quote path
const { swapParams, extData } = sdk.buildRelaySwapParams({
  quote: { path: requestBody.path },
  minAmountOut: BigInt(requestBody.minAmountOut),
  feeInUsdt: BigInt(requestBody.feeInUsdt),
  deadline: BigInt(requestBody.deadline),
});

// 3. Submit to Relay contract
await relayerWallet.writeContract({
  address: relayAddress,
  abi: relayAbi,
  functionName: "mintAndSwap",
  args: [
    {
      pointToken: pointTokenAddress,
      receiver,
      amount,
      deadline,
      minterSig,
      receiverSig: requestBody.receiverSig,
      extData,
    },
    swapParams,
  ],
});

Development

This package is part of the pafi-sdk monorepo. To work on it in isolation:

cd packages/core
pnpm build       # Compile TypeScript with tsup (ESM + CJS dual output)
pnpm test        # Run vitest test suite
pnpm typecheck   # Type-check without emitting (tsc --noEmit)

From the monorepo root:

pnpm build       # Build all packages
pnpm test        # Test all packages
pnpm typecheck   # Type-check all packages

Output

tsup produces dual ESM + CJS output under dist/. Each sub-path export (./eip712, ./relay, etc.) has its own entry in the bundle with full type declarations.


License

UNLICENSED