UNPKG

namillum

Version:
1,807 lines 59.6 kB
import { scillaJSONParams, scillaJSONVal } from "@zilliqa-js/scilla-json-utils";

import { BN, Zilliqa } from "@zilliqa-js/zilliqa";
import { expect } from "@jest/globals";
import fs from "fs";
import { getAddressFromPrivateKey, schnorr } from "@zilliqa-js/crypto";

import {
  getBNum,
  increaseBNum,
  getErrorMsg,
  ZERO_ADDRESS,
  expectTransitions,
  BalanceTracker,
  expectEvents,
  zilBalancesGetter,
  zrc2BalancesGetter,
  zrc2AllowncesGetter,
  expectDeltas,
  expectTokenOwners,
} from "./testutils";

import {
  API,
  TX_PARAMS,
  CONTRACTS,
  FAUCET_PARAMS,
  FIXED_PRICE_ERROR,
  asyncNoop,
} from "./config";

const JEST_WORKER_ID = Number(process.env["JEST_WORKER_ID"]);
const GENESIS_PRIVATE_KEY = global.GENESIS_PRIVATE_KEYS[JEST_WORKER_ID - 1];

const zilliqa = new Zilliqa(API);
zilliqa.wallet.addByPrivateKey(GENESIS_PRIVATE_KEY);

let globalBNum;

let globalTokenAddress;
let globalPaymentTokenAddress;
let globalMarketplaceAddress;
let globalNotAllowedPaymentTokenAddress;

let globalTestAccounts: Array<{
  privateKey: string;
  address: string;
}> = [];
const SELLER = 0;
const BUYER = 1;
const MARKETPLACE_CONTRACT_OWNER = 2;
const STRANGER = 3;
const FORBIDDEN = 4;

const getTestAddr = (index) => globalTestAccounts[index]?.address as string;

beforeAll(async () => {
  const accounts = Array.from({ length: 5 }, schnorr.generatePrivateKey).map(
    (privateKey) => ({
      privateKey,
      address: getAddressFromPrivateKey(privateKey),
    })
  );

  for (const { privateKey, address } of accounts) {
    zilliqa.wallet.addByPrivateKey(privateKey);
    const tx = await zilliqa.blockchain.createTransaction(
      zilliqa.transactions.new(
        {
          ...FAUCET_PARAMS,
          toAddr: address,
        },
        false
      )
    );
    if (!tx.getReceipt()?.success) {
      throw new Error();
    }
  }
  globalTestAccounts = accounts;

  console.table({
    GENESIS_PRIVATE_KEY,
    SELLER: getTestAddr(SELLER),
    BUYER: getTestAddr(BUYER),
    MARKETPLACE_CONTRACT_OWNER: getTestAddr(MARKETPLACE_CONTRACT_OWNER),
    STRANGER: getTestAddr(STRANGER),
    FORBIDDEN: getTestAddr(FORBIDDEN),
  });

  zilliqa.wallet.setDefault(getTestAddr(STRANGER));
  const [, contract] = await zilliqa.contracts
    .new(
      fs.readFileSync(CONTRACTS.wzil.path).toString(),
      scillaJSONParams({
        _scilla_version: ["Uint32", 0],
        contract_owner: ["ByStr20", getTestAddr(STRANGER)],
        name: ["String", CONTRACTS.wzil.name],
        symbol: ["String", CONTRACTS.wzil.symbol],
        decimals: ["Uint32", CONTRACTS.wzil.decimal],
        init_supply: ["Uint128", CONTRACTS.wzil.initial_supply],
      })
    )
    .deploy(TX_PARAMS, 33, 1000, true);
  globalNotAllowedPaymentTokenAddress = contract.address;
});

beforeEach(async () => {
  globalBNum = await getBNum(zilliqa);

  // SELLER is the zrc6 contract owner
  zilliqa.wallet.setDefault(getTestAddr(SELLER));
  let [, contract] = await zilliqa.contracts
    .new(
      fs.readFileSync(CONTRACTS.zrc6.path).toString(),
      scillaJSONParams({
        _scilla_version: ["Uint32", 0],
        initial_contract_owner: ["ByStr20", getTestAddr(SELLER)],
        initial_base_uri: ["String", CONTRACTS.zrc6.baseURI],
        name: ["String", CONTRACTS.zrc6.name],
        symbol: ["String", CONTRACTS.zrc6.symbol],
      })
    )
    .deploy(TX_PARAMS, 33, 1000, true);
  globalTokenAddress = contract.address;
  if (globalTokenAddress === undefined) {
    throw new Error();
  }

  // BUYER is the WZIL contract owner
  zilliqa.wallet.setDefault(getTestAddr(BUYER));
  [, contract] = await zilliqa.contracts
    .new(
      fs.readFileSync(CONTRACTS.wzil.path).toString(),
      scillaJSONParams({
        _scilla_version: ["Uint32", 0],
        contract_owner: ["ByStr20", getTestAddr(BUYER)],
        name: ["String", CONTRACTS.wzil.name],
        symbol: ["String", CONTRACTS.wzil.symbol],
        decimals: ["Uint32", CONTRACTS.wzil.decimal],
        init_supply: ["Uint128", CONTRACTS.wzil.initial_supply],
      })
    )
    .deploy(TX_PARAMS, 33, 1000, true);
  globalPaymentTokenAddress = contract.address;
  if (globalPaymentTokenAddress === undefined) {
    throw new Error();
  }

  // MARKETPLACE_CONTRACT_OWNER is the allowlist contract owner
  zilliqa.wallet.setDefault(getTestAddr(MARKETPLACE_CONTRACT_OWNER));
  [, contract] = await zilliqa.contracts
    .new(
      fs.readFileSync(CONTRACTS.allowlist.path).toString(),
      scillaJSONParams({
        _scilla_version: ["Uint32", 0],
        initial_contract_owner: [
          "ByStr20",
          getTestAddr(MARKETPLACE_CONTRACT_OWNER),
        ],
      })
    )
    .deploy(TX_PARAMS, 33, 1000, true);
  const localAllowlistAddress = contract.address;
  if (localAllowlistAddress === undefined) {
    throw new Error();
  }

  // MARKETPLACE_CONTRACT_OWNER is the zrc6 marketplace contract owner
  zilliqa.wallet.setDefault(getTestAddr(MARKETPLACE_CONTRACT_OWNER));
  [, contract] = await zilliqa.contracts
    .new(
      fs.readFileSync(CONTRACTS.fixed_price.path).toString(),
      scillaJSONParams({
        _scilla_version: ["Uint32", 0],
        initial_contract_owner: [
          "ByStr20",
          getTestAddr(MARKETPLACE_CONTRACT_OWNER),
        ],
      })
    )
    .deploy(TX_PARAMS, 33, 1000, true);
  globalMarketplaceAddress = contract.address;
  if (globalMarketplaceAddress === undefined) {
    throw new Error();
  }

  for (const call of [
    {
      beforeTransition: asyncNoop,
      sender: getTestAddr(MARKETPLACE_CONTRACT_OWNER),
      contract: localAllowlistAddress,
      transition: "Allow",
      transitionParams: scillaJSONParams({
        address_list: [
          "List (ByStr20)",
          [
            getTestAddr(MARKETPLACE_CONTRACT_OWNER),
            getTestAddr(SELLER),
            getTestAddr(BUYER),
            getTestAddr(STRANGER),
          ],
        ],
      }),
      txParams: TX_PARAMS,
    },
    {
      beforeTransition: asyncNoop,
      sender: getTestAddr(MARKETPLACE_CONTRACT_OWNER),
      contract: globalMarketplaceAddress,
      transition: "SetAllowlist",
      transitionParams: scillaJSONParams({
        address: ["ByStr20", localAllowlistAddress],
      }),
      txParams: TX_PARAMS,
    },
    {
      beforeTransition: asyncNoop,
      sender: getTestAddr(SELLER),
      contract: globalTokenAddress,
      transition: "BatchMint",
      transitionParams: scillaJSONParams({
        to_token_uri_pair_list: [
          "List (Pair (ByStr20) (String))",
          [
            [getTestAddr(SELLER), ""],
            [getTestAddr(SELLER), ""],
            [getTestAddr(SELLER), ""],
          ],
        ],
      }),
      txParams: TX_PARAMS,
    },
    {
      beforeTransition: asyncNoop,
      sender: getTestAddr(MARKETPLACE_CONTRACT_OWNER),
      contract: globalMarketplaceAddress,
      transition: "AllowPaymentTokenAddress",
      transitionParams: scillaJSONParams({
        address: ["ByStr20", globalPaymentTokenAddress],
      }),
      txParams: TX_PARAMS,
    },
    {
      beforeTransition: asyncNoop,
      sender: getTestAddr(BUYER),
      contract: globalPaymentTokenAddress,
      transition: "IncreaseAllowance",
      transitionParams: scillaJSONParams({
        spender: ["ByStr20", globalMarketplaceAddress],
        amount: ["Uint128", 100 * 1000],
      }),
      txParams: TX_PARAMS,
    },
    ...[1, 2, 3].map((tokenId) => ({
      beforeTransition: asyncNoop,
      sender: getTestAddr(SELLER),
      contract: globalTokenAddress,
      transition: "SetSpender",
      transitionParams: scillaJSONParams({
        spender: ["ByStr20", globalMarketplaceAddress],
        token_id: ["Uint256", tokenId],
      }),
      txParams: TX_PARAMS,
    })),
  ]) {
    await call.beforeTransition();
    zilliqa.wallet.setDefault(call.sender);
    const tx: any = await zilliqa.contracts
      .at(call.contract)
      .call(call.transition, call.transitionParams, call.txParams);
    if (!tx.receipt.success) {
      throw new Error();
    }
  }
});

describe("Native ZIL", () => {
  beforeEach(async () => {
    for (const call of [
      {
        beforeTransition: asyncNoop,
        sender: getTestAddr(SELLER),
        contract: globalMarketplaceAddress,
        transition: "SetOrder",
        transitionParams: scillaJSONParams({
          token_address: ["ByStr20", globalTokenAddress],
          token_id: ["Uint256", 1],
          payment_token_address: ["ByStr20", ZERO_ADDRESS],
          sale_price: ["Uint128", 10000],
          side: ["Uint32", 0],
          expiration_bnum: ["BNum", globalBNum + 5],
        }),
        txParams: TX_PARAMS,
      },
      {
        beforeTransition: asyncNoop,
        sender: getTestAddr(BUYER),
        contract: globalMarketplaceAddress,
        transition: "SetOrder",
        transitionParams: scillaJSONParams({
          token_address: ["ByStr20", globalTokenAddress],
          token_id: ["Uint256", 1],
          payment_token_address: ["ByStr20", ZERO_ADDRESS],
          sale_price: ["Uint128", 10000],
          side: ["Uint32", 1],
          expiration_bnum: ["BNum", globalBNum + 5],
        }),
        txParams: { ...TX_PARAMS, amount: new BN("10000") },
      },
    ]) {
      await call.beforeTransition();
      zilliqa.wallet.setDefault(call.sender);
      const tx: any = await zilliqa.contracts
        .at(call.contract)
        .call(call.transition, call.transitionParams, call.txParams);
      if (!tx.receipt.success) {
        throw new Error();
      }
    }
    const res = await zilliqa.blockchain.getBalance(globalMarketplaceAddress);
    expect(res.result.balance).toBe("10000");
  });

  const testCases = [
    {
      name: "throws NotAllowedUserError",
      transition: "SetOrder",
      getSender: () => getTestAddr(FORBIDDEN),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 20000],
        side: ["Uint32", 0],
        expiration_bnum: ["BNum", globalBNum + 5],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.NotAllowedUserError,
      want: undefined,
    },
    {
      name: "throws NotTokenOwnerError (stranger creates sell order for token #1)",
      transition: "SetOrder",
      getSender: () => getTestAddr(STRANGER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 20000],
        side: ["Uint32", 0],
        expiration_bnum: ["BNum", globalBNum + 5],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.NotTokenOwnerError,
      want: undefined,
    },
    {
      name: "throws TokenOwnerError (seller creates buy order for token #1)",
      transition: "SetOrder",
      getSender: () => getTestAddr(SELLER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 20000],
        side: ["Uint32", 1],
        expiration_bnum: ["BNum", globalBNum + 5],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.TokenOwnerError,
      want: undefined,
    },
    {
      name: "throws NotSelfError (stranger must not update the order)",
      transition: "SetOrder",
      txAmount: 10000,
      getSender: () => getTestAddr(STRANGER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 1],
        expiration_bnum: ["BNum", globalBNum + 10],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.NotSelfError,
      want: undefined,
    },
    {
      name: "buyer updates expiration_bnum of buy order",
      transition: "SetOrder",
      txAmount: 0, // _amount is irelevant for the update operaton
      getSender: () => getTestAddr(BUYER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 1],
        expiration_bnum: ["BNum", 99999],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(SELLER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: 0,
          [getTestAddr(SELLER)]: 0,
          [getTestAddr(BUYER)]: 0,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 0,
          [getTestAddr(STRANGER)]: 0,
        }),
        getAllowanceDeltas: () => ({
          [[getTestAddr(BUYER), globalMarketplaceAddress].join()]: 0,
        }),
        events: [
          {
            name: "SetOrder",
            getParams: () => ({
              maker: ["ByStr20", getTestAddr(BUYER).toLowerCase()],
              side: ["Uint32", 1],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", ZERO_ADDRESS],
              sale_price: ["Uint128", 10000],
              expiration_bnum: ["BNum", (99999).toString()],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.buy_orders)).toBe(
            JSON.stringify({
              [globalTokenAddress.toLowerCase()]: {
                [1]: {
                  [ZERO_ADDRESS.toLowerCase()]: {
                    [10000]: scillaJSONVal(
                      `${globalMarketplaceAddress}.Order.Order.of.ByStr20.BNum`,
                      [getTestAddr(BUYER), 99999]
                    ),
                  },
                },
              },
            })
          );
        },
      },
    },
    {
      name: "Seller creates sell order for token #1",
      transition: "SetOrder",
      getSender: () => getTestAddr(SELLER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 20000],
        side: ["Uint32", 0],
        expiration_bnum: ["BNum", globalBNum + 5],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(SELLER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: 0,
          [getTestAddr(SELLER)]: 0,
          [getTestAddr(BUYER)]: 0,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 0,
          [getTestAddr(STRANGER)]: 0,
        }),
        events: [
          {
            name: "SetOrder",
            getParams: () => ({
              maker: ["ByStr20", getTestAddr(SELLER).toLowerCase()],
              side: ["Uint32", 0],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", ZERO_ADDRESS],
              sale_price: ["Uint128", 20000],
              expiration_bnum: ["BNum", (globalBNum + 5).toString()],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.sell_orders)).toBe(
            JSON.stringify({
              [globalTokenAddress.toLowerCase()]: {
                [1]: {
                  [ZERO_ADDRESS]: {
                    [10000]: scillaJSONVal(
                      `${globalMarketplaceAddress}.Order.Order.of.ByStr20.BNum`,
                      [getTestAddr(SELLER), globalBNum + 5]
                    ),
                    [20000]: scillaJSONVal(
                      `${globalMarketplaceAddress}.Order.Order.of.ByStr20.BNum`,
                      [getTestAddr(SELLER), globalBNum + 5]
                    ),
                  },
                },
              },
            })
          );
        },
      },
    },
    {
      name: "Buyer creates buy order for token #1",
      transition: "SetOrder",
      txAmount: 20000,
      getSender: () => getTestAddr(BUYER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 20000],
        side: ["Uint32", 1],
        expiration_bnum: ["BNum", globalBNum + 5],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(SELLER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: 20000,
          [getTestAddr(SELLER)]: 0,
          [getTestAddr(BUYER)]: -20000,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 0,
          [getTestAddr(STRANGER)]: 0,
        }),
        events: [
          {
            name: "SetOrder",
            getParams: () => ({
              maker: ["ByStr20", getTestAddr(BUYER).toLowerCase()],
              side: ["Uint32", 1],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", ZERO_ADDRESS],
              sale_price: ["Uint128", 20000],
              expiration_bnum: ["BNum", (globalBNum + 5).toString()],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.buy_orders)).toBe(
            JSON.stringify({
              [globalTokenAddress.toLowerCase()]: {
                ["1"]: {
                  [ZERO_ADDRESS]: {
                    [10000]: scillaJSONVal(
                      `${globalMarketplaceAddress}.Order.Order.of.ByStr20.BNum`,
                      [getTestAddr(BUYER), globalBNum + 5]
                    ),
                    [20000]: scillaJSONVal(
                      `${globalMarketplaceAddress}.Order.Order.of.ByStr20.BNum`,
                      [getTestAddr(BUYER), globalBNum + 5]
                    ),
                  },
                },
              },
            })
          );
        },
      },
    },

    {
      name: "throws NotAllowedUserError",
      transition: "FulfillOrder",
      txAmount: 10000,
      getSender: () => getTestAddr(FORBIDDEN),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 0],
        dest: ["ByStr20", getTestAddr(BUYER)],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.NotAllowedUserError,
      want: undefined,
    },
    {
      name: "throws SellOrderNotFoundError",
      transition: "FulfillOrder",
      getSender: () => getTestAddr(BUYER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 999],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 0],
        dest: ["ByStr20", getTestAddr(BUYER)],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.SellOrderNotFoundError,
      want: undefined,
    },
    {
      name: "throws BuyOrderNotFoundError",
      transition: "FulfillOrder",
      getSender: () => getTestAddr(SELLER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 999],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 1],
        dest: ["ByStr20", getTestAddr(SELLER)],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.BuyOrderNotFoundError,
      want: undefined,
    },
    {
      name: "throws ExpiredError",
      transition: "FulfillOrder",
      getSender: () => getTestAddr(BUYER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 0],
        dest: ["ByStr20", getTestAddr(BUYER)],
      }),
      beforeTransition: async () => {
        await increaseBNum(zilliqa, 5);
      },
      error: FIXED_PRICE_ERROR.ExpiredError,
      want: undefined,
    },
    {
      name: "throws NotEqualAmountError",
      transition: "FulfillOrder",
      txAmount: 1000,
      getSender: () => getTestAddr(BUYER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 0],
        dest: ["ByStr20", getTestAddr(BUYER)],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.NotEqualAmountError,
      want: undefined,
    },
    {
      name: "Buyer fullfills sell order",
      transition: "FulfillOrder",
      txAmount: 10000,
      getSender: () => getTestAddr(BUYER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 0],
        dest: ["ByStr20", getTestAddr(BUYER)],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(BUYER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: 0,
          [getTestAddr(SELLER)]: 9750,
          [getTestAddr(BUYER)]: -10000,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 250,
          [getTestAddr(STRANGER)]: 0,
        }),
        events: [
          {
            name: "FulfillOrder",
            getParams: () => ({
              taker: ["ByStr20", getTestAddr(BUYER)],
              side: ["Uint32", 0],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", ZERO_ADDRESS],
              sale_price: ["Uint128", 10000],
              seller: ["ByStr20", getTestAddr(SELLER)],
              buyer: ["ByStr20", getTestAddr(BUYER)],
              asset_recipient: ["ByStr20", getTestAddr(BUYER)],
              payment_tokens_recipient: ["ByStr20", getTestAddr(SELLER)],
              royalty_recipient: ["ByStr20", getTestAddr(SELLER)],
              royalty_amount: ["Uint128", 1000],
              service_fee: ["Uint128", 250],
            }),
          },
          // NFT transfer
          {
            name: "TransferFrom",
            getParams: () => ({
              from: ["ByStr20", getTestAddr(SELLER)],
              to: ["ByStr20", getTestAddr(BUYER)],
              token_id: ["Uint256", 1],
            }),
          },
        ],
        transitions: [
          {
            amount: 1000,
            recipient: getTestAddr(SELLER),
            tag: "AddFunds",
            getParams: () => ({}),
          },
          {
            amount: 250,
            recipient: getTestAddr(MARKETPLACE_CONTRACT_OWNER),
            tag: "AddFunds",
            getParams: () => ({}),
          },
          {
            amount: 8750,
            recipient: getTestAddr(SELLER),
            tag: "AddFunds",
            getParams: () => ({}),
          },
          // NFT transfer
          {
            tag: "TransferFrom",
            getParams: () => ({
              to: ["ByStr20", getTestAddr(BUYER)],
              token_id: ["Uint256", 1],
            }),
          },
          {
            tag: "ZRC6_RecipientAcceptTransferFrom",
            getParams: () => ({
              from: ["ByStr20", getTestAddr(SELLER)],
              to: ["ByStr20", getTestAddr(BUYER)],
              token_id: ["Uint256", 1],
            }),
          },
          {
            tag: "ZRC6_TransferFromCallback",
            getParams: () => ({
              from: ["ByStr20", getTestAddr(SELLER)],
              to: ["ByStr20", getTestAddr(BUYER)],
              token_id: ["Uint256", 1],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.sell_orders)).toBe(
            JSON.stringify({ [globalTokenAddress.toLowerCase()]: {} })
          );
        },
      },
    },
    {
      name: "Seller fullfills buy order",
      transition: "FulfillOrder",
      getSender: () => getTestAddr(SELLER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 1],
        dest: ["ByStr20", getTestAddr(BUYER)],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(BUYER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: -10000,
          [getTestAddr(SELLER)]: 1000 + 8750,
          [getTestAddr(BUYER)]: 0,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 250,
          [getTestAddr(STRANGER)]: 0,
        }),
        events: [
          {
            name: "FulfillOrder",
            getParams: () => ({
              taker: ["ByStr20", getTestAddr(SELLER)],
              side: ["Uint32", 1],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", ZERO_ADDRESS],
              sale_price: ["Uint128", 10000],
              seller: ["ByStr20", getTestAddr(SELLER)],
              buyer: ["ByStr20", getTestAddr(BUYER)],
              asset_recipient: ["ByStr20", getTestAddr(BUYER)],
              payment_tokens_recipient: ["ByStr20", getTestAddr(SELLER)],
              royalty_recipient: ["ByStr20", getTestAddr(SELLER)],
              royalty_amount: ["Uint128", 1000],
              service_fee: ["Uint128", 250],
            }),
          },

          // NFT transfer
          {
            name: "TransferFrom",
            getParams: () => ({
              from: ["ByStr20", getTestAddr(SELLER)],
              to: ["ByStr20", getTestAddr(BUYER)],
              token_id: ["Uint256", 1],
            }),
          },
        ],
        transitions: [
          {
            amount: 1000,
            recipient: getTestAddr(SELLER),
            tag: "AddFunds",
            getParams: () => ({}),
          },
          {
            amount: 250,
            recipient: getTestAddr(MARKETPLACE_CONTRACT_OWNER),
            tag: "AddFunds",
            getParams: () => ({}),
          },
          {
            amount: 8750,
            recipient: getTestAddr(SELLER),
            tag: "AddFunds",
            getParams: () => ({}),
          },
          // NFT transfer
          {
            tag: "TransferFrom",
            getParams: () => ({
              to: ["ByStr20", getTestAddr(BUYER)],
              token_id: ["Uint256", 1],
            }),
          },
          {
            tag: "ZRC6_RecipientAcceptTransferFrom",
            getParams: () => ({
              from: ["ByStr20", getTestAddr(SELLER)],
              to: ["ByStr20", getTestAddr(BUYER)],
              token_id: ["Uint256", 1],
            }),
          },
          {
            tag: "ZRC6_TransferFromCallback",
            getParams: () => ({
              from: ["ByStr20", getTestAddr(SELLER)],
              to: ["ByStr20", getTestAddr(BUYER)],
              token_id: ["Uint256", 1],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.buy_orders)).toBe(
            JSON.stringify({
              [globalTokenAddress.toLowerCase()]: {
                [1]: { [ZERO_ADDRESS]: {} },
              },
            })
          );
        },
      },
    },
    {
      name: "throws NotAllowedToCancelOrder by stranger",
      transition: "CancelOrder",
      getSender: () => getTestAddr(STRANGER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 1],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.NotAllowedToCancelOrder,
    },
    {
      name: "Buyer cancels buy order",
      transition: "CancelOrder",
      getSender: () => getTestAddr(BUYER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 1],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(SELLER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: -10000,
          [getTestAddr(SELLER)]: 0,
          [getTestAddr(BUYER)]: 10000,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 0,
          [getTestAddr(STRANGER)]: 0,
        }),
        events: [
          {
            name: "CancelOrder",
            getParams: () => ({
              maker: ["ByStr20", getTestAddr(BUYER)],
              side: ["Uint32", 1],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", ZERO_ADDRESS],
              sale_price: ["Uint128", 10000],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.buy_orders)).toBe(
            JSON.stringify({
              [globalTokenAddress.toLowerCase()]: {
                [1]: { [ZERO_ADDRESS]: {} },
              },
            })
          );
        },
      },
    },
    {
      name: "Seller cancels sell order",
      transition: "CancelOrder",
      getSender: () => getTestAddr(SELLER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", ZERO_ADDRESS],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 0],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(SELLER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: 0,
          [getTestAddr(SELLER)]: 0,
          [getTestAddr(BUYER)]: 0,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 0,
          [getTestAddr(STRANGER)]: 0,
        }),
        events: [
          {
            name: "CancelOrder",
            getParams: () => ({
              maker: ["ByStr20", getTestAddr(SELLER)],
              side: ["Uint32", 0],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", ZERO_ADDRESS],
              sale_price: ["Uint128", 10000],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.sell_orders)).toBe(
            JSON.stringify({
              [globalTokenAddress.toLowerCase()]: {
                [1]: { [ZERO_ADDRESS]: {} },
              },
            })
          );
        },
      },
    },
  ];

  for (const testCase of testCases) {
    it(`${testCase.transition}: ${testCase.name}`, async () => {
      await testCase.beforeTransition();

      let balanceTracker;
      if (testCase.want) {
        balanceTracker = new BalanceTracker(
          zilliqa,
          Object.keys(await testCase.want.getBalanceDeltas()),
          zilBalancesGetter(zilliqa)
        );
        await balanceTracker.init();
      }

      zilliqa.wallet.setDefault(testCase.getSender());

      const tx: any = await zilliqa.contracts
        .at(globalMarketplaceAddress)
        .call(testCase.transition, scillaJSONParams(testCase.getParams()), {
          ...TX_PARAMS,
          amount: new BN(testCase.txAmount || 0),
        });

      if (testCase.want === undefined) {
        // Negative Cases
        expect(tx.receipt.success).toBe(false);
        expect(tx.receipt.exceptions[0].message).toBe(
          getErrorMsg(testCase.error)
        );
      } else {
        // Positive Cases
        if (!tx.receipt.success) {
          console.log(JSON.stringify(tx.receipt));
        }
        expect(tx.receipt.success).toBe(true);
        expectEvents(tx.receipt.event_logs, testCase.want.events);
        testCase.want.transitions &&
          expectTransitions(tx.receipt.transitions, testCase.want.transitions);
        testCase.want.expectState(
          await zilliqa.contracts.at(globalMarketplaceAddress).getState()
        );
        expectTokenOwners(
          (await zilliqa.contracts.at(globalTokenAddress).getState())[
            "token_owners"
          ],
          testCase.want.getTokenOwners()
        );
        expectDeltas(
          await balanceTracker.deltas(),
          await testCase.want?.getBalanceDeltas(),
          tx,
          testCase.getSender()
        );
      }
    });
  }
});

describe("WZIL", () => {
  beforeEach(async () => {
    for (const call of [
      {
        beforeTransition: asyncNoop,
        sender: getTestAddr(SELLER),
        contract: globalMarketplaceAddress,
        transition: "SetOrder",
        transitionParams: scillaJSONParams({
          token_address: ["ByStr20", globalTokenAddress],
          token_id: ["Uint256", 1],
          payment_token_address: ["ByStr20", globalPaymentTokenAddress],
          sale_price: ["Uint128", 10000],
          side: ["Uint32", 0],
          expiration_bnum: ["BNum", globalBNum + 5],
        }),
        txParams: TX_PARAMS,
      },
      {
        beforeTransition: asyncNoop,
        sender: getTestAddr(BUYER),
        contract: globalMarketplaceAddress,
        transition: "SetOrder",
        transitionParams: scillaJSONParams({
          token_address: ["ByStr20", globalTokenAddress],
          token_id: ["Uint256", 1],
          payment_token_address: ["ByStr20", globalPaymentTokenAddress],
          sale_price: ["Uint128", 10000],
          side: ["Uint32", 1],
          expiration_bnum: ["BNum", globalBNum + 5],
        }),
        txParams: { ...TX_PARAMS },
      },
    ]) {
      await call.beforeTransition();
      zilliqa.wallet.setDefault(call.sender);
      const tx: any = await zilliqa.contracts
        .at(call.contract)
        .call(call.transition, call.transitionParams, call.txParams);
      if (!tx.receipt.success) {
        throw new Error();
      }
    }
    const res = await zilliqa.blockchain.getBalance(globalMarketplaceAddress);
    expect(res.result.balance).toBe("0");
  });

  const testCases = [
    {
      name: "throws NotAllowedPaymentToken",
      transition: "SetOrder",
      getSender: () => getTestAddr(SELLER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", globalNotAllowedPaymentTokenAddress],
        sale_price: ["Uint128", 20000],
        side: ["Uint32", 0], // 0 is sell, 1 is buy,
        expiration_bnum: ["BNum", globalBNum + 5],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.NotAllowedPaymentToken,
      want: undefined,
    },
    {
      name: "throws NotTokenOwnerError (stranger creates sell order for token #1)",
      transition: "SetOrder",
      getSender: () => getTestAddr(STRANGER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 20000],
        side: ["Uint32", 0],
        expiration_bnum: ["BNum", globalBNum + 5],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.NotTokenOwnerError,
      want: undefined,
    },
    {
      name: "throws TokenOwnerError (seller must not create a buy order for token #1)",
      transition: "SetOrder",
      getSender: () => getTestAddr(SELLER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 20000],
        side: ["Uint32", 1],
        expiration_bnum: ["BNum", globalBNum + 5],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.TokenOwnerError,
      want: undefined,
    },
    {
      name: "throws NotSelfError (stranger must not update the order)",
      transition: "SetOrder",
      getSender: () => getTestAddr(STRANGER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 1],
        expiration_bnum: ["BNum", globalBNum + 10],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.NotSelfError,
      want: undefined,
    },
    {
      name: "buyer updates expiration_bnum of buy order",
      transition: "SetOrder",
      getSender: () => getTestAddr(BUYER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 1],
        expiration_bnum: ["BNum", 99999],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(SELLER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: 0,
          [getTestAddr(SELLER)]: 0,
          [getTestAddr(BUYER)]: 0,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 0,
          [getTestAddr(STRANGER)]: 0,
        }),
        getAllowanceDeltas: () => ({
          [[getTestAddr(BUYER), globalMarketplaceAddress].join()]: 0,
        }),
        events: [
          {
            name: "SetOrder",
            getParams: () => ({
              maker: ["ByStr20", getTestAddr(BUYER).toLowerCase()],
              side: ["Uint32", 1],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", globalPaymentTokenAddress],
              sale_price: ["Uint128", 10000],
              expiration_bnum: ["BNum", (99999).toString()],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.buy_orders)).toBe(
            JSON.stringify({
              [globalTokenAddress.toLowerCase()]: {
                [1]: {
                  [globalPaymentTokenAddress.toLowerCase()]: {
                    [10000]: scillaJSONVal(
                      `${globalMarketplaceAddress}.Order.Order.of.ByStr20.BNum`,
                      [getTestAddr(BUYER), 99999]
                    ),
                  },
                },
              },
            })
          );
        },
      },
    },
    {
      name: "Seller creates sell order for token #1",
      transition: "SetOrder",
      getSender: () => getTestAddr(SELLER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 20000],
        side: ["Uint32", 0],
        expiration_bnum: ["BNum", globalBNum + 5],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(SELLER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: 0,
          [getTestAddr(SELLER)]: 0,
          [getTestAddr(BUYER)]: 0,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 0,
          [getTestAddr(STRANGER)]: 0,
        }),
        getAllowanceDeltas: () => ({
          [[getTestAddr(BUYER), globalMarketplaceAddress].join()]: 0,
        }),
        events: [
          {
            name: "SetOrder",
            getParams: () => ({
              maker: ["ByStr20", getTestAddr(SELLER).toLowerCase()],
              side: ["Uint32", 0],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", globalPaymentTokenAddress],
              sale_price: ["Uint128", 20000],
              expiration_bnum: ["BNum", (globalBNum + 5).toString()],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.sell_orders)).toBe(
            JSON.stringify({
              [globalTokenAddress.toLowerCase()]: {
                [1]: {
                  [globalPaymentTokenAddress.toLowerCase()]: {
                    [10000]: scillaJSONVal(
                      `${globalMarketplaceAddress}.Order.Order.of.ByStr20.BNum`,
                      [getTestAddr(SELLER), globalBNum + 5]
                    ),
                    [20000]: scillaJSONVal(
                      `${globalMarketplaceAddress}.Order.Order.of.ByStr20.BNum`,
                      [getTestAddr(SELLER), globalBNum + 5]
                    ),
                  },
                },
              },
            })
          );
        },
      },
    },
    {
      name: "Buyer creates buy order for token #1",
      transition: "SetOrder",
      getSender: () => getTestAddr(BUYER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 20000],
        side: ["Uint32", 1],
        expiration_bnum: ["BNum", globalBNum + 5],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(SELLER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: 0,
          [getTestAddr(SELLER)]: 0,
          [getTestAddr(BUYER)]: 0,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 0,
          [getTestAddr(STRANGER)]: 0,
        }),
        getAllowanceDeltas: () => ({
          [[getTestAddr(BUYER), globalMarketplaceAddress].join()]: 0,
        }),
        events: [
          {
            name: "SetOrder",
            getParams: () => ({
              maker: ["ByStr20", getTestAddr(BUYER).toLowerCase()],
              side: ["Uint32", 1],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", globalPaymentTokenAddress],
              sale_price: ["Uint128", 20000],
              expiration_bnum: ["BNum", (globalBNum + 5).toString()],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.buy_orders)).toBe(
            JSON.stringify({
              [globalTokenAddress.toLowerCase()]: {
                [1]: {
                  [globalPaymentTokenAddress.toLowerCase()]: {
                    [10000]: scillaJSONVal(
                      `${globalMarketplaceAddress}.Order.Order.of.ByStr20.BNum`,
                      [getTestAddr(BUYER), globalBNum + 5]
                    ),
                    [20000]: scillaJSONVal(
                      `${globalMarketplaceAddress}.Order.Order.of.ByStr20.BNum`,
                      [getTestAddr(BUYER), globalBNum + 5]
                    ),
                  },
                },
              },
            })
          );
        },
      },
    },
    {
      name: "throws SellOrderNotFoundError",
      transition: "FulfillOrder",
      getSender: () => getTestAddr(BUYER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 999],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 0],
        dest: ["ByStr20", getTestAddr(BUYER)],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.SellOrderNotFoundError,
      want: undefined,
    },
    {
      name: "throws BuyOrderNotFoundError",
      transition: "FulfillOrder",
      getSender: () => getTestAddr(SELLER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 999],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 1],
        dest: ["ByStr20", getTestAddr(SELLER)],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.BuyOrderNotFoundError,
      want: undefined,
    },
    {
      name: "throws ExpiredError",
      transition: "FulfillOrder",
      getSender: () => getTestAddr(BUYER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 0],
        dest: ["ByStr20", getTestAddr(BUYER)],
      }),
      beforeTransition: async () => {
        await increaseBNum(zilliqa, 5);
      },
      error: FIXED_PRICE_ERROR.ExpiredError,
      want: undefined,
    },
    {
      name: "Buyer fullfills sell order",
      transition: "FulfillOrder",
      getSender: () => getTestAddr(BUYER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 0],
        dest: ["ByStr20", getTestAddr(BUYER)],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(BUYER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: 0,
          [getTestAddr(SELLER)]: 9750,
          [getTestAddr(BUYER)]: -10000,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 250,
          [getTestAddr(STRANGER)]: 0,
        }),
        getAllowanceDeltas: () => ({
          [[getTestAddr(BUYER), globalMarketplaceAddress].join()]: -10000,
        }),
        events: [
          {
            name: "FulfillOrder",
            getParams: () => ({
              taker: ["ByStr20", getTestAddr(BUYER)],
              side: ["Uint32", 0],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", globalPaymentTokenAddress],
              sale_price: ["Uint128", 10000],
              seller: ["ByStr20", getTestAddr(SELLER)],
              buyer: ["ByStr20", getTestAddr(BUYER)],
              asset_recipient: ["ByStr20", getTestAddr(BUYER)],
              payment_tokens_recipient: ["ByStr20", getTestAddr(SELLER)],
              royalty_recipient: ["ByStr20", getTestAddr(SELLER)],
              royalty_amount: ["Uint128", 1000],
              service_fee: ["Uint128", 250],
            }),
          },

          // royalty fee
          {
            name: "TransferFromSuccess",
            getParams: () => ({
              initiator: ["ByStr20", globalMarketplaceAddress],
              sender: ["ByStr20", getTestAddr(BUYER)],
              recipient: ["ByStr20", getTestAddr(SELLER)],
              amount: ["Uint128", 1000],
            }),
          },

          // service fee
          {
            name: "TransferFromSuccess",
            getParams: () => ({
              initiator: ["ByStr20", globalMarketplaceAddress],
              sender: ["ByStr20", getTestAddr(BUYER)],
              recipient: ["ByStr20", getTestAddr(MARKETPLACE_CONTRACT_OWNER)],
              amount: ["Uint128", 250],
            }),
          },

          // seller profit
          {
            name: "TransferFromSuccess",
            getParams: () => ({
              initiator: ["ByStr20", globalMarketplaceAddress],
              sender: ["ByStr20", getTestAddr(BUYER)],
              recipient: ["ByStr20", getTestAddr(SELLER)],
              amount: ["Uint128", 8750],
            }),
          },

          // NFT transfer
          {
            name: "TransferFrom",
            getParams: () => ({
              from: ["ByStr20", getTestAddr(SELLER)],
              to: ["ByStr20", getTestAddr(BUYER)],
              token_id: ["Uint256", 1],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.sell_orders)).toBe(
            JSON.stringify({
              [globalTokenAddress.toLowerCase()]: {},
            })
          );
        },
      },
    },
    {
      name: "Seller fullfills buy order",
      transition: "FulfillOrder",
      getSender: () => getTestAddr(SELLER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 1],
        dest: ["ByStr20", getTestAddr(BUYER)],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(BUYER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: 0,
          [getTestAddr(SELLER)]: 1000 + 8750,
          [getTestAddr(BUYER)]: -10000,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 250,
          [getTestAddr(STRANGER)]: 0,
        }),
        getAllowanceDeltas: () => ({
          [[getTestAddr(BUYER), globalMarketplaceAddress].join()]: -10000,
        }),
        events: [
          {
            name: "FulfillOrder",
            getParams: () => ({
              taker: ["ByStr20", getTestAddr(SELLER)],
              side: ["Uint32", 1],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", globalPaymentTokenAddress],
              sale_price: ["Uint128", 10000],
              seller: ["ByStr20", getTestAddr(SELLER)],
              buyer: ["ByStr20", getTestAddr(BUYER)],
              asset_recipient: ["ByStr20", getTestAddr(BUYER)],
              payment_tokens_recipient: ["ByStr20", getTestAddr(SELLER)],
              royalty_recipient: ["ByStr20", getTestAddr(SELLER)],
              royalty_amount: ["Uint128", 1000],
              service_fee: ["Uint128", 250],
            }),
          },

          // royalty fee
          {
            name: "TransferFromSuccess",
            getParams: () => ({
              initiator: ["ByStr20", globalMarketplaceAddress],
              sender: ["ByStr20", getTestAddr(BUYER)],
              recipient: ["ByStr20", getTestAddr(SELLER)],
              amount: ["Uint128", 1000],
            }),
          },

          // service fee
          {
            name: "TransferFromSuccess",
            getParams: () => ({
              initiator: ["ByStr20", globalMarketplaceAddress],
              sender: ["ByStr20", getTestAddr(BUYER)],
              recipient: ["ByStr20", getTestAddr(MARKETPLACE_CONTRACT_OWNER)],
              amount: ["Uint128", 250],
            }),
          },

          // seller profit
          {
            name: "TransferFromSuccess",
            getParams: () => ({
              initiator: ["ByStr20", globalMarketplaceAddress],
              sender: ["ByStr20", getTestAddr(BUYER)],
              recipient: ["ByStr20", getTestAddr(SELLER)],
              amount: ["Uint128", 8750],
            }),
          },

          // NFT transfer
          {
            name: "TransferFrom",
            getParams: () => ({
              from: ["ByStr20", getTestAddr(SELLER)],
              to: ["ByStr20", getTestAddr(BUYER)],
              token_id: ["Uint256", 1],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.buy_orders)).toBe(
            JSON.stringify({
              [globalTokenAddress.toLowerCase()]: {
                [1]: { [globalPaymentTokenAddress.toLowerCase()]: {} },
              },
            })
          );
        },
      },
    },
    {
      name: "throws NotAllowedToCancelOrder by stranger",
      transition: "CancelOrder",
      getSender: () => getTestAddr(STRANGER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 1],
      }),
      beforeTransition: asyncNoop,
      error: FIXED_PRICE_ERROR.NotAllowedToCancelOrder,
    },
    {
      name: "Buyer cancels buy order",
      transition: "CancelOrder",
      getSender: () => getTestAddr(BUYER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 1],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(SELLER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: 0,
          [getTestAddr(SELLER)]: 0,
          [getTestAddr(BUYER)]: 0,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 0,
          [getTestAddr(STRANGER)]: 0,
        }),
        getAllowanceDeltas: () => ({
          [[getTestAddr(BUYER), globalMarketplaceAddress].join()]: 0,
        }),
        events: [
          {
            name: "CancelOrder",
            getParams: () => ({
              maker: ["ByStr20", getTestAddr(BUYER)],
              side: ["Uint32", 1],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", globalPaymentTokenAddress],
              sale_price: ["Uint128", 10000],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.buy_orders)).toBe(
            JSON.stringify({
              [globalTokenAddress.toLowerCase()]: {
                [1]: { [globalPaymentTokenAddress.toLowerCase()]: {} },
              },
            })
          );
        },
      },
    },
    {
      name: "Seller cancels sell order",
      transition: "CancelOrder",
      getSender: () => getTestAddr(SELLER),
      getParams: () => ({
        token_address: ["ByStr20", globalTokenAddress],
        token_id: ["Uint256", 1],
        payment_token_address: ["ByStr20", globalPaymentTokenAddress],
        sale_price: ["Uint128", 10000],
        side: ["Uint32", 0],
      }),
      beforeTransition: asyncNoop,
      error: undefined,
      want: {
        getTokenOwners: () => ({
          1: getTestAddr(SELLER),
          2: getTestAddr(SELLER),
          3: getTestAddr(SELLER),
        }),
        getBalanceDeltas: () => ({
          [globalMarketplaceAddress]: 0,
          [getTestAddr(SELLER)]: 0,
          [getTestAddr(BUYER)]: 0,
          [getTestAddr(MARKETPLACE_CONTRACT_OWNER)]: 0,
          [getTestAddr(STRANGER)]: 0,
        }),
        getAllowanceDeltas: () => ({
          [[getTestAddr(BUYER), globalMarketplaceAddress].join()]: 0,
        }),
        events: [
          {
            name: "CancelOrder",
            getParams: () => ({
              maker: ["ByStr20", getTestAddr(SELLER)],
              side: ["Uint32", 0],
              token_address: ["ByStr20", globalTokenAddress],
              token_id: ["Uint256", 1],
              payment_token_address: ["ByStr20", globalPaymentTokenAddress],
              sale_price: ["Uint128", 10000],
            }),
          },
        ],
        expectState: (state) => {
          expect(JSON.stringify(state.sell_orders)).toBe(
            JSON.stringify({
              [globalTokenAddress.toLowerCase()]: {
                [1]: { [globalPaymentTokenAddress.toLowerCase()]: {} },
              },
            })
          );
        },
      },
    },
  ];

  for (const testCase of testCases) {
    it(`${testCase.transition}: ${testCase.name}`, async () => {
      await testCase.beforeTransition();

      let balanceTracker;
      if (testCase.want) {
        balanceTracker = new BalanceTracker(
          zilliqa,
          Object.keys(await testCase.want.getBalanceDeltas()),
          zrc2BalancesGetter(zilliqa, globalPaymentTokenAddress)
        );
        await balanceTracker.init();
      }

      let allowanceTracker;
      if (testCase.want) {
        allowanceTracker = new BalanceTracker(
          zilliqa,
          Object.keys(await testCase.want.getAllowanceDeltas()),
          zrc2AllowncesGetter(zilliqa, globalPaymentTokenAddress)
        );
        await allowanceTracker.init();
      }

      zilliqa.wallet.setDefault(testCase.getSender());

      const tx: any = await zilliqa.contracts
        .at(globalMarketplaceAddress)
        .call(testCase.transition, scillaJSONParams(testCase.getParams()), {
          ...TX_PARAMS,
        });

      if (testCase.want === undefined) {
        // Negative Cases
        expect(tx.receipt.success).toBe(false);
        expect(tx.receipt.exceptions[0].message).toBe(
          getErrorMsg(testCase.error)
        );
      } else {
        // Positive Cases
        expect(tx.receipt.success).toBe(true);
        expectEvents(tx.receipt.event_logs, testCase.want.events);
        testCase.want.expectState(
          await zilliqa.contracts.at(globalMarketplaceAddress).getState()
        );
        expectTokenOwners(
          (await zilliqa.contracts.at(globalTokenAddress).getState())[
            "token_owners"
          ],
          testCase.want.getTokenOwners()
        );
        expectDeltas(
          await balanceTracker.deltas(),
          await testCase.want?.getBalanceDeltas()
        );
        expectDeltas(
          await allowanceTracker.deltas(),
          await testCase.want?.getAllowanceDeltas()
        );
      }
    });
  }
});