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
3 changes: 3 additions & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.71.1",
"@types/lodash": "^4.17.16",
"@wagmi/connectors": "^5.8.5",
"@wagmi/core": "^2.17.3",
"@web3icons/react": "^4.0.13",
"axios": "^1.8.4",
"bs58": "^6.0.0",
Expand All @@ -43,6 +45,7 @@
"html-to-image": "^1.11.13",
"html2canvas": "^1.4.1",
"json5": "^2.2.3",
"keyv": "4.5.4",
"lodash": "^4.17.21",
"lucide-react": "^0.485.0",
"next": "^15.3.1",
Expand Down
217 changes: 217 additions & 0 deletions packages/app/src/hooks/adapters/AptosChainAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import {
Aptos,
AptosConfig,
Network,
TypeTagAddress,
TypeTagU64,
U64,
Account
} from "@aptos-labs/ts-sdk";
import { BaseChainAdapter } from "./base/BaseChainAdapter";
import type { AptosChainConfig } from "@/aptos/config";
import type {
ChainType,
ChainBalance,
PreparedTransaction,
TransactionResult,
TransactionReceipt
} from "./base/ChainAdapter.interface";

export interface AptosWalletState {
account: Account | null;
address: string | null;
isReady: boolean;
}

export class AptosChainAdapter extends BaseChainAdapter {
private aptosConfig: AptosChainConfig;
private walletState: AptosWalletState;

constructor(config: AptosChainConfig, walletState: AptosWalletState) {
super({
chainId: config.id,
name: config.name,
color: config.color,
logo: config.logo,
testnet: config.testnet,
layer: config.layer
});

this.aptosConfig = config;
this.walletState = walletState;
}

get chainType(): ChainType {
return 'aptos';
}

isWalletReady(): boolean {
return this.walletState.isReady && !!this.walletState.account;
}

getWalletAddress(): string {
return this.walletState.address || '';
}

async checkBalance(): Promise<ChainBalance> {
if (!this.walletState.account || !this.walletState.isReady) {
return this.createErrorBalance("Aptos wallet still loading...");
}

return this.withTimeout(
this.withRetry(async () => {
const aptosConfig = new AptosConfig({
network: this.aptosConfig.network as Network,
fullnode: this.aptosConfig.rpcUrl,
});
const aptos = new Aptos(aptosConfig);

const balance = BigInt(await aptos.getAccountAPTAmount({
accountAddress: this.walletState.account!.accountAddress
}));

// Minimum balance threshold: 0.001 APT (100,000 octas since APT uses 8 decimals)
const hasBalance = balance > BigInt(100_000);

return {
chainId: this.chainId,
balance,
hasBalance,
};
})
).catch(error => this.createErrorBalance(this.formatError(error)));
}

async prepareTransactions(count: number): Promise<PreparedTransaction[]> {
if (!this.walletState.account) {
throw new Error(`Aptos wallet not ready for ${this.aptosConfig.id}`);
}

const aptosConfig = new AptosConfig({
network: this.aptosConfig.network as Network,
fullnode: this.aptosConfig.rpcUrl,
});
const aptos = new Aptos(aptosConfig);

// Fetch sequence number for the account
const accountData = await aptos.getAccountInfo({
accountAddress: this.walletState.account.accountAddress
});
const sequenceNumber = BigInt(accountData.sequence_number);

// Build and sign all transactions
const buildAndSignTransaction = async (txIndex: number, aptosSeqNo: bigint) => {
const transaction = await aptos.transaction.build.simple({
sender: this.walletState.account!.accountAddress,
data: {
function: "0x1::aptos_account::transfer",
functionArguments: [this.walletState.account!.accountAddress, new U64(0)], // Transfer 0 APT to self
abi: {
signers: 1,
typeParameters: [],
parameters: [new TypeTagAddress(), new TypeTagU64()]
}
},
options: {
accountSequenceNumber: aptosSeqNo + BigInt(txIndex),
gasUnitPrice: 100, // Default gas price
maxGasAmount: 1000, // Set a max gas
}
});

return {
transaction,
senderAuthenticator: aptos.transaction.sign({
signer: this.walletState.account!,
transaction,
})
};
};

const signedTransactionPromises = [];
for (let txIndex = 0; txIndex < count; txIndex++) {
signedTransactionPromises.push(buildAndSignTransaction(txIndex, sequenceNumber));
}

const signedTransactions = await Promise.all(signedTransactionPromises);

return signedTransactions.map((signedTx, index) => ({
index,
data: {
signedTransaction: signedTx,
aptos,
}
}));
}

async executeTransaction(tx: PreparedTransaction): Promise<TransactionResult> {
const startTime = Date.now();

try {
if (!tx.data.signedTransaction || !tx.data.aptos) {
throw new Error(`No pre-signed transaction available for Aptos tx #${tx.index}`);
}

if (typeof tx.data.signedTransaction !== "object" || !("senderAuthenticator" in tx.data.signedTransaction)) {
throw new Error(`Invalid signed transaction for Aptos tx #${tx.index}`);
}

const response = await tx.data.aptos.transaction.submit.simple(tx.data.signedTransaction);

// Wait for transaction confirmation
await tx.data.aptos.waitForTransaction({
transactionHash: response.hash,
options: {
waitForIndexer: false
}
});

const latency = Date.now() - startTime;

const hash = response.hash.startsWith('0x')
? response.hash as `0x${string}`
: `0x${response.hash}` as `0x${string}`;

return {
hash,
latency,
success: true
};
} catch (error) {
const latency = Date.now() - startTime;
return {
latency,
success: false,
error: this.formatAptosError(error)
};
}
}

// Aptos transactions are already confirmed when executed
async waitForConfirmation(txHash: string): Promise<TransactionReceipt> {
return {
hash: txHash,
confirmed: true
};
}

private formatAptosError(error: unknown): string {
if (error instanceof Error) {
const message = error.message;

if (message.includes("insufficient funds")) {
return "Insufficient APT for transaction fees";
}
if (message.includes("timeout")) {
return "Aptos network timeout - please try again";
}
if (message.includes("SEQUENCE_NUMBER_TOO_OLD")) {
return "Transaction sequence error - please try again";
}

return message.split('\n')[0] || message;
}

return String(error);
}
}
92 changes: 92 additions & 0 deletions packages/app/src/hooks/adapters/ChainAdapterFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { AnyChainConfig } from "@/chain/networks";
import type { SolanaChainConfig } from "@/solana/config";
import type { FuelChainConfig } from "@/fuel/config";
import type { AptosChainConfig } from "@/aptos/config";
import type { SoonChainConfig } from "@/soon/config";
import type { StarknetChainConfig } from "@/starknet/config";
import type { Chain } from "viem";

import { ChainAdapter } from "./base/ChainAdapter.interface";
import { EvmChainAdapter, type EvmWalletState } from "./EvmChainAdapter";
import { SolanaChainAdapter, type SolanaWalletState } from "./SolanaChainAdapter";
import { FuelChainAdapter, type FuelWalletState } from "./FuelChainAdapter";
import { AptosChainAdapter, type AptosWalletState } from "./AptosChainAdapter";
import { SoonChainAdapter, type SoonWalletState } from "./SoonChainAdapter";
import { StarknetChainAdapter, type StarknetWalletState } from "./StarknetChainAdapter";

// Helper functions to distinguish chain types (copied from original useChainRace)
function isEvmChain(chain: AnyChainConfig): chain is Chain & { testnet: boolean; color: string; logo: string; faucetUrl?: string; layer: 'L1' | 'L2'; } {
return 'id' in chain && typeof chain.id === 'number';
}

function isSolanaChain(chain: AnyChainConfig): chain is SolanaChainConfig {
return 'cluster' in chain;
}

function isFuelChain(chain: AnyChainConfig): chain is FuelChainConfig {
return chain.name === "Fuel Testnet" || chain.name === "Fuel Mainnet";
}

function isAptosChain(chain: AnyChainConfig): chain is AptosChainConfig {
return 'network' in chain && typeof chain.network === 'string' &&
('id' in chain && typeof chain.id === 'string' && chain.id.startsWith('aptos-'));
}

function isSoonChain(chain: AnyChainConfig): chain is SoonChainConfig {
return 'id' in chain && typeof chain.id === 'string' && chain.id.startsWith('soon-');
}

function isStarknetChain(chain: AnyChainConfig): chain is StarknetChainConfig {
return chain.id === "starknet-testnet" || chain.id === "starknet-mainnet";
}

export interface WalletStates {
evm: EvmWalletState;
solana: SolanaWalletState;
fuel: FuelWalletState;
aptos: AptosWalletState;
soon: SoonWalletState;
starknet: StarknetWalletState;
}

export class ChainAdapterFactory {
static create(chain: AnyChainConfig, walletStates: WalletStates): ChainAdapter {
if (isEvmChain(chain)) {
return new EvmChainAdapter({
chainId: chain.id,
name: chain.name,
color: chain.color,
logo: chain.logo,
testnet: chain.testnet,
layer: chain.layer,
chain: chain as Chain
}, walletStates.evm);
}

if (isSolanaChain(chain)) {
return new SolanaChainAdapter(chain, walletStates.solana);
}

if (isFuelChain(chain)) {
return new FuelChainAdapter(chain, walletStates.fuel);
}

if (isAptosChain(chain)) {
return new AptosChainAdapter(chain, walletStates.aptos);
}

if (isSoonChain(chain)) {
return new SoonChainAdapter(chain, walletStates.soon);
}

if (isStarknetChain(chain)) {
return new StarknetChainAdapter(chain, walletStates.starknet);
}

throw new Error(`Unsupported chain type: ${(chain as any).name}`);
}

static createMultiple(chains: AnyChainConfig[], walletStates: WalletStates): ChainAdapter[] {
return chains.map(chain => this.create(chain, walletStates));
}
}
Loading