Solana PayでPDAにSOLを送金すると「CreateTransferError: recipient not found」エラー

SOLを送るときは、 @solana/web3.js ではなく @solana/pay を使ったほうが利便性がよいため、最近は @solana/pay で送金しているが、そこでハマった現象。

現象

findProgramAddressSync でPDAを作成後に、@solana/payのcreateTransfer でPDAにSOLを送金すると以下のエラー。

CreateTransferError: recipient not found

また、アカウントを直接見ると、 Balance(SOL) が Account does not exist になっている。

原因

前提として、Solanaでは、rent costを持っていないと、アカウントが有効にならない。

findProgramAddressSync は、まだ作成されていないPDAを探すだけで、実際にアカウントの作成まではしていない。
@solana/payのcreateTransferは、送信者・送信先のアカウントが存在するかチェック(getAccountInfo)していて、存在しないとエラーにしている。

solana-pay/core/src/createTransfer.ts

    const recipientInfo = await connection.getAccountInfo(recipient);
    if (!recipientInfo) throw new CreateTransferError('recipient not found');

対応

アカウントを作成(有効化)すればよい。

案1:rent costを直接送金

getMinimumBalanceForRentExemption でrent costを調べてから、フロントエンドでfindProgramAddressSyncしたPDAにSOLを送金する。一番手軽。

関係ないソースも混じっているが、送金部分は以下のような形。

  it("Create(Activate) PDA by Provider", async () => {
    // ---------------------------------------------
    //  Get Event
    // ---------------------------------------------
    const showEventsUrl = backendServerHost + '/events/';
    const event = await axios.get(showEventsUrl + eventId)
      .catch(function (error) {
        console.log(error);
      });

    // ---------------------------------------------
    //  Transfer SOL of Rent Exempotion to PDA
    // ---------------------------------------------
    if (!event) throw new Error('Not found event');
    const collectFeeWallet = new PublicKey(event.data.collectFeeWallet); // PDA

    const space = 0;
    // Seed the created account with lamports for rent exemption
    const rentExemptionAmount = await provider.connection.getMinimumBalanceForRentExemption(space);
    let transaction = new Transaction();

    transaction.add(SystemProgram.transfer({
      fromPubkey: provider.wallet.publicKey,
      toPubkey: collectFeeWallet,
      lamports: rentExemptionAmount,
    }));

    const signature = await sendAndConfirmTransaction(
      provider.connection,
      transaction,
      [provider.wallet.payer],
    );

    console.log('rentExemptionAmount =>', rentExemptionAmount / LAMPORTS_PER_SOL, 'SOL');
    console.log('signature =>', signature);

    const collectFeeWalletInfo = await provider.connection.getAccountInfo(collectFeeWallet);
    assert.ok(collectFeeWalletInfo);
  });

案2:Program側でアカウントを作成

フロントエンドでfindProgramAddressSyncしたPDAをAnchorでアカウント作成。

PDA作成のサンプルプログラム

ただし、Anchorを使ったRust実装が必要になってしまい、プログラムもオンチェーンにデプロイする必要がある。

案3:フロントエンドで直接アカウント作成

createAccountWithSeed で、直接PDAを作成できる。

createAccountWithSeedのサンプルプログラム

まだ詳しく調べていないが、以下が気になって使わなかった。

  • キーペアを作成してそれを新規アカウントにしている(そうなるとPDAではないような気が。もしかして暫定的に作成しているだけ? 要ソース確認)
  • サンプルプログラムであまり見かけない。ただし、GitHubで検索するとたくさんHITはするので、自分が知らないだけの可能性もある