Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
7 changes: 5 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions src/shared/services/process.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export enum Process {
ZANO_ASSET_WHITELIST = 'ZanoAssetWhitelist',
TRADE_APPROVAL_DATE = 'TradeApprovalDate',
SUPPORT_BOT = 'SupportBot',
GUARANTEED_PRICE = 'GuaranteedPrice',
}

const safetyProcesses: Process[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 },
},
});
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
17 changes: 16 additions & 1 deletion src/subdomains/core/history/dto/history-query.dto.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export class TransactionDtoMapper {
asset: buyCrypto.networkStartAsset,
}
: null,
userCountry: buyCrypto.transaction.userData?.country?.symbol,
};

return Object.assign(new TransactionDto(), dto);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 8 additions & 2 deletions src/subdomains/core/history/services/history.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 12 additions & 2 deletions src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -387,13 +396,14 @@ export class BuyFiat extends IEntity {
return [this.id, update];
}

setOutput(outputAmount: number, priceSteps: PriceStep[]): UpdateResult<BuyFiat> {
this.priceStepsObject = [...this.priceStepsObject, ...(priceSteps ?? [])];
setOutput(outputAmount: number, priceSteps: PriceStep[], quoteMarketRatio?: number): UpdateResult<BuyFiat> {
this.priceStepsObject = quoteMarketRatio ? priceSteps : [...this.priceStepsObject, ...(priceSteps ?? [])];

const update: Partial<BuyFiat> = {
outputAmount,
outputReferenceAmount: this.outputReferenceAmount ?? outputAmount,
priceSteps: this.priceSteps,
quoteMarketRatio,
};

Object.assign(this, update);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
3 changes: 3 additions & 0 deletions src/subdomains/supporting/payment/dto/transaction.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading