Skip to content

Commit aaeb4ce

Browse files
committed
BEgin refactor of governance e2e tests
1 parent 70445f8 commit aaeb4ce

File tree

2 files changed

+301
-1
lines changed

2 files changed

+301
-1
lines changed

packages/polkadot/src/__snapshots__/polkadot.governance.e2e.test.ts.snap

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,3 +570,69 @@ exports[`Polkadot Governance > referendum lifecycle test 2 - submission, decisio
570570
},
571571
]
572572
`;
573+
574+
exports[`Polkadot Governance > referendum submission > referendum info after submission 1`] = `
575+
{
576+
"ongoing": {
577+
"deciding": null,
578+
"decisionDeposit": null,
579+
"enactment": {
580+
"after": 1,
581+
},
582+
"inQueue": false,
583+
"origin": {
584+
"origins": "SmallTipper",
585+
},
586+
"proposal": {
587+
"inline": "0x00001468656c6c6f",
588+
},
589+
"submissionDeposit": {
590+
"amount": 10000000000,
591+
"who": "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5",
592+
},
593+
"tally": {
594+
"ayes": 0,
595+
"nays": 0,
596+
"support": 0,
597+
},
598+
"track": 30,
599+
},
600+
}
601+
`;
602+
603+
exports[`Polkadot Governance > referendum submission > referendum submission events 1`] = `
604+
[
605+
{
606+
"data": {
607+
"proposal": {
608+
"Inline": "0x00001468656c6c6f",
609+
},
610+
"track": 30,
611+
},
612+
"method": "Submitted",
613+
"section": "referenda",
614+
},
615+
]
616+
`;
617+
618+
exports[`Polkadot Governance > referendum submission > refund submission deposit events 1`] = `[]`;
619+
620+
exports[`Polkadot Governance > referendum submission > timed-out referendum event 1`] = `
621+
{
622+
"event": {
623+
"data": [
624+
1584,
625+
{
626+
"ayes": 0,
627+
"nays": 0,
628+
"support": 0,
629+
},
630+
],
631+
"index": "0x150a",
632+
},
633+
"phase": {
634+
"initialization": null,
635+
},
636+
"topics": [],
637+
}
638+
`;

packages/shared/src/governance.ts

Lines changed: 235 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BN } from 'bn.js'
2-
import { assert, describe, test } from 'vitest'
2+
import { assert, describe, expect, test } from 'vitest'
33

44
import { type Chain, defaultAccountsSr25519 } from '@e2e-test/networks'
55
import { type Client, setupNetworks } from '@e2e-test/shared'
@@ -76,6 +76,236 @@ function referendumCmp(
7676
/// -------
7777
/// -------
7878

79+
/**
80+
* Test the process of submitting a referendum for a treasury spend
81+
*/
82+
export async function submitReferendumTest<
83+
TCustom extends Record<string, unknown> | undefined,
84+
TInitStorages extends Record<string, Record<string, any>> | undefined,
85+
>(chain: Chain<TCustom, TInitStorages>, addressEncoding: number) {
86+
const [client] = await setupNetworks(chain)
87+
88+
// Fund test accounts not already provisioned in the test chain spec.
89+
await client.dev.setStorage({
90+
System: {
91+
account: [
92+
[[devAccounts.alice.address], { providers: 1, data: { free: 10e10 } }],
93+
[[devAccounts.bob.address], { providers: 1, data: { free: 10e10 } }],
94+
],
95+
},
96+
})
97+
98+
// Get the referendum's intended track data
99+
100+
const referendaTracks = client.api.consts.referenda.tracks
101+
const smallTipper = referendaTracks.find((track) => track[1].name.eq('small_tipper'))!
102+
103+
/**
104+
* Get current referendum count i.e. the next referendum's index
105+
*/
106+
const referendumIndex = await client.api.query.referenda.referendumCount()
107+
108+
/**
109+
* Submit a new referendum
110+
*/
111+
const submissionTx = client.api.tx.referenda.submit(
112+
{
113+
Origins: 'SmallTipper',
114+
} as any,
115+
{
116+
Inline: client.api.tx.system.remark('hello').method.toHex(),
117+
},
118+
{
119+
After: 1,
120+
},
121+
)
122+
const submissionEvents = await sendTransaction(submissionTx.signAsync(devAccounts.alice))
123+
124+
await client.dev.newBlock()
125+
126+
// Fields to be removed, check comment below.
127+
const unwantedFields = /index/
128+
await checkEvents(submissionEvents, 'referenda')
129+
.redact({ removeKeys: unwantedFields })
130+
.toMatchSnapshot('referendum submission events')
131+
132+
/**
133+
* Check the created referendum's data
134+
*/
135+
let referendumDataOpt: Option<PalletReferendaReferendumInfoConvictionVotingTally> =
136+
await client.api.query.referenda.referendumInfoFor(referendumIndex)
137+
assert(referendumDataOpt.isSome, "submitted referendum's data cannot be `None`")
138+
let referendumData: PalletReferendaReferendumInfoConvictionVotingTally = referendumDataOpt.unwrap()
139+
140+
// These fields must be excised from the queried referendum data before being put in the test
141+
// snapshot.
142+
// These fields contain epoch-sensitive data, which will cause spurious test failures
143+
// periodically.
144+
const unwantedFields2 = /alarm|submitted/
145+
await check(referendumData)
146+
.redact({ removeKeys: unwantedFields2 })
147+
.toMatchSnapshot('referendum info after submission')
148+
149+
expect(referendumData.isOngoing).toBe(true)
150+
// Ongoing referendum data, prior to the decision deposit.
151+
const ongoingReferendum: PalletReferendaReferendumStatusConvictionVotingTally = referendumData.asOngoing
152+
153+
// Check the entirety of the stored referendum's data
154+
155+
expect(ongoingReferendum.track.toNumber()).toBe(smallTipper[0].toNumber())
156+
expect(ongoingReferendum.origin.toJSON()).toMatchObject({ origins: 'SmallTipper' })
157+
158+
expect(ongoingReferendum.proposal.asInline.toHex()).toBe(client.api.tx.system.remark('hello').method.toHex())
159+
160+
// The referendum was above set to be enacted 1 block after its passing.
161+
expect(ongoingReferendum.enactment.isAfter).toBe(true)
162+
expect(ongoingReferendum.enactment.asAfter.toNumber()).toBe(1)
163+
164+
// Check submission block
165+
const currentBlock = (await client.api.rpc.chain.getHeader()).number.toNumber()
166+
expect(ongoingReferendum.submitted.toNumber()).toBe(currentBlock)
167+
168+
expect(ongoingReferendum.submissionDeposit.who.toString()).toBe(
169+
encodeAddress(devAccounts.alice.address, addressEncoding),
170+
)
171+
expect(ongoingReferendum.submissionDeposit.amount.toNumber()).toBe(
172+
client.api.consts.referenda.submissionDeposit.toNumber(),
173+
)
174+
175+
// Immediately after a referendum's submission, it will not have a decision deposit,
176+
// which it will need to begin the decision period.
177+
expect(ongoingReferendum.decisionDeposit.isNone).toBe(true)
178+
expect(ongoingReferendum.deciding.isNone).toBe(true)
179+
180+
// Current voting state of the referendum.
181+
const votes = {
182+
ayes: 0,
183+
nays: 0,
184+
support: 0,
185+
}
186+
187+
// Check that voting data is empty
188+
await check(ongoingReferendum.tally).toMatchObject(votes)
189+
190+
// The referendum should not have been put in a queue - this test assumes there's room in the referendum's
191+
// track.
192+
expect(ongoingReferendum.inQueue.isFalse).toBe(true)
193+
194+
// Check the alarm
195+
expect(ongoingReferendum.alarm.isSome).toBe(true)
196+
const undecidingTimeoutAlarm = ongoingReferendum.alarm.unwrap()[0]
197+
const blocksUntilAlarm = undecidingTimeoutAlarm.sub(ongoingReferendum.submitted)
198+
// Check that the referendum's alarm is set to ring after the (globally predetermined) timeout
199+
// of 14 days, or 201600 blocks.
200+
expect(blocksUntilAlarm.toNumber()).toBe(client.api.consts.referenda.undecidingTimeout.toNumber())
201+
const alarm = [undecidingTimeoutAlarm, [undecidingTimeoutAlarm, 0]]
202+
expect(ongoingReferendum.alarm.unwrap().eq(alarm)).toBe(true)
203+
204+
// Modify the referendum to simulate a timeout caused by an unplaced decision deposit.
205+
206+
const undecidedTimeout = undecidingTimeoutAlarm.sub(ongoingReferendum.submitted)
207+
const newSubmitted = 1 + (currentBlock - undecidedTimeout.toNumber())
208+
209+
await client.dev.setStorage({
210+
Referenda: {
211+
ReferendumInfoFor: [
212+
[
213+
[referendumIndex],
214+
{
215+
Ongoing: {
216+
track: ongoingReferendum.track,
217+
origin: ongoingReferendum.origin,
218+
proposal: ongoingReferendum.proposal,
219+
enactment: ongoingReferendum.enactment,
220+
submitted: newSubmitted,
221+
submissionDeposit: ongoingReferendum.submissionDeposit,
222+
decisionDeposit: ongoingReferendum.decisionDeposit,
223+
deciding: ongoingReferendum.deciding,
224+
tally: ongoingReferendum.tally,
225+
inQueue: ongoingReferendum.inQueue,
226+
alarm: [newSubmitted + undecidedTimeout.toNumber(), [newSubmitted + undecidedTimeout.toNumber(), 0]],
227+
},
228+
},
229+
],
230+
],
231+
},
232+
// An accompanying nudge call must also be scheduled, otherwise the above referendum will not be serviced.
233+
Scheduler: {
234+
Agenda: [
235+
[
236+
[currentBlock + 1],
237+
[
238+
{
239+
call: { Inline: client.api.tx.referenda.nudgeReferendum(referendumIndex).method.toHex() },
240+
origin: { system: 'Root' },
241+
},
242+
],
243+
],
244+
],
245+
},
246+
})
247+
248+
await client.dev.newBlock()
249+
250+
// Check event for the timed-out referendum
251+
let events = await client.api.query.system.events()
252+
253+
const referendaEvents = events.filter((record) => {
254+
const { event } = record
255+
return event.section === 'referenda'
256+
})
257+
258+
expect(referendaEvents.length, 'cancelling a referendum should emit 1 event').toBe(1)
259+
260+
const timedOutEvent = referendaEvents[0]
261+
expect(client.api.events.referenda.TimedOut.is(timedOutEvent.event)).toBe(true)
262+
263+
await check(timedOutEvent).toMatchSnapshot('timed-out referendum event')
264+
265+
// Check the timed-out referendum's data
266+
267+
referendumDataOpt = await client.api.query.referenda.referendumInfoFor(referendumIndex)
268+
assert(referendumDataOpt.isSome, "submitted referendum's data cannot be `None`")
269+
referendumData = referendumDataOpt.unwrap()
270+
expect(referendumData.isTimedOut).toBe(true)
271+
272+
const timedOutRef: ITuple<[u32, Option<PalletReferendaDeposit>, Option<PalletReferendaDeposit>]> =
273+
referendumData.asTimedOut
274+
275+
expect(
276+
timedOutRef.eq([
277+
newSubmitted + undecidedTimeout.toNumber(),
278+
{
279+
who: encodeAddress(defaultAccountsSr25519.alice.address, addressEncoding),
280+
amount: client.api.consts.referenda.submissionDeposit,
281+
},
282+
null,
283+
]),
284+
).toBe(true)
285+
286+
// Attempt to refund the submission deposit
287+
288+
const refundTx = client.api.tx.referenda.refundSubmissionDeposit(referendumIndex)
289+
await sendTransaction(refundTx.signAsync(devAccounts.alice))
290+
291+
await client.dev.newBlock()
292+
293+
events = await client.api.query.system.events()
294+
295+
const refundEvents = events.filter((record) => {
296+
const { event } = record
297+
return event.section === 'system' && event.method === 'ExtrinsicFailed'
298+
})
299+
300+
// Timed out referenda cannot have their submission deposit refunded.
301+
const refundEvent = refundEvents[0]
302+
assert(client.api.events.system.ExtrinsicFailed.is(refundEvent.event))
303+
const dispatchError = refundEvent.event.data.dispatchError
304+
assert(dispatchError.isModule)
305+
306+
expect(client.api.errors.referenda.BadStatus.is(dispatchError.asModule)).toBe(true)
307+
}
308+
79309
/**
80310
* Test the process of
81311
* 1. submitting a referendum for a treasury spend
@@ -880,6 +1110,10 @@ export function governanceE2ETests<
8801110
describe(testConfig.testSuiteName, async () => {
8811111
const [client] = await setupNetworks(chain)
8821112

1113+
test('referendum submission', async () => {
1114+
await submitReferendumTest(chain, testConfig.addressEncoding)
1115+
})
1116+
8831117
test('referendum lifecycle test - submission, decision deposit, various voting should all work', async () => {
8841118
await referendumLifecycleTest(client, testConfig.addressEncoding)
8851119
})

0 commit comments

Comments
 (0)