@pafi-dev/core
v0.10.0
Published
EIP-712 signing, contract interaction, and Relay calldata for the PAFI point token system
Readme
@pafi-dev/core
Pure primitives for the PAFI point-token system. EIP-712 signing, contract ABIs + addresses (v1.6), ERC-4337 UserOp building, EIP-7702 delegation, sponsor-auth signing, perp deposit calldata, fee quoting, MintFeeWrapper helpers.
Browser + Node-safe. Zero HTTP client. Peer-dep: viem ^2.
What's new in 0.10.0 (breaking)
| Change | Detail |
|---|---|
| ReceiverConsent removed | The ReceiverConsent EIP-712 type, helpers (buildReceiverConsentTypedData, signReceiverConsent, verifyReceiverConsent), constants (receiverConsentTypes), and contract reader (getReceiverConsentNonce) are all deleted. The deployed PointToken contract never had a 3rd "sponsored" mint path — that pattern is implemented at the relayer layer (sponsor-relayer pays gas for path-2 MintForRequest sig-gated mint). The previous fallback that read ERC-2612 nonces(owner) was semantically wrong and is gone. |
| PafiSDK.consent namespace removed | The convenience class no longer exposes consent.buildTypedData / sign / verify / getNonce. |
| Action required for callers | Replace any signReceiverConsent / getReceiverConsentNonce usage with the path-2 sig-gated mint flow (MintRequest + getMintRequestNonce). |
What's new in v1.6 (since 0.9.0)
| Change | Detail |
|---|---|
| MintFeeWrapper (NEW) | Single global wrapper contract that skims a configurable fee on every sig-gated mint and distributes to per-PT recipients |
| EIP-712 typehash | MintRequest(to, amount, nonce, deadline) → MintForRequest(user, receiver, amount, nonce, deadline) (5 fields) |
| Issuer struct | Dropped declaredTotalSupply + capBasisPoints — caps moved to MintingOracle.tokenCaps(pointToken) (per-token, not per-issuer) |
| verifyMintCap signature | Now takes pointToken as first arg |
| New helpers | getMintFeeBps, getMintFeeRecipients, getTokenCap |
| Subgraph URL | Hardcoded to pafi-subgraph-v3 (getPafiServiceUrls(chainId)) |
| pafiHook | DEPRECATED — V4 swap-time fee removed entirely in v1.6 |
| getPtPerUsdt18dec math fix (0.9.6) | Subgraph V4 returns HUMAN-readable price; old formula 10^24/raw was off by ~10^11 |
Requirements
- Node.js ≥ 18 (or modern browser with native
fetch) - TypeScript ≥ 5.0
viem^2.0.0 (peer)
Installation
pnpm add @pafi-dev/core viemPackage layout (v1.6)
Core ships data + primitives only. Higher-level orchestration in siblings:
| Concern | Package |
| --- | --- |
| Pure primitives (this package) | @pafi-dev/core |
| Issuer backend (claim / redeem / mobile flows) | @pafi-dev/issuer |
| Issuer DB ledger (TypeORM + Postgres) | @pafi-dev/issuer-postgres |
| Trading (swap + quote, FE-callable) | @pafi-dev/trading |
Exports cheatsheet
| Symbol / area | Provides |
| --- | --- |
| getContractAddresses(chainId) | All deployed PAFI v1.6 addresses keyed by chain |
| signMintRequest, verifyMintRequest, buildMintRequestTypedData | EIP-712 sign/recover for v1.6 MintForRequest (5 fields) |
| signBurnRequest, verifyBurnRequest, buildBurnRequestTypedData | EIP-712 for BurnRequest |
| signSponsorAuth, verifySponsorAuth, buildAndSignSponsorAuth | EIP-712 for sponsor-relayer auth |
| buildPartialUserOperation, assembleUserOperation, computeUserOpHash | ERC-4337 v0.7+ UserOp builder + hash |
| encodeBatchExecute, decodeBatchExecuteCalls | BatchExecutor (Pimlico Simple7702Account) calldata |
| buildDelegationUserOp, computeAuthorizationHash, parseEip7702DelegatedAddress, splitAuthorizationSig, buildEip7702Authorization, delegateDirect | EIP-7702 helpers (build + introspect) |
| buildPerpDepositViaRelay, buildPerpDepositWithGasDeduction, ORDERLY_RELAY_ABI, ORDERLY_VAULT_ABI, BROKER_HASHES, TOKEN_HASHES, computeAccountId | Orderly perp deposit calldata + types |
| quoteOperatorFeePt, quoteOperatorFeeUsdt | Gas-reimbursement fee quoter (Chainlink + V4 subgraph) |
| getMintFeeBps, getMintFeeRecipients | v1.6 — read wrapper fee config per pointToken |
| getTokenCap | v1.6 — read MintingOracle.tokenCaps(pointToken) |
| verifyMintCap, getPointTokenIssuer | MintingOracle read helpers (v1.6 signature) |
| getIssuer, isActiveIssuer, issuerRegistryGetIssuerFlatAbi | IssuerRegistry helpers (v1.6 — 7 fields) |
| getMintRequestNonce, getBurnRequestNonce, getPointTokenBalance, isMinter, getTokenName, getPointTokenIssuerAddress | PointToken read helpers |
| fetchPafiPools, PAFI_SUBGRAPH_URL | Pool discovery via PAFI subgraph v3 |
| getPafiServiceUrls(chainId) | Hardcoded PAFI service endpoints (sponsorRelayer, issuerApi) |
| pointTokenAbi (alias POINT_TOKEN_V2_ABI), issuerRegistryAbi, mintingOracleAbi, mintFeeWrapperAbi, pointTokenFactoryAbi, erc20Abi, permit2Abi, universalRouterAbi, v4QuoterAbi | Contract ABIs (regenerated from Foundry artifacts) |
| createLoginMessage | EIP-4361 SIWE login builder |
Contract addresses (v1.6)
import { getContractAddresses } from "@pafi-dev/core";
const addr = getContractAddresses(8453); // Base mainnet
// addr.issuerRegistry = 0xAB1d1e117c41636f30bb10194Fe6B774B6Da9E01
// addr.mintingOracle = 0x2f4cf8C5F8b41efC970c5b46a5d905CeA1f871a0
// addr.mintFeeWrapper = 0xD324EE2e3220B23d1b1BfbB85f5bC1EF2E917B93 (NEW)
// addr.usdt = 0x3F7e71B150e97316Bb9f363A32c19CcD36ac2382
// addr.batchExecutor = 0xe6Cae83BdE06E4c305530e199D7217f42808555B (Pimlico Simple7702)
// addr.pafiFeeRecipient = ... (PAFI ops wallet)
// addr.chainlinkEthUsd = 0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70
// addr.pafiHook = 0x...dead (DEPRECATED in v1.6)
// addr.pointToken = dead-zero (PER-ISSUER — read from env or factory)Per-issuer
pointTokenclones are NOT in the chain-level address bag. Read from your issuer env (e.g.POINT_TOKEN_ADDRESS) orPointTokenFactory.createToken()at runtime.
Deploy block: 45683465 (canonical for all v1.6 indexers).
EIP-712 — MintForRequest (v1.6)
import { signMintRequest, verifyMintRequest } from "@pafi-dev/core";
import { privateKeyToAccount } from "viem/accounts";
import { createWalletClient, http } from "viem";
const wallet = createWalletClient({
account: privateKeyToAccount(MINTER_PK),
transport: http(),
});
const sig = await signMintRequest(
wallet,
{ name: "POINT", chainId: 8453, verifyingContract: pointTokenAddress },
{
user, // off-chain spender (drives nonces)
receiver: wrapperAddress, // on-chain caller of PointToken.mint
// (= wrapper for wrapper-mediated; = user for direct)
amount: 1000n * 10n ** 18n,
nonce: currentMintRequestNonce, // from PointToken.mintRequestNonces(user)
deadline: BigInt(Math.floor(Date.now() / 1000) + 900),
},
);
// sig.serialized — bytes hex passed to PointToken.mint(...) or wrapper.mintWithFee(...)Wrapper-mediated mint puts receiver = wrapperAddress; direct mint puts
receiver = user. The contract requires msg.sender == receiver.
MintFeeWrapper helpers (v1.6)
import { getMintFeeBps, getMintFeeRecipients, getContractAddresses } from "@pafi-dev/core";
const { mintFeeWrapper } = getContractAddresses(8453);
const POINT_TOKEN = "0x855c2046...";
const bps = await getMintFeeBps(provider, mintFeeWrapper, POINT_TOKEN);
// bps = 50 (= 0.50%)
const recipients = await getMintFeeRecipients(provider, mintFeeWrapper, POINT_TOKEN);
// [{ account: 0x..., basisPoints: 20 }, { account: 0x..., basisPoints: 30 }]Wrapper deducts gross × bps / 10000 PT on every mint and distributes
to recipients. User receives gross × (1 - bps/10000) net.
Operator fee quoter
Two helpers for FE direct usage (no backend round-trip):
import { quoteOperatorFeeUsdt, quoteOperatorFeePt } from "@pafi-dev/core";
// USDT-denominated (replaces issuer's GET /gas-fee). Chainlink-only.
const gasFeeUsdt = await quoteOperatorFeeUsdt({
provider, chainId: 8453, scenario: "mint",
});
// PT-denominated (used by mint/burn flows). Adds V4 subgraph call.
const gasFeePt = await quoteOperatorFeePt({
provider, chainId: 8453, scenario: "mint",
pointTokenAddress: POINT_TOKEN,
allowStaleFallback: true, // recommended: don't block on subgraph outage
});Both return bigint raw units. Fallback prices (0.1 USDT/PT default,
3000 USD/ETH default) used when oracles unreachable + allowStaleFallback
is true. Sponsor-relayer's FeeValidator runs the same math server-side
with a 5% tolerance window.
See docs/FEE_FLOW.md in the main repo for
full math derivation.
ERC-4337 UserOp building
import { encodeBatchExecute, buildPartialUserOperation } from "@pafi-dev/core";
const callData = encodeBatchExecute([
{ target: mintFeeWrapper, value: 0n, data: mintWithFeeCallData },
{ target: pointToken, value: 0n, data: feeTransferCallData },
]);
const partial = buildPartialUserOperation({
sender: userAddress,
nonce: aaNonce,
callData,
gasLimits: {
callGasLimit: 300_000n,
verificationGasLimit: 150_000n,
preVerificationGas: 50_000n,
},
});computeUserOpHash(userOp, chainId) — EntryPoint v0.8 EIP-712 digest.
Mobile signs this hash via eth_signTypedData_v4 (NOT personal_sign —
Pimlico Simple7702Account does raw ecrecover without EIP-191 prefix).
EIP-7702 — delegateDirect
FE one-shot delegation, no AA / no sponsor:
import { delegateDirect } from "@pafi-dev/core";
import { useSign7702Authorization, useWallets } from "@privy-io/react-auth";
const { signAuthorization } = useSign7702Authorization();
const wallet = useWallets().find((w) => w.walletClientType === "privy");
const result = await delegateDirect({
userAddress: wallet.address,
chainId: 8453,
publicClient,
walletClient,
signAuthorization,
// optional: skipIfAlreadyDelegated (default true), waitForReceipt (default true)
});
// result.status: "already-delegated" | "broadcasted"
// result.txHash, result.delegatedTo, result.receipt?Privy embedded wallet only — external wallets (MetaMask) can't sign
EIP-7702 (no raw secp256k1_sign exposed). User pays ~$0.01–0.10 ETH gas.
Sponsor-auth signing
import { buildAndSignSponsorAuth } from "@pafi-dev/core";
const sponsorAuth = await buildAndSignSponsorAuth({
userAddress,
callData: userOp.callData,
chainId: 8453,
scenario: "mint", // "mint" | "burn" | "swap" | "perp-deposit" | "delegate"
issuerId: ISSUER_ID,
issuerSignerWallet, // KMS-backed in prod
});
// Pass to sponsor-relayer's POST /paymaster/sponsor as `sponsorAuth` fieldIssuer signer must be registered in IssuerRegistry (signerAddress
field) and whitelisted via PointToken.addMinter(signer).
Verify on-chain state (debug)
import {
getIssuer, getTokenCap, getMintFeeBps,
getMintRequestNonce, isMinter,
} from "@pafi-dev/core";
// Issuer registry record (v1.6 — 7 fields)
const issuer = await getIssuer(provider, registry, issuerAddress);
// { issuerAddress, signerAddress, name, symbol, active, pointToken, mintingOracle }
// Per-token cap (v1.6 — moved off issuer struct)
const cap = await getTokenCap(provider, mintingOracle, pointToken);
// { declaredTotalSupply, capBasisPoints }
// Fee config
const bps = await getMintFeeBps(provider, wrapper, pointToken);
// Mint state
const nonce = await getMintRequestNonce(provider, pointToken, user);
const authorized = await isMinter(provider, pointToken, signerAddress);v1.5 → v1.6 migration
pnpm add @pafi-dev/core@latest(≥ 0.9.6)- Replace
signMintRequest({ to, amount, nonce, deadline })→signMintRequest({ user, receiver, amount, nonce, deadline })- Direct mint:
receiver = user - Wrapper-mediated mint:
receiver = mintFeeWrapper
- Direct mint:
- Replace
verifyMintCap(client, oracle, issuer, amount)→verifyMintCap(client, oracle, pointToken, issuer, amount) - Remove uses of
Issuer.declaredTotalSupply/Issuer.capBasisPoints— read fromgetTokenCap(provider, oracle, pointToken)instead - Drop any reference to
pafiHookfor swap fees — V4 hook removed in v1.6 - Update calldata to call
mintFeeWrapper.mintWithFee(...)when fee is configured.@pafi-dev/issuer'sRelayServicedoes this automatically based ongetContractAddresses(chainId).mintFeeWrapper
References
- v1.6 deploy block: 45683465 on Base mainnet (2026-05-07)
- Architecture:
ARCHITECTURE.mdat SDK root - Fee math:
docs/FEE_FLOW.md
License
Apache-2.0
