SOLを送るときは、 @solana/web3.js ではなく @solana/pay を使ったほうが利便性がよいため、最近は @solana/pay で送金しているが、そこでハマった現象。
Agenda
現象
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でアカウント作成。
ただし、Anchorを使ったRust実装が必要になってしまい、プログラムもオンチェーンにデプロイする必要がある。
案3:フロントエンドで直接アカウント作成
createAccountWithSeed で、直接PDAを作成できる。
createAccountWithSeedのサンプルプログラム
まだ詳しく調べていないが、以下が気になって使わなかった。
- キーペアを作成してそれを新規アカウントにしている(そうなるとPDAではないような気が。もしかして暫定的に作成しているだけ? 要ソース確認)
- サンプルプログラムであまり見かけない。ただし、GitHubで検索するとたくさんHITはするので、自分が知らないだけの可能性もある