import bs58 from 'bs58';
import {Buffer} from 'buffer';
import nacl from 'tweetnacl';
import {expect} from 'chai';

import {Connection} from '../src/connection';
import {Keypair} from '../src/keypair';
import {PublicKey} from '../src/publickey';
import {Transaction, TransactionInstruction} from '../src/transaction';
import {StakeProgram} from '../src/stake-program';
import {SystemProgram} from '../src/system-program';
import {Message} from '../src/message';
import invariant from '../src/util/assert';
import {toBuffer} from '../src/util/to-buffer';
import {helpers} from './mocks/rpc-http';
import {url} from './url';

describe('Transaction', () => {
  describe('compileMessage', () => {
    it('accountKeys are ordered', () => {
      // These pubkeys are chosen specially to be in sort order.
      const payer = new PublicKey(
        '3qMLYYyNvaxNZP7nW8u5abHMoJthYqQehRLbFVPNNcvQ',
      );
      const accountWritableSigner2 = new PublicKey(
        '3XLtLo5Z4DG8b6PteJidF6kFPNDfxWjxv4vTLrjaHTvd',
      );
      const accountWritableSigner3 = new PublicKey(
        '4rvqGPb4sXgyUKQcvmPxnWEZTTiTqNUZ2jjnw7atKVxa',
      );
      const accountSigner4 = new PublicKey(
        '5oGjWjyoKDoXGpboGBfqm9a5ZscyAjRi3xuGYYu1ayQg',
      );
      const accountSigner5 = new PublicKey(
        '65Rkc3VmDEV6zTRGtgdwkTcQUxDJnJszj2s4WoXazYpC',
      );
      const accountWritable6 = new PublicKey(
        '72BxBZ9eD9Ue6zoJ9bzfit7MuaDAnq1qhirgAoFUXz9q',
      );
      const accountWritable7 = new PublicKey(
        'BtYrPUeVphVgRHJkf2bKz8DLRxJdQmZyANrTM12xFqZL',
      );
      const accountRegular8 = new PublicKey(
        'Di1MbqFwpodKzNrkjGaUHhXC4TJ1SHUAxo9agPZphNH1',
      );
      const accountRegular9 = new PublicKey(
        'DYzzsfHTgaNhCgn7wMaciAYuwYsGqtVNg9PeFZhH93Pc',
      );
      const programId = new PublicKey(
        'Fx9svCTdxnACvmEmx672v2kP1or4G1zC73tH7XsXbKkP',
      );

      const recentBlockhash = Keypair.generate().publicKey.toBase58();
      const transaction = new Transaction({
        blockhash: recentBlockhash,
        lastValidBlockHeight: 9999,
      }).add({
        keys: [
          // Regular accounts
          {pubkey: accountRegular9, isSigner: false, isWritable: false},
          {pubkey: accountRegular8, isSigner: false, isWritable: false},
          // Writable accounts
          {pubkey: accountWritable7, isSigner: false, isWritable: true},
          {pubkey: accountWritable6, isSigner: false, isWritable: true},
          // Signers
          {pubkey: accountSigner5, isSigner: true, isWritable: false},
          {pubkey: accountSigner4, isSigner: true, isWritable: false},
          // Writable Signers
          {pubkey: accountWritableSigner3, isSigner: true, isWritable: true},
          {pubkey: accountWritableSigner2, isSigner: true, isWritable: true},
          // Payer.
          {pubkey: payer, isSigner: true, isWritable: true},
        ],
        programId,
      });

      transaction.feePayer = payer;

      const message = transaction.compileMessage();
      // Payer comes first.
      expect(message.accountKeys[0].equals(payer)).to.be.true;
      // Writable signers come next, in pubkey order.
      expect(message.accountKeys[1].equals(accountWritableSigner2)).to.be.true;
      expect(message.accountKeys[2].equals(accountWritableSigner3)).to.be.true;
      // Signers come next, in pubkey order.
      expect(message.accountKeys[3].equals(accountSigner4)).to.be.true;
      expect(message.accountKeys[4].equals(accountSigner5)).to.be.true;
      // Writable accounts come next, in pubkey order.
      expect(message.accountKeys[5].equals(accountWritable6)).to.be.true;
      expect(message.accountKeys[6].equals(accountWritable7)).to.be.true;
      // Everything else afterward, in pubkey order.
      expect(message.accountKeys[7].equals(accountRegular8)).to.be.true;
      expect(message.accountKeys[8].equals(accountRegular9)).to.be.true;
      expect(message.accountKeys[9].equals(programId)).to.be.true;
    });

    it('accountKeys collapses signedness and writability of duplicate accounts', () => {
      // These pubkeys are chosen specially to be in sort order.
      const payer = new PublicKey(
        '2eBgaMN8dCnCjx8B8Wrwk974v5WHwA6Vvj4N2mW9KDyt',
      );
      const account2 = new PublicKey(
        'DL8FErokCN7rerLdmJ7tQvsL1FsqDu1sTKLLooWmChiW',
      );
      const account3 = new PublicKey(
        'EdPiTYbXFxNrn1vqD7ZdDyauRKG4hMR6wY54RU1YFP2e',
      );
      const account4 = new PublicKey(
        'FThXbyKK4kYJBngSSuvo9e6kc7mwPHEgw4V8qdmz1h3k',
      );
      const programId = new PublicKey(
        'Gcatgv533efD1z2knsH9UKtkrjRWCZGi12f8MjNaDzmN',
      );
      const account5 = new PublicKey(
        'rBtwG4bx85Exjr9cgoupvP1c7VTe7u5B36rzCg1HYgi',
      );

      const recentBlockhash = Keypair.generate().publicKey.toBase58();
      const transaction = new Transaction({
        blockhash: recentBlockhash,
        lastValidBlockHeight: 9999,
      }).add({
        keys: [
          // Should sort last.
          {pubkey: account5, isSigner: false, isWritable: false},
          {pubkey: account5, isSigner: false, isWritable: false},
          // Should be considered writeable.
          {pubkey: account4, isSigner: false, isWritable: false},
          {pubkey: account4, isSigner: false, isWritable: true},
          // Should be considered a signer.
          {pubkey: account3, isSigner: false, isWritable: false},
          {pubkey: account3, isSigner: true, isWritable: false},
          // Should be considered a writable signer.
          {pubkey: account2, isSigner: false, isWritable: true},
          {pubkey: account2, isSigner: true, isWritable: false},
          // Payer.
          {pubkey: payer, isSigner: true, isWritable: true},
        ],
        programId,
      });

      transaction.feePayer = payer;

      const message = transaction.compileMessage();
      // Payer comes first.
      expect(message.accountKeys[0].equals(payer)).to.be.true;
      // Writable signer comes first.
      expect(message.accountKeys[1].equals(account2)).to.be.true;
      // Signer comes next.
      expect(message.accountKeys[2].equals(account3)).to.be.true;
      // Writable account comes next.
      expect(message.accountKeys[3].equals(account4)).to.be.true;
      // Regular accounts come last.
      expect(message.accountKeys[4].equals(programId)).to.be.true;
      expect(message.accountKeys[5].equals(account5)).to.be.true;
    });

    it('payer is first account meta', () => {
      const payer = Keypair.generate();
      const other = Keypair.generate();
      const recentBlockhash = Keypair.generate().publicKey.toBase58();
      const programId = Keypair.generate().publicKey;
      const transaction = new Transaction({
        blockhash: recentBlockhash,
        lastValidBlockHeight: 9999,
      }).add({
        keys: [
          {pubkey: other.publicKey, isSigner: true, isWritable: true},
          {pubkey: payer.publicKey, isSigner: true, isWritable: true},
        ],
        programId,
      });

      transaction.sign(payer, other);
      const message = transaction.compileMessage();
      expect(message.accountKeys[0]).to.eql(payer.publicKey);
      expect(message.accountKeys[1]).to.eql(other.publicKey);
      expect(message.header.numRequiredSignatures).to.eq(2);
      expect(message.header.numReadonlySignedAccounts).to.eq(0);
      expect(message.header.numReadonlyUnsignedAccounts).to.eq(1);
    });

    it('validation', () => {
      const payer = Keypair.generate();
      const recentBlockhash = Keypair.generate().publicKey.toBase58();

      const transaction = new Transaction();
      expect(() => {
        transaction.compileMessage();
      }).to.throw('Transaction recentBlockhash required');

      transaction.recentBlockhash = recentBlockhash;

      expect(() => {
        transaction.compileMessage();
      }).to.throw('Transaction fee payer required');

      transaction.setSigners(payer.publicKey, Keypair.generate().publicKey);

      expect(() => {
        transaction.compileMessage();
      }).to.throw('unknown signer');

      // Expect compile to succeed with implicit fee payer from signers
      transaction.setSigners(payer.publicKey);
      transaction.compileMessage();

      // Expect compile to succeed with fee payer and no signers
      transaction.signatures = [];
      transaction.feePayer = payer.publicKey;
      transaction.compileMessage();
    });

    it('payer is writable', () => {
      const payer = Keypair.generate();
      const recentBlockhash = Keypair.generate().publicKey.toBase58();
      const programId = Keypair.generate().publicKey;
      const transaction = new Transaction({
        blockhash: recentBlockhash,
        lastValidBlockHeight: 9999,
      }).add({
        keys: [{pubkey: payer.publicKey, isSigner: true, isWritable: false}],
        programId,
      });

      transaction.sign(payer);
      const message = transaction.compileMessage();
      expect(message.accountKeys[0]).to.eql(payer.publicKey);
      expect(message.header.numRequiredSignatures).to.eq(1);
      expect(message.header.numReadonlySignedAccounts).to.eq(0);
      expect(message.header.numReadonlyUnsignedAccounts).to.eq(1);
    });

    it('uses the nonce as the recent blockhash when compiling nonce-based transactions', () => {
      const nonce = new PublicKey(1);
      const nonceAuthority = new PublicKey(2);
      const nonceInfo = {
        nonce: nonce.toBase58(),
        nonceInstruction: SystemProgram.nonceAdvance({
          noncePubkey: nonce,
          authorizedPubkey: nonceAuthority,
        }),
      };
      const transaction = new Transaction({
        feePayer: nonceAuthority,
        nonceInfo,
      });
      const message = transaction.compileMessage();
      expect(message.recentBlockhash).to.equal(nonce.toBase58());
    });

    it('prepends the nonce advance instruction when compiling nonce-based transactions', () => {
      const nonce = new PublicKey(1);
      const nonceAuthority = new PublicKey(2);
      const nonceInfo = {
        nonce: nonce.toBase58(),
        nonceInstruction: SystemProgram.nonceAdvance({
          noncePubkey: nonce,
          authorizedPubkey: nonceAuthority,
        }),
      };
      const transaction = new Transaction({
        feePayer: nonceAuthority,
        nonceInfo,
      }).add(
        SystemProgram.transfer({
          fromPubkey: nonceAuthority,
          lamports: 1,
          toPubkey: new PublicKey(3),
        }),
      );
      const message = transaction.compileMessage();
      expect(message.instructions).to.have.length(2);
      const expectedNonceAdvanceCompiledInstruction = {
        accounts: [1, 4, 0],
        data: (() => {
          const expectedData = Buffer.alloc(4);
          expectedData.writeInt32LE(
            4 /* SystemInstruction::AdvanceNonceAccount */,
            0,
          );
          return bs58.encode(expectedData);
        })(),
        programIdIndex: (() => {
          let foundIndex = -1;
          message.accountKeys.find((publicKey, ii) => {
            if (publicKey.equals(SystemProgram.programId)) {
              foundIndex = ii;
              return true;
            }
          });
          return foundIndex;
        })(),
      };
      expect(message.instructions[0]).to.deep.equal(
        expectedNonceAdvanceCompiledInstruction,
      );
    });

    it('does not prepend the nonce advance instruction when compiling nonce-based transactions if it is already there', () => {
      const nonce = new PublicKey(1);
      const nonceAuthority = new PublicKey(2);
      const nonceInfo = {
        nonce: nonce.toBase58(),
        nonceInstruction: SystemProgram.nonceAdvance({
          noncePubkey: nonce,
          authorizedPubkey: nonceAuthority,
        }),
      };
      const transaction = new Transaction({
        feePayer: nonceAuthority,
        nonceInfo,
      })
        .add(nonceInfo.nonceInstruction)
        .add(
          SystemProgram.transfer({
            fromPubkey: nonceAuthority,
            lamports: 1,
            toPubkey: new PublicKey(3),
          }),
        );
      const message = transaction.compileMessage();
      expect(message.instructions).to.have.length(2);
      const expectedNonceAdvanceCompiledInstruction = {
        accounts: [1, 4, 0],
        data: (() => {
          const expectedData = Buffer.alloc(4);
          expectedData.writeInt32LE(
            4 /* SystemInstruction::AdvanceNonceAccount */,
            0,
          );
          return bs58.encode(expectedData);
        })(),
        programIdIndex: (() => {
          let foundIndex = -1;
          message.accountKeys.find((publicKey, ii) => {
            if (publicKey.equals(SystemProgram.programId)) {
              foundIndex = ii;
              return true;
            }
          });
          return foundIndex;
        })(),
      };
      expect(message.instructions[0]).to.deep.equal(
        expectedNonceAdvanceCompiledInstruction,
      );
    });
  });

  if (process.env.TEST_LIVE) {
    it('getEstimatedFee', async () => {
      const connection = new Connection(url);
      const accountFrom = Keypair.generate();
      const accountTo = Keypair.generate();

      const latestBlockhash = await helpers.latestBlockhash({connection});

      const transaction = new Transaction({
        feePayer: accountFrom.publicKey,
        ...latestBlockhash,
      }).add(
        SystemProgram.transfer({
          fromPubkey: accountFrom.publicKey,
          toPubkey: accountTo.publicKey,
          lamports: 10,
        }),
      );

      const fee = await transaction.getEstimatedFee(connection);
      expect(fee).to.eq(5000);
    });
  }

  it('partialSign', () => {
    const account1 = Keypair.generate();
    const account2 = Keypair.generate();
    const recentBlockhash = account1.publicKey.toBase58(); // Fake recentBlockhash
    const transfer = SystemProgram.transfer({
      fromPubkey: account1.publicKey,
      toPubkey: account2.publicKey,
      lamports: 123,
    });

    const transaction = new Transaction({
      blockhash: recentBlockhash,
      lastValidBlockHeight: 9999,
    }).add(transfer);
    transaction.sign(account1, account2);

    const partialTransaction = new Transaction({
      blockhash: recentBlockhash,
      lastValidBlockHeight: 9999,
    }).add(transfer);
    partialTransaction.setSigners(account1.publicKey, account2.publicKey);
    expect(partialTransaction.signatures[0].signature).to.be.null;
    expect(partialTransaction.signatures[1].signature).to.be.null;

    partialTransaction.partialSign(account1);
    expect(partialTransaction.signatures[0].signature).not.to.be.null;
    expect(partialTransaction.signatures[1].signature).to.be.null;

    expect(() => partialTransaction.serialize()).to.throw();
    expect(() =>
      partialTransaction.serialize({requireAllSignatures: false}),
    ).not.to.throw();

    partialTransaction.partialSign(account2);

    expect(partialTransaction.signatures[0].signature).not.to.be.null;
    expect(partialTransaction.signatures[1].signature).not.to.be.null;

    expect(() => partialTransaction.serialize()).not.to.throw();

    expect(partialTransaction).to.eql(transaction);

    invariant(partialTransaction.signatures[0].signature);
    partialTransaction.signatures[0].signature[0] = 0;
    expect(() =>
      partialTransaction.serialize({requireAllSignatures: false}),
    ).to.throw();
    expect(() =>
      partialTransaction.serialize({
        verifySignatures: false,
        requireAllSignatures: false,
      }),
    ).not.to.throw();
  });

  describe('dedupe', () => {
    const payer = Keypair.generate();
    const duplicate1 = payer;
    const duplicate2 = payer;
    const recentBlockhash = Keypair.generate().publicKey.toBase58();
    const programId = Keypair.generate().publicKey;

    it('setSigners', () => {
      const transaction = new Transaction({
        blockhash: recentBlockhash,
        lastValidBlockHeight: 9999,
      }).add({
        keys: [
          {pubkey: duplicate1.publicKey, isSigner: true, isWritable: true},
          {pubkey: payer.publicKey, isSigner: false, isWritable: true},
          {pubkey: duplicate2.publicKey, isSigner: true, isWritable: false},
        ],
        programId,
      });

      transaction.setSigners(
        payer.publicKey,
        duplicate1.publicKey,
        duplicate2.publicKey,
      );

      expect(transaction.signatures).to.have.length(1);
      expect(transaction.signatures[0].publicKey).to.eql(payer.publicKey);

      const message = transaction.compileMessage();
      expect(message.accountKeys[0]).to.eql(payer.publicKey);
      expect(message.header.numRequiredSignatures).to.eq(1);
      expect(message.header.numReadonlySignedAccounts).to.eq(0);
      expect(message.header.numReadonlyUnsignedAccounts).to.eq(1);

      transaction.signatures;
    });

    it('sign', () => {
      const transaction = new Transaction({
        blockhash: recentBlockhash,
        lastValidBlockHeight: 9999,
      }).add({
        keys: [
          {pubkey: duplicate1.publicKey, isSigner: true, isWritable: true},
          {pubkey: payer.publicKey, isSigner: false, isWritable: true},
          {pubkey: duplicate2.publicKey, isSigner: true, isWritable: false},
        ],
        programId,
      });

      transaction.sign(payer, duplicate1, duplicate2);

      expect(transaction.signatures).to.have.length(1);
      expect(transaction.signatures[0].publicKey).to.eql(payer.publicKey);

      const message = transaction.compileMessage();
      expect(message.accountKeys[0]).to.eql(payer.publicKey);
      expect(message.header.numRequiredSignatures).to.eq(1);
      expect(message.header.numReadonlySignedAccounts).to.eq(0);
      expect(message.header.numReadonlyUnsignedAccounts).to.eq(1);

      transaction.signatures;
    });
  });

  it('transfer signatures', () => {
    const account1 = Keypair.generate();
    const account2 = Keypair.generate();
    const recentBlockhash = account1.publicKey.toBase58(); // Fake recentBlockhash
    const transfer1 = SystemProgram.transfer({
      fromPubkey: account1.publicKey,
      toPubkey: account2.publicKey,
      lamports: 123,
    });
    const transfer2 = SystemProgram.transfer({
      fromPubkey: account2.publicKey,
      toPubkey: account1.publicKey,
      lamports: 123,
    });

    const latestBlockhash = {
      blockhash: recentBlockhash,
      lastValidBlockHeight: 9999,
    };

    const orgTransaction = new Transaction({
      ...latestBlockhash,
    }).add(transfer1, transfer2);
    orgTransaction.sign(account1, account2);

    const newTransaction = new Transaction({
      ...latestBlockhash,
      signatures: orgTransaction.signatures,
    }).add(transfer1, transfer2);

    expect(newTransaction).to.eql(orgTransaction);
  });

  it('dedup signatures', () => {
    const account1 = Keypair.generate();
    const account2 = Keypair.generate();
    const recentBlockhash = account1.publicKey.toBase58(); // Fake recentBlockhash
    const transfer1 = SystemProgram.transfer({
      fromPubkey: account1.publicKey,
      toPubkey: account2.publicKey,
      lamports: 123,
    });
    const transfer2 = SystemProgram.transfer({
      fromPubkey: account1.publicKey,
      toPubkey: account2.publicKey,
      lamports: 123,
    });

    const orgTransaction = new Transaction({
      blockhash: recentBlockhash,
      lastValidBlockHeight: 9999,
    }).add(transfer1, transfer2);
    orgTransaction.sign(account1);
  });

  it('use nonce', () => {
    const account1 = Keypair.generate();
    const account2 = Keypair.generate();
    const nonceAccount = Keypair.generate();
    const nonce = account2.publicKey.toBase58(); // Fake Nonce hash

    const nonceInfo = {
      nonce,
      nonceInstruction: SystemProgram.nonceAdvance({
        noncePubkey: nonceAccount.publicKey,
        authorizedPubkey: account1.publicKey,
      }),
    };

    const transferTransaction = new Transaction({nonceInfo}).add(
      SystemProgram.transfer({
        fromPubkey: account1.publicKey,
        toPubkey: account2.publicKey,
        lamports: 123,
      }),
    );
    transferTransaction.sign(account1);

    expect(transferTransaction.instructions).to.have.length(1);
    expect(transferTransaction.recentBlockhash).to.be.undefined;

    const stakeAccount = Keypair.generate();
    const voteAccount = Keypair.generate();
    const stakeTransaction = new Transaction({nonceInfo}).add(
      StakeProgram.delegate({
        stakePubkey: stakeAccount.publicKey,
        authorizedPubkey: account1.publicKey,
        votePubkey: voteAccount.publicKey,
      }),
    );
    stakeTransaction.sign(account1);

    expect(stakeTransaction.instructions).to.have.length(1);
    expect(stakeTransaction.recentBlockhash).to.be.undefined;
  });

  it('parse wire format and serialize', () => {
    const sender = Keypair.fromSeed(Uint8Array.from(Array(32).fill(8))); // Arbitrary known account
    const recentBlockhash = 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k'; // Arbitrary known recentBlockhash
    const recipient = new PublicKey(
      'J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i99',
    ); // Arbitrary known public key
    const transfer = SystemProgram.transfer({
      fromPubkey: sender.publicKey,
      toPubkey: recipient,
      lamports: 49,
    });
    const expectedTransaction = new Transaction({
      blockhash: recentBlockhash,
      feePayer: sender.publicKey,
      lastValidBlockHeight: 9999,
    }).add(transfer);
    expectedTransaction.sign(sender);

    const serializedTransaction = Buffer.from(
      'AVuErQHaXv0SG0/PchunfxHKt8wMRfMZzqV0tkC5qO6owYxWU2v871AoWywGoFQr4z+q/7mE8lIufNl/kxj+nQ0BAAEDE5j2LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9Q5/Mtmcn8onFxt47xKj+XdXXd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0ROug7bEsbx0xxuDkqEvwUusBAgIAAQwCAAAAMQAAAAAAAAA=',
      'base64',
    );
    const deserializedTransaction = Transaction.from(serializedTransaction);

    expect(expectedTransaction.serialize()).to.eql(serializedTransaction);
    expect(deserializedTransaction.serialize()).to.eql(serializedTransaction);
  });

  it('populate transaction', () => {
    const recentBlockhash = new PublicKey(1).toString();
    const message = {
      accountKeys: [
        new PublicKey(1).toString(),
        new PublicKey(2).toString(),
        new PublicKey(3).toString(),
        new PublicKey(4).toString(),
        new PublicKey(5).toString(),
      ],
      header: {
        numReadonlySignedAccounts: 0,
        numReadonlyUnsignedAccounts: 3,
        numRequiredSignatures: 2,
      },
      instructions: [
        {
          accounts: [1, 2, 3],
          data: bs58.encode(Buffer.alloc(5).fill(9)),
          programIdIndex: 4,
        },
      ],
      recentBlockhash,
    };

    const signatures = [
      bs58.encode(Buffer.alloc(64).fill(1)),
      bs58.encode(Buffer.alloc(64).fill(2)),
    ];

    const transaction = Transaction.populate(new Message(message), signatures);
    expect(transaction.instructions).to.have.length(1);
    expect(transaction.signatures).to.have.length(2);
    expect(transaction.recentBlockhash).to.eq(recentBlockhash);
  });

  it('populate then compile transaction', () => {
    const recentBlockhash = new PublicKey(1).toString();
    const message = new Message({
      accountKeys: [
        new PublicKey(1).toString(),
        new PublicKey(2).toString(),
        new PublicKey(3).toString(),
        new PublicKey(4).toString(),
        new PublicKey(5).toString(),
      ],
      header: {
        numReadonlySignedAccounts: 0,
        numReadonlyUnsignedAccounts: 3,
        numRequiredSignatures: 2,
      },
      instructions: [
        {
          accounts: [1, 2, 3],
          data: bs58.encode(Buffer.alloc(5).fill(9)),
          programIdIndex: 2,
        },
      ],
      recentBlockhash,
    });

    const signatures = [
      bs58.encode(Buffer.alloc(64).fill(1)),
      bs58.encode(Buffer.alloc(64).fill(2)),
    ];

    const transaction = Transaction.populate(message, signatures);
    const compiledMessage = transaction.compileMessage();
    expect(compiledMessage).to.eql(message);

    // show that without caching the message, the populated message
    // might not be the same when re-compiled
    transaction._message = undefined;
    const compiledMessage2 = transaction.compileMessage();
    expect(compiledMessage2).not.to.eql(message);

    // show that even if message is cached, transaction may still
    // be modified
    transaction._message = message;
    transaction.recentBlockhash = new PublicKey(100).toString();
    const compiledMessage3 = transaction.compileMessage();
    expect(compiledMessage3).not.to.eql(message);
  });

  it('constructs a transaction with nonce info', () => {
    const nonce = new PublicKey(1);
    const nonceAuthority = new PublicKey(2);
    const nonceInfo = {
      nonce: nonce.toBase58(),
      nonceInstruction: SystemProgram.nonceAdvance({
        noncePubkey: nonce,
        authorizedPubkey: nonceAuthority,
      }),
    };
    const transaction = new Transaction({nonceInfo});
    expect(transaction.recentBlockhash).to.be.undefined;
    expect(transaction.lastValidBlockHeight).to.be.undefined;
    expect(transaction.nonceInfo).to.equal(nonceInfo);
  });

  it('constructs a transaction with last valid block height', () => {
    const blockhash = 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k';
    const lastValidBlockHeight = 1234;
    const transaction = new Transaction({
      blockhash,
      lastValidBlockHeight,
    });
    expect(transaction.recentBlockhash).to.eq(blockhash);
    expect(transaction.lastValidBlockHeight).to.eq(lastValidBlockHeight);
  });

  it('constructs a transaction with only a recent blockhash', () => {
    const recentBlockhash = 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k';
    const transaction = new Transaction({
      recentBlockhash,
    });
    expect(transaction.recentBlockhash).to.eq(recentBlockhash);
    expect(transaction.lastValidBlockHeight).to.be.undefined;
  });

  it('serialize unsigned transaction', () => {
    const sender = Keypair.fromSeed(Uint8Array.from(Array(32).fill(8))); // Arbitrary known account
    const recentBlockhash = 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k'; // Arbitrary known recentBlockhash
    const recipient = new PublicKey(
      'J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i99',
    ); // Arbitrary known public key
    const transfer = SystemProgram.transfer({
      fromPubkey: sender.publicKey,
      toPubkey: recipient,
      lamports: 49,
    });
    const expectedTransaction = new Transaction({
      blockhash: recentBlockhash,
      lastValidBlockHeight: 9999,
    }).add(transfer);

    // Empty signature array fails.
    expect(expectedTransaction.signatures).to.have.length(0);
    expect(() => {
      expectedTransaction.serialize();
    }).to.throw('Transaction fee payer required');
    expect(() => {
      expectedTransaction.serialize({verifySignatures: false});
    }).to.throw('Transaction fee payer required');
    expect(() => {
      expectedTransaction.serializeMessage();
    }).to.throw('Transaction fee payer required');

    expectedTransaction.feePayer = sender.publicKey;

    // Transactions with missing signatures will fail sigverify.
    expect(() => {
      expectedTransaction.serialize();
    }).to.throw('Signature verification failed');

    // Serializing without signatures is allowed if sigverify disabled.
    expectedTransaction.serialize({verifySignatures: false});

    // Serializing the message is allowed when signature array has null signatures
    expectedTransaction.serializeMessage();

    expectedTransaction.feePayer = undefined;
    expectedTransaction.setSigners(sender.publicKey);
    expect(expectedTransaction.signatures).to.have.length(1);

    // Transactions with missing signatures will fail sigverify.
    expect(() => {
      expectedTransaction.serialize();
    }).to.throw('Signature verification failed');

    // Serializing without signatures is allowed if sigverify disabled.
    expectedTransaction.serialize({verifySignatures: false});

    // Serializing the message is allowed when signature array has null signatures
    expectedTransaction.serializeMessage();

    const expectedSerializationWithNoSignatures = Buffer.from(
      'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
        'AAAAAAAAAAAAAAAAAAABAAEDE5j2LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9' +
        'Q5/Mtmcn8onFxt47xKj+XdXXd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAAAAAAAAAAAAAA' +
        'AAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0ROug7bEsbx0xxuDkqEvwUusBAgIAAQwC' +
        'AAAAMQAAAAAAAAA=',
      'base64',
    );
    expect(expectedTransaction.serialize({requireAllSignatures: false})).to.eql(
      expectedSerializationWithNoSignatures,
    );

    // Properly signed transaction succeeds
    expectedTransaction.partialSign(sender);
    expect(expectedTransaction.signatures).to.have.length(1);
    const expectedSerialization = Buffer.from(
      'AVuErQHaXv0SG0/PchunfxHKt8wMRfMZzqV0tkC5qO6owYxWU2v871AoWywGoFQr4z+q/7mE8lIufNl/' +
        'kxj+nQ0BAAEDE5j2LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9Q5/Mtmcn8onFxt47xKj+XdXX' +
        'd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0' +
        'ROug7bEsbx0xxuDkqEvwUusBAgIAAQwCAAAAMQAAAAAAAAA=',
      'base64',
    );
    expect(expectedTransaction.serialize()).to.eql(expectedSerialization);
    expect(expectedTransaction.signatures).to.have.length(1);
  });

  it('deprecated - externally signed stake delegate', () => {
    const authority = Keypair.fromSeed(Uint8Array.from(Array(32).fill(1)));
    const stake = new PublicKey(2);
    const recentBlockhash = new PublicKey(3).toBuffer();
    const vote = new PublicKey(4);
    var tx = StakeProgram.delegate({
      stakePubkey: stake,
      authorizedPubkey: authority.publicKey,
      votePubkey: vote,
    });
    const from = authority;
    tx.recentBlockhash = bs58.encode(recentBlockhash);
    tx.setSigners(from.publicKey);
    const tx_bytes = tx.serializeMessage();
    const signature = nacl.sign.detached(tx_bytes, from.secretKey);
    tx.addSignature(from.publicKey, toBuffer(signature));
    expect(tx.verifySignatures()).to.be.true;
  });

  it('externally signed stake delegate', () => {
    const authority = Keypair.fromSeed(Uint8Array.from(Array(32).fill(1)));
    const stake = new PublicKey(2);
    const recentBlockhash = new PublicKey(3).toBuffer();
    const vote = new PublicKey(4);
    var tx = StakeProgram.delegate({
      stakePubkey: stake,
      authorizedPubkey: authority.publicKey,
      votePubkey: vote,
    });
    const from = authority;
    tx.recentBlockhash = bs58.encode(recentBlockhash);
    tx.feePayer = from.publicKey;
    const tx_bytes = tx.serializeMessage();
    const signature = nacl.sign.detached(tx_bytes, from.secretKey);
    tx.addSignature(from.publicKey, toBuffer(signature));
    expect(tx.verifySignatures()).to.be.true;
  });

  it('can serialize, deserialize, and reserialize with a partial signer', () => {
    const signer = Keypair.generate();
    const acc0Writable = Keypair.generate();
    const acc1Writable = Keypair.generate();
    const acc2Writable = Keypair.generate();
    const t0 = new Transaction({
      blockhash: 'HZaTsZuhN1aaz9WuuimCFMyH7wJ5xiyMUHFCnZSMyguH',
      feePayer: signer.publicKey,
      lastValidBlockHeight: 9999,
    });
    t0.add(
      new TransactionInstruction({
        keys: [
          {
            pubkey: signer.publicKey,
            isWritable: true,
            isSigner: true,
          },
          {
            pubkey: acc0Writable.publicKey,
            isWritable: true,
            isSigner: false,
          },
        ],
        programId: Keypair.generate().publicKey,
      }),
    );
    t0.add(
      new TransactionInstruction({
        keys: [
          {
            pubkey: acc1Writable.publicKey,
            isWritable: false,
            isSigner: false,
          },
        ],
        programId: Keypair.generate().publicKey,
      }),
    );
    t0.add(
      new TransactionInstruction({
        keys: [
          {
            pubkey: acc2Writable.publicKey,
            isWritable: true,
            isSigner: false,
          },
        ],
        programId: Keypair.generate().publicKey,
      }),
    );
    t0.add(
      new TransactionInstruction({
        keys: [
          {
            pubkey: signer.publicKey,
            isWritable: true,
            isSigner: true,
          },
          {
            pubkey: acc0Writable.publicKey,
            isWritable: false,
            isSigner: false,
          },
          {
            pubkey: acc2Writable.publicKey,
            isWritable: false,
            isSigner: false,
          },
          {
            pubkey: acc1Writable.publicKey,
            isWritable: true,
            isSigner: false,
          },
        ],
        programId: Keypair.generate().publicKey,
      }),
    );
    const t1 = Transaction.from(t0.serialize({requireAllSignatures: false}));
    t1.partialSign(signer);
    t1.serialize();
  });
});
