Solana AnchorのProvider.sendでTransaction fee payer requiredエラー

現象

Reactで以下(AnchorのProvider.send)を実装して試したところ、Transaction fee payer requiredエラーが出力される。

@project-serum / anchor - v0.20.1 / Provider / send

  async function transfer() {
    const provider = await getProvider();
    const connection = provider.connection;

    let recentBlockhash = await connection.getRecentBlockhash();
    let transaction = new web3.Transaction({
        recentBlockhash: recentBlockhash.blockhash,
        feePayer: payer.publicKey,
    });

    const tx_instructions = web3.SystemProgram.transfer({
        fromPubkey: payer.publicKey,
        toPubkey: toAccount.publicKey,
        lamports: web3.LAMPORTS_PER_SOL * 0.1,
    });
    transaction.add(tx_instructions);
    console.log(transaction);

    try {
      const signature = await provider.send(
        transaction,
        [payer], // signers
      );
      console.log('Sent! Signature: ', signature);
    } catch (err) {
      console.log("Transaction Error: ", err);
    }
  }

(この例だと「recentBlockhash」と「feePayer」の設定は不要だが、わかりやすくするためにあえて追加)

JSのconsole.logの出力結果。

Transaction Error:  Error: Transaction fee payer required
    at Transaction.compileMessage (transaction.ts:227:1)
    at Transaction._compile (transaction.ts:376:1)
    at Transaction.serializeMessage (transaction.ts:402:1)
    at o (inpage.js:263:601)
    at a.<anonymous> (inpage.js:321:1744)
    at Generator.next (<anonymous>)
    at inpage.js:321:622
    at new Promise (<anonymous>)
    at c (inpage.js:321:370)
    at a.signTransaction (inpage.js:321:1566)

feePayerがnullになっている。

Transaction {signatures: Array(0), feePayer: undefined, instructions: Array(1), recentBlockhash: undefined, nonceInfo: undefined}feePayer: nullinstructions: [TransactionInstruction]nonceInfo: undefinedrecentBlockhash: "HiYZ1oyU7JP6y1g35QHeXZwKZygAKc4ptj3y94B5CLYG"signatures: []data: (...)keys: (...)programId: (...)signature: (...)[[Prototype]]: Object

feePayer: null
instructions: [TransactionInstruction]
nonceInfo: undefined
recentBlockhash: "HiYZ1oyU7JP6y1g35QHeXZwKZygAKc4ptj3y94B5CLYG"
signatures: []
data: (...)
keys: (...)
programId: (...)
signature: (...)
[[Prototype]]: Object

原因

トランザクション実行時のfeePayerが設定されていないため(Instructionではなく)。

対応

トランザクション実行時にウォレット(Phantom)接続してApproveする処理にする(ポップアップで表示される「Approve Transaction」画面を表示する)。

Phantomの呼び出し例。

  async function getProvider() {
    const network = "http://127.0.0.1:8899";
    const connection = new Connection(network, opts.preflightCommitment);
    const wallet = window.solana;

    const provider = new Provider(
      connection, wallet, opts.preflightCommitment,
    );
    return provider;
  }

  async function connectWallet() {
    try {
        const resp = await window.solana.connect();
        console.log("Conneted! Public Key: ", resp.publicKey.toString());
    } catch (err) {
        console.log(err);
        // => { code: 4001, message: 'User rejected the request.' }
    }
  }

Approveすると処理が通り、feePayerにデータ(_r {_bn: h})が入る。

Transaction {signatures: Array(0), feePayer: undefined, instructions: Array(1), recentBlockhash: undefined, nonceInfo: undefined}

feePayer: _r {_bn: h}
instructions: [TransactionInstruction]
nonceInfo: undefined
recentBlockhash: "5pCue1JGmvgcXpDQ1YsKFHHMvxwvZsrTsRQ2XbS9dC75"
signatures: (2) [{…}, {…}]
data: (...)
keys: (...)
programId: (...)
signature: (...)
[[Prototype]]: Object
add: add() { for (var _len4 = arguments.length, items = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { items[_key4] = arguments[_key4]; } if (items.length === 0) { throw new Error('No instructions'); } items.forEach(item => {…}
addSignature: ƒ addSignature(pubkey, signature)
compileMessage: compileMessage() { const { nonceInfo } = this; if (nonceInfo && this.instructions[0] != nonceInfo.nonceInstruction) { this.recentBlockhash = nonceInfo.nonce; this.instructions.unshift(nonceInfo.nonceInstruction); } const { recentBlockhash } = this; if (!recentBlockhash) { throw new Error('Transaction recentBlockhash required'); } if (this.instructions.length < 1) { console.warn('No instructions provided'); } let feePayer; if (this.feePayer) { feePayer = this.feePayer; } else if (this.signatures.length > 0 && this.signatures[0].publicKey) { // Use implicit fee payer feePayer = this.signatures[0].publicKey; } else { throw new Error('Transaction fee payer required'); } for (let i = 0; i < this.instructions.length; i++) { if (this.instructions[i].programId === undefined) { throw new Error(`Transaction instruction index ${i} has undefined program id`); } } const programIds = []; const accountMetas = []; this.instructions.forEach(instruction => {…}
constructor: class Transaction
data: (...)
keys: (...)
partialSign: ƒ partialSign()
programId: (...)
serialize: ƒ serialize(config)
serializeMessage: ƒ serializeMessage()
setSigners: setSigners() { for (var _len5 = arguments.length, signers = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { signers[_key5] = arguments[_key5]; } if (signers.length === 0) { throw new Error('No signers'); } const seen = new Set(); this.signatures = signers.filter(publicKey => {…}
sign: sign() { for (var _len6 = arguments.length, signers = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) { signers[_key6] = arguments[_key6]; } if (signers.length === 0) { throw new Error('No signers'); } // Dedupe signers const seen = new Set(); const uniqueSigners = []; for (const signer of signers) { const key = signer.publicKey.toString(); if (seen.has(key)) { continue; } else { seen.add(key); uniqueSigners.push(signer); } } this.signatures = uniqueSigners.map(signer => {…}
signature: (...)
verifySignatures: ƒ verifySignatures()
_addSignature: _addSignature(pubkey, signature) { assert(signature.length === 64); const index = this.signatures.findIndex(sigpair => {…}
_compile: _compile() { const message = this.compileMessage(); const signedKeys = message.accountKeys.slice(0, message.header.numRequiredSignatures); if (this.signatures.length === signedKeys.length) { const valid = this.signatures.every((pair, index) => {…}
_partialSign: _partialSign(message) { const signData = message.serialize(); for (var _len8 = arguments.length, signers = new Array(_len8 > 1 ? _len8 - 1 : 0), _key8 = 1; _key8 < _len8; _key8++) { signers[_key8 - 1] = arguments[_key8]; } signers.forEach(signer => {…}
_serialize: _serialize(signData) { const { signatures } = this; const signatureCount = []; encodeLength(signatureCount, signatures.length); const transactionLength = signatureCount.length + signatures.length * 64 + signData.length; const wireTransaction = buffer__WEBPACK_IMPORTED_MODULE_1__.Buffer.alloc(transactionLength); assert(signatures.length < 256); buffer__WEBPACK_IMPORTED_MODULE_1__.Buffer.from(signatureCount).copy(wireTransaction, 0); signatures.forEach((_ref2, index) => {…}
_verifySignatures: ƒ _verifySignatures(signData, requireAllSignatures)
get data: ƒ data()
get keys: keys() { assert(this.instructions.length === 1); return this.instructions[0].keys.map(keyObj => {…}
get programId: ƒ programId()
get signature: ƒ signature()
[[Prototype]]: Object