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
2 changes: 1 addition & 1 deletion apps/shell/messages/common/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"unbonding-amount": "Unbonding: {amount}",
"undelegate": "Undelegate",
"unsupported-network-message": "Your current action cannot be performed as the application is connected to an unsupported network. Please select one of the supported networks from the list below to proceed.",
"unsupported-network-title": "Unsupported <br /> Network",
"unsupported-network-title": "Unsupported Network",
"version": "version: ",
"vested-amount": "Vested: {amount}",
"vote-option-abstain": "Abstain",
Expand Down
21 changes: 21 additions & 0 deletions apps/shell/src/app/[locale]/bridge/deploy-token/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
HydrationBoundary,
QueryClient,
dehydrate,
} from '@tanstack/react-query';
import { TokenDeploymentPage } from '@haqq/shell-bridge';

export const dynamic = 'force-dynamic';
export const fetchCache = 'force-no-store';

export default async function TokenDeployment() {
const queryClient = new QueryClient();

const dehydratedState = dehydrate(queryClient);

return (
<HydrationBoundary state={dehydratedState}>
<TokenDeploymentPage />
</HydrationBoundary>
);
}
21 changes: 21 additions & 0 deletions apps/shell/src/app/[locale]/bridge/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
HydrationBoundary,
QueryClient,
dehydrate,
} from '@tanstack/react-query';
import { BridgePage } from '@haqq/shell-bridge';

export const dynamic = 'force-dynamic';
export const fetchCache = 'force-no-store';

export default async function ValidatorList() {
const queryClient = new QueryClient();

const dehydratedState = dehydrate(queryClient);

return (
<HydrationBoundary state={dehydratedState}>
<BridgePage />
</HydrationBoundary>
);
}
5 changes: 5 additions & 0 deletions apps/shell/src/app/[locale]/bridge/recovery/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { WithdrawalRecoveryPage } from '@haqq/shell-bridge';

export default function RecoveryPage() {
return <WithdrawalRecoveryPage />;
}
159 changes: 159 additions & 0 deletions apps/shell/src/app/api/tokens/balances/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { NextRequest, NextResponse } from 'next/server';
import { haqqMainnet, haqqTestedge2, sepolia } from 'viem/chains';
import { haqqTestethic } from '@haqq/shell-shared';

export interface TokenBalance {
symbol: string;
address: string;
name: string;
balance: string;
decimals: number;
formattedBalance: number;
}

export interface ExplorerToken {
value: string;
token: {
symbol: string;
address_hash?: string;
address?: string;
name: string;
decimals: string;
};
}

export interface ExplorerApiResponse {
items: ExplorerToken[];
}

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const address = searchParams.get('address');
const chainId = searchParams.get('chainId');

if (!address || !chainId) {
return NextResponse.json(
{ error: 'Missing required parameters: address and chainId' },
{ status: 400 },
);
}

// Get chain configuration
const chainConfig = getChainConfig(Number(chainId));
if (!chainConfig) {
return NextResponse.json(
{ error: `Unsupported chain ID: ${chainId}` },
{ status: 400 },
);
}

// Make the API request to the explorer
const url = `${chainConfig.apiUrl}/v2/addresses/${address}/tokens?type=ERC-20`;

console.log('Explorer API URL:', url);

const response = await fetch(url, {
headers: {
Accept: 'application/json',
},
});

console.log('Explorer API response status:', response.status);

if (!response.ok) {
console.log('Explorer API error:', response.status, response.statusText);

return NextResponse.json(
{
error: `Explorer API error: ${response.status} ${response.statusText}`,
},
{ status: response.status },
);
}

const data = (await response.json()) as ExplorerApiResponse;

console.log('Explorer API data items count:', data.items?.length || 0);

// Filter out tokens with zero balance
const filteredTokens: ExplorerToken[] = data.items.filter((item) => {
return item.value !== '0' && item.value !== '0x0';
});

console.log(
`Found ${filteredTokens.length} tokens with non-zero balance out of ${data.items.length} total tokens`,
);

// Process token balances
const tokenBalances: TokenBalance[] = filteredTokens.map((item) => {
const decimals = Number(item.token.decimals);
const balanceWei = BigInt(item.value);
const formattedBalance = Number(balanceWei) / Math.pow(10, decimals);

return {
symbol: item.token.symbol,
address: item.token.address_hash || item.token.address || '',
name: item.token.name,
balance: item.value,
decimals,
formattedBalance,
};
});

console.log(
'Processed token balances:',
tokenBalances.map((t) => {
return `${t.symbol}: ${t.formattedBalance}`;
}),
);

return NextResponse.json({
tokens: tokenBalances,
total: filteredTokens.length,
});
} catch (error) {
console.error('Error in token balances API:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 },
);
}
}

interface ChainConfig {
apiUrl: string;
nativeSymbol: string;
nativeName: string;
}

function getChainConfig(chainId: number): ChainConfig | null {
const configs: Record<number, ChainConfig> = {
[haqqTestethic.id]: {
// HAQQ Devnet1
apiUrl: haqqTestethic.blockExplorers.default.apiUrl,
nativeSymbol: 'ISLM',
nativeName: 'Islamic Coin',
},
[sepolia.id]: {
// Sepolia
apiUrl: 'https://eth-sepolia.blockscout.com/api',
nativeSymbol: 'ETH',
nativeName: 'Ethereum',
},
[haqqMainnet.id]: {
// HAQQ Mainnet
apiUrl: haqqMainnet.blockExplorers.default.apiUrl,
nativeSymbol: 'ISLM',
nativeName: 'Islamic Coin',
},
[haqqTestedge2.id]: {
// HAQQ Testedge2
apiUrl: haqqTestedge2.blockExplorers.default.apiUrl,
nativeSymbol: 'ISLM',
nativeName: 'Islamic Coin',
},
};

return configs[chainId] || null;
}
104 changes: 104 additions & 0 deletions apps/shell/src/app/api/tokens/native-balance/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { NextRequest, NextResponse } from 'next/server';
import { createPublicClient, http, formatEther, Chain } from 'viem';
import { haqqMainnet, haqqTestedge2, sepolia } from 'viem/chains';
import { haqqTestethic } from '@haqq/shell-shared';

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const address = searchParams.get('address');
const chainId = searchParams.get('chainId');

if (!address || !chainId) {
return NextResponse.json(
{ error: 'Missing required parameters: address and chainId' },
{ status: 400 },
);
}

// Get chain configuration
const chainConfig = getChainConfig(parseInt(chainId));
if (!chainConfig) {
return NextResponse.json(
{ error: `Unsupported chain ID: ${chainId}` },
{ status: 400 },
);
}

// Create RPC client for the chain
const client = createPublicClient({
chain: chainConfig.chain,
transport: http(chainConfig.rpcUrl),
});

try {
// Get native token balance using RPC
const balance = await client.getBalance({
address: address as `0x${string}`,
});

const formattedBalance = parseFloat(formatEther(balance));

const nativeToken = {
symbol: chainConfig.nativeSymbol,
address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', // ETH placeholder
name: chainConfig.nativeName,
balance: balance.toString(),
decimals: 18,
formattedBalance,
};

return NextResponse.json({
token: nativeToken,
});
} catch {
return NextResponse.json(
{ error: 'Failed to fetch native token balance via RPC' },
{ status: 500 },
);
}
} catch {
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 },
);
}
}

interface ChainConfig {
chain: Chain;
rpcUrl: string;
nativeSymbol: string;
nativeName: string;
}

function getChainConfig(chainId: number): ChainConfig | null {
const configs: Record<number, ChainConfig> = {
[haqqTestethic.id]: {
chain: haqqTestethic,
rpcUrl: haqqTestethic.rpcUrls.default.http[0],
nativeSymbol: 'ETH',
nativeName: 'Ethereum',
},
[sepolia.id]: {
chain: sepolia,
rpcUrl: sepolia.rpcUrls.default.http[0],
nativeSymbol: 'ETH',
nativeName: 'Ethereum',
},
[haqqMainnet.id]: {
chain: haqqMainnet,
rpcUrl: haqqMainnet.rpcUrls.default.http[0],
nativeSymbol: 'ISLM',
nativeName: 'Islamic Coin',
},
[haqqTestedge2.id]: {
chain: haqqTestedge2,
rpcUrl: haqqTestedge2.rpcUrls.default.http[0],
nativeSymbol: 'ISLM',
nativeName: 'Islamic Coin',
},
};

return configs[chainId] || null;
}
Loading
Loading