Sui Weather Oracle
This guide demonstrates writing a module (smart contract) in Move, deploying it on Devnet, and adding a backend, which fetches the weather data from the OpenWeather API every 10 minutes and updates the weather conditions for each city. The dApp created in this guide is called Sui Weather Oracle and it provides real-time weather data for over 1,000 locations around the world.
You can access and use the weather data from the OpenWeather API for various applications, such as randomness, betting, gaming, insurance, travel, education, or research. You can also mint a weather NFT based on the weather data of a city, using the mint
function of the SUI Weather Oracle smart contract.
This guide assumes you have installed Sui and understand Sui fundamentals.
Move smart contract
As with all Sui dApps, a Move package on chain powers the logic of Sui Weather Oracle. The following instruction walks you through creating and publishing the module.
Weather Oracle module
Before you get started, you must initialize a Move package. Open a terminal or console in the directory you want to store the example and run the following command to create an empty package with the name weather_oracle
:
sui move new weather_oracle
With that done, it's time to jump into some code. Create a new file in the sources
directory with the name weather.move
and populate the file with the following code:
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module oracle::weather {
use std::option::{Self, Option};
use std::string::{Self, String};
use sui::dynamic_object_field as dof;
use sui::object::{Self, UID};
use sui::package;
use sui::transfer::{Self};
use sui::tx_context::{Self, TxContext};
}
There are few details to take note of in this code:
- The fourth line declares the module name as
weather
within the packageoracle
. - Seven lines begin with the
use
keyword, which enables this module to use types and functions declared in other modules.
Next, add some more code to this module:
/// Define a capability for the admin of the oracle.
struct AdminCap has key, store { id: UID }
/// // Define a one-time witness to create the `Publisher` of the oracle.
struct WEATHER has drop {}
// Define a struct for the weather oracle
struct WeatherOracle has key {
id: UID,
/// The address of the oracle.
address: address,
/// The name of the oracle.
name: String,
/// The description of the oracle.
description: String,
}
struct CityWeatherOracle has key, store {
id: UID,
geoname_id: u32, // The unique identifier of the city
name: String, // The name of the city
country: String, // The country of the city
latitude: u32, // The latitude of the city in degrees
positive_latitude: bool, // Whether the latitude is positive (north) or negative (south)
longitude: u32, // The longitude of the city in degrees
positive_longitude: bool, // Whether the longitude is positive (east) or negative (west)
weather_id: u16, // The weather condition code
temp: u32, // The temperature in kelvin
pressure: u32, // The atmospheric pressure in hPa
humidity: u8, // The humidity percentage
visibility: u16, // The visibility in meters
wind_speed: u16, // The wind speed in meters per second
wind_deg: u16, // The wind direction in degrees
wind_gust: Option<u16>, // The wind gust in meters per second (optional)
clouds: u8, // The cloudiness percentage
dt: u32 // The timestamp of the weather update in seconds since epoch
}
fun init(otw: WEATHER, ctx: &mut TxContext) {
package::claim_and_keep(otw, ctx); // Claim ownership of the one-time witness and keep it
let cap = AdminCap { id: object::new(ctx) }; // Create a new admin capability object
transfer::share_object(WeatherOracle {
id: object::new(ctx),
address: tx_context::sender(ctx),
name: string::utf8(b"SuiMeteo"),
description: string::utf8(b"A weather oracle."),
});
transfer::public_transfer(cap, tx_context::sender(ctx)); // Transfer the admin capability to the sender.
}
- The first struct,
AdminCap
, is a capability. - The second struct,
WEATHER
, is a one-time witness that ensures only a single instance of thisWeather
ever exists. - The
WeatherOracle
struct works as a registry and stores thegeoname_id
s of theCityWeatherOracle
s as dynamic fields. - The
init
function creates and sends thePublisher
andAdminCap
objects to the sender. Also, it creates a shared object for all theCityWeatherOracle
s.
So far, you've set up the data structures within the module.
Now, create a function that initializes a CityWeatherOracle
and adds it as dynamic fields to the WeatherOracle
object:
public fun add_city(
_: &AdminCap, // The admin capability
oracle: &mut WeatherOracle, // A mutable reference to the oracle object
geoname_id: u32, // The unique identifier of the city
name: String, // The name of the city
country: String, // The country of the city
latitude: u32, // The latitude of the city in degrees
positive_latitude: bool, // The whether the latitude is positive (north) or negative (south)
longitude: u32, // The longitude of the city in degrees
positive_longitude: bool, // The whether the longitude is positive (east) or negative (west)
ctx: &mut TxContext // A mutable reference to the transaction context
) {
dof::add(&mut oracle.id, geoname_id, // Add a new dynamic object field to the oracle object with the geoname ID as the key and a new city weather oracle object as the value.
CityWeatherOracle {
id: object::new(ctx), // Assign a unique ID to the city weather oracle object
geoname_id, // Set the geoname ID of the city weather oracle object
name, // Set the name of the city weather oracle object
country, // Set the country of the city weather oracle object
latitude, // Set the latitude of the city weather oracle object
positive_latitude, // Set whether the latitude is positive (north) or negative (south)
longitude, // Set the longitude of the city weather oracle object
positive_longitude, // Set whether the longitude is positive (east) or negative (west)
weather_id: 0, // Initialize the weather condition code to be zero
temp: 0, // Initialize the temperature to be zero
pressure: 0, // Initialize the pressure to be zero
humidity: 0, // Initialize the humidity to be zero
visibility: 0, // Initialize the visibility to be zero
wind_speed: 0, // Initialize the wind speed to be zero
wind_deg: 0, // Initialize the wind direction to be zero
wind_gust: option::none(), // Initialize the wind gust to be none
clouds: 0, // Initialize the cloudiness to be zero
dt: 0 // Initialize the timestamp to be zero
}
);
}
The add_city
function is a public function that allows the owner of the AdminCap
of the Sui Weather Oracle smart contract to add a new CityWeatherOracle
. The function requires the admin to provide a capability object that proves their permission to add a city. The function also requires a mutable reference to the oracle object, which is the main object that stores the weather data on the blockchain. The function takes several parameters that describe the city, such as the geoname ID, name, country, latitude, longitude, and positive latitude and longitude. The function then creates a new city weather oracle object, which is a sub-object that stores and updates the weather data for a specific city. The function initializes the city weather oracle object with the parameters provided by the admin, and sets the weather data to be zero or none. The function then adds a new dynamic object field to the oracle object, using the geoname ID as the key and the city weather oracle object as the value. This way, the function adds a new city to the oracle, and makes it ready to receive and update the weather data from the backend service.
If you want to delete a city from the Sui Weather Oracle, call the remove_city
function of the smart contract. The remove_city
function allows the admin of the smart contract to remove a city from the oracle. The function requires the admin to provide a capability object that proves their permission to remove a city. The function also requires a mutable reference to the oracle object, which is the main object that stores and updates the weather data on the blockchain. The function takes the geoname ID of the city as a parameter, and deletes the city weather oracle object for the city. The function also removes the dynamic object field for the city from the oracle object. This way, the function deletes a city from the oracle, and frees up some storage space on the blockchain.
public fun remove_city(
_: &AdminCap,
oracle: &mut WeatherOracle,
geoname_id: u32
) {
let CityWeatherOracle {
id,
geoname_id: _,
name: _,
country: _,
latitude: _,
positive_latitude: _,
longitude: _,
positive_longitude: _,
weather_id: _,
temp: _,
pressure: _,
humidity: _,
visibility: _,
wind_speed: _,
wind_deg: _,
wind_gust: _,
clouds: _,
dt: _ } = dof::remove(&mut oracle.id, geoname_id);
object::delete(id);
}
Now that you have implemented the add_city
and remove_city
functions, you can move on to the next step, which is to see how you can update the weather data for each city. The backend service fetches the weather data from the OpenWeather API every 10 minutes, and then passes the data to the update
function of the Sui Weather Oracle smart contract. The update
function takes the geoname ID and the new weather data of the city as parameters, and updates the city weather oracle object with the new data. This way, the weather data on the blockchain is always up to date and accurate.
public fun update(
_: &AdminCap,
oracle: &mut WeatherOracle,
geoname_id: u32,
weather_id: u16,
temp: u32,
pressure: u32,
humidity: u8,
visibility: u16,
wind_speed: u16,
wind_deg: u16,
wind_gust: Option<u16>,
clouds: u8,
dt: u32
) {
let city_weather_oracle_mut = dof::borrow_mut<u32, CityWeatherOracle>(&mut oracle.id, geoname_id); // Borrow a mutable reference to the city weather oracle object with the geoname ID as the key
city_weather_oracle_mut.weather_id = weather_id;
city_weather_oracle_mut.temp = temp;
city_weather_oracle_mut.pressure = pressure;
city_weather_oracle_mut.humidity = humidity;
city_weather_oracle_mut.visibility = visibility;
city_weather_oracle_mut.wind_speed = wind_speed;
city_weather_oracle_mut.wind_deg = wind_deg;
city_weather_oracle_mut.wind_gust = wind_gust;
city_weather_oracle_mut.clouds = clouds;
city_weather_oracle_mut.dt = dt;
}
You have defined the data structure of the Sui Weather Oracle smart contract, but you need some functions to access and manipulate the data. Now, add some helper functions that read and return the weather data for a WeatherOracle
object. These functions allow you to get the weather data for a specific city in the oracle. These functions also allow you to format and display the weather data in a user-friendly way.
// --------------- Read-only References ---------------
/// Returns the `name` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_name(
weather_oracle: &WeatherOracle,
geoname_id: u32
): String {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.name
}
/// Returns the `country` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_country(
weather_oracle: &WeatherOracle,
geoname_id: u32
): String {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.country
}
/// Returns the `latitude` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_latitude(
weather_oracle: &WeatherOracle,
geoname_id: u32
): u32 {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.latitude
}
/// Returns the `positive_latitude` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_positive_latitude(
weather_oracle: &WeatherOracle,
geoname_id: u32
): bool {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.positive_latitude
}
/// Returns the `longitude` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_longitude(
weather_oracle: &WeatherOracle,
geoname_id: u32
): u32 {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.longitude
}
/// Returns the `positive_longitude` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_positive_longitude(
weather_oracle: &WeatherOracle,
geoname_id: u32
): bool {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.positive_longitude
}
/// Returns the `weather_id` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_weather_id(
weather_oracle: &WeatherOracle,
geoname_id: u32
): u16 {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.weather_id
}
/// Returns the `temp` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_temp(
weather_oracle: &WeatherOracle,
geoname_id: u32
): u32 {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.temp
}
/// Returns the `pressure` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_pressure(
weather_oracle: &WeatherOracle,
geoname_id: u32
): u32 {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.pressure
}
/// Returns the `humidity` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_humidity(
weather_oracle: &WeatherOracle,
geoname_id: u32
): u8 {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.humidity
}
/// Returns the `visibility` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_visibility(
weather_oracle: &WeatherOracle,
geoname_id: u32
): u16 {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.visibility
}
/// Returns the `wind_speed` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_wind_speed(
weather_oracle: &WeatherOracle,
geoname_id: u32
): u16 {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.wind_speed
}
/// Returns the `wind_deg` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_wind_deg(
weather_oracle: &WeatherOracle,
geoname_id: u32
): u16 {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.wind_deg
}
/// Returns the `wind_gust` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_wind_gust(
weather_oracle: &WeatherOracle,
geoname_id: u32
): Option<u16> {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.wind_gust
}
/// Returns the `clouds` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_clouds(
weather_oracle: &WeatherOracle,
geoname_id: u32
): u8 {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.clouds
}
/// Returns the `dt` of the `CityWeatherOracle` with the given `geoname_id`.
public fun city_weather_oracle_dt(
weather_oracle: &WeatherOracle,
geoname_id: u32
): u32 {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
city_weather_oracle.dt
}
Finally, add an extra feature that allows anyone to mint a WeatherNFT
with the current conditions of a city by passing the geoname ID. The mint
function is a public function that allows anyone to mint a weather NFT based on the weather data of a city. The function takes the WeatherOracle
shared object and the geoname ID of the city as parameters, and returns a new WeatherNFT
object for the city. The WeatherNFT
object is a unique and non-fungible token that represents the weather of the city at the time of minting. The WeatherNFT
object has the same data as the CityWeatherOracle
object, such as the geonameID
, name
, country
, latitude
, longitude
, positive latitude
and longitude
, weather ID
, temperature
, pressure
, humidity
, visibility
, wind speed
, wind degree
, wind gust
, clouds
, and timestamp
. The function creates the WeatherNFT
object by borrowing a reference to the CityWeatherOracle
object with the geoname ID as the key, and assigning a unique ID (UID
) to the WeatherNFT
object. The function then transfers the ownership of the WeatherNFT
object to the sender of the transaction. This way, the function allows anyone to mint a weather NFT and own a digital representation of the weather of a city. You can use this feature to create your own collection of weather NFTs, or to use them for other applications that require verifiable and immutable weather data.
struct WeatherNFT has key, store {
id: UID,
geoname_id: u32,
name: String,
country: String,
latitude: u32,
positive_latitude: bool,
longitude: u32,
positive_longitude: bool,
weather_id: u16,
temp: u32,
pressure: u32,
humidity: u8,
visibility: u16,
wind_speed: u16,
wind_deg: u16,
wind_gust: Option<u16>,
clouds: u8,
dt: u32
}
public fun mint(
oracle: &WeatherOracle,
geoname_id: u32,
ctx: &mut TxContext
): WeatherNFT {
let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&oracle.id, geoname_id);
WeatherNFT {
id: object::new(ctx),
geoname_id: city_weather_oracle.geoname_id,
name: city_weather_oracle.name,
country: city_weather_oracle.country,
latitude: city_weather_oracle.latitude,
positive_latitude: city_weather_oracle.positive_latitude,
longitude: city_weather_oracle.longitude,
positive_longitude: city_weather_oracle.positive_longitude,
weather_id: city_weather_oracle.weather_id,
temp: city_weather_oracle.temp,
pressure: city_weather_oracle.pressure,
humidity: city_weather_oracle.humidity,
visibility: city_weather_oracle.visibility,
wind_speed: city_weather_oracle.wind_speed,
wind_deg: city_weather_oracle.wind_deg,
wind_gust: city_weather_oracle.wind_gust,
clouds: city_weather_oracle.clouds,
dt: city_weather_oracle.dt
}
}
And with that, your weather.move
code is complete.
Deployment
See Publish a Package for a more detailed guide on publishing packages or Sui Client CLI for a complete reference of client
commands in the Sui CLI.
Before publishing your code, you must first initialize the Sui Client CLI. To do so, in a terminal or console at the root directory of the project enter sui client
. You receive the following response:
Config file ["[LINK_TO_PATH/.sui/sui_config/client.yaml"] doesn't exist, do you want to connect to a Sui Full node server [y/N]?
Enter y
to proceed. You receive the following response:
Sui Full node server URL (Defaults to Sui Devnet if not specified) :
Leave this blank (press Enter). You receive the following response:
Select key scheme to generate keypair (0 for ed25519, 1 for secp256k1, 2: for secp256r1):
Select 0
. Now you should have a Sui address set up.
Before being able to publish your package to Devnet, however, you need Devnet SUI tokens. To get some, join the Sui Discord, complete the verification steps, enter the #devnet-faucet
channel and type !faucet <WALLET ADDRESS>
. For other ways to get SUI in your Devnet account, see Get SUI Tokens.
Now that you have an account with some Devnet SUI, you can deploy your contracts. To publish your package, use the following command in the same terminal or console:
sui client publish --gas-budget <GAS-BUDGET>
For the gas budget, use a standard value such as 20000000
.
Backend
You have successfully deployed the Sui Weather Oracle smart contract on the blockchain. Now, it's time to create an Express backend that can interact with it. The Express backend performs the following tasks:
- Initialize the smart contract with 1,000 cities using the
add_city
function of the smart contract. The backend passes the geoname ID, name, country, latitude, longitude, and positive latitude and longitude of each city as parameters to the function. - Fetch the weather data for each city from the OpenWeather API every 10 minutes, using the API key that you obtained from the website. The backend parses the JSON response and extracts the weather data for each city, such as the weather ID, temperature, pressure, humidity, visibility, wind speed, wind degree, wind gust, clouds, and timestamp.
- Update the weather data for each city on the blockchain, using the
update
function of the smart contract. The backend passes the geoname ID and the new weather data of each city as parameters to the function.
The Express backend uses the Sui Typescript SDK, a TypeScript library that enables you to interact with the Sui blockchain and smart contracts. With the Sui Typescript SDK, you can connect to the Sui network, sign and submit transactions, and query the state of the smart contract. You also use the OpenWeather API to fetch the weather data for each city and update the smart contract every 10 minutes. Additionally, you can mint weather NFTs, if you want to explore that feature of the smart contract.
Initialize the project
First, initialize your backend project. To do this, you need to follow these steps:
- Create a new folder named
weather-oracle-backend
and navigate to it in your terminal. - Run
npm init -y
to create a package.json file with default values. - Run
npm install express --save
to install Express as a dependency and save it to your package.json file. - Run
npm install @mysten/bcs @mysten/sui.js axios csv-parse csv-parser dotenv pino retry-axios --save
to install the other dependencies and save them to your package.json file. These dependencies are:- @mysten/bcs: a library for blockchain services.
- @mysten/sui.js: a library for smart user interfaces.
- axios: a library for making HTTP requests.
- csv-parse: a library for parsing CSV data.
- csv-parser: a library for transforming CSV data into JSON objects.
- dotenv: a library for loading environment variables from a .env file.
- pino: a library for fast and low-overhead logging.
- retry-axios: a library for retrying failed axios requests.
- Create a new file named
init.ts
import {
Connection,
Ed25519Keypair,
JsonRpcProvider,
RawSigner,
TransactionBlock,
} from "@mysten/sui.js";
import * as dotenv from "dotenv";
import { City } from "./city";
import { get1000Geonameids } from "./filter-cities";
import { latitudeMultiplier, longitudeMultiplier } from "./multipliers";
import { getCities, getWeatherOracleDynamicFields } from "./utils";
import { logger } from "./utils/logger";
dotenv.config({ path: "../.env" });
const phrase = process.env.ADMIN_PHRASE;
const fullnode = process.env.FULLNODE!;
const keypair = Ed25519Keypair.deriveKeypair(phrase!);
const provider = new JsonRpcProvider(
new Connection({
fullnode: fullnode,
})
);
const signer = new RawSigner(keypair, provider);
const packageId = process.env.PACKAGE_ID;
const adminCap = process.env.ADMIN_CAP_ID!;
const weatherOracleId = process.env.WEATHER_ORACLE_ID!;
const moduleName = "weather";
const NUMBER_OF_CITIES = 10;
async function addCityWeather() {
const cities: City[] = await getCities();
const thousandGeoNameIds = await get1000Geonameids();
const weatherOracleDynamicFields = await getWeatherOracleDynamicFields(
provider,
weatherOracleId
);
const geonames = weatherOracleDynamicFields.map(function (obj) {
return obj.name;
});
let counter = 0;
let transactionBlock = new TransactionBlock();
for (let c in cities) {
if (
!geonames.includes(cities[c].geonameid) &&
thousandGeoNameIds.includes(cities[c].geonameid)
) {
transactionBlock.moveCall({
target: `${packageId}::${moduleName}::add_city`,
arguments: [
transactionBlock.object(adminCap), // adminCap
transactionBlock.object(weatherOracleId), // WeatherOracle
transactionBlock.pure(cities[c].geonameid), // geoname_id
transactionBlock.pure(cities[c].asciiname), // asciiname
transactionBlock.pure(cities[c].countryCode), // country
transactionBlock.pure(cities[c].latitude * latitudeMultiplier), // latitude
transactionBlock.pure(cities[c].latitude > 0), // positive_latitude
transactionBlock.pure(cities[c].longitude * longitudeMultiplier), // longitude
transactionBlock.pure(cities[c].longitude > 0), // positive_longitude
],
});
counter++;
if (counter === NUMBER_OF_CITIES) {
await signAndExecuteTransactionBlock(transactionBlock);
counter = 0;
transactionBlock = new TransactionBlock();
}
}
}
await signAndExecuteTransactionBlock(transactionBlock);
}
async function signAndExecuteTransactionBlock(
transactionBlock: TransactionBlock
) {
transactionBlock.setGasBudget(5000000000);
await signer
.signAndExecuteTransactionBlock({
transactionBlock,
requestType: "WaitForLocalExecution",
options: {
showObjectChanges: true,
showEffects: true,
},
})
.then(function (res) {
logger.info(res);
});
}
addCityWeather();
The code of init.ts does the following:
- Imports the necessary modules and classes from the library, such as
Connection
,Ed25519Keypair
,JsonRpcProvider
,RawSigner
, andTransactionBlock
. - Imports the
dotenv
module to load environment variables from a .env file. - Imports some custom modules and functions from the local files, such as
City
,get1000Geonameids
,getCities
,getWeatherOracleDynamicFields
, andlogger
. - Derives a key pair from a phrase stored in the
ADMIN_PHRASE
environment variable. - Creates a provider object that connects to a Full node specified by the
FULLNODE
environment variable. - Creates a signer object that uses the key pair and the provider to sign and execute transactions on the blockchain.
- Reads some other environment variables, such as
PACKAGE_ID
,ADMIN_CAP_ID
,WEATHER_ORACLE_ID
, andMODULE_NAME
, which are used to identify the weather oracle contract and its methods. - Defines a constant
NUMBER_OF_CITIES
, which is the number of cities to be added to the weather oracle in each batch. - Defines an async function
addCityWeather
, which does the following:- Gets an array of cities from the
getCities
function. - Gets an array of 1,000 geonameids from the
get1000Geonameids
function. - Gets an array of weather oracle dynamic fields from the
getWeatherOracleDynamicFields
function, which contains the geonameids of the existing cities in the weather oracle. - Initializes a counter and a transaction block object.
- Loops through the cities array and checks if the city's geonameid is not in the weather oracle dynamic fields array and is in the 1,000 geonameids array.
- If the condition is met, adds a
moveCall
to the transaction block, which calls theadd_city
method of the weather oracle contract with the city's information, such asgeonameid
,asciiname
,country
,latitude
, andlongitude
. - Increments the counter and checks if it reaches the
NUMBER_OF_CITIES
. If so, calls another async function,signAndExecuteTransactionBlock
, with the transaction block as an argument, which signs and executes the transaction block on the blockchain and logs the result. The code then resets the counter and the transaction block. - After the loop ends, calls the
signAndExecuteTransactionBlock
function again with the remaining transaction block.
- Gets an array of cities from the
You have now initialized the WeatherOracle
shared object. The next step is to learn how to update them every 10 minutes with the latest weather data from the OpenWeatherMap API.
import {
Connection,
Ed25519Keypair,
JsonRpcProvider,
RawSigner,
TransactionBlock,
} from "@mysten/sui.js";
import * as dotenv from "dotenv";
import { City } from "./city";
import {
tempMultiplier,
windGustMultiplier,
windSpeedMultiplier,
} from "./multipliers";
import { getWeatherData } from "./openweathermap";
import { getCities, getWeatherOracleDynamicFields } from "./utils";
import { logger } from "./utils/logger";
dotenv.config({ path: "../.env" });
const phrase = process.env.ADMIN_PHRASE;
const fullnode = process.env.FULLNODE!;
const keypair = Ed25519Keypair.deriveKeypair(phrase!);
const provider = new JsonRpcProvider(
new Connection({
fullnode: fullnode,
})
);
const signer = new RawSigner(keypair, provider);
const packageId = process.env.PACKAGE_ID;
const adminCap = process.env.ADMIN_CAP_ID!;
const weatherOracleId = process.env.WEATHER_ORACLE_ID!;
const appid = process.env.APPID!;
const moduleName = "weather";
const CHUNK_SIZE = 25;
const MS = 1000;
const MINUTE = 60 * MS;
const TEN_MINUTES = 10 * MINUTE;
async function performUpdates(
cities: City[],
weatherOracleDynamicFields: {
name: number;
objectId: string;
}[]
) {
let startTime = new Date().getTime();
const geonames = weatherOracleDynamicFields.map(function (obj) {
return obj.name;
});
const filteredCities = cities.filter((c) => geonames.includes(c.geonameid));
for (let i = 0; i < filteredCities.length; i += CHUNK_SIZE) {
const chunk = filteredCities.slice(i, i + CHUNK_SIZE);
let transactionBlock = await getTransactionBlock(chunk);
try {
await signer.signAndExecuteTransactionBlock({
transactionBlock,
});
} catch (e) {
logger.error(e);
}
}
let endTime = new Date().getTime();
setTimeout(
performUpdates,
TEN_MINUTES - (endTime - startTime),
cities,
weatherOracleDynamicFields
);
}
async function getTransactionBlock(cities: City[]) {
let transactionBlock = new TransactionBlock();
let counter = 0;
for (let c in cities) {
const weatherData = await getWeatherData(
cities[c].latitude,
cities[c].longitude,
appid
);
counter++;
if (weatherData?.main?.temp !== undefined) {
transactionBlock.moveCall({
target: `${packageId}::${moduleName}::update`,
arguments: [
transactionBlock.object(adminCap), // AdminCap
transactionBlock.object(weatherOracleId), // WeatherOracle
transactionBlock.pure(cities[c].geonameid), // geoname_id
transactionBlock.pure(weatherData.weather[0].id), // weather_id
transactionBlock.pure(weatherData.main.temp * tempMultiplier), // temp
transactionBlock.pure(weatherData.main.pressure), // pressure
transactionBlock.pure(weatherData.main.humidity), // humidity
transactionBlock.pure(weatherData.visibility), // visibility
transactionBlock.pure(weatherData.wind.speed * windSpeedMultiplier), // wind_speed
transactionBlock.pure(weatherData.wind.deg), // wind_deg
transactionBlock.pure(
weatherData.wind.gust === undefined
? []
: [weatherData.wind.gust * windGustMultiplier],
"vector<u16>"
), // wind_gust
transactionBlock.pure(weatherData.clouds.all), // clouds
transactionBlock.pure(weatherData.dt), // dt
],
});
} else logger.warn(`No weather data for ${cities[c].asciiname} `);
}
return transactionBlock;
}
async function run() {
const cities: City[] = await getCities();
const weatherOracleDynamicFields: {
name: number;
objectId: string;
}[] = await getWeatherOracleDynamicFields(provider, weatherOracleId);
performUpdates(cities, weatherOracleDynamicFields);
}
run();
The code in index.ts does the following:
- Uses
dotenv
to load some environment variables from a.env
file, such asADMIN_PHRASE
,FULLNODE
,PACKAGE_ID
,ADMIN_CAP_ID
,WEATHER_ORACLE_ID
,APPID
, andMODULE_NAME
. These variables are used to configure some parameters for the code, such as the key pair, the provider, the signer, and the target package and module. - Defines some constants, such as
CHUNK_SIZE
,MS
,MINUTE
, andTEN_MINUTES
. These constants are used to control the frequency and size of the updates that the code performs. - Defines an async function called
performUpdates
, which takes two arguments:cities
andweatherOracleDynamicFields
. This function is the main logic of the code, and it does the following:- Filters the
cities
array based on theweatherOracleDynamicFields
array, which contains the names and object IDs of the weather oracle dynamic fields that the code needs to update. - Loops through the filtered cities in chunks of
CHUNK_SIZE
, and for each chunk, it calls another async function calledgetTransactionBlock
, which returns a transaction block that contains the Move calls to update the weather oracle dynamic fields with the latest weather data from the OpenWeatherMap API. - Tries to sign and execute the transaction block using the signer, and catches any errors that may occur.
- Calculates the time it took to perform the updates, and sets a timeout to call itself again after
TEN_MINUTES
minus the elapsed time.
- Filters the
- The code defines another async function called
getTransactionBlock
, which takes one argument:cities
. This function does the following:- Creates a new transaction block object.
- Loops through the
cities
array, and for each city, it calls another async function calledgetWeatherData
, which takes thelatitude
,longitude
, andappid
as arguments, and returns the weather data for the city from the OpenWeatherMap API. - Checks if the weather data is valid, and if so, adds a Move call to the transaction block, which calls the
update
function of the target package and module, and passes the admin cap, the weather oracle id, the geoname id, and the weather data as arguments. - Returns the transaction block object.
- An async
run
function is defined, which does the following:- Calls another async function called
getCities
, which returns an array of city objects that contain information such as name, geoname id, latitude, and longitude. - Calls another async function called
getWeatherOracleDynamicFields
, which takes the package id, the module name, and the signer as arguments, and returns an array of weather oracle dynamic field objects that contain information such as name and object id. - Calls the
performUpdates
function with the cities and weather oracle dynamic fields arrays as arguments.
- Calls another async function called
Congratulations, you completed the Sui Weather Oracle tutorial. You can carry the lessons learned here forward when building your next Sui project.