diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..5ae46ca11d --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,42 @@ +name: "CodeQL Advanced" + +on: + push: + branches: [ "develop", "master" ] + pull_request: + branches: [ "develop", "master" ] + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + config-file: ./.github/codeql/codeql-config.yml + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/src/integration/exchange/services/exchange.service.ts b/src/integration/exchange/services/exchange.service.ts index eb49df34fe..c2229c711b 100644 --- a/src/integration/exchange/services/exchange.service.ts +++ b/src/integration/exchange/services/exchange.service.ts @@ -296,7 +296,7 @@ export abstract class ExchangeService extends PricingProvider implements OnModul // orders - private async trade(from: string, to: string, amount: number): Promise { + protected async trade(from: string, to: string, amount: number): Promise { // place the order const { pair, direction } = await this.getTradePair(from, to); const { amount: amountPrecision } = await this.getPrecision(pair); diff --git a/src/integration/exchange/services/kraken.service.ts b/src/integration/exchange/services/kraken.service.ts index 02408bed4a..dc846f3c2a 100644 --- a/src/integration/exchange/services/kraken.service.ts +++ b/src/integration/exchange/services/kraken.service.ts @@ -1,14 +1,20 @@ -import { Injectable } from '@nestjs/common'; -import { kraken, Order } from 'ccxt'; +import { Inject, Injectable } from '@nestjs/common'; +import { kraken, Order, TradingFeeInterface } from 'ccxt'; import { GetConfig } from 'src/config/config'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; +import { ExchangeTradingFeeDto } from 'src/shared/models/setting/dto/exchange-trading-fee.dto'; +import { SettingService } from 'src/shared/models/setting/setting.service'; import { DfxLogger } from 'src/shared/services/dfx-logger'; +import { ExchangeName } from '../enums/exchange.enum'; import { ExchangeService } from './exchange.service'; @Injectable() export class KrakenService extends ExchangeService { protected readonly logger = new DfxLogger(KrakenService); + private static readonly FEE_MAX_AGE_MINUTES = 60; + private static readonly TRACKED_SYMBOLS = ['BTC/CHF', 'USDT/CHF']; + // use auto-detect for kraken protected networks: { [b in Blockchain]: string | false } = { Arbitrum: false, @@ -46,14 +52,80 @@ export class KrakenService extends ExchangeService { Yapeal: undefined, }; + @Inject() private readonly settingService: SettingService; + constructor() { super(kraken, GetConfig().kraken); } + // Override trade to ensure ALL trading fees are up-to-date before each trade + protected async trade(from: string, to: string, amount: number): Promise { + await this.ensureAllTradingFeesUpToDate(); + return super.trade(from, to, amount); + } + protected async updateOrderPrice(order: Order, amount: number, price: number): Promise { // order ID does not change for Kraken return this.callApi((e) => e.editOrder(order.id, order.symbol, order.type, order.side, amount, price)).then( () => order.id, ); } + + async getTradingFee(symbol: string): Promise { + return this.callApi((e) => e.fetchTradingFee(symbol)); + } + + // Public getter for other services + async getKrakenTradingFee(symbol: string): Promise { + return this.settingService.getObjCached(this.getTradingFeeKey(symbol)); + } + + private async ensureAllTradingFeesUpToDate(): Promise { + await Promise.all(KrakenService.TRACKED_SYMBOLS.map((symbol) => this.refreshTradingFeeIfStale(symbol))); + } + + private async refreshTradingFeeIfStale(symbol: string): Promise { + const key = this.getTradingFeeKey(symbol); + const current = await this.settingService.getObj(key); + + if (current && !this.isStale(current.updated)) { + return current; + } + + try { + const fee = await this.getTradingFee(symbol); + const newFee: ExchangeTradingFeeDto = { + exchange: ExchangeName.KRAKEN, + symbol: fee.symbol, + maker: fee.maker, + taker: fee.taker, + percentage: fee.percentage, + tierBased: fee.tierBased, + updated: new Date().toISOString(), + }; + + if (current?.maker !== newFee.maker || current?.taker !== newFee.taker) { + this.logger.info( + `Kraken trading fee for ${symbol} changed: maker ${current?.maker ?? 'N/A'} -> ${newFee.maker}, taker ${current?.taker ?? 'N/A'} -> ${newFee.taker}`, + ); + } + + await this.settingService.setObj(key, newFee); + return newFee; + } catch (e) { + this.logger.warn(`Failed to refresh trading fee for ${symbol}, using cached value:`, e); + return current; + } + } + + private getTradingFeeKey(symbol: string): string { + return `krakenTradingFee:${symbol}`; + } + + private isStale(updated: string): boolean { + const updatedDate = new Date(updated); + if (isNaN(updatedDate.getTime())) return true; // Treat invalid date as stale + const ageMinutes = (Date.now() - updatedDate.getTime()) / 1000 / 60; + return ageMinutes > KrakenService.FEE_MAX_AGE_MINUTES; + } } diff --git a/src/shared/models/setting/dto/exchange-trading-fee.dto.ts b/src/shared/models/setting/dto/exchange-trading-fee.dto.ts new file mode 100644 index 0000000000..1fd949cae9 --- /dev/null +++ b/src/shared/models/setting/dto/exchange-trading-fee.dto.ts @@ -0,0 +1,9 @@ +export class ExchangeTradingFeeDto { + exchange: string; + symbol: string; + maker: number; + taker: number; + percentage: boolean; + tierBased: boolean; + updated: string; +} diff --git a/src/subdomains/supporting/bank/bank/bank.service.ts b/src/subdomains/supporting/bank/bank/bank.service.ts index 02b43d3075..d81c9ec7fc 100644 --- a/src/subdomains/supporting/bank/bank/bank.service.ts +++ b/src/subdomains/supporting/bank/bank/bank.service.ts @@ -107,6 +107,8 @@ export class BankService implements OnModuleInit { private static blockchainToBankName(blockchain: Blockchain): IbanBankName | undefined { switch (blockchain) { + case Blockchain.MAERKI_BAUMANN: + return IbanBankName.MAERKI; case Blockchain.OLKYPAY: return IbanBankName.OLKY; case Blockchain.YAPEAL: diff --git a/src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum.ts b/src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum.ts index 310cd236ce..f947c87230 100644 --- a/src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum.ts +++ b/src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum.ts @@ -9,6 +9,7 @@ export enum QuoteError { LIMIT_EXCEEDED = 'LimitExceeded', NATIONALITY_NOT_ALLOWED = 'NationalityNotAllowed', NAME_REQUIRED = 'NameRequired', + PAYMENT_METHOD_NOT_ALLOWED = 'PaymentMethodNotAllowed', IBAN_CURRENCY_MISMATCH = 'IbanCurrencyMismatch', RECOMMENDATION_REQUIRED = 'RecommendationRequired', EMAIL_REQUIRED = 'EmailRequired', diff --git a/src/subdomains/supporting/payment/entities/fee.entity.ts b/src/subdomains/supporting/payment/entities/fee.entity.ts index d0fb27e106..fdf3b5e677 100644 --- a/src/subdomains/supporting/payment/entities/fee.entity.ts +++ b/src/subdomains/supporting/payment/entities/fee.entity.ts @@ -220,7 +220,7 @@ export class Fee extends IEntity { //*** GETTER METHODS ***// get assetList(): number[] { - return this.assets?.split(';')?.map(Number); + return this.assets ? this.assets.split(';')?.map(Number) : undefined; } get excludedAssetList(): number[] { @@ -228,7 +228,7 @@ export class Fee extends IEntity { } get fiatList(): number[] { - return this.fiats?.split(';')?.map(Number); + return this.fiats ? this.fiats.split(';')?.map(Number) : undefined; } get excludedUserDataList(): number[] { diff --git a/src/subdomains/supporting/payment/services/transaction-helper.ts b/src/subdomains/supporting/payment/services/transaction-helper.ts index 3d0a4d2f6e..cd00bdf80b 100644 --- a/src/subdomains/supporting/payment/services/transaction-helper.ts +++ b/src/subdomains/supporting/payment/services/transaction-helper.ts @@ -890,14 +890,12 @@ export class TransactionHelper implements OnModuleInit { : QuoteError.EMAIL_REQUIRED; } + // Credit card payments disabled + if (paymentMethodIn === FiatPaymentMethod.CARD) return QuoteError.PAYMENT_METHOD_NOT_ALLOWED; + if (isSell && ibanCountry && !to.isIbanCountryAllowed(ibanCountry)) return QuoteError.IBAN_CURRENCY_MISMATCH; - if ( - nationality && - ((isBuy && !nationality.bankEnable) || - (paymentMethodIn === FiatPaymentMethod.CARD && !nationality.checkoutEnable) || - ((isSell || isSwap) && !nationality.cryptoEnable)) - ) + if (nationality && ((isBuy && !nationality.bankEnable) || ((isSell || isSwap) && !nationality.cryptoEnable))) return QuoteError.NATIONALITY_NOT_ALLOWED; // KYC checks @@ -935,15 +933,7 @@ export class TransactionHelper implements OnModuleInit { // verification checks if ( - paymentMethodIn === FiatPaymentMethod.CARD && - user && - !user.userData.completeName && - !user.userData.verifiedName - ) - return QuoteError.NAME_REQUIRED; - - if ( - ((isSell && to.name !== 'CHF') || paymentMethodIn === FiatPaymentMethod.CARD || isSwap) && + ((isSell && to.name !== 'CHF') || isSwap) && user && !user.userData.hasBankTxVerification && txAmountChf > Config.tradingLimits.monthlyDefaultWoKyc