トランザクション(Transaction)の概要
トランザクションの概要
トランザクションコード
transactionから始まるCadence言語で書かれた実行メソッドです。スマートコントラクトをimportしそのスマートコントラクトのメソッドを実行することができます。スマートコントラクトのメソッドはaccess(all)メソッドであれば誰でも呼び出せます。
例
const txId = await fcl.mutate({
cadence: `
import CookStocker from 0xb576a3926d239682
transaction(id: String, add: [String], remove: [String]) {
prepare(signer: &Account) {
CookStocker.setCookStockerInfo(id: id, add: add, remove: remove)
}
execute {
log("success")
}
}
`,
args: (arg, t) => [
arg(id.toString(), t.String),
arg(add, t.Array(t.String)),
arg(remove, t.Array(t.String)),
],
トランザクションは値を返すことができません。トランザクションはトランザクションIDを返します。このトランザクションIDを使用してトランザクション結果を確認します。
例
fcl.tx(txId).subscribe((res: any) => {
if (res.status === 4 && !res.errorMessage) {
isProcessing = false; // ローディングを停止
syncBalance(); // 残高を更新
changeScene(); // 次の処理へ
} else if (res.errorMessage) {
isProcessing = false;
alert(res.errorMessage); // トランザクションがpanicで出力した内容もしくは残高不足などのエラーメッセージが含まれます。
}
});
prepareブロック
prepare(signer: &Account) {
prepareブロックの引数にはトランザクションに署名したアカウントの数だけそのアカウントの情報が渡されます。
Entitlement
エンタイトルメントと呼びます。「権利」という意味になり、スマートコントラクトの中で宣言することでトランザクションコードでも使用可能になります。
例
import "FlowToken"
import "FungibleToken"
transaction(to: Address) {
let sentVault: @{FungibleToken.Vault}
prepare(signer: auth(BorrowValue) &Account) {
// Hardcoded amount: 5.0
let vaultRef = signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
from: /storage/flowTokenVault
) ?? panic("Could not borrow reference to the owner's Vault!")
self.sentVault <- vaultRef.withdraw(amount: 5.0)
}
execute {
let receiverRef = getAccount(to)
.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
.borrow()
?? panic("Could not borrow receiver reference to the recipient's Vault!")
receiverRef.deposit(from: <-self.sentVault)
}
}
これは署名したアカウントから5.0FLOWを引き出してtransactionブロックの引数のtoアドレスに対して振り込んでいます。
ごく一般的な送金処理です。
FungibleTokenコントラクトの中などで宣言されたEntitlementはトランザクションコード内でauth()で囲んで使用します。FungibleToken.WithdrawがEntitlementです。BorrowValueはストレージに対するEntitlementです。
どうしても慣れる方が早いので「習うより慣れろ」、という学習スタイルになります。
上記のコードはもっと短くできます。
import "FlowToken"
import "FungibleToken"
transaction(to: Address) {
prepare(signer: auth(BorrowValue) &Account) {
// Hardcoded amount: 5.0
let vaultRef = signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
from: /storage/flowTokenVault)
?? panic("Could not borrow reference to the owner's Vault")
let sentVault <- vaultRef.withdraw(amount: 5.0)
let receiverRef = getAccount(to)
.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
.borrow()
?? panic("Could not borrow receiver reference to the recipient's Vault")
receiverRef.deposit(from: <-sentVault)
}
}
executeブロックは署名したアカウントの情報が引数に渡されません。そのため、prepareブロックに必要な情報が全て揃っているため、ここに必要なロジックを書くだけでトランザクションは成立します。transactionブロックの外側にも変数を宣言することができ、その変数を使用してexecuteブロックに値を渡してメイン処理を行う、というのがマナーの良い書き方になります。(しかし、コードが短い方が視認性が良いと考える人はprepareブロックだけを使用します。実際一つ前のコードと見比べてください。明らかにexecuteブロック無しの方が分かりやすいです。手続き型言語のCOBOLのように利用したい場合は、transactionブロックの中で使用できるprepare ブロック、preブロック、execute ブロック、postブロック全てを使用するのもありだと思います。)
クエリー(query)
トランザクションは値を返さずトランザクションIDを返しますが、クエリーは値を指定した型で返します。
export const getInfo = async function (id) {
const result = await fcl.query({
cadence: `
import CookStocker from 0x1234abcd5678efgh
access(all) fun main(id: String): CookStocker.VegeData? {
return CookStocker.getCookStockerInfo(id: id)
}
`,
args: (arg, t) => [arg(id, t.String)],
});
return result;
};
access(all) fun mainブロックに対し、:をつけて取得する値の型を指定します。CookStocker.VegeData?というOptional型を返します。Resouce型であれば先頭に@がつくので自動的にVegeDataはCookStockerコントラクトのStruct(構造体)であることが分かります。
スマートコントラクトで独自の型を持つのは一般的にContract型とStruct型とResource型だけです。(EventやEntitlementも型と呼ぶ場合はそれも含みます)
ブロックチェーンにアクセスするのは主にこの2つ、トランザクションとクエリーですが、もう一つブロックチェーンにアクセスする方法があります。Eventsです。
Events
例
import * as fcl from "@onflow/fcl";
// Subscribe / Query events by event name and block range
const events = await fcl.events("A.0xContractAddress.ContractName.EventName")
.fromBlockHeight(10000000)
.toBlockHeight(10000100)
.subscribe((event) => {
console.log("Event received:", event);
});
tx
トランザクション結果をすぐに取得したい場合はEventsではなくtxメソッドを使用します。
fcl.tx(txId).subscribe((res: any) => {
if (res.status === 4 && !res.errorMessage) {
isProcessing = false; // ローディングを停止
syncBalance(); // 残高を更新 (中ではfcl.queryを行っています)
changeScene(); // 次の処理へ
} else if (res.errorMessage) {
isProcessing = false;
alert(res.errorMessage); // トランザクションがpanicで出力した内容もしくは残高不足などのエラーメッセージが含まれます。
}
});
クエリーとトランザクションの違い
クエリーは値を変更しようとするとpanicが起こります。(トランザクションフィーを渡していないのでそもそも変更ができない)
1分間に複数回クエリーを発行することを実現する為にはノードを契約するという手法があります。
無料のノードを使用したい場合は以下のようにAccess Nodeを指定します。
let isTestnet = true;
if (isTestnet) {
fcl.config({
"discovery.wallet": "https://lab.blsqui.net/authn",
"accessNode.api": "https://rest-testnet.onflow.org",// <-無料
"flow.network": "testnet",
"app.detail.title": "Your IP Title here",
"app.detail.icon": "Your IP Icon URL here",
});
} else {
fcl.config({
"discovery.wallet": "https://wallet.blsqui.net/authn",
"accessNode.api": "https://rest-mainnet.onflow.org",// <-無料
"flow.network": "mainnet",
"app.detail.title": "Your IP Title here",
"app.detail.icon": "Your IP Icon URL here",
});
}
無料の公開ノードは通常1秒間にリクエスト回数を1回までに押さえておけばエラーになりません。レートリミットを超えても1秒間に1回は正しく値を返してくれることがあります。
リソースのメソッドはaccess(all)であっても決して所有者以外は呼び出すことはできません。(リソースはアカウントストレージ内に保存されるため所有者以外はアクセスできないからです) しかし、viewResolver または Capabilityを使用すると呼び出すことが可能になります。
viewResolverとCapability
あるユーザーがそのリソースに対する「ケイパビリティ(Capability)」を公開していない限り、スクリプトは別のユーザーのプライベートな /storage/ からリソースを任意に読み込んだり借用したりすることはできません。そのため、リソースのデータはメソッドがaccess(all)であってもCapability無しでは決して所有者以外は呼び出すことはできません。
調査するには、主に2つの方法があります。
例1: ViewResolver & MetadataViews
こちらはViewを使用する方法でコントラクト内で設定する様式が細かい為、難易度は高めです。以下は一例。
import MetadataViews from 0x631edd8282631f7b
access(all) fun main(address: Address, id: UInt64): AnyStruct? {
// Get public account reference
let account = getAccount(address)
// Borrow public capability/collection reference
let collectionRef = account.capabilities.borrow<&{MetadataViews.ResolverCollection}>(
/public/MyNFTCollection
) ?? panic("Could not borrow public collection reference")
// Resolve the view data safely
let view = collectionRef.borrowViewResolver(id: id)
return view.resolveView(Type<MetadataViews.Display>())
}
例2: Capabilityの基本的な使い方を利用する
こちらの方が独自性のあるコードを書けます。
/* スマートコントラクト */
access(all) contract GameItem {
// Declare Entitlement for write/privileged actions
access(all) entitlement ManageItem
access(all) resource Arms {
access(all) var level: UInt64
init() { self.level = 10 }
// PUBLIC: Anyone holding an un-entitled capability can call this
access(all) fun getLevel(): UInt64 {
return self.level
}
// PROTECTED: Only callers holding auth(ManageItem) capability can call this
access(ManageItem) fun levelUp() {
self.level = self.level + 1
}
}
}
/* ここからトランザクション: リソースを署名者のストレージに格納 */
import GameItem from 0xCONTRACT_ADDRESS
transaction {
prepare(signer: &Account) {
// Save resource
signer.storage.save(<-GameItem.createArms(), to: /storage/MyArms)
/* Issue un-entitled capability(auth(ManageItem)がついていないので持ち主以外はlevelUpメソッドを呼び出せません) */
let cap = signer.capabilities.storage.issue<&GameItem.Arms>(/storage/MyArms)
/* Publish to /public/ ストレージ */
signer.capabilities.publish(cap, at: /public/MyArms)
}
}
/* クエリー処理 */
import GameItem from 0xCONTRACT_ADDRESS
access(all) fun main(player: Address): UInt64 {
let ref = getAccount(player)
.capabilities
.borrow<&GameItem.Arms>(/public/MyArms)
?? panic("Could not borrow public capability")
// Can call access(all) method directly.
return ref.getLevel()
}
リソースの作成と保存は、「Struct(構造体)と Resource(リソース)の全体像」でも述べます。
Capability
Capabilityは人が保持している(ストレージに保存されている)リソースのメソッドを呼び出すことができます。リソースをストレージに保存する時、ユーザーはストレージに対する保存を許可する署名を行います。その時同時に、どれぐらいまでそのリソースに対するアクセスを人(システム)に許可するかをEntitlementと一緒にCapability で設定します。Entitlement を付けなかったCapabilityはaccess(all)メソッドだけが他人でもアクセスできます。Capabilityを公開しなければ、access(all)も呼び出すことは不可能ですが、署名者はトランザクション時に自由にアクセスできますので特段不都合がある訳ではありません。ゲームであれば必要な情報は構造体(Struct)にまとめておく方が圧倒的に便利であり、Capabilityが必要になるのは(能力<Capability>を委任したい場合など)複雑なシステムだけということが往々にしてあります。
Capabilityを発行する(
auth()がついてないのでaccess(all)のメソッドのみ呼べます)let cap = signer.capabilities.storage.issue<&GameItem.Arms>(/storage/MyArms)Capabilityを公開する
signer.capabilities.publish(cap, at: /public/MyArms)Capabilityを拝借する
let ref = getAccount(player)
.capabilities
.borrow<&GameItem.Arms>(/public/MyArms)
?? panic("Could not borrow public capability")
4. Capabilityを通じて呼び出す。
return ref.getLevel()
EntitlementをつけてCapabilityを発行する時は以下のようにします。(1.と違ってauth(GameItem.ManageItem)がついています)
let entitledCap = signer.capabilities.storage.issue<
auth(GameItem.ManageItem) &GameItem.Arms
>(/storage/MyArms)
Capabilityは必要に迫られるまでは作成しないので最初は気にする必要はありません。
アクセス修飾子
access(all) 権限なくアクセス可能
access(contract) コントラクト内からであればアクセス可能
access(self) リソースまたはStruct内からのみアクセス可能
access(Entitlement) 宣言したEntitlementの権限を持っていればアクセス可能
例:
access(ManageItem) fun levelUp() {
self.level = self.level + 1
}
ウォレットアカウントの情報を取得する
fcl.currentUser().subscribe((newVal: any) => {
user = newVal;
syncBalance();
});
Flowネイティブ通貨の残高を取得する
// Get Player's balance
async function syncBalance() {
if (!user?.addr) {
esportsBalance = '-';
return;
};
const account = await fcl.account(user.addr);
esportsBalance = String(account.balance);
if (!esportsBalance.includes('.')) {
const num = parseFloat(esportsBalance) / 100000000;
esportsBalance = num.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 8
});
}
}
ブロックチェーンシステムを作るに当たり、最初に覚えておかなければならないことはこれぐらいです。これだけ覚えておけば誰でも決済システム・eスポーツを作れます。
