Skip to content

Commit 777235d

Browse files
wa0x6eCopilotChaituVR
authored
feat: allow vote and proposal handling from starknet aliases (#542)
* feat: allow vote and proposal handling from starknet aliases * Update src/helpers/alias.ts Co-authored-by: Copilot <[email protected]> * test: update test name * feat: allow starknet aliases to update proposal * test: test improvement * fix: remove duplicate * test: improve tests * refactor: code improvement * fix: fix invalid date comparison * fix: allow optional alias to update proposal * fix lint here for now --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Chaitanya <[email protected]>
1 parent 8579de2 commit 777235d

File tree

6 files changed

+217
-19
lines changed

6 files changed

+217
-19
lines changed

src/helpers/alias.ts

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,104 @@
1+
import { uniq } from 'lodash';
12
import db from './mysql';
23

3-
export async function isValidAlias(address: string, alias: string): Promise<boolean> {
4-
const thirtyDaysAgo = Math.floor(new Date().getTime() / 1000) - 30 * 24 * 60 * 60;
4+
const DEFAULT_ALIAS_EXPIRY_DAYS = 30;
55

6-
const query =
7-
'SELECT address, alias FROM aliases WHERE address = ? AND alias = ? AND created > ? LIMIT 1';
8-
const results = await db.queryAsync(query, [address, alias, thirtyDaysAgo]);
9-
return !!results[0];
6+
// These types can always be executed with an alias
7+
const TYPES_EXECUTABLE_BY_ALIAS = [
8+
'follow',
9+
'unfollow',
10+
'subscribe',
11+
'unsubscribe',
12+
'profile',
13+
'statement'
14+
] as const;
15+
16+
// These types can be executed with an alias only when enabled in the space settings
17+
const OPTIONAL_TYPES_EXECUTABLE_BY_ALIAS = [
18+
'vote',
19+
'vote-array',
20+
'vote-string',
21+
'proposal',
22+
'update-proposal',
23+
'delete-proposal'
24+
] as const;
25+
26+
// These types can be executed with a Starknet alias
27+
const TYPES_EXECUTABLE_BY_STARKNET_ALIAS = [...OPTIONAL_TYPES_EXECUTABLE_BY_ALIAS] as const;
28+
29+
// Memoization cache for getAllowedTypes
30+
const allowedTypesCache = new Map<string, ExecutableType[]>();
31+
32+
// Types
33+
type ExecutableType =
34+
| (typeof TYPES_EXECUTABLE_BY_ALIAS)[number]
35+
| (typeof OPTIONAL_TYPES_EXECUTABLE_BY_ALIAS)[number]
36+
| 'update-proposal';
37+
38+
/**
39+
* Checks if an alias relationship exists and is not expired
40+
* @param address - The original address
41+
* @param alias - The alias address to check
42+
* @param expiryDays - Number of days after which alias expires (default: 30)
43+
* @returns Promise<boolean> - True if valid alias exists
44+
*/
45+
export async function isExistingAlias(
46+
address: string,
47+
alias: string,
48+
expiryDays = DEFAULT_ALIAS_EXPIRY_DAYS
49+
): Promise<boolean> {
50+
const query = `SELECT 1
51+
FROM aliases
52+
WHERE address = ? AND alias = ?
53+
AND created > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL ? DAY))
54+
LIMIT 1`;
55+
56+
const results = await db.queryAsync(query, [address, alias, expiryDays]);
57+
return results.length > 0;
58+
}
59+
60+
export async function verifyAlias(type: string, body: any, optionalAlias = false): Promise<void> {
61+
const { message } = body.data;
62+
63+
if (body.address === message.from) return;
64+
65+
if (
66+
!getAllowedTypes(optionalAlias, isStarknetAddress(message.from)).includes(
67+
type as ExecutableType
68+
)
69+
) {
70+
return Promise.reject(`alias not allowed for the type: ${type}`);
71+
}
72+
73+
if (!(await isExistingAlias(message.from, body.address))) {
74+
return Promise.reject('wrong alias');
75+
}
76+
}
77+
78+
// Loose checking here, as we're looking for this address in the database later,
79+
// which will always be a formatted starknet address if valid.
80+
export function isStarknetAddress(address: string): boolean {
81+
return /^0x[0-9a-fA-F]{64}$/.test(address);
82+
}
83+
84+
export function getAllowedTypes(withAlias: boolean, forStarknet: boolean): ExecutableType[] {
85+
const cacheKey = `${withAlias}-${forStarknet}`;
86+
87+
if (allowedTypesCache.has(cacheKey)) {
88+
return allowedTypesCache.get(cacheKey)!;
89+
}
90+
91+
const types: ExecutableType[] = [...TYPES_EXECUTABLE_BY_ALIAS];
92+
93+
if (withAlias) {
94+
types.push(...OPTIONAL_TYPES_EXECUTABLE_BY_ALIAS);
95+
}
96+
97+
if (forStarknet) {
98+
types.push(...TYPES_EXECUTABLE_BY_STARKNET_ALIAS);
99+
}
100+
101+
const result = uniq(types);
102+
allowedTypesCache.set(cacheKey, result);
103+
return result;
10104
}

src/helpers/shutter.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ export async function getDecryptionKey(proposal: string, url: string = SHUTTER_U
6363
return result;
6464
}
6565

66-
6766
async function setEonPubkey(params) {
6867
log.info(`[shutter] set eon pubkey ${JSON.stringify(params)}`);
6968
return true;

src/ingestor.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import snapshot from '@snapshot-labs/snapshot.js';
55
import hashTypes from '@snapshot-labs/snapshot.js/src/sign/hashedTypes.json';
66
import castArray from 'lodash/castArray';
77
import { getProposal, getSpace } from './helpers/actions';
8-
import { isValidAlias } from './helpers/alias';
8+
import { verifyAlias } from './helpers/alias';
99
import envelope from './helpers/envelope.json';
1010
import { doesMessageExist, storeMsg } from './helpers/highlight';
1111
import log from './helpers/log';
@@ -104,17 +104,7 @@ export default async function ingestor(req) {
104104
}
105105
}
106106

107-
// Check if signing address is an alias
108-
const aliasTypes = ['follow', 'unfollow', 'subscribe', 'unsubscribe', 'profile', 'statement'];
109-
const aliasOptionTypes = ['vote', 'vote-array', 'vote-string', 'proposal', 'delete-proposal'];
110-
if (body.address !== message.from) {
111-
if (!aliasTypes.includes(type) && !aliasOptionTypes.includes(type))
112-
return Promise.reject('wrong from');
113-
114-
if (aliasOptionTypes.includes(type) && !aliased) return Promise.reject('alias not enabled');
115-
116-
if (!(await isValidAlias(message.from, body.address))) return Promise.reject('wrong alias');
117-
}
107+
await verifyAlias(type, body, aliased);
118108

119109
// Check if signature is valid
120110
try {

test/fixtures/alias.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export const aliasesSqlFixtures: Record<string, any>[] = [
2+
{
3+
id: '1',
4+
ipfs: 'Qm...',
5+
address: '0x0000000000000000000000000000000000000000',
6+
alias: '0x91FD2c8d24767db4Ece7069AA27832ffaf8590f3',
7+
created: Math.floor(Date.now() / 1000)
8+
},
9+
{
10+
id: '2',
11+
ipfs: 'Qm...',
12+
address: '0x02a0a8f3b6097e7a6bd7649deb30715323072a159c0e6b71b689bd245c146cc0',
13+
alias: '0x91FD2c8d24767db4Ece7069AA27832ffaf8590f3',
14+
created: Math.floor(Date.now() / 1000)
15+
},
16+
{
17+
id: '3',
18+
ipfs: 'Qm...',
19+
address: '0x02a0a8f3b6097e7a6bd7649deb30715323072a159c0e6b71b689bd245c146cc0',
20+
alias: '0x91FD2c8d24767db4Ece7069AA27832ffaf8590f4',
21+
created: 1
22+
}
23+
];
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { isExistingAlias } from '../../../src/helpers/alias';
2+
import db, { sequencerDB } from '../../../src/helpers/mysql';
3+
import { aliasesSqlFixtures } from '../../fixtures/alias';
4+
5+
describe('alias', () => {
6+
const seed = Date.now().toFixed(0);
7+
8+
beforeAll(async () => {
9+
await db.queryAsync('DELETE from snapshot_sequencer_test.aliases');
10+
await Promise.all(
11+
aliasesSqlFixtures.map(alias => {
12+
const values = {
13+
...alias,
14+
ipfs: seed
15+
};
16+
return db.queryAsync('INSERT INTO snapshot_sequencer_test.aliases SET ?', values);
17+
})
18+
);
19+
});
20+
21+
afterEach(async () => {
22+
await db.queryAsync('DELETE from snapshot_sequencer_test.aliases where ipfs = ?', seed);
23+
});
24+
25+
afterAll(async () => {
26+
await db.endAsync();
27+
await sequencerDB.endAsync();
28+
});
29+
30+
describe('isExistingAlias()', () => {
31+
it('should return true for valid alias', () => {
32+
expect(
33+
isExistingAlias(aliasesSqlFixtures[0].address, aliasesSqlFixtures[0].alias)
34+
).resolves.toBe(true);
35+
});
36+
37+
it('should return false for un-existing alias', () => {
38+
expect(isExistingAlias(aliasesSqlFixtures[0].address, 'invalid-alias')).resolves.toBe(false);
39+
});
40+
41+
it('should return false for expired alias', () => {
42+
expect(
43+
isExistingAlias(aliasesSqlFixtures[2].address, aliasesSqlFixtures[2].alias)
44+
).resolves.toBe(false);
45+
});
46+
});
47+
});

test/unit/helpers/alias.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { getAllowedTypes, isStarknetAddress } from '../../../src/helpers/alias';
2+
3+
describe('Alias', () => {
4+
describe('isStarknetAddress()', () => {
5+
it('should return true for a starknet address', () => {
6+
expect(
7+
isStarknetAddress('0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef')
8+
).toBe(true);
9+
});
10+
it('should return false for a non-starknet address', () => {
11+
expect(isStarknetAddress('0x91FD2c8d24767db4Ece7069AA27832ffaf8590f3')).toBe(false);
12+
});
13+
it('should return false for an invalid address', () => {
14+
expect(isStarknetAddress('')).toBe(false);
15+
expect(isStarknetAddress('test')).toBe(false);
16+
});
17+
});
18+
19+
describe('getAllowedTypes()', () => {
20+
it('should return the correct types when both withAlias and forStarknet are false', () => {
21+
const result = getAllowedTypes(false, false);
22+
expect(result).toContain('profile');
23+
expect(result).not.toContain('vote');
24+
expect(result).not.toContain('update-proposal');
25+
});
26+
it('should return the correct types when withAlias is true and forStarknet is false', () => {
27+
const result = getAllowedTypes(true, false);
28+
expect(result).toContain('profile');
29+
expect(result).toContain('vote');
30+
expect(result).toContain('update-proposal');
31+
});
32+
it('should return the correct types when withAlias is false and forStarknet is true', () => {
33+
const result = getAllowedTypes(false, true);
34+
expect(result).toContain('profile');
35+
expect(result).toContain('vote');
36+
expect(result).toContain('update-proposal');
37+
});
38+
it('should return the correct types when both withAlias and forStarknet are true', () => {
39+
const result = getAllowedTypes(true, true);
40+
expect(result).toContain('profile');
41+
expect(result).toContain('vote');
42+
expect(result).toContain('update-proposal');
43+
});
44+
});
45+
});

0 commit comments

Comments
 (0)