@notabene/verify-proof 1.11.1-next.1 → 1.12.0-next.12

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.
Files changed (85) hide show
  1. package/dist/{bitcoin-3CW4MNAW.cjs → cjs/bitcoin-UIEQV3IM.cjs} +105 -16
  2. package/dist/cjs/bitcoin-UIEQV3IM.cjs.map +1 -0
  3. package/dist/{cardano-DYBYEAAF.cjs → cjs/cardano-QG2S452K.cjs} +5 -22
  4. package/dist/cjs/cardano-QG2S452K.cjs.map +1 -0
  5. package/dist/cjs/concordium-4B33UJJ5.cjs +44 -0
  6. package/dist/cjs/concordium-4B33UJJ5.cjs.map +1 -0
  7. package/dist/cjs/cosmos-JTT3AZCF.cjs +92 -0
  8. package/dist/cjs/cosmos-JTT3AZCF.cjs.map +1 -0
  9. package/dist/{eth-3DX3PXDU.cjs → cjs/eth-BVXX47HH.cjs} +2 -3
  10. package/dist/cjs/eth-BVXX47HH.cjs.map +1 -0
  11. package/dist/{index.cjs → cjs/index.cjs} +10 -12
  12. package/dist/cjs/index.cjs.map +1 -0
  13. package/dist/{solana-PQ5K4NGO.cjs → cjs/solana-4WNBEQSU.cjs} +6 -23
  14. package/dist/cjs/solana-4WNBEQSU.cjs.map +1 -0
  15. package/dist/{tron-F5AARBY4.cjs → cjs/tron-IM2O2VLM.cjs} +2 -3
  16. package/dist/cjs/tron-IM2O2VLM.cjs.map +1 -0
  17. package/dist/{xlm-5GODWWL2.cjs → cjs/xlm-CHT7RN4Z.cjs} +2 -3
  18. package/dist/cjs/xlm-CHT7RN4Z.cjs.map +1 -0
  19. package/dist/cjs/xrpl-HJ2XWSOO.cjs +74 -0
  20. package/dist/cjs/xrpl-HJ2XWSOO.cjs.map +1 -0
  21. package/dist/{bitcoin-QK53ILBF.js → esm/bitcoin-BTZ4TXEK.js} +105 -16
  22. package/dist/esm/bitcoin-BTZ4TXEK.js.map +1 -0
  23. package/dist/{cardano-WE6YXYLW.js → esm/cardano-OMGAPH5N.js} +4 -5
  24. package/dist/esm/cardano-OMGAPH5N.js.map +1 -0
  25. package/dist/esm/concordium-JJ5VEIOA.js +42 -0
  26. package/dist/esm/concordium-JJ5VEIOA.js.map +1 -0
  27. package/dist/esm/cosmos-AVIWAIX7.js +90 -0
  28. package/dist/esm/cosmos-AVIWAIX7.js.map +1 -0
  29. package/dist/{eth-ICLGRJE5.js → esm/eth-GI4Z5V4T.js} +2 -3
  30. package/dist/esm/eth-GI4Z5V4T.js.map +1 -0
  31. package/dist/{index.js → esm/index.js} +10 -12
  32. package/dist/esm/index.js.map +1 -0
  33. package/dist/{solana-4KQFLZUC.js → esm/solana-ZARDAF6O.js} +3 -4
  34. package/dist/esm/solana-ZARDAF6O.js.map +1 -0
  35. package/dist/{tron-OBLPB2LN.js → esm/tron-FI2B6DRS.js} +2 -3
  36. package/dist/esm/tron-FI2B6DRS.js.map +1 -0
  37. package/dist/{xlm-GX2QGFLI.js → esm/xlm-TNRZ5CFC.js} +2 -3
  38. package/dist/esm/xlm-TNRZ5CFC.js.map +1 -0
  39. package/dist/esm/xrpl-52IBJ4EW.js +69 -0
  40. package/dist/esm/xrpl-52IBJ4EW.js.map +1 -0
  41. package/package.json +21 -9
  42. package/src/bitcoin.ts +43 -19
  43. package/src/cardano.ts +2 -2
  44. package/src/cashaddr.ts +157 -0
  45. package/src/concordium.ts +34 -283
  46. package/src/cosmos.ts +44 -6
  47. package/src/index.ts +0 -1
  48. package/src/solana.ts +1 -1
  49. package/src/tests/bitcoin.test.ts +527 -16
  50. package/src/tests/concordium.test.ts +96 -134
  51. package/src/tests/cosmos.test.ts +139 -0
  52. package/src/tests/index.test.ts +7 -7
  53. package/src/tests/xrpl.test.ts +8 -6
  54. package/dist/bitcoin-3CW4MNAW.cjs.map +0 -1
  55. package/dist/bitcoin-QK53ILBF.js.map +0 -1
  56. package/dist/cardano-DYBYEAAF.cjs.map +0 -1
  57. package/dist/cardano-WE6YXYLW.js.map +0 -1
  58. package/dist/chunk-E3V5ATTC.js +0 -38
  59. package/dist/chunk-E3V5ATTC.js.map +0 -1
  60. package/dist/chunk-OAXNH5XR.cjs +0 -42
  61. package/dist/chunk-OAXNH5XR.cjs.map +0 -1
  62. package/dist/concordium-HQC37GCK.cjs +0 -188
  63. package/dist/concordium-HQC37GCK.cjs.map +0 -1
  64. package/dist/concordium-XX4XYLLU.js +0 -186
  65. package/dist/concordium-XX4XYLLU.js.map +0 -1
  66. package/dist/cosmos-64MKE5FJ.cjs +0 -683
  67. package/dist/cosmos-64MKE5FJ.cjs.map +0 -1
  68. package/dist/cosmos-QMH7BK7S.js +0 -681
  69. package/dist/cosmos-QMH7BK7S.js.map +0 -1
  70. package/dist/eth-3DX3PXDU.cjs.map +0 -1
  71. package/dist/eth-ICLGRJE5.js.map +0 -1
  72. package/dist/index.cjs.map +0 -1
  73. package/dist/index.js.map +0 -1
  74. package/dist/solana-4KQFLZUC.js.map +0 -1
  75. package/dist/solana-PQ5K4NGO.cjs.map +0 -1
  76. package/dist/tron-F5AARBY4.cjs.map +0 -1
  77. package/dist/tron-OBLPB2LN.js.map +0 -1
  78. package/dist/xlm-5GODWWL2.cjs.map +0 -1
  79. package/dist/xlm-GX2QGFLI.js.map +0 -1
  80. package/dist/xrpl-7HQLIDAK.cjs +0 -1174
  81. package/dist/xrpl-7HQLIDAK.cjs.map +0 -1
  82. package/dist/xrpl-YCDFXBGQ.js +0 -1169
  83. package/dist/xrpl-YCDFXBGQ.js.map +0 -1
  84. /package/dist/{index.d.cts → cjs/index.d.cts} +0 -0
  85. /package/dist/{index.d.ts → esm/index.d.ts} +0 -0
@@ -0,0 +1,157 @@
1
+ import { bech32 } from "@scure/base";
2
+
3
+ /**
4
+ * CashAddr base32 character set.
5
+ * Each character maps to its 5-bit index value (0–31).
6
+ * Spec: https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md
7
+ */
8
+ const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
9
+
10
+ /**
11
+ * Generator polynomials for the CashAddr BCH checksum.
12
+ * These five constants define the GF(2^32) polynomial used to compute
13
+ * and validate the 40-bit checksum appended to every CashAddr string.
14
+ */
15
+ const GENERATOR = [
16
+ BigInt("0x98f2bc8e61"),
17
+ BigInt("0x79b76d99e2"),
18
+ BigInt("0xf33e5fb3c4"),
19
+ BigInt("0xae2eabe2a8"),
20
+ BigInt("0x1e4f43e470"),
21
+ ];
22
+
23
+ /**
24
+ * Computes the CashAddr BCH checksum over an array of 5-bit values.
25
+ *
26
+ * The input should be the concatenation of:
27
+ * - the prefix expansion (see {@link prefixExpand})
28
+ * - all payload words including the 8 checksum words
29
+ *
30
+ * A valid address produces a result of `0n`; any other value indicates
31
+ * a corrupt or invalid address.
32
+ *
33
+ * @param values - Array of 5-bit integers covering prefix + full payload
34
+ * @returns The 40-bit checksum residue as a bigint (0n = valid)
35
+ */
36
+ function polymod(values: number[]): bigint {
37
+ let c = BigInt(1);
38
+ for (const d of values) {
39
+ // Extract the top 5 bits before shifting
40
+ const c0 = c >> BigInt(35);
41
+ // Shift the accumulator left by 5 bits and XOR in the next data word
42
+ c = ((c & BigInt("0x07ffffffff")) << BigInt(5)) ^ BigInt(d);
43
+ // Apply each generator polynomial for set bits in c0
44
+ for (let i = 0; i < 5; i++) {
45
+ if ((c0 >> BigInt(i)) & BigInt(1)) {
46
+ c ^= GENERATOR[i];
47
+ }
48
+ }
49
+ }
50
+ // Final XOR with 1 so a valid address yields 0
51
+ return c ^ BigInt(1);
52
+ }
53
+
54
+ /**
55
+ * Expands a CashAddr prefix into the form required by the checksum algorithm.
56
+ *
57
+ * Each character of the prefix is reduced to its lower 5 bits, then a zero
58
+ * byte is appended as a separator between the prefix and the payload words.
59
+ *
60
+ * @param prefix - The human-readable prefix (e.g. `"bitcoincash"`)
61
+ * @returns Array of 5-bit integers representing the expanded prefix
62
+ */
63
+ function prefixExpand(prefix: string): number[] {
64
+ const result: number[] = [];
65
+ for (let i = 0; i < prefix.length; i++) {
66
+ // Keep only the lower 5 bits of each character's ASCII code
67
+ result.push(prefix.charCodeAt(i) & 0x1f);
68
+ }
69
+ // Zero separator between prefix and payload
70
+ result.push(0);
71
+ return result;
72
+ }
73
+
74
+ /**
75
+ * Decodes a CashAddr string into its prefix, address type, and raw hash.
76
+ *
77
+ * Supports both prefixed (`bitcoincash:q...`) and prefix-less (`q...`) forms.
78
+ * Validates the BCH checksum and rejects non-P2PKH types or malformed input.
79
+ *
80
+ * Address type values (upper nibble of the version byte):
81
+ * - `0` — P2PKH (pay-to-public-key-hash)
82
+ * - `1` — P2SH (pay-to-script-hash)
83
+ *
84
+ * @param address - The CashAddr string to decode
85
+ * @param defaultPrefix - Prefix to assume when none is present in the address (default: `"bitcoincash"`)
86
+ * @returns An object with:
87
+ * - `prefix` — the decoded human-readable prefix
88
+ * - `type` — the address type (0 = P2PKH, 1 = P2SH, …)
89
+ * - `hash` — the raw public-key or script hash bytes
90
+ * @throws If the address contains invalid characters, fails the checksum, has a mismatched hash length,
91
+ * contains mixed upper/lower case, or if the payload contains invalid 5-to-8-bit padding
92
+ * (thrown by {@link bech32.fromWords} from `@scure/base`)
93
+ */
94
+ export function decodeCashAddr(
95
+ address: string,
96
+ defaultPrefix = "bitcoincash"
97
+ ): { prefix: string; type: number; hash: Uint8Array } {
98
+ let prefix: string;
99
+
100
+ // Split on ":" to separate an explicit prefix from the payload
101
+ const colonIndex = address.indexOf(":");
102
+ if (colonIndex !== -1) {
103
+ prefix = address.slice(0, colonIndex).toLowerCase();
104
+ } else {
105
+ prefix = defaultPrefix;
106
+ }
107
+
108
+ // CashAddr spec forbids mixed upper/lower case
109
+ const rawPayload = colonIndex !== -1 ? address.slice(colonIndex + 1) : address;
110
+ if (/[A-Z]/.test(rawPayload) && /[a-z]/.test(rawPayload)) {
111
+ throw new Error("Invalid CashAddr: mixed case");
112
+ }
113
+ const payload = rawPayload.toLowerCase();
114
+
115
+ // Map each character to its 5-bit index in the CashAddr alphabet
116
+ const words: number[] = [];
117
+ for (const ch of payload) {
118
+ const idx = CHARSET.indexOf(ch);
119
+ if (idx === -1) throw new Error("Invalid CashAddr character: " + ch);
120
+ words.push(idx);
121
+ }
122
+
123
+ // Minimum length: at least 1 data word + 8 checksum words
124
+ if (words.length < 8) throw new Error("CashAddr too short");
125
+
126
+ // Verify the BCH checksum over prefix expansion + all payload words
127
+ const checksumData = [...prefixExpand(prefix), ...words];
128
+ if (polymod(checksumData) !== BigInt(0)) {
129
+ throw new Error("Invalid CashAddr checksum");
130
+ }
131
+
132
+ // Strip the 8 trailing checksum words; the remainder encodes (version_byte || hash)
133
+ const dataWords = words.slice(0, -8);
134
+ // Convert from 5-bit words to 8-bit bytes using bech32.fromWords (equivalent to convertBits(data, 5, 8))
135
+ const payloadBytes = bech32.fromWords(dataWords);
136
+ if (payloadBytes.length === 0) {
137
+ throw new Error("Invalid CashAddr padding");
138
+ }
139
+
140
+ // The first byte is the version byte: upper nibble = type, lower 3 bits = hash size index
141
+ const versionByte = payloadBytes[0];
142
+ const type = (versionByte >> 3) & 0x0f;
143
+ const hashSizeBits = versionByte & 0x07;
144
+
145
+ // Look up the expected hash length in bytes from the size index
146
+ const expectedSizes = [20, 24, 28, 32, 40, 48, 56, 64];
147
+ const expectedSize = expectedSizes[hashSizeBits];
148
+
149
+ const hashBytes = payloadBytes.slice(1);
150
+ if (hashBytes.length !== expectedSize) {
151
+ throw new Error(
152
+ `Hash length mismatch: expected ${expectedSize}, got ${hashBytes.length}`
153
+ );
154
+ }
155
+
156
+ return { prefix, type, hash: Uint8Array.from(hashBytes) };
157
+ }
package/src/concordium.ts CHANGED
@@ -1,309 +1,60 @@
1
1
  import { ProofStatus, SignatureProof } from "@notabene/javascript-sdk";
2
+ import { VerifiablePresentation } from "@concordium/web-sdk";
2
3
 
3
- // Concordium network configurations
4
- interface ConcordiumNetwork {
5
- grpcUrl: string;
6
- walletProxyUrl: string;
7
- }
8
-
9
- const NETWORKS: Record<string, ConcordiumNetwork> = {
10
- testnet: {
11
- grpcUrl: "https://grpc.testnet.concordium.com:20000",
12
- walletProxyUrl: "https://wallet-proxy.testnet.concordium.com"
13
- },
14
- mainnet: {
15
- grpcUrl: "https://grpc.mainnet.concordium.com:20000",
16
- walletProxyUrl: "https://wallet-proxy.mainnet.concordium.software"
17
- }
18
- };
19
-
20
- // Configuration options for verification
21
- interface ConcordiumVerificationOptions {
22
- network?: "testnet" | "mainnet";
23
- timeout?: number; // timeout in milliseconds
24
- retries?: number; // number of retry attempts
25
- testMode?: boolean; // skip network calls for testing
26
- }
27
-
28
- // Signature object type
29
- interface ConcordiumSignature {
30
- [key: string]: string | ConcordiumSignature;
31
- }
32
-
33
- // Default options
34
- const DEFAULT_OPTIONS: Required<ConcordiumVerificationOptions> = {
35
- network: "testnet",
36
- timeout: 50000, // 10 seconds
37
- retries: 3,
38
- testMode: true
39
- };
4
+ const CONCORDIUM_VERIFIER_URL =
5
+ "https://web3id-verifier.mainnet.concordium.software";
40
6
 
41
7
  /**
42
- * Verifies a Concordium signature proof with proper cryptographic validation
8
+ * Verifies a Concordium signature proof using Concordium's hosted verifier service
43
9
  * @param proof The signature proof to verify
44
- * @param options Optional configuration for network and timeouts
45
10
  * @returns Promise resolving to the proof with updated status
46
11
  */
47
12
  export const verifyConcordiumSignature = async (
48
13
  proof: SignatureProof,
49
- options: ConcordiumVerificationOptions = {}
50
14
  ): Promise<SignatureProof> => {
51
- // Merge with default options
52
- const config = { ...DEFAULT_OPTIONS, ...options };
53
-
54
- // Parse and validate address format
55
- const [ns, networkId, address] = proof.address.split(/:/);
15
+ const [ns] = proof.address.split(/:/);
16
+
56
17
  if (ns !== "ccd") {
57
18
  return { ...proof, status: ProofStatus.FAILED };
58
19
  }
59
20
 
60
- // Determine network from address or use config
61
- let network = config.network;
62
- if (networkId) {
63
- // If network ID is specified in address, use it to determine network
64
- network = networkId.includes("testnet") ? "testnet" : "mainnet";
65
- }
66
-
67
21
  try {
68
- // Validate signature format and extract signature data
69
- let signature: ConcordiumSignature;
70
- try {
71
- signature = JSON.parse(proof.proof) as ConcordiumSignature;
72
- } catch {
73
- return { ...proof, status: ProofStatus.FAILED };
74
- }
22
+ const presentationJson = JSON.parse(proof.proof) as VerifiablePresentation;
75
23
 
76
- // Basic signature structure validation
77
- if (!signature || typeof signature !== 'object' || Object.keys(signature).length === 0) {
78
- return { ...proof, status: ProofStatus.FAILED };
24
+ // Validate presentation context matches attestation
25
+ if (presentationJson.presentationContext !== proof.attestation) {
26
+ throw new Error("Presentation context does not match attestation");
79
27
  }
80
28
 
81
- // In test mode, skip network validation but still validate signature structure
82
- if (config.testMode) {
83
- // Perform signature format validation
84
- try {
85
- const signatureHex = convertSignatureToHex(signature);
86
-
87
- // Validate signature format
88
- if (!signatureHex || signatureHex.length < 64 || !/^[0-9a-fA-F]+$/.test(signatureHex)) {
89
- return { ...proof, status: ProofStatus.FAILED };
90
- }
91
-
92
- return { ...proof, status: ProofStatus.VERIFIED };
93
-
94
- } catch {
95
- return { ...proof, status: ProofStatus.FAILED };
96
- }
97
- }
98
-
99
- // Production mode: validate account existence and get account info with retry logic
100
- const accountInfo = await retryWithTimeout(
101
- () => validateAccountAndGetInfo(address, network, config.timeout),
102
- config.retries
103
- );
104
-
105
- if (!accountInfo) {
106
- return { ...proof, status: ProofStatus.FAILED };
29
+ // Validate verifiableCredential exists and is not empty
30
+ if (
31
+ !presentationJson.verifiableCredential ||
32
+ presentationJson.verifiableCredential.length === 0
33
+ ) {
34
+ throw new Error("Verifiable credential is missing or empty");
107
35
  }
108
36
 
109
- // Perform cryptographic signature verification
110
- const isValidSignature = await retryWithTimeout(
111
- () => verifyCryptographicSignature(
112
- signature,
113
- config.timeout,
114
- // proof.attestation,
115
- // address,
116
- // network
117
- ),
118
- config.retries
119
- );
37
+ // Use Concordium's hosted verifier instead of gRPC
38
+ const response = await fetch(`${CONCORDIUM_VERIFIER_URL}/v0/verify`, {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify(presentationJson),
42
+ });
120
43
 
121
- if (isValidSignature) {
122
- return { ...proof, status: ProofStatus.VERIFIED };
123
- } else {
124
- return { ...proof, status: ProofStatus.FAILED };
44
+ if (!response.ok) {
45
+ const errorText = await response.text();
46
+ throw new Error(`Verifier returned ${response.status}: ${errorText}`);
125
47
  }
126
48
 
49
+ // If we get here, the proof is valid
50
+ return {
51
+ ...proof,
52
+ status: ProofStatus.VERIFIED,
53
+ };
127
54
  } catch {
128
- return { ...proof, status: ProofStatus.FAILED };
55
+ return {
56
+ ...proof,
57
+ status: ProofStatus.FAILED,
58
+ };
129
59
  }
130
60
  };
131
-
132
- /**
133
- * Validates that a Concordium account exists and retrieves account information
134
- */
135
- async function validateAccountAndGetInfo(
136
- address: string,
137
- network: "testnet" | "mainnet",
138
- timeout: number
139
- ): Promise<boolean> {
140
- const networkConfig = NETWORKS[network];
141
-
142
- try {
143
- // Check account existence via wallet proxy (faster than gRPC for existence check)
144
- const controller = new AbortController();
145
- const timeoutId = setTimeout(() => controller.abort(), timeout);
146
-
147
- const response = await fetch(
148
- `${networkConfig.walletProxyUrl}/v0/accEncryptionKey/${address}`,
149
- {
150
- method: 'GET',
151
- headers: {
152
- 'Accept': 'application/json',
153
- 'User-Agent': 'verify-proof/1.6.0'
154
- },
155
- signal: controller.signal
156
- }
157
- );
158
-
159
- clearTimeout(timeoutId);
160
- return response.ok;
161
-
162
- } catch (error) {
163
- if (error instanceof Error && error.name === 'AbortError') {
164
- throw new Error(`Account validation timeout after ${timeout}ms`);
165
- }
166
- throw error;
167
- }
168
- }
169
-
170
- /**
171
- * Performs cryptographic verification of the signature using Concordium SDK
172
- * For production use, this would use the actual Concordium SDK verification methods
173
- * Currently implementing a comprehensive validation approach
174
- */
175
- async function verifyCryptographicSignature(
176
- signature: ConcordiumSignature,
177
- timeout: number,
178
- // message?: string, // intentionally left out for now. Will enable for mainnet.
179
- // address?: string,
180
- // network?: "testnet" | "mainnet"
181
- ): Promise<boolean> {
182
- try {
183
- // Convert signature format for verification
184
- const signatureHex = convertSignatureToHex(signature);
185
-
186
- // For production, implement proper signature verification
187
- // This is a placeholder for the actual SDK verification
188
- // The exact method depends on the available SDK version
189
-
190
- // Validate that we have a proper signature hex string
191
- if (!signatureHex || signatureHex.length < 64) {
192
- return false;
193
- }
194
-
195
- // For now, return true if we have a valid signature structure
196
- // In production, this should call the actual SDK verification method
197
- // Example:
198
- // const client = new ConcordiumGRPCNodeClient(networkConfig.grpcUrl, 20000, credentials.createInsecure(), { timeout });
199
- // const result = await client.verifyAccountSignature(address, message, signatureHex);
200
-
201
- // Placeholder validation - replace with actual SDK call
202
- return signatureHex.length >= 64 && /^[0-9a-fA-F]+$/.test(signatureHex);
203
-
204
- } catch (error) {
205
- // Handle specific error types
206
- if (error instanceof Error) {
207
- if (error.message?.includes("timeout")) {
208
- throw new Error(`Signature verification timeout after ${timeout}ms`);
209
- }
210
-
211
- if (error.message?.includes("UNAVAILABLE")) {
212
- throw new Error("Concordium node unavailable");
213
- }
214
-
215
- if (error.message?.includes("NOT_FOUND")) {
216
- return false;
217
- }
218
- }
219
-
220
- throw error;
221
- }
222
- }
223
-
224
- /**
225
- * Converts the signature object to the format expected by Concordium SDK
226
- */
227
- function convertSignatureToHex(signature: ConcordiumSignature): string {
228
- try {
229
- // Handle different signature formats
230
- if (typeof signature === 'string') {
231
- return signature;
232
- }
233
-
234
- // Handle nested signature object format (common from wallet)
235
- if (signature && typeof signature === 'object') {
236
- // Extract signature from nested structure
237
- const extractSignature = (obj: ConcordiumSignature): string | null => {
238
- if (typeof obj === 'string') {
239
- return obj;
240
- }
241
-
242
- if (obj && typeof obj === 'object') {
243
- for (const key in obj) {
244
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
245
- const value = obj[key];
246
- if (typeof value === 'string') {
247
- return value;
248
- } else if (typeof value === 'object') {
249
- const result = extractSignature(value);
250
- if (result) return result;
251
- }
252
- }
253
- }
254
- }
255
-
256
- return null;
257
- };
258
-
259
- const extractedSig = extractSignature(signature);
260
- if (extractedSig) {
261
- return extractedSig;
262
- }
263
- }
264
-
265
- throw new Error("Unable to extract signature from object");
266
-
267
- } catch (error) {
268
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
269
- throw new Error(`Invalid signature format: ${errorMessage}`);
270
- }
271
- }
272
-
273
- /**
274
- * Utility function to retry operations with exponential backoff
275
- */
276
- async function retryWithTimeout<T>(
277
- operation: () => Promise<T>,
278
- maxRetries: number
279
- ): Promise<T> {
280
- let lastError: Error = new Error("No attempts made");
281
-
282
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
283
- try {
284
- return await operation();
285
- } catch (error) {
286
- const errorInstance = error instanceof Error ? error : new Error(String(error));
287
- lastError = errorInstance;
288
-
289
- // Don't retry on certain types of errors
290
- if (
291
- errorInstance.message?.includes("Invalid signature") ||
292
- errorInstance.message?.includes("Account not found")
293
- ) {
294
- throw errorInstance;
295
- }
296
-
297
- // Don't retry on the last attempt
298
- if (attempt === maxRetries) {
299
- break;
300
- }
301
-
302
- // Exponential backoff: wait 2^attempt * 1000ms
303
- const delay = Math.min(Math.pow(2, attempt) * 1000, 5000);
304
- await new Promise(resolve => setTimeout(resolve, delay));
305
- }
306
- }
307
-
308
- throw lastError;
309
- }
package/src/cosmos.ts CHANGED
@@ -8,6 +8,7 @@ import { StdSignDoc, serializeSignDoc } from "@cosmjs/amino";
8
8
  import { secp256k1 } from "@noble/curves/secp256k1";
9
9
  import { ripemd160 } from "@noble/hashes/legacy";
10
10
  import { sha256 } from "@noble/hashes/sha2";
11
+ import { keccak_256 } from "@noble/hashes/sha3";
11
12
 
12
13
  // Base64 decode
13
14
  function base64ToUint8Array(base64: string): Uint8Array {
@@ -26,12 +27,47 @@ function toBase64Bytes(str: string): string {
26
27
 
27
28
  // Cosmos address derivation (ADR-36)
28
29
  // https://github.com/cosmos/cosmos-sdk/blob/214b11dcbaa129f7b4c0013b2103db9d54b85e9e/docs/architecture/adr-036-arbitrary-signature.md
29
- function deriveCosmosAddress(pubkey: Uint8Array, prefix = "cosmos"): string {
30
+ function deriveCosmosAddress(pubkey: Uint8Array, prefix: string): string {
30
31
  const sha = sha256(pubkey);
31
32
  const rip = ripemd160(sha);
32
33
  return bech32.encode(prefix, bech32.toWords(rip));
33
34
  }
34
35
 
36
+ // Ethermint-style chains (Injective, Evmos, ...) use eth-secp256k1 keys: the account is
37
+ // the Ethereum address of the key, bech32-encoded under the chain prefix.
38
+ function deriveEthAddress(pubkey: Uint8Array, prefix: string): string {
39
+ const uncompressed = secp256k1.ProjectivePoint.fromHex(pubkey)
40
+ .toRawBytes(false)
41
+ .slice(1);
42
+ const eth = keccak_256(uncompressed).slice(-20);
43
+ return bech32.encode(prefix, bech32.toWords(eth));
44
+ }
45
+
46
+ // Same selection Keplr's verifyADR36AminoSignDoc makes via its `algo` argument. The signed
47
+ // address tells us which key algorithm the chain uses, so we do not depend on the
48
+ // pub_key.type string wallets put in the amino signature.
49
+ type CosmosKeyAlgo = "secp256k1" | "ethsecp256k1";
50
+
51
+ function keyAlgoForAddress(
52
+ pubkey: Uint8Array,
53
+ address: string
54
+ ): CosmosKeyAlgo | undefined {
55
+ const prefix = bech32Prefix(address);
56
+ if (deriveCosmosAddress(pubkey, prefix) === address) {
57
+ return "secp256k1";
58
+ }
59
+ if (deriveEthAddress(pubkey, prefix) === address) {
60
+ return "ethsecp256k1";
61
+ }
62
+ return undefined;
63
+ }
64
+
65
+ // Each Cosmos SDK chain has its own human-readable part (cosmos, celestia, axelar, ...),
66
+ // and bech32 separates it from the data with the last "1" in the string.
67
+ function bech32Prefix(address: string): string {
68
+ return address.slice(0, address.lastIndexOf("1"));
69
+ }
70
+
35
71
  export async function verifyCosmosSignature(
36
72
  proof: SignatureProof
37
73
  ): Promise<SignatureProof> {
@@ -47,9 +83,10 @@ export async function verifyCosmosSignature(
47
83
  );
48
84
  const signatureBytes = base64ToUint8Array(proof.proof);
49
85
 
50
- // Step 1: Derive address from pubkey
51
- const derivedAddress = deriveCosmosAddress(pubKeyBytes);
52
- if (derivedAddress !== address) {
86
+ // Step 1: The pubkey must derive to the signed address; which derivation matches
87
+ // also decides the digest used in step 3.
88
+ const algo = keyAlgoForAddress(pubKeyBytes, address);
89
+ if (!algo) {
53
90
  return { ...proof, status: ProofStatus.FAILED };
54
91
  }
55
92
 
@@ -71,9 +108,10 @@ export async function verifyCosmosSignature(
71
108
  memo: "",
72
109
  };
73
110
 
74
- // Step 3: Serialize + hash
111
+ // Step 3: Serialize + hash (ADR-036 digests are keccak256 on eth-secp256k1 chains)
75
112
  const signBytes = await serializeSignDoc(signDoc);
76
- const digest = sha256(signBytes);
113
+ const digest =
114
+ algo === "ethsecp256k1" ? keccak_256(signBytes) : sha256(signBytes);
77
115
 
78
116
  // Step 4: Verify using secp256k1
79
117
  const verified = secp256k1.verify(signatureBytes, digest, pubKeyBytes);
package/src/index.ts CHANGED
@@ -69,7 +69,6 @@ export async function verifyProof(
69
69
  const { verifyPersonalSignTIP191 } = await import("./tron");
70
70
  return verifyPersonalSignTIP191(proof as SignatureProof);
71
71
  }
72
- case ProofTypes.BIP137_XPUB:
73
72
  case ProofTypes.MicroTransfer:
74
73
  }
75
74
  return proof;
package/src/solana.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as nacl from "tweetnacl";
1
+ import nacl from "tweetnacl";
2
2
  import { ProofStatus, SignatureProof, type SIWXInput, type SolanaMetadata } from "@notabene/javascript-sdk";
3
3
  import { base64, base58 } from "@scure/base";
4
4