@pafi-dev/core 0.3.0-beta.8 → 0.4.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.
@@ -155,9 +155,97 @@ function optionalDate(fields, key) {
155
155
  return new Date(raw);
156
156
  }
157
157
 
158
+ // src/auth/sponsorAuth.ts
159
+ import { keccak256, recoverTypedDataAddress } from "viem";
160
+ var SPONSOR_AUTH_DOMAIN_NAME = "PafiSponsorAuth";
161
+ var SPONSOR_AUTH_TYPES = {
162
+ SponsorAuth: [
163
+ { name: "chainId", type: "uint256" },
164
+ { name: "sender", type: "address" },
165
+ { name: "callDataHash", type: "bytes32" },
166
+ { name: "nonce", type: "uint256" },
167
+ { name: "expiresAt", type: "uint256" },
168
+ { name: "scenario", type: "string" },
169
+ { name: "issuerId", type: "string" }
170
+ ]
171
+ };
172
+ function buildSponsorAuthDomain(chainId) {
173
+ return {
174
+ name: SPONSOR_AUTH_DOMAIN_NAME,
175
+ version: "1",
176
+ chainId,
177
+ verifyingContract: "0x0000000000000000000000000000000000000000"
178
+ };
179
+ }
180
+ function buildMessage(payload) {
181
+ return {
182
+ chainId: BigInt(payload.chainId),
183
+ sender: payload.sender,
184
+ callDataHash: payload.callDataHash,
185
+ nonce: payload.nonce,
186
+ expiresAt: BigInt(payload.expiresAt),
187
+ scenario: payload.scenario,
188
+ issuerId: payload.issuerId
189
+ };
190
+ }
191
+ function buildSponsorAuthTypedData(payload) {
192
+ return {
193
+ domain: buildSponsorAuthDomain(payload.chainId),
194
+ types: SPONSOR_AUTH_TYPES,
195
+ primaryType: "SponsorAuth",
196
+ message: buildMessage(payload)
197
+ };
198
+ }
199
+ function computeCallDataHash(callData) {
200
+ return keccak256(callData);
201
+ }
202
+ async function signSponsorAuth(wallet, payload) {
203
+ const { domain, types, primaryType, message } = buildSponsorAuthTypedData(payload);
204
+ return wallet.signTypedData({
205
+ account: wallet.account,
206
+ domain,
207
+ types,
208
+ primaryType,
209
+ message
210
+ });
211
+ }
212
+ async function verifySponsorAuth(payload, signature, expectedSigner) {
213
+ const nowSec = Math.floor(Date.now() / 1e3);
214
+ if (payload.expiresAt < nowSec) {
215
+ return { ok: false, reason: "EXPIRED" };
216
+ }
217
+ if (signature.length < 132) {
218
+ return { ok: false, reason: "INVALID_SIGNATURE_FORMAT" };
219
+ }
220
+ let recovered;
221
+ try {
222
+ const { domain, types, primaryType, message } = buildSponsorAuthTypedData(payload);
223
+ recovered = await recoverTypedDataAddress({
224
+ domain,
225
+ types,
226
+ primaryType,
227
+ message,
228
+ signature
229
+ });
230
+ } catch {
231
+ return { ok: false, reason: "INVALID_SIGNATURE_FORMAT" };
232
+ }
233
+ if (recovered.toLowerCase() !== expectedSigner.toLowerCase()) {
234
+ return { ok: false, reason: "INVALID_SIGNER", recoveredAddress: recovered };
235
+ }
236
+ return { ok: true, recoveredAddress: recovered };
237
+ }
238
+
158
239
  export {
159
240
  createLoginMessage,
160
241
  parseLoginMessage,
161
- verifyLoginMessage
242
+ verifyLoginMessage,
243
+ SPONSOR_AUTH_DOMAIN_NAME,
244
+ SPONSOR_AUTH_TYPES,
245
+ buildSponsorAuthDomain,
246
+ buildSponsorAuthTypedData,
247
+ computeCallDataHash,
248
+ signSponsorAuth,
249
+ verifySponsorAuth
162
250
  };
163
- //# sourceMappingURL=chunk-O4SMTUOY.js.map
251
+ //# sourceMappingURL=chunk-W6VULMCO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/auth/loginMessage.ts","../src/auth/sponsorAuth.ts"],"sourcesContent":["import { getAddress, verifyMessage, recoverMessageAddress } from \"viem\";\nimport type { Address, Hex } from \"viem\";\nimport type { LoginMessageParams, VerifyLoginResult } from \"./types\";\n\nconst DEFAULT_VERSION = \"1\";\nconst DEFAULT_STATEMENT = \"Sign in with Ethereum to PAFI.\";\n\n/**\n * Build an EIP-4361 login message string.\n *\n * The output is a deterministic plain-text message that the wallet signs via\n * `personal_sign`. The same message can be parsed back with\n * {@link parseLoginMessage} and verified with {@link verifyLoginMessage}.\n */\nexport function createLoginMessage(params: LoginMessageParams): string {\n const {\n domain,\n address,\n chainId,\n nonce,\n uri,\n statement = DEFAULT_STATEMENT,\n version = DEFAULT_VERSION,\n issuedAt = new Date(),\n expirationTime,\n notBefore,\n requestId,\n } = params;\n\n if (!domain) throw new Error(\"createLoginMessage: domain required\");\n if (!nonce) throw new Error(\"createLoginMessage: nonce required\");\n if (!uri) throw new Error(\"createLoginMessage: uri required\");\n\n const checksummed = getAddress(address);\n\n const lines: string[] = [];\n lines.push(`${domain} wants you to sign in with your Ethereum account:`);\n lines.push(checksummed);\n lines.push(\"\");\n if (statement) {\n lines.push(statement);\n lines.push(\"\");\n }\n lines.push(`URI: ${uri}`);\n lines.push(`Version: ${version}`);\n lines.push(`Chain ID: ${chainId}`);\n lines.push(`Nonce: ${nonce}`);\n lines.push(`Issued At: ${issuedAt.toISOString()}`);\n if (expirationTime) {\n lines.push(`Expiration Time: ${expirationTime.toISOString()}`);\n }\n if (notBefore) {\n lines.push(`Not Before: ${notBefore.toISOString()}`);\n }\n if (requestId) {\n lines.push(`Request ID: ${requestId}`);\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Parse a login message string built by {@link createLoginMessage} back into\n * its structured fields. Throws if the message does not match the expected\n * EIP-4361 layout.\n */\nexport function parseLoginMessage(message: string): LoginMessageParams {\n const lines = message.split(\"\\n\");\n if (lines.length < 7) {\n throw new Error(\"parseLoginMessage: message too short\");\n }\n\n const headerLine = lines[0] ?? \"\";\n const headerMatch = headerLine.match(\n /^(?<domain>.+) wants you to sign in with your Ethereum account:$/,\n );\n if (!headerMatch || !headerMatch.groups) {\n throw new Error(\"parseLoginMessage: invalid header line\");\n }\n const domain = headerMatch.groups[\"domain\"]!;\n\n const addressLine = lines[1] ?? \"\";\n if (!/^0x[0-9a-fA-F]{40}$/.test(addressLine)) {\n throw new Error(\"parseLoginMessage: invalid address line\");\n }\n const address = getAddress(addressLine);\n\n // After address: blank line, optional statement + blank line, then key:value lines.\n let cursor = 2;\n if (lines[cursor] !== \"\") {\n throw new Error(\"parseLoginMessage: missing blank line after address\");\n }\n cursor++;\n\n let statement: string | undefined;\n // Statement is present if the next line is not a known key.\n if (cursor < lines.length && !looksLikeKeyValue(lines[cursor]!)) {\n statement = lines[cursor];\n cursor++;\n if (lines[cursor] !== \"\") {\n throw new Error(\"parseLoginMessage: missing blank line after statement\");\n }\n cursor++;\n }\n\n const fields = new Map<string, string>();\n for (; cursor < lines.length; cursor++) {\n const line = lines[cursor]!;\n if (line === \"\") continue;\n const idx = line.indexOf(\": \");\n if (idx === -1) {\n throw new Error(`parseLoginMessage: malformed field line: \"${line}\"`);\n }\n const key = line.slice(0, idx);\n const value = line.slice(idx + 2);\n fields.set(key, value);\n }\n\n const uri = requireField(fields, \"URI\");\n const version = requireField(fields, \"Version\");\n const chainId = Number(requireField(fields, \"Chain ID\"));\n if (!Number.isInteger(chainId)) {\n throw new Error(\"parseLoginMessage: Chain ID is not an integer\");\n }\n const nonce = requireField(fields, \"Nonce\");\n const issuedAt = new Date(requireField(fields, \"Issued At\"));\n const expirationTime = optionalDate(fields, \"Expiration Time\");\n const notBefore = optionalDate(fields, \"Not Before\");\n const requestId = fields.get(\"Request ID\");\n\n const result: LoginMessageParams = {\n domain,\n address,\n chainId,\n nonce,\n uri,\n version,\n issuedAt,\n };\n if (statement !== undefined) result.statement = statement;\n if (expirationTime !== undefined) result.expirationTime = expirationTime;\n if (notBefore !== undefined) result.notBefore = notBefore;\n if (requestId !== undefined) result.requestId = requestId;\n return result;\n}\n\n/**\n * Verify that a login message was signed by the address embedded in the\n * message. Returns `{ valid, address }` where `address` is the recovered\n * signer (checksummed). Does NOT check expiration / not-before / nonce\n * consumption — those are the AuthService's responsibility.\n */\nexport async function verifyLoginMessage(\n message: string,\n signature: Hex,\n): Promise<VerifyLoginResult> {\n const parsed = parseLoginMessage(message);\n const valid = await verifyMessage({\n address: parsed.address,\n message,\n signature,\n });\n if (valid) {\n return { valid: true, address: parsed.address };\n }\n // Recover anyway so callers can log the mismatch.\n const recovered = await recoverMessageAddress({ message, signature });\n return { valid: false, address: getAddress(recovered) as Address };\n}\n\n// -------------------------------------------------------------------------\n// helpers\n// -------------------------------------------------------------------------\n\nconst KNOWN_KEYS = new Set([\n \"URI\",\n \"Version\",\n \"Chain ID\",\n \"Nonce\",\n \"Issued At\",\n \"Expiration Time\",\n \"Not Before\",\n \"Request ID\",\n]);\n\nfunction looksLikeKeyValue(line: string): boolean {\n const idx = line.indexOf(\": \");\n if (idx === -1) return false;\n return KNOWN_KEYS.has(line.slice(0, idx));\n}\n\nfunction requireField(fields: Map<string, string>, key: string): string {\n const value = fields.get(key);\n if (value === undefined) {\n throw new Error(`parseLoginMessage: missing required field \"${key}\"`);\n }\n return value;\n}\n\nfunction optionalDate(\n fields: Map<string, string>,\n key: string,\n): Date | undefined {\n const raw = fields.get(key);\n if (raw === undefined) return undefined;\n return new Date(raw);\n}\n","import { keccak256, recoverTypedDataAddress } from \"viem\";\nimport type { Address, Hex, WalletClient } from \"viem\";\n\nexport const SPONSOR_AUTH_DOMAIN_NAME = \"PafiSponsorAuth\";\n\nexport const SPONSOR_AUTH_TYPES = {\n SponsorAuth: [\n { name: \"chainId\", type: \"uint256\" },\n { name: \"sender\", type: \"address\" },\n { name: \"callDataHash\", type: \"bytes32\" },\n { name: \"nonce\", type: \"uint256\" },\n { name: \"expiresAt\", type: \"uint256\" },\n { name: \"scenario\", type: \"string\" },\n { name: \"issuerId\", type: \"string\" },\n ],\n} as const;\n\nexport interface SponsorAuthPayload {\n chainId: number;\n sender: Address;\n callDataHash: Hex;\n nonce: bigint;\n expiresAt: number;\n scenario: string;\n issuerId: string;\n}\n\nexport interface SponsorAuthVerifyResult {\n ok: boolean;\n recoveredAddress?: Address;\n reason?: \"EXPIRED\" | \"INVALID_SIGNER\" | \"INVALID_SIGNATURE_FORMAT\";\n}\n\nexport function buildSponsorAuthDomain(chainId: number) {\n return {\n name: SPONSOR_AUTH_DOMAIN_NAME,\n version: \"1\",\n chainId,\n verifyingContract: \"0x0000000000000000000000000000000000000000\" as Address,\n };\n}\n\nfunction buildMessage(payload: SponsorAuthPayload) {\n return {\n chainId: BigInt(payload.chainId),\n sender: payload.sender,\n callDataHash: payload.callDataHash,\n nonce: payload.nonce,\n expiresAt: BigInt(payload.expiresAt),\n scenario: payload.scenario,\n issuerId: payload.issuerId,\n };\n}\n\nexport function buildSponsorAuthTypedData(payload: SponsorAuthPayload) {\n return {\n domain: buildSponsorAuthDomain(payload.chainId),\n types: SPONSOR_AUTH_TYPES,\n primaryType: \"SponsorAuth\" as const,\n message: buildMessage(payload),\n };\n}\n\nexport function computeCallDataHash(callData: Hex): Hex {\n return keccak256(callData);\n}\n\nexport async function signSponsorAuth(\n wallet: WalletClient,\n payload: SponsorAuthPayload,\n): Promise<Hex> {\n const { domain, types, primaryType, message } =\n buildSponsorAuthTypedData(payload);\n return wallet.signTypedData({\n account: wallet.account!,\n domain,\n types,\n primaryType,\n message,\n });\n}\n\nexport async function verifySponsorAuth(\n payload: SponsorAuthPayload,\n signature: Hex,\n expectedSigner: Address,\n): Promise<SponsorAuthVerifyResult> {\n const nowSec = Math.floor(Date.now() / 1000);\n if (payload.expiresAt < nowSec) {\n return { ok: false, reason: \"EXPIRED\" };\n }\n\n if (signature.length < 132) {\n return { ok: false, reason: \"INVALID_SIGNATURE_FORMAT\" };\n }\n\n let recovered: Address;\n try {\n const { domain, types, primaryType, message } =\n buildSponsorAuthTypedData(payload);\n recovered = await recoverTypedDataAddress({\n domain,\n types,\n primaryType,\n message,\n signature,\n });\n } catch {\n return { ok: false, reason: \"INVALID_SIGNATURE_FORMAT\" };\n }\n\n if (recovered.toLowerCase() !== expectedSigner.toLowerCase()) {\n return { ok: false, reason: \"INVALID_SIGNER\", recoveredAddress: recovered };\n }\n\n return { ok: true, recoveredAddress: recovered };\n}\n"],"mappings":"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","names":[]}
@@ -150,35 +150,32 @@ interface UserOpReceipt {
150
150
  declare const ZERO_VALUE = 0n;
151
151
 
152
152
  /**
153
- * v1.5 — Scenario 4: Swap PT → USDT on PAFI Web with **gas fee
154
- * deducted in PT** from the user's balance.
153
+ * v1.5 — Scenario 4: Swap PT → USDT on PAFI Web with gas fee
154
+ * deducted in PT from the user's balance.
155
155
  *
156
- * Builds an unsigned `PartialUserOperation` that packages three inner
157
- * calls into a single `BatchExecutor.execute(calls[])`:
156
+ * Builds an unsigned `PartialUserOperation` that packages up to four
157
+ * inner calls into a single `BatchExecutor.execute(calls[])`:
158
158
  *
159
- * 1. `PT.approve(UniversalRouter, amountIn)` — let the router pull
160
- * `amountIn` PT for the swap
161
- * 2. `UniversalRouter.execute(commands, inputs, deadline)` V4 swap
162
- * PT USDT, user receives `minAmountOut` USDT
163
- * 3. `PT.transfer(feeRecipient, gasFeePt)` pay the operator back
164
- * for sponsoring the gas, in PT (application-level recovery —
165
- * no custom paymaster contract needed)
159
+ * 1. `PT.approve(Permit2, amountIn)` — grant Permit2 ERC-20 allowance
160
+ * 2. `Permit2.approve(PT, router, amountIn, deadline)` authorize
161
+ * the UniversalRouter to pull PT via Permit2
162
+ * 3. `UniversalRouter.execute(commands, inputs, deadline)` V4 swap
163
+ * PT USDT; user receives `minAmountOut` USDT
164
+ * 4. `PT.transfer(feeRecipient, gasFeePt)` pay the operator back
165
+ * for sponsoring the gas, in PT (omitted when `gasFeePt` is 0)
166
166
  *
167
- * The user's wallet must hold `amountIn + gasFeePt` PT **before** the
168
- * UserOp runs. The three inner calls execute atomically via
169
- * EIP-7702 delegation (`msg.sender = user`), so a reverting swap
170
- * unwinds the approve + fee transfer too.
167
+ * The user's wallet must hold `amountIn + gasFeePt` PT before the
168
+ * UserOp runs. All inner calls execute atomically via EIP-7702
169
+ * delegation (`msg.sender = user`), so a reverting swap unwinds
170
+ * the approvals and fee transfer too.
171
171
  *
172
172
  * ## Fee model
173
173
  *
174
- * Exact-out on the **USDT side** is not attempted here. The user
175
- * specifies `amountIn` PT for the swap; the minimum USDT out comes
176
- * from a separate quote (not this function). The gas fee is a
177
- * separate `transfer` call — it does NOT come out of the swap's USDT
178
- * output, because that output goes straight to the user's wallet with
179
- * no intermediate hook to skim from.
174
+ * The gas fee is a separate `transfer` after the swap — it does NOT
175
+ * come out of the USDT output, because that output goes straight to
176
+ * the user's wallet with no intermediate hook.
180
177
  *
181
- * If the FE wants "user receives exactly X USDT after gas", it should:
178
+ * If the FE wants "user receives exactly X USDT after gas":
182
179
  * 1. Quote PT→USDT for candidate `amountIn` values
183
180
  * 2. Pick an `amountIn` where `minAmountOut ≈ X`
184
181
  * 3. Separately compute `gasFeePt` from the operator's rate
@@ -186,11 +183,8 @@ declare const ZERO_VALUE = 0n;
186
183
  *
187
184
  * ## Order of operations
188
185
  *
189
- * Approve swap fee transfer. The fee transfer comes **last** so
190
- * if the swap reverts, the user is also refunded the fee (atomic
191
- * batch revert). Doing fee transfer first would still unwind on
192
- * revert, but ordering it last matches the intuitive "only charge
193
- * gas on success" semantics.
186
+ * Fee transfer is last so a reverting swap also refunds the fee
187
+ * (atomic batch revert semantics).
194
188
  */
195
189
  interface BuildSwapWithGasDeductionParams {
196
190
  /** User's EOA (with EIP-7702 delegation to BatchExecutor). */
@@ -218,12 +212,7 @@ interface BuildSwapWithGasDeductionParams {
218
212
  gasFeePt: bigint;
219
213
  /** Where the gas fee lands — typically the operator or fee collector. */
220
214
  feeRecipient: Address;
221
- /**
222
- * Optional — pre-signed Permit2 / EIP-2612 approval. If omitted, a
223
- * standard `approve()` call is appended as the first batch op.
224
- * Future work: support Permit2 so the approve is part of the swap
225
- * input rather than a separate call.
226
- */
215
+ /** Override ERC-4337 gas estimates. Defaults are conservative; tune down for cheaper UserOps. */
227
216
  gasLimits?: {
228
217
  callGasLimit?: bigint;
229
218
  verificationGasLimit?: bigint;
@@ -150,35 +150,32 @@ interface UserOpReceipt {
150
150
  declare const ZERO_VALUE = 0n;
151
151
 
152
152
  /**
153
- * v1.5 — Scenario 4: Swap PT → USDT on PAFI Web with **gas fee
154
- * deducted in PT** from the user's balance.
153
+ * v1.5 — Scenario 4: Swap PT → USDT on PAFI Web with gas fee
154
+ * deducted in PT from the user's balance.
155
155
  *
156
- * Builds an unsigned `PartialUserOperation` that packages three inner
157
- * calls into a single `BatchExecutor.execute(calls[])`:
156
+ * Builds an unsigned `PartialUserOperation` that packages up to four
157
+ * inner calls into a single `BatchExecutor.execute(calls[])`:
158
158
  *
159
- * 1. `PT.approve(UniversalRouter, amountIn)` — let the router pull
160
- * `amountIn` PT for the swap
161
- * 2. `UniversalRouter.execute(commands, inputs, deadline)` V4 swap
162
- * PT USDT, user receives `minAmountOut` USDT
163
- * 3. `PT.transfer(feeRecipient, gasFeePt)` pay the operator back
164
- * for sponsoring the gas, in PT (application-level recovery —
165
- * no custom paymaster contract needed)
159
+ * 1. `PT.approve(Permit2, amountIn)` — grant Permit2 ERC-20 allowance
160
+ * 2. `Permit2.approve(PT, router, amountIn, deadline)` authorize
161
+ * the UniversalRouter to pull PT via Permit2
162
+ * 3. `UniversalRouter.execute(commands, inputs, deadline)` V4 swap
163
+ * PT USDT; user receives `minAmountOut` USDT
164
+ * 4. `PT.transfer(feeRecipient, gasFeePt)` pay the operator back
165
+ * for sponsoring the gas, in PT (omitted when `gasFeePt` is 0)
166
166
  *
167
- * The user's wallet must hold `amountIn + gasFeePt` PT **before** the
168
- * UserOp runs. The three inner calls execute atomically via
169
- * EIP-7702 delegation (`msg.sender = user`), so a reverting swap
170
- * unwinds the approve + fee transfer too.
167
+ * The user's wallet must hold `amountIn + gasFeePt` PT before the
168
+ * UserOp runs. All inner calls execute atomically via EIP-7702
169
+ * delegation (`msg.sender = user`), so a reverting swap unwinds
170
+ * the approvals and fee transfer too.
171
171
  *
172
172
  * ## Fee model
173
173
  *
174
- * Exact-out on the **USDT side** is not attempted here. The user
175
- * specifies `amountIn` PT for the swap; the minimum USDT out comes
176
- * from a separate quote (not this function). The gas fee is a
177
- * separate `transfer` call — it does NOT come out of the swap's USDT
178
- * output, because that output goes straight to the user's wallet with
179
- * no intermediate hook to skim from.
174
+ * The gas fee is a separate `transfer` after the swap — it does NOT
175
+ * come out of the USDT output, because that output goes straight to
176
+ * the user's wallet with no intermediate hook.
180
177
  *
181
- * If the FE wants "user receives exactly X USDT after gas", it should:
178
+ * If the FE wants "user receives exactly X USDT after gas":
182
179
  * 1. Quote PT→USDT for candidate `amountIn` values
183
180
  * 2. Pick an `amountIn` where `minAmountOut ≈ X`
184
181
  * 3. Separately compute `gasFeePt` from the operator's rate
@@ -186,11 +183,8 @@ declare const ZERO_VALUE = 0n;
186
183
  *
187
184
  * ## Order of operations
188
185
  *
189
- * Approve swap fee transfer. The fee transfer comes **last** so
190
- * if the swap reverts, the user is also refunded the fee (atomic
191
- * batch revert). Doing fee transfer first would still unwind on
192
- * revert, but ordering it last matches the intuitive "only charge
193
- * gas on success" semantics.
186
+ * Fee transfer is last so a reverting swap also refunds the fee
187
+ * (atomic batch revert semantics).
194
188
  */
195
189
  interface BuildSwapWithGasDeductionParams {
196
190
  /** User's EOA (with EIP-7702 delegation to BatchExecutor). */
@@ -218,12 +212,7 @@ interface BuildSwapWithGasDeductionParams {
218
212
  gasFeePt: bigint;
219
213
  /** Where the gas fee lands — typically the operator or fee collector. */
220
214
  feeRecipient: Address;
221
- /**
222
- * Optional — pre-signed Permit2 / EIP-2612 approval. If omitted, a
223
- * standard `approve()` call is appended as the first batch op.
224
- * Future work: support Permit2 so the approve is part of the swap
225
- * input rather than a separate call.
226
- */
215
+ /** Override ERC-4337 gas estimates. Defaults are conservative; tune down for cheaper UserOps. */
227
216
  gasLimits?: {
228
217
  callGasLimit?: bigint;
229
218
  verificationGasLimit?: bigint;