VegeSeller Smart Contract の作り方
Cadence言語は思いついた事をなんでも実現できると断言できます。システムに平等さを組み込むことができます。
ブロックチェーンの情報はjavascript があれば取得できます。今、どんな組み込みソフトウェアでもWebKitは使えるはずですので、つまり、全ての機器はブロックチェーンから情報を取得することができます。
今から、この画像にあるような無人野菜販売所のアプリを作ります。
(無人野菜販売所に立てかけたQRコードで支払ってもらう購入手数料が0のシステムです)
ではスマートコントラクトから見ていきましょう。ベースはCookStockerスマートコントラクトで、違うところは在庫の概念を追加したこととお金を徴収できるようにしたところです。差分は以下です。
import "FlowToken"
import "FungibleToken"
access(all) contract VegeSeller {
access(self) let FlowTokenVault: Capability<&{FungibleToken.Receiver}>
まず、お金を徴収できるコントラクト(FlowTokenとFungibleToken)をインポートします。
本当は入金先を生産者別に用意すべきですが、ここではとりあえずこのコントラクトをデプロイしているアカウントに全ての代金を入金させます。
/* setter */
access(contract) fun set(id: String, add: [String], addAmount: [UInt8], remove: [String]) {
if let saved = self.data[id] {
for index, name in add {
saved.set(category: name, amount: addAmount[index])
}
for name in remove {
saved.unset(category: name)
}
// Save
self.data[id] = saved
差分の1つ目です。forループにindexがありますので、これを使用してsetメソッドを生産者による在庫調整のメソッドに変えていきます。但し、forループをあまり大量に実行するとトランザクション費用が比例して上がることになります。
2行目のIf let で取得したオブジェクトの要素(saved となっている変数)は変更しても、再度オブジェクト(self.data)にセットしないとスマートコントラクト内で変更は反映されません。(1番下の行)
access(contract) fun buy(id: String, buy: [String]) {
if let saved = self.data[id] {
for name in buy {
if (saved.data[name] == nil) {
panic("The stock is empty.")
}
saved.set(category: name, amount: saved.data[name]! - 1)
}
// Save
self.data[id] = saved
} else {
panic("No data.")
}
}
購入処理(在庫の野菜数を減少させる処理)のスマートコントラクトのコードです。
/* setter */
access(contract) fun set(category: String, amount: UInt8) {
self.data[category] = amount
}
在庫処理のコードです。前回のスマートコントラクトで日時を入れていたところを数値に置き換えただけです。
// BUY
access(all) fun buyVege(id: String, amount: @FlowToken.Vault, buy: [String]) {
self.info.buy(id: id, buy: buy)
self.FlowTokenVault.borrow()!.deposit(from: <- amount)
}
購入処理のメソッドの始まりのコードです。このメソッドに対して外部のjavascriptからfclを使ってトランザクション送信することで購入処理を実行することができます。3行目ではこのスマートコントラクトをデプロイしているアカウントに対して代金を入金しています。
init() {
self.info = Info()
self.FlowTokenVault = self.account
.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
}
}
最後に、このスマートコントラクトをデプロしているアドレスを入金先にして完成です。
全体のスマートコントラクトは以下です。
import "FlowToken"
import "FungibleToken"
access(all) contract VegeSeller {
access(self) let FlowTokenVault: Capability<&{FungibleToken.Receiver}>
access(self) let info: Info
/* 構造体 */
access(all) struct Info {
access(contract) var data: {String: VegeData}
/* setter */
access(contract) fun set(id: String, add: [String], addAmount: [UInt8], remove: [String]) {
if let saved = self.data[id] {
for index, name in add {
saved.set(category: name, amount: addAmount[index])
}
for name in remove {
saved.unset(category: name)
}
// Save
self.data[id] = saved
} else {
var data = VegeData()
for index, name in add {
data.set(category: name, amount: addAmount[index])
}
for name in remove {
data.unset(category: name)
}
self.data[id] = data
}
}
access(contract) fun buy(id: String, buy: [String]) {
if let saved = self.data[id] {
for name in buy {
if (saved.data[name] == nil) {
panic("The stock is empty.")
}
saved.set(category: name, amount: saved.data[name]! - 1)
}
// Save
self.data[id] = saved
} else {
panic("No data.")
}
}
init() {
self.data = {}
}
}
/* Info内の構造体 */
access(all) struct VegeData {
access(contract) var data: {String: UInt8}
/* setter */
access(contract) fun set(category: String, amount: UInt8) {
self.data[category] = amount
}
access(contract) fun unset(category: String) {
self.data[category] = nil
}
init() {
self.data = {}
}
}
// GET
access(all) fun getVegeSellerInfo(id: String): VegeData? {
return self.info.data[id]
}
// PUT
access(all) fun setVegeSellerInfo(id: String, add: [String], addAmount: [UInt8], remove: [String]) {
self.info.set(id: id, add: add, addAmount: addAmount, remove: remove)
}
// BUY
access(all) fun buyVege(id: String, amount: @FlowToken.Vault, buy: [String]) {
self.info.buy(id: id, buy: buy)
self.FlowTokenVault.borrow()!.deposit(from: <- amount)
}
init() {
self.info = Info()
self.FlowTokenVault = self.account
.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
}
}
このスマートコントラクトを blsqui.net のウェブサイトの左上にあるデプロイボタンでデプロイします。
buyVegeメソッドのamount引数が@FlowToken.Vaultという特別な型を持ってます(灰色の部分)。これはリソースの型を表します。Cadence言語においてResourceの型は特別な型なので@をつける仕様になっています。
&{FungibleToken.Receiver}となっている部分はこれはインタフェースを型にする時に使用します。&{}でインタフェースを型にする(インターセクションと呼ばれます)のはJava言語にもあるようで結構普遍的な書き方です。Entitlementというインタフェースの簡易版がでていますので現在はあまり使われなくなりましたが、以前はCapabilityを公開する時に、Capabilityのインタフェースを型にするため、この&{}を使用していました。
野菜購入のトランザクション
flow_blockchainフォルダにtransactions.jsファイルを新規作成します。
import { mutate, authz } from “@onflow/fcl";
export const buyVege = async function (id, buy, price) {
const txId = await mutate({
cadence: `
import "VegeSeller"
import "FlowToken"
import "FungibleToken"
transaction(id: String, buy: [String], price: UFix64) {
prepare(signer: auth(BorrowValue) &Account) {
let payment <- signer
.storage
.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)!
.withdraw(amount: price) as! @FlowToken.Vault
VegeSeller.buyVege(id: id, amount: <- payment, buy: buy)
}
execute {
log("success")
}
}
`,
args: (arg, t) => [
arg(id.toString(), t.String),
arg(buy, t.Array(t.String)),
arg(price, t.UFix64),
],
proposer: authz,
payer: authz,
authorizations: [authz],
limit: 999,
});
console.log(txId);
return txId;
};
(コードはこちらにあります。)
これを実行すると野菜を購入するためにお金を支払うことを承認するように求めるポップアップがログインしているユーザが見ている画面に表示されます。
(このポップアップはiFrameによるものですが、それはウォレットプロバイダーが自由に設定することができます。ブラウザの別タブに承認画面を出させることも出来ます。)
fclが提供しているmutateメソッドはトランザクションIDを返します。これをFlowscanのホームページ (https://www.flowscan.io/) に貼り付けるとトランザクションが成功したかどうかをすぐに分かりますので、大体の場合はどこかにログとして残しておきます。
txメソッドのsubscribeで得られるステータスを確認し、ステータスがSEALEDになったら購入者に解錠ボタンを押してもらい、無人野菜販売所の野菜ストッカーは内蔵されるWebKitがブロックチェーンと通信し野菜が購入がされたかを確認し、アンロックする機能だけがあればシステムとして完成です。
