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
5 changes: 5 additions & 0 deletions src/integration/blockchain/deuro/deuro.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { LogService } from 'src/subdomains/supporting/log/log.service';
import { PriceCurrency, PricingService } from 'src/subdomains/supporting/pricing/services/pricing.service';
import { FrankencoinService } from '../frankencoin/frankencoin.service';
import { CollateralWithTotalBalance } from '../shared/dto/frankencoin-based.dto';
import { Blockchain } from '../shared/enums/blockchain.enum';
import { EvmUtil } from '../shared/evm/evm.util';
import { FrankencoinBasedService } from '../shared/frankencoin/frankencoin-based.service';
import { BlockchainRegistryService } from '../shared/services/blockchain-registry.service';
Expand All @@ -33,6 +34,10 @@ export class DEuroService extends FrankencoinBasedService implements OnModuleIni
private static readonly LOG_SYSTEM = 'EvmInformation';
private static readonly LOG_SUBSYSTEM = 'DEuroSmartContract';

readonly stableTokenName = 'dEURO';
readonly equityTokenName = 'nDEPS';
readonly blockchain = Blockchain.ETHEREUM;

private deuroClient: DEuroClient;

private frankencoinService: FrankencoinService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { LogSeverity } from 'src/subdomains/supporting/log/log.entity';
import { LogService } from 'src/subdomains/supporting/log/log.service';
import { PriceCurrency, PricingService } from 'src/subdomains/supporting/pricing/services/pricing.service';
import { CollateralWithTotalBalance } from '../shared/dto/frankencoin-based.dto';
import { Blockchain } from '../shared/enums/blockchain.enum';
import { EvmUtil } from '../shared/evm/evm.util';
import { FrankencoinBasedService } from '../shared/frankencoin/frankencoin-based.service';
import { BlockchainRegistryService } from '../shared/services/blockchain-registry.service';
Expand All @@ -29,6 +30,10 @@ export class FrankencoinService extends FrankencoinBasedService implements OnMod
private static readonly LOG_SYSTEM = 'EvmInformation';
private static readonly LOG_SUBSYSTEM = 'FrankencoinSmartContract';

readonly stableTokenName = 'ZCHF';
readonly equityTokenName = 'FPS';
readonly blockchain = Blockchain.ETHEREUM;

private frankencoinClient: FrankencoinClient;

constructor(
Expand Down
4 changes: 4 additions & 0 deletions src/integration/blockchain/juice/juice.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export class JuiceService extends FrankencoinBasedService implements OnModuleIni
private static readonly LOG_SYSTEM = 'EvmInformation';
private static readonly LOG_SUBSYSTEM = 'JuiceSmartContract';

readonly stableTokenName = 'JUSD';
readonly equityTokenName = 'JUICE';
readonly blockchain = Blockchain.CITREA;

private juiceClient: JuiceClient;

constructor(
Expand Down
4 changes: 2 additions & 2 deletions src/integration/blockchain/shared/evm/citrea-base-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export abstract class CitreaBaseClient extends EvmClient {
return tx.hash;
}

override async getSwapResult(txId: string, asset: Asset): Promise<number> {
override async getSwapResult(txId: string, asset: Asset, recipientAddress?: string): Promise<number> {
if (asset.type === AssetType.COIN) {
const receipt = await this.getTxReceipt(txId);
const withdrawalTopic = ethers.utils.id('Withdrawal(address,uint256)');
Expand All @@ -247,7 +247,7 @@ export abstract class CitreaBaseClient extends EvmClient {
return EvmUtil.fromWeiAmount(withdrawalLog.data);
}

return super.getSwapResult(txId, asset);
return super.getSwapResult(txId, asset, recipientAddress);
}

// --- FEE METHODS --- //
Expand Down
4 changes: 2 additions & 2 deletions src/integration/blockchain/shared/evm/evm-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -676,10 +676,10 @@ export abstract class EvmClient extends BlockchainClient {
return { pool, amountOut, sqrtPriceX96After, gasEstimate, route };
}

async getSwapResult(txId: string, asset: Asset): Promise<number> {
async getSwapResult(txId: string, asset: Asset, recipientAddress?: string): Promise<number> {
const receipt = await this.getTxReceipt(txId);

const walletTopic = ethers.utils.hexZeroPad(this.walletAddress.toLowerCase(), 32);
const walletTopic = ethers.utils.hexZeroPad((recipientAddress ?? this.walletAddress).toLowerCase(), 32);

const swapLog = receipt?.logs?.find(
(l) => l.address.toLowerCase() === asset.chainId.toLowerCase() && l.topics[2]?.toLowerCase() === walletTopic,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export abstract class FrankencoinBasedService {
this.registryService = registryService;
}

abstract readonly stableTokenName: string;
abstract readonly equityTokenName: string;
abstract readonly blockchain: Blockchain;

abstract getEquityContract(): Contract;
abstract getWrapperContract(): Contract;
abstract getEquityPrice(): Promise<number>;
Expand Down
162 changes: 162 additions & 0 deletions src/subdomains/core/custody/adapter/equity-order-step.adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Injectable } from '@nestjs/common';
import { ethers } from 'ethers';
import { Config } from 'src/config/config';
import ERC20_ABI from 'src/integration/blockchain/shared/evm/abi/erc20.abi.json';
import { EvmClient } from 'src/integration/blockchain/shared/evm/evm-client';
import { EvmUtil } from 'src/integration/blockchain/shared/evm/evm.util';
import { BlockchainRegistryService } from 'src/integration/blockchain/shared/services/blockchain-registry.service';
import { DfxLogger } from 'src/shared/services/dfx-logger';
import { EquityPairMatch, EquityPairService } from '../services/equity-pair.service';
import { CustodyOrderStep } from '../entities/custody-order-step.entity';
import { CustodyOrderStepCommand, CustodyOrderType } from '../enums/custody';

@Injectable()
export class EquityOrderStepAdapter {
private readonly logger = new DfxLogger(EquityOrderStepAdapter);

constructor(
private readonly blockchainRegistry: BlockchainRegistryService,
private readonly equityPairService: EquityPairService,
) {}

async execute(step: CustodyOrderStep): Promise<string> {
switch (step.command) {
case CustodyOrderStepCommand.CHARGE_CUSTODY:
return this.chargeCustody(step);
case CustodyOrderStepCommand.APPROVE_TOKEN:
return this.approveToken(step);
case CustodyOrderStepCommand.MINT:
return this.mint(step);
case CustodyOrderStepCommand.REDEEM:
return this.redeem(step);
default:
throw new Error(`Unsupported equity step command: ${step.command}`);
}
}

async isComplete(step: CustodyOrderStep): Promise<boolean> {
const client = this.getEvmClient(step);
return client.isTxComplete(step.correlationId);
}

async getOutputAmount(step: CustodyOrderStep): Promise<number> {
const client = this.getEvmClient(step);
const custodyAddress = this.getCustodyWalletAddress(step);

return client.getSwapResult(step.correlationId, step.order.inputAsset, custodyAddress);
}

// --- COMMANDS --- //

private async chargeCustody(step: CustodyOrderStep): Promise<string> {
const client = this.getEvmClient(step);
const custodyAddress = this.getCustodyWalletAddress(step);

const gasPrice = await client.getRecommendedGasPrice();
const isMint = step.order.type === CustodyOrderType.EQUITY_MINT;
const gasUnits = isMint ? 400000 : 200000; // approve + mint, or just redeem
const gasAmount = EvmUtil.fromWeiAmount(gasPrice.mul(gasUnits));
const chargeAmount = gasAmount * 1.5; // 50% buffer

return client.sendNativeCoinFromDex(custodyAddress, chargeAmount);
}

private async approveToken(step: CustodyOrderStep): Promise<string> {
const client = this.getEvmClient(step);
const custodyAccount = Config.blockchain.evm.custodyAccount(step.order.user.custodyAddressIndex);
const { config } = this.getEquityPair(step);

const stableAsset = step.order.outputAsset;
const equityContractAddress = config.service.getEquityContract().address;

const erc20Iface = new ethers.utils.Interface(ERC20_ABI);
const data = erc20Iface.encodeFunctionData('approve', [equityContractAddress, ethers.constants.MaxUint256]);

const tx = await client.sendRawTransactionFromAccount(custodyAccount, {
to: stableAsset.chainId,
data,
value: 0,
gasLimit: ethers.BigNumber.from(100000),
});

return tx.hash;
}

private async mint(step: CustodyOrderStep): Promise<string> {
const client = this.getEvmClient(step);
const custodyAccount = Config.blockchain.evm.custodyAccount(step.order.user.custodyAddressIndex);
const { config } = this.getEquityPair(step);

const stableAsset = step.order.outputAsset;
const amount = step.order.outputAmount;
const weiAmount = EvmUtil.toWeiAmount(amount, stableAsset.decimals);

const equityContract = config.service.getEquityContract();
const expectedShares = await equityContract.calculateShares(weiAmount);
const minShares = expectedShares.mul(98).div(100); // 2% slippage tolerance

const data = equityContract.interface.encodeFunctionData('invest', [weiAmount, minShares]);

const tx = await client.sendRawTransactionFromAccount(custodyAccount, {
to: equityContract.address,
data,
value: 0,
gasLimit: ethers.BigNumber.from(300000),
});

return tx.hash;
}

private async redeem(step: CustodyOrderStep): Promise<string> {
const client = this.getEvmClient(step);
const custodyAccount = Config.blockchain.evm.custodyAccount(step.order.user.custodyAddressIndex);
const { config } = this.getEquityPair(step);

const equityAsset = step.order.outputAsset;
const amount = step.order.outputAmount;
const weiAmount = EvmUtil.toWeiAmount(amount, equityAsset.decimals);

const custodyAddress = this.getCustodyWalletAddress(step);
const equityContract = config.service.getEquityContract();

const equityPrice = await config.service.getEquityPrice();
const expectedProceeds = EvmUtil.toWeiAmount(amount * equityPrice * 0.98, 18); // 2% slippage tolerance

const data = equityContract.interface.encodeFunctionData('redeemExpected', [
custodyAddress,
weiAmount,
expectedProceeds,
]);

const tx = await client.sendRawTransactionFromAccount(custodyAccount, {
to: equityContract.address,
data,
value: 0,
gasLimit: ethers.BigNumber.from(300000),
});

return tx.hash;
}

// --- HELPERS --- //

private getEquityPair(step: CustodyOrderStep): EquityPairMatch {
const sourceAsset = step.order.outputAsset;
const targetAsset = step.order.inputAsset;

const pair = this.equityPairService.getEquityPairConfig(sourceAsset.name, targetAsset.name);
if (!pair) throw new Error(`No equity pair found for ${sourceAsset.name} -> ${targetAsset.name}`);

return pair;
}

private getEvmClient(step: CustodyOrderStep): EvmClient {
const { config } = this.getEquityPair(step);
return this.blockchainRegistry.getEvmClient(config.blockchain);
}

private getCustodyWalletAddress(step: CustodyOrderStep): string {
const custodyAccount = Config.blockchain.evm.custodyAccount(step.order.user.custodyAddressIndex);
return EvmUtil.createWallet(custodyAccount).address;
}
}
9 changes: 9 additions & 0 deletions src/subdomains/core/custody/config/order-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ export const OrderConfig: {
{ context: CustodyOrderStepContext.DFX, command: CustodyOrderStepCommand.CHARGE_ROUTE },
{ context: CustodyOrderStepContext.DFX, command: CustodyOrderStepCommand.SEND_TO_ROUTE },
],
[CustodyOrderType.EQUITY_MINT]: [
{ context: CustodyOrderStepContext.EQUITY, command: CustodyOrderStepCommand.CHARGE_CUSTODY },
{ context: CustodyOrderStepContext.EQUITY, command: CustodyOrderStepCommand.APPROVE_TOKEN },
{ context: CustodyOrderStepContext.EQUITY, command: CustodyOrderStepCommand.MINT },
],
[CustodyOrderType.EQUITY_REDEEM]: [
{ context: CustodyOrderStepContext.EQUITY, command: CustodyOrderStepCommand.CHARGE_CUSTODY },
{ context: CustodyOrderStepContext.EQUITY, command: CustodyOrderStepCommand.REDEEM },
],
[CustodyOrderType.SAVING_DEPOSIT]: [],
[CustodyOrderType.SAVING_WITHDRAWAL]: [],
};
4 changes: 4 additions & 0 deletions src/subdomains/core/custody/custody.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { BuyCryptoModule } from '../buy-crypto/buy-crypto.module';
import { ReferralModule } from '../referral/referral.module';
import { SellCryptoModule } from '../sell-crypto/sell-crypto.module';
import { DfxOrderStepAdapter } from './adapter/dfx-order-step.adapter';
import { EquityOrderStepAdapter } from './adapter/equity-order-step.adapter';
import { EquityPairService } from './services/equity-pair.service';
import { CustodyAdminController, CustodyController } from './controllers/custody.controller';
import { CustodyAccountController } from './controllers/custody-account.controller';
import { CustodyBalance } from './entities/custody-balance.entity';
Expand Down Expand Up @@ -46,6 +48,8 @@ import { CustodyAccountReadGuard, CustodyAccountWriteGuard } from './guards/cust
CustodyOrderRepository,
CustodyOrderStepRepository,
DfxOrderStepAdapter,
EquityOrderStepAdapter,
EquityPairService,
CustodyOrderService,
CustodyJobService,
CustodyPdfService,
Expand Down
8 changes: 8 additions & 0 deletions src/subdomains/core/custody/enums/custody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export enum CustodyOrderType {

SWAP = 'Swap',

EQUITY_MINT = 'EquityMint',
EQUITY_REDEEM = 'EquityRedeem',

SAVING_DEPOSIT = 'SavingDeposit',
SAVING_WITHDRAWAL = 'SavingWithdrawal',
}
Expand All @@ -33,11 +36,16 @@ export enum CustodyOrderStepStatus {

export enum CustodyOrderStepContext {
DFX = 'DFX',
EQUITY = 'Equity',
}

export enum CustodyOrderStepCommand {
CHARGE_ROUTE = 'ChargeRoute',
SEND_TO_ROUTE = 'SendToRoute',
CHARGE_CUSTODY = 'ChargeCustody',
APPROVE_TOKEN = 'ApproveToken',
MINT = 'Mint',
REDEEM = 'Redeem',
}

// accounts
Expand Down
Loading
Loading