coral-xyz / anchor

⚓ Solana Sealevel Framework
https://anchor-lang.com
Apache License 2.0
3.72k stars 1.36k forks source link

AnchorProvider won't work with @solana/web3.js useWallet WalletContextState interface #1933

Open netpoe opened 2 years ago

netpoe commented 2 years ago

Using:

"@project-serum/anchor": "^0.24.2",
    "@solana/wallet-adapter-base": "^0.9.5",
    "@solana/wallet-adapter-react": "0.15.4",
    "@solana/wallet-adapter-react-ui": "^0.9.6",
    "@solana/wallet-adapter-wallets": "^0.16.1",
    "@solana/web3.js": "^1.42.0",

on a NextJS application, I'm trying to instantiate an AnchorProvider in order to sign a transaction, but Typescript complains about Wallet types mismatch.

Implementation:

import * as anchor from "@project-serum/anchor";
import { PublicKey, Transaction } from "@solana/web3.js";
import { WalletContextState } from "@solana/wallet-adapter-react";
import { useCallback, useEffect } from "react";

import { useSolanaWallet } from "context/wallet/solana/useSolanaWallet";

import { IDL } from "./coinflip";
import { UseSolanaProgramArgs } from "./useSolanaProgram.types";

const houseWallet: PublicKey = new PublicKey("Dmf5FjnsdNQsVf488btGi4iWsx4ra8YzPCxNSG9NorBr");

/**
 * NOTE: tried to override the interface, but then got an error regarding Error: Signature verification failed
 */
type SolanaWallet = WalletContextState & {
  publicKey: PublicKey;
  signTransaction(tx: Transaction): Promise<Transaction>;
  signAllTransactions(txs: Transaction[]): Promise<Transaction[]>;
};

export const useSolanaProgram = () => {
  const solanaWallet = useSolanaWallet();

  const getProvider = useCallback(
    () =>
      /**
       * NOTE: removing `as SolanaWallet` will throw: 
       * 
       * Argument of type 'WalletContextState' is not assignable to parameter of type 'Wallet'.
  Types of property 'signTransaction' are incompatible.
    Type '((transaction: Transaction) => Promise<Transaction>) | undefined' is not assignable to type '(tx: Transaction) => Promise<Transaction>'.
      Type 'undefined' is not assignable to type '(tx: Transaction) => Promise<Transaction>'.ts(2345)
       */
      new anchor.AnchorProvider(solanaWallet.connection, solanaWallet.wallet as SolanaWallet, {
        preflightCommitment: "processed",
      }),
    [solanaWallet.connection, solanaWallet.wallet],
  );

  useEffect(() => {
    (async () => {
      const programId = "52VzNo9N2x3cBPkkxLXsKq7dBxZ8nahhDzG3Uz7mD6XG";
      const provider = getProvider();
      const program = new anchor.Program(IDL, programId, provider);
      const game = new PublicKey(programId);

      try {
        const result = await program.methods
          .setupGame({ houseWallet })
          .accounts({
            game,
          })
          .rpc();

        console.log(result);
      } catch (error) {
        console.log(error);
      }
    })();
  }, [getProvider, solanaWallet.connection, solanaWallet.wallet]);

  const play = async ({ amount }: UseSolanaProgramArgs) => {
    let programId: string;

    switch (amount) {
      case "0.1":
        programId = "FUcFYhv9m54FjdUBiyrViqY6ds7rJfhox4EChHeKS9je";
        break;
      case "0.01":
        programId = "52VzNo9N2x3cBPkkxLXsKq7dBxZ8nahhDzG3Uz7mD6XG";
        break;
      default:
        programId = "52VzNo9N2x3cBPkkxLXsKq7dBxZ8nahhDzG3Uz7mD6XG";
        break;
    }

    const provider = getProvider();
    const program = new anchor.Program(IDL, programId, provider);
    const game = new PublicKey(programId);

    try {
      const result = await program.methods
        .flip()
        .accounts({
          game,
          player: solanaWallet.wallet.publicKey!,
          houseWallet,
        })
        .rpc();

      console.log(result);
    } catch (error) {
      console.log(error);
    }
  };

  return {
    play,
  };
};

Question, since the introduction of AnchorProvider, is there something we need to do in order for const wallet = useWallet() to be passed without errors? OR how to instantiate an AnchorProvider correctly on web with the dependencies' versions I specified?

netpoe commented 2 years ago

@paul-schaaf @sushi-shi would appreciate your input here since you worked on AnchorProvider last time. Thx!

Retamogordo commented 2 years ago

Facing the same issue here. Actually I could not import * as anchor from "@project-serum/anchor" in a React Typescript app, I do not know if this is somehow related to Webpack. Did you have any solution for your problem ? I feel I hit a wall here.

netpoe commented 2 years ago

Hi @Retamogordo My repo is private, but have a look at this, I made it work:

import * as anchor from "@project-serum/anchor";
import { PublicKey, Transaction } from "@solana/web3.js";
import { WalletContextState } from "@solana/wallet-adapter-react";
import { useCallback } from "react";

import { useSolanaWallet } from "context/wallet/solana/useSolanaWallet";

import { IDL } from "./coinflip";
import { UseSolanaProgramArgs } from "./useSolanaProgram.types";

const houseWallet = new PublicKey("Dmf5FjnsdNQsVf488btGi4iWsx4ra8YzPCxNSG9NorBr");
const { SystemProgram } = anchor.web3;

/**
 * NOTE: tried to override the interface, but then got an error regarding Error: Signature verification failed
 */
type SolanaWallet = WalletContextState & {
  publicKey: PublicKey;
  signTransaction(tx: Transaction): Promise<Transaction>;
  signAllTransactions(txs: Transaction[]): Promise<Transaction[]>;
};

const createPDA = async (account: PublicKey, programId: PublicKey) => {
  const [pdaPubKey] = await PublicKey.findProgramAddress(
    [anchor.utils.bytes.utf8.encode("player_account"), account.toBuffer()],
    programId,
  );

  return pdaPubKey;
};

export const useSolanaProgram = () => {
  const solanaWallet = useSolanaWallet();

  const getProvider = useCallback(
    () =>
      new anchor.AnchorProvider(
        solanaWallet.connection,
        solanaWallet.wallet as SolanaWallet,
        anchor.AnchorProvider.defaultOptions(),
      ),
    [solanaWallet.connection, solanaWallet.wallet],
  );

  /**
   *
   * Leave this commented. Each program needs to be initialized only once. We either write a separate file and run it with ts-node
   * or do this only once for each program 🤷
   */
  // useEffect(() => {
  //   (async () => {
  //     const GAME_COST = 10_000_000;
  //     const FEE = 0.034;

  //     const programId = "AndfDy8zyG2U6zyVDFD1WY4VPdqQ3WmsE5n8fMoYzM6K";
  //     const provider = getProvider();
  //     const program = new anchor.Program(IDL, programId, provider);
  //     const gameAccount = anchor.web3.Keypair.generate();

  //     const setupArgs = {
  //       amount: new anchor.BN(GAME_COST),
  //       feePercentage: FEE,
  //       houseWallet,
  //     };

  //     try {
  //       const result = await program.methods
  //         .setupGame(setupArgs)
  //         .accounts({
  //           game: gameAccount.publicKey,
  //           owner: solanaWallet.wallet.publicKey!,
  //           systemProgram: SystemProgram.programId,
  //         })
  //         .signers([gameAccount])
  //         .rpc();

  //       console.log(result);
  //     } catch (error) {
  //       console.log(error);
  //     }
  //   })();
  // }, [getProvider, solanaWallet.connection, solanaWallet.wallet]);

  const getProgram = ({ amount }: UseSolanaProgramArgs) => {
    let programId: string;
    let gameId: string;

    switch (amount) {
      case "0.1":
        programId = "AndfDy8zyG2U6zyVDFD1WY4VPdqQ3WmsE5n8fMoYzM6K";
        gameId = "3ea14ZiPoFuo8LqzFody2hfC8G3tr2DAUHBgairTBY3N";
        break;
      case "0.01":
        programId = "AndfDy8zyG2U6zyVDFD1WY4VPdqQ3WmsE5n8fMoYzM6K";
        gameId = "3ea14ZiPoFuo8LqzFody2hfC8G3tr2DAUHBgairTBY3N";
        break;
      default:
        programId = "AndfDy8zyG2U6zyVDFD1WY4VPdqQ3WmsE5n8fMoYzM6K";
        gameId = "3ea14ZiPoFuo8LqzFody2hfC8G3tr2DAUHBgairTBY3N";
        break;
    }

    const provider = getProvider();
    const program = new anchor.Program(IDL, programId, provider);
    const gameAccount = new anchor.web3.PublicKey(gameId);

    return { program, gameAccount };
  };

  const play = (args: UseSolanaProgramArgs): Promise<boolean> =>
    new Promise((resolve, reject) => {
      const { program, gameAccount } = getProgram(args);

      createPDA(solanaWallet.wallet.publicKey!, program.programId).then(async (playerAccount) => {
        try {
          const result = await program.methods
            .flip()
            .accounts({
              game: gameAccount,
              player: solanaWallet.wallet.publicKey!,
              playerData: playerAccount,
              houseWallet,
              systemProgram: SystemProgram.programId,
            })
            .rpc();

          console.log(result);

          // Wait for the tx to be finalized, otherwise it returns null at first
          setTimeout(async () => {
            const tx = await solanaWallet.connection.getTransaction(result, { commitment: "confirmed" });

            console.log(tx);

            const logMessage = tx!.meta!.logMessages![7];
            const flipResult = logMessage.slice(-4);

            // The result from the Solana program returns base64 booleans as "h" & "i". AQ== equals losing, AA== equals winning
            /**
             * logMessages: Array(9)
              0: "Program AndfDy8zyG2U6zyVDFD1WY4VPdqQ3WmsE5n8fMoYzM6K invoke [1]"
              1: "Program log: Instruction: Flip"
              2: "Program 11111111111111111111111111111111 invoke [2]"
              3: "Program 11111111111111111111111111111111 success"
              4: "Program 11111111111111111111111111111111 invoke [2]"
              5: "Program 11111111111111111111111111111111 success"
              6: "Program AndfDy8zyG2U6zyVDFD1WY4VPdqQ3WmsE5n8fMoYzM6K consumed 20491 of 200000 compute units"
              7: "Program return: AndfDy8zyG2U6zyVDFD1WY4VPdqQ3WmsE5n8fMoYzM6K AQ=="
              8: "Program AndfDy8zyG2U6zyVDFD1WY4VPdqQ3WmsE5n8fMoYzM6K success"
            */
            resolve(flipResult === "AA==");
          }, 3000);
        } catch (error) {
          console.log(error);
          reject(new Error("ERR_FLIP_FAILED"));
        }
      });
    });

  const getBalance = async (args: UseSolanaProgramArgs): Promise<string> => {
    const { program } = getProgram(args);

    try {
      const playerAccount = await createPDA(solanaWallet.wallet.publicKey!, program.programId);
      const playerDataState = await program.account.playerAccount.fetch(playerAccount);

      return playerDataState.balance.toString() === "0"
        ? "0.00"
        : (playerDataState.balance.toNumber() / anchor.web3.LAMPORTS_PER_SOL).toString();
    } catch (error) {
      console.log(error);
      throw new Error("ERR_GET_BALANCE_FAILED");
    }
  };

  const claim = async (args: UseSolanaProgramArgs): Promise<void> => {
    const { program } = getProgram(args);

    const playerAccount = await createPDA(solanaWallet.wallet.publicKey!, program.programId);

    try {
      const result = await program.methods
        .claim()
        .accounts({
          player: solanaWallet.wallet.publicKey!,
          playerData: playerAccount,
        })
        .rpc();

      const tx = await solanaWallet.connection.getTransaction(result, { commitment: "confirmed" });

      console.log(tx);
    } catch (error) {
      console.log(error);
      throw new Error("ERR_CLAIM_FAILED");
    }
  };

  return {
    play,
    claim,
    getBalance,
  };
};

Also these tests may be helpful:

import * as anchor from "@project-serum/anchor";
import { AnchorError, Program } from "@project-serum/anchor";
import { PublicKey } from "@solana/web3.js";
import { Coinflip } from "../target/types/coinflip";
import { expect } from "chai";

describe("coinflip", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.local());

  const program = anchor.workspace.Coinflip as Program<Coinflip>;
  const programProvider = program.provider as anchor.AnchorProvider;
  const ONE_SOL = anchor.web3.LAMPORTS_PER_SOL * 1;
  const GAME_COST = 10_000_000;
  const FEE = 0.034;

  async function setupGame(gameKeypair, houseWallet) {
    const setupArgs = {
      amount: new anchor.BN(GAME_COST),
      feePercentage: FEE,
      houseWallet: houseWallet.publicKey,
    };

    program.idl.types;

    return await program.methods
      .setupGame(setupArgs)
      .accounts({
        game: gameKeypair.publicKey,
      })
      .signers([gameKeypair])
      .rpc();
  }

  async function createAccount(coins) {
    const Keypair = anchor.web3.Keypair.generate();
    const signature = await programProvider.connection.requestAirdrop(
      Keypair.publicKey,
      anchor.web3.LAMPORTS_PER_SOL * coins,
    );
    await programProvider.connection.confirmTransaction(signature);
    return Keypair;
  }

  async function createPDA(account) {
    const [pdaPubKey, _] = await PublicKey.findProgramAddress(
      [anchor.utils.bytes.utf8.encode("player_account"), account.publicKey.toBuffer()],
      program.programId,
    );
    return pdaPubKey;
  }

  async function play(times, player, playerAccount, game, houseWallet) {
    for (let i = 0; i < times; i++) {
      await program.methods
        .flip()
        .accounts({
          player: player.publicKey,
          playerData: playerAccount,
          game: game.publicKey,
          houseWallet: houseWallet.publicKey,
        })
        .signers([player])
        .rpc();
    }
  }

  async function claim(playerAccount, playerData) {
    const playerDataState = await program.account.playerAccount.fetch(playerData);

    try {
      await program.methods
        .claim()
        .accounts({
          player: playerAccount.publicKey,
          playerData: playerData,
        })
        .signers([playerAccount])
        .rpc();

      if (playerDataState.balance.toString() == "0") {
        return expect.fail("Claim should be failed");
      }
    } catch (e) {
      expect(e.error.errorMessage).to.equal("ERR_CLAIM_FAILED");
    }
  }

  it("setup_game", async () => {
    // Accounts definitions
    const gameKeypair = await createAccount(1);
    const houseWallet = await createAccount(1);

    const tx = await setupGame(gameKeypair, houseWallet);

    console.log("Your transaction signature", tx);

    let gameState = await program.account.game.fetch(gameKeypair.publicKey);
    expect(gameState.isInitialized).to.equal(true);
    expect(gameState.shouldWin).to.equal(false);
    expect(gameState.feePercentage).to.equal(0.034);
    //expect(gameState.staticAmount).to.equal(1_000);
  });

  it("setup_game: ERR_ALREADY_INITIALIZED", async () => {
    // Accounts definitions
    const gameAccount = await createAccount(1);
    const houseAccount = await createAccount(1);

    // Create game
    await setupGame(gameAccount, houseAccount);

    try {
      await setupGame(gameAccount, houseAccount);

      return expect.fail("setupGame should be failed");
    } catch (error) {
      expect(error.logs.toString()).to.contains("already in use");
    }
  });

  it("play: ERR_INSUFFICIENT_FUNDS", async () => {
    // Accounts definitions
    const gameAccount = await createAccount(1);
    const houseAccount = await createAccount(1);

    const playerAccount = anchor.web3.Keypair.generate();
    const signature = await programProvider.connection.requestAirdrop(
      playerAccount.publicKey,
      1002240, // This only cover the account Rent but not for to play
    );
    await programProvider.connection.confirmTransaction(signature);

    const playerPDA = await createPDA(playerAccount);

    // Create game
    await setupGame(gameAccount, houseAccount);

    try {
      await program.methods
        .flip()
        .accounts({
          player: playerAccount.publicKey,
          playerData: playerPDA,
          game: gameAccount.publicKey,
          houseWallet: houseAccount.publicKey,
        })
        .signers([playerAccount])
        .rpc();

      return expect.fail("Flip should be failed");
    } catch (error) {
      expect(error).to.be.instanceOf(AnchorError);
      const err: AnchorError = error;
      expect(err.error.errorMessage).to.equal("ERR_INSUFFICIENT_FUNDS");
    }
  });

  it("play: flip x1 -> Lose", async () => {
    // Accounts definitions
    const gameAccount = await createAccount(1);
    const houseAccount = await createAccount(1);
    const playerAccount = await createAccount(1);
    const playerPDA = await createPDA(playerAccount);

    // Create game
    await setupGame(gameAccount, houseAccount);

    // Play 1 time
    const times = 1;
    await play(times, playerAccount, playerPDA, gameAccount, houseAccount);

    // Checking balances
    let playerDataState = await program.account.playerAccount.fetch(playerPDA);
    expect(playerDataState.balance.toString()).to.equal("0");
    var balance = await programProvider.connection.getBalance(houseAccount.publicKey);
    expect(balance).to.equal(ONE_SOL + FEE * GAME_COST * times);
    balance = await programProvider.connection.getBalance(gameAccount.publicKey);
    expect(balance).to.equal(ONE_SOL + (GAME_COST - FEE * GAME_COST));

    // Claim
    var oldPlayerBalance = await programProvider.connection.getBalance(playerAccount.publicKey);
    await claim(playerAccount, playerPDA);
    var newPlayerBalance = await programProvider.connection.getBalance(playerAccount.publicKey);
    expect(newPlayerBalance).to.equal(oldPlayerBalance);
  });

  it("play: flip x2 -> Win", async () => {
    // Accounts definitions
    const gameAccount = await createAccount(1);
    const houseAccount = await createAccount(1);
    const playerAccount = await createAccount(1);
    const playerPDA = await createPDA(playerAccount);

    // Create game
    await setupGame(gameAccount, houseAccount);

    // Play 2 times
    const times = 2;
    await play(times, playerAccount, playerPDA, gameAccount, houseAccount);

    // Checking balances
    let playerDataState = await program.account.playerAccount.fetch(playerPDA);
    expect(playerDataState.balance.toString()).to.equal(((GAME_COST - FEE * GAME_COST) * 2).toString());
    var balance = await programProvider.connection.getBalance(houseAccount.publicKey);
    expect(balance).to.equal(ONE_SOL + FEE * GAME_COST * times);
    balance = await programProvider.connection.getBalance(gameAccount.publicKey);
    expect(balance).to.equal(ONE_SOL);

    // Claim
    var oldPlayerBalance = await programProvider.connection.getBalance(playerAccount.publicKey);
    await claim(playerAccount, playerPDA);
    var newPlayerBalance = await programProvider.connection.getBalance(playerAccount.publicKey);
    expect(newPlayerBalance).to.equal(oldPlayerBalance + (GAME_COST - FEE * GAME_COST) * 2);

    playerDataState = await program.account.playerAccount.fetch(playerPDA);
    expect(playerDataState.balance.toString()).to.equal("0");
  });

  it("play: flip x3 -> Lose", async () => {
    // Accounts definitions
    const gameAccount = await createAccount(1);
    const houseAccount = await createAccount(1);
    const playerAccount = await createAccount(1);
    const playerPDA = await createPDA(playerAccount);

    // Create game
    await setupGame(gameAccount, houseAccount);

    // Play 3 times
    const times = 3;
    await play(times, playerAccount, playerPDA, gameAccount, houseAccount);

    // Checking balances
    let playerDataState = await program.account.playerAccount.fetch(playerPDA);
    expect(playerDataState.balance.toString()).to.equal(((GAME_COST - FEE * GAME_COST) * 2).toString());
    var balance = await programProvider.connection.getBalance(houseAccount.publicKey);
    expect(balance).to.equal(ONE_SOL + FEE * GAME_COST * times);
    var balance = await programProvider.connection.getBalance(gameAccount.publicKey);
    expect(balance).to.equal(ONE_SOL + (GAME_COST - FEE * GAME_COST));

    // Claim
    var oldPlayerBalance = await programProvider.connection.getBalance(playerAccount.publicKey);
    await claim(playerAccount, playerPDA);
    var newPlayerBalance = await programProvider.connection.getBalance(playerAccount.publicKey);
    expect(newPlayerBalance).to.equal(oldPlayerBalance + (GAME_COST - FEE * GAME_COST) * 2);

    playerDataState = await program.account.playerAccount.fetch(playerPDA);
    expect(playerDataState.balance.toString()).to.equal("0");
  });

  it("play: flip x4 -> Win", async () => {
    // Accounts definitions
    const gameAccount = await createAccount(1);
    const houseAccount = await createAccount(1);
    const playerAccount = await createAccount(1);
    const playerPDA = await createPDA(playerAccount);

    // Create game
    await setupGame(gameAccount, houseAccount);

    // Play 4 times
    const times = 4;
    await play(times, playerAccount, playerPDA, gameAccount, houseAccount);

    // Checking balances
    let playerDataState = await program.account.playerAccount.fetch(playerPDA);
    expect(playerDataState.balance.toString()).to.equal(((GAME_COST - FEE * GAME_COST) * 4).toString());
    var balance = await programProvider.connection.getBalance(houseAccount.publicKey);
    expect(balance).to.equal(ONE_SOL + FEE * GAME_COST * times);
    balance = await programProvider.connection.getBalance(gameAccount.publicKey);
    expect(balance).to.equal(ONE_SOL);

    // Claim
    var oldPlayerBalance = await programProvider.connection.getBalance(playerAccount.publicKey);
    await claim(playerAccount, playerPDA);
    var newPlayerBalance = await programProvider.connection.getBalance(playerAccount.publicKey);
    expect(newPlayerBalance).to.equal(oldPlayerBalance + (GAME_COST - FEE * GAME_COST) * 4);

    playerDataState = await program.account.playerAccount.fetch(playerPDA);
    expect(playerDataState.balance.toString()).to.equal("0");
  });
});
ares-dev05 commented 2 years ago

Is it possible to init Program without wallet connection? I want to get the program data with the private key.

ferdasonmez commented 8 months ago

Hi @netpoe, Thanks for sharing your code. I am trying to understand this. I had the same "AnchorProvider won't work with @solana/web3.js useWallet WalletContextState interface" problem and found this page. Anyway, I need help related to below line. How can I install this useSolanaWallet hook? Regards, Ferda import { useSolanaWallet } from "context/wallet/solana/useSolanaWallet";

ferdasonmez commented 8 months ago

@netpoe,

Hi @netpoe, Can you also please share the files related to below parts. I am new to Solana development. I do not know how to create the .types file. Is is an autogenerated file? Also I need to know how to pass program arguments so it would be useful if you can send the context whatever file.. There are many samples showing how to connect wallet etc. but this is the best code sample regarding how to call the solana program but for me still it did not work due to missing parts. If you do not want to share openly please write me on ferdaozdemir@gmail.com or answer on twitter @FerdaSonmez.

import { useSolanaWallet } from "context/wallet/solana/useSolanaWallet";

import { UseSolanaProgramArgs } from "./useSolanaProgram.types";

I would be very happy if you can help me on this.
Regards, Ferda

mehul-srivastava commented 3 months ago

Hi @netpoe, Thanks for sharing your code. I am trying to understand this. I had the same "AnchorProvider won't work with @solana/web3.js useWallet WalletContextState interface" problem and found this page. Anyway, I need help related to below line. How can I install this useSolanaWallet hook? Regards, Ferda import { useSolanaWallet } from "context/wallet/solana/useSolanaWallet";

You can instead use wallet object from the useAnchorWallet hook. After going through the docs, I figured out that the provider variable needs three args. The second arg is of importance here. This wallet actually comes from anchor and not from solana's in-built wallet adapter.

const wallet = useAnchorWallet()
new anchor.AnchorProvider(connection, wallet, anchor.AnchorProvider.defaultOptions())

I hope this will solve your issue