@pafi-dev/trading 0.1.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.
- package/README.md +239 -0
- package/dist/index.cjs +220 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +140 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.js +204 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# @pafi-dev/trading
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@pafi-dev/trading)
|
|
4
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
5
|
+
|
|
6
|
+
Stateless on-chain trading handlers for PAFI — swap PT → USDT and deposit USDC into Orderly perp.
|
|
7
|
+
|
|
8
|
+
All handlers are purely on-chain (Uniswap V4 Quoter + UniversalRouter + Orderly Vault). No
|
|
9
|
+
ledger, no signer, no database. Safe to use in any backend (NestJS, Express, Hono) or
|
|
10
|
+
client-side (React, React Native).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Node.js >= 18 (server) or any modern bundler (client)
|
|
17
|
+
- TypeScript >= 5.0
|
|
18
|
+
- `viem` ^2.0.0 and `@pafi-dev/core` ^0.5.0 (peer dependencies)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @pafi-dev/trading @pafi-dev/core viem
|
|
26
|
+
# or
|
|
27
|
+
pnpm add @pafi-dev/trading @pafi-dev/core viem
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { createPublicClient, http } from "viem";
|
|
36
|
+
import { base } from "viem/chains";
|
|
37
|
+
import { TradingHandlers } from "@pafi-dev/trading";
|
|
38
|
+
|
|
39
|
+
const provider = createPublicClient({ chain: base, transport: http(RPC_URL) });
|
|
40
|
+
const trading = new TradingHandlers({ provider, chainId: 8453 });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Handlers
|
|
46
|
+
|
|
47
|
+
### `handleQuote` — GET /quote
|
|
48
|
+
|
|
49
|
+
Quote exact-input PT → USDT via Uniswap V4 on-chain Quoter. Uses multicall to batch all
|
|
50
|
+
candidate routes into a single RPC call.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const quote = await trading.handleQuote({
|
|
54
|
+
chainId: 8453,
|
|
55
|
+
pointTokenAddress: "0x7d25E7156E51F865D522fd3ef257a6B5DD41b97e",
|
|
56
|
+
amount: 1000n * 10n ** 18n, // 1000 PT
|
|
57
|
+
pools: poolsFromSubgraph, // optional — combined with COMMON_POOLS
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// quote.estimatedUsdtOut — raw USDT (6 decimals)
|
|
61
|
+
// quote.gasEstimate — estimated gas units
|
|
62
|
+
// quote.quoteError — "QUOTE_UNAVAILABLE" | "AMOUNT_TOO_SMALL_FOR_GAS" | undefined
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Returns `quoteError` instead of throwing when no pool/path is found, so the caller can
|
|
66
|
+
show a soft "unavailable" UI state without 500-ing.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### `handleSwap` — POST /swap
|
|
71
|
+
|
|
72
|
+
Quote the best route, apply slippage, and build an unsigned `PartialUserOperation` that
|
|
73
|
+
batches: `PT.approve` → `Permit2.approve` → `UniversalRouter.execute` (→ `PT.transfer` fee).
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const swap = await trading.handleSwap({
|
|
77
|
+
chainId: 8453,
|
|
78
|
+
userAddress: "0xUserEOA",
|
|
79
|
+
pointTokenAddress: "0x7d25E7156E51F865D522fd3ef257a6B5DD41b97e",
|
|
80
|
+
amount: 1000n * 10n ** 18n,
|
|
81
|
+
aaNonce: 0n, // from EntryPoint.getNonce(user, 0)
|
|
82
|
+
slippageBps: 50, // 0.5% — default
|
|
83
|
+
pools: poolsFromSubgraph,
|
|
84
|
+
// optional gas fee deduction in PT:
|
|
85
|
+
// gasFeePt: 5n * 10n ** 18n,
|
|
86
|
+
// feeRecipient: "0xOperatorAddress",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// swap.userOp — unsigned PartialUserOperation
|
|
90
|
+
// swap.estimatedUsdtOut — raw USDT before slippage
|
|
91
|
+
// swap.minAmountOut — encoded in UserOp (revert if less)
|
|
92
|
+
// swap.deadline — unix seconds (now + 5 min)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
After receiving the response, the frontend:
|
|
96
|
+
1. Requests paymaster sponsorship from `POST /pimlico` (sponsor-relayer, Privy auth)
|
|
97
|
+
2. Signs the UserOp hash via Privy
|
|
98
|
+
3. Submits to the Bundler
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### `handlePerpDeposit` — POST /perp-deposit
|
|
103
|
+
|
|
104
|
+
Quote the LayerZero fee and build an unsigned UserOp that batches:
|
|
105
|
+
`USDC.approve(vault)` → `vault.deposit{value: layerZeroFee}(data)`.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const deposit = await trading.handlePerpDeposit({
|
|
109
|
+
chainId: 8453,
|
|
110
|
+
userAddress: "0xUserEOA",
|
|
111
|
+
amount: 100_000_000n, // 100 USDC (6 decimals)
|
|
112
|
+
aaNonce: 0n,
|
|
113
|
+
brokerId: "woofi_pro", // "woofi_pro" | "orderly" | "logx"
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// deposit.userOp — unsigned PartialUserOperation
|
|
117
|
+
// deposit.layerZeroFee — ETH wei — user wallet must hold this as native ETH
|
|
118
|
+
// deposit.accountId — Orderly account ID for (user, broker) pair
|
|
119
|
+
// deposit.brokerHash — keccak256(brokerId)
|
|
120
|
+
// deposit.usdcAddress — USDC resolved from Vault.getAllowedToken()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> **Native ETH constraint:** The paymaster sponsors ERC-4337 gas, but `msg.value` (the
|
|
124
|
+
> LayerZero fee) must come from the user's own native ETH balance. Surface `layerZeroFee`
|
|
125
|
+
> in the UI and warn the user if their ETH is insufficient before submitting.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Wiring into a NestJS controller
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { Controller, Get, Post, Body, Query } from "@nestjs/common";
|
|
133
|
+
import { TradingHandlers } from "@pafi-dev/trading";
|
|
134
|
+
|
|
135
|
+
@Controller()
|
|
136
|
+
export class TradingController {
|
|
137
|
+
constructor(private readonly trading: TradingHandlers) {}
|
|
138
|
+
|
|
139
|
+
@Get("quote")
|
|
140
|
+
async quote(@Query() q: QuoteQueryDto) {
|
|
141
|
+
return this.trading.handleQuote({
|
|
142
|
+
chainId: q.chainId,
|
|
143
|
+
pointTokenAddress: q.pointToken,
|
|
144
|
+
amount: BigInt(q.amount),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Post("swap")
|
|
149
|
+
async swap(@Body() body: SwapBodyDto) {
|
|
150
|
+
return this.trading.handleSwap({
|
|
151
|
+
chainId: body.chainId,
|
|
152
|
+
userAddress: body.userAddress,
|
|
153
|
+
pointTokenAddress: body.pointTokenAddress,
|
|
154
|
+
amount: BigInt(body.amount),
|
|
155
|
+
aaNonce: BigInt(body.aaNonce),
|
|
156
|
+
slippageBps: body.slippageBps,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@Post("perp-deposit")
|
|
161
|
+
async perpDeposit(@Body() body: PerpDepositBodyDto) {
|
|
162
|
+
return this.trading.handlePerpDeposit({
|
|
163
|
+
chainId: body.chainId,
|
|
164
|
+
userAddress: body.userAddress,
|
|
165
|
+
amount: BigInt(body.amount),
|
|
166
|
+
aaNonce: BigInt(body.aaNonce),
|
|
167
|
+
brokerId: body.brokerId,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Register `TradingHandlers` as a provider:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { createPublicClient, http } from "viem";
|
|
177
|
+
import { base } from "viem/chains";
|
|
178
|
+
import { TradingHandlers } from "@pafi-dev/trading";
|
|
179
|
+
|
|
180
|
+
@Module({
|
|
181
|
+
providers: [
|
|
182
|
+
{
|
|
183
|
+
provide: TradingHandlers,
|
|
184
|
+
useFactory: (config: ConfigService) =>
|
|
185
|
+
new TradingHandlers({
|
|
186
|
+
provider: createPublicClient({
|
|
187
|
+
chain: base,
|
|
188
|
+
transport: http(config.get("RPC_URL")),
|
|
189
|
+
}),
|
|
190
|
+
chainId: config.get<number>("CHAIN_ID"),
|
|
191
|
+
}),
|
|
192
|
+
inject: [ConfigService],
|
|
193
|
+
},
|
|
194
|
+
TradingController,
|
|
195
|
+
],
|
|
196
|
+
})
|
|
197
|
+
export class TradingModule {}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## API types
|
|
203
|
+
|
|
204
|
+
All request/response types are exported for use in frontend SDKs or OpenAPI generation:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import type {
|
|
208
|
+
ApiQuoteRequest,
|
|
209
|
+
ApiQuoteResponse,
|
|
210
|
+
ApiQuoteError,
|
|
211
|
+
ApiSwapRequest,
|
|
212
|
+
ApiSwapResponse,
|
|
213
|
+
ApiPerpDepositRequest,
|
|
214
|
+
ApiPerpDepositResponse,
|
|
215
|
+
} from "@pafi-dev/trading";
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Since `bigint` does not serialize to JSON natively, HTTP controllers should convert
|
|
219
|
+
`bigint` fields to strings in the response (e.g. `.toString()`).
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Relationship to other PAFI packages
|
|
224
|
+
|
|
225
|
+
| Package | Scope |
|
|
226
|
+
|---|---|
|
|
227
|
+
| `@pafi-dev/core` | Chain primitives — EIP-712, UserOp builders, on-chain reads, ABIs |
|
|
228
|
+
| `@pafi-dev/issuer` | Issuer backend — mint/redeem auth, ledger, policy, indexer |
|
|
229
|
+
| `@pafi-dev/trading` | On-chain trading — swap quote + UserOp, perp deposit UserOp |
|
|
230
|
+
|
|
231
|
+
`@pafi-dev/trading` depends on `@pafi-dev/core` for `findBestQuote`,
|
|
232
|
+
`buildSwapWithGasDeduction`, `buildPerpDepositWithGasDeduction`, and contract addresses.
|
|
233
|
+
It does not depend on `@pafi-dev/issuer`.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
Apache-2.0
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
TradingHandlers: () => TradingHandlers
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/api/handlers.ts
|
|
28
|
+
var import_viem = require("viem");
|
|
29
|
+
var import_core = require("@pafi-dev/core");
|
|
30
|
+
var TradingHandlers = class {
|
|
31
|
+
provider;
|
|
32
|
+
chainId;
|
|
33
|
+
constructor(config) {
|
|
34
|
+
this.provider = config.provider;
|
|
35
|
+
this.chainId = config.chainId;
|
|
36
|
+
}
|
|
37
|
+
// =========================================================================
|
|
38
|
+
// GET /quote
|
|
39
|
+
// =========================================================================
|
|
40
|
+
/**
|
|
41
|
+
* Quote exact-input PT → USDT via Uniswap V4 on-chain Quoter.
|
|
42
|
+
*
|
|
43
|
+
* Uses multicall to batch all candidate routes into a single RPC call.
|
|
44
|
+
* Returns `quoteError: "QUOTE_UNAVAILABLE"` when no pool/path exists
|
|
45
|
+
* rather than throwing, so callers can show a soft "unavailable" UI
|
|
46
|
+
* state without 500-ing.
|
|
47
|
+
*/
|
|
48
|
+
async handleQuote(request) {
|
|
49
|
+
if (request.chainId !== this.chainId) {
|
|
50
|
+
throw new Error(`handleQuote: unsupported chainId ${request.chainId}`);
|
|
51
|
+
}
|
|
52
|
+
if (request.amount === 0n) {
|
|
53
|
+
return { pointAmount: 0n, estimatedUsdtOut: 0n, gasEstimate: 0n };
|
|
54
|
+
}
|
|
55
|
+
const { usdt } = (0, import_core.getContractAddresses)(request.chainId);
|
|
56
|
+
const pointTokenAddress = (0, import_viem.getAddress)(request.pointTokenAddress);
|
|
57
|
+
const pools = request.pools ?? [];
|
|
58
|
+
try {
|
|
59
|
+
const best = await (0, import_core.findBestQuote)(
|
|
60
|
+
this.provider,
|
|
61
|
+
request.chainId,
|
|
62
|
+
pointTokenAddress,
|
|
63
|
+
usdt,
|
|
64
|
+
request.amount,
|
|
65
|
+
pools
|
|
66
|
+
);
|
|
67
|
+
return {
|
|
68
|
+
pointAmount: request.amount,
|
|
69
|
+
estimatedUsdtOut: best.bestRoute.amountOut,
|
|
70
|
+
gasEstimate: best.bestRoute.gasEstimate
|
|
71
|
+
};
|
|
72
|
+
} catch {
|
|
73
|
+
return {
|
|
74
|
+
pointAmount: request.amount,
|
|
75
|
+
estimatedUsdtOut: 0n,
|
|
76
|
+
gasEstimate: 0n,
|
|
77
|
+
quoteError: "QUOTE_UNAVAILABLE"
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// =========================================================================
|
|
82
|
+
// POST /swap
|
|
83
|
+
// =========================================================================
|
|
84
|
+
/**
|
|
85
|
+
* Build a PT → USDT swap UserOp.
|
|
86
|
+
*
|
|
87
|
+
* Quotes the best route, applies slippage, then encodes a 4-step
|
|
88
|
+
* batch: PT.approve → Permit2.approve → UniversalRouter.execute →
|
|
89
|
+
* PT.transfer (fee, omitted when gasFeePt = 0). Returns an unsigned
|
|
90
|
+
* `PartialUserOperation`; caller attaches paymaster data + user
|
|
91
|
+
* signature and submits to the Bundler.
|
|
92
|
+
*/
|
|
93
|
+
async handleSwap(request) {
|
|
94
|
+
if (request.chainId !== this.chainId) {
|
|
95
|
+
throw new Error(`handleSwap: unsupported chainId ${request.chainId}`);
|
|
96
|
+
}
|
|
97
|
+
if (request.amount <= 0n) {
|
|
98
|
+
throw new Error("handleSwap: amount must be positive");
|
|
99
|
+
}
|
|
100
|
+
const { usdt } = (0, import_core.getContractAddresses)(request.chainId);
|
|
101
|
+
const universalRouter = import_core.UNIVERSAL_ROUTER_ADDRESSES[request.chainId];
|
|
102
|
+
if (!universalRouter) {
|
|
103
|
+
throw new Error(`handleSwap: no UniversalRouter for chainId ${request.chainId}`);
|
|
104
|
+
}
|
|
105
|
+
const pointTokenAddress = (0, import_viem.getAddress)(request.pointTokenAddress);
|
|
106
|
+
const userAddress = (0, import_viem.getAddress)(request.userAddress);
|
|
107
|
+
const pools = request.pools ?? [];
|
|
108
|
+
const slippageBps = request.slippageBps ?? 50;
|
|
109
|
+
const gasFeePt = request.gasFeePt ?? 0n;
|
|
110
|
+
if (gasFeePt > 0n && !request.feeRecipient) {
|
|
111
|
+
throw new Error("handleSwap: feeRecipient required when gasFeePt > 0");
|
|
112
|
+
}
|
|
113
|
+
let quoteResult;
|
|
114
|
+
try {
|
|
115
|
+
quoteResult = await (0, import_core.findBestQuote)(
|
|
116
|
+
this.provider,
|
|
117
|
+
request.chainId,
|
|
118
|
+
pointTokenAddress,
|
|
119
|
+
usdt,
|
|
120
|
+
request.amount,
|
|
121
|
+
pools
|
|
122
|
+
);
|
|
123
|
+
} catch {
|
|
124
|
+
throw new Error("handleSwap: no swap path found for this point token");
|
|
125
|
+
}
|
|
126
|
+
const estimatedUsdtOut = quoteResult.bestRoute.amountOut;
|
|
127
|
+
const minAmountOut = estimatedUsdtOut * BigInt(1e4 - slippageBps) / 10000n;
|
|
128
|
+
const deadline = BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
|
|
129
|
+
const userOp = (0, import_core.buildSwapWithGasDeduction)({
|
|
130
|
+
userAddress,
|
|
131
|
+
aaNonce: request.aaNonce,
|
|
132
|
+
pointTokenAddress,
|
|
133
|
+
outputTokenAddress: usdt,
|
|
134
|
+
universalRouterAddress: universalRouter,
|
|
135
|
+
amountIn: request.amount,
|
|
136
|
+
minAmountOut,
|
|
137
|
+
swapPath: quoteResult.bestRoute.path,
|
|
138
|
+
deadline,
|
|
139
|
+
gasFeePt,
|
|
140
|
+
feeRecipient: request.feeRecipient ?? userAddress
|
|
141
|
+
});
|
|
142
|
+
return { userOp, estimatedUsdtOut, minAmountOut, deadline };
|
|
143
|
+
}
|
|
144
|
+
// =========================================================================
|
|
145
|
+
// POST /perp-deposit
|
|
146
|
+
// =========================================================================
|
|
147
|
+
/**
|
|
148
|
+
* Build an Orderly perp deposit UserOp.
|
|
149
|
+
*
|
|
150
|
+
* Resolves USDC address and LayerZero fee from on-chain Vault reads,
|
|
151
|
+
* then encodes a 2-step batch: USDC.approve → Vault.deposit{value}.
|
|
152
|
+
* The `layerZeroFee` in the response is the ETH the user must hold
|
|
153
|
+
* natively — paymaster sponsors ERC-4337 gas only, not msg.value.
|
|
154
|
+
*/
|
|
155
|
+
async handlePerpDeposit(request) {
|
|
156
|
+
if (request.chainId !== this.chainId) {
|
|
157
|
+
throw new Error(`handlePerpDeposit: unsupported chainId ${request.chainId}`);
|
|
158
|
+
}
|
|
159
|
+
if (request.amount <= 0n) {
|
|
160
|
+
throw new Error("handlePerpDeposit: amount must be positive");
|
|
161
|
+
}
|
|
162
|
+
const vault = import_core.ORDERLY_VAULT_ADDRESSES[request.chainId];
|
|
163
|
+
if (!vault) {
|
|
164
|
+
throw new Error(`handlePerpDeposit: no Orderly Vault for chainId ${request.chainId}`);
|
|
165
|
+
}
|
|
166
|
+
const brokerHash = import_core.BROKER_HASHES[request.brokerId];
|
|
167
|
+
if (!brokerHash) {
|
|
168
|
+
throw new Error(`handlePerpDeposit: unknown brokerId "${request.brokerId}"`);
|
|
169
|
+
}
|
|
170
|
+
const tokenHash = import_core.TOKEN_HASHES.USDC;
|
|
171
|
+
const userAddress = (0, import_viem.getAddress)(request.userAddress);
|
|
172
|
+
const [usdcAddress, brokerAllowed] = await Promise.all([
|
|
173
|
+
this.provider.readContract({
|
|
174
|
+
address: vault,
|
|
175
|
+
abi: import_core.ORDERLY_VAULT_ABI,
|
|
176
|
+
functionName: "getAllowedToken",
|
|
177
|
+
args: [tokenHash]
|
|
178
|
+
}),
|
|
179
|
+
this.provider.readContract({
|
|
180
|
+
address: vault,
|
|
181
|
+
abi: import_core.ORDERLY_VAULT_ABI,
|
|
182
|
+
functionName: "getAllowedBroker",
|
|
183
|
+
args: [brokerHash]
|
|
184
|
+
})
|
|
185
|
+
]);
|
|
186
|
+
if (!brokerAllowed) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`handlePerpDeposit: broker "${request.brokerId}" is not whitelisted on Orderly Vault`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const accountId = (0, import_core.computeAccountId)(userAddress, brokerHash);
|
|
192
|
+
const depositData = {
|
|
193
|
+
accountId,
|
|
194
|
+
brokerHash,
|
|
195
|
+
tokenHash,
|
|
196
|
+
tokenAmount: request.amount
|
|
197
|
+
};
|
|
198
|
+
const layerZeroFee = await this.provider.readContract({
|
|
199
|
+
address: vault,
|
|
200
|
+
abi: import_core.ORDERLY_VAULT_ABI,
|
|
201
|
+
functionName: "getDepositFee",
|
|
202
|
+
args: [userAddress, depositData]
|
|
203
|
+
});
|
|
204
|
+
const userOp = (0, import_core.buildPerpDepositWithGasDeduction)({
|
|
205
|
+
userAddress,
|
|
206
|
+
aaNonce: request.aaNonce,
|
|
207
|
+
chainId: request.chainId,
|
|
208
|
+
usdcAddress,
|
|
209
|
+
amount: request.amount,
|
|
210
|
+
depositData,
|
|
211
|
+
layerZeroFee
|
|
212
|
+
});
|
|
213
|
+
return { userOp, layerZeroFee, accountId, brokerHash, usdcAddress };
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
217
|
+
0 && (module.exports = {
|
|
218
|
+
TradingHandlers
|
|
219
|
+
});
|
|
220
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/api/handlers.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","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"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Address, PublicClient } from 'viem';
|
|
2
|
+
import { PartialUserOperation, PoolKey } from '@pafi-dev/core';
|
|
3
|
+
|
|
4
|
+
interface ApiQuoteRequest {
|
|
5
|
+
chainId: number;
|
|
6
|
+
pointTokenAddress: Address;
|
|
7
|
+
/** PT amount (18-decimal raw units). */
|
|
8
|
+
amount: bigint;
|
|
9
|
+
/**
|
|
10
|
+
* PT/USDT pools to include in routing. Combined with COMMON_POOLS
|
|
11
|
+
* from `@pafi-dev/core`. Pass empty array to use only COMMON_POOLS.
|
|
12
|
+
*/
|
|
13
|
+
pools?: PoolKey[];
|
|
14
|
+
}
|
|
15
|
+
type ApiQuoteError = "QUOTE_UNAVAILABLE" | "AMOUNT_TOO_SMALL_FOR_GAS";
|
|
16
|
+
interface ApiQuoteResponse {
|
|
17
|
+
pointAmount: bigint;
|
|
18
|
+
estimatedUsdtOut: bigint;
|
|
19
|
+
gasEstimate: bigint;
|
|
20
|
+
quoteError?: ApiQuoteError;
|
|
21
|
+
}
|
|
22
|
+
interface ApiSwapRequest {
|
|
23
|
+
chainId: number;
|
|
24
|
+
userAddress: Address;
|
|
25
|
+
pointTokenAddress: Address;
|
|
26
|
+
/** PT amount to swap (18-decimal raw units). */
|
|
27
|
+
amount: bigint;
|
|
28
|
+
/** ERC-4337 account nonce for the user's EOA (from EntryPoint). */
|
|
29
|
+
aaNonce: bigint;
|
|
30
|
+
/** Slippage tolerance in basis points (1 bps = 0.01%). Default: 50 (0.5%). */
|
|
31
|
+
slippageBps?: number;
|
|
32
|
+
/** PT/USDT pools. Combined with COMMON_POOLS. Pass empty to use only COMMON_POOLS. */
|
|
33
|
+
pools?: PoolKey[];
|
|
34
|
+
/**
|
|
35
|
+
* PT amount deducted from user balance as operator gas fee.
|
|
36
|
+
* Default: 0n (no fee deduction). When > 0, `feeRecipient` is required.
|
|
37
|
+
*/
|
|
38
|
+
gasFeePt?: bigint;
|
|
39
|
+
/** Recipient of the gasFeePt deduction. Required when gasFeePt > 0. */
|
|
40
|
+
feeRecipient?: Address;
|
|
41
|
+
}
|
|
42
|
+
interface ApiSwapResponse {
|
|
43
|
+
/** Unsigned UserOp — attach paymaster data + user signature, then submit to Bundler. */
|
|
44
|
+
userOp: PartialUserOperation;
|
|
45
|
+
/** Raw USDT out before slippage (6 decimals). For display. */
|
|
46
|
+
estimatedUsdtOut: bigint;
|
|
47
|
+
/** Minimum USDT accepted — encoded in the UserOp calldata. */
|
|
48
|
+
minAmountOut: bigint;
|
|
49
|
+
/** Swap deadline (unix seconds). Re-request if user doesn't submit in time. */
|
|
50
|
+
deadline: bigint;
|
|
51
|
+
}
|
|
52
|
+
interface ApiPerpDepositRequest {
|
|
53
|
+
chainId: number;
|
|
54
|
+
userAddress: Address;
|
|
55
|
+
/** USDC amount to deposit (6-decimal raw units). */
|
|
56
|
+
amount: bigint;
|
|
57
|
+
/** ERC-4337 account nonce for the user's EOA (from EntryPoint). */
|
|
58
|
+
aaNonce: bigint;
|
|
59
|
+
/**
|
|
60
|
+
* Orderly broker ID — must be whitelisted on the Vault.
|
|
61
|
+
* Known values: woofi_pro, orderly, logx.
|
|
62
|
+
*/
|
|
63
|
+
brokerId: string;
|
|
64
|
+
}
|
|
65
|
+
interface ApiPerpDepositResponse {
|
|
66
|
+
/** Unsigned UserOp — attach paymaster data + user signature, then submit to Bundler. */
|
|
67
|
+
userOp: PartialUserOperation;
|
|
68
|
+
/**
|
|
69
|
+
* LayerZero fee in ETH wei. User wallet must hold at least this as
|
|
70
|
+
* native ETH — even when ERC-4337 gas is sponsored by paymaster.
|
|
71
|
+
*/
|
|
72
|
+
layerZeroFee: bigint;
|
|
73
|
+
/** Orderly account ID for this (user, broker) pair. */
|
|
74
|
+
accountId: `0x${string}`;
|
|
75
|
+
/** keccak256(brokerId) — for client-side verification. */
|
|
76
|
+
brokerHash: `0x${string}`;
|
|
77
|
+
/** USDC contract address (resolved from Vault.getAllowedToken). */
|
|
78
|
+
usdcAddress: Address;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface TradingHandlersConfig {
|
|
82
|
+
provider: PublicClient;
|
|
83
|
+
chainId: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Framework-agnostic handlers for on-chain trading actions.
|
|
87
|
+
*
|
|
88
|
+
* All handlers are stateless — they need only a PublicClient for RPC
|
|
89
|
+
* calls. No ledger, no signer, no DB. Issuers wrap these in their own
|
|
90
|
+
* HTTP controllers (Express / NestJS / Hono / etc.) the same way they
|
|
91
|
+
* wrap `IssuerApiHandlers` from `@pafi-dev/issuer`.
|
|
92
|
+
*
|
|
93
|
+
* Example (NestJS):
|
|
94
|
+
*
|
|
95
|
+
* const trading = new TradingHandlers({ provider, chainId });
|
|
96
|
+
*
|
|
97
|
+
* // GET /quote
|
|
98
|
+
* const quote = await trading.handleQuote({ chainId, pointTokenAddress, amount, pools });
|
|
99
|
+
*
|
|
100
|
+
* // POST /swap
|
|
101
|
+
* const swap = await trading.handleSwap({ chainId, userAddress, pointTokenAddress, amount, aaNonce });
|
|
102
|
+
*
|
|
103
|
+
* // POST /perp-deposit
|
|
104
|
+
* const deposit = await trading.handlePerpDeposit({ chainId, userAddress, amount, aaNonce, brokerId });
|
|
105
|
+
*/
|
|
106
|
+
declare class TradingHandlers {
|
|
107
|
+
private readonly provider;
|
|
108
|
+
private readonly chainId;
|
|
109
|
+
constructor(config: TradingHandlersConfig);
|
|
110
|
+
/**
|
|
111
|
+
* Quote exact-input PT → USDT via Uniswap V4 on-chain Quoter.
|
|
112
|
+
*
|
|
113
|
+
* Uses multicall to batch all candidate routes into a single RPC call.
|
|
114
|
+
* Returns `quoteError: "QUOTE_UNAVAILABLE"` when no pool/path exists
|
|
115
|
+
* rather than throwing, so callers can show a soft "unavailable" UI
|
|
116
|
+
* state without 500-ing.
|
|
117
|
+
*/
|
|
118
|
+
handleQuote(request: ApiQuoteRequest): Promise<ApiQuoteResponse>;
|
|
119
|
+
/**
|
|
120
|
+
* Build a PT → USDT swap UserOp.
|
|
121
|
+
*
|
|
122
|
+
* Quotes the best route, applies slippage, then encodes a 4-step
|
|
123
|
+
* batch: PT.approve → Permit2.approve → UniversalRouter.execute →
|
|
124
|
+
* PT.transfer (fee, omitted when gasFeePt = 0). Returns an unsigned
|
|
125
|
+
* `PartialUserOperation`; caller attaches paymaster data + user
|
|
126
|
+
* signature and submits to the Bundler.
|
|
127
|
+
*/
|
|
128
|
+
handleSwap(request: ApiSwapRequest): Promise<ApiSwapResponse>;
|
|
129
|
+
/**
|
|
130
|
+
* Build an Orderly perp deposit UserOp.
|
|
131
|
+
*
|
|
132
|
+
* Resolves USDC address and LayerZero fee from on-chain Vault reads,
|
|
133
|
+
* then encodes a 2-step batch: USDC.approve → Vault.deposit{value}.
|
|
134
|
+
* The `layerZeroFee` in the response is the ETH the user must hold
|
|
135
|
+
* natively — paymaster sponsors ERC-4337 gas only, not msg.value.
|
|
136
|
+
*/
|
|
137
|
+
handlePerpDeposit(request: ApiPerpDepositRequest): Promise<ApiPerpDepositResponse>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { type ApiPerpDepositRequest, type ApiPerpDepositResponse, type ApiQuoteError, type ApiQuoteRequest, type ApiQuoteResponse, type ApiSwapRequest, type ApiSwapResponse, TradingHandlers, type TradingHandlersConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Address, PublicClient } from 'viem';
|
|
2
|
+
import { PartialUserOperation, PoolKey } from '@pafi-dev/core';
|
|
3
|
+
|
|
4
|
+
interface ApiQuoteRequest {
|
|
5
|
+
chainId: number;
|
|
6
|
+
pointTokenAddress: Address;
|
|
7
|
+
/** PT amount (18-decimal raw units). */
|
|
8
|
+
amount: bigint;
|
|
9
|
+
/**
|
|
10
|
+
* PT/USDT pools to include in routing. Combined with COMMON_POOLS
|
|
11
|
+
* from `@pafi-dev/core`. Pass empty array to use only COMMON_POOLS.
|
|
12
|
+
*/
|
|
13
|
+
pools?: PoolKey[];
|
|
14
|
+
}
|
|
15
|
+
type ApiQuoteError = "QUOTE_UNAVAILABLE" | "AMOUNT_TOO_SMALL_FOR_GAS";
|
|
16
|
+
interface ApiQuoteResponse {
|
|
17
|
+
pointAmount: bigint;
|
|
18
|
+
estimatedUsdtOut: bigint;
|
|
19
|
+
gasEstimate: bigint;
|
|
20
|
+
quoteError?: ApiQuoteError;
|
|
21
|
+
}
|
|
22
|
+
interface ApiSwapRequest {
|
|
23
|
+
chainId: number;
|
|
24
|
+
userAddress: Address;
|
|
25
|
+
pointTokenAddress: Address;
|
|
26
|
+
/** PT amount to swap (18-decimal raw units). */
|
|
27
|
+
amount: bigint;
|
|
28
|
+
/** ERC-4337 account nonce for the user's EOA (from EntryPoint). */
|
|
29
|
+
aaNonce: bigint;
|
|
30
|
+
/** Slippage tolerance in basis points (1 bps = 0.01%). Default: 50 (0.5%). */
|
|
31
|
+
slippageBps?: number;
|
|
32
|
+
/** PT/USDT pools. Combined with COMMON_POOLS. Pass empty to use only COMMON_POOLS. */
|
|
33
|
+
pools?: PoolKey[];
|
|
34
|
+
/**
|
|
35
|
+
* PT amount deducted from user balance as operator gas fee.
|
|
36
|
+
* Default: 0n (no fee deduction). When > 0, `feeRecipient` is required.
|
|
37
|
+
*/
|
|
38
|
+
gasFeePt?: bigint;
|
|
39
|
+
/** Recipient of the gasFeePt deduction. Required when gasFeePt > 0. */
|
|
40
|
+
feeRecipient?: Address;
|
|
41
|
+
}
|
|
42
|
+
interface ApiSwapResponse {
|
|
43
|
+
/** Unsigned UserOp — attach paymaster data + user signature, then submit to Bundler. */
|
|
44
|
+
userOp: PartialUserOperation;
|
|
45
|
+
/** Raw USDT out before slippage (6 decimals). For display. */
|
|
46
|
+
estimatedUsdtOut: bigint;
|
|
47
|
+
/** Minimum USDT accepted — encoded in the UserOp calldata. */
|
|
48
|
+
minAmountOut: bigint;
|
|
49
|
+
/** Swap deadline (unix seconds). Re-request if user doesn't submit in time. */
|
|
50
|
+
deadline: bigint;
|
|
51
|
+
}
|
|
52
|
+
interface ApiPerpDepositRequest {
|
|
53
|
+
chainId: number;
|
|
54
|
+
userAddress: Address;
|
|
55
|
+
/** USDC amount to deposit (6-decimal raw units). */
|
|
56
|
+
amount: bigint;
|
|
57
|
+
/** ERC-4337 account nonce for the user's EOA (from EntryPoint). */
|
|
58
|
+
aaNonce: bigint;
|
|
59
|
+
/**
|
|
60
|
+
* Orderly broker ID — must be whitelisted on the Vault.
|
|
61
|
+
* Known values: woofi_pro, orderly, logx.
|
|
62
|
+
*/
|
|
63
|
+
brokerId: string;
|
|
64
|
+
}
|
|
65
|
+
interface ApiPerpDepositResponse {
|
|
66
|
+
/** Unsigned UserOp — attach paymaster data + user signature, then submit to Bundler. */
|
|
67
|
+
userOp: PartialUserOperation;
|
|
68
|
+
/**
|
|
69
|
+
* LayerZero fee in ETH wei. User wallet must hold at least this as
|
|
70
|
+
* native ETH — even when ERC-4337 gas is sponsored by paymaster.
|
|
71
|
+
*/
|
|
72
|
+
layerZeroFee: bigint;
|
|
73
|
+
/** Orderly account ID for this (user, broker) pair. */
|
|
74
|
+
accountId: `0x${string}`;
|
|
75
|
+
/** keccak256(brokerId) — for client-side verification. */
|
|
76
|
+
brokerHash: `0x${string}`;
|
|
77
|
+
/** USDC contract address (resolved from Vault.getAllowedToken). */
|
|
78
|
+
usdcAddress: Address;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface TradingHandlersConfig {
|
|
82
|
+
provider: PublicClient;
|
|
83
|
+
chainId: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Framework-agnostic handlers for on-chain trading actions.
|
|
87
|
+
*
|
|
88
|
+
* All handlers are stateless — they need only a PublicClient for RPC
|
|
89
|
+
* calls. No ledger, no signer, no DB. Issuers wrap these in their own
|
|
90
|
+
* HTTP controllers (Express / NestJS / Hono / etc.) the same way they
|
|
91
|
+
* wrap `IssuerApiHandlers` from `@pafi-dev/issuer`.
|
|
92
|
+
*
|
|
93
|
+
* Example (NestJS):
|
|
94
|
+
*
|
|
95
|
+
* const trading = new TradingHandlers({ provider, chainId });
|
|
96
|
+
*
|
|
97
|
+
* // GET /quote
|
|
98
|
+
* const quote = await trading.handleQuote({ chainId, pointTokenAddress, amount, pools });
|
|
99
|
+
*
|
|
100
|
+
* // POST /swap
|
|
101
|
+
* const swap = await trading.handleSwap({ chainId, userAddress, pointTokenAddress, amount, aaNonce });
|
|
102
|
+
*
|
|
103
|
+
* // POST /perp-deposit
|
|
104
|
+
* const deposit = await trading.handlePerpDeposit({ chainId, userAddress, amount, aaNonce, brokerId });
|
|
105
|
+
*/
|
|
106
|
+
declare class TradingHandlers {
|
|
107
|
+
private readonly provider;
|
|
108
|
+
private readonly chainId;
|
|
109
|
+
constructor(config: TradingHandlersConfig);
|
|
110
|
+
/**
|
|
111
|
+
* Quote exact-input PT → USDT via Uniswap V4 on-chain Quoter.
|
|
112
|
+
*
|
|
113
|
+
* Uses multicall to batch all candidate routes into a single RPC call.
|
|
114
|
+
* Returns `quoteError: "QUOTE_UNAVAILABLE"` when no pool/path exists
|
|
115
|
+
* rather than throwing, so callers can show a soft "unavailable" UI
|
|
116
|
+
* state without 500-ing.
|
|
117
|
+
*/
|
|
118
|
+
handleQuote(request: ApiQuoteRequest): Promise<ApiQuoteResponse>;
|
|
119
|
+
/**
|
|
120
|
+
* Build a PT → USDT swap UserOp.
|
|
121
|
+
*
|
|
122
|
+
* Quotes the best route, applies slippage, then encodes a 4-step
|
|
123
|
+
* batch: PT.approve → Permit2.approve → UniversalRouter.execute →
|
|
124
|
+
* PT.transfer (fee, omitted when gasFeePt = 0). Returns an unsigned
|
|
125
|
+
* `PartialUserOperation`; caller attaches paymaster data + user
|
|
126
|
+
* signature and submits to the Bundler.
|
|
127
|
+
*/
|
|
128
|
+
handleSwap(request: ApiSwapRequest): Promise<ApiSwapResponse>;
|
|
129
|
+
/**
|
|
130
|
+
* Build an Orderly perp deposit UserOp.
|
|
131
|
+
*
|
|
132
|
+
* Resolves USDC address and LayerZero fee from on-chain Vault reads,
|
|
133
|
+
* then encodes a 2-step batch: USDC.approve → Vault.deposit{value}.
|
|
134
|
+
* The `layerZeroFee` in the response is the ETH the user must hold
|
|
135
|
+
* natively — paymaster sponsors ERC-4337 gas only, not msg.value.
|
|
136
|
+
*/
|
|
137
|
+
handlePerpDeposit(request: ApiPerpDepositRequest): Promise<ApiPerpDepositResponse>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { type ApiPerpDepositRequest, type ApiPerpDepositResponse, type ApiQuoteError, type ApiQuoteRequest, type ApiQuoteResponse, type ApiSwapRequest, type ApiSwapResponse, TradingHandlers, type TradingHandlersConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// src/api/handlers.ts
|
|
2
|
+
import { getAddress } from "viem";
|
|
3
|
+
import {
|
|
4
|
+
findBestQuote,
|
|
5
|
+
buildSwapWithGasDeduction,
|
|
6
|
+
buildPerpDepositWithGasDeduction,
|
|
7
|
+
getContractAddresses,
|
|
8
|
+
UNIVERSAL_ROUTER_ADDRESSES,
|
|
9
|
+
ORDERLY_VAULT_ABI,
|
|
10
|
+
ORDERLY_VAULT_ADDRESSES,
|
|
11
|
+
BROKER_HASHES,
|
|
12
|
+
TOKEN_HASHES,
|
|
13
|
+
computeAccountId
|
|
14
|
+
} from "@pafi-dev/core";
|
|
15
|
+
var TradingHandlers = class {
|
|
16
|
+
provider;
|
|
17
|
+
chainId;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.provider = config.provider;
|
|
20
|
+
this.chainId = config.chainId;
|
|
21
|
+
}
|
|
22
|
+
// =========================================================================
|
|
23
|
+
// GET /quote
|
|
24
|
+
// =========================================================================
|
|
25
|
+
/**
|
|
26
|
+
* Quote exact-input PT → USDT via Uniswap V4 on-chain Quoter.
|
|
27
|
+
*
|
|
28
|
+
* Uses multicall to batch all candidate routes into a single RPC call.
|
|
29
|
+
* Returns `quoteError: "QUOTE_UNAVAILABLE"` when no pool/path exists
|
|
30
|
+
* rather than throwing, so callers can show a soft "unavailable" UI
|
|
31
|
+
* state without 500-ing.
|
|
32
|
+
*/
|
|
33
|
+
async handleQuote(request) {
|
|
34
|
+
if (request.chainId !== this.chainId) {
|
|
35
|
+
throw new Error(`handleQuote: unsupported chainId ${request.chainId}`);
|
|
36
|
+
}
|
|
37
|
+
if (request.amount === 0n) {
|
|
38
|
+
return { pointAmount: 0n, estimatedUsdtOut: 0n, gasEstimate: 0n };
|
|
39
|
+
}
|
|
40
|
+
const { usdt } = getContractAddresses(request.chainId);
|
|
41
|
+
const pointTokenAddress = getAddress(request.pointTokenAddress);
|
|
42
|
+
const pools = request.pools ?? [];
|
|
43
|
+
try {
|
|
44
|
+
const best = await findBestQuote(
|
|
45
|
+
this.provider,
|
|
46
|
+
request.chainId,
|
|
47
|
+
pointTokenAddress,
|
|
48
|
+
usdt,
|
|
49
|
+
request.amount,
|
|
50
|
+
pools
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
pointAmount: request.amount,
|
|
54
|
+
estimatedUsdtOut: best.bestRoute.amountOut,
|
|
55
|
+
gasEstimate: best.bestRoute.gasEstimate
|
|
56
|
+
};
|
|
57
|
+
} catch {
|
|
58
|
+
return {
|
|
59
|
+
pointAmount: request.amount,
|
|
60
|
+
estimatedUsdtOut: 0n,
|
|
61
|
+
gasEstimate: 0n,
|
|
62
|
+
quoteError: "QUOTE_UNAVAILABLE"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// =========================================================================
|
|
67
|
+
// POST /swap
|
|
68
|
+
// =========================================================================
|
|
69
|
+
/**
|
|
70
|
+
* Build a PT → USDT swap UserOp.
|
|
71
|
+
*
|
|
72
|
+
* Quotes the best route, applies slippage, then encodes a 4-step
|
|
73
|
+
* batch: PT.approve → Permit2.approve → UniversalRouter.execute →
|
|
74
|
+
* PT.transfer (fee, omitted when gasFeePt = 0). Returns an unsigned
|
|
75
|
+
* `PartialUserOperation`; caller attaches paymaster data + user
|
|
76
|
+
* signature and submits to the Bundler.
|
|
77
|
+
*/
|
|
78
|
+
async handleSwap(request) {
|
|
79
|
+
if (request.chainId !== this.chainId) {
|
|
80
|
+
throw new Error(`handleSwap: unsupported chainId ${request.chainId}`);
|
|
81
|
+
}
|
|
82
|
+
if (request.amount <= 0n) {
|
|
83
|
+
throw new Error("handleSwap: amount must be positive");
|
|
84
|
+
}
|
|
85
|
+
const { usdt } = getContractAddresses(request.chainId);
|
|
86
|
+
const universalRouter = UNIVERSAL_ROUTER_ADDRESSES[request.chainId];
|
|
87
|
+
if (!universalRouter) {
|
|
88
|
+
throw new Error(`handleSwap: no UniversalRouter for chainId ${request.chainId}`);
|
|
89
|
+
}
|
|
90
|
+
const pointTokenAddress = getAddress(request.pointTokenAddress);
|
|
91
|
+
const userAddress = getAddress(request.userAddress);
|
|
92
|
+
const pools = request.pools ?? [];
|
|
93
|
+
const slippageBps = request.slippageBps ?? 50;
|
|
94
|
+
const gasFeePt = request.gasFeePt ?? 0n;
|
|
95
|
+
if (gasFeePt > 0n && !request.feeRecipient) {
|
|
96
|
+
throw new Error("handleSwap: feeRecipient required when gasFeePt > 0");
|
|
97
|
+
}
|
|
98
|
+
let quoteResult;
|
|
99
|
+
try {
|
|
100
|
+
quoteResult = await findBestQuote(
|
|
101
|
+
this.provider,
|
|
102
|
+
request.chainId,
|
|
103
|
+
pointTokenAddress,
|
|
104
|
+
usdt,
|
|
105
|
+
request.amount,
|
|
106
|
+
pools
|
|
107
|
+
);
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error("handleSwap: no swap path found for this point token");
|
|
110
|
+
}
|
|
111
|
+
const estimatedUsdtOut = quoteResult.bestRoute.amountOut;
|
|
112
|
+
const minAmountOut = estimatedUsdtOut * BigInt(1e4 - slippageBps) / 10000n;
|
|
113
|
+
const deadline = BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
|
|
114
|
+
const userOp = buildSwapWithGasDeduction({
|
|
115
|
+
userAddress,
|
|
116
|
+
aaNonce: request.aaNonce,
|
|
117
|
+
pointTokenAddress,
|
|
118
|
+
outputTokenAddress: usdt,
|
|
119
|
+
universalRouterAddress: universalRouter,
|
|
120
|
+
amountIn: request.amount,
|
|
121
|
+
minAmountOut,
|
|
122
|
+
swapPath: quoteResult.bestRoute.path,
|
|
123
|
+
deadline,
|
|
124
|
+
gasFeePt,
|
|
125
|
+
feeRecipient: request.feeRecipient ?? userAddress
|
|
126
|
+
});
|
|
127
|
+
return { userOp, estimatedUsdtOut, minAmountOut, deadline };
|
|
128
|
+
}
|
|
129
|
+
// =========================================================================
|
|
130
|
+
// POST /perp-deposit
|
|
131
|
+
// =========================================================================
|
|
132
|
+
/**
|
|
133
|
+
* Build an Orderly perp deposit UserOp.
|
|
134
|
+
*
|
|
135
|
+
* Resolves USDC address and LayerZero fee from on-chain Vault reads,
|
|
136
|
+
* then encodes a 2-step batch: USDC.approve → Vault.deposit{value}.
|
|
137
|
+
* The `layerZeroFee` in the response is the ETH the user must hold
|
|
138
|
+
* natively — paymaster sponsors ERC-4337 gas only, not msg.value.
|
|
139
|
+
*/
|
|
140
|
+
async handlePerpDeposit(request) {
|
|
141
|
+
if (request.chainId !== this.chainId) {
|
|
142
|
+
throw new Error(`handlePerpDeposit: unsupported chainId ${request.chainId}`);
|
|
143
|
+
}
|
|
144
|
+
if (request.amount <= 0n) {
|
|
145
|
+
throw new Error("handlePerpDeposit: amount must be positive");
|
|
146
|
+
}
|
|
147
|
+
const vault = ORDERLY_VAULT_ADDRESSES[request.chainId];
|
|
148
|
+
if (!vault) {
|
|
149
|
+
throw new Error(`handlePerpDeposit: no Orderly Vault for chainId ${request.chainId}`);
|
|
150
|
+
}
|
|
151
|
+
const brokerHash = BROKER_HASHES[request.brokerId];
|
|
152
|
+
if (!brokerHash) {
|
|
153
|
+
throw new Error(`handlePerpDeposit: unknown brokerId "${request.brokerId}"`);
|
|
154
|
+
}
|
|
155
|
+
const tokenHash = TOKEN_HASHES.USDC;
|
|
156
|
+
const userAddress = getAddress(request.userAddress);
|
|
157
|
+
const [usdcAddress, brokerAllowed] = await Promise.all([
|
|
158
|
+
this.provider.readContract({
|
|
159
|
+
address: vault,
|
|
160
|
+
abi: ORDERLY_VAULT_ABI,
|
|
161
|
+
functionName: "getAllowedToken",
|
|
162
|
+
args: [tokenHash]
|
|
163
|
+
}),
|
|
164
|
+
this.provider.readContract({
|
|
165
|
+
address: vault,
|
|
166
|
+
abi: ORDERLY_VAULT_ABI,
|
|
167
|
+
functionName: "getAllowedBroker",
|
|
168
|
+
args: [brokerHash]
|
|
169
|
+
})
|
|
170
|
+
]);
|
|
171
|
+
if (!brokerAllowed) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`handlePerpDeposit: broker "${request.brokerId}" is not whitelisted on Orderly Vault`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const accountId = computeAccountId(userAddress, brokerHash);
|
|
177
|
+
const depositData = {
|
|
178
|
+
accountId,
|
|
179
|
+
brokerHash,
|
|
180
|
+
tokenHash,
|
|
181
|
+
tokenAmount: request.amount
|
|
182
|
+
};
|
|
183
|
+
const layerZeroFee = await this.provider.readContract({
|
|
184
|
+
address: vault,
|
|
185
|
+
abi: ORDERLY_VAULT_ABI,
|
|
186
|
+
functionName: "getDepositFee",
|
|
187
|
+
args: [userAddress, depositData]
|
|
188
|
+
});
|
|
189
|
+
const userOp = buildPerpDepositWithGasDeduction({
|
|
190
|
+
userAddress,
|
|
191
|
+
aaNonce: request.aaNonce,
|
|
192
|
+
chainId: request.chainId,
|
|
193
|
+
usdcAddress,
|
|
194
|
+
amount: request.amount,
|
|
195
|
+
depositData,
|
|
196
|
+
layerZeroFee
|
|
197
|
+
});
|
|
198
|
+
return { userOp, layerZeroFee, accountId, brokerHash, usdcAddress };
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
export {
|
|
202
|
+
TradingHandlers
|
|
203
|
+
};
|
|
204
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/api/handlers.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"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pafi-dev/trading",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Stateless on-chain trading handlers for PAFI — swap, quote, perp deposit",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@pafi-dev/core": "0.5.1"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"viem": "^2.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"tsup": "^8.0.0",
|
|
32
|
+
"typescript": "^5.5.0",
|
|
33
|
+
"viem": "^2.21.0",
|
|
34
|
+
"vitest": "^2.0.0"
|
|
35
|
+
},
|
|
36
|
+
"license": "Apache-2.0",
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"test": "vitest run --passWithNoTests",
|
|
40
|
+
"test:watch": "vitest",
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
42
|
+
}
|
|
43
|
+
}
|