I am dealing with an issue with a codebase that was previously working fine, but now it's not working as expected.
Context
The Smart Contract JobPost
provides a decentralized platform for job postings, allowing users to create, update, and retrieve job listings. It introduces two events, JobPostCreated and JobPostUpdated, to emit notifications upon job creation and updates, enhancing the contract's transparency and providing hooks for off-chain applications to react to these changes.
The Hashgraph Javascript script: The JavaScript code interacts with the Hedera network to deploy and interact with the JobPost contract.
The Issue My issue is that when I try to post a new job, it says that the job was successfully created, but whenever I try to fetch the javascript code, it returns an empty element, meaning that it seems like the job was not successfully posted to the blockchain with its parameters.
Snippets of the source code
Smart contract:
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
pragma experimental ABIEncoderV2;
contract JobPost {
struct Job {
uint256 jobId;
string jobTitle;
string paymentMethod;
string accountId;
address account;
}
enum paymentMethods {
CASH,
DEBIT,
CREDIT,
CRYPTO
}
event JobPostCreated(
uint256 jobId,
string jobTitle,
string paymentMethod,
string accountId,
address account
);
event JobPostUpdated(
uint256 _jobId,
string _jobTitle,
string _paymentMethod,
address _account,
string _accountId
);
// mapping(uint => Job) JobsByIndex;
mapping(address => Job[]) JobsByAccount;
uint256 public jobCounter = 0;
constructor() {}
function newJobPostByAccount(
string memory _jobTitle,
string memory _paymentMethod,
string memory _accountId
) public {
jobCounter++;
JobsByAccount[msg.sender].push(
Job({
jobId: jobCounter,
jobTitle: _jobTitle,
paymentMethod: _paymentMethod,
accountId: _accountId,
account: msg.sender
})
);
emit JobPostCreated(
jobCounter,
_jobTitle,
_paymentMethod,
_accountId,
msg.sender
);
}
function getOneJobPostByAccount2(
address _account,
uint256 _jobId
)
public
view
returns (
address account,
string memory accountId,
uint256 jobId,
string memory jobTitle,
string memory paymentMethod
)
{
// require(_account <= jobCounter);
Job[] storage jobs = JobsByAccount[_account];
for (uint256 index = 0; index < jobs.length; index++) {
if (jobs[index].jobId == _jobId) {
return (
jobs[index].account,
jobs[index].accountId,
jobs[index].jobId,
jobs[index].jobTitle,
jobs[index].paymentMethod
);
}
}
}
function getOneJobPostByAccount(
address _account,
uint256 _jobId
) public view returns (Job memory job) {
// require(_account <= jobCounter);
Job[] storage jobs = JobsByAccount[_account];
for (uint256 index = 0; index < jobs.length; index++) {
if (jobs[index].jobId == _jobId) {
return (jobs[index]);
}
}
// return (0,"","",0x0);
}
function updateJobPostByAccount(
uint256 _jobId,
string memory _jobTitle,
string memory _paymentMethod,
address _account,
string memory _accountId
) public {
Job[] storage jobs = JobsByAccount[_account];
require(_jobId <= jobCounter);
// require(jobs, "This user does not exist");
require(jobs.length > 0, "No Job post exists for this user");
for (uint256 i = 0; i < jobs.length; i++) {
if (jobs[i].jobId == _jobId) {
jobs[i].jobTitle = _jobTitle;
jobs[i].paymentMethod = _paymentMethod;
jobs[i].account = _account;
jobs[i].accountId = _accountId;
}
}
emit JobPostUpdated(
_jobId,
_jobTitle,
_paymentMethod,
_account,
_accountId
);
}
function getAllJobsByAccount(
address _account
) public view returns (Job[] memory) {
Job[] storage jobs = JobsByAccount[_account];
// require(jobs.length > 0, "No Job post exists for this user");
if (jobs.length > 0) {
return jobs;
} else {
Job[] memory emptyJobs;
return emptyJobs;
}
}
}
Javascript Code
console.clear();
require("dotenv").config({ path: "../.env" });
const {
Client,
AccountId,
PrivateKey,
Hbar,
FileCreateTransaction,
FileAppendTransaction,
ContractCallQuery,
ContractCreateTransaction,
ContractFunctionParameters,
ContractExecuteTransaction,
} = require("@hashgraph/sdk");
const fs = require("fs");
const Web3 = require("web3");
const web3 = new Web3();
// console.log(process.env.HEDERA_ACCOUNT_ID);
const operatorId = process.env.HEDERA_ACCOUNT_ID;
const operatorKey = PrivateKey.fromStringECDSA(process.env.HEDERA_PRIVATE_KEY);
const bytecode = require("../build/contracts/JobPost.json", "utf8").bytecode;
const abi = require("../build/contracts/JobPost.json", "utf8").abi;
let bytecodeFileId;
const client = Client.forTestnet().setOperator(operatorId, operatorKey);
// Deploy the smart Contract to Hedera (createContractBytecodeFileId + uploadByteCode + instantiateContract)
await deploySmartContract(client, operatorKey, bytecode, 3000000, 10);
// Create a New Job Post
await newJobPostByAccount(
client,
contractId,
operatorId,
"Job Post #1",
"CASH"
);
// Get One Job Post By Account
await getOneJobPostByAccount2(
client,
"getOneJobPostByAccount2",
contractId,
operatorId,
2
);
async function newJobPostByAccount(
_client,
_contractId,
_accountId,
_title,
_paymentMethod,
_gas = 3000000
) {
const contractExecTx = await new ContractExecuteTransaction()
.setContractId(_contractId)
.setGas(_gas)
.setFunction(
"newJobPostByAccount",
new ContractFunctionParameters()
.addString(_title)
.addString(_paymentMethod)
.addString(_accountId)
);
const contractExecSubmit = await contractExecTx.execute(_client);
const contractExecRx = await contractExecSubmit.getReceipt(_client);
console.log(contractExecRx);
console.log(`\nCREATING A NEW JOB POST ======= \n`);
console.log(`- New Job Post Created: ${contractExecRx.status.toString()}`);
console.log(`\nRETRIEVE A JOB POST BY ACCOUNT ======= \n`);
}
async function decodeFunctionResult(functionName, resultAsBytes) {
const functionAbi = abi.find(
(func) => func.name === functionName && func.type === "function"
);
const functionParameters = functionAbi.outputs;
const resultHex = "0x".concat(Buffer.from(resultAsBytes).toString("hex"));
const result = await web3.eth.abi.decodeParameters(
functionParameters,
resultHex
);
return result;
}
async function getOneJobPostByAccount2(
_client,
_functionName,
_contractId,
_accountId,
_jobId,
_gas = 100000,
_queryPaymentInHBars = 2
) {
const contractQuery = await new ContractCallQuery()
//Set the gas for the query
.setGas(_gas)
//Set the contract ID to return the request for
.setContractId(_contractId)
//Set the contract function to call
.setFunction(
_functionName,
new ContractFunctionParameters()
.addAddress(AccountId.fromString(_accountId).toSolidityAddress())
.addUint256(_jobId)
)
//Set the query payment for the node returning the request
//This value must cover the cost of the request otherwise will fail
.setQueryPayment(new Hbar(_queryPaymentInHBars));
//Submit to a Hedera network
const contractExec = await contractQuery.execute(_client);
// console.log(contractExec);
console.log(`\nRETRIEVE A JOB POST BY ACCOUNT ======= \n`);
console.log(
`- New Job Post Account Owner: ${await contractExec.getString(0)}`
);
// console.log(`- New Job Post Created2: ${await contractExec.getString(1)}`)
console.log(`- New Job Post Title: ${await contractExec.getString(2)}`);
console.log(
`- New Job Post Payment Method: ${await contractExec.getString(3)}`
);
const res = await decodeFunctionResult(_functionName, contractExec.bytes);
console.log(res[0]);
return res[0];
}
Console Output
Below is the link to the deployed contract and what my console is outputting
The problem lies in the usage of AccountId.fromString(_accountId).toSolidityAddress()
. The JavaScript code will work as expected if you change it with your actual EVM address (0xdfad8138e04c7932c26c0a37d3711fa9165704f1
).
The toSolidityAddress()
function is static and does not access the network. Therefore, it works only for non-EVM native accounts, converting the account ID into hex values.
For example, this 0.0.5102042
is just a random account with a ED25519 key, and its EVM address derives from the account ID; it is 0x00000000000000000000000000000000004dd9da
, and that can be computed offline.
In your case, you are using an EVM native account instead, so the address is derived from the public key, not the account ID, so the toSolidityAddress()
function cannot be used because it will return an incorrect value.
About the behavior change - it was working before, but now it is not - can be due to using a non-EVM native account the first time. In that case, the toSolidityAddress()
incompatibility didn't emerge.