From 697ff5121c619961b51cca9277db00c5cfc16399 Mon Sep 17 00:00:00 2001 From: JGiter Date: Tue, 1 Feb 2022 11:57:37 +0200 Subject: [PATCH] refactor(staking): clean old staking BREAKING CHANGE: StakingService is obsolete --- e2e/claims.service.e2e.ts | 3 - e2e/domains.service.e2e.ts | 3 - e2e/staking.pool.e2e.ts | 260 ------------------- e2e/staking.service.e2e.ts | 251 ------------------ src/init.ts | 7 +- src/modules/staking/index.ts | 3 +- src/modules/staking/staking-pool.service.ts | 2 +- src/modules/staking/staking.service.ts | 272 -------------------- src/modules/staking/staking.types.ts | 23 ++ 9 files changed, 27 insertions(+), 797 deletions(-) delete mode 100644 e2e/staking.pool.e2e.ts delete mode 100644 e2e/staking.service.e2e.ts delete mode 100644 src/modules/staking/staking.service.ts create mode 100644 src/modules/staking/staking.types.ts diff --git a/e2e/claims.service.e2e.ts b/e2e/claims.service.e2e.ts index 0dadabe4..742ae61e 100644 --- a/e2e/claims.service.e2e.ts +++ b/e2e/claims.service.e2e.ts @@ -13,7 +13,6 @@ import { RegistrationTypes, SignerService, chainConfigs, - StakingService, DidRegistry, MessagingService, IClaimIssuance, @@ -106,8 +105,6 @@ jest.mock('../src/modules/messaging/messaging.service', () => { }; }); -StakingService.create = jest.fn(); - describe('Enrollment claim tests', () => { let claimsService: ClaimsService; let signerService: SignerService; diff --git a/e2e/domains.service.e2e.ts b/e2e/domains.service.e2e.ts index 89613782..2752662a 100644 --- a/e2e/domains.service.e2e.ts +++ b/e2e/domains.service.e2e.ts @@ -9,7 +9,6 @@ import { ENSOwnerNotValidAddressError, RegistrationTypes, SignerService, - StakingService, } from '../src'; import { replenish, root, rpcUrl, setupENS } from './utils/setup_contracts'; @@ -30,8 +29,6 @@ jest.mock('../src/modules/cacheClient/cacheClient.service', () => { MessagingService.create = (signerService: SignerService) => Promise.resolve(new MessagingService(signerService)); -StakingService.create = (signerService: SignerService, domainsService: DomainsService) => - Promise.resolve(new StakingService(signerService, domainsService)); describe('Domains service', () => { let domainsService: DomainsService; diff --git a/e2e/staking.pool.e2e.ts b/e2e/staking.pool.e2e.ts deleted file mode 100644 index 671156fa..00000000 --- a/e2e/staking.pool.e2e.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { IRoleDefinition } from '@energyweb/iam-contracts'; -import { Methods, Chain } from '@ew-did-registry/did'; -import { KeyTags } from '@ew-did-registry/did-resolver-interface'; - -import { BigNumber, providers, utils, Wallet } from 'ethers'; -import { - DomainsService, - initWithPrivateKeySigner, - MessagingService, - ProviderType, - RegistrationTypes, - SignerService, - StakeStatus, - StakingPool, - StakingService, -} from '../src'; -import { - calculateReward, - defaultPrincipalThreshold, - setupStakingPoolFactory, -} from './utils/staking'; -import { replenish, root, rpcUrl, setupENS } from './utils/setup_contracts'; - -const { parseEther } = utils; - -const defaultMinStakingPeriod = 1; -const patronRewardPortion = 1000; -const orgName = 'orgname'; -const orgDomain = `${orgName}.${root}`; -const patronRole = 'patronrole'; - -const provider = new providers.JsonRpcProvider(rpcUrl); -const rootOwner = Wallet.createRandom().connect(provider); -const orgOwner = Wallet.createRandom().connect(provider); -const orgOwnerDid = `did:${Methods.Erc1056}:${Chain.VOLTA}:${orgOwner.address}`; -const patron = Wallet.createRandom().connect(provider); -const patronDID = `did:${Methods.Erc1056}:${Chain.VOLTA}:${patron.address}`; -MessagingService.create = (signerService: SignerService) => - Promise.resolve(new MessagingService(signerService)); -const mockPublish = jest.fn(); -jest.mock('../src/modules/messaging/messaging.service', () => { - return { - MessagingService: jest.fn().mockImplementation(() => { - return { publish: mockPublish }; - }), - }; -}); - -const mockGetRoleDefinition = jest.fn(); -const mockGetDidDocument = jest.fn().mockImplementation(({ did }: { did: string }) => { - return { publicKey: [{ id: `did:${Methods.Erc1056}:${Chain.VOLTA}:${did}-${KeyTags.OWNER}` }] }; -}); -const mockGetApplicationsByOrgNamespace = jest.fn(); -const mockRequestClaim = jest.fn(); - -jest.mock('../src/modules/cacheClient/cacheClient.service', () => { - return { - CacheClient: jest.fn().mockImplementation(() => { - return { - getRoleDefinition: mockGetRoleDefinition, - getDidDocument: mockGetDidDocument, - getApplicationsByOrganization: mockGetApplicationsByOrgNamespace, - init: jest.fn(), - login: jest.fn(), - requestClaim: mockRequestClaim, - issueClaim: jest.fn(), - }; - }), - }; -}); - -describe('StakingPool tests', () => { - let stakingService: StakingService; - let domainsService: DomainsService; - let signerService: SignerService; - let pool: StakingPool; - - beforeAll(async () => { - await replenish(orgOwner.address); - await replenish(patron.address); - await replenish(rootOwner.address); - await setupENS(rootOwner.address); - await setupStakingPoolFactory(); - let connectToCacheServer; - ({ connectToCacheServer, signerService } = await initWithPrivateKeySigner( - rootOwner.privateKey, - rpcUrl - )); - await signerService.publicKeyAndIdentityToken(); - let connectToDidRegistry; - ({ domainsService, stakingService, connectToDidRegistry } = await connectToCacheServer()); - const { claimsService } = await connectToDidRegistry(); - - const data: IRoleDefinition = { - fields: [], - issuerFields: [], - issuer: { - issuerType: 'DID', - did: [orgOwnerDid], - }, - metadata: [], - roleName: patronRole, - roleType: 'test', - version: 1, - enrolmentPreconditions: [], - }; - - await domainsService.createRole({ - roleName: patronRole, - namespace: root, - data, - }); - - await domainsService.createOrganization({ - orgName, - namespace: root, - data: { orgName }, - returnSteps: false, - }); - - mockGetApplicationsByOrgNamespace.mockReturnValueOnce([]); - await domainsService.changeOrgOwnership({ - namespace: `${orgName}.${root}`, - newOwner: orgOwner.address, - }); - - const registrationTypes = [RegistrationTypes.OnChain]; - await signerService.connect(patron, ProviderType.PrivateKey); - mockGetRoleDefinition.mockReturnValueOnce(data); - await claimsService.createClaimRequest({ - claim: { claimType: `${patronRole}.${root}`, claimTypeVersion: 1, requestorFields: [] }, - registrationTypes, - }); - - const [message] = mockRequestClaim.mock.calls.pop(); - const { id, subjectAgreement, token } = message; - - await signerService.connect(orgOwner, ProviderType.PrivateKey); - mockGetRoleDefinition.mockReturnValueOnce(data); - await claimsService.issueClaimRequest({ - id, - registrationTypes, - requester: patronDID, - subjectAgreement, - token, - }); - }); - - beforeEach(async () => { - jest.clearAllMocks(); - await setupStakingPoolFactory(); - await stakingService.init(); - await launchPool(); - await signerService.connect(patron, ProviderType.PrivateKey); - await replenish(patron.address); - }); - - async function launchPool( - minStakingPeriod = defaultMinStakingPeriod, - principalThreshold = defaultPrincipalThreshold - ) { - await replenish(orgOwner.address, principalThreshold); - await signerService.connect(orgOwner, ProviderType.PrivateKey); - await stakingService.launchPool({ - org: orgDomain, - minStakingPeriod, - patronRewardPortion, - patronRoles: [`${patronRole}.${root}`], - principal: principalThreshold, - }); - pool = (await stakingService.getPool(orgDomain)) as StakingPool; - } - - it('patron should be able to stake', async () => { - const amount = parseEther('0.1'); - await pool.putStake(amount); - return expect(pool.getStake()).resolves.toMatchObject({ status: StakeStatus.STAKING }); - }); - - it('should not be able to stake without having patron role', async () => { - const nonPatron = Wallet.createRandom().connect(provider); - await replenish(await nonPatron.getAddress()); - await signerService.connect(nonPatron, ProviderType.PrivateKey); - return expect(pool.putStake(parseEther('0.1'))).rejects.toThrow( - 'StakingPool: patron is not registered with patron role' - ); - }); - - it("should reject when stake amount isn't provided", async () => { - return expect(pool.putStake(parseEther('0'))).rejects.toThrow( - 'StakingPool: stake amount is not provided' - ); - }); - - it('should be able to stake whole balance', async () => { - const stake = await signerService.balance(); - await pool.putStake(stake); - return expect(pool.getStake()).resolves.toMatchObject({ status: StakeStatus.STAKING }); - }); - - it('stake should not be replenished', async () => { - await pool.putStake(parseEther('0.1')); - return expect(pool.putStake(parseEther('0.1'))).rejects.toThrow( - 'StakingPool: Replenishment of the stake is not allowed' - ); - }); - - it('staker should be able to request withdraw', async () => { - await pool.putStake(parseEther('0.1')); - const requestDelay = await pool.requestWithdrawDelay(); - await new Promise((resolve) => setTimeout(resolve, 1000 * requestDelay)); - await pool.requestWithdraw(); - return expect(pool.getStake()).resolves.toMatchObject({ status: StakeStatus.WITHDRAWING }); - }); - - it("can't request withdraw when no stake", async () => { - return expect(pool.requestWithdraw()).rejects.toThrow('StakingPool: No stake to withdraw'); - }); - - it("can't request withdraw until minimum staking period is last", async () => { - await setupStakingPoolFactory(); - await launchPool(10); - await signerService.connect(patron, ProviderType.PrivateKey); - await pool.putStake(parseEther('0.1')); - return expect(pool.requestWithdraw()).rejects.toThrow( - 'StakingPool: Minimum staking period is not expired yet' - ); - }); - - it("withdraw can't be requested twice", async () => { - await pool.putStake(parseEther('0.1')); - const requestDelay = await pool.requestWithdrawDelay(); - await new Promise((resolve) => setTimeout(resolve, 1000 * requestDelay)); - await pool.requestWithdraw(); - return expect(pool.requestWithdraw()).rejects.toThrow('StakingPool: No stake to withdraw'); - }); - - it('stake can be withdrawn', async () => { - const stake = parseEther('0.1'); - await pool.putStake(stake); - const depositStart = (await pool.getStake()).depositStart; - const requestDelay = await pool.requestWithdrawDelay(); - await new Promise((resolve) => setTimeout(resolve, 1000 * requestDelay)); - await pool.requestWithdraw(); - const depositEnd = (await pool.getStake()).depositEnd; - const expectedReward = calculateReward( - stake, - depositEnd.sub(depositStart), - BigNumber.from(patronRewardPortion) - ); - - expect(await pool.checkReward()).toEqual(expectedReward); - const withdrawalDelay = await pool.withdrawalDelay(); - await new Promise((resolve) => setTimeout(resolve, 1000 * withdrawalDelay)); - const balanceBeforeWithdraw = await patron.getBalance(); - await pool.withdraw(); - const balanceAfterWithdraw = await patron.getBalance(); - expect(balanceAfterWithdraw.eq(balanceBeforeWithdraw.add(stake).add(expectedReward))); - }); -}); diff --git a/e2e/staking.service.e2e.ts b/e2e/staking.service.e2e.ts deleted file mode 100644 index 1fded0bf..00000000 --- a/e2e/staking.service.e2e.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { providers, Wallet } from 'ethers'; -import { IRoleDefinition } from '@energyweb/iam-contracts'; -import { Methods, Chain } from '@ew-did-registry/did'; -import { KeyTags } from '@ew-did-registry/did-resolver-interface'; -import { StakingService } from '../src/modules/staking/staking.service'; -import { ProviderType } from '../src/modules/signer/signer.types'; -import { SignerService } from '../src/modules/signer/signer.service'; -import { DomainsService } from '../src/modules/domains/domains.service'; -import { RegistrationTypes } from '../src/modules/claims/claims.types'; -import { replenish, root, rpcUrl, setupENS } from './utils/setup_contracts'; -import { defaultPrincipalThreshold, setupStakingPoolFactory } from './utils/staking'; -import { initWithPrivateKeySigner } from '../src/init'; -import { MessagingService } from '../src/modules/messaging/messaging.service'; - -const defaultMinStakingPeriod = 1; -const patronRewardPortion = 800; -const patronRole = 'patronRole'; -const orgName = 'orgname'; -const domain = `${orgName}.${root}`; - -const provider = new providers.JsonRpcProvider(rpcUrl); -const rootOwner = Wallet.createRandom().connect(provider); -const orgOwner = Wallet.createRandom().connect(provider); -const orgOwnerDid = `did:${Methods.Erc1056}:${Chain.VOLTA}:${orgOwner.address}`; -const patron = Wallet.createRandom().connect(provider); -const patronDID = `did:${Methods.Erc1056}:${Chain.VOLTA}:${patron.address}`; - -MessagingService.create = (signerService: SignerService) => - Promise.resolve(new MessagingService(signerService)); -const mockPublish = jest.fn(); -jest.mock('../src/modules/messaging/messaging.service', () => { - return { - MessagingService: jest.fn().mockImplementation(() => { - return { publish: mockPublish }; - }), - }; -}); - -const mockGetRoleDefinition = jest.fn(); -const mockGetDidDocument = jest.fn().mockImplementation(({ did }: { did: string }) => { - return { publicKey: [{ id: `did:${Methods.Erc1056}:${Chain.VOLTA}:${did}-${KeyTags.OWNER}` }] }; -}); -const mockGetApplicationsByOrgNamespace = jest.fn(); -const mockRequestClaim = jest.fn(); - -jest.mock('../src/modules/cacheClient/cacheClient.service', () => { - return { - CacheClient: jest.fn().mockImplementation(() => { - return { - getRoleDefinition: mockGetRoleDefinition, - getDidDocument: mockGetDidDocument, - getApplicationsByOrganization: mockGetApplicationsByOrgNamespace, - init: jest.fn(), - login: jest.fn(), - requestClaim: mockRequestClaim, - issueClaim: jest.fn(), - }; - }), - }; -}); - -describe('Staking service tests', () => { - let signerService: SignerService; - let stakingService: StakingService; - let domainsService: DomainsService; - - beforeAll(async () => { - await replenish(orgOwner.address); - await replenish(patron.address); - await replenish(rootOwner.address); - await setupENS(rootOwner.address); - await setupStakingPoolFactory(); - let connectToCacheServer; - ({ connectToCacheServer, signerService } = await initWithPrivateKeySigner( - rootOwner.privateKey, - rpcUrl - )); - await signerService.publicKeyAndIdentityToken(); - let connectToDidRegistry; - ({ domainsService, stakingService, connectToDidRegistry } = await connectToCacheServer()); - const { claimsService } = await connectToDidRegistry(); - - const data: IRoleDefinition = { - fields: [], - issuerFields: [], - issuer: { - issuerType: 'DID', - did: [orgOwnerDid], - }, - metadata: [], - roleName: patronRole, - roleType: 'test', - version: 1, - enrolmentPreconditions: [], - }; - - await domainsService.createRole({ - roleName: patronRole, - namespace: root, - data, - }); - - await domainsService.createOrganization({ - orgName, - namespace: root, - data: { orgName }, - returnSteps: false, - }); - - mockGetApplicationsByOrgNamespace.mockReturnValueOnce([]); - await domainsService.changeOrgOwnership({ - namespace: `${orgName}.${root}`, - newOwner: orgOwner.address, - }); - - const registrationTypes = [RegistrationTypes.OnChain]; - await signerService.connect(patron, ProviderType.PrivateKey); - mockGetRoleDefinition.mockReturnValueOnce(data); - await claimsService.createClaimRequest({ - claim: { claimType: `${patronRole}.${root}`, claimTypeVersion: 1, requestorFields: [] }, - registrationTypes, - }); - - const [message] = mockRequestClaim.mock.calls.pop(); - const { id, subjectAgreement, token } = message; - - await signerService.connect(orgOwner, ProviderType.PrivateKey); - mockGetRoleDefinition.mockReturnValueOnce(data); - await claimsService.issueClaimRequest({ - id, - registrationTypes, - requester: patronDID, - subjectAgreement, - token, - }); - }); - - beforeEach(async () => { - jest.clearAllMocks(); - await setupStakingPoolFactory(); - await stakingService.init(); - }); - - test('organization owner should be able to launch pool', async () => { - await replenish(orgOwner.address, defaultPrincipalThreshold); - await signerService.connect(orgOwner, ProviderType.PrivateKey); - await stakingService.launchPool({ - org: domain, - minStakingPeriod: defaultMinStakingPeriod, - patronRewardPortion, - patronRoles: [`${patronRole}.${root}`], - principal: defaultPrincipalThreshold, - }); - const services = await stakingService.allServices(); - expect(services).toContainEqual(expect.objectContaining({ org: domain })); - }); - - it('non-owner of organization should not be able to launch pool', async () => { - const nonOwnerWallet = Wallet.createRandom(); - await replenish(nonOwnerWallet.address); - const nonOwner = Wallet.createRandom().connect(provider); - - await replenish(nonOwnerWallet.address, defaultPrincipalThreshold); - await signerService.connect(nonOwner, ProviderType.PrivateKey); - return expect( - stakingService.launchPool({ - org: domain, - minStakingPeriod: defaultMinStakingPeriod, - patronRewardPortion, - patronRoles: [`${patronRole}.${root}`], - principal: defaultPrincipalThreshold, - }) - ).rejects.toThrow('StakingPoolFactory: Not authorized to create pool for this organization'); - }); - - it('should be able to get all services', async () => { - const orgName2 = 'orgname2'; - await signerService.connect(rootOwner, ProviderType.PrivateKey); - await domainsService.createOrganization({ - orgName: orgName2, - namespace: root, - data: { orgName }, - returnSteps: false, - }); - await domainsService.changeOrgOwnership({ - namespace: `${orgName2}.${root}`, - newOwner: orgOwner.address, - }); - - await signerService.connect(orgOwner, ProviderType.PrivateKey); - await stakingService.launchPool({ - org: domain, - minStakingPeriod: defaultMinStakingPeriod, - patronRewardPortion, - patronRoles: [`${patronRole}.${root}`], - principal: defaultPrincipalThreshold, - }); - await stakingService.launchPool({ - org: `${orgName2}.${root}`, - minStakingPeriod: defaultMinStakingPeriod, - patronRewardPortion, - patronRoles: [`${patronRole}.${root}`], - principal: defaultPrincipalThreshold, - }); - - expect( - (await stakingService.allServices()).map((s) => ({ org: s.org, provider: s.provider })) - ).toStrictEqual([ - { org: `${orgName}.${root}`, provider: orgOwner.address }, - { org: `${orgName2}.${root}`, provider: orgOwner.address }, - ]); - }); - - it('pool should not be launched when principal less then threshold', async () => { - await replenish(orgOwner.address, defaultPrincipalThreshold); - - await signerService.connect(orgOwner, ProviderType.PrivateKey); - return expect( - stakingService.launchPool({ - org: domain, - minStakingPeriod: defaultMinStakingPeriod, - patronRewardPortion, - patronRoles: [`${patronRole}.${root}`], - principal: defaultPrincipalThreshold.div(2), - }) - ).rejects.toThrow('StakingPoolFactory: principal less than threshold'); - }); - - it('should not be possible to launch two pools for one organization', async () => { - await replenish(orgOwner.address, defaultPrincipalThreshold.mul(2)); - - await signerService.connect(orgOwner, ProviderType.PrivateKey); - await stakingService.launchPool({ - org: domain, - minStakingPeriod: defaultMinStakingPeriod, - patronRewardPortion, - patronRoles: [`${patronRole}.${root}`], - principal: defaultPrincipalThreshold, - }); - - return expect( - stakingService.launchPool({ - org: domain, - minStakingPeriod: defaultMinStakingPeriod, - patronRewardPortion, - patronRoles: [`${patronRole}.${root}`], - principal: defaultPrincipalThreshold, - }) - ).rejects.toThrow('StakingPoolFactory: pool for organization already launched'); - }); -}); diff --git a/src/init.ts b/src/init.ts index c9f1d2bd..10506cdf 100644 --- a/src/init.ts +++ b/src/init.ts @@ -9,7 +9,7 @@ import { ProviderType, SignerService, } from './modules/signer'; -import { StakingFactoryService, StakingService } from './modules/staking'; +import { StakingFactoryService } from './modules/staking'; import { DidRegistry } from './modules/didRegistry'; import { MessagingService } from './modules/messaging'; import { CacheClient } from './modules/cacheClient'; @@ -63,10 +63,6 @@ export async function init(signerService: SignerService) { const stakingAddressProvided = stakingPoolFactoryAddress && stakingPoolFactoryAddress.length; - const stakingService = stakingAddressProvided - ? await StakingService.create(signerService, domainsService) - : null; - const stakingPoolService = stakingAddressProvided ? await StakingFactoryService.create(signerService, domainsService) : null; @@ -93,7 +89,6 @@ export async function init(signerService: SignerService) { return { cacheClient, domainsService, - stakingService, assetsService, connectToDidRegistry, stakingPoolService, diff --git a/src/modules/staking/index.ts b/src/modules/staking/index.ts index 844f60fc..b194893c 100644 --- a/src/modules/staking/index.ts +++ b/src/modules/staking/index.ts @@ -1,2 +1,3 @@ -export * from './staking.service'; export * from './staking-pool.service'; +export * from './staking.types'; + diff --git a/src/modules/staking/staking-pool.service.ts b/src/modules/staking/staking-pool.service.ts index 6fa8a0db..a8730cf1 100644 --- a/src/modules/staking/staking-pool.service.ts +++ b/src/modules/staking/staking-pool.service.ts @@ -5,7 +5,7 @@ import { ERROR_MESSAGES } from '../../errors/ErrorMessages'; import { SignerService } from '../signer/signer.service'; import { chainConfigs } from '../../config/chain.config'; import { DomainsService } from '../domains/domains.service'; -import { Service, Stake, StakeStatus } from './staking.service'; +import { Service, Stake, StakeStatus } from './staking.types'; const { namehash, parseUnits } = utils; diff --git a/src/modules/staking/staking.service.ts b/src/modules/staking/staking.service.ts deleted file mode 100644 index 85bfd5f5..00000000 --- a/src/modules/staking/staking.service.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { BigNumber, providers, utils } from 'ethers'; -import { StakingPool as StakingPoolContract, StakingPoolFactory } from '../../../ethers'; -import { StakingPoolFactory__factory } from '../../../ethers/factories/StakingPoolFactory__factory'; -import { StakingPool__factory } from '../../../ethers/factories/StakingPool__factory'; -import { ERROR_MESSAGES } from '../../errors/ErrorMessages'; -import { emptyAddress } from '../../utils/constants'; -import { SignerService } from '../signer/signer.service'; -import { chainConfigs } from '../../config/chain.config'; -import { DomainsService } from '../domains/domains.service'; - -const { namehash, parseUnits } = utils; - -export enum StakeStatus { - NONSTAKING = 0, - STAKING = 1, - WITHDRAWING = 2, -} - -export type Service = { - /** organization ENS name */ - org: string; - /** pool address */ - pool: string; - /** provider address */ - provider: string; -}; - -export type Stake = { - amount: BigNumber; - depositStart: BigNumber; - depositEnd: BigNumber; - status: StakeStatus; -}; - -/** - * Inteneded for staking pools management - */ -export class StakingService { - private _stakingPoolFactoryAddress: string; - private _stakingPoolFactory: StakingPoolFactory; - - constructor(private _signerService: SignerService, private _domainsService: DomainsService) { - this._signerService.onInit(this.init.bind(this)); - } - - static async create(signerService: SignerService, domainsService: DomainsService) { - const service = new StakingService(signerService, domainsService); - await service.init(); - return service; - } - - /** - * @description Connects to the same chain as `signer`. The signer must be connected - * @param signer Signer with connected provider - */ - async init() { - const chainId = this._signerService.chainId; - this._stakingPoolFactoryAddress = chainConfigs()[chainId].stakingPoolFactoryAddress; - - this._stakingPoolFactory = new StakingPoolFactory__factory( - StakingPoolFactory__factory.createInterface(), - StakingPoolFactory__factory.bytecode - ) - .attach(this._stakingPoolFactoryAddress) - .connect(this._signerService.provider); - } - - /** - * @description Deployes organization staking pool - * @emits StakingPoolFactory.StakingPoolLaunched - */ - async launchPool({ - org, - minStakingPeriod, - patronRewardPortion, - patronRoles, - principal, - }: { - /** organization ENS name */ - org: string; - /** minimum staking period in seconds */ - minStakingPeriod: number | BigNumber; - /** patron's part of the reward in fractions of thousandth */ - patronRewardPortion: number; - /** roles required to stake */ - patronRoles: string[]; - /** stake put by service provider when pool is launched */ - principal: BigNumber; - }): Promise { - const tx: providers.TransactionRequest = { - to: this._stakingPoolFactoryAddress, - data: this._stakingPoolFactory.interface.encodeFunctionData('launchStakingPool', [ - namehash(org), - minStakingPeriod, - patronRewardPortion, - patronRoles.map((r) => namehash(r)), - ]), - value: principal, - }; - await this._signerService.send(tx); - } - - /** - * @description Returns all services for which pools are launched - */ - async allServices(): Promise { - const orgs = await this._stakingPoolFactory.orgsList(); - return Promise.all( - orgs.map((org) => - this._stakingPoolFactory - .services(org) - .then((service) => ({ ...service, org })) - .then((service) => - this._domainsService.readName(service.org).then((org) => ({ ...service, org })) - ) - ) - ); - } - - /** - * @description Returns pool launched for `org` if any - * @param org ENS name of organization - */ - async getPool(org: string): Promise { - const { pool } = await this._stakingPoolFactory - .connect(this._signerService.signer) - .services(namehash(org)); - if (pool === emptyAddress) { - return null; - } - return new StakingPool(this._signerService, pool); - } -} - -/** - * Abstraction over staking pool smart contract - */ -export class StakingPool { - private overrides = { - gasPrice: parseUnits('0.01', 'gwei'), - gasLimit: BigNumber.from(490000), - }; - private pool: StakingPoolContract; - - constructor(private signerService: SignerService, address: string) { - this.pool = new StakingPool__factory( - StakingPool__factory.createInterface(), - StakingPool__factory.bytecode - ).attach(address); - } - - /** - * @description Locks stake and starts accumulating reward - * @emits StakingPool.StakePut - */ - async putStake( - /** stake amount */ - stake: BigNumber | number - ): Promise { - stake = BigNumber.from(stake); - const tx: providers.TransactionRequest = { - to: this.pool.address, - from: this.signerService.address, - data: this.pool.interface.encodeFunctionData('putStake'), - value: stake, - }; - - const [gasPrice, gas, balance] = await Promise.all([ - this.signerService.signer.getGasPrice(), - this.signerService.provider.estimateGas(tx), - this.signerService.balance(), - ]); - - // multiplier 2 chosen arbitrarily because it is not known how reasonably to choose it - const fee = gasPrice.mul(gas).mul(2); - - const maxStake = balance.sub(fee); - - if (maxStake.lte(0)) { - throw new Error(ERROR_MESSAGES.INSUFFICIENT_BALANCE); - } - tx.value = stake.lt(maxStake) ? stake : maxStake; - await this.signerService.send(tx); - } - - /** - * @description Returns time left to enable request withdraw - */ - async requestWithdrawDelay(): Promise { - const { depositStart, status } = await this.getStake(); - if (status !== StakeStatus.STAKING) { - throw new Error(ERROR_MESSAGES.STAKE_WAS_NOT_PUT); - } - const requestAvailableFrom = depositStart.add( - await this.pool.connect(this.signerService.signer).minStakingPeriod() - ); - const now = await this.now(); - if (requestAvailableFrom.lte(now)) { - return 0; - } else { - return requestAvailableFrom.sub(now).toNumber(); - } - } - - /** - * Accumulated reward - */ - async checkReward(): Promise { - return this.pool.connect(this.signerService.signer).checkReward(); - } - - /** - * @param patron staker address - * @returns Stake - */ - async getStake(patron?: string): Promise { - if (!patron) { - patron = this.signerService.address; - } - return this.pool.connect(this.signerService.signer).stakes(patron); - } - - /** - * @description Stops accumulating of the reward and prepars stake to withdraw after withdraw delay. - * Withdraw request unavailable until minimum staking period ends - */ - async requestWithdraw(): Promise { - const tx: providers.TransactionRequest = { - to: this.pool.address, - data: this.pool.interface.encodeFunctionData('requestWithdraw'), - ...this.overrides, - }; - await this.signerService.send(tx); - } - - /** - * @description Returns time left to enable withdraw - */ - async withdrawalDelay(): Promise { - const { depositEnd, status } = await this.getStake(); - if (status !== StakeStatus.WITHDRAWING) { - throw new Error(ERROR_MESSAGES.WITHDRAWAL_WAS_NOT_REQUESTED); - } - const withdrawAvailableFrom = depositEnd.add( - await this.pool.connect(this.signerService.signer).withdrawDelay() - ); - const now = await this.now(); - if (withdrawAvailableFrom.lte(now)) { - return 0; - } else { - return withdrawAvailableFrom.sub(now).toNumber(); - } - } - - /** - * @description pays back stake with accumulated reward. Withdrawn unavailable until withdrawn delay ends - * @emits StakingPool.StakeWithdrawn - */ - async withdraw(): Promise { - const tx: providers.TransactionRequest = { - to: this.pool.address, - data: this.pool.interface.encodeFunctionData('withdraw'), - ...this.overrides, - }; - await this.signerService.send(tx); - } - - private async now(): Promise { - const lastBlock = await this.signerService.provider.getBlockNumber(); - return BigNumber.from((await this.signerService.provider.getBlock(lastBlock)).timestamp); - } -} diff --git a/src/modules/staking/staking.types.ts b/src/modules/staking/staking.types.ts new file mode 100644 index 00000000..f28ed932 --- /dev/null +++ b/src/modules/staking/staking.types.ts @@ -0,0 +1,23 @@ +import { BigNumber } from "ethers"; + +export enum StakeStatus { + NONSTAKING = 0, + STAKING = 1, + WITHDRAWING = 2, + } + + export type Service = { + /** organization ENS name */ + org: string; + /** pool address */ + pool: string; + /** provider address */ + provider: string; + }; + + export type Stake = { + amount: BigNumber; + depositStart: BigNumber; + depositEnd: BigNumber; + status: StakeStatus; + }; \ No newline at end of file