RideShare Smart Contract の作り方
ドライバーの受注管理と、乗客の発注機能をスマートコントラクトに組み込みます。
スマートコントラクトはこちら
import "FlowToken"
import "FungibleToken"
access(all) contract RideShare {
access(self) let FlowTokenVault: Capability<&{FungibleToken.Receiver}>
access(self) let DriverFlowTokenVault: {UInt: Capability<&{FungibleToken.Receiver}>}
access(self) let info: Info
access(self) var totalDrivers: UInt // Driver ID
/* 構造体 */
access(all) struct Info {
access(contract) var driverData: {UInt: DriverData}
access(contract) var orderQueue: [Order]
/* setter */
access(contract) fun setDriver(id: UInt, keys: [String], values: [String]): UInt {
if let saved = self.driverData[id] {
for index, key in keys {
saved.set(key: key, value: values[index])
}
self.driverData[id] = saved // 更新
return id
} else {
// 新規登録
RideShare.totalDrivers = RideShare.totalDrivers + 1
var data = DriverData()
for index, key in keys {
data.set(key: key, value: values[index])
}
self.driverData[RideShare.totalDrivers] = data
return RideShare.totalDrivers
}
}
access(contract) fun setOrder(execTime: UFix64, driverId: UInt, start: String, goal: String, wage: UFix64) {
var order = Order(execTime: execTime, driverId: driverId, start: start, goal: goal, wage: wage)
self.orderQueue.append(order)
}
init() {
self.driverData = {}
self.orderQueue = []
}
}
/* Info内の構造体 */
access(all) struct DriverData {
access(contract) var data: {String: String}
/* setter */
access(contract) fun set(key: String, value: String) {
self.data[key] = value
}
init() {
self.data = {}
}
}
/* Info内の構造体 */
access(all) struct Order {
access(all) let execTime: UFix64
access(all) let driverId: UInt
access(all) let start: String
access(all) let goal: String
access(all) let wage: UFix64
init(execTime: UFix64, driverId: UInt, start: String, goal: String, wage: UFix64) {
self.execTime = execTime
self.driverId = driverId
self.start = start
self.goal = goal
self.wage = wage
}
}
// GET
access(all) fun getInfo(): Info {
return self.info
}
// PUT
access(all) fun setDriverInfo(driverId: UInt, keys: [String], values: [String], flow_vault_receiver: Capability<&{FungibleToken.Receiver}>?) {
let _driverId = RideShare.info.setDriver(id: driverId, keys: keys, values: values)
if (flow_vault_receiver != nil) {
RideShare.DriverFlowTokenVault[_driverId] = flow_vault_receiver
}
}
// PUT
access(all) fun newOrder(payment: @FlowToken.Vault, execTime: UFix64, driverId: UInt, start: String, goal: String) {
self.info.setOrder(execTime: execTime, driverId: driverId, start: start, goal: goal, wage: payment.balance * 0.97)
self.FlowTokenVault.borrow()!.deposit(from: <- payment)
}
access(all) resource Admin {
// ORDER COMPLETE
access(all) fun payWage(driverId: UInt, wage: UFix64) {
pre {
RideShare.DriverFlowTokenVault[driverId] != nil: "Deposit destination is nil, so we can't send wage."
}
// Pay the wage.
let wage <- RideShare.account.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(from: /storage/flowTokenVault)!.withdraw(amount: wage) as! @FlowToken.Vault
RideShare.DriverFlowTokenVault[driverId]!.borrow()!.deposit(from: <- wage)
}
}
init() {
self.info = Info()
self.FlowTokenVault = self.account.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
self.DriverFlowTokenVault = {}
self.totalDrivers = 0
self.account.storage.save( <- create Admin(), to: /storage/RideShareAdmin)
}
}
ここでリソース(resource)が初登場します。access(all) resource Admin {
専売的にスマートコントラクトが持っているメソッドでトランザクションを実行する為には、リソースが不可欠だからです。誰でもシステム運営者の所持金を送金できたら、資金が盗難されて困るからです。
スマートコントラクトをデプロイする時(init)にデプロイアカウントに対してAdminというResourceを持たせています。self.account.storage.save( <- create Admin(), to: /storage/RideShareAdmin)
スマートコントラクトのリソースに定義されたメソッドはそのリソースの持ち主にしか呼び出せません。このリソースがドライバーに対して賃金を支払う役割を果たします。(バックエンドで秘密鍵を保持していればバックエンドサーバーが自動でこれを行えます。つまりリソースの保持者かどうかは秘密鍵があっているかどうかで判定します。SSHログインと同じですね。そのため、秘密鍵が1箇所に集約されていないBlsquiはゲームユーザーの権利を守る上で強固なんですね。)
さぁ、あとはスマートコントラクト内部のメソッドを呼ぶ実装をするだけです。
structとresourceの違い
struct:
静的変数のNoSQL、但し厳格に型を定める
宣言時はaccess(all)であり、構造体の型は公開される
init以外での値の書き換えにはsetterが必要
resource:
クラスオブジェクトに近い
ポインタの概念を持つ
宣言時はaccess(all)でありクラスの型は公開される
create演算子でオブジェクトを生成し、オブジェクトはストレージに保存される
cpu消費を避ける為、基本的にポインタを通じてresourceのメソッドを呼ぶ
動くことができ、ストレージ間を移動する(=所有できる)
注文読み取り
- スマートコントラクトの構造体の情報を取得します。
import { query } from "@onflow/fcl";
export const getRideShareInfo = async function () {
const result = await query({
cadence: `
import "RideShare"
access(all) fun main(): RideShare.Info? {
return RideShare.getInfo()
}
`,
args: (arg, t) => [],
});
return result;
};
- 複数のアカウントの所持金を取得する
export const getBalances = async function (address) {
let driverAddress;
if (import.meta.env && import.meta.env.VITE_DRIVER_ADDRESS) {
driverAddress = import.meta.env.VITE_DRIVER_ADDRESS;
}
const systemAddress = "0xb576a3926d239682";
const result = await query({
cadence: `
import "FlowToken"
import "FungibleToken"
access(all) fun main(
address: Address,
driverAddress: Address,
systemAddress: Address
): [UFix64] {
let vaultRef = getAccount(address)
.capabilities
.borrow<&FlowToken.Vault>(/public/flowTokenBalance)
?? panic("Something wrong happened.")
let driverVaultRef = getAccount(driverAddress)
.capabilities
.borrow<&FlowToken.Vault>(/public/flowTokenBalance)
?? panic("Something wrong happened.")
let systemVaultRef = getAccount(systemAddress)
.capabilities
.borrow<&FlowToken.Vault>(/public/flowTokenBalance)
?? panic("Something wrong happened.")
var addresses: [UFix64] = []
addresses.append(vaultRef.balance)
addresses.append(driverVaultRef.balance)
addresses.append(systemVaultRef.balance)
return addresses
}
`,
args: (arg, t) => [
arg(address, t.Address),
arg(driverAddress, t.Address),
arg(systemAddress, t.Address),
],
});
return result;
};
RideShareスマートコントラクト内に登録ドライバー全員のアドレスを保存しておくことで、クエリー時にドライバー全員のアドレスを取得することで、ドライバー全員の所持金を一度に取得することも可能です。
トランザクションの送信
- クリプトによる支払い(リソースのストレージ移動はアプリ利用者による署名が必要な処理です)
import { mutate, authz } from "@onflow/fcl";
const formatUFix64 = (num) => Number(num).toFixed(8);
export const newOrder = async function (
execDateTime,
driverId,
start,
goal,
price
) {
const txId = await mutate({
cadence: `
import "RideShare"
import "FlowToken"
import "FungibleToken"
transaction(
execTime: UFix64,
driverId: UInt,
start: String,
goal: 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
RideShare.newOrder(
payment: <- payment, // リソースを移動する時は <- (Linear Types記号)を使用
execTime: execTime,
driverId: driverId,
start: start,
goal: goal
)
}
execute {
}
}
`,
args: (arg, t) => [
arg(formatUFix64(new Date(execDateTime).getTime() / 1000), t.UFix64),
arg(driverId, t.UInt),
arg(start, t.String),
arg(goal, t.String),
arg(formatUFix64(price), t.UFix64),
],
proposer: authz,
payer: authz,
authorizations: [authz],
limit: 999,
});
console.log(txId);
return txId;
};
このjavascriptメソッドを呼び出すことでRideShareサービスに注文を発行し、同時に支払われたクリプトは一時的にシステムアカウントが受け取ります。注文が実行された時,ドライバーにその内の97%を支払います。3%は支払いマージンとして利益とします。
トランザクションのprepareメソッドはexecuteメソッドと違い、引数にsigner(署名者アカウントのオブジェクト)が入ります。そのためsignerが必要な処理はprepareメソッド内に書きます。
transactionメソッドにはprepare, execute以外にもpreとpostが存在し、トランザクションの処理はprepare -> pre -> execute -> postの順番に進行します。
- ドライバーを登録する処理 (秘密鍵をバックエンドで保持していればバックエンドで実行可能)
import RideShare from 0xb576a3926d239682
import FlowToken from 0x1654653399040a61
import FungibleToken from 0xf233dcee88fe0abe
transaction(driverId: UInt, driverAddress: Address, keys: [String], values: [String]) {
prepare() {
let FlowTokenReceiver = getAccount(driverAddress)
.capabilities
.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
RideShare.setDriverInfo(driverId: driverId, keys: keys, values: values,
flow_vault_receiver: FlowTokenReceiver)
}
execute {
log("success")
}
}
- ドライバーに報酬を支払う処理
const transaction = `
import RideShare from 0xb576a3926d239682
transaction(driverId: UInt, wage: UFix64) {
prepare(signer: auth(BorrowValue) &Account) {
let RideShareAdmin = signer
.storage
.borrow<&RideShare.Admin>(from: /storage/RideShareAdmin)
?? panic("Could not borrow reference to the Administrator Resource.")
RideShareAdmin.payWage(driverId: driverId, wage: wage)
}
execute {
}
}
`;
ドライバーに報酬を支払う事はシステムによる特権なので、Adminリソースがその役割を果たします。
こういったシステムはバックエンドで秘密鍵を保持しておくのが通常なので、以下のようなコードで承認関数(authFunctionとauthFunctionForProposer)を自作して自動実行します。
import { config, sansPrefix, withPrefix, mutate, tx } from "@onflow/fcl";
txId = await mutate({
cadence: transaction,
args: (arg, t) => [
arg(message.driverId, t.UInt),
arg(message.wage, t.UFix64),
],
proposer: authFunctionForProposer,
payer: authFunction,
authorizations: [authFunction],
limit: 999,
});
console.log(`txId: ${txId}`);
tx(txId).subscribe((res) => {
console.log(res);
});
これはNode.jsによる実行ですが、Go言語でもできます。(FlowとCadenceはGoで書かれているので親和性も高いです)
トランザクション承認を担う関数は少し長いですがこのようになります。
try {
var IT_KEY_ID = 0;
if (fs.existsSync('/tmp/sequence.txt')) {
IT_KEY_ID = parseInt(fs.readFileSync('/tmp/sequence.txt', { encoding: 'utf8' }));
} else {
IT_KEY_ID = 10;
}
IT_KEY_ID = !IT_KEY_ID || IT_KEY_ID >= 10 ? 0 : IT_KEY_ID + 1;
fs.writeFileSync('/tmp/sequence.txt', IT_KEY_ID.toString());
console.log('IT_KEY_ID', IT_KEY_ID);
// CHANGE THESE THINGS FOR YOU
const PRIVATE_KEY = fs.readFileSync('testnet-account.pkey', { encoding: 'utf8' });
const ADDRESS = '0x975b04756864e9ea';
const KEY_ID = 0;
const hash = (message) => {
const sha = new SHA3(256);
sha.update(Buffer.from(message, 'hex'));
return sha.digest();
};
const sign = (message) => {
const key = ec.keyFromPrivate(Buffer.from(PRIVATE_KEY, 'hex'));
const sig = key.sign(hash(message)); // hashMsgHex -> hash
const n = 32;
const r = sig.r.toArrayLike(Buffer, 'be', n);
const s = sig.s.toArrayLike(Buffer, 'be', n);
return Buffer.concat([r, s]).toString('hex');
};
async function authFunction(account) {
return {
...account,
tempId: `${ADDRESS}-${KEY_ID}`,
addr: sansPrefix(ADDRESS),
keyId: Number(KEY_ID),
signingFunction: async (signable) => {
return {
addr: withPrefix(ADDRESS),
keyId: Number(KEY_ID),
signature: sign(signable.message)
};
}
};
}
async function authFunctionForProposer(account) {
return {
...account,
tempId: `${ADDRESS}-${IT_KEY_ID}`,
addr: sansPrefix(ADDRESS),
keyId: Number(IT_KEY_ID),
signingFunction: async (signable) => {
return {
addr: withPrefix(ADDRESS),
keyId: Number(IT_KEY_ID),
signature: sign(signable.message)
};
}
};
}
IT_KEY_ID = !IT_KEY_ID || IT_KEY_ID >= 10 ? 0 : IT_KEY_ID + 1;は何をしているかというと、バックエンドでは連続でトランザクションを実行することになると思いますが、ブロックチェーン側は連続処理に対し、KEY_IDの変更を求めてきます。(リプレイアタックの防止)
txId = await mutate({
cadence: transaction,
args: (arg, t) => [arg(gamerId, t.UInt), arg(outcome, t.Bool)],
proposer: authFunctionForProposer, // キーをサイクルする承認関数
payer: authFunction, // キーをサイクルしない承認関数
authorizations: [authFunction], // キーをサイクルしない承認関数
limit: 999,
});
(サンプルコードはこちらにあります。)
これでドライバーが売り上げの97%を取得できるアプリが明日からでも運用できます。
