cross-contract-callを実装しよう
⚔️ cross contract call
を実装しよう
これまでのレッスンの中で
アカウントが他のアカウントにftを転送するにはftコントラクト
のメソッドを呼び出せばよいことがわかりました。
そして本プロジェクトではいくつか必要な機能がまだ残っています。
そのうちの1つが
bikeコントラクト
からバイクの点検をしてくれたユーザへ報酬として ft を支払う
という機能です。
この場合の処理の流れを整理します。
- バイクを点検中のユーザがバイクの返却を
bikeコントラクト
に申請する。 bikeコントラクト
はユーザのアカウントIDを照合し報酬としてftをユーザへ転送。bikeコントラクト
はftの転送後、バイクの返却手続きを進める。
これを実現するには以下の処理を同期的に行う必要があります。
- ユーザが
bikeコントラクト
のバイク返却メソッドを呼ぶ。 bikeコントラクト
がftコントラクト
のft転送メソッドを呼ぶ。- ft の転送が成功していれば
bikeコントラクト
の返却処理を進める。
cross contract call
とcallback
関数という機能を使用してこの機能を実装します。
cross contract call
はあるコントラクトから別のコントラクトのメソッドを呼び出す仕組みです。
callback
関数はcross contract call
の結果に対応した処理を行う際に使用します。
まずはコントラクト内に下記のコードを追加してください!
// lib.rs
use near_sdk::{
borsh::{self, BorshDeserialize, BorshSerialize},
env, ext_contract, log, near_bindgen, AccountId, Gas, Promise, PromiseResult,
};
const FT_CONTRACT_ACCOUNT: &str = "sub.ft_account.testnet"; // <- あなたのftコントラクトをデプロイしたアカウントに変更してください!
const AMOUNT_REWARD_FOR_INSPECTIONS: u128 = 15;
/// 外部コントラクト(ftコントラクト)に実装されているメソッドをトレイトで定義
#[ext_contract(ext_ft)]
trait FungibleToken {
fn ft_transfer(&mut self, receiver_id: String, amount: String, memo: Option<String>);
}
const DEFAULT_NUM_OF_BIKES: usize = 5;
/// バイクの状態遷移を表します。
// ...
/// コントラクトを定義します
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct Contract {
bikes: Vec<Bike>,
}
/// デフォルト処理を定義します。
// ...
/// メソッドの実装です。
#[near_bindgen]
impl Contract {
// ...
pub fn inspect_bike(&mut self, index: usize) {
// ...
}
// バイク 使用中or点検中 -> 使用可
pub fn return_bike(&mut self, index: usize) {
let user_id = env::predecessor_account_id();
log!("{} returns bike", &user_id);
match &self.bikes[index] {
Bike::Available => panic!("Bike is already available"),
Bike::InUse(user) => {
assert_eq!(user.clone(), user_id, "Fail due to wrong account");
self.bikes[index] = Bike::Available
}
Bike::Inspection(inspector) => {
assert_eq!(inspector.clone(), user_id, "Fail due to wrong account");
Self::return_inspected_bike(index); // <- 関数実行に変更!
}
};
}
/// 点検中から返却に変更する際の挙動を定義します。
/// 点検をしてくれたユーザに報酬(ft)を支払い、コールバックで返却処理をします。
pub fn return_inspected_bike(index: usize) -> Promise {
let contract_id = FT_CONTRACT_ACCOUNT.parse().unwrap();
let amount = AMOUNT_REWARD_FOR_INSPECTIONS.to_string();
let receiver_id = env::predecessor_account_id().to_string();
log!(
"{} transfer to {}: {} FT",
env::current_account_id(),
&receiver_id,
&amount
);
// cross contract call (contract_idのft_transfer()メソッドを呼び出す)
ext_ft::ext(contract_id)
.with_attached_deposit(1)
.ft_transfer(receiver_id, amount, None)
.then(
// callback (自身のcallback_return_bike()メソッドを呼び出す)
Self::ext(env::current_account_id())
.with_static_gas(Gas(3_000_000_000_000))
.callback_return_bike(index),
)
}
/// cross contract call の結果を元に処理を条件分岐します。
// #[private]: predecessor(このメソッドを呼び出しているアカウント)とcurrent_account(このコントラクトのアカウント)が同じことをチェックするマクロです.
// callbackの場合、コントラクトが自身のメソッドを呼び出すことを期待しています.
#[private]
pub fn callback_return_bike(&mut self, index: usize) {
assert_eq!(env::promise_results_count(), 1, "This is a callback method");
match env::promise_result(0) {
PromiseResult::NotReady => unreachable!(),
PromiseResult::Failed => panic!("Fail cross-contract call"),
// 成功時のみBikeを返却(使用可能に変更)
PromiseResult::Successful(_) => self.bikes[index] = Bike::Available,
}
}
}
// ...
変更点を見ていきましょう。
cross contract call
を使用するには、呼び出す外部コントラクトのメソッドをトレイトで定義しておきます。
今回はftコントラクトのft_transfer
メソッドを呼び出すのでドキュ メントやソースコードを参考にft_transfer
のプロトタイプ宣言をここで記述します。
トレイトの注釈には#[ext_contract(ext_ft)]
をつけます。
ext_ft
は後でメソッド呼び出しに使用する略称で任意の名前です。
またFungibleToken
も任意の名前です。
文法に関して詳しくはこちらを参照してください。
/// 外部コントラクト(ftコントラクト)に実装されているメソッドをトレイトで定義
#[ext_contract(ext_ft)]
trait FungibleToken {
fn ft_transfer(&mut self, receiver_id: String, amount: String, memo: Option<String>);
}
続いてreturn_bike
メソッドに新たに追加された関数を見ていきましょう。
/// メソッドの実装です。
#[near_bindgen]
impl Contract {
// ...
// バイク 使用中or点検中 -> 使用可
pub fn return_bike(&mut self, index: usize) {
// ...
match &self.bikes[index] {
// ...
Bike::Inspection(inspector) => {
assert_eq!(inspector.clone(), user_id, "Fail due to wrong account");
Self::return_inspected_bike(index); // <- 関数実行に変更!
}
};
}
/// 点検中から返却に変更する際の挙動を定義します。
/// 点検をしてくれたユーザに報酬(ft)を支払い、コールバックで返却処理をします。
pub fn return_inspected_bike(index: usize) -> Promise {
let contract_id = FT_CONTRACT_ACCOUNT.parse().unwrap();
let amount = AMOUNT_REWARD_FOR_INSPECTIONS.to_string();
let receiver_id = env::predecessor_account_id().to_string();
log!(
"{} transfer to {}: {} FT",
env::current_account_id(),
&receiver_id,
&amount
);
// cross contract call (contract_idのft_transfer()メソッドを呼び出す)
ext_ft::ext(contract_id)
.with_attached_deposit(1)
.ft_transfer(receiver_id, amount, None)
.then(
// callback (自身のcallback_return_bike()メソッドを呼び出す)
Self::ext(env::current_account_id())
.with_static_gas(Gas(3_000_000_000_000))
.callback_return_bike(index),
)
}
// ...
}
return_inspected_bike
関数はcross contract call
によってftをreceiver_idへ送信,
その後の処理をcallback
関数で行っています。
cross contract call
を実行するためには先ほど定義したext_ft
を使用して外部メソッドの呼び出しを行います。
そしてthen
で、外部コントラクトのメソッド呼び出し後に実行するアクションとしてcallback
関数を待機させます。
cross contract call
の裏で起きていることについて コントラクト A でのcross contract call
の呼び出しを受け付けたランタイム(コントラクトを実行するレイヤ)は,callback
関数のアクションを待機させつつ、外部コントラクト B のメソッドを呼び出すことをreceiptを通して次のブロックに知らせます。 次のブロックではreceipt
により(コントラクト B を含んだシャードにて)メソッドが実行されます。 そしてその実行結果はまたreceipt
を通して次のブロックへ伝えられます。 次のブロックでは(コントラクト A を含んだシャードにて)待機されていたcallback
関数の実行が行われます。
文法に関して詳しくはこちらを参照してください。
最後に、callback
関数ではcross contract call
の結果をenvを通して取得し処理をしています。
💁 コントラクトをアップデートしよう
ここで、コントラクトを少しアップデートします。
今までコントラクトの初期化にはdefault
関数を使用していました。
コード内該当箇所。
// lib.rs
const DEFAULT_NUM_OF_BIKES: usize = 5;
/// コントラクトを定義します
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct Contract {
bikes: Vec<Bike>,
}
/// デフォルト処理を定義します。
impl Default for Contract {
fn default() -> Self {
// ...
}
}
ですが、初期値を引数で渡し たい時もあります。
その時は#[init]
の注釈をつけたメソッドをストラクトに定義することで可能です。
用意していたDefault
の実装を削除し、以下のようなメソッドを追加します。
(PanicOnDefault
はDefault
の実装をしないことを明記するものです)
/// コントラクトを定義します
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] // <- PanicOnDefault 追加
pub struct Contract {
bikes: Vec<Bike>,
}
/* Defaultの実装を削除してください
impl Default for Contract {
fn default() -> Self {
Self {
bikes: {
let mut bikes = Vec::new();
for _i in 0..DEFAULT_NUM_OF_BIKES {
bikes.push(Bike::Available);
}
bikes
},
}
}
}
*/
/// メソッドの実装です。
#[near_bindgen]
impl Contract {
/// init関数の実装です。
#[init]
pub fn new(num_of_bikes: usize) -> Self {
log!("initialize Contract with {} bikes", num_of_bikes);
Self {
bikes: {
let mut bikes = Vec::new();
for _i in 0..num_of_bikes {
bikes.push(Bike::Available);
}
bikes
},
}
}
// ...
}
テストコードで使用していたdefault
をnew