import * as anchor from "@coral-xyz/anchor"; import { Program, web3, BN, AnchorError, LangErrorCode, LangErrorMessage, translateError, parseIdlErrors, } from "@coral-xyz/anchor"; import { Optional } from "../target/types/optional"; import { AllowMissingOptionals } from "../target/types/allow_missing_optionals"; import { assert, expect } from "chai"; describe("Optional", () => { // configure the client to use the local cluster anchor.setProvider(anchor.AnchorProvider.env()); const anchorProvider = anchor.AnchorProvider.env(); const program = anchor.workspace.Optional as Program; const DATA_PDA_PREFIX = "data_pda"; const makeDataPdaSeeds = (dataAccount: web3.PublicKey) => { return [Buffer.from(DATA_PDA_PREFIX), dataAccount.toBuffer()]; }; const findDataPda = ( dataAccount: web3.PublicKey ): [web3.PublicKey, number] => { return web3.PublicKey.findProgramAddressSync( makeDataPdaSeeds(dataAccount), program.programId ); }; // payer of the transactions const payerWallet = (program.provider as anchor.AnchorProvider).wallet; const payer = payerWallet.publicKey; const systemProgram = web3.SystemProgram.programId; const requiredKeypair1 = web3.Keypair.generate(); const requiredKeypair2 = web3.Keypair.generate(); let createRequiredIx1: web3.TransactionInstruction; let createRequiredIx2: web3.TransactionInstruction; const dataAccountKeypair1 = web3.Keypair.generate(); const dataAccountKeypair2 = web3.Keypair.generate(); const dataPda1 = findDataPda(dataAccountKeypair1.publicKey); const dataPda2 = findDataPda(dataAccountKeypair2.publicKey); const initializeValue1 = new BN(10); const initializeValue2 = new BN(100); const initializeKey = web3.PublicKey.default; const createRequired = async ( requiredKeypair?: web3.Keypair ): Promise<[web3.Keypair, web3.TransactionInstruction]> => { const keypair = requiredKeypair ?? new web3.Keypair(); const createIx = await program.account.dataAccount.createInstruction( keypair ); return [keypair, createIx]; }; before("Setup async stuff", async () => { createRequiredIx1 = (await createRequired(requiredKeypair1))[1]; createRequiredIx2 = (await createRequired(requiredKeypair2))[1]; }); describe("Missing optionals feature tests", async () => { it("Fails with missing optional accounts at the end by default", async () => { const [requiredKeypair, createRequiredIx] = await createRequired(); const initializeIx = await program.methods .initialize(initializeValue1, initializeKey) .accounts({ payer: null, optionalAccount: null, systemProgram, required: requiredKeypair.publicKey, optionalPda: null, }) .signers([requiredKeypair]) .instruction(); initializeIx.keys.pop(); const initializeTxn = new web3.Transaction() .add(createRequiredIx) .add(initializeIx); try { await anchorProvider .sendAndConfirm(initializeTxn, [requiredKeypair]) .catch((e) => { throw translateError(e, parseIdlErrors(program.idl)); }); assert.fail( "Unexpected success in creating a transaction that should have failed with `AccountNotEnoughKeys` error" ); } catch (e) { // @ts-ignore assert.isTrue(e instanceof AnchorError, e.toString()); const err: AnchorError = e; const errorCode = LangErrorCode.AccountNotEnoughKeys; assert.strictEqual( err.error.errorMessage, LangErrorMessage.get(errorCode) ); assert.strictEqual(err.error.errorCode.number, errorCode); } }); it("Succeeds with missing optional accounts at the end with the feature on", async () => { const allowMissingOptionals = anchor.workspace .AllowMissingOptionals as Program; const doStuffIx = await allowMissingOptionals.methods .doStuff() .accounts({ payer, systemProgram, optional2: null, }) .instruction(); doStuffIx.keys.pop(); doStuffIx.keys.pop(); const doStuffTxn = new web3.Transaction().add(doStuffIx); await anchorProvider.sendAndConfirm(doStuffTxn); }); }); describe("Initialize tests", async () => { it("Initialize with required null fails anchor-ts validation", async () => { const [requiredKeypair, createRequiredIx] = await createRequired(); try { await program.methods .initialize(initializeValue1, initializeKey) .preInstructions([createRequiredIx]) .accounts({ payer, systemProgram, // @ts-ignore required: null, //requiredKeypair.publicKey, optionalPda: null, optionalAccount: null, }) .signers([requiredKeypair]) .rpc(); assert.fail( "Unexpected success in creating a transaction that should have failed at the client level" ); } catch (e) { const errMsg = "Invalid arguments: required not provided"; // @ts-ignore let error: string = e.toString(); assert(error.includes(errMsg), `Unexpected error: ${e}`); } }); it("Can initialize with no payer and no optionals", async () => { const [requiredKeypair, createRequiredIx] = await createRequired(); await program.methods .initialize(initializeValue1, initializeKey) .preInstructions([createRequiredIx]) .accounts({ payer: null, systemProgram, required: requiredKeypair.publicKey, optionalPda: null, optionalAccount: null, }) .signers([requiredKeypair]) .rpc(); let required = await program.account.dataAccount.fetch( requiredKeypair.publicKey ); expect(required.data.toNumber()).to.equal(0); }); it("Can initialize with no optionals", async () => { const [requiredKeypair, createRequiredIx] = await createRequired(); await program.methods .initialize(initializeValue1, initializeKey) .preInstructions([createRequiredIx]) .accounts({ payer: null, systemProgram: null, required: requiredKeypair.publicKey, optionalPda: null, optionalAccount: null, }) .signers([requiredKeypair]) .rpc(); let required = await program.account.dataAccount.fetch( requiredKeypair.publicKey ); expect(required.data.toNumber()).to.equal(0); }); it("Initialize with optionals and missing system program fails optional checks", async () => { const [requiredKeypair, createRequiredIx] = await createRequired(); const dataAccount = new web3.Keypair(); try { await program.methods .initialize(initializeValue1, initializeKey) .preInstructions([createRequiredIx]) .accounts({ payer, systemProgram: null, required: requiredKeypair.publicKey, optionalPda: null, optionalAccount: dataAccount.publicKey, }) .signers([requiredKeypair, dataAccount]) .rpc(); assert.fail( "Unexpected success in creating a transaction that should have failed with `ConstraintAccountIsNone` error" ); } catch (e) { // @ts-ignore assert.isTrue(e instanceof AnchorError, e.toString()); const err: AnchorError = e; const errorCode = LangErrorCode.ConstraintAccountIsNone; assert.strictEqual( err.error.errorMessage, LangErrorMessage.get(errorCode) ); assert.strictEqual(err.error.errorCode.number, errorCode); } }); it("Unwrapping None account in constraint panics", async () => { const [requiredKeypair, createRequiredIx] = await createRequired(); const dataAccount = new web3.Keypair(); const [dataPda] = findDataPda(dataAccount.publicKey); try { await program.methods .initialize(initializeValue1, initializeKey) .preInstructions([createRequiredIx]) .accounts({ payer, systemProgram, required: requiredKeypair.publicKey, optionalPda: dataPda, optionalAccount: null, }) .signers([requiredKeypair]) .rpc(); assert.fail( "Unexpected success in creating a transaction that should have failed with `ProgramFailedToComplete` error" ); } catch (e) { const errMsg = "Program failed to complete"; // @ts-ignore let error: string = e.toString(); assert(error.includes(errMsg), `Unexpected error: ${e}`); } }); it("Can initialize with required and optional account", async () => { await program.methods .initialize(initializeValue1, initializeKey) .preInstructions([createRequiredIx1]) .accounts({ payer, systemProgram, required: requiredKeypair1.publicKey, optionalPda: null, optionalAccount: dataAccountKeypair1.publicKey, }) .signers([requiredKeypair1, dataAccountKeypair1]) .rpc(); const requiredDataAccount = await program.account.dataAccount.fetch( requiredKeypair1.publicKey ); expect(requiredDataAccount.data.toNumber()).to.equal(0); const optionalDataAccount = await program.account.dataAccount.fetch( dataAccountKeypair1.publicKey ); expect(optionalDataAccount.data.toNumber()).to.equal( initializeValue1.muln(2).toNumber() ); }); it("Invalid seeds with all accounts provided fails", async () => { try { await program.methods .initialize(initializeValue2, initializeKey) .preInstructions([createRequiredIx2]) .accounts({ payer, systemProgram, required: requiredKeypair2.publicKey, optionalPda: dataPda1[0], optionalAccount: dataAccountKeypair2.publicKey, }) .signers([requiredKeypair2, dataAccountKeypair2]) .rpc(); assert.fail( "Unexpected success in creating a transaction that should have failed with `ConstraintSeeds` error" ); } catch (e) { // @ts-ignore assert.isTrue(e instanceof AnchorError, e.toString()); const err: AnchorError = e; const errorCode = LangErrorCode.ConstraintSeeds; assert.strictEqual( err.error.errorMessage, LangErrorMessage.get(errorCode) ); assert.strictEqual(err.error.errorCode.number, errorCode); } }); it("Can initialize with all accounts provided", async () => { await program.methods .initialize(initializeValue2, initializeKey) .preInstructions([createRequiredIx2]) .accounts({ payer, systemProgram, required: requiredKeypair2.publicKey, optionalPda: dataPda2[0], optionalAccount: dataAccountKeypair2.publicKey, }) .signers([requiredKeypair2, dataAccountKeypair2]) .rpc(); const requiredDataAccount = await program.account.dataAccount.fetch( requiredKeypair2.publicKey ); expect(requiredDataAccount.data.toNumber()).to.equal(0); const optionalDataAccount = await program.account.dataAccount.fetch( dataAccountKeypair2.publicKey ); expect(optionalDataAccount.data.toNumber()).to.equal( initializeValue2.toNumber() ); const optionalDataPda = await program.account.dataPda.fetch(dataPda2[0]); expect(optionalDataPda.dataAccount.toString()).to.equal( initializeKey.toString() ); }); }); describe("Update tests", async () => { it("Can update with invalid explicit pda bump with no pda", async () => { await program.methods .update(initializeValue2, initializeKey, dataPda2[1] - 1) .accounts({ payer, optionalPda: null, optionalAccount: null, }) .rpc(); }); it("Errors with invalid explicit pda bump with pda included", async () => { try { await program.methods .update(initializeValue2, initializeKey, dataPda2[1] - 1) .accounts({ payer, optionalPda: dataPda2[0], optionalAccount: dataAccountKeypair2.publicKey, }) .signers([dataAccountKeypair2]) .rpc(); assert.fail( "Unexpected success in creating a transaction that should have failed with `ConstraintSeeds` error" ); } catch (e) { // @ts-ignore assert.isTrue(e instanceof AnchorError, e.toString()); const err: AnchorError = e; const errorCode = LangErrorCode.ConstraintSeeds; assert.strictEqual( err.error.errorMessage, LangErrorMessage.get(errorCode) ); assert.strictEqual(err.error.errorCode.number, errorCode); } }); it("Fails with a missing signer", async () => { try { let txn = await program.methods .update(initializeValue2, initializeKey, dataPda2[1]) .accounts({ payer, optionalPda: dataPda2[0], optionalAccount: dataAccountKeypair2.publicKey, }) .transaction(); txn.instructions[0].keys.forEach((meta) => { if (meta.pubkey.equals(dataAccountKeypair2.publicKey)) { meta.isSigner = false; } }); await anchorProvider.sendAndConfirm(txn); assert.fail( "Unexpected success in creating a transaction that should have failed with `ConstraintSigner` error" ); } catch (e) { // @ts-ignore assert.isTrue(e instanceof web3.SendTransactionError, e.toString()); const err: web3.SendTransactionError = e; const anchorError = AnchorError.parse(err.logs!)!; const errorCode = LangErrorCode.ConstraintSigner; assert.strictEqual( anchorError.error.errorMessage, LangErrorMessage.get(errorCode) ); assert.strictEqual(anchorError.error.errorCode.number, errorCode); } }); it("Can trigger raw constraint violations with references to optional accounts", async () => { try { await program.methods .update(initializeValue2, initializeKey, dataPda2[1]) .accounts({ payer: null, optionalPda: dataPda2[0], optionalAccount: dataAccountKeypair2.publicKey, }) .signers([dataAccountKeypair2]) .rpc(); assert.fail( "Unexpected success in creating a transaction that should have failed with `ConstraintRaw` error" ); } catch (e) { // @ts-ignore assert.isTrue(e instanceof AnchorError, e.toString()); const err: AnchorError = e; const errorCode = LangErrorCode.ConstraintRaw; assert.strictEqual( err.error.errorMessage, LangErrorMessage.get(errorCode) ); assert.strictEqual(err.error.errorCode.number, errorCode); } }); it("Can update an optional account", async () => { await program.methods .update(initializeValue2.muln(3), initializeKey, dataPda2[1]) .accounts({ payer, optionalPda: null, optionalAccount: dataAccountKeypair2.publicKey, }) .signers([dataAccountKeypair2]) .rpc(); const dataAccount = await program.account.dataAccount.fetch( dataAccountKeypair2.publicKey ); expect(dataAccount.data.toNumber()).to.equal( initializeValue2.muln(3).toNumber() ); }); it("Can update both accounts", async () => { const newKey = web3.PublicKey.unique(); await program.methods .update(initializeValue2, newKey, dataPda2[1]) .accounts({ payer, optionalPda: dataPda2[0], optionalAccount: dataAccountKeypair2.publicKey, }) .signers([dataAccountKeypair2]) .rpc(); const dataPda = await program.account.dataPda.fetch(dataPda2[0]); expect(dataPda.dataAccount.toString()).to.equal(newKey.toString()); const dataAccount = await program.account.dataAccount.fetch( dataAccountKeypair2.publicKey ); expect(dataAccount.data.toNumber()).to.equal(initializeValue2.toNumber()); }); }); describe("Realloc tests", async () => { it("Realloc with no payer fails", async () => { try { await program.methods .realloc(new BN(100)) .accounts({ payer: null, required: dataAccountKeypair1.publicKey, optionalPda: null, optionalAccount: dataAccountKeypair2.publicKey, systemProgram, }) .signers([dataAccountKeypair2]) .rpc(); assert.fail( "Unexpected success in creating a transaction that should have failed with `ConstraintAccountIsNone` error" ); } catch (e) { // @ts-ignore assert.isTrue(e instanceof AnchorError, e.toString()); const err: AnchorError = e; const errorCode = LangErrorCode.ConstraintAccountIsNone; assert.strictEqual( err.error.errorMessage, LangErrorMessage.get(errorCode) ); assert.strictEqual(err.error.errorCode.number, errorCode); } }); it("Realloc with no system program fails", async () => { try { await program.methods .realloc(new BN(100)) .accounts({ payer, required: dataAccountKeypair1.publicKey, optionalPda: null, optionalAccount: dataAccountKeypair2.publicKey, systemProgram: null, }) .signers([dataAccountKeypair2]) .rpc(); assert.fail( "Unexpected success in creating a transaction that should have failed with `ConstraintAccountIsNone` error" ); } catch (e) { // @ts-ignore assert.isTrue(e instanceof AnchorError, e.toString()); const err: AnchorError = e; const errorCode = LangErrorCode.ConstraintAccountIsNone; assert.strictEqual( err.error.errorMessage, LangErrorMessage.get(errorCode) ); assert.strictEqual(err.error.errorCode.number, errorCode); } }); it("Wrong type of account is caught for optional accounts", async () => { try { await program.methods .realloc(new BN(100)) .accounts({ payer, required: dataAccountKeypair1.publicKey, optionalPda: dataAccountKeypair2.publicKey, optionalAccount: null, systemProgram, }) .rpc(); assert.fail( "Unexpected success in creating a transaction that should have failed with `AccountDiscriminatorMismatch` error" ); } catch (e) { // @ts-ignore assert.isTrue(e instanceof AnchorError, e.toString()); const err: AnchorError = e; const errorCode = LangErrorCode.AccountDiscriminatorMismatch; assert.strictEqual( err.error.errorMessage, LangErrorMessage.get(errorCode) ); assert.strictEqual(err.error.errorCode.number, errorCode); } }); it("Can realloc with optional accounts", async () => { const newLength = 100; await program.methods .realloc(new BN(newLength)) .accounts({ payer, required: dataAccountKeypair1.publicKey, optionalPda: null, optionalAccount: dataAccountKeypair2.publicKey, systemProgram, }) .signers([dataAccountKeypair2]) .rpc(); const dataAccount = await program.provider.connection.getAccountInfo( dataAccountKeypair2.publicKey ); assert.exists(dataAccount); expect(dataAccount!.data.length).to.equal(newLength); }); it("Can realloc back to original size with optional accounts", async () => { const newLength = program.account.dataAccount.size; await program.methods .realloc(new BN(newLength)) .accounts({ payer, required: dataAccountKeypair1.publicKey, optionalPda: null, optionalAccount: dataAccountKeypair2.publicKey, systemProgram, }) .signers([dataAccountKeypair2]) .rpc(); const dataAccount = await program.provider.connection.getAccountInfo( dataAccountKeypair2.publicKey ); assert.exists(dataAccount); expect(dataAccount!.data.length).to.equal(newLength); }); it("Can realloc multiple optional accounts", async () => { const newLength = 100; await program.methods .realloc(new BN(newLength)) .accounts({ payer, required: dataAccountKeypair1.publicKey, optionalPda: dataPda2[0], optionalAccount: dataAccountKeypair2.publicKey, systemProgram, }) .signers([dataAccountKeypair2]) .rpc(); const dataAccount = await program.provider.connection.getAccountInfo( dataAccountKeypair2.publicKey ); assert.exists(dataAccount); expect(dataAccount!.data.length).to.equal(newLength); const dataPda = await program.provider.connection.getAccountInfo( dataPda2[0] ); assert.exists(dataPda); expect(dataPda!.data.length).to.equal(newLength); }); }); describe("Close tests", async () => { const requiredKeypair3 = web3.Keypair.generate(); const requiredKeypair4 = web3.Keypair.generate(); let createRequiredIx3: web3.TransactionInstruction; let createRequiredIx4: web3.TransactionInstruction; const dataAccountKeypair3 = web3.Keypair.generate(); const dataAccountKeypair4 = web3.Keypair.generate(); const dataPda3 = findDataPda(dataAccountKeypair3.publicKey); const dataPda4 = findDataPda(dataAccountKeypair4.publicKey); const initializeValue3 = new BN(50); const initializeValue4 = new BN(1000); before("Setup additional accounts", async () => { createRequiredIx3 = (await createRequired(requiredKeypair3))[1]; createRequiredIx4 = (await createRequired(requiredKeypair4))[1]; const assertInitSuccess = async ( requiredPubkey: web3.PublicKey, dataPdaPubkey: web3.PublicKey, dataAccountPubkey: web3.PublicKey, initializeValue: BN ) => { const requiredDataAccount = await program.account.dataAccount.fetch( requiredPubkey ); expect(requiredDataAccount.data.toNumber()).to.equal(0); const optionalDataAccount = await program.account.dataAccount.fetch( dataAccountPubkey ); expect(optionalDataAccount.data.toNumber()).to.equal( initializeValue.toNumber() ); const optionalDataPda = await program.account.dataPda.fetch( dataPdaPubkey ); expect(optionalDataPda.dataAccount.toString()).to.equal( initializeKey.toString() ); }; await program.methods .initialize(initializeValue3, initializeKey) .preInstructions([createRequiredIx3]) .accounts({ payer, systemProgram, required: requiredKeypair3.publicKey, optionalPda: dataPda3[0], optionalAccount: dataAccountKeypair3.publicKey, }) .signers([requiredKeypair3, dataAccountKeypair3]) .rpc(); await assertInitSuccess( requiredKeypair3.publicKey, dataPda3[0], dataAccountKeypair3.publicKey, initializeValue3 ); await program.methods .initialize(initializeValue4, initializeKey) .preInstructions([createRequiredIx4]) .accounts({ payer, systemProgram, required: requiredKeypair4.publicKey, optionalPda: dataPda4[0], optionalAccount: dataAccountKeypair4.publicKey, }) .signers([requiredKeypair4, dataAccountKeypair4]) .rpc(); await assertInitSuccess( requiredKeypair4.publicKey, dataPda4[0], dataAccountKeypair4.publicKey, initializeValue4 ); await program.methods .update(initializeValue3, dataAccountKeypair3.publicKey, dataPda3[1]) .accounts({ payer, optionalPda: dataPda3[0], optionalAccount: dataAccountKeypair3.publicKey, }) .signers([dataAccountKeypair3]) .rpc(); const optionalPda3 = await program.account.dataPda.fetch(dataPda3[0]); expect(optionalPda3.dataAccount.toString()).to.equal( dataAccountKeypair3.publicKey.toString() ); await program.methods .update(initializeValue4, dataAccountKeypair4.publicKey, dataPda4[1]) .accounts({ payer, optionalPda: dataPda4[0], optionalAccount: dataAccountKeypair4.publicKey, }) .signers([dataAccountKeypair4]) .rpc(); const optionalPda4 = await program.account.dataPda.fetch(dataPda4[0]); expect(optionalPda4.dataAccount.toString()).to.equal( dataAccountKeypair4.publicKey.toString() ); }); it("Close with no close target fails", async () => { try { await program.methods .close() .accounts({ payer: null, optionalPda: null, dataAccount: dataAccountKeypair3.publicKey, systemProgram, }) .signers([dataAccountKeypair3]) .rpc(); assert.fail( "Unexpected success in creating a transaction that should have failed with `ConstraintRaw` error" ); } catch (e) { // @ts-ignore assert.isTrue(e instanceof AnchorError, e.toString()); const err: AnchorError = e; const errorCode = LangErrorCode.ConstraintAccountIsNone; assert.strictEqual( err.error.errorMessage, LangErrorMessage.get(errorCode) ); assert.strictEqual(err.error.errorCode.number, errorCode); } }); it("Has one constraints are caught with optional accounts", async () => { try { await program.methods .close() .accounts({ payer, optionalPda: dataPda4[0], dataAccount: dataAccountKeypair3.publicKey, systemProgram, }) .signers([dataAccountKeypair3]) .rpc(); assert.fail( "Unexpected success in creating a transaction that should have failed with `ConstraintHasOne` error" ); } catch (e) { // @ts-ignore assert.isTrue(e instanceof AnchorError, e.toString()); const err: AnchorError = e; const errorCode = LangErrorCode.ConstraintHasOne; assert.strictEqual( err.error.errorMessage, LangErrorMessage.get(errorCode) ); assert.strictEqual(err.error.errorCode.number, errorCode); } }); it("Can close an optional account", async () => { await program.methods .close() .accounts({ payer, optionalPda: null, dataAccount: dataAccountKeypair3.publicKey, systemProgram, }) .signers([dataAccountKeypair3]) .rpc(); const dataAccount = await program.provider.connection.getAccountInfo( dataAccountKeypair3.publicKey ); assert.isNull(dataAccount); }); it("Can close multiple optional accounts", async () => { await program.methods .close() .accounts({ payer, optionalPda: dataPda4[0], dataAccount: dataAccountKeypair4.publicKey, systemProgram, }) .signers([dataAccountKeypair4]) .rpc(); const dataAccount = await program.provider.connection.getAccountInfo( dataAccountKeypair4.publicKey ); assert.isNull(dataAccount); }); }); });