@subsquid/evm-typegen 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getReturnType = exports.getStructType = exports.getTupleType = exports.getFullTupleType = exports.getType = void 0;
4
+ // taken from: https://github.com/ethers-io/ethers.js/blob/948f77050dae884fe88932fd88af75560aac9d78/packages/cli/src.ts/typescript.ts#L10
5
+ function getType(param) {
6
+ if (param.baseType === 'array') {
7
+ return 'Array<' + getType(param.arrayChildren) + '>';
8
+ }
9
+ if (param.baseType === 'tuple') {
10
+ return getFullTupleType(param.components);
11
+ }
12
+ if (param.type === 'address' || param.type === 'string') {
13
+ return 'string';
14
+ }
15
+ if (param.type === 'bool') {
16
+ return 'boolean';
17
+ }
18
+ let match = param.type.match(/^(u?int)([0-9]+)$/);
19
+ if (match) {
20
+ return parseInt(match[2]) < 53 ? 'number' : 'ethers.BigNumber';
21
+ }
22
+ if (param.type.substring(0, 5) === 'bytes') {
23
+ return 'string';
24
+ }
25
+ throw new Error('unknown type');
26
+ }
27
+ exports.getType = getType;
28
+ function getFullTupleType(params) {
29
+ let tuple = getTupleType(params);
30
+ let struct = getStructType(params);
31
+ if (struct == '{}') {
32
+ return tuple;
33
+ }
34
+ else {
35
+ return `(${tuple} & ${struct})`;
36
+ }
37
+ }
38
+ exports.getFullTupleType = getFullTupleType;
39
+ function getTupleType(params) {
40
+ return '[' + params.map(p => {
41
+ return p.name ? `${p.name}: ${getType(p)}` : getType(p);
42
+ }).join(', ') + ']';
43
+ }
44
+ exports.getTupleType = getTupleType;
45
+ // https://github.com/ethers-io/ethers.js/blob/948f77050dae884fe88932fd88af75560aac9d78/packages/abi/src.ts/coders/tuple.ts#L29
46
+ function getStructType(params) {
47
+ let array = [];
48
+ let counts = {};
49
+ for (let p of params) {
50
+ if (p.name && array[p.name] == null) {
51
+ counts[p.name] = (counts[p.name] || 0) + 1;
52
+ }
53
+ }
54
+ let fields = params.filter(p => counts[p.name] == 1);
55
+ return '{' + fields.map(f => `${f.name}: ${getType(f)}`).join(', ') + '}';
56
+ }
57
+ exports.getStructType = getStructType;
58
+ function getReturnType(outputs) {
59
+ return outputs.length == 1 ? getType(outputs[0]) : getFullTupleType(outputs);
60
+ }
61
+ exports.getReturnType = getReturnType;
62
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/util/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@subsquid/evm-typegen",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "description": "CLI for generating typescript types and decode implementations for evm logs",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": "git@github.com:subsquid/squid.git",
@@ -17,16 +17,22 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "commander": "^9.3.0",
20
- "@ethersproject/abi": "^5.6.4",
21
- "@subsquid/util-internal-code-printer": "^0.0.2",
22
- "@subsquid/util-internal": "^0.0.1"
20
+ "@ethersproject/abi": "^5.7.0",
21
+ "@ethersproject/address": "^5.7.0",
22
+ "@subsquid/logger": "^0.3.0",
23
+ "@subsquid/util-internal-code-printer": "^0.1.0",
24
+ "@subsquid/util-internal-commander": "^0.0.1",
25
+ "@subsquid/util-internal": "^1.0.0",
26
+ "node-fetch": "^2.6.7"
23
27
  },
24
28
  "devDependencies": {
25
29
  "@types/node": "^16.11.41",
30
+ "@types/node-fetch": "^2.6.2",
31
+ "ethers": "^5.7.2",
26
32
  "typescript": "~4.7.4"
27
33
  },
28
34
  "scripts": {
29
35
  "build": "rm -rf lib && tsc"
30
36
  },
31
- "readme": "# @subsquid/evm-typegen\n\nThis package provides `squid-evm-typegen(1)` command \nwhich can generate TypeScript definitions for evm logs\nto be used within [substrate-processor](../substrate-processor) mapping handlers.\n"
37
+ "readme": "# @subsquid/evm-typegen\n\nGenerates TypeScript facades for EVM transactions, logs and `eth_call` queries. \n\nThe generated facade classes are assumed to be used by [squids](https://docs.Subsquid.io/overview) indexing EVM data. \nThe generated classes depend on [ethers](https://www.npmjs.com/package/ethers).\n\n## Usage\n\n```\nnpm i -g @subsquid/evm-typegen\n```\n\n```\nArguments:\n output-dir output directory for generated definitions\n abi ABI file\n\nOptions:\n --multicall generate facade for MakerDAO multicall contract\n --etherscan-api <url> etherscan API to fetch contract ABI by a known address\n --clean delete output directory before run\n -h, --help display help for command\n\nABI file can be specified in three ways:\n\n1. as a plain JSON file:\n\nsquid-evm-typegen src/abi erc20.json\n\n2. as a contract address (to fetch ABI from etherscan)\n\nsquid-evm-typegen src/abi 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413\n\n3. as an arbitrary http url\n\nsquid-evm-typegen src/abi https://example.com/erc721.json\n\nIn all cases typegen will use ABI's basename as a basename of generated files.\nYou can overwrite basename of generated files using fragment (#) suffix.\n\nsquid-evm-typegen src/abi 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413#contract \n```\n"
32
38
  }
@@ -0,0 +1,115 @@
1
+ import * as ethers from 'ethers'
2
+
3
+
4
+ export interface LogRecord {
5
+ topics: string[]
6
+ data: string
7
+ }
8
+
9
+
10
+ export class LogEvent<Args> {
11
+ private fragment: ethers.utils.EventFragment
12
+
13
+ constructor(private abi: ethers.utils.Interface, public readonly topic: string) {
14
+ this.fragment = abi.getEvent(topic)
15
+ }
16
+
17
+ decode(rec: LogRecord): Args {
18
+ return this.abi.decodeEventLog(this.fragment, rec.data, rec.topics) as any as Args
19
+ }
20
+ }
21
+
22
+
23
+ export class Func<Args extends any[], FieldArgs, Result> {
24
+ private fragment: ethers.utils.FunctionFragment
25
+
26
+ constructor(private abi: ethers.utils.Interface, public readonly sighash: string) {
27
+ this.fragment = abi.getFunction(sighash)
28
+ }
29
+
30
+ decode(input: ethers.utils.BytesLike): Args & FieldArgs {
31
+ return this.abi.decodeFunctionData(this.fragment, input) as any as Args & FieldArgs
32
+ }
33
+
34
+ encode(args: Args): string {
35
+ return this.abi.encodeFunctionData(this.fragment, args)
36
+ }
37
+
38
+ decodeResult(output: ethers.utils.BytesLike): Result {
39
+ const decoded = this.abi.decodeFunctionResult(this.fragment, output)
40
+ return decoded.length > 1 ? decoded : decoded[0]
41
+ }
42
+
43
+ tryDecodeResult(output: ethers.utils.BytesLike): Result | undefined {
44
+ try {
45
+ return this.decodeResult(output)
46
+ } catch(err: any) {
47
+ return undefined
48
+ }
49
+ }
50
+ }
51
+
52
+
53
+ export function isFunctionResultDecodingError(val: unknown): val is Error & {data: string} {
54
+ if (!(val instanceof Error)) return false
55
+ let err = val as any
56
+ return err.code == 'CALL_EXCEPTION'
57
+ && typeof err.data == 'string'
58
+ && !err.errorArgs
59
+ && !err.errorName
60
+ }
61
+
62
+
63
+ export interface ChainContext {
64
+ _chain: Chain
65
+ }
66
+
67
+
68
+ export interface BlockContext {
69
+ _chain: Chain
70
+ block: Block
71
+ }
72
+
73
+
74
+ export interface Block {
75
+ height: number
76
+ }
77
+
78
+
79
+ export interface Chain {
80
+ client: {
81
+ call: <T=any>(method: string, params?: unknown[]) => Promise<T>
82
+ }
83
+ }
84
+
85
+
86
+ export class ContractBase {
87
+ private readonly _chain: Chain
88
+ private readonly blockHeight: number
89
+ readonly address: string
90
+
91
+ constructor(ctx: BlockContext, address: string)
92
+ constructor(ctx: ChainContext, block: Block, address: string)
93
+ constructor(ctx: BlockContext, blockOrAddress: Block | string, address?: string) {
94
+ this._chain = ctx._chain
95
+ if (typeof blockOrAddress === 'string') {
96
+ this.blockHeight = ctx.block.height
97
+ this.address = ethers.utils.getAddress(blockOrAddress)
98
+ } else {
99
+ if (address == null) {
100
+ throw new Error('missing contract address')
101
+ }
102
+ this.blockHeight = blockOrAddress.height
103
+ this.address = ethers.utils.getAddress(address)
104
+ }
105
+ }
106
+
107
+ async eth_call<Args extends any[], FieldArgs, Result>(func: Func<Args, FieldArgs, Result>, args: Args): Promise<Result> {
108
+ let data = func.encode(args)
109
+ let result = await this._chain.client.call('eth_call', [
110
+ {to: this.address, data},
111
+ '0x'+this.blockHeight.toString(16)
112
+ ])
113
+ return func.decodeResult(result)
114
+ }
115
+ }
package/src/main.ts CHANGED
@@ -1,34 +1,194 @@
1
- import {program} from "commander"
2
- import path from "path"
3
- import process from "process"
4
- import {Typegen} from "./typegen"
5
-
6
- export function run(): void {
7
- program.description(`
8
- Generates TypeScript definitions for evm log events
9
- for use within substrate-processor mapping handlers.
1
+ import {Interface} from '@ethersproject/abi'
2
+ import {isAddress} from '@ethersproject/address'
3
+ import {createLogger} from '@subsquid/logger'
4
+ import {runProgram, wait} from '@subsquid/util-internal'
5
+ import {OutDir} from '@subsquid/util-internal-code-printer'
6
+ import * as validator from '@subsquid/util-internal-commander'
7
+ import {InvalidArgumentError, program} from 'commander'
8
+ import * as fs from 'fs'
9
+ import path from 'path'
10
+ import {Typegen} from './typegen'
11
+ import {GET} from './util/fetch'
12
+
13
+
14
+ const LOG = createLogger('sqd:evm-typegen')
15
+
16
+
17
+ runProgram(async function() {
18
+ program
19
+ .description(`
20
+ Generates TypeScript facades for EVM transactions, logs and eth_call queries.
21
+
22
+ The generated facades are assumed to be used by "squids" indexing EVM data.
10
23
  `.trim())
11
- .requiredOption('--abi <path>', 'path to a JSON abi file')
12
- .requiredOption('--output <path>', 'path for output typescript file');
24
+ .name('squid-evm-typegen')
25
+ .argument('<output-dir>', 'output directory for generated definitions')
26
+ .argument('[abi...]', 'ABI file', specArgument)
27
+ .option('--multicall', 'generate facade for MakerDAO multicall contract')
28
+ .option(
29
+ '--etherscan-api <url>',
30
+ 'etherscan API to fetch contract ABI by a known address',
31
+ validator.Url(['http:', 'https:'])
32
+ )
33
+ .option('--clean', 'delete output directory before run')
34
+ .addHelpText('afterAll', `
35
+ ABI file can be specified in three ways:
36
+
37
+ 1. as a plain JSON file:
38
+
39
+ squid-evm-typegen src/abi erc20.json
40
+
41
+ 2. as a contract address (to fetch ABI from etherscan)
42
+
43
+ squid-evm-typegen src/abi 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413
44
+
45
+ 3. as an arbitrary http url
46
+
47
+ squid-evm-typegen src/abi https://example.com/erc721.json
48
+
49
+ In all cases typegen will use ABI's basename as a basename of generated files.
50
+ You can overwrite basename of generated files using fragment (#) suffix.
51
+
52
+ squid-evm-typegen src/abi 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413#contract
53
+ `)
54
+
55
+ program.parse()
56
+
57
+ let opts = program.opts() as {
58
+ clean?: boolean,
59
+ multicall?: boolean,
60
+ etherscanApi?: string
61
+ }
62
+ let dest = new OutDir(program.processedArgs[0])
63
+ let specs = program.processedArgs[1] as Spec[]
64
+
65
+ if (opts.clean && dest.exists()) {
66
+ LOG.info(`deleting ${dest.path()}`)
67
+ dest.del()
68
+ }
69
+
70
+ if (specs.length == 0 && !opts.multicall) {
71
+ LOG.warn('no ABI files given, nothing to generate')
72
+ return
73
+ }
74
+
75
+ dest.add('abi.support.ts', [__dirname, '../src/abi.support.ts'])
76
+ LOG.info(`saved ${dest.path('abi.support.ts')}`)
77
+
78
+ if (opts.multicall) {
79
+ dest.add('multicall.ts', [__dirname, '../src/multicall.ts'])
80
+ LOG.info(`saved ${dest.path('multicall.ts')}`)
81
+ }
13
82
 
14
- program.parse();
83
+ for (let spec of specs) {
84
+ LOG.info(`processing ${spec.src}`)
85
+ let abi_json = await read(spec, opts)
86
+ let abi = new Interface(abi_json)
87
+ new Typegen(dest, abi, spec.name, LOG).generate()
88
+ }
89
+ }, err => LOG.fatal(err))
15
90
 
16
- const options = program.opts();
17
- const inputPath = options.abi;
18
- const outputPath = options.output;
19
91
 
20
- if (path.parse(inputPath).ext !== ".json") {
21
- throw new Error("invalid abi file extension");
92
+ async function read(spec: Spec, options?: {etherscanApi?: string}): Promise<any> {
93
+ if (spec.kind == 'address') {
94
+ return fetchFromEtherscan(spec.src, options?.etherscanApi)
95
+ }
96
+ let abi: any
97
+ if (spec.kind == 'url') {
98
+ abi = await GET(spec.src)
99
+ } else {
100
+ abi = JSON.parse(fs.readFileSync(spec.src, 'utf-8'))
22
101
  }
102
+ if (Array.isArray(abi)) {
103
+ return abi
104
+ } else if (Array.isArray(abi?.abi)) {
105
+ return abi.abi
106
+ } else {
107
+ throw new Error('Unrecognized ABI format')
108
+ }
109
+ }
110
+
111
+
112
+ async function fetchFromEtherscan(address: string, api?: string): Promise<any> {
113
+ api = api || 'https://api.etherscan.io/'
114
+ let url = new URL('api?module=contract&action=getabi', api)
115
+ url.searchParams.set('address', address)
116
+ let response: {status: string, result: string}
117
+ let attempts = 2
118
+ while (true) {
119
+ response = await GET(url.toString())
120
+ if (response.status == '0' && response.result.includes('rate limit') && --attempts) {
121
+ LOG.warn('faced rate limit error while trying to fetch contract ABI. Trying again in 2 seconds.')
122
+ await wait(2000)
123
+ } else {
124
+ break
125
+ }
126
+ }
127
+ if (response.status == '1') {
128
+ return JSON.parse(response.result)
129
+ } else {
130
+ throw new Error(`Failed to fetch contract ABI from ${api}: ${response.result}`)
131
+ }
132
+ }
133
+
23
134
 
24
- if (path.parse(outputPath).ext !== ".ts") {
25
- throw new Error("invalid output file extension");
135
+ interface Spec {
136
+ kind: 'address' | 'url' | 'file'
137
+ src: string
138
+ name: string
139
+ }
140
+
141
+
142
+ function specArgument(value: string, prev: Spec[]): Spec[] {
143
+ let spec = parseSpec(value)
144
+ prev.push(spec)
145
+ return prev
146
+ }
147
+
148
+
149
+ function parseSpec(spec: string): Spec {
150
+ let [src, fragment] = splitFragment(spec)
151
+ if (src.startsWith('0x')) {
152
+ if (!isAddress(src)) throw new InvalidArgumentError('Invalid contract address')
153
+ return {
154
+ kind: 'address',
155
+ src,
156
+ name: fragment || src
157
+ }
158
+ } else if (src.includes('://')) {
159
+ let u = new URL(
160
+ validator.Url(['http:', 'https:'])(src)
161
+ )
162
+ return {
163
+ kind: 'url',
164
+ src,
165
+ name: fragment || basename(u.pathname)
166
+ }
167
+ } else {
168
+ return {
169
+ kind: 'file',
170
+ src,
171
+ name: fragment || basename(src)
172
+ }
26
173
  }
174
+ }
175
+
27
176
 
28
- try {
29
- new Typegen(inputPath, outputPath).generate();
30
- } catch (err: any) {
31
- console.error(`evm-typegen error: ${err.toString()}`);
32
- process.exit(1);
177
+ function splitFragment(spec: string): [string, string] {
178
+ let parts = spec.split('#')
179
+ if (parts.length > 1) {
180
+ let fragment = parts.pop()!
181
+ return [parts.join('#'), fragment]
182
+ } else {
183
+ return [spec, '']
33
184
  }
34
185
  }
186
+
187
+
188
+ function basename(file: string): string {
189
+ let name = path.parse(file).name
190
+ if (name) return name
191
+ throw new InvalidArgumentError(
192
+ `Can't derive target basename for output files. Use url fragment to specify it, e.g. #erc20`
193
+ )
194
+ }
@@ -0,0 +1,212 @@
1
+ import * as ethers from 'ethers'
2
+ import {ContractBase, Func} from './abi.support'
3
+
4
+
5
+ const abi = new ethers.utils.Interface([
6
+ {
7
+ type: 'function',
8
+ name: 'aggregate',
9
+ stateMutability: 'nonpayable',
10
+ inputs: [
11
+ {
12
+ name: 'calls',
13
+ type: 'tuple[]',
14
+ components: [
15
+ {name: 'target', type: 'address'},
16
+ {name: 'callData', type: 'bytes'},
17
+ ]
18
+ }
19
+ ],
20
+ outputs: [
21
+ {name: 'blockNumber', type: 'uint256'},
22
+ {name: 'returnData', type: 'bytes[]'},
23
+ ]
24
+ },
25
+ {
26
+ name: 'tryAggregate',
27
+ type: 'function',
28
+ stateMutability: 'nonpayable',
29
+ inputs: [
30
+ {name: 'requireSuccess', type: 'bool'},
31
+ {
32
+ name: 'calls',
33
+ type: 'tuple[]',
34
+ components: [
35
+ {name: 'target', type: 'address'},
36
+ {name: 'callData', type: 'bytes'},
37
+ ]
38
+ }
39
+ ],
40
+ outputs: [
41
+ {
42
+ name: 'returnData',
43
+ type: 'tuple[]',
44
+ components: [
45
+ {name: 'success', type: 'bool'},
46
+ {name: 'returnData', type: 'bytes'},
47
+ ]
48
+ },
49
+ ]
50
+ }
51
+ ])
52
+
53
+
54
+ type AnyFunc = Func<any, {}, any>
55
+ type Call = [address: string, bytes: string]
56
+
57
+
58
+ const aggregate = new Func<[calls: Call[]], {}, {blockNumber: ethers.BigNumber, returnData: string[]}>(
59
+ abi, abi.getSighash('aggregate')
60
+ )
61
+
62
+
63
+ const try_aggregate = new Func<[requireSuccess: boolean, calls: Array<[target: string, callData: string]>], {}, Array<{success: boolean, returnData: string}>>(
64
+ abi, abi.getSighash('tryAggregate')
65
+ )
66
+
67
+
68
+ export type MulticallResult<T> = {
69
+ success: true
70
+ value: T
71
+ } | {
72
+ success: false
73
+ returnData?: string
74
+ value?: undefined
75
+ }
76
+
77
+
78
+ export class Multicall extends ContractBase {
79
+ static aggregate = aggregate
80
+ static try_aggregate = try_aggregate
81
+
82
+ aggregate<Args extends any[], R>(
83
+ func: Func<Args, {}, R>,
84
+ address: string,
85
+ calls: Args[],
86
+ paging?: number
87
+ ): Promise<R[]>
88
+
89
+ aggregate<Args extends any[], R>(
90
+ func: Func<Args, {}, R>,
91
+ calls: [address: string, args: Args][],
92
+ paging?: number
93
+ ): Promise<R[]>
94
+
95
+ aggregate(
96
+ calls: [func: AnyFunc, address: string, args: any[]][],
97
+ paging?: number
98
+ ): Promise<any[]>
99
+
100
+ async aggregate(...args: any[]): Promise<any[]> {
101
+ let [calls, funcs, page] = this.makeCalls(args)
102
+ let size = calls.length
103
+ let results = new Array(size)
104
+ for (let [from, to] of splitIntoPages(size, page)) {
105
+ let {returnData} = await this.eth_call(aggregate, [calls.slice(from, to)])
106
+ for (let i = from; i < to; i++) {
107
+ let data = returnData[i - from]
108
+ results[i] = funcs[i].decodeResult(data)
109
+ }
110
+ }
111
+ return results
112
+ }
113
+
114
+ tryAggregate<Args extends any[], R>(
115
+ func: Func<Args, {}, R>,
116
+ address: string,
117
+ calls: Args[],
118
+ paging?: number
119
+ ): Promise<MulticallResult<R>[]>
120
+
121
+ tryAggregate<Args extends any[], R>(
122
+ func: Func<Args, {}, R>,
123
+ calls: [address: string, args: Args][],
124
+ paging?: number
125
+ ): Promise<MulticallResult<R>[]>
126
+
127
+ tryAggregate(
128
+ calls: [func: AnyFunc, address: string, args: any[]][],
129
+ paging?: number
130
+ ): Promise<MulticallResult<any>[]>
131
+
132
+ async tryAggregate(...args: any[]): Promise<any[]> {
133
+ let [calls, funcs, page] = this.makeCalls(args)
134
+ let size = calls.length
135
+ let results = new Array(size)
136
+ for (let [from, to] of splitIntoPages(size, page)) {
137
+ let response = await this.eth_call(try_aggregate, [false, calls.slice(from, to)])
138
+ for (let i = from; i < to; i++) {
139
+ let res = response[i - from]
140
+ if (res.success) {
141
+ try {
142
+ results[i] = {
143
+ success: true,
144
+ value: funcs[i].decodeResult(res.returnData)
145
+ }
146
+ } catch(err: any) {
147
+ results[i] = {success: false, returnData: res.returnData}
148
+ }
149
+ } else {
150
+ results[i] = {success: false}
151
+ }
152
+ }
153
+ }
154
+ return results
155
+ }
156
+
157
+ private makeCalls(args: any[]): [calls: Call[], funcs: AnyFunc[], page: number] {
158
+ let page = typeof args[args.length-1] == 'number' ? args.pop()! : Number.MAX_SAFE_INTEGER
159
+ switch(args.length) {
160
+ case 1: {
161
+ let list: [func: AnyFunc, address: string, args: any[]][] = args[0]
162
+ let calls = new Array(list.length)
163
+ let funcs = new Array(list.length)
164
+ for (let i = 0; i < list.length; i++) {
165
+ let [func, address, args] = list[i]
166
+ calls[i] = [address, func.encode(args)]
167
+ funcs[i] = func
168
+ }
169
+ return [calls, funcs, page]
170
+ }
171
+ case 2: {
172
+ let func: AnyFunc = args[0]
173
+ let list: [address: string, args: any[]][] = args[1]
174
+ let calls = new Array(list.length)
175
+ let funcs = new Array(list.length)
176
+ for (let i = 0; i < list.length; i++) {
177
+ let [address, args] = list[i]
178
+ calls[i] = [address, func.encode(args)]
179
+ funcs[i] = func
180
+ }
181
+ return [calls, funcs, page]
182
+ }
183
+ case 3: {
184
+ let func: AnyFunc = args[0]
185
+ let address: string = args[1]
186
+ let list: any[][] = args[2]
187
+ let calls = new Array(list.length)
188
+ let funcs = new Array(list.length)
189
+ for (let i = 0; i < list.length; i++) {
190
+ let args = list[i]
191
+ calls[i] = [address, func.encode(args)]
192
+ funcs[i] = func
193
+ }
194
+ return [calls, funcs, page]
195
+ }
196
+ default:
197
+ throw new Error('unexpected number of arguments')
198
+ }
199
+ }
200
+ }
201
+
202
+
203
+ function* splitIntoPages(size: number, page: number): Iterable<[from: number, to: number]> {
204
+ let from = 0
205
+ while (size) {
206
+ let step = Math.min(page, size)
207
+ let to = from + step
208
+ yield [from, to]
209
+ size -= step
210
+ from = to
211
+ }
212
+ }