Skip to content
Merged
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
19 changes: 19 additions & 0 deletions migration/1777985314136-AddUrbleWalletApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = class AddUrbleWalletApp1777985314136 {
name = 'AddUrbleWalletApp1777985314136';

async up(queryRunner) {
await queryRunner.query(`
IF NOT EXISTS (SELECT 1 FROM "dbo"."wallet_app" WHERE "name" = 'urble')
INSERT INTO "dbo"."wallet_app" (
"name", "websiteUrl", "iconUrl", "deepLink", "blockchains", "assets", "active"
) VALUES (
'urble', 'https://urble.io', 'https://dfx.swiss/images/app/urble.webp', 'urble:',
'Bitcoin;Cardano;Ethereum', '113;406;123;145;337', 1
)
`);
}

async down(queryRunner) {
await queryRunner.query(`DELETE FROM "dbo"."wallet_app" WHERE "name" = 'urble'`);
}
};
1 change: 1 addition & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@ export class Configuration {
template: 'realunit',
forcedLang: 'de',
centralizedWelcome: true,
isPreferred: true,
},
}),
},
Expand Down
20 changes: 12 additions & 8 deletions src/integration/blockchain/shared/evm/citrea-base-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,18 @@ export abstract class CitreaBaseClient extends EvmClient {
const balances: BlockchainTokenBalance[] = [];

for (const asset of assets) {
const balance = isPoolBalance
? await this.getPoolTokenBalance(asset, owner)
: await this.getTokenBalance(asset, owner);
balances.push({
owner,
contractAddress: asset.chainId,
balance,
});
try {
const balance = isPoolBalance
? await this.getPoolTokenBalance(asset, owner)
: await this.getTokenBalance(asset, owner);
balances.push({
owner,
contractAddress: asset.chainId,
balance,
});
} catch (e) {
this.logger.error(`Failed to process token balance for ${asset.uniqueName}:`, e);
}
}

return balances;
Expand Down
12 changes: 8 additions & 4 deletions src/integration/blockchain/shared/evm/evm-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,14 @@ export abstract class EvmClient extends BlockchainClient {
const tokenBalances = await this.alchemyService.getTokenBalances(this.chainId, owner, assets);

for (const tokenBalance of tokenBalances) {
const token = await this.getTokenByAddress(tokenBalance.contractAddress);
const balance = EvmUtil.fromWeiAmount(tokenBalance.tokenBalance ?? 0, token.decimals);

evmTokenBalances.push({ owner, contractAddress: tokenBalance.contractAddress, balance: balance });
try {
const token = await this.getTokenByAddress(tokenBalance.contractAddress);
const balance = EvmUtil.fromWeiAmount(tokenBalance.tokenBalance ?? 0, token.decimals);

evmTokenBalances.push({ owner, contractAddress: tokenBalance.contractAddress, balance: balance });
} catch (e) {
this.logger.error(`Failed to process token balance for ${tokenBalance.contractAddress}:`, e);
}
}

return evmTokenBalances;
Expand Down
35 changes: 24 additions & 11 deletions src/integration/exchange/services/scrypt-websocket-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ enum ScryptRequestType {
PAGE = 'page',
}

export const TRANSIENT_WS_ERROR_MARKERS = ['Connection closed', 'unknown reqid'];

export function isTransientWsError(e: Error): boolean {
return TRANSIENT_WS_ERROR_MARKERS.some((m) => e.message?.toLowerCase().includes(m.toLowerCase()));
}

interface ScryptRequest {
reqid?: number;
type: ScryptRequestType | ScryptMessageType;
Expand Down Expand Up @@ -89,14 +95,18 @@ export class ScryptWebSocketConnection {
}

async fetch<T>(streamName: ScryptMessageType, filters?: Record<string, unknown>): Promise<T[]> {
const response = await this.request({
type: ScryptRequestType.SUBSCRIBE,
streams: [{ name: streamName, ...filters }],
});
const doFetch = async (): Promise<T[]> => {
const response = await this.request({
type: ScryptRequestType.SUBSCRIBE,
streams: [{ name: streamName, ...filters }],
});

if (!response.initial) throw new Error(`Expected initial ${streamName} message`);
if (!response.initial) throw new Error(`Expected initial ${streamName} message`);

return (response.data ?? []) as T[];
return (response.data ?? []) as T[];
};

return this.retryOnTransientWsError(doFetch, `fetch ${streamName}`);
}

async fetchAll<T>(streamName: ScryptMessageType, filters?: Record<string, unknown>): Promise<T[]> {
Expand Down Expand Up @@ -127,13 +137,16 @@ export class ScryptWebSocketConnection {
return allData;
};

// Retry once on connection/session errors
return this.retryOnTransientWsError(doFetch, `fetchAll ${streamName}`);
}

private async retryOnTransientWsError<T>(operation: () => Promise<T>, label: string): Promise<T> {
try {
return await doFetch();
return await operation();
} catch (error) {
if (error.message?.includes('unknown reqid') || error.message?.includes('Connection closed')) {
this.logger.warn(`Retrying fetchAll for ${streamName} after error: ${error.message}`);
return doFetch();
if (isTransientWsError(error)) {
this.logger.warn(`Retrying ${label} after transient error: ${error.message}`);
return operation();
}
throw error;
}
Expand Down
4 changes: 2 additions & 2 deletions src/integration/lightning/lightning-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export class LightningClient implements CoinOnly {
}

async getLndRoutes(publicKey: string, amount: number): Promise<LndRouteDto[]> {
const amountInSat = LightningHelper.btcToSat(amount);
const amountInSat = Math.ceil(LightningHelper.btcToSat(amount));

return this.http
.get<{
Expand Down Expand Up @@ -144,7 +144,7 @@ export class LightningClient implements CoinOnly {
`${Config.blockchain.lightning.lnd.apiUrl}/channels/transactions`,
{
dest: Buffer.from(publicKey, 'hex').toString('base64'),
amt: LightningHelper.btcToSat(amount),
amt: Math.round(LightningHelper.btcToSat(amount)),
final_cltv_delta: 0,
payment_hash: paymentHash,
dest_custom_records: { 5482373484: preImage.toString('base64') },
Expand Down
2 changes: 1 addition & 1 deletion src/integration/lightning/services/lightning.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export class LightningService {
`https://${checkUrl.hostname}/api/v1/payments`,
{
out: false,
amount: amount ? LightningHelper.btcToSat(amount) : 1,
amount: amount ? Math.round(LightningHelper.btcToSat(amount)) : 1,
memo: 'Payment by DFX.swiss',
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Inject, Injectable, forwardRef } from '@nestjs/common';
import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum';
import { ScryptOrderInfo, ScryptOrderSide, ScryptTransactionStatus } from 'src/integration/exchange/dto/scrypt.dto';
import { TradeChangedException } from 'src/integration/exchange/exceptions/trade-changed.exception';
import { isTransientWsError } from 'src/integration/exchange/services/scrypt-websocket-connection';
import { ScryptService } from 'src/integration/exchange/services/scrypt.service';
import { Asset } from 'src/shared/models/asset/asset.entity';
import { AssetService } from 'src/shared/models/asset/asset.service';
Expand Down Expand Up @@ -288,6 +289,11 @@ export class ScryptAdapter extends LiquidityActionAdapter {
return false;
}

if (isTransientWsError(e)) {
this.logger.warn(`Transient WS error checking order ${order.id}, will retry next tick: ${e.message}`);
return false;
}

throw new OrderFailedException(e.message);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export class KycNotificationService {
context: MailContext.KYC_REMINDER,
input: {
userData,
wallet: userData.wallet,
title: `${MailTranslationKey.KYC_REMINDER}.title`,
salutation: { key: `${MailTranslationKey.KYC_REMINDER}.salutation` },
texts: [
Expand Down Expand Up @@ -98,7 +97,6 @@ export class KycNotificationService {
context: MailContext.KYC_FAILED,
input: {
userData,
wallet: userData.wallet,
title: `${MailTranslationKey.KYC_FAILED}.title`,
salutation: { key: `${MailTranslationKey.KYC_FAILED}.salutation`, params: { stepName } },
texts: [
Expand Down Expand Up @@ -140,7 +138,6 @@ export class KycNotificationService {
context: MailContext.KYC_MISSING_DATA,
input: {
userData,
wallet: userData.wallet,
title: `${MailTranslationKey.KYC_MISSING_DATA}.title`,
salutation: { key: `${MailTranslationKey.KYC_MISSING_DATA}.salutation`, params: { stepName } },
texts: [
Expand Down Expand Up @@ -177,7 +174,6 @@ export class KycNotificationService {
context: MailContext.KYC_CHANGED,
input: {
userData,
wallet: userData.wallet,
title: `${MailTranslationKey.KYC_SUCCESS}.title`,
salutation: { key: `${MailTranslationKey.KYC_SUCCESS}.salutation` },
texts: [
Expand Down Expand Up @@ -207,7 +203,6 @@ export class KycNotificationService {
context: MailContext.KYC_PAYMENT_DATA,
input: {
userData,
wallet: userData.wallet,
title: `${MailTranslationKey.KYC_PAYMENT_DATA}.title`,
salutation: { key: `${MailTranslationKey.KYC_PAYMENT_DATA}.salutation` },
texts: [
Expand Down
1 change: 0 additions & 1 deletion src/subdomains/generic/kyc/services/tfa.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ export class TfaService {
context,
input: {
userData: userData,
wallet: userData.wallet,
title: `${MailTranslationKey.VERIFICATION_CODE}.${tag}.title`,
salutation: {
key: `${MailTranslationKey.VERIFICATION_CODE}.${tag}.salutation`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ export class AccountMergeService {
context: MailContext.ACCOUNT_MERGE_REQUEST,
input: {
userData: receiver,
wallet: receiver.wallet,
title: `${MailTranslationKey.ACCOUNT_MERGE_REQUEST}.title`,
salutation: { key: `${MailTranslationKey.ACCOUNT_MERGE_REQUEST}.salutation` },
texts: [
Expand Down
1 change: 0 additions & 1 deletion src/subdomains/generic/user/models/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,6 @@ export class AuthService {
context: MailContext.LOGIN,
input: {
userData,
wallet: userData.wallet,
title: `${MailTranslationKey.LOGIN}.title`,
salutation: { key: `${MailTranslationKey.LOGIN}.salutation` },
texts: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,6 @@ export class RecommendationService {
context: MailContext.RECOMMENDATION_MAIL,
input: {
userData: entity.recommended,
wallet: entity.recommended.wallet,
title: `${MailTranslationKey.RECOMMENDATION_MAIL}.title`,
salutation: { key: `${MailTranslationKey.RECOMMENDATION_MAIL}.salutation` },
texts: [
Expand Down Expand Up @@ -402,7 +401,6 @@ export class RecommendationService {
context: MailContext.RECOMMENDATION_CONFIRMATION,
input: {
userData: entity.recommender,
wallet: entity.recommender.wallet,
title: `${MailTranslationKey.RECOMMENDATION_CONFIRMATION}.title`,
salutation: { key: `${MailTranslationKey.RECOMMENDATION_CONFIRMATION}.salutation` },
texts: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export class UserDataNotificationService {
context: MailContext.ACCOUNT_DEACTIVATION,
input: {
userData,
wallet: userData.wallet,
title: `${MailTranslationKey.ACCOUNT_DEACTIVATION}.title`,
salutation: { key: `${MailTranslationKey.ACCOUNT_DEACTIVATION}.salutation` },
texts: [
Expand Down Expand Up @@ -66,7 +65,6 @@ export class UserDataNotificationService {
context: MailContext.ADDED_ADDRESS,
input: {
userData: master,
wallet: master.wallet,
title: `${MailTranslationKey.ACCOUNT_MERGE_ADDED_ADDRESS}.title`,
salutation: { key: `${MailTranslationKey.ACCOUNT_MERGE_ADDED_ADDRESS}.salutation` },
texts: [
Expand Down Expand Up @@ -105,7 +103,6 @@ export class UserDataNotificationService {
context: MailContext.CHANGED_MAIL,
input: {
userData: master,
wallet: master.wallet,
title: `${MailTranslationKey.ACCOUNT_MERGE_CHANGED_MAIL}.title`,
salutation: { key: `${MailTranslationKey.ACCOUNT_MERGE_CHANGED_MAIL}.salutation` },
texts: [
Expand All @@ -130,7 +127,6 @@ export class UserDataNotificationService {
context: MailContext.CHANGED_MAIL,
input: {
userData: slave,
wallet: slave.wallet,
title: `${MailTranslationKey.ACCOUNT_MERGE_CHANGED_MAIL}.title`,
salutation: { key: `${MailTranslationKey.ACCOUNT_MERGE_CHANGED_MAIL}.salutation` },
texts: [
Expand Down Expand Up @@ -178,7 +174,6 @@ export class UserDataNotificationService {
context: MailContext.BLACK_SQUAD,
input: {
userData: entity,
wallet: entity.wallet,
title: `${MailTranslationKey.BLACK_SQUAD}.title`,
prefix: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ export class BankTxReturnNotificationService {
context: MailContext.BANK_TX_RETURN,
input: {
userData: entity.userData,
wallet: entity.wallet,
title: `${MailTranslationKey.FIAT_CHARGEBACK}.title`,
salutation: { key: `${MailTranslationKey.FIAT_CHARGEBACK}.salutation` },
texts: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Mail } from './base/mail';

export interface MailRequestPersonalInput {
userData: UserData;
wallet: Wallet;
wallet?: Wallet;
bcc?: string;
title: string;
salutation?: TranslationItem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Mail } from './base/mail';

export interface MailRequestUserInputV2 {
userData: UserData;
wallet: Wallet;
wallet?: Wallet;
title: string;
salutation?: TranslationItem;
texts: TranslationItem[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface WalletMailConfig {
template: string;
forcedLang: string; // when set, all UserMailV2 mails for this wallet are rendered in this language regardless of userData.language
centralizedWelcome: boolean; // when true, MailFactory prepends a personal welcome line to every UserMailV2/PersonalMail of this wallet (no need for service callers to add it manually)
isPreferred: boolean;
}

export interface MailOptions {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Injectable } from '@nestjs/common';
import { Config } from 'src/config/config';
import { DfxLogger } from 'src/shared/services/dfx-logger';
import { User } from 'src/subdomains/generic/user/models/user/user.entity';
import { DataSource, In } from 'typeorm';
import { UpdateNotificationDto } from '../dto/update-notification.dto';
import { Notification } from '../entities/notification.entity';
import { MailFactory } from '../factories/mail.factory';
Expand All @@ -15,9 +18,12 @@ export class NotificationService {
private readonly mailFactory: MailFactory,
private readonly mailService: MailService,
private readonly notificationRepo: NotificationRepository,
private readonly dataSource: DataSource,
) {}

async sendMail(request: MailRequest): Promise<void> {
await this.resolveMailWallet(request);

const mail = this.mailFactory.createMail(request);
if (!mail) return;

Expand Down Expand Up @@ -104,4 +110,28 @@ export class NotificationService {
});
if (existingNotification) return newNotification.isSuppressed(existingNotification);
}

private async resolveMailWallet(request: MailRequest): Promise<void> {
const input = request.input;
if (!('userData' in input) || !input.userData?.id) return;

// skip if caller explicitly set a wallet
if (input.wallet) return;

const brandedWalletNames = Object.entries(Config.mail.wallet)
.filter(([, config]) => config.isPreferred)
.map(([name]) => name);

const mailWallet = brandedWalletNames.length
? await this.dataSource
.getRepository(User)
.findOne({
where: { userData: { id: input.userData.id }, wallet: { name: In(brandedWalletNames) } },
relations: { wallet: true },
})
.then((u) => u?.wallet)
: undefined;

input.wallet = mailWallet ?? input.userData.wallet;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ export class TransactionNotificationService {
context: MailContext.UNASSIGNED_TX,
input: {
userData,
wallet: userData.wallet,
title: `${MailTranslationKey.UNASSIGNED_FIAT_INPUT}.title`,
salutation: { key: `${MailTranslationKey.UNASSIGNED_FIAT_INPUT}.salutation` },
texts: [
Expand Down
Loading
Loading