@project-serum/anchor#AnchorError TypeScript Examples

The following examples show how to use @project-serum/anchor#AnchorError. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: test-v1.ts    From protocol with Apache License 2.0 4 votes vote down vote up
describe('Protocol v1 test', () => {
  const program: anchor.Program = anchor.workspace.Dialect;
  const connection = program.provider.connection;

  describe('Metadata tests', () => {
    let owner: web3.Keypair;
    let writer: web3.Keypair;

    beforeEach(async () => {
      owner = (
        await createUser({
          requestAirdrop: true,
          createMeta: false,
        })
      ).user;
      writer = (
        await createUser({
          requestAirdrop: true,
          createMeta: false,
        })
      ).user;
    });

    it('Create user metadata object(s)', async () => {
      for (const member of [owner, writer]) {
        const metadata = await createMetadata(program, member);
        const gottenMetadata = await getMetadata(program, member.publicKey);
        expect(metadata).to.be.deep.eq(gottenMetadata);
      }
    });

    it('Owner deletes metadata', async () => {
      for (const member of [owner, writer]) {
        await createMetadata(program, member);
        await getMetadata(program, member.publicKey);
        await deleteMetadata(program, member);
        chai
          .expect(getMetadata(program, member.publicKey))
          .to.eventually.be.rejectedWith(Error);
      }
    });
  });

  describe('Dialect initialization tests', () => {
    let owner: web3.Keypair;
    let writer: web3.Keypair;
    let nonmember: web3.Keypair;

    let members: Member[] = [];

    beforeEach(async () => {
      owner = (
        await createUser({
          requestAirdrop: true,
          createMeta: true,
        })
      ).user;
      writer = (
        await createUser({
          requestAirdrop: true,
          createMeta: true,
        })
      ).user;
      nonmember = (
        await createUser({
          requestAirdrop: true,
          createMeta: false,
        })
      ).user;
      members = [
        {
          publicKey: owner.publicKey,
          scopes: [true, false], // owner, read-only
        },
        {
          publicKey: writer.publicKey,
          scopes: [false, true], // non-owner, read-write
        },
      ];
    });

    it('Confirm only each user (& dialect) can read encrypted device tokens', async () => {
      // TODO: Implement
      chai.expect(true).to.be.true;
    });

    it("Fail to create a dialect if the owner isn't a member with admin privileges", async () => {
      try {
        await createDialect(program, nonmember, members, true);
        chai.assert(
          false,
          "Creating a dialect whose owner isn't a member should fail.",
        );
      } catch (e) {
        chai.assert(
          (e as AnchorError).message.includes(
            'The dialect owner must be a member with admin privileges.',
          ),
        );
      }

      try {
        // TODO: write this in a nicer way
        await createDialect(program, writer, members, true);
        chai.assert(
          false,
          "Creating a dialect whose owner isn't a member should fail.",
        );
      } catch (e) {
        chai.assert(
          (e as AnchorError).message.includes(
            'The dialect owner must be a member with admin privileges.',
          ),
        );
      }
    });

    it('Fail to create a dialect for unsorted members', async () => {
      // use custom unsorted version of createDialect for unsorted members
      const unsortedMembers = members.sort(
        (a, b) => -a.publicKey.toBuffer().compare(b.publicKey.toBuffer()),
      );
      const [publicKey, nonce] = await getDialectProgramAddress(
        program,
        unsortedMembers,
      );
      // TODO: assert owner in members
      const keyedMembers = unsortedMembers.reduce(
        (ms, m, idx) => ({ ...ms, [`member${idx}`]: m.publicKey }),
        {},
      );
      chai
        .expect(
          program.rpc.createDialect(
            new anchor.BN(nonce),
            members.map((m) => m.scopes),
            {
              accounts: {
                dialect: publicKey,
                owner: owner.publicKey,
                ...keyedMembers,
                rent: anchor.web3.SYSVAR_RENT_PUBKEY,
                systemProgram: anchor.web3.SystemProgram.programId,
              },
              signers: [owner],
            },
          ),
        )
        .to.eventually.be.rejectedWith(Error);
    });

    it('Create encrypted dialect for 2 members, with owner and write scopes, respectively', async () => {
      const dialectAccount = await createDialect(program, owner, members, true);
      expect(dialectAccount.dialect.encrypted).to.be.true;
    });

    it('Create unencrypted dialect for 2 members, with owner and write scopes, respectively', async () => {
      const dialectAccount = await createDialect(
        program,
        owner,
        members,
        false,
      );
      expect(dialectAccount.dialect.encrypted).to.be.false;
    });

    it('Creates unencrypted dialect by default', async () => {
      const dialectAccount = await createDialect(program, owner, members);
      expect(dialectAccount.dialect.encrypted).to.be.false;
    });

    it('Fail to create a second dialect for the same members', async () => {
      chai
        .expect(createDialect(program, owner, members))
        .to.eventually.be.rejectedWith(Error);
    });

    it('Fail to create a dialect for duplicate members', async () => {
      const duplicateMembers = [
        { publicKey: owner.publicKey, scopes: [true, true] } as Member,
        { publicKey: owner.publicKey, scopes: [true, true] } as Member,
      ];
      chai
        .expect(createDialect(program, owner, duplicateMembers))
        .to.be.rejectedWith(Error);
    });

    it('Find a dialect for a given member pair, verify correct scopes.', async () => {
      await createDialect(program, owner, members);
      const dialect = await getDialectForMembers(program, members);
      members.every((m, i) =>
        expect(
          m.publicKey.equals(dialect.dialect.members[i].publicKey) &&
            m.scopes.every(
              (s, j) => s === dialect.dialect.members[i].scopes[j],
            ),
        ),
      );
    });

    it('Subscribe users to dialect', async () => {
      const dialect = await createDialect(program, owner, members);
      // owner subscribes themselves
      await subscribeUser(program, dialect, owner.publicKey, owner);
      // owner subscribes writer
      await subscribeUser(program, dialect, writer.publicKey, owner);
      const ownerMeta = await getMetadata(program, owner.publicKey);
      const writerMeta = await getMetadata(program, writer.publicKey);
      chai
        .expect(
          ownerMeta.subscriptions.filter((s) =>
            s.pubkey.equals(dialect.publicKey),
          ).length,
        )
        .to.equal(1);
      chai
        .expect(
          writerMeta.subscriptions.filter((s) =>
            s.pubkey.equals(dialect.publicKey),
          ).length,
        )
        .to.equal(1);
    });

    it('Should return list of dialects sorted by time desc', async () => {
      // given
      console.log('Creating users');
      const [user1, user2, user3] = await Promise.all([
        createUser({
          requestAirdrop: true,
          createMeta: true,
        }).then((it) => it.user),
        createUser({
          requestAirdrop: true,
          createMeta: true,
        }).then((it) => it.user),
        createUser({
          requestAirdrop: true,
          createMeta: true,
        }).then((it) => it.user),
      ]);
      console.log('Creating dialects');
      // create first dialect and subscribe users
      const dialect1 = await createDialectAndSubscribeAllMembers(
        program,
        user1,
        user2,
        false,
      );
      const dialect2 = await createDialectAndSubscribeAllMembers(
        program,
        user1,
        user3,
        false,
      );
      // when
      const afterCreatingDialects = await getDialects(program, user1);
      await sleep(3000); // wait a bit to avoid equal timestamp, since since we get utc seconds as a timestamp
      await sendMessage(
        program,
        dialect1,
        user1,
        'Dummy message to increment latest message timestamp',
      );
      const afterSendingMessageToDialect1 = await getDialects(program, user1);
      await sleep(3000); // wait a bit to avoid equal timestamp, since since we get utc seconds as a timestamp
      await sendMessage(
        program,
        dialect2,
        user1,
        'Dummy message to increment latest message timestamp',
      );
      const afterSendingMessageToDialect2 = await getDialects(program, user1);
      // then
      // assert dialects before sending messages
      chai
        .expect(afterCreatingDialects.map((it) => it.publicKey))
        .to.be.deep.eq([dialect2.publicKey, dialect1.publicKey]); // dialect 2 was created after dialect 1
      // assert dialects after sending message to first dialect
      chai
        .expect(afterSendingMessageToDialect1.map((it) => it.publicKey))
        .to.be.deep.eq([dialect1.publicKey, dialect2.publicKey]);
      // assert dialects after sending message to second dialect
      chai
        .expect(afterSendingMessageToDialect2.map((it) => it.publicKey))
        .to.be.deep.eq([dialect2.publicKey, dialect1.publicKey]);
    });

    it('Non-owners fail to delete the dialect', async () => {
      const dialect = await createDialect(program, owner, members);
      chai
        .expect(deleteDialect(program, dialect, writer))
        .to.eventually.be.rejectedWith(Error);
      chai
        .expect(deleteDialect(program, dialect, nonmember))
        .to.eventually.be.rejectedWith(Error);
    });

    it('Owner deletes the dialect', async () => {
      const dialect = await createDialect(program, owner, members);
      await deleteDialect(program, dialect, owner);
      chai
        .expect(getDialectForMembers(program, members))
        .to.eventually.be.rejectedWith(Error);
    });

    it('Fail to subscribe a user twice to the same dialect (silent, noop)', async () => {
      const dialect = await createDialect(program, owner, members);
      await subscribeUser(program, dialect, writer.publicKey, owner);
      const metadata = await getMetadata(program, writer.publicKey);
      // subscribed once
      chai
        .expect(
          metadata.subscriptions.filter((s) =>
            s.pubkey.equals(dialect.publicKey),
          ).length,
        )
        .to.equal(1);
      chai
        .expect(subscribeUser(program, dialect, writer.publicKey, owner))
        .to.be.rejectedWith(Error);
      // still subscribed just once
      chai
        .expect(
          metadata.subscriptions.filter((s) =>
            s.pubkey.equals(dialect.publicKey),
          ).length,
        )
        .to.equal(1);
    });
  });

  describe('Find dialects', () => {
    it('Can find all dialects filtering by user public key', async () => {
      // given
      const [user1, user2, user3] = await Promise.all([
        createUser({
          requestAirdrop: true,
          createMeta: false,
        }).then((it) => it.user),
        createUser({
          requestAirdrop: true,
          createMeta: false,
        }).then((it) => it.user),
        createUser({
          requestAirdrop: true,
          createMeta: false,
        }).then((it) => it.user),
      ]);
      const [user1User2Dialect, user1User3Dialect, user2User3Dialect] =
        await Promise.all([
          createDialect(program, user1, [
            {
              publicKey: user1.publicKey,
              scopes: [true, true],
            },
            {
              publicKey: user2.publicKey,
              scopes: [false, true],
            },
          ]),
          createDialect(program, user1, [
            {
              publicKey: user1.publicKey,
              scopes: [true, true],
            },
            {
              publicKey: user3.publicKey,
              scopes: [false, true],
            },
          ]),
          createDialect(program, user2, [
            {
              publicKey: user2.publicKey,
              scopes: [true, true],
            },
            {
              publicKey: user3.publicKey,
              scopes: [false, true],
            },
          ]),
        ]);
      // when
      const [
        user1Dialects,
        user2Dialects,
        user3Dialects,
        nonExistingUserDialects,
      ] = await Promise.all([
        findDialects(program, {
          userPk: user1.publicKey,
        }),
        findDialects(program, {
          userPk: user2.publicKey,
        }),
        findDialects(program, {
          userPk: user3.publicKey,
        }),
        findDialects(program, {
          userPk: anchor.web3.Keypair.generate().publicKey,
        }),
      ]);
      // then
      expect(
        user1Dialects.map((it) => it.publicKey),
      ).to.deep.contain.all.members([
        user1User2Dialect.publicKey,
        user1User3Dialect.publicKey,
      ]);
      expect(
        user2Dialects.map((it) => it.publicKey),
      ).to.deep.contain.all.members([
        user1User2Dialect.publicKey,
        user2User3Dialect.publicKey,
      ]);
      expect(
        user3Dialects.map((it) => it.publicKey),
      ).to.deep.contain.all.members([
        user2User3Dialect.publicKey,
        user1User3Dialect.publicKey,
      ]);
      expect(nonExistingUserDialects.length).to.be.eq(0);
    });
  });

  describe('Unencrypted messaging tests', () => {
    let owner: web3.Keypair;
    let writer: web3.Keypair;
    let nonmember: web3.Keypair;
    let members: Member[] = [];
    let dialect: DialectAccount;

    beforeEach(async () => {
      (owner = await createUser({
        requestAirdrop: true,
        createMeta: true,
      }).then((it) => it.user)),
        (writer = await createUser({
          requestAirdrop: true,
          createMeta: true,
        }).then((it) => it.user)),
        (nonmember = await createUser({
          requestAirdrop: true,
          createMeta: false,
        }).then((it) => it.user)),
        (members = [
          {
            publicKey: owner.publicKey,
            scopes: [true, false], // owner, read-only
          },
          {
            publicKey: writer.publicKey,
            scopes: [false, true], // non-owner, read-write
          },
        ]);
      dialect = await createDialect(program, owner, members, false);
    });

    it('Message sender and receiver can read the message text and time', async () => {
      // given
      const dialect = await getDialectForMembers(program, members);
      const text = generateRandomText(256);
      // when
      await sendMessage(program, dialect, writer, text);
      // then
      const senderDialect = await getDialectForMembers(
        program,
        dialect.dialect.members,
      );
      const message = senderDialect.dialect.messages[0];
      chai.expect(message.text).to.be.eq(text);
      chai
        .expect(senderDialect.dialect.lastMessageTimestamp)
        .to.be.eq(message.timestamp);
    });

    it('Anonymous user can read any of the messages', async () => {
      // given
      const senderDialect = await getDialectForMembers(program, members);
      const text = generateRandomText(256);
      await sendMessage(program, senderDialect, writer, text);
      // when / then
      const nonMemberDialect = await getDialectForMembers(
        program,
        dialect.dialect.members,
      );
      const message = nonMemberDialect.dialect.messages[0];
      chai.expect(message.text).to.be.eq(text);
      chai.expect(message.owner).to.be.deep.eq(writer.publicKey);
      chai
        .expect(nonMemberDialect.dialect.lastMessageTimestamp)
        .to.be.eq(message.timestamp);
    });

    it('New messages overwrite old, retrieved messages are in order.', async () => {
      // emulate ideal message alignment withing buffer
      const rawBufferSize = 8192;
      const messagesPerDialect = 16;
      const numMessages = messagesPerDialect * 2;
      const salt = 3;
      const targetRawMessageSize = rawBufferSize / messagesPerDialect - salt;
      const timestampSize = 4;
      const ownerMemberIdxSize = 1;
      const messageSerializationOverhead =
        ITEM_METADATA_OVERHEAD + timestampSize + ownerMemberIdxSize;
      const targetTextSize =
        targetRawMessageSize - messageSerializationOverhead;
      const texts = Array(numMessages)
        .fill(0)
        .map(() => generateRandomText(targetTextSize));
      for (let messageIdx = 0; messageIdx < numMessages; messageIdx++) {
        // verify last last N messages look correct
        const messageCounter = messageIdx + 1;
        const text = texts[messageIdx];
        const dialect = await getDialectForMembers(program, members);
        console.log(
          `Sending message ${messageCounter}/${texts.length}
    len = ${text.length}
    idx: ${dialect.dialect.nextMessageIdx}`,
        );
        await sendMessage(program, dialect, writer, text);
        const sliceStart =
          messageCounter <= messagesPerDialect
            ? 0
            : messageCounter - messagesPerDialect;
        const expectedMessagesCount = Math.min(
          messageCounter,
          messagesPerDialect,
        );
        const sliceEnd = sliceStart + expectedMessagesCount;
        const expectedMessages = texts.slice(sliceStart, sliceEnd).reverse();
        const d = await getDialect(program, dialect.publicKey);
        const actualMessages = d.dialect.messages.map((m) => m.text);
        console.log(`  msgs count after send: ${actualMessages.length}\n`);
        expect(actualMessages).to.be.deep.eq(expectedMessages);
      }
    });

    it('Message text limit of 853 bytes can be sent/received', async () => {
      const maxMessageSizeBytes = 853;
      const texts = Array(30)
        .fill(0)
        .map(() => generateRandomText(maxMessageSizeBytes));
      for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) {
        const text = texts[messageIdx];
        const messageCounter = messageIdx + 1;
        const dialect = await getDialectForMembers(program, members);
        console.log(
          `Sending message ${messageCounter}/${texts.length}
  len = ${text.length}
  idx: ${dialect.dialect.nextMessageIdx}`,
        );
        // when
        await sendMessage(program, dialect, writer, text);
        const d = await getDialect(program, dialect.publicKey);
        const actualMessages = d.dialect.messages;
        const lastMessage = actualMessages[0];
        console.log(`  msgs count after send: ${actualMessages.length}\n`);
        // then
        expect(lastMessage.text).to.be.deep.eq(text);
      }
    });
  });

  describe('Encrypted messaging tests', () => {
    let owner: web3.Keypair;
    let ownerEncryptionProps: EncryptionProps;
    let writer: web3.Keypair;
    let writerEncryptionProps: EncryptionProps;
    let nonmember: web3.Keypair;
    let nonmemberEncryptionProps: EncryptionProps;
    let members: Member[] = [];
    let dialect: DialectAccount;

    beforeEach(async () => {
      const ownerUser = await createUser({
        requestAirdrop: true,
        createMeta: true,
      });
      owner = ownerUser.user;
      ownerEncryptionProps = ownerUser.encryptionProps;
      const writerUser = await createUser({
        requestAirdrop: true,
        createMeta: true,
      });
      writer = writerUser.user;
      writerEncryptionProps = writerUser.encryptionProps;
      const nonmemberUser = await createUser({
        requestAirdrop: true,
        createMeta: false,
      });
      nonmember = nonmemberUser.user;
      nonmemberEncryptionProps = nonmemberUser.encryptionProps;
      members = [
        {
          publicKey: owner.publicKey,
          scopes: [true, false], // owner, read-only
        },
        {
          publicKey: writer.publicKey,
          scopes: [false, true], // non-owner, read-write
        },
      ];
      dialect = await createDialect(program, owner, members, true);
    });

    it('Message sender can send msg and then read the message text and time', async () => {
      // given
      const dialect = await getDialectForMembers(
        program,
        members,
        writerEncryptionProps,
      );
      const text = generateRandomText(256);
      // when
      await sendMessage(program, dialect, writer, text, writerEncryptionProps);
      // then
      const senderDialect = await getDialectForMembers(
        program,
        dialect.dialect.members,
        writerEncryptionProps,
      );
      const message = senderDialect.dialect.messages[0];
      chai.expect(message.text).to.be.eq(text);
      chai.expect(message.owner).to.be.deep.eq(writer.publicKey);
      chai
        .expect(senderDialect.dialect.lastMessageTimestamp)
        .to.be.eq(message.timestamp);
    });

    it('Message receiver can read the message text and time sent by sender', async () => {
      // given
      const senderDialect = await getDialectForMembers(
        program,
        members,
        writerEncryptionProps,
      );
      const text = generateRandomText(256);
      // when
      await sendMessage(
        program,
        senderDialect,
        writer,
        text,
        writerEncryptionProps,
      );
      // then
      const receiverDialect = await getDialectForMembers(
        program,
        dialect.dialect.members,
        ownerEncryptionProps,
      );
      const message = receiverDialect.dialect.messages[0];
      chai.expect(message.text).to.be.eq(text);
      chai.expect(message.owner).to.be.deep.eq(writer.publicKey);
      chai
        .expect(receiverDialect.dialect.lastMessageTimestamp)
        .to.be.eq(message.timestamp);
    });

    it("Non-member can't read (decrypt) any of the messages", async () => {
      // given
      const senderDialect = await getDialectForMembers(
        program,
        members,
        writerEncryptionProps,
      );
      const text = generateRandomText(256);
      await sendMessage(
        program,
        senderDialect,
        writer,
        text,
        writerEncryptionProps,
      );
      // when / then
      expect(
        getDialectForMembers(
          program,
          dialect.dialect.members,
          nonmemberEncryptionProps,
        ),
      ).to.eventually.be.rejected;
    });

    it('New messages overwrite old, retrieved messages are in order.', async () => {
      // emulate ideal message alignment withing buffer
      const rawBufferSize = 8192;
      const messagesPerDialect = 16;
      const numMessages = messagesPerDialect * 2;
      const salt = 3;
      const targetRawMessageSize = rawBufferSize / messagesPerDialect - salt;
      const timestampSize = 4;
      const ownerMemberIdxSize = 1;
      const messageSerializationOverhead =
        ITEM_METADATA_OVERHEAD +
        ENCRYPTION_OVERHEAD_BYTES +
        NONCE_SIZE_BYTES +
        timestampSize +
        ownerMemberIdxSize;
      const targetTextSize =
        targetRawMessageSize - messageSerializationOverhead;
      const texts = Array(numMessages)
        .fill(0)
        .map(() => generateRandomText(targetTextSize));
      for (let messageIdx = 0; messageIdx < numMessages; messageIdx++) {
        // verify last last N messages look correct
        const messageCounter = messageIdx + 1;
        const text = texts[messageIdx];
        const dialect = await getDialectForMembers(
          program,
          members,
          writerEncryptionProps,
        );
        console.log(
          `Sending message ${messageCounter}/${texts.length}
    len = ${text.length}
    idx: ${dialect.dialect.nextMessageIdx}`,
        );
        await sendMessage(
          program,
          dialect,
          writer,
          text,
          writerEncryptionProps,
        );
        const sliceStart =
          messageCounter <= messagesPerDialect
            ? 0
            : messageCounter - messagesPerDialect;
        const expectedMessagesCount = Math.min(
          messageCounter,
          messagesPerDialect,
        );
        const sliceEnd = sliceStart + expectedMessagesCount;
        const expectedMessages = texts.slice(sliceStart, sliceEnd).reverse();
        const d = await getDialect(
          program,
          dialect.publicKey,
          writerEncryptionProps,
        );
        const actualMessages = d.dialect.messages.map((m) => m.text);
        console.log(`  msgs count after send: ${actualMessages.length}\n`);
        expect(actualMessages).to.be.deep.eq(expectedMessages);
      }
    });

    it('Send/receive random size messages.', async () => {
      const texts = Array(32)
        .fill(0)
        .map(() => generateRandomText(randomInt(256, 512)));
      for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) {
        const text = texts[messageIdx];
        const messageCounter = messageIdx + 1;
        const dialect = await getDialectForMembers(
          program,
          members,
          writerEncryptionProps,
        );
        console.log(
          `Sending message ${messageCounter}/${texts.length}
    len = ${text.length}
    idx: ${dialect.dialect.nextMessageIdx}`,
        );
        // when
        await sendMessage(
          program,
          dialect,
          writer,
          text,
          writerEncryptionProps,
        );
        const d = await getDialect(
          program,
          dialect.publicKey,
          writerEncryptionProps,
        );
        const actualMessages = d.dialect.messages;
        const lastMessage = actualMessages[0];
        console.log(`  msgs count after send: ${actualMessages.length}\n`);
        // then
        expect(lastMessage.text).to.be.deep.eq(text);
      }
    });

    /* UTF-8 encoding summary:
     - ASCII characters are encoded using 1 byte
     - Roman, Greek, Cyrillic, Coptic, Armenian, Hebrew, Arabic characters are encoded using 2 bytes
     - Chinese and Japanese among others are encoded using 3 bytes
     - Emoji are encoded using 4 bytes
    A note about message length limit and summary:
     - len >= 814 hits max transaction size limit = 1232 bytes https://docs.solana.com/ru/proposals/transactions-v2
     - => best case: 813 symbols per msg (ascii only)
     - => worst case: 203 symbols (e.g. emoji only)
     - => average case depends on character set, see details below:
     ---- ASCII: ±800 characters
     ---- Roman, Greek, Cyrillic, Coptic, Armenian, Hebrew, Arabic: ± 406 characters
     ---- Chinese and japanese: ± 270 characters
     ---- Emoji: ± 203 characters*/
    it('Message text limit of 813 bytes can be sent/received', async () => {
      const maxMessageSizeBytes = 813;
      const texts = Array(30)
        .fill(0)
        .map(() => generateRandomText(maxMessageSizeBytes));
      for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) {
        const text = texts[messageIdx];
        const messageCounter = messageIdx + 1;
        const dialect = await getDialectForMembers(
          program,
          members,
          writerEncryptionProps,
        );
        console.log(
          `Sending message ${messageCounter}/${texts.length}
  len = ${text.length}
  idx: ${dialect.dialect.nextMessageIdx}`,
        );
        // when
        await sendMessage(
          program,
          dialect,
          writer,
          text,
          writerEncryptionProps,
        );
        const d = await getDialect(
          program,
          dialect.publicKey,
          writerEncryptionProps,
        );
        const actualMessages = d.dialect.messages;
        const lastMessage = actualMessages[0];
        console.log(`  msgs count after send: ${actualMessages.length}\n`);
        // then
        expect(lastMessage.text).to.be.deep.eq(text);
      }
    });

    it('2 writers can send a messages and read them when dialect state is linearized before sending msg', async () => {
      // given
      const writer1 = await createUser({
        requestAirdrop: true,
        createMeta: true,
      });
      const writer2 = await createUser({
        requestAirdrop: true,
        createMeta: true,
      });
      members = [
        {
          publicKey: writer1.user.publicKey,
          scopes: [true, true], // owner, read-only
        },
        {
          publicKey: writer2.user.publicKey,
          scopes: [false, true], // non-owner, read-write
        },
      ];
      await createDialect(program, writer1.user, members, true);
      // when
      let writer1Dialect = await getDialectForMembers(
        program,
        members,
        writer1.encryptionProps,
      );
      const writer1Text = generateRandomText(256);
      await sendMessage(
        program,
        writer1Dialect,
        writer1.user,
        writer1Text,
        writer1.encryptionProps,
      );
      let writer2Dialect = await getDialectForMembers(
        program,
        members,
        writer2.encryptionProps,
      ); // ensures dialect state linearization
      const writer2Text = generateRandomText(256);
      await sendMessage(
        program,
        writer2Dialect,
        writer2.user,
        writer2Text,
        writer2.encryptionProps,
      );

      writer1Dialect = await getDialectForMembers(
        program,
        members,
        writer1.encryptionProps,
      );
      writer2Dialect = await getDialectForMembers(
        program,
        members,
        writer2.encryptionProps,
      );

      // then check writer1 dialect state
      const message1Writer1 = writer1Dialect.dialect.messages[1];
      const message2Writer1 = writer1Dialect.dialect.messages[0];
      chai.expect(message1Writer1.text).to.be.eq(writer1Text);
      chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.user.publicKey);
      chai.expect(message2Writer1.text).to.be.eq(writer2Text);
      chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.user.publicKey);
      // then check writer2 dialect state
      const message1Writer2 = writer2Dialect.dialect.messages[1];
      const message2Writer2 = writer2Dialect.dialect.messages[0];
      chai.expect(message1Writer2.text).to.be.eq(writer1Text);
      chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.user.publicKey);
      chai.expect(message2Writer2.text).to.be.eq(writer2Text);
      chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.user.publicKey);
    });

    // This test was failing before changing nonce generation algorithm
    it('2 writers can send a messages and read them when dialect state is not linearized before sending msg', async () => {
      // given
      const writer1 = await createUser({
        requestAirdrop: true,
        createMeta: true,
      });
      const writer2 = await createUser({
        requestAirdrop: true,
        createMeta: true,
      });
      members = [
        {
          publicKey: writer1.user.publicKey,
          scopes: [true, true], // owner, read-only
        },
        {
          publicKey: writer2.user.publicKey,
          scopes: [false, true], // non-owner, read-write
        },
      ];
      await createDialect(program, writer1.user, members, true);
      // when
      let writer1Dialect = await getDialectForMembers(
        program,
        members,
        writer1.encryptionProps,
      );
      let writer2Dialect = await getDialectForMembers(
        program,
        members,
        writer2.encryptionProps,
      ); // ensures no dialect state linearization
      const writer1Text = generateRandomText(256);
      await sendMessage(
        program,
        writer1Dialect,
        writer1.user,
        writer1Text,
        writer1.encryptionProps,
      );
      const writer2Text = generateRandomText(256);
      await sendMessage(
        program,
        writer2Dialect,
        writer2.user,
        writer2Text,
        writer2.encryptionProps,
      );

      writer1Dialect = await getDialectForMembers(
        program,
        members,
        writer1.encryptionProps,
      );
      writer2Dialect = await getDialectForMembers(
        program,
        members,
        writer2.encryptionProps,
      );

      // then check writer1 dialect state
      const message1Writer1 = writer1Dialect.dialect.messages[1];
      const message2Writer1 = writer1Dialect.dialect.messages[0];
      chai.expect(message1Writer1.text).to.be.eq(writer1Text);
      chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.user.publicKey);
      chai.expect(message2Writer1.text).to.be.eq(writer2Text);
      chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.user.publicKey);
      // then check writer2 dialect state
      const message1Writer2 = writer2Dialect.dialect.messages[1];
      const message2Writer2 = writer2Dialect.dialect.messages[0];
      chai.expect(message1Writer2.text).to.be.eq(writer1Text);
      chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.user.publicKey);
      chai.expect(message2Writer2.text).to.be.eq(writer2Text);
      chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.user.publicKey);
    });
  });

  describe('Subscription tests', () => {
    let owner: web3.Keypair;
    let writer: web3.Keypair;

    beforeEach(async () => {
      owner = await createUser({
        requestAirdrop: true,
        createMeta: false,
      }).then((it) => it.user);
      writer = await createUser({
        requestAirdrop: true,
        createMeta: false,
      }).then((it) => it.user);
    });

    it('Can subscribe to events and receive them and unsubscribe', async () => {
      // given
      const eventsAccumulator: Event[] = [];
      const expectedEvents = 8;
      const countDownLatch = new CountDownLatch(expectedEvents);
      const subscription = await subscribeToEvents(program, async (it) => {
        console.log('event', it);
        countDownLatch.countDown();
        return eventsAccumulator.push(it);
      });
      // when
      await createMetadata(program, owner); // 1 event
      await createMetadata(program, writer); // 1 event
      const dialectAccount = await createDialectAndSubscribeAllMembers(
        program,
        owner,
        writer,
        false,
      ); // 3 events
      await deleteMetadata(program, owner); // 1 event
      await deleteMetadata(program, writer); // 1 event
      await deleteDialect(program, dialectAccount, owner); // 1 event
      await countDownLatch.await(5000);
      await subscription.unsubscribe();
      // events below should be ignored
      await createMetadata(program, owner);
      await createMetadata(program, writer);
      // then
      chai.expect(eventsAccumulator.length).to.be.eq(expectedEvents);
    });
  });

  async function createUser(
    { requestAirdrop, createMeta }: CreateUserOptions = {
      requestAirdrop: true,
      createMeta: true,
    },
  ) {
    const user = web3.Keypair.generate();
    if (requestAirdrop) {
      const airDropRequest = await connection.requestAirdrop(
        user.publicKey,
        10 * web3.LAMPORTS_PER_SOL,
      );
      await connection.confirmTransaction(airDropRequest);
    }
    if (createMeta) {
      await createMetadata(program, user);
    }
    const encryptionProps = {
      ed25519PublicKey: user.publicKey.toBytes(),
      diffieHellmanKeyPair: ed25519KeyPairToCurve25519({
        publicKey: user.publicKey.toBytes(),
        secretKey: user.secretKey,
      }),
    };
    return { user, encryptionProps };
  }
});
Example #2
Source File: burnWriterForQuote.ts    From psyoptions with Apache License 2.0 4 votes vote down vote up
// TODO: exercise a token so there are quote assets in the pool

describe("burnWriterForQuote", () => {
  const payer = anchor.web3.Keypair.generate();
  const mintAuthority = anchor.web3.Keypair.generate();
  const program = anchor.workspace.PsyAmerican as Program<PsyAmerican>;
  const provider = program.provider;

  const minter = anchor.web3.Keypair.generate();
  const exerciser = anchor.web3.Keypair.generate();

  let quoteToken: Token;
  let underlyingToken: Token;
  let writerToken: Token;
  let underlyingAmountPerContract: anchor.BN;
  let quoteAmountPerContract: anchor.BN;
  let optionMarketKey: PublicKey;
  let optionMarket: OptionMarketV2;
  let exerciseFeeKey: PublicKey;
  let optionMintAccount: Keypair;
  let writerTokenMintAccount: Keypair;
  let underlyingAssetPoolAccount: Keypair;
  let quoteAssetPoolAccount: Keypair;
  let remainingAccounts: AccountMeta[] = [];
  let instructions: TransactionInstruction[] = [];

  let minterWriterAcct: Keypair;
  let minterOptionAcct: Keypair;
  let minterUnderlyingAccount: Keypair;
  let minterQuoteAccount: Keypair;
  let size = new u64(1);

  before(async () => {
    // airdrop SOL to the payer and minter
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        payer.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        minter.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        exerciser.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
  });

  describe("Unexpired OptionMarket", () => {
    before(async () => {
      // Initialize a new OptionMarket
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        optionMarketKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {
        // set expiration to 2 seconds from now
        expiration: new anchor.BN(new Date().getTime() / 1000 + 600),
      }));
      writerToken = new Token(
        provider.connection,
        optionMarket.writerTokenMint,
        TOKEN_PROGRAM_ID,
        payer
      );
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new minter
      ({
        optionAccount: minterOptionAcct,
        underlyingAccount: minterUnderlyingAccount,
        writerTokenAccount: minterWriterAcct,
        quoteAccount: minterQuoteAccount,
      } = await createMinter(
        provider.connection,
        minter,
        mintAuthority,
        underlyingToken,
        new anchor.BN(100)
          .mul(optionMarket.underlyingAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.writerTokenMint,
        quoteToken
      ));
      // Mint a bunch of contracts to the minter
      await mintOptionsTx(
        program,
        minter,
        minterOptionAcct,
        minterWriterAcct,
        minterUnderlyingAccount,
        new anchor.BN(100),
        optionMarket
      );
    });
    beforeEach(async () => {
      size = new u64(1);
    });

    describe("No quotes in pool", () => {
      it("should error", async () => {
        try {
          await burnWriterForQuote(
            program,
            minter,
            size,
            optionMarket.key,
            optionMarket.writerTokenMint,
            minterWriterAcct.publicKey,
            optionMarket.quoteAssetPool,
            minterQuoteAccount.publicKey
          );
          assert.ok(false);
        } catch (err) {
          const errMsg = "Not enough assets in the quote asset pool";
          assert.equal((err as AnchorError).error.errorMessage, errMsg);
        }
      });
    });
    describe("someone has exercised", () => {
      before(async () => {
        const optionToken = new Token(
          provider.connection,
          optionMarket.optionMint,
          TOKEN_PROGRAM_ID,
          payer
        );
        // Create an exerciser
        const {
          optionAccount: exerciserOptionAcct,
          quoteAccount: exerciserQuoteAcct,
          underlyingAccount: exerciserUnderlyingAcct,
        } = await createExerciser(
          provider.connection,
          exerciser,
          mintAuthority,
          quoteToken,
          new anchor.BN(100)
            .mul(optionMarket.quoteAmountPerContract)
            .muln(2)
            .toNumber(),
          optionMarket.optionMint,
          optionMarket.underlyingAssetMint
        );
        // Transfer options to the exerciser
        await optionToken.transfer(
          minterOptionAcct.publicKey,
          exerciserOptionAcct.publicKey,
          minter,
          [],
          new u64(100)
        );
        // exercise 100 options so there's plenty of quote assets in the pool
        await exerciseOptionTx(
          program,
          new anchor.BN(100),
          optionMarket.key,
          optionMarket.optionMint,
          exerciser,
          exerciser,
          exerciserOptionAcct.publicKey,
          optionMarket.underlyingAssetPool,
          exerciserUnderlyingAcct.publicKey,
          optionMarket.quoteAssetPool,
          exerciserQuoteAcct.publicKey,
          [
            {
              pubkey: exerciseFeeKey,
              isWritable: true,
              isSigner: false,
            },
          ]
        );
      });
      describe("proper burn writer for quote", () => {
        it("should burn the WriteToken and transfer the quote assets", async () => {
          const writerMintBefore = await writerToken.getMintInfo();
          const writerQuoteBefore = await quoteToken.getAccountInfo(
            minterQuoteAccount.publicKey
          );
          try {
            await burnWriterForQuote(
              program,
              minter,
              size,
              optionMarket.key,
              optionMarket.writerTokenMint,
              minterWriterAcct.publicKey,
              optionMarket.quoteAssetPool,
              minterQuoteAccount.publicKey
            );
          } catch (err) {
            console.error((err as AnchorError).error.errorMessage);
            throw err;
          }
          const writerMintAfter = await writerToken.getMintInfo();
          const writerMintDiff = writerMintAfter.supply.sub(
            writerMintBefore.supply
          );
          assert.equal(writerMintDiff.toString(), size.neg().toString());

          const writerQuoteAfter = await quoteToken.getAccountInfo(
            minterQuoteAccount.publicKey
          );
          const writerQuoteDiff = writerQuoteAfter.amount.sub(
            writerQuoteBefore.amount
          );
          assert.equal(
            writerQuoteDiff.toString(),
            size.mul(quoteAmountPerContract).toString()
          );
        });
      });
      describe("Quote pool does not match OptionMarket", () => {
        let badQuotePool: Keypair;
        before(async () => {
          const { tokenAccount } = await initNewTokenAccount(
            provider.connection,
            payer.publicKey,
            underlyingToken.publicKey,
            payer
          );
          badQuotePool = tokenAccount;
        });
        it("should error", async () => {
          try {
            await burnWriterForQuote(
              program,
              minter,
              size,
              optionMarket.key,
              optionMarket.writerTokenMint,
              minterWriterAcct.publicKey,
              badQuotePool.publicKey,
              minterQuoteAccount.publicKey
            );
            assert.ok(false);
          } catch (err) {
            const errMsg =
              "Quote pool account does not match the value on the OptionMarket";
            assert.equal((err as AnchorError).error.errorMessage, errMsg);
          }
        });
      });
      describe("WriterToken mint does not match OptionMarket", () => {
        let badWriterMint: Keypair;
        before(async () => {
          const { mintAccount } = await initNewTokenMint(
            provider.connection,
            payer.publicKey,
            payer
          );
          badWriterMint = mintAccount;
        });
        it("should error", async () => {
          try {
            await burnWriterForQuote(
              program,
              minter,
              size,
              optionMarket.key,
              badWriterMint.publicKey,
              minterWriterAcct.publicKey,
              optionMarket.quoteAssetPool,
              minterQuoteAccount.publicKey
            );
            assert.ok(false);
          } catch (err) {
            const errMsg =
              "WriterToken mint does not match the value on the OptionMarket";
            assert.equal((err as AnchorError).error.errorMessage, errMsg);
          }
        });
      });
    });
  });
});
Example #3
Source File: closePosition.ts    From psyoptions with Apache License 2.0 4 votes vote down vote up
describe("closeOptionPosition", () => {
  const payer = anchor.web3.Keypair.generate();
  const mintAuthority = anchor.web3.Keypair.generate();
  const program = anchor.workspace.PsyAmerican as Program<PsyAmerican>;
  const provider = program.provider;

  const minter = anchor.web3.Keypair.generate();
  const exerciser = anchor.web3.Keypair.generate();

  let quoteToken: Token;
  let underlyingToken: Token;
  let underlyingAmountPerContract: anchor.BN;
  let optionMarket: OptionMarketV2;
  let remainingAccounts: AccountMeta[] = [];
  let instructions: TransactionInstruction[] = [];

  let minterWriterAcct: Keypair;
  let minterOptionAcct: Keypair;
  let minterUnderlyingAccount: Keypair;
  let size = new u64(1);

  before(async () => {
    // airdrop SOL to the payer and minter
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        payer.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        minter.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        exerciser.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
  });

  describe("Unexpired OptionMarket", () => {
    before(async () => {
      // Initialize a new OptionMarket
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {
        // set expiration to 2 seconds from now
        expiration: new anchor.BN(new Date().getTime() / 1000 + 600),
      }));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new minter
      ({
        optionAccount: minterOptionAcct,
        underlyingAccount: minterUnderlyingAccount,
        writerTokenAccount: minterWriterAcct,
      } = await createMinter(
        provider.connection,
        minter,
        mintAuthority,
        underlyingToken,
        new anchor.BN(100)
          .mul(optionMarket.underlyingAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.writerTokenMint,
        quoteToken
      ));
      // Mint a bunch of contracts to the minter
      await mintOptionsTx(
        program,
        minter,
        minterOptionAcct,
        minterWriterAcct,
        minterUnderlyingAccount,
        new anchor.BN(100),
        optionMarket
      );
    });
    beforeEach(async () => {
      size = new u64(1);
    });

    describe("proper close position", () => {
      it("should burn the WriteToken + OptionToken and transfer the underlying", async () => {
        const writerToken = new Token(
          provider.connection,
          optionMarket.writerTokenMint,
          TOKEN_PROGRAM_ID,
          payer
        );
        const optionToken = new Token(
          provider.connection,
          optionMarket.optionMint,
          TOKEN_PROGRAM_ID,
          payer
        );
        const writerMintBefore = await writerToken.getMintInfo();
        const optionMintBefore = await optionToken.getMintInfo();
        const minterUnderlyingBefore = await underlyingToken.getAccountInfo(
          minterUnderlyingAccount.publicKey
        );
        try {
          await closeOptionPosition(
            program,
            minter,
            size,
            optionMarket.key,
            optionMarket.writerTokenMint,
            minterWriterAcct.publicKey,
            optionMarket.optionMint,
            minterOptionAcct.publicKey,
            optionMarket.underlyingAssetPool,
            minterUnderlyingAccount.publicKey
          );
        } catch (err) {
          console.error((err as AnchorError).error.errorMessage);
          throw err;
        }
        const writerMintAfter = await writerToken.getMintInfo();
        const writerMintDiff = writerMintAfter.supply.sub(
          writerMintBefore.supply
        );
        assert.equal(writerMintDiff.toString(), size.neg().toString());

        const optionMintAfter = await optionToken.getMintInfo();
        const optionMintDiff = optionMintAfter.supply.sub(
          optionMintBefore.supply
        );
        assert.equal(optionMintDiff.neg().toString(), size.toString());

        const minterUnderlyingAfter = await underlyingToken.getAccountInfo(
          minterUnderlyingAccount.publicKey
        );
        const minterUnderlyingDiff = minterUnderlyingAfter.amount.sub(
          minterUnderlyingBefore.amount
        );
        assert.equal(
          minterUnderlyingDiff.toString(),
          size.mul(underlyingAmountPerContract).toString()
        );
      });
    });

    describe("WriterToken mint does not match OptionMarket", () => {
      let badWriterMint: Keypair;
      before(async () => {
        const { mintAccount } = await initNewTokenMint(
          provider.connection,
          payer.publicKey,
          payer
        );
        badWriterMint = mintAccount;
      });
      it("should error", async () => {
        try {
          await closeOptionPosition(
            program,
            minter,
            size,
            optionMarket.key,
            badWriterMint.publicKey,
            minterWriterAcct.publicKey,
            optionMarket.optionMint,
            minterOptionAcct.publicKey,
            optionMarket.underlyingAssetPool,
            minterUnderlyingAccount.publicKey
          );
          assert.ok(false);
        } catch (err) {
          const errorMsg =
            "WriterToken mint does not match the value on the OptionMarket";
          assert.equal((err as AnchorError).error.errorMessage, errorMsg);
        }
      });
    });

    describe("OptionToken mint does not match OptionMarket", () => {
      let badOptionMint: Keypair;
      before(async () => {
        const { mintAccount } = await initNewTokenMint(
          provider.connection,
          payer.publicKey,
          payer
        );
        badOptionMint = mintAccount;
      });
      it("should error", async () => {
        try {
          await closeOptionPosition(
            program,
            minter,
            size,
            optionMarket.key,
            optionMarket.writerTokenMint,
            minterWriterAcct.publicKey,
            badOptionMint.publicKey,
            minterOptionAcct.publicKey,
            optionMarket.underlyingAssetPool,
            minterUnderlyingAccount.publicKey
          );
          assert.ok(false);
        } catch (err) {
          const errorMsg =
            "OptionToken mint does not match the value on the OptionMarket";
          assert.equal((err as AnchorError).error.errorMessage, errorMsg);
        }
      });
    });

    describe("Underlying asset pool does not match OptionMarket", () => {
      let badUnderlyingPool: Keypair;
      before(async () => {
        const { tokenAccount } = await initNewTokenAccount(
          provider.connection,
          payer.publicKey,
          underlyingToken.publicKey,
          payer
        );
        badUnderlyingPool = tokenAccount;
      });
      it("should error", async () => {
        try {
          await closeOptionPosition(
            program,
            minter,
            size,
            optionMarket.key,
            optionMarket.writerTokenMint,
            minterWriterAcct.publicKey,
            optionMarket.optionMint,
            minterOptionAcct.publicKey,
            badUnderlyingPool.publicKey,
            minterUnderlyingAccount.publicKey
          );
          assert.ok(false);
        } catch (err) {
          const errorMsg =
            "Underlying pool account does not match the value on the OptionMarket";
          assert.equal((err as AnchorError).error.errorMessage, errorMsg);
        }
      });
    });
  });
  describe("Expired OptionMarket", () => {
    before(async () => {
      // Initialize a new OptionMarket
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {
        // set expiration to 2 seconds from now
        expiration: new anchor.BN(new Date().getTime() / 1000 + 3),
      }));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new minter
      ({
        optionAccount: minterOptionAcct,
        underlyingAccount: minterUnderlyingAccount,
        writerTokenAccount: minterWriterAcct,
      } = await createMinter(
        provider.connection,
        minter,
        mintAuthority,
        underlyingToken,
        new anchor.BN(100)
          .mul(optionMarket.underlyingAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.writerTokenMint,
        quoteToken
      ));
      // Mint a bunch of contracts to the minter
      await mintOptionsTx(
        program,
        minter,
        minterOptionAcct,
        minterWriterAcct,
        minterUnderlyingAccount,
        new anchor.BN(100),
        optionMarket
      );
      // Wait till the market is expired
      await wait(3000);
    });
    beforeEach(async () => {
      size = new u64(1);
    });

    describe("proper close position", () => {
      it("should burn the WriteToken + OptionToken and transfer the underlying", async () => {
        const writerToken = new Token(
          provider.connection,
          optionMarket.writerTokenMint,
          TOKEN_PROGRAM_ID,
          payer
        );
        const optionToken = new Token(
          provider.connection,
          optionMarket.optionMint,
          TOKEN_PROGRAM_ID,
          payer
        );
        const writerMintBefore = await writerToken.getMintInfo();
        const optionMintBefore = await optionToken.getMintInfo();
        const minterUnderlyingBefore = await underlyingToken.getAccountInfo(
          minterUnderlyingAccount.publicKey
        );
        try {
          await closeOptionPosition(
            program,
            minter,
            size,
            optionMarket.key,
            optionMarket.writerTokenMint,
            minterWriterAcct.publicKey,
            optionMarket.optionMint,
            minterOptionAcct.publicKey,
            optionMarket.underlyingAssetPool,
            minterUnderlyingAccount.publicKey
          );
        } catch (err) {
          console.error((err as AnchorError).error.errorMessage);
          throw err;
        }
        const writerMintAfter = await writerToken.getMintInfo();
        const writerMintDiff = writerMintAfter.supply.sub(
          writerMintBefore.supply
        );
        assert.equal(writerMintDiff.toString(), size.neg().toString());

        const optionMintAfter = await optionToken.getMintInfo();
        const optionMintDiff = optionMintAfter.supply.sub(
          optionMintBefore.supply
        );
        assert.equal(optionMintDiff.neg().toString(), size.toString());

        const minterUnderlyingAfter = await underlyingToken.getAccountInfo(
          minterUnderlyingAccount.publicKey
        );
        const minterUnderlyingDiff = minterUnderlyingAfter.amount.sub(
          minterUnderlyingBefore.amount
        );
        assert.equal(
          minterUnderlyingDiff.toString(),
          size.mul(underlyingAmountPerContract).toString()
        );
      });
    });
  });
});
Example #4
Source File: closePostExpiration.ts    From psyoptions with Apache License 2.0 4 votes vote down vote up
describe("closePostExpiration", () => {
  const payer = anchor.web3.Keypair.generate();
  const mintAuthority = anchor.web3.Keypair.generate();
  const program = anchor.workspace.PsyAmerican as Program<PsyAmerican>;
  const provider = program.provider;

  const minter = anchor.web3.Keypair.generate();
  const exerciser = anchor.web3.Keypair.generate();

  let quoteToken: Token;
  let underlyingToken: Token;
  let optionToken: Token;
  let underlyingAmountPerContract: anchor.BN;
  let optionMarket: OptionMarketV2;
  let exerciserOptionAcct: Keypair;
  let remainingAccounts: AccountMeta[] = [];
  let instructions: TransactionInstruction[] = [];

  let minterWriterAcct: Keypair;
  let minterOptionAcct: Keypair;
  let minterUnderlyingAccount: Keypair;
  let size = new u64(1);

  before(async () => {
    // airdrop SOL to the payer and minter
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        payer.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        minter.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        exerciser.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
  });

  describe("Expired OptionMarket", () => {
    before(async () => {
      // Initialize a new OptionMarket
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        optionMarket,
        optionToken,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {
        // set expiration to 2 seconds from now
        expiration: new anchor.BN(new Date().getTime() / 1000 + 3),
      }));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new minter
      ({
        optionAccount: minterOptionAcct,
        underlyingAccount: minterUnderlyingAccount,
        writerTokenAccount: minterWriterAcct,
      } = await createMinter(
        provider.connection,
        minter,
        mintAuthority,
        underlyingToken,
        new anchor.BN(100)
          .mul(optionMarket.underlyingAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.writerTokenMint,
        quoteToken
      ));
      // Mint a bunch of contracts to the minter
      await mintOptionsTx(
        program,
        minter,
        minterOptionAcct,
        minterWriterAcct,
        minterUnderlyingAccount,
        new anchor.BN(100),
        optionMarket
      );
      // Create an exerciser
      ({ optionAccount: exerciserOptionAcct } = await createExerciser(
        provider.connection,
        exerciser,
        mintAuthority,
        quoteToken,
        new anchor.BN(100)
          .mul(optionMarket.quoteAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.underlyingAssetMint
      ));

      // Transfer a options to the exerciser
      await optionToken.transfer(
        minterOptionAcct.publicKey,
        exerciserOptionAcct.publicKey,
        minter,
        [],
        new u64(100)
      );
      // Wait so the market is expired
      await wait(3000);
    });
    beforeEach(async () => {
      size = new u64(1);
    });

    describe("proper close post expiration", () => {
      it("should burn the WriteToken and transfer the underlying", async () => {
        const writerToken = new Token(
          provider.connection,
          optionMarket.writerTokenMint,
          TOKEN_PROGRAM_ID,
          payer
        );
        const writerMintBefore = await writerToken.getMintInfo();
        const minterUnderlyingBefore = await underlyingToken.getAccountInfo(
          minterUnderlyingAccount.publicKey
        );
        try {
          await closePostExpiration(
            program,
            minter,
            size,
            optionMarket.key,
            optionMarket.writerTokenMint,
            minterWriterAcct.publicKey,
            optionMarket.underlyingAssetPool,
            minterUnderlyingAccount.publicKey
          );
        } catch (err) {
          console.error((err as AnchorError).error.errorMessage);
          throw err;
        }
        const writerMintAfter = await writerToken.getMintInfo();
        const writerMintDiff = writerMintAfter.supply.sub(
          writerMintBefore.supply
        );
        assert.equal(writerMintDiff.toString(), size.neg().toString());

        const minterUnderlyingAfter = await underlyingToken.getAccountInfo(
          minterUnderlyingAccount.publicKey
        );
        const minterUnderlyingDiff = minterUnderlyingAfter.amount.sub(
          minterUnderlyingBefore.amount
        );
        assert.equal(
          minterUnderlyingDiff.toString(),
          size.mul(underlyingAmountPerContract).toString()
        );
      });
    });
    describe("underlying asset pool does not match the OptionMarket", () => {
      let badUnderlyingPool: Keypair;
      before(async () => {
        const { tokenAccount } = await initNewTokenAccount(
          provider.connection,
          payer.publicKey,
          underlyingToken.publicKey,
          payer
        );
        badUnderlyingPool = tokenAccount;
      });
      it("should error", async () => {
        try {
          await closePostExpiration(
            program,
            minter,
            size,
            optionMarket.key,
            optionMarket.writerTokenMint,
            minterWriterAcct.publicKey,
            badUnderlyingPool.publicKey,
            minterUnderlyingAccount.publicKey
          );
          assert.ok(false);
        } catch (err) {
          const errorMsg =
            "Underlying pool account does not match the value on the OptionMarket";
          assert.equal((err as AnchorError).error.errorMessage, errorMsg);
        }
      });
    });
    describe("Validate the writer mint is the same as the OptionMarket", () => {
      let badWriterMint: Keypair;
      before(async () => {
        const { mintAccount } = await initNewTokenMint(
          provider.connection,
          payer.publicKey,
          payer
        );
        badWriterMint = mintAccount;
      });
      it("should error", async () => {
        try {
          await closePostExpiration(
            program,
            minter,
            size,
            optionMarket.key,
            badWriterMint.publicKey,
            minterWriterAcct.publicKey,
            optionMarket.underlyingAssetPool,
            minterUnderlyingAccount.publicKey
          );
          assert.ok(false);
        } catch (err) {
          const errorMsg =
            "WriterToken mint does not match the value on the OptionMarket";
          assert.equal((err as AnchorError).error.errorMessage, errorMsg);
        }
      });
    });
    describe("Validate the underlying destination is the same as the OptionMarket", () => {
      let badUnderlyingDest: Keypair;
      before(async () => {
        const { tokenAccount } = await initNewTokenAccount(
          provider.connection,
          payer.publicKey,
          quoteToken.publicKey,
          payer
        );
        badUnderlyingDest = tokenAccount;
      });
      it("should error", async () => {
        try {
          await closePostExpiration(
            program,
            minter,
            size,
            optionMarket.key,
            optionMarket.writerTokenMint,
            minterWriterAcct.publicKey,
            optionMarket.underlyingAssetPool,
            badUnderlyingDest.publicKey
          );
          assert.ok(false);
        } catch (err) {
          const errorMsg =
            "Underlying destination mint must match underlying asset mint address";
          assert.equal((err as AnchorError).error.errorMessage, errorMsg);
        }
      });
    });
  });
  describe("Unexpired OptionMarket", () => {
    before(async () => {
      // Initialize a new OptionMarket
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        optionMarket,
        optionToken,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {
        // set expiration to 2 seconds from now
        expiration: new anchor.BN(new Date().getTime() / 1000 + 600),
      }));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new minter
      ({
        optionAccount: minterOptionAcct,
        underlyingAccount: minterUnderlyingAccount,
        writerTokenAccount: minterWriterAcct,
      } = await createMinter(
        provider.connection,
        minter,
        mintAuthority,
        underlyingToken,
        new anchor.BN(100)
          .mul(optionMarket.underlyingAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.writerTokenMint,
        quoteToken
      ));
      // Mint a bunch of contracts to the minter
      await mintOptionsTx(
        program,
        minter,
        minterOptionAcct,
        minterWriterAcct,
        minterUnderlyingAccount,
        new anchor.BN(100),
        optionMarket
      );
      // Create an exerciser
      ({ optionAccount: exerciserOptionAcct } = await createExerciser(
        provider.connection,
        exerciser,
        mintAuthority,
        quoteToken,
        new anchor.BN(100)
          .mul(optionMarket.quoteAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.underlyingAssetMint
      ));

      // Transfer a options to the exerciser
      await optionToken.transfer(
        minterOptionAcct.publicKey,
        exerciserOptionAcct.publicKey,
        minter,
        [],
        new u64(100)
      );
    });
    beforeEach(async () => {
      size = new u64(1);
    });

    it("should error", async () => {
      try {
        await closePostExpiration(
          program,
          minter,
          size,
          optionMarket.key,
          optionMarket.writerTokenMint,
          minterWriterAcct.publicKey,
          optionMarket.underlyingAssetPool,
          minterUnderlyingAccount.publicKey
        );
        assert.ok(false);
      } catch (err) {
        const errorMsg = "OptionMarket has not expired, can't close";
        assert.equal((err as AnchorError).error.errorMessage, errorMsg);
      }
    });
  });
});
Example #5
Source File: exercise_tests.ts    From psyoptions with Apache License 2.0 4 votes vote down vote up
describe("exerciseOption", () => {
  const payer = anchor.web3.Keypair.generate();
  const mintAuthority = anchor.web3.Keypair.generate();
  const program = anchor.workspace.PsyAmerican as Program<PsyAmerican>;
  const provider = program.provider;
  // @ts-ignore
  const wallet = provider.wallet as unknown as anchor.Wallet;

  const minter = anchor.web3.Keypair.generate();
  const minterProvider = new AnchorProvider(
    provider.connection,
    new anchor.Wallet(minter),
    {}
  );
  const minterProgram = new Program(
    program.idl,
    program.programId,
    minterProvider
  );
  const exerciser = anchor.web3.Keypair.generate();

  let quoteToken: Token;
  let underlyingToken: Token;
  let optionToken: Token;
  let underlyingAmountPerContract: anchor.BN;
  let quoteAmountPerContract: anchor.BN;
  let optionMarketKey: PublicKey;
  let optionMarket: OptionMarketWithKey;
  let exerciseFeeKey: PublicKey;
  let exerciserOptionAcct: Keypair;
  let exerciserQuoteAcct: Keypair;
  let exerciserUnderlyingAcct: Keypair;
  let remainingAccounts: AccountMeta[] = [];
  let instructions: TransactionInstruction[] = [];

  let size = new u64(2);

  before(async () => {
    // airdrop SOL to the payer and minter
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        payer.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        minter.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        exerciser.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
  });

  describe("Non-nft OptionMarket", () => {
    before(async () => {
      // Initialize a new OptionMarket
      ({
        quoteToken,
        underlyingToken,
        optionToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        optionMarketKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new minter
      const {
        optionAccount: minterOptionAcct,
        underlyingAccount: minterUnderlyingAccount,
        writerTokenAccount: minterWriterAcct,
      } = await createMinter(
        provider.connection,
        minter,
        mintAuthority,
        underlyingToken,
        new anchor.BN(100)
          .mul(optionMarket.underlyingAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.writerTokenMint,
        quoteToken
      );
      // Mint a bunch of contracts to the minter
      const { ix: mintOptionsIx } =
        await psyAmericanInstructions.mintOptionV2Instruction(
          minterProgram,
          minterOptionAcct.publicKey,
          minterWriterAcct.publicKey,
          minterUnderlyingAccount.publicKey,
          new anchor.BN(100),
          optionMarket
        );
      await provider.sendAndConfirm!(new Transaction().add(mintOptionsIx), [
        minter,
      ]);
      // Create an exerciser
      ({
        optionAccount: exerciserOptionAcct,
        quoteAccount: exerciserQuoteAcct,
        underlyingAccount: exerciserUnderlyingAcct,
      } = await createExerciser(
        provider.connection,
        exerciser,
        mintAuthority,
        quoteToken,
        new anchor.BN(100)
          .mul(optionMarket.quoteAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        underlyingToken.publicKey
      ));

      // Transfer a options to the exerciser
      await optionToken.transfer(
        minterOptionAcct.publicKey,
        exerciserOptionAcct.publicKey,
        minter,
        [],
        new u64(100)
      );
    });
    beforeEach(async () => {
      size = new u64(2);
    });

    it("should be properly setup", async () => {
      const exerciserOption = await optionToken.getAccountInfo(
        exerciserOptionAcct.publicKey
      );
      assert.equal(exerciserOption.amount.toString(), new u64(100).toString());
    });

    describe("proper exercise", () => {
      it("should burn the option token, swap the quote and underlying assets", async () => {
        const optionTokenBefore = await optionToken.getMintInfo();
        const underlyingPoolBefore = await underlyingToken.getAccountInfo(
          optionMarket.underlyingAssetPool
        );
        const quotePoolBefore = await quoteToken.getAccountInfo(
          optionMarket.quoteAssetPool
        );
        const exerciserQuoteBefore = await quoteToken.getAccountInfo(
          exerciserQuoteAcct.publicKey
        );
        try {
          await exerciseOptionTx(
            program,
            size,
            optionMarket.key,
            optionMarket.optionMint,
            exerciser,
            exerciser,
            exerciserOptionAcct.publicKey,
            optionMarket.underlyingAssetPool,
            exerciserUnderlyingAcct.publicKey,
            optionMarket.quoteAssetPool,
            exerciserQuoteAcct.publicKey,
            [
              {
                pubkey: exerciseFeeKey,
                isWritable: true,
                isSigner: false,
              },
            ]
          );
        } catch (err) {
          console.error((err as AnchorError).error.errorMessage);
          throw err;
        }
        const optionTokenAfter = await optionToken.getMintInfo();
        const optionTokenDiff = optionTokenAfter.supply.sub(
          optionTokenBefore.supply
        );
        assert.equal(optionTokenDiff.toString(), size.neg().toString());

        const underlyingPoolAfter = await underlyingToken.getAccountInfo(
          optionMarket.underlyingAssetPool
        );
        const underlyingPoolDiff = underlyingPoolAfter.amount.sub(
          underlyingPoolBefore.amount
        );
        assert.equal(
          underlyingPoolDiff.toString(),
          size.mul(underlyingAmountPerContract).neg().toString()
        );

        const quotePoolAfter = await quoteToken.getAccountInfo(
          optionMarket.quoteAssetPool
        );
        const quotePoolDiff = quotePoolAfter.amount.sub(quotePoolBefore.amount);
        assert.equal(
          quotePoolDiff.toString(),
          size.mul(quoteAmountPerContract).toString()
        );

        const exerciserQuoteAfter = await quoteToken.getAccountInfo(
          exerciserQuoteAcct.publicKey
        );
        const exerciserQuoteDiff = exerciserQuoteAfter.amount.sub(
          exerciserQuoteBefore.amount
        );
        const exerciseFeePerContract = feeAmountPerContract(
          quoteAmountPerContract
        );
        const exerciseFee = exerciseFeePerContract.mul(size);
        console.log("*** exerciseFee", exerciseFee.toString());
        assert.equal(
          exerciserQuoteDiff.neg().toString(),
          exerciseFee.add(size.mul(quoteAmountPerContract)).toString()
        );
      });
    });
    describe("exercise fee key does not match OptionMarket", () => {
      let badExerciseFeeKey: PublicKey;
      beforeEach(async () => {
        // Create a new token account and set it as the mintFeeKey
        const { tokenAccount } = await initNewTokenAccount(
          provider.connection,
          FEE_OWNER_KEY,
          quoteToken.publicKey,
          payer
        );
        badExerciseFeeKey = tokenAccount.publicKey;
      });
      it("should error", async () => {
        try {
          await exerciseOptionTx(
            program,
            size,
            optionMarketKey,
            optionToken.publicKey,
            exerciser,
            exerciser,
            exerciserOptionAcct.publicKey,
            optionMarket.underlyingAssetPool,
            exerciserUnderlyingAcct.publicKey,
            optionMarket.quoteAssetPool,
            exerciserQuoteAcct.publicKey,
            [
              {
                pubkey: badExerciseFeeKey,
                isWritable: true,
                isSigner: false,
              },
            ]
          );
          assert.ok(false);
        } catch (err) {
          const errMsg =
            "exerciseFee key does not match the value on the OptionMarket";
          assert.equal((err as AnchorError).error.errorMessage, errMsg);
        }
      });
    });
    describe("quote asset pool is not the same as the OptionMarket", () => {
      let badQuoteAssetPoolAcct: Keypair;
      beforeEach(async () => {
        // Create a new token account and set it as the mintFeeKey
        const { tokenAccount } = await initNewTokenAccount(
          provider.connection,
          FEE_OWNER_KEY,
          quoteToken.publicKey,
          payer
        );
        badQuoteAssetPoolAcct = tokenAccount;
      });
      it("should error", async () => {
        try {
          await exerciseOptionTx(
            program,
            size,
            optionMarket.key,
            optionMarket.optionMint,
            exerciser,
            exerciser,
            exerciserOptionAcct.publicKey,
            optionMarket.underlyingAssetPool,
            exerciserUnderlyingAcct.publicKey,
            badQuoteAssetPoolAcct.publicKey,
            exerciserQuoteAcct.publicKey,
            [
              {
                pubkey: exerciseFeeKey,
                isWritable: true,
                isSigner: false,
              },
            ]
          );
          assert.ok(false);
        } catch (err) {
          const errMsg =
            "Quote pool account does not match the value on the OptionMarket";
          assert.equal((err as AnchorError).error.errorMessage, errMsg);
        }
      });
    });
    describe("Underlying asset pool is not the same as the OptionMarket", () => {
      let badUnderlyingAssetPoolAcct: Keypair;
      beforeEach(async () => {
        // Create a new token account and set it as the mintFeeKey
        const { tokenAccount } = await initNewTokenAccount(
          provider.connection,
          FEE_OWNER_KEY,
          underlyingToken.publicKey,
          payer
        );
        badUnderlyingAssetPoolAcct = tokenAccount;
      });
      it("should error", async () => {
        try {
          await exerciseOptionTx(
            program,
            size,
            optionMarket.key,
            optionMarket.optionMint,
            exerciser,
            exerciser,
            exerciserOptionAcct.publicKey,
            badUnderlyingAssetPoolAcct.publicKey,
            exerciserUnderlyingAcct.publicKey,
            optionMarket.quoteAssetPool,
            exerciserQuoteAcct.publicKey,
            [
              {
                pubkey: exerciseFeeKey,
                isWritable: true,
                isSigner: false,
              },
            ]
          );
          assert.ok(false);
        } catch (err) {
          const errMsg =
            "Underlying pool account does not match the value on the OptionMarket";
          assert.equal((err as AnchorError).error.errorMessage, errMsg);
        }
      });
    });
    describe("Underlying destination mint is not the same as the underlying asset", () => {
      let badUnderlyingDest: Keypair;
      beforeEach(async () => {
        // Create a new token account and set it as the mintFeeKey
        const { tokenAccount } = await initNewTokenAccount(
          provider.connection,
          FEE_OWNER_KEY,
          quoteToken.publicKey,
          payer
        );
        badUnderlyingDest = tokenAccount;
      });
      it("should error", async () => {
        try {
          await exerciseOptionTx(
            program,
            size,
            optionMarket.key,
            optionMarket.optionMint,
            exerciser,
            exerciser,
            exerciserOptionAcct.publicKey,
            optionMarket.underlyingAssetPool,
            badUnderlyingDest.publicKey,
            optionMarket.quoteAssetPool,
            exerciserQuoteAcct.publicKey,
            [
              {
                pubkey: exerciseFeeKey,
                isWritable: true,
                isSigner: false,
              },
            ]
          );
          assert.ok(false);
        } catch (err) {
          const errMsg =
            "Underlying destination mint must match underlying asset mint address";
          assert.equal((err as AnchorError).error.errorMessage, errMsg);
        }
      });
    });
    describe("OptionToken Mint is not the same as the OptionMarket", () => {
      let badOptionToken: Token;
      beforeEach(async () => {
        // Create a new token account and set it as the mintFeeKey
        const { mintAccount } = await initNewTokenMint(
          provider.connection,
          FEE_OWNER_KEY,
          payer
        );
        badOptionToken = new Token(
          provider.connection,
          mintAccount.publicKey,
          TOKEN_PROGRAM_ID,
          payer
        );
      });
      it("should error", async () => {
        try {
          await exerciseOptionTx(
            program,
            size,
            optionMarket.key,
            badOptionToken.publicKey,
            exerciser,
            exerciser,
            exerciserOptionAcct.publicKey,
            optionMarket.underlyingAssetPool,
            exerciserUnderlyingAcct.publicKey,
            optionMarket.quoteAssetPool,
            exerciserQuoteAcct.publicKey,
            [
              {
                pubkey: exerciseFeeKey,
                isWritable: true,
                isSigner: false,
              },
            ]
          );
          assert.ok(false);
        } catch (err) {
          const errMsg =
            "OptionToken mint does not match the value on the OptionMarket";
          assert.equal((err as AnchorError).error.errorMessage, errMsg);
        }
      });
    });
    describe("Fee Owner does not match the program's fee owner", () => {
      let badFeeOwner: Keypair;
      beforeEach(async () => {
        badFeeOwner = new Keypair();
      });
      it("should error", async () => {
        try {
          await exerciseOptionTx(
            program,
            size,
            optionMarket.key,
            optionMarket.optionMint,
            exerciser,
            exerciser,
            exerciserOptionAcct.publicKey,
            optionMarket.underlyingAssetPool,
            exerciserUnderlyingAcct.publicKey,
            optionMarket.quoteAssetPool,
            exerciserQuoteAcct.publicKey,
            [
              {
                pubkey: exerciseFeeKey,
                isWritable: true,
                isSigner: false,
              },
            ],
            { feeOwner: badFeeOwner.publicKey }
          );
          assert.ok(false);
        } catch (err) {
          const errMsg = "Fee owner does not match the program's fee owner";
          assert.equal((err as AnchorError).error.errorMessage, errMsg);
        }
      });
    });
  });

  describe("Expired option market", () => {
    before(async () => {
      // Initialize a new OptionMarket
      ({
        quoteToken,
        underlyingToken,
        optionToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        optionMarketKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {
        // set expiration to 2 seconds from now
        expiration: new anchor.BN(new Date().getTime() / 1000 + 4),
      }));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new minter
      const {
        optionAccount: minterOptionAcct,
        underlyingAccount: minterUnderlyingAccount,
        writerTokenAccount: minterWriterAcct,
      } = await createMinter(
        provider.connection,
        minter,
        mintAuthority,
        underlyingToken,
        new anchor.BN(100)
          .mul(optionMarket.underlyingAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.writerTokenMint,
        quoteToken
      );
      // Mint a bunch of contracts to the minter
      const { ix: mintOptionsIx } =
        await psyAmericanInstructions.mintOptionV2Instruction(
          minterProgram,
          minterOptionAcct.publicKey,
          minterWriterAcct.publicKey,
          minterUnderlyingAccount.publicKey,
          new anchor.BN(100),
          optionMarket
        );
      await program.provider.sendAndConfirm!(
        new Transaction().add(mintOptionsIx),
        [minter]
      );
      // Create an exerciser
      ({
        optionAccount: exerciserOptionAcct,
        quoteAccount: exerciserQuoteAcct,
        underlyingAccount: exerciserUnderlyingAcct,
      } = await createExerciser(
        provider.connection,
        exerciser,
        mintAuthority,
        quoteToken,
        new anchor.BN(100)
          .mul(optionMarket.quoteAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.underlyingAssetMint
      ));

      // Transfer a options to the exerciser
      await optionToken.transfer(
        minterOptionAcct.publicKey,
        exerciserOptionAcct.publicKey,
        minter,
        [],
        new u64(100)
      );
    });
    beforeEach(async () => {
      size = new u64(2);
    });
    it("should error", async () => {
      try {
        await wait(3000);
        await exerciseOptionTx(
          program,
          size,
          optionMarket.key,
          optionMarket.optionMint,
          exerciser,
          exerciser,
          exerciserOptionAcct.publicKey,
          optionMarket.underlyingAssetPool,
          exerciserUnderlyingAcct.publicKey,
          optionMarket.quoteAssetPool,
          exerciserQuoteAcct.publicKey,
          [
            {
              pubkey: exerciseFeeKey,
              isWritable: true,
              isSigner: false,
            },
          ]
        );
        assert.ok(false);
      } catch (err) {
        const errMsg = "OptionMarket is expired, can't exercise";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });
  describe("OptionMarket is for NFT", () => {
    before(async () => {
      // Initialize a new OptionMarket
      ({
        quoteToken,
        underlyingToken,
        optionToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        optionMarketKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {
        quoteAmountPerContract: new anchor.BN(1),
      }));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new minter
      const {
        optionAccount: minterOptionAcct,
        underlyingAccount: minterUnderlyingAccount,
        writerTokenAccount: minterWriterAcct,
      } = await createMinter(
        provider.connection,
        minter,
        mintAuthority,
        underlyingToken,
        new anchor.BN(100)
          .mul(optionMarket.underlyingAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.writerTokenMint,
        quoteToken
      );
      // Mint a bunch of contracts to the minter
      const { ix: mintOptionsIx } =
        await psyAmericanInstructions.mintOptionV2Instruction(
          minterProgram,
          minterOptionAcct.publicKey,
          minterWriterAcct.publicKey,
          minterUnderlyingAccount.publicKey,
          new anchor.BN(100),
          optionMarket
        );
      await program.provider.sendAndConfirm!(
        new Transaction().add(mintOptionsIx),
        [minter]
      );
      // Create an exerciser
      ({
        optionAccount: exerciserOptionAcct,
        quoteAccount: exerciserQuoteAcct,
        underlyingAccount: exerciserUnderlyingAcct,
      } = await createExerciser(
        provider.connection,
        exerciser,
        mintAuthority,
        quoteToken,
        new anchor.BN(100)
          .mul(optionMarket.quoteAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.underlyingAssetMint
      ));

      size = new u64(2);
      // Transfer a options to the exerciser
      await optionToken.transfer(
        minterOptionAcct.publicKey,
        exerciserOptionAcct.publicKey,
        minter,
        [],
        size
      );
    });
    it("should transfer enough lamports as required by the fee", async () => {
      const exerciserBefore = await provider.connection.getAccountInfo(
        exerciser.publicKey
      );
      const feeOwnerBefore =
        (await provider.connection.getAccountInfo(FEE_OWNER_KEY))?.lamports ||
        0;
      try {
        await exerciseOptionTx(
          program,
          size,
          optionMarket.key,
          optionMarket.optionMint,
          exerciser,
          exerciser,
          exerciserOptionAcct.publicKey,
          optionMarket.underlyingAssetPool,
          exerciserUnderlyingAcct.publicKey,
          optionMarket.quoteAssetPool,
          exerciserQuoteAcct.publicKey,
          [
            {
              pubkey: exerciseFeeKey,
              isWritable: true,
              isSigner: false,
            },
          ]
        );
      } catch (err) {
        console.error((err as AnchorError).error.errorMessage);
        throw err;
      }
      const exerciserAfter = await provider.connection.getAccountInfo(
        exerciser.publicKey
      );
      const feeOwnerAfter =
        (await provider.connection.getAccountInfo(FEE_OWNER_KEY))?.lamports ||
        0;
      if (!exerciserAfter?.lamports || !exerciserBefore?.lamports) {
        throw new Error("minter has no lamports");
      }
      const exerciserDiff =
        exerciserAfter?.lamports - exerciserBefore?.lamports;
      const feeOwnerDiff = feeOwnerAfter - feeOwnerBefore;
      assert.equal(
        -exerciserDiff,
        size.mul(new BN(NFT_MINT_LAMPORTS)).toNumber()
      );
      assert.equal(
        feeOwnerDiff,
        size.mul(new BN(NFT_MINT_LAMPORTS)).toNumber()
      );
    });
  });
});
Example #6
Source File: exercise_v2.ts    From psyoptions with Apache License 2.0 4 votes vote down vote up
describe("exerciseOption", () => {
  const payer = anchor.web3.Keypair.generate();
  const mintAuthority = anchor.web3.Keypair.generate();
  const program = anchor.workspace.PsyAmerican as anchor.Program<PsyAmerican>;
  const provider = program.provider;
  // @ts-ignore
  const wallet = provider.wallet as unknown as anchor.Wallet;

  const minter = anchor.web3.Keypair.generate();
  const minterProvider = new AnchorProvider(
    provider.connection,
    new Wallet(minter),
    {}
  );
  const minterProgram = new Program(
    program.idl,
    program.programId,
    minterProvider
  );
  const exerciser = anchor.web3.Keypair.generate();
  const exerciserProvider = new AnchorProvider(
    provider.connection,
    new Wallet(exerciser),
    {}
  );
  const exerciserProgram = new Program(
    program.idl,
    program.programId,
    exerciserProvider
  );

  let quoteToken: Token;
  let underlyingToken: Token;
  let optionToken: Token;
  let underlyingAmountPerContract: anchor.BN;
  let quoteAmountPerContract: anchor.BN;
  let optionMarketKey: PublicKey;
  let optionMarket: OptionMarketWithKey;
  let exerciseFeeKey: PublicKey;
  let exerciserOptionAcct: Keypair;
  let exerciserQuoteAcct: Keypair;
  let exerciserUnderlyingAcct: Keypair;
  let remainingAccounts: AccountMeta[] = [];
  let instructions: TransactionInstruction[] = [];

  let size = new u64(2);

  before(async () => {
    // airdrop SOL to the payer and minter
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        payer.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        minter.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        exerciser.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
  });

  describe("Non-nft OptionMarket", () => {
    before(async () => {
      // Initialize a new OptionMarket
      ({
        quoteToken,
        underlyingToken,
        optionToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        optionMarketKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new minter
      const {
        optionAccount: minterOptionAcct,
        underlyingAccount: minterUnderlyingAccount,
        writerTokenAccount: minterWriterAcct,
      } = await createMinter(
        provider.connection,
        minter,
        mintAuthority,
        underlyingToken,
        new anchor.BN(100)
          .mul(optionMarket.underlyingAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.writerTokenMint,
        quoteToken
      );
      // Mint a bunch of contracts to the minter
      const { ix: mintOptionsIx } =
        await psyAmericanInstructions.mintOptionV2Instruction(
          minterProgram,
          minterOptionAcct.publicKey,
          minterWriterAcct.publicKey,
          minterUnderlyingAccount.publicKey,
          new anchor.BN(100),
          optionMarket
        );
      await program.provider.sendAndConfirm!(
        new Transaction().add(mintOptionsIx),
        [minter]
      );
      // Create an exerciser
      ({
        optionAccount: exerciserOptionAcct,
        quoteAccount: exerciserQuoteAcct,
        underlyingAccount: exerciserUnderlyingAcct,
      } = await createExerciser(
        provider.connection,
        exerciser,
        mintAuthority,
        quoteToken,
        new anchor.BN(100)
          .mul(optionMarket.quoteAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        underlyingToken.publicKey
      ));

      // Transfer a options to the exerciser
      await optionToken.transfer(
        minterOptionAcct.publicKey,
        exerciserOptionAcct.publicKey,
        minter,
        [],
        new u64(100)
      );
    });
    beforeEach(async () => {
      size = new u64(2);
    });

    it("should be properly setup", async () => {
      const exerciserOption = await optionToken.getAccountInfo(
        exerciserOptionAcct.publicKey
      );
      assert.equal(exerciserOption.amount.toString(), new u64(100).toString());
    });

    describe("proper exercise", () => {
      it("should burn the option token, swap the quote and underlying assets", async () => {
        const optionTokenBefore = await optionToken.getMintInfo();
        const underlyingPoolBefore = await underlyingToken.getAccountInfo(
          optionMarket.underlyingAssetPool
        );
        const quotePoolBefore = await quoteToken.getAccountInfo(
          optionMarket.quoteAssetPool
        );
        const exerciserQuoteBefore = await quoteToken.getAccountInfo(
          exerciserQuoteAcct.publicKey
        );
        try {
          const instruction =
            psyAmericanInstructions.exerciseOptionsV2Instruction(
              exerciserProgram,
              size,
              optionMarket,
              exerciserOptionAcct.publicKey,
              exerciserUnderlyingAcct.publicKey,
              exerciserQuoteAcct.publicKey
            );
          await exerciserProgram.provider.sendAndConfirm!(
            new Transaction().add(instruction)
          );
        } catch (err) {
          console.error((err as AnchorError).error.errorMessage);
          throw err;
        }
        const optionTokenAfter = await optionToken.getMintInfo();
        const optionTokenDiff = optionTokenAfter.supply.sub(
          optionTokenBefore.supply
        );
        assert.equal(optionTokenDiff.toString(), size.neg().toString());

        const underlyingPoolAfter = await underlyingToken.getAccountInfo(
          optionMarket.underlyingAssetPool
        );
        const underlyingPoolDiff = underlyingPoolAfter.amount.sub(
          underlyingPoolBefore.amount
        );
        assert.equal(
          underlyingPoolDiff.toString(),
          size.mul(underlyingAmountPerContract).neg().toString()
        );

        const quotePoolAfter = await quoteToken.getAccountInfo(
          optionMarket.quoteAssetPool
        );
        const quotePoolDiff = quotePoolAfter.amount.sub(quotePoolBefore.amount);
        assert.equal(
          quotePoolDiff.toString(),
          size.mul(quoteAmountPerContract).toString()
        );

        const exerciserQuoteAfter = await quoteToken.getAccountInfo(
          exerciserQuoteAcct.publicKey
        );
        const exerciserQuoteDiff = exerciserQuoteAfter.amount.sub(
          exerciserQuoteBefore.amount
        );
        const exerciseFee = new BN(0);
        assert.equal(
          exerciserQuoteDiff.neg().toString(),
          exerciseFee.add(size.mul(quoteAmountPerContract)).toString()
        );
      });
    });
    describe("quote asset pool is not the same as the OptionMarket", () => {
      let badQuoteAssetPoolAcct: Keypair;
      beforeEach(async () => {
        // Create a new token account and set it as the mintFeeKey
        const { tokenAccount } = await initNewTokenAccount(
          provider.connection,
          FEE_OWNER_KEY,
          quoteToken.publicKey,
          payer
        );
        badQuoteAssetPoolAcct = tokenAccount;
      });
      it("should error", async () => {
        try {
          const instruction =
            psyAmericanInstructions.exerciseOptionsV2Instruction(
              exerciserProgram,
              size,
              {
                ...optionMarket,
                quoteAssetPool: badQuoteAssetPoolAcct.publicKey,
              },
              exerciserOptionAcct.publicKey,
              exerciserUnderlyingAcct.publicKey,
              exerciserQuoteAcct.publicKey
            );
          await exerciserProgram.provider.sendAndConfirm!(
            new Transaction().add(instruction)
          );
          assert.ok(false);
        } catch (err) {
          const programError = parseTransactionError(err);
          console.log("*** programError", programError);
          const errMsg =
            "Quote pool account does not match the value on the OptionMarket";
          assert.equal(programError.msg, errMsg);
        }
      });
    });
    describe("Underlying asset pool is not the same as the OptionMarket", () => {
      let badUnderlyingAssetPoolAcct: Keypair;
      beforeEach(async () => {
        // Create a new token account and set it as the mintFeeKey
        const { tokenAccount } = await initNewTokenAccount(
          provider.connection,
          FEE_OWNER_KEY,
          underlyingToken.publicKey,
          payer
        );
        badUnderlyingAssetPoolAcct = tokenAccount;
      });
      it("should error", async () => {
        try {
          const instruction =
            psyAmericanInstructions.exerciseOptionsV2Instruction(
              exerciserProgram,
              size,
              {
                ...optionMarket,
                underlyingAssetPool: badUnderlyingAssetPoolAcct.publicKey,
              },
              exerciserOptionAcct.publicKey,
              exerciserUnderlyingAcct.publicKey,
              exerciserQuoteAcct.publicKey
            );
          await exerciserProgram.provider.sendAndConfirm!(
            new Transaction().add(instruction)
          );
          assert.ok(false);
        } catch (err) {
          const programError = parseTransactionError(err);
          const errMsg =
            "Underlying pool account does not match the value on the OptionMarket";
          assert.equal(programError.msg, errMsg);
        }
      });
    });
    describe("Underlying destination mint is not the same as the underlying asset", () => {
      let badUnderlyingDest: Keypair;
      beforeEach(async () => {
        // Create a new token account and set it as the mintFeeKey
        const { tokenAccount } = await initNewTokenAccount(
          provider.connection,
          FEE_OWNER_KEY,
          quoteToken.publicKey,
          payer
        );
        badUnderlyingDest = tokenAccount;
      });
      it("should error", async () => {
        try {
          const instruction =
            psyAmericanInstructions.exerciseOptionsV2Instruction(
              exerciserProgram,
              size,
              optionMarket,
              exerciserOptionAcct.publicKey,
              badUnderlyingDest.publicKey,
              exerciserQuoteAcct.publicKey
            );
          await exerciserProgram.provider.sendAndConfirm!(
            new Transaction().add(instruction)
          );
          assert.ok(false);
        } catch (err) {
          const programError = parseTransactionError(err);
          const errMsg =
            "Underlying destination mint must match underlying asset mint address";
          assert.equal(programError.msg, errMsg);
        }
      });
    });
    describe("OptionToken Mint is not the same as the OptionMarket", () => {
      let badOptionToken: Token;
      beforeEach(async () => {
        // Create a new token account and set it as the mintFeeKey
        const { mintAccount } = await initNewTokenMint(
          provider.connection,
          FEE_OWNER_KEY,
          payer
        );
        badOptionToken = new Token(
          provider.connection,
          mintAccount.publicKey,
          TOKEN_PROGRAM_ID,
          payer
        );
      });
      it("should error", async () => {
        try {
          const instruction =
            psyAmericanInstructions.exerciseOptionsV2Instruction(
              exerciserProgram,
              size,
              { ...optionMarket, optionMint: badOptionToken.publicKey },
              exerciserOptionAcct.publicKey,
              exerciserUnderlyingAcct.publicKey,
              exerciserQuoteAcct.publicKey
            );
          await exerciserProgram.provider.sendAndConfirm!(
            new Transaction().add(instruction)
          );
          assert.ok(false);
        } catch (err) {
          const programError = parseTransactionError(err);
          const errMsg =
            "OptionToken mint does not match the value on the OptionMarket";
          assert.equal(programError.msg, errMsg);
        }
      });
    });
  });

  describe("Expired option market", () => {
    before(async () => {
      // Initialize a new OptionMarket
      ({
        quoteToken,
        underlyingToken,
        optionToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        optionMarketKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {
        // set expiration to 2 seconds from now
        expiration: new anchor.BN(new Date().getTime() / 1000 + 4),
      }));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new minter
      const {
        optionAccount: minterOptionAcct,
        underlyingAccount: minterUnderlyingAccount,
        writerTokenAccount: minterWriterAcct,
      } = await createMinter(
        provider.connection,
        minter,
        mintAuthority,
        underlyingToken,
        new anchor.BN(100)
          .mul(optionMarket.underlyingAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.writerTokenMint,
        quoteToken
      );
      // Mint a bunch of contracts to the minter
      const { ix: mintOptionsIx } =
        await psyAmericanInstructions.mintOptionV2Instruction(
          minterProgram,
          minterOptionAcct.publicKey,
          minterWriterAcct.publicKey,
          minterUnderlyingAccount.publicKey,
          new anchor.BN(100),
          optionMarket
        );
      await program.provider.sendAndConfirm!(
        new Transaction().add(mintOptionsIx),
        [minter]
      );
      // Create an exerciser
      ({
        optionAccount: exerciserOptionAcct,
        quoteAccount: exerciserQuoteAcct,
        underlyingAccount: exerciserUnderlyingAcct,
      } = await createExerciser(
        provider.connection,
        exerciser,
        mintAuthority,
        quoteToken,
        new anchor.BN(100)
          .mul(optionMarket.quoteAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.underlyingAssetMint
      ));

      // Transfer a options to the exerciser
      await optionToken.transfer(
        minterOptionAcct.publicKey,
        exerciserOptionAcct.publicKey,
        minter,
        [],
        new u64(100)
      );
    });
    beforeEach(async () => {
      size = new u64(2);
    });
    it("should error", async () => {
      try {
        await wait(3000);
        const instruction =
          psyAmericanInstructions.exerciseOptionsV2Instruction(
            exerciserProgram,
            size,
            optionMarket,
            exerciserOptionAcct.publicKey,
            exerciserUnderlyingAcct.publicKey,
            exerciserQuoteAcct.publicKey
          );
        await exerciserProgram.provider.sendAndConfirm!(
          new Transaction().add(instruction)
        );
        assert.ok(false);
      } catch (err) {
        const programError = parseTransactionError(err);
        const errMsg = "OptionMarket is expired, can't exercise";
        assert.equal(programError.msg, errMsg);
      }
    });
  });
});
Example #7
Source File: initialize.ts    From psyoptions with Apache License 2.0 4 votes vote down vote up
describe("initializeMarket", () => {
  // Configure the client to use the local cluster.
  const payer = anchor.web3.Keypair.generate();
  const mintAuthority = anchor.web3.Keypair.generate();
  const program = anchor.workspace.PsyAmerican as Program<PsyAmerican>;
  const provider = program.provider;

  let quoteToken: Token;
  let underlyingToken: Token;
  let optionToken: Token;
  let underlyingAmountPerContract: anchor.BN;
  let quoteAmountPerContract: anchor.BN;
  let expiration: anchor.BN;
  let optionMarket: OptionMarketV2;
  let remainingAccounts: AccountMeta[] = [];
  let instructions: TransactionInstruction[] = [];

  beforeEach(async () => {
    underlyingAmountPerContract = new anchor.BN("10000000000");
    quoteAmountPerContract = new anchor.BN("50000000000");
    expiration = new anchor.BN(new Date().getTime() / 1000 + 3600);
    remainingAccounts = [];
    instructions = [];
    // airdrop to the user so it has funds to use
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(payer.publicKey, 10_000_000_000),
      "confirmed"
    );
  });

  describe("good account setup", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        optionToken,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {}));
    });
    it("Creates new OptionMarket!", async () => {
      try {
        await initOptionMarket(
          program,
          payer,
          optionMarket,
          remainingAccounts,
          instructions
        );
      } catch (err) {
        console.error((err as AnchorError).error.errorMessage);
        throw err;
      }
      // Fetch the account for the newly created OptionMarket
      const onChainOptionMarket = (await program.account.optionMarket.fetch(
        optionMarket.key
      )) as OptionMarketV2;

      assert.equal(
        onChainOptionMarket.underlyingAssetMint?.toString(),
        underlyingToken.publicKey.toString()
      );
      assert.equal(
        onChainOptionMarket.quoteAssetMint?.toString(),
        quoteToken.publicKey.toString()
      );
      assert.equal(
        onChainOptionMarket.underlyingAssetPool?.toString(),
        optionMarket.underlyingAssetPool.toString()
      );
      assert.equal(
        onChainOptionMarket.quoteAssetPool?.toString(),
        optionMarket.quoteAssetPool.toString()
      );
      assert.equal(
        onChainOptionMarket.mintFeeAccount?.toString(),
        optionMarket.mintFeeAccount?.toString()
      );
      assert.equal(
        onChainOptionMarket.exerciseFeeAccount?.toString(),
        optionMarket.exerciseFeeAccount?.toString()
      );
      assert.equal(
        onChainOptionMarket.expired?.toString(),
        optionMarket.expired?.toString()
      );
      // Fetch the OptionToken Mint info
      const optionTokenMint = await optionToken.getMintInfo();
      assert.ok(optionTokenMint.mintAuthority?.equals(optionMarket.key));
    });
  });
  describe("Expiration is in the past", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        optionToken,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {
        expiration: new anchor.BN(new Date().getTime() / 1000 - 3600),
      }));
    });
    it("should error", async () => {
      try {
        await initOptionMarket(
          program,
          payer,
          optionMarket,
          remainingAccounts,
          instructions
        );
        assert.ok(false);
      } catch (err) {
        const errMsg = "Expiration must be in the future";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });
  describe("underlying asset amount <= 0", () => {
    beforeEach(async () => {
      underlyingAmountPerContract = new anchor.BN(0);
      ({ optionMarket, remainingAccounts, instructions } = await initSetup(
        provider,
        payer,
        mintAuthority,
        program,
        {
          underlyingAmountPerContract,
        }
      ));
    });
    it("Should error", async () => {
      try {
        await initOptionMarket(
          program,
          payer,
          optionMarket,
          remainingAccounts,
          instructions
        );
        assert.ok(false);
      } catch (err) {
        const errMsg =
          "Quote amount and underlying amount per contract must be > 0";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });
  describe("quote asset amount <= 0", () => {
    beforeEach(async () => {
      quoteAmountPerContract = new anchor.BN(0);
      ({ optionMarket, remainingAccounts, instructions } = await initSetup(
        provider,
        payer,
        mintAuthority,
        program,
        {
          quoteAmountPerContract,
        }
      ));
    });
    it("Should error", async () => {
      try {
        await initOptionMarket(
          program,
          payer,
          optionMarket,
          remainingAccounts,
          instructions
        );
        assert.ok(false);
      } catch (err) {
        const errMsg =
          "Quote amount and underlying amount per contract must be > 0";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });

  describe("mint fee is required based on underlying assets per contract", () => {
    describe("Mint fee owner is incorrect", () => {
      beforeEach(async () => {
        ({ optionMarket, remainingAccounts, instructions } = await initSetup(
          provider,
          payer,
          mintAuthority,
          program,
          {
            mintFeeOwner: payer.publicKey,
          }
        ));
      });
      it("should error", async () => {
        try {
          await initOptionMarket(
            program,
            payer,
            optionMarket,
            remainingAccounts,
            instructions
          );
          assert.ok(false);
        } catch (err) {
          const errMsg = "Mint fee account must be owned by the FEE_OWNER";
          assert.equal((err as AnchorError).error.errorMessage, errMsg);
        }
      });
    });
    describe("Mint fee token is not the underlying asset token", () => {
      beforeEach(async () => {
        const { mintAccount } = await initNewTokenMint(
          provider.connection,
          payer.publicKey,
          payer
        );
        const mintFeeToken = new Token(
          provider.connection,
          mintAccount.publicKey,
          TOKEN_PROGRAM_ID,
          payer
        );
        ({ optionMarket, remainingAccounts, instructions } = await initSetup(
          provider,
          payer,
          mintAuthority,
          program,
          {
            mintFeeToken,
          }
        ));
      });
      it("should error", async () => {
        try {
          await initOptionMarket(
            program,
            payer,
            optionMarket,
            remainingAccounts,
            instructions
          );
          assert.ok(false);
        } catch (err) {
          const errMsg =
            "Mint fee token must be the same as the underlying asset";
          assert.equal((err as AnchorError).error.errorMessage, errMsg);
        }
      });
    });
  });
  describe("exercise fee is required based on quote assets per contract", () => {
    describe("Exercise fee owner is incorrect", () => {
      beforeEach(async () => {
        ({ optionMarket, remainingAccounts, instructions } = await initSetup(
          provider,
          payer,
          mintAuthority,
          program,
          {
            exerciseFeeOwner: payer.publicKey,
          }
        ));
      });
      it("should error", async () => {
        try {
          await initOptionMarket(
            program,
            payer,
            optionMarket,
            remainingAccounts,
            instructions
          );
          assert.ok(false);
        } catch (err) {
          const errMsg = "Exercise fee account must be owned by the FEE_OWNER";
          assert.equal((err as AnchorError).error.errorMessage, errMsg);
        }
      });
    });
    describe("Exercise fee token is not the quote asset token", () => {
      beforeEach(async () => {
        const { mintAccount } = await initNewTokenMint(
          provider.connection,
          payer.publicKey,
          payer
        );
        const exerciseFeeToken = new Token(
          provider.connection,
          mintAccount.publicKey,
          TOKEN_PROGRAM_ID,
          payer
        );
        ({ optionMarket, remainingAccounts, instructions } = await initSetup(
          provider,
          payer,
          mintAuthority,
          program,
          {
            exerciseFeeToken,
          }
        ));
      });
      it("should error", async () => {
        try {
          await initOptionMarket(
            program,
            payer,
            optionMarket,
            remainingAccounts,
            instructions
          );
          assert.ok(false);
        } catch (err) {
          const errMsg =
            "Exercise fee token must be the same as the quote asset";
          assert.equal((err as AnchorError).error.errorMessage, errMsg);
        }
      });
    });
  });
});
Example #8
Source File: mint_tests.ts    From psyoptions with Apache License 2.0 4 votes vote down vote up
describe("mintOption", () => {
  const payer = anchor.web3.Keypair.generate();
  const mintAuthority = anchor.web3.Keypair.generate();
  const program = anchor.workspace.PsyAmerican as Program<PsyAmerican>;
  const provider = program.provider;

  const minter = anchor.web3.Keypair.generate();

  let quoteToken: Token;
  let underlyingToken: Token;
  let optionToken: Token;
  let optionMarket: OptionMarketV2;
  let underlyingAmountPerContract: anchor.BN;
  let quoteAmountPerContract: anchor.BN;
  let expiration: anchor.BN;
  let optionMarketKey: PublicKey;
  let bumpSeed: number;
  let mintFeeKey: PublicKey | null;
  let exerciseFeeKey: PublicKey;
  let optionMintAccount: Keypair;
  let writerTokenMintAccount: Keypair;
  let underlyingAssetPoolAccount: Keypair;
  let quoteAssetPoolAccount: Keypair;
  let remainingAccounts: AccountMeta[] = [];
  let instructions: TransactionInstruction[] = [];

  let optionAccount: Keypair;
  let underlyingAccount: Keypair;
  let writerTokenAccount: Keypair;
  let size = new u64(2);

  beforeEach(async () => {
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(payer.publicKey, 10_000_000_000),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        minter.publicKey,
        10_000_000_000
      ),
      "confirmed"
    );
    size = new u64(2);
  });

  const mintOptionsTx = async (
    opts: {
      underlyingAssetPoolKey?: PublicKey;
      remainingAccounts?: AccountMeta[];
      feeOwner?: PublicKey;
    } = {}
  ) => {
    await program.rpc.mintOption(size, {
      accounts: {
        userAuthority: minter.publicKey,
        underlyingAssetMint: optionMarket?.underlyingAssetMint,
        underlyingAssetPool:
          opts.underlyingAssetPoolKey || optionMarket?.underlyingAssetPool,
        underlyingAssetSrc: underlyingAccount.publicKey,
        optionMint: optionMarket?.optionMint,
        mintedOptionDest: optionAccount.publicKey,
        writerTokenMint: optionMarket?.writerTokenMint,
        mintedWriterTokenDest: writerTokenAccount.publicKey,
        optionMarket: optionMarket?.key,
        feeOwner: opts.feeOwner || FEE_OWNER_KEY,
        tokenProgram: TOKEN_PROGRAM_ID,
        associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
        clock: SYSVAR_CLOCK_PUBKEY,
        rent: SYSVAR_RENT_PUBKEY,
        systemProgram: SystemProgram.programId,
      },
      remainingAccounts: opts.remainingAccounts
        ? opts.remainingAccounts
        : remainingAccounts,
      signers: [minter],
    });
  };

  describe("proper mint", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        optionToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          size.mul(optionMarket.underlyingAmountPerContract).muln(2).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));
    });
    it("should mint size OptionTokens", async () => {
      try {
        await mintOptionsTx();
      } catch (err) {
        console.error((err as AnchorError).error.errorMessage);
        throw err;
      }
      const mintInfo = await optionToken.getMintInfo();
      assert.equal(mintInfo.supply.toString(), size.toString());
    });

    it("should mint size WriterTokens", async () => {
      try {
        await mintOptionsTx();
      } catch (err) {
        console.error((err as AnchorError).error.errorMessage);
        throw err;
      }
      const writerToken = new Token(
        provider.connection,
        optionMarket.writerTokenMint,
        TOKEN_PROGRAM_ID,
        payer
      );
      const mintInfo = await writerToken.getMintInfo();
      assert.equal(mintInfo.supply.toString(), size.toString());
    });

    it("should transfer the underlying from the minter to the pool and take a fee", async () => {
      if (!mintFeeKey) {
        throw new Error("mintFeeKey wasn't set when it should be");
      }
      const mintFeeAcctBefore = await underlyingToken.getAccountInfo(
        mintFeeKey
      );
      const underlyingPoolBefore = await underlyingToken.getAccountInfo(
        optionMarket.underlyingAssetPool
      );
      const minterUnderlyingBefore = await underlyingToken.getAccountInfo(
        underlyingAccount.publicKey
      );
      try {
        await mintOptionsTx();
      } catch (err) {
        console.error((err as AnchorError).error.errorMessage);
        throw err;
      }
      const expectedUnderlyingTransfered = size.mul(
        underlyingAmountPerContract
      );
      const mintFeeAmountPerContract = feeAmountPerContract(
        underlyingAmountPerContract
      );
      const mintFeeAmount = mintFeeAmountPerContract.mul(size);

      const underlyingPoolAfter = await underlyingToken.getAccountInfo(
        optionMarket.underlyingAssetPool
      );
      const poolDiff = underlyingPoolAfter.amount.sub(
        underlyingPoolBefore.amount
      );
      const mintFeeAcctAfter = await underlyingToken.getAccountInfo(mintFeeKey);
      assert.equal(
        poolDiff.toString(),
        expectedUnderlyingTransfered.toString()
      );

      const minterUnderlyingAfter = await underlyingToken.getAccountInfo(
        underlyingAccount.publicKey
      );
      const minterUnderlyingDiff = minterUnderlyingAfter.amount.sub(
        minterUnderlyingBefore.amount
      );
      assert.equal(
        expectedUnderlyingTransfered.add(mintFeeAmount).neg().toString(),
        minterUnderlyingDiff.toString()
      );
      const mintFeeAcctDiff = mintFeeAcctAfter.amount.sub(
        mintFeeAcctBefore.amount
      );
      assert.equal(mintFeeAcctDiff.toString(), mintFeeAmount.toString());
    });
  });

  describe("OptionMarket expired", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {
        // set expiration to 2 seconds from now
        expiration: new anchor.BN(new Date().getTime() / 1000 + 2),
      }));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          optionMarket.underlyingAmountPerContract.muln(2).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));
    });
    it("should error", async () => {
      try {
        await wait(2000);
        await mintOptionsTx();
        assert.ok(false);
      } catch (err) {
        const errMsg = "OptionMarket is expired, can't mint";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });

  describe("Underlying pool key differs from option market", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          size.mul(optionMarket.underlyingAmountPerContract.muln(2)).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));
      // Create a new token account and set it as the underlyingAssetPoolAccount
      const { tokenAccount } = await initNewTokenAccount(
        provider.connection,
        payer.publicKey,
        underlyingToken.publicKey,
        payer
      );
      optionMarket.underlyingAssetPool = tokenAccount.publicKey;
    });
    it("should error", async () => {
      try {
        await mintOptionsTx();
        assert.ok(false);
      } catch (err) {
        const errMsg =
          "Underlying pool account does not match the value on the OptionMarket";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });

  describe("OptionToken Mint key differs from option market", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new token mint and set it as the optionMintAccount
      const { mintAccount } = await initNewTokenMint(
        provider.connection,
        payer.publicKey,
        payer
      );
      optionMarket.optionMint = mintAccount.publicKey;
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          size.mul(optionMarket.underlyingAmountPerContract.muln(2)).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));
    });
    it("should error", async () => {
      try {
        await mintOptionsTx();
        assert.ok(false);
      } catch (err) {
        const errMsg =
          "OptionToken mint does not match the value on the OptionMarket";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });

  describe("WriterToken Mint key differs from option market", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new token mint and set it as the optionMintAccount
      const { mintAccount } = await initNewTokenMint(
        provider.connection,
        payer.publicKey,
        payer
      );
      optionMarket.writerTokenMint = mintAccount.publicKey;
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          size.mul(optionMarket.underlyingAmountPerContract.muln(2)).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));
    });
    it("should error", async () => {
      try {
        await mintOptionsTx();
        assert.ok(false);
      } catch (err) {
        const errMsg =
          "WriterToken mint does not match the value on the OptionMarket";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });

  describe("MintFee account differs from option market", () => {
    let badMintFeeKey: PublicKey;
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new token account and set it as the mintFeeKey
      const { tokenAccount } = await initNewTokenAccount(
        provider.connection,
        FEE_OWNER_KEY,
        underlyingToken.publicKey,
        payer
      );
      badMintFeeKey = tokenAccount.publicKey;

      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          optionMarket.underlyingAmountPerContract.muln(2).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));
    });
    it("should error", async () => {
      try {
        await mintOptionsTx({
          remainingAccounts: [
            {
              pubkey: badMintFeeKey,
              isWritable: true,
              isSigner: false,
            },
          ],
        });
        assert.ok(false);
      } catch (err) {
        const errMsg =
          "MintFee key does not match the value on the OptionMarket";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });

  describe("Size <= 0", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          optionMarket.underlyingAmountPerContract.muln(2).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));

      // Set the size to 0 to trigger an error
      size = new anchor.BN(0);
    });
    it("should error", async () => {
      try {
        await mintOptionsTx();
        assert.ok(false);
      } catch (err) {
        const errMsg = "The size argument must be > 0";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });
  describe("Fee owner is incorrect", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          optionMarket.underlyingAmountPerContract.muln(2).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));
    });
    it("should error", async () => {
      try {
        await mintOptionsTx({
          feeOwner: new Keypair().publicKey,
        });
        assert.ok(false);
      } catch (err) {
        const errMsg = "Fee owner does not match the program's fee owner";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });
  describe("OptionMarket is for NFT", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {
        underlyingAmountPerContract: new anchor.BN("1"),
      }));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          optionMarket.underlyingAmountPerContract.muln(2).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));
    });
    it("should transfer enough lamports as required by the fee", async () => {
      const minterBefore = await provider.connection.getAccountInfo(
        minter.publicKey
      );
      const feeOwnerBefore =
        (await provider.connection.getAccountInfo(FEE_OWNER_KEY))?.lamports ||
        0;
      try {
        await mintOptionsTx();
      } catch (err) {
        console.error((err as AnchorError).error.errorMessage);
        throw err;
      }
      const minterAfter = await provider.connection.getAccountInfo(
        minter.publicKey
      );
      const feeOwnerAfter =
        (await provider.connection.getAccountInfo(FEE_OWNER_KEY))?.lamports ||
        0;
      if (!minterAfter?.lamports || !minterBefore?.lamports) {
        throw new Error("minter has no lamports");
      }
      const minterDiff = minterAfter?.lamports - minterBefore?.lamports;
      const feeOwnerDiff = feeOwnerAfter - feeOwnerBefore;
      assert.equal(-minterDiff, size.mul(new BN(NFT_MINT_LAMPORTS)));
      assert.equal(feeOwnerDiff, size.mul(new BN(NFT_MINT_LAMPORTS)));
    });
  });
});
Example #9
Source File: mint_v2.ts    From psyoptions with Apache License 2.0 4 votes vote down vote up
describe("mintOption", () => {
  const payer = anchor.web3.Keypair.generate();
  const mintAuthority = anchor.web3.Keypair.generate();
  const program = anchor.workspace.PsyAmerican as Program<PsyAmerican>;
  const provider = program.provider;

  const minter = anchor.web3.Keypair.generate();

  let quoteToken: Token;
  let underlyingToken: Token;
  let optionToken: Token;
  let optionMarket: OptionMarketV2;
  let underlyingAmountPerContract: anchor.BN;
  let quoteAmountPerContract: anchor.BN;
  let expiration: anchor.BN;
  let optionMarketKey: PublicKey;
  let bumpSeed: number;
  let mintFeeKey: PublicKey | null;
  let exerciseFeeKey: PublicKey;
  let optionMintAccount: Keypair;
  let writerTokenMintAccount: Keypair;
  let underlyingAssetPoolAccount: Keypair;
  let quoteAssetPoolAccount: Keypair;
  let remainingAccounts: AccountMeta[] = [];
  let instructions: TransactionInstruction[] = [];

  let optionAccount: Keypair;
  let underlyingAccount: Keypair;
  let writerTokenAccount: Keypair;
  let size = new u64(2);

  beforeEach(async () => {
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(payer.publicKey, 10_000_000_000),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        minter.publicKey,
        10_000_000_000
      ),
      "confirmed"
    );
    size = new u64(2);
  });

  const mintOptionsTx = async (
    opts: {
      underlyingAssetPoolKey?: PublicKey;
      remainingAccounts?: AccountMeta[];
    } = {}
  ) => {
    await program.rpc.mintOptionV2(size, {
      accounts: {
        userAuthority: minter.publicKey,
        underlyingAssetMint: optionMarket?.underlyingAssetMint,
        underlyingAssetPool:
          opts.underlyingAssetPoolKey || optionMarket?.underlyingAssetPool,
        underlyingAssetSrc: underlyingAccount.publicKey,
        optionMint: optionMarket?.optionMint,
        mintedOptionDest: optionAccount.publicKey,
        writerTokenMint: optionMarket?.writerTokenMint,
        mintedWriterTokenDest: writerTokenAccount.publicKey,
        optionMarket: optionMarket?.key,
        tokenProgram: TOKEN_PROGRAM_ID,
      },
      remainingAccounts: opts.remainingAccounts
        ? opts.remainingAccounts
        : remainingAccounts,
      signers: [minter],
    });
  };

  describe("proper mint", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        optionToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          size.mul(optionMarket.underlyingAmountPerContract).muln(2).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));
    });
    it("should mint size OptionTokens", async () => {
      try {
        await mintOptionsTx();
      } catch (err) {
        console.error((err as AnchorError).error.errorMessage);
        throw err;
      }
      const mintInfo = await optionToken.getMintInfo();
      assert.equal(mintInfo.supply.toString(), size.toString());
    });

    it("should mint size WriterTokens", async () => {
      try {
        await mintOptionsTx();
      } catch (err) {
        console.error((err as AnchorError).error.errorMessage);
        throw err;
      }
      const writerToken = new Token(
        provider.connection,
        optionMarket.writerTokenMint,
        TOKEN_PROGRAM_ID,
        payer
      );
      const mintInfo = await writerToken.getMintInfo();
      assert.equal(mintInfo.supply.toString(), size.toString());
    });

    it("should transfer the underlying from the minter to the pool", async () => {
      if (!mintFeeKey) {
        throw new Error("mintFeeKey wasn't set when it should be");
      }
      const underlyingPoolBefore = await underlyingToken.getAccountInfo(
        optionMarket.underlyingAssetPool
      );
      const minterUnderlyingBefore = await underlyingToken.getAccountInfo(
        underlyingAccount.publicKey
      );
      try {
        await mintOptionsTx();
      } catch (err) {
        console.error((err as AnchorError).error.errorMessage);
        throw err;
      }
      const expectedUnderlyingTransfered = size.mul(
        underlyingAmountPerContract
      );

      const underlyingPoolAfter = await underlyingToken.getAccountInfo(
        optionMarket.underlyingAssetPool
      );
      const poolDiff = underlyingPoolAfter.amount.sub(
        underlyingPoolBefore.amount
      );
      assert.equal(
        poolDiff.toString(),
        expectedUnderlyingTransfered.toString()
      );

      const minterUnderlyingAfter = await underlyingToken.getAccountInfo(
        underlyingAccount.publicKey
      );
      const minterUnderlyingDiff = minterUnderlyingAfter.amount.sub(
        minterUnderlyingBefore.amount
      );
      assert.equal(
        expectedUnderlyingTransfered.neg().toString(),
        minterUnderlyingDiff.toString()
      );
    });
  });

  describe("OptionMarket expired", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {
        // set expiration to 2 seconds from now
        expiration: new anchor.BN(new Date().getTime() / 1000 + 2),
      }));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          optionMarket.underlyingAmountPerContract.muln(2).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));
    });
    it("should error", async () => {
      try {
        await wait(2000);
        await mintOptionsTx();
        assert.ok(false);
      } catch (err) {
        const errMsg = "OptionMarket is expired, can't mint";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });

  describe("Underlying pool key differs from option market", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          size.mul(optionMarket.underlyingAmountPerContract.muln(2)).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));
      // Create a new token account and set it as the underlyingAssetPoolAccount
      const { tokenAccount } = await initNewTokenAccount(
        provider.connection,
        payer.publicKey,
        underlyingToken.publicKey,
        payer
      );
      optionMarket.underlyingAssetPool = tokenAccount.publicKey;
    });
    it("should error", async () => {
      try {
        await mintOptionsTx();
        assert.ok(false);
      } catch (err) {
        const errMsg =
          "Underlying pool account does not match the value on the OptionMarket";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });

  describe("OptionToken Mint key differs from option market", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new token mint and set it as the optionMintAccount
      const { mintAccount } = await initNewTokenMint(
        provider.connection,
        payer.publicKey,
        payer
      );
      optionMarket.optionMint = mintAccount.publicKey;
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          size.mul(optionMarket.underlyingAmountPerContract.muln(2)).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));
    });
    it("should error", async () => {
      try {
        await mintOptionsTx();
        assert.ok(false);
      } catch (err) {
        const errMsg =
          "OptionToken mint does not match the value on the OptionMarket";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });

  describe("WriterToken Mint key differs from option market", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new token mint and set it as the optionMintAccount
      const { mintAccount } = await initNewTokenMint(
        provider.connection,
        payer.publicKey,
        payer
      );
      optionMarket.writerTokenMint = mintAccount.publicKey;
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          size.mul(optionMarket.underlyingAmountPerContract.muln(2)).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));
    });
    it("should error", async () => {
      try {
        await mintOptionsTx();
        assert.ok(false);
      } catch (err) {
        const errMsg =
          "WriterToken mint does not match the value on the OptionMarket";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });

  describe("Size <= 0", () => {
    beforeEach(async () => {
      ({
        quoteToken,
        underlyingToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        expiration,
        optionMarketKey,
        bumpSeed,
        mintFeeKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      ({ optionAccount, underlyingAccount, writerTokenAccount } =
        await createMinter(
          provider.connection,
          minter,
          mintAuthority,
          underlyingToken,
          optionMarket.underlyingAmountPerContract.muln(2).toNumber(),
          optionMarket.optionMint,
          optionMarket.writerTokenMint,
          quoteToken
        ));

      // Set the size to 0 to trigger an error
      size = new anchor.BN(0);
    });
    it("should error", async () => {
      try {
        await mintOptionsTx();
        assert.ok(false);
      } catch (err) {
        const errMsg = "The size argument must be > 0";
        assert.equal((err as AnchorError).error.errorMessage, errMsg);
      }
    });
  });
});
Example #10
Source File: initPermissionedMarket.ts    From psyoptions with Apache License 2.0 4 votes vote down vote up
describe("permissioned-markets", () => {
  // Anchor client setup.
  const payer = anchor.web3.Keypair.generate();
  const program = anchor.workspace.PsyAmerican as Program<PsyAmerican>;
  const provider = program.provider;
  // @ts-ignore
  const wallet = provider.wallet as unknown as anchor.Wallet;
  let usdcMint: Keypair;
  // Global PsyOptions accounts
  let optionMarket: OptionMarketV2;
  const mintAuthority = anchor.web3.Keypair.generate();
  before(async () => {
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(payer.publicKey, 10_000_000_000),
      "confirmed"
    );
    const { mintAccount } = await initNewTokenMint(
      provider.connection,
      wallet.payer.publicKey,
      wallet.payer
    );
    usdcMint = mintAccount;
  });
  describe("OptionMarket is initialized", () => {
    let bids: Keypair, asks: Keypair, eventQueue: Keypair;
    beforeEach(async () => {
      // Set up and initialize a new OptionMarket
      const {
        optionMarket: newOptionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, wallet.payer, mintAuthority, program);
      optionMarket = newOptionMarket;
      await initOptionMarket(
        program,
        wallet.payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      ({ bids, asks, eventQueue } = await createFirstSetOfAccounts({
        provider,
        wallet,
        dexProgramId: DEX_PID,
      }));
    });

    it("Initializes new Serum market for OptionMarket", async () => {
      const { serumMarketKey, marketAuthority } = await initSerum(
        provider,
        program,
        optionMarket,
        usdcMint.publicKey,
        eventQueue.publicKey,
        bids.publicKey,
        asks.publicKey,
        DEX_PID
      );
      // Test that a Serum market is created with the proper authority
      const accounts = await provider.connection.getProgramAccounts(DEX_PID, {
        filters: [
          {
            memcmp: {
              /** offset into program account data to start comparison */
              offset: MARKET_STATE_LAYOUT_V3.offsetOf("authority"),
              /** data to match, as base-58 encoded string and limited to less than 129 bytes */
              bytes: marketAuthority.toBase58(),
            },
          },
          {
            memcmp: {
              /** offset into program account data to start comparison */
              offset: MARKET_STATE_LAYOUT_V3.offsetOf("pruneAuthority"),
              /** data to match, as base-58 encoded string and limited to less than 129 bytes */
              bytes: marketAuthority.toBase58(),
            },
          },
        ],
      });
      assert.equal(accounts.length, 1);
    });
    describe("Coin mint is not the Option mint", () => {
      beforeEach(async () => {
        const { mintAccount } = await initNewTokenMint(
          provider.connection,
          payer.publicKey,
          payer
        );
        optionMarket.optionMint = mintAccount.publicKey;
      });

      it("should error", async () => {
        try {
          await initSerum(
            provider,
            program,
            optionMarket,
            usdcMint.publicKey,
            eventQueue.publicKey,
            bids.publicKey,
            asks.publicKey,
            DEX_PID
          );
          assert.ok(false);
        } catch (err) {
          const errMsg = "Coin mint must match option mint";
          assert.equal((err as AnchorError).error.errorMessage, errMsg);
        }
      });
    });
  });
});
Example #11
Source File: prune.ts    From psyoptions with Apache License 2.0 4 votes vote down vote up
describe("Serum Prune", () => {
  const program = anchor.workspace.PsyAmerican as Program<PsyAmerican>;
  const provider = program.provider;
  // @ts-ignore
  const wallet = provider.wallet as unknown as anchor.Wallet;

  const mintAuthority = anchor.web3.Keypair.generate();
  let underlyingToken: Token, usdcToken: Token, optionToken: Token;
  // Global PsyOptions variables
  let optionMarket: OptionMarketV2;
  // Global DEX variables
  let marketProxy: MarketProxy,
    optionAccount: anchor.web3.Keypair,
    marketAuthority: anchor.web3.PublicKey,
    marketAuthorityBump: number,
    usdcMint: anchor.web3.PublicKey,
    usdcAccount: anchor.web3.PublicKey,
    referral: anchor.web3.PublicKey,
    openOrdersKey: anchor.web3.PublicKey,
    openOrdersOwner: anchor.web3.PublicKey,
    openOrdersBump: number;
  describe("option market is not expired", () => {
    before(async () => {
      // create PsyOptions OptionMarket
      const {
        optionMarket: newOptionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, wallet.payer, mintAuthority, program);
      optionMarket = newOptionMarket;
      await initOptionMarket(
        program,
        wallet.payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      [usdcMint, usdcAccount] = await createMintAndVault(
        provider,
        new anchor.BN("1000000000000000000"),
        undefined,
        6
      );
      // Initialize a permissioned Serum Market
      ({ marketAuthority, marketAuthorityBump } =
        await getMarketAndAuthorityInfo(
          program,
          optionMarket,
          DEX_PID,
          usdcMint
        ));
      ({ marketA: marketProxy } = await initMarket(
        provider,
        program,
        marketLoader(provider, program, optionMarket.key, marketAuthorityBump),
        optionMarket,
        usdcMint
      ));
      // Set the token variables for use in later tests
      underlyingToken = new Token(
        provider.connection,
        optionMarket.underlyingAssetMint,
        TOKEN_PROGRAM_ID,
        wallet.payer
      );
      optionToken = new Token(
        provider.connection,
        optionMarket.optionMint,
        TOKEN_PROGRAM_ID,
        wallet.payer
      );
      usdcToken = new Token(
        provider.connection,
        usdcMint,
        TOKEN_PROGRAM_ID,
        wallet.payer
      );
      referral = await usdcToken.createAssociatedTokenAccount(FEE_OWNER_KEY);
      // Create an OpenOrders account for a user
      openOrdersOwner = wallet.publicKey;
      [openOrdersKey, openOrdersBump] =
        await anchor.web3.PublicKey.findProgramAddress(
          [
            openOrdersSeed,
            DEX_PID.toBuffer(),
            marketProxy.market.address.toBuffer(),
            openOrdersOwner.toBuffer(),
          ],
          program.programId
        );
      const dummy = new anchor.web3.Keypair();
      const tx = new anchor.web3.Transaction();
      tx.add(
        await marketProxy.instruction.initOpenOrders(
          openOrdersOwner,
          marketProxy.market.address,
          dummy.publicKey,
          dummy.publicKey
        )
      );
      await provider.sendAndConfirm!(tx);
      // place a bunch of bids on the order book
      const tx2 = new anchor.web3.Transaction();
      tx2.add(
        marketProxy.instruction.newOrderV3({
          owner: wallet.publicKey,
          payer: usdcAccount,
          side: "buy",
          price: 1,
          size: 1,
          orderType: "postOnly",
          clientId: new anchor.BN(999),
          openOrdersAddressKey: openOrdersKey,
          selfTradeBehavior: "abortTransaction",
        })
      );
      tx2.add(
        marketProxy.instruction.newOrderV3({
          owner: wallet.publicKey,
          payer: usdcAccount,
          side: "buy",
          price: 2,
          size: 1,
          orderType: "postOnly",
          clientId: new anchor.BN(1000),
          openOrdersAddressKey: openOrdersKey,
          selfTradeBehavior: "abortTransaction",
        })
      );
      tx2.add(
        marketProxy.instruction.newOrderV3({
          owner: wallet.publicKey,
          payer: usdcAccount,
          side: "buy",
          price: 3,
          size: 1,
          orderType: "postOnly",
          clientId: new anchor.BN(1001),
          openOrdersAddressKey: openOrdersKey,
          selfTradeBehavior: "abortTransaction",
        })
      );
      await provider.sendAndConfirm!(tx2);
    });
    it("should error trying to prune", async () => {
      let openOrders = OpenOrders.load(
        provider.connection,
        openOrdersKey,
        DEX_PID
      );
      let orders = (await openOrders).orders;
      assert.equal(orders.filter((id) => !id.isZero()).length, 3);

      let bids = await marketProxy.market.loadBids(provider.connection);
      let l2 = await bids.getL2(3);
      const expectedBids = [
        [3, 1],
        [2, 1],
        [1, 1],
      ];
      l2.forEach((bid, index) => {
        expect(bid.slice(0, 2)).eql(expectedBids[index]);
      });

      const tx = new anchor.web3.Transaction();
      tx.add(
        await marketProxy.instruction.prune(openOrdersKey, marketAuthority)
      );
      try {
        await provider.sendAndConfirm!(tx);
        assert.ok(false);
      } catch (err) {
        const programError = parseTransactionError(err);
        const errMsg = "Cannot prune the market while it's still active";
        assert.equal(programError.msg, errMsg);
      }

      // Assert that the order book has not changed
      bids = await marketProxy.market.loadBids(provider.connection);
      l2 = await bids.getL2(3);
      assert.equal(l2.length, 3);
    });
  });

  describe("option market is expired", () => {
    before(async () => {
      // create PsyOptions OptionMarket
      const {
        optionMarket: newOptionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, wallet.payer, mintAuthority, program, {
        expiration: new anchor.BN(new Date().getTime() / 1000 + 1),
      });
      optionMarket = newOptionMarket;
      await initOptionMarket(
        program,
        wallet.payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      [usdcMint, usdcAccount] = await createMintAndVault(
        provider,
        new anchor.BN("1000000000000000000"),
        undefined,
        6
      );
      // Initialize a permissioned Serum Market
      ({ marketAuthority, marketAuthorityBump } =
        await getMarketAndAuthorityInfo(
          program,
          optionMarket,
          DEX_PID,
          usdcMint
        ));
      ({ marketA: marketProxy } = await initMarket(
        provider,
        program,
        marketLoader(provider, program, optionMarket.key, marketAuthorityBump),
        optionMarket,
        usdcMint
      ));
      // Set the token variables for use in later tests
      underlyingToken = new Token(
        provider.connection,
        optionMarket.underlyingAssetMint,
        TOKEN_PROGRAM_ID,
        wallet.payer
      );
      optionToken = new Token(
        provider.connection,
        optionMarket.optionMint,
        TOKEN_PROGRAM_ID,
        wallet.payer
      );
      usdcToken = new Token(
        provider.connection,
        usdcMint,
        TOKEN_PROGRAM_ID,
        wallet.payer
      );
      referral = await usdcToken.createAssociatedTokenAccount(FEE_OWNER_KEY);
      // Create an OpenOrders account for a user
      openOrdersOwner = wallet.publicKey;
      [openOrdersKey, openOrdersBump] =
        await anchor.web3.PublicKey.findProgramAddress(
          [
            openOrdersSeed,
            DEX_PID.toBuffer(),
            marketProxy.market.address.toBuffer(),
            openOrdersOwner.toBuffer(),
          ],
          program.programId
        );
      const dummy = new anchor.web3.Keypair();
      const tx = new anchor.web3.Transaction();
      tx.add(
        await marketProxy.instruction.initOpenOrders(
          openOrdersOwner,
          marketProxy.market.address,
          dummy.publicKey,
          dummy.publicKey
        )
      );
      await provider.sendAndConfirm!(tx);
      // place a bunch of bids on the order book
      const tx2 = new anchor.web3.Transaction();
      tx2.add(
        marketProxy.instruction.newOrderV3({
          owner: wallet.publicKey,
          payer: usdcAccount,
          side: "buy",
          price: 1,
          size: 1,
          orderType: "postOnly",
          clientId: new anchor.BN(999),
          openOrdersAddressKey: openOrdersKey,
          selfTradeBehavior: "abortTransaction",
        })
      );
      tx2.add(
        marketProxy.instruction.newOrderV3({
          owner: wallet.publicKey,
          payer: usdcAccount,
          side: "buy",
          price: 2,
          size: 1,
          orderType: "postOnly",
          clientId: new anchor.BN(1000),
          openOrdersAddressKey: openOrdersKey,
          selfTradeBehavior: "abortTransaction",
        })
      );
      tx2.add(
        marketProxy.instruction.newOrderV3({
          owner: wallet.publicKey,
          payer: usdcAccount,
          side: "buy",
          price: 3,
          size: 1,
          orderType: "postOnly",
          clientId: new anchor.BN(1001),
          openOrdersAddressKey: openOrdersKey,
          selfTradeBehavior: "abortTransaction",
        })
      );
      await provider.sendAndConfirm!(tx2);
      // Make sure the option market is expired
      wait(1_000);
    });
    it("should prune the market", async () => {
      let openOrders = OpenOrders.load(
        provider.connection,
        openOrdersKey,
        DEX_PID
      );
      let orders = (await openOrders).orders;
      assert.equal(orders.filter((id) => !id.isZero()).length, 3);

      let bids = await marketProxy.market.loadBids(provider.connection);
      let l2 = await bids.getL2(3);
      const expectedBids = [
        [3, 1],
        [2, 1],
        [1, 1],
      ];
      l2.forEach((bid, index) => {
        expect(bid.slice(0, 2)).eql(expectedBids[index]);
      });

      const tx = new anchor.web3.Transaction();
      tx.add(
        await marketProxy.instruction.prune(openOrdersKey, marketAuthority)
      );
      try {
        await provider.sendAndConfirm!(tx);
      } catch (err) {
        console.log((err as AnchorError).error.errorMessage);
      }

      // Assert that the order book has not changed
      bids = await marketProxy.market.loadBids(provider.connection);
      l2 = await bids.getL2(3);
      assert.equal(l2.length, 0);
    });
  });
});