Skip to content

Commit

Permalink
Merge pull request #19 from MVPWorkshop/feature/roll-base-tokens
Browse files Browse the repository at this point in the history
feat: roll base tokens
  • Loading branch information
kaliman93 authored Feb 1, 2023
2 parents cff9e83 + 25425a9 commit ac05646
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 2 deletions.
5 changes: 4 additions & 1 deletion src/init/thea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
FungibleTrading,
Orderbook,
NFTTrading,
Offset
Offset,
RollBaseTokens
} from "../modules";
import { TheaNetwork, ProviderOrSigner } from "../types";
import { consts, getCurrentNBTTokenAddress, TheaError } from "../utils";
Expand All @@ -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);
Expand All @@ -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);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
73 changes: 73 additions & 0 deletions src/modules/rollTokens.ts
Original file line number Diff line number Diff line change
@@ -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<IBaseTokenManagerContract> {
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<ContractReceipt> {
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<ConvertEvent>(
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;
}
}
6 changes: 6 additions & 0 deletions src/types/IBaseTokenManagerContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export interface IBaseTokenManagerContract extends Contract {
overrides?: TransactionOptions
): Promise<ContractTransaction>;

rollTokens(
id: PromiseOrValue<BigNumberish>,
amount: PromiseOrValue<BigNumberish>,
overrides?: TransactionOptions
): Promise<ContractTransaction>;

recover(
id: PromiseOrValue<BigNumberish>,
amount: PromiseOrValue<BigNumberish>,
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/utils/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export const PROPERTY_ABI = [
export enum Events {
unwrap = "UnwrapRequested",
convert = "Converted",
recover = "Recovered"
recover = "Recovered",
rollTokens = "Rolled"
}

export type EnvConfig = {
Expand Down
2 changes: 2 additions & 0 deletions test/init/thea.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Offset,
Orderbook,
Recover,
RollBaseTokens,
TheaNetwork,
TheaSDK,
Unwrap
Expand Down Expand Up @@ -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");
});
Expand Down
137 changes: 137 additions & 0 deletions test/modules/rollTokens.spec.ts
Original file line number Diff line number Diff line change
@@ -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<ContractTransaction> = {
wait: jest.fn().mockResolvedValue({
to: baseTokenManagerContractAddress,
from: "0x123",
contractAddress: baseTokenManagerContractAddress
})
};

const mockContract: Partial<IBaseTokenManagerContract> = {
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> = {
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> = {
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());
});
});
});

0 comments on commit ac05646

Please sign in to comment.