Skip to main content

Web3 Request

In this tutorial, we'll create smart contracts that interact with Oyster Serverless. You'll learn how to make Web3 requests from your contracts and handle the response callbacks.

Prerequisites​

Before starting, ensure you have:

  • A Metamask wallet with an active account
  • Arbitrum One ETH (approximately 0.001 ETH) to cover deployment and transaction costs
  • USDC tokens on Arbitrum One for paying serverless fees
  • Node.js installed

Installation​

Begin by cloning the Oyster Serverless development tools repository and navigating to the repository directory:

git clone https://github.com/marlinprotocol/oyster-serverless-devtools.git
cd oyster-serverless-devtools/user_contract_builder

Initialize and install the npm dependencies:

npm install

Run the following command to verify your Hardhat installation:

npx hardhat

Building your smart contract​

First, create a new Solidity file called contracts/User.sol with the following code:

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract User is Ownable {
using SafeERC20 for IERC20;
address public relayAddress;

IERC20 public usdcToken;

constructor(address _relayAddress, address _token, address _owner) Ownable(_owner) {
relayAddress = _relayAddress;
usdcToken = IERC20(_token);
}

event OwnerEthWithdrawal();
event OwnerUsdcWithdrawal();
error EthWithdrawalFailed();
error UsdcWithdrawalFailed();

function relayJob(
uint8 _env,
bytes32 _codehash,
bytes memory _codeInputs,
uint256 _userTimeout,
uint256 _maxGasPrice,
uint256 _usdcDeposit,
uint256 _callbackDeposit,
address _refundAccount,
address _callbackContract,
uint256 _callbackGasLimit
) external returns (bool) {
usdcToken.safeIncreaseAllowance(relayAddress, _usdcDeposit);

(bool success, ) = relayAddress.call{value: _callbackDeposit}(
abi.encodeWithSignature(
"relayJob(uint8,bytes32,bytes,uint256,uint256,address,address,uint256)",
_env,
_codehash,
_codeInputs,
_userTimeout,
_maxGasPrice,
_refundAccount,
_callbackContract,
_callbackGasLimit
)
);
return success;
}

event CalledBack(
uint256 indexed jobId,
address jobOwner,
bytes32 codehash,
bytes codeInputs,
bytes outputs,
uint8 errorCode
);

function oysterResultCall(
uint256 _jobId,
address _jobOwner,
bytes32 _codehash,
bytes calldata _codeInputs,
bytes calldata _output,
uint8 _errorCode
) public {
emit CalledBack(_jobId, _jobOwner, _codehash, _codeInputs, _output, _errorCode);
}

function withdrawEth() external onlyOwner {
(bool success, ) = msg.sender.call{value: address(this).balance}("");
if (!success) revert EthWithdrawalFailed();

emit OwnerEthWithdrawal();
}

function withdrawUsdc() external onlyOwner {
uint256 balance = usdcToken.balanceOf(address(this));
usdcToken.transfer(owner(), balance);

emit OwnerUsdcWithdrawal();
}

receive() external payable {}
}

Compile the contract by running:

npx hardhat compile

Deploying your contract​

Create a deployment script in the script/deploy directory called User.ts with the following code:

import { ethers } from "hardhat";

async function main() {

let relayAddress = "0xD28179711eeCe385bc2096c5D199E15e6415A4f5";
let usdcToken = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
let owner = await (await ethers.getSigners())[0].getAddress();
const user = await ethers.deployContract(
"User",
[
relayAddress,
usdcToken,
owner
]
);
await user.waitForDeployment();
console.log("User Contract is deployed at ", user.target);
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

Ensure your Metamask account has sufficient Arbitrum One ETH and obtain your private key. Additionally, obtain an Arbiscan API Key by following their API key generation guide. Both the private key and API key will be needed for deployment.

Create a .env file in the user_contract_builder directory with the following environment variables. Replace the placeholders with your actual values:

ARBITRUM_DEPLOYER_KEY=<deployer-account-private-key>
ARBISCAN_API_KEY=<arbiscan-api-key>

Finally, run the following command to deploy the contract to the Arbitrum One Mainnet. Upon successful deployment, the contract address will be printed to the console:

npx hardhat run script/deploy/User.ts --network arb1

Transfer ETH to deployed user contract address to be used as deposit for response callbacks. Follow this Metamask Guide to transfer ETH. User contract address is used as recipient and amount of 0.0005 ETH is sufficient for this example.

Transfer USDC tokens to deployed user contract address to be used as deposit and payment for serverless job. USDC token contract used here is deployed at address 0xaf88d065e77c8cC2239327C5EDb3A432268e5831. Send 5 USDC for this example.

Create a serverless request​

Start hardhat console by running npx hardhat console --network arb1.

Create a User contract instance by running (replace the address with your deployed contract address):

user = (await ethers.getContractFactory("User")).attach("0x2843B47367C5FE819F7F086Ba01Ae90963B3E8F5");

Call the relayJob function to create a serverless request.

await user.relayJob(1, "0x27139ece7a6558c26c3773647822837612a5159c0d757c96cc86c008466dfb7a", "0x", 2000, ethers.parseUnits("5", 7), 5000, ethers.parseUnits("5", 14), <User Address>, user.target, 5000, {gasLimit: 2000000});

Verify the response callback​

After approximately 60 seconds, verify that the response callback was received by checking the event logs. Run the following command to query for "CalledBack" events:

await user.queryFilter("CalledBack");

Get back remaining funds​

Withdraw the remaining funds from the user contract by calling the withdrawEth or withdrawUsdc functions.

await user.withdrawEth();
await user.withdrawUsdc();

Congratulations! You've successfully created a smart contract that makes serverless requests and handles response callbacks. You've learned how to deploy the contract, fund it, execute requests, and withdraw funds. Next, you'll learn how to create subscription-based serverless requests using smart contracts.