diff --git a/.github/workflows/api-dev.yaml b/.github/workflows/api-dev.yaml index 863f9b9273..620ca44adc 100644 --- a/.github/workflows/api-dev.yaml +++ b/.github/workflows/api-dev.yaml @@ -51,6 +51,9 @@ jobs: run: npm audit --audit-level=high continue-on-error: true + - name: Write version file + run: echo "${{ github.sha }}" > dist/version.txt + - name: Deploy to Azure App Service (DEV) uses: azure/webapps-deploy@v3 with: diff --git a/.github/workflows/api-pr.yaml b/.github/workflows/api-pr.yaml index fedaa8712e..dbbfcbdc78 100644 --- a/.github/workflows/api-pr.yaml +++ b/.github/workflows/api-pr.yaml @@ -12,8 +12,39 @@ permissions: env: NODE_VERSION: '20.x' + DEV_API_URL: https://dev.api.dfx.swiss jobs: + verify-dev-deployment: + name: Verify DEV Deployment + if: github.base_ref == 'main' && github.head_ref == 'develop' + runs-on: ubuntu-latest + steps: + - name: Wait for DEV deployment + timeout-minutes: 15 + run: | + EXPECTED=$(curl -s https://api.github.com/repos/${{ github.repository }}/commits/develop | jq -r '.sha') + echo "Expected commit: $EXPECTED" + + for i in {1..30}; do + if RESPONSE=$(curl -sf ${{ env.DEV_API_URL }}/version 2>&1); then + ACTUAL=$(echo "$RESPONSE" | jq -r '.commit') + echo "Attempt $i: DEV running $ACTUAL" + + if [ "$EXPECTED" == "$ACTUAL" ]; then + echo "DEV is running the latest develop commit" + exit 0 + fi + else + echo "Attempt $i: DEV API not reachable" + fi + + sleep 30 + done + + echo "::error::DEV is not running the latest develop commit after 15 minutes" + exit 1 + build: name: Build and test if: github.head_ref != 'develop' diff --git a/.github/workflows/api-prd.yaml b/.github/workflows/api-prd.yaml index b65407a89e..34691522ab 100644 --- a/.github/workflows/api-prd.yaml +++ b/.github/workflows/api-prd.yaml @@ -51,6 +51,9 @@ jobs: run: npm audit --audit-level=high continue-on-error: true + - name: Write version file + run: echo "${{ github.sha }}" > dist/version.txt + - name: Deploy to Azure App Service (PRD) uses: azure/webapps-deploy@v3 with: diff --git a/src/app.controller.ts b/src/app.controller.ts index cdf58cb849..557bb0ef4f 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Param, Query, Redirect, Req, Res, VERSION_NEUTRAL, Ver import { ApiExcludeEndpoint } from '@nestjs/swagger'; import { Request, Response } from 'express'; import { UserAgent } from 'express-useragent'; +import { readFileSync } from 'fs'; import { RealIP } from 'nestjs-real-ip'; import { Config } from './config/config'; import { AdDto, AdSettings, AdvertisementDto } from './shared/dto/advertisement.dto'; @@ -27,6 +28,7 @@ enum Manufacturer { @Controller('') export class AppController { + private readonly startedAt = new Date(); private readonly homepageUrl = 'https://dfx.swiss/'; private readonly appleStoreUrl = 'https://apps.apple.com/app'; private readonly googleStoreUrl = 'https://play.app.goo.gl/?link=https://play.google.com/store/apps/details'; @@ -60,6 +62,21 @@ export class AppController { // nothing to do (redirect to Swagger UI) } + @Get('version') + @ApiExcludeEndpoint() + @Version(VERSION_NEUTRAL) + getVersion(): { commit: string; startedAt: Date } { + return { commit: this.getCommit(), startedAt: this.startedAt }; + } + + private getCommit(): string { + try { + return readFileSync('dist/version.txt', 'utf8').trim(); + } catch { + return 'unknown'; + } + } + @Get('app/announcements') @ApiExcludeEndpoint() async getAnnouncements(): Promise { diff --git a/src/integration/blockchain/bitcoin-testnet4/services/bitcoin-testnet4-fee.service.ts b/src/integration/blockchain/bitcoin-testnet4/services/bitcoin-testnet4-fee.service.ts index 8373b72869..e69b888867 100644 --- a/src/integration/blockchain/bitcoin-testnet4/services/bitcoin-testnet4-fee.service.ts +++ b/src/integration/blockchain/bitcoin-testnet4/services/bitcoin-testnet4-fee.service.ts @@ -35,7 +35,7 @@ export class BitcoinTestnet4FeeService { return 1; } - return feeRate; + return Math.max(feeRate, 1); }, undefined, true, // fallbackToCache on error diff --git a/src/integration/exchange/services/exchange.service.ts b/src/integration/exchange/services/exchange.service.ts index 702c29b247..cf7946d06d 100644 --- a/src/integration/exchange/services/exchange.service.ts +++ b/src/integration/exchange/services/exchange.service.ts @@ -165,7 +165,7 @@ export abstract class ExchangeService extends PricingProvider implements OnModul this.logger.verbose(`Could not update order ${order.id} price: ${JSON.stringify(updatedOrder)}`); if (updatedOrder.status === OrderStatus.OPEN) - await this.callApi((e) => e.cancelOrder(order.id, order.symbol)).catch((e) => + await this.cancelOrder(order.id, order.symbol).catch((e) => this.logger.error(`Error while cancelling order ${order.id}:`, e), ); } @@ -376,6 +376,10 @@ export abstract class ExchangeService extends PricingProvider implements OnModul ); } + protected async cancelOrder(orderId: string, symbol: string): Promise { + await this.callApi((e) => e.cancelOrder(orderId, symbol)); + } + // other protected async callApi(action: (exchange: Exchange) => Promise): Promise { return this.queue.handle(() => diff --git a/src/integration/exchange/services/mexc.service.ts b/src/integration/exchange/services/mexc.service.ts index 4f203cbe4f..b344ba205f 100644 --- a/src/integration/exchange/services/mexc.service.ts +++ b/src/integration/exchange/services/mexc.service.ts @@ -355,14 +355,17 @@ export class MexcService extends ExchangeService { protected async updateOrderPrice(order: Order, amount: number, price: number): Promise { // MEXC doesn't support editOrder, so cancel and create new - await this.cancelOrderById(order.id, order.symbol); + await this.cancelOrder(order.id, order.symbol); const newOrder = await this.createOrder(order.symbol, order.side as OrderSide, amount, price); return newOrder.id; } - private async cancelOrderById(orderId: string, pair: string): Promise { + protected async cancelOrder(orderId: string, pair: string): Promise { const symbol = pair.replace('/', ''); - await this.request('DELETE', 'order', { symbol, orderId }); + await this.request('DELETE', 'order', { symbol, orderId }).catch((e) => { + // -2011 means order is already cancelled + if (!e.message?.includes('-2011')) throw e; + }); } } diff --git a/src/subdomains/core/liquidity-management/adapters/actions/clementine-bridge.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/clementine-bridge.adapter.ts index 5ccaa8a246..a5caec0461 100644 --- a/src/subdomains/core/liquidity-management/adapters/actions/clementine-bridge.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/actions/clementine-bridge.adapter.ts @@ -213,14 +213,14 @@ export class ClementineBridgeAdapter extends LiquidityActionAdapter { private async withdraw(order: LiquidityManagementOrder): Promise { const { pipeline: { - rule: { targetAsset: bitcoinAsset }, + rule: { targetAsset: citreaAsset }, }, } = order; - // Validate asset is BTC on Bitcoin - if (bitcoinAsset.type !== AssetType.COIN || bitcoinAsset.blockchain !== this.btcBlockchain) { + // Validate asset is cBTC on Citrea (we withdraw FROM cBTC TO BTC) + if (citreaAsset.type !== AssetType.COIN || citreaAsset.blockchain !== this.citreaBlockchain) { throw new OrderNotProcessableException( - `Clementine withdraw only supports BTC (native coin) on ${this.btcBlockchain}`, + `Clementine withdraw only supports cBTC (native coin) on ${this.citreaBlockchain}`, ); } @@ -230,8 +230,8 @@ export class ClementineBridgeAdapter extends LiquidityActionAdapter { // Validate network consistency on first use await this.validateNetworkConsistency(); - // Get the corresponding Citrea cBTC asset - const citreaAsset = await this.getCitreaAsset(); + // Get the corresponding Bitcoin BTC asset + const bitcoinAsset = await this.getBtcAsset(); // Check cBTC balance on Citrea - must have at least 10 cBTC (fixed bridge amount) const cbtcBalance = await this.citreaClient.getNativeCoinBalance(); @@ -739,10 +739,6 @@ export class ClementineBridgeAdapter extends LiquidityActionAdapter { return this.network === ClementineNetwork.TESTNET4; } - private get btcBlockchain(): Blockchain { - return this.isTestnet ? Blockchain.BITCOIN_TESTNET4 : Blockchain.BITCOIN; - } - private get citreaBlockchain(): Blockchain { return this.isTestnet ? Blockchain.CITREA_TESTNET : Blockchain.CITREA; } @@ -751,10 +747,6 @@ export class ClementineBridgeAdapter extends LiquidityActionAdapter { return this.isTestnet ? this.assetService.getBitcoinTestnet4Coin() : this.assetService.getBtcCoin(); } - private getCitreaAsset(): Promise { - return this.isTestnet ? this.assetService.getCitreaTestnetCoin() : this.assetService.getCitreaCoin(); - } - private getFeeRate(): Promise { return this.isTestnet ? this.bitcoinTestnet4FeeService.getRecommendedFeeRate()