cruzo-web3 0.1.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.
- package/README.md +242 -0
- package/lib/components/icons/copy-icon.component.ts +14 -0
- package/lib/components/web3-signer/web3-signer.component.module.css +106 -0
- package/lib/components/web3-signer/web3-signer.component.ts +320 -0
- package/lib/components/web3-signing/web3-signing.component.module.css +36 -0
- package/lib/components/web3-signing/web3-signing.component.ts +239 -0
- package/lib/components/web3-wallet-picker/web3-wallet-picker.bucket.ts +22 -0
- package/lib/components/web3-wallet-picker/web3-wallet-picker.component.module.css +62 -0
- package/lib/components/web3-wallet-picker/web3-wallet-picker.component.ts +171 -0
- package/lib/crypto/account-signature.ts +175 -0
- package/lib/crypto/decode-bytes.ts +93 -0
- package/lib/crypto/ecdsa-signature.ts +45 -0
- package/lib/crypto/keccak256.ts +117 -0
- package/lib/crypto/secp256k1-verify.ts +232 -0
- package/lib/crypto/sha256.ts +8 -0
- package/lib/crypto/verify-signature.ts +54 -0
- package/lib/env.d.ts +4 -0
- package/lib/errors/web3-error.ts +49 -0
- package/lib/index.ts +20 -0
- package/lib/providers/eip1193.provider.ts +152 -0
- package/lib/providers/injected.ts +71 -0
- package/lib/providers/message-bytes.ts +13 -0
- package/lib/providers/solana.provider.ts +116 -0
- package/lib/providers/tonconnect.provider.ts +161 -0
- package/lib/providers/tron.provider.ts +142 -0
- package/lib/providers/wallet-transport.ts +1 -0
- package/lib/providers/wallet.ts +92 -0
- package/lib/providers/walletconnect-ethereum.provider.ts +49 -0
- package/lib/pub-key.ts +144 -0
- package/lib/signing-url.ts +334 -0
- package/lib/types/signer-state.ts +10 -0
- package/lib/types/web3-types.ts +27 -0
- package/lib/utils/format-pub-key.ts +18 -0
- package/lib/web3-wallet.ts +31 -0
- package/lib/web3.service.ts +419 -0
- package/package.json +58 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { PubKey } from "../types/web3-types";
|
|
2
|
+
import { decodeBase58, decodeHex } from "./decode-bytes";
|
|
3
|
+
import { normalizeSecp256k1Signature } from "./ecdsa-signature";
|
|
4
|
+
import { hashEvmPersonalMessage, keccak256 } from "./keccak256";
|
|
5
|
+
import { recoverSecp256k1PublicKey } from "./secp256k1-verify";
|
|
6
|
+
import { sha256 } from "./sha256";
|
|
7
|
+
|
|
8
|
+
function bytesToHex(bytes: Uint8Array) {
|
|
9
|
+
let hex = "";
|
|
10
|
+
|
|
11
|
+
for (const byte of bytes) {
|
|
12
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return hex;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeEvmAddress(value: string) {
|
|
19
|
+
const hex = value.startsWith("0x") || value.startsWith("0X") ? value.slice(2) : value;
|
|
20
|
+
|
|
21
|
+
if (!/^[0-9a-fA-F]{40}$/.test(hex)) return null;
|
|
22
|
+
|
|
23
|
+
return `0x${hex.toLowerCase()}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isEvmAddressPubKey(pubKey: PubKey) {
|
|
27
|
+
if (pubKey.type !== "secp256k1" || pubKey.encoding !== "hex") return false;
|
|
28
|
+
|
|
29
|
+
return normalizeEvmAddress(pubKey.value) != null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isTronAddressPubKey(pubKey: PubKey) {
|
|
33
|
+
if (pubKey.type !== "secp256k1" || pubKey.encoding !== "base58") return false;
|
|
34
|
+
|
|
35
|
+
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(pubKey.value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isSecp256k1PublicKeyPubKey(pubKey: PubKey) {
|
|
39
|
+
if (pubKey.type !== "secp256k1" || pubKey.encoding !== "hex") return false;
|
|
40
|
+
|
|
41
|
+
const hex = pubKey.value.startsWith("0x") ? pubKey.value.slice(2) : pubKey.value;
|
|
42
|
+
|
|
43
|
+
return hex.length === 66 || hex.length === 130;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseSecp256k1Signature(signature: string, encoding: PubKey["encoding"]) {
|
|
47
|
+
const bytes = encoding === "hex"
|
|
48
|
+
? decodeHex(signature)
|
|
49
|
+
: encoding === "base64"
|
|
50
|
+
? null
|
|
51
|
+
: decodeBase58(signature);
|
|
52
|
+
|
|
53
|
+
if (!bytes?.length) return null;
|
|
54
|
+
|
|
55
|
+
let recoveryId: number | undefined;
|
|
56
|
+
|
|
57
|
+
if (bytes.length === 65) {
|
|
58
|
+
recoveryId = bytes[64];
|
|
59
|
+
|
|
60
|
+
if (recoveryId >= 27) recoveryId -= 27;
|
|
61
|
+
if (recoveryId > 1) recoveryId = undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const normalized = normalizeSecp256k1Signature(bytes);
|
|
65
|
+
|
|
66
|
+
if (!normalized) return null;
|
|
67
|
+
|
|
68
|
+
return { signature: normalized, recoveryId };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function evmAddressFromUncompressedPublicKey(publicKey: Uint8Array) {
|
|
72
|
+
if (publicKey.length !== 65 || publicKey[0] !== 4) return null;
|
|
73
|
+
|
|
74
|
+
const hash = keccak256(publicKey.slice(1));
|
|
75
|
+
|
|
76
|
+
return `0x${bytesToHex(hash.slice(12))}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function hashTronSignMessageV2(content: Uint8Array) {
|
|
80
|
+
const messageBytes = content;
|
|
81
|
+
const messageDigest = messageBytes.length === 32
|
|
82
|
+
? messageBytes
|
|
83
|
+
: await sha256(messageBytes);
|
|
84
|
+
|
|
85
|
+
const prefix = new TextEncoder().encode("\x19TRON Signed Message:\n32");
|
|
86
|
+
const payload = new Uint8Array(prefix.length + messageDigest.length);
|
|
87
|
+
|
|
88
|
+
payload.set(prefix, 0);
|
|
89
|
+
payload.set(messageDigest, prefix.length);
|
|
90
|
+
|
|
91
|
+
return keccak256(payload);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function decodeTronAddressPayload(base58: string) {
|
|
95
|
+
const decoded = decodeBase58(base58);
|
|
96
|
+
|
|
97
|
+
if (!decoded || decoded.length !== 25 || decoded[0] !== 0x41) return null;
|
|
98
|
+
|
|
99
|
+
return decoded.slice(1, 21);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function verifyTronAddressSignature(
|
|
103
|
+
content: Uint8Array,
|
|
104
|
+
signature: string,
|
|
105
|
+
expectedAddress: string,
|
|
106
|
+
) {
|
|
107
|
+
const parsed = parseSecp256k1Signature(signature, "hex");
|
|
108
|
+
|
|
109
|
+
if (!parsed) return false;
|
|
110
|
+
|
|
111
|
+
const expected = decodeTronAddressPayload(expectedAddress);
|
|
112
|
+
|
|
113
|
+
if (!expected) return false;
|
|
114
|
+
|
|
115
|
+
const messageHash = await hashTronSignMessageV2(content);
|
|
116
|
+
const publicKey = recoverSecp256k1PublicKey(messageHash, parsed.signature, parsed.recoveryId);
|
|
117
|
+
|
|
118
|
+
if (!publicKey) return false;
|
|
119
|
+
|
|
120
|
+
const recoveredPayload = keccak256(publicKey.slice(1)).slice(12);
|
|
121
|
+
|
|
122
|
+
if (recoveredPayload.length !== expected.length) return false;
|
|
123
|
+
|
|
124
|
+
for (let index = 0; index < expected.length; index++) {
|
|
125
|
+
if (recoveredPayload[index] !== expected[index]) return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function verifyEvmAddressSignature(
|
|
132
|
+
content: Uint8Array,
|
|
133
|
+
signature: string,
|
|
134
|
+
expectedAddress: string,
|
|
135
|
+
evmPersonalSign: boolean,
|
|
136
|
+
) {
|
|
137
|
+
const parsed = parseSecp256k1Signature(signature, "hex");
|
|
138
|
+
|
|
139
|
+
if (!parsed) return false;
|
|
140
|
+
|
|
141
|
+
const normalizedExpected = normalizeEvmAddress(expectedAddress);
|
|
142
|
+
|
|
143
|
+
if (!normalizedExpected) return false;
|
|
144
|
+
|
|
145
|
+
const messageHash = evmPersonalSign ? hashEvmPersonalMessage(content) : content;
|
|
146
|
+
const publicKey = recoverSecp256k1PublicKey(messageHash, parsed.signature, parsed.recoveryId);
|
|
147
|
+
|
|
148
|
+
if (!publicKey) return false;
|
|
149
|
+
|
|
150
|
+
const recoveredAddress = evmAddressFromUncompressedPublicKey(publicKey);
|
|
151
|
+
|
|
152
|
+
return recoveredAddress === normalizedExpected;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function verifyAccountSignedContent(
|
|
156
|
+
content: Uint8Array,
|
|
157
|
+
signature: string,
|
|
158
|
+
pubKey: PubKey,
|
|
159
|
+
options?: { evmPersonalSign?: boolean },
|
|
160
|
+
) {
|
|
161
|
+
if (isEvmAddressPubKey(pubKey)) {
|
|
162
|
+
return verifyEvmAddressSignature(
|
|
163
|
+
content,
|
|
164
|
+
signature,
|
|
165
|
+
pubKey.value,
|
|
166
|
+
options?.evmPersonalSign ?? true,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (isTronAddressPubKey(pubKey)) {
|
|
171
|
+
return verifyTronAddressSignature(content, signature, pubKey.value);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { PubKeyEncoding } from "../types/web3-types";
|
|
2
|
+
|
|
3
|
+
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
4
|
+
|
|
5
|
+
export function decodeHex(value: string): Uint8Array | null {
|
|
6
|
+
const hex = value.startsWith("0x") || value.startsWith("0X") ? value.slice(2) : value;
|
|
7
|
+
|
|
8
|
+
if (!hex.length || hex.length % 2 || !/^[0-9a-fA-F]+$/.test(hex)) return null;
|
|
9
|
+
|
|
10
|
+
const out = new Uint8Array(hex.length / 2);
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < out.length; i++) {
|
|
13
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function decodeBase64(value: string): Uint8Array | null {
|
|
20
|
+
if (!value.length) return null;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const binary = atob(value);
|
|
24
|
+
const out = new Uint8Array(binary.length);
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < binary.length; i++) {
|
|
27
|
+
out[i] = binary.charCodeAt(i);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return out;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function decodeBase58(value: string): Uint8Array | null {
|
|
37
|
+
if (!value.length) return null;
|
|
38
|
+
|
|
39
|
+
const bytes: number[] = [0];
|
|
40
|
+
|
|
41
|
+
for (const char of value) {
|
|
42
|
+
const digit = BASE58_ALPHABET.indexOf(char);
|
|
43
|
+
|
|
44
|
+
if (digit === -1) return null;
|
|
45
|
+
|
|
46
|
+
let carry = digit;
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
49
|
+
carry += bytes[i] * 58;
|
|
50
|
+
bytes[i] = carry & 0xff;
|
|
51
|
+
carry >>= 8;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
while (carry > 0) {
|
|
55
|
+
bytes.push(carry & 0xff);
|
|
56
|
+
carry >>= 8;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let leadingZeros = 0;
|
|
61
|
+
|
|
62
|
+
for (const char of value) {
|
|
63
|
+
if (char === "1") leadingZeros++;
|
|
64
|
+
else break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const decoded = bytes.reverse();
|
|
68
|
+
|
|
69
|
+
if (!leadingZeros) return new Uint8Array(decoded);
|
|
70
|
+
|
|
71
|
+
const out = new Uint8Array(leadingZeros + decoded.length);
|
|
72
|
+
out.set(decoded, leadingZeros);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function decodeEncodedBytes(value: string, encoding: PubKeyEncoding): Uint8Array | null {
|
|
77
|
+
switch (encoding) {
|
|
78
|
+
case "hex":
|
|
79
|
+
return decodeHex(value);
|
|
80
|
+
case "base64":
|
|
81
|
+
return decodeBase64(value);
|
|
82
|
+
case "base58":
|
|
83
|
+
return decodeBase58(value);
|
|
84
|
+
default:
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function toBytes(content: string | Uint8Array): Uint8Array {
|
|
90
|
+
if (content instanceof Uint8Array) return content;
|
|
91
|
+
|
|
92
|
+
return new TextEncoder().encode(content);
|
|
93
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function parseEcdsaDerSignature(signature: Uint8Array): Uint8Array | null {
|
|
2
|
+
if (signature.length === 64) return signature;
|
|
3
|
+
if (signature.length === 65) return signature.slice(0, 64);
|
|
4
|
+
if (signature[0] !== 0x30) return null;
|
|
5
|
+
|
|
6
|
+
let offset = 2;
|
|
7
|
+
|
|
8
|
+
if (signature[1] & 0x80) {
|
|
9
|
+
offset = 2 + (signature[1] & 0x7f);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const readInt = () => {
|
|
13
|
+
if (signature[offset] !== 0x02) return null;
|
|
14
|
+
|
|
15
|
+
const len = signature[offset + 1];
|
|
16
|
+
let bytes = signature.slice(offset + 2, offset + 2 + len);
|
|
17
|
+
|
|
18
|
+
while (bytes.length > 32 && bytes[0] === 0) {
|
|
19
|
+
bytes = bytes.slice(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const out = new Uint8Array(32);
|
|
23
|
+
out.set(bytes, 32 - bytes.length);
|
|
24
|
+
offset = offset + 2 + len;
|
|
25
|
+
return out;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const r = readInt();
|
|
29
|
+
const s = readInt();
|
|
30
|
+
|
|
31
|
+
if (!r || !s) return null;
|
|
32
|
+
|
|
33
|
+
const out = new Uint8Array(64);
|
|
34
|
+
out.set(r, 0);
|
|
35
|
+
out.set(s, 32);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeSecp256k1Signature(signature: Uint8Array): Uint8Array | null {
|
|
40
|
+
if (signature.length === 64 || signature.length === 65) {
|
|
41
|
+
return signature.length === 65 ? signature.slice(0, 64) : signature;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return parseEcdsaDerSignature(signature);
|
|
45
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Keccak-256 sponge (Ethereum). Adapted from @noble/hashes (MIT).
|
|
2
|
+
|
|
3
|
+
const U32_MASK = 0xffffffff;
|
|
4
|
+
|
|
5
|
+
const rotlSH = (h: number, l: number, s: number) => ((h << s) | (l >>> (32 - s))) | 0;
|
|
6
|
+
const rotlSL = (h: number, l: number, s: number) => ((l << s) | (h >>> (32 - s))) >>> 0;
|
|
7
|
+
const rotlBH = (h: number, l: number, s: number) => ((l << (s - 32)) | (h >>> (64 - s))) | 0;
|
|
8
|
+
const rotlBL = (h: number, l: number, s: number) => ((h << (s - 32)) | (l >>> (64 - s))) >>> 0;
|
|
9
|
+
const rotlH = (h: number, l: number, s: number) => (s > 32 ? rotlBH(h, l, s) : rotlSH(h, l, s));
|
|
10
|
+
const rotlL = (h: number, l: number, s: number) => (s > 32 ? rotlBL(h, l, s) : rotlSL(h, l, s));
|
|
11
|
+
|
|
12
|
+
const SHA3_PI: number[] = [];
|
|
13
|
+
const SHA3_ROTL: number[] = [];
|
|
14
|
+
const SHA3_IOTA_H = new Uint32Array(24);
|
|
15
|
+
const SHA3_IOTA_L = new Uint32Array(24);
|
|
16
|
+
|
|
17
|
+
for (let round = 0, r = 1n, x = 1, y = 0; round < 24; round++) {
|
|
18
|
+
[x, y] = [y, (2 * x + 3 * y) % 5];
|
|
19
|
+
SHA3_PI.push(2 * (5 * y + x));
|
|
20
|
+
SHA3_ROTL.push((((round + 1) * (round + 2)) / 2) % 64);
|
|
21
|
+
|
|
22
|
+
let t = 0n;
|
|
23
|
+
|
|
24
|
+
for (let j = 0; j < 7; j++) {
|
|
25
|
+
r = ((r << 1n) ^ ((r >> 7n) * 0x71n)) % 256n;
|
|
26
|
+
if (r & 2n) t ^= 1n << ((1n << BigInt(j)) - 1n);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
SHA3_IOTA_H[round] = Number(t & BigInt(U32_MASK));
|
|
30
|
+
SHA3_IOTA_L[round] = Number((t >> 32n) & BigInt(U32_MASK));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function keccakP(state32: Uint32Array) {
|
|
34
|
+
const bc = new Uint32Array(10);
|
|
35
|
+
|
|
36
|
+
for (let round = 0; round < 24; round++) {
|
|
37
|
+
for (let x = 0; x < 10; x++) {
|
|
38
|
+
bc[x] = state32[x] ^ state32[x + 10] ^ state32[x + 20] ^ state32[x + 30] ^ state32[x + 40];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (let x = 0; x < 10; x += 2) {
|
|
42
|
+
const idx1 = (x + 8) % 10;
|
|
43
|
+
const idx0 = (x + 2) % 10;
|
|
44
|
+
const b0 = bc[idx0];
|
|
45
|
+
const b1 = bc[idx0 + 1];
|
|
46
|
+
const th = rotlH(b0, b1, 1) ^ bc[idx1];
|
|
47
|
+
const tl = rotlL(b0, b1, 1) ^ bc[idx1 + 1];
|
|
48
|
+
|
|
49
|
+
for (let y = 0; y < 50; y += 10) {
|
|
50
|
+
state32[x + y] ^= th;
|
|
51
|
+
state32[x + y + 1] ^= tl;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let curH = state32[2];
|
|
56
|
+
let curL = state32[3];
|
|
57
|
+
|
|
58
|
+
for (let t = 0; t < 24; t++) {
|
|
59
|
+
const shift = SHA3_ROTL[t];
|
|
60
|
+
const th = rotlH(curH, curL, shift);
|
|
61
|
+
const tl = rotlL(curH, curL, shift);
|
|
62
|
+
const index = SHA3_PI[t];
|
|
63
|
+
curH = state32[index];
|
|
64
|
+
curL = state32[index + 1];
|
|
65
|
+
state32[index] = th;
|
|
66
|
+
state32[index + 1] = tl;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (let y = 0; y < 50; y += 10) {
|
|
70
|
+
for (let x = 0; x < 10; x++) bc[x] = state32[y + x];
|
|
71
|
+
|
|
72
|
+
for (let x = 0; x < 10; x++) {
|
|
73
|
+
state32[y + x] ^= ~bc[(x + 2) % 10] & bc[(x + 4) % 10];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
state32[0] ^= SHA3_IOTA_H[round];
|
|
78
|
+
state32[1] ^= SHA3_IOTA_L[round];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function keccak256(input: Uint8Array): Uint8Array {
|
|
83
|
+
const blockLen = 136;
|
|
84
|
+
const state = new Uint8Array(200);
|
|
85
|
+
const state32 = new Uint32Array(state.buffer, state.byteOffset, state.length / 4);
|
|
86
|
+
let pos = 0;
|
|
87
|
+
|
|
88
|
+
for (let offset = 0; offset < input.length; ) {
|
|
89
|
+
const take = Math.min(blockLen - pos, input.length - offset);
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < take; i++) {
|
|
92
|
+
state[pos++] ^= input[offset++];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (pos === blockLen) {
|
|
96
|
+
keccakP(state32);
|
|
97
|
+
pos = 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
state[pos] ^= 0x01;
|
|
102
|
+
state[blockLen - 1] ^= 0x80;
|
|
103
|
+
keccakP(state32);
|
|
104
|
+
|
|
105
|
+
return state.slice(0, 32);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function hashEvmPersonalMessage(message: Uint8Array): Uint8Array {
|
|
109
|
+
const prefix = `\x19Ethereum Signed Message:\n${message.length}`;
|
|
110
|
+
const prefixBytes = new TextEncoder().encode(prefix);
|
|
111
|
+
const payload = new Uint8Array(prefixBytes.length + message.length);
|
|
112
|
+
|
|
113
|
+
payload.set(prefixBytes, 0);
|
|
114
|
+
payload.set(message, prefixBytes.length);
|
|
115
|
+
|
|
116
|
+
return keccak256(payload);
|
|
117
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { normalizeSecp256k1Signature } from "./ecdsa-signature";
|
|
2
|
+
|
|
3
|
+
const P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn;
|
|
4
|
+
const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
5
|
+
const GX = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n;
|
|
6
|
+
const GY = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n;
|
|
7
|
+
|
|
8
|
+
function mod(a: bigint, m = P) {
|
|
9
|
+
const result = a % m;
|
|
10
|
+
return result >= 0n ? result : result + m;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function invert(value: bigint, m = P) {
|
|
14
|
+
const n = mod(value, m);
|
|
15
|
+
|
|
16
|
+
if (n === 0n) throw new Error("invert(0)");
|
|
17
|
+
|
|
18
|
+
return pow2(n, m - 2n, m);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pow2(x: bigint, power: bigint, m = P) {
|
|
22
|
+
let result = 1n;
|
|
23
|
+
let base = mod(x, m);
|
|
24
|
+
let exp = power;
|
|
25
|
+
|
|
26
|
+
while (exp > 0n) {
|
|
27
|
+
if (exp & 1n) result = mod(result * base, m);
|
|
28
|
+
base = mod(base * base, m);
|
|
29
|
+
exp >>= 1n;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sqrtMod(y: bigint) {
|
|
36
|
+
return pow2(y, (P + 1n) / 4n);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type Point = { x: bigint; y: bigint } | null;
|
|
40
|
+
|
|
41
|
+
const inf: Point = null;
|
|
42
|
+
|
|
43
|
+
function isValidPoint(point: Point) {
|
|
44
|
+
if (!point) return true;
|
|
45
|
+
|
|
46
|
+
const { x, y } = point;
|
|
47
|
+
|
|
48
|
+
return mod(y * y) === mod(x * x * x + 7n);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizePublicKey(pubKey: Uint8Array): Point | null {
|
|
52
|
+
if (pubKey.length === 65 && pubKey[0] === 4) {
|
|
53
|
+
const x = bytesToBigInt(pubKey.slice(1, 33));
|
|
54
|
+
const y = bytesToBigInt(pubKey.slice(33, 65));
|
|
55
|
+
const point = { x: mod(x), y: mod(y) };
|
|
56
|
+
|
|
57
|
+
return isValidPoint(point) ? point : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (pubKey.length === 33 && (pubKey[0] === 2 || pubKey[0] === 3)) {
|
|
61
|
+
const x = bytesToBigInt(pubKey.slice(1));
|
|
62
|
+
const y2 = mod(x * x * x + 7n);
|
|
63
|
+
let y = sqrtMod(y2);
|
|
64
|
+
|
|
65
|
+
if (mod(y & 1n) !== BigInt(pubKey[0] & 1)) {
|
|
66
|
+
y = mod(-y);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const point = { x, y };
|
|
70
|
+
|
|
71
|
+
return isValidPoint(point) ? point : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function pointAdd(a: Point, b: Point): Point {
|
|
78
|
+
if (!a) return b;
|
|
79
|
+
if (!b) return a;
|
|
80
|
+
|
|
81
|
+
if (a.x === b.x && mod(a.y + b.y) === 0n) return inf;
|
|
82
|
+
|
|
83
|
+
const lambda =
|
|
84
|
+
a.x === b.x && a.y === b.y
|
|
85
|
+
? mod(3n * a.x * a.x * invert(2n * a.y))
|
|
86
|
+
: mod((b.y - a.y) * invert(b.x - a.x));
|
|
87
|
+
|
|
88
|
+
const x = mod(lambda * lambda - a.x - b.x);
|
|
89
|
+
const y = mod(lambda * (a.x - x) - a.y);
|
|
90
|
+
|
|
91
|
+
return { x, y };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function pointDouble(point: Point): Point {
|
|
95
|
+
return pointAdd(point, point);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function pointMultiply(point: Point, scalar: bigint): Point {
|
|
99
|
+
let k = mod(scalar, N);
|
|
100
|
+
let acc: Point = inf;
|
|
101
|
+
let addend: Point = point;
|
|
102
|
+
|
|
103
|
+
while (k > 0n) {
|
|
104
|
+
if (k & 1n) acc = pointAdd(acc, addend);
|
|
105
|
+
addend = pointDouble(addend);
|
|
106
|
+
k >>= 1n;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return acc;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function bytesToBigInt(bytes: Uint8Array) {
|
|
113
|
+
let value = 0n;
|
|
114
|
+
|
|
115
|
+
for (const byte of bytes) {
|
|
116
|
+
value = (value << 8n) + BigInt(byte);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeSignature(signature: Uint8Array) {
|
|
123
|
+
return normalizeSecp256k1Signature(signature);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function bigIntTo32Bytes(value: bigint) {
|
|
127
|
+
const out = new Uint8Array(32);
|
|
128
|
+
let v = mod(value);
|
|
129
|
+
|
|
130
|
+
for (let index = 31; index >= 0; index--) {
|
|
131
|
+
out[index] = Number(v & 0xffn);
|
|
132
|
+
v >>= 8n;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function pointToUncompressedBytes(point: Point) {
|
|
139
|
+
if (!point) return null;
|
|
140
|
+
|
|
141
|
+
const out = new Uint8Array(65);
|
|
142
|
+
out[0] = 4;
|
|
143
|
+
out.set(bigIntTo32Bytes(point.x), 1);
|
|
144
|
+
out.set(bigIntTo32Bytes(point.y), 33);
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function decompressRecoveryPoint(x: bigint, recoveryBit: number): Point | null {
|
|
149
|
+
const y2 = mod(x * x * x + 7n);
|
|
150
|
+
let y = sqrtMod(y2);
|
|
151
|
+
|
|
152
|
+
if (mod(y & 1n) !== BigInt(recoveryBit)) {
|
|
153
|
+
y = mod(-y);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const point = { x: mod(x), y };
|
|
157
|
+
|
|
158
|
+
return isValidPoint(point) ? point : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function recoverSecp256k1PublicKey(
|
|
162
|
+
messageHash: Uint8Array,
|
|
163
|
+
signature: Uint8Array,
|
|
164
|
+
recoveryId?: number,
|
|
165
|
+
) {
|
|
166
|
+
if (messageHash.length !== 32) return null;
|
|
167
|
+
|
|
168
|
+
const normalizedSignature = normalizeSignature(signature);
|
|
169
|
+
|
|
170
|
+
if (!normalizedSignature) return null;
|
|
171
|
+
|
|
172
|
+
const r = bytesToBigInt(normalizedSignature.slice(0, 32));
|
|
173
|
+
const s = bytesToBigInt(normalizedSignature.slice(32, 64));
|
|
174
|
+
|
|
175
|
+
if (r <= 0n || r >= N || s <= 0n || s >= N) return null;
|
|
176
|
+
|
|
177
|
+
const hash = mod(bytesToBigInt(messageHash), N);
|
|
178
|
+
const attempts = recoveryId === undefined ? [0, 1] : [recoveryId];
|
|
179
|
+
const generator = { x: mod(GX), y: mod(GY) };
|
|
180
|
+
|
|
181
|
+
for (const recId of attempts) {
|
|
182
|
+
const x = mod(r + BigInt(recId) * N);
|
|
183
|
+
|
|
184
|
+
if (x >= P) continue;
|
|
185
|
+
|
|
186
|
+
const recoveryPoint = decompressRecoveryPoint(x, recId);
|
|
187
|
+
|
|
188
|
+
if (!recoveryPoint) continue;
|
|
189
|
+
|
|
190
|
+
const rInv = invert(r, N);
|
|
191
|
+
const u1 = mod(N - mod(hash * rInv, N), N);
|
|
192
|
+
const u2 = mod(s * rInv, N);
|
|
193
|
+
const recovered = pointAdd(pointMultiply(generator, u1), pointMultiply(recoveryPoint, u2));
|
|
194
|
+
const bytes = pointToUncompressedBytes(recovered);
|
|
195
|
+
|
|
196
|
+
if (bytes) return bytes;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function verifySecp256k1(
|
|
203
|
+
signature: Uint8Array,
|
|
204
|
+
messageHash: Uint8Array,
|
|
205
|
+
publicKey: Uint8Array
|
|
206
|
+
) {
|
|
207
|
+
if (messageHash.length !== 32) return false;
|
|
208
|
+
|
|
209
|
+
const normalizedSignature = normalizeSignature(signature);
|
|
210
|
+
|
|
211
|
+
if (!normalizedSignature) return false;
|
|
212
|
+
|
|
213
|
+
const r = bytesToBigInt(normalizedSignature.slice(0, 32));
|
|
214
|
+
const s = bytesToBigInt(normalizedSignature.slice(32, 64));
|
|
215
|
+
|
|
216
|
+
if (r <= 0n || r >= N || s <= 0n || s >= N) return false;
|
|
217
|
+
|
|
218
|
+
const point = normalizePublicKey(publicKey);
|
|
219
|
+
|
|
220
|
+
if (!point) return false;
|
|
221
|
+
|
|
222
|
+
const hash = mod(bytesToBigInt(messageHash), N);
|
|
223
|
+
const w = invert(s, N);
|
|
224
|
+
const u1 = mod(hash * w, N);
|
|
225
|
+
const u2 = mod(r * w, N);
|
|
226
|
+
const generator = { x: mod(GX), y: mod(GY) };
|
|
227
|
+
const recovered = pointAdd(pointMultiply(generator, u1), pointMultiply(point, u2));
|
|
228
|
+
|
|
229
|
+
if (!recovered) return false;
|
|
230
|
+
|
|
231
|
+
return mod(recovered.x, N) === r;
|
|
232
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export async function sha256(input: Uint8Array): Promise<Uint8Array> {
|
|
2
|
+
if (!globalThis.crypto?.subtle) {
|
|
3
|
+
throw new Error("SHA-256 requires Web Crypto (crypto.subtle)");
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const hash = await crypto.subtle.digest("SHA-256", input as BufferSource);
|
|
7
|
+
return new Uint8Array(hash);
|
|
8
|
+
}
|