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
9 changes: 9 additions & 0 deletions src/config/chains.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface EvmChainStaticConfig {
swapContractAddress?: string;
quoteContractAddress?: string;
swapFactoryAddress?: string;
swapGatewayAddress?: string;
}

export const EVM_CHAINS = {
Expand Down Expand Up @@ -57,10 +58,18 @@ export const EVM_CHAINS = {
citreaTestnet: {
chainId: 5115,
gatewayUrl: 'https://rpc.testnet.citreascan.com',
swapContractAddress: '0x26C106BC45E0dd599cbDD871605497B2Fc87c185',
swapFactoryAddress: '0xdd6Db52dB41CE2C03002bB1adFdCC8E91C594238',
quoteContractAddress: '0x719a4C7B49E5361a39Dc83c23b353CA220D9B99d',
swapGatewayAddress: '0x8eE3Dd585752805A258ad3a963949a7c3fec44eB',
},
citrea: {
chainId: 4114,
gatewayUrl: 'https://rpc.citreascan.com',
swapContractAddress: '0x565eD3D57fe40f78A46f348C220121AE093c3cF8',
swapFactoryAddress: '0xd809b1285aDd8eeaF1B1566Bf31B2B4C4Bba8e82',
quoteContractAddress: '0x428f20dd8926Eabe19653815Ed0BE7D6c36f8425',
swapGatewayAddress: '0xAFcfD58Fe17BEb0c9D15C51D19519682dFcdaab9',
},
} satisfies Record<string, EvmChainStaticConfig>;

Expand Down
138 changes: 4 additions & 134 deletions src/integration/blockchain/citrea-testnet/citrea-testnet-client.ts
Original file line number Diff line number Diff line change
@@ -1,141 +1,11 @@
import { ethers } from 'ethers';
import ERC20_ABI from 'src/integration/blockchain/shared/evm/abi/erc20.abi.json';
import {
BlockscoutTokenTransfer,
BlockscoutTransaction,
} from 'src/integration/blockchain/shared/blockscout/blockscout.service';
import { Asset } from 'src/shared/models/asset/asset.entity';
import { DfxLogger } from 'src/shared/services/dfx-logger';
import { BlockchainTokenBalance } from '../shared/dto/blockchain-token-balance.dto';
import { Direction, EvmClient, EvmClientParams } from '../shared/evm/evm-client';
import { EvmUtil } from '../shared/evm/evm.util';
import { EvmCoinHistoryEntry, EvmTokenHistoryEntry } from '../shared/evm/interfaces';
import { CitreaBaseClient } from '../shared/evm/citrea-base-client';
import { EvmClientParams } from '../shared/evm/evm-client';

export class CitreaTestnetClient extends EvmClient {
export class CitreaTestnetClient extends CitreaBaseClient {
protected override readonly logger = new DfxLogger(CitreaTestnetClient);

constructor(params: EvmClientParams) {
super({
...params,
alchemyService: undefined, // Citrea not supported by Alchemy
});
}

// Alchemy method overrides
async getNativeCoinBalance(): Promise<number> {
return this.getNativeCoinBalanceForAddress(this.walletAddress);
}

async getNativeCoinBalanceForAddress(address: string): Promise<number> {
const balance = await this.provider.getBalance(address);
return EvmUtil.fromWeiAmount(balance.toString());
}

async getTokenBalance(asset: Asset, address?: string): Promise<number> {
const owner = address ?? this.walletAddress;
const contract = new ethers.Contract(asset.chainId, ERC20_ABI, this.provider);

try {
const balance = await contract.balanceOf(owner);
const decimals = await contract.decimals();
return EvmUtil.fromWeiAmount(balance.toString(), decimals);
} catch (error) {
this.logger.error(`Failed to get token balance for ${asset.chainId}:`, error);
return 0;
}
}

async getTokenBalances(assets: Asset[], address?: string): Promise<BlockchainTokenBalance[]> {
const owner = address ?? this.walletAddress;
const balances: BlockchainTokenBalance[] = [];

for (const asset of assets) {
const balance = await this.getTokenBalance(asset, owner);
balances.push({
owner,
contractAddress: asset.chainId,
balance,
});
}

return balances;
}

// --- HISTORY --- //

async getNativeCoinTransactions(
walletAddress: string,
fromBlock: number,
_toBlock?: number,
direction = Direction.BOTH,
): Promise<EvmCoinHistoryEntry[]> {
const transactions = await this.blockscoutService.getTransactions(this.blockscoutApiUrl, walletAddress, fromBlock);
return this.mapBlockscoutToEvmCoinHistory(transactions, walletAddress, direction);
}

async getERC20Transactions(
walletAddress: string,
fromBlock: number,
_toBlock?: number,
direction = Direction.BOTH,
): Promise<EvmTokenHistoryEntry[]> {
const transfers = await this.blockscoutService.getTokenTransfers(this.blockscoutApiUrl, walletAddress, fromBlock);
return this.mapBlockscoutToEvmTokenHistory(transfers, walletAddress, direction);
}

private mapBlockscoutToEvmCoinHistory(
transactions: BlockscoutTransaction[],
walletAddress: string,
direction: Direction,
): EvmCoinHistoryEntry[] {
const lowerWallet = walletAddress.toLowerCase();

return transactions
.filter((tx) => {
if (direction === Direction.INCOMING) {
return tx.to?.hash.toLowerCase() === lowerWallet;
} else if (direction === Direction.OUTGOING) {
return tx.from.hash.toLowerCase() === lowerWallet;
}
return true; // Direction.BOTH
})
.map((tx) => ({
blockNumber: tx.block_number.toString(),
timeStamp: tx.timestamp,
hash: tx.hash,
from: tx.from.hash,
to: tx.to?.hash || '',
value: tx.value,
contractAddress: '',
}));
}

private mapBlockscoutToEvmTokenHistory(
transfers: BlockscoutTokenTransfer[],
walletAddress: string,
direction: Direction,
): EvmTokenHistoryEntry[] {
const lowerWallet = walletAddress.toLowerCase();

return transfers
.filter((tx) => {
if (direction === Direction.INCOMING) {
return tx.to.hash.toLowerCase() === lowerWallet;
} else if (direction === Direction.OUTGOING) {
return tx.from.hash.toLowerCase() === lowerWallet;
}
return true; // Direction.BOTH
})
.map((tx) => ({
blockNumber: tx.block_number.toString(),
timeStamp: tx.timestamp,
hash: tx.tx_hash,
from: tx.from.hash,
contractAddress: tx.token.address,
to: tx.to.hash,
value: tx.total.value,
tokenName: tx.token.name || tx.token.symbol,
tokenDecimal: tx.total.decimals || tx.token.decimals,
}));
super(params);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export class CitreaTestnetService extends EvmService {
citreaTestnetWalletPrivateKey,
citreaTestnetChainId,
blockscoutApiUrl,
swapContractAddress,
swapFactoryAddress,
quoteContractAddress,
swapGatewayAddress,
} = GetConfig().blockchain.citreaTestnet;

super(CitreaTestnetClient, {
Expand All @@ -24,6 +28,10 @@ export class CitreaTestnetService extends EvmService {
chainId: citreaTestnetChainId,
blockscoutService,
blockscoutApiUrl,
swapContractAddress,
swapFactoryAddress,
quoteContractAddress,
swapGatewayAddress,
});
}
}
138 changes: 4 additions & 134 deletions src/integration/blockchain/citrea/citrea-client.ts
Original file line number Diff line number Diff line change
@@ -1,141 +1,11 @@
import { ethers } from 'ethers';
import {
BlockscoutTokenTransfer,
BlockscoutTransaction,
} from 'src/integration/blockchain/shared/blockscout/blockscout.service';
import ERC20_ABI from 'src/integration/blockchain/shared/evm/abi/erc20.abi.json';
import { Asset } from 'src/shared/models/asset/asset.entity';
import { DfxLogger } from 'src/shared/services/dfx-logger';
import { BlockchainTokenBalance } from '../shared/dto/blockchain-token-balance.dto';
import { Direction, EvmClient, EvmClientParams } from '../shared/evm/evm-client';
import { EvmUtil } from '../shared/evm/evm.util';
import { EvmCoinHistoryEntry, EvmTokenHistoryEntry } from '../shared/evm/interfaces';
import { CitreaBaseClient } from '../shared/evm/citrea-base-client';
import { EvmClientParams } from '../shared/evm/evm-client';

export class CitreaClient extends EvmClient {
export class CitreaClient extends CitreaBaseClient {
protected override readonly logger = new DfxLogger(CitreaClient);

constructor(params: EvmClientParams) {
super({
...params,
alchemyService: undefined, // Citrea not supported by Alchemy
});
}

// Alchemy method overrides
async getNativeCoinBalance(): Promise<number> {
return this.getNativeCoinBalanceForAddress(this.walletAddress);
}

async getNativeCoinBalanceForAddress(address: string): Promise<number> {
const balance = await this.provider.getBalance(address);
return EvmUtil.fromWeiAmount(balance.toString());
}

async getTokenBalance(asset: Asset, address?: string): Promise<number> {
const owner = address ?? this.walletAddress;
const contract = new ethers.Contract(asset.chainId, ERC20_ABI, this.provider);

try {
const balance = await contract.balanceOf(owner);
const decimals = await contract.decimals();
return EvmUtil.fromWeiAmount(balance.toString(), decimals);
} catch (error) {
this.logger.error(`Failed to get token balance for ${asset.chainId}:`, error);
return 0;
}
}

async getTokenBalances(assets: Asset[], address?: string): Promise<BlockchainTokenBalance[]> {
const owner = address ?? this.walletAddress;
const balances: BlockchainTokenBalance[] = [];

for (const asset of assets) {
const balance = await this.getTokenBalance(asset, owner);
balances.push({
owner,
contractAddress: asset.chainId,
balance,
});
}

return balances;
}

// --- HISTORY --- //

async getNativeCoinTransactions(
walletAddress: string,
fromBlock: number,
_toBlock?: number,
direction = Direction.BOTH,
): Promise<EvmCoinHistoryEntry[]> {
const transactions = await this.blockscoutService.getTransactions(this.blockscoutApiUrl, walletAddress, fromBlock);
return this.mapBlockscoutToEvmCoinHistory(transactions, walletAddress, direction);
}

async getERC20Transactions(
walletAddress: string,
fromBlock: number,
_toBlock?: number,
direction = Direction.BOTH,
): Promise<EvmTokenHistoryEntry[]> {
const transfers = await this.blockscoutService.getTokenTransfers(this.blockscoutApiUrl, walletAddress, fromBlock);
return this.mapBlockscoutToEvmTokenHistory(transfers, walletAddress, direction);
}

private mapBlockscoutToEvmCoinHistory(
transactions: BlockscoutTransaction[],
walletAddress: string,
direction: Direction,
): EvmCoinHistoryEntry[] {
const lowerWallet = walletAddress.toLowerCase();

return transactions
.filter((tx) => {
if (direction === Direction.INCOMING) {
return tx.to?.hash.toLowerCase() === lowerWallet;
} else if (direction === Direction.OUTGOING) {
return tx.from.hash.toLowerCase() === lowerWallet;
}
return true; // Direction.BOTH
})
.map((tx) => ({
blockNumber: tx.block_number.toString(),
timeStamp: tx.timestamp,
hash: tx.hash,
from: tx.from.hash,
to: tx.to?.hash || '',
value: tx.value,
contractAddress: '',
}));
}

private mapBlockscoutToEvmTokenHistory(
transfers: BlockscoutTokenTransfer[],
walletAddress: string,
direction: Direction,
): EvmTokenHistoryEntry[] {
const lowerWallet = walletAddress.toLowerCase();

return transfers
.filter((tx) => {
if (direction === Direction.INCOMING) {
return tx.to.hash.toLowerCase() === lowerWallet;
} else if (direction === Direction.OUTGOING) {
return tx.from.hash.toLowerCase() === lowerWallet;
}
return true; // Direction.BOTH
})
.map((tx) => ({
blockNumber: tx.block_number.toString(),
timeStamp: tx.timestamp,
hash: tx.tx_hash,
from: tx.from.hash,
contractAddress: tx.token.address,
to: tx.to.hash,
value: tx.total.value,
tokenName: tx.token.name || tx.token.symbol,
tokenDecimal: tx.total.decimals || tx.token.decimals,
}));
super(params);
}
}
17 changes: 15 additions & 2 deletions src/integration/blockchain/citrea/citrea.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@ import { CitreaClient } from './citrea-client';
@Injectable()
export class CitreaService extends EvmService {
constructor(http: HttpService, blockscoutService: BlockscoutService) {
const { citreaGatewayUrl, citreaApiKey, citreaWalletPrivateKey, citreaChainId, blockscoutApiUrl } =
GetConfig().blockchain.citrea;
const {
citreaGatewayUrl,
citreaApiKey,
citreaWalletPrivateKey,
citreaChainId,
blockscoutApiUrl,
swapContractAddress,
swapFactoryAddress,
quoteContractAddress,
swapGatewayAddress,
} = GetConfig().blockchain.citrea;

super(CitreaClient, {
http,
Expand All @@ -19,6 +28,10 @@ export class CitreaService extends EvmService {
chainId: citreaChainId,
blockscoutService,
blockscoutApiUrl,
swapContractAddress,
swapFactoryAddress,
quoteContractAddress,
swapGatewayAddress,
});
}
}
Loading
Loading