-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add testing to governance package, start with interactive builder uni…
…t tests
- Loading branch information
Showing
10 changed files
with
253 additions
and
122 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
[{"contract":"Governance","function":"setConstitution","args":["0x19F78d207493Bf6f7E8D54900d01bb387F211b28","0xa91ee0dc","900000000000000000000000"],"value":"0"}] |
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 |
---|---|---|
@@ -1,5 +1,5 @@ | ||
module.exports = { | ||
preset: 'ts-jest', | ||
testMatch: ['<rootDir>/src/**/?(*.)+(spec|test).ts?(x)'], | ||
testMatch: ['<rootDir>/src/**/?(*.)+(spec|test).ts'], | ||
verbose: true, | ||
} |
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,7 @@ | ||
module.exports = { | ||
preset: 'ts-jest', | ||
testMatch: ['<rootDir>/src/**/?(*.)+(test).ts'], | ||
setupFilesAfterEnv: ['@celo/dev-utils/lib/matchers', '<rootDir>/jest_setup.ts'], | ||
globalSetup: '<rootDir>/src/test-utils/setup.global.ts', | ||
verbose: true, | ||
} |
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,12 @@ | ||
import { URL } from 'node:url' | ||
// @ts-ignore | ||
global.URL = URL | ||
|
||
// @ts-ignore | ||
const fetchMock = require('fetch-mock') | ||
|
||
const fetchMockSandbox = fetchMock.sandbox() | ||
jest.mock('cross-fetch', () => fetchMockSandbox) | ||
|
||
// @ts-ignore | ||
global.fetchMock = fetchMockSandbox |
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
99 changes: 99 additions & 0 deletions
99
packages/sdk/governance/src/interactive-proposal-builder.test.ts
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,99 @@ | ||
import { newKitFromWeb3 } from '@celo/contractkit' | ||
import inquirer from 'inquirer' | ||
import { InteractiveProposalBuilder } from './interactive-proposal-builder' | ||
import { ProposalBuilder } from './proposals' | ||
jest.mock('inquirer') | ||
|
||
import { testWithAnvilL2 } from '@celo/dev-utils/lib/anvil-test' | ||
|
||
testWithAnvilL2('InteractiveProposalBuilder', (web3) => { | ||
let builder: ProposalBuilder | ||
let interactiveBuilder: InteractiveProposalBuilder | ||
let fromJsonTxSpy: jest.SpyInstance | ||
|
||
beforeEach(() => { | ||
const kit = newKitFromWeb3(web3) | ||
builder = new ProposalBuilder(kit) | ||
fromJsonTxSpy = jest.spyOn(builder, 'fromJsonTx') | ||
interactiveBuilder = new InteractiveProposalBuilder(builder) | ||
}) | ||
|
||
describe('promptTransactions', () => { | ||
it('should prompt for transactions and return them', async () => { | ||
const mockPrompt = jest.spyOn(inquirer, 'prompt') | ||
// TODO the mock order is not right for the inputs | ||
mockPrompt | ||
.mockResolvedValueOnce({ 'Celo Contract': 'Governance' }) | ||
.mockResolvedValueOnce({ 'Governance Function': 'setConstitution' }) | ||
.mockResolvedValueOnce({ destination: '0x19F78d207493Bf6f7E8D54900d01bb387F211b28' }) | ||
.mockResolvedValueOnce({ functionId: '0xa91ee0dc' }) | ||
.mockResolvedValueOnce({ threshold: '900000000000000000000000' }) | ||
.mockResolvedValueOnce({ 'Celo Contract': '✔ done' }) | ||
|
||
const transactions = await interactiveBuilder.promptTransactions() | ||
|
||
expect(transactions).toEqual([ | ||
{ | ||
contract: 'Governance', | ||
function: 'setConstitution', | ||
args: [ | ||
'0x19F78d207493Bf6f7E8D54900d01bb387F211b28', | ||
'0xa91ee0dc', | ||
'900000000000000000000000', | ||
], | ||
value: '0', | ||
}, | ||
]) | ||
expect(fromJsonTxSpy.mock.calls).toMatchInlineSnapshot(` | ||
[ | ||
[ | ||
{ | ||
"args": [ | ||
"0x19F78d207493Bf6f7E8D54900d01bb387F211b28", | ||
"0xa91ee0dc", | ||
"900000000000000000000000", | ||
], | ||
"contract": "Governance", | ||
"function": "setConstitution", | ||
"value": "0", | ||
}, | ||
], | ||
] | ||
`) | ||
}) | ||
|
||
it('should handle invalid transactions and retry', async () => { | ||
const mockPrompt = jest.spyOn(inquirer, 'prompt') | ||
mockPrompt | ||
.mockResolvedValueOnce({ 'Celo Contract': 'Governance' }) | ||
.mockResolvedValueOnce({ 'Governance Function': 'setConstitution' }) | ||
.mockResolvedValueOnce({ destination: 'invalid' }) | ||
.mockResolvedValueOnce({ functionId: 'invalid' }) | ||
.mockResolvedValueOnce({ threshold: '900000000000000000000000' }) | ||
.mockResolvedValueOnce({ 'Celo Contract': '✔ done' }) | ||
|
||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation() | ||
|
||
const transactions = await interactiveBuilder.promptTransactions() | ||
|
||
expect(transactions).toEqual([]) // No valid transactions added | ||
expect(fromJsonTxSpy.mock.calls).toMatchInlineSnapshot(` | ||
[ | ||
[ | ||
{ | ||
"args": [ | ||
"invalid", | ||
"invalid", | ||
"900000000000000000000000", | ||
], | ||
"contract": "Governance", | ||
"function": "setConstitution", | ||
"value": "0", | ||
}, | ||
], | ||
] | ||
`) | ||
consoleErrorSpy.mockRestore() | ||
}) | ||
}) | ||
}) |
121 changes: 121 additions & 0 deletions
121
packages/sdk/governance/src/interactive-proposal-builder.ts
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,121 @@ | ||
import { isHexString } from '@celo/base/lib/address' | ||
import { ABIDefinition } from '@celo/connect' | ||
import { CeloContract, RegisteredContracts } from '@celo/contractkit' | ||
import { isValidAddress } from '@celo/utils/lib/address' | ||
import BigNumber from 'bignumber.js' | ||
import inquirer from 'inquirer' | ||
import type { ProposalTransactionJSON } from './proposals' | ||
import { ProposalBuilder } from './proposals' | ||
|
||
const DONE_CHOICE = '✔ done' | ||
|
||
export class InteractiveProposalBuilder { | ||
constructor(private readonly builder: ProposalBuilder) {} | ||
|
||
async outputTransactions() { | ||
const transactionList = this.builder.build() | ||
console.log(JSON.stringify(transactionList, null, 2)) | ||
} | ||
|
||
async promptTransactions() { | ||
const transactions: ProposalTransactionJSON[] = [] | ||
// eslint-disable-next-line no-constant-condition | ||
while (true) { | ||
console.log(`Transaction #${transactions.length + 1}:`) | ||
|
||
// prompt for contract | ||
const contractPromptName = 'Celo Contract' | ||
const contractAnswer = await inquirer.prompt({ | ||
name: contractPromptName, | ||
type: 'list', | ||
choices: [DONE_CHOICE, ...RegisteredContracts], | ||
}) | ||
const choice = contractAnswer[contractPromptName] | ||
if (choice === DONE_CHOICE) { | ||
break | ||
} | ||
|
||
const contractName = choice as CeloContract | ||
console.warn('DEBUG', contractPromptName, contractName, contractAnswer) | ||
|
||
const contractABI = require('@celo/abis/web3/' + contractName).ABI as ABIDefinition[] | ||
|
||
const txMethods = contractABI.filter( | ||
(def) => def.type === 'function' && def.stateMutability !== 'view' | ||
) | ||
const txMethodNames = txMethods.map((def) => def.name!) | ||
// prompt for function | ||
const functionPromptName = contractName + ' Function' | ||
const functionAnswer = await inquirer.prompt<Record<string, string>>({ | ||
name: functionPromptName, | ||
type: 'list', | ||
choices: txMethodNames, | ||
}) | ||
const functionName = functionAnswer[functionPromptName] | ||
const idx = txMethodNames.findIndex((m) => m === functionName) | ||
const txDefinition = txMethods[idx] | ||
// prompt individually for each argument | ||
const args = [] | ||
for (const functionInput of txDefinition.inputs!) { | ||
const inputAnswer: Record<string, string> = await inquirer.prompt({ | ||
name: functionInput.name, | ||
type: 'input', | ||
validate: async (input: string) => { | ||
switch (functionInput.type) { | ||
case 'uint256': | ||
try { | ||
new BigNumber(input) | ||
return true | ||
} catch (e) { | ||
return false | ||
} | ||
case 'boolean': | ||
return input === 'true' || input === 'false' | ||
case 'address': | ||
return isValidAddress(input) | ||
case 'bytes': | ||
return isHexString(input) | ||
default: | ||
return true | ||
} | ||
}, | ||
}) | ||
|
||
const answer: string = inputAnswer[functionInput.name] | ||
// transformedValue may not be in scientific notation | ||
const transformedValue = | ||
functionInput.type === 'uint256' ? new BigNumber(answer).toString(10) : answer | ||
args.push(transformedValue) | ||
} | ||
// prompt for value only when tx is payable | ||
let value: string | ||
if (txDefinition.payable) { | ||
const valuePromptName = 'Value' | ||
const valueAnswer = await inquirer.prompt({ | ||
name: valuePromptName, | ||
type: 'input', | ||
}) | ||
value = valueAnswer[valuePromptName] | ||
} else { | ||
value = '0' | ||
} | ||
const tx: ProposalTransactionJSON = { | ||
contract: contractName, | ||
function: functionName, | ||
args, | ||
value, | ||
} | ||
|
||
try { | ||
// use fromJsonTx as well-formed tx validation | ||
await this.builder.fromJsonTx(tx) | ||
transactions.push(tx) | ||
} catch (error) { | ||
console.error(error) | ||
console.error('Please retry forming this transaction') | ||
} | ||
} | ||
|
||
return transactions | ||
} | ||
} |
Oops, something went wrong.