- Requirements
- Compile a Smart Contract
- Spin Up a Local Ethereum Blockchain
- Connecting to Ethereum Blockchain: RPC
- Deploy a Smart Contract With ethers.js
- Interacting With the Smart Contract
- Deploy to a Real Testnet
- Verify and Publish Smart Contract
We have previously talked about Blockchain and Solidity on a previous post, so if you don’t know what Blockchain, Smart Contracts or Solidity are, go ahead and take a look at Getting Started with Solidity.
The goal is to deploy a Smart Contract written in Solidity with JavaScript!
Requirements
- Install NodeJS, NPM and/or Yarn.
For MacOS users:
brew install node
npm install -g corepack
NodeJS comes with NPM and Corepack installs Yarn.
- Install a Solidity compiler for JavaScript. We are going to choose Solc-JS.
yarn add solc
You can check the version with yarn solcjs --version
.
This would install the compiler for the latest Solidity version. At the time I’m writing this article it’s
solc@0.8.17
. You can install a specific version withyarn add solc@0.8.7-fixed
.
Compile a Smart Contract
- Create a folder for your Smart Contracts
- Create a Smart Contract with Solidity
MyContract.sol
:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;
contract MyContract {
// content of the contract
}
- Compile Smart Contract
yarn solcjs --bin --abi --include-path node_modules/ --base-path . -o . MyContract.sol
where --bin
means we want the binaries; --bin
to generate the Application Binary Interface; --include-path
to include required libraries; --base-path
is the path used as the base path; -o
is where to put the compiled Smart Contract; finally specify the Solidity file that contains the Smart Contract definition.
Two files would be generated the binary (with bin
extension) and the ABI (with abi
extension).
Spin Up a Local Ethereum Blockchain
You can spin up a local Ethereum blockchain with Ganache. Once it’s installed on your machine you can copy the RPC Server URL (something like HTTP://127.0.0.1:7545
) and use it on your Smart Contract.
Connecting to Ethereum Blockchain: RPC
You could interact with the Ethereum Blockchain directly by using the Ethereum JSON-RCP specification. Luckily there are JavaScript libraries out there like ethers.js or web3.js that hides the complexity of making RPC API calls.
We are going to use ethers.js
for this example.
To install ethers.js run:
yarn add ethers
Now you can import ethers.js
and make the connection on your deployment script:
deploy.js
:
const ethers = require("ethers");
async function main() {
// Ganache RPC Server: http://127.0.0.1:7545
const provider = new ethers.providers.JsonRpcProvider(
"http://127.0.0.1:7545"
);
const wallet = new ethers.Wallet(
"<ONE_OF_THE_PRIVATE_KEYS_FROM_GANACHE>", // put this in an env variable
provider
);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
It’s highly recommended to not hardcode the private keys on your code, instead use something like
dotenv
to store the private key on an.env
file and read it from your code. You can installdotenv
withyarn add dotenv
. From now on we will use env variables. IMPORTANT add.env
file to your.gitignore
file. Additionally you can also encrypt the private key.
Private Key Encryption
We can encrypt the private key with the ethers.js
library and store the encrypted private key on a file (.encryptedKey.json
).
encryptedKey.js
:
const ethers = require("ethers");
const fs = require("fs-extra");
require("dotenv").config();
async function main() {
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
const encryptedJsonKey = await wallet.encrypt(
process.env.PRIVATE_KEY_PASSWORD,
process.env.PRIVATE_KEY
);
fs.writeFileSync("./.encryptedKey.json", encryptedJsonKey);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
then we can decrypt the encrypted private key with fromEncryptedJsonSync():
const encryptedJson = fs.readFileSync("./.encryptedKey.json", "utf8");
let wallet = new ethers.Wallet.fromEncryptedJsonSync(encryptedJson, process.env.PRIVATE_KEY_PASSWORD);
and we can remove from our .env
file the PRIVATE_KEY
env variable.
Deploy a Smart Contract With ethers.js
Three things are required for deploying the contract to the Ganache Ethereum Blockchain:
- A connection to the Ganache Server.
- A wallet private key.
- The ABI and binary files.
deploy.js
:
const ethers = require("ethers");
const fs = require("fs-extra");
require("dotenv").config();
async function main() {
const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL);
const encryptedJson = fs.readFileSync("./.encryptedKey.json", "utf8");
let wallet = new ethers.Wallet.fromEncryptedJsonSync(encryptedJson, process.env.PRIVATE_KEY_PASSWORD);
wallet = await wallet.connect(provider);
const abi = fs.readFileSync("./MyContract_sol_MyContract.abi", "utf8");
const binary = fs.readFileSync("./MyContract_sol_MyContract.bin", "utf8");
const contractFactory = new ethers.ContractFactory(abi, binary, wallet);
console.log("Start deployment!");
const contract = await contractFactory.deploy();
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
It’s important to use
await
when we calldeploy()
since the method returns aContract
wrapped in aPromise
.
Once the contract is deployed you can see that some Ethereum have been subtracted on the used address as a result of deploying the contract.
The contractFactory.deploy()
function accepts optional parameters to override things like the endowment value
, transaction nonce
, gasLimit
, gasPrice
, value
or to
.
Alternatively, you can deploy a contract by providing the transaction details, signing the transaction and finally sending the transaction.
Transaction details:
const nonce = await wallet.getTransactionCount(); // next transaction index
const tx = {
nonce: nonce,
gasPrice: 20000000000, // from Ganache
gasLimit: 1000000,
to: null, // we are creating a contract so we are not sending crypto to someone else
value: 0, // we are creating a contract so we are not sending crypto
data: "0x<BINARY_CONTENT>", // requires 0x in front of the binary
chainId: 1337, // network ID on Ganache
};
Sign transaction (not required when calling sendTransaction()
):
const signedTxResponse = await wallet.signTransaction(tx);
Send transaction:
const sendTxResponse = await wallet.sendTransaction(tx);
await sendTxResponse.wait(1);
sendTransaction(tx)
signs the transaction and sends it to the network.
If the deployment fails with
chainId
not found, change the Ganache Network address to 1337.
deploy.js
:
const ethers = require("ethers");
const fs = require("fs-extra");
async function main() {
const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL);
const encryptedJson = fs.readFileSync("./.encryptedKey.json", "utf8");
let wallet = new ethers.Wallet.fromEncryptedJsonSync(encryptedJson, process.env.PRIVATE_KEY_PASSWORD);
wallet = await wallet.connect(provider);
const abi = fs.readFileSync("./MyContract_sol_MyContract.abi", "utf8");
const binary = fs.readFileSync("./MyContract_sol_MyContract.bin", "utf8");
const nonce = await wallet.getTransactionCount();
const tx = {
nonce: nonce,
gasPrice: 20000000000,
gasLimit: 1000000,
to: null, // we are creating a contract so we are not sending crypto to someone else
value: 0, // we are creating a contract so we are not sending crypto
data: "0x<BINARY_CONTENT>",
chainId: 1337, // network ID on Ganache
};
const sendTxResponse = await wallet.sendTransaction(tx);
await sendTxResponse.wait(1);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Transaction Receipt
We can get the transaction receipt with:
const transactionReceipt = await contract.deployTransaction.wait(1); // we wait for 1 block confirmation
For getting the transaction receipt you must wait for the block to be confirmed. This is different to the transaction response (
contract.deployTransaction
) which is returned when the transaction is created.
Interacting With the Smart Contract
The ABI (Application Binary Interface) contains all the functions defined in your contract and you can invoke them with JavaScript.
Given the following contract:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;
contract MyContract {
function helloWorld() public view returns (string memory) {
return "Hello World!";
}
}
yarn solcjs --bin --abi --include-path node_modules/ --base-path . -o . MyContract.sol
generates this ABI:
[
{
"inputs":[
],
"name":"helloWorld",
"outputs":[
{
"internalType":"string",
"name":"",
"type":"string"
}
],
"stateMutability":"view",
"type":"function"
}
]
so now you can call the function like this:
deploy.js
:
const ethers = require("ethers");
const fs = require("fs-extra");
async function main() {
const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL);
const encryptedJson = fs.readFileSync("./.encryptedKey.json", "utf8");
let wallet = new ethers.Wallet.fromEncryptedJsonSync(encryptedJson, process.env.PRIVATE_KEY_PASSWORD);
wallet = await wallet.connect(provider);
const abi = fs.readFileSync("./MyContract_sol_MyContract.abi", "utf8");
const binary = fs.readFileSync("./MyContract_sol_MyContract.bin", "utf8");
const contractFactory = new ethers.ContractFactory(abi, binary, wallet);
console.log("Deploying contract...");
const contract = await contractFactory.deploy();
await contract.deployTransaction.wait(1);
const helloWorld = await contract.helloWorld();
console.log(helloWorld);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
node deploy.js
prints:
Deploying contract...
Hello World!
Deploy to a Real Testnet
- Create account on Alchemy. You can choose a different testnet provider.
- Create App on Alchemy:
- Chain: Ethereum
- Network: Goerli (this might change in the future)
- Click on View Key and copy the HTTPS URL.
- Replace the value of
RPC_URL
with the URL from Alchemy. - Get a real Wallet Private Key without real funds (DO NOT USE A PRIVATE KEY WITH REAL FUNDS!).
- Encrypt the private key:
PRIVATE_KEY=<YOUR_PRIVATE_KEY> PRIVATE_KEY_PASSWORD=<YOUR_PRIVATE_KEY_PASSWORD> node encryptKey.js
- Deploy contract:
PRIVATE_KEY_PASSWORD=<YOUR_PRIVATE_KEY_PASSWORD> node deploy.js
- Go to the Goerli Etherscan page and paste your public key. You should be able to see the contract creation! 🎉
Alchemy allows you to use a node to communicate with the blockchain.
Verify and Publish Smart Contract
- Go to the contract address (e.g. https://goerli.etherscan.io/address/0xb7770df4fc2dd064f5867f009ce405ba3a93f5eb)
- Click on the Contract tab and Verify and Publish.
- Select the Compiler Type, Version and License Type that you used on your contract. Finally click on next.
- Now copy and paste the contract and click on Verify and Publish.
- Now you can click on the Contract Source Code to see the contract (e.g. https://goerli.etherscan.io/verifyContract-solc?a=0xb7770df4fc2dd064f5867f009ce405ba3a93f5eb&c=v0.8.7%2bcommit.e28d00a7&lictype=3). The contract address should be the same one as the use for deploying the contract.