UNPKG

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

Version:
385 lines (337 loc) 12.6 kB
import { Account, Address, BASE_FEE, Contract, Keypair, nativeToScVal, rpc as StellarRpc, TransactionBuilder, } from '@stellar/stellar-sdk'; import type { ContractSchema, FunctionParameter, NetworkConfig, StellarNetworkConfig, } from '@openzeppelin/contracts-ui-builder-types'; import { logger, userRpcConfigService } from '@openzeppelin/contracts-ui-builder-utils'; import { parseStellarInput } from '../transform'; import { formatStellarFunctionResult } from '../transform/output-formatter'; import { convertStellarTypeToScValType } from '../utils'; import { isStellarViewFunction } from './view-checker'; /** * Private helper to get a Soroban RPC Server instance for view queries. * Prioritizes custom RPC configuration, then falls back to network default. */ function getSorobanRpcServer(networkConfig: StellarNetworkConfig): StellarRpc.Server { // Check if there's a custom RPC configuration 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}`); } logger.info( 'getSorobanRpcServer', `Creating Soroban RPC server for ${networkConfig.name} using RPC: ${rpcUrl}` ); // Allow HTTP for localhost development const allowHttp = new URL(rpcUrl).hostname === 'localhost'; return new StellarRpc.Server(rpcUrl, { allowHttp, }); } /** * Creates a dummy transaction to simulate a contract function call. * This is used for view functions that don't modify state. */ async function createSimulationTransaction( contractAddress: string, functionName: string, args: unknown[], paramTypes: string[], networkConfig: StellarNetworkConfig ): Promise<TransactionBuilder> { try { // Create a dummy source account for simulation // We'll use a well-known testnet account for simulation const dummyKeypair = Keypair.random(); const sourceAccount = new Account(dummyKeypair.publicKey(), '0'); // Create contract instance const contract = new Contract(contractAddress); // Convert args to ScVal with proper type hints const scValArgs = args.map((arg, index) => { const paramType = paramTypes[index]; if (!paramType) { // Fallback to direct conversion if type info missing return nativeToScVal(arg); } // Special cases that don't need type hints if (paramType === 'Bool' || paramType === 'Bytes' || paramType.match(/^BytesN<\d+>$/)) { return nativeToScVal(arg); } // Use common utility for type conversion with hints const typeHint = convertStellarTypeToScValType(paramType); return nativeToScVal(arg, { type: typeHint }); }); // Build the transaction for simulation const transaction = new TransactionBuilder(sourceAccount, { fee: BASE_FEE, networkPassphrase: networkConfig.networkPassphrase, }) .addOperation(contract.call(functionName, ...scValArgs)) .setTimeout(30); return transaction; } catch (error) { logger.error('createSimulationTransaction', 'Failed to create simulation transaction:', error); throw new Error(`Failed to create simulation transaction: ${(error as Error).message}`); } } /** * Determines if a Stellar contract function modifies state by simulating it * and checking for state changes. * * @param contractAddress Address of the contract. * @param functionName Name of the function to check. * @param networkConfig The specific network configuration. * @param inputTypes Parameter types for the function (empty array for parameterless functions). * @returns True if the function modifies state, false if it's read-only. */ export async function checkStellarFunctionStateMutability( contractAddress: string, functionName: string, networkConfig: StellarNetworkConfig, inputTypes: string[] = [] ): Promise<boolean> { logger.info( 'checkStellarFunctionStateMutability', `Checking state mutability for function: ${functionName} on ${contractAddress}` ); try { // --- Validate Contract Address --- // try { Address.fromString(contractAddress); } catch { throw new Error(`Invalid Stellar contract address provided: ${contractAddress}`); } // --- Get Soroban RPC Server --- // const rpcServer = getSorobanRpcServer(networkConfig); // --- Create dummy parameters for simulation --- // // For state mutability detection, we can use dummy values since we only care about state changes const dummyArgs = inputTypes.map((paramType) => { // Create minimal dummy values for different types switch (paramType) { case 'Bool': return false; case 'I32': case 'U32': case 'I64': case 'U64': case 'I128': case 'U128': case 'I256': case 'U256': return 0; case 'String': return ''; case 'Bytes': return Buffer.alloc(0); case 'Address': // Use the contract address as a dummy address return contractAddress; default: // For complex types, use null which often gets converted to appropriate defaults return null; } }); // --- Create Simulation Transaction --- // const transactionBuilder = await createSimulationTransaction( contractAddress, functionName, dummyArgs, inputTypes, networkConfig ); const transaction = transactionBuilder.build(); logger.debug( 'checkStellarFunctionStateMutability', `[Check ${functionName}] Simulating transaction for state mutability check` ); // --- Simulate Transaction --- // let simulationResult: StellarRpc.Api.SimulateTransactionResponse; try { simulationResult = await rpcServer.simulateTransaction(transaction); } catch (simulationError) { logger.warn( 'checkStellarFunctionStateMutability', `[Check ${functionName}] Simulation failed, assuming function modifies state:`, simulationError ); // If simulation fails, err on the side of caution and assume it modifies state return true; } // --- Process Simulation Result --- // if (StellarRpc.Api.isSimulationError(simulationResult)) { logger.warn( 'checkStellarFunctionStateMutability', `[Check ${functionName}] Simulation error, assuming function modifies state:`, simulationResult.error ); // If there's a simulation error, assume it modifies state for safety return true; } // --- Check State Changes (following Laboratory approach) --- // const hasStateChanges = simulationResult.stateChanges && simulationResult.stateChanges.length > 0; logger.info( 'checkStellarFunctionStateMutability', `[Check ${functionName}] State mutability check complete:`, { hasStateChanges, stateChangesCount: simulationResult.stateChanges?.length || 0, modifiesState: Boolean(hasStateChanges), } ); return Boolean(hasStateChanges); } catch (error) { logger.warn( 'checkStellarFunctionStateMutability', `Failed to check state mutability for ${functionName}, assuming it modifies state:`, error ); // If anything goes wrong, assume the function modifies state as a safe default return true; } } /** * Core logic for querying a Stellar view function. * * @param contractAddress Address of the contract. * @param functionId ID of the function to query. * @param networkConfig The specific network configuration. * @param params Raw parameters for the function call. * @param contractSchema Optional pre-loaded contract schema. * @param loadContractFn Function reference to load contract schema if not provided. * @returns The decoded result of the view function call. */ export async function queryStellarViewFunction( contractAddress: string, functionId: string, networkConfig: NetworkConfig, params: unknown[] = [], contractSchema?: ContractSchema, loadContractFn?: (source: string) => Promise<ContractSchema> ): Promise<unknown> { logger.info( 'queryStellarViewFunction', `Querying Stellar view function: ${functionId} on ${contractAddress} (${networkConfig.name})`, { params } ); if (networkConfig.ecosystem !== 'stellar') { throw new Error('Invalid network configuration for Stellar query.'); } const stellarConfig = networkConfig as StellarNetworkConfig; try { // --- Validate Contract Address --- // try { Address.fromString(contractAddress); } catch { throw new Error(`Invalid Stellar contract address provided: ${contractAddress}`); } // --- Get Soroban RPC Server --- // const rpcServer = getSorobanRpcServer(stellarConfig); // --- Get Schema & Function Details --- // let schema = contractSchema; if (!schema && loadContractFn) { schema = await loadContractFn(contractAddress); } if (!schema) { throw new Error( `Contract schema not provided and loadContractFn not available for ${contractAddress}` ); } const functionDetails = schema.functions.find((fn) => fn.id === functionId); if (!functionDetails) { throw new Error(`Function with ID ${functionId} not found in contract schema.`); } if (!isStellarViewFunction(functionDetails)) { throw new Error(`Function ${functionDetails.name} is not a view function.`); } // --- Parse Input Parameters --- // const expectedInputs: readonly FunctionParameter[] = functionDetails.inputs; if (params.length !== expectedInputs.length) { throw new Error( `Incorrect number of parameters provided for ${functionDetails.name}. Expected ${expectedInputs.length}, got ${params.length}.` ); } const args = expectedInputs.map((inputParam: FunctionParameter, index: number) => { const rawValue = params[index]; return parseStellarInput(rawValue, inputParam.type); }); logger.debug('queryStellarViewFunction', 'Parsed Args for contract call:', args); // --- Create Simulation Transaction --- // const paramTypes = expectedInputs.map((input) => input.type); const transactionBuilder = await createSimulationTransaction( contractAddress, functionDetails.name, args, paramTypes, stellarConfig ); const transaction = transactionBuilder.build(); logger.debug( 'queryStellarViewFunction', `[Query ${functionDetails.name}] Simulating transaction:`, transaction.toXDR() ); // --- Simulate Transaction --- // let simulationResult: StellarRpc.Api.SimulateTransactionResponse; try { simulationResult = await rpcServer.simulateTransaction(transaction); } catch (simulationError) { logger.error( 'queryStellarViewFunction', `[Query ${functionDetails.name}] Simulation failed:`, simulationError ); throw new Error( `Soroban RPC simulation failed for ${functionDetails.name}: ${(simulationError as Error).message}` ); } // --- Process Simulation Result --- // if (StellarRpc.Api.isSimulationError(simulationResult)) { logger.error( 'queryStellarViewFunction', `[Query ${functionDetails.name}] Simulation error:`, simulationResult.error ); throw new Error(`Contract simulation failed: ${simulationResult.error}`); } if (!simulationResult.result) { throw new Error(`No result returned from contract simulation for ${functionDetails.name}`); } const rawResult = simulationResult.result.retval; logger.debug( 'queryStellarViewFunction', `[Query ${functionDetails.name}] Raw simulation result:`, rawResult ); // --- Format Result --- // const formattedResult = formatStellarFunctionResult(rawResult, functionDetails); logger.info( 'queryStellarViewFunction', `[Query ${functionDetails.name}] Formatted result:`, formattedResult ); return formattedResult; } catch (error) { const errorMessage = `Failed to query Stellar view function ${functionId} on network ${networkConfig.name}: ${(error as Error).message}`; logger.error('queryStellarViewFunction', errorMessage, { contractAddress, functionId, params, networkConfig, error, }); throw new Error(errorMessage); } }