Skip to content

Commit 3c5eb38

Browse files
authored
Merge pull request #41 from ethernautdao/feature/optigov
[Feature] Optigov plugin
2 parents fdd59c9 + 1e3b677 commit 3c5eb38

File tree

25 files changed

+2131
-47
lines changed

25 files changed

+2131
-47
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,27 @@ jobs:
7474
github-token: ${{ secrets.COVERALLS_NEW }}
7575
flag-name: ethernaut-oso
7676
parallel: true
77+
ethernaut-optigov:
78+
runs-on: ubuntu-latest
79+
strategy:
80+
matrix:
81+
node-version: ['20.12.0']
82+
steps:
83+
- uses: actions/checkout@v3
84+
- name: Use Node.js ${{ matrix.node-version }}
85+
uses: actions/setup-node@v3
86+
with:
87+
node-version: ${{ matrix.node-version }}
88+
- run: npm ci
89+
- run: npm run build --if-present
90+
- run: npm run compile --if-present
91+
- run: cd packages/ethernaut-optigov && npm t
92+
- name: Coveralls
93+
uses: coverallsapp/github-action@v2.3.6
94+
with:
95+
github-token: ${{ secrets.COVERALLS_NEW }}
96+
flag-name: ethernaut-optigov
97+
parallel: true
7798
ethernaut-util:
7899
runs-on: ubuntu-latest
79100
strategy:
@@ -386,6 +407,7 @@ jobs:
386407
[
387408
ethernaut-common,
388409
ethernaut-oso,
410+
ethernaut-optigov,
389411
ethernaut-util,
390412
ethernaut-util-ui,
391413
ethernaut-ui,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ethernaut-cli-monorepo",
3-
"version": "1.0.0",
3+
"version": "1.1.12",
44
"private": true,
55
"workspaces": [
66
"packages/*"

packages/ethernaut-cli/test/update.test.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,13 @@ describe('update', function () {
2525
before('cache', async function () {
2626
const config = storage.readConfig()
2727
cachedAutoUpdate = config.general?.autoUpdate
28-
2928
cachedPkg = fs.readFileSync('package.json', 'utf8')
3029
})
3130

3231
after('restore', async function () {
3332
const config = storage.readConfig()
3433
config.general.autoUpdate = cachedAutoUpdate
3534
storage.saveConfig(config)
36-
3735
fs.writeFileSync('package.json', cachedPkg, 'utf8')
3836
})
3937

@@ -69,6 +67,7 @@ describe('update', function () {
6967
describe('when auto update is the current version', function () {
7068
before('modify', async function () {
7169
const pkg = JSON.parse(cachedPkg)
70+
pkg.version = '0.0.0'
7271
const config = storage.readConfig()
7372
config.general.autoUpdate = pkg.version
7473
storage.saveConfig(config)
@@ -79,7 +78,7 @@ describe('update', function () {
7978
})
8079

8180
it('displays navigation', async function () {
82-
terminal.has('Pick a task or scope')
81+
terminal.has('A new version of the ethernaut-cli is available')
8382
})
8483
})
8584

packages/ethernaut-optigov/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ This plugin doesn't depend on any other plugins.
3333
This plugin adds the tasks listed below.
3434

3535
- login Logs in to the Agora RetroPGF API with SIWE (Sign in with Ethereum)
36+
- projects Prints a list of projects registered in RetroPGF, given specified filters
37+
- proposals Prints a list of proposals registered in RetroPGF, given specified filters
38+
- delegate Prints a list of delegates on Agora, or details a specific delegate, with optional related data (votes or delegators)
39+
3640

3741
## Environment extensions
3842

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const axios = require('axios')
2+
const EthernautCliError = require('ethernaut-common/src/error/error')
3+
const debug = require('ethernaut-common/src/ui/debug')
4+
5+
const API_BASE_URL = 'https://vote.optimism.io/api/v1'
6+
const AGORA_API_KEY = process.env.AGORA_API_KEY
7+
8+
class Agora {
9+
constructor() {
10+
this.apiKey = AGORA_API_KEY
11+
this.apiBaseUrl = API_BASE_URL
12+
this.bearerToken = null
13+
}
14+
15+
// Axios instance setup
16+
createAxiosInstance() {
17+
const headers = {
18+
Authorization: this.bearerToken
19+
? `Bearer ${this.bearerToken}`
20+
: `Bearer ${this.apiKey}`,
21+
}
22+
23+
return axios.create({
24+
baseURL: this.apiBaseUrl,
25+
headers,
26+
})
27+
}
28+
29+
// Handle common API errors
30+
handleError(error) {
31+
if (error.response) {
32+
throw new EthernautCliError(
33+
'ethernaut-optigov',
34+
`Http status error: ${error.response.data}`,
35+
)
36+
} else {
37+
throw new EthernautCliError(
38+
'ethernaut-optigov',
39+
`Http status error: ${error.message}`,
40+
)
41+
}
42+
}
43+
44+
// common method for getting API spec
45+
async getSpec() {
46+
try {
47+
const axiosInstance = this.createAxiosInstance()
48+
const response = await axiosInstance.get('/spec')
49+
debug.log(`Spec: ${response.data}`, 'ethernaut-optigov')
50+
return response.data
51+
} catch (error) {
52+
this.handleError(error)
53+
}
54+
}
55+
56+
// Set the bearer token after authentication
57+
setBearerToken(token) {
58+
this.bearerToken = token
59+
}
60+
}
61+
62+
module.exports = Agora

packages/ethernaut-optigov/src/internal/agora/Auth.js

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,38 @@
1-
const axios = require('axios')
21
const debug = require('ethernaut-common/src/ui/debug')
3-
const EthernautCliError = require('ethernaut-common/src/error/error')
4-
const API_BASE_URL = 'https://vote.optimism.io/api/v1'
52

63
class Auth {
7-
constructor() {}
4+
constructor(agora) {
5+
this.agora = agora
6+
}
87

9-
// Get a nonce from the Agora API
108
async getNonce() {
119
try {
12-
const response = await axios.get(`${API_BASE_URL}/auth/nonce`)
13-
10+
const axiosInstance = this.agora.createAxiosInstance()
11+
const response = await axiosInstance.get('/auth/nonce')
1412
debug.log(`Nonce: ${response.data.nonce}`, 'ethernaut-optigov')
15-
return response.data
13+
return response.data.nonce
1614
} catch (error) {
17-
throw new EthernautCliError(
18-
'ethernaut-optigov',
19-
`Http status error: ${error.message}`,
20-
)
15+
this.agora.handleError(error)
2116
}
2217
}
2318

24-
// Authenticate with the Agora API
2519
async authenticateWithAgora(message, signature, nonce) {
2620
try {
27-
const response = await axios.post(
28-
`${API_BASE_URL}/auth/verify`,
29-
{
30-
message,
31-
signature,
32-
nonce,
33-
},
34-
{
35-
headers: {
36-
'Content-Type': 'application/json',
37-
},
38-
},
39-
)
21+
const axiosInstance = this.agora.createAxiosInstance()
22+
const response = await axiosInstance.post('/auth/verify', {
23+
message,
24+
signature,
25+
nonce,
26+
})
4027

4128
debug.log('Auth Response Status:', response.status)
42-
// save the Bearer token
43-
this.bearerToken = response.data.access_token
29+
30+
// Set the Bearer token for future requests
31+
this.agora.setBearerToken(response.data.access_token)
32+
4433
return response.data.access_token
4534
} catch (error) {
46-
if (error.response) {
47-
// Server responded with a status other than 2xx
48-
throw new EthernautCliError(
49-
'ethernaut-optigov',
50-
`Http status error: ${error.response.data}`,
51-
)
52-
} else {
53-
// Other errors (e.g., network issue)
54-
throw new EthernautCliError(
55-
'ethernaut-optigov',
56-
`Http status error: ${error.message}`,
57-
)
58-
}
35+
this.agora.handleError(error)
5936
}
6037
}
6138
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
const debug = require('ethernaut-common/src/ui/debug')
2+
3+
class Delegates {
4+
constructor(agora) {
5+
this.agora = agora
6+
}
7+
8+
// Get a list of delegates with pagination
9+
async getDelegates({ limit = 10, offset = 0, sort } = {}) {
10+
try {
11+
const axiosInstance = this.agora.createAxiosInstance()
12+
const response = await axiosInstance.get('/delegates', {
13+
params: { limit, offset, sort },
14+
})
15+
16+
debug.log(`Delegates: ${response.data.data}`, 'ethernaut-optigov')
17+
return response.data.data.map((delegate) => ({
18+
address: delegate.address,
19+
votingPower: delegate.votingPower?.total,
20+
twitter: delegate.statement?.payload?.twitter,
21+
discord: delegate.statement?.payload?.discord,
22+
delegateStatement:
23+
delegate.statement?.payload?.delegateStatement?.substring(0, 100),
24+
}))
25+
} catch (error) {
26+
this.agora.handleError(error)
27+
}
28+
}
29+
30+
// Get a specific delegate by address or ENS name
31+
async getDelegateById(addressOrEnsName) {
32+
try {
33+
const axiosInstance = this.agora.createAxiosInstance()
34+
const response = await axiosInstance.get(`/delegates/${addressOrEnsName}`)
35+
36+
debug.log(`Delegate: ${response.data}`, 'ethernaut-optigov')
37+
const data = response.data
38+
return {
39+
address: data.address,
40+
votingPower: {
41+
advanced: data.votingPower.advanced,
42+
direct: data.votingPower.direct,
43+
total: data.votingPower.total,
44+
},
45+
votingPowerRelativeToVotableSupply:
46+
data.votingPowerRelativeToVotableSupply,
47+
votingPowerRelativeToQuorum: data.votingPowerRelativeToQuorum,
48+
proposalsCreated: data.proposalsCreated,
49+
proposalsVotedOn: data.proposalsVotedOn,
50+
votedFor: data.votedFor,
51+
votedAgainst: data.votedAgainst,
52+
votedAbstain: data.votedAbstain,
53+
votingParticipation: data.votingParticipation,
54+
lastTenProps: data.lastTenProps,
55+
numOfDelegators: data.numOfDelegators,
56+
}
57+
} catch (error) {
58+
this.agora.handleError(error)
59+
}
60+
}
61+
62+
// Get a paginated list of votes for a specific delegate
63+
async getDelegateVotes({
64+
addressOrEnsName,
65+
limit = 10,
66+
offset = 0,
67+
sort,
68+
} = {}) {
69+
try {
70+
const axiosInstance = this.agora.createAxiosInstance()
71+
const response = await axiosInstance.get(
72+
`/delegates/${addressOrEnsName}/votes`,
73+
{
74+
params: { limit, offset, sort },
75+
},
76+
)
77+
78+
debug.log(
79+
`Votes for Delegate ${addressOrEnsName}: ${response.data.data}`,
80+
'ethernaut-optigov',
81+
)
82+
return response.data.data.map((vote) => ({
83+
transactionHash: vote.transactionHash,
84+
proposalId: vote.proposalId,
85+
address: vote.address,
86+
support: vote.support,
87+
reason: vote.reason,
88+
weight: vote.weight,
89+
proposalValue: vote.proposalValue,
90+
proposalTitle: vote.proposalTitle,
91+
proposalType: vote.proposalType,
92+
timestamp: vote.timestamp,
93+
}))
94+
} catch (error) {
95+
this.agora.handleError(error)
96+
}
97+
}
98+
99+
// Get a paginated list of delegators for a specific delegate
100+
async getDelegateDelegators({
101+
addressOrEnsName,
102+
limit = 10,
103+
offset = 0,
104+
sort,
105+
} = {}) {
106+
try {
107+
const axiosInstance = this.agora.createAxiosInstance()
108+
const response = await axiosInstance.get(
109+
`/delegates/${addressOrEnsName}/delegators`,
110+
{
111+
params: { limit, offset, sort },
112+
},
113+
)
114+
115+
debug.log(
116+
`Delegators for Delegate ${addressOrEnsName}: ${response.data.data}`,
117+
'ethernaut-optigov',
118+
)
119+
return response.data.data.map((delegator) => ({
120+
from: delegator.from,
121+
allowance: delegator.allowance,
122+
timestamp: delegator.timestamp,
123+
type: delegator.type,
124+
amount: delegator.amount,
125+
}))
126+
} catch (error) {
127+
this.agora.handleError(error)
128+
}
129+
}
130+
}
131+
132+
module.exports = Delegates

0 commit comments

Comments
 (0)