UNPKG

@openzeppelin/contracts-ui-builder-adapter-stellar

Version:
223 lines (180 loc) 8.07 kB
import { Account, BASE_FEE, Contract, rpc as StellarRpc, TransactionBuilder, } from '@stellar/stellar-sdk'; import type { ExecutionConfig, StellarNetworkConfig, TransactionStatusUpdate, TxStatus, } from '@openzeppelin/contracts-ui-builder-types'; import { logger, userRpcConfigService } from '@openzeppelin/contracts-ui-builder-utils'; import { valueToScVal } from '../transform/input-parser'; import { getStellarWalletConnectionStatus, signTransaction } from '../wallet/connection'; import { ExecutionStrategy } from './execution-strategy'; import type { StellarTransactionData } from './formatter'; const SYSTEM_LOG_TAG = 'EoaExecutionStrategy'; /** * Get Soroban RPC Server instance with proper configuration */ function getSorobanRpcServer(networkConfig: StellarNetworkConfig): StellarRpc.Server { const customRpcConfig = userRpcConfigService.getUserRpcConfig(networkConfig.id); const rpcUrl = customRpcConfig?.url || networkConfig.sorobanRpcUrl; if (!rpcUrl) { throw new Error(`No Soroban RPC URL available for network ${networkConfig.name}`); } // Allow HTTP for localhost development const allowHttp = new URL(rpcUrl).hostname === 'localhost'; return new StellarRpc.Server(rpcUrl, { allowHttp, }); } /** * Implements the ExecutionStrategy for a standard Externally Owned Account (EOA). * This strategy involves signing and broadcasting a transaction directly from the user's * connected Stellar wallet, which is the most common way of interacting with Stellar/Soroban contracts. */ export class EoaExecutionStrategy implements ExecutionStrategy { public async execute( transactionData: StellarTransactionData, executionConfig: ExecutionConfig, networkConfig: StellarNetworkConfig, onStatusChange: (status: TxStatus, details: TransactionStatusUpdate) => void, // runtimeApiKey is unused in EOA strategy but required by the interface _runtimeApiKey?: string ): Promise<{ txHash: string }> { logger.info(SYSTEM_LOG_TAG, 'Using Stellar EOA execution strategy'); // Validate execution config is for EOA if (executionConfig.method !== 'eoa') { throw new Error(`Expected EOA execution config, got: ${executionConfig.method}`); } return this.executeEoaTransaction(transactionData, networkConfig, onStatusChange); } private async executeEoaTransaction( txData: StellarTransactionData, stellarConfig: StellarNetworkConfig, onStatusChange: (status: TxStatus, details: TransactionStatusUpdate) => void ): Promise<{ txHash: string }> { try { // --- Step 1: Get RPC Server and Connected Wallet Address --- // const rpcServer = getSorobanRpcServer(stellarConfig); const connectedAddress = this.getConnectedWalletAddress(); logger.info(SYSTEM_LOG_TAG, `Connected address: ${connectedAddress}`); // --- Step 2: Get Account Details --- // let sourceAccount: Account; try { const accountResponse = await rpcServer.getAccount(connectedAddress); sourceAccount = new Account(connectedAddress, accountResponse.sequenceNumber()); } catch (error) { throw new Error(`Failed to load account details: ${(error as Error).message}`); } // --- Step 3: Build Transaction --- // const contract = new Contract(txData.contractAddress); const transactionBuilder = new TransactionBuilder(sourceAccount, { fee: BASE_FEE, networkPassphrase: stellarConfig.networkPassphrase, }); // Add the contract call operation (convert args to ScVal with comprehensive type support) const scValArgs = txData.args.map((arg, index) => { const argType = txData.argTypes[index]; const argSchema = txData.argSchema?.[index]; // Pass schema for struct field type resolution return valueToScVal(arg, argType, argSchema); }); transactionBuilder.addOperation(contract.call(txData.functionName, ...scValArgs)); // Set timeout (default 30 seconds) transactionBuilder.setTimeout(30); let transaction = transactionBuilder.build(); // --- Step 4: Simulate Transaction First --- // try { const simulation = await rpcServer.simulateTransaction(transaction); if (StellarRpc.Api.isSimulationError(simulation)) { throw new Error(`Transaction simulation failed: ${simulation.error}`); } // Prepare the transaction with simulation results transaction = await rpcServer.prepareTransaction(transaction); } catch (error) { throw new Error(`Transaction simulation/preparation failed: ${(error as Error).message}`); } onStatusChange('pendingSignature', {}); // --- Step 5: Sign Transaction --- // try { const signResult = await signTransaction(transaction.toXDR(), connectedAddress); const signedTx = TransactionBuilder.fromXDR( signResult.signedTxXdr, stellarConfig.networkPassphrase ); // Type guard to ensure we have a regular Transaction, not a FeeBumpTransaction if ('memo' in signedTx && 'sequence' in signedTx) { transaction = signedTx; } else { throw new Error('Unexpected transaction type returned from signing'); } } catch (error) { if ((error as Error).message.includes('User declined')) { throw new Error('Transaction was rejected by user'); } throw new Error(`Failed to sign transaction: ${(error as Error).message}`); } onStatusChange('pendingConfirmation', {}); // --- Step 6: Send Transaction --- // let sendResult: StellarRpc.Api.SendTransactionResponse; try { sendResult = await rpcServer.sendTransaction(transaction); } catch (error) { throw new Error(`Failed to broadcast transaction: ${(error as Error).message}`); } if (sendResult.status !== 'PENDING') { throw new Error(`Transaction failed to submit: ${sendResult.status}`); } const txHash = sendResult.hash; logger.info(SYSTEM_LOG_TAG, `Transaction submitted successfully: ${txHash}`); // Status remains 'pendingConfirmation' during confirmation wait // --- Step 7: Wait for Confirmation --- // try { let txResponse; const MAX_ATTEMPTS = 10; let attempts = 0; while (attempts++ < MAX_ATTEMPTS && txResponse?.status !== 'SUCCESS') { await new Promise((resolve) => setTimeout(resolve, 1000)); txResponse = await rpcServer.getTransaction(txHash); switch (txResponse.status) { case 'FAILED': throw new Error(`Transaction failed: ${JSON.stringify(txResponse.resultXdr)}`); case 'NOT_FOUND': continue; case 'SUCCESS': break; default: // Continue waiting } } if (attempts >= MAX_ATTEMPTS || txResponse?.status !== 'SUCCESS') { logger.warn(SYSTEM_LOG_TAG, `Transaction confirmation timeout for ${txHash}`); // Don't throw error, just return the hash - transaction might still succeed } } catch (confirmError) { // Log the error but don't fail the transaction - it was already submitted logger.error(SYSTEM_LOG_TAG, 'Error waiting for confirmation:', confirmError); } onStatusChange('success', { txHash, }); return { txHash }; } catch (error) { const errorMessage = `Failed to execute Stellar EOA transaction: ${(error as Error).message}`; logger.error(SYSTEM_LOG_TAG, errorMessage, error); onStatusChange('error', {}); throw new Error(errorMessage); } } private getConnectedWalletAddress(): string { const connectionStatus = getStellarWalletConnectionStatus(); if (!connectionStatus.isConnected || !connectionStatus.address) { throw new Error('No connected wallet found. Please connect your Stellar wallet first.'); } return connectionStatus.address; } }