Substrate With A WASM Smart Contract Environment
Acurast can be used to fulfill directly to smart contracts deployed on Substrate chains with a pallet-contracts integration.
Example integration with a WASM smart contract
The following example shows simple WASM smart contracts implemented with ink!.
Keep in mind that you can do much more with Acurast and get access to all modules besides these examples.
- Price Feed
- Verifiable Randomness
This example contract can store muliple prices for different symbols in the prices
map. Furthermore, only processors that have their address added to the allow_list
through the allow
method will be able to provide a price.
#![cfg_attr(not(feature = "std"), no_std, no_main)]
#[ink::contract]
mod simple_oracle {
use ink::storage::Mapping;
use ink::prelude::vec::Vec;
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
CallerNotInAllowList,
NotAdmin
}
pub type Result<T> = core::result::Result<T, Error>;
#[ink(storage)]
pub struct SimpleOracle {
prices: Mapping<Vec<u8>, u128>,
allow_list: Mapping<AccountId, ()>,
admin: Option<AccountId>,
}
impl SimpleOracle {
#[ink(constructor)]
pub fn new(admin: Option<AccountId>) -> Self {
Self {
prices: Mapping::new(),
allow_list: Mapping::new(),
admin,
}
}
#[ink(constructor)]
pub fn default() -> Self {
Self::new(None)
}
#[ink(message)]
pub fn fulfill(&mut self, values: Vec<(Vec<u8>, u128)>) -> Result<()> {
if !self.allow_list.contains(self.env().caller()) {
return Err(Error::CallerNotInAllowList);
}
for (key, value) in values {
self.prices.insert(key, &value);
}
Ok(())
}
#[ink(message)]
pub fn allow(&mut self, account: AccountId) -> Result<()> {
if let Some(admin) = self.admin {
if self.env().caller() != admin {
return Err(Error::NotAdmin)
}
}
self.allow_list.insert(account, &());
Ok(())
}
#[ink(message)]
pub fn disallow(&mut self, account: AccountId) -> Result<()> {
if let Some(admin) = self.admin {
if self.env().caller() != admin {
return Err(Error::NotAdmin)
}
}
self.allow_list.remove(account);
Ok(())
}
/// Returns the current value for given key.
#[ink(message)]
pub fn get(&self, key: Vec<u8>) -> Option<u128> {
self.prices.get(key)
}
}
#[cfg(test)]
mod tests {
/// Imports all the definitions from the outer scope so we can use them here.
use super::*;
/// We test if the default constructor does its job.
#[ink::test]
fn default_works() {
let simple_oracle = SimpleOracle::default();
assert_eq!(simple_oracle.get(Default::default()), None);
}
/// We test a simple use case of our contract.
#[ink::test]
fn it_works() {
let mut simple_oracle = SimpleOracle::new(None);
assert_eq!(simple_oracle.get(Default::default()), None);
simple_oracle.allow(AccountId::from([1; 32])).expect("can allow account");
simple_oracle.fulfill(vec![(vec![0], 10)]).expect("fulfill works");
assert_eq!(simple_oracle.get(vec![0]), Some(10));
}
}
}
#![cfg_attr(not(feature = "std"), no_std)]
use ink;
#[ink::contract]
mod receiver {
#[ink(storage)]
pub struct Receiver {
price: u128,
}
/// Defines the storage of your contract.
/// Add new fields to the below struct in order
/// to add new static storage fields to your contract.
#[ink(storage)]
pub struct Receiver {
random_bytes: Vec<u8>,
}
impl Receiver {
#[ink(constructor)]
pub fn default() -> Self {
Self {
random_bytes: Default::default(),
}
}
#[ink(message)]
pub fn fulfill(&mut self, bytes: Vec<u8>) {
self.random_bytes = bytes;
}
#[ink(message)]
pub fn get_bytes(&self) -> Vec<u8> {
self.random_bytes.clone()
}
}
}
Job Specification
Now that the contract has been deployed, we can prepare the script that will get executed by the Processor to provision the price feed or entropy.
Go to the Acurast Console to test and deploy your script, these templates can also be found there.
- Price Feed
- Verifiable Randomness
const callIndex = "<#CALL_INDEX#>"; // the call index for the 'call' extrinsic.
const destination = "<#CONTRACT_ADDRESS#>"; // contract address that will receive the 'fulfill' call.
_STD_.chains.substrate.signer.setSigner("SECP256K1"); // the type of signer used for sign the extrinsic call
const symbol = "AAVEBUSD";
httpGET(
"https://api.binance.com/api/v3/ticker/price?symbol=" + symbol,
{},
(response, _certificate) => {
const price = Number(JSON.parse(response)["price"]) * 10 ** 18;
const key = stringToHex(symbol);
const payload =
_STD_.chains.substrate.codec.encodeCompactUnsignedNumber(1) +
_STD_.chains.substrate.codec.encodeBytes(key) +
_STD_.chains.substrate.codec.encodeUnsignedNumber(price, 128);
_STD_.chains.substrate.contract.fulfill(
"<#NODE_URL#>",
callIndex,
destination,
payload,
{
refTime: "1470675162",
proofSize: "37458",
},
(opHash) => {
print("Succeeded: " + opHash);
},
(err) => {
print("Failed fulfill: " + err);
}
);
},
(err) => {
print("Failed get price: " + err);
}
);
function stringToHex(str) {
let hexString = "";
for (let i = 0; i < str.length; i++) {
let hex = str.charCodeAt(i).toString(16);
hexString += hex.length === 1 ? "0" + hex : hex;
}
return hexString;
}
const callIndex = "0x4606"; // the call index for the 'call' extrinsic.
const destination = "<MY_WASM_CONTRACT_ADDRESS>"; // contract address that will receive the 'fulfill' call.
_STD_.chains.substrate.signer.setSigner("SECP256K1"); // the type of signer used for sign the extrinsic call
httpGET(
"https://api.binance.com/api/v3/ticker/price?symbol=AAVEBUSD",
{},
(response, _certificate) => {
const price = JSON.parse(response)["price"] * 10 ** 18;
const payload = _STD_.chains.substrate.codec.encodeUnsignedNumber(
price,
128
);
_STD_.chains.substrate.contract.fulfill(
"https://rpc.shibuya.astar.network",
callIndex,
destination,
payload,
{
refTime: "3951114240",
proofSize: "629760",
},
(opHash) => {
print("Succeeded: " + opHash);
},
(err) => {
print("Failed fulfill: " + err);
}
);
},
(err) => {
print("Failed get price: " + err);
}
);
Job Registration
Check out How to get started with the Acurast Console to register your Job.