Last active
December 7, 2024 06:40
-
-
Save Phathdt/5ea98e6d26731100cbdbb40196f61ecb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import * as crypto from 'crypto'; | |
| import { ethers } from 'ethers'; | |
| import { Token, TokenPrice, TokenRepository } from '@bitfi-mock-pmm/token'; | |
| import { TradeService } from '@bitfi-mock-pmm/trade'; | |
| import { BadRequestException, HttpException, Injectable } from '@nestjs/common'; | |
| import { ConfigService } from '@nestjs/config'; | |
| import { QuoteSessionRepository } from './quote-session.repository'; | |
| import { | |
| CommitmentQuoteResponse, GetCommitmentQuoteDto, GetIndicativeQuoteDto, IndicativeQuoteResponse | |
| } from './quote.dto'; | |
| @Injectable() | |
| export class QuoteService { | |
| private readonly EVM_ADDRESS: string; | |
| private readonly BTC_ADDRESS: string; | |
| constructor( | |
| private readonly configService: ConfigService, | |
| private readonly tokenRepo: TokenRepository, | |
| private readonly tradeService: TradeService, | |
| private readonly sessionRepo: QuoteSessionRepository | |
| ) { | |
| this.EVM_ADDRESS = this.configService.getOrThrow<string>('PMM_EVM_ADDRESS'); | |
| this.BTC_ADDRESS = this.configService.getOrThrow<string>('PMM_BTC_ADDRESS'); | |
| } | |
| private getPmmAddressByNetworkType(token: Token): string { | |
| switch (token.networkType.toUpperCase()) { | |
| case 'EVM': | |
| return this.EVM_ADDRESS; | |
| case 'BTC': | |
| case 'TBTC': | |
| return this.BTC_ADDRESS; | |
| default: | |
| throw new BadRequestException( | |
| `Unsupported network type: ${token.networkType}` | |
| ); | |
| } | |
| } | |
| private generateSessionId(): string { | |
| return crypto.randomBytes(16).toString('hex'); | |
| } | |
| private calculateBestQuote( | |
| amountIn: string, | |
| fromToken: Token, | |
| toToken: Token, | |
| fromTokenPrice: TokenPrice, | |
| toTokenPrice: TokenPrice | |
| ): string { | |
| const amount = ethers.getBigInt(amountIn); | |
| const fromDecimals = ethers.getBigInt(fromToken.tokenDecimals); | |
| const toDecimals = ethers.getBigInt(toToken.tokenDecimals); | |
| const fromPrice = ethers.getBigInt( | |
| Math.round(fromTokenPrice.currentPrice * 1e6) | |
| ); | |
| const toPrice = ethers.getBigInt( | |
| Math.round(toTokenPrice.currentPrice * 1e6) | |
| ); | |
| const rawQuote = | |
| (amount * fromPrice * 10n ** toDecimals) / | |
| (toPrice * 10n ** fromDecimals); | |
| const quoteWithBonus = (rawQuote * 110n) / 100n; | |
| return quoteWithBonus.toString(); | |
| } | |
| async getIndicativeQuote( | |
| dto: GetIndicativeQuoteDto | |
| ): Promise<IndicativeQuoteResponse> { | |
| const sessionId = dto.sessionId || this.generateSessionId(); | |
| try { | |
| const [fromToken, toToken] = await Promise.all([ | |
| this.tokenRepo.getTokenByTokenId(dto.fromTokenId), | |
| this.tokenRepo.getTokenByTokenId(dto.toTokenId), | |
| ]).catch((error) => { | |
| throw new BadRequestException( | |
| `Failed to fetch tokens: ${error.message}` | |
| ); | |
| }); | |
| const [fromTokenPrice, toTokenPrice] = await Promise.all([ | |
| this.tokenRepo.getTokenPrice(fromToken.tokenSymbol), | |
| this.tokenRepo.getTokenPrice(toToken.tokenSymbol), | |
| ]).catch((error) => { | |
| throw new BadRequestException( | |
| `Failed to fetch token prices: ${error.message}` | |
| ); | |
| }); | |
| const quote = this.calculateBestQuote( | |
| dto.amount, | |
| fromToken, | |
| toToken, | |
| fromTokenPrice, | |
| toTokenPrice | |
| ); | |
| const pmmAddress = this.getPmmAddressByNetworkType(fromToken); | |
| await this.sessionRepo.save(sessionId, { | |
| fromToken: dto.fromTokenId, | |
| toToken: dto.toTokenId, | |
| amount: dto.amount, | |
| pmmReceivingAddress: pmmAddress, | |
| indicativeQuote: quote, | |
| }); | |
| return { | |
| sessionId, | |
| pmmReceivingAddress: pmmAddress, | |
| indicativeQuote: quote, | |
| error: '', | |
| }; | |
| } catch (error: any) { | |
| if (error instanceof HttpException) { | |
| throw error; | |
| } | |
| throw new BadRequestException(error.message); | |
| } | |
| } | |
| async getCommitmentQuote( | |
| dto: GetCommitmentQuoteDto | |
| ): Promise<CommitmentQuoteResponse> { | |
| try { | |
| await this.sessionRepo.validate( | |
| dto.sessionId, | |
| dto.fromTokenId, | |
| dto.toTokenId, | |
| dto.amount | |
| ); | |
| const session = await this.sessionRepo.findById(dto.sessionId); | |
| if (!session) { | |
| throw new BadRequestException('Session expired during processing'); | |
| } | |
| const [fromToken, toToken] = await Promise.all([ | |
| this.tokenRepo.getTokenByTokenId(dto.fromTokenId), | |
| this.tokenRepo.getTokenByTokenId(dto.toTokenId), | |
| ]).catch((error) => { | |
| throw new BadRequestException( | |
| `Failed to fetch tokens: ${error.message}` | |
| ); | |
| }); | |
| const [fromTokenPrice, toTokenPrice] = await Promise.all([ | |
| this.tokenRepo.getTokenPrice(fromToken.tokenSymbol), | |
| this.tokenRepo.getTokenPrice(toToken.tokenSymbol), | |
| ]).catch((error) => { | |
| throw new BadRequestException( | |
| dto.tradeId, | |
| `Failed to fetch token prices: ${error.message}` | |
| ); | |
| }); | |
| await this.tradeService.deleteTrade(dto.tradeId); | |
| const quote = this.calculateBestQuote( | |
| dto.amount, | |
| fromToken, | |
| toToken, | |
| fromTokenPrice, | |
| toTokenPrice | |
| ); | |
| const trade = await this.tradeService | |
| .createTrade({ | |
| tradeId: dto.tradeId, | |
| fromTokenId: dto.fromTokenId, | |
| toTokenId: dto.toTokenId, | |
| fromUser: dto.fromUserAddress, | |
| toUser: dto.toUserAddress, | |
| amount: dto.amount, | |
| fromNetworkId: fromToken.networkId, | |
| toNetworkId: toToken.networkId, | |
| userDepositTx: dto.userDepositTx, | |
| userDepositVault: dto.userDepositVault, | |
| tradeDeadline: dto.tradeDeadline, | |
| scriptDeadline: dto.scriptDeadline, | |
| }) | |
| .catch((error) => { | |
| throw new BadRequestException( | |
| `Failed to create trade: ${error.message}` | |
| ); | |
| }); | |
| await this.tradeService | |
| .updateTradeQuote(trade.tradeId, { | |
| commitmentQuote: quote, | |
| }) | |
| .catch((error) => { | |
| throw new BadRequestException( | |
| `Failed to update trade quote: ${error.message}` | |
| ); | |
| }); | |
| return { | |
| tradeId: dto.tradeId, | |
| commitmentQuote: quote, | |
| error: '', | |
| }; | |
| } catch (error: any) { | |
| if (error instanceof HttpException) { | |
| throw error; | |
| } | |
| throw new BadRequestException(error.message); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Queue } from 'bull'; | |
| import * as ethers from 'ethers'; | |
| import { stringToHex, toString } from '@bitfi-mock-pmm/shared'; | |
| import { TradeService } from '@bitfi-mock-pmm/trade'; | |
| import { Router, Router__factory } from '@bitfi-mock-pmm/typechains'; | |
| import { InjectQueue } from '@nestjs/bull'; | |
| import { BadRequestException, HttpException, Injectable } from '@nestjs/common'; | |
| import { ConfigService } from '@nestjs/config'; | |
| import { Trade, TradeStatus } from '@prisma/client'; | |
| import { | |
| AckSettlementDto, AckSettlementResponseDto, GetSettlementSignatureDto, | |
| SettlementSignatureResponseDto, SignalPaymentDto, SignalPaymentResponseDto | |
| } from './settlement.dto'; | |
| import { getCommitInfoHash } from './signatures/getInfoHash'; | |
| import getSignature, { SignatureType } from './signatures/getSignature'; | |
| import { TRANSFER_SETTLEMENT_QUEUE, TransferSettlementEvent } from './types'; | |
| @Injectable() | |
| export class SettlementService { | |
| private readonly pmmWallet: ethers.Wallet; | |
| private contract: Router; | |
| private provider: ethers.JsonRpcProvider; | |
| private pmmId: string; | |
| constructor( | |
| private readonly configService: ConfigService, | |
| private readonly tradeService: TradeService, | |
| @InjectQueue(TRANSFER_SETTLEMENT_QUEUE) | |
| private transferSettlementQueue: Queue | |
| ) { | |
| const rpcUrl = this.configService.getOrThrow<string>('RPC_URL'); | |
| const pmmPrivateKey = | |
| this.configService.getOrThrow<string>('PMM_PRIVATE_KEY'); | |
| const contractAddress = | |
| this.configService.getOrThrow<string>('ROUTER_ADDRESS'); | |
| this.pmmId = stringToHex(this.configService.getOrThrow<string>('PMM_ID')); | |
| console.log('π ~ SettlementService ~ pmmId:', this.pmmId); | |
| this.provider = new ethers.JsonRpcProvider(rpcUrl); | |
| this.pmmWallet = new ethers.Wallet(pmmPrivateKey, this.provider); | |
| this.contract = Router__factory.connect(contractAddress, this.pmmWallet); | |
| } | |
| async getSettlementSignature( | |
| dto: GetSettlementSignatureDto, | |
| trade: Trade | |
| ): Promise<SettlementSignatureResponseDto> { | |
| try { | |
| const { tradeId } = trade; | |
| const [presigns, tradeData] = await Promise.all([ | |
| this.contract.getPresigns(tradeId), | |
| this.contract.getTradeData(tradeId), | |
| ]); | |
| const { toChain } = tradeData.tradeInfo; | |
| const scriptTimeout = BigInt(Math.floor(Date.now() / 1000) + 1800); | |
| const pmmPresign = presigns.find((t) => t.pmmId === this.pmmId); | |
| if (!pmmPresign) { | |
| throw new BadRequestException('pmmPresign not found'); | |
| } | |
| console.log('π ~ pmmPresign.pmmId:', pmmPresign.pmmId); | |
| console.log('π ~ pmmPresign.pmmRecvAddress:', pmmPresign.pmmRecvAddress); | |
| console.log('π ~ pmmPresign.toChain[1]:', toChain[1]); | |
| console.log('π ~ pmmPresign.toChain[2]:', toChain[2]); | |
| console.log('π ~ dto.committedQuote:', dto.committedQuote); | |
| console.log('π ~ scriptTimeout:', scriptTimeout); | |
| const commitInfoHash = getCommitInfoHash( | |
| pmmPresign.pmmId, | |
| pmmPresign.pmmRecvAddress, | |
| toChain[1], | |
| toChain[2], | |
| BigInt(dto.committedQuote), | |
| scriptTimeout | |
| ); | |
| console.log('π ~ SettlementService ~ commitInfoHash:', commitInfoHash); | |
| const signerAddress = await this.contract.SIGNER(); | |
| console.log('π ~ SettlementService ~ signerAddress:', signerAddress); | |
| const signature = await getSignature( | |
| this.pmmWallet, | |
| this.provider, | |
| signerAddress, | |
| tradeId, | |
| commitInfoHash, | |
| SignatureType.VerifyingContract | |
| ); | |
| console.log('π ~ SettlementService ~ tradeId:', tradeId); | |
| console.log('π ~ SettlementService ~ signature:', signature); | |
| await this.tradeService.updateTradeStatus(tradeId, TradeStatus.COMMITTED); | |
| return { | |
| tradeId: tradeId, | |
| signature, | |
| deadline: parseInt(dto.tradeDeadline), | |
| error: '', | |
| }; | |
| } catch (error: any) { | |
| if (error instanceof HttpException) { | |
| throw error; | |
| } | |
| throw new BadRequestException(error.message); | |
| } | |
| } | |
| async ackSettlement( | |
| dto: AckSettlementDto, | |
| trade: Trade | |
| ): Promise<AckSettlementResponseDto> { | |
| try { | |
| if (trade.status !== TradeStatus.SETTLING) { | |
| throw new BadRequestException(`Invalid trade status: ${trade.status}`); | |
| } | |
| // Update trade status based on chosen status | |
| const newStatus = | |
| dto.chosen === 'true' ? TradeStatus.SETTLING : TradeStatus.FAILED; | |
| await this.tradeService.updateTradeStatus( | |
| dto.tradeId, | |
| newStatus, | |
| dto.chosen === 'false' ? 'PMM not chosen for settlement' : undefined | |
| ); | |
| return { | |
| tradeId: dto.tradeId, | |
| status: 'acknowledged', | |
| error: '', | |
| }; | |
| } catch (error: any) { | |
| if (error instanceof HttpException) { | |
| throw error; | |
| } | |
| throw new BadRequestException(error.message); | |
| } | |
| } | |
| async signalPayment( | |
| dto: SignalPaymentDto, | |
| trade: Trade | |
| ): Promise<SignalPaymentResponseDto> { | |
| try { | |
| if (trade.status !== TradeStatus.SETTLING) { | |
| throw new BadRequestException(`Invalid trade status: ${trade.status}`); | |
| } | |
| const eventData = { | |
| tradeId: dto.tradeId, | |
| } as TransferSettlementEvent; | |
| await this.transferSettlementQueue.add('transfer', toString(eventData)); | |
| // You might want to store the protocol fee amount or handle it in your business logic | |
| await this.tradeService.updateTradeStatus( | |
| dto.tradeId, | |
| TradeStatus.SETTLING | |
| ); | |
| return { | |
| tradeId: dto.tradeId, | |
| status: 'acknowledged', | |
| error: '', | |
| }; | |
| } catch (error: any) { | |
| if (error instanceof HttpException) { | |
| throw error; | |
| } | |
| throw new BadRequestException(error.message); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment