@snapshot-labs/snapshot.js 0.11.38 → 0.12.0-beta.1
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.
- package/LICENSE +0 -0
- package/dist/index.d.ts +2 -29
- package/dist/schemas/index.d.ts +2 -29
- package/dist/sign/types.d.ts +0 -3
- package/dist/snapshot.cjs.js +1549 -1906
- package/dist/snapshot.esm.js +1550 -1907
- package/dist/snapshot.js/src/index.d.ts +677 -0
- package/dist/snapshot.js/src/sign/index.d.ts +28 -0
- package/dist/snapshot.min.js +16 -5
- package/dist/src/index.d.ts +725 -0
- package/dist/src/schemas/index.d.ts +671 -0
- package/dist/src/sign/index.d.ts +29 -0
- package/dist/src/sign/types.d.ts +227 -0
- package/dist/src/utils/blockfinder.d.ts +1 -0
- package/dist/src/utils/delegation.d.ts +18 -0
- package/dist/src/utils/multicaller.d.ts +12 -0
- package/dist/src/utils/provider.d.ts +5 -0
- package/dist/src/utils/web3.d.ts +2 -0
- package/dist/src/utils.d.ts +91 -0
- package/dist/src/verify/evm.d.ts +4 -0
- package/dist/src/verify/evm.spec.d.ts +1 -0
- package/dist/src/verify/index.d.ts +11 -0
- package/dist/src/verify/index.spec.d.ts +1 -0
- package/dist/src/verify/starknet.d.ts +6 -0
- package/dist/src/verify/starknet.spec.d.ts +1 -0
- package/dist/src/voting/approval.d.ts +22 -0
- package/dist/src/voting/index.d.ts +14 -0
- package/dist/src/voting/quadratic.d.ts +20 -0
- package/dist/src/voting/rankedChoice.d.ts +18 -0
- package/dist/src/voting/singleChoice.d.ts +18 -0
- package/dist/src/voting/types.d.ts +35 -0
- package/dist/src/voting/weighted.d.ts +26 -0
- package/package.json +5 -3
- package/src/sign/hashedTypes.json +5 -1
- package/src/utils/dist/provider.js +47 -0
- package/src/utils/provider.ts +8 -2
- package/src/utils/web3.ts +1 -1
- package/src/utils.spec.js +36 -1
- package/src/utils.ts +44 -14
- package/src/verify/evm.spec.ts +32 -0
- package/src/verify/evm.ts +82 -0
- package/src/verify/index.spec.ts +84 -0
- package/src/verify/index.ts +41 -0
- package/src/verify/starknet.spec.ts +55 -0
- package/src/verify/starknet.ts +82 -0
- package/src/sign/eip1271.ts +0 -55
- package/src/sign/utils.ts +0 -27
package/src/utils/provider.ts
CHANGED
|
@@ -6,9 +6,15 @@ import {
|
|
|
6
6
|
const providers = {};
|
|
7
7
|
const batchedProviders = {};
|
|
8
8
|
|
|
9
|
+
export type ProviderOptions = {
|
|
10
|
+
broviderUrl?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DEFAULT_BROVIDER_URL = 'https://rpc.snapshot.org';
|
|
14
|
+
|
|
9
15
|
export default function getProvider(
|
|
10
16
|
network,
|
|
11
|
-
{ broviderUrl =
|
|
17
|
+
{ broviderUrl = DEFAULT_BROVIDER_URL }: ProviderOptions = {}
|
|
12
18
|
) {
|
|
13
19
|
const url = `${broviderUrl}/${network}`;
|
|
14
20
|
if (!providers[network])
|
|
@@ -25,7 +31,7 @@ export default function getProvider(
|
|
|
25
31
|
|
|
26
32
|
export function getBatchedProvider(
|
|
27
33
|
network,
|
|
28
|
-
{ broviderUrl =
|
|
34
|
+
{ broviderUrl = DEFAULT_BROVIDER_URL }: ProviderOptions = {}
|
|
29
35
|
) {
|
|
30
36
|
const url = `${broviderUrl}/${network}`;
|
|
31
37
|
if (!batchedProviders[network])
|
package/src/utils/web3.ts
CHANGED
package/src/utils.spec.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect, vi, afterEach } from 'vitest';
|
|
2
2
|
import * as crossFetch from 'cross-fetch';
|
|
3
|
-
import { validate, getScores, getVp } from './utils';
|
|
3
|
+
import { validate, getScores, getVp, getFormattedAddress } from './utils';
|
|
4
4
|
|
|
5
5
|
vi.mock('cross-fetch', async () => {
|
|
6
6
|
const actual = await vi.importActual('cross-fetch');
|
|
@@ -492,4 +492,39 @@ describe('utils', () => {
|
|
|
492
492
|
});
|
|
493
493
|
});
|
|
494
494
|
});
|
|
495
|
+
|
|
496
|
+
describe('getFormattedAddress', () => {
|
|
497
|
+
test('returns a checksummed EVM address', () => {
|
|
498
|
+
const address = '0x91fd2c8d24767db4ece7069aa27832ffaf8590f3';
|
|
499
|
+
expect(getFormattedAddress(address, ['evm'])).toEqual(
|
|
500
|
+
'0x91FD2c8d24767db4Ece7069AA27832ffaf8590f3'
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test('returns a padded and lowercased starknet address', () => {
|
|
505
|
+
const address =
|
|
506
|
+
'0x2a0a8f3b6097e7a6bd7649deb30715323072a159c0e6b71b689bd245c146cc0';
|
|
507
|
+
expect(getFormattedAddress(address, ['starknet'])).toEqual(
|
|
508
|
+
'0x02a0a8f3b6097e7a6bd7649deb30715323072a159c0e6b71b689bd245c146cc0'
|
|
509
|
+
);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test('returns an EVM address as starknet address', () => {
|
|
513
|
+
const address = '0x91FD2c8d24767db4Ece7069AA27832ffaf8590f3';
|
|
514
|
+
expect(getFormattedAddress(address, ['starknet'])).toEqual(
|
|
515
|
+
'0x00000000000000000000000091fd2c8d24767db4ece7069aa27832ffaf8590f3'
|
|
516
|
+
);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('throws an error when the address is not a starknet address', () => {
|
|
520
|
+
const address = 'hello';
|
|
521
|
+
expect(() => getFormattedAddress(address, ['starknet'])).toThrow();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test('throws an error when the address is not an EVM address', () => {
|
|
525
|
+
const address =
|
|
526
|
+
'0x2a0a8f3b6097e7a6bd7649deb30715323072a159c0e6b71b689bd245c146cc0';
|
|
527
|
+
expect(() => getFormattedAddress(address, ['evm'])).toThrow();
|
|
528
|
+
});
|
|
529
|
+
});
|
|
495
530
|
});
|
package/src/utils.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fetch from 'cross-fetch';
|
|
2
2
|
import { Interface } from '@ethersproject/abi';
|
|
3
3
|
import { Contract } from '@ethersproject/contracts';
|
|
4
|
-
import { isAddress } from '@ethersproject/address';
|
|
4
|
+
import { getAddress, isAddress } from '@ethersproject/address';
|
|
5
5
|
import { parseUnits } from '@ethersproject/units';
|
|
6
6
|
import { namehash, ensNormalize } from '@ethersproject/hash';
|
|
7
7
|
import { jsonToGraphQLQuery } from 'json-to-graphql-query';
|
|
@@ -12,11 +12,12 @@ import Multicaller from './utils/multicaller';
|
|
|
12
12
|
import { getSnapshots } from './utils/blockfinder';
|
|
13
13
|
import getProvider from './utils/provider';
|
|
14
14
|
import { signMessage, getBlockNumber } from './utils/web3';
|
|
15
|
-
import { getHash, verify } from './
|
|
15
|
+
import { getHash, verify } from './verify';
|
|
16
16
|
import gateways from './gateways.json';
|
|
17
17
|
import networks from './networks.json';
|
|
18
18
|
import voting from './voting';
|
|
19
19
|
import getDelegatesBySpace, { SNAPSHOT_SUBGRAPH_URL } from './utils/delegation';
|
|
20
|
+
import { validateAndParseAddress } from 'starknet';
|
|
20
21
|
|
|
21
22
|
interface Options {
|
|
22
23
|
url?: string;
|
|
@@ -65,7 +66,7 @@ async function parseScoreAPIResponse(res: any) {
|
|
|
65
66
|
let data: any = await res.text();
|
|
66
67
|
try {
|
|
67
68
|
data = JSON.parse(data);
|
|
68
|
-
} catch (e) {
|
|
69
|
+
} catch (e: any) {
|
|
69
70
|
return Promise.reject({
|
|
70
71
|
code: res.status || 500,
|
|
71
72
|
message: 'Failed to parse response from score API',
|
|
@@ -90,17 +91,17 @@ ajv.addFormat('address', {
|
|
|
90
91
|
validate: (value: string) => {
|
|
91
92
|
try {
|
|
92
93
|
return isAddress(value);
|
|
93
|
-
} catch (
|
|
94
|
+
} catch (e: any) {
|
|
94
95
|
return false;
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
});
|
|
98
99
|
|
|
99
|
-
ajv.addFormat('
|
|
100
|
+
ajv.addFormat('starknetAddress', {
|
|
100
101
|
validate: (value: string) => {
|
|
101
102
|
try {
|
|
102
|
-
return
|
|
103
|
-
} catch (
|
|
103
|
+
return isStarknetAddress(value);
|
|
104
|
+
} catch (e: any) {
|
|
104
105
|
return false;
|
|
105
106
|
}
|
|
106
107
|
}
|
|
@@ -204,7 +205,7 @@ export async function call(provider, abi: any[], call: any[], options?) {
|
|
|
204
205
|
try {
|
|
205
206
|
const params = call[2] || [];
|
|
206
207
|
return await contract[call[1]](...params, options || {});
|
|
207
|
-
} catch (e) {
|
|
208
|
+
} catch (e: any) {
|
|
208
209
|
return Promise.reject(e);
|
|
209
210
|
}
|
|
210
211
|
}
|
|
@@ -245,7 +246,7 @@ export async function multicall(
|
|
|
245
246
|
return results.map((call, i) =>
|
|
246
247
|
itf.decodeFunctionResult(calls[i][1], call)
|
|
247
248
|
);
|
|
248
|
-
} catch (e) {
|
|
249
|
+
} catch (e: any) {
|
|
249
250
|
return Promise.reject(e);
|
|
250
251
|
}
|
|
251
252
|
}
|
|
@@ -263,7 +264,7 @@ export async function subgraphRequest(url: string, query, options: any = {}) {
|
|
|
263
264
|
let responseData: any = await res.text();
|
|
264
265
|
try {
|
|
265
266
|
responseData = JSON.parse(responseData);
|
|
266
|
-
} catch (e) {
|
|
267
|
+
} catch (e: any) {
|
|
267
268
|
throw new Error(
|
|
268
269
|
`Errors found in subgraphRequest: URL: ${url}, Status: ${
|
|
269
270
|
res.status
|
|
@@ -388,7 +389,7 @@ export async function getScores(
|
|
|
388
389
|
return options.returnValue === 'all'
|
|
389
390
|
? response.result
|
|
390
391
|
: response.result[options.returnValue || 'scores'];
|
|
391
|
-
} catch (e) {
|
|
392
|
+
} catch (e: any) {
|
|
392
393
|
if (e.errno) {
|
|
393
394
|
return Promise.reject({ code: e.errno, message: e.toString(), data: '' });
|
|
394
395
|
}
|
|
@@ -448,7 +449,7 @@ export async function getVp(
|
|
|
448
449
|
const res = await fetch(url, init);
|
|
449
450
|
const response = await parseScoreAPIResponse(res);
|
|
450
451
|
return response.result;
|
|
451
|
-
} catch (e) {
|
|
452
|
+
} catch (e: any) {
|
|
452
453
|
if (e.errno) {
|
|
453
454
|
return Promise.reject({ code: e.errno, message: e.toString(), data: '' });
|
|
454
455
|
}
|
|
@@ -502,7 +503,7 @@ export async function validate(
|
|
|
502
503
|
const res = await fetch(url, init);
|
|
503
504
|
const response = await parseScoreAPIResponse(res);
|
|
504
505
|
return response.result;
|
|
505
|
-
} catch (e) {
|
|
506
|
+
} catch (e: any) {
|
|
506
507
|
if (e.errno) {
|
|
507
508
|
return Promise.reject({ code: e.errno, message: e.toString(), data: '' });
|
|
508
509
|
}
|
|
@@ -563,7 +564,7 @@ export async function getSpaceUri(
|
|
|
563
564
|
): Promise<string | null> {
|
|
564
565
|
try {
|
|
565
566
|
return await getEnsTextRecord(id, 'snapshot', network, options);
|
|
566
|
-
} catch (e) {
|
|
567
|
+
} catch (e: any) {
|
|
567
568
|
console.log(e);
|
|
568
569
|
return null;
|
|
569
570
|
}
|
|
@@ -647,6 +648,32 @@ function isValidSnapshot(snapshot: number | string, network: string) {
|
|
|
647
648
|
);
|
|
648
649
|
}
|
|
649
650
|
|
|
651
|
+
export function isStarknetAddress(address: string): boolean {
|
|
652
|
+
if (!address) return false;
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
validateAndParseAddress(address);
|
|
656
|
+
return true;
|
|
657
|
+
} catch (e: any) {
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function isEvmAddress(address: string): boolean {
|
|
663
|
+
return isAddress(address);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function getFormattedAddress(
|
|
667
|
+
address: string,
|
|
668
|
+
format: string[] = ['evm', 'starknet']
|
|
669
|
+
): string {
|
|
670
|
+
if (format.includes('evm') && isAddress(address)) return getAddress(address);
|
|
671
|
+
if (format.includes('starknet') && isStarknetAddress(address))
|
|
672
|
+
return validateAndParseAddress(address);
|
|
673
|
+
|
|
674
|
+
throw new Error(`Invalid address: ${address}`);
|
|
675
|
+
}
|
|
676
|
+
|
|
650
677
|
function inputError(message: string) {
|
|
651
678
|
return Promise.reject(new Error(message));
|
|
652
679
|
}
|
|
@@ -681,5 +708,8 @@ export default {
|
|
|
681
708
|
getHash,
|
|
682
709
|
verify,
|
|
683
710
|
validate,
|
|
711
|
+
isStarknetAddress,
|
|
712
|
+
isEvmAddress,
|
|
713
|
+
getFormattedAddress,
|
|
684
714
|
SNAPSHOT_SUBGRAPH_URL
|
|
685
715
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { test, expect, describe } from 'vitest';
|
|
2
|
+
import evmMessage from '../../test/fixtures/evm/message-alias.json';
|
|
3
|
+
import verify, { getHash } from './evm';
|
|
4
|
+
|
|
5
|
+
describe('verify/evm', () => {
|
|
6
|
+
describe('getHash()', () => {
|
|
7
|
+
test('should return a hash string', () => {
|
|
8
|
+
const hash = getHash(evmMessage.data);
|
|
9
|
+
expect(hash).toBe(
|
|
10
|
+
'0x82ed8be33f43c86f9b83d14736e5762c89108fbc9b8b54f6e993818fc8a53525'
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('verify()', () => {
|
|
16
|
+
test('should return true if the signature is valid', () => {
|
|
17
|
+
expect(
|
|
18
|
+
verify(evmMessage.address, evmMessage.sig, evmMessage.data)
|
|
19
|
+
).resolves.toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('should throw an error if the signature is invalid', () => {
|
|
23
|
+
expect(
|
|
24
|
+
verify(
|
|
25
|
+
'0xDD983E11Cf84746f3b7589ee1Dc2081c08c40Cb3',
|
|
26
|
+
evmMessage.sig,
|
|
27
|
+
evmMessage.data
|
|
28
|
+
)
|
|
29
|
+
).rejects.toThrowError(/isValidSignature/);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { verifyTypedData } from '@ethersproject/wallet';
|
|
2
|
+
import { _TypedDataEncoder } from '@ethersproject/hash';
|
|
3
|
+
import { arrayify } from '@ethersproject/bytes';
|
|
4
|
+
import getProvider, { type ProviderOptions } from '../utils/provider';
|
|
5
|
+
import { call } from '../utils';
|
|
6
|
+
import type { SignaturePayload } from '.';
|
|
7
|
+
import type { StaticJsonRpcProvider } from '@ethersproject/providers';
|
|
8
|
+
|
|
9
|
+
function isEqual(a: string, b: string): boolean {
|
|
10
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getHash(data: SignaturePayload): string {
|
|
14
|
+
const { domain, types, message } = data;
|
|
15
|
+
return _TypedDataEncoder.hash(domain, types, message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default async function verify(
|
|
19
|
+
address: string,
|
|
20
|
+
sig: string,
|
|
21
|
+
data: SignaturePayload,
|
|
22
|
+
network = '1',
|
|
23
|
+
options: ProviderOptions = {}
|
|
24
|
+
): Promise<boolean> {
|
|
25
|
+
const { domain, types, message } = data;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const recoverAddress = verifyTypedData(domain, types, message, sig);
|
|
29
|
+
if (isEqual(address, recoverAddress)) return true;
|
|
30
|
+
} catch (e: any) {}
|
|
31
|
+
|
|
32
|
+
const provider = getProvider(network, options);
|
|
33
|
+
const hash = getHash(data);
|
|
34
|
+
|
|
35
|
+
if (await verifyDefault(address, sig, hash, provider)) return true;
|
|
36
|
+
|
|
37
|
+
return await verifyOldVersion(address, sig, hash, provider);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function verifyDefault(
|
|
41
|
+
address: string,
|
|
42
|
+
sig: string,
|
|
43
|
+
hash: string,
|
|
44
|
+
provider: StaticJsonRpcProvider
|
|
45
|
+
): Promise<boolean> {
|
|
46
|
+
let returnValue: string;
|
|
47
|
+
const magicValue = '0x1626ba7e';
|
|
48
|
+
const abi =
|
|
49
|
+
'function isValidSignature(bytes32 _hash, bytes memory _signature) public view returns (bytes4 magicValue)';
|
|
50
|
+
try {
|
|
51
|
+
returnValue = await call(
|
|
52
|
+
provider,
|
|
53
|
+
[abi],
|
|
54
|
+
[address, 'isValidSignature', [arrayify(hash), sig]]
|
|
55
|
+
);
|
|
56
|
+
} catch (e: any) {
|
|
57
|
+
if (e.message.startsWith('missing revert data in call exception')) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return isEqual(returnValue, magicValue);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function verifyOldVersion(
|
|
67
|
+
address: string,
|
|
68
|
+
sig: string,
|
|
69
|
+
hash: string,
|
|
70
|
+
provider: StaticJsonRpcProvider
|
|
71
|
+
): Promise<boolean> {
|
|
72
|
+
const magicValue = '0x20c13b0b';
|
|
73
|
+
const abi =
|
|
74
|
+
'function isValidSignature(bytes _hash, bytes memory _signature) public view returns (bytes4 magicValue)';
|
|
75
|
+
|
|
76
|
+
const returnValue = await call(
|
|
77
|
+
provider,
|
|
78
|
+
[abi],
|
|
79
|
+
[address, 'isValidSignature', [arrayify(hash), sig]]
|
|
80
|
+
);
|
|
81
|
+
return isEqual(returnValue, magicValue);
|
|
82
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { test, expect, describe, vi } from 'vitest';
|
|
2
|
+
import { verify, getHash } from '.';
|
|
3
|
+
import evmMessage from '../../test/fixtures/evm/message-alias.json';
|
|
4
|
+
import starknetMessage from '../../test/fixtures/starknet/message-alias.json';
|
|
5
|
+
import * as evmVerification from './evm';
|
|
6
|
+
import * as starknetVerification from './starknet';
|
|
7
|
+
|
|
8
|
+
const evmVerificationMock = vi.spyOn(evmVerification, 'default');
|
|
9
|
+
const starknetVerificationMock = vi.spyOn(starknetVerification, 'default');
|
|
10
|
+
evmVerificationMock.mockImplementation(() => Promise.resolve(true));
|
|
11
|
+
starknetVerificationMock.mockImplementation(() => Promise.resolve(true));
|
|
12
|
+
|
|
13
|
+
const evmGetHashMock = vi.spyOn(evmVerification, 'getHash');
|
|
14
|
+
const starknetGetHashMock = vi.spyOn(starknetVerification, 'getHash');
|
|
15
|
+
evmGetHashMock.mockImplementation(() => '');
|
|
16
|
+
starknetGetHashMock.mockImplementation(() => '');
|
|
17
|
+
|
|
18
|
+
describe('sign/utils', () => {
|
|
19
|
+
describe('getHash', () => {
|
|
20
|
+
test('should return an EVM hash on EVM address', () => {
|
|
21
|
+
expect.assertions(1);
|
|
22
|
+
getHash(evmMessage.data, evmMessage.address);
|
|
23
|
+
|
|
24
|
+
expect(evmGetHashMock).toHaveBeenCalled();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should return a Starknet hash on starknet payload and starknet address', () => {
|
|
28
|
+
expect.assertions(1);
|
|
29
|
+
getHash(starknetMessage.data, starknetMessage.address);
|
|
30
|
+
|
|
31
|
+
expect(starknetGetHashMock).toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('should return a Starknet hash on starknet payload and EVM address', () => {
|
|
35
|
+
expect.assertions(1);
|
|
36
|
+
getHash(starknetMessage.data, evmMessage.address);
|
|
37
|
+
|
|
38
|
+
expect(starknetGetHashMock).toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('verify', () => {
|
|
43
|
+
test('should call EVM verification on EVM address', async () => {
|
|
44
|
+
expect.assertions(1);
|
|
45
|
+
await verify(evmMessage.address, evmMessage.sig, evmMessage.data);
|
|
46
|
+
|
|
47
|
+
expect(evmVerificationMock).toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('should call Starknet verification on Starknet payload', async () => {
|
|
51
|
+
expect.assertions(1);
|
|
52
|
+
await verify(
|
|
53
|
+
starknetMessage.address,
|
|
54
|
+
starknetMessage.sig,
|
|
55
|
+
starknetMessage.data
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(starknetVerificationMock).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should call Starknet verification on Starknet payload and EVM message', async () => {
|
|
62
|
+
expect.assertions(1);
|
|
63
|
+
await verify(
|
|
64
|
+
evmMessage.address,
|
|
65
|
+
starknetMessage.sig,
|
|
66
|
+
starknetMessage.data
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(starknetVerificationMock).toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('should throw an error on empty address', () => {
|
|
73
|
+
expect(
|
|
74
|
+
verify('', evmMessage.sig, evmMessage.data)
|
|
75
|
+
).rejects.toThrowError();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('should throw an error on invalid address', () => {
|
|
79
|
+
expect(
|
|
80
|
+
verify('hello-world', evmMessage.sig, evmMessage.data)
|
|
81
|
+
).rejects.toThrowError();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as starknet from './starknet';
|
|
2
|
+
import * as evm from './evm';
|
|
3
|
+
import { isEvmAddress, isStarknetAddress } from '../utils';
|
|
4
|
+
import type { StarkNetType } from 'starknet';
|
|
5
|
+
import type { TypedDataField } from '@ethersproject/abstract-signer';
|
|
6
|
+
import type { ProviderOptions } from '../utils/provider';
|
|
7
|
+
|
|
8
|
+
export type SignaturePayload = {
|
|
9
|
+
domain: Record<string, string>;
|
|
10
|
+
types: Record<string, StarkNetType[] | TypedDataField[]>;
|
|
11
|
+
primaryType?: string;
|
|
12
|
+
message: Record<string, any>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function getHash(data: SignaturePayload, address?: string): string {
|
|
16
|
+
const networkType = starknet.isStarknetMessage(data) ? starknet : evm;
|
|
17
|
+
|
|
18
|
+
return networkType.getHash(data, address as string);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function verify(
|
|
22
|
+
address: string,
|
|
23
|
+
sig: string | string[],
|
|
24
|
+
data: SignaturePayload,
|
|
25
|
+
network = '1',
|
|
26
|
+
options: ProviderOptions = {}
|
|
27
|
+
): Promise<boolean> {
|
|
28
|
+
if (!isStarknetAddress(address) && !isEvmAddress(address)) {
|
|
29
|
+
throw new Error('Invalid address');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const networkType = starknet.isStarknetMessage(data) ? starknet : evm;
|
|
33
|
+
|
|
34
|
+
return await networkType.default(
|
|
35
|
+
address,
|
|
36
|
+
sig as any,
|
|
37
|
+
data,
|
|
38
|
+
network as any,
|
|
39
|
+
options
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { test, expect, describe } from 'vitest';
|
|
2
|
+
import starknetMessage from '../../test/fixtures/starknet/message-alias.json';
|
|
3
|
+
import verify, { getHash } from './starknet';
|
|
4
|
+
|
|
5
|
+
describe('verify/starknet', () => {
|
|
6
|
+
describe('getHash()', () => {
|
|
7
|
+
test('should return a hash string', () => {
|
|
8
|
+
const hash = getHash(starknetMessage.data, starknetMessage.address);
|
|
9
|
+
expect(hash).toBe(
|
|
10
|
+
'0x2513fb25c2147469748ec099bf849a21dde6c9b7cfb6b86af431b5e898309bd'
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('should return a different by address', () => {
|
|
15
|
+
const hash = getHash(starknetMessage.data, '0x0');
|
|
16
|
+
expect(hash).not.toBe(
|
|
17
|
+
'0x2513fb25c2147469748ec099bf849a21dde6c9b7cfb6b86af431b5e898309bd'
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('verify()', () => {
|
|
23
|
+
test('should return true if the signature is valid', () => {
|
|
24
|
+
expect(
|
|
25
|
+
verify(
|
|
26
|
+
starknetMessage.address,
|
|
27
|
+
starknetMessage.sig,
|
|
28
|
+
starknetMessage.data,
|
|
29
|
+
'SN_SEPOLIA'
|
|
30
|
+
)
|
|
31
|
+
).resolves.toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('should throw an error if message is on wrong network', () => {
|
|
35
|
+
expect(
|
|
36
|
+
verify(
|
|
37
|
+
starknetMessage.address,
|
|
38
|
+
starknetMessage.sig,
|
|
39
|
+
starknetMessage.data,
|
|
40
|
+
'SN_MAIN'
|
|
41
|
+
)
|
|
42
|
+
).rejects.toThrowError();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should throw an error if the signature is invalid', () => {
|
|
46
|
+
expect(
|
|
47
|
+
verify(
|
|
48
|
+
'0x7667469b8e93faa642573078b6bf8c790d3a6184b2a1bb39c5c923a732862e1',
|
|
49
|
+
starknetMessage.sig,
|
|
50
|
+
starknetMessage.data
|
|
51
|
+
)
|
|
52
|
+
).rejects.toThrow();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Contract, RpcProvider, typedData } from 'starknet';
|
|
2
|
+
import type { SignaturePayload } from '.';
|
|
3
|
+
import type { ProviderOptions } from '../utils/provider';
|
|
4
|
+
|
|
5
|
+
export type NetworkType = 'SN_MAIN' | 'SN_SEPOLIA';
|
|
6
|
+
|
|
7
|
+
const RPC_URLS: Record<NetworkType, string> = {
|
|
8
|
+
SN_MAIN: 'https://starknet-mainnet.public.blastapi.io',
|
|
9
|
+
SN_SEPOLIA: 'https://starknet-sepolia.public.blastapi.io'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const ABI = [
|
|
13
|
+
{
|
|
14
|
+
name: 'argent::account::interface::IDeprecatedArgentAccount',
|
|
15
|
+
type: 'interface',
|
|
16
|
+
items: [
|
|
17
|
+
{
|
|
18
|
+
name: 'isValidSignature',
|
|
19
|
+
type: 'function',
|
|
20
|
+
inputs: [
|
|
21
|
+
{
|
|
22
|
+
name: 'hash',
|
|
23
|
+
type: 'core::felt252'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'signatures',
|
|
27
|
+
type: 'core::array::Array::<core::felt252>'
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
outputs: [
|
|
31
|
+
{
|
|
32
|
+
type: 'core::felt252'
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
state_mutability: 'view'
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function getProvider(network: NetworkType, options: ProviderOptions) {
|
|
42
|
+
if (!RPC_URLS[network]) throw new Error('Invalid network');
|
|
43
|
+
|
|
44
|
+
return new RpcProvider({
|
|
45
|
+
nodeUrl: options?.broviderUrl ?? RPC_URLS[network]
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isStarknetMessage(data: SignaturePayload): boolean {
|
|
50
|
+
return !!data.primaryType && !!data.types.StarkNetDomain;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getHash(data: SignaturePayload, address: string): string {
|
|
54
|
+
const { domain, types, primaryType, message } =
|
|
55
|
+
data as Required<SignaturePayload>;
|
|
56
|
+
|
|
57
|
+
return typedData.getMessageHash(
|
|
58
|
+
{ types, primaryType, domain, message },
|
|
59
|
+
address
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default async function verify(
|
|
64
|
+
address: string,
|
|
65
|
+
sig: string[],
|
|
66
|
+
data: SignaturePayload,
|
|
67
|
+
network: NetworkType = 'SN_MAIN',
|
|
68
|
+
options: ProviderOptions = {}
|
|
69
|
+
): Promise<boolean> {
|
|
70
|
+
const contractAccount = new Contract(
|
|
71
|
+
ABI,
|
|
72
|
+
address,
|
|
73
|
+
getProvider(network, options)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
await contractAccount.isValidSignature(getHash(data, address), [
|
|
77
|
+
sig[0],
|
|
78
|
+
sig[1]
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
return true;
|
|
82
|
+
}
|
package/src/sign/eip1271.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { arrayify } from '@ethersproject/bytes';
|
|
2
|
-
import { StaticJsonRpcProvider } from '@ethersproject/providers';
|
|
3
|
-
import getProvider from '../utils/provider';
|
|
4
|
-
import { call } from '../utils';
|
|
5
|
-
|
|
6
|
-
export async function verifyDefault(
|
|
7
|
-
address: string,
|
|
8
|
-
sig: string,
|
|
9
|
-
hash: string,
|
|
10
|
-
provider: StaticJsonRpcProvider
|
|
11
|
-
) {
|
|
12
|
-
let returnValue;
|
|
13
|
-
const magicValue = '0x1626ba7e';
|
|
14
|
-
const abi =
|
|
15
|
-
'function isValidSignature(bytes32 _hash, bytes memory _signature) public view returns (bytes4 magicValue)';
|
|
16
|
-
try {
|
|
17
|
-
returnValue = await call(
|
|
18
|
-
provider,
|
|
19
|
-
[abi],
|
|
20
|
-
[address, 'isValidSignature', [arrayify(hash), sig]]
|
|
21
|
-
);
|
|
22
|
-
} catch (e) {
|
|
23
|
-
// @ts-ignore
|
|
24
|
-
if (e.message.startsWith('missing revert data in call exception')) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
throw e;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return returnValue.toLowerCase() === magicValue.toLowerCase();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function verifyOldVersion(
|
|
34
|
-
address: string,
|
|
35
|
-
sig: string,
|
|
36
|
-
hash: string,
|
|
37
|
-
provider: StaticJsonRpcProvider
|
|
38
|
-
) {
|
|
39
|
-
const magicValue = '0x20c13b0b';
|
|
40
|
-
const abi =
|
|
41
|
-
'function isValidSignature(bytes _hash, bytes memory _signature) public view returns (bytes4 magicValue)';
|
|
42
|
-
|
|
43
|
-
const returnValue = await call(
|
|
44
|
-
provider,
|
|
45
|
-
[abi],
|
|
46
|
-
[address, 'isValidSignature', [arrayify(hash), sig]]
|
|
47
|
-
);
|
|
48
|
-
return returnValue.toLowerCase() === magicValue.toLowerCase();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function verify(address, sig, hash, network = '1', options = {}) {
|
|
52
|
-
const provider = getProvider(network, options);
|
|
53
|
-
if (await verifyDefault(address, sig, hash, provider)) return true;
|
|
54
|
-
return await verifyOldVersion(address, sig, hash, provider);
|
|
55
|
-
}
|