Skip to main content

Command Palette

Search for a command to run...

RideShare Smart Contract の作り方

Updated
6 min readView as Markdown
T
Founder & Architect of Blsqui. Building the cloud-native infrastructure layer that transforms standard video games into instant Web3 eSports platforms. Passionate about democratizing global revenue networks for the next generation of independent, AI-driven creators. Creating plug-and-play, no-code backend frameworks for Unity and Godot that integrate frictionless stablecoin microtransactions without client-side execution overhead.

ドライバーの受注管理と、乗客の発注機能をスマートコントラクトに組み込みます。

スマートコントラクトはこちら

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リソースがその役割を果たします。

こういったシステムはバックエンドで秘密鍵を保持しておくのが通常なので、以下のようなコードで承認関数(authFunctionauthFunctionForProposer)を自作して自動実行します。

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%を取得できるアプリが明日からでも運用できます。

13 views