Skip to main content

Sui 天气 Oracle

本指南演示了如何在Move中编写一个模块(智能合约),在Devnet上部署它,并添加一个后端,每10分钟从OpenWeather API获取天气数据并更新每个城市的天气状况。本指南创建的dApp称为Sui天气Oracle,它为全球1000多个地点提供实时天气数据。

你可以访问并使用OpenWeather API的天气数据用于各种应用,例如随机性、投注、游戏、保险、旅行、教育或研究。你还可以基于城市的天气数据使用SUI天气Oracle智能合约的mint函数铸造天气NFT。

本指南假定你已经安装了Sui并了解Sui的基本知识。

Move智能合约

与所有Sui dApps一样,Move链上的一个Move包支持Sui天气Oracle的逻辑。以下说明将指导你创建和发布该模块。

天气Oracle模块

在开始之前,你必须初始化一个Move包。在要存储示例的目录中打开终端或控制台,并运行以下命令以创建一个名为 weather_oracle 的空包:

sui move new weather_oracle

现在,一切准备就绪,是时候开始编写一些代码了。在 sources 目录中创建一个名为 weather.move 的新文件,并将文件填充为以下代码:

weather.move
// 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};
}

在这段代码中有一些细节需要注意:

  1. 第四行将模块名称声明为 oracle 包中的 weather
  2. 七行以 use 关键字开头,该关键字使得该模块能够使用在其他模块中声明的类型和函数。

接下来,在这个模块中添加一些代码:

weather.move
/// 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.
}

到目前为止,你已经在模块中设置了数据结构。 现在,创建一个函数,该函数初始化一个 CityWeatherOracle 并将其添加为动态字段(dynamic fields)WeatherOracle 对象中:

weather.move
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
}
);
}

add_city 函数是一个公共函数,允许 Sui 天气 Oracle 智能合约的 AdminCap 持有者添加一个新的 CityWeatherOracle。该函数要求管理员提供一个能证明其有权限添加城市的能力对象。该函数还需要对 Oracle 对象的可变引用,该对象是在区块链上存储天气数据的主要对象。该函数接受描述城市的多个参数,例如 geoname ID、名称、国家、纬度、经度以及正纬度和经度。然后,该函数创建一个新的城市天气 Oracle 对象,这是一个存储和更新特定城市天气数据的子对象。该函数使用管理员提供的参数初始化城市天气 Oracle 对象,并将天气数据设置为零或无。然后,该函数使用 geoname ID 作为键,城市天气 Oracle 对象作为值,向 Oracle 对象添加一个新的动态对象字段。通过这种方式,函数向 Oracle 添加一个新城市,并使其准备好从后端服务接收和更新天气数据。

如果想要从 Sui 天气 Oracle 中删除一个城市,请调用智能合约的 remove_city 函数。remove_city 函数允许智能合约的管理员从 Oracle 中删除一个城市。该函数要求管理员提供一个能证明其有权限删除城市的能力对象。该函数还需要对 Oracle 对象的可变引用,该对象是在区块链上存储天气数据的主要对象。该函数以城市的 geoname ID 作为参数,并删除该城市的城市天气 Oracle 对象。该函数还从 Oracle 对象中删除城市的动态对象字段。通过这种方式,函数从 Oracle 中删除一个城市,并在区块链上释放一些存储空间。

weather.move
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);
}

现在你已经实现了 add_cityremove_city 函数,可以继续下一步,即了解如何更新每个城市的天气数据。后端服务每隔 10 分钟从 OpenWeather API 获取天气数据,然后将数据传递给 Sui 天气 Oracle 智能合约的 update 函数。update 函数以城市的 geoname ID 和城市的新天气数据作为参数,并使用新数据更新城市天气 Oracle 对象。通过这种方式,区块链上的天气数据始终保持最新和准确。

weather.move
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;
}

你已经定义了 Sui 天气 Oracle 智能合约的数据结构,但是你需要一些函数来访问和操作数据。现在,添加一些辅助函数,以读取并返回 WeatherOracle 对象的天气数据。这些函数允许你获取 Oracle 中特定城市的天气数据。这些函数还允许你以用户友好的方式格式化和显示天气数据。

weather.move
// --------------- 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
}

最后,添加一个额外的功能,允许任何人通过传递 geoname ID 铸造一个带有城市当前条件的 WeatherNFTmint 函数是一个公共函数,允许任何人根据城市的天气数据铸造一个天气 NFT。该函数以 WeatherOracle 共享对象和城市的 geoname ID 作为参数,并返回该城市的新 WeatherNFT 对象。WeatherNFT 对象是一个独特且不可替代的代币,代表了铸造时城市的天气情况。WeatherNFT 对象具有与 CityWeatherOracle 对象相同的数据,如 geonameIDnamecountrylatitudelongitudepositive latitudelongitudeweather IDtemperaturepressurehumidityvisibilitywind speedwind degreewind gustcloudstimestamp。该函数通过使用 geoname ID 作为键借用引用 CityWeatherOracle 对象,并为 WeatherNFT 对象分配唯一的 ID(UID)来创建 WeatherNFT 对象。然后,该函数将 WeatherNFT 对象的所有权转移给交易发起者。通过这种方式,该函数允许任何人铸造天气 NFT 并拥有城市天气的数字表示。你可以使用此功能创建自己的天气 NFT 收藏,或在需要可验证和不可变天气数据的其他应用中使用它们。

weather.move
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
}
}

至此,你的 weather.move 代码已经完成。

部署

info

想要了解更多关于发布包的信息,请参阅 Publish a Package 的详细指南。或者,您可以访问 Sui 客户端 CLI 了解 Sui CLI 中 client 命令的全部信息。

在发布代码前,请先初始化 Sui 客户端 CLI。在项目根目录的终端或控制台中,输入 sui client。若出现如下提示,请按照指示操作:

Config file ["<FILE-PATH>/.sui/sui_config/client.yaml"] doesn't exist, do you want to connect to a Sui Full node server [y/N]?

输入 y 来继续。接下来您会看到:

Sui Full node server URL (Defaults to Sui Devnet if not specified) :

此处可直接按 Enter 键跳过。然后会出现:

Select key scheme to generate keypair (0 for ed25519, 1 for secp256k1, 2: for secp256r1):

输入 0。此时,您应该已经成功创建了一个 Sui 地址。

在您把包发布到 Devnet 上之前,需要先获取一些 Devnet SUI 代币。您可以通过加入 Sui Discord 社区,完成验证后,在 #devnet-faucet 频道输入 !faucet <WALLET ADDRESS> 来获取代币。想了解更多获取 Devnet 账户中 SUI 的方法,请参阅 获取 SUI 代币

一旦您的账户中有了 Devnet SUI 代币,就可以开始部署合约了。要发布您的包,请在终端或控制台中输入以下命令:

sui client publish --gas-budget <GAS-BUDGET>

在设置 Gas 费用时,可以使用标准值,比如 20000000

后端

你已成功在区块链上部署了 Sui 天气 Oracle 智能合约。现在,是时候创建一个可以与其进行交互的 Express 后端了。Express 后端执行以下任务:

  • 使用智能合约的 add_city 函数,通过将每个城市的 geoname ID、名称、国家、纬度、经度以及正纬度和经度作为参数,对智能合约进行初始化,添加 1,000 个城市。
  • 每隔 10 分钟从 OpenWeather API 获取每个城市的天气数据,使用你从该网站获取的 API 密钥。后端解析 JSON 响应,并提取每个城市的天气数据,如天气 ID、温度、气压、湿度、能见度、风速、风向、阵风、云量和时间戳。
  • 使用智能合约的 update 函数,在区块链上更新每个城市的天气数据。后端将每个城市的 geoname ID 和新的天气数据作为参数传递给该函数。

Express 后端使用 Sui Typescript SDK,这是一个 TypeScript 库,使你能够与 Sui 区块链和智能合约进行交互。使用 Sui Typescript SDK,你可以连接到 Sui 网络,签名并提交交易,并查询智能合约的状态。你还将使用 OpenWeather API 每隔 10 分钟获取每个城市的天气数据并更新智能合约。此外,如果你希望探索智能合约的该功能,还可以铸造天气 NFT。

初始化项目

首先,初始化你的后端项目。要执行此操作,你需要按照以下步骤操作:

  • 创建一个名为 weather-oracle-backend 的新文件夹,并在终端中导航到该文件夹。
  • 运行 npm init -y 以使用默认值创建一个带有 package.json 文件的项目。
  • 运行 npm install express --save 以将 Express 安装为依赖项并将其保存到 package.json 文件中。
  • 运行 npm install @mysten/bcs @mysten/sui.js axios csv-parse csv-parser dotenv pino retry-axios --save 以安装其他依赖项并将其保存到 package.json 文件中。这些依赖项包括:
    • @mysten/bcs:用于区块链服务的库。
    • @mysten/sui.js:用于智能用户界面的库。
    • axios:用于进行 HTTP 请求的库。
    • csv-parse:用于解析 CSV 数据的库。
    • csv-parser:用于将 CSV 数据转换为 JSON 对象的库。
    • dotenv:从 .env 文件中加载环境变量的库。
    • pino:用于快速且低开销的日志记录的库。
    • retry-axios:用于重试失败的 axios 请求的库。
  • 创建一个名为 init.ts 的新文件
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();

init.ts 代码执行以下操作:

  • 从库中导入必要的模块和类,例如 ConnectionEd25519KeypairJsonRpcProviderRawSignerTransactionBlock
  • dotenv 模块中导入以从 .env 文件加载环境变量。
  • 从本地文件导入一些自定义模块和函数,例如 Cityget1000GeonameidsgetCitiesgetWeatherOracleDynamicFieldslogger
  • 从存储在 ADMIN_PHRASE 环境变量中的短语派生密钥对。
  • 创建一个连接到由 FULLNODE 环境变量指定的全节点的提供程序对象。
  • 创建一个使用密钥对和提供程序来签署和执行区块链上交易的签名者对象。
  • 读取其他一些环境变量,例如 PACKAGE_IDADMIN_CAP_IDWEATHER_ORACLE_IDMODULE_NAME,用于标识天气 Oracle 合约及其方法。
  • 定义一个常量 NUMBER_OF_CITIES,表示每批次要添加到天气 Oracle 中的城市数量。
  • 定义一个异步函数 addCityWeather,执行以下操作:
    • getCities 函数获取城市数组。
    • get1000Geonameids 函数获取 1,000 个地理名称 ID 的数组。
    • getWeatherOracleDynamicFields 函数获取包含天气 Oracle 中现有城市的地理名称 ID 的数组。
    • 初始化计数器和事务块对象。
    • 循环遍历城市数组,检查城市的地理名称 ID 是否不在天气 Oracle 动态字段数组中且在 1,000 个地理名称 ID 数组中。
    • 如果满足条件,则向事务块添加一个 moveCall,调用天气 Oracle 合约的 add_city 方法,提供城市的信息,如 geonameidasciinamecountrylatitudelongitude
    • 增加计数器,并检查是否达到 NUMBER_OF_CITIES。如果是,则使用事务块调用另一个异步函数 signAndExecuteTransactionBlock,将事务块作为参数,该函数会在区块链上签署和执行事务块,并记录结果。然后代码重置计数器和事务块。
    • 循环结束后,再次使用剩余事务块调用 signAndExecuteTransactionBlock 函数。

现在你已初始化了 WeatherOracle 共享对象。下一步是学习如何每隔 10 分钟使用来自 OpenWeatherMap API 的最新天气数据更新它们。

index.ts
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 as ADMIN_PHRASE, FULLNODE, PACKAGE_ID, ADMIN_CAP_ID, WEATHER_ORACLE_ID, APPID, and MODULE_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, and TEN_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 and weatherOracleDynamicFields. This function is the main logic of the code, and it does the following:
    • Filters the cities array based on the weatherOracleDynamicFields 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 called getTransactionBlock, 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.
  • 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 called getWeatherData, which takes the latitude, longitude, and appid 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.

Congratulations, you completed the Sui Weather Oracle tutorial. You can carry the lessons learned here forward when building your next Sui project.