In this article, we will learn how to build a dapp that can deploy smart contracts. Usually dapps interact with smart contracts by invoking its methods, but I wanted to explore how contracts actually get deployed on a blockchain network. Hence, I decided to build a web interface for a smart contract deployer as an alterantive to the usual CLI.
Introduction
To deploy a new smart contract on the Stellar network, we essentially need to do two things:
- Upload the contract wasm file on the network. This is what
soroban contract install
does. Thetransaction that uploads the wasm file returns the hash of the wasm file. This is needed in the next step - Creating the contract itself. This is what
soroban contract deploy
does. This creates a contract instance linked to the wasm file that we uploaded. This step needs to be provided with the wasm hash we receive in the previous step, since the contract needs to know which wasm binary to execute.
Getting started
You just need Node.js v18 or higher for this project. We'll be using Next.js and use its server side rendering capabilities to contact the Soroban RPC and deploy the contract.
Initialize the project:
npx create-next-app@latest
I've decided to use the Pages router here but you can use the App router if you wish to.
We'll need to install the following packages as well:
npm install --save stellar-sdk formidable
Building the deployer
Now lets start building our UI. We need two things to deploy a new contract:
- The private key of a Stellar account.
- The wasm executable
This means we need a password field (since its a private key) and a file upload field. Lets go ahead and build that:
return (
<>
<div>
<NavBar />
</div>
<div className='container'>
<h2> Your private key </h2>
<p> (its not logged or stored! 😉) </p>
<input type="password" value={textField} onChange={handleTextChange} />
<h2> Upload the wasm file </h2>
<input type="file" onChange={handleFileChange} />
{isLoading ? (
<div className="spinner"></div> // Spinner element
) : (
<button onClick={handleSubmit} disabled={isLoading}>Deploy</button>
)}
<Modal isOpen={isModalOpen} onClose={closeModal} wasmHash={wasmHash} contractID={contractID} />
</div>
</>
);
Now lets a handler for the "Deploy" button. The handler will call a server side function with the private key and the wasm file. It'll get back the contract ID and the wasm hash.
const handleSubmit = async () => {
// activate spinner
setIsLoading(true);
if (file) {
const formData = new FormData();
formData.append('file', file);
formData.append('textField', textField);
try {
// Send the file and the private key to the server side function.
const response = await fetch('/api/deployer', {
method: 'POST',
body: formData,
});
console.log(response.statusText)
const body = await response.json();
console.log(body);
setWasmHash(body.wasmHash);
setContractID(body.id);
setModalOpen(true);
} catch (error) {
console.log(error);
} finally {
// deactivate spinner
setIsLoading(false);
}
} else {
// deactivate spinner
setIsLoading(false);
}
};
Now lets take a look at the server side handler that actually deploys the contract:
export default function handler(req: NextApiRequest, res: NextApiResponse<ContractData | String>) {
if (req.method === 'POST') {
const form = new formidable.IncomingForm();
form.parse(req, async (err, fields, files) => {
if (err) {
console.error('Error parsing the form:', err);
return res.status(500).send('Error parsing the form');
}
try {
// Get the private key
const pk = fields.textField[0];
const file = files.file;
// Get the wasm file buffer
const data = fs.readFileSync(file[0].filepath);
} catch (error) {
console.error('Error processing form:', error);
res.status(500).send('Error processing form');
}
});
}
Here we parse the request form and extract the private key and the wasm file. Now its time to constrcut a keypair and upload the wasm file:
let op = sdk.Operation.uploadContractWasm({ wasm: data });
// To store the contract hash returned by the RPC.
let contractHash = '';
const sourceKeypair = sdk.Keypair.fromSecret(pk);
const sourcePublicKey = sourceKeypair.publicKey();
const account = await server.getAccount(sourcePublicKey);
// Build, prepare, sign and send the upload contract wasm transaction.
let tx = new sdk.TransactionBuilder(account, { fee: sdk.BASE_FEE })
.setNetworkPassphrase(sdk.Networks.FUTURENET)
.setTimeout(30)
.addOperation(op)
.build();
try {
const preparedTransaction = await server.prepareTransaction(tx);
preparedTransaction.sign(sourceKeypair);
const uploadResp = await server.sendTransaction(preparedTransaction);
while (true) {
const val = await server.getTransaction(uploadResp.hash);
if (val.returnValue === undefined || val.returnValue === null) {
console.log("continuing");
continue
} else {
// The first 16 characters are of no importance.
contractHash = val.returnValue.toXDR('hex').slice(16);
console.log("contract hash", contractHash);
break;
}
}
} catch (error) {
console.log("error uploading", error);
return res.status(500).send('Error uploading the contract wasm file');
}
After we have uploaded the wasm file to the network, lets create the contract:
const hash = sdk.hash(data);
// The operation to create a contract on the network.
let contractOp = sdk.Operation.createCustomContract({
address: new sdk.Address(sourcePublicKey),
wasmHash: hash,
});
// To store the contract ID.
let contractID = '';
// Build, prepare, sign and send the create contract transaction.
let contractTx = new sdk.TransactionBuilder(account, { fee: sdk.BASE_FEE })
.setNetworkPassphrase(sdk.Networks.FUTURENET)
.setTimeout(30)
.addOperation(contractOp)
.build();
try {
const preparedTransaction = await server.prepareTransaction(contractTx);
preparedTransaction.sign(sourceKeypair);
const contractResp = await server.sendTransaction(preparedTransaction);
while (true) {
const val = await server.getTransaction(contractResp.hash);
if (val.returnValue === undefined || val.returnValue === null) {
console.log("continuing create");
continue
} else {
contractID = sdk.Address.contract(val.returnValue.address().contractId()).toString();
console.log("contract id", contractID);
break;
}
}
} catch (error) {
console.log("error creating", error);
return res.status(500).send('Error deploying the contract');
}
Now lets return the contract information back to the UI so that it can display it to the user:
res.status(201).send({
wasmHash: contractHash,
id: contractID
});
Now we just have one final thing remaining: a modal to display the contract infomation. Lets go ahead and build that:
// Modal to display the wasm hash and contract ID.
const Modal = ({ isOpen, onClose, wasmHash, contractID }) => {
if (!isOpen) {
return null;
}
return (
<div className="modal-backdrop">
<div className="modal">
<h2> Contract deployed! </h2>
<br></br>
<h3>WASM hash: {wasmHash}</h3>
<h3>Contract ID: {contractID}</h3>
<div className="modal-actions">
<button onClick={onClose}>Close</button>
</div>
</div>
</div>
);
};
Now we can use this in our main component:
return (
<>
<div>
<NavBar />
</div>
<div className='container'>
<h2> Your private key </h2>
<p> (its not logged or stored! 😉) </p>
<input type="password" value={textField} onChange={handleTextChange} />
<h2> Upload the wasm file </h2>
<input type="file" onChange={handleFileChange} />
{isLoading ? (
<div className="spinner"></div> // Spinner element
) : (
<button onClick={handleSubmit} disabled={isLoading}>Deploy</button>
)}
<Modal isOpen={isModalOpen} onClose={closeModal} wasmHash={wasmHash} contractID={contractID} />
</div>
</>
);
Conclusion
And here we are, with a web GUI that can deploy Soroban smart contracts. We learn how smart contracts get deployed and the necessary transactions and steps involved.
Good luck on your dapp building journrey! ❤️
Top comments (0)