Skip to content

Commit 4777a55

Browse files
authored
Merge pull request #37 from middalukawunti-lang/fix/issue-18-soroban-deployment
fix: implement real Soroban contract deployment (fixes #18)
2 parents 21c6690 + 68dd834 commit 4777a55

5 files changed

Lines changed: 347 additions & 83 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,4 @@ node_modules/
183183
.vscode
184184
.cursor*
185185
.DS_Store
186+
package-lock.json

quantara/frontend/src/services/contract.js

Lines changed: 140 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,81 +4,170 @@
44
* Handles Soroban smart contract deployment and lifecycle management
55
* on the Stellar network.
66
*
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
810
*/
911

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';
1222
import { axiosInstance } from '../utils/axios';
1323
import { notify, ToastWithLink } from '../components/layout/notifier/Notifier';
1424

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+
1536
/**
1637
* Deploy a Soroban smart contract for a user.
1738
*
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.
2143
*
2244
* @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
2447
*/
2548
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.');
30141
}
31142

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);
64148

65149
notify(
66150
ToastWithLink(
67-
'Soroban contract deployment initiated',
68-
'#',
69-
'Contract Pending'
151+
'Soroban contract deployed successfully',
152+
explorerContractUrl(contractAddress),
153+
'View Contract'
70154
),
71155
'success'
72156
);
73157

74158
return {
75-
transactionHash: 'pending_soroban_deployment',
159+
transactionHash: sendResponse.hash,
76160
contractAddress,
77161
};
78-
} catch (error) {
79-
console.error('Error deploying contract:', error);
80-
throw error;
81162
}
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}`);
82171
}
83172

84173
/**
@@ -113,5 +202,6 @@ export async function checkAndDeployContract(walletId) {
113202
}
114203
} catch (error) {
115204
console.error('Error checking contract status:', error);
205+
throw error;
116206
}
117207
}

quantara/frontend/src/services/soroban.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function getSorobanServer() {
3333
* @param {number} maxAttempts - Maximum polling attempts (default 30 = ~30 seconds)
3434
* @returns {Promise<object>} The transaction result
3535
*/
36-
async function pollForTransaction(server, hash, maxAttempts = 30) {
36+
export async function pollForTransaction(server, hash, maxAttempts = 30) {
3737
for (let attempt = 0; attempt < maxAttempts; attempt++) {
3838
const result = await server.getTransaction(hash);
3939
if (result.status === 'SUCCESS') {

quantara/frontend/src/utils/constants.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ export const ONE_HOUR_IN_MILLISECONDS = 3600000;
4141
// Telegram bot link
4242
export const TELEGRAM_BOT_LINK = 'https://t.me/quantara_bot';
4343

44+
/**
45+
* Get contract deployment data for a Soroban contract.
46+
*
47+
* @param {string} walletId - The Stellar public key
48+
* @returns {object} Contract deployment parameters
49+
*/
50+
/**
51+
* Generate a cryptographically random 32-byte hex string for use as a Soroban salt.
52+
* This ensures each user gets a unique contract instance.
53+
*/
54+
export function generateSalt() {
55+
const bytes = new Uint8Array(32);
56+
crypto.getRandomValues(bytes);
57+
return Array.from(bytes)
58+
.map((b) => b.toString(16).padStart(2, '0'))
59+
.join('');
60+
}
61+
4462
/**
4563
* Get contract deployment data for a Soroban contract.
4664
*
@@ -50,7 +68,7 @@ export const TELEGRAM_BOT_LINK = 'https://t.me/quantara_bot';
5068
export function getDeployContractData(walletId) {
5169
return {
5270
wasmHash: SOROBAN_WASM_HASH,
53-
salt: `0x${Math.floor(Math.random() * 1e16).toString(16)}`, // Generate random salt
71+
salt: generateSalt(), // 32-byte random hex salt for deterministic address
5472
constructorCalldata: [walletId],
5573
};
5674
}

0 commit comments

Comments
 (0)