Skip to content

Commit 2f95e9b

Browse files
wa0x6eCopilotChaituVR
authored
feat: add starknet support to getProvider() and multicall() and to networks.json (#1155)
* feat: add starknet to networks list and providers * fix: support number network in provider * fix: update starknet first block to 0 * fix: use starknet rpc from brovider * fix: add multicall address for starknet * test: add more tests * test: add tests for multicall * test: add tests for the multicaller class * refactor: add types * revert: revert obsolete changes * test: fix tests fixtures * refactor: prepare new structure to support starknet multicall * feat: implement starknet multicall * fix: fix types to support starknet providers * fix: relax types * fix: fix starknet multicall ignoring block number * fix: return looser type for backward compatibility * test: improve provider test * test: refactor tests * fix: implement own multicall starknet feature, since x-multicall does not support specific block search * test: update test for specific block tag search * chore: remove unused import * test: fix test * Update src/multicall/starknet.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * style: lint fix * style: fix import ordering * fix: handle more starknet types * fix: update start_block to block of multicall contract deployment block * refactor: use starknet constant instead of hardcoded chain ids * refactor: improve type * fix: remove getBatchProvider * refactor: centralize multicall export * chore: add instruction about test file * test: fix test * Update package.json --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Chaitanya <yourchaitu@gmail.com>
1 parent 2d6e1b2 commit 2f95e9b

17 files changed

+1412
-143
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@snapshot-labs/snapshot.js",
3-
"version": "0.13.1",
3+
"version": "0.14.0",
44
"repository": "snapshot-labs/snapshot.js",
55
"license": "MIT",
66
"main": "dist/snapshot.cjs.js",

src/multicall/evm.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {
2+
Fragment,
3+
FunctionFragment,
4+
Interface,
5+
JsonFragment
6+
} from '@ethersproject/abi';
7+
import { Signer } from '@ethersproject/abstract-signer';
8+
import { Contract } from '@ethersproject/contracts';
9+
import { Provider } from '@ethersproject/providers';
10+
11+
const multicallAbi = [
12+
'function aggregate(tuple(address target, bytes callData)[] calls) view returns (uint256 blockNumber, bytes[] returnData)'
13+
];
14+
15+
export default async function multicall(
16+
address: string,
17+
provider: Signer | Provider,
18+
abi: string | (Fragment | JsonFragment | string)[],
19+
calls: [string, FunctionFragment | string, any[]][],
20+
limit: number,
21+
options = {}
22+
): Promise<any> {
23+
const multi = new Contract(address, multicallAbi, provider);
24+
const itf = new Interface(abi);
25+
try {
26+
const pages = Math.ceil(calls.length / limit);
27+
const promises: any = [];
28+
Array.from(Array(pages)).forEach((x, i) => {
29+
const callsInPage = calls.slice(limit * i, limit * (i + 1));
30+
promises.push(
31+
multi.aggregate(
32+
callsInPage.map((call) => [
33+
call[0].toLowerCase(),
34+
itf.encodeFunctionData(call[1], call[2])
35+
]),
36+
options
37+
)
38+
);
39+
});
40+
let results: any[] = await Promise.all(promises);
41+
results = results.reduce((prev: any, [, res]: any) => prev.concat(res), []);
42+
return results.map((call, i) =>
43+
itf.decodeFunctionResult(calls[i][1], call)
44+
);
45+
} catch (e: any) {
46+
return Promise.reject(e);
47+
}
48+
}

src/multicall/index.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { constants } from 'starknet';
2+
import multicallEvm from './evm';
3+
import multicallStarknet from './starknet';
4+
import Multicaller from './multicaller';
5+
import networks from '../networks.json';
6+
7+
type NetworkId = keyof typeof networks;
8+
9+
const STARKNET_CHAIN_IDS: NetworkId[] = [
10+
constants.StarknetChainId.SN_MAIN,
11+
constants.StarknetChainId.SN_SEPOLIA
12+
] as const;
13+
14+
const MULTICALLS_FN = {
15+
evm: multicallEvm,
16+
starknet: multicallStarknet
17+
} as const;
18+
19+
async function multicall(
20+
network: string,
21+
provider,
22+
abi: any[],
23+
calls: any[],
24+
options: Record<string, any> = {}
25+
) {
26+
const address = options?.multicallAddress || networks[network].multicall;
27+
28+
if (!address) {
29+
throw new Error('missing multicall address');
30+
}
31+
32+
const multicallOptions = { ...options };
33+
const limit = multicallOptions?.limit || 500;
34+
35+
delete multicallOptions.limit;
36+
delete multicallOptions.multicallAddress;
37+
38+
const protocol = STARKNET_CHAIN_IDS.includes(network as NetworkId)
39+
? 'starknet'
40+
: 'evm';
41+
42+
return MULTICALLS_FN[protocol](
43+
address,
44+
provider,
45+
abi,
46+
calls,
47+
limit,
48+
multicallOptions
49+
);
50+
}
51+
52+
export { multicall, Multicaller };

src/utils/multicaller.ts renamed to src/multicall/multicaller.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
11
import { StaticJsonRpcProvider } from '@ethersproject/providers';
22
import set from 'lodash.set';
3-
import { multicall } from '../utils';
3+
import { RpcProvider } from 'starknet';
4+
import { multicall } from './';
5+
6+
type Path = string | number | number[] | string[];
47

58
export default class Multicaller {
69
public network: string;
7-
public provider: StaticJsonRpcProvider;
10+
public provider: StaticJsonRpcProvider | RpcProvider;
811
public abi: any[];
912
public options: any = {};
1013
public calls: any[] = [];
11-
public paths: any[] = [];
14+
public paths: Path[] = [];
1215

1316
constructor(
1417
network: string,
15-
provider: StaticJsonRpcProvider,
18+
provider: StaticJsonRpcProvider | RpcProvider,
1619
abi: any[],
17-
options?
20+
options: any = {}
1821
) {
1922
this.network = network;
2023
this.provider = provider;
2124
this.abi = abi;
22-
this.options = options || {};
25+
this.options = options;
2326
}
2427

25-
call(path, address, fn, params?): Multicaller {
28+
call(path: Path, address: string, fn: string, params?: any[]): Multicaller {
2629
this.calls.push([address, fn, params]);
2730
this.paths.push(path);
2831
return this;
@@ -37,7 +40,9 @@ export default class Multicaller {
3740
this.calls,
3841
this.options
3942
);
40-
result.forEach((r, i) => set(obj, this.paths[i], r.length > 1 ? r : r[0]));
43+
result.forEach((r: any, i: number) =>
44+
set(obj, this.paths[i], r.length > 1 ? r : r[0])
45+
);
4146
this.calls = [];
4247
this.paths = [];
4348
return obj;

src/multicall/starknet.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { num, RpcProvider, shortString, transaction, uint256 } from 'starknet';
2+
3+
/**
4+
* Parses the raw result from a Starknet function call based on its ABI.
5+
* It handles different types like felt252, u8, u256, etc., and decodes them accordingly.
6+
* @param rawResult - The raw result from the Starknet function call.
7+
* @param functionAbi - The ABI of the function that was called.
8+
* @returns The parsed result in a more usable format.
9+
*/
10+
function parseStarknetResult(rawResult: string[], functionAbi: any): any {
11+
if (
12+
!functionAbi ||
13+
!functionAbi.outputs ||
14+
!Array.isArray(rawResult) ||
15+
rawResult.length === 0
16+
) {
17+
return rawResult;
18+
}
19+
20+
const output = functionAbi.outputs[0];
21+
const rawValue = rawResult[0];
22+
23+
try {
24+
switch (output.type) {
25+
case 'core::felt252':
26+
// Try to decode as shortString (for name, symbol)
27+
try {
28+
return shortString.decodeShortString(rawValue);
29+
} catch {
30+
// If shortString decode fails, return as hex
31+
return rawValue;
32+
}
33+
34+
// Unsigned integers
35+
case 'core::integer::u8':
36+
case 'core::integer::u16':
37+
case 'core::integer::u32':
38+
case 'core::integer::u64':
39+
return parseInt(rawValue, 16);
40+
41+
case 'core::integer::u128':
42+
case 'core::integer::usize':
43+
return BigInt(rawValue).toString();
44+
45+
case 'core::integer::u256':
46+
return uint256.uint256ToBN({
47+
low: rawValue,
48+
high: rawResult[1] || '0x0'
49+
});
50+
51+
// Signed integers
52+
case 'core::integer::i8':
53+
case 'core::integer::i16':
54+
case 'core::integer::i32':
55+
case 'core::integer::i64':
56+
return parseInt(rawValue, 16);
57+
58+
case 'core::integer::i128':
59+
return BigInt(rawValue).toString();
60+
61+
// Boolean type
62+
case 'core::bool':
63+
return rawValue === '0x1' || rawValue === '0x01';
64+
65+
// Address types
66+
case 'core::starknet::contract_address::ContractAddress':
67+
case 'core::starknet::class_hash::ClassHash':
68+
case 'core::starknet::storage_access::StorageAddress':
69+
return rawValue;
70+
71+
// Byte array
72+
case 'core::bytes_31::bytes31':
73+
return rawValue;
74+
75+
default:
76+
// Return raw value for unknown types
77+
return rawValue;
78+
}
79+
} catch {
80+
// Fallback to raw result if parsing fails
81+
return rawResult;
82+
}
83+
}
84+
85+
/**
86+
* Partitions the responses from a Starknet multicall into individual call results.
87+
* Each response starts with its length, followed by the actual response data.
88+
* @param responses - The array of responses from the Starknet multicall.
89+
* @returns An array of arrays, where each inner array contains the response data for a single call.
90+
*/
91+
const partitionResponses = (responses: string[]): string[][] => {
92+
if (responses.length === 0) {
93+
return [];
94+
}
95+
96+
const [responseLength, ...restResponses] = responses;
97+
const responseLengthInt = Number(num.toBigInt(responseLength));
98+
const response = restResponses.slice(0, responseLengthInt);
99+
const remainingResponses = restResponses.slice(responseLengthInt);
100+
101+
return [response, ...partitionResponses(remainingResponses)];
102+
};
103+
104+
export default async function multicall(
105+
address: string,
106+
provider: RpcProvider,
107+
abi: any[],
108+
calls: any[],
109+
limit: number,
110+
options: Record<string, any> = {}
111+
) {
112+
const callData = calls.map((call) => {
113+
return {
114+
contractAddress: call[0],
115+
entrypoint: call[1],
116+
calldata: call[2] || []
117+
};
118+
});
119+
120+
// Chunk calls into batches based on limit
121+
const chunks: any[][] = [];
122+
for (let i = 0; i < callData.length; i += limit) {
123+
chunks.push(callData.slice(i, i + limit));
124+
}
125+
126+
// Process each chunk
127+
const paginatedResults = await Promise.all(
128+
chunks.map((chunk) =>
129+
provider.callContract(
130+
{
131+
contractAddress: address,
132+
entrypoint: 'aggregate',
133+
calldata: transaction.fromCallsToExecuteCalldata(chunk)
134+
},
135+
options.blockTag ?? 'latest'
136+
)
137+
)
138+
);
139+
140+
const callResults = paginatedResults
141+
.map((callContractResult) => {
142+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
143+
const [_blockNumber, _totalLength, ...results] = callContractResult;
144+
return partitionResponses(results);
145+
})
146+
.flat();
147+
148+
return callResults.map((result, index) => {
149+
const [, functionName] = calls[index];
150+
const functionAbi = abi.find((item) => item.name === functionName);
151+
152+
return [parseStarknetResult(result, functionAbi)];
153+
});
154+
}

src/networks.json

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1970,5 +1970,34 @@
19701970
},
19711971
"start": 7521509,
19721972
"logo": "ipfs://QmNnGPr1CNvj12SSGzKARtUHv9FyEfE5nES73U4vBWQSJL"
1973+
},
1974+
"0x534e5f4d41494e": {
1975+
"key": "sn",
1976+
"name": "Starknet",
1977+
"shortName": "Starknet",
1978+
"chainId": "0x534e5f4d41494e",
1979+
"network": "mainnet",
1980+
"multicall": "0x05754af3760f3356da99aea5c3ec39ccac7783d925a19666ebbeca58ff0087f4",
1981+
"explorer": {
1982+
"url": "https://starkscan.co"
1983+
},
1984+
"rpc": [],
1985+
"start": 8446,
1986+
"logo": "ipfs://bafkreihbjafyh7eud7r6e5743esaamifcttsvbspfwcrfoc5ykodjdi67m"
1987+
},
1988+
"0x534e5f5345504f4c4941": {
1989+
"key": "sn-sep",
1990+
"name": "Starknet Testnet",
1991+
"shortName": "testnet",
1992+
"chainId": "0x534e5f5345504f4c4941",
1993+
"network": "testnet",
1994+
"testnet": true,
1995+
"multicall": "0x05754af3760f3356da99aea5c3ec39ccac7783d925a19666ebbeca58ff0087f4",
1996+
"explorer": {
1997+
"url": "https://sepolia.starkscan.co"
1998+
},
1999+
"rpc": [],
2000+
"start": 7,
2001+
"logo": "ipfs://bafkreihbjafyh7eud7r6e5743esaamifcttsvbspfwcrfoc5ykodjdi67m"
19732002
}
1974-
}
2003+
}

src/schemas/space.json

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,7 @@
9191
"type": "string",
9292
"snapshotNetwork": true,
9393
"title": "network",
94-
"minLength": 1,
95-
"maxLength": 32
94+
"minLength": 1
9695
},
9796
"symbol": {
9897
"type": "string",
@@ -135,7 +134,6 @@
135134
},
136135
"network": {
137136
"type": "string",
138-
"maxLength": 12,
139137
"title": "network",
140138
"snapshotNetwork": true
141139
},
@@ -291,11 +289,7 @@
291289
"type": "string",
292290
"title": "Delegation network",
293291
"description": "The network of your delegation contract",
294-
"anyOf": [
295-
{ "snapshotNetwork": true },
296-
{ "starknetNetwork": true }
297-
],
298-
"errorMessage": "Must be a valid network"
292+
"snapshotNetwork": true
299293
},
300294
"delegationApi": {
301295
"type": "string",
@@ -417,11 +411,7 @@
417411
"network": {
418412
"type": "string",
419413
"title": "Network",
420-
"anyOf": [
421-
{ "snapshotNetwork": true },
422-
{ "starknetNetwork": true }
423-
],
424-
"errorMessage": "Must be a valid network"
414+
"snapshotNetwork": true
425415
}
426416
},
427417
"required": ["name", "address", "network"],

0 commit comments

Comments
 (0)