Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: roll base tokens #19

Merged
merged 1 commit into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
});
});
});