-
Notifications
You must be signed in to change notification settings - Fork 14
feat: validate space strategies #513
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 66 commits
fac6311
09cde68
611e081
4d8f1ad
ae2a23f
badd12a
fd8ab6f
f30be53
3104246
c8121e9
a1dee26
a21aaca
da6af19
ec30ecd
b6d764d
cf3719a
589cf8b
61578e2
dcde46a
5adf223
815b897
fa1b786
03b1ad7
35375df
1fc2775
94b079f
c81c533
155fe15
f4d464f
b2a3b0c
4321bc9
190484e
71535a7
b7ea17c
7b25718
2df80e6
181ca38
e8c87a2
4dbcfeb
1d1a24d
c36e9d8
271152c
89b2188
a45151a
5e73cc2
ca67b26
6195859
7eee9a6
ff9cae4
b17284f
ec91679
49162ef
18c9919
2eb90a5
e12e40c
a8b3668
c214894
6c57cdf
e7f0ecf
ea54370
e5e4e8c
6fc5edb
28e34f2
87184eb
3fe0ba1
87bb640
83e61c8
09ba5a1
cb2a274
01a27e4
35d40b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import snapshot from '@snapshot-labs/snapshot.js'; | ||
import log from './log'; | ||
import { strategies } from './strategies'; | ||
|
||
const DEFAULT_SNAPSHOT_ENV: string = 'testnet'; | ||
|
||
export async function validateSpaceSettings( | ||
originalSpace: any, | ||
snapshotEnv = DEFAULT_SNAPSHOT_ENV | ||
): Promise<void> { | ||
const spaceType = originalSpace.turbo ? 'turbo' : 'default'; | ||
const space = snapshot.utils.clone(originalSpace); | ||
|
||
if (space?.deleted) return Promise.reject('space deleted, contact admin'); | ||
|
||
delete space.deleted; | ||
delete space.flagged; | ||
delete space.verified; | ||
delete space.turbo; | ||
delete space.hibernated; | ||
delete space.id; | ||
|
||
if (space.parent && space.parent === originalSpace.id) { | ||
return Promise.reject('space cannot be its own parent'); | ||
} | ||
|
||
if ( | ||
space.children && | ||
Array.isArray(space.children) && | ||
space.children.includes(originalSpace.id) | ||
) { | ||
return Promise.reject('space cannot be its own child'); | ||
} | ||
|
||
const schemaIsValid: any = snapshot.utils.validateSchema(snapshot.schemas.space, space, { | ||
spaceType, | ||
snapshotEnv | ||
}); | ||
|
||
if (schemaIsValid !== true) { | ||
log.warn('[writer] Wrong space format', schemaIsValid); | ||
const firstErrorObject: any = Object.values(schemaIsValid)[0]; | ||
if (firstErrorObject.message === 'network not allowed') { | ||
return Promise.reject(firstErrorObject.message); | ||
} | ||
return Promise.reject('wrong space format'); | ||
} | ||
|
||
const strategiesIds: string[] = space.strategies.map((strategy: any) => strategy.name); | ||
if (snapshotEnv !== 'testnet') { | ||
const hasTicket = strategiesIds.includes('ticket'); | ||
const hasVotingValidation = | ||
space.voteValidation?.name && !['any'].includes(space.voteValidation.name); | ||
|
||
if (hasTicket && !hasVotingValidation) { | ||
return Promise.reject('space with ticket requires voting validation'); | ||
} | ||
|
||
const hasProposalValidation = | ||
(space.validation?.name && space.validation.name !== 'any') || | ||
space.filters?.minScore || | ||
space.filters?.onlyMembers; | ||
|
||
if (!hasProposalValidation) { | ||
return Promise.reject('space missing proposal validation'); | ||
} | ||
} | ||
|
||
for (const id of strategiesIds) { | ||
const strategy = strategies[id]; | ||
|
||
if (!strategy) { | ||
return Promise.reject(`strategy "${id}" is not a valid strategy`); | ||
} | ||
|
||
if (strategy.disabled) { | ||
return Promise.reject(`strategy "${id}" is not available anymore`); | ||
} | ||
|
||
if (strategy.override && spaceType !== 'turbo') { | ||
return Promise.reject(`strategy "${id}" is only available for pro spaces`); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { URL } from 'url'; | ||
import { capture } from '@snapshot-labs/snapshot-sentry'; | ||
import snapshot from '@snapshot-labs/snapshot.js'; | ||
import log from './log'; | ||
|
||
type Strategy = { | ||
id: string; | ||
override: boolean; | ||
disabled: boolean; | ||
}; | ||
|
||
const RUN_INTERVAL = 60e3 * 5; // 5 minutes | ||
const MAX_CONSECUTIVE_FAILS = 3; | ||
const URI = new URL( | ||
'/api/strategies', | ||
process.env.SCORE_API_URL ?? 'https://score.snapshot.org' | ||
).toString(); | ||
|
||
let consecutiveFailsCount = 0; | ||
let shouldStop = false; | ||
export let strategies: Record<Strategy['id'], Strategy> = {}; | ||
|
||
async function loadStrategies() { | ||
const res = await snapshot.utils.getJSON(URI); | ||
|
||
if ('error' in res) { | ||
capture(new Error('Failed to load strategies'), { | ||
contexts: { input: { uri: URI }, res } | ||
}); | ||
return true; | ||
} | ||
|
||
const strat = Object.values(res).map((strategy: any) => ({ | ||
id: strategy.key, | ||
override: strategy.dependOnOtherAddress || false, | ||
disabled: strategy.disabled || false | ||
})); | ||
|
||
strategies = Object.fromEntries(strat.map(strategy => [strategy.id, strategy])); | ||
} | ||
|
||
export async function initialize() { | ||
log.info('[strategies] Initial strategies load'); | ||
await loadStrategies(); | ||
log.info('[strategies] Initial strategies load complete'); | ||
} | ||
|
||
export async function run() { | ||
while (!shouldStop) { | ||
try { | ||
log.info('[strategies] Start strategies refresh'); | ||
await loadStrategies(); | ||
consecutiveFailsCount = 0; | ||
log.info('[strategies] End strategies refresh'); | ||
} catch (e: any) { | ||
consecutiveFailsCount++; | ||
|
||
if (consecutiveFailsCount >= MAX_CONSECUTIVE_FAILS) { | ||
capture(e); | ||
} | ||
log.error(`[strategies] failed to load ${JSON.stringify(e)}`); | ||
} | ||
|
||
// if stop() has been called after sleep started, | ||
// the loop will exit only after the sleep has completed | ||
await snapshot.utils.sleep(RUN_INTERVAL); | ||
} | ||
} | ||
|
||
export function stop() { | ||
log.info('[strategies] Stopping strategies refresh'); | ||
shouldStop = true; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,7 +58,7 @@ describe('POST /flag', () => { | |
settings: JSON.stringify(space.settings) | ||
})) | ||
.map(async space => { | ||
db.queryAsync('INSERT INTO snapshot_sequencer_test.spaces SET ?', space); | ||
return db.queryAsync('INSERT INTO snapshot_sequencer_test.spaces SET ?', space); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This return statement inside a map callback doesn't affect the outer function's return value. The map is creating an array of promises but they're not being awaited. Consider using Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||
}) | ||
); | ||
|
||
|
@@ -75,7 +75,7 @@ describe('POST /flag', () => { | |
vp_value_by_strategy: JSON.stringify(proposal.vp_value_by_strategy || []) | ||
})) | ||
.map(async proposal => { | ||
db.queryAsync('INSERT INTO snapshot_sequencer_test.proposals SET ?', proposal); | ||
return db.queryAsync('INSERT INTO snapshot_sequencer_test.proposals SET ?', proposal); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same issue as above - this return statement inside a map callback doesn't affect the outer function's return value. The map is creating an array of promises but they're not being awaited. Consider using Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||
}) | ||
); | ||
}); | ||
|
Uh oh!
There was an error while loading. Please reload this page.