@pafi-dev/trading 0.1.4 → 0.1.5
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/README.md +103 -11
- package/dist/index.cjs +70 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -6
- package/dist/index.d.ts +62 -6
- package/dist/index.js +72 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,28 +164,70 @@ After receiving the response, the frontend:
|
|
|
164
164
|
|
|
165
165
|
### `handlePerpDeposit` — POST /perp-deposit
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
Build an unsigned UserOp that deposits USDC into Orderly perp. The
|
|
168
|
+
handler picks one of two paths automatically:
|
|
169
|
+
|
|
170
|
+
#### Path 1 — Relay (default, gas-sponsored)
|
|
171
|
+
|
|
172
|
+
`USDC.approve(relay)` → `relay.deposit(req)`
|
|
173
|
+
|
|
174
|
+
The PAFI Orderly Relay holds an ETH reserve and pays Orderly's
|
|
175
|
+
LayerZero `msg.value` out of it. The user pays a USDC fee instead
|
|
176
|
+
(quoted via `Relay.quoteTokenFee` and capped by `maxRelayFee`).
|
|
177
|
+
Combined with paymaster sponsorship of ERC-4337 gas, **the user does
|
|
178
|
+
not need any native ETH** — they only need USDC.
|
|
169
179
|
|
|
170
180
|
```ts
|
|
171
181
|
const deposit = await trading.handlePerpDeposit({
|
|
172
182
|
chainId: 8453,
|
|
173
183
|
userAddress: "0xUserEOA",
|
|
174
|
-
amount: 100_000_000n,
|
|
184
|
+
amount: 100_000_000n, // 100 USDC (6 decimals)
|
|
175
185
|
aaNonce: 0n,
|
|
176
|
-
brokerId: "woofi_pro",
|
|
186
|
+
brokerId: "woofi_pro", // "woofi_pro" | "orderly" | "logx"
|
|
187
|
+
// viaRelay: true, // default
|
|
188
|
+
// maxRelayFee: 5_000_000n, // optional, default = 5% of amount
|
|
177
189
|
});
|
|
178
190
|
|
|
191
|
+
// deposit.path === "relay"
|
|
192
|
+
// deposit.userOp — USDC.approve(relay) + relay.deposit(req)
|
|
193
|
+
// deposit.relayTokenFee — USDC fee charged by the Relay
|
|
194
|
+
// deposit.layerZeroFee — ETH wei the Relay covers on the user's behalf (informational)
|
|
195
|
+
// deposit.relayAddress — getContractAddresses(chainId).orderlyRelay
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### Path 2 — Vault (fallback, requires native ETH)
|
|
199
|
+
|
|
200
|
+
`USDC.approve(vault)` → `vault.deposit{value: layerZeroFee}(data)`
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const deposit = await trading.handlePerpDeposit({
|
|
204
|
+
chainId: 8453,
|
|
205
|
+
userAddress: "0xUserEOA",
|
|
206
|
+
amount: 100_000_000n,
|
|
207
|
+
aaNonce: 0n,
|
|
208
|
+
brokerId: "woofi_pro",
|
|
209
|
+
viaRelay: false, // explicit opt-out of Relay
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// deposit.path === "vault"
|
|
179
213
|
// deposit.userOp — unsigned PartialUserOperation
|
|
180
|
-
// deposit.layerZeroFee —
|
|
181
|
-
// deposit.
|
|
182
|
-
// deposit.brokerHash — keccak256(brokerId)
|
|
183
|
-
// deposit.usdcAddress — USDC resolved from Vault.getAllowedToken()
|
|
214
|
+
// deposit.layerZeroFee — REQUIRED native ETH balance on the sender
|
|
215
|
+
// deposit.relayTokenFee — 0n
|
|
184
216
|
```
|
|
185
217
|
|
|
186
|
-
> **
|
|
187
|
-
> LayerZero fee) must come from the user's
|
|
188
|
-
>
|
|
218
|
+
> **Vault path = native ETH required.** The paymaster sponsors ERC-4337
|
|
219
|
+
> gas, but `msg.value` (the LayerZero fee) must come from the user's
|
|
220
|
+
> own ETH balance. Always check `walletEth >= layerZeroFee` before
|
|
221
|
+
> submitting, OR — far better — leave `viaRelay` at its default to
|
|
222
|
+
> stay on the zero-ETH path.
|
|
223
|
+
|
|
224
|
+
#### Auto-fallback
|
|
225
|
+
|
|
226
|
+
When `viaRelay` is `true` (default) but
|
|
227
|
+
`getContractAddresses(chainId).orderlyRelay` is the placeholder
|
|
228
|
+
sentinel (Relay not deployed for that chain), the handler silently
|
|
229
|
+
falls back to the Vault path. Inspect `deposit.path` if you need to
|
|
230
|
+
distinguish.
|
|
189
231
|
|
|
190
232
|
---
|
|
191
233
|
|
|
@@ -299,6 +341,56 @@ It does not depend on `@pafi-dev/issuer`.
|
|
|
299
341
|
|
|
300
342
|
## Changelog
|
|
301
343
|
|
|
344
|
+
### 0.1.5
|
|
345
|
+
|
|
346
|
+
`handlePerpDeposit` now defaults to the **PAFI Orderly Relay** path —
|
|
347
|
+
zero native ETH required.
|
|
348
|
+
|
|
349
|
+
**Why.** The previous Vault-direct path required the user wallet to
|
|
350
|
+
hold `layerZeroFee` as native ETH (paymaster sponsors ERC-4337 gas
|
|
351
|
+
but never `msg.value`). For users coming through Privy embedded
|
|
352
|
+
wallets / sponsored onboarding flows that's often `0 ETH`, so the
|
|
353
|
+
deposit reverted with `BatchExecutor.CallFailed(1, 0x)` (no-data
|
|
354
|
+
revert from the Vault when it can't pay LayerZero out of the EOA).
|
|
355
|
+
|
|
356
|
+
The Relay (deployed at `getContractAddresses(8453).orderlyRelay
|
|
357
|
+
=` `0xDA082DAce1522c185aeB5A713FcA6fa6B6E99e7f` on Base) holds an ETH
|
|
358
|
+
reserve and pays LayerZero out of it; the user pays a USDC fee
|
|
359
|
+
instead. Combined with paymaster sponsorship, the user only needs
|
|
360
|
+
USDC.
|
|
361
|
+
|
|
362
|
+
**API additions** on `ApiPerpDepositRequest`:
|
|
363
|
+
|
|
364
|
+
- `viaRelay?: boolean` — default `true`. Set `false` to force the
|
|
365
|
+
Vault fallback.
|
|
366
|
+
- `maxRelayFee?: bigint` — slippage cap on the Relay's USDC fee.
|
|
367
|
+
Defaults to 5% of `amount`.
|
|
368
|
+
- `pointTokenAddress?` / `gasFeePt?` / `gasFeePtRecipient?` — optional
|
|
369
|
+
PT gas-fee transfer prepended to the batch (sponsored issuer flow).
|
|
370
|
+
|
|
371
|
+
**API additions** on `ApiPerpDepositResponse`:
|
|
372
|
+
|
|
373
|
+
- `path: "relay" | "vault"` — which execution path the handler chose.
|
|
374
|
+
Inspect this when `viaRelay` was left at its default to know if the
|
|
375
|
+
auto-fallback to Vault triggered (e.g. on a chain without a deployed
|
|
376
|
+
Relay).
|
|
377
|
+
- `relayTokenFee: bigint` — USDC fee the Relay will charge. `0n` when
|
|
378
|
+
`path === "vault"`.
|
|
379
|
+
- `relayAddress: Address` — the address the UserOp targets. Equal to
|
|
380
|
+
the Vault address when `path === "vault"`.
|
|
381
|
+
|
|
382
|
+
When `viaRelay` is `true` but no Relay is deployed for the chain
|
|
383
|
+
(`getContractAddresses(chainId).orderlyRelay` is a placeholder), the
|
|
384
|
+
handler silently falls back to the Vault path — inspect
|
|
385
|
+
`response.path` to detect this.
|
|
386
|
+
|
|
387
|
+
### 0.1.4
|
|
388
|
+
|
|
389
|
+
- README rewrite for the **frontend "quote review" pattern** — direct
|
|
390
|
+
client-side `trading.handleQuote` call, no HTTP hop through the
|
|
391
|
+
issuer backend. Quotes are stateless on-chain reads, so the
|
|
392
|
+
pre-existing `GET /quote` proxy was redundant.
|
|
393
|
+
|
|
302
394
|
### 0.1.3
|
|
303
395
|
|
|
304
396
|
- Peer dependency `@pafi-dev/core` bumped to `0.5.17` so the swap path
|
package/dist/index.cjs
CHANGED
|
@@ -149,10 +149,20 @@ var TradingHandlers = class {
|
|
|
149
149
|
/**
|
|
150
150
|
* Build an Orderly perp deposit UserOp.
|
|
151
151
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
152
|
+
* Default path is the **PAFI Orderly Relay** (`viaRelay: true`):
|
|
153
|
+
* USDC.approve(relay) + relay.deposit(req). The Relay holds an ETH
|
|
154
|
+
* reserve and pays Orderly's LayerZero `msg.value` out of it; the
|
|
155
|
+
* user pays a USDC fee (quoted via `Relay.quoteTokenFee`) instead.
|
|
156
|
+
* No native ETH on the user wallet is required, so paymaster
|
|
157
|
+
* sponsorship of the ERC-4337 gas is sufficient end-to-end.
|
|
158
|
+
*
|
|
159
|
+
* Fallback path (`viaRelay: false`): direct `Vault.deposit{value}`.
|
|
160
|
+
* Reserved for chains where no Relay is deployed — the user wallet
|
|
161
|
+
* **must** hold `layerZeroFee` as native ETH.
|
|
162
|
+
*
|
|
163
|
+
* The Relay path automatically falls back to Vault when
|
|
164
|
+
* `getContractAddresses(chainId).orderlyRelay` is the placeholder
|
|
165
|
+
* sentinel (Relay not deployed for that chain).
|
|
156
166
|
*/
|
|
157
167
|
async handlePerpDeposit(request) {
|
|
158
168
|
if (request.chainId !== this.chainId) {
|
|
@@ -203,6 +213,49 @@ var TradingHandlers = class {
|
|
|
203
213
|
functionName: "getDepositFee",
|
|
204
214
|
args: [userAddress, depositData]
|
|
205
215
|
});
|
|
216
|
+
const useRelay = request.viaRelay !== false;
|
|
217
|
+
const relayAddress = (0, import_core.getContractAddresses)(request.chainId).orderlyRelay;
|
|
218
|
+
const relayDeployed = !isPlaceholderAddress(relayAddress);
|
|
219
|
+
if (useRelay && relayDeployed) {
|
|
220
|
+
const maxRelayFee = request.maxRelayFee ?? request.amount * 500n / 10000n;
|
|
221
|
+
const relayRequest = {
|
|
222
|
+
token: usdcAddress,
|
|
223
|
+
receiver: userAddress,
|
|
224
|
+
brokerHash,
|
|
225
|
+
totalAmount: request.amount,
|
|
226
|
+
maxFee: maxRelayFee
|
|
227
|
+
};
|
|
228
|
+
const relayTokenFee = await this.provider.readContract({
|
|
229
|
+
address: relayAddress,
|
|
230
|
+
abi: import_core.ORDERLY_RELAY_ABI,
|
|
231
|
+
functionName: "quoteTokenFee",
|
|
232
|
+
args: [relayRequest]
|
|
233
|
+
});
|
|
234
|
+
if (relayTokenFee > maxRelayFee) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`handlePerpDeposit: Relay tokenFee ${relayTokenFee} exceeds maxRelayFee ${maxRelayFee}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const userOp2 = (0, import_core.buildPerpDepositViaRelay)({
|
|
240
|
+
userAddress,
|
|
241
|
+
aaNonce: request.aaNonce,
|
|
242
|
+
relayAddress,
|
|
243
|
+
request: relayRequest,
|
|
244
|
+
pointTokenAddress: request.pointTokenAddress,
|
|
245
|
+
gasFeePt: request.gasFeePt,
|
|
246
|
+
gasFeePtRecipient: request.gasFeePtRecipient
|
|
247
|
+
});
|
|
248
|
+
return {
|
|
249
|
+
userOp: userOp2,
|
|
250
|
+
path: "relay",
|
|
251
|
+
layerZeroFee,
|
|
252
|
+
relayTokenFee,
|
|
253
|
+
accountId,
|
|
254
|
+
brokerHash,
|
|
255
|
+
usdcAddress,
|
|
256
|
+
relayAddress
|
|
257
|
+
};
|
|
258
|
+
}
|
|
206
259
|
const userOp = (0, import_core.buildPerpDepositWithGasDeduction)({
|
|
207
260
|
userAddress,
|
|
208
261
|
aaNonce: request.aaNonce,
|
|
@@ -212,9 +265,21 @@ var TradingHandlers = class {
|
|
|
212
265
|
depositData,
|
|
213
266
|
layerZeroFee
|
|
214
267
|
});
|
|
215
|
-
return {
|
|
268
|
+
return {
|
|
269
|
+
userOp,
|
|
270
|
+
path: "vault",
|
|
271
|
+
layerZeroFee,
|
|
272
|
+
relayTokenFee: 0n,
|
|
273
|
+
accountId,
|
|
274
|
+
brokerHash,
|
|
275
|
+
usdcAddress,
|
|
276
|
+
relayAddress: vault
|
|
277
|
+
};
|
|
216
278
|
}
|
|
217
279
|
};
|
|
280
|
+
function isPlaceholderAddress(addr) {
|
|
281
|
+
return /^0x0{36}[0-9a-fA-F]{4}$/i.test(addr);
|
|
282
|
+
}
|
|
218
283
|
|
|
219
284
|
// src/pools.ts
|
|
220
285
|
var import_core2 = require("@pafi-dev/core");
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/api/handlers.ts","../src/pools.ts"],"sourcesContent":["export { TradingHandlers } from \"./api/handlers\";\nexport type { TradingHandlersConfig } from \"./api/handlers\";\n\nexport type {\n ApiQuoteRequest,\n ApiQuoteResponse,\n ApiQuoteError,\n ApiSwapRequest,\n ApiSwapResponse,\n ApiPerpDepositRequest,\n ApiPerpDepositResponse,\n} from \"./api/types\";\n\nexport { fetchPafiPools, PAFI_SUBGRAPH_URL } from \"./pools\";\n","import { getAddress } from \"viem\";\nimport type { Address, PublicClient } from \"viem\";\nimport {\n findBestQuote,\n buildSwapWithGasDeduction,\n buildPerpDepositWithGasDeduction,\n getContractAddresses,\n UNIVERSAL_ROUTER_ADDRESSES,\n ORDERLY_VAULT_ABI,\n ORDERLY_VAULT_ADDRESSES,\n BROKER_HASHES,\n TOKEN_HASHES,\n computeAccountId,\n} from \"@pafi-dev/core\";\nimport type {\n ApiQuoteRequest,\n ApiQuoteResponse,\n ApiSwapRequest,\n ApiSwapResponse,\n ApiPerpDepositRequest,\n ApiPerpDepositResponse,\n} from \"./types\";\n\nexport interface TradingHandlersConfig {\n provider: PublicClient;\n chainId: number;\n}\n\n/**\n * Framework-agnostic handlers for on-chain trading actions.\n *\n * All handlers are stateless — they need only a PublicClient for RPC\n * calls. No ledger, no signer, no DB. Issuers wrap these in their own\n * HTTP controllers (Express / NestJS / Hono / etc.) the same way they\n * wrap `IssuerApiHandlers` from `@pafi-dev/issuer`.\n *\n * Example (NestJS):\n *\n * const trading = new TradingHandlers({ provider, chainId });\n *\n * // GET /quote\n * const quote = await trading.handleQuote({ chainId, pointTokenAddress, amount, pools });\n *\n * // POST /swap\n * const swap = await trading.handleSwap({ chainId, userAddress, pointTokenAddress, amount, aaNonce });\n *\n * // POST /perp-deposit\n * const deposit = await trading.handlePerpDeposit({ chainId, userAddress, amount, aaNonce, brokerId });\n */\nexport class TradingHandlers {\n private readonly provider: PublicClient;\n private readonly chainId: number;\n\n constructor(config: TradingHandlersConfig) {\n this.provider = config.provider;\n this.chainId = config.chainId;\n }\n\n // =========================================================================\n // GET /quote\n // =========================================================================\n\n /**\n * Quote exact-input PT → USDT via Uniswap V4 on-chain Quoter.\n *\n * Uses multicall to batch all candidate routes into a single RPC call.\n * Returns `quoteError: \"QUOTE_UNAVAILABLE\"` when no pool/path exists\n * rather than throwing, so callers can show a soft \"unavailable\" UI\n * state without 500-ing.\n */\n async handleQuote(request: ApiQuoteRequest): Promise<ApiQuoteResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleQuote: unsupported chainId ${request.chainId}`);\n }\n if (request.amount === 0n) {\n return { pointAmount: 0n, estimatedUsdtOut: 0n, gasEstimate: 0n };\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const pools = request.pools ?? [];\n\n try {\n const best = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: best.bestRoute.amountOut,\n gasEstimate: best.bestRoute.gasEstimate,\n };\n } catch {\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: 0n,\n gasEstimate: 0n,\n quoteError: \"QUOTE_UNAVAILABLE\",\n };\n }\n }\n\n // =========================================================================\n // POST /swap\n // =========================================================================\n\n /**\n * Build a PT → USDT swap UserOp.\n *\n * Quotes the best route, applies slippage, then encodes a 4-step\n * batch: PT.approve → Permit2.approve → UniversalRouter.execute →\n * PT.transfer (fee, omitted when gasFeePt = 0). Returns an unsigned\n * `PartialUserOperation`; caller attaches paymaster data + user\n * signature and submits to the Bundler.\n */\n async handleSwap(request: ApiSwapRequest): Promise<ApiSwapResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleSwap: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handleSwap: amount must be positive\");\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const universalRouter = UNIVERSAL_ROUTER_ADDRESSES[request.chainId];\n if (!universalRouter) {\n throw new Error(`handleSwap: no UniversalRouter for chainId ${request.chainId}`);\n }\n\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const userAddress = getAddress(request.userAddress);\n const pools = request.pools ?? [];\n const slippageBps = request.slippageBps ?? 50;\n const gasFeePt = request.gasFeePt ?? 0n;\n\n if (gasFeePt > 0n && !request.feeRecipient) {\n throw new Error(\"handleSwap: feeRecipient required when gasFeePt > 0\");\n }\n\n let quoteResult: Awaited<ReturnType<typeof findBestQuote>>;\n try {\n quoteResult = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n } catch {\n throw new Error(\"handleSwap: no swap path found for this point token\");\n }\n\n const estimatedUsdtOut = quoteResult.bestRoute.amountOut;\n const minAmountOut = (estimatedUsdtOut * BigInt(10000 - slippageBps)) / 10000n;\n const deadline = BigInt(Math.floor(Date.now() / 1000) + 5 * 60);\n\n const userOp = buildSwapWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n pointTokenAddress,\n outputTokenAddress: usdt,\n universalRouterAddress: universalRouter,\n amountIn: request.amount,\n minAmountOut,\n swapPath: quoteResult.bestRoute.path,\n deadline,\n gasFeePt,\n feeRecipient: request.feeRecipient ?? userAddress,\n });\n\n return { userOp, estimatedUsdtOut, minAmountOut, deadline };\n }\n\n // =========================================================================\n // POST /perp-deposit\n // =========================================================================\n\n /**\n * Build an Orderly perp deposit UserOp.\n *\n * Resolves USDC address and LayerZero fee from on-chain Vault reads,\n * then encodes a 2-step batch: USDC.approve → Vault.deposit{value}.\n * The `layerZeroFee` in the response is the ETH the user must hold\n * natively — paymaster sponsors ERC-4337 gas only, not msg.value.\n */\n async handlePerpDeposit(request: ApiPerpDepositRequest): Promise<ApiPerpDepositResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handlePerpDeposit: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handlePerpDeposit: amount must be positive\");\n }\n\n const vault = ORDERLY_VAULT_ADDRESSES[request.chainId];\n if (!vault) {\n throw new Error(`handlePerpDeposit: no Orderly Vault for chainId ${request.chainId}`);\n }\n\n const brokerHash = BROKER_HASHES[request.brokerId as keyof typeof BROKER_HASHES];\n if (!brokerHash) {\n throw new Error(`handlePerpDeposit: unknown brokerId \"${request.brokerId}\"`);\n }\n const tokenHash = TOKEN_HASHES.USDC;\n const userAddress = getAddress(request.userAddress);\n\n const [usdcAddress, brokerAllowed] = await Promise.all([\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedToken\",\n args: [tokenHash],\n }) as Promise<Address>,\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedBroker\",\n args: [brokerHash],\n }) as Promise<boolean>,\n ]);\n\n if (!brokerAllowed) {\n throw new Error(\n `handlePerpDeposit: broker \"${request.brokerId}\" is not whitelisted on Orderly Vault`,\n );\n }\n\n const accountId = computeAccountId(userAddress, brokerHash);\n const depositData = {\n accountId,\n brokerHash,\n tokenHash,\n tokenAmount: request.amount,\n };\n\n const layerZeroFee = await this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getDepositFee\",\n args: [userAddress, depositData],\n }) as bigint;\n\n const userOp = buildPerpDepositWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n chainId: request.chainId,\n usdcAddress,\n amount: request.amount,\n depositData,\n layerZeroFee,\n });\n\n return { userOp, layerZeroFee, accountId, brokerHash, usdcAddress };\n }\n}\n","// Re-export from @pafi-dev/core — fetchPafiPools lives in core so all\n// SDK packages share one implementation.\nexport { fetchPafiPools, PAFI_SUBGRAPH_URL } from \"@pafi-dev/core\";\n"],"mappings":"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","names":["import_core"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/api/handlers.ts","../src/pools.ts"],"sourcesContent":["export { TradingHandlers } from \"./api/handlers\";\nexport type { TradingHandlersConfig } from \"./api/handlers\";\n\nexport type {\n ApiQuoteRequest,\n ApiQuoteResponse,\n ApiQuoteError,\n ApiSwapRequest,\n ApiSwapResponse,\n ApiPerpDepositRequest,\n ApiPerpDepositResponse,\n} from \"./api/types\";\n\nexport { fetchPafiPools, PAFI_SUBGRAPH_URL } from \"./pools\";\n","import { getAddress } from \"viem\";\nimport type { Address, PublicClient } from \"viem\";\nimport {\n findBestQuote,\n buildSwapWithGasDeduction,\n buildPerpDepositWithGasDeduction,\n buildPerpDepositViaRelay,\n ORDERLY_RELAY_ABI,\n getContractAddresses,\n UNIVERSAL_ROUTER_ADDRESSES,\n ORDERLY_VAULT_ABI,\n ORDERLY_VAULT_ADDRESSES,\n BROKER_HASHES,\n TOKEN_HASHES,\n computeAccountId,\n} from \"@pafi-dev/core\";\nimport type {\n ApiQuoteRequest,\n ApiQuoteResponse,\n ApiSwapRequest,\n ApiSwapResponse,\n ApiPerpDepositRequest,\n ApiPerpDepositResponse,\n} from \"./types\";\n\nexport interface TradingHandlersConfig {\n provider: PublicClient;\n chainId: number;\n}\n\n/**\n * Framework-agnostic handlers for on-chain trading actions.\n *\n * All handlers are stateless — they need only a PublicClient for RPC\n * calls. No ledger, no signer, no DB. Issuers wrap these in their own\n * HTTP controllers (Express / NestJS / Hono / etc.) the same way they\n * wrap `IssuerApiHandlers` from `@pafi-dev/issuer`.\n *\n * Example (NestJS):\n *\n * const trading = new TradingHandlers({ provider, chainId });\n *\n * // GET /quote\n * const quote = await trading.handleQuote({ chainId, pointTokenAddress, amount, pools });\n *\n * // POST /swap\n * const swap = await trading.handleSwap({ chainId, userAddress, pointTokenAddress, amount, aaNonce });\n *\n * // POST /perp-deposit\n * const deposit = await trading.handlePerpDeposit({ chainId, userAddress, amount, aaNonce, brokerId });\n */\nexport class TradingHandlers {\n private readonly provider: PublicClient;\n private readonly chainId: number;\n\n constructor(config: TradingHandlersConfig) {\n this.provider = config.provider;\n this.chainId = config.chainId;\n }\n\n // =========================================================================\n // GET /quote\n // =========================================================================\n\n /**\n * Quote exact-input PT → USDT via Uniswap V4 on-chain Quoter.\n *\n * Uses multicall to batch all candidate routes into a single RPC call.\n * Returns `quoteError: \"QUOTE_UNAVAILABLE\"` when no pool/path exists\n * rather than throwing, so callers can show a soft \"unavailable\" UI\n * state without 500-ing.\n */\n async handleQuote(request: ApiQuoteRequest): Promise<ApiQuoteResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleQuote: unsupported chainId ${request.chainId}`);\n }\n if (request.amount === 0n) {\n return { pointAmount: 0n, estimatedUsdtOut: 0n, gasEstimate: 0n };\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const pools = request.pools ?? [];\n\n try {\n const best = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: best.bestRoute.amountOut,\n gasEstimate: best.bestRoute.gasEstimate,\n };\n } catch {\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: 0n,\n gasEstimate: 0n,\n quoteError: \"QUOTE_UNAVAILABLE\",\n };\n }\n }\n\n // =========================================================================\n // POST /swap\n // =========================================================================\n\n /**\n * Build a PT → USDT swap UserOp.\n *\n * Quotes the best route, applies slippage, then encodes a 4-step\n * batch: PT.approve → Permit2.approve → UniversalRouter.execute →\n * PT.transfer (fee, omitted when gasFeePt = 0). Returns an unsigned\n * `PartialUserOperation`; caller attaches paymaster data + user\n * signature and submits to the Bundler.\n */\n async handleSwap(request: ApiSwapRequest): Promise<ApiSwapResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleSwap: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handleSwap: amount must be positive\");\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const universalRouter = UNIVERSAL_ROUTER_ADDRESSES[request.chainId];\n if (!universalRouter) {\n throw new Error(`handleSwap: no UniversalRouter for chainId ${request.chainId}`);\n }\n\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const userAddress = getAddress(request.userAddress);\n const pools = request.pools ?? [];\n const slippageBps = request.slippageBps ?? 50;\n const gasFeePt = request.gasFeePt ?? 0n;\n\n if (gasFeePt > 0n && !request.feeRecipient) {\n throw new Error(\"handleSwap: feeRecipient required when gasFeePt > 0\");\n }\n\n let quoteResult: Awaited<ReturnType<typeof findBestQuote>>;\n try {\n quoteResult = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n } catch {\n throw new Error(\"handleSwap: no swap path found for this point token\");\n }\n\n const estimatedUsdtOut = quoteResult.bestRoute.amountOut;\n const minAmountOut = (estimatedUsdtOut * BigInt(10000 - slippageBps)) / 10000n;\n const deadline = BigInt(Math.floor(Date.now() / 1000) + 5 * 60);\n\n const userOp = buildSwapWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n pointTokenAddress,\n outputTokenAddress: usdt,\n universalRouterAddress: universalRouter,\n amountIn: request.amount,\n minAmountOut,\n swapPath: quoteResult.bestRoute.path,\n deadline,\n gasFeePt,\n feeRecipient: request.feeRecipient ?? userAddress,\n });\n\n return { userOp, estimatedUsdtOut, minAmountOut, deadline };\n }\n\n // =========================================================================\n // POST /perp-deposit\n // =========================================================================\n\n /**\n * Build an Orderly perp deposit UserOp.\n *\n * Default path is the **PAFI Orderly Relay** (`viaRelay: true`):\n * USDC.approve(relay) + relay.deposit(req). The Relay holds an ETH\n * reserve and pays Orderly's LayerZero `msg.value` out of it; the\n * user pays a USDC fee (quoted via `Relay.quoteTokenFee`) instead.\n * No native ETH on the user wallet is required, so paymaster\n * sponsorship of the ERC-4337 gas is sufficient end-to-end.\n *\n * Fallback path (`viaRelay: false`): direct `Vault.deposit{value}`.\n * Reserved for chains where no Relay is deployed — the user wallet\n * **must** hold `layerZeroFee` as native ETH.\n *\n * The Relay path automatically falls back to Vault when\n * `getContractAddresses(chainId).orderlyRelay` is the placeholder\n * sentinel (Relay not deployed for that chain).\n */\n async handlePerpDeposit(request: ApiPerpDepositRequest): Promise<ApiPerpDepositResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handlePerpDeposit: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handlePerpDeposit: amount must be positive\");\n }\n\n const vault = ORDERLY_VAULT_ADDRESSES[request.chainId];\n if (!vault) {\n throw new Error(`handlePerpDeposit: no Orderly Vault for chainId ${request.chainId}`);\n }\n\n const brokerHash = BROKER_HASHES[request.brokerId as keyof typeof BROKER_HASHES];\n if (!brokerHash) {\n throw new Error(`handlePerpDeposit: unknown brokerId \"${request.brokerId}\"`);\n }\n const tokenHash = TOKEN_HASHES.USDC;\n const userAddress = getAddress(request.userAddress);\n\n const [usdcAddress, brokerAllowed] = await Promise.all([\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedToken\",\n args: [tokenHash],\n }) as Promise<Address>,\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedBroker\",\n args: [brokerHash],\n }) as Promise<boolean>,\n ]);\n\n if (!brokerAllowed) {\n throw new Error(\n `handlePerpDeposit: broker \"${request.brokerId}\" is not whitelisted on Orderly Vault`,\n );\n }\n\n const accountId = computeAccountId(userAddress, brokerHash);\n const depositData = {\n accountId,\n brokerHash,\n tokenHash,\n tokenAmount: request.amount,\n };\n\n // Always read layerZeroFee for response — even on the Relay path\n // it's useful informational output (lets the FE show \"Relay\n // covers ~X ETH for you\").\n const layerZeroFee = (await this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getDepositFee\",\n args: [userAddress, depositData],\n })) as bigint;\n\n const useRelay = request.viaRelay !== false;\n const relayAddress = getContractAddresses(request.chainId).orderlyRelay;\n const relayDeployed = !isPlaceholderAddress(relayAddress);\n\n if (useRelay && relayDeployed) {\n // Default: 5% slippage cap on the Relay's USDC fee. The Relay\n // converts msg.value (ETH) to USDC at its oracle price; capping\n // protects the user from outsized swings between request and\n // mining.\n const maxRelayFee =\n request.maxRelayFee ?? (request.amount * 500n) / 10_000n;\n\n const relayRequest = {\n token: usdcAddress,\n receiver: userAddress,\n brokerHash,\n totalAmount: request.amount,\n maxFee: maxRelayFee,\n };\n\n const relayTokenFee = (await this.provider.readContract({\n address: relayAddress,\n abi: ORDERLY_RELAY_ABI,\n functionName: \"quoteTokenFee\",\n args: [relayRequest],\n })) as bigint;\n\n if (relayTokenFee > maxRelayFee) {\n throw new Error(\n `handlePerpDeposit: Relay tokenFee ${relayTokenFee} exceeds maxRelayFee ${maxRelayFee}`,\n );\n }\n\n const userOp = buildPerpDepositViaRelay({\n userAddress,\n aaNonce: request.aaNonce,\n relayAddress,\n request: relayRequest,\n pointTokenAddress: request.pointTokenAddress,\n gasFeePt: request.gasFeePt,\n gasFeePtRecipient: request.gasFeePtRecipient,\n });\n\n return {\n userOp,\n path: \"relay\",\n layerZeroFee,\n relayTokenFee,\n accountId,\n brokerHash,\n usdcAddress,\n relayAddress,\n };\n }\n\n // Fallback: direct Vault.deposit{value} — user wallet MUST hold\n // `layerZeroFee` as native ETH (paymaster does not sponsor msg.value).\n const userOp = buildPerpDepositWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n chainId: request.chainId,\n usdcAddress,\n amount: request.amount,\n depositData,\n layerZeroFee,\n });\n\n return {\n userOp,\n path: \"vault\",\n layerZeroFee,\n relayTokenFee: 0n,\n accountId,\n brokerHash,\n usdcAddress,\n relayAddress: vault,\n };\n }\n}\n\n/**\n * `addresses.ts` uses `0x000…<suffix>` sentinels for chains where a\n * given contract is not yet deployed. Detect them by upper-160-bits =\n * 0 so we route to the Vault fallback automatically.\n */\nfunction isPlaceholderAddress(addr: Address): boolean {\n return /^0x0{36}[0-9a-fA-F]{4}$/i.test(addr);\n}\n","// Re-export from @pafi-dev/core — fetchPafiPools lives in core so all\n// SDK packages share one implementation.\nexport { fetchPafiPools, PAFI_SUBGRAPH_URL } from \"@pafi-dev/core\";\n"],"mappings":"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","names":["userOp","import_core"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -62,21 +62,67 @@ interface ApiPerpDepositRequest {
|
|
|
62
62
|
* Known values: woofi_pro, orderly, logx.
|
|
63
63
|
*/
|
|
64
64
|
brokerId: string;
|
|
65
|
+
/**
|
|
66
|
+
* Route via the PAFI Orderly Relay (recommended).
|
|
67
|
+
*
|
|
68
|
+
* - `true` (default): zero-ETH path. Relay covers the LayerZero
|
|
69
|
+
* `msg.value`, user pays a USDC fee. Paymaster sponsorship is
|
|
70
|
+
* sufficient — user does not need native ETH.
|
|
71
|
+
* - `false`: direct Vault call. User must hold the returned
|
|
72
|
+
* `layerZeroFee` as native ETH or the deposit reverts.
|
|
73
|
+
*
|
|
74
|
+
* The Relay path falls back to Vault automatically if
|
|
75
|
+
* `getContractAddresses(chainId).orderlyRelay` is the placeholder
|
|
76
|
+
* sentinel (no Relay deployed for this chain).
|
|
77
|
+
*/
|
|
78
|
+
viaRelay?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Max acceptable USDC fee charged by the Relay (slippage cap on
|
|
81
|
+
* its USD-pricing of `msg.value`). Ignored when `viaRelay: false`.
|
|
82
|
+
* Defaults to 5% of `amount` (`amount * 500 / 10000`).
|
|
83
|
+
*/
|
|
84
|
+
maxRelayFee?: bigint;
|
|
85
|
+
/**
|
|
86
|
+
* Optional PT gas-fee transfer prepended to the batch (sponsored
|
|
87
|
+
* flow — issuer reimburses PAFI in PT for the ERC-4337 gas it
|
|
88
|
+
* sponsored). Set both `gasFeePt` and `gasFeePtRecipient`, or pass
|
|
89
|
+
* neither for the unsponsored fallback path.
|
|
90
|
+
*/
|
|
91
|
+
pointTokenAddress?: Address;
|
|
92
|
+
gasFeePt?: bigint;
|
|
93
|
+
gasFeePtRecipient?: Address;
|
|
65
94
|
}
|
|
66
95
|
interface ApiPerpDepositResponse {
|
|
67
96
|
/** Unsigned UserOp — attach paymaster data + user signature, then submit to Bundler. */
|
|
68
97
|
userOp: PartialUserOperation;
|
|
69
98
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
99
|
+
* Which execution path the handler chose.
|
|
100
|
+
* - `"relay"`: zero-ETH path — paymaster sponsorship alone is sufficient.
|
|
101
|
+
* - `"vault"`: direct Vault call — user wallet must hold
|
|
102
|
+
* `layerZeroFee` as native ETH.
|
|
103
|
+
*/
|
|
104
|
+
path: "relay" | "vault";
|
|
105
|
+
/**
|
|
106
|
+
* Orderly Vault deposit fee, ETH wei (LayerZero msg.value).
|
|
107
|
+
* - When `path === "relay"`: informational only — the Relay covers
|
|
108
|
+
* it, the user does not pay this in ETH.
|
|
109
|
+
* - When `path === "vault"`: required native-ETH balance on the
|
|
110
|
+
* sender wallet.
|
|
72
111
|
*/
|
|
73
112
|
layerZeroFee: bigint;
|
|
113
|
+
/**
|
|
114
|
+
* USDC fee charged by the Relay (`Relay.quoteTokenFee`), capped at
|
|
115
|
+
* `maxRelayFee`. `0n` when `path === "vault"`.
|
|
116
|
+
*/
|
|
117
|
+
relayTokenFee: bigint;
|
|
74
118
|
/** Orderly account ID for this (user, broker) pair. */
|
|
75
119
|
accountId: `0x${string}`;
|
|
76
120
|
/** keccak256(brokerId) — for client-side verification. */
|
|
77
121
|
brokerHash: `0x${string}`;
|
|
78
122
|
/** USDC contract address (resolved from Vault.getAllowedToken). */
|
|
79
123
|
usdcAddress: Address;
|
|
124
|
+
/** Relay address used. Same as `vaultAddress` when `path === "vault"`. */
|
|
125
|
+
relayAddress: Address;
|
|
80
126
|
}
|
|
81
127
|
|
|
82
128
|
interface TradingHandlersConfig {
|
|
@@ -130,10 +176,20 @@ declare class TradingHandlers {
|
|
|
130
176
|
/**
|
|
131
177
|
* Build an Orderly perp deposit UserOp.
|
|
132
178
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
179
|
+
* Default path is the **PAFI Orderly Relay** (`viaRelay: true`):
|
|
180
|
+
* USDC.approve(relay) + relay.deposit(req). The Relay holds an ETH
|
|
181
|
+
* reserve and pays Orderly's LayerZero `msg.value` out of it; the
|
|
182
|
+
* user pays a USDC fee (quoted via `Relay.quoteTokenFee`) instead.
|
|
183
|
+
* No native ETH on the user wallet is required, so paymaster
|
|
184
|
+
* sponsorship of the ERC-4337 gas is sufficient end-to-end.
|
|
185
|
+
*
|
|
186
|
+
* Fallback path (`viaRelay: false`): direct `Vault.deposit{value}`.
|
|
187
|
+
* Reserved for chains where no Relay is deployed — the user wallet
|
|
188
|
+
* **must** hold `layerZeroFee` as native ETH.
|
|
189
|
+
*
|
|
190
|
+
* The Relay path automatically falls back to Vault when
|
|
191
|
+
* `getContractAddresses(chainId).orderlyRelay` is the placeholder
|
|
192
|
+
* sentinel (Relay not deployed for that chain).
|
|
137
193
|
*/
|
|
138
194
|
handlePerpDeposit(request: ApiPerpDepositRequest): Promise<ApiPerpDepositResponse>;
|
|
139
195
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -62,21 +62,67 @@ interface ApiPerpDepositRequest {
|
|
|
62
62
|
* Known values: woofi_pro, orderly, logx.
|
|
63
63
|
*/
|
|
64
64
|
brokerId: string;
|
|
65
|
+
/**
|
|
66
|
+
* Route via the PAFI Orderly Relay (recommended).
|
|
67
|
+
*
|
|
68
|
+
* - `true` (default): zero-ETH path. Relay covers the LayerZero
|
|
69
|
+
* `msg.value`, user pays a USDC fee. Paymaster sponsorship is
|
|
70
|
+
* sufficient — user does not need native ETH.
|
|
71
|
+
* - `false`: direct Vault call. User must hold the returned
|
|
72
|
+
* `layerZeroFee` as native ETH or the deposit reverts.
|
|
73
|
+
*
|
|
74
|
+
* The Relay path falls back to Vault automatically if
|
|
75
|
+
* `getContractAddresses(chainId).orderlyRelay` is the placeholder
|
|
76
|
+
* sentinel (no Relay deployed for this chain).
|
|
77
|
+
*/
|
|
78
|
+
viaRelay?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Max acceptable USDC fee charged by the Relay (slippage cap on
|
|
81
|
+
* its USD-pricing of `msg.value`). Ignored when `viaRelay: false`.
|
|
82
|
+
* Defaults to 5% of `amount` (`amount * 500 / 10000`).
|
|
83
|
+
*/
|
|
84
|
+
maxRelayFee?: bigint;
|
|
85
|
+
/**
|
|
86
|
+
* Optional PT gas-fee transfer prepended to the batch (sponsored
|
|
87
|
+
* flow — issuer reimburses PAFI in PT for the ERC-4337 gas it
|
|
88
|
+
* sponsored). Set both `gasFeePt` and `gasFeePtRecipient`, or pass
|
|
89
|
+
* neither for the unsponsored fallback path.
|
|
90
|
+
*/
|
|
91
|
+
pointTokenAddress?: Address;
|
|
92
|
+
gasFeePt?: bigint;
|
|
93
|
+
gasFeePtRecipient?: Address;
|
|
65
94
|
}
|
|
66
95
|
interface ApiPerpDepositResponse {
|
|
67
96
|
/** Unsigned UserOp — attach paymaster data + user signature, then submit to Bundler. */
|
|
68
97
|
userOp: PartialUserOperation;
|
|
69
98
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
99
|
+
* Which execution path the handler chose.
|
|
100
|
+
* - `"relay"`: zero-ETH path — paymaster sponsorship alone is sufficient.
|
|
101
|
+
* - `"vault"`: direct Vault call — user wallet must hold
|
|
102
|
+
* `layerZeroFee` as native ETH.
|
|
103
|
+
*/
|
|
104
|
+
path: "relay" | "vault";
|
|
105
|
+
/**
|
|
106
|
+
* Orderly Vault deposit fee, ETH wei (LayerZero msg.value).
|
|
107
|
+
* - When `path === "relay"`: informational only — the Relay covers
|
|
108
|
+
* it, the user does not pay this in ETH.
|
|
109
|
+
* - When `path === "vault"`: required native-ETH balance on the
|
|
110
|
+
* sender wallet.
|
|
72
111
|
*/
|
|
73
112
|
layerZeroFee: bigint;
|
|
113
|
+
/**
|
|
114
|
+
* USDC fee charged by the Relay (`Relay.quoteTokenFee`), capped at
|
|
115
|
+
* `maxRelayFee`. `0n` when `path === "vault"`.
|
|
116
|
+
*/
|
|
117
|
+
relayTokenFee: bigint;
|
|
74
118
|
/** Orderly account ID for this (user, broker) pair. */
|
|
75
119
|
accountId: `0x${string}`;
|
|
76
120
|
/** keccak256(brokerId) — for client-side verification. */
|
|
77
121
|
brokerHash: `0x${string}`;
|
|
78
122
|
/** USDC contract address (resolved from Vault.getAllowedToken). */
|
|
79
123
|
usdcAddress: Address;
|
|
124
|
+
/** Relay address used. Same as `vaultAddress` when `path === "vault"`. */
|
|
125
|
+
relayAddress: Address;
|
|
80
126
|
}
|
|
81
127
|
|
|
82
128
|
interface TradingHandlersConfig {
|
|
@@ -130,10 +176,20 @@ declare class TradingHandlers {
|
|
|
130
176
|
/**
|
|
131
177
|
* Build an Orderly perp deposit UserOp.
|
|
132
178
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
179
|
+
* Default path is the **PAFI Orderly Relay** (`viaRelay: true`):
|
|
180
|
+
* USDC.approve(relay) + relay.deposit(req). The Relay holds an ETH
|
|
181
|
+
* reserve and pays Orderly's LayerZero `msg.value` out of it; the
|
|
182
|
+
* user pays a USDC fee (quoted via `Relay.quoteTokenFee`) instead.
|
|
183
|
+
* No native ETH on the user wallet is required, so paymaster
|
|
184
|
+
* sponsorship of the ERC-4337 gas is sufficient end-to-end.
|
|
185
|
+
*
|
|
186
|
+
* Fallback path (`viaRelay: false`): direct `Vault.deposit{value}`.
|
|
187
|
+
* Reserved for chains where no Relay is deployed — the user wallet
|
|
188
|
+
* **must** hold `layerZeroFee` as native ETH.
|
|
189
|
+
*
|
|
190
|
+
* The Relay path automatically falls back to Vault when
|
|
191
|
+
* `getContractAddresses(chainId).orderlyRelay` is the placeholder
|
|
192
|
+
* sentinel (Relay not deployed for that chain).
|
|
137
193
|
*/
|
|
138
194
|
handlePerpDeposit(request: ApiPerpDepositRequest): Promise<ApiPerpDepositResponse>;
|
|
139
195
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
findBestQuote,
|
|
5
5
|
buildSwapWithGasDeduction,
|
|
6
6
|
buildPerpDepositWithGasDeduction,
|
|
7
|
+
buildPerpDepositViaRelay,
|
|
8
|
+
ORDERLY_RELAY_ABI,
|
|
7
9
|
getContractAddresses,
|
|
8
10
|
UNIVERSAL_ROUTER_ADDRESSES,
|
|
9
11
|
ORDERLY_VAULT_ABI,
|
|
@@ -132,10 +134,20 @@ var TradingHandlers = class {
|
|
|
132
134
|
/**
|
|
133
135
|
* Build an Orderly perp deposit UserOp.
|
|
134
136
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
137
|
+
* Default path is the **PAFI Orderly Relay** (`viaRelay: true`):
|
|
138
|
+
* USDC.approve(relay) + relay.deposit(req). The Relay holds an ETH
|
|
139
|
+
* reserve and pays Orderly's LayerZero `msg.value` out of it; the
|
|
140
|
+
* user pays a USDC fee (quoted via `Relay.quoteTokenFee`) instead.
|
|
141
|
+
* No native ETH on the user wallet is required, so paymaster
|
|
142
|
+
* sponsorship of the ERC-4337 gas is sufficient end-to-end.
|
|
143
|
+
*
|
|
144
|
+
* Fallback path (`viaRelay: false`): direct `Vault.deposit{value}`.
|
|
145
|
+
* Reserved for chains where no Relay is deployed — the user wallet
|
|
146
|
+
* **must** hold `layerZeroFee` as native ETH.
|
|
147
|
+
*
|
|
148
|
+
* The Relay path automatically falls back to Vault when
|
|
149
|
+
* `getContractAddresses(chainId).orderlyRelay` is the placeholder
|
|
150
|
+
* sentinel (Relay not deployed for that chain).
|
|
139
151
|
*/
|
|
140
152
|
async handlePerpDeposit(request) {
|
|
141
153
|
if (request.chainId !== this.chainId) {
|
|
@@ -186,6 +198,49 @@ var TradingHandlers = class {
|
|
|
186
198
|
functionName: "getDepositFee",
|
|
187
199
|
args: [userAddress, depositData]
|
|
188
200
|
});
|
|
201
|
+
const useRelay = request.viaRelay !== false;
|
|
202
|
+
const relayAddress = getContractAddresses(request.chainId).orderlyRelay;
|
|
203
|
+
const relayDeployed = !isPlaceholderAddress(relayAddress);
|
|
204
|
+
if (useRelay && relayDeployed) {
|
|
205
|
+
const maxRelayFee = request.maxRelayFee ?? request.amount * 500n / 10000n;
|
|
206
|
+
const relayRequest = {
|
|
207
|
+
token: usdcAddress,
|
|
208
|
+
receiver: userAddress,
|
|
209
|
+
brokerHash,
|
|
210
|
+
totalAmount: request.amount,
|
|
211
|
+
maxFee: maxRelayFee
|
|
212
|
+
};
|
|
213
|
+
const relayTokenFee = await this.provider.readContract({
|
|
214
|
+
address: relayAddress,
|
|
215
|
+
abi: ORDERLY_RELAY_ABI,
|
|
216
|
+
functionName: "quoteTokenFee",
|
|
217
|
+
args: [relayRequest]
|
|
218
|
+
});
|
|
219
|
+
if (relayTokenFee > maxRelayFee) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`handlePerpDeposit: Relay tokenFee ${relayTokenFee} exceeds maxRelayFee ${maxRelayFee}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const userOp2 = buildPerpDepositViaRelay({
|
|
225
|
+
userAddress,
|
|
226
|
+
aaNonce: request.aaNonce,
|
|
227
|
+
relayAddress,
|
|
228
|
+
request: relayRequest,
|
|
229
|
+
pointTokenAddress: request.pointTokenAddress,
|
|
230
|
+
gasFeePt: request.gasFeePt,
|
|
231
|
+
gasFeePtRecipient: request.gasFeePtRecipient
|
|
232
|
+
});
|
|
233
|
+
return {
|
|
234
|
+
userOp: userOp2,
|
|
235
|
+
path: "relay",
|
|
236
|
+
layerZeroFee,
|
|
237
|
+
relayTokenFee,
|
|
238
|
+
accountId,
|
|
239
|
+
brokerHash,
|
|
240
|
+
usdcAddress,
|
|
241
|
+
relayAddress
|
|
242
|
+
};
|
|
243
|
+
}
|
|
189
244
|
const userOp = buildPerpDepositWithGasDeduction({
|
|
190
245
|
userAddress,
|
|
191
246
|
aaNonce: request.aaNonce,
|
|
@@ -195,9 +250,21 @@ var TradingHandlers = class {
|
|
|
195
250
|
depositData,
|
|
196
251
|
layerZeroFee
|
|
197
252
|
});
|
|
198
|
-
return {
|
|
253
|
+
return {
|
|
254
|
+
userOp,
|
|
255
|
+
path: "vault",
|
|
256
|
+
layerZeroFee,
|
|
257
|
+
relayTokenFee: 0n,
|
|
258
|
+
accountId,
|
|
259
|
+
brokerHash,
|
|
260
|
+
usdcAddress,
|
|
261
|
+
relayAddress: vault
|
|
262
|
+
};
|
|
199
263
|
}
|
|
200
264
|
};
|
|
265
|
+
function isPlaceholderAddress(addr) {
|
|
266
|
+
return /^0x0{36}[0-9a-fA-F]{4}$/i.test(addr);
|
|
267
|
+
}
|
|
201
268
|
|
|
202
269
|
// src/pools.ts
|
|
203
270
|
import { fetchPafiPools, PAFI_SUBGRAPH_URL } from "@pafi-dev/core";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api/handlers.ts","../src/pools.ts"],"sourcesContent":["import { getAddress } from \"viem\";\nimport type { Address, PublicClient } from \"viem\";\nimport {\n findBestQuote,\n buildSwapWithGasDeduction,\n buildPerpDepositWithGasDeduction,\n getContractAddresses,\n UNIVERSAL_ROUTER_ADDRESSES,\n ORDERLY_VAULT_ABI,\n ORDERLY_VAULT_ADDRESSES,\n BROKER_HASHES,\n TOKEN_HASHES,\n computeAccountId,\n} from \"@pafi-dev/core\";\nimport type {\n ApiQuoteRequest,\n ApiQuoteResponse,\n ApiSwapRequest,\n ApiSwapResponse,\n ApiPerpDepositRequest,\n ApiPerpDepositResponse,\n} from \"./types\";\n\nexport interface TradingHandlersConfig {\n provider: PublicClient;\n chainId: number;\n}\n\n/**\n * Framework-agnostic handlers for on-chain trading actions.\n *\n * All handlers are stateless — they need only a PublicClient for RPC\n * calls. No ledger, no signer, no DB. Issuers wrap these in their own\n * HTTP controllers (Express / NestJS / Hono / etc.) the same way they\n * wrap `IssuerApiHandlers` from `@pafi-dev/issuer`.\n *\n * Example (NestJS):\n *\n * const trading = new TradingHandlers({ provider, chainId });\n *\n * // GET /quote\n * const quote = await trading.handleQuote({ chainId, pointTokenAddress, amount, pools });\n *\n * // POST /swap\n * const swap = await trading.handleSwap({ chainId, userAddress, pointTokenAddress, amount, aaNonce });\n *\n * // POST /perp-deposit\n * const deposit = await trading.handlePerpDeposit({ chainId, userAddress, amount, aaNonce, brokerId });\n */\nexport class TradingHandlers {\n private readonly provider: PublicClient;\n private readonly chainId: number;\n\n constructor(config: TradingHandlersConfig) {\n this.provider = config.provider;\n this.chainId = config.chainId;\n }\n\n // =========================================================================\n // GET /quote\n // =========================================================================\n\n /**\n * Quote exact-input PT → USDT via Uniswap V4 on-chain Quoter.\n *\n * Uses multicall to batch all candidate routes into a single RPC call.\n * Returns `quoteError: \"QUOTE_UNAVAILABLE\"` when no pool/path exists\n * rather than throwing, so callers can show a soft \"unavailable\" UI\n * state without 500-ing.\n */\n async handleQuote(request: ApiQuoteRequest): Promise<ApiQuoteResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleQuote: unsupported chainId ${request.chainId}`);\n }\n if (request.amount === 0n) {\n return { pointAmount: 0n, estimatedUsdtOut: 0n, gasEstimate: 0n };\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const pools = request.pools ?? [];\n\n try {\n const best = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: best.bestRoute.amountOut,\n gasEstimate: best.bestRoute.gasEstimate,\n };\n } catch {\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: 0n,\n gasEstimate: 0n,\n quoteError: \"QUOTE_UNAVAILABLE\",\n };\n }\n }\n\n // =========================================================================\n // POST /swap\n // =========================================================================\n\n /**\n * Build a PT → USDT swap UserOp.\n *\n * Quotes the best route, applies slippage, then encodes a 4-step\n * batch: PT.approve → Permit2.approve → UniversalRouter.execute →\n * PT.transfer (fee, omitted when gasFeePt = 0). Returns an unsigned\n * `PartialUserOperation`; caller attaches paymaster data + user\n * signature and submits to the Bundler.\n */\n async handleSwap(request: ApiSwapRequest): Promise<ApiSwapResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleSwap: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handleSwap: amount must be positive\");\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const universalRouter = UNIVERSAL_ROUTER_ADDRESSES[request.chainId];\n if (!universalRouter) {\n throw new Error(`handleSwap: no UniversalRouter for chainId ${request.chainId}`);\n }\n\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const userAddress = getAddress(request.userAddress);\n const pools = request.pools ?? [];\n const slippageBps = request.slippageBps ?? 50;\n const gasFeePt = request.gasFeePt ?? 0n;\n\n if (gasFeePt > 0n && !request.feeRecipient) {\n throw new Error(\"handleSwap: feeRecipient required when gasFeePt > 0\");\n }\n\n let quoteResult: Awaited<ReturnType<typeof findBestQuote>>;\n try {\n quoteResult = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n } catch {\n throw new Error(\"handleSwap: no swap path found for this point token\");\n }\n\n const estimatedUsdtOut = quoteResult.bestRoute.amountOut;\n const minAmountOut = (estimatedUsdtOut * BigInt(10000 - slippageBps)) / 10000n;\n const deadline = BigInt(Math.floor(Date.now() / 1000) + 5 * 60);\n\n const userOp = buildSwapWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n pointTokenAddress,\n outputTokenAddress: usdt,\n universalRouterAddress: universalRouter,\n amountIn: request.amount,\n minAmountOut,\n swapPath: quoteResult.bestRoute.path,\n deadline,\n gasFeePt,\n feeRecipient: request.feeRecipient ?? userAddress,\n });\n\n return { userOp, estimatedUsdtOut, minAmountOut, deadline };\n }\n\n // =========================================================================\n // POST /perp-deposit\n // =========================================================================\n\n /**\n * Build an Orderly perp deposit UserOp.\n *\n * Resolves USDC address and LayerZero fee from on-chain Vault reads,\n * then encodes a 2-step batch: USDC.approve → Vault.deposit{value}.\n * The `layerZeroFee` in the response is the ETH the user must hold\n * natively — paymaster sponsors ERC-4337 gas only, not msg.value.\n */\n async handlePerpDeposit(request: ApiPerpDepositRequest): Promise<ApiPerpDepositResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handlePerpDeposit: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handlePerpDeposit: amount must be positive\");\n }\n\n const vault = ORDERLY_VAULT_ADDRESSES[request.chainId];\n if (!vault) {\n throw new Error(`handlePerpDeposit: no Orderly Vault for chainId ${request.chainId}`);\n }\n\n const brokerHash = BROKER_HASHES[request.brokerId as keyof typeof BROKER_HASHES];\n if (!brokerHash) {\n throw new Error(`handlePerpDeposit: unknown brokerId \"${request.brokerId}\"`);\n }\n const tokenHash = TOKEN_HASHES.USDC;\n const userAddress = getAddress(request.userAddress);\n\n const [usdcAddress, brokerAllowed] = await Promise.all([\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedToken\",\n args: [tokenHash],\n }) as Promise<Address>,\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedBroker\",\n args: [brokerHash],\n }) as Promise<boolean>,\n ]);\n\n if (!brokerAllowed) {\n throw new Error(\n `handlePerpDeposit: broker \"${request.brokerId}\" is not whitelisted on Orderly Vault`,\n );\n }\n\n const accountId = computeAccountId(userAddress, brokerHash);\n const depositData = {\n accountId,\n brokerHash,\n tokenHash,\n tokenAmount: request.amount,\n };\n\n const layerZeroFee = await this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getDepositFee\",\n args: [userAddress, depositData],\n }) as bigint;\n\n const userOp = buildPerpDepositWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n chainId: request.chainId,\n usdcAddress,\n amount: request.amount,\n depositData,\n layerZeroFee,\n });\n\n return { userOp, layerZeroFee, accountId, brokerHash, usdcAddress };\n }\n}\n","// Re-export from @pafi-dev/core — fetchPafiPools lives in core so all\n// SDK packages share one implementation.\nexport { fetchPafiPools, PAFI_SUBGRAPH_URL } from \"@pafi-dev/core\";\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/api/handlers.ts","../src/pools.ts"],"sourcesContent":["import { getAddress } from \"viem\";\nimport type { Address, PublicClient } from \"viem\";\nimport {\n findBestQuote,\n buildSwapWithGasDeduction,\n buildPerpDepositWithGasDeduction,\n buildPerpDepositViaRelay,\n ORDERLY_RELAY_ABI,\n getContractAddresses,\n UNIVERSAL_ROUTER_ADDRESSES,\n ORDERLY_VAULT_ABI,\n ORDERLY_VAULT_ADDRESSES,\n BROKER_HASHES,\n TOKEN_HASHES,\n computeAccountId,\n} from \"@pafi-dev/core\";\nimport type {\n ApiQuoteRequest,\n ApiQuoteResponse,\n ApiSwapRequest,\n ApiSwapResponse,\n ApiPerpDepositRequest,\n ApiPerpDepositResponse,\n} from \"./types\";\n\nexport interface TradingHandlersConfig {\n provider: PublicClient;\n chainId: number;\n}\n\n/**\n * Framework-agnostic handlers for on-chain trading actions.\n *\n * All handlers are stateless — they need only a PublicClient for RPC\n * calls. No ledger, no signer, no DB. Issuers wrap these in their own\n * HTTP controllers (Express / NestJS / Hono / etc.) the same way they\n * wrap `IssuerApiHandlers` from `@pafi-dev/issuer`.\n *\n * Example (NestJS):\n *\n * const trading = new TradingHandlers({ provider, chainId });\n *\n * // GET /quote\n * const quote = await trading.handleQuote({ chainId, pointTokenAddress, amount, pools });\n *\n * // POST /swap\n * const swap = await trading.handleSwap({ chainId, userAddress, pointTokenAddress, amount, aaNonce });\n *\n * // POST /perp-deposit\n * const deposit = await trading.handlePerpDeposit({ chainId, userAddress, amount, aaNonce, brokerId });\n */\nexport class TradingHandlers {\n private readonly provider: PublicClient;\n private readonly chainId: number;\n\n constructor(config: TradingHandlersConfig) {\n this.provider = config.provider;\n this.chainId = config.chainId;\n }\n\n // =========================================================================\n // GET /quote\n // =========================================================================\n\n /**\n * Quote exact-input PT → USDT via Uniswap V4 on-chain Quoter.\n *\n * Uses multicall to batch all candidate routes into a single RPC call.\n * Returns `quoteError: \"QUOTE_UNAVAILABLE\"` when no pool/path exists\n * rather than throwing, so callers can show a soft \"unavailable\" UI\n * state without 500-ing.\n */\n async handleQuote(request: ApiQuoteRequest): Promise<ApiQuoteResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleQuote: unsupported chainId ${request.chainId}`);\n }\n if (request.amount === 0n) {\n return { pointAmount: 0n, estimatedUsdtOut: 0n, gasEstimate: 0n };\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const pools = request.pools ?? [];\n\n try {\n const best = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: best.bestRoute.amountOut,\n gasEstimate: best.bestRoute.gasEstimate,\n };\n } catch {\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: 0n,\n gasEstimate: 0n,\n quoteError: \"QUOTE_UNAVAILABLE\",\n };\n }\n }\n\n // =========================================================================\n // POST /swap\n // =========================================================================\n\n /**\n * Build a PT → USDT swap UserOp.\n *\n * Quotes the best route, applies slippage, then encodes a 4-step\n * batch: PT.approve → Permit2.approve → UniversalRouter.execute →\n * PT.transfer (fee, omitted when gasFeePt = 0). Returns an unsigned\n * `PartialUserOperation`; caller attaches paymaster data + user\n * signature and submits to the Bundler.\n */\n async handleSwap(request: ApiSwapRequest): Promise<ApiSwapResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleSwap: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handleSwap: amount must be positive\");\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const universalRouter = UNIVERSAL_ROUTER_ADDRESSES[request.chainId];\n if (!universalRouter) {\n throw new Error(`handleSwap: no UniversalRouter for chainId ${request.chainId}`);\n }\n\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const userAddress = getAddress(request.userAddress);\n const pools = request.pools ?? [];\n const slippageBps = request.slippageBps ?? 50;\n const gasFeePt = request.gasFeePt ?? 0n;\n\n if (gasFeePt > 0n && !request.feeRecipient) {\n throw new Error(\"handleSwap: feeRecipient required when gasFeePt > 0\");\n }\n\n let quoteResult: Awaited<ReturnType<typeof findBestQuote>>;\n try {\n quoteResult = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n } catch {\n throw new Error(\"handleSwap: no swap path found for this point token\");\n }\n\n const estimatedUsdtOut = quoteResult.bestRoute.amountOut;\n const minAmountOut = (estimatedUsdtOut * BigInt(10000 - slippageBps)) / 10000n;\n const deadline = BigInt(Math.floor(Date.now() / 1000) + 5 * 60);\n\n const userOp = buildSwapWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n pointTokenAddress,\n outputTokenAddress: usdt,\n universalRouterAddress: universalRouter,\n amountIn: request.amount,\n minAmountOut,\n swapPath: quoteResult.bestRoute.path,\n deadline,\n gasFeePt,\n feeRecipient: request.feeRecipient ?? userAddress,\n });\n\n return { userOp, estimatedUsdtOut, minAmountOut, deadline };\n }\n\n // =========================================================================\n // POST /perp-deposit\n // =========================================================================\n\n /**\n * Build an Orderly perp deposit UserOp.\n *\n * Default path is the **PAFI Orderly Relay** (`viaRelay: true`):\n * USDC.approve(relay) + relay.deposit(req). The Relay holds an ETH\n * reserve and pays Orderly's LayerZero `msg.value` out of it; the\n * user pays a USDC fee (quoted via `Relay.quoteTokenFee`) instead.\n * No native ETH on the user wallet is required, so paymaster\n * sponsorship of the ERC-4337 gas is sufficient end-to-end.\n *\n * Fallback path (`viaRelay: false`): direct `Vault.deposit{value}`.\n * Reserved for chains where no Relay is deployed — the user wallet\n * **must** hold `layerZeroFee` as native ETH.\n *\n * The Relay path automatically falls back to Vault when\n * `getContractAddresses(chainId).orderlyRelay` is the placeholder\n * sentinel (Relay not deployed for that chain).\n */\n async handlePerpDeposit(request: ApiPerpDepositRequest): Promise<ApiPerpDepositResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handlePerpDeposit: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handlePerpDeposit: amount must be positive\");\n }\n\n const vault = ORDERLY_VAULT_ADDRESSES[request.chainId];\n if (!vault) {\n throw new Error(`handlePerpDeposit: no Orderly Vault for chainId ${request.chainId}`);\n }\n\n const brokerHash = BROKER_HASHES[request.brokerId as keyof typeof BROKER_HASHES];\n if (!brokerHash) {\n throw new Error(`handlePerpDeposit: unknown brokerId \"${request.brokerId}\"`);\n }\n const tokenHash = TOKEN_HASHES.USDC;\n const userAddress = getAddress(request.userAddress);\n\n const [usdcAddress, brokerAllowed] = await Promise.all([\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedToken\",\n args: [tokenHash],\n }) as Promise<Address>,\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedBroker\",\n args: [brokerHash],\n }) as Promise<boolean>,\n ]);\n\n if (!brokerAllowed) {\n throw new Error(\n `handlePerpDeposit: broker \"${request.brokerId}\" is not whitelisted on Orderly Vault`,\n );\n }\n\n const accountId = computeAccountId(userAddress, brokerHash);\n const depositData = {\n accountId,\n brokerHash,\n tokenHash,\n tokenAmount: request.amount,\n };\n\n // Always read layerZeroFee for response — even on the Relay path\n // it's useful informational output (lets the FE show \"Relay\n // covers ~X ETH for you\").\n const layerZeroFee = (await this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getDepositFee\",\n args: [userAddress, depositData],\n })) as bigint;\n\n const useRelay = request.viaRelay !== false;\n const relayAddress = getContractAddresses(request.chainId).orderlyRelay;\n const relayDeployed = !isPlaceholderAddress(relayAddress);\n\n if (useRelay && relayDeployed) {\n // Default: 5% slippage cap on the Relay's USDC fee. The Relay\n // converts msg.value (ETH) to USDC at its oracle price; capping\n // protects the user from outsized swings between request and\n // mining.\n const maxRelayFee =\n request.maxRelayFee ?? (request.amount * 500n) / 10_000n;\n\n const relayRequest = {\n token: usdcAddress,\n receiver: userAddress,\n brokerHash,\n totalAmount: request.amount,\n maxFee: maxRelayFee,\n };\n\n const relayTokenFee = (await this.provider.readContract({\n address: relayAddress,\n abi: ORDERLY_RELAY_ABI,\n functionName: \"quoteTokenFee\",\n args: [relayRequest],\n })) as bigint;\n\n if (relayTokenFee > maxRelayFee) {\n throw new Error(\n `handlePerpDeposit: Relay tokenFee ${relayTokenFee} exceeds maxRelayFee ${maxRelayFee}`,\n );\n }\n\n const userOp = buildPerpDepositViaRelay({\n userAddress,\n aaNonce: request.aaNonce,\n relayAddress,\n request: relayRequest,\n pointTokenAddress: request.pointTokenAddress,\n gasFeePt: request.gasFeePt,\n gasFeePtRecipient: request.gasFeePtRecipient,\n });\n\n return {\n userOp,\n path: \"relay\",\n layerZeroFee,\n relayTokenFee,\n accountId,\n brokerHash,\n usdcAddress,\n relayAddress,\n };\n }\n\n // Fallback: direct Vault.deposit{value} — user wallet MUST hold\n // `layerZeroFee` as native ETH (paymaster does not sponsor msg.value).\n const userOp = buildPerpDepositWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n chainId: request.chainId,\n usdcAddress,\n amount: request.amount,\n depositData,\n layerZeroFee,\n });\n\n return {\n userOp,\n path: \"vault\",\n layerZeroFee,\n relayTokenFee: 0n,\n accountId,\n brokerHash,\n usdcAddress,\n relayAddress: vault,\n };\n }\n}\n\n/**\n * `addresses.ts` uses `0x000…<suffix>` sentinels for chains where a\n * given contract is not yet deployed. Detect them by upper-160-bits =\n * 0 so we route to the Vault fallback automatically.\n */\nfunction isPlaceholderAddress(addr: Address): boolean {\n return /^0x0{36}[0-9a-fA-F]{4}$/i.test(addr);\n}\n","// Re-export from @pafi-dev/core — fetchPafiPools lives in core so all\n// SDK packages share one implementation.\nexport { fetchPafiPools, PAFI_SUBGRAPH_URL } from \"@pafi-dev/core\";\n"],"mappings":"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","names":["userOp"]}
|