@openzeppelin/contracts-ui-builder-adapter-stellar
Version:
Stellar Adapter for Contracts UI Builder
157 lines (138 loc) • 4.92 kB
text/typescript
import { rpc as StellarRpc } 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 { EoaExecutionStrategy } from './eoa';
import { ExecutionStrategy } from './execution-strategy';
import type { StellarTransactionData } from './formatter';
import { RelayerExecutionStrategy } from './relayer';
const SYSTEM_LOG_TAG = 'adapter-stellar';
// --- Helper Functions ---
/**
* 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,
});
}
/**
* Sign and broadcast a Stellar transaction using the strategy pattern.
* This follows the same architecture as the EVM adapter.
*
* @param transactionData - The formatted transaction data from formatStellarTransactionData
* @param executionConfig - Execution configuration specifying method (eoa, relayer, etc.)
* @param networkConfig - Stellar network configuration
* @param onStatusChange - Callback for status updates
* @param runtimeApiKey - Optional session-only API key for methods like Relayer
* @returns Promise resolving to the transaction hash
*/
export async function signAndBroadcastStellarTransaction(
transactionData: unknown,
executionConfig: ExecutionConfig,
networkConfig: StellarNetworkConfig,
onStatusChange?: (status: TxStatus, details: TransactionStatusUpdate) => void,
runtimeApiKey?: string
): Promise<{ txHash: string }> {
logger.info(
SYSTEM_LOG_TAG,
'Stellar signAndBroadcast called with executionConfig:',
executionConfig
);
// Validate network config
if (!networkConfig || networkConfig.ecosystem !== 'stellar') {
throw new Error('Invalid Stellar network configuration provided.');
}
const txData = transactionData as StellarTransactionData;
let strategy: ExecutionStrategy;
// Select execution strategy based on method
switch (executionConfig.method) {
case 'eoa':
strategy = new EoaExecutionStrategy();
break;
case 'relayer':
strategy = new RelayerExecutionStrategy();
break;
case 'multisig':
// TODO: Implement MultisigExecutionStrategy when Stellar multisig support is added
throw new Error('Multisig execution method not yet implemented for Stellar.');
default: {
const exhaustiveCheck: never = executionConfig;
logger.error(SYSTEM_LOG_TAG, `Unsupported execution method encountered: ${exhaustiveCheck}`);
throw new Error(`Unsupported execution method: ${exhaustiveCheck}`);
}
}
return strategy.execute(
txData,
executionConfig,
networkConfig,
onStatusChange || (() => {}),
runtimeApiKey
);
}
/**
* Waits for a transaction to be confirmed on the blockchain.
*
* @param txHash - The hash of the transaction to wait for.
* @param networkConfig - The network configuration.
* @returns A promise resolving to the final status and receipt/error.
*/
export async function waitForStellarTransactionConfirmation(
txHash: string,
networkConfig: StellarNetworkConfig
): Promise<{
status: 'success' | 'error';
receipt?: unknown;
error?: Error;
}> {
try {
const rpcServer = getSorobanRpcServer(networkConfig);
const MAX_ATTEMPTS = 20; // More attempts for confirmation
let attempts = 0;
while (attempts++ < MAX_ATTEMPTS) {
try {
const txResponse = await rpcServer.getTransaction(txHash);
switch (txResponse.status) {
case 'SUCCESS':
return {
status: 'success',
receipt: txResponse,
};
case 'FAILED':
return {
status: 'error',
error: new Error(`Transaction failed: ${JSON.stringify(txResponse.resultXdr)}`),
};
case 'NOT_FOUND':
// Continue waiting
break;
default:
// Continue waiting for other statuses
}
await new Promise((resolve) => setTimeout(resolve, 1000));
} catch (error) {
logger.error('waitForStellarTransactionConfirmation', `Attempt ${attempts} failed:`, error);
}
}
return {
status: 'error',
error: new Error('Transaction confirmation timeout'),
};
} catch (error) {
return {
status: 'error',
error: error as Error,
};
}
}