From a9b7a8fc09fc77a9f90005235d0c78df77ca9e88 Mon Sep 17 00:00:00 2001 From: williamsfu99 Date: Thu, 13 Jan 2022 15:38:15 -0800 Subject: [PATCH] Add PerpV2LeverageAPI and tests --- package.json | 2 +- src/Set.ts | 11 +- src/api/PerpV2LeverageAPI.ts | 162 +++++++++++++ src/api/SlippageIssuanceAPI.ts | 2 +- src/api/index.ts | 2 + src/types/common.ts | 1 + .../PerpV2LeverageModuleWrapper.ts | 41 ++-- .../SlippageIssuanceModuleWrapper.ts | 8 +- test/api/PerpV2LeverageAPI.spec.ts | 227 ++++++++++++++++++ 9 files changed, 430 insertions(+), 26 deletions(-) create mode 100644 src/api/PerpV2LeverageAPI.ts create mode 100644 test/api/PerpV2LeverageAPI.spec.ts diff --git a/package.json b/package.json index 1f96d0b..7c32612 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "set.js", - "version": "0.4.8", + "version": "0.4.9", "description": "A javascript library for interacting with the Set Protocol v2", "keywords": [ "set.js", diff --git a/src/Set.ts b/src/Set.ts index db049f9..8871a44 100644 --- a/src/Set.ts +++ b/src/Set.ts @@ -1,5 +1,5 @@ /* - Copyright 2020 Set Labs Inc. + Copyright 2022 Set Labs Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import { DebtIssuanceV2API, SlippageIssuanceAPI, } from './api/index'; +import PerpV2LeverageAPI from './api/PerpV2LeverageAPI'; const ethersProviders = require('ethers').providers; @@ -112,6 +113,13 @@ class Set { */ public slippageIssuance: SlippageIssuanceAPI; + /** + * An instance of the PerpV2LeverageAPI class. Contains getters for fetching + * positional (per Set) and notional (across all Sets) units for collateral, vAssets, and debt. + * Initially used for Perpetual Leverage Tokens. + */ + public perpV2Leverage: PerpV2LeverageAPI; + /** * An instance of the BlockchainAPI class. Contains interfaces for * interacting with the blockchain @@ -145,6 +153,7 @@ class Set { this.debtIssuance = new DebtIssuanceAPI(ethersProvider, config.debtIssuanceModuleAddress); this.debtIssuanceV2 = new DebtIssuanceV2API(ethersProvider, config.debtIssuanceModuleV2Address); this.slippageIssuance = new SlippageIssuanceAPI(ethersProvider, config.slippageIssuanceModuleAddress); + this.perpV2Leverage = new PerpV2LeverageAPI(ethersProvider, config.perpV2LeverageModuleAddress); this.blockchain = new BlockchainAPI(ethersProvider, assertions); } } diff --git a/src/api/PerpV2LeverageAPI.ts b/src/api/PerpV2LeverageAPI.ts new file mode 100644 index 0000000..4ef218e --- /dev/null +++ b/src/api/PerpV2LeverageAPI.ts @@ -0,0 +1,162 @@ +/* + Copyright 2022 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +import { ContractTransaction } from 'ethers'; +import { Provider } from '@ethersproject/providers'; +import { Address } from '@setprotocol/set-protocol-v2/utils/types'; +import { TransactionOverrides } from '@setprotocol/set-protocol-v2/dist/typechain'; +import { BigNumber } from 'ethers/lib/ethers'; + +import PerpV2LeverageModuleWrapper from '../wrappers/set-protocol-v2/PerpV2LeverageModuleWrapper'; +import Assertions from '../assertions'; + +/** + * @title PerpV2LeverageAPI + * @author Set Protocol + * + * The PerpV2LeverageAPI exposes issue and redeem functionality for Sets that contain poitions that accrue + * interest per block. The getter function syncs the position balance to the current block, so subsequent blocks + * will cause the position value to be slightly out of sync (a buffer is needed). This API is primarily used for Sets + * that rely on the ALM contracts to manage debt. The manager can define arbitrary issuance logic + * in the manager hook, as well as specify issue and redeem fees. + * + */ +export default class PerpV2LeverageAPI { + private perpV2LeverageModuleWrapper: PerpV2LeverageModuleWrapper; + private assert: Assertions; + + public constructor(provider: Provider, perpV2LeverageModuleAddress: Address, assertions?: Assertions) { + this.perpV2LeverageModuleWrapper = new PerpV2LeverageModuleWrapper(provider, perpV2LeverageModuleAddress); + this.assert = assertions || new Assertions(); + } + + /** + * Initializes the PerpV2LeverageModule to the SetToken. Only callable by the SetToken's manager. + * + * @param setTokenAddress Address of the SetToken to initialize + * @param callerAddress The address of user transferring from (optional) + * @param txOpts Overrides for transaction (optional) + * + * @return Transaction hash of the initialize transaction + */ + public async initializeAsync( + setTokenAddress: Address, + callerAddress: Address = undefined, + txOpts: TransactionOverrides = {} + ): Promise { + this.assert.schema.isValidAddress('setTokenAddress', setTokenAddress); + + return await this.perpV2LeverageModuleWrapper.initialize( + setTokenAddress, + callerAddress, + txOpts, + ); + } + + /** + * Gets the address of the collateral token + * + * @param callerAddress The address of user transferring from (optional) + * @return The address of the ERC20 collateral token + */ + public async getCollateralTokenAsync( + callerAddress: Address = undefined, + ): Promise
{ + return await this.perpV2LeverageModuleWrapper.collateralToken(callerAddress); + } + + /** + * Gets decimals of the collateral token + * + * @param callerAddress The address of user transferring from (optional) + * @return The decimals of the ERC20 collateral token + */ + public async getcollateralDecimalsAsync( + callerAddress: Address = undefined, + ): Promise { + return await this.perpV2LeverageModuleWrapper.collateralDecimals(callerAddress); + } + + /** + * Returns a tuple of arrays representing all positions open for the SetToken. + * + * @param _setToken Instance of SetToken + * @param callerAddress The address of user transferring from (optional) + * + * @return address[] baseToken: addresses + * @return BigNumber[] baseBalance: baseToken balances as notional quantity (10**18) + * @return BigNumber[] quoteBalance: USDC quote asset balances as notional quantity (10**18) + */ + public async getPositionNotionalInfoAsync( + setTokenAddress: Address, + callerAddress: Address = undefined, + ): Promise<(Address|BigNumber)[][]> { + this.assert.schema.isValidAddress('setTokenAddress', setTokenAddress); + + return await this.perpV2LeverageModuleWrapper.getPositionNotionalInfo( + setTokenAddress, + callerAddress, + ); + } + + /** + * Returns a tuple of arrays representing all positions open for the SetToken. + * + * @param _setToken Instance of SetToken + * @param callerAddress The address of user transferring from (optional) + * + * @return address[] baseToken: addresses + * @return BigNumber[] baseUnit: baseToken balances as position unit (10**18) + * @return BigNumber[] quoteUnit: USDC quote asset balances as position unit (10**18) + */ + public async getPositionUnitInfoAsync( + setTokenAddress: Address, + callerAddress: Address = undefined, + ): Promise<(Address|BigNumber)[][]> { + this.assert.schema.isValidAddress('setTokenAddress', setTokenAddress); + + return await this.perpV2LeverageModuleWrapper.getPositionUnitInfo( + setTokenAddress, + callerAddress, + ); + } + + /** + * Gets Perp account info for SetToken. Returns an AccountInfo struct containing account wide + * (rather than position specific) balance info + * + * @param _setToken Instance of the SetToken + * @param callerAddress The address of user transferring from (optional) + * + * @return BigNumber collateral balance (10**18, regardless of underlying collateral decimals) + * @return BigNumber owed realized Pnl` (10**18) + * @return BigNumber pending funding payments (10**18) + * @return BigNumber net quote balance (10**18) + */ + public async getAccountInfoAsync( + setTokenAddress: Address, + callerAddress: Address = undefined, + ): Promise { + this.assert.schema.isValidAddress('setTokenAddress', setTokenAddress); + + return await this.perpV2LeverageModuleWrapper.getAccountInfo( + setTokenAddress, + callerAddress, + ); + } +} diff --git a/src/api/SlippageIssuanceAPI.ts b/src/api/SlippageIssuanceAPI.ts index 7a860c7..97855f0 100644 --- a/src/api/SlippageIssuanceAPI.ts +++ b/src/api/SlippageIssuanceAPI.ts @@ -1,5 +1,5 @@ /* - Copyright 2021 Set Labs Inc. + Copyright 2022 Set Labs Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/api/index.ts b/src/api/index.ts index 6240a74..2f53ec2 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -10,6 +10,7 @@ import PriceOracleAPI from './PriceOracleAPI'; import DebtIssuanceAPI from './DebtIssuanceAPI'; import DebtIssuanceV2API from './DebtIssuanceV2API'; import SlippageIssuanceAPI from './SlippageIssuanceAPI'; +import PerpV2LeverageAPI from './PerpV2LeverageAPI'; import { TradeQuoter, CoinGeckoDataService, @@ -29,6 +30,7 @@ export { DebtIssuanceAPI, DebtIssuanceV2API, SlippageIssuanceAPI, + PerpV2LeverageAPI, TradeQuoter, CoinGeckoDataService, GasOracleService diff --git a/src/types/common.ts b/src/types/common.ts index 336a698..7eda425 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -21,6 +21,7 @@ export interface SetJSConfig { zeroExApiKey?: string; debtIssuanceModuleV2Address: Address; slippageIssuanceModuleAddress: Address; + perpV2LeverageModuleAddress: Address; } export type SetDetails = { diff --git a/src/wrappers/set-protocol-v2/PerpV2LeverageModuleWrapper.ts b/src/wrappers/set-protocol-v2/PerpV2LeverageModuleWrapper.ts index 36ba5a2..25d7dc3 100644 --- a/src/wrappers/set-protocol-v2/PerpV2LeverageModuleWrapper.ts +++ b/src/wrappers/set-protocol-v2/PerpV2LeverageModuleWrapper.ts @@ -67,8 +67,8 @@ export default class PerpV2LeverageModuleWrapper { /** * Gets the address of the collateral token * - * @param callerAddress Address of the method caller - * @return The address of the ERC20 collateral token + * @param callerAddress Address of the method caller + * @return The address of the ERC20 collateral token */ public async collateralToken( callerAddress: Address = undefined, @@ -84,8 +84,8 @@ export default class PerpV2LeverageModuleWrapper { /** * Gets decimals of the collateral token * - * @param callerAddress Address of the method caller - * @return The decimals of the ERC20 collateral token + * @param callerAddress Address of the method caller + * @return The decimals of the ERC20 collateral token */ public async collateralDecimals( callerAddress: Address = undefined, @@ -99,13 +99,14 @@ export default class PerpV2LeverageModuleWrapper { } /** - * Returns a PositionUnitNotionalInfo array representing all positions open for the SetToken. + * Returns a tuple of arrays representing all positions open for the SetToken. * - * @param _setToken Instance of SetToken + * @param _setToken Instance of SetToken + * @param callerAddress Address of the method caller * - * @return address[] baseToken: addresses - * @return BigNumber[] baseBalance: baseToken balances as notional quantity (10**18) - * @return BigNumber[] quoteBalance: USDC quote asset balances as notional quantity (10**18) + * @return address[] baseToken: addresses + * @return BigNumber[] baseBalance: baseToken balances as notional quantity (10**18) + * @return BigNumber[] quoteBalance: USDC quote asset balances as notional quantity (10**18) */ public async getPositionNotionalInfo( setTokenAddress: Address, @@ -122,13 +123,14 @@ export default class PerpV2LeverageModuleWrapper { } /** - * Returns a PositionUnitInfo array representing all positions open for the SetToken. + * Returns a tuple of arrays representing all positions open for the SetToken. * - * @param _setToken Instance of SetToken + * @param _setToken Instance of SetToken + * @param callerAddress Address of the method caller * - * @return address[] baseToken: addresses - * @return BigNumber[] baseUnit: baseToken balances as position unit (10**18) - * @return BigNumber[] quoteUnit: USDC quote asset balances as position unit (10**18) + * @return address[] baseToken: addresses + * @return BigNumber[] baseUnit: baseToken balances as position unit (10**18) + * @return BigNumber[] quoteUnit: USDC quote asset balances as position unit (10**18) */ public async getPositionUnitInfo( setTokenAddress: Address, @@ -148,12 +150,13 @@ export default class PerpV2LeverageModuleWrapper { * Gets Perp account info for SetToken. Returns an AccountInfo struct containing account wide * (rather than position specific) balance info * - * @param _setToken Instance of the SetToken + * @param _setToken Instance of the SetToken + * @param callerAddress Address of the method caller * - * @return BigNumber collateral balance (10**18, regardless of underlying collateral decimals) - * @return BigNumber owed realized Pnl` (10**18) - * @return BigNumber pending funding payments (10**18) - * @return BigNumber net quote balance (10**18) + * @return BigNumber collateral balance (10**18, regardless of underlying collateral decimals) + * @return BigNumber owed realized Pnl` (10**18) + * @return BigNumber pending funding payments (10**18) + * @return BigNumber net quote balance (10**18) */ public async getAccountInfo( setTokenAddress: Address, diff --git a/src/wrappers/set-protocol-v2/SlippageIssuanceModuleWrapper.ts b/src/wrappers/set-protocol-v2/SlippageIssuanceModuleWrapper.ts index 8c45295..0fc104d 100644 --- a/src/wrappers/set-protocol-v2/SlippageIssuanceModuleWrapper.ts +++ b/src/wrappers/set-protocol-v2/SlippageIssuanceModuleWrapper.ts @@ -1,5 +1,5 @@ /* - Copyright 2021 Set Labs Inc. + Copyright 2022 Set Labs Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -24,13 +24,13 @@ import { generateTxOpts } from '../../utils/transactions'; import ContractWrapper from './ContractWrapper'; /** - * @title SlippageIssuanceModuleV2Wrapper + * @title SlippageIssuanceModuleWrapper * @author Set Protocol * - * The SlippageIssuanceModuleV2Wrapper forwards functionality from the SlippageIssuanceModule contract + * The SlippageIssuanceModuleWrapper forwards functionality from the SlippageIssuanceModule contract * */ -export default class SlippageIssuanceModuleV2Wrapper { +export default class SlippageIssuanceModuleWrapper { private provider: Provider; private contracts: ContractWrapper; diff --git a/test/api/PerpV2LeverageAPI.spec.ts b/test/api/PerpV2LeverageAPI.spec.ts new file mode 100644 index 0000000..7900d61 --- /dev/null +++ b/test/api/PerpV2LeverageAPI.spec.ts @@ -0,0 +1,227 @@ +/* + Copyright 2022 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { ethers } from 'ethers'; +import { BigNumber, ContractTransaction } from 'ethers/lib/ethers'; +import { Address } from '@setprotocol/set-protocol-v2/utils/types'; + +import PerpV2LeverageAPI from '@src/api/PerpV2LeverageAPI'; +import PerpV2LeverageModuleWrapper from '@src/wrappers/set-protocol-v2/PerpV2LeverageModuleWrapper'; +import { expect } from '../utils/chai'; + +const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545'); + +jest.mock('@src/wrappers/set-protocol-v2/PerpV2LeverageModuleWrapper'); + +describe('PerpV2LeverageAPI', () => { + let perpV2LeverageModuleAddress: Address; + let setTokenAddress: Address; + let owner: Address; + + let perpV2LeverageAPI: PerpV2LeverageAPI; + let perpV2LeverageModuleWrapper: PerpV2LeverageModuleWrapper; + + beforeEach(async () => { + [ + owner, + perpV2LeverageModuleAddress, + setTokenAddress, + ] = await provider.listAccounts(); + + perpV2LeverageAPI = new PerpV2LeverageAPI(provider, perpV2LeverageModuleAddress); + perpV2LeverageModuleWrapper = (PerpV2LeverageModuleWrapper as any).mock.instances[0]; + }); + + afterEach(() => { + (PerpV2LeverageModuleWrapper as any).mockClear(); + }); + + describe('#initializeAsync', () => { + let subjectSetTokenAddress: Address; + let subjectCallerAddress: Address; + + let subjectTransactionOptions: any; + + beforeEach(async () => { + subjectSetTokenAddress = setTokenAddress; + subjectCallerAddress = owner; + subjectTransactionOptions = {}; + }); + + async function subject(): Promise { + return perpV2LeverageAPI.initializeAsync( + subjectSetTokenAddress, + subjectCallerAddress, + subjectTransactionOptions, + ); + } + + it('should call initialize on the PerpV2LeverageModuleWrapper', async () => { + await subject(); + + expect(perpV2LeverageModuleWrapper.initialize).to.have.beenCalledWith( + subjectSetTokenAddress, + subjectCallerAddress, + subjectTransactionOptions, + ); + }); + }); + + describe('#getCollateralTokenAsync', () => { + let nullCallerAddress: Address; + + beforeEach(async () => { + nullCallerAddress = '0x0000000000000000000000000000000000000000'; + }); + + async function subject(): Promise { + return await perpV2LeverageAPI.getCollateralTokenAsync( + nullCallerAddress, + ); + } + + it('should call the PerpV2LeverageModuleWrapper with correct params', async () => { + await subject(); + + expect(perpV2LeverageModuleWrapper.collateralToken).to.have.beenCalledWith(nullCallerAddress); + }); + }); + + describe('#getcollateralDecimalsAsync', () => { + let nullCallerAddress: Address; + + beforeEach(async () => { + nullCallerAddress = '0x0000000000000000000000000000000000000000'; + }); + + async function subject(): Promise { + return await perpV2LeverageAPI.getcollateralDecimalsAsync( + nullCallerAddress, + ); + } + + it('should call the PerpV2LeverageModuleWrapper with correct params', async () => { + await subject(); + + expect(perpV2LeverageModuleWrapper.collateralDecimals).to.have.beenCalledWith(nullCallerAddress); + }); + }); + + describe('#getPositionNotionalInfoAsync', () => { + let subjectTokenAddress: Address; + let nullCallerAddress: Address; + + beforeEach(async () => { + subjectTokenAddress = '0xEC0815AA9B462ed4fC84B5dFc43Fd2a10a54B569'; + nullCallerAddress = '0x0000000000000000000000000000000000000000'; + }); + + async function subject(): Promise<(Address|BigNumber)[][]> { + return await perpV2LeverageAPI.getPositionNotionalInfoAsync( + subjectTokenAddress, + nullCallerAddress, + ); + } + + it('should call the PerpV2LeverageModuleWrapper with correct params', async () => { + await subject(); + + expect(perpV2LeverageModuleWrapper.getPositionNotionalInfo).to.have.beenCalledWith( + subjectTokenAddress, + nullCallerAddress, + ); + }); + + describe('when the SetToken address is invalid', () => { + beforeEach(async () => { + subjectTokenAddress = '0xInvalidAddress'; + }); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + }); + + describe('#getPositionUnitInfoAsync', () => { + let subjectTokenAddress: Address; + let nullCallerAddress: Address; + + beforeEach(async () => { + subjectTokenAddress = '0xEC0815AA9B462ed4fC84B5dFc43Fd2a10a54B569'; + nullCallerAddress = '0x0000000000000000000000000000000000000000'; + }); + + async function subject(): Promise<(Address|BigNumber)[][]> { + return await perpV2LeverageAPI.getPositionUnitInfoAsync( + subjectTokenAddress, + nullCallerAddress, + ); + } + + it('should call the PerpV2LeverageModuleWrapper with correct params', async () => { + await subject(); + + expect(perpV2LeverageModuleWrapper.getPositionUnitInfo).to.have.beenCalledWith( + subjectTokenAddress, + nullCallerAddress, + ); + }); + + describe('when the SetToken address is invalid', () => { + beforeEach(async () => { + subjectTokenAddress = '0xInvalidAddress'; + }); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + }); + + describe('#getAccountInfoAsync', () => { + let subjectTokenAddress: Address; + let nullCallerAddress: Address; + + beforeEach(async () => { + subjectTokenAddress = '0xEC0815AA9B462ed4fC84B5dFc43Fd2a10a54B569'; + nullCallerAddress = '0x0000000000000000000000000000000000000000'; + }); + + async function subject(): Promise { + return await perpV2LeverageAPI.getAccountInfoAsync( + subjectTokenAddress, + nullCallerAddress, + ); + } + + it('should call the PerpV2LeverageModuleWrapper with correct params', async () => { + await subject(); + + expect(perpV2LeverageModuleWrapper.getAccountInfo).to.have.beenCalledWith(subjectTokenAddress, nullCallerAddress); + }); + + describe('when the SetToken address is invalid', () => { + beforeEach(async () => { + subjectTokenAddress = '0xInvalidAddress'; + }); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + }); +});