anchor/tests/optional/tests/optional.ts

846 lines
28 KiB
TypeScript

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<Optional>;
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 = <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<AllowMissingOptionals>;
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 = <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 = <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 = <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 = <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 = <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 = <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 = <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 = <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 = <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 = <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);
});
});
});