@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.
- package/dist/{bitcoin-3CW4MNAW.cjs → cjs/bitcoin-UIEQV3IM.cjs} +105 -16
- package/dist/cjs/bitcoin-UIEQV3IM.cjs.map +1 -0
- package/dist/{cardano-DYBYEAAF.cjs → cjs/cardano-QG2S452K.cjs} +5 -22
- package/dist/cjs/cardano-QG2S452K.cjs.map +1 -0
- package/dist/cjs/concordium-4B33UJJ5.cjs +44 -0
- package/dist/cjs/concordium-4B33UJJ5.cjs.map +1 -0
- package/dist/cjs/cosmos-JTT3AZCF.cjs +92 -0
- package/dist/cjs/cosmos-JTT3AZCF.cjs.map +1 -0
- package/dist/{eth-3DX3PXDU.cjs → cjs/eth-BVXX47HH.cjs} +2 -3
- package/dist/cjs/eth-BVXX47HH.cjs.map +1 -0
- package/dist/{index.cjs → cjs/index.cjs} +10 -12
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/{solana-PQ5K4NGO.cjs → cjs/solana-4WNBEQSU.cjs} +6 -23
- package/dist/cjs/solana-4WNBEQSU.cjs.map +1 -0
- package/dist/{tron-F5AARBY4.cjs → cjs/tron-IM2O2VLM.cjs} +2 -3
- package/dist/cjs/tron-IM2O2VLM.cjs.map +1 -0
- package/dist/{xlm-5GODWWL2.cjs → cjs/xlm-CHT7RN4Z.cjs} +2 -3
- package/dist/cjs/xlm-CHT7RN4Z.cjs.map +1 -0
- package/dist/cjs/xrpl-HJ2XWSOO.cjs +74 -0
- package/dist/cjs/xrpl-HJ2XWSOO.cjs.map +1 -0
- package/dist/{bitcoin-QK53ILBF.js → esm/bitcoin-BTZ4TXEK.js} +105 -16
- package/dist/esm/bitcoin-BTZ4TXEK.js.map +1 -0
- package/dist/{cardano-WE6YXYLW.js → esm/cardano-OMGAPH5N.js} +4 -5
- package/dist/esm/cardano-OMGAPH5N.js.map +1 -0
- package/dist/esm/concordium-JJ5VEIOA.js +42 -0
- package/dist/esm/concordium-JJ5VEIOA.js.map +1 -0
- package/dist/esm/cosmos-AVIWAIX7.js +90 -0
- package/dist/esm/cosmos-AVIWAIX7.js.map +1 -0
- package/dist/{eth-ICLGRJE5.js → esm/eth-GI4Z5V4T.js} +2 -3
- package/dist/esm/eth-GI4Z5V4T.js.map +1 -0
- package/dist/{index.js → esm/index.js} +10 -12
- package/dist/esm/index.js.map +1 -0
- package/dist/{solana-4KQFLZUC.js → esm/solana-ZARDAF6O.js} +3 -4
- package/dist/esm/solana-ZARDAF6O.js.map +1 -0
- package/dist/{tron-OBLPB2LN.js → esm/tron-FI2B6DRS.js} +2 -3
- package/dist/esm/tron-FI2B6DRS.js.map +1 -0
- package/dist/{xlm-GX2QGFLI.js → esm/xlm-TNRZ5CFC.js} +2 -3
- package/dist/esm/xlm-TNRZ5CFC.js.map +1 -0
- package/dist/esm/xrpl-52IBJ4EW.js +69 -0
- package/dist/esm/xrpl-52IBJ4EW.js.map +1 -0
- package/package.json +21 -9
- package/src/bitcoin.ts +43 -19
- package/src/cardano.ts +2 -2
- package/src/cashaddr.ts +157 -0
- package/src/concordium.ts +34 -283
- package/src/cosmos.ts +44 -6
- package/src/index.ts +0 -1
- package/src/solana.ts +1 -1
- package/src/tests/bitcoin.test.ts +527 -16
- package/src/tests/concordium.test.ts +96 -134
- package/src/tests/cosmos.test.ts +139 -0
- package/src/tests/index.test.ts +7 -7
- package/src/tests/xrpl.test.ts +8 -6
- package/dist/bitcoin-3CW4MNAW.cjs.map +0 -1
- package/dist/bitcoin-QK53ILBF.js.map +0 -1
- package/dist/cardano-DYBYEAAF.cjs.map +0 -1
- package/dist/cardano-WE6YXYLW.js.map +0 -1
- package/dist/chunk-E3V5ATTC.js +0 -38
- package/dist/chunk-E3V5ATTC.js.map +0 -1
- package/dist/chunk-OAXNH5XR.cjs +0 -42
- package/dist/chunk-OAXNH5XR.cjs.map +0 -1
- package/dist/concordium-HQC37GCK.cjs +0 -188
- package/dist/concordium-HQC37GCK.cjs.map +0 -1
- package/dist/concordium-XX4XYLLU.js +0 -186
- package/dist/concordium-XX4XYLLU.js.map +0 -1
- package/dist/cosmos-64MKE5FJ.cjs +0 -683
- package/dist/cosmos-64MKE5FJ.cjs.map +0 -1
- package/dist/cosmos-QMH7BK7S.js +0 -681
- package/dist/cosmos-QMH7BK7S.js.map +0 -1
- package/dist/eth-3DX3PXDU.cjs.map +0 -1
- package/dist/eth-ICLGRJE5.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/solana-4KQFLZUC.js.map +0 -1
- package/dist/solana-PQ5K4NGO.cjs.map +0 -1
- package/dist/tron-F5AARBY4.cjs.map +0 -1
- package/dist/tron-OBLPB2LN.js.map +0 -1
- package/dist/xlm-5GODWWL2.cjs.map +0 -1
- package/dist/xlm-GX2QGFLI.js.map +0 -1
- package/dist/xrpl-7HQLIDAK.cjs +0 -1174
- package/dist/xrpl-7HQLIDAK.cjs.map +0 -1
- package/dist/xrpl-YCDFXBGQ.js +0 -1169
- package/dist/xrpl-YCDFXBGQ.js.map +0 -1
- /package/dist/{index.d.cts → cjs/index.d.cts} +0 -0
- /package/dist/{index.d.ts → esm/index.d.ts} +0 -0
package/src/cashaddr.ts
ADDED
|
@@ -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
|
-
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
77
|
-
if (
|
|
78
|
-
|
|
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
|
-
//
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
//
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 (
|
|
122
|
-
|
|
123
|
-
|
|
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 {
|
|
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
|
|
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:
|
|
51
|
-
|
|
52
|
-
|
|
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 =
|
|
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
package/src/solana.ts
CHANGED