diff --git a/README.md b/README.md index 86d9c5d972..133e5aa38b 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ Links to the productive API are used in the further documentation. - [Swagger UI](https://dev.api.dfx.swiss) - [Swagger JSON](https://dev.api.dfx.swiss/swagger-json) +### API Conventions + +**Amount Representation:** All amount fields in the API use human-readable display units. For example, `1.5` means 1.5 BTC (not 150,000,000 satoshis). Crypto amounts are typically rounded to ~5 decimal places, fiat amounts to 2 decimal places. + ## On-/Off-Ramp This section explains the key concepts for using the DFX on-ramp and off-ramp. diff --git a/src/config/config.ts b/src/config/config.ts index ee826ed680..df59821956 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -50,6 +50,7 @@ export class Configuration { defaultWalletId = 1; transactionRefundExpirySeconds = 300; // 5 minutes - enough time to fill out the refund form txRequestWaitingExpiryDays = 7; + txRequestValidityMinutes = 30; financeLogTotalBalanceChangeLimit = 5000; faucetAmount = 20; //CHF faucetEnabled = process.env.FAUCET_ENABLED === 'true'; diff --git a/src/main.ts b/src/main.ts index 8a02d765e5..9f8fbd5cad 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,8 +3,8 @@ import { NestFactory } from '@nestjs/core'; import { WsAdapter } from '@nestjs/platform-ws'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import * as AppInsights from 'applicationinsights'; -import { useContainer } from 'class-validator'; import { spawnSync } from 'child_process'; +import { useContainer } from 'class-validator'; import cors from 'cors'; import { json, raw, text } from 'express'; import helmet from 'helmet'; @@ -77,7 +77,10 @@ async function bootstrap() { const swaggerOptions = new DocumentBuilder() .setTitle('DFX API') - .setDescription(`DFX API ${Config.environment.toUpperCase()} (updated on ${new Date().toLocaleString()})`) + .setDescription( + `DFX API ${Config.environment.toUpperCase()} (updated on ${new Date().toLocaleString()})\n\n` + + '**Amount Convention:** All amount fields use human-readable display units (e.g., 1.5 BTC, not 150,000,000 satoshis). ', + ) .setExternalDoc('Github documentation', Config.social.github) .setVersion(Config.defaultVersionString) .addBearerAuth() diff --git a/src/shared/services/process.service.ts b/src/shared/services/process.service.ts index 67af5d0bea..f16da5d8c0 100644 --- a/src/shared/services/process.service.ts +++ b/src/shared/services/process.service.ts @@ -90,6 +90,7 @@ export enum Process { ZANO_ASSET_WHITELIST = 'ZanoAssetWhitelist', TRADE_APPROVAL_DATE = 'TradeApprovalDate', SUPPORT_BOT = 'SupportBot', + GUARANTEED_PRICE = 'GuaranteedPrice', } const safetyProcesses: Process[] = [ diff --git a/src/subdomains/core/buy-crypto/process/entities/buy-crypto.entity.ts b/src/subdomains/core/buy-crypto/process/entities/buy-crypto.entity.ts index fce6fff7e7..ab2219b952 100644 --- a/src/subdomains/core/buy-crypto/process/entities/buy-crypto.entity.ts +++ b/src/subdomains/core/buy-crypto/process/entities/buy-crypto.entity.ts @@ -271,6 +271,15 @@ export class BuyCrypto extends IEntity { @Column({ length: 'MAX', nullable: true }) priceSteps?: string; + /** + * Ratio of quoted rate to market rate at execution. + * > 1 = user got better rate than market + * < 1 = user got worse rate than market + * null = quote not used (expired or not available) + */ + @Column({ type: 'float', nullable: true }) + quoteMarketRatio?: number; + // Transaction details @Column({ length: 256, nullable: true }) txId?: string; diff --git a/src/subdomains/core/buy-crypto/process/services/buy-crypto-batch.service.ts b/src/subdomains/core/buy-crypto/process/services/buy-crypto-batch.service.ts index 09b22b5469..aab31b860c 100644 --- a/src/subdomains/core/buy-crypto/process/services/buy-crypto-batch.service.ts +++ b/src/subdomains/core/buy-crypto/process/services/buy-crypto-batch.service.ts @@ -3,7 +3,8 @@ import { Config } from 'src/config/config'; import { Asset, AssetType } from 'src/shared/models/asset/asset.entity'; import { FiatService } from 'src/shared/models/fiat/fiat.service'; import { DfxLogger, LogLevel } from 'src/shared/services/dfx-logger'; -import { Util } from 'src/shared/utils/util'; +import { DisabledProcess, Process } from 'src/shared/services/process.service'; +import { AmountType, Util } from 'src/shared/utils/util'; import { LiquidityManagementOrder } from 'src/subdomains/core/liquidity-management/entities/liquidity-management-order.entity'; import { LiquidityManagementPipeline } from 'src/subdomains/core/liquidity-management/entities/liquidity-management-pipeline.entity'; import { LiquidityManagementRuleStatus } from 'src/subdomains/core/liquidity-management/enums'; @@ -72,7 +73,7 @@ export class BuyCryptoBatchService { cryptoInput: true, buy: { user: true }, cryptoRoute: { user: true }, - transaction: { userData: true }, + transaction: { userData: true, request: true }, liquidityPipeline: { orders: true }, }, }); @@ -144,7 +145,23 @@ export class BuyCryptoBatchService { ? await this.findAllExchangeOrders(tx.liquidityPipeline) : undefined; - tx.calculateOutputReferenceAmount(price, exchangeOrders); + // Price from transaction request + const quoteResult = !DisabledProcess(Process.GUARANTEED_PRICE) + ? tx.transaction?.request?.calculateQuoteOutput( + Config.txRequestValidityMinutes, + tx.inputReferenceAmountMinusFee, + price.price, + AmountType.ASSET, + ) + : undefined; + + if (quoteResult) { + tx.outputReferenceAmount = quoteResult.outputAmount; + tx.priceStepsObject = [...tx.inputPriceStep, ...quoteResult.priceSteps]; + tx.quoteMarketRatio = quoteResult.quoteMarketRatio; + } else { + tx.calculateOutputReferenceAmount(price, exchangeOrders); + } } } catch (e) { if (e instanceof PriceInvalidException) { diff --git a/src/subdomains/core/buy-crypto/routes/buy/dto/buy-payment-info.dto.ts b/src/subdomains/core/buy-crypto/routes/buy/dto/buy-payment-info.dto.ts index cde563d7a5..d2a43002e3 100644 --- a/src/subdomains/core/buy-crypto/routes/buy/dto/buy-payment-info.dto.ts +++ b/src/subdomains/core/buy-crypto/routes/buy/dto/buy-payment-info.dto.ts @@ -45,6 +45,9 @@ export class BuyPaymentInfoDto extends BankInfoDto { @ApiProperty({ description: 'UID of the transaction order' }) uid?: string; + @ApiPropertyOptional({ description: 'URL to the order status page' }) + statusUrl?: string; + @ApiProperty({ description: 'Price timestamp' }) timestamp: Date; @@ -121,4 +124,7 @@ export class BuyPaymentInfoDto extends BankInfoDto { @ApiPropertyOptional({ description: 'Whether this uses a personal IBAN' }) isPersonalIban?: boolean; + + @ApiPropertyOptional({ description: 'Expiration timestamp of the quote' }) + expiryDate?: Date; } diff --git a/src/subdomains/core/buy-crypto/routes/buy/dto/get-buy-quote.dto.ts b/src/subdomains/core/buy-crypto/routes/buy/dto/get-buy-quote.dto.ts index 3e40a13ad8..38017e0500 100644 --- a/src/subdomains/core/buy-crypto/routes/buy/dto/get-buy-quote.dto.ts +++ b/src/subdomains/core/buy-crypto/routes/buy/dto/get-buy-quote.dto.ts @@ -69,4 +69,9 @@ export class GetBuyQuoteDto { @IsOptional() @IsString() country?: string; + + @ApiPropertyOptional({ description: 'State or province code (e.g. US-NY, CA-BC)' }) + @IsOptional() + @IsString() + stateProvince?: string; } diff --git a/src/subdomains/core/buy-crypto/routes/swap/dto/swap-payment-info.dto.ts b/src/subdomains/core/buy-crypto/routes/swap/dto/swap-payment-info.dto.ts index 3cc8c31ca4..708a55bcfb 100644 --- a/src/subdomains/core/buy-crypto/routes/swap/dto/swap-payment-info.dto.ts +++ b/src/subdomains/core/buy-crypto/routes/swap/dto/swap-payment-info.dto.ts @@ -15,6 +15,9 @@ export class SwapPaymentInfoDto { @ApiProperty({ description: 'UID of the transaction order' }) uid?: string; + @ApiPropertyOptional({ description: 'URL to the order status page' }) + statusUrl?: string; + @ApiProperty({ description: 'Price timestamp' }) timestamp: Date; @@ -103,4 +106,7 @@ export class SwapPaymentInfoDto { description: 'Whether gasless transaction is available for this request', }) gaslessAvailable?: boolean; + + @ApiPropertyOptional({ description: 'Expiration timestamp of the quote' }) + expiryDate?: Date; } diff --git a/src/subdomains/core/history/dto/history-query.dto.ts b/src/subdomains/core/history/dto/history-query.dto.ts index 60a2befdb7..03e5631607 100644 --- a/src/subdomains/core/history/dto/history-query.dto.ts +++ b/src/subdomains/core/history/dto/history-query.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsDate, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { IsDate, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; import { ExportType } from '../services/history.service'; import { HistoryFilter } from './history-filter.dto'; @@ -32,6 +32,21 @@ export class HistoryQuery extends HistoryFilter { @IsOptional() @IsString() blockchains?: string; + + @ApiPropertyOptional({ description: 'Maximum number of transactions to return', default: 1000 }) + @IsOptional() + @IsNumber() + @Min(1) + @Max(10000) + @Type(() => Number) + limit?: number; + + @ApiPropertyOptional({ description: 'Number of transactions to skip', default: 0 }) + @IsOptional() + @IsNumber() + @Min(0) + @Type(() => Number) + offset?: number; } export class HistoryQueryUser extends HistoryQuery { diff --git a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts index 205d7ad292..0ee138c402 100644 --- a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts +++ b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts @@ -111,6 +111,7 @@ export class TransactionDtoMapper { asset: buyCrypto.networkStartAsset, } : null, + userCountry: buyCrypto.transaction.userData?.country?.symbol, }; return Object.assign(new TransactionDto(), dto); @@ -181,6 +182,7 @@ export class TransactionDtoMapper { chargebackDate: buyFiat.chargebackDate, date: buyFiat.transaction.created, externalTransactionId: buyFiat.transaction.externalId, + userCountry: buyFiat.transaction.userData?.country?.symbol, }; return Object.assign(new TransactionDto(), dto); @@ -241,6 +243,7 @@ export class TransactionDtoMapper { date: txRequest.created, externalTransactionId: null, networkStartTx: null, + userCountry: txRequest.user?.userData?.country?.symbol, }; return Object.assign(new TransactionDto(), dto); @@ -303,6 +306,7 @@ export class TransactionDtoMapper { chargebackTxUrl: undefined, chargebackDate: undefined, date: refReward.transaction.created, + userCountry: refReward.transaction.userData?.country?.symbol, }; return Object.assign(new TransactionDto(), dto); diff --git a/src/subdomains/core/history/services/history.service.ts b/src/subdomains/core/history/services/history.service.ts index e72a61f947..df3e21667b 100644 --- a/src/subdomains/core/history/services/history.service.ts +++ b/src/subdomains/core/history/services/history.service.ts @@ -122,8 +122,14 @@ export class HistoryService { ): Promise<{ buyCryptos: BuyCrypto[]; buyFiats: BuyFiat[]; refRewards: RefReward[] }> { const transactions = user instanceof UserData - ? await this.transactionService.getTransactionsForAccount(user.id, query.from, query.to) - : await this.transactionService.getTransactionsForUsers([user.id], query.from, query.to); + ? await this.transactionService.getTransactionsForAccount( + user.id, + query.from, + query.to, + query.limit, + query.offset, + ) + : await this.transactionService.getTransactionsForUsers([user.id], query.from, query.to, query.limit); const all = query.buy == null && query.sell == null && query.staking == null && query.ref == null && query.lm == null; diff --git a/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts b/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts index 15904d8a9e..27f5daa883 100644 --- a/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts +++ b/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts @@ -206,6 +206,15 @@ export class BuyFiat extends IEntity { @Column({ length: 'MAX', nullable: true }) priceSteps?: string; + /** + * Ratio of quoted rate to market rate at execution. + * > 1 = user got better rate than market + * < 1 = user got worse rate than market + * null = quote not used (expired or not available) + */ + @Column({ type: 'float', nullable: true }) + quoteMarketRatio?: number; + // Transaction details @Column({ length: 256, nullable: true }) remittanceInfo?: string; @@ -387,13 +396,14 @@ export class BuyFiat extends IEntity { return [this.id, update]; } - setOutput(outputAmount: number, priceSteps: PriceStep[]): UpdateResult { - this.priceStepsObject = [...this.priceStepsObject, ...(priceSteps ?? [])]; + setOutput(outputAmount: number, priceSteps: PriceStep[], quoteMarketRatio?: number): UpdateResult { + this.priceStepsObject = quoteMarketRatio ? priceSteps : [...this.priceStepsObject, ...(priceSteps ?? [])]; const update: Partial = { outputAmount, outputReferenceAmount: this.outputReferenceAmount ?? outputAmount, priceSteps: this.priceSteps, + quoteMarketRatio, }; Object.assign(this, update); diff --git a/src/subdomains/core/sell-crypto/process/services/buy-fiat-preparation.service.ts b/src/subdomains/core/sell-crypto/process/services/buy-fiat-preparation.service.ts index f6947d1bf5..c60b9cfd63 100644 --- a/src/subdomains/core/sell-crypto/process/services/buy-fiat-preparation.service.ts +++ b/src/subdomains/core/sell-crypto/process/services/buy-fiat-preparation.service.ts @@ -3,6 +3,7 @@ import { isBankHoliday } from 'src/config/bank-holiday.config'; import { Config } from 'src/config/config'; import { CountryService } from 'src/shared/models/country/country.service'; import { DfxLogger } from 'src/shared/services/dfx-logger'; +import { DisabledProcess, Process } from 'src/shared/services/process.service'; import { AmountType, Util } from 'src/shared/utils/util'; import { BlockAmlReasons } from 'src/subdomains/core/aml/enums/aml-reason.enum'; import { AmlService } from 'src/subdomains/core/aml/services/aml.service'; @@ -321,7 +322,7 @@ export class BuyFiatPreparationService { outputAmount: IsNull(), priceDefinitionAllowedDate: Not(IsNull()), }, - relations: { sell: true, cryptoInput: true, transaction: { userData: true } }, + relations: { sell: true, cryptoInput: true, transaction: { userData: true, request: true } }, }); for (const entity of entities) { @@ -331,21 +332,42 @@ export class BuyFiatPreparationService { const price = !entity.outputReferenceAmount ? await this.pricingService.getPrice(asset, currency, PriceValidity.VALID_ONLY) : undefined; - const priceSteps = price?.steps ?? [ - PriceStep.create( - Config.priceSourceManual, - entity.inputReferenceAsset, - entity.outputReferenceAsset.name, - entity.inputReferenceAmountMinusFee / entity.outputReferenceAmount, - ), - ]; - await this.buyFiatRepo.update( - ...entity.setOutput( - entity.outputReferenceAmount ?? price.convert(entity.inputReferenceAmountMinusFee), - priceSteps, - ), - ); + // Price from transaction request + const quoteResult = + !DisabledProcess(Process.GUARANTEED_PRICE) && price + ? entity.transaction?.request?.calculateQuoteOutput( + Config.txRequestValidityMinutes, + entity.inputReferenceAmountMinusFee, + price.price, + AmountType.FIAT, + ) + : undefined; + + let outputAmount: number; + let priceSteps: PriceStep[]; + let quoteMarketRatio: number | undefined; + + if (quoteResult) { + outputAmount = quoteResult.outputAmount; + priceSteps = quoteResult.priceSteps; + quoteMarketRatio = quoteResult.quoteMarketRatio; + } else if (entity.outputReferenceAmount) { + outputAmount = entity.outputReferenceAmount; + priceSteps = [ + PriceStep.create( + Config.priceSourceManual, + entity.inputReferenceAsset, + entity.outputReferenceAsset.name, + entity.inputReferenceAmountMinusFee / entity.outputReferenceAmount, + ), + ]; + } else { + outputAmount = price.convert(entity.inputReferenceAmountMinusFee); + priceSteps = price.steps; + } + + await this.buyFiatRepo.update(...entity.setOutput(outputAmount, priceSteps, quoteMarketRatio)); for (const feeId of entity.usedFees.split(';')) { await this.feeService.increaseTxUsages(entity.amountInChf, Number.parseInt(feeId), entity.userData); diff --git a/src/subdomains/core/sell-crypto/route/dto/get-sell-quote.dto.ts b/src/subdomains/core/sell-crypto/route/dto/get-sell-quote.dto.ts index 2d3e8b0e18..b06c8acd9f 100644 --- a/src/subdomains/core/sell-crypto/route/dto/get-sell-quote.dto.ts +++ b/src/subdomains/core/sell-crypto/route/dto/get-sell-quote.dto.ts @@ -62,4 +62,9 @@ export class GetSellQuoteDto { @IsOptional() @IsString() country?: string; + + @ApiPropertyOptional({ description: 'State or province code (e.g. US-NY, CA-BC)' }) + @IsOptional() + @IsString() + stateProvince?: string; } diff --git a/src/subdomains/core/sell-crypto/route/dto/sell-payment-info.dto.ts b/src/subdomains/core/sell-crypto/route/dto/sell-payment-info.dto.ts index 760adc6bc4..662022df94 100644 --- a/src/subdomains/core/sell-crypto/route/dto/sell-payment-info.dto.ts +++ b/src/subdomains/core/sell-crypto/route/dto/sell-payment-info.dto.ts @@ -24,6 +24,9 @@ export class SellPaymentInfoDto { @ApiProperty({ description: 'UID of the transaction order' }) uid?: string; + @ApiPropertyOptional({ description: 'URL to the order status page' }) + statusUrl?: string; + @ApiProperty({ description: 'Price timestamp' }) timestamp: Date; @@ -118,4 +121,7 @@ export class SellPaymentInfoDto { description: 'Whether gasless transaction is available for this request', }) gaslessAvailable?: boolean; + + @ApiPropertyOptional({ description: 'Expiration timestamp of the quote' }) + expiryDate?: Date; } diff --git a/src/subdomains/supporting/payment/dto/transaction.dto.ts b/src/subdomains/supporting/payment/dto/transaction.dto.ts index 1fe08fd854..2170148aaf 100644 --- a/src/subdomains/supporting/payment/dto/transaction.dto.ts +++ b/src/subdomains/supporting/payment/dto/transaction.dto.ts @@ -265,6 +265,9 @@ export class TransactionDto extends UnassignedTransactionDto { @ApiPropertyOptional({ type: NetworkStartTxDto }) networkStartTx?: NetworkStartTxDto; + + @ApiPropertyOptional({ description: 'User country code (ISO 3166-1 alpha-2)' }) + userCountry?: string; } export class TransactionDetailDto extends TransactionDto { diff --git a/src/subdomains/supporting/payment/entities/transaction-request.entity.ts b/src/subdomains/supporting/payment/entities/transaction-request.entity.ts index 3406861e15..2924f62134 100644 --- a/src/subdomains/supporting/payment/entities/transaction-request.entity.ts +++ b/src/subdomains/supporting/payment/entities/transaction-request.entity.ts @@ -1,9 +1,12 @@ import { IEntity, UpdateResult } from 'src/shared/models/entity'; +import { AmountType, Util } from 'src/shared/utils/util'; import { CustodyOrder } from 'src/subdomains/core/custody/entities/custody-order.entity'; import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity'; import { User } from 'src/subdomains/generic/user/models/user/user.entity'; +import { PriceStep } from 'src/subdomains/supporting/pricing/domain/entities/price'; import { Column, Entity, ManyToOne, OneToMany, OneToOne } from 'typeorm'; import { SupportIssue } from '../../support-issue/entities/support-issue.entity'; +import { FeeDto } from '../dto/fee.dto'; import { PaymentMethod } from '../dto/payment-method.enum'; import { QuoteError } from '../dto/transaction-helper/quote-error.enum'; import { Transaction } from './transaction.entity'; @@ -82,6 +85,12 @@ export class TransactionRequest extends IEntity { @Column({ type: 'float', nullable: true }) totalFee?: number; + @Column({ type: 'simple-json', nullable: true }) + fees?: FeeDto; + + @Column({ type: 'simple-json', nullable: true }) + priceSteps?: PriceStep[]; + @Column({ default: false }) exactPrice: boolean; @@ -120,4 +129,27 @@ export class TransactionRequest extends IEntity { return [this.id, update]; } + + /** + * Calculates output using quoted price if quote is still valid. + */ + calculateQuoteOutput( + validityMinutes: number, + inputAmount: number, + marketPrice: number, + amountType: AmountType, + ): { outputAmount: number; priceSteps: PriceStep[]; quoteMarketRatio: number } | null { + if (!this.priceSteps?.length || !this.created) return null; + + const quoteAgeMinutes = Util.minutesDiff(this.created); + if (quoteAgeMinutes > validityMinutes) return null; + + const quoteRate = this.priceSteps.reduce((acc, step) => acc * step.price, 1); + + return { + outputAmount: Util.roundReadable(inputAmount * quoteRate, amountType), + priceSteps: this.priceSteps, + quoteMarketRatio: Util.round(quoteRate / marketPrice, 8), + }; + } } diff --git a/src/subdomains/supporting/payment/services/transaction-request.service.ts b/src/subdomains/supporting/payment/services/transaction-request.service.ts index 00f42dc3de..ab62c8ccf5 100644 --- a/src/subdomains/supporting/payment/services/transaction-request.service.ts +++ b/src/subdomains/supporting/payment/services/transaction-request.service.ts @@ -130,6 +130,8 @@ export class TransactionRequestService { dfxFee: response.fees.dfx, networkFee: response.fees.network, totalFee: response.fees.total, + fees: response.fees, + priceSteps: response.priceSteps, user: { id: userId }, uid, }); @@ -186,6 +188,8 @@ export class TransactionRequestService { await this.transactionRequestRepo.save(transactionRequest); response.id = transactionRequest.id; response.uid = uid; + response.statusUrl = `${Config.frontend.services}/tx/${uid}`; + response.expiryDate = Util.minutesAfter(Config.txRequestValidityMinutes); // create order at sift (without waiting) if (siftOrder) diff --git a/src/subdomains/supporting/payment/services/transaction.service.ts b/src/subdomains/supporting/payment/services/transaction.service.ts index 5eba49db5b..917f2237f1 100644 --- a/src/subdomains/supporting/payment/services/transaction.service.ts +++ b/src/subdomains/supporting/payment/services/transaction.service.ts @@ -172,7 +172,13 @@ export class TransactionService { return query.orderBy('transaction.id', 'DESC').getMany(); } - async getTransactionsForAccount(userDataId: number, from = new Date(0), to = new Date()): Promise { + async getTransactionsForAccount( + userDataId: number, + from = new Date(0), + to = new Date(), + limit?: number, + offset?: number, + ): Promise { return this.repo.find({ where: { userData: { id: userDataId }, type: Not(IsNull()), created: Between(from, to) }, relations: { @@ -189,6 +195,9 @@ export class TransactionService { bankTx: { transaction: true }, bankTxReturn: true, }, + order: { created: 'DESC' }, + take: limit, + skip: offset, }); }