Skip to content

Commit

Permalink
refactor: improve typing on sx.js repo (#77)
Browse files Browse the repository at this point in the history
Using common config, enabling noImplicitAny and noUncheckedIndexedAccess
  • Loading branch information
Sekhmet authored Feb 22, 2024
1 parent 352b978 commit e638604
Show file tree
Hide file tree
Showing 19 changed files with 117 additions and 59 deletions.
1 change: 1 addition & 0 deletions packages/sx.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@types/bn.js": "^5.1.1",
"@types/elliptic": "^6.4.14",
"@types/node": "^18.11.9",
"@types/randombytes": "^2.0.3",
"eslint": "^8.53.0",
"prettier": "^3.1.0",
"typescript": "^5.2.2",
Expand Down
1 change: 1 addition & 0 deletions packages/sx.js/src/authenticators/starknet/ethSig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export default function createEthSigAuthenticator(): Authenticator {
}

const [r, s, v] = envelope.signatureData.signature;
if (!r || !s || !v) throw new Error('signature needs to contain r, s and v');

const compiled = callData.compile('authenticate_vote', [
r,
Expand Down
2 changes: 1 addition & 1 deletion packages/sx.js/src/clients/evm/ethereum-sig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class EthereumSig {
};
}

public async send(envelope) {
public async send(envelope: Envelope<Propose | UpdateProposal | Vote>) {
const body = {
method: 'POST',
headers: {
Expand Down
2 changes: 1 addition & 1 deletion packages/sx.js/src/clients/offchain/ethereum-sig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Signer, TypedDataSigner, TypedDataField } from '@ethersproject/abs
import type { Vote, Envelope, SignatureData, EIP712VoteMessage, EIP712Message } from '../types';
import type { OffchainNetworkConfig } from '../../../types';

const SEQUENCER_URLS = {
const SEQUENCER_URLS: Record<OffchainNetworkConfig['eip712ChainId'], string> = {
1: 'https://seq.snapshot.org',
5: 'https://testnet.seq.snapshot.org'
};
Expand Down
2 changes: 1 addition & 1 deletion packages/sx.js/src/clients/starknet/starknet-sig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class StarknetSig {
return `0x${randomBytes(4).toString('hex')}`;
}

public async send(envelope) {
public async send(envelope: Envelope<Propose | UpdateProposal | Vote>) {
const body = {
method: 'POST',
headers: {
Expand Down
4 changes: 3 additions & 1 deletion packages/sx.js/src/offchainNetworks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
function createStandardConfig(eip712ChainId: number) {
import type { OffchainNetworkConfig } from './types';

function createStandardConfig(eip712ChainId: OffchainNetworkConfig['eip712ChainId']) {
return {
eip712ChainId
};
Expand Down
14 changes: 12 additions & 2 deletions packages/sx.js/src/strategies/evm/merkleWhitelist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { AbiCoder } from '@ethersproject/abi';
import { StandardMerkleTree } from '@openzeppelin/merkle-tree';
import { Strategy, StrategyConfig } from '../../clients/evm/types';

type Entry = {
address: string;
votingPower: bigint;
};

function getProofForVoter(tree: StandardMerkleTree<[string, bigint]>, voter: string) {
for (const [i, v] of tree.entries()) {
if ((v[0] as string).toLowerCase() === voter.toLowerCase()) {
Expand All @@ -25,7 +30,10 @@ export default function createMerkleWhitelist(): Strategy {

if (!tree) throw new Error('Invalid metadata. Missing tree');

const whitelist: [string, bigint][] = tree.map(entry => [entry.address, entry.votingPower]);
const whitelist: [string, bigint][] = tree.map((entry: Entry) => [
entry.address,
entry.votingPower
]);
const merkleTree = StandardMerkleTree.of(whitelist, ['address', 'uint96']);
const proof = getProofForVoter(merkleTree, signerAddress);

Expand All @@ -46,7 +54,9 @@ export default function createMerkleWhitelist(): Strategy {

if (!tree) throw new Error('Invalid metadata. Missing tree');

const match = tree.find(entry => entry.address.toLowerCase() === voterAddress.toLowerCase());
const match = tree.find(
(entry: Entry) => entry.address.toLowerCase() === voterAddress.toLowerCase()
);

if (match) {
return BigInt(match.votingPower.toString());
Expand Down
5 changes: 4 additions & 1 deletion packages/sx.js/src/strategies/starknet/erc20Votes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export default function createErc20VotesStrategy(): Strategy {
const isEthereumAddress = voterAddress.length === 42;
if (isEthereumAddress) return 0n;

const contract = new Contract(ERC20VotesTokenAbi, params[0], clientConfig.starkProvider);
const [tokenAddress] = params;
if (!tokenAddress) throw new Error('Missing token address');

const contract = new Contract(ERC20VotesTokenAbi, tokenAddress, clientConfig.starkProvider);

if (timestamp) {
return contract.get_past_votes(voterAddress, timestamp);
Expand Down
31 changes: 21 additions & 10 deletions packages/sx.js/src/strategies/starknet/merkleWhitelist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

import { uint256, validateAndParseAddress } from 'starknet';
import type { ClientConfig, Envelope, Strategy, Propose, Vote } from '../../types';
import { Leaf, generateMerkleProof } from '../../utils/merkletree';
import { AddressType, Leaf, generateMerkleProof } from '../../utils/merkletree';

type Entry = {
type: AddressType;
address: string;
votingPower: bigint;
};

export default function createMerkleWhitelistStrategy(): Strategy {
return {
Expand All @@ -20,24 +26,27 @@ export default function createMerkleWhitelistStrategy(): Strategy {

if (!tree) throw new Error('Invalid metadata. Missing tree');

const leaves: Leaf[] = tree.map(leaf => new Leaf(leaf.type, leaf.address, leaf.votingPower));
const leaves: Leaf[] = tree.map(
(entry: Entry) => new Leaf(entry.type, entry.address, entry.votingPower)
);
const hashes = leaves.map(leaf => leaf.hash);
const voterIndex = leaves.findIndex(
leaf => validateAndParseAddress(leaf.address) === validateAndParseAddress(signerAddress)
);

if (voterIndex === -1) throw new Error('Signer is not in whitelist');
const leaf = leaves[voterIndex];
if (voterIndex === -1 || !leaf) throw new Error('Signer is not in whitelist');

const votingPowerUint256 = uint256.bnToUint256(leaves[voterIndex].votingPower);
const votingPowerUint256 = uint256.bnToUint256(leaf.votingPower);

const proof = generateMerkleProof(hashes, voterIndex);

return [
leaves[voterIndex].type,
leaves[voterIndex].address,
votingPowerUint256.low,
votingPowerUint256.high,
proof.length,
leaf.type.toString(),
leaf.address,
votingPowerUint256.low.toString(),
votingPowerUint256.high.toString(),
proof.length.toString(),
...proof
];
},
Expand All @@ -53,7 +62,9 @@ export default function createMerkleWhitelistStrategy(): Strategy {

if (!tree) throw new Error('Invalid metadata. Missing tree');

const leaves: Leaf[] = tree.map(leaf => new Leaf(leaf.type, leaf.address, leaf.votingPower));
const leaves: Leaf[] = tree.map(
(entry: Entry) => new Leaf(entry.type, entry.address, entry.votingPower)
);
const voter = leaves.find(
leaf => validateAndParseAddress(leaf.address) === validateAndParseAddress(voterAddress)
);
Expand Down
16 changes: 11 additions & 5 deletions packages/sx.js/src/strategies/starknet/ozVotesStorageProof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default function createOzVotesStorageProofStrategy({
`0x${blockNumber.toString(16)}`
]);

const proofs = proof.storageProof.map(({ proof }) =>
const proofs = proof.storageProof.map(({ proof }: { proof: string[] }) =>
proof.map((node: string) =>
node
.slice(2)
Expand Down Expand Up @@ -104,10 +104,13 @@ export default function createOzVotesStorageProofStrategy({
chainId
);

const [checkpointMptProof, exclusionMptProof] = proofs;
if (!checkpointMptProof || !exclusionMptProof) throw new Error('Invalid proofs');

return CallData.compile({
checkpointIndex,
checkpointMptProof: proofs[0],
exclusionMpt: proofs[1]
checkpointMptProof,
exclusionMptProof
});
},
async getVotingPower(
Expand Down Expand Up @@ -149,14 +152,17 @@ export default function createOzVotesStorageProofStrategy({
chainId
);

const [checkpointMptProof, exclusionMptProof] = proofs;
if (!checkpointMptProof || !exclusionMptProof) throw new Error('Invalid proofs');

return contract.get_voting_power(
timestamp,
getUserAddressEnum('ETHEREUM', voterAddress),
params,
CallData.compile({
checkpointIndex,
checkpointMptProof: proofs[0],
exclusionMptProof: proofs[1]
checkpointMptProof,
exclusionMptProof
})
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/sx.js/src/types/networkConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,5 @@ export type EvmNetworkConfig = Omit<
};

export type OffchainNetworkConfig = {
eip712ChainId: number;
eip712ChainId: 1 | 5;
};
5 changes: 4 additions & 1 deletion packages/sx.js/src/utils/encoding/calldata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ export function flatten2DArray(array2D: string[][]): string[] {
let offset = 0;
flatArray.push('0x0'); // offset of first array
for (let i = 0; i < array2D.length - 1; i++) {
offset += array2D[i].length;
const subarray = array2D[i];
if (!subarray) throw new Error('Sparse arrays not supported');

offset += subarray.length;
flatArray.push(`0x${offset.toString(16)}`);
}
const elements = array2D.reduce((accumulator, value) => accumulator.concat(value), []);
Expand Down
10 changes: 8 additions & 2 deletions packages/sx.js/src/utils/ints-sequence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,21 @@ export class IntsSequence {

toSplitUint256(): SplitUint256 {
const rem = this.bytesLength % 8;
let uint = BigInt(this.values[this.values.length - 1]);
const value = this.values[this.values.length - 1];
if (!value) throw new Error('Missing value');

let uint = BigInt(value);
let shift = BigInt(0);
if (rem == 0) {
shift += BigInt(64);
} else {
shift += BigInt(rem * 8);
}
for (let i = 0; i < this.values.length - 1; i++) {
uint += BigInt(this.values[this.values.length - 2 - i]) << BigInt(shift);
const value = this.values[this.values.length - 2 - i];
if (!value) throw new Error('Missing value');

uint += BigInt(value) << BigInt(shift);
shift += BigInt(64);
}
return SplitUint256.fromUint(uint);
Expand Down
45 changes: 28 additions & 17 deletions packages/sx.js/src/utils/merkletree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,26 @@ export function generateMerkleRoot(hashes: string[]) {
for (let i = 0; i < hashes.length; i += 2) {
let left: string;
let right: string;
if (BigInt(hashes[i]) > BigInt(hashes[i + 1])) {
left = hashes[i];
right = hashes[i + 1];

const firstValue = hashes[i];
const secondValue = hashes[i + 1];

if (!firstValue || !secondValue) throw new Error('Invalid hash');

if (BigInt(firstValue) > BigInt(secondValue)) {
left = firstValue;
right = secondValue;
} else {
left = hashes[i + 1];
right = hashes[i];
left = secondValue;
right = firstValue;
}

newHashes.push(ec.starkCurve.pedersen(left, right));
}

return generateMerkleRoot(newHashes);
}

export function generateMerkleProof(hashes: string[], index: number) {
export function generateMerkleProof(hashes: string[], index: number): string[] {
if (hashes.length === 1) {
return [];
}
Expand All @@ -68,22 +73,28 @@ export function generateMerkleProof(hashes: string[], index: number) {
for (let i = 0; i < hashes.length; i += 2) {
let left: string;
let right: string;
if (BigInt(hashes[i]) > BigInt(hashes[i + 1])) {
left = hashes[i];
right = hashes[i + 1];

const firstValue = hashes[i];
const secondValue = hashes[i + 1];

if (!firstValue || !secondValue) throw new Error('Invalid hash');

if (BigInt(firstValue) > BigInt(secondValue)) {
left = firstValue;
right = secondValue;
} else {
left = hashes[i + 1];
right = hashes[i];
left = secondValue;
right = firstValue;
}

newHashes.push(ec.starkCurve.pedersen(left, right));
}

const proof = generateMerkleProof(newHashes, Math.floor(index / 2));

if (index % 2 === 0) {
return [hashes[index + 1], ...proof];
} else {
return [hashes[index - 1], ...proof];
}
const prefixIndex = index % 2 === 0 ? index + 1 : index - 1;
const prefix = hashes[prefixIndex];
if (!prefix) throw new Error('Invalid hash');

return [prefix, ...proof];
}
5 changes: 4 additions & 1 deletion packages/sx.js/src/utils/storage-proofs/proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export function encodeParams(
* @returns Decoded parameters
*/
export function decodeParams(params: string[]): string[][] {
const slot: string[] = [params[0], params[1], params[2], params[3]];
const [v0, v1, v2, v3] = params;
if (!v0 || !v1 || !v2 || !v3) throw new Error('Invalid storage proof parameters');

const slot: string[] = [v0, v1, v2, v3];
const numNodes = Number(params[4]);
const proofSizesBytes = params.slice(5, 5 + numNodes);
const proofSizesWords = params.slice(5 + numNodes, 5 + 2 * numNodes);
Expand Down
13 changes: 6 additions & 7 deletions packages/sx.js/src/utils/strategies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ export async function getStrategies(
config: ClientConfig
): Promise<StrategiesAddresses> {
const addresses = await Promise.all(
data.strategies.map(
id =>
config.starkProvider.getStorageAt(
data.space,
getStorageVarAddress('Voting_voting_strategies_store', id.index.toString(16))
) as Promise<string>
data.strategies.map(id =>
config.starkProvider.getStorageAt(
data.space,
getStorageVarAddress('Voting_voting_strategies_store', id.index.toString(16))
)
)
);

return data.strategies.map((v, i) => ({
index: v.index,
address: addresses[i]
address: addresses[i] as string
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('merkleWhitelist', () => {
config
);

expect(params).toEqual([1, '0x556B14CbdA79A36dC33FcD461a04A5BCb5dC2A70', '0x2a', '0x0', 0]);
expect(params).toEqual(['1', '0x556B14CbdA79A36dC33FcD461a04A5BCb5dC2A70', '0x2a', '0x0', '0']);
});

describe('getVotingPower', () => {
Expand Down
9 changes: 2 additions & 7 deletions packages/sx.js/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "es2022",
"module": "ESNext",
"moduleResolution": "Node",
"allowJs": true,
"declaration": true,
"outDir": "./dist",
"esModuleInterop": true,
"strict": true,
"resolveJsonModule": true,
"noImplicitAny": false,
"skipLibCheck": true
"outDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
Expand Down
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3074,6 +3074,13 @@
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.11.tgz#208d8a30bc507bd82e03ada29e4732ea46a6bbda"
integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==

"@types/randombytes@^2.0.3":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/randombytes/-/randombytes-2.0.3.tgz#c83a107ef51ae7a8611a7b964f54b21cb782bbed"
integrity sha512-+NRgihTfuURllWCiIAhm1wsJqzsocnqXM77V/CalsdJIYSRGEHMnritxh+6EsBklshC+clo1KgnN14qgSGeQdw==
dependencies:
"@types/node" "*"

"@types/range-parser@*":
version "1.2.7"
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
Expand Down

0 comments on commit e638604

Please sign in to comment.