|
4 | 4 | * Handles Soroban smart contract deployment and lifecycle management |
5 | 5 | * on the Stellar network. |
6 | 6 | * |
7 | | - * Replaces the former Starknet contract deployment service. |
| 7 | + * Each user deploys their own instance of the leverage looping contract |
| 8 | + * via the Soroban RPC endpoint. The deployment follows the standard |
| 9 | + * Soroban flow: Build → Simulate → Assemble → Sign → Submit → Poll |
8 | 10 | */ |
9 | 11 |
|
10 | | -import { getWalletPublicKey } from './wallet'; |
11 | | -import { getDeployContractData } from '../utils/constants'; |
| 12 | +import { |
| 13 | + SorobanRpc, |
| 14 | + Operation, |
| 15 | + TransactionBuilder, |
| 16 | + Address, |
| 17 | + scValToNative, |
| 18 | +} from '@stellar/stellar-sdk'; |
| 19 | +import { getWalletPublicKey, signStellarTransaction, getNetworkPassphrase } from './wallet'; |
| 20 | +import { getSorobanServer, pollForTransaction } from './soroban'; |
| 21 | +import { SOROBAN_WASM_HASH } from '../utils/constants'; |
12 | 22 | import { axiosInstance } from '../utils/axios'; |
13 | 23 | import { notify, ToastWithLink } from '../components/layout/notifier/Notifier'; |
14 | 24 |
|
| 25 | +/** |
| 26 | + * Build a Stellar Expert URL for a Soroban contract. |
| 27 | + * |
| 28 | + * @param {string} contractAddress - The Soroban contract ID (C... format) |
| 29 | + * @returns {string} The explorer URL |
| 30 | + */ |
| 31 | +const explorerContractUrl = (contractAddress) => { |
| 32 | + const network = process.env.VITE_STELLAR_NETWORK === 'PUBLIC' ? 'public' : 'testnet'; |
| 33 | + return `https://stellar.expert/explorer/${network}/contract/${contractAddress}`; |
| 34 | +}; |
| 35 | + |
15 | 36 | /** |
16 | 37 | * Deploy a Soroban smart contract for a user. |
17 | 38 | * |
18 | | - * In the Quantara architecture, each user deploys their own instance |
19 | | - * of the leverage looping contract. This function handles that deployment |
20 | | - * via the Soroban RPC endpoint. |
| 39 | + * Uses `Operation.createCustomContract` to create a contract instance |
| 40 | + * from a pre-installed WASM identified by its hash. The WASM hash is |
| 41 | + * loaded from the VITE_QUANTARA_WASM_HASH environment variable or |
| 42 | + * fetched from the backend /api/contract-wasm-hash endpoint. |
21 | 43 | * |
22 | 44 | * @param {string} walletId - The Stellar public key of the user |
23 | | - * @returns {Promise<object>} Deployment result with contract address |
| 45 | + * @returns {Promise<{transactionHash: string, contractAddress: string}>} |
| 46 | + * Deployment result with real Soroban contract ID (C… format) and transaction hash |
24 | 47 | */ |
25 | 48 | export async function deployContract(walletId) { |
26 | | - try { |
27 | | - const publicKey = await getWalletPublicKey(); |
28 | | - if (!publicKey) { |
29 | | - throw new Error('Please connect your Freighter wallet first'); |
| 49 | + const publicKey = await getWalletPublicKey(); |
| 50 | + if (!publicKey) { |
| 51 | + throw new Error('Please connect your Freighter wallet first'); |
| 52 | + } |
| 53 | + |
| 54 | + console.log('Deploying Soroban contract for wallet:', walletId); |
| 55 | + |
| 56 | + // ------------------------------------------------------------------ // |
| 57 | + // 1. Resolve the WASM hash (env var → backend fallback) |
| 58 | + // ------------------------------------------------------------------ // |
| 59 | + let wasmHash = SOROBAN_WASM_HASH; |
| 60 | + if (!wasmHash) { |
| 61 | + try { |
| 62 | + const response = await axiosInstance.get('/api/contract-wasm-hash'); |
| 63 | + wasmHash = response.data.wasm_hash || response.data.wasmHash; |
| 64 | + } catch { |
| 65 | + throw new Error( |
| 66 | + 'Soroban WASM hash not configured. Set VITE_QUANTARA_WASM_HASH or ensure the backend serves /api/contract-wasm-hash.' |
| 67 | + ); |
| 68 | + } |
| 69 | + } |
| 70 | + if (!wasmHash) { |
| 71 | + throw new Error('Soroban WASM hash is empty or not configured.'); |
| 72 | + } |
| 73 | + |
| 74 | + // ------------------------------------------------------------------ // |
| 75 | + // 2. Connect to Soroban RPC & prepare account / salt |
| 76 | + // ------------------------------------------------------------------ // |
| 77 | + const server = getSorobanServer(); |
| 78 | + const networkPassphrase = getNetworkPassphrase(); |
| 79 | + const salt = crypto.getRandomValues(new Uint8Array(32)); |
| 80 | + |
| 81 | + const account = await server.getAccount(publicKey); |
| 82 | + |
| 83 | + // ------------------------------------------------------------------ // |
| 84 | + // 3. Build the create_custom_contract operation |
| 85 | + // ------------------------------------------------------------------ // |
| 86 | + const createOp = Operation.createCustomContract({ |
| 87 | + wasmHash, |
| 88 | + address: Address.fromString(publicKey), |
| 89 | + salt, |
| 90 | + }); |
| 91 | + |
| 92 | + const tempTx = new TransactionBuilder(account, { |
| 93 | + fee: '100', |
| 94 | + networkPassphrase, |
| 95 | + }) |
| 96 | + .addOperation(createOp) |
| 97 | + .setTimeout(30) |
| 98 | + .build(); |
| 99 | + |
| 100 | + // ------------------------------------------------------------------ // |
| 101 | + // 4. Simulate → Assemble → Sign → Submit → Poll |
| 102 | + // ------------------------------------------------------------------ // |
| 103 | + |
| 104 | + const simulation = await server.simulateTransaction(tempTx); |
| 105 | + |
| 106 | + if (!SorobanRpc.isSimulationSuccess(simulation)) { |
| 107 | + const errorMsg = simulation?.error || 'Unknown simulation error'; |
| 108 | + throw new Error(`Contract deployment simulation failed: ${errorMsg}`); |
| 109 | + } |
| 110 | + |
| 111 | + // Assemble the final transaction from the simulation result |
| 112 | + const assembledTx = simulation.transaction; |
| 113 | + |
| 114 | + // Sign with Freighter |
| 115 | + const signedXdr = await signStellarTransaction(assembledTx.toXDR(), { |
| 116 | + network: process.env.VITE_STELLAR_NETWORK || 'TESTNET', |
| 117 | + }); |
| 118 | + |
| 119 | + // Submit the signed transaction |
| 120 | + const sendResponse = await server.sendTransaction( |
| 121 | + TransactionBuilder.fromXDR(signedXdr, networkPassphrase) |
| 122 | + ); |
| 123 | + |
| 124 | + if (sendResponse.status === 'PENDING' || sendResponse.status === 'TRY_AGAIN_LATER') { |
| 125 | + // Poll for the result |
| 126 | + const result = await pollForTransaction(server, sendResponse.hash); |
| 127 | + |
| 128 | + // Extract the contract ID from the return value. |
| 129 | + // createCustomContract returns the contract address as an ScVal of type ScvAddress. |
| 130 | + let contractAddress; |
| 131 | + if (result.returnValue) { |
| 132 | + try { |
| 133 | + contractAddress = scValToNative(result.returnValue); |
| 134 | + } catch { |
| 135 | + throw new Error( |
| 136 | + 'Deployment succeeded but could not parse the returned contract address.' |
| 137 | + ); |
| 138 | + } |
| 139 | + } else { |
| 140 | + throw new Error('Deployment succeeded but no contract address was returned.'); |
30 | 141 | } |
31 | 142 |
|
32 | | - console.log('Deploying Soroban contract for wallet:', walletId); |
33 | | - |
34 | | - // TODO: Implement Soroban contract deployment using @stellar/stellar-sdk |
35 | | - // This will use the Soroban RPC to deploy a pre-compiled WASM contract. |
36 | | - // |
37 | | - // Example: |
38 | | - // const { SorobanRpc, TransactionBuilder, BASE_FEE, Contract, nativeToScVal } = |
39 | | - // await import('@stellar/stellar-sdk'); |
40 | | - // const server = new SorobanRpc.Server(sorobanRpcUrl); |
41 | | - // |
42 | | - // // Get the contract WASM hash from the backend |
43 | | - // const { data: { wasm_hash } } = await axiosInstance.get('/api/contract-wasm-hash'); |
44 | | - // |
45 | | - // // Build deployment transaction |
46 | | - // const account = await server.getAccount(publicKey); |
47 | | - // const deployOp = Operation.createCustomContract({ |
48 | | - // wasmHash: wasm_hash, |
49 | | - // ...constructorArgs, |
50 | | - // }); |
51 | | - // const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase }) |
52 | | - // .addOperation(deployOp) |
53 | | - // .setTimeout(30) |
54 | | - // .build(); |
55 | | - // |
56 | | - // // Sign with Freighter |
57 | | - // const signedXDR = await signStellarTransaction(tx.toXDR()); |
58 | | - // const result = await server.sendTransaction(signedXDR); |
59 | | - |
60 | | - const getDeployData = getDeployContractData(walletId); |
61 | | - |
62 | | - // Mock result for current architecture |
63 | | - const contractAddress = `C${walletId.slice(0, 10)}...`; // Placeholder Soroban contract ID |
| 143 | + if (!contractAddress || typeof contractAddress !== 'string') { |
| 144 | + throw new Error(`Invalid contract address returned: ${contractAddress}`); |
| 145 | + } |
| 146 | + |
| 147 | + console.log('Soroban contract deployed at address:', contractAddress); |
64 | 148 |
|
65 | 149 | notify( |
66 | 150 | ToastWithLink( |
67 | | - 'Soroban contract deployment initiated', |
68 | | - '#', |
69 | | - 'Contract Pending' |
| 151 | + 'Soroban contract deployed successfully', |
| 152 | + explorerContractUrl(contractAddress), |
| 153 | + 'View Contract' |
70 | 154 | ), |
71 | 155 | 'success' |
72 | 156 | ); |
73 | 157 |
|
74 | 158 | return { |
75 | | - transactionHash: 'pending_soroban_deployment', |
| 159 | + transactionHash: sendResponse.hash, |
76 | 160 | contractAddress, |
77 | 161 | }; |
78 | | - } catch (error) { |
79 | | - console.error('Error deploying contract:', error); |
80 | | - throw error; |
81 | 162 | } |
| 163 | + |
| 164 | + if (sendResponse.status === 'ERROR') { |
| 165 | + throw new Error( |
| 166 | + `Soroban deployment submission error: ${sendResponse.errorResult?.error || 'Unknown error'}` |
| 167 | + ); |
| 168 | + } |
| 169 | + |
| 170 | + throw new Error(`Unexpected submission status: ${sendResponse.status}`); |
82 | 171 | } |
83 | 172 |
|
84 | 173 | /** |
@@ -113,5 +202,6 @@ export async function checkAndDeployContract(walletId) { |
113 | 202 | } |
114 | 203 | } catch (error) { |
115 | 204 | console.error('Error checking contract status:', error); |
| 205 | + throw error; |
116 | 206 | } |
117 | 207 | } |
0 commit comments