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
214 changes: 214 additions & 0 deletions EDGE_REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# Edge Exchange Provider API Requirements - Tracking

## Status-Legende
- [ ] Nicht begonnen
- [~] In Arbeit
- [x] Erledigt

---

## Allgemeine Anforderungen (Swap + On/Off Ramp)

### 1. Chain and Token Identification `[~]` ~70%
**Anforderung:** Quotes/Orders via `chainNetwork` + `tokenId` (Contract Address). Für EVM-Chains zusätzlich `chainNetwork: "evmGeneric"` + `chainId`.

**Ist-Zustand:**
- Asset-Entity hat `chainId` (Contract Address) und `blockchain` Enum
- EVM Chain-Config mit numerischen chainIds vorhanden (ETH=1, BSC=56, etc.)
- `evmChainId` Feld in `AssetInDto` (numerische EVM Chain ID)
- `EvmUtil.getBlockchain()` Reverse-Lookup (chainId → Blockchain)
- `resolveAsset()` unterstützt `evmChainId` + `chainId` Kombination

**Offen:**
- [ ] Support für `chainNetwork` als String-Identifier (Edge-Format)
- [ ] Mapping zwischen Edge chain identifiers und DFX Blockchain-Enum

---

### 2. Order Identification and Status Page `[~]` ~70%
**Anforderung:** Unique `orderId` pro Order, abfragbar via unauthenticated API. Status-Page URL: `https://status.provider.com/orderStatus/{orderId}`

**Ist-Zustand:**
- TransactionRequest hat `uid` als unique identifier
- `GET /transaction/single?uid=...` existiert (unauthenticated)

**Offen:**
- [ ] Konsistentes `orderId` Feld in allen Responses
- [ ] Status-Page URL bereitstellen (Frontend oder Redirect)
- [ ] Path-Parameter statt Query-Parameter: `GET /api/status/{orderId}`

---

### 3. Error Handling `[~]` ~80%
**Anforderung:** Alle möglichen Fehler gleichzeitig zurückgeben. Hardened error codes: `RegionRestricted`, `AssetUnsupported`, `OverLimitError` (mit min/max in Source UND Destination Asset).

**Ist-Zustand:**
- QuoteError Enum: `AmountTooLow`, `AmountTooHigh`, `KycRequired`, `LimitExceeded`, `NationalityNotAllowed`, `PaymentMethodNotAllowed`, `RegionRestricted`, `AssetUnsupported`
- `minVolume`/`maxVolume` + `minVolumeTarget`/`maxVolumeTarget` in Quote-Response
- `errors` Array in allen Quote-DTOs (Buy/Sell/Swap) mit `StructuredErrorDto`
- `QuoteErrorUtil.mapToStructuredErrors()` mappt bestehende Errors auf Edge-Format
- Error-Mapping: `NationalityNotAllowed` → `RegionRestricted`, Amount-Errors → `OverLimitError`/`UnderLimitError` mit `sourceAmountLimit`/`destinationAmountLimit`

**Offen:**
- [ ] Alle Fehler gleichzeitig als Array zurückgeben (aktuell: einzelner Error → Array mit einem Element)

---

### 4. Quoting Requirements `[x]` 100%
**Anforderung:** Bi-directional quoting (Source ODER Destination Amount).

**Ist-Zustand:**
- XOR-Validation in `GetSwapQuoteDto`: `amount` oder `targetAmount`
- Gleiche Logik in Sell-Quotes
- **Vollständig erfüllt.**

---

### 5. Transaction Status API `[x]` 100%
**Anforderung:** `GET /api/status/{orderId}` mit Status: `pending | processing | infoNeeded | expired | refunded | completed`

**Ist-Zustand:**
- `GET /transaction/single` existiert (unauthenticated)
- TransactionState hat 14 verschiedene States
- `GET /transaction/status/:orderId` Endpoint (unauthenticated)
- `ProviderTransactionStatus` Enum mit Edge-Format
- `mapToProviderStatus()` mappt alle 14 States auf 6 Edge-Status-Werte
- Lookup via `uid` und `orderUid`

**Erledigt.**

---

### 6. Reporting API `[~]` ~40%
**Anforderung:** Authentifizierte API für alle Transaktionen. Pagination (start date, end date, count). Felder: orderId, status, dates, source/dest network/token/amount, payin/payout addresses, txids, EVM chainIds.

**Ist-Zustand:**
- `GET /history/:exportType` mit API-Key Auth
- `GET /transaction/detail` mit Bearer Token
- Date-Filter vorhanden

**Offen:**
- [ ] Explizite Pagination (startDate, endDate, limit/offset)
- [ ] Response-Format mit allen Edge-Pflichtfeldern
- [ ] `orderId` konsistent (statt `id`/`uid`)
- [ ] EVM `chainId` im Response
- [ ] Source/Dest Network + TokenId + Amount + Addresses + Txids

---

### 7. Account Activation `[ ]` 0%
**Anforderung:** Bei XRP/HBAR: unactivated Addresses erkennen und Aktivierungs-Transaktion senden.

**Ist-Zustand:**
- XRP und HBAR sind nicht in der Blockchain-Enum

**Offen:**
- [ ] Prüfen ob XRP/HBAR überhaupt unterstützt werden sollen
- [ ] Falls ja: Activation-Logik implementieren

---

### 8. Affiliate Revenue Withdrawal `[ ]` ~10%
**Anforderung:** Auto-Withdraw innerhalb 24h nach Monatsende. In BTC/ETH/USDC an feste, verifizierte Adresse.

**Ist-Zustand:**
- Referral/RefReward System vorhanden

**Offen:**
- [ ] Klären ob DFX Affiliate-Programm mit Edge hat
- [ ] Auto-Withdrawal Mechanismus (Cron-Job)
- [ ] Fixed address Verwaltung mit 2FA/Email-Verification bei Änderung

---

## Zusätzliche Anforderungen für Fiat On/Off Ramp

### 9. User Authentication `[~]` ~60%
**Anforderung:** Auth via cryptographic random `authKey`. Auto-Account-Erstellung wenn authKey nicht existiert. KYC-Info via API.

**Ist-Zustand:**
- Wallet-Signature basierte Auth (`POST /auth`)
- Auto-Signup bei `POST /auth/signUp`

**Offen:**
- [ ] `authKey`-basierte Authentifizierung (random key statt wallet signature)
- [ ] Auto-Create Account bei unbekanntem authKey
- [ ] authKey in Quote/Order-Endpoints akzeptieren

---

### 10. Regional and Fiat Currency Support `[~]` ~40%
**Anforderung:** Quote-Request mit Region (Country/Province) und Fiat-Currency. Proper Errors bei unsupported.

**Ist-Zustand:**
- IP-basierte Country-Detection
- Fiat-Currency Support in Quotes

**Offen:**
- [ ] Expliziter `region`/`country` Parameter in Quote-Requests
- [ ] Expliziter `fiatCurrency` Parameter
- [ ] Spezifische Errors: `RegionNotSupported`, `CurrencyNotSupported`

---

### 11. KYC Information `[~]` ~60%
**Anforderung:** KYC-Daten (Name, Address, Phone, Email) via API submitten (kein Widget).

**Ist-Zustand:**
- UserData-Update mit firstname, surname, street, phone, mail etc.
- KYC-Controller vorhanden

**Offen:**
- [ ] Dedizierter KYC-Submission-Endpoint für Edge
- [ ] Sicherstellen dass alle Felder via API setzbar sind (ohne Widget)

---

### 12. Bank Information `[~]` ~40%
**Anforderung:** Bank-Info (Account Number, IBAN, Routing Number) via API submitten.

**Ist-Zustand:**
- BankData-Controller existiert (Admin-only)
- CreateBankDataDto: IBAN, BIC, Name

**Offen:**
- [ ] User-zugängliche API (nicht nur Admin)
- [ ] `accountNumber` und `routingNumber` Felder (für US-Markt)

---

### 13. Verification `[ ]` ~10%
**Anforderung:** KYC-Verification-Codes (Phone/Email) über API senden. Edge informieren wenn KYC-Info fehlt/veraltet.

**Ist-Zustand:**
- KYC via externen Provider (SumSub)

**Offen:**
- [ ] Phone-Verification-Code senden via API
- [ ] Email-Verification-Code senden via API
- [ ] Verification-Code validieren via API
- [ ] "KYC info missing/outdated" Status in API-Response

---

### 14. Widgets `[~]` ~30%
**Anforderung:** Widgets müssen Return-URIs unterstützen, damit Edge Webviews schließen kann.

**Ist-Zustand:**
- Redirect-Logik in Auth vorhanden

**Offen:**
- [ ] Return-URI Parameter für alle Widget-Flows
- [ ] Callback nach Widget-Completion

---

### 15. Off-Ramp Flow `[~]` ~50%
**Anforderung:** No-Widget Off-Ramp: Crypto-Address + Expiration-Time bereitstellen.

**Ist-Zustand:**
- Sell-Flow mit Deposit-Address vorhanden

**Offen:**
- [ ] Expiration-Time für Deposit-Addresses
- [ ] Sicherstellen dass Flow komplett ohne Widget funktioniert
7 changes: 7 additions & 0 deletions src/integration/blockchain/shared/evm/evm.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ export class EvmUtil {
return this.blockchainToChainIdMap.get(blockchain);
}

static getBlockchain(chainId: number): Blockchain | undefined {
for (const [blockchain, id] of this.blockchainToChainIdMap.entries()) {
if (id === chainId) return blockchain;
}
return undefined;
}

static createWallet({ seed, index }: WalletAccount, provider?: ethers.providers.JsonRpcProvider): ethers.Wallet {
const wallet = ethers.Wallet.fromMnemonic(seed, this.getPathFor(index));
return provider ? wallet.connect(provider) : wallet;
Expand Down
11 changes: 8 additions & 3 deletions src/shared/models/asset/dto/asset.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsEnum, IsInt, IsNotEmpty, IsString, ValidateIf } from 'class-validator';
import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, ValidateIf } from 'class-validator';
import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum';
import { AssetCategory, AssetType } from '../asset.entity';

Expand Down Expand Up @@ -77,7 +77,7 @@ export class AssetDto {
export class AssetInDto {
@ApiPropertyOptional()
@IsNotEmpty()
@ValidateIf((a: AssetInDto) => Boolean(a.id || !(a.chainId || a.blockchain)))
@ValidateIf((a: AssetInDto) => Boolean(a.id || !(a.chainId || a.blockchain || a.evmChainId)))
@IsInt()
id?: number;

Expand All @@ -89,9 +89,14 @@ export class AssetInDto {

@ApiPropertyOptional()
@IsNotEmpty()
@ValidateIf((a: AssetInDto) => Boolean(a.blockchain || !a.id))
@ValidateIf((a: AssetInDto) => Boolean(a.blockchain || (!a.id && !a.evmChainId)))
@IsEnum(Blockchain)
blockchain?: Blockchain;

@ApiPropertyOptional({ description: 'Numeric EVM chain ID (e.g. 1, 56, 137)' })
@IsOptional()
@IsInt()
evmChainId?: number;
}

export class AssetLimitsDto {
Expand Down
14 changes: 11 additions & 3 deletions src/shared/services/payment-info.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { EvmUtil } from 'src/integration/blockchain/shared/evm/evm.util';
import { Config, Environment } from 'src/config/config';
import { AmlRule } from 'src/subdomains/core/aml/enums/aml-rule.enum';
import { CreateBuyDto } from 'src/subdomains/core/buy-crypto/routes/buy/dto/create-buy.dto';
Expand Down Expand Up @@ -129,8 +130,15 @@ export class PaymentInfoService {
}

async resolveAsset(asset: Asset): Promise<Asset> {
return asset.id
? this.assetService.getAssetById(asset.id)
: this.assetService.getAssetByChainId(asset.blockchain, asset.chainId);
if (asset.id) return this.assetService.getAssetById(asset.id);

let blockchain = asset.blockchain;
const evmChainId = (asset as any).evmChainId as number | undefined;
if (evmChainId && !blockchain) {
blockchain = EvmUtil.getBlockchain(evmChainId);
if (!blockchain) throw new BadRequestException(`Unsupported EVM chain ID: ${evmChainId}`);
}

return this.assetService.getAssetByChainId(blockchain, asset.chainId);
}
}
2 changes: 2 additions & 0 deletions src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { VirtualIbanDto } from 'src/subdomains/supporting/bank/virtual-iban/dto/
import { VirtualIbanMapper } from 'src/subdomains/supporting/bank/virtual-iban/dto/virtual-iban.mapper';
import { VirtualIbanService } from 'src/subdomains/supporting/bank/virtual-iban/virtual-iban.service';
import { CryptoPaymentMethod, FiatPaymentMethod } from 'src/subdomains/supporting/payment/dto/payment-method.enum';
import { QuoteErrorUtil } from 'src/subdomains/supporting/payment/dto/transaction-helper/quote-error.util';
import { TransactionRequestStatus } from 'src/subdomains/supporting/payment/entities/transaction-request.entity';
import { SwissQRService } from 'src/subdomains/supporting/payment/services/swiss-qr.service';
import { TransactionHelper } from 'src/subdomains/supporting/payment/services/transaction-helper';
Expand Down Expand Up @@ -136,6 +137,7 @@ export class BuyController {
priceSteps,
isValid,
error,
errors: QuoteErrorUtil.mapToStructuredErrors(error, minVolume, minVolumeTarget, maxVolume, maxVolumeTarget),
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { FeeDto } from 'src/subdomains/supporting/payment/dto/fee.dto';
import { QuoteError } from 'src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum';
import { StructuredErrorDto } from 'src/subdomains/supporting/payment/dto/transaction-helper/structured-error.dto';
import { PriceStep } from 'src/subdomains/supporting/pricing/domain/entities/price';

export class BuyQuoteDto {
Expand Down Expand Up @@ -45,4 +46,7 @@ export class BuyQuoteDto {

@ApiPropertyOptional({ enum: QuoteError, description: 'Error message in case isValid is false' })
error?: QuoteError;

@ApiPropertyOptional({ type: [StructuredErrorDto], description: 'Structured errors array' })
errors?: StructuredErrorDto[];
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { FeeDto } from 'src/subdomains/supporting/payment/dto/fee.dto';
import { QuoteError } from 'src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum';
import { StructuredErrorDto } from 'src/subdomains/supporting/payment/dto/transaction-helper/structured-error.dto';
import { PriceStep } from 'src/subdomains/supporting/pricing/domain/entities/price';

export class SwapQuoteDto {
Expand Down Expand Up @@ -42,4 +43,7 @@ export class SwapQuoteDto {

@ApiPropertyOptional({ enum: QuoteError, description: 'Error message in case isValid is false' })
error?: QuoteError;

@ApiPropertyOptional({ type: [StructuredErrorDto], description: 'Structured errors array' })
errors?: StructuredErrorDto[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { UnsignedTxDto } from 'src/subdomains/core/sell-crypto/route/dto/unsigne
import { UserService } from 'src/subdomains/generic/user/models/user/user.service';
import { DepositDtoMapper } from 'src/subdomains/supporting/address-pool/deposit/dto/deposit-dto.mapper';
import { CryptoPaymentMethod } from 'src/subdomains/supporting/payment/dto/payment-method.enum';
import { QuoteErrorUtil } from 'src/subdomains/supporting/payment/dto/transaction-helper/quote-error.util';
import { TransactionDto } from 'src/subdomains/supporting/payment/dto/transaction.dto';
import { TransactionHelper } from 'src/subdomains/supporting/payment/services/transaction-helper';
import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service';
Expand Down Expand Up @@ -137,6 +138,7 @@ export class SwapController {
priceSteps,
isValid,
error,
errors: QuoteErrorUtil.mapToStructuredErrors(error, minVolume, minVolumeTarget, maxVolume, maxVolumeTarget),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { TransactionHelper } from 'src/subdomains/supporting/payment/services/tr
import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service';
import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service';
import { FindOptionsRelations } from 'typeorm';
import { TransactionStatusDto, mapToProviderStatus } from '../../../supporting/payment/dto/transaction-status.dto';
import {
TransactionDetailDto,
TransactionDto,
Expand Down Expand Up @@ -147,6 +148,20 @@ export class TransactionController {
return dto;
}

@Get('status/:orderId')
@ApiOkResponse({ type: TransactionStatusDto })
async getTransactionStatus(@Param('orderId') orderId: string): Promise<TransactionStatusDto> {
const tx = await this.getTransaction({ uid: orderId, orderUid: orderId });

const dto = await this.getTransactionDto(tx);
if (!dto) throw new NotFoundException('Transaction not found');

return {
orderId,
status: mapToProviderStatus(dto.state),
};
}

@Put('csv')
@ApiOkResponse()
@ApiOperation({ description: 'Initiate CSV history export' })
Expand Down
4 changes: 4 additions & 0 deletions src/subdomains/core/sell-crypto/route/dto/sell-quote.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { FeeDto } from 'src/subdomains/supporting/payment/dto/fee.dto';
import { QuoteError } from 'src/subdomains/supporting/payment/dto/transaction-helper/quote-error.enum';
import { StructuredErrorDto } from 'src/subdomains/supporting/payment/dto/transaction-helper/structured-error.dto';
import { PriceStep } from 'src/subdomains/supporting/pricing/domain/entities/price';

export class SellQuoteDto {
Expand Down Expand Up @@ -45,4 +46,7 @@ export class SellQuoteDto {

@ApiPropertyOptional({ enum: QuoteError, description: 'Error message in case isValid is false' })
error?: QuoteError;

@ApiPropertyOptional({ type: [StructuredErrorDto], description: 'Structured errors array' })
errors?: StructuredErrorDto[];
}
Loading
Loading