Search code examples
rustsolanasolana-web3jsanchor-solana

Passing complex types to anchor rpc fails


I modified the basic tutorial example to pass structs but when i run "anchor test" it fails with TypeError: Blob.encode[data] requires (length 4) Buffer as src I could not find anything on the internet when I searched for this error.

The code is attached. Please let me know if I got it wrong somehow. Thanks,

Lib.rs


use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
mod basic_1 {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        let my_account = &mut ctx.accounts.my_account;
        my_account.data = data;
        Ok(())
    }

    pub fn update(ctx: Context<Update>, data: u64) -> Result<()> {
        let my_account = &mut ctx.accounts.my_account;
        my_account.data = data;
        Ok(())
    }

    pub fn add_signatory(ctx: Context<Update>, signatory: Signatory) -> Result<()> {
        let my_account = &mut ctx.accounts.my_account;
        //my_account.data = data;
        my_account.signatories.push(signatory);
        Ok(())
    }

    pub fn add_signatories(ctx: Context<Update>, signatories: Vec<Signatory>) -> Result<()> {
        let my_account = &mut ctx.accounts.my_account;
        //my_account.data = data;
        my_account.signatories = signatories;
        Ok(())
    }


}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 100)]
    pub my_account: Account<'info, MyAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Update<'info> {
    #[account(mut)]
    pub my_account: Account<'info, MyAccount>,
}

#[account]
pub struct MyAccount {
    pub data: u64,
    pub project_id: u64,
    pub project_name: Vec<u8>,
    pub signatories: Vec<Signatory>,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct Signatory {
    pub name: Vec<u8>,
    pub public_key: Pubkey,
}

basic-1.js

const assert = require("assert");
const anchor = require("@project-serum/anchor");
const { SystemProgram } = anchor.web3;

describe("basic-1", () => {
  // Use a local provider.
  const provider = anchor.AnchorProvider.local();

  // Configure the client to use the local cluster.
  anchor.setProvider(provider);

  it("Creates and initializes an account in a single atomic transaction (simplified)", async () => {
    // #region code-simplified
    // The program to execute.
    const program = anchor.workspace.Basic1;

    // The Account to create.
    const myAccount = anchor.web3.Keypair.generate();

    // Create the new account and initialize it with the program.
    // #region code-simplified
    await program.rpc.initialize(new anchor.BN(1234), {
      accounts: {
        myAccount: myAccount.publicKey,
        user: provider.wallet.publicKey,
        systemProgram: SystemProgram.programId,
      },
      signers: [myAccount],
    });
    // #endregion code-simplified

    // Fetch the newly created account from the cluster.
    const account = await program.account.myAccount.fetch(myAccount.publicKey);

    // Check it's state was initialized.
    assert.ok(account.data.eq(new anchor.BN(1234)));

    // Store the account for the next test.
    _myAccount = myAccount;
  });

  it("Updates a previously created account", async () => {
    const myAccount = _myAccount;

    // #region update-test

    // The program to execute.
    const program = anchor.workspace.Basic1;

    // Invoke the update rpc.
    await program.rpc.update(new anchor.BN(4321), {
      accounts: {
        myAccount: myAccount.publicKey,
      },
    });

    // Fetch the newly updated account.
    const account = await program.account.myAccount.fetch(myAccount.publicKey);

    // Check it's state was mutated.
    assert.ok(account.data.eq(new anchor.BN(4321)));

    // #endregion update-test
  });

  it("add a single signatory", async () => {
    const myAccount = _myAccount;

    // #region update-test

    // The program to execute.
    const program = anchor.workspace.Basic1;
    const pubkey1 = anchor.web3.Keypair.generate();
    const pubkey2 = anchor.web3.Keypair.generate();
    const signatory1 =
    {
      name: "matt",
      public_key: pubkey1,
    };

    // Invoke the update rpc.
    await program.rpc.addSignatory(signatory1, {
      accounts: {
        myAccount: myAccount.publicKey,
      },
    });

    // Fetch the newly updated account.
    const account = await program.account.myAccount.fetch(myAccount.publicKey);

    // Check it's state was mutated.
    assert.ok(account.data.eq(new anchor.BN(4321)));

    // #endregion update-test
  });

  it("add multiple signatories", async () => {
    const myAccount = _myAccount;

    // #region update-test

    // The program to execute.
    const program = anchor.workspace.Basic1;
    const pubkey1 = anchor.web3.Keypair.generate();
    const pubkey2 = anchor.web3.Keypair.generate();

    const signatories1 = [
      {
        name: "matt",
        public_key: pubkey1,
      },
      {
        name: "smith",
        public_key: pubkey2,
      },
    ];


    // Invoke the update rpc.
    await program.rpc.addSignatories(signatories1, {
      accounts: {
        myAccount: myAccount.publicKey,
      },
    });

    // Fetch the newly updated account.
    const account = await program.account.myAccount.fetch(myAccount.publicKey);

    // Check it's state was mutated.
    assert.ok(account.data.eq(new anchor.BN(4321)));

    // #endregion update-test
  });


});

anchor test output

abcd@abcd:~/code/anchor/examples/tutorial/basic-1$ anchor test
BPF SDK: /home/abcd/.local/share/solana/install/releases/1.10.8/solana-release/bin/sdk/bpf
cargo-build-bpf child: rustup toolchain list -v
cargo-build-bpf child: cargo +bpf build --target bpfel-unknown-unknown --release
    Finished release [optimized] target(s) in 0.17s
cargo-build-bpf child: /home/abcd/.local/share/solana/install/releases/1.10.8/solana-release/bin/sdk/bpf/dependencies/bpf-tools/llvm/bin/llvm-readelf --dyn-symbols /home/abcd/code/anchor/examples/tutorial/basic-1/target/deploy/basic_1.so

To deploy this program:
  $ solana program deploy /home/abcd/code/anchor/examples/tutorial/basic-1/target/deploy/basic_1.so
The program address will default to this keypair (override with --program-id):
  /home/abcd/code/anchor/examples/tutorial/basic-1/target/deploy/basic_1-keypair.json

Found a 'test' script in the Anchor.toml. Running it as a test suite!

Running test suite: "/home/abcd/code/anchor/examples/tutorial/basic-1/Anchor.toml"

yarn run v1.22.18
$ /home/abcd/code/anchor/examples/tutorial/node_modules/.bin/mocha -t 1000000 tests/


  basic-1
    ✔ Creates and initializes an account in a single atomic transaction (simplified) (250ms)
    ✔ Updates a previously created account (411ms)
    1) add a single signatory
    2) add multiple signatories


  2 passing (698ms)
  2 failing

  1) basic-1
       add a single signatory:
     TypeError: Blob.encode[data] requires (length 4) Buffer as src
      at Blob.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/buffer-layout/lib/Layout.js:2321:13)
      at Structure.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/buffer-layout/lib/Layout.js:1263:26)
      at WrappedLayout.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/@project-serum/borsh/dist/lib/index.js:67:28)
      at Structure.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/buffer-layout/lib/Layout.js:1263:26)
      at Structure.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/buffer-layout/lib/Layout.js:1263:26)
      at BorshInstructionCoder._encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/coder/borsh/instruction.js:91:28)
      at BorshInstructionCoder.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/coder/borsh/instruction.js:76:21)
      at /home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/program/namespace/index.js:41:100
      at ix (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/program/namespace/instruction.js:50:23)
      at txFn (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/program/namespace/transaction.js:16:20)
      at Object.rpc [as addSignatory] (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/program/namespace/rpc.js:9:24)
      at Context.<anonymous> (tests/basic-1.js:82:23)
      at processImmediate (node:internal/timers:466:21)

  2) basic-1
       add multiple signatories:
     TypeError: Blob.encode[data] requires (length 4) Buffer as src
      at Blob.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/buffer-layout/lib/Layout.js:2321:13)
      at Structure.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/buffer-layout/lib/Layout.js:1263:26)
      at WrappedLayout.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/@project-serum/borsh/dist/lib/index.js:67:28)
      at Structure.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/buffer-layout/lib/Layout.js:1263:26)
      at /home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/buffer-layout/lib/Layout.js:1113:25
      at Array.reduce (<anonymous>)
      at Sequence.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/buffer-layout/lib/Layout.js:1112:22)
      at Structure.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/buffer-layout/lib/Layout.js:1263:26)
      at WrappedLayout.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/@project-serum/borsh/dist/lib/index.js:67:28)
      at Structure.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/node_modules/buffer-layout/lib/Layout.js:1263:26)
      at BorshInstructionCoder._encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/coder/borsh/instruction.js:91:28)
      at BorshInstructionCoder.encode (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/coder/borsh/instruction.js:76:21)
      at /home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/program/namespace/index.js:41:100
      at ix (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/program/namespace/instruction.js:50:23)
      at txFn (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/program/namespace/transaction.js:16:20)
      at Object.rpc [as addSignatories] (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/program/namespace/rpc.js:9:24)
      at Context.<anonymous> (tests/basic-1.js:120:23)
      at processImmediate (node:internal/timers:466:21)



error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

basic_1.json

{
  "version": "0.1.0",
  "name": "basic_1",
  "instructions": [
    {
      "name": "initialize",
      "accounts": [
        {
          "name": "myAccount",
          "isMut": true,
          "isSigner": true
        },
        {
          "name": "user",
          "isMut": true,
          "isSigner": true
        },
        {
          "name": "systemProgram",
          "isMut": false,
          "isSigner": false
        }
      ],
      "args": [
        {
          "name": "data",
          "type": "u64"
        }
      ]
    },
    {
      "name": "update",
      "accounts": [
        {
          "name": "myAccount",
          "isMut": true,
          "isSigner": false
        }
      ],
      "args": [
        {
          "name": "data",
          "type": "u64"
        }
      ]
    },
    {
      "name": "addSignatory",
      "accounts": [
        {
          "name": "myAccount",
          "isMut": true,
          "isSigner": false
        }
      ],
      "args": [
        {
          "name": "signatory",
          "type": {
            "defined": "Signatory"
          }
        }
      ]
    },
    {
      "name": "addSignatories",
      "accounts": [
        {
          "name": "myAccount",
          "isMut": true,
          "isSigner": false
        }
      ],
      "args": [
        {
          "name": "signatories",
          "type": {
            "vec": {
              "defined": "Signatory"
            }
          }
        }
      ]
    }
  ],
  "accounts": [
    {
      "name": "MyAccount",
      "type": {
        "kind": "struct",
        "fields": [
          {
            "name": "data",
            "type": "u64"
          },
          {
            "name": "projectId",
            "type": "u64"
          },
          {
            "name": "projectName",
            "type": "bytes"
          },
          {
            "name": "signatories",
            "type": {
              "vec": {
                "defined": "Signatory"
              }
            }
          }
        ]
      }
    }
  ],
  "types": [
    {
      "name": "Signatory",
      "type": {
        "kind": "struct",
        "fields": [
          {
            "name": "name",
            "type": "bytes"
          },
          {
            "name": "publicKey",
            "type": "publicKey"
          }
        ]
      }
    }
  ],
  "metadata": {
    "address": "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
  }
}

UPDATE 1

After changing the type to String as suggested by Jon C below - the test runs. However if I call the addSignatory method twice it fails with the error "Error: AnchorError caused by account: my_account. Error Code: AccountDidNotSerialize. Error Number: 3004. Error Message: Failed to serialize the account.".

Updated basic-1.js

it("add a single signatory", async () => {
    const myAccount = _myAccount;

    // #region update-test

    // The program to execute.
    const program = anchor.workspace.Basic1;
    const pubkey1 = anchor.web3.Keypair.generate();
    const pubkey2 = anchor.web3.Keypair.generate();
    const signatory1 =
    {
      name: "matt",
      public_key: pubkey1,
    };

    // Invoke the update rpc.
    await program.rpc.addSignatory(signatory1, {
      accounts: {
        myAccount: myAccount.publicKey,
      },
    });

    // Fetch the newly updated account.
    const account = await program.account.myAccount.fetch(myAccount.publicKey);
    //assert.ok(account.signatories.len().eq(new anchor.BN(1)));
    assert.equal(account.signatories.length, 1);


    const signatory2 =
    {
      name: "smith",
      public_key: pubkey2,
    };

    // Invoke the update rpc.
    await program.rpc.addSignatory(signatory2, {
      accounts: {
        myAccount: myAccount.publicKey,
      },
    });

    // Fetch the newly updated account.
    const account2 = await program.account.myAccount.fetch(myAccount.publicKey);
    //assert.ok(account.signatories.len().eq(new anchor.BN(1)));
    assert.equal(account2.signatories.length, 2);



    // Check it's state was mutated.
    assert.ok(account.data.eq(new anchor.BN(4321)));

    // #endregion update-test
  });

anchor test output

1) basic-1
       add a single signatory:
     Error: AnchorError caused by account: my_account. Error Code: AccountDidNotSerialize. Error Number: 3004. Error Message: Failed to serialize the account.
      at Function.parse (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/error.js:138:20)
      at translateError (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/error.js:224:37)
      at Object.rpc [as addSignatory] (/home/abcd/code/anchor/examples/tutorial/node_modules/@project-serum/anchor/dist/cjs/program/namespace/rpc.js:18:53)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
      at async Context.<anonymous> (tests/basic-1.js:101:5)

Update 2

So I figured some more things out, Thanks @JonC for the hint which helped me move forward.

  1. To call the rust methods you need to change the method name to snakecase in javascript so add_signatory will be called as addSignatory from JS.
  2. You need to add quotes around strings in JS before passing as arguments to the rust method. So you need to ensure that string variables are padded with '"' in the JS code.
  3. I am still not sure why this will not work but sending public keys as Strings from JS to Rust causes anchor test to fail with "Account cannot be serialized" error like described here. Still trying to figure this out.

Solution

  • The name field in your signatory is defined as a Vec<u8> on the backend, but you're passing in a string from the frontend. Try updating the Signatory type to:

    #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
    pub struct Signatory {
        pub name: String,
        pub public_key: Pubkey,
    }
    

    That way, the serializer will know to properly encode the name as a string, and not as bytes.