@optimex-xyz/market-maker-sdk 0.0.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 +918 -0
- package/dist/index.d.mts +4813 -0
- package/dist/index.d.ts +4813 -0
- package/dist/index.js +4000 -0
- package/dist/index.mjs +3928 -0
- package/package.json +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
# PMM SDK Integration Documentation
|
|
2
|
+
|
|
3
|
+
A comprehensive toolkit for implementing Private Market Makers (PMMs) in the cross-chain trading network. This guide covers the required integration points between PMMs and our solver backend, enabling cross-chain liquidity provision and settlement.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [PMM SDK Integration Documentation](#pmm-sdk-integration-documentation)
|
|
8
|
+
- [Table of Contents](#table-of-contents)
|
|
9
|
+
- [1. Overview](#1-overview)
|
|
10
|
+
- [1.1. Repository Structure](#11-repository-structure)
|
|
11
|
+
- [1.2. Example Implementation](#12-example-implementation)
|
|
12
|
+
- [2. Quick Start](#2-quick-start)
|
|
13
|
+
- [2.1. Installation](#21-installation)
|
|
14
|
+
- [2.2. Environment Setup](#22-environment-setup)
|
|
15
|
+
- [3. PMM Backend APIs](#3-pmm-backend-apis)
|
|
16
|
+
- [3.1. Endpoint: `/indicative-quote`](#31-endpoint-indicative-quote)
|
|
17
|
+
- [Description](#description)
|
|
18
|
+
- [Request Parameters](#request-parameters)
|
|
19
|
+
- [Example Request](#example-request)
|
|
20
|
+
- [Expected Response](#expected-response)
|
|
21
|
+
- [Example Implementation](#example-implementation)
|
|
22
|
+
- [3.2. Endpoint: `/commitment-quote`](#32-endpoint-commitment-quote)
|
|
23
|
+
- [Description](#description-1)
|
|
24
|
+
- [Request Parameters](#request-parameters-1)
|
|
25
|
+
- [Example Request](#example-request-1)
|
|
26
|
+
- [Expected Response](#expected-response-1)
|
|
27
|
+
- [Example Implementation](#example-implementation-1)
|
|
28
|
+
- [3.3. Endpoint: `/settlement-signature`](#33-endpoint-settlement-signature)
|
|
29
|
+
- [Description](#description-2)
|
|
30
|
+
- [Request Parameters](#request-parameters-2)
|
|
31
|
+
- [Example Request](#example-request-2)
|
|
32
|
+
- [Expected Response](#expected-response-2)
|
|
33
|
+
- [Example Implementation](#example-implementation-2)
|
|
34
|
+
- [3.4. Endpoint: `/ack-settlement`](#34-endpoint-ack-settlement)
|
|
35
|
+
- [Description](#description-3)
|
|
36
|
+
- [Request Parameters](#request-parameters-3)
|
|
37
|
+
- [Example Request](#example-request-3)
|
|
38
|
+
- [Expected Response](#expected-response-3)
|
|
39
|
+
- [Example Implementation](#example-implementation-3)
|
|
40
|
+
- [3.5. Endpoint: `/signal-payment`](#35-endpoint-signal-payment)
|
|
41
|
+
- [Description](#description-4)
|
|
42
|
+
- [Request Parameters](#request-parameters-4)
|
|
43
|
+
- [Example Request](#example-request-4)
|
|
44
|
+
- [Expected Response](#expected-response-4)
|
|
45
|
+
- [Example Implementation](#example-implementation-4)
|
|
46
|
+
- [4. SDK Functions for PMMs](#4-sdk-functions-for-pmms)
|
|
47
|
+
- [4.1. Function: getTokens](#41-function-gettokens)
|
|
48
|
+
- [Description](#description-5)
|
|
49
|
+
- [Example Code](#example-code)
|
|
50
|
+
- [4.2. Function: submitSettlementTx](#42-function-submitsettlementtx)
|
|
51
|
+
- [Description](#description-6)
|
|
52
|
+
- [Example Implementation](#example-implementation-5)
|
|
53
|
+
- [Notes](#notes)
|
|
54
|
+
- [5. PMM Making Payment](#5-pmm-making-payment)
|
|
55
|
+
- [5.1. EVM](#51-evm)
|
|
56
|
+
- [5.2. Bitcoin](#52-bitcoin)
|
|
57
|
+
|
|
58
|
+
## 1. Overview
|
|
59
|
+
|
|
60
|
+
This repository contains everything needed to integrate your PMM with 's solver network:
|
|
61
|
+
|
|
62
|
+
```mermaid
|
|
63
|
+
sequenceDiagram
|
|
64
|
+
participant User
|
|
65
|
+
participant Solver
|
|
66
|
+
participant PMM
|
|
67
|
+
participant Chain
|
|
68
|
+
|
|
69
|
+
Note over User,Chain: Phase 1: Indicative Quote
|
|
70
|
+
User->>Solver: Request quote
|
|
71
|
+
Solver->>PMM: GET /indicative-quote
|
|
72
|
+
PMM-->>Solver: Return indicative quote
|
|
73
|
+
Solver-->>User: Show quote
|
|
74
|
+
|
|
75
|
+
Note over User,Chain: Phase 2: Commitment
|
|
76
|
+
User->>Solver: Accept quote
|
|
77
|
+
Solver->>PMM: GET /commitment-quote
|
|
78
|
+
PMM-->>Solver: Return commitment quote
|
|
79
|
+
|
|
80
|
+
Note over User,Chain: Phase 3: Settlement
|
|
81
|
+
Solver->>PMM: GET /settlement-signature
|
|
82
|
+
PMM-->>Solver: Return signature
|
|
83
|
+
Solver->>PMM: POST /ack-settlement
|
|
84
|
+
PMM-->>Solver: Acknowledge settlement
|
|
85
|
+
Solver->>PMM: POST /signal-payment
|
|
86
|
+
PMM-->>Solver: Acknowledge signal
|
|
87
|
+
PMM->>Chain: Execute settlement (transfer)
|
|
88
|
+
PMM->>Solver: POST /submit-settlement-tx
|
|
89
|
+
Solver-->>PMM: Confirm settlement submission
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 1.1. Repository Structure
|
|
93
|
+
The repository consists of:
|
|
94
|
+
- `abi/`: Smart contract ABIs and interfaces
|
|
95
|
+
- `example/`: A complete mock PMM implementation showing how to integrate the SDK
|
|
96
|
+
- `src/`: Source code for the market maker SDK
|
|
97
|
+
|
|
98
|
+
### 1.2. Example Implementation
|
|
99
|
+
The [Example](example/) directory contains a fully functional mock PMM. Use this implementation as a reference while integrating the `@optimex-xyz/market-maker-sdk` into your own PMM service.
|
|
100
|
+
|
|
101
|
+
## 2. Quick Start
|
|
102
|
+
|
|
103
|
+
### 2.1. Installation
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm install @optimex-xyz/market-maker-sdk
|
|
107
|
+
# or
|
|
108
|
+
yarn add @optimex-xyz/market-maker-sdk
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 2.2. Environment Setup
|
|
112
|
+
|
|
113
|
+
| Variable | Development | Production |
|
|
114
|
+
| -------- | ----------- | ---------- |
|
|
115
|
+
| SDK_ENV | dev | production |
|
|
116
|
+
|
|
117
|
+
## 3. PMM Backend APIs
|
|
118
|
+
|
|
119
|
+
These are the APIs that PMMs must implement for Solver integration. These endpoints allow Solvers to communicate with your PMM service.
|
|
120
|
+
|
|
121
|
+
### 3.1. Endpoint: `/indicative-quote`
|
|
122
|
+
|
|
123
|
+
#### Description
|
|
124
|
+
|
|
125
|
+
Provides an indicative quote for the given token pair and trade amount. The quote is used for informational purposes before a commitment is made.
|
|
126
|
+
|
|
127
|
+
#### Request Parameters
|
|
128
|
+
|
|
129
|
+
- **HTTP Method**: `GET`
|
|
130
|
+
- **Query Parameters**:
|
|
131
|
+
- `from_token_id` (string): The ID of the source token.
|
|
132
|
+
- `to_token_id` (string): The ID of the destination token.
|
|
133
|
+
- `amount` (string): The amount of the source token to be traded, represented as a string in base 10 to accommodate large numbers.
|
|
134
|
+
- `session_id` (string, optional): A unique identifier for the session.
|
|
135
|
+
|
|
136
|
+
#### Example Request
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
GET /indicative-quote?from_token_id=ETH&to_token_id=BTC&amount=1000000000000000000&session_id=12345
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Expected Response
|
|
143
|
+
|
|
144
|
+
- **HTTP Status**: `200 OK`
|
|
145
|
+
- **Response Body** (JSON):
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"session_id": "12345",
|
|
150
|
+
"pmm_receiving_address": "0xReceivingAddress",
|
|
151
|
+
"indicative_quote": "123456789000000000",
|
|
152
|
+
"error": "" // Empty if no error
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- `session_id` (string): The session ID associated with the request.
|
|
157
|
+
- `pmm_receiving_address` (string): The receiving address where the user will send the `from_token`.
|
|
158
|
+
- `indicative_quote` (string): The indicative quote value, represented as a string.
|
|
159
|
+
- `error` (string): Error message, if any (empty if no error).
|
|
160
|
+
|
|
161
|
+
#### Example Implementation
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
import { Token, tokenService } from '@optimex-xyz/market-maker-sdk'
|
|
165
|
+
|
|
166
|
+
export const IndicativeQuoteResponseSchema = z.object({
|
|
167
|
+
sessionId: z.string(),
|
|
168
|
+
pmmReceivingAddress: z.string(),
|
|
169
|
+
indicativeQuote: z.string(),
|
|
170
|
+
error: z.string().optional(),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
export type IndicativeQuoteResponse = z.infer<
|
|
174
|
+
typeof IndicativeQuoteResponseSchema
|
|
175
|
+
>;
|
|
176
|
+
|
|
177
|
+
async getIndicativeQuote(dto: GetIndicativeQuoteDto): Promise<IndicativeQuoteResponse> {
|
|
178
|
+
const sessionId = dto.sessionId || this.generateSessionId()
|
|
179
|
+
|
|
180
|
+
const [fromToken, toToken] = Promise.all([
|
|
181
|
+
this.tokenService.getTokenByTokenId(dto.fromTokenId),
|
|
182
|
+
this.tokenService.getTokenByTokenId(dto.toTokenId),
|
|
183
|
+
])
|
|
184
|
+
|
|
185
|
+
const quote = this.calculateBestQuote()
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
sessionId,
|
|
189
|
+
pmmReceivingAddress,
|
|
190
|
+
indicativeQuote: quote,
|
|
191
|
+
error: '',
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 3.2. Endpoint: `/commitment-quote`
|
|
197
|
+
|
|
198
|
+
#### Description
|
|
199
|
+
|
|
200
|
+
Provides a commitment quote for a specific trade, representing a firm commitment to proceed under the quoted conditions.
|
|
201
|
+
|
|
202
|
+
#### Request Parameters
|
|
203
|
+
|
|
204
|
+
- **HTTP Method**: `GET`
|
|
205
|
+
- **Query Parameters**:
|
|
206
|
+
- `session_id` (string): A unique identifier for the session.
|
|
207
|
+
- `trade_id` (string): The unique identifier for the trade.
|
|
208
|
+
- `from_token_id` (string): The ID of the source token.
|
|
209
|
+
- `to_token_id` (string): The ID of the destination token.
|
|
210
|
+
- `amount` (string): The amount of the source token to be traded, in base 10.
|
|
211
|
+
- `from_user_address` (string): The address of the user initiating the trade.
|
|
212
|
+
- `to_user_address` (string): The address where the user will receive the `to_token`.
|
|
213
|
+
- `user_deposit_tx` (string): The transaction hash where the user deposited their funds.
|
|
214
|
+
- `user_deposit_vault` (string): The vault where the user's deposit is kept.
|
|
215
|
+
- `trade_deadline` (string): The UNIX timestamp (in seconds) by which the user expects to receive payment.
|
|
216
|
+
- `script_deadline` (string): The UNIX timestamp (in seconds) after which the user can withdraw their deposit if not paid.
|
|
217
|
+
|
|
218
|
+
#### Example Request
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
GET /commitment-quote?session_id=12345&trade_id=abcd1234&from_token_id=ETH&to_token_id=BTC&amount=1000000000000000000&from_user_address=0xUserAddress&to_user_address=0xReceivingAddress&user_deposit_tx=0xDepositTxHash&user_deposit_vault=VaultData&trade_deadline=1696012800&script_deadline=1696016400
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### Expected Response
|
|
225
|
+
|
|
226
|
+
- **HTTP Status**: `200 OK`
|
|
227
|
+
- **Response Body** (JSON):
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"trade_id": "abcd1234",
|
|
232
|
+
"commitment_quote": "987654321000000000",
|
|
233
|
+
"error": "" // Empty if no error
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
- `trade_id` (string): The trade ID associated with the request.
|
|
238
|
+
- `commitment_quote` (string): The committed quote value, represented as a string.
|
|
239
|
+
- `error` (string): Error message, if any (empty if no error).
|
|
240
|
+
|
|
241
|
+
#### Example Implementation
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
import { Token, tokenService } from '@optimex-xyz/market-maker-sdk'
|
|
245
|
+
|
|
246
|
+
export const GetCommitmentQuoteSchema = z.object({
|
|
247
|
+
sessionId: z.string(),
|
|
248
|
+
tradeId: z.string(),
|
|
249
|
+
fromTokenId: z.string(),
|
|
250
|
+
toTokenId: z.string(),
|
|
251
|
+
amount: z.string(),
|
|
252
|
+
fromUserAddress: z.string(),
|
|
253
|
+
toUserAddress: z.string(),
|
|
254
|
+
userDepositTx: z.string(),
|
|
255
|
+
userDepositVault: z.string(),
|
|
256
|
+
tradeDeadline: z.string(),
|
|
257
|
+
scriptDeadline: z.string(),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
export class GetCommitmentQuoteDto extends createZodDto(
|
|
261
|
+
GetCommitmentQuoteSchema
|
|
262
|
+
) {}
|
|
263
|
+
|
|
264
|
+
async getCommitmentQuote(dto: GetCommitmentQuoteDto): Promise<CommitmentQuoteResponse> {
|
|
265
|
+
const session = await this.sessionRepo.findById(dto.sessionId)
|
|
266
|
+
|
|
267
|
+
const [fromToken, toToken] = await Promise.all([
|
|
268
|
+
tokenService.getTokenByTokenId(dto.fromTokenId),
|
|
269
|
+
tokenService.getTokenByTokenId(dto.toTokenId),
|
|
270
|
+
])
|
|
271
|
+
|
|
272
|
+
const quote = this.calculateBestQuote(...)
|
|
273
|
+
|
|
274
|
+
await this.tradeService.createTrade({ tradeId: dto.tradeId })
|
|
275
|
+
|
|
276
|
+
await this.tradeService.updateTradeQuote(dto.tradeId, { commitmentQuote: quote })
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
tradeId: dto.tradeId,
|
|
280
|
+
commitmentQuote: quote,
|
|
281
|
+
error: '',
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### 3.3. Endpoint: `/settlement-signature`
|
|
287
|
+
|
|
288
|
+
#### Description
|
|
289
|
+
|
|
290
|
+
Returns a signature from the PMM to confirm the settlement quote, required to finalize the trade.
|
|
291
|
+
|
|
292
|
+
#### Request Parameters
|
|
293
|
+
|
|
294
|
+
- **HTTP Method**: `GET`
|
|
295
|
+
- **Query Parameters**:
|
|
296
|
+
- `trade_id` (string): The unique identifier for the trade.
|
|
297
|
+
- `committed_quote` (string): The committed quote value in base 10.
|
|
298
|
+
- `trade_deadline` (string): The UNIX timestamp (in seconds) by which the user expects to receive payment.
|
|
299
|
+
- `script_deadline` (string): The UNIX timestamp (in seconds) after which the user can withdraw their deposit if not paid.
|
|
300
|
+
|
|
301
|
+
#### Example Request
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
GET /settlement-signature?trade_id=abcd1234&committed_quote=987654321000000000&trade_deadline=1696012800&script_deadline=1696016400
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
#### Expected Response
|
|
308
|
+
|
|
309
|
+
- **HTTP Status**: `200 OK`
|
|
310
|
+
- **Response Body** (JSON):
|
|
311
|
+
|
|
312
|
+
```json
|
|
313
|
+
{
|
|
314
|
+
"trade_id": "abcd1234",
|
|
315
|
+
"signature": "0xSignatureData",
|
|
316
|
+
"deadline": 1696012800,
|
|
317
|
+
"error": "" // Empty if no error
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
- `trade_id` (string): The trade ID associated with the request.
|
|
322
|
+
- `signature` (string): The signature provided by the PMM.
|
|
323
|
+
- `deadline` (integer): The UNIX timestamp (in seconds) indicating the PMM's expected payment deadline.
|
|
324
|
+
- `error` (string): Error message, if any (empty if no error).
|
|
325
|
+
|
|
326
|
+
#### Example Implementation
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import {
|
|
330
|
+
getCommitInfoHash,
|
|
331
|
+
getSignature,
|
|
332
|
+
routerService,
|
|
333
|
+
SignatureType,
|
|
334
|
+
signerService,
|
|
335
|
+
} from '@optimex-xyz/market-maker-sdk'
|
|
336
|
+
|
|
337
|
+
export const GetSettlementSignatureSchema = z.object({
|
|
338
|
+
tradeId: z.string(),
|
|
339
|
+
committedQuote: z.string(),
|
|
340
|
+
tradeDeadline: z.string(),
|
|
341
|
+
scriptDeadline: z.string(),
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
export class GetSettlementSignatureDto extends createZodDto(GetSettlementSignatureSchema) {}
|
|
345
|
+
|
|
346
|
+
async getSettlementSignature(dto: GetSettlementSignatureDto, trade: Trade): Promise<SettlementSignatureResponseDto> {
|
|
347
|
+
try {
|
|
348
|
+
const { tradeId } = trade
|
|
349
|
+
|
|
350
|
+
// Get data directly from l2 contract or using routerService ( wrapper of l2 contract )
|
|
351
|
+
const [presigns, tradeData] = await Promise.all([
|
|
352
|
+
routerService.getPresigns(tradeId),
|
|
353
|
+
routerService.getTradeData(tradeId),
|
|
354
|
+
])
|
|
355
|
+
|
|
356
|
+
const { toChain } = tradeData.tradeInfo
|
|
357
|
+
const deadline = BigInt(Math.floor(Date.now() / 1000) + 1800)
|
|
358
|
+
|
|
359
|
+
const pmmId = ... // hexString
|
|
360
|
+
const pmmPresign = presigns.find((t) => t.pmmId === pmmId)
|
|
361
|
+
if (!pmmPresign) {
|
|
362
|
+
throw new BadRequestException('pmmPresign not found')
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// get amountOut from the committed quote
|
|
366
|
+
const amountOut = BigInt(dto.committedQuote)
|
|
367
|
+
|
|
368
|
+
const commitInfoHash = getCommitInfoHash(
|
|
369
|
+
pmmPresign.pmmId,
|
|
370
|
+
pmmPresign.pmmRecvAddress,
|
|
371
|
+
toChain[1],
|
|
372
|
+
toChain[2],
|
|
373
|
+
amountOut,
|
|
374
|
+
deadline
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
const signerAddress = await this.routerService.getSigner()
|
|
378
|
+
|
|
379
|
+
const domain = await signerService.getDomain()
|
|
380
|
+
|
|
381
|
+
const signature = await getSignature(
|
|
382
|
+
this.pmmWallet,
|
|
383
|
+
this.provider,
|
|
384
|
+
signerAddress,
|
|
385
|
+
tradeId,
|
|
386
|
+
commitInfoHash,
|
|
387
|
+
SignatureType.VerifyingContract,
|
|
388
|
+
domain
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
tradeId: tradeId,
|
|
393
|
+
signature,
|
|
394
|
+
deadline: Number(deadline),
|
|
395
|
+
error: '',
|
|
396
|
+
}
|
|
397
|
+
} catch (error: any) {
|
|
398
|
+
// Handle error
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### 3.4. Endpoint: `/ack-settlement`
|
|
404
|
+
|
|
405
|
+
#### Description
|
|
406
|
+
|
|
407
|
+
Used by the solver to acknowledge to the PMM about a successful settlement, indicating whether the PMM is selected.
|
|
408
|
+
|
|
409
|
+
#### Request Parameters
|
|
410
|
+
|
|
411
|
+
- **HTTP Method**: `POST`
|
|
412
|
+
- **Form Parameters**:
|
|
413
|
+
- `trade_id` (string): The unique identifier for the trade.
|
|
414
|
+
- `trade_deadline` (string): The UNIX timestamp (in seconds) by which the user expects to receive payment.
|
|
415
|
+
- `script_deadline` (string): The UNIX timestamp (in seconds) after which the user can withdraw their deposit if not paid.
|
|
416
|
+
- `chosen` (string): `"true"` if the PMM is selected, `"false"` otherwise.
|
|
417
|
+
|
|
418
|
+
#### Example Request
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
POST /ack-settlement
|
|
422
|
+
Content-Type: application/x-www-form-urlencoded
|
|
423
|
+
|
|
424
|
+
trade_id=abcd1234&trade_deadline=1696012800&script_deadline=1696016400&chosen=true
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### Expected Response
|
|
428
|
+
|
|
429
|
+
- **HTTP Status**: `200 OK`
|
|
430
|
+
- **Response Body** (JSON):
|
|
431
|
+
|
|
432
|
+
```json
|
|
433
|
+
{
|
|
434
|
+
"trade_id": "abcd1234",
|
|
435
|
+
"status": "acknowledged",
|
|
436
|
+
"error": "" // Empty if no error
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
- `trade_id` (string): The trade ID associated with the request.
|
|
441
|
+
- `status` (string): Status of the acknowledgment (always `"acknowledged"`).
|
|
442
|
+
- `error` (string): Error message, if any (empty if no error).
|
|
443
|
+
|
|
444
|
+
#### Example Implementation
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
export const AckSettlementSchema = z.object({
|
|
448
|
+
tradeId: z.string(),
|
|
449
|
+
tradeDeadline: z.string(),
|
|
450
|
+
scriptDeadline: z.string(),
|
|
451
|
+
chosen: z.string().refine((val) => val === 'true' || val === 'false', {
|
|
452
|
+
message: "chosen must be 'true' or 'false'",
|
|
453
|
+
}),
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
export class AckSettlementDto extends createZodDto(AckSettlementSchema) {}
|
|
457
|
+
|
|
458
|
+
async ackSettlement(dto: AckSettlementDto, trade: Trade): Promise<AckSettlementResponseDto> {
|
|
459
|
+
try {
|
|
460
|
+
return {
|
|
461
|
+
tradeId: dto.tradeId,
|
|
462
|
+
status: 'acknowledged',
|
|
463
|
+
error: '',
|
|
464
|
+
}
|
|
465
|
+
} catch (error: any) {
|
|
466
|
+
if (error instanceof HttpException) {
|
|
467
|
+
throw error
|
|
468
|
+
}
|
|
469
|
+
throw new BadRequestException(error.message)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### 3.5. Endpoint: `/signal-payment`
|
|
475
|
+
|
|
476
|
+
#### Description
|
|
477
|
+
|
|
478
|
+
Used by the solver to signal the chosen PMM to start submitting their payment.
|
|
479
|
+
|
|
480
|
+
#### Request Parameters
|
|
481
|
+
|
|
482
|
+
- **HTTP Method**: `POST`
|
|
483
|
+
- **Form Parameters**:
|
|
484
|
+
- `trade_id` (string): The unique identifier for the trade.
|
|
485
|
+
- `protocol_fee_amount` (string): The amount of protocol fee the PMM has to submit, in base 10.
|
|
486
|
+
- `trade_deadline` (string): The UNIX timestamp (in seconds) by which the user expects to receive payment.
|
|
487
|
+
- `script_deadline` (string): The UNIX timestamp (in seconds) after which the user can withdraw their deposit if not paid.
|
|
488
|
+
|
|
489
|
+
#### Example Request
|
|
490
|
+
|
|
491
|
+
```
|
|
492
|
+
POST /signal-payment
|
|
493
|
+
Content-Type: application/x-www-form-urlencoded
|
|
494
|
+
|
|
495
|
+
trade_id=abcd1234&protocol_fee_amount=1000000000000000&trade_deadline=1696012800&script_deadline=1696016400
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
#### Expected Response
|
|
499
|
+
|
|
500
|
+
- **HTTP Status**: `200 OK`
|
|
501
|
+
- **Response Body** (JSON):
|
|
502
|
+
|
|
503
|
+
```json
|
|
504
|
+
{
|
|
505
|
+
"trade_id": "abcd1234",
|
|
506
|
+
"status": "acknowledged",
|
|
507
|
+
"error": "" // Empty if no error
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
- `trade_id` (string): The trade ID associated with the request.
|
|
512
|
+
- `status` (string): Status of the acknowledgment (always `"acknowledged"`).
|
|
513
|
+
- `error` (string): Error message, if any (empty if no error).
|
|
514
|
+
|
|
515
|
+
#### Example Implementation
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
export const SignalPaymentSchema = z.object({
|
|
519
|
+
tradeId: z.string(),
|
|
520
|
+
protocolFeeAmount: z.string(),
|
|
521
|
+
tradeDeadline: z.string(),
|
|
522
|
+
scriptDeadline: z.string(),
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
export class SignalPaymentDto extends createZodDto(SignalPaymentSchema) {}
|
|
526
|
+
|
|
527
|
+
async signalPayment(dto: SignalPaymentDto, trade: Trade): Promise<SignalPaymentResponseDto> {
|
|
528
|
+
try {
|
|
529
|
+
// enqueue transfer with dto and trade
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
tradeId: dto.tradeId,
|
|
533
|
+
status: 'acknowledged',
|
|
534
|
+
error: '',
|
|
535
|
+
}
|
|
536
|
+
} catch (error: any) {
|
|
537
|
+
if (error instanceof HttpException) {
|
|
538
|
+
throw error
|
|
539
|
+
}
|
|
540
|
+
throw new BadRequestException(error.message)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
## 4. SDK Functions for PMMs
|
|
546
|
+
|
|
547
|
+
These SDK functions facilitate PMM-Solver communication and are essential for implementing the required backend APIs.
|
|
548
|
+
|
|
549
|
+
### 4.1. Function: getTokens
|
|
550
|
+
|
|
551
|
+
#### Description
|
|
552
|
+
|
|
553
|
+
Returns a list of all supported tokens across different networks.
|
|
554
|
+
|
|
555
|
+
#### Example Code
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
import { tokenService } from '@optimex-xyz/market-maker-sdk'
|
|
559
|
+
|
|
560
|
+
tokenService.getTokens()
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### 4.2. Function: submitSettlementTx
|
|
564
|
+
|
|
565
|
+
#### Description
|
|
566
|
+
|
|
567
|
+
Allows the PMM to submit the settlement transaction hash for one or more trades. This step is necessary to complete the trade settlement process.
|
|
568
|
+
|
|
569
|
+
Parameters:
|
|
570
|
+
- `trade_ids` (array of strings): An array of trade IDs associated with the settlement transaction.
|
|
571
|
+
- `pmm_id` (string): The PMM's ID, which must match the one committed for the trade(s).
|
|
572
|
+
- `settlement_tx` (string): The raw transaction data (in hex) representing the settlement.
|
|
573
|
+
- `signature` (string): The PMM's signature on the settlement transaction.
|
|
574
|
+
- `start_index` (integer): The index indicating the starting point for settlement processing (used for batch settlements).
|
|
575
|
+
- `signed_at` (integer): The UNIX timestamp (in seconds) when the PMM signed the settlement transaction.
|
|
576
|
+
|
|
577
|
+
#### Example Implementation
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
import {
|
|
581
|
+
getMakePaymentHash,
|
|
582
|
+
getSignature,
|
|
583
|
+
routerService,
|
|
584
|
+
SignatureType,
|
|
585
|
+
signerService,
|
|
586
|
+
solverService,
|
|
587
|
+
} from '@optimex-xyz/market-maker-sdk'
|
|
588
|
+
|
|
589
|
+
async submit(job: Job<string>) {
|
|
590
|
+
const { tradeId, paymentTxId } = toObject(job.data) as SubmitSettlementEvent
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
const tradeIds: BytesLike[] = [tradeId]
|
|
594
|
+
const startIdx = BigInt(tradeIds.indexOf(tradeId))
|
|
595
|
+
|
|
596
|
+
const signerAddress = await this.routerService.getSigner()
|
|
597
|
+
|
|
598
|
+
const signedAt = Math.floor(Date.now() / 1000)
|
|
599
|
+
|
|
600
|
+
const makePaymentInfoHash = getMakePaymentHash(tradeIds, BigInt(signedAt), startIdx, ensureHexPrefix(paymentTxId))
|
|
601
|
+
|
|
602
|
+
const domain = await signerService.getDomain()
|
|
603
|
+
|
|
604
|
+
const signature = await getSignature(
|
|
605
|
+
this.pmmWallet,
|
|
606
|
+
this.provider,
|
|
607
|
+
signerAddress,
|
|
608
|
+
tradeId,
|
|
609
|
+
makePaymentInfoHash,
|
|
610
|
+
SignatureType.MakePayment,
|
|
611
|
+
domain
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
const pmmId = ... // string
|
|
615
|
+
const requestPayload = {
|
|
616
|
+
tradeIds: [tradeId],
|
|
617
|
+
pmmId: pmmId,
|
|
618
|
+
settlementTx: ensureHexPrefix(paymentTxId),
|
|
619
|
+
signature: signature,
|
|
620
|
+
startIndex: 0,
|
|
621
|
+
signedAt: signedAt,
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const response = await this.solverSerivce.submitSettlementTx(requestPayload)
|
|
625
|
+
|
|
626
|
+
return response
|
|
627
|
+
} catch (error: any) {
|
|
628
|
+
this.logger.error('submit settlement error', error.stack)
|
|
629
|
+
|
|
630
|
+
throw error // Re-throw the error for the queue to handle
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
#### Notes
|
|
636
|
+
|
|
637
|
+
- **Trade IDs**: Provide all trade IDs included in the settlement transaction.
|
|
638
|
+
- **Start Index**: Used when submitting a batch of settlements to indicate the position within the batch.
|
|
639
|
+
- **Signature**: Must be valid and verifiable by the solver backend.
|
|
640
|
+
|
|
641
|
+
## 5. PMM Making Payment
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
import { Token } from '@optimex-xyz/market-maker-sdk'
|
|
645
|
+
|
|
646
|
+
export interface TransferParams {
|
|
647
|
+
toAddress: string
|
|
648
|
+
amount: bigint
|
|
649
|
+
token: Token
|
|
650
|
+
tradeId: string
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export interface ITransferStrategy {
|
|
654
|
+
transfer(params: TransferParams): Promise<string>
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### 5.1. EVM
|
|
659
|
+
|
|
660
|
+
In case the target chain is EVM-based, the transaction should emit the event from the `l1 payment contract` with the correct values for pmmAmountOut and protocolFee.
|
|
661
|
+
|
|
662
|
+
Example implementation:
|
|
663
|
+
|
|
664
|
+
```ts
|
|
665
|
+
import { config, ensureHexPrefix, ERC20__factory, Payment__factory, routerService } from '@optimex-xyz/market-maker-sdk'
|
|
666
|
+
|
|
667
|
+
import { ITransferStrategy, TransferParams } from '../interfaces/transfer-strategy.interface'
|
|
668
|
+
|
|
669
|
+
@Injectable()
|
|
670
|
+
export class EVMTransferStrategy implements ITransferStrategy {
|
|
671
|
+
private pmmPrivateKey: string
|
|
672
|
+
|
|
673
|
+
private routerService = routerService
|
|
674
|
+
private readonly rpcMap = new Map<string, string>([['ethereum_sepolia', 'https://eth-sepolia.public.blastapi.io']])
|
|
675
|
+
|
|
676
|
+
constructor(private configService: ConfigService) {
|
|
677
|
+
this.pmmPrivateKey = this.configService.getOrThrow<string>('PMM_EVM_PRIVATE_KEY')
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async transfer(params: TransferParams): Promise<string> {
|
|
681
|
+
const { toAddress, amount, token, tradeId } = params
|
|
682
|
+
const { tokenAddress, networkId } = token
|
|
683
|
+
|
|
684
|
+
const signer = this.getSigner(networkId)
|
|
685
|
+
|
|
686
|
+
const paymentAddress = this.getPaymentAddress(networkId)
|
|
687
|
+
|
|
688
|
+
if (tokenAddress !== 'native') {
|
|
689
|
+
// allowance with ERC20
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const paymentContract = Payment__factory.connect(paymentAddress, signer)
|
|
693
|
+
|
|
694
|
+
const feeDetail = await this.routerService.getFeeDetails(tradeId)
|
|
695
|
+
|
|
696
|
+
const deadline = BigInt(Math.floor(Date.now() / 1000) + 30 * 60)
|
|
697
|
+
|
|
698
|
+
const decoder = errorDecoder()
|
|
699
|
+
|
|
700
|
+
const tx = await paymentContract.payment(
|
|
701
|
+
tradeId,
|
|
702
|
+
tokenAddress === 'native' ? ZeroAddress : tokenAddress,
|
|
703
|
+
toAddress,
|
|
704
|
+
amount,
|
|
705
|
+
feeDetail.totalAmount,
|
|
706
|
+
deadline,
|
|
707
|
+
{
|
|
708
|
+
value: tokenAddress === 'native' ? amount : 0n,
|
|
709
|
+
}
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
this.logger.log(`Transfer transaction sent: ${tx.hash}`)
|
|
713
|
+
|
|
714
|
+
return ensureHexPrefix(tx.hash)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private getSigner(networkId: string) {
|
|
718
|
+
const rpcUrl = this.rpcMap.get(networkId)
|
|
719
|
+
|
|
720
|
+
if (!rpcUrl) {
|
|
721
|
+
throw new Error(`Unsupported networkId: ${networkId}`)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl)
|
|
725
|
+
return new ethers.Wallet(this.pmmPrivateKey, provider)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private getPaymentAddress(networkId: string) {
|
|
729
|
+
const paymentAddress = config.getPaymentAddress(networkId)
|
|
730
|
+
if (!paymentAddress) {
|
|
731
|
+
throw new Error(`Unsupported networkId: ${networkId}`)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return paymentAddress
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### 5.2. Bitcoin
|
|
740
|
+
|
|
741
|
+
In case the target chain is Bitcoin, the transaction should have at least N + 1 outputs, with the first N outputs being the settlement UTXOs for trades, and one of them being the change UTXO for the user with the correct amount. The output N + 1 is the OP_RETURN output with the hash of tradeIds.
|
|
742
|
+
|
|
743
|
+
Example implementation:
|
|
744
|
+
|
|
745
|
+
```ts
|
|
746
|
+
import * as bitcoin from 'bitcoinjs-lib'
|
|
747
|
+
import { ECPairFactory } from 'ecpair'
|
|
748
|
+
import * as ecc from 'tiny-secp256k1'
|
|
749
|
+
|
|
750
|
+
import { getTradeIdsHash, Token } from '@optimex-xyz/market-maker-sdk'
|
|
751
|
+
|
|
752
|
+
import { ITransferStrategy, TransferParams } from '../interfaces/transfer-strategy.interface'
|
|
753
|
+
|
|
754
|
+
interface UTXO {
|
|
755
|
+
txid: string
|
|
756
|
+
vout: number
|
|
757
|
+
value: number
|
|
758
|
+
status: {
|
|
759
|
+
confirmed: boolean
|
|
760
|
+
block_height: number
|
|
761
|
+
block_hash: string
|
|
762
|
+
block_time: number
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
@Injectable()
|
|
767
|
+
export class BTCTransferStrategy implements ITransferStrategy {
|
|
768
|
+
private readonly privateKey: string
|
|
769
|
+
private readonly ECPair = ECPairFactory(ecc)
|
|
770
|
+
|
|
771
|
+
private readonly networkMap = new Map<string, bitcoin.Network>([
|
|
772
|
+
['bitcoin_testnet', bitcoin.networks.testnet],
|
|
773
|
+
['bitcoin', bitcoin.networks.bitcoin],
|
|
774
|
+
])
|
|
775
|
+
|
|
776
|
+
private readonly rpcMap = new Map<string, string>([
|
|
777
|
+
['bitcoin_testnet', 'https://blockstream.info/testnet'],
|
|
778
|
+
['bitcoin', 'https://blockstream.info'],
|
|
779
|
+
])
|
|
780
|
+
|
|
781
|
+
constructor(private configService: ConfigService) {
|
|
782
|
+
this.privateKey = this.configService.getOrThrow<string>('PMM_BTC_PRIVATE_KEY')
|
|
783
|
+
bitcoin.initEccLib(ecc)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async transfer(params: TransferParams): Promise<string> {
|
|
787
|
+
const { toAddress, amount, token, tradeId } = params
|
|
788
|
+
|
|
789
|
+
const network = this.getNetwork(token.networkId)
|
|
790
|
+
const rpcUrl = this.getRpcUrl(token.networkId)
|
|
791
|
+
|
|
792
|
+
const txId = await this.sendBTC(this.privateKey, toAddress, amount, network, rpcUrl, token, [tradeId])
|
|
793
|
+
|
|
794
|
+
return ensureHexPrefix(txId)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private createPayment(publicKey: Uint8Array, network: bitcoin.Network) {
|
|
798
|
+
const p2tr = bitcoin.payments.p2tr({
|
|
799
|
+
internalPubkey: Buffer.from(publicKey.slice(1, 33)),
|
|
800
|
+
network,
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
return {
|
|
804
|
+
payment: p2tr,
|
|
805
|
+
keypair: this.ECPair.fromWIF(this.privateKey, network),
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
private async sendBTC(
|
|
810
|
+
privateKey: string,
|
|
811
|
+
toAddress: string,
|
|
812
|
+
amountInSatoshis: bigint,
|
|
813
|
+
network: bitcoin.Network,
|
|
814
|
+
rpcUrl: string,
|
|
815
|
+
token: Token,
|
|
816
|
+
tradeIds: string[]
|
|
817
|
+
): Promise<string> {
|
|
818
|
+
const keyPair = this.ECPair.fromWIF(privateKey, network)
|
|
819
|
+
const { payment, keypair } = this.createPayment(keyPair.publicKey, network)
|
|
820
|
+
|
|
821
|
+
if (!payment.address) {
|
|
822
|
+
throw new Error('Could not generate address')
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const utxos = await this.getUTXOs(payment.address, rpcUrl)
|
|
826
|
+
if (utxos.length === 0) {
|
|
827
|
+
throw new Error(`No UTXOs found in ${token.networkSymbol} wallet`)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const psbt = new bitcoin.Psbt({ network })
|
|
831
|
+
let totalInput = 0n
|
|
832
|
+
|
|
833
|
+
for (const utxo of utxos) {
|
|
834
|
+
if (!payment.output) {
|
|
835
|
+
throw new Error('Could not generate output script')
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const internalKey = Buffer.from(keypair.publicKey.slice(1, 33))
|
|
839
|
+
|
|
840
|
+
psbt.addInput({
|
|
841
|
+
hash: utxo.txid,
|
|
842
|
+
index: utxo.vout,
|
|
843
|
+
witnessUtxo: {
|
|
844
|
+
script: payment.output,
|
|
845
|
+
value: BigInt(utxo.value),
|
|
846
|
+
},
|
|
847
|
+
tapInternalKey: internalKey,
|
|
848
|
+
})
|
|
849
|
+
|
|
850
|
+
totalInput += BigInt(utxo.value)
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
this.logger.log(`Total input: ${totalInput.toString()} ${token.tokenSymbol}`)
|
|
854
|
+
|
|
855
|
+
if (totalInput < amountInSatoshis) {
|
|
856
|
+
throw new Error(
|
|
857
|
+
`Insufficient balance in ${token.networkSymbol} wallet. ` +
|
|
858
|
+
`Need ${amountInSatoshis} satoshis, but only have ${totalInput} satoshis`
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const feeRate = await this.getFeeRate(rpcUrl)
|
|
863
|
+
const fee = BigInt(Math.ceil(200 * feeRate))
|
|
864
|
+
const changeAmount = totalInput - amountInSatoshis - fee
|
|
865
|
+
|
|
866
|
+
psbt.addOutput({
|
|
867
|
+
address: toAddress,
|
|
868
|
+
value: amountInSatoshis,
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
if (changeAmount > 546n) {
|
|
872
|
+
psbt.addOutput({
|
|
873
|
+
address: payment.address,
|
|
874
|
+
value: changeAmount,
|
|
875
|
+
})
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const tradeIdsHash = getTradeIdsHash(tradeIds)
|
|
879
|
+
|
|
880
|
+
// Add OP_RETURN output with tradeIds hash
|
|
881
|
+
psbt.addOutput({
|
|
882
|
+
script: bitcoin.script.compile([bitcoin.opcodes['OP_RETURN'], Buffer.from(tradeIdsHash.slice(2), 'hex')]),
|
|
883
|
+
value: 0n,
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
const toXOnly = (pubKey: Uint8Array) => (pubKey.length === 32 ? pubKey : pubKey.slice(1, 33))
|
|
887
|
+
const tweakedSigner = keyPair.tweak(bitcoin.crypto.taggedHash('TapTweak', toXOnly(keyPair.publicKey)))
|
|
888
|
+
|
|
889
|
+
for (let i = 0; i < psbt.data.inputs.length; i++) {
|
|
890
|
+
psbt.signInput(i, tweakedSigner, [bitcoin.Transaction.SIGHASH_DEFAULT])
|
|
891
|
+
this.logger.log(`Input ${i} signed successfully`)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
psbt.finalizeAllInputs()
|
|
895
|
+
|
|
896
|
+
const tx = psbt.extractTransaction()
|
|
897
|
+
const rawTx = tx.toHex()
|
|
898
|
+
|
|
899
|
+
const response = await axios.post(`${rpcUrl}/api/tx`, rawTx, {
|
|
900
|
+
headers: {
|
|
901
|
+
'Content-Type': 'text/plain',
|
|
902
|
+
},
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
return response.data
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private async getUTXOs(address: string, rpcUrl: string): Promise<UTXO[]> {
|
|
909
|
+
const response = await axios.get<UTXO[]>(`${rpcUrl}/api/address/${address}/utxo`)
|
|
910
|
+
return response.data
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private async getFeeRate(rpcUrl: string): Promise<number> {
|
|
914
|
+
try {
|
|
915
|
+
const response = await axios.get<{ [key: string]: number }>(`${rpcUrl}/api/fee-estimates`)
|
|
916
|
+
return response.data[0]
|
|
917
|
+
} catch (error) {
|
|
918
|
+
console.error(`Error fetching
|