Mina Developer Book
  • What is this book about?
  • Module 1
    • Introduction
    • Why ZK?
    • Mathematics of Zero-Knowledge Proofs
    • OPTIONAL: Advanced Resources
  • Module 2
    • zkFibonacci
    • Mina Protocol
    • o1js
      • SmartContract
      • ZkProgram
      • AccountUpdate
      • Merkle Tree
    • End-to-End zkApp development
      • Mastermind introduction
      • Building the zkApp
      • Testing environment
  • Next steps
  • New to blockchain pack
Powered by GitBook
On this page
  1. Module 2
  2. End-to-End zkApp development

Testing environment

Now its time to test the game. Before all, lets test if the utils.ts work properly. Create a file named utils.test.ts to run the tests.

To build the file, use:

npm run build

For the tests, use:

npm run test
import {
  getClueFromGuess,
  separateCombinationDigits,
  validateCombination,
} from './utils';
import { Field } from 'o1js';

describe('Provable utilities - unit tests', () => {
  describe('Tests for separateCombinationDigits function', () => {
    it('should reject a 3-digit combination', () => {
      const combination = Field(123);
      const expectedErrorMessage =
        'The combination must be a four-digit Field!';
      expect(() => separateCombinationDigits(combination)).toThrowError(
        expectedErrorMessage
      );
    });

    it('should reject a 5-digit combination', () => {
      const combination = Field(12345);
      const expectedErrorMessage =
        'The combination must be a four-digit Field!';
      expect(() => separateCombinationDigits(combination)).toThrowError(
        expectedErrorMessage
      );
    });

    it('should return the correct separated digits - case 1', () => {
      const combination = Field(1234);
      const expectedDigits = [1, 2, 3, 4].map(Field);

      expect(separateCombinationDigits(combination)).toEqual(expectedDigits);
    });

    it('should return the correct separated digits - case 2', () => {
      const combination = Field(5678);
      const expectedDigits = [5, 6, 7, 8].map(Field);

      expect(separateCombinationDigits(combination)).toEqual(expectedDigits);
    });

    it('should return the correct separated digits - case 3', () => {
      const combination = Field(7185);
      const expectedDigits = [7, 1, 8, 5].map(Field);

      expect(separateCombinationDigits(combination)).toEqual(expectedDigits);
    });
  });
  describe('Tests for validateCombination function', () => {
    describe('InValid Combinations: contains 0', () => {
      // No need to check if the first digit is 0, as this would reduce the combination to a 3-digit value.
      it('should reject combination: second digit is 0', () => {
        const expectedErrorMessage = 'Combination digit 2 should not be zero!';
        const combination = [1, 0, 9, 8].map(Field);
        expect(() => validateCombination(combination)).toThrowError(
          expectedErrorMessage
        );
      });

      it('should reject combination: third digit is 0', () => {
        const expectedErrorMessage = 'Combination digit 3 should not be zero!';
        const combination = [7, 2, 0, 5].map(Field);
        expect(() => validateCombination(combination)).toThrowError(
          expectedErrorMessage
        );
      });

      it('should reject combination: fourth digit is 0', () => {
        const expectedErrorMessage = 'Combination digit 4 should not be zero!';
        const combination = [9, 1, 5, 0].map(Field);
        expect(() => validateCombination(combination)).toThrowError(
          expectedErrorMessage
        );
      });
    });

    describe('Invalid Combinations: Not unique digits', () => {
      it('should reject combination: second digit is not unique', () => {
        const expectedErrorMessage = 'Combination digit 2 is not unique!';
        const combination = [1, 1, 9, 3].map(Field);
        expect(() => validateCombination(combination)).toThrowError(
          expectedErrorMessage
        );
      });

      it('should reject combination: third digit is not unique', () => {
        const expectedErrorMessage = 'Combination digit 3 is not unique!';
        const combination = [2, 5, 5, 7].map(Field);
        expect(() => validateCombination(combination)).toThrowError(
          expectedErrorMessage
        );
      });

      it('should reject combination: fourth digit is not unique', () => {
        const expectedErrorMessage = 'Combination digit 4 is not unique!';
        const combination = [2, 7, 5, 2].map(Field);
        expect(() => validateCombination(combination)).toThrowError(
          expectedErrorMessage
        );
      });
    });

    describe('Valid Combinations', () => {
      it('should accept a valid combination: case 1', () => {
        const combination = [2, 7, 5, 3].map(Field);
        expect(() => validateCombination(combination)).not.toThrow();
      });

      it('should accept a valid combination: case 2', () => {
        const combination = [9, 8, 6, 4].map(Field);
        expect(() => validateCombination(combination)).not.toThrow();
      });

      it('should accept a valid combination: case 3', () => {
        const combination = [7, 1, 3, 5].map(Field);
        expect(() => validateCombination(combination)).not.toThrow();
      });
    });
  });

  describe('Tests for getClueFromGuess function', () => {
    it('should return the correct clue: 0 hits - 0 blows', () => {
      const solution = [1, 2, 3, 4].map(Field);
      const guess = [5, 7, 8, 9].map(Field);
      const clue = getClueFromGuess(guess, solution);

      expect(clue).toEqual([0, 0, 0, 0].map(Field));
    });

    it('should return the correct clue: 1 hits - 0 blows', () => {
      const solution = [1, 2, 3, 4].map(Field);
      const guess = [1, 7, 8, 9].map(Field);
      const clue = getClueFromGuess(guess, solution);

      expect(clue).toEqual([2, 0, 0, 0].map(Field));
    });

    it('should return the correct clue: 4 hits - 0 blows', () => {
      const solution = [1, 7, 3, 9].map(Field);
      const guess = [1, 7, 3, 9].map(Field);
      const clue = getClueFromGuess(guess, solution);

      expect(clue).toEqual([2, 2, 2, 2].map(Field));
    });

    it('should return the correct clue: 1 hits - 1 blows', () => {
      const guess = [1, 7, 8, 2].map(Field);
      const solution = [1, 2, 3, 4].map(Field);
      const clue = getClueFromGuess(guess, solution);

      expect(clue).toEqual([2, 0, 0, 1].map(Field));
    });

    it('should return the correct clue: 2 hits - 2 blows', () => {
      const guess = [5, 3, 2, 7].map(Field);
      const solution = [5, 2, 3, 7].map(Field);
      const clue = getClueFromGuess(guess, solution);

      expect(clue).toEqual([2, 1, 1, 2].map(Field));
    });

    it('should return the correct clue: 0 hits - 4 blows', () => {
      const guess = [1, 2, 3, 4].map(Field);
      const solution = [4, 3, 2, 1].map(Field);
      const clue = getClueFromGuess(guess, solution);

      expect(clue).toEqual([1, 1, 1, 1].map(Field));
    });
  });
});

Now as a one last step, you should add Mastermind.test.ts :

/**
 * This file contains integration tests for the Mastermind zkApp. The tests involve deploying, initializing,
 * and advancing the game logic in a controlled sequence.
 *
 * The integration tests cover various scenarios, including a case where the Code Breaker fails to solve the game
 * with `maxAttempts = 5` and another where the game is successfully solved.
 *
 * These tests focus on verifying:
 * - Method access control, ensuring zkApp method calls are restricted to the correct addresses (e.g., `makeGuess` restricted to the Code Breaker).
 * - Enforcement of method call frequency limits (e.g., ensuring `initGame` and `createGame` are executed only once).
 * - Correct method call sequence (e.g., verifying that `createGame` is called before `makeGuess`).
 * - Validation of input integrity, including checks on value ranges, sizes, and the correctness of hashes and salts.
 * - Accurate updates to the on-chain state following method executions.
 */

import { MastermindZkApp } from './Mastermind';
import { Field, Mina, PrivateKey, PublicKey, AccountUpdate, UInt8 } from 'o1js';
import { deserializeClue, compressCombinationDigits } from './utils';

let proofsEnabled = false;

async function localDeploy(
  zkapp: MastermindZkApp,
  deployerKey: PrivateKey,
  zkappPrivateKey: PrivateKey
) {
  const deployerAccount = deployerKey.toPublicKey();
  const tx = await Mina.transaction(deployerAccount, async () => {
    AccountUpdate.fundNewAccount(deployerAccount);
    zkapp.deploy();
  });

  await tx.prove();
  await tx.sign([deployerKey, zkappPrivateKey]).send();
}

async function initializeGame(
  zkapp: MastermindZkApp,
  deployerKey: PrivateKey,
  rounds: number
) {
  const deployerAccount = deployerKey.toPublicKey();

  // The deployer initializes the Mastermind zkapp
  const initTx = await Mina.transaction(deployerAccount, async () => {
    await zkapp.initGame(UInt8.from(rounds));
  });

  await initTx.prove();
  await initTx.sign([deployerKey]).send();
}

describe('Mastermind ZkApp Tests', () => {
  let codemasterKey: PrivateKey,
    codemasterPubKey: PublicKey,
    codemasterSalt: Field,
    codebreakerKey: PrivateKey,
    codebreakerPubKey: PublicKey,
    intruderKey: PrivateKey,
    zkappAddress: PublicKey,
    zkappPrivateKey: PrivateKey,
    zkapp: MastermindZkApp;

  beforeAll(async () => {
    if (proofsEnabled) await MastermindZkApp.compile();

    // Set up the Mina local blockchain
    const Local = await Mina.LocalBlockchain({ proofsEnabled });
    Mina.setActiveInstance(Local);

    // Local.testAccounts is an array of 10 test accounts that have been pre-filled with Mina
    codemasterKey = Local.testAccounts[0].key;
    codemasterPubKey = codemasterKey.toPublicKey();

    // Generate random field as salt for the codemaster
    codemasterSalt = Field.random();

    codebreakerKey = Local.testAccounts[1].key;
    codebreakerPubKey = codebreakerKey.toPublicKey();

    intruderKey = Local.testAccounts[2].key;

    // Set up the zkapp account
    zkappPrivateKey = PrivateKey.random();
    zkappAddress = zkappPrivateKey.toPublicKey();
    zkapp = new MastermindZkApp(zkappAddress);
  });

  describe('Deploy and initialize Mastermind zkApp', () => {
    it('Deploy a `Mastermind` zkApp', async () => {
      await localDeploy(zkapp, codemasterKey, zkappPrivateKey);
    });

    it('Should reject calling `createGame` method before `initGame`', async () => {
      const createGameTx = async () => {
        const tx = await Mina.transaction(codemasterPubKey, async () => {
          await zkapp.createGame(Field(1234), codemasterSalt);
        });

        await tx.prove();
        await tx.sign([codemasterKey]).send();
      };

      const expectedErrorMessage = 'The game has not been initialized yet!';
      await expect(createGameTx()).rejects.toThrowError(expectedErrorMessage);
    });

    it('Should reject calling `giveClue` method before `initGame`', async () => {
      const giveClueTx = async () => {
        const tx = await Mina.transaction(codemasterPubKey, async () => {
          await zkapp.giveClue(Field(1234), codemasterSalt);
        });

        await tx.prove();
        await tx.sign([codemasterKey]).send();
      };

      const expectedErrorMessage = 'The game has not been initialized yet!';
      await expect(giveClueTx()).rejects.toThrowError(expectedErrorMessage);
    });

    it('should reject calling `initGame` when maxAttempts exceeds 15', async () => {
      const initTx = async () => await initializeGame(zkapp, codemasterKey, 20);

      const expectedErrorMessage =
        'The maximum number of attempts allowed is 15!';
      await expect(initTx()).rejects.toThrowError(expectedErrorMessage);
    });

    it('should reject calling `initGame` when maxAttempts is below 5', async () => {
      const initTx = async () => await initializeGame(zkapp, codemasterKey, 4);

      const expectedErrorMessage =
        'The minimum number of attempts allowed is 5!';
      await expect(initTx()).rejects.toThrowError(expectedErrorMessage);
    });

    // This test verifies that the zkapp initial state values are correctly set up
    it('Initialize game', async () => {
      const maxAttempts = 5;
      await initializeGame(zkapp, codemasterKey, maxAttempts);

      // Initialized with `super.init()`
      const turnCount = zkapp.turnCount.get();
      expect(turnCount).toEqual(new UInt8(0));

      const codemasterId = zkapp.codemasterId.get();
      expect(codemasterId).toEqual(Field(0));

      const codebreakerId = zkapp.codebreakerId.get();
      expect(codebreakerId).toEqual(Field(0));

      const solutionHash = zkapp.solutionHash.get();
      expect(solutionHash).toEqual(Field(0));

      const unseparatedGuess = zkapp.unseparatedGuess.get();
      expect(unseparatedGuess).toEqual(Field(0));

      const serializedClue = zkapp.serializedClue.get();
      expect(serializedClue).toEqual(Field(0));

      // Initialized manually
      const rounds = zkapp.maxAttempts.get();
      expect(rounds).toEqual(UInt8.from(maxAttempts));

      const isSolved = zkapp.isSolved.get().toBoolean();
      expect(isSolved).toEqual(false);
    });
  });

  describe('createGame method tests', () => {
    async function testInvalidCreateGame(
      combination: number[],
      expectedErrorMessage?: string
    ) {
      const secretCombination = compressCombinationDigits(
        combination.map(Field)
      );

      const createGameTx = async () => {
        const tx = await Mina.transaction(codemasterPubKey, async () => {
          await zkapp.createGame(secretCombination, codemasterSalt);
        });

        await tx.prove();
        await tx.sign([codemasterKey]).send();
      };

      await expect(createGameTx()).rejects.toThrowError(expectedErrorMessage);
    }

    it('should reject calling `initGame` a second time', async () => {
      const initTx = async () => await initializeGame(zkapp, codemasterKey, 5);

      const expectedErrorMessage = 'The game has already been initialized!';
      await expect(initTx()).rejects.toThrowError(expectedErrorMessage);
    });

    it('should reject codemaster with invalid secret combination: second digit is 0', async () => {
      const expectedErrorMessage = 'Combination digit 2 should not be zero!';
      await testInvalidCreateGame([5, 0, 4, 6], expectedErrorMessage);
    });

    it('should reject codemaster with invalid secret combination: third digit is not unique', () => {
      const expectedErrorMessage = 'Combination digit 3 is not unique!';
      testInvalidCreateGame([2, 3, 2, 9], expectedErrorMessage);
    });

    // secretCombination = [1, 2, 3, 4]
    it('should create a game and update codemasterId & turnCount on-chain', async () => {
      const secretCombination = Field(1234);

      const createGameTx = await Mina.transaction(
        codemasterKey.toPublicKey(),
        async () => {
          zkapp.createGame(secretCombination, codemasterSalt);
        }
      );

      await createGameTx.prove();
      await createGameTx.sign([codemasterKey]).send();

      // Test that the on-chain states are updated
      const codemasterId = zkapp.codemasterId.get();
      expect(codemasterId).not.toEqual(Field(0));

      const turnCount = zkapp.turnCount.get().toNumber();
      expect(turnCount).toEqual(1);
    });

    it('should prevent players from re-creating a game: current codemaster included', async () => {
      const expectedErrorMessage = 'A mastermind game is already created!';
      testInvalidCreateGame([2, 3, 4, 5], expectedErrorMessage);
    });

    describe('makeGuess method tests: first guess', () => {
      async function testInvalidGuess(
        guess: number[],
        expectedErrorMessage?: string
      ) {
        const unseparatedGuess = compressCombinationDigits(guess.map(Field));

        const makeGuessTx = async () => {
          const tx = await Mina.transaction(
            codebreakerKey.toPublicKey(),
            async () => {
              await zkapp.makeGuess(unseparatedGuess);
            }
          );

          await tx.prove();
          await tx.sign([codebreakerKey]).send();
        };

        await expect(makeGuessTx()).rejects.toThrowError(expectedErrorMessage);
      }

      it('should reject codebreaker with invalid guess combination: fouth digit is 0', async () => {
        const expectedErrorMessage = 'Combination digit 4 should not be zero!';
        await testInvalidGuess([6, 9, 3, 0], expectedErrorMessage);
      });

      it('should reject codebreaker with invalid guess combination: second digit is not unique', async () => {
        const expectedErrorMessage = 'Combination digit 2 is not unique!';
        await testInvalidGuess([1, 1, 2, 9], expectedErrorMessage);
      });

      // validGuess = [1, 5, 6, 2]
      it('should accept codebreaker valid guess & update on-chain state', async () => {
        // Test that the codebreakerId is not updated yet
        const codebreakerId = zkapp.codebreakerId.get();
        expect(codebreakerId).toEqual(Field(0));

        const firstGuess = [1, 5, 6, 2];
        const unseparatedGuess = compressCombinationDigits(
          firstGuess.map(Field)
        );

        const makeGuessTx = await Mina.transaction(
          codebreakerPubKey,
          async () => {
            await zkapp.makeGuess(unseparatedGuess);
          }
        );

        await makeGuessTx.prove();
        await makeGuessTx.sign([codebreakerKey]).send();

        // Test that the on-chain states are updated
        const updatedCodebreakerId = zkapp.codebreakerId.get();
        expect(updatedCodebreakerId).not.toEqual(Field(0));

        const turnCount = zkapp.turnCount.get().toNumber();
        expect(turnCount).toEqual(2);
      });

      it('should reject the codebraker from calling this method if the clue from previous turn is not reported yet', async () => {
        const expectedErrorMessage =
          'Please wait for the codemaster to give you a clue!';
        await testInvalidGuess([1, 2, 2, 9], expectedErrorMessage);
      });
    });

    describe('giveClue method tests', () => {
      async function testInvalidClue(
        combination: number[],
        expectedErrorMessage?: string,
        signerKey = codemasterKey,
        signerSalt = codemasterSalt
      ) {
        const secretCombination = compressCombinationDigits(
          combination.map(Field)
        );

        const giveClueTx = async () => {
          const tx = await Mina.transaction(
            signerKey.toPublicKey(),
            async () => {
              await zkapp.giveClue(secretCombination, signerSalt);
            }
          );

          await tx.prove();
          await tx.sign([signerKey]).send();
        };

        await expect(giveClueTx()).rejects.toThrowError(expectedErrorMessage);
      }

      it('should reject any caller other than the codemaster', async () => {
        const expectedErrorMessage =
          'Only the codemaster of this game is allowed to give clue!';
        await testInvalidClue([1, 2, 3, 4], expectedErrorMessage, intruderKey);
      });

      it('should reject codemaster with different salt', async () => {
        const differentSalt = Field.random();
        const expectedErrorMessage =
          'The secret combination is not compliant with the stored hash on-chain!';
        await testInvalidClue(
          [1, 2, 3, 4],
          expectedErrorMessage,
          codemasterKey,
          differentSalt
        );
      });

      it('should reject codemaster with non-compliant secret combination', async () => {
        const expectedErrorMessage =
          'The secret combination is not compliant with the stored hash on-chain!';
        await testInvalidClue([1, 5, 3, 4], expectedErrorMessage);
      });

      it('should accept codemaster clue and update on-chain state', async () => {
        const solution = [1, 2, 3, 4];
        const unseparatedSolution = compressCombinationDigits(
          solution.map(Field)
        );

        const giveClueTx = await Mina.transaction(
          codemasterKey.toPublicKey(),
          async () => {
            zkapp.giveClue(unseparatedSolution, codemasterSalt);
          }
        );

        await giveClueTx.prove();
        await giveClueTx.sign([codemasterKey]).send();

        // Test that the on-chain states are updated: serializedClue, isSolved, and turnCount
        const serializedClue = zkapp.serializedClue.get();
        const clue = deserializeClue(serializedClue);
        expect(clue).toEqual([2, 0, 0, 1].map(Field));

        const isSolved = zkapp.isSolved.get().toBoolean();
        expect(isSolved).toEqual(false);

        const turnCount = zkapp.turnCount.get().toNumber();
        expect(turnCount).toEqual(3);
      });

      it('should reject the codemaster from calling this method out of sequence', async () => {
        const expectedErrorMessage =
          'Please wait for the codebreaker to make a guess!';
        await testInvalidClue([1, 2, 3, 4], expectedErrorMessage);
      });
    });

    describe('makeGuess method tests: second guess onwards', () => {
      async function testInvalidGuess(
        guess: number[],
        expectedErrorMessage?: string,
        signerKey = codebreakerKey
      ) {
        const unseparatedGuess = compressCombinationDigits(guess.map(Field));

        const makeGuessTx = async () => {
          const tx = await Mina.transaction(
            signerKey.toPublicKey(),
            async () => {
              await zkapp.makeGuess(unseparatedGuess);
            }
          );

          await tx.prove();
          await tx.sign([signerKey]).send();
        };

        await expect(makeGuessTx()).rejects.toThrowError(expectedErrorMessage);
      }

      it('should reject any caller other than the codebreaker', async () => {
        const expectedErrorMessage =
          'You are not the codebreaker of this game!';
        await testInvalidGuess([1, 4, 7, 2], expectedErrorMessage, intruderKey);
      });

      // validGuess2 = [1, 4, 7, 2]
      it('should accept another valid guess & update on-chain state', async () => {
        const secondGuess = [1, 4, 7, 2];
        const compactGuess = compressCombinationDigits(secondGuess.map(Field));

        const makeGuessTx = await Mina.transaction(
          codebreakerKey.toPublicKey(),
          async () => {
            await zkapp.makeGuess(compactGuess);
          }
        );

        await makeGuessTx.prove();
        await makeGuessTx.sign([codebreakerKey]).send();

        // Test that the on-chain states are updated
        const updatedCodebreakerId = zkapp.codebreakerId.get();
        expect(updatedCodebreakerId).not.toEqual(Field(0));

        const turnCount = zkapp.turnCount.get().toNumber();
        expect(turnCount).toEqual(4);
      });

      it('should reject the codebraker from calling this method out of sequence', async () => {
        const expectedErrorMessage =
          'Please wait for the codemaster to give you a clue!';
        await testInvalidGuess([1, 2, 4, 8], expectedErrorMessage);
      });
    });

    describe('test game to completion reaching number limit of attempts=5', () => {
      async function makeGuess(guess: number[]) {
        const unseparatedGuess = compressCombinationDigits(guess.map(Field));

        const makeGuessTx = await Mina.transaction(
          codebreakerKey.toPublicKey(),
          async () => {
            await zkapp.makeGuess(unseparatedGuess);
          }
        );

        await makeGuessTx.prove();
        await makeGuessTx.sign([codebreakerKey]).send();
      }

      async function giveClue(expectedClue: number[]) {
        const solution = [1, 2, 3, 4];
        const unseparatedSolution = compressCombinationDigits(
          solution.map(Field)
        );

        const giveClueTx = await Mina.transaction(
          codemasterKey.toPublicKey(),
          async () => {
            await zkapp.giveClue(unseparatedSolution, codemasterSalt);
          }
        );

        await giveClueTx.prove();
        await giveClueTx.sign([codemasterKey]).send();

        const serializedClue = zkapp.serializedClue.get();
        const clue = deserializeClue(serializedClue);
        expect(clue).toEqual(expectedClue.map(Field));
      }

      // Should give clue of second guess and then alternate guess/clue round till roundsLimit=5
      it('should give clue of second guess', async () => {
        await giveClue([2, 1, 0, 1]);
      });

      it('should make third guess', async () => {
        await makeGuess([1, 3, 4, 8]);
      });

      it('should give clue of third guess', async () => {
        await giveClue([2, 1, 1, 0]);
      });

      it('should make fourth guess', async () => {
        await makeGuess([5, 8, 3, 7]);
      });

      it('should give clue of fourth guess', async () => {
        await giveClue([0, 0, 2, 0]);
      });

      it('should make fifth guess', async () => {
        await makeGuess([9, 1, 2, 4]);
      });

      it('should give clue of fifth guess', async () => {
        await giveClue([0, 1, 1, 2]);
      });

      it('should reject 6th guess: reached limited number of attempts', async () => {
        const expectedErrorMessage =
          'You have reached the number limit of attempts to solve the secret combination!';
        await expect(makeGuess([1, 2, 3, 4])).rejects.toThrowError(
          expectedErrorMessage
        );
      });

      it('should reject giving 6th clue: reached limited number of attempts', async () => {
        const expectedErrorMessage =
          'The codebreaker has finished the number of attempts without solving the secret combination!';
        await expect(giveClue([2, 2, 2, 2])).rejects.toThrowError(
          expectedErrorMessage
        );
      });
    });
  });
});

describe('Deploy new Game and  block the game upon solving the secret combination', () => {
  let codemasterKey: PrivateKey,
    codebreakerKey: PrivateKey,
    zkappAddress: PublicKey,
    zkappPrivateKey: PrivateKey,
    zkapp: MastermindZkApp,
    codemasterSalt: Field;

  beforeAll(async () => {
    if (proofsEnabled) await MastermindZkApp.compile();

    // Set up the Mina local blockchain
    const Local = await Mina.LocalBlockchain({ proofsEnabled });
    Mina.setActiveInstance(Local);

    // Local.testAccounts is an array of 10 test accounts that have been pre-filled with Mina
    codemasterKey = Local.testAccounts[0].key;
    codebreakerKey = Local.testAccounts[1].key;

    // Set up the zkapp account
    zkappPrivateKey = PrivateKey.random();
    zkappAddress = zkappPrivateKey.toPublicKey();
    zkapp = new MastermindZkApp(zkappAddress);

    // Generate random field as salt for the codemaster
    codemasterSalt = Field.random();
  });

  async function makeGuess(guess: number[]) {
    const unseparatedGuess = compressCombinationDigits(guess.map(Field));

    const makeGuessTx = await Mina.transaction(
      codebreakerKey.toPublicKey(),
      async () => {
        await zkapp.makeGuess(unseparatedGuess);
      }
    );

    await makeGuessTx.prove();
    await makeGuessTx.sign([codebreakerKey]).send();
  }

  async function giveClue(expectedClue: number[]) {
    const solution = [7, 1, 6, 3];
    const unseparatedSolution = compressCombinationDigits(solution.map(Field));

    const giveClueTx = await Mina.transaction(
      codemasterKey.toPublicKey(),
      async () => {
        await zkapp.giveClue(unseparatedSolution, codemasterSalt);
      }
    );

    await giveClueTx.prove();
    await giveClueTx.sign([codemasterKey]).send();

    const serializedClue = zkapp.serializedClue.get();
    const clue = deserializeClue(serializedClue);
    expect(clue).toEqual(expectedClue.map(Field));
  }

  it('Generate and Deploy `Mastermind` smart contract', async () => {
    await localDeploy(zkapp, codemasterKey, zkappPrivateKey);
  });

  // Initialize the game to reset the game
  it('Initialize a new game', async () => {
    await initializeGame(zkapp, codemasterKey, 10);
  });

  // Create a new game
  it('should create a new game with new secret', async () => {
    const secretCombination = [7, 1, 6, 3];
    const compactSecretCombination = compressCombinationDigits(
      secretCombination.map(Field)
    );

    const createGameTx = await Mina.transaction(
      codemasterKey.toPublicKey(),
      async () => {
        await zkapp.createGame(compactSecretCombination, codemasterSalt);
      }
    );

    await createGameTx.prove();
    await createGameTx.sign([codemasterKey]).send();
  });

  it('should solve the game in the first round', async () => {
    await makeGuess([7, 1, 6, 3]);
  });

  it('should give clue and report that the secret is solved', async () => {
    await giveClue([2, 2, 2, 2]);

    const isSolved = zkapp.isSolved.get().toBoolean();
    expect(isSolved).toEqual(true);
  });

  it('should reject next guess: secret is already solved', async () => {
    const expectedErrorMessage =
      'You have already solved the secret combination!';
    await expect(makeGuess([7, 1, 6, 3])).rejects.toThrowError(
      expectedErrorMessage
    );
  });

  it('should reject next clue: secret is already solved', async () => {
    const expectedErrorMessage =
      'The codebreaker has already solved the secret combination!';
    await expect(giveClue([2, 2, 2, 2])).rejects.toThrowError(
      expectedErrorMessage
    );
  });
});

Here is the final step: running the tests to see if everything works fine. You've read all the code, navigaed to the source code explanations, read through all tests: Now its time to see if tests passes. Use:

npm run build

To build the code and

npm run test

To run the tests.

As a result, you should see those green lines that fills you with the euphoria of accomplishment. You deserved that.

PreviousBuilding the zkAppNextNext steps

Last updated 8 months ago