본문으로 건너뛰기

Sui Weather Oracle

이 가이드는 Move로 module(smart contract)을 작성하고, 이를 Devnet에 배포하고, 10분마다 OpenWeather API에서 날씨 데이터를 가져와 각 도시의 기상 상태를 업데이트하는 backend를 추가하는 방법을 보여 준다.

이 가이드에서 만드는 앱의 이름은 Sui Weather Oracle이며 전 세계 1,000개가 넘는 위치에 대한 실시간 날씨 데이터를 제공한다.

OpenWeather API의 날씨 데이터는 randomness, betting, gaming, insurance, travel, education, research 등 다양한 애플리케이션에서 접근하여 사용할 수 있다.

또한 Sui Weather Oracle smart contract의 mint 함수를 사용해 도시의 날씨 데이터를 기반으로 날씨 NFT를 mint할 수도 있다.

Click to open

Create Sui account and setup CLI environment

$ sui client

If this is the first time running the sui client CLI tool, it asks you to provide a Sui full node server URL and a meaningful environment alias. It also generates an address with a random key pair in sui.keystore and a config client.yaml.

By default, the client.yaml and sui.keystore files are located in ~/.sui/sui_config. For more information, refer to the Sui client CLI tutorial.

If this is not your first time running sui client, then you already have a client.yaml file in your local environment. If you'd like to create a new address for this tutorial, use the command:

$ sui client new-address ed25519
  • Obtain test tokens.
Click to open

How to obtain tokens

If you are connected to Devnet or Testnet networks, use the Faucet UI to request tokens.

If you are connected to a local full node, learn how to get local network tokens.

Move smart contract

모든 Sui 앱과 마찬가지로 온체인 Move package가 Sui Weather Oracle의 로직을 구동한다.

다음 지침은 module을 생성하고 게시하는 과정을 안내한다.

weather_oracle module

시작하기 전에 Move package를 초기화해야 한다.

예제를 저장할 디렉터리에서 터미널 또는 콘솔을 열고 다음 명령을 실행해 weather_oracle이라는 이름의 빈 package를 생성한다:

$ sui move new weather_oracle

이제 코드로 들어갈 차례이다.

sources 디렉터리에 weather.move라는 이름의 새 파일을 만들고 다음 코드를 채운다:

// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

module oracle::weather {
use std::string::String;
use sui::dynamic_object_field as dof;
use sui::package;
}

이 코드에서 주목할 세부 사항은 다음과 같다:

  1. 네 번째 줄은 package oracle 안에서 module 이름을 weather로 선언한다.
  2. 일곱 줄은 use 키워드로 시작하며, 이 module이 다른 module에 선언된 type과 함수를 사용할 수 있게 한다.

다음으로 이 module에 코드를 조금 더 추가한다:

/// oracle의 admin을 위한 capability를 정의한다.
public struct AdminCap has key, store { id: UID }

/// oracle의 `Publisher`를 생성하기 위한 one-time witness를 정의한다.
public struct WEATHER has drop {}

// weather oracle을 위한 struct를 정의한다.
public struct WeatherOracle has key {
id: UID,
/// oracle의 address.
address: address,
/// oracle의 이름.
name: String,
/// oracle의 설명.
description: String,
}

public struct CityWeatherOracle 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, // hPa 단위 대기압
humidity: u8, // 습도 백분율
visibility: u16, // 미터 단위 가시거리
wind_speed: u16, // 초당 미터 단위 풍속
wind_deg: u16, // 도 단위 풍향
wind_gust: Option<u16>, // 초당 미터 단위 돌풍 속도(선택 사항)
clouds: u8, // 운량 비율
dt: u32 // epoch 이후 초 단위 날씨 업데이트 timestamp
}

fun init(otw: WEATHER, ctx: &mut TxContext) {
package::claim_and_keep(otw, ctx); // one-time witness의 소유권을 획득하고 유지한다.

let cap = AdminCap { id: object::new(ctx) }; // 새로운 admin capability object를 생성한다.
transfer::share_object(WeatherOracle {
id: object::new(ctx),
address: ctx.sender(),
name: b"SuiMeteo".to_string(),
description: b"A weather oracle.".to_string(),
});
transfer::public_transfer(cap, ctx.sender()); // admin capability를 sender에게 전송한다.
}
  • 첫 번째 struct인 AdminCap은 capability이다.
  • 두 번째 struct인 WEATHER는 이 Weather의 단일 인스턴스만 존재하도록 보장하는 one-time witness이다.
  • 자세한 내용은 The Move Book의 One Time Witness를 본다.
  • WeatherOracle struct는 레지스트리로 동작하며 CityWeatherOraclegeoname_id를 dynamic field로 저장한다.
  • dynamic field에 대해 더 알아보려면 The Move Book을 본다.
  • init 함수는 PublisherAdminCap object를 생성해 sender에게 보낸다.
  • 또한 모든 CityWeatherOracle을 위한 shared object를 생성한다.
  • 자세한 내용은 The Move Book의 Module Initializer를 본다.

지금까지는 module 안의 데이터 구조를 설정했다.

이제 CityWeatherOracle을 초기화하고 이를 WeatherOracle object의 dynamic field로 추가하는 함수를 만든다:

public fun add_city(
_: &AdminCap, // admin capability
oracle: &mut WeatherOracle, // oracle object에 대한 mutable reference
geoname_id: u32, // 도시의 고유 식별자
name: String, // 도시의 이름
country: String, // 도시의 국가
latitude: u32, // 도의 단위 도시 위도
positive_latitude: bool, // 위도가 양수(북반구)인지 음수(남반구)인지 여부
longitude: u32, // 도의 단위 도시 경도
positive_longitude: bool, // 경도가 양수(동경)인지 음수(서경)인지 여부
ctx: &mut TxContext // transaction context에 대한 mutable reference
) {
dof::add(&mut oracle.id, geoname_id, // geoname ID를 키로 하고 새 도시 weather oracle object를 값으로 하는 새로운 dynamic object field를 oracle object에 추가한다.
CityWeatherOracle {
id: object::new(ctx), // 도시 weather oracle object에 고유 ID를 할당한다.
geoname_id, // 도시 weather oracle object의 geoname ID를 설정한다.
name, // 도시 weather oracle object의 이름을 설정한다.
country, // 도시 weather oracle object의 국가를 설정한다.
latitude, // 도시 weather oracle object의 위도를 설정한다.
positive_latitude, // 위도가 양수(북반구)인지 음수(남반구)인지 설정한다.
longitude, // 도시 weather oracle object의 경도를 설정한다.
positive_longitude, // 경도가 양수(동경)인지 음수(서경)인지 설정한다.
weather_id: 0, // 기상 상태 코드를 0으로 초기화한다.
temp: 0, // 온도를 0으로 초기화한다.
pressure: 0, // 기압을 0으로 초기화한다.
humidity: 0, // 습도를 0으로 초기화한다.
visibility: 0, // 가시거리를 0으로 초기화한다.
wind_speed: 0, // 풍속을 0으로 초기화한다.
wind_deg: 0, // 풍향을 0으로 초기화한다.
wind_gust: option::none(), // 돌풍 속도를 none으로 초기화한다.
clouds: 0, // 운량을 0으로 초기화한다.
dt: 0 // timestamp를 0으로 초기화한다.
}
);
}

add_city 함수는 AdminCap 보유자가 Sui Weather Oracle에 새 도시를 추가할 수 있게 한다.

호출자는 자신의 AdminCap을 제시하고 메인 Oracle object에 대한 mutable reference를 전달해야 한다.

이 함수는 geoname ID, 이름, 국가, 좌표(위도, 경도, 그리고 각각의 positive flag) 같은 도시 메타데이터를 받는다.

그런 다음 CityWeatherOracle 하위 object를 생성하고 weather field를 empty/zero로 초기화한 뒤 geoname ID를 키로 하여 oracle의 dynamic field에 삽입한다.

이후에는 도시가 등록되고 backend에서 update를 받을 준비가 된다.

도시를 삭제하려면 remove_city를 호출한다.

Admin은 다시 AdminCap, mutable oracle reference, 그리고 도시의 geoname ID를 제공한다.

이 함수는 해당 CityWeatherOracle 하위 object를 제거하고 oracle에서 그 dynamic field 항목을 삭제한다.

이렇게 하면 도시가 깔끔하게 등록 해제되고 관련 온체인 스토리지가 해제된다.

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분마다 backend 서비스가 OpenWeather API에서 상태를 가져오고 결과를 사용해 weather_oracle module의 update 함수를 호출한다.

update 함수는 도시의 geoname ID와 새 weather field를 받아 해당 CityWeatherOracle object에 이를 기록한다.

이렇게 하면 온체인 날씨 데이터가 최신이고 정확한 상태로 유지된다.

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); // geoname ID를 키로 하는 도시 weather oracle object에 대한 mutable reference를 빌린다.
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 Weather Oracle smart contract의 데이터 구조는 정의했지만, 데이터를 접근하고 조작하기 위한 함수도 필요하다.

이제 WeatherOracle object의 날씨 데이터를 읽고 반환하는 helper 함수를 추가한다.

이 함수들은 oracle의 특정 도시에 대한 날씨 데이터를 가져올 수 있게 하며 사용자 친화적인 방식으로 날씨 데이터를 형식화하고 표시할 수 있게 한다.

// --------------- 읽기 전용 참조 ---------------

/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `name`을 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `country`를 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `latitude`를 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `positive_latitude`를 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `longitude`를 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `positive_longitude`를 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `weather_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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `temp`를 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `pressure`를 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `humidity`를 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `visibility`를 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `wind_speed`를 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `wind_deg`를 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `wind_gust`를 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `clouds`를 반환한다.
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
}
/// 주어진 `geoname_id`를 가진 `CityWeatherOracle`의 `dt`를 반환한다.
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
}

mint 함수는 geoname ID와 shared WeatherOracle을 전달하여 누구나 도시용 WeatherNFT를 생성할 수 있게 한다.

이 함수는 해당 ID의 CityWeatherOracle을 조회하고 그 값으로부터 새 NFT를 빌드하고 새 UID를 할당한 뒤 NFT를 transaction sender에게 전송한다.

AdminCap은 필요하지 않다.

WeatherNFT는 mint 시점의 도시 상태를 포착하며 CityWeatherOraclegeonameID, name, country, latitude, longitude, positive-latitude/longitude flag, weather_id, temperature, pressure, humidity, visibility, wind_speed, wind_degree, wind_gust, clouds, timestamp field를 복사한다.

Collector는 이러한 NFT를 과거 날씨 snapshot으로 보유할 수 있고, 앱은 검증 가능하고 immutable한 날씨 데이터가 필요한 곳이라면 어디서나 이를 사용할 수 있다.

public 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 코드는 완성되었다.

Deployment

정보

See "Hello, World!" 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, if you haven't already. To do so, in a terminal or console at the root directory of the project enter sui client. If you receive the following response, complete the remaining instructions:

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]?

Enter y to proceed. You receive the following response:

Sui full node server URL (Defaults to Sui Testnet if not specified) :

Leave this blank (press Enter). You receive the following response:

Select key scheme to generate key pair (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 Testnet, you need Testnet SUI tokens. To get some, visit the online faucet at https://faucet.sui.io/. For other ways to get SUI in your Testnet account, see Get SUI Tokens.

Now that you have an account with some Testnet 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

이제 Sui Weather Oracle smart contract를 블록체인에 성공적으로 배포했다.

이제 이를 상호작용할 수 있는 Express backend를 만들 차례이며, Express backend는 다음 작업을 수행한다:

  • Smart contract의 add_city 함수를 사용해 1,000개 도시로 smart contract를 초기화한다.
  • Backend는 geoname ID, 이름, 국가, 위도, 경도, 그리고 위도와 경도의 positive 여부를 함수 파라미터로 전달한다.
  • Website에서 얻은 API 키를 사용해 OpenWeather API에서 10분마다 각 도시의 날씨 데이터를 가져온다.
  • Backend는 JSON 응답을 파싱하고 weather ID, 온도, 기압, 습도, 가시거리, 풍속, 풍향, 돌풍, 구름, timestamp 같은 각 도시의 날씨 데이터를 추출한다.
  • Smart contract의 update 함수를 사용해 블록체인에 각 도시의 날씨 데이터를 업데이트한다.
  • Backend는 geoname ID와 각 도시의 새로운 날씨 데이터를 함수 파라미터로 전달한다.

Express backend는 Sui 블록체인과 smart contract를 상호작용할 수 있게 해 주는 TypeScript 라이브러리인 Sui Typescript SDK를 사용한다.

Sui Typescript SDK를 사용하면 Sui 네트워크에 연결하고, transaction에 서명하고 제출하고, smart contract의 상태를 조회할 수 있다.

또한 OpenWeather API를 사용해 각 도시의 날씨 데이터를 가져오고 10분마다 smart contract를 업데이트한다.

추가로 smart contract의 그 기능을 살펴보고 싶다면 날씨 NFT를 mint할 수도 있다.

Initialize the project

먼저 backend project를 초기화한다.

이를 위해 다음 단계를 따른다:

  • weather-oracle-backend라는 새 폴더를 만들고 터미널에서 그곳으로 이동한다.
  • 기본값으로 package.json 파일을 생성하려면 npm init -y를 실행한다.
  • Express를 dependency로 설치하고 package.json에 저장하려면 npm install express --save를 실행한다.
  • 다른 dependency를 설치하고 package.json에 저장하려면 npm install @mysten/bcs @mysten/sui axios csv-parse csv-parser dotenv pino retry-axios --save를 실행한다.
    • @mysten/bcs: 블록체인 서비스를 위한 라이브러리이다.
    • @mysten/sui: smart user interface를 위한 라이브러리이다.
    • axios: HTTP request를 보내는 라이브러리이다.
    • csv-parse: CSV 데이터를 파싱하는 라이브러리이다.
    • csv-parser: CSV 데이터를 JSON object로 변환하는 라이브러리이다.
    • dotenv: .env 파일에서 environment variable을 불러오는 라이브러리이다.
    • pino: 빠르고 오버헤드가 낮은 logging 라이브러리이다.
    • retry-axios: 실패한 axios request를 재시도하는 라이브러리이다.
  • init.ts라는 새 파일을 만든다.
import { SuiGrpcClient } from '@mysten/sui/grpc';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { Transaction } from '@mysten/sui/transactions';
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 client = new SuiGrpcClient({
baseUrl: fullnode,
network: 'mainnet',
});

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(client, weatherOracleId);
const geonames = weatherOracleDynamicFields.map(function (obj) {
return obj.name;
});

let counter = 0;
let transaction = new Transaction();
for (let c in cities) {
if (
!geonames.includes(cities[c].geonameid) &&
thousandGeoNameIds.includes(cities[c].geonameid)
) {
transaction.moveCall({
target: `${packageId}::${moduleName}::add_city`,
arguments: [
transaction.object(adminCap), // adminCap
transaction.object(weatherOracleId), // WeatherOracle
transaction.pure(cities[c].geonameid), // geoname_id
transaction.pure(cities[c].asciiname), // asciiname
transaction.pure(cities[c].countryCode), // country
transaction.pure(cities[c].latitude * latitudeMultiplier), // latitude
transaction.pure(cities[c].latitude > 0), // positive_latitude
transaction.pure(cities[c].longitude * longitudeMultiplier), // longitude
transaction.pure(cities[c].longitude > 0), // positive_longitude
],
});

counter++;
if (counter === NUMBER_OF_CITIES) {
await signAndExecuteTransaction(transaction);
counter = 0;
transaction = new Transaction();
}
}
}
await signAndExecuteTransaction(transaction);
}

async function signAndExecuteTransaction(transaction: Transaction) {
transaction.setGasBudget(5000000000);
await client
.signAndExecuteTransaction({
signer: keypair,
transaction,
})
.then(function (res) {
logger.info(res);
});
}

addCityWeather();

init.ts 코드는 다음을 수행한다:

  • Ed25519Keypair, SuiClient, Transaction 같은 필요한 module과 class를 라이브러리에서 import한다.
  • .env 파일에서 environment variable을 불러오기 위해 dotenv module을 import한다.
  • City, get1000Geonameids, getCities, getWeatherOracleDynamicFields, logger 같은 일부 custom module과 함수를 로컬 파일에서 import한다.
  • ADMIN_PHRASE environment variable에 저장된 phrase로부터 key pair를 도출한다.
  • FULLNODE environment variable로 지정한 Full node에 연결되는 sui client object를 생성한다.
  • 날씨 oracle contract와 그 method를 식별하는 데 사용되는 PACKAGE_ID, ADMIN_CAP_ID, WEATHER_ORACLE_ID, MODULE_NAME 같은 다른 environment variable도 읽는다.
  • 각 batch에서 weather oracle에 추가할 도시 수인 NUMBER_OF_CITIES 상수를 정의한다.
  • 다음을 수행하는 addCityWeather라는 async 함수를 정의한다:
    • getCities 함수에서 도시 배열을 가져온다.
    • get1000Geonameids 함수에서 1,000개 geonameid 배열을 가져온다.
    • 날씨 oracle에 이미 존재하는 도시의 geonameid를 포함하는 getWeatherOracleDynamicFields 함수에서 weather oracle dynamic field 배열을 가져온다.
    • counter와 transaction object를 초기화한다.
    • 도시 배열을 순회하면서 도시의 geonameid가 weather oracle dynamic field 배열에 없고 1,000개 geonameid 배열에는 있는지 확인한다.
    • 조건이 만족되면 geonameid, asciiname, country, latitude, longitude 같은 도시 정보를 사용해 weather oracle contract의 add_city method를 호출하는 moveCall 명령을 transaction에 추가한다.
    • counter를 증가시키고 NUMBER_OF_CITIES에 도달했는지 확인한다.
    • 도달했다면 transaction을 인자로 받아 블록체인에서 서명하고 실행한 뒤 결과를 기록하는 또 다른 async 함수 signAndExecuteTransaction을 호출한다.
    • 그런 다음 counter와 transaction을 재설정한다.
    • 루프가 끝난 뒤 남은 transaction으로 signAndExecuteTransaction 함수를 다시 호출한다.

이제 WeatherOracle shared object를 초기화했다.

다음 단계는 OpenWeatherMap API의 최신 날씨 데이터로 이를 10분마다 업데이트하는 방법을 배우는 것이다.

import { SuiGrpcClient } from '@mysten/sui/grpc';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { Transaction } from '@mysten/sui/transactions';
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 client = new SuiGrpcClient({
baseUrl: fullnode,
network: 'mainnet',
});

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 transaction = await getTransaction(chunk);
try {
await client.signAndExecuteTransaction({
signer: keypair,
transaction,
});
} catch (e) {
logger.error(e);
}
}

let endTime = new Date().getTime();
setTimeout(
performUpdates,
TEN_MINUTES - (endTime - startTime),
cities,
weatherOracleDynamicFields,
);
}

async function getTransaction(cities: City[]) {
let transaction = new Transaction();

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) {
transaction.moveCall({
target: `${packageId}::${moduleName}::update`,
arguments: [
transaction.object(adminCap), // AdminCap
transaction.object(weatherOracleId), // WeatherOracle
transaction.pure(cities[c].geonameid), // geoname_id
transaction.pure(weatherData.weather[0].id), // weather_id
transaction.pure(weatherData.main.temp * tempMultiplier), // temp
transaction.pure(weatherData.main.pressure), // pressure
transaction.pure(weatherData.main.humidity), // humidity
transaction.pure(weatherData.visibility), // visibility
transaction.pure(weatherData.wind.speed * windSpeedMultiplier), // wind_speed
transaction.pure(weatherData.wind.deg), // wind_deg
transaction.pure(
weatherData.wind.gust === undefined ? [] : [weatherData.wind.gust * windGustMultiplier],
'vector<u16>',
), // wind_gust
transaction.pure(weatherData.clouds.all), // clouds
transaction.pure(weatherData.dt), // dt
],
});
} else logger.warn(`No weather data for ${cities[c].asciiname} `);
}
return transaction;
}

async function run() {
const cities: City[] = await getCities();
const weatherOracleDynamicFields: {
name: number;
objectId: string;
}[] = await getWeatherOracleDynamicFields(client, weatherOracleId);
performUpdates(cities, weatherOracleDynamicFields);
}

run();

index.ts의 코드는 다음을 수행한다:

  • ADMIN_PHRASE, FULLNODE, PACKAGE_ID, ADMIN_CAP_ID, WEATHER_ORACLE_ID, APPID, MODULE_NAME 같은 일부 environment variable을 .env 파일에서 불러오기 위해 dotenv를 사용한다.
  • 이 variable은 key pair, client, target package와 module 같은 코드의 일부 파라미터를 구성하는 데 사용된다.
  • CHUNK_SIZE, MS, MINUTE, TEN_MINUTES 같은 상수를 정의한다.
  • 이 상수는 코드가 수행하는 update의 빈도와 크기를 제어하는 데 사용된다.
  • citiesweatherOracleDynamicFields라는 두 인자를 받는 performUpdates라는 async 함수를 정의한다.
  • 이 함수는 코드의 메인 로직이며 다음을 수행한다:
    • update가 필요한 weather oracle dynamic field의 이름과 object ID를 담고 있는 weatherOracleDynamicFields 배열을 기준으로 cities 배열을 필터링한다.
    • 필터링된 도시를 CHUNK_SIZE 크기의 청크로 순회하고, 각 청크마다 최신 날씨 데이터를 OpenWeatherMap API에서 가져와 weather oracle dynamic field를 업데이트하는 Move call을 담은 transaction을 반환하는 또 다른 async 함수 getTransaction을 호출한다.
    • client와 key pair를 사용해 transaction에 서명하고 실행하려 시도하고, 발생할 수 있는 오류를 잡는다.
    • update 수행에 걸린 시간을 계산하고 경과 시간을 뺀 TEN_MINUTES 후에 자신을 다시 호출하도록 timeout을 설정한다.
  • cities라는 하나의 인자를 받는 getTransaction이라는 또 다른 async 함수를 정의한다.
  • 이 함수는 다음을 수행한다:
    • transaction object를 생성한다.
    • cities 배열을 순회하면서 각 도시에 대해 latitude, longitude, appid를 인자로 받아 OpenWeatherMap API에서 해당 도시의 날씨 데이터를 반환하는 또 다른 async 함수 getWeatherData를 호출한다.
    • 날씨 데이터가 유효한지 확인하고, 유효하면 target package와 module의 update 함수를 호출하고 admin cap, weather oracle id, geoname id, 날씨 데이터를 인자로 전달하는 Move call 명령을 transaction에 추가한다.
    • transaction object를 반환한다.
  • 다음을 수행하는 async run 함수를 정의한다:
    • 이름, geoname id, 위도, 경도 같은 정보를 담은 도시 object 배열을 반환하는 또 다른 async 함수 getCities를 호출한다.
    • package id, module name, client를 인자로 받아 이름과 object id 같은 정보를 담은 weather oracle dynamic field object 배열을 반환하는 또 다른 async 함수 getWeatherOracleDynamicFields를 호출한다.
    • 도시 배열과 weather oracle dynamic field 배열을 인자로 사용해 performUpdates 함수를 호출한다.

축하한다. 이제 Sui Weather Oracle 튜토리얼을 완료했다.

여기서 배운 내용을 다음 Sui project를 빌드할 때 이어서 활용할 수 있다.