Each service defines a number of different ways you can interact with it as a developer, and these comprise the Hedera Application Programming Interfaces (HAPIs). However, HAPIs are very close to the metal, and a developer needs to handle gRPCs and protocol buffers (among other things) to work with them successfully. Thankfully there are Hedera SDKs, which abstract these low-level complexities away. These SDKs allow you to interact with the various Hedera services via APIs exposed in a variety of different programming languages.
Available Hedera SDKs
At the time of writing, July 2023, these Hedera SDKs are available in the following languages:
Please refer to SDKs for an up to date list of SDKs, including additional community-maintained SDKs.
In this tutorial, you will be using Hedera SDK JS to interact with HSCS. Specifically, you will use it to deploy a smart contract, query its state by invoking functions, and modify its state by invoking other functions.
Prerequisites
✅ Complete the Setup section of this same tutorial.
✅ Complete the Solidity section of this same tutorial.
We have already written the smart contract in the Intro section of this tutorial. Let's copy that into this directory so that we may continue working on it.
cp../intro/trogdor.sol./trogdor.sol
Using solc to compile smart contracts
Let's install the Solidity compiler, solc from npm.
npminstall--globalsolc@0.8.17
You can verify that it has installed successfully by asking it to output its version. Note that while the package name on npm is solc, the executable present on PATH is spelled slightly differently: solcjs.
solcjs--version
If it does not error, and outputs its version, you know it has installed successfully.
0.8.17+commit.8df45f5f.Emscripten.clang
Let's explore its command line interface:
solcjs--help
There are relatively few flags and options. In this tutorial, you will only be using --bin, and --abi.
Usage: solcjs [options]
Options:
-V, --version output the version number
--version Show version and exit.
--optimize Enable bytecode optimizer. (default: false)
--optimize-runs <optimize-runs> The number of runs specifies roughly how often each opcode of the deployed code
will be executed across the lifetime of the contract. Lower values will optimize
more for initial deployment cost, higher values will optimize more for
high-frequency usage.
--bin Binary of the contracts in hex.
--abi ABI of the contracts.
--standard-json Turn on Standard JSON Input / Output mode.
--base-path <path> Root of the project source tree. The import callback will attempt to interpret
all import paths as relative to this directory.
--include-path <path...> Extra source directories available to the import callback. When using a package
manager to install libraries, use this option to specify directories where
packages are installed. Can be used multiple times to provide multiple
locations.
-o, --output-dir <output-directory> Output directory for the contracts.
-p, --pretty-json Pretty-print all JSON output. (default: false)
-v, --verbose More detailed console output. (default: false)
-h, --help display help for command
Let's compile the Solidity file.
solcjs--bin--abi./trogdor.solls
Those flags instruct solcjs to output both EVM bytecode and ABI. The ls command lists the files that are in the directory, and the following files should be present.
Nothing much we can glean by looking at this really!
This bytecode is used to deploy the smart contract onto the Hedera network.
EVM bytecode categories
The EVM bytecode that is output by the Solidity compiler is not the same as the EVM bytecode that is stored and executed on the network after it has been deployed.
The Solidity compiler's output bytecode is creation bytecode, sometimes also referred to as init bytecode.
The bytecode that is stored on the network is runtime bytecode, sometimes also referred to as deployed bytecode.
If you are using a POSIX-compliant shell, and have jq installed, you can view the ABI output like so.
jq<./trogdor_sol_Trogdor.abi
The ABI essentially tells any user/ developer who wishes to interact with the EVM bytecode, what the exposed interface is. In fact ABI stands for Application Binary Interface. This interface will include any functions and events, which are needed by any clients (e.g. DApps), or other smart contracts, to be able to interact with it.
This is extremely useful, because by examining the bytecode, which is what is deployed onto the Hedera network, you are likely to have no idea what it does, or how to interact with it. If you have the corresponding ABI in hand, however, you will have a very good idea of how you can interact with this smart contract, and perhaps can infer what it does as well.
Deploying smart contracts
Let's edit the deploy-sc.js file. In this script, you'll use Hedera SDK JS to deploy your smart contract onto Hedera Testnet.
Step E1: Initialise operator account
This script has already been set up to read in environment variables from the .env file that you have set up in the Intro section of this tutorial via the dotenv npm package, and they are now accessible using process.env.
We will use the OPERATOR_ID and OPERATOR_KEY environment variables to initialise an operator account, connect to Hedera Testnet.
The fileId that you obtained in the previous step references the EVM bytecode stored on HFS. The ContractCreateTransaction references this file on HFS during the deployment process.
In the final line above, obtain the smart contract ID from the ContractCreateTransaction's receipt, as scId - you will need it later.
The smart contract is now deployed, and ready to be interacted with.
Run the script.
node./deploy-sc.js
You should see output similar to the following, which contains:
You should get redirected to a "Contract" page, e.g. https://hashscan.io/testnet/contract/0.0.15388539.
In it you can see the EVM address, e.g. 0x0000000000000000000000000000000000eacf7b.
Under "Contract Bytecode", you can see "Runtime Bytecode".
Interacting with smart contacts
Let's edit the interact-sc.js file. In this script, you'll use Hedera SDK JS to interact with your smart contract on Hedera Testnet.
Step F1: Specify deployed contract ID
Copy the smart contract ID, obtained during the previous step, and add paste this into this file, where the main function is invoked (at the bottom of the file).
contractId:'0.0.15388539',
Step F2: Initialise operator account
Similar to what we did in the deployment script, we will use the OPERATOR_ID and OPERATOR_KEY environment variables to initialise an operator account, connect to Hedera Testnet.
The burninate function in this smart contract is public and payable. This means that the function may be invoked with a transaction that has a value (HBAR) attached to it - accessible as msg.value in Solidity. The value will be added to this smart contracts balance if this function is executed successfully.
Recall that when you implemented the burninate function in the Intro section of this tutorial, that there is this require statement:
require(msg.value >= MIN_FEE,"pay at least minimum fee");
This essentially specifies that the function will error, and therefore not execute successfully, when the value sent with the transaction is anything less than 100 tinybar (MIN_FEE).
Now we're going to invoke this function with a zero value transaction, i.e. Invoke burninate with msg.value = 0. This is done on purpose, to trip up this require statement, so that we can witness the rejection.
This will send a transaction to Hedera Testnet, which contains a request to HSCS to (potentially) modify the state of this smart contract.
When this is run, we expect the transaction to fail, with a CONTRACT_REVERT_EXECUTED error. The reason for this is the require statement in this function, as described above - we need to send some HBAR!
Step F4: Invoke payable function with non-zero value
Next invoke the same burninate function once again, with only one change: the transaction will contain a value of 123 tinybars, i.e. msg.value = 123.
This time, the function invocation will succeed, as it passes that require statement.
The burninate function is one that can (and does) modify the persisted state of the smart contract. However there are other functions which do not do so, and instead merely read (query) the currently persisted state of the smart contract. These functions have the view modifier.
There are also other functions which neither read the currently nor modify the persisted state of the smart contract. These functions have the pure modifier.
These are typically used as utility functions, intended to be invoked by other functions within a smart contract.
The totalBurnt is a view function, and to invoke that, let's use ContractCallQuery.
ContractExecuteTransaction: Use for modifying state
Once the ContractCallQuery is executed, extract the its return value using the getter function with the appropriate type. Since the totalBurnt function specifies returns(uint256) in its signature, use getUint256() to extract that return value.
The ContractCallQuery has setQueryPayment, which is to pay for the costs of querying the data. Note that this is different from other EVM-compatible networks, which allow you to query smart contract state without paying any fee.\
In the subsequent step, we will use the operator account as an input parameter in a function invocation. However, we need to convert this from an Account ID format, which looks like 0.0.3996280, to an EVM address format, which looks like 0x7394111093687e9710b7a7aeba3ba0f417c54474. This is because the EVM (and by extension Solidity), does not understand Hedera-native accounts. Instead it only understands EVM accounts.
To do so, we start with the private key of the operator account, from that we derive its public key, and finally from that we derive its EVM account. Thankfully Hedera SDK JS has utility functions for these, and the conversion can be performed quite easily.
Step F7: Invoke auto-generated view function with parameters
In this smart contract amounts is a view function, and to invoke that, let's use ContractCallQuery. There are a couple of key differences though:
The amounts function requires an input parameter, or type address
The amounts function was not written using Solidity code, But instead was auto-generated by the Solidity compiler for the public state variable with the same name.
Auto-generated getter function
This is the actual code for amounts in the Solidity file:
mapping(address=>uint256) public amounts;
This is what the auto-generated function for amountswould have looked like, if you needed to write it manually.
functionamounts(address account)publicviewreturns(uint256) {// implementation goes here }
Let's send a ContractCallQuery to the amounts function. Use the operatorEvmAddress obtained in the previous step as the input parameter.
There is a ContractFunctionParameters, which we've used in the previous smart contract invocations, but it was always "empty", in the sense that there were no parameters. Since amounts requires a single parameter of type address, use addAddress() to specify its value.
Once the ContractCallQuery is executed, extract the its return value using the getter function with the appropriate type. The amounts mapping specifies uint256 as its value type, this is equivalent to a function specifying returns(uint256) in its signature. Use getUint256() to extract that return value.
Note that the first ContractExecuteTransaction fails, and this is expected. On the other hand, the second ContractExecuteTransaction passes, because this time we sent the payable the required number of HBAR.
The ContractFunctionResult has queried the data, and return value simply extracts the relevant value from it.
Check smart contract interactions using Hashscan
Visit the "Contract" page for your previously deployed smart contract, e.g. https://hashscan.io/testnet/contract/0.0.15388539
Scroll down to the "Recent Contract Calls" section
If you see "REFRESH PAUSED" at the top right of this section, press the "play" button next to it to unpause (otherwise it does not load new transactions)
You should see a list of transactions, with most recent at the top
There should be a failed transaction, denoted by an exclamation mark in a red triangle, e.g. https://hashscan.io/testnet/transaction/1689235951.444001003
Click on the row for that failed transaction to navigate to its "Transaction" page
Scroll down to the "Contract Result" section
You should see "Result" as CONTRACT_REVERT_EXECUTED
You should also see "Error Message" as pay at least minimum fee
Go back to the "Contract" page
Scroll down to the "Recent Contract Calls" section
There should be a successful transaction, denoted by the absence of an exclamation mark in a red triangle, e.g. https://hashscan.io/testnet/transaction/1689235952.436013392
Scroll down to the "Contract Result" section
You should see "Result" as SUCCESS
You should also see "Error Message" as None
Scroll down to the "Logs" section
You should see a single log entry (address, data, index, and topics)
The "Address" field matches that of the smart contract
The "Index" field should be 0 since there was only a single event that was emitted
The "Topics" field corresponds to the hash of the signature of the event that was emitted, e.g. Burnination(address,uint256)
The "Data" field corresponds to the values of the event parameters, e.g. 0x00000000000000000000000000000000000000000000000000000000000004a2000000000000000000000000000000000000000000000000000000000000007b is:
0x00000000000000000000000000000000000004a2 (your address) and
0x007b is the amount (123 when converted to decimal)