From 25425a908370daee5609d3d07c3369c4789f1190 Mon Sep 17 00:00:00 2001 From: Kaliman Date: Wed, 1 Feb 2023 12:11:43 +0100 Subject: [PATCH] feat: roll base tokens --- src/init/thea.ts | 5 +- src/modules/index.ts | 1 + src/modules/rollTokens.ts | 73 +++++++++++++ src/types/IBaseTokenManagerContract.ts | 6 ++ src/types/index.ts | 1 + src/utils/consts.ts | 3 +- test/init/thea.spec.ts | 2 + test/modules/rollTokens.spec.ts | 137 +++++++++++++++++++++++++ 8 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 src/modules/rollTokens.ts create mode 100644 test/modules/rollTokens.spec.ts diff --git a/src/init/thea.ts b/src/init/thea.ts index 5402f54..cd8a2be 100644 --- a/src/init/thea.ts +++ b/src/init/thea.ts @@ -11,7 +11,8 @@ import { FungibleTrading, Orderbook, NFTTrading, - Offset + Offset, + RollBaseTokens } from "../modules"; import { TheaNetwork, ProviderOrSigner } from "../types"; import { consts, getCurrentNBTTokenAddress, TheaError } from "../utils"; @@ -35,6 +36,7 @@ export class TheaSDK { readonly nftTokenList: GetTokenList; readonly nftOrderbook: Orderbook; readonly nftTrading: NFTTrading; + readonly rollBaseTokens: RollBaseTokens; private constructor(readonly providerOrSigner: ProviderOrSigner, readonly network: TheaNetwork) { this.unwrap = new Unwrap(this.providerOrSigner, network); @@ -46,6 +48,7 @@ export class TheaSDK { this.nftTokenList = new GetTokenList(network); this.nftOrderbook = new Orderbook(network); this.nftTrading = new NFTTrading(this.providerOrSigner, network, this.nftOrderbook); + this.rollBaseTokens = new RollBaseTokens(this.providerOrSigner, network); } /** diff --git a/src/modules/index.ts b/src/modules/index.ts index d7a8ccb..384bf9f 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -6,6 +6,7 @@ export * from "./offset"; export * from "./tokenization"; export * from "./getCharacteristicsBytes"; export * from "./fungibleTrading"; +export * from "./rollTokens"; export * from "./NFTtrading/getTokenList"; export * from "./NFTtrading/orderbook"; export * from "./NFTtrading/NFTTrading"; diff --git a/src/modules/rollTokens.ts b/src/modules/rollTokens.ts new file mode 100644 index 0000000..0a3df7c --- /dev/null +++ b/src/modules/rollTokens.ts @@ -0,0 +1,73 @@ +import { ProviderOrSigner, IBaseTokenManagerContract, ConvertEvent, TheaNetwork, RollTokensEvent } from "../types"; +import { ContractWrapper, signerRequired, Events, consts, amountShouldBeGTZero } from "../utils"; +import BaseTokenManager_ABI from "../abi/BaseTokenManager_ABI.json"; +import { BigNumberish } from "@ethersproject/bignumber"; +import { ContractReceipt, Event } from "@ethersproject/contracts"; +import { approve, checkBalance, executeWithResponse } from "./shared"; +import { Signer } from "@ethersproject/abstract-signer"; + +export class RollBaseTokens extends ContractWrapper { + constructor(readonly providerOrSigner: ProviderOrSigner, readonly network: TheaNetwork) { + super(providerOrSigner, BaseTokenManager_ABI, consts[`${network}`].baseTokenManagerContract); + } + + /** + * Roll's old base tokens.Which includes burning specified amounts of the old base tokens and vintage tokens and minting new base tokens. + * @param vintage vintage to roll + * @param amount amount of tokens to roll + * @returns A promise fulfilled with the contract transaction. + */ + async rollTokens(vintage: BigNumberish, amount: BigNumberish): Promise { + signerRequired(this.providerOrSigner); + amountShouldBeGTZero(amount); + + await checkBalance(this.providerOrSigner as Signer, this.network, { + token: "ERC20", + amount, + tokenName: "CurrentNBT" + }); + await checkBalance(this.providerOrSigner as Signer, this.network, { + token: "ERC20", + amount, + tokenName: "Vintage" + }); + + const spender = this.contractDetails.address; + await approve(this.providerOrSigner as Signer, this.network, { + token: "ERC20", + spender, + amount, + tokenName: "CurrentNBT" + }); + + await approve(this.providerOrSigner as Signer, this.network, { + token: "ERC20", + spender, + amount, + tokenName: "Vintage" + }); + + return executeWithResponse( + this.contract.rollTokens(vintage, amount), + { + ...this.contractDetails, + contractFunction: "rollTokens" + }, + this.extractInfoFromEvent + ); + } + + extractInfoFromEvent(events?: Event[]): RollTokensEvent { + const response: RollTokensEvent = { user: undefined, vintage: undefined, amount: undefined }; + if (events) { + const event = events.find((event) => event.event === Events.rollTokens); + if (event) { + response.user = event.args?.user.toString(); + response.vintage = event.args?.vintage.toString(); + response.amount = event.args?.amount.toString(); + } + } + + return response; + } +} diff --git a/src/types/IBaseTokenManagerContract.ts b/src/types/IBaseTokenManagerContract.ts index be56fb6..55a16c1 100644 --- a/src/types/IBaseTokenManagerContract.ts +++ b/src/types/IBaseTokenManagerContract.ts @@ -9,6 +9,12 @@ export interface IBaseTokenManagerContract extends Contract { overrides?: TransactionOptions ): Promise; + rollTokens( + id: PromiseOrValue, + amount: PromiseOrValue, + overrides?: TransactionOptions + ): Promise; + recover( id: PromiseOrValue, amount: PromiseOrValue, diff --git a/src/types/index.ts b/src/types/index.ts index 27fea52..998da92 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -144,6 +144,7 @@ export type TokenizationRequest = ClientDetails & { export type ConvertEvent = { id?: string; amount?: string }; export type RecoverEvent = { id?: string; amount?: string }; +export type RollTokensEvent = { user?: string; vintage?: string; amount?: string }; export type BaseTokenCharactaristics = { vintage: BigNumber; diff --git a/src/utils/consts.ts b/src/utils/consts.ts index 560cfeb..2be1352 100644 --- a/src/utils/consts.ts +++ b/src/utils/consts.ts @@ -46,7 +46,8 @@ export const PROPERTY_ABI = [ export enum Events { unwrap = "UnwrapRequested", convert = "Converted", - recover = "Recovered" + recover = "Recovered", + rollTokens = "Rolled" } export type EnvConfig = { diff --git a/test/init/thea.spec.ts b/test/init/thea.spec.ts index d88a439..22bbd77 100644 --- a/test/init/thea.spec.ts +++ b/test/init/thea.spec.ts @@ -7,6 +7,7 @@ import { Offset, Orderbook, Recover, + RollBaseTokens, TheaNetwork, TheaSDK, Unwrap @@ -85,6 +86,7 @@ describe("TheaSDK", () => { expect(Orderbook).toBeCalled(); expect(NFTTrading).toBeCalled(); expect(GetTokenList).toBeCalled(); + expect(RollBaseTokens).toBeCalled(); expect(currentNbtSpy).toBeCalled(); expect(consts[TheaNetwork.MUMBAI].currentNbtTokenContract).toBe("0x5FbDB2315678afecb367f032d93F642f64180aa3"); }); diff --git a/test/modules/rollTokens.spec.ts b/test/modules/rollTokens.spec.ts new file mode 100644 index 0000000..c0c30fb --- /dev/null +++ b/test/modules/rollTokens.spec.ts @@ -0,0 +1,137 @@ +import { BigNumber } from "@ethersproject/bignumber"; +import { ContractTransaction, Event } from "@ethersproject/contracts"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { Wallet } from "@ethersproject/wallet"; +import { Events, TheaError, RollBaseTokens, IBaseTokenManagerContract, TheaNetwork, consts } from "../../src"; +import { PRIVATE_KEY } from "../mocks"; +import * as shared from "../../src/modules/shared"; +import BaseTokenManager_ABI from "../../src/abi/BaseTokenManager_ABI.json"; + +const baseTokenManagerContractAddress = consts[TheaNetwork.GANACHE].baseTokenManagerContract; +jest.mock("../../src/modules/shared", () => { + return { + checkBalance: jest.fn(), + approve: jest.fn(), + executeWithResponse: jest.fn().mockImplementation(() => { + return { + to: baseTokenManagerContractAddress, + from: "0x123", + contractAddress: baseTokenManagerContractAddress, + vintage: "2021", + user: "0x123", + amount: "1000" + }; + }) + }; +}); + +describe("RollTokens", () => { + const providerOrSigner = new Wallet(PRIVATE_KEY); + let rollTokens: RollBaseTokens; + const vintage = "2021"; + const user = "0x123"; + const amount = BigNumber.from(1000); + const network = TheaNetwork.GANACHE; + const contractTransaction: Partial = { + wait: jest.fn().mockResolvedValue({ + to: baseTokenManagerContractAddress, + from: "0x123", + contractAddress: baseTokenManagerContractAddress + }) + }; + + const mockContract: Partial = { + rollTokens: jest.fn().mockResolvedValue(contractTransaction as ContractTransaction) + }; + + beforeEach(() => { + rollTokens = new RollBaseTokens(providerOrSigner, network); + rollTokens.contract = mockContract as IBaseTokenManagerContract; + }); + + describe("rollTokens", () => { + it("should throw error that signer is required", async () => { + rollTokens = new RollBaseTokens(new JsonRpcProvider(), network); + await expect(rollTokens.rollTokens(vintage, amount)).rejects.toThrow( + new TheaError({ + type: "SIGNER_REQUIRED", + message: "Signer is required for this operation. You must pass in a signer on SDK initialization" + }) + ); + }); + + it("should throw error if amount is not valid", async () => { + await expect(rollTokens.rollTokens(vintage, BigNumber.from(-1))).rejects.toThrow( + new TheaError({ + type: "INVALID_TOKEN_AMOUNT_VALUE", + message: "Amount should be greater than 0" + }) + ); + }); + + it("should call rollTokens method from contract", async () => { + const txPromise = Promise.resolve(contractTransaction as ContractTransaction); + const rollTokensSpy = jest.spyOn(rollTokens.contract, "rollTokens").mockReturnValue(txPromise); + const checkBalanceSpy = jest.spyOn(shared, "checkBalance"); + const approveSpy = jest.spyOn(shared, "approve"); + const executeSpy = jest.spyOn(shared, "executeWithResponse"); + + const result = await rollTokens.rollTokens(vintage, amount); + expect(checkBalanceSpy).toBeCalledTimes(2); + expect(approveSpy).toBeCalledTimes(2); + expect(executeSpy).toHaveBeenCalledWith( + txPromise, + { + name: BaseTokenManager_ABI.contractName, + address: baseTokenManagerContractAddress, + contractFunction: "rollTokens" + }, + rollTokens.extractInfoFromEvent + ); + expect(rollTokensSpy).toHaveBeenCalledWith(vintage, amount); + expect(result).toMatchObject({ + to: baseTokenManagerContractAddress, + from: "0x123", + contractAddress: baseTokenManagerContractAddress + }); + }); + }); + + describe("extractInfoFromEvent", () => { + it("should return undefined user, vintage and amount if no events passed", () => { + const result = rollTokens.extractInfoFromEvent(); + expect(result.user).toBeUndefined(); + expect(result.vintage).toBeUndefined(); + expect(result.amount).toBeUndefined(); + }); + + it("should return undefined user, vintage and amount if no RollBaseTokens event passed", () => { + const result = rollTokens.extractInfoFromEvent(); + expect(result.user).toBeUndefined(); + expect(result.vintage).toBeUndefined(); + expect(result.amount).toBeUndefined(); + }); + + it("should return undefined user, vintage and amount if no args in event", () => { + const event: Partial = { + event: Events.rollTokens + }; + const result = rollTokens.extractInfoFromEvent([event as Event]); + expect(result.user).toBeUndefined(); + expect(result.vintage).toBeUndefined(); + expect(result.amount).toBeUndefined(); + }); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + it("should extract user, vintage and amount from event", () => { + const event: Partial = { + event: Events.rollTokens, + args: { vintage, user, amount } as any + }; + const result = rollTokens.extractInfoFromEvent([event as Event]); + expect(result.user).toBe(user); + expect(result.vintage).toBe(vintage); + expect(result.amount).toBe(amount.toString()); + }); + }); +});