<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>0xtomo</title>
        <link>https://paragraph.com/@0xtomo</link>
        <description>[Twitter](https://twitter.com/HAIL)</description>
        <lastBuildDate>Sun, 19 Apr 2026 05:17:56 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>0xtomo</title>
            <url>https://storage.googleapis.com/papyrus_images/5df738d240c2485695e8102b600193023c5ae4d2bfdbcfd0684df2213b909899.png</url>
            <link>https://paragraph.com/@0xtomo</link>
        </image>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[Tenderly + Vercel + Foundry + monorepoでのEthereum開発が楽すぎる話]]></title>
            <link>https://paragraph.com/@0xtomo/tenderly-vercel-foundry-monorepo</link>
            <guid>7GoLXj2VwDvWeT0r7Xjx</guid>
            <pubDate>Sat, 20 Dec 2025 17:48:12 GMT</pubDate>
            <description><![CDATA[こんにちは。TanéでCTOをしていますTomo(X: HAIL)です。この記事はWeb3 Advent Calendar 2025 18日目の記事になります。2記事目の寄稿なんですが、1記事目はQiitaに直接書き、今回はこのParagraph(旧Mirrors.xyz)に書いてみます。一体どこがいいやら。 ついこの間まで5年以上にわたり同じプロダクトをメンテしていてそのtipsをまとめたりしたんですが、今久しぶりに完全新規の開発をしていて、ハードルの下がり方がすごいのでシェアします。想定読者はEVMチェーン上(Ethereum, Base, Arbitrum, Polygon, ...)でサービス開発をしている、もしくはしようと思っているエンジニアです。別段超最新のstackでもないので、知っとるわ！ という方もいるかとは思いますが、そういう人は連絡ください。採用しますｗ目的とStack果たしたい目的は以下を考えます:EVM上で動作するwebアプリを爆速で開発したいフロントエンドもコントラクトも頻繁に実装が変わる中、それに対応するQA環境を爆速で整備したいEVM上で動作するw...]]></description>
            <content:encoded><![CDATA[<p>こんにちは。<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://tanelabs.com/ja">Tané</a>でCTOをしていますTomo(X: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://x.com/HAIL">HAIL</a>)です。この記事は<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://qiita.com/advent-calendar/2025/web3"><u>Web3 Advent Calendar 2025</u></a> 18日目の記事になります。2記事目の寄稿なんですが、1記事目はQiitaに直接書き、今回はこのParagraph(旧Mirrors.xyz)に書いてみます。一体どこがいいやら。</p><p>ついこの間まで5年以上にわたり同じプロダクトをメンテしていて<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://qiita.com/tomohiro-n/items/44c237c3ae5f96af4ecf">そのtipsをまとめたりした</a>んですが、今久しぶりに完全新規の開発をしていて、ハードルの下がり方がすごいのでシェアします。想定読者はEVMチェーン上(Ethereum, Base, Arbitrum, Polygon, ...)でサービス開発をしている、もしくはしようと思っているエンジニアです。別段超最新のstackでもないので、知っとるわ！　という方もいるかとは思いますが、そういう人は連絡ください。採用しますｗ</p><h1 id="h-stack" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">目的とStack</h1><p>果たしたい目的は以下を考えます:</p><ul><li><p>EVM上で動作するwebアプリを爆速で開発したい</p></li><li><p>フロントエンドもコントラクトも頻繁に実装が変わる中、それに対応するQA環境を爆速で整備したい</p></li></ul><p>EVM上で動作するwebアプリのシステム構成としては、</p><ol><li><p>フロントエンド</p></li><li><p>スマートコントラクト</p></li><li><p>オンチェーンデータをクエリできるAPI(The Graphなど)</p></li><li><p>必要であればバックエンドやデータベース</p></li></ol><p>だと思いますが、この記事では1, 2をメインでカバーし、3は軽く触れます。</p><p>Stack:</p><ul><li><p>フロントエンド: Next.js</p><ul><li><p>ホスティング: Vercel</p></li></ul></li><li><p>Solidity開発、デプロイ: Foundry</p></li><li><p>CI/CD: GitHub Actions</p></li><li><p>テストネット: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://tenderly.co/">Tenderly</a></p></li><li><p>レポジトリ・パッケージ: monorepo (<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://turborepo.com/">Turborepo</a>を採用しましたが何でも良い) , pnpm packages</p></li></ul><h1 id="h-" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">作ったプロセス</h1><ol><li><p>monorepoをそのままLLMが使えるIDEで開き、爆速開発する</p></li><li><p>GitHubに変更をpushする</p></li><li><p>GitHub Actionが走る。この時点ではVercelのデプロイは行われない</p></li><li><p>コントラクトに変更がある場合は5, ない場合、つまりフロントエンドだけの変更の場合は8に進む</p></li><li><p>新規のプライベートなテストネットが作成される</p></li><li><p>そのテストネットに必要なコントラクトがデプロイされる</p></li><li><p>デプロイされたコントラクトのアドレスがVercelの環境変数に記述される</p></li><li><p>Vercelのデプロイが走る</p></li><li><p>変更で入った新機能をテストできるQA環境のできあがり！</p></li></ol><h1 id="h-" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">主要プロセスの解説</h1><h2 id="h-monorepoide" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">monorepoをそのままIDEで開き、爆速開発する</h2><p>これはなんていうか別にEVM開発に限らず、従来のフロントエンド、バックエンド開発でも同じですが、複数のシステムをまたいでLLMにファイルをメンションし、「このコントラクトの変更をもとにこのUIの挙動をこう変更してくれよな！」と命令するのが圧倒的に楽だと言うだけの話です。僕はIDEはCursor、モデルはGPT 5.2, Gemini 3あたりを使っていますが何を使おうがこの点に関してはほぼ一緒かなと思います。<br>参考までにファイル構成は以下のようになります。</p><pre data-type="codeBlock" text="# 省略しているフォルダやファイルも多くあります
.github/ # GitHub Actions
  workflows/
apps/
  web/ # Next.js
    src/
      app/ # Next.js App Router
packages/
  contracts/ ## Foundryプロジェクトのtop
    broadcast/
    script/
    src/
    subgraph/
pnpm-workspace.yaml # pnpm packagesの設定ファイル
turbo.json # Turborepoの設定ファイル"><code># 省略しているフォルダやファイルも多くあります
.github/ # GitHub Actions
  workflows<span class="hljs-operator">/</span>
apps<span class="hljs-operator">/</span>
  web<span class="hljs-operator">/</span> # Next.js
    src<span class="hljs-operator">/</span>
      app<span class="hljs-operator">/</span> # Next.js App Router
packages<span class="hljs-operator">/</span>
  contracts<span class="hljs-operator">/</span> ## Foundryプロジェクトのtop
    broadcast<span class="hljs-operator">/</span>
    script<span class="hljs-operator">/</span>
    src<span class="hljs-operator">/</span>
    subgraph<span class="hljs-operator">/</span>
pnpm<span class="hljs-operator">-</span>workspace.yaml # pnpm packagesの設定ファイル
turbo.json # Turborepoの設定ファイル</code></pre><h2 id="h-github-actionvercel" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">GitHub Actionが走る。この時点でVercelデプロイは行われない</h2><p>コントラクトの変更やそれが動くテストネットのRPC URLなどを反映させたフロントエンドを自動でデプロイしたいので、push時点でのVercelデプロイはignore(キャンセル)させます。VercelはNext.jsアプリ直下に <code>vercel.json</code> を置くとそれを読んでくれます。</p><pre data-type="codeBlock" text="# apps/web/vercel.json
{
  &quot;$schema&quot;: &quot;https://openapi.vercel.sh/vercel.json&quot;,
  &quot;ignoreCommand&quot;: &quot;bash ./scripts/vercel-ignore.sh&quot;
}"><code># apps<span class="hljs-operator">/</span>web<span class="hljs-operator">/</span>vercel.json
{
  <span class="hljs-string">"$schema"</span>: <span class="hljs-string">"https://openapi.vercel.sh/vercel.json"</span>,
  <span class="hljs-string">"ignoreCommand"</span>: <span class="hljs-string">"bash ./scripts/vercel-ignore.sh"</span>
}</code></pre><p><code>scripts/vercel-ignore.sh</code> にはどういうときにgit pushだけでデプロイが動いてほしいか、ほしくないかを記述しました。例えば、本番環境やステージングは動いてほしいとか、この環境変数が立っているときはしてほしいとか、開発のステージや要件次第。</p><h2 id="h-5-8" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">コントラクトに変更がある場合は5, ない場合、つまりフロントエンドだけの変更の場合は8に進む</h2><p>GitHub Actionの設定でこんな感じで記述しました。まあこんなの要件を伝えればAIが書いてくれるんで、新規で作っても以下をコピペして「こういう風に変えたい」とか言っても数分で終わるでしょう。</p><pre data-type="codeBlock" text="# 例えば .github/workflows/qa-main.yml 
jobs:
  changes:
    if: github.event_name == 'workflow_dispatch' || vars.QA_MAIN_ENABLED == 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      contracts_changed: ${{ steps.diff.outputs.contracts_changed }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - id: diff
        run: |
          set -euo pipefail
          BEFORE=&quot;${{ github.event.before }}&quot;
          AFTER=&quot;${{ github.sha }}&quot;
          if [ -z &quot;$BEFORE&quot; ] || [ &quot;$BEFORE&quot; = &quot;0000000000000000000000000000000000000000&quot; ]; then
            echo &quot;contracts_changed=true&quot; &gt;&gt; &quot;$GITHUB_OUTPUT&quot;
            exit 0
          fi
          if git diff --name-only &quot;$BEFORE&quot; &quot;$AFTER&quot; | grep -q '^packages/contracts/'; then
            echo &quot;contracts_changed=true&quot; &gt;&gt; &quot;$GITHUB_OUTPUT&quot;
          else
            echo &quot;contracts_changed=false&quot; &gt;&gt; &quot;$GITHUB_OUTPUT&quot;
          fi"><code># 例えば .github/workflows<span class="hljs-operator">/</span>qa<span class="hljs-operator">-</span>main.yml 
jobs:
  changes:
    <span class="hljs-keyword">if</span>: github.event_name <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">'workflow_dispatch'</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> vars.QA_MAIN_ENABLED <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">'true'</span>
    runs<span class="hljs-operator">-</span>on: ubuntu<span class="hljs-operator">-</span>latest
    permissions:
      contents: read
    outputs:
      contracts_changed: ${{ steps.diff.outputs.contracts_changed }}
    steps:
      <span class="hljs-operator">-</span> uses: actions<span class="hljs-operator">/</span>checkout@v4
        with:
          fetch<span class="hljs-operator">-</span>depth: <span class="hljs-number">0</span>
      <span class="hljs-operator">-</span> id: diff
        run: <span class="hljs-operator">|</span>
          set <span class="hljs-operator">-</span>euo pipefail
          BEFORE<span class="hljs-operator">=</span><span class="hljs-string">"${{ github.event.before }}"</span>
          AFTER<span class="hljs-operator">=</span><span class="hljs-string">"${{ github.sha }}"</span>
          <span class="hljs-keyword">if</span> [ <span class="hljs-operator">-</span>z <span class="hljs-string">"$BEFORE"</span> ] <span class="hljs-operator">|</span><span class="hljs-operator">|</span> [ <span class="hljs-string">"$BEFORE"</span> <span class="hljs-operator">=</span> <span class="hljs-string">"0000000000000000000000000000000000000000"</span> ]; then
            echo <span class="hljs-string">"contracts_changed=true"</span> <span class="hljs-operator">&gt;</span><span class="hljs-operator">&gt;</span> <span class="hljs-string">"$GITHUB_OUTPUT"</span>
            exit <span class="hljs-number">0</span>
          fi
          <span class="hljs-keyword">if</span> git diff <span class="hljs-operator">-</span><span class="hljs-operator">-</span>name<span class="hljs-operator">-</span>only <span class="hljs-string">"$BEFORE"</span> <span class="hljs-string">"$AFTER"</span> <span class="hljs-operator">|</span> grep <span class="hljs-operator">-</span>q <span class="hljs-string">'^packages/contracts/'</span>; then
            echo <span class="hljs-string">"contracts_changed=true"</span> <span class="hljs-operator">&gt;</span><span class="hljs-operator">&gt;</span> <span class="hljs-string">"$GITHUB_OUTPUT"</span>
          <span class="hljs-keyword">else</span>
            echo <span class="hljs-string">"contracts_changed=false"</span> <span class="hljs-operator">&gt;</span><span class="hljs-operator">&gt;</span> <span class="hljs-string">"$GITHUB_OUTPUT"</span>
          fi</code></pre><p>これで <code>contracts_changed</code> が <code>true</code> ならコントラクト変更あり、 <code>false</code> ならコントラクト変更無しということでどのステップに進むかを指示するわけですね。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">新規のプライベートなテストネットが作成される</h2><p>ここからは<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://tenderly.co/virtual-testnets">TenderlyのVirtual TestNets</a>というプロダクトを使います。これは自分たち専用のテストネットを、既存のチェーンをforkする形で作れるプロダクトです。例えば「Base mainnetの最新blockをforkしたチェーン作って」みたいな感じです。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/cc1f3d55b1a616c2bcd5652020cd4c3bebc83f2cfdd6f65cee80647b27b301c3.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAKCAIAAABaL8vzAAAACXBIWXMAABYlAAAWJQFJUiTwAAACW0lEQVR4nJ2SQU8TQRTHh2brshl2t7N2O0zd2oWhlsaiSCslrYNZDbHt1iyyYUMWaKwJicd6MCp68AugYDDBEMOBRAzxTKI3TDAhXsDwWYg3zTJxqcaLvvwPb5L35r3/Lw+oakwUJQhlUZR4AoNQeuRuqQdACAUhKghRUZRUNaaqMQhl6aSCt0Ao8wIAIgBEBCEKocwruYCmxbMXBg3DpJQaRqrP7DdSfRifPUcYyy2bZta27eHhEUopQpqqxs6nzHS63zBMw0hROkApdZw7jjPp+7PVas1xJjOZbLhNMEDXEzdv3HIct9ls3W5MWtbE3Oy90li+mH306fmPKyPjR0ffDg4O2+0HCGnxeLxQKHrejOf5jFn1WiOfH3q5vLyy8mpt7c3e3pft7Q9TU1MARPg2wYBfiGCH3zMQQlWNi92oq0sAQfxmXxRFzkfqDsTbOcYTwoFCEwAhjVKKMSEkSUiSUprLXcS4l5Ckbdc9b8Z1p6vVmm03GBtHSNP1xKWhyxiTZNJIp810OmBFSPLqaIkxq1yuFAqjmUw2nx86RcSYNe16rut53gxj1sLCfdf1KpXrGxub+/tfd3c/r6+/3dp6v7j4zDBSGBNeUK81GLNarYV2+2G1Wp+fv/v4yVP+j2VNNBrOKaLQVCjuVxQlACIQyghpnGnnhfAuRVEQ0iCUdT2BMeEK60NEAx1+g3Mqlcb4c3Awp+uJPy7vrwrnKYrC81NE5fI1z/N9f77ZbPn+3NLSi52dj5ub71ZXXx8ffy8UioIQDTf6VwWIFEXptM8TbhPj3v/7NxzwEwKSjY0MbWfFAAAAAElFTkSuQmCC" nextheight="240" nextwidth="746" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">作成したチェーン情報例</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/647959bb3633ec5cdc370d4d05e8a8216bc430571b20add157f89a1d22f34b67.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAIAAACU62+bAAAACXBIWXMAABYlAAAWJQFJUiTwAAADNUlEQVR4nIVUYWvbVhQVqazwrEhWnl+UJ8eTlbzIsowSNUVF2HNVv6pV1bgumk2oU08kpHVRgstCKWTrSOmfKYzCCGz0y/phP6wfNhy1wYntVBzEFbqHe9995x4GYwxhvlAoYqycA2OsFIslVdUmgbHCvH377uzsr35/7/j4zWBwmCTDweAwDB83Gnc9j46D0vumaTEfPvzx5ct/STL89Omfz5///fjxz7Ozv3u9uNG4S+n9cfj+w2p1g9H1iqYRw6gSsq5pRFVXNW1N1yuWdXMSqqoxhBDXrSMk63pZ09YIIekbwrwkLV4BQkvMPer/9uvp2iqJ4/0o6uzs7Pb7ce/pM01blaRFCPPjQEhmEFrCGItiDoAszws8L4hiThRzV1LhBUHTVk2zijEmhKysjIaL0NLUbJgSguDRycnvjnP79euTJBkOj35penSyGTjWkqwoBQjzsrwsy1iWl79TAX6NliBEkiR9i2cTLGvjceuJYVSSZNjrxXF8UKs1eF6YSkNIZhSlYBgVSVpUlMIPxVKhUERIvq6COBoogDDP8wLHzXPc/KwTw5RAqU8pVVXt5/5ev783GBwSQq6bEiHEMCoYY9O0HMdtNO7I8vLlPHSJcHG75/1wGY5LP1PxpH/hOCGKOsfHb0zTiqLOXnzwU9SNos7R0StKfc9r7vbibnfnosMRwXXrUdRR1ZJlbVrWpmGYaUBGz7rjuLa9danCgiAAAEQxx7IZALIAgAzHsWyGZTMLgrAgTLS0admtVtswzMPkVa3WCILwHn0Qhtut1hNVLaU7AMcJplkNglBVS5T6lrVR//GO7wdBEAZBuLJSvDJfhGQGgCzLsqKYm5u7wXHzGY6bm7vBsiwA2ekL5DhuGD4ihPh+4Di3Xbfuec1bt5yZWvK8Zhzv67rRbkdhuN1uR93ujuc1Z940fy6hdEqpltJxXbeitVpdUQq2vWWaVV0v2/ZW6hrTCbZ9M4o6ul7uPX12cPAyjvdfDo4o9a+T97mkJY6b53lBkhYByH7HNWx7C2OFkHVdL6tqCWNlQrBjhDDcPj19T8j6i+fJbi9OXVnXy1OLfDUyWR45/oUbpCYyq8L/PHfU246QBGgAAAAASUVORK5CYII=" nextheight="736" nextwidth="376" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">チェーンごとの機能一覧。なんとなく察せるっしょ</figcaption></figure><p>TenderlyはGitHub Actionsを用意してくれているのでそれを使います。使い方例は以下、レポジトリは<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/Tenderly/vnet-github-action">ここ</a>です。</p><pre data-type="codeBlock" text="jobs:
  qa:
    steps:
      - name: Create Tenderly Virtual TestNet
        if: inputs.run_tenderly || inputs.run_foundry || (inputs.run_vercel &amp;&amp; inputs.vercel_update_env) # 走らせる条件。要件次第
        uses: Tenderly/vnet-github-action@v1.0.17
        with:
          mode: CD
          access_key: ${{ secrets.tenderly_access_token }}
          account_name: ${{ inputs.tenderly_account_name }}
          project_name: ${{ inputs.tenderly_project_name }}
          testnet_name: ${{ inputs.testnet_name }} # 特に何も入れないでもユニークな名前が割り当てられます
          network_id: |
            ${{ inputs.tenderly_network_id }}
          chain_id_prefix: ${{ inputs.chain_id_prefix }}
          state_sync: ${{ inputs.state_sync }}
          public_explorer: ${{ inputs.public_explorer }}"><code>jobs:
  qa:
    steps:
      <span class="hljs-operator">-</span> name: Create Tenderly Virtual TestNet
        <span class="hljs-keyword">if</span>: inputs.run_tenderly <span class="hljs-operator">|</span><span class="hljs-operator">|</span> inputs.run_foundry <span class="hljs-operator">|</span><span class="hljs-operator">|</span> (inputs.run_vercel <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> inputs.vercel_update_env) # 走らせる条件。要件次第
        uses: Tenderly<span class="hljs-operator">/</span>vnet<span class="hljs-operator">-</span>github<span class="hljs-operator">-</span>action@v1<span class="hljs-number">.0</span><span class="hljs-number">.17</span>
        with:
          mode: CD
          access_key: ${{ secrets.tenderly_access_token }}
          account_name: ${{ inputs.tenderly_account_name }}
          project_name: ${{ inputs.tenderly_project_name }}
          testnet_name: ${{ inputs.testnet_name }} # 特に何も入れないでもユニークな名前が割り当てられます
          network_id: <span class="hljs-operator">|</span>
            ${{ inputs.tenderly_network_id }}
          chain_id_prefix: ${{ inputs.chain_id_prefix }}
          state_sync: ${{ inputs.state_sync }}
          public_explorer: ${{ inputs.public_explorer }}</code></pre><p>これで自分たち専用の、どこかのチェーンをforkしたテストネットが作成されました。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">そのテストネットに必要なコントラクトがデプロイされる</h2><p>まずデプロイにはgasが必要なので、それを送ります。自分たちのテストネットでやりたい放題して大丈夫ですから、なんのトークンでも送れる機能があります。fork元のチェーンにあるトークンならなんでもいけます。下はシンプルにETHを送るときですね。 <code>tenderly_setBalance</code> という命令を使っているのが分かると思います。</p><pre data-type="codeBlock" text="jobs:
  qa:
    steps:
      ...

      - name: Fund deployer (Tenderly unlimited faucet)
        if: inputs.run_foundry
        env:
          ADMIN_RPC_URL: ${{ env.ADMIN_RPC_URL }}
          DEPLOYER_ADDRESS: ${{ inputs.deployer_address }}
          DEPLOYER_BALANCE_WEI_HEX: ${{ inputs.deployer_balance_wei_hex }}
        run: |
          set -euo pipefail
          [ -n &quot;${ADMIN_RPC_URL:-}&quot; ] &amp;&amp; echo &quot;::add-mask::$ADMIN_RPC_URL&quot;
          curl -sS &quot;${ADMIN_RPC_URL}&quot; \
            -X POST \
            -H &quot;Content-Type: application/json&quot; \
            -d '{
              &quot;jsonrpc&quot;: &quot;2.0&quot;,
              &quot;method&quot;: &quot;tenderly_setBalance&quot;,
              &quot;params&quot;: [[&quot;'&quot;${DEPLOYER_ADDRESS}&quot;'&quot;], &quot;'&quot;${DEPLOYER_BALANCE_WEI_HEX}&quot;'&quot;],
              &quot;id&quot;: 1
            }'"><code>jobs:
  qa:
    steps:
      ...

      <span class="hljs-operator">-</span> name: Fund deployer (Tenderly unlimited faucet)
        <span class="hljs-keyword">if</span>: inputs.run_foundry
        env:
          ADMIN_RPC_URL: ${{ env.ADMIN_RPC_URL }}
          DEPLOYER_ADDRESS: ${{ inputs.deployer_address }}
          DEPLOYER_BALANCE_WEI_HEX: ${{ inputs.deployer_balance_wei_hex }}
        run: <span class="hljs-operator">|</span>
          set <span class="hljs-operator">-</span>euo pipefail
          [ <span class="hljs-operator">-</span>n <span class="hljs-string">"${ADMIN_RPC_URL:-}"</span> ] <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> echo <span class="hljs-string">"::add-mask::$ADMIN_RPC_URL"</span>
          curl <span class="hljs-operator">-</span>sS <span class="hljs-string">"${ADMIN_RPC_URL}"</span> \
            <span class="hljs-operator">-</span>X POST \
            <span class="hljs-operator">-</span>H <span class="hljs-string">"Content-Type: application/json"</span> \
            <span class="hljs-operator">-</span>d <span class="hljs-string">'{
              "jsonrpc": "2.0",
              "method": "tenderly_setBalance",
              "params": [["'</span><span class="hljs-string">"${DEPLOYER_ADDRESS}"</span><span class="hljs-string">'"], "'</span><span class="hljs-string">"${DEPLOYER_BALANCE_WEI_HEX}"</span><span class="hljs-string">'"],
              "id": 1
            }'</span></code></pre><p>そしたらコントラクトをデプロイしましょう。デプロイするスクリプトは一つにまとめておいたほうが楽かなとは思いますが、複数実行してもいいでしょう。</p><pre data-type="codeBlock" text="jobs:
  qa:
    steps:
      ...

      - name: Install Foundry
        if: inputs.run_foundry
        uses: foundry-rs/foundry-toolchain@v1

      - name: Deploy contracts with Forge Script
        if: inputs.run_foundry
        working-directory: ${{ inputs.foundry_dir }}
        env:
          PRIVATE_KEY: ${{ inputs.deployer_private_key }} # 基本使って捨てるQA環境用なので、漏れてもいい前提の鍵を利用するのをおすすめします。漏れちゃいけない鍵の管理は各自考えましょう
          RPC_URL: ${{ env.RPC_URL }}
        run: |
          set -euo pipefail
          forge script script/${{ inputs.forge_script_file }} \
            --rpc-url &quot;$RPC_URL&quot; \
            --private-key &quot;$PRIVATE_KEY&quot; \
            --broadcast \
            -vvv"><code>jobs:
  qa:
    steps:
      ...

      <span class="hljs-operator">-</span> name: Install Foundry
        <span class="hljs-keyword">if</span>: inputs.run_foundry
        uses: foundry<span class="hljs-operator">-</span>rs<span class="hljs-operator">/</span>foundry<span class="hljs-operator">-</span>toolchain@v1

      <span class="hljs-operator">-</span> name: Deploy contracts with Forge Script
        <span class="hljs-keyword">if</span>: inputs.run_foundry
        working<span class="hljs-operator">-</span>directory: ${{ inputs.foundry_dir }}
        env:
          PRIVATE_KEY: ${{ inputs.deployer_private_key }} # 基本使って捨てるQA環境用なので、漏れてもいい前提の鍵を利用するのをおすすめします。漏れちゃいけない鍵の管理は各自考えましょう
          RPC_URL: ${{ env.RPC_URL }}
        run: <span class="hljs-operator">|</span>
          set <span class="hljs-operator">-</span>euo pipefail
          forge script script<span class="hljs-operator">/</span>${{ inputs.forge_script_file }} \
            <span class="hljs-operator">-</span><span class="hljs-operator">-</span>rpc<span class="hljs-operator">-</span>url <span class="hljs-string">"$RPC_URL"</span> \
            <span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-keyword">private</span><span class="hljs-operator">-</span>key <span class="hljs-string">"$PRIVATE_KEY"</span> \
            <span class="hljs-operator">-</span><span class="hljs-operator">-</span>broadcast \
            <span class="hljs-operator">-</span>vvv</code></pre><h2 id="h-vercel" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">デプロイされたコントラクトのアドレスがVercelの環境変数に記述される</h2><p><code>forge script</code> コマンドは <code>broadcast/{{Chain ID}}/{{スクリプトファイル名}}/run-latest.json</code>にデプロイ結果を記述するので、そこからデプロイされたコントラクトのアドレスを抜き出します。</p><pre data-type="codeBlock" text="jobs:
  qa:
    steps:
      ...

      - name: Extract deployed contract addresses
        if: inputs.run_foundry
        env:
          FOUNDRY_DIR: ${{ inputs.foundry_dir }}
          CHAIN_ID: ${{ env.CHAIN_ID }}
          FORGE_SCRIPT_FILE: ${{ inputs.forge_script_file }}
        run: |
          set -euo pipefail
          CONTRACTS_ENV_FILE=&quot;/tmp/contracts.env&quot;
          : &gt; &quot;$CONTRACTS_ENV_FILE&quot;

          bash ./scripts/ci/extract-foundry-address.sh &quot;${FOUNDRY_DIR}&quot; &quot;${CHAIN_ID}&quot; &quot;${FORGE_SCRIPT_FILE}&quot; &quot;${CONTRACTS_ENV_FILE}&quot;

          # Export for subsequent steps as environment variables.
          cat &quot;$CONTRACTS_ENV_FILE&quot; &gt;&gt; &quot;$GITHUB_ENV&quot;

          echo &quot;Extracted:&quot;
          cat &quot;$CONTRACTS_ENV_FILE&quot; || true"><code>jobs:
  qa:
    steps:
      ...

      <span class="hljs-operator">-</span> name: Extract deployed <span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">addresses</span>
        <span class="hljs-title"><span class="hljs-keyword">if</span></span>: <span class="hljs-title">inputs</span>.<span class="hljs-title">run_foundry</span>
        <span class="hljs-title">env</span>:
          <span class="hljs-title">FOUNDRY_DIR</span>: <span class="hljs-title">$</span></span>{{ inputs.foundry_dir }}
          CHAIN_ID: ${{ env.CHAIN_ID }}
          FORGE_SCRIPT_FILE: ${{ inputs.forge_script_file }}
        run: <span class="hljs-operator">|</span>
          set <span class="hljs-operator">-</span>euo pipefail
          CONTRACTS_ENV_FILE<span class="hljs-operator">=</span><span class="hljs-string">"/tmp/contracts.env"</span>
          : <span class="hljs-operator">&gt;</span> <span class="hljs-string">"$CONTRACTS_ENV_FILE"</span>

          bash ./scripts<span class="hljs-operator">/</span>ci<span class="hljs-operator">/</span>extract<span class="hljs-operator">-</span>foundry<span class="hljs-operator">-</span><span class="hljs-keyword">address</span>.sh <span class="hljs-string">"${FOUNDRY_DIR}"</span> <span class="hljs-string">"${CHAIN_ID}"</span> <span class="hljs-string">"${FORGE_SCRIPT_FILE}"</span> <span class="hljs-string">"${CONTRACTS_ENV_FILE}"</span>

          # Export <span class="hljs-keyword">for</span> subsequent steps <span class="hljs-keyword">as</span> environment variables.
          cat <span class="hljs-string">"$CONTRACTS_ENV_FILE"</span> <span class="hljs-operator">&gt;</span><span class="hljs-operator">&gt;</span> <span class="hljs-string">"$GITHUB_ENV"</span>

          echo <span class="hljs-string">"Extracted:"</span>
          cat <span class="hljs-string">"$CONTRACTS_ENV_FILE"</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> <span class="hljs-literal">true</span></code></pre><p><code>scripts/ci/extract-foundry-address.sh</code> で抜き出しているわけですが、このコードはちょっと長いので割愛します。まあAIに言えば秒で100行書いてくれますから。注意点としては、proxyパターンを使っているときに、例えば <code>ABCToken</code> をデプロイしたので <code>NEXT_PUBLIC_CONTRACT_ABCTOKEN</code> 環境変数を作成したいのに、 <code>NEXT_PUBLIC_ERC1967PROXY</code> なんてproxyコントラクト名の環境変数を作成されても困ります。なので使ってるパターンやproxyの種類に応じてスクリプトを調整しましょう。大丈夫です、まあAIに言えば略</p><p>さて、環境変数一覧ができあがりましたから、それを実際にVercelに反映しましょう。反映するのは</p><ul><li><p>作られたテストネットのRPC URL</p></li><li><p>作られたテストネットのChain ID</p></li><li><p>デプロイされたコントラクトのアドレス(複数)</p></li></ul><p>です。ちょっと長いのでまあ参考程度に。一点気をつけるところとしては、 <code>FORCE_BUILD_KEY</code> というのをいじっていますが、これは上の <code>vercel.json</code> でデプロイする条件を厳しくしているので、その条件をこの後通過するよう(e.g. <code>./scripts/vercel-ignore.sh</code> が <code>1</code> を返すように) <code>1</code> <code>true</code> などの値に設定しています。</p><pre data-type="codeBlock" text="jobs:
  qa:
    steps:
      ...

      - name: Upsert Vercel env (RPC/ChainId/Contracts + force-build=1)
        if: inputs.run_vercel &amp;&amp; inputs.vercel_update_env
        env:
          VERCEL_TOKEN: ${{ secrets.vercel_token }}
          VERCEL_PROJECT: ${{ secrets.vercel_project }}

          VERCEL_ENV_TARGET: ${{ inputs.vercel_env_target }}
          FORCE_BUILD_KEY: ${{ inputs.force_build_key }}
          NEXT_PUBLIC_QA_RPC_URL_KEY: ${{ inputs.next_public_rpc_key }}
          NEXT_PUBLIC_QA_CHAIN_ID_KEY: ${{ inputs.next_public_chain_key }}

          RPC_URL: ${{ env.RPC_URL }}
          CHAIN_ID: ${{ env.CHAIN_ID }}
        run: |
          set -euo pipefail

          if [ -z &quot;${VERCEL_TOKEN:-}&quot; ] || [ -z &quot;${VERCEL_PROJECT:-}&quot; ]; then
            echo &quot;ERROR: missing Vercel secrets (vercel_token/vercel_project).&quot; &gt;&amp;2
            exit 1
          fi

          if ! command -v jq &gt;/dev/null 2&gt;&amp;1; then
            sudo apt-get update -y
            sudo apt-get install -y jq
          fi

          # Read contract envs from the persisted file (preferred) or from current environment (fallback).
          CONTRACT_KVS=&quot;$(cat /tmp/contracts.env 2&gt;/dev/null | grep '^CONTRACT_' || true)&quot;
          if [ -z &quot;$CONTRACT_KVS&quot; ]; then
            CONTRACT_KVS=&quot;$(env | grep '^CONTRACT_' || true)&quot;
          fi

          payload=$(jq -n \
            --arg target &quot;${VERCEL_ENV_TARGET}&quot; \
            --arg forceKey &quot;${FORCE_BUILD_KEY}&quot; \
            --arg rpcKey &quot;${NEXT_PUBLIC_QA_RPC_URL_KEY}&quot; \
            --arg chainKey &quot;${NEXT_PUBLIC_QA_CHAIN_ID_KEY}&quot; \
            --arg rpcVal &quot;${RPC_URL}&quot; \
            --arg chainVal &quot;${CHAIN_ID}&quot; \
            '[
              {key:$forceKey, value:&quot;1&quot;, type:&quot;encrypted&quot;, target:[$target]},
              {key:$rpcKey,   value:$rpcVal, type:&quot;encrypted&quot;, target:[$target]},
              {key:$chainKey, value:$chainVal, type:&quot;encrypted&quot;, target:[$target]}
            ]'
          )

          while IFS= read -r line; do
            [ -z &quot;$line&quot; ] &amp;&amp; continue
            key=&quot;${line%%=*}&quot;
            val=&quot;${line#*=}&quot;
            payload=$(jq \
              --arg target &quot;${VERCEL_ENV_TARGET}&quot; \
              --arg k &quot;NEXT_PUBLIC_${key}&quot; \
              --arg v &quot;${val}&quot; \
              '. + [{key:$k, value:$v, type:&quot;encrypted&quot;, target:[$target]}]' \
              &lt;&lt;&lt;&quot;$payload&quot;
            )
          done &lt;&lt;&lt;&quot;$CONTRACT_KVS&quot;

          echo &quot;$payload&quot; &gt; /tmp/vercel-env-payload.json

          curl -sS -X POST &quot;https://api.vercel.com/v10/projects/${VERCEL_PROJECT}/env?upsert=true&quot; \
            -H &quot;Authorization: Bearer ${VERCEL_TOKEN}&quot; \
            -H &quot;Content-Type: application/json&quot; \
            --data-binary @/tmp/vercel-env-payload.json"><code>jobs:
  qa:
    steps:
      ...

      <span class="hljs-operator">-</span> name: Upsert Vercel env (RPC<span class="hljs-operator">/</span>ChainId<span class="hljs-operator">/</span>Contracts <span class="hljs-operator">+</span> force<span class="hljs-operator">-</span>build<span class="hljs-operator">=</span><span class="hljs-number">1</span>)
        <span class="hljs-keyword">if</span>: inputs.run_vercel <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> inputs.vercel_update_env
        env:
          VERCEL_TOKEN: ${{ secrets.vercel_token }}
          VERCEL_PROJECT: ${{ secrets.vercel_project }}

          VERCEL_ENV_TARGET: ${{ inputs.vercel_env_target }}
          FORCE_BUILD_KEY: ${{ inputs.force_build_key }}
          NEXT_PUBLIC_QA_RPC_URL_KEY: ${{ inputs.next_public_rpc_key }}
          NEXT_PUBLIC_QA_CHAIN_ID_KEY: ${{ inputs.next_public_chain_key }}

          RPC_URL: ${{ env.RPC_URL }}
          CHAIN_ID: ${{ env.CHAIN_ID }}
        run: <span class="hljs-operator">|</span>
          set <span class="hljs-operator">-</span>euo pipefail

          <span class="hljs-keyword">if</span> [ <span class="hljs-operator">-</span>z <span class="hljs-string">"${VERCEL_TOKEN:-}"</span> ] <span class="hljs-operator">|</span><span class="hljs-operator">|</span> [ <span class="hljs-operator">-</span>z <span class="hljs-string">"${VERCEL_PROJECT:-}"</span> ]; then
            echo <span class="hljs-string">"ERROR: missing Vercel secrets (vercel_token/vercel_project)."</span> <span class="hljs-operator">&gt;</span><span class="hljs-operator">&amp;</span><span class="hljs-number">2</span>
            exit <span class="hljs-number">1</span>
          fi

          <span class="hljs-keyword">if</span> <span class="hljs-operator">!</span> command <span class="hljs-operator">-</span>v jq <span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>dev<span class="hljs-operator">/</span>null <span class="hljs-number">2</span><span class="hljs-operator">&gt;</span><span class="hljs-operator">&amp;</span><span class="hljs-number">1</span>; then
            sudo apt<span class="hljs-operator">-</span>get update <span class="hljs-operator">-</span>y
            sudo apt<span class="hljs-operator">-</span>get install <span class="hljs-operator">-</span>y jq
          fi

          # Read <span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">envs</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-title">the</span> <span class="hljs-title">persisted</span> <span class="hljs-title">file</span> (<span class="hljs-params">preferred</span>) <span class="hljs-title">or</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-title">current</span> <span class="hljs-title">environment</span> (<span class="hljs-params"><span class="hljs-keyword">fallback</span></span>).
          <span class="hljs-title">CONTRACT_KVS</span>="<span class="hljs-title">$</span>(<span class="hljs-params">cat /tmp/contracts.env <span class="hljs-number">2</span>&gt;/dev/null | grep <span class="hljs-string">'^CONTRACT_'</span> || <span class="hljs-literal">true</span></span>)"
          <span class="hljs-title"><span class="hljs-keyword">if</span></span> [ -<span class="hljs-title">z</span> "<span class="hljs-title">$CONTRACT_KVS</span>" ]; <span class="hljs-title">then</span>
            <span class="hljs-title">CONTRACT_KVS</span>="<span class="hljs-title">$</span>(<span class="hljs-params">env | grep <span class="hljs-string">'^CONTRACT_'</span> || <span class="hljs-literal">true</span></span>)"
          <span class="hljs-title">fi</span>

          <span class="hljs-title">payload</span>=<span class="hljs-title">$</span>(<span class="hljs-params">jq -n \
            --arg target <span class="hljs-string">"${VERCEL_ENV_TARGET}"</span> \
            --arg forceKey <span class="hljs-string">"${FORCE_BUILD_KEY}"</span> \
            --arg rpcKey <span class="hljs-string">"${NEXT_PUBLIC_QA_RPC_URL_KEY}"</span> \
            --arg chainKey <span class="hljs-string">"${NEXT_PUBLIC_QA_CHAIN_ID_KEY}"</span> \
            --arg rpcVal <span class="hljs-string">"${RPC_URL}"</span> \
            --arg chainVal <span class="hljs-string">"${CHAIN_ID}"</span> \
            <span class="hljs-string">'[
              {key:$forceKey, value:"1", type:"encrypted", target:[$target]},
              {key:$rpcKey,   value:$rpcVal, type:"encrypted", target:[$target]},
              {key:$chainKey, value:$chainVal, type:"encrypted", target:[$target]}
            ]'</span>
          </span>)

          <span class="hljs-title"><span class="hljs-keyword">while</span></span> <span class="hljs-title">IFS</span>= <span class="hljs-title">read</span> -<span class="hljs-title">r</span> <span class="hljs-title">line</span>; <span class="hljs-title">do</span>
            [ -<span class="hljs-title">z</span> "<span class="hljs-title">$line</span>" ] &amp;&amp; <span class="hljs-title"><span class="hljs-keyword">continue</span></span>
            <span class="hljs-title">key</span>="<span class="hljs-title">$</span></span>{line<span class="hljs-operator">%</span><span class="hljs-operator">%</span><span class="hljs-operator">=</span><span class="hljs-operator">*</span>}<span class="hljs-string">"
            val="</span>${line#<span class="hljs-operator">*</span><span class="hljs-operator">=</span>}<span class="hljs-string">"
            payload=$(jq \
              --arg target "</span>${VERCEL_ENV_TARGET}<span class="hljs-string">" \
              --arg k "</span>NEXT_PUBLIC_${key}<span class="hljs-string">" \
              --arg v "</span>${val}<span class="hljs-string">" \
              '. + [{key:$k, value:$v, type:"</span>encrypted<span class="hljs-string">", target:[$target]}]' \
              &lt;&lt;&lt;"</span>$payload<span class="hljs-string">"
            )
          done &lt;&lt;&lt;"</span>$CONTRACT_KVS<span class="hljs-string">"

          echo "</span>$payload<span class="hljs-string">" &gt; /tmp/vercel-env-payload.json

          curl -sS -X POST "</span>https:<span class="hljs-comment">//api.vercel.com/v10/projects/${VERCEL_PROJECT}/env?upsert=true" \</span>
            <span class="hljs-operator">-</span>H <span class="hljs-string">"Authorization: Bearer ${VERCEL_TOKEN}"</span> \
            <span class="hljs-operator">-</span>H <span class="hljs-string">"Content-Type: application/json"</span> \
            <span class="hljs-operator">-</span><span class="hljs-operator">-</span>data<span class="hljs-operator">-</span>binary @<span class="hljs-operator">/</span>tmp<span class="hljs-operator">/</span>vercel<span class="hljs-operator">-</span>env<span class="hljs-operator">-</span>payload.json</code></pre><h2 id="h-vercel" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Vercelのデプロイが走る</h2><p>Vercelにはgit pushやVercel CLI/UIでの手動デプロイの他に、Deploy Hookを呼ぶことでデプロイするという方法が用意されているので、それを使います。 <code>Settings → Git</code>にあります。</p><pre data-type="codeBlock" text="      - name: Trigger Vercel Deploy Hook
        if: inputs.run_vercel
        env:
          HOOK: ${{ secrets.vercel_deploy_hook_url }}
        run: |
          set -euo pipefail
          if [ -z &quot;${HOOK:-}&quot; ]; then
            echo &quot;ERROR: missing vercel_deploy_hook_url secret.&quot; &gt;&amp;2
            exit 1
          fi
          curl -sS -X POST &quot;$HOOK&quot;"><code>      <span class="hljs-operator">-</span> name: Trigger Vercel Deploy Hook
        <span class="hljs-keyword">if</span>: inputs.run_vercel
        env:
          HOOK: ${{ secrets.vercel_deploy_hook_url }}
        run: <span class="hljs-operator">|</span>
          set <span class="hljs-operator">-</span>euo pipefail
          <span class="hljs-keyword">if</span> [ <span class="hljs-operator">-</span>z <span class="hljs-string">"${HOOK:-}"</span> ]; then
            echo <span class="hljs-string">"ERROR: missing vercel_deploy_hook_url secret."</span> <span class="hljs-operator">&gt;</span><span class="hljs-operator">&amp;</span><span class="hljs-number">2</span>
            exit <span class="hljs-number">1</span>
          fi
          curl <span class="hljs-operator">-</span>sS <span class="hljs-operator">-</span>X POST <span class="hljs-string">"$HOOK"</span></code></pre><p>これでデプロイされたNext.jsアプリは新しく作られた専用テストネットを向いていて、新規追加や変更されたコントラクトにアクセスできています。一度仕組み化してしまえば、Git pushだけで全てが起こります。</p><p>一応、フロントエンド上の設定例は以下です。</p><pre data-type="codeBlock" text="import type { Chain } from 'viem'
...

  const rpcUrl = process.env.NEXT_PUBLIC_QA_RPC_URL
  const chainIdRaw = process.env.NEXT_PUBLIC_QA_CHAIN_ID
  if (!rpcUrl || !chainIdRaw) return null

  const id = Number(chainIdRaw)
  if (!Number.isFinite(id) || id &lt;= 0) return null

  const chain: Chain = {
    id,
    name: 'Tenderly QA',
    nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
    rpcUrls: {
      default: { http: [rpcUrl] },
      public: { http: [rpcUrl] },
    },
    testnet: true,
  }"><code><span class="hljs-keyword">import</span> <span class="hljs-title"><span class="hljs-keyword">type</span></span> { <span class="hljs-title">Chain</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'viem'</span>
...

  <span class="hljs-title">const</span> <span class="hljs-title">rpcUrl</span> <span class="hljs-operator">=</span> <span class="hljs-title">process</span>.<span class="hljs-title">env</span>.<span class="hljs-title">NEXT_PUBLIC_QA_RPC_URL</span>
  <span class="hljs-title">const</span> <span class="hljs-title">chainIdRaw</span> <span class="hljs-operator">=</span> <span class="hljs-title">process</span>.<span class="hljs-title">env</span>.<span class="hljs-title">NEXT_PUBLIC_QA_CHAIN_ID</span>
  <span class="hljs-title"><span class="hljs-keyword">if</span></span> (<span class="hljs-operator">!</span><span class="hljs-title">rpcUrl</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> <span class="hljs-operator">!</span><span class="hljs-title">chainIdRaw</span>) <span class="hljs-title"><span class="hljs-keyword">return</span></span> <span class="hljs-title">null</span>

  <span class="hljs-title">const</span> <span class="hljs-title">id</span> <span class="hljs-operator">=</span> <span class="hljs-title">Number</span>(<span class="hljs-title">chainIdRaw</span>)
  <span class="hljs-title"><span class="hljs-keyword">if</span></span> (<span class="hljs-operator">!</span><span class="hljs-title">Number</span>.<span class="hljs-title">isFinite</span>(<span class="hljs-title">id</span>) <span class="hljs-operator">|</span><span class="hljs-operator">|</span> <span class="hljs-title">id</span> <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> 0) <span class="hljs-title"><span class="hljs-keyword">return</span></span> <span class="hljs-title">null</span>

  <span class="hljs-title">const</span> <span class="hljs-title">chain</span>: <span class="hljs-title">Chain</span> <span class="hljs-operator">=</span> {
    <span class="hljs-title">id</span>,
    <span class="hljs-title">name</span>: <span class="hljs-string">'Tenderly QA'</span>,
    <span class="hljs-title">nativeCurrency</span>: { <span class="hljs-title">name</span>: <span class="hljs-string">'Ether'</span>, <span class="hljs-title">symbol</span>: <span class="hljs-string">'ETH'</span>, <span class="hljs-title">decimals</span>: 18 },
    <span class="hljs-title">rpcUrls</span>: {
      <span class="hljs-title">default</span>: { <span class="hljs-title">http</span>: [<span class="hljs-title">rpcUrl</span>] },
      <span class="hljs-title"><span class="hljs-keyword">public</span></span>: { <span class="hljs-title">http</span>: [<span class="hljs-title">rpcUrl</span>] },
    },
    <span class="hljs-title">testnet</span>: <span class="hljs-title"><span class="hljs-literal">true</span></span>,
  }</code></pre><h2 id="h-the-graph" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">The Graph連携</h2><p>詳細は割愛しますが、その新規テストネット上で動くsubgraphのデプロイも自動化で来ます。ここに<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.tenderly.co/virtual-testnets/develop/thegraph">公式ドキュメント</a>があるのでどうぞ！</p><hr><p>以上、Ethereum開発が楽になった話でした。</p><p>余談ですが、この仕組みの構築にかかった時間は数時間でした。数時間でこれが達成できる理由は言わずもがなですが、</p><ol><li><p>Tenderly, Vercel, Foundryなど素晴らしいツールが揃っていること</p></li><li><p>AIがコードをすぐに書いてくれること</p></li><li><p>自分がAIに適切な指示ができること</p></li></ol><p>です。適切な指示にはやはり経験が物を言います。できるかどうかも見当がつかないことを指示したり質問するのは人間には難しいためです。こういう記事を読んで、新しくEthereum開発の世界に入ってくる人の知識・経験の足しになり、サービスを開発していく助けになれば嬉しいです。</p><p>最後に、TanéではEthereumエコシステム上でサービスを作る仲間を募集しています。興味のある方はXのDMあたりで連絡ください。よろしくお願いします！</p><br>]]></content:encoded>
            <author>0xtomo@newsletter.paragraph.com (0xtomo)</author>
            <category>tenderly</category>
            <category>japanese</category>
            <category>ethereum</category>
            <category>solidity</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/96eda450d64bdd590c63ecab49f138f01c2e27262de34399634e1bf1e7992082.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[EIP-7702凄すぎ説]]></title>
            <link>https://paragraph.com/@0xtomo/eip-7702</link>
            <guid>rM9ZOpPU6HbF0gWm8OHt</guid>
            <pubDate>Wed, 30 Apr 2025 17:56:10 GMT</pubDate>
            <description><![CDATA[こんにちは。この記事は以下の続きのような感じです。 https://mirror.xyz/0xtomo.eth/uMrMMzPFbK2gEkusMNZ66qQQbQr9LjCNWeBd2U5tnBM 前の記事で、EIP-7702はブロックチェーン玄人にとっては有力なオプションで、既存または新規のEOAに code をセットしてAccount Abstractionを含むコントラクトウォレット(e.g. smart account)のメリットを享受するといい、しかしブロックチェーンにもセキュリティにも疎い初心者ユーザには使いづらいのではないか? 引用すると実際にこのユーザ群にとって良い選択肢である一方で、無知なユーザに必要以上の権限委譲をさせてしまう危険性を孕んでいると感じました。ウォレットやdAppの提供者は、ユーザのEOAに「この code をセットしませんか」と促すことができます。その code はユーザが理解していない副作用を持つかもしれません。また、セットできる code は一つなので、ウォレット提供者はともかくdAppがそれをセットするのは、他のdAppも違う code...]]></description>
            <content:encoded><![CDATA[<p>こんにちは。この記事は以下の続きのような感じです。</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/0xtomo.eth/uMrMMzPFbK2gEkusMNZ66qQQbQr9LjCNWeBd2U5tnBM">https://mirror.xyz/0xtomo.eth/uMrMMzPFbK2gEkusMNZ66qQQbQr9LjCNWeBd2U5tnBM</a></p><p>前の記事で、<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md">EIP-7702</a>はブロックチェーン玄人にとっては有力なオプションで、既存または新規のEOAに <code>code</code> をセットしてAccount Abstractionを含むコントラクトウォレット(e.g. smart account)のメリットを享受するといい、しかしブロックチェーンにもセキュリティにも疎い初心者ユーザには使いづらいのではないか? 引用すると</p><blockquote><p>実際にこのユーザ群にとって良い選択肢である一方で、無知なユーザに必要以上の権限委譲をさせてしまう危険性を孕んでいると感じました。ウォレットやdAppの提供者は、ユーザのEOAに「この <code>code</code> をセットしませんか」と促すことができます。その <code>code</code> はユーザが理解していない副作用を持つかもしれません。また、セットできる <code>code</code> は一つなので、ウォレット提供者はともかくdAppがそれをセットするのは、他のdAppも違う <code>code</code> をセットさせたいのではないか? その <code>code</code> にユーザが乗り換えようとしたとき、最初の <code>code</code> とデータに互換性はあるのか?</p></blockquote><p>のような懸念を書きました。しかし、さらに考えを進めたり最近のサイトをいくつか見ているうちに、思った以上に簡単で、セキュアで、コストも低いウォレット管理が初心者にも提供できるのではないかと思うに至りました。もはや <code>Smarter EOA</code> という標題を超えて、EOAを全く意識しないことも可能そうです。例として、使う技術は以下が考えられます。</p><ul><li><p>EIP-7702</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://webauthn.io/">WebAuthn</a></p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://fidoalliance.org/passkeys/">Passkey</a></p></li><li><p>ES256, P-256( <code>secp256r1</code> )</p></li></ul></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md">RIP-7212</a></p></li><li><p>( <code>&lt;iframe&gt;</code> )</p></li></ul><p>上記の殆どは以下2つのサイトで使用されています。</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gelato-eip-7702-demo.vercel.app/">https://gelato-eip-7702-demo.vercel.app/</a></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eip7702.rath.fi/">https://eip7702.rath.fi/</a></p><p>下のサイトはほぼ間違いなく上のサイトのコード(<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/gelatodigital/gelato-eip-7702-demo">ソースコードリンク</a>)を参考にして作られていますが、まず上のサイトは以下のログイン(Connect Wallet)、トランザクション体験を提供しています。</p><ol><li><p>ランダムなEthereumの秘密鍵( <code>secp256k1 ECDSA</code>)をフロントエンドで生成。EOAアドレスAが導出される</p></li><li><p>EIP-7702で、そのアドレスAの <code>code</code> に、既にデプロイされているコントラクトウォレット実装をセット</p></li><li><p>Passkey + WebAuthnを使って、P-256( <code>secp256r1 ECDSA</code> )の秘密鍵、公開鍵(以下 <code>es256PubKey</code> )を生成。256の後が <code>k</code> か <code>r</code> かが上と異なることに注意。秘密鍵自体は例えばMacOSで言うとKeychainに保存され、値を見ることはできないが任意のメッセージに署名することができる。コントラクトウォレットの初期化プロセスとして <code>es256PubKey</code> に他のコントラクト実行の権限を与える。コードは以下の <code>authorize</code> <code>execute</code> 関数を参照。RIP-7212は、 以下の <code>P256.verify(digest, signature, key.publicKey)</code> の、この実行をしていいかを検証する部分を安価にするための仕組み</p><ol><li><p>背景として、P-256/ES256による署名はPasskeyのベースであるFIDO2/WebAuthnのデフォルト署名形式であり、Ethereumにおけるデフォルト署名形式と互換性がありません</p></li></ol></li></ol><pre data-type="codeBlock" text="    /// @notice Authorizes a new public key.
    /// @param publicKey - The public key to authorize.
    /// @param expiry - The Unix timestamp at which the key expires.
    function authorize(
        ECDSA.PublicKey calldata publicKey,
        uint256 expiry
    ) public returns (uint32 keyIndex) {
        if (msg.sender != address(this)) revert InvalidAuthority();

        Key memory key = Key({
            authorized: true,
            expiry: expiry,
            publicKey: publicKey
        });
        keys.push(key);
        emit Authorised();
        return uint32(keys.length - 1);
    }
"><code>    <span class="hljs-comment">/// @notice Authorizes a new public key.</span>
    <span class="hljs-comment">/// @param publicKey - The public key to authorize.</span>
    <span class="hljs-comment">/// @param expiry - The Unix timestamp at which the key expires.</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">authorize</span>(<span class="hljs-params">
        ECDSA.PublicKey <span class="hljs-keyword">calldata</span> publicKey,
        <span class="hljs-keyword">uint256</span> expiry
    </span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint32</span> keyIndex</span>) </span>{
        <span class="hljs-keyword">if</span> (<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span> <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)) <span class="hljs-keyword">revert</span> InvalidAuthority();

        Key <span class="hljs-keyword">memory</span> key <span class="hljs-operator">=</span> Key({
            authorized: <span class="hljs-literal">true</span>,
            expiry: expiry,
            publicKey: publicKey
        });
        keys.<span class="hljs-built_in">push</span>(key);
        <span class="hljs-keyword">emit</span> Authorised();
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">uint32</span>(keys.<span class="hljs-built_in">length</span> <span class="hljs-operator">-</span> <span class="hljs-number">1</span>);
    }
</code></pre><pre data-type="codeBlock" text="    // /// @notice Executes a set of calls on behalf of the Authority, provided a P256 signature over the calls and a public key index.
    // /// @param calls - The calls to execute.
    // /// @param signature - The P256 signature over the calls: `p256.sign(keccak256(nonce ‖ calls))`.
    // /// @param keyIndex - The index of the authorized public key to use.
    // /// @param prehash - Whether to SHA-256 hash the digest.
    function execute(
        bytes memory calls,
        ECDSA.Signature memory signature,
        uint32 keyIndex,
        bool prehash
    ) public {
        bytes32 digest = keccak256(abi.encodePacked(nonce++, calls));
        if (prehash) digest = sha256(abi.encodePacked(digest));

        Key memory key = keys[keyIndex];
        if (!key.authorized) revert KeyNotAuthorized();
        if (key.expiry &gt; 0 &amp;&amp; key.expiry &lt; block.timestamp) revert KeyExpired();

        if (!P256.verify(digest, signature, key.publicKey)) {
            revert InvalidSignature();
        }
        emit Executed();
        multiSend(calls);
    }
"><code>    <span class="hljs-comment">// /// @notice Executes a set of calls on behalf of the Authority, provided a P256 signature over the calls and a public key index.</span>
    <span class="hljs-comment">// /// @param calls - The calls to execute.</span>
    <span class="hljs-comment">// /// @param signature - The P256 signature over the calls: `p256.sign(keccak256(nonce ‖ calls))`.</span>
    <span class="hljs-comment">// /// @param keyIndex - The index of the authorized public key to use.</span>
    <span class="hljs-comment">// /// @param prehash - Whether to SHA-256 hash the digest.</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">execute</span>(<span class="hljs-params">
        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> calls,
        ECDSA.Signature <span class="hljs-keyword">memory</span> signature,
        <span class="hljs-keyword">uint32</span> keyIndex,
        <span class="hljs-keyword">bool</span> prehash
    </span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-keyword">bytes32</span> digest <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(nonce<span class="hljs-operator">+</span><span class="hljs-operator">+</span>, calls));
        <span class="hljs-keyword">if</span> (prehash) digest <span class="hljs-operator">=</span> <span class="hljs-built_in">sha256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(digest));

        Key <span class="hljs-keyword">memory</span> key <span class="hljs-operator">=</span> keys[keyIndex];
        <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>key.authorized) <span class="hljs-keyword">revert</span> KeyNotAuthorized();
        <span class="hljs-keyword">if</span> (key.expiry <span class="hljs-operator">></span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> key.expiry <span class="hljs-operator">&#x3C;</span> <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>) <span class="hljs-keyword">revert</span> KeyExpired();

        <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>P256.verify(digest, signature, key.publicKey)) {
            <span class="hljs-keyword">revert</span> InvalidSignature();
        }
        <span class="hljs-keyword">emit</span> Executed();
        multiSend(calls);
    }
</code></pre><p>この仕組みが凄いのは、ステップ1で作られたランダムなアドレスAの秘密鍵が、ステップ3終了時点でもう破棄可能で、これ以降、そのウォレットを使ってなんらかのトランザクションを起こしたいときにはEOAを気にすることなくPasskeyによる認証をすれば良い。多くのデバイスでは指紋認証や顔認証など手軽さ・簡単さは満点、セキュリティについては少なくとも鍵の保管場所という意味では十分な強度を手に入れていることです。</p><p>しかしながら、下のサイトについては上のようにオンチェーンでのES256/P-256署名検証はされていません(<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://sepolia.etherscan.io/tx/0x35df9cbb8c34c6dedbde613e599568d989386e71d280c4d78b0ec2af98a75c00">トランザクション例</a>)。何故かと言うと下のサイトはEthereum Sepoliaテストネットで動いており、そこではRIP-7212は利用できず、オンチェーンでの検証はコスト大幅高に繋がるためです。このプロポーザルは元々EIP-7212、つまりEthereumを含む殆ど全てのEVMチェーンにて利用できることを想定して提案されたようですが、途中でL1に比べ変更が容易であるRollupチェーンでの利用を目指すRIPに移行したようです。</p><ul><li><p><code>secp256k1</code>(Ethereum標準)の署名からアドレスを復元するコストは3,000gas以下程度</p></li><li><p><code>RIP-7212</code> を用いたP-256( <code>secp256r1</code> )のそれは数千~数万 gas程度</p></li><li><p><code>secp256r1</code> の検証をSolidityで実装した場合: 数百万gas程度</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://pay.daimo.com/">Daimo</a>プロジェクトによる<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/daimo-eth/p256-verifier?tab=readme-ov-file">実装</a>が33万gasを達成したとされたが長い間更新されていない……?</p></li></ul></li></ul><p><code>RIP-7212</code> もしくは他のなんらかの手段(例えばオフチェーンでの検証 + ZKなど……?)で安価な検証ができたとすると、指紋認証だけで、webブラウザ上でウォレットを作成してその後も認証・署名できます。</p><p>他の小さな問題を解決するためにinline frame ( <code>&lt;iframe&gt;</code> )あたりが多用されていくかと思います。これは新しい仕組みでもなんでもなく、Google AdsenseのバナーやX(Twitter)のポストを表示したりするのに使われている、他のドメインのHTMLを埋め込むためのものです。これが何故有効かというと、WebAuthnをユーザの認証に使うにあたりサービスが利用するID(Relying Party ID, RP ID)はサービスのホスト名(e.g. <code>app.uniswap.org</code>)を利用する<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://webauthn.wtf/how-it-works/relying-party">決まり</a>になっており、サービスをまたいで(e.g.<code>someapp.com</code> <code>anotherapp.com</code> 間で)同じ鍵、ひいては同じウォレットの認証をすることに支障が出てしまうためです。そこで、iframeによるウォレットの仕組みを提供しているドメインのHTMLを埋め込むことで、サービス間で同じウォレットを利用することができるでしょう。 勿論、逆にあるサービスは別のコントラクトウォレット実装を使う必要があることを理由として、iframeを使わず自分たちのドメインによるWebAuthn認証を好むかもしれません。テクニカルな知識を持たないユーザにどう危ないサイトを警告するのか、危ないサイトが作られることを防止するのか(例としては、ウォレット提供側はサービスのドメインをホワイトリスト制にしたり)というセキュリティ上の課題はあるものの、便利なUXを提供する手段が増えたのは単純に良いことかと思います。</p><p>まだ情報がかなり少ないですが、<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://porto.sh/">Porto</a>などはこの記事に挙げた仕組みの多くを活用していくような記述が見られます。</p><hr><p>このように、他のいくつかのEIP, RIPやPasskeyなどのWebの先進技術と組み合わせることで、EIP-7702は非常に多くのユーザに対しウォレットUXの改善をもたらし得ると感じました。コストの関係でまずはL2での利用が目立つかも知れませんが、L1も追いついていくでしょう。楽しみですね！</p>]]></content:encoded>
            <author>0xtomo@newsletter.paragraph.com (0xtomo)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/e7b219dffb171d8c110a5b831bf8d9d0c85caca318cd976c140f7cedb86b50a0.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Ethereum Wallet UX in April 2025]]></title>
            <link>https://paragraph.com/@0xtomo/ethereum-wallet-ux-in-april-2025</link>
            <guid>MWLD1fCNEmmX93jonrCm</guid>
            <pubDate>Wed, 09 Apr 2025 13:35:04 GMT</pubDate>
            <description><![CDATA[ウォレットやトランザクションについて考えることが職業柄多く、最近特に発展してきたなと思うのでまとめておきます。内容の一部はEthereum界隈の人全般向け、一部はその中でもエンジニア向けになっています。最近推しのウォレット、ウォレット関連のEIP、玄人や素人は何をつかうべきなのか? みたいな章構成です。Who I amCrypto、特にEthereum界隈に2018年ぐらいからいる。はじめて参加したDevconは大阪。Bitcoinを持ち始めたのは2017年1月Startbahnという会社のCTOをやっており、5年ほど前からマスに向けたweb3サービスを提供しているAzuki owner(#439)そこそこ色んなdAppsを触ってきましたがDeFiをめちゃくちゃやる方ではないWallets that we used使ってきたEthereumウォレットはMetaMaskBrave WalletPhantomBitget WalletBinance WalletとCrypto分かる人の中ではおそらく極めて普通。開発しているプロダクト内では、Crypto全然分からない人向けにはWeb3...]]></description>
            <content:encoded><![CDATA[<p>ウォレットやトランザクションについて考えることが職業柄多く、最近特に発展してきたなと思うのでまとめておきます。内容の一部はEthereum界隈の人全般向け、一部はその中でもエンジニア向けになっています。最近推しのウォレット、ウォレット関連のEIP、玄人や素人は何をつかうべきなのか? みたいな章構成です。</p><h1 id="h-who-i-am" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Who I am</h1><ul><li><p>Crypto、特にEthereum界隈に2018年ぐらいからいる。はじめて参加したDevconは大阪。Bitcoinを持ち始めたのは2017年1月</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://startbahn.io/">Startbahn</a>という会社のCTOをやっており、5年ほど前からマスに向けたweb3サービスを提供している</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.azuki.com/">Azuki</a> owner(<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://opensea.io/item/ethereum/0xed5af388653567af2f388e6224dc7c4b3241c544/439">#439</a>)</p></li><li><p>そこそこ色んなdAppsを触ってきましたがDeFiをめちゃくちゃやる方ではない</p></li></ul><h1 id="h-wallets-that-we-used" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Wallets that we used</h1><p>使ってきたEthereumウォレットは</p><ul><li><p>MetaMask</p></li><li><p>Brave Wallet</p></li><li><p>Phantom</p></li><li><p>Bitget Wallet</p></li><li><p>Binance Wallet</p></li></ul><p>とCrypto分かる人の中ではおそらく極めて普通。開発しているプロダクト内では、Crypto全然分からない人向けには<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://web3auth.io/">Web3Auth</a>(旧Torus)を用いて、EmailやGoogleログインなどで秘密鍵を取得し認証する仕組みを採用してきました。余談ですが、弊社プロダクトの2020年リリース当時、</p><ul><li><p>一般の人が使えて</p><ul><li><p>GoogleなどのOAuth認証を含む</p></li></ul></li><li><p>Custodialではなく(鍵全体に対するアクセスをサービス提供者が持たない)</p></li><li><p>dAppsにConnect Walletできる(e.g. 決まったトランザクションだけではなく任意のメッセージに署名できる)</p></li></ul><p>ウォレットに選択肢は殆どありませんでした。今では<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.privy.io/">Privy</a>などの採用を多く見かけますね。</p><h1 id="h-ambire-wallet" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Ambire Wallet</h1><p>ここから本題に入っていきますが、最近推しのウォレットができました。2024年秋のDevcon Bangkokの前から目をつけていて中の人とも話して気にはなっていた<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.ambire.com/">Ambire Wallet</a>です。モバイルアプリも提供されていますが、今v1からv2への移行期ということで、<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://chromewebstore.google.com/detail/ambire-web3-wallet/ehgjhhccekdedpbkifaojjaefeohnoea?hl=en">先にv2が採用されているChrome Extention</a>だけを今は利用しています。もし招待コードが必要でしたら、 <code>cb81584f8cc8</code> を利用していただけたら幸いです。</p><p>Account Abstraction(ERC-4337)ウォレットでのTXが非常に見やすいです。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c553b5cac11667e19dde89558dd174837446f35b00bec69632fb1fd693c95cfe.png" alt="何が行われたか見やすい" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">何が行われたか見やすい</figcaption></figure><p>ERC-20トークンである <code>$USDC</code> のApprovalと, <code>$WALLET</code> とのSwapが一つのトランザクション内で行われたことがよく分かります。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/5745da001059fdbc575ef393637d25f8cc29de5bbd5feee0f98bafc5cfc3bb36.png" alt="Swap時のルーティングも分かりやすい" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Swap時のルーティングも分かりやすい</figcaption></figure><p>Gas Tankという機能を使って、一つのチェーンに <code>$USDC</code> などをガス代用にdepositしておくことで、このウォレットからの(基本的に)あらゆるチェーンでのあらゆるトランザクションの手数料をそこから支払うこともできます。 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://particle.network/#universal-accounts">Particle</a>が少し前からGas Abstractionと言っていましたが、一般的な機能になってきましたね、素晴らしい。</p><p>ちなみに、このウォレットのインタフェース上でのSwapは現時点で0.25%の手数料が取られます。僕はこのウォレットを推しますが、利用は自己責任でお願いいたします🙏</p><h1 id="h-execute-aa-transaction-and-connect-the-wallet" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Execute AA transaction and connect the wallet</h1><p>このウォレットを使ってdApps、例としてNFTマーケットプレイスである<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://magiceden.io/">Magic Eden</a>にConnectしてみましょう。</p><p>普通のEthereumウォレット(EOA, Externally Owned Address)と同様に決まった書式のメッセージに署名を求められます。ウォレットアイコンの左上にある <code>SA</code> はERC-4337を利用した <code>Smart Account</code> を意味します。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/50199fc4f0597a0edbd814209efe4681b63823ec853b8be4f921465e5cb87882.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/e9517750f6681fd0b056fcf97a54ae58006c88e900bafbe050e2fa52447679e3.png" alt="Connectできました" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Connectできました</figcaption></figure><p>また、既にEtherScan(や同系列のBaseScanなど)も既にこれらのトランザクションを非常に上手く表示する機能を搭載してくれています。是非Ambireで一つトランザクションを実行してexplorerで確認してみてください。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/95477293997726c7de8b0e70871f043b249a73228eb4859bd595d98a54be1339.png" alt="注釈やAA Transactionタブへの動線が確認できます" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">注釈やAA Transactionタブへの動線が確認できます</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/517a5859da07482b5699dcb78ae98fcf42ff88c408044565af5ee50eafe06af2.png" alt="各UserOperationが何をしたかの詳細画面" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">各UserOperationが何をしたかの詳細画面</figcaption></figure><h1 id="h-eipsercs" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">EIPs/ERCs</h1><p>関連するEIP/ERCについても考えていきましょう。</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-4337">4337</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-1271">1271</a></p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-6492">6492</a></p></li></ul></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-7702">7702</a></p></li></ul><p>ERC-4337は、Account Abstractionを利用したウォレットインタフェースの中で、おそらく一番利用されているものです。ユーザは生のトランザクションではなく、<code>UserOperation</code> というオブジェクトに署名をして、 <code>Bundler</code> という役割を行っている相手(ノード)にそれを投げます。この一つの <code>UserOperation</code> は複数のコントラクト実行、例えば上の例で言うと</p><ol><li><p><code>$USDC</code>コントラクトの <code>Approve</code> をするための関数</p></li><li><p>DEXコントラクトのSwap用の関数</p></li></ol><p>の実行を含んでいて、これらはEOAの世界では2つのトランザクションに別々に署名が必要でガス代も多めにかかっていたところが、1つになっています。</p><p>ERC-1271は、どうコントラクトウォレットの署名を検証するのかを規定しています。おそらく上でSwapのトランザクションにおいても、Magic EdenへのConnectにおいても、<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmbireTech/ambire-common/blob/34018e0642a5e2e179a78318f21f1c39e742eee3/contracts/AmbireAccount.sol#L254">該当ウォレットのisValidSignature関数</a>を実行することで認証しています。これはEOAにおける、dAppsバックエンドがsecp256k1 ECDSA署名から <code>ecrecover</code> アルゴリズムを利用して署名元のEthereumアドレスを復元し、署名したと言っているアドレスと一致するかを検証することで認証していたものなどを代替するものです。</p><p>ERC-6492はそれを補完するものです。Contract Walletを含むコントラクトのデプロイにはコストがかかるため、オンチェーンでそのウォレットがトランザクションを実行するまではデプロイを控えたい。しかしERC-1271は既にそのネットワークにデプロイされているコントラクトに対してしか実行できないという弱点を、そのデプロイや検証のバイトコードをオフチェーンでも実行する仕組みを提供することで補完しています。結構前ですが、<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/tomohiro-n/sign-in-with-eip6492/blob/main/server/src/app.service.ts#L60">それを使った認証を実装したコードも書いてみた</a>( <code>web</code> <code>server</code> <code>contracts</code> がそれぞれフロントエンド, バックエンド, コントラクトに対応しています)ので気になる方は参照してみてください。</p><p>EIP-7702は、比較的新しいもので、今ちょうどEthereum L1ではテストネットにはリリース済み、メインネットには近々リリースされるPectra(execution layerへのPragueリリースとconsensus layerへのElectraリリースの総称)に含まれています。他のEVM L1やL2も対応を複数発表しています。表題は <code>Smarter EOA</code> で、ERC-4337を代表としたAccount Abstractionを含むContract Walletの考えは素晴らしく、また長期的には全面的に採用されていくべきもの。しかし現在ユーザはEOAに慣れきっているため、橋渡しをしていく意味で短中期的にユーザはあくまでEOAを使っているがContract Walletの機能を享受できるようにしよう、というものです。技術的な詳細は<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.youtube.com/watch?v=_k5fKlKBWV4">この動画</a>が分かりやすいです。こちらについても<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/tomohiro-n/hello-eip-7702">Hello World的なコードを書いてみたので気になる方はどうぞ</a>。かいつまんで言うと、Pectra以降、各EOAにはコード( <code>code</code> )をセットすることができるようになり、Contract Walletをアドレスや鍵を変えずに利用することができます。セットできるコードは1つです。以下はEIP-7702を享受している、つまり <code>code</code> がセットされているEOAが、トランザクションのFromにもToにも使われうることを示します。前者は今まで通りそのEOAの鍵を利用してトランザクションを発行した場合、後者は <code>code</code> を他のEthereum Address(EOAかもしれないしコントラクトかもしれない)が実行した場合です。EIP-7702以前は、(ネイティブ通貨(e.g. <code>$ETH</code> )のtransferを除き)EOAは常に署名をする側、つまりFromでしかあり得ませんでした。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/e25ee5e6ba199efd2462a63cee812c81bfc6cd9ed824c729a6a6af86bdac4c2e.png" alt="ToがそのEOA" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">ToがそのEOA</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/32256c03137a61b4bbb0c01823f972ce56d844d2eaa4192358d953f5bbe1d0e1.png" alt="FromがそのEOA。上のToと同じアドレス" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">FromがそのEOA。上のToと同じアドレス</figcaption></figure><p>(Explorerはローカルで動作する<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/otterscan/otterscan">Otterscan</a>を利用しています👏)</p><p>上の例で、Block 5の時点で既に<code>0x709…</code> は <code>0xf39…</code> に一定の権限を付与されているため、その権限に基づきトランザクションを実行しています。 <code>0xf39…</code> がERC-4337のウォレットで、 <code>0x709…</code> が <code>UserOperation</code> に署名するEOAという図式を代替したと言っていいでしょう。一方で、 <code>0xf39…</code> の秘密鍵は依然としてフルの権限を持っているため、Block 6において別のトランザクションを実行しています。これが、このEIPがEOAとContract Walletの世界の橋渡しをする <code>Smarter EOA</code> の中身です。</p><h1 id="h-so-who-should-use-what-now" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">So, who should use what now?</h1><p>さて、では我々Crypto慣れている勢と、Crypto何も分からない勢はどのウォレットを使うべきなのでしょうか。</p><p>比較的簡単なのは前者です。既にMetaMaskに代表されるEOAウォレットを使って色んなアプリを利用しているでしょうから、徐々にAmbireやParticleなどのERC-4337対応のウォレットに移行していけばいいでしょう。上でMagic EdenへのConnectを紹介しましたが、本記事執筆時点でAmbire Walletを利用した、Ledgerデバイスで署名するERC-4337アカウントではMagic Eden, Uniswap, OpenSeaにはConnectできましたが、記事が書かれているMirrorにはConnectできませんでした。初期に比べて格段にサポートが良くなっていて、実用し始めるには十分と思いますが、まだ全てのアプリで使う準備ができているとは言えないでしょう。また、既に利用しているEOAのトランザクション履歴を大事にしたい場合には、そのEOAに好きな <code>code</code> をセットして、EIP-7702ウォレットとして利用していくのも有力な選択肢と言えます。</p><p>後者については個人的にはいまだに凄く良い解がありません。EIP-7702の開発者はこのユーザ群のbootstrapを一つの目的として掲げています。実際にこのユーザ群にとって良い選択肢である一方で、無知なユーザに必要以上の権限委譲をさせてしまう危険性を孕んでいると感じました。ウォレットやdAppの提供者は、ユーザのEOAに「この <code>code</code> をセットしませんか」と促すことができます。その <code>code</code> はユーザが理解していない副作用を持つかもしれません。また、セットできる <code>code</code> は一つなので、ウォレット提供者はともかくdAppがそれをセットするのは、他のdAppも違う <code>code</code> をセットさせたいのではないか? その <code>code</code> にユーザが乗り換えようとしたとき、最初の <code>code</code> とデータに互換性はあるのか? などを考えると、対象ユーザ群のリテラシが低いことを認識しているからこそ気が引けます。これらの理由から、このEIPを利用した素晴らしいユースケースは出てくることが期待できる反面、良からぬ事故が起きてしまうことも想定されるし、dapp開発者として利用することを躊躇してしまう場面もあります。結局のところ、よりTrustfulだがメールが使えれば使えるPrivyのような選択肢のほうが期待値的に良いUXを与えるのが現状という気がします。</p><hr><p>以上です。お読みくださりありがとうございました。質問があればいつでもTwitterなどでくださればと思います。また、より詳しい方や似た悩みをお持ちの人がいれば是非話させてください！</p>]]></content:encoded>
            <author>0xtomo@newsletter.paragraph.com (0xtomo)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/6dc6b1c4d2988fff3222079142bc7e09f8a53c9db99d7980134215f2e8bf7946.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>