Skip to content

Commit 28e146b

Browse files
wa0x6eChaituVRdependabot[bot]
authored
feat: get turbo limits from database (#494)
* feat: get turbo limits from database * refactor: fetch options from DB, without cache * fix: fix missing await * fix: fix wrong options name * feat: migrate turbo limits checks from SDK (#495) * feat: validate number of strategies depending on space type * feat: check proposal limits * refactor: avoid duplicate method calls * chore(deps): bump @snapshot-labs/snapshot.js from 0.12.45 to 0.12.46 (#499) Bumps [@snapshot-labs/snapshot.js](https://github.com/snapshot-labs/snapshot.js) from 0.12.45 to 0.12.46. - [Release notes](https://github.com/snapshot-labs/snapshot.js/releases) - [Commits](snapshot-labs/snapshot.js@v0.12.45...v0.12.46) --- updated-dependencies: - dependency-name: "@snapshot-labs/snapshot.js" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Chaitanya <[email protected]> * fix tests * Update src/writer/proposal.ts * fix lint and remove extra brace * Update src/writer/proposal.ts * perf: get limits with a single sql query * move getSpaceType after validateSpace --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: Chaitanya <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 7fd2aee commit 28e146b

File tree

11 files changed

+284
-105
lines changed

11 files changed

+284
-105
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"@snapshot-labs/pineapple": "^1.1.0",
3333
"@snapshot-labs/snapshot-metrics": "^1.4.1",
3434
"@snapshot-labs/snapshot-sentry": "^1.5.5",
35-
"@snapshot-labs/snapshot.js": "^0.12.45",
35+
"@snapshot-labs/snapshot.js": "^0.12.46",
3636
"bluebird": "^3.7.2",
3737
"connection-string": "^1.0.1",
3838
"cors": "^2.8.5",

src/helpers/limits.ts

Lines changed: 0 additions & 54 deletions
This file was deleted.

src/helpers/options.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import db from './mysql';
2+
3+
async function getOptions<T>(
4+
keys: string[],
5+
defaultValue: T,
6+
formattingFn: (val: string) => T
7+
): Promise<Record<string, T>> {
8+
const results = keys.reduce((acc, key) => {
9+
acc[key] = defaultValue;
10+
return acc;
11+
}, {});
12+
13+
const options = await db.queryAsync('select name, value from options where name in (?)', [keys]);
14+
15+
options.forEach(result => {
16+
results[result.name] = formattingFn(result.value);
17+
});
18+
19+
return results;
20+
}
21+
22+
export async function getLimit(key: string): Promise<number> {
23+
return (await getLimits([key]))[key];
24+
}
25+
26+
export async function getList(key: string): Promise<string[]> {
27+
return (await getLists([key]))[key];
28+
}
29+
30+
export async function getLimits(keys: string[]): Promise<Record<string, number>> {
31+
return await getOptions<number>(keys, 0, val => Number(val));
32+
}
33+
34+
export async function getLists(keys: string[]): Promise<Record<string, string[]>> {
35+
return await getOptions<string[]>(keys, [], val => val.split(','));
36+
}
37+
38+
export async function getSpaceType(
39+
space: {
40+
verified: boolean;
41+
turbo: boolean;
42+
flagged: boolean;
43+
id: string;
44+
},
45+
withEcosystem = false
46+
) {
47+
let type = 'default';
48+
49+
if (withEcosystem && (await getList('space.ecosystem.list')).includes(space.id)) {
50+
type = 'ecosystem';
51+
}
52+
if (space.flagged) type = 'flagged';
53+
if (space.verified) type = 'verified';
54+
if (space.turbo) type = 'turbo';
55+
56+
return type;
57+
}

src/writer/follow.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { FOLLOWS_LIMIT_PER_USER } from '../helpers/limits';
21
import db from '../helpers/mysql';
2+
import { getLimit } from '../helpers/options';
33
import { DEFAULT_NETWORK_ID, NETWORK_IDS } from '../helpers/utils';
44

55
export const getFollowsCount = async (follower: string): Promise<number> => {
@@ -22,9 +22,10 @@ export async function verify(message): Promise<any> {
2222
if (follows.length !== 0) return Promise.reject('you are already following this space');
2323

2424
const count = await getFollowsCount(message.from);
25+
const limit = await getLimit('user.default.follow_limit');
2526

26-
if (count >= FOLLOWS_LIMIT_PER_USER) {
27-
return Promise.reject(`you can join max ${FOLLOWS_LIMIT_PER_USER} spaces`);
27+
if (count >= limit) {
28+
return Promise.reject(`you can join max ${limit} spaces`);
2829
}
2930

3031
if (message.network && !NETWORK_IDS.includes(message.network)) {

src/writer/proposal.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import snapshot from '@snapshot-labs/snapshot.js';
33
import networks from '@snapshot-labs/snapshot.js/src/networks.json';
44
import { validateSpaceSettings } from './settings';
55
import { getSpace } from '../helpers/actions';
6-
import { ACTIVE_PROPOSAL_BY_AUTHOR_LIMIT, getSpaceLimits } from '../helpers/limits';
76
import log from '../helpers/log';
87
import { containsFlaggedLinks, flaggedAddresses } from '../helpers/moderation';
98
import { isMalicious } from '../helpers/monitoring';
109
import db from '../helpers/mysql';
10+
import { getLimits, getSpaceType } from '../helpers/options';
1111
import { captureError, getQuorum, jsonParse, validateChoices } from '../helpers/utils';
1212

1313
const scoreAPIUrl = process.env.SCORE_API_URL || 'https://score.snapshot.org';
@@ -57,7 +57,6 @@ export async function verify(body): Promise<any> {
5757
const created = parseInt(msg.timestamp);
5858
const addressLC = body.address.toLowerCase();
5959
const space = await getSpace(msg.space);
60-
6160
try {
6261
await validateSpace(space);
6362
} catch (e) {
@@ -66,6 +65,17 @@ export async function verify(body): Promise<any> {
6665

6766
space.id = msg.space;
6867

68+
const spaceType = await getSpaceType(space);
69+
const spaceTypeWithEcosystem = await getSpaceType(space, true);
70+
71+
const limits = await getLimits([
72+
`space.${spaceType}.body_limit`,
73+
`space.${spaceType}.choices_limit`,
74+
'space.active_proposal_limit_per_author',
75+
`space.${spaceTypeWithEcosystem}.proposal_limit_per_day`,
76+
`space.${spaceTypeWithEcosystem}.proposal_limit_per_month`
77+
]);
78+
6979
const schemaIsValid = snapshot.utils.validateSchema(snapshot.schemas.proposal, msg.payload, {
7080
spaceType: space.turbo ? 'turbo' : 'default'
7181
});
@@ -186,16 +196,29 @@ export async function verify(body): Promise<any> {
186196
space.id,
187197
body.address
188198
);
189-
const [dayLimit, monthLimit] = getSpaceLimits(space);
199+
200+
const dayLimit = limits[`space.${spaceTypeWithEcosystem}.proposal_limit_per_day`];
201+
const monthLimit = limits[`space.${spaceTypeWithEcosystem}.proposal_limit_per_month`];
190202

191203
if (dayCount >= dayLimit || monthCount >= monthLimit)
192204
return Promise.reject('proposal limit reached');
193-
if (!isAuthorized && activeProposalsByAuthor >= ACTIVE_PROPOSAL_BY_AUTHOR_LIMIT)
205+
const activeProposalLimitPerAuthor = limits['space.active_proposal_limit_per_author'];
206+
if (!isAuthorized && activeProposalsByAuthor >= activeProposalLimitPerAuthor)
194207
return Promise.reject('active proposal limit reached for author');
195208
} catch (e) {
196209
capture(e);
197210
return Promise.reject('failed to check proposals limit');
198211
}
212+
213+
const bodyLengthLimit = limits[`space.${spaceType}.body_limit`];
214+
if (msg.payload.body.length > bodyLengthLimit) {
215+
return Promise.reject(`proposal body length can not exceed ${bodyLengthLimit} characters`);
216+
}
217+
218+
const choicesLimit = limits[`space.${spaceType}.choices_limit`];
219+
if (msg.payload.choices.length > choicesLimit) {
220+
return Promise.reject(`number of choices can not exceed ${choicesLimit}`);
221+
}
199222
}
200223

201224
export async function action(body, ipfs, receipt, id): Promise<void> {

src/writer/settings.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import isEqual from 'lodash/isEqual';
44
import { addOrUpdateSpace, getSpace } from '../helpers/actions';
55
import log from '../helpers/log';
66
import db from '../helpers/mysql';
7+
import { getLimit, getSpaceType } from '../helpers/options';
78
import { clearStampCache, DEFAULT_NETWORK, jsonParse } from '../helpers/utils';
89

910
const SNAPSHOT_ENV = process.env.NETWORK || 'testnet';
@@ -73,6 +74,12 @@ export async function verify(body): Promise<any> {
7374
return Promise.reject(e);
7475
}
7576

77+
const strategiesLimit = await getLimit(`space.${await getSpaceType(space)}.strategies_limit`);
78+
79+
if (msg.payload.strategies.length > strategiesLimit) {
80+
return Promise.reject(`max number of strategies is ${strategiesLimit}`);
81+
}
82+
7683
const controller = await snapshot.utils.getSpaceController(msg.space, DEFAULT_NETWORK, {
7784
broviderUrl
7885
});

test/integration/ingestor.test.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,62 @@
1-
import ingestor from '../../src/ingestor';
2-
import proposalInput from '../fixtures/ingestor-payload/proposal.json';
3-
import { spacesGetSpaceFixtures } from '../fixtures/space';
4-
import voteInput from '../fixtures/ingestor-payload/vote.json';
51
import cloneDeep from 'lodash/cloneDeep';
62
import omit from 'lodash/omit';
73
import db, { sequencerDB } from '../../src/helpers/mysql';
84
import relayer from '../../src/helpers/relayer';
5+
import ingestor from '../../src/ingestor';
6+
import proposalInput from '../fixtures/ingestor-payload/proposal.json';
7+
import voteInput from '../fixtures/ingestor-payload/vote.json';
8+
import { spacesGetSpaceFixtures } from '../fixtures/space';
9+
10+
const LIMITS = {
11+
'space.active_proposal_limit_per_author': 20,
12+
'space.ecosystem.proposal_limit_per_day': 150,
13+
'space.ecosystem.proposal_limit_per_month': 750,
14+
'space.ecosystem.choices_limit': 20,
15+
'space.ecosystem.body_length': 10000,
16+
'space.ecosystem.strategies_limit': 8,
17+
'space.flagged.proposal_limit_per_day': 5,
18+
'space.flagged.proposal_limit_per_month': 7,
19+
'space.flagged.choices_limit': 20,
20+
'space.flagged.body_length': 10000,
21+
'space.flagged.strategies_limit': 8,
22+
'space.default.proposal_limit_per_day': 10,
23+
'space.default.proposal_limit_per_month': 150,
24+
'space.default.choices_limit': 20,
25+
'space.default.body_length': 10000,
26+
'space.default.strategies_limit': 8,
27+
'space.turbo.proposal_limit_per_day': 40,
28+
'space.turbo.proposal_limit_per_month': 200,
29+
'space.turbo.choices_limit': 1000,
30+
'space.turbo.body_length': 40000,
31+
'space.turbo.strategies_limit': 10,
32+
'space.verified.proposal_limit_per_day': 20,
33+
'space.verified.proposal_limit_per_month': 100,
34+
'space.verified.choices_limit': 20,
35+
'space.verified.body_length': 10000,
36+
'space.verified.strategies_limit': 6,
37+
'user.default.follow.limit': 25
38+
};
39+
const ECOSYSTEM_LIST = ['test.eth', 'snapshot.eth'];
40+
jest.mock('../../src/helpers/options', () => {
41+
const originalModule = jest.requireActual('../../src/helpers/options');
42+
43+
return {
44+
__esModule: true,
45+
...originalModule,
46+
getList: () => {
47+
return ECOSYSTEM_LIST;
48+
},
49+
getLimit: async (key: string) => {
50+
return LIMITS[key];
51+
},
52+
getLimits: () => {
53+
return LIMITS;
54+
},
55+
getSpaceType: () => {
56+
return 'default';
57+
}
58+
};
59+
});
960

1061
jest.mock('../../src/helpers/moderation', () => {
1162
const originalModule = jest.requireActual('../../src/helpers/moderation');

test/integration/writer/follows.test.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1-
import { FOLLOWS_LIMIT_PER_USER } from '../../../src/helpers/limits';
21
import db, { sequencerDB } from '../../../src/helpers/mysql';
32
import { action, verify } from '../../../src/writer/follow';
43
import { spacesSqlFixtures } from '../../fixtures/space';
54

5+
const LIMIT = 10;
6+
7+
jest.mock('../../../src/helpers/options', () => {
8+
const originalModule = jest.requireActual('../../../src/helpers/options');
9+
10+
return {
11+
__esModule: true,
12+
...originalModule,
13+
getLimit: () => LIMIT
14+
};
15+
});
16+
617
describe('writer/follow', () => {
718
const TEST_PREFIX = 'test-follow-';
819
const space = spacesSqlFixtures[1];
@@ -21,7 +32,7 @@ describe('writer/follow', () => {
2132
let i = 1;
2233
const promises: Promise<any>[] = [];
2334

24-
while (i <= FOLLOWS_LIMIT_PER_USER) {
35+
while (i <= LIMIT) {
2536
promises.push(
2637
db.queryAsync('INSERT INTO snapshot_sequencer_test.spaces SET ?', {
2738
...space,
@@ -45,7 +56,7 @@ describe('writer/follow', () => {
4556

4657
it('rejects when the user has followed too much spaces', () => {
4758
return expect(verify({ from: followerId })).rejects.toEqual(
48-
`you can join max ${FOLLOWS_LIMIT_PER_USER} spaces`
59+
`you can join max ${LIMIT} spaces`
4960
);
5061
});
5162

0 commit comments

Comments
 (0)