@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 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