Skip to content

Instantly share code, notes, and snippets.

@Phathdt
Last active December 7, 2024 06:40
Show Gist options
  • Select an option

  • Save Phathdt/5ea98e6d26731100cbdbb40196f61ecb to your computer and use it in GitHub Desktop.

Select an option

Save Phathdt/5ea98e6d26731100cbdbb40196f61ecb to your computer and use it in GitHub Desktop.
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);
}
}
}
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