あいさつ
こんにちは、ryosk7です。
この記事は、Ruby on Rails Advent Calendar 2023 16日目の記事です。
はじめに
技術書典15で出展した「Rubyでつくる NFTマーケットプレイス」を参考に、MetaMask認証機能を作っていきます。
元々、NFTマーケットプレイスを実装する際に、こちらの記事を参考にしていました。
ただし、こちらの記事ではJavaScript(以下JS)をゴリゴリ書いており、筆者はそこまでJSを書きたくなかったので、なにかいい手はないかと考えたところ、一つの解決策に辿り着きました。
それが、『importmap-rails』 です。
importmap-rails
Rails 7.0から標準搭載となった機能です。
こちらの記事がとてもわかりやすいです。
要約すると、
- JS の Bundling が不要
- ブラウザが必要なファイルを取得してくれる
- HTTP/2で並列処理
- 複数ファイルを並列して処理してくれる
- Bundlingが不要になったため、変更のないファイルはキャッシュを利用可能
- 新たにダウンロードするファイルも最小限に済む
フロントエンドの開発体験がかなりよくなりました。
NFTをはじめとした、Web3界隈の技術は、RubyよりもJSの方が盛んな印象です。
Railsを使って開発する時も、importmap-railsを使えば妥協することなく、JSの技術を利用することができます。
実践編
まずはRailsアプリを用意しましょう。importmap-railsはすでに使える状態でインストールされます。
Railsをセットアップ
rails new myapp
cd myapp
Userモデルを作成
続いて、Userモデルを作成します。
rails g model user eth_address:string eth_nonce:string username:string
rails db:migrate
それぞれ説明します。
- eth_address: ユーザーのEthereumアドレスです。アプリケーションでの認証に使用されます。
- eth_nonce: 署名のなりすましを防ぐために使用されます。
- username: ユーザー名です。任意の名前が入ります。
作成されたapp/models/user.rb
に以下のコードを追加してください。
class User < ApplicationRecord
validates :eth_address, presence: true, uniqueness: true
validates :eth_nonce, presence: true, uniqueness: true
validates :username, presence: true, uniqueness: true
end
続いて、users_controller.rbを作成しましょう。必要なのはcreateアクションのみです。
rails g controller users create
route.rbを以下のように書き換えます。
Rails.application.routes.draw do
resources :users, only: [:create]
# 省略
importmap-railsを使って、Web3.jsをインストール
MetaMask 認証のような、ウォレットの認証は Web3.js を使うのが便利です。
以下のコマンドでインストールします。
bin/importmap pin web3
モジュールはconfig/importmap.rb
に追加されたと思います。確認しましょう。
# <--- 省略 --->
pin_all_from "app/javascript/controllers", under: "controllers"
pin "web3", to: "https://..."
pin "@adraffy/ens-normalize", to: "https://..."
pin "@ethereumjs/rlp", to: "https://..."
pin "@noble/curves/secp256k1", to: "https://..."
pin "@noble/hashes/_assert", to: "https://..."
pin "@noble/hashes/crypto", to: "https://..."
pin "@noble/hashes/hmac", to: "https://..."
pin "@noble/hashes/pbkdf2", to: "https://..."
pin "@noble/hashes/scrypt", to: "https://..."
pin "@noble/hashes/sha256", to: "https://..."
# <--- 省略 --->
pin "web3-eth-contract", to: "https://..."
pin "web3-eth-ens", to: "https://..."
pin "web3-eth-iban", to: "https://..."
pin "web3-eth-personal", to: "https://..."
pin "web3-net", to: "https://..."
pin "web3-providers-http", to: "https://..."
pin "web3-providers-ws", to: "https://..."
pin "web3-rpc-methods", to: "https://..."
pin "web3-types", to: "https://..."
pin "web3-utils", to: "https://..."
pin "web3-validator", to: "https://..."
pin "zod", to: "https://..."
ファイルの中に、pin_all_from というのがあると思います。これは、app/javascript/controllers 配下に配置した場合に import で読み込めるようにできます。
# <--- 省略 --->
pin_all_from "app/javascript/controllers", under: "controllers"
# <--- 省略 --->
Web3.jsと eth gemで認証機能を作る。
ウォレットに接続するための機能を実装していきます。先ほどインストールしてWeb3.jsを利用します。
そのためにはま ず、app/javascript/controllers 配下に新たにコントローラーを追加します。 metamask コントローラーという名前のコントローラーをつくり、ここで接続処理を記述しましょう。以下のコマンドを打ってください。
bin/rails g stimulus metamask
app/javascript/
配下に metamask.js が追加されました。
コマンドで利用した『Stimulus』は、 Rails上でJSを扱うためのレールのような存在です。
ファイルを開き、Web3.js を import しましょう。
import { Controller } from "@hotwired/stimulus"
import Web3 from "web3" // 追加
// Connects to data-controller="metamask"
export default class extends Controller {
connect() {}
}
application.html.erbを編集して、ウォレット接続ボタンを追加します。
<div data-controller="metamask">
<a data-action="click->metamask#signin" href="#">
ウォレット接続
</a>
</div>
先ほど作成した、metamask.js
に signin()
を追加します。
async signin() {
const provider = window.ethereum || window.web3?.provider || null
if (!provider) {
console.error('!provider')
return
}
const web3 = new Web3(provider)
const account = await web3.eth.requestAccounts();
const nonce = account[0]
const eth_address = account[1]
const customTitle = "Ethereum on Rails";
const requestTime = new Date().getTime();
const eth_message = customTitle + "," + requestTime + "," + nonce;
const password = ''
const eth_signature = await web3.eth.personal.sign(
eth_message, eth_address, password
)
const csrfToken = document
.querySelector("meta[name='csrf-token']")
.getAttribute("content");
fetch("http://localhost:3000/users/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken,
},
body: JSON.stringify({eth_message, eth_address, eth_signature}),
}).then(response => response.json())
.then(data => {
if (data.connected) {
this.element.style.display = 'none';
}
});
}
Web3.js のインスタンスを作成し、MetaMask ウォレットに対してリクエストを投げます。そして、署名を作成してください。これは、サーバー側で認証をさせるために必要になります。
fetch 関数を使い、サーバーに対して、アクセスしています。JSON 形式で渡したいため、JSON.stringify で加工しています。
eth gemをインストール
サーバー側で、イーサリアムを扱うために、eth gem という gem を使用します。Gemfile に追加し、bundle install してください。
gem "eth"
bundle install
最後にusers_controller.rbに以下を追加します。これは、元々参考にしていた、こちらのセクションを参考にしています。
class UsersController < ApplicationController
def create
user = User.find_by(eth_address: params[:eth_address])
if user.present?
if params[:eth_signature]
message = params[:eth_message]
signature = params[:eth_signature]
# フォームからではなく、データベースからのユーザー アドレスとノンスを使用する
user_address = user.eth_address
user_nonce = user.eth_nonce
custom_title, request_time, signed_nonce = message.split(",")
request_time = Time.at(request_time.to_f / 1000.0)
expiry_time = request_time + 300
if request_time && Time.current < expiry_time
# 署名されたノンスが記録にあるものであることを強制する
if signed_nonce == user_nonce
# 署名からアドレスを復元する
signature_pubkey = Eth::Signature.personal_recover message, signature
signature_address = Eth::Util.public_key_to_address signature_pubkey
if user_address.downcase == signature_address.to_s.downcase
session[:user_id] = user.id
user.eth_nonce = SecureRandom.uuid
user.save
redirect_to root_path, notice: "Logged in successfully!"
end
end
end
end
end
end
end
これで一通りのMetaMaskログインが完成です。
おわりに
いかがでしたでしょうか。
元々の記事と比較すると、少ないJSの記述で同じ認証基盤を構築できたことがわかります。
importmap-railsにより、気軽にnpmライブラリを利用できるので、今回の事例で扱ったWeb3.jsに限らず、みなさんもぜひ様々なライブラリで試してみてください!
最後に紹介となりますが、株式会社KINECAで、エンタメマッチングアプリ『pato』の開発を行っています。
本サービスもRails7を使っており、常にモダンな環境にするためアップデートメンテナンスを絶やさず行っています。
気になる方、一緒に働きたい方、おしゃべりしたい方はぜひ気軽にXにDMをお願いします🙌