見出し画像

Oracle External Pluginを活用して「US Market Trading Experience」を作成してみよう!

この記事では、新しいオラクルプラグインを活用して、米国市場の取引時間内でのみ取引可能なNFTコレクションを作成します。

はじめに

External Plugin

「External Plugin」は、その動作が外部のソースによって制御されるプラグインです。コアプログラムはこれらのプラグイン用のアダプターを提供しますが、開発者はこのアダプターを外部のデータソースに向けることで動作を決定します。

各「External Adapter」は「Lifecycle Events」に「Lifecycle checks」を割り当てる能力があり、実行される「Lifecycle Events」の動作に影響を与えます。つまり、create、transfer、update、burnなどの「Lifecycle Events」に以下のチェックを割り当てることができます。

  • Listen: 「Lifecycle Events」が発生したときにプラグインに警告する「web3」ウェブフック。これはデータの追跡やアクションの実行に特に役立ちます。

  • Reject: プラグインが「Lifecycle Events」を拒否できます。

  • Approve: プラグインが「Lifecycle Events」を承認できます。

「External Plugin」についてさらに詳しく知りたい場合は、こちらをお読みください。

Oracle Plugin

「Oracle Plugin」は「External Plugin」の機能を活用して、外部の権限者がCore資産の外部にあるオンチェーンデータアカウントにアクセスすることで更新できるデータを保存します。これにより、アセットが権限者によって設定された「Lifecycle Events」を動的に拒否することができます。外部オラクルアカウントはいつでも更新可能で「Lifecycle Events」の承認動作を変更できるため、柔軟で動的な体験を提供します。

「Oracle Plugin」についてさらに詳しく知りたい場合は、こちらをお読みください。

始める前に:アイデアの背後にあるプロトコルの理解

米国市場の取引時間内でのみ取引可能なNFTコレクションを作成するには、時刻に基づいてオンチェーンデータを更新する信頼性の高い方法が必要です。プロトコル設計は以下のようになります。

プログラム概要

このプログラムには、2つの主要な指示(1つはオラクルの作成用、もう1つはその値の更新用)と、実装を簡単にするための2つのヘルパー関数があります。

主要な指示

  • オラクル初期化の指示:この指示は、オラクルアカウントを作成します。これにより、このタイムゲート機能をコレクションに使用したいユーザーは、NFTオラクルプラグインをこのオンチェーンアカウントアドレスにリダイレクトできます。

  • オラクルクランクの指示:この指示は、常に正しい最新のデータを持つようにオラクルの状態データを更新します。

ヘルパー関数

  • isUsMarketOpen:米国市場が開いているかどうかをチェックします。

  • isWithin15mOfMarketOpenOrClose:現在の時刻が市場の開閉時間の15分以内かどうかをチェックします。

注意:「crank_oracle_instruction」は、正確なデータでプロトコルを更新することを保証し、最新の情報を維持する人々にインセンティブを提供します。これについては次のセクションで詳しく説明します。

インセンティブメカニズム

このオラクルを信頼のソースとして使用する各コレクションは、オラクルが常に最新の状態であることを確実にするために、独自のクランクを実行する必要があります。しかし、耐障害性を高めるために、プロトコル開発者は複数の人々がプロトコルをクランクするためのインセンティブを作ることを検討すべきです。これにより、社内のクランクがデータの更新に失敗した場合でも、オラクルデータの正確性を保つ安全網が確保されます。

現在のプログラム設計では、オラクルを維持するクランカーに0.001 SOLの報酬を与えています。この金額は管理可能であり、同時にクランカーがオラクルの状態アカウントを最新に保つための十分なインセンティブとなります。

注意:これらのインセンティブは、市場の開閉時間の最初の15分間にクランクが実行された場合にのみ支払われ、スマートコントラクト内のボールトから資金が提供されます。ボールトは、オラクルボールトアドレスにSOLを送ることで補充する必要があります。

プログラムの構築:実践的なコーディング

プロトコルの背後にあるロジックが明確になったので、コードに深く入り込んで、すべてをまとめていきましょう!

Anchorの概要

このガイドでは、Anchorフレームワークを使用しますが、ネイティブプログラムを使用して実装することもできます。Anchorフレームワークについて詳しくはこちらをご覧ください。

簡単にするため、通常の分離方式ではなく、ヘルパー、状態、アカウント、指示のすべてをlib.rsに記述するモノファイルアプローチを採用します。

注意:Solana Playground(Solanaプログラムを構築・デプロイするためのオンラインツール)でこの例を確認し、実際に試すことができます。

ヘルパーと定数

いくつかの入力を繰り返し宣言する代わりに、指示や関数で簡単に参照できる定数を作成するのは良いアイデアです。

このオラクルプロトコルで使用される定数は以下の通りです。

// Constants
const SECONDS_IN_AN_HOUR: i64 = 3600;
const SECONDS_IN_A_MINUTE: i64 = 60;
const SECONDS_IN_A_DAY: i64 = 86400;

const MARKET_OPEN_TIME: i64 = 14 * SECONDS_IN_AN_HOUR + 30 * SECONDS_IN_A_MINUTE; // 14:30 UTC == 9:30 EST
const MARKET_CLOSE_TIME: i64 = 21 * SECONDS_IN_AN_HOUR; // 21:00 UTC == 16:00 EST
const MARKET_OPEN_CLOSE_MARGIN: i64 = 15 * SECONDS_IN_A_MINUTE; // 15 minutes in seconds
const REWARD_IN_LAMPORTS: u64 = 10000000; // 0.001 SOL

スマートコントラクトのロジックをチェックするヘルパーを作成するのは理にかなっています。例えば、米国市場が開いているかどうか、また開閉時間の15分以内かどうかをチェックするなどです。

is_us_market_open helper:

fn is_us_market_open(unix_timestamp: i64) -> bool {
    let seconds_since_midnight = unix_timestamp % SECONDS_IN_A_DAY;
    let weekday = (unix_timestamp / SECONDS_IN_A_DAY + 4) % 7;

    // Check if it's a weekday (Monday = 0, ..., Friday = 4)
    if weekday >= 5 {
        return false;
    }

    // Check if current time is within market hours
    seconds_since_midnight >= MARKET_OPEN_TIME && seconds_since_midnight < MARKET_CLOSE_TIME
}

このヘルパーは、与えられたUnixタイムスタンプに基づいて米国市場が開いているかどうかをチェックします。真夜中からの経過秒数と曜日を計算し、現在の時刻が平日で、かつ市場時間内であれば、trueを返します。

注意:これは単なる例であり、特別な場合は考慮されません。

is_within_15_minutes_of_market_open_or_close helper:

fn is_within_15_minutes_of_market_open_or_close(unix_timestamp: i64) -> bool {
    let seconds_since_midnight = unix_timestamp % SECONDS_IN_A_DAY;

    // Check if current time is within 15 minutes after market open or within 15 minutes after market close
    (seconds_since_midnight >= MARKET_OPEN_TIME && seconds_since_midnight < MARKET_OPEN_TIME + MARKET_OPEN_CLOSE_MARGIN) ||
    (seconds_since_midnight >= MARKET_CLOSE_TIME && seconds_since_midnight < MARKET_CLOSE_TIME + MARKET_OPEN_CLOSE_MARGIN)
}

このヘルパーは、現在の時刻が市場の開閉時間の15分以内かどうかをチェックします。真夜中からの経過秒数を計算し、それを市場の開閉時間と比較し、15分のマージンを追加します。

State

Solanaでは、チェーン上にデータを保存するために、デシリアライズされた後にこのデータを表す構造体を作成する必要があります。

以下が、オラクルアカウントに使用する構造体です。

#[account]
pub struct Oracle {
    pub validation: OracleValidation,
    pub bump: u8,
    pub vault_bump: u8,
}

impl Space for Oracle {
    const INIT_SPACE: usize = 8 + 5 + 1;
}

この構造体を作成する際のいくつかの選択について説明しましょう。

  • 初期化後は誰でも操作できるパーミッションレスになるため、adminフィールドはありません。

  • validationフィールドを最初に配置することで、NFTで検索するオフセットを設定する際に、discriminatorサイズ(8バイト)だけを使用する自然な方法を活用できます。これにより、Oracle Pluginの設定でカスタムオフセットを使用する必要がなくなります。

  • Oracle PDAとOracle Vault PDAの両方のbumpを保存することで、指示にこれらのアカウントを含めるたびにbumpを導出する必要がなくなります。これはSolana開発の標準的な方法で、Compute Usageの節約に役立ちます。詳細はこちらをご覧ください。

スペース計算に関しては、Anchor用のSpace実装を直接使用し「INIT_SPACE」という定数を作成して、PDAの作成時とレント免除のための十分なSOLを保存する際に参照します。唯一の特殊な点は、mpl-coreのOracleValidation構造体のサイズを5バイトにする必要があることです。それ以外のスペース計算は標準的です。スペース計算について詳しくはこちらをご覧ください。

アカウント

Anchorにおけるアカウントは、Solanaプログラムへの入力からデシリアライズできる検証済みアカウントの構造です。

私たちのプログラムでは、両方の指示に使用されるアカウント構造は非常に似ています。ただし、一方ではオラクルアカウントを初期化し、もう一方では単にそれを参照します。

「CreateOracle」アカウントを見てみましょう。

#[derive(Accounts)]
pub struct CreateOracle<'info> {
    pub signer: Signer<'info>,
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        init,
        payer = payer,
        space = Oracle::INIT_SPACE,
        seeds = [b"oracle"],
        bump
    )]
    pub oracle: Account<'info, Oracle>,
    #[account(
        seeds = [b"reward_vault", oracle.key().as_ref()],
        bump,
    )]
    pub reward_vault: SystemAccount<'info>,
    pub system_program: Program<'info, System>,
}

この構造体は、この指示の署名者と支払者の2つの別々のアカウントを提示しています。これは、ほとんどの指示で標準的なものですが、ここでは厳密には必要ありません。PDAがトランザクションに署名する場合でも、手数料を支払うアカウントがあることを保証します。両方ともトランザクションの署名者である必要があります。

その他の詳細

  • オラクルアカウントは初期化され、シードとして [b"oracle"] を持ちます。これにより、複数のオラクルアカウントを作成する可能性がなくなります。割り当てられるスペースは「INIT_SPACE」定数で定義されています。

  • 「reward_vault」アカウントは、次の指示で使用するためにbumpを保存するためにこの指示に含まれています。

  • Systemプログラムは、initマクロがシステムプログラムの「create_account」指示を使用するため、Solana上で新しいアカウントを作成するために必要です。

次に「CrankOracle」アカウントを見てみましょう。

#[derive(Accounts)]
pub struct CrankOracle<'info> {
    pub signer: Signer<'info>,
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        mut,
        seeds = [b"oracle"],
        bump = oracle.bump,
    )]
    pub oracle: Account<'info, Oracle>,
    #[account(
        mut, 
        seeds = [b"reward_vault", oracle.key().as_ref()],
        bump = oracle.vault_bump,
    )]
    pub reward_vault: SystemAccount<'info>,
    pub system_program: Program<'info, System>,
}

この構造は「CreateOracle」アカウントと似ていますが、oracleとreward_vaultが可変(mutable)に設定されている点が異なります。これは、オラクルがその検証入力を更新する必要があり、reward_vaultがクランカーに支払うlamportsを調整する必要があるためです。bumpフィールドはオラクルアカウントから明示的に定義されており、毎回再計算する必要がありません。

指示

最後に、最も重要な部分である指示に到達しました。ここでマジックが起こります!

Create Oracle Instruction:

pub fn create_oracle(ctx: Context<CreateOracle>) -> Result<()> {
    // Set the Oracle validation based on the time and if the US market is open
    match is_us_market_open(Clock::get()?.unix_timestamp) {
        true => {
            ctx.accounts.oracle.set_inner(
                Oracle {
                    validation: OracleValidation::V1 {
                        transfer: ExternalValidationResult::Approved,
                        create: ExternalValidationResult::Pass,
                        update: ExternalValidationResult::Pass,
                        burn: ExternalValidationResult::Pass,
                    },
                    bump: ctx.bumps.oracle,
                    vault_bump: ctx.bumps.reward_vault,
                }
            );
        }
        false => {
            ctx.accounts.oracle.set_inner(
                Oracle {
                    validation: OracleValidation::V1 {
                        transfer: ExternalValidationResult::Rejected,
                        create: ExternalValidationResult::Pass,
                        update: ExternalValidationResult::Pass,
                        burn: ExternalValidationResult::Pass,
                    },
                    bump: ctx.bumps.oracle,
                    vault_bump: ctx.bumps.reward_vault,
                }
            );
        }
    }

    Ok(())
}

この指示は、set_innerを使用してOracle State Structを正しく設定し、オラクルアカウントを初期化します。is_us_market_open関数の結果に基づいて、そのアカウントを参照するNFTの転送を承認または拒否します。さらに、ctx.bumpsを使用してbumpを保存します。

Crank Oracle Instruction:

pub fn crank_oracle(ctx: Context<CrankOracle>) -> Result<()> {
    match is_us_market_open(Clock::get()?.unix_timestamp) {
        true => {
            require!(
                ctx.accounts.oracle.validation == OracleValidation::V1 {
                    transfer: ExternalValidationResult::Rejected,
                    create: ExternalValidationResult::Pass,
                    burn: ExternalValidationResult::Pass,
                    update: ExternalValidationResult::Pass
                },
                Errors::AlreadyUpdated
            );
            ctx.accounts.oracle.validation = OracleValidation::V1 {
                transfer: ExternalValidationResult::Approved,
                create: ExternalValidationResult::Pass,
                burn: ExternalValidationResult::Pass,
                update: ExternalValidationResult::Pass,
            };
        }
        false => {
            require!(
                ctx.accounts.oracle.validation == OracleValidation::V1 {
                    transfer: ExternalValidationResult::Approved,
                    create: ExternalValidationResult::Pass,
                    burn: ExternalValidationResult::Pass,
                    update: ExternalValidationResult::Pass
                },
                Errors::AlreadyUpdated
            );
            ctx.accounts.oracle.validation = OracleValidation::V1 {
                transfer: ExternalValidationResult::Rejected,
                create: ExternalValidationResult::Pass,
                burn: ExternalValidationResult::Pass,
                update: ExternalValidationResult::Pass,
            };
        }
    }

    let reward_vault_lamports = ctx.accounts.reward_vault.lamports();
    let oracle_key = ctx.accounts.oracle.key().clone();
    let signer_seeds = &[b"reward_vault", oracle_key.as_ref(), &[ctx.accounts.oracle.bump]];
    
    if is_within_15_minutes_of_market_open_or_close(Clock::get()?.unix_timestamp) && reward_vault_lamports > REWARD_IN_LAMPORTS {
        // Reward cranker for updating Oracle within 15 minutes of market open or close
        transfer(
            CpiContext::new_with_signer(
                ctx.accounts.system_program.to_account_info(), 
                Transfer {
                    from: ctx.accounts.reward_vault.to_account_info(),
                    to: ctx.accounts.signer.to_account_info(),
                }, 
                &[signer_seeds]
            ),
            REWARD_IN_LAMPORTS
        )?
    }

    Ok(())
}

この指示は、create_oracle指示と同様に機能しますが、追加のチェックが行われます。

is_us_market_open関数の応答に基づいて、状態が既に更新されているかどうかを確認します。更新されていない場合は、状態を更新します。

指示の後半部分では、is_within_15_minutes_of_market_open_or_closeがtrueであるかどうか、そしてreward vaultにクランカーに支払うのに十分なlamportsがあるかどうかをチェックします。両方の条件が満たされている場合、クランカーに報酬を転送します。そうでない場合は何もしません。

NFTの作成

この旅の最後の部分は、コレクションを作成し、それをオラクルアカウントに向けることです。これにより、そのコレクションに含める全てのアセットがカスタムオラクルルールに従うことになります!

まずは、Umiを使用するための環境をセットアップしましょう。(Umiは、SolanaプログラムのJavaScriptクライアントを構築および使用するためのモジュラーフレームワークです。詳細はこちらをご覧ください)

import { createSignerFromKeypair, signerIdentity } from '@metaplex-foundation/umi'
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'

// SecretKey for the wallet you're going to use 
import wallet from "../wallet.json";

const umi = createUmi("https://api.devnet.solana.com", "finalized")

let keyair = umi.eddsa.createKeypairFromSecretKey(new Uint8Array(wallet));
const myKeypairSigner = createSignerFromKeypair(umi, keyair);
umi.use(signerIdentity(myKeypairSigner));

次に、「CreateCollection instruction」を使用してオラクルプラグインを含むコレクションを作成します。

// Generate the Collection PublicKey
const collection = generateSigner(umi)
console.log("Collection Address: \n", collection.publicKey.toString())

const oracleAccount = publicKey("...")

// Generate the collection
const collectionTx = await createCollection(umi, {  
    collection: collection,
    name: 'My Collection',
    uri: 'https://example.com/my-collection.json',
    plugins: [
        {
            type: "Oracle",
            resultsOffset: {
                type: 'Anchor',
            },
            baseAddress: oracleAccount,
            authority: {
                type: 'UpdateAuthority',
            },
            lifecycleChecks: {
                transfer: [CheckResult.CAN_REJECT],
            },
            baseAddressConfig: undefined,
        }
    ]
}).sendAndConfirm(umi)

// Deserialize the Signature from the Transaction
let signature = base58.deserialize(collectinTx.signature)[0];  
console.log(signature);

結論

おめでとうございます!これで、オラクルプラグインを使用して米国市場の取引時間内でのみ取引可能なNFTコレクションを作成する準備が整いました。CoreとMetaplexについてさらに学びたい場合は、developer hubをチェックしてください。

この記事が気に入ったらサポートをしてみませんか?