Skip to content

Commit

Permalink
Updating democracy module for Substrate PR #5294. (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
jnaviask authored Apr 6, 2020
1 parent 0762684 commit f091add
Show file tree
Hide file tree
Showing 26 changed files with 578 additions and 427 deletions.
9 changes: 8 additions & 1 deletion client/scripts/controllers/app/web_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { web3Accounts, web3Enable, web3FromAddress, isWeb3Injected } from '@polk
import { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
import { Signer } from '@polkadot/api/types';

import AddressSwapper from 'views/components/addresses/address_swapper';

// TODO: make this a generic controller, and have the polkadot-js extension implementation inherit
class WebWalletController {
// GETTERS/SETTERS
Expand All @@ -24,7 +26,12 @@ class WebWalletController {

public async getSigner(who: string): Promise<Signer> {
// finds an injector for an address
const injector = await web3FromAddress(who);
// web wallet stores addresses in testnet format for now, so we have to re-encode
const reencodedAddress = AddressSwapper({
address: who,
currentPrefix: 42,
});
const injector = await web3FromAddress(reencodedAddress);
return injector.signer;
}

Expand Down
2 changes: 1 addition & 1 deletion client/scripts/controllers/chain/edgeware/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class Edgeware extends IChainAdapter<SubstrateCoin, SubstrateAccount> {
this.phragmenElections.init(this.chain, this.accounts, 'elections'),
this.council.init(this.chain, this.accounts),
this.democracyProposals.init(this.chain, this.accounts),
this.democracy.init(this.chain, this.accounts),
this.democracy.init(this.chain, this.accounts, false),
this.treasury.init(this.chain, this.accounts),
this.identities.init(this.chain, this.accounts),
this.signaling.init(this.chain, this.accounts),
Expand Down
172 changes: 50 additions & 122 deletions client/scripts/controllers/chain/substrate/account.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/* eslint-disable consistent-return */
import { Observable, combineLatest, of, Observer, empty } from 'rxjs';
import { Observable, combineLatest, of, Observer, empty, from } from 'rxjs';
import { map, flatMap, auditTime, switchMap, first } from 'rxjs/operators';

import { ApiRx } from '@polkadot/api';
import { DeriveStakingValidators } from '@polkadot/api-derive/types';
import Keyring, { decodeAddress } from '@polkadot/keyring';
import { KeyringPair, KeyringOptions } from '@polkadot/keyring/types';
import {
AccountData, BalanceOf, BalanceLock, BalanceLockTo212,
AccountData, Balance, BalanceLock, BalanceLockTo212,
AccountId, Exposure, Conviction, StakingLedger, Registration
} from '@polkadot/types/interfaces';
import { Vec } from '@polkadot/types';
Expand All @@ -17,7 +17,7 @@ import { stringToU8a, u8aToHex, hexToU8a } from '@polkadot/util';

import { IApp } from 'state';
import { formatCoin } from 'adapters/currency';
import { Account, IAccountsModule } from 'models/models';
import { Account, IAccountsModule, ChainClass } from 'models/models';
import { AccountsStore } from 'models/stores';
import { Codec } from '@polkadot/types/types';
import { SubstrateCoin } from 'adapters/chain/substrate/types';
Expand Down Expand Up @@ -186,15 +186,15 @@ export class SubstrateAccount extends Account<SubstrateCoin> {
// GETTERS AND SETTERS
// staking
public get stakedBalance(): Observable<SubstrateCoin> {
if(!this._Chain?.apiInitialized) return;
if (!this._Chain?.apiInitialized) return;
return this.stakingExposure.pipe(
map((exposure) => this._Chain.coins(exposure ? exposure.total : NaN))
);
}

// The total balance
public get balance(): Observable<SubstrateCoin> {
if(!this._Chain?.apiInitialized) return;
if (!this._Chain?.apiInitialized) return;
return this._Chain.query((api: ApiRx) => api.derive.balances.all(this.address))
.pipe(map(({
freeBalance,
Expand All @@ -203,62 +203,26 @@ export class SubstrateAccount extends Account<SubstrateCoin> {
}

// The quantity of unlocked balance
// TODO: note that this uses `availableBalance` and not `freeBalance` here -- this is because freeBalance
// only includes subtracted reserves, and not locks! And we want to see free for usage now.
public get freeBalance(): Observable<SubstrateCoin> {
if(!this._Chain?.apiInitialized) return;
return this._Chain.query((api: ApiRx) => api.derive.balances.all(this.address))
.pipe(map(({ freeBalance }) => this._Chain.coins(freeBalance)));
}

// The amount of balance locked up in reserve
public get reservedBalance(): Observable<SubstrateCoin> {
// TODO: should this return all coins if a votelock is on the account?
if(!this._Chain?.apiInitialized) return;
return this._Chain.query((api: ApiRx) => api.derive.balances.all(this.address))
.pipe(map(({ reservedBalance }) => this._Chain.coins(reservedBalance)));
}

public get miscFrozen(): Observable<SubstrateCoin> {
if(!this._Chain?.apiInitialized) return;
return this._Chain.query((api: ApiRx) => api.derive.balances.all(this.address))
.pipe(map(({ frozenMisc }) => this._Chain.coins(frozenMisc)));
}

public get feeFrozen(): Observable<SubstrateCoin> {
if(!this._Chain?.apiInitialized) return;
if (!this._Chain?.apiInitialized) return;
return this._Chain.query((api: ApiRx) => api.derive.balances.all(this.address))
.pipe(map(({ frozenFee }) => this._Chain.coins(frozenFee)));
.pipe(map(({ availableBalance }) => this._Chain.coins(availableBalance)));
}

public get lockedBalance(): Observable<SubstrateCoin> {
if(!this._Chain?.apiInitialized) return;
if (!this._Chain?.apiInitialized) return;
return this._Chain.query((api: ApiRx) => api.derive.balances.all(this.address))
.pipe(map(({ lockedBalance }) => this._Chain.coins(lockedBalance)));
}

// public get voteLock(): Observable<number | null> {
// if (!this._Chain?.apiInitialized) return; // TODO
// return this._Chain.query((api: ApiRx) => api.query.democracy.locks(this.address))
// .pipe(map((lockOpt) => lockOpt.isSome ? lockOpt.unwrap().toNumber() : null));
// }


// returns true if the funds can be withdrawn
public async canWithdraw(amount: BN, isTxFee = false): Promise<boolean> {
// const isLocked = await this.voteLock.pipe(first()).toPromise();
// if (isLocked > app.chain.block.height) {
// return false;
// }

const free = await this.freeBalance.pipe(first()).toPromise();
const minimum = await (isTxFee
? this.feeFrozen.pipe(first()).toPromise()
: this.miscFrozen.pipe(first()).toPromise());
return free.sub(minimum).gte(amount);
.pipe(map(({ availableBalance, votingBalance }) =>
// we compute illiquid balance by doing (total - available), because there's no query
// or parameter to fetch it
this._Chain.coins(votingBalance.sub(availableBalance))));
}

// The coin locks this account has on them
public get locks(): Observable<(BalanceLock | BalanceLockTo212)[]> {
if(!this._Chain?.apiInitialized) return;
if (!this._Chain?.apiInitialized) return;
return this._Chain.query((api: ApiRx) => api.derive.balances.all(this.address))
.pipe(map(({ lockedBreakdown }) => (lockedBreakdown.length > 0 ? lockedBreakdown : [])));
}
Expand All @@ -267,19 +231,19 @@ export class SubstrateAccount extends Account<SubstrateCoin> {
// TODO: this only checks the council collective, we may want to include a list of all
// collective memberships.
public get isCouncillor(): Observable<boolean> {
if(!this._Chain?.apiInitialized) return;
if (!this._Chain?.apiInitialized) return;
return this._Chain.query((api: ApiRx) => api.query.council.members())
.pipe(map((members: Vec<AccountId>) => undefined !== members.find((m) => m.toString() === this.address)));
}

// The amount staked by this account & accounts who have nominated it
public get stakingExposure(): Observable<Exposure> {
if(!this._Chain?.apiInitialized) return;
if (!this._Chain?.apiInitialized) return;
return this._Chain.query((api: ApiRx) => api.query.staking.stakers(this.address)) as Observable<Exposure>;
}

public get bonded(): Observable<SubstrateAccount> {
if(!this._Chain?.apiInitialized) return;
if( !this._Chain?.apiInitialized) return;
return this._Chain.query((api: ApiRx) => api.query.staking.bonded(this.address))
.pipe(map((accountId) => {
if (accountId && accountId.isSome) {
Expand All @@ -291,7 +255,7 @@ export class SubstrateAccount extends Account<SubstrateCoin> {
}

public get stakingLedger(): Observable<StakingLedger> {
if(!this._Chain?.apiInitialized) return;
if (!this._Chain?.apiInitialized) return;
return this._Chain.query((api: ApiRx) => api.query.staking.ledger(this.address))
.pipe(map((ledger) => {
if (ledger && ledger.isSome) {
Expand All @@ -304,7 +268,7 @@ export class SubstrateAccount extends Account<SubstrateCoin> {

// Accounts may set a proxy that can take council and democracy actions on behalf of their account
public get proxyFor(): Observable<SubstrateAccount> {
if(!this._Chain?.apiInitialized) return;
if (!this._Chain?.apiInitialized) return;
return this._Chain.query((api: ApiRx) => api.query.democracy.proxy(this.address))
.pipe(map((proxy) => {
if (proxy && proxy.isSome) {
Expand All @@ -317,7 +281,7 @@ export class SubstrateAccount extends Account<SubstrateCoin> {

// Accounts may delegate their voting power for democracy referenda. This always incurs the maximum locktime
public get delegation(): Observable<[ SubstrateAccount, number ]> {
if(!this._Chain?.apiInitialized) return;
if (!this._Chain?.apiInitialized) return;
// we have to hack around the type here because of the linked_map wrapper
return this._Chain.query((api: ApiRx) => api.query.democracy.delegations<Delegation[]>(this.address))
.pipe(map(([ delegation ]: [ Delegation ]) => {
Expand Down Expand Up @@ -422,77 +386,29 @@ export class SubstrateAccount extends Account<SubstrateCoin> {
}

// TRANSACTIONS
public async sendBalanceTx(recipient: SubstrateAccount, amount: SubstrateCoin) {
console.log('send balance tx');
const fundsAvailable = await this.canWithdraw(amount, false);
if (!fundsAvailable) {
throw new Error('not enough liquid funds');
}
return this._Chain.createTXModalData(
this,
(api: ApiRx) => api.tx.balances.transfer(recipient.address, amount),
'balanceTransfer',
`${formatCoin(amount)} to ${recipient.address}`
);
}

public async setProxyTx(proxy: SubstrateAccount) {
const proxyFor = await proxy.proxyFor.pipe(first()).toPromise();
if (proxyFor) {
throw new Error('already a proxy');
}
return this._Chain.createTXModalData(
this,
(api: ApiRx) => api.tx.democracy.setProxy(proxy.address),
'setProxy',
`${this.address} sets proxy to ${proxy.address}`
);
}

public async resignProxyTx() {
const proxyFor = await this.proxyFor.pipe(first()).toPromise();
if (proxyFor) {
throw new Error('not a proxy');
}
return this._Chain.createTXModalData(
this,
(api: ApiRx) => api.tx.democracy.resignProxy(),
'resignProxy',
`${this.address} resigns as proxy`
);
}

public async removeProxyTx(proxy: SubstrateAccount) {
const proxyFor = await proxy.proxyFor.pipe(first()).toPromise();
if (!proxyFor) {
throw new Error('not a proxy');
public get balanceTransferFee(): Observable<SubstrateCoin> {
if (this.chainClass === ChainClass.Edgeware) {
// grab const tx fee on edgeware
return this._Chain.api.pipe(
map((api: ApiRx) => this._Chain.coins(api.consts.balances.transferFee as Balance))
);
} else {
// compute fee on Kusama
const dummyTxFunc = (api: ApiRx) => api.tx.balances.transfer(this.address, '0');
return from(this._Chain.computeFees(this.address, dummyTxFunc));
}
return this._Chain.createTXModalData(
this,
(api: ApiRx) => api.tx.democracy.removeProxy(proxy.address),
'removeProxy',
`${this.address} removes proxy ${proxy.address}`
);
}

public delegateTx(toAccount: SubstrateAccount, conviction: Conviction) {
return this._Chain.createTXModalData(
this,
(api: ApiRx) => api.tx.democracy.delegate(toAccount.address, conviction),
'delegate',
`${this.address} delegates to ${toAccount.address}`
);
}

public undelegateTx() {
if (!this.delegation) {
throw new Error('Account not delegated');
public async sendBalanceTx(recipient: SubstrateAccount, amount: SubstrateCoin) {
const txFunc = (api: ApiRx) => api.tx.balances.transfer(recipient.address, amount);
if (!(await this._Chain.canPayFee(this, txFunc, amount))) {
throw new Error('insufficient funds');
}
return this._Chain.createTXModalData(
this,
(api: ApiRx) => api.tx.democracy.undelegate(),
'undelegate',
`undelegating ${this.address}`
txFunc,
'balanceTransfer',
`${formatCoin(amount)} to ${recipient.address}`
);
}

Expand Down Expand Up @@ -585,6 +501,18 @@ export class SubstrateAccount extends Account<SubstrateCoin> {
);
}

public unlockTx() {
if (this.chainClass !== ChainClass.Kusama) {
throw new Error('unlock only supported on Kusama');
}
return this._Chain.createTXModalData(
this,
(api: ApiRx) => api.tx.democracy.unlock(this.address),
'unlock',
`${this.address} attempts to unlock from democracy`,
);
}

// Nickname module is not used by chains we are supporting now
//
// public get nickname(): Observable<string> {
Expand Down
26 changes: 14 additions & 12 deletions client/scripts/controllers/chain/substrate/collective.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,6 @@ class SubstrateCollective extends ProposalModule<
const func = this._Chain.getTxMethod('treasury', 'rejectProposal');
return this.createTx(author, threshold, func(treasuryIdx).method);
}
public pushExternalProposal(
author: SubstrateAccount, threshold: number, hash: string, votingPeriod: number, delay: number
) {
const func = this._Chain.getTxMethod('democracy', 'externalPush');
return this.createTx(author, threshold, func(hash, votingPeriod, delay).method, true);
}
public createExternalProposal(author: SubstrateAccount, threshold: number, action: Call) {
const func = this._Chain.getTxMethod('democracy', 'externalPropose');
return this.createTx(author, threshold, func(action.hash).method);
Expand All @@ -95,13 +89,21 @@ class SubstrateCollective extends ProposalModule<
const func = this._Chain.getTxMethod('democracy', 'externalProposeMajority');
return this.createTx(author, threshold, func(action.hash).method);
}
public createExternalProposalDefault(author: SubstrateAccount, threshold: number, action: Call) {
// only on kusama
const func = this._Chain.getTxMethod('democracy', 'externalProposeDefault');
return this.createTx(author, threshold, func(action.hash).method);
}
public createFastTrack(
author: SubstrateAccount,
threshold: number,
hash: string,
votingPeriod: number,
delay: number
) {
// only on kusama
// TODO: we must check if Instant is allowed and if
// votingPeriod is valid wrt FastTrackVotingPeriod
const func = (this._Chain.getTxMethod('democracy', 'fastTrack'));
return this.createTx(
author,
Expand Down Expand Up @@ -260,18 +262,18 @@ extends Proposal<
label: 'Create majority-approval council proposal (2/3 councillors, majority public approval)',
description: 'Introduces a council proposal. Requires approval from 2/3 of councillors, after which ' +
'it turns into a 50% approval referendum.',
// pushExternalProposal and createFastTrack not supported on edgeware
// createExternalProposalDefault and createFastTrack not supported on edgeware
// XXX: support on Kusama
//}, {
// name: 'pushExternalProposal',
// label: 'Push majority-approval council proposal to voting immediately',
// description: 'Immediately begins voting on a majority-public-approval council proposal. ' +
// 'Requires approval from 2/3 of councillors.',
// name: 'createExternalProposalDefault',
// label: 'Create negative-turnout-bias council proposal (100% councillors, supermajority public rejection)',
// description: 'Introduces a council proposal. Requires approval from all councillors, after which ' +
// 'it turns into a supermajority rejection referendum (passes without supermajority voting "no").',
// }, {
// name: 'createFastTrack',
// label: 'Fast-track the current exteranlly-proposed majority-approval referendum',
// description: 'Schedules a current democracy proposal for immediate consideration (i.e. a vote). ' +
// 'If there is no externally-proposed referendum currently, or it is not majority-carried, it fails.'
// 'If there is no externally-proposed referendum currently, or it is not majority-carried, it fails.'
}, {
name: 'createEmergencyCancellation',
label: 'Emergency cancel referendum',
Expand Down
Loading

0 comments on commit f091add

Please sign in to comment.