Solana AnchorでANCHOR_PROVIDER_URL、ANCHOR_WALLET、no such file or directoryエラー

Anchorで開発したプログラムをExpress x Prisma x TypeScriptのサーバーで読み込もうとしたら(厳密には、Anchorが出力されたIDLを読み込んでるだけでスマコンは呼んでない)発生したエラー。

Anchor内でtest実行する分には問題なかったが、上記環境からAnchorのIDLファイルをExpress内で呼ぼうとすると発生。

ANCHOR_PROVIDER_URL is not definedエラー

現象

ExpressでAPIを実行すると以下のエラー。

Error: ANCHOR_PROVIDER_URL is not defined

原因

環境変数に ANCHOR_PROVIDER_URL がないため。

対応

envファイルに以下を追加する。そのあとにExpressを再起動(npm run dev)して環境変数を読み込ませる。

ANCHOR_PROVIDER_URL="http://localhost:8899"

ANCHOR_WALLET is not setエラー

現象

ExpressでAPIを実行すると以下のエラー。

Error: expected environment variable `ANCHOR_WALLET` is not set.

原因

環境変数に ANCHOR_WALLET がないため。

対応

envファイルに以下を追加する。そのあとにExpressを再起動(npm run dev)して環境変数を読み込ませる。

ANCHOR_WALLET="./id.json"

no such file or directory(id.json)エラー

現象

ExpressでAPIを実行すると以下のエラー。

Error: ENOENT: no such file or directory, open '~/.config/solana/id.json'

原因

環境変数 ANCHOR_WALLET のファイルパスが間違っているため。

対応

envファイルを正しいパスに書き直す。自分の場合は以下が正しいパスだった。
そのあとにExpressを再起動(npm run dev)して環境変数を読み込ませる。

ANCHOR_WALLET="./id.json"

補足

相対パスや絶対パスで自分のHomeディレクトリ指定だとダメで、PJルートフォルダ配下じゃないと読み取れなかった。
以下が指定がダメだった例。

~/.config/solana/id.json
 /Users/user/.config/solana/id.json

あと、設定を変更したらExpressを再起動しないと環境変数が読み込まれないため注意。

最終版ソース

少し複雑で長くなってしまう上に作業用メモも多いが、理解に重要なため、必要なファイル群と全ソースを以下に記載。

app/contollers/events.ts

// --- Lib ---
import * as fs from 'fs';

// --- Express/Prisma ---
import * as dotenv from 'dotenv'
import { Router } from 'express';
import { PrismaClient } from '@prisma/client';

// --- Solana/Anchor ---
import * as anchor from "@coral-xyz/anchor";
import {
  Keypair,
  PublicKey,
} from '@solana/web3.js';

dotenv.config();
export const router = Router();
const prisma = new PrismaClient();

router.get("/", async (req, res, next) => {
  try {
    res.header('Content-Type', 'application/json; charset=utf-8');

    const events = await prisma.event.findMany();

    return res.json(events);
  } catch (error) {
    return res.status(400).json(error);
  }
});

router.post("/", async (req, res, next) => {
  // PDA
  let publickey: PublicKey; // For seed
  let pda: PublicKey;
  let bump: number;

  try {
    // Anchor Config
    const provider = anchor.AnchorProvider.env();
    anchor.setProvider(provider);

    const idl = JSON.parse(
      fs.readFileSync("./app/target/idl/hok.json", "utf8")
    );

    const programId = new anchor.web3.PublicKey("CTgTMTJ2jkCU2TKkWa7FNiLa8aEcsu1knKPahSN9tLD"); // TODO: add to .env
    const program = new anchor.Program(idl, programId);

    // PDA
    publickey = Keypair.generate().publicKey; // Seed for PDA

    [pda, bump] = await PublicKey.findProgramAddressSync(
      [
        Buffer.from('prize'),
        publickey.toBuffer(),
        provider.wallet.publicKey.toBuffer(),
      ],
      program.programId,
    );

    console.log('pda =>', pda.toString());
    console.log('bump =>', bump);
  } catch (error) {
    console.log(error);

    return res.status(400).json(error);
  }

  try {
    res.header('Content-Type', 'application/json; charset=utf-8');

    const { startDate, endDate } = req.body;

    const event = await prisma.event.create({
      data: {
        publickey: publickey.toString(),
        startDate: startDate,
        endDate: endDate,
        collectFeeWallet: pda.toString(),
        prize: 0,
      }
    });

    return res.json(event);
  } catch (error) {
    return res.status(400).json(error);
  }
});

router.patch("/", async (req, res, next) => {
  try {
    res.header('Content-Type', 'application/json; charset=utf-8');

    const publickey = Keypair.generate().publicKey; // Seed for PDA
    const { startDate, endDate } = req.body;

    const event = await prisma.event.create({
      data: {
        publickey: publickey.toString(),
        startDate: startDate,
        endDate: endDate,
        prize: 0,
      }
    });

    return res.json(event);
  } catch (error) {
    return res.status(400).json(error);
  }
});

app/target/idl/hok.json

{
  "version": "0.1.0",
  "name": "hok",
  "instructions": [
    {
      "name": "transferProviderSol",
      "accounts": [
        {
          "name": "pda",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "taker",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "event",
          "isMut": false,
          "isSigner": false
        },
        {
          "name": "authority",
          "isMut": false,
          "isSigner": true
        },
        {
          "name": "systemProgram",
          "isMut": false,
          "isSigner": false
        }
      ],
      "args": [
        {
          "name": "amount",
          "type": "u64"
        },
        {
          "name": "bump",
          "type": "u8"
        }
      ]
    }
  ],
  "metadata": {
    "address": "CTgTMTJ2jkCU2TKkWa7FNiLa8aEcsu1knKPahSN9tLD"
  }
}

.env.dev

# Anchor
ANCHOR_PROVIDER_URL="http://localhost:8899"
ANCHOR_WALLET="./id.json"

補足:.envに上記を記述してもよいし、envファイルを使わずにターミナル上で上記コマンドを入力してからExpress起動でも問題ない。どうにかして上記の環境変数を読み込めればよい。

package.json

{
  "name": "backend-server",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www",
    "dev": "nodemon -r dotenv/config app.ts dotenv_config_path=.env.dev"
  },
  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  },
  "dependencies": {
    "@coral-xyz/anchor": "^0.26.0",
    "@prisma/client": "^4.8.0",
    "@solana/web3.js": "^1.73.0",
    "@types/cookie-parser": "^1.4.3",
    "@types/debug": "^4.1.7",
    "@types/express": "^4.17.14",
    "@types/http-errors": "^2.0.1",
    "@types/morgan": "^1.9.3",
    "@types/node": "^18.11.9",
    "cookie-parser": "~1.4.6",
    "cors": "^2.8.5",
    "debug": "~4.3.4",
    "dotenv": "^16.0.3",
    "ejs": "~3.1.8",
    "express": "~4.18.2",
    "http-errors": "~2.0.0",
    "morgan": "~1.10.0",
    "ts-node": "^10.9.1",
    "typescript": "^4.8.4"
  },
  "devDependencies": {
    "@types/cors": "^2.8.12",
    "nodemon": "^2.0.20",
    "npm": "^8.19.2",
    "prisma": "^4.8.0"
  }
}

ファイル構成(PJルート)

.env
.env.dev
.git
.gitignore
LICENSE
app.ts
docs
node_modules
package.json
public
app
├── contollers
│   └── events.ts
├── modules
├── services
├── target
│   └── idl
│       └── hok.json
└── views
    └── error.ejs
bin
id.json
package-lock.json
prisma
tsconfig.json

補足

Macで環境変数を出力したい場合は以下コマンド

% export -p

参考

anchor/examples/tutorial/basic-0/client.js