generated from PolymeshAssociation/typescript-boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: 🎸 allow controller to nominate validators
add `account.staking.nominate` method to allow a controller to select validators
- Loading branch information
1 parent
8e6857a
commit b324475
Showing
6 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import { AccountId } from '@polkadot/types/interfaces'; | ||
import { Vec } from '@polkadot/types-codec'; | ||
import BigNumber from 'bignumber.js'; | ||
import { when } from 'jest-when'; | ||
|
||
import { | ||
getAuthorization, | ||
Params, | ||
prepareNominateValidators, | ||
prepareStorage, | ||
Storage, | ||
} from '~/api/procedures/nominateValidators'; | ||
import { Account, Context, PolymeshError } from '~/internal'; | ||
import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks'; | ||
import { getAccountInstance } from '~/testUtils/mocks/entities'; | ||
import { Mocked } from '~/testUtils/types'; | ||
import { ErrorCode } from '~/types'; | ||
import { PolymeshTx } from '~/types/internal'; | ||
import { DUMMY_ACCOUNT_ID } from '~/utils/constants'; | ||
import * as utilsConversionModule from '~/utils/conversion'; | ||
|
||
describe('nominateValidators procedure', () => { | ||
beforeAll(() => { | ||
entityMockUtils.initMocks(); | ||
dsMockUtils.initMocks(); | ||
procedureMockUtils.initMocks(); | ||
}); | ||
|
||
let mockContext: Mocked<Context>; | ||
let nominateTx: PolymeshTx<[Vec<AccountId>]>; | ||
let actingAccount: Account; | ||
let validator: Account; | ||
let rawAccountId: AccountId; | ||
|
||
let stringToAccountIdSpy: jest.SpyInstance; | ||
|
||
let storage: Storage; | ||
|
||
beforeEach(() => { | ||
nominateTx = dsMockUtils.createTxMock('staking', 'nominate'); | ||
mockContext = dsMockUtils.getContextInstance(); | ||
actingAccount = entityMockUtils.getAccountInstance({ address: DUMMY_ACCOUNT_ID }); | ||
validator = entityMockUtils.getAccountInstance({ | ||
address: '5FvreMigHtY1c6XTzDccjn8SVLiAeHz58z4MV4reJYyrdmj3', | ||
stakingGetCommission: { commission: new BigNumber(7), blocked: false }, | ||
}); | ||
rawAccountId = dsMockUtils.createMockAccountId(validator.address); | ||
|
||
stringToAccountIdSpy = jest.spyOn(utilsConversionModule, 'stringToAccountId'); | ||
|
||
when(stringToAccountIdSpy) | ||
.calledWith(validator.address, mockContext) | ||
.mockReturnValue(rawAccountId); | ||
|
||
storage = { | ||
actingAccount, | ||
ledger: { | ||
active: new BigNumber(10), | ||
stash: getAccountInstance(), | ||
unlocking: [], | ||
total: new BigNumber(10), | ||
claimedRewards: [], | ||
}, | ||
}; | ||
}); | ||
|
||
afterEach(() => { | ||
entityMockUtils.reset(); | ||
procedureMockUtils.reset(); | ||
dsMockUtils.reset(); | ||
}); | ||
|
||
afterAll(() => { | ||
procedureMockUtils.cleanup(); | ||
dsMockUtils.cleanup(); | ||
}); | ||
|
||
it('should throw an error if the target is not a controller', async () => { | ||
const proc = procedureMockUtils.getInstance<Params, void, Storage>(mockContext, { | ||
...storage, | ||
ledger: null, | ||
}); | ||
|
||
const expectedError = new PolymeshError({ | ||
code: ErrorCode.ValidationError, | ||
message: 'The acting account must be a controller', | ||
}); | ||
|
||
await expect( | ||
prepareNominateValidators.call(proc, { | ||
validators: [], | ||
}) | ||
).rejects.toThrow(expectedError); | ||
}); | ||
|
||
it('should throw an error if a validator is repeated', async () => { | ||
const proc = procedureMockUtils.getInstance<Params, void, Storage>(mockContext, { | ||
...storage, | ||
ledger: null, | ||
}); | ||
|
||
const expectedError = new PolymeshError({ | ||
code: ErrorCode.ValidationError, | ||
message: 'Validators cannot be repeated', | ||
}); | ||
|
||
await expect( | ||
prepareNominateValidators.call(proc, { | ||
validators: [validator, validator], | ||
}) | ||
).rejects.toThrow(expectedError); | ||
}); | ||
|
||
it('should throw an error if the target has not set commission', async () => { | ||
const proc = procedureMockUtils.getInstance<Params, void, Storage>(mockContext, storage); | ||
|
||
const expectedError = new PolymeshError({ | ||
code: ErrorCode.DataUnavailable, | ||
message: 'Commission not found for validator(s)', | ||
}); | ||
|
||
await expect( | ||
prepareNominateValidators.call(proc, { | ||
validators: [entityMockUtils.getAccountInstance({ stakingGetCommission: null })], | ||
}) | ||
).rejects.toThrow(expectedError); | ||
}); | ||
|
||
it('should return a nominate transaction spec', async () => { | ||
const proc = procedureMockUtils.getInstance<Params, void, Storage>(mockContext, storage); | ||
|
||
const args = { | ||
validators: [validator], | ||
}; | ||
|
||
const result = await prepareNominateValidators.call(proc, args); | ||
|
||
expect(result).toEqual({ | ||
transaction: nominateTx, | ||
args: [[rawAccountId]], | ||
resolver: undefined, | ||
}); | ||
}); | ||
|
||
describe('getAuthorization', () => { | ||
it('should return the appropriate roles and permissions', () => { | ||
const proc = procedureMockUtils.getInstance<Params, void, Storage>(mockContext, storage); | ||
const boundFunc = getAuthorization.bind(proc); | ||
|
||
expect(boundFunc()).toEqual({ | ||
permissions: { | ||
transactions: [], | ||
assets: [], | ||
portfolios: [], | ||
}, | ||
}); | ||
}); | ||
}); | ||
|
||
describe('prepareStorage', () => { | ||
it('should return the storage', () => { | ||
mockContext.getActingAccount.mockResolvedValue(actingAccount); | ||
|
||
const proc = procedureMockUtils.getInstance<Params, void, Storage>(mockContext); | ||
const boundFunc = prepareStorage.bind(proc); | ||
|
||
return expect(boundFunc()).resolves.toEqual( | ||
expect.objectContaining({ | ||
actingAccount: expect.objectContaining({ address: DUMMY_ACCOUNT_ID }), | ||
ledger: null, | ||
}) | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import { uniqBy } from 'lodash'; | ||
|
||
import { PolymeshError, Procedure } from '~/internal'; | ||
import { Account, ErrorCode, NominateValidatorsParams, StakingLedger } from '~/types'; | ||
import { ExtrinsicParams, ProcedureAuthorization, TransactionSpec } from '~/types/internal'; | ||
import { stringToAccountId } from '~/utils/conversion'; | ||
import { asAccount } from '~/utils/internal'; | ||
|
||
export interface Storage { | ||
actingAccount: Account; | ||
ledger: StakingLedger | null; | ||
} | ||
|
||
/** | ||
* @hidden | ||
*/ | ||
export type Params = NominateValidatorsParams; | ||
|
||
/** | ||
* @hidden | ||
*/ | ||
export async function prepareNominateValidators( | ||
this: Procedure<Params, void, Storage>, | ||
args: Params | ||
): Promise<TransactionSpec<void, ExtrinsicParams<'staking', 'nominate'>>> { | ||
const { | ||
context: { | ||
polymeshApi: { | ||
tx: { | ||
staking: { nominate }, | ||
}, | ||
}, | ||
}, | ||
context, | ||
storage: { actingAccount, ledger }, | ||
} = this; | ||
const { validators: validatorsInput } = args; | ||
|
||
const validators = validatorsInput.map(validator => asAccount(validator, context)); | ||
|
||
if (uniqBy(validators, 'address').length !== validators.length) { | ||
throw new PolymeshError({ | ||
code: ErrorCode.ValidationError, | ||
message: 'Validators cannot be repeated', | ||
}); | ||
} | ||
|
||
const commissions = await Promise.all( | ||
validators.map(validator => { | ||
return validator.staking.getCommission(); | ||
}) | ||
); | ||
|
||
const missingCommissions = commissions.reduce((missing, commission, index) => { | ||
if (!commission) { | ||
missing.push(index); | ||
} | ||
|
||
return missing; | ||
}, [] as number[]); | ||
|
||
if (missingCommissions.length) { | ||
throw new PolymeshError({ | ||
code: ErrorCode.DataUnavailable, | ||
message: 'Commission not found for validator(s)', | ||
data: { | ||
missingCommissions: missingCommissions.map( | ||
missingIndex => validators[missingIndex].address | ||
), | ||
}, | ||
}); | ||
} | ||
|
||
if (!ledger) { | ||
throw new PolymeshError({ | ||
code: ErrorCode.ValidationError, | ||
message: 'The acting account must be a controller', | ||
data: { actingAccount: actingAccount.address }, | ||
}); | ||
} | ||
|
||
const rawTargets = validators.map(validator => stringToAccountId(validator.address, context)); | ||
|
||
return { | ||
transaction: nominate, | ||
args: [rawTargets], | ||
resolver: undefined, | ||
}; | ||
} | ||
|
||
/** | ||
* @hidden | ||
* @note the staking module is exempt from permission checks | ||
*/ | ||
export function getAuthorization(this: Procedure<Params, void, Storage>): ProcedureAuthorization { | ||
return { | ||
permissions: { | ||
assets: [], | ||
transactions: [], | ||
portfolios: [], | ||
}, | ||
}; | ||
} | ||
|
||
/** | ||
* @hidden | ||
*/ | ||
export async function prepareStorage(this: Procedure<Params, void, Storage>): Promise<Storage> { | ||
const { context } = this; | ||
|
||
const actingAccount = await context.getActingAccount(); | ||
|
||
const ledger = await actingAccount.staking.getLedger(); | ||
|
||
return { | ||
actingAccount, | ||
ledger, | ||
}; | ||
} | ||
|
||
/** | ||
* @hidden | ||
*/ | ||
export const nominateValidators = (): Procedure<Params, void, Storage> => | ||
new Procedure(prepareNominateValidators, getAuthorization, prepareStorage); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters