neozip-blockchain 0.5.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 +22 -0
- package/README.md +189 -0
- package/dist/browser/TokenVerifierBrowser.d.ts +63 -0
- package/dist/browser/TokenVerifierBrowser.d.ts.map +1 -0
- package/dist/browser/TokenVerifierBrowser.js +358 -0
- package/dist/browser/TokenVerifierBrowser.js.map +1 -0
- package/dist/browser/WalletManagerBrowser.d.ts +99 -0
- package/dist/browser/WalletManagerBrowser.d.ts.map +1 -0
- package/dist/browser/WalletManagerBrowser.js +434 -0
- package/dist/browser/WalletManagerBrowser.js.map +1 -0
- package/dist/browser/ZipkitMinterBrowser.d.ts +59 -0
- package/dist/browser/ZipkitMinterBrowser.d.ts.map +1 -0
- package/dist/browser/ZipkitMinterBrowser.js +337 -0
- package/dist/browser/ZipkitMinterBrowser.js.map +1 -0
- package/dist/browser/index.d.ts +12 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +19 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/constants/index.d.ts +5 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +21 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/constants/metadata.d.ts +18 -0
- package/dist/constants/metadata.d.ts.map +1 -0
- package/dist/constants/metadata.js +33 -0
- package/dist/constants/metadata.js.map +1 -0
- package/dist/constants/servers.d.ts +38 -0
- package/dist/constants/servers.d.ts.map +1 -0
- package/dist/constants/servers.js +54 -0
- package/dist/constants/servers.js.map +1 -0
- package/dist/core/ContractVersionRegistry.d.ts +63 -0
- package/dist/core/ContractVersionRegistry.d.ts.map +1 -0
- package/dist/core/ContractVersionRegistry.js +128 -0
- package/dist/core/ContractVersionRegistry.js.map +1 -0
- package/dist/core/WalletManager.d.ts +123 -0
- package/dist/core/WalletManager.d.ts.map +1 -0
- package/dist/core/WalletManager.js +360 -0
- package/dist/core/WalletManager.js.map +1 -0
- package/dist/core/ZipkitMinter.d.ts +155 -0
- package/dist/core/ZipkitMinter.d.ts.map +1 -0
- package/dist/core/ZipkitMinter.js +625 -0
- package/dist/core/ZipkitMinter.js.map +1 -0
- package/dist/core/ZipkitVerifier.d.ts +252 -0
- package/dist/core/ZipkitVerifier.d.ts.map +1 -0
- package/dist/core/ZipkitVerifier.js +1370 -0
- package/dist/core/ZipkitVerifier.js.map +1 -0
- package/dist/core/adapters/AdapterFactory.d.ts +22 -0
- package/dist/core/adapters/AdapterFactory.d.ts.map +1 -0
- package/dist/core/adapters/AdapterFactory.js +62 -0
- package/dist/core/adapters/AdapterFactory.js.map +1 -0
- package/dist/core/adapters/ContractVersionAdapter.d.ts +96 -0
- package/dist/core/adapters/ContractVersionAdapter.d.ts.map +1 -0
- package/dist/core/adapters/ContractVersionAdapter.js +9 -0
- package/dist/core/adapters/ContractVersionAdapter.js.map +1 -0
- package/dist/core/adapters/V2_10Adapter.d.ts +24 -0
- package/dist/core/adapters/V2_10Adapter.d.ts.map +1 -0
- package/dist/core/adapters/V2_10Adapter.js +87 -0
- package/dist/core/adapters/V2_10Adapter.js.map +1 -0
- package/dist/core/adapters/V2_11Adapter.d.ts +24 -0
- package/dist/core/adapters/V2_11Adapter.d.ts.map +1 -0
- package/dist/core/adapters/V2_11Adapter.js +87 -0
- package/dist/core/adapters/V2_11Adapter.js.map +1 -0
- package/dist/core/adapters/V2_50Adapter.d.ts +105 -0
- package/dist/core/adapters/V2_50Adapter.d.ts.map +1 -0
- package/dist/core/adapters/V2_50Adapter.js +263 -0
- package/dist/core/adapters/V2_50Adapter.js.map +1 -0
- package/dist/core/contracts.d.ts +169 -0
- package/dist/core/contracts.d.ts.map +1 -0
- package/dist/core/contracts.js +731 -0
- package/dist/core/contracts.js.map +1 -0
- package/dist/core/index.d.ts +24 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +82 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/node/WalletManagerNode.d.ts +47 -0
- package/dist/node/WalletManagerNode.d.ts.map +1 -0
- package/dist/node/WalletManagerNode.js +203 -0
- package/dist/node/WalletManagerNode.js.map +1 -0
- package/dist/node/index.d.ts +12 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +21 -0
- package/dist/node/index.js.map +1 -0
- package/dist/ots/ZipkitOTS.d.ts +152 -0
- package/dist/ots/ZipkitOTS.d.ts.map +1 -0
- package/dist/ots/ZipkitOTS.js +544 -0
- package/dist/ots/ZipkitOTS.js.map +1 -0
- package/dist/ots/index.d.ts +9 -0
- package/dist/ots/index.d.ts.map +1 -0
- package/dist/ots/index.js +25 -0
- package/dist/ots/index.js.map +1 -0
- package/dist/types/index.d.ts +84 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +37 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/Logger.d.ts +73 -0
- package/dist/utils/Logger.d.ts.map +1 -0
- package/dist/utils/Logger.js +156 -0
- package/dist/utils/Logger.js.map +1 -0
- package/dist/utils/crc32.d.ts +6 -0
- package/dist/utils/crc32.d.ts.map +1 -0
- package/dist/utils/crc32.js +23 -0
- package/dist/utils/crc32.js.map +1 -0
- package/dist/zipstamp-server/CalendarManager.d.ts +238 -0
- package/dist/zipstamp-server/CalendarManager.d.ts.map +1 -0
- package/dist/zipstamp-server/CalendarManager.js +364 -0
- package/dist/zipstamp-server/CalendarManager.js.map +1 -0
- package/dist/zipstamp-server/ZipstampServerAPI.d.ts +427 -0
- package/dist/zipstamp-server/ZipstampServerAPI.d.ts.map +1 -0
- package/dist/zipstamp-server/ZipstampServerAPI.js +696 -0
- package/dist/zipstamp-server/ZipstampServerAPI.js.map +1 -0
- package/dist/zipstamp-server/ZipstampServerClient.d.ts +590 -0
- package/dist/zipstamp-server/ZipstampServerClient.d.ts.map +1 -0
- package/dist/zipstamp-server/ZipstampServerClient.js +433 -0
- package/dist/zipstamp-server/ZipstampServerClient.js.map +1 -0
- package/dist/zipstamp-server/ZipstampServerHelpers.d.ts +430 -0
- package/dist/zipstamp-server/ZipstampServerHelpers.d.ts.map +1 -0
- package/dist/zipstamp-server/ZipstampServerHelpers.js +503 -0
- package/dist/zipstamp-server/ZipstampServerHelpers.js.map +1 -0
- package/dist/zipstamp-server/index.d.ts +24 -0
- package/dist/zipstamp-server/index.d.ts.map +1 -0
- package/dist/zipstamp-server/index.js +67 -0
- package/dist/zipstamp-server/index.js.map +1 -0
- package/package.json +117 -0
|
@@ -0,0 +1,1370 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* ZipkitVerifier - Handles NFT token verification for ZIP files
|
|
5
|
+
* Verifier API for NZIP-NFT smart contract.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ZipkitVerifier = void 0;
|
|
9
|
+
const ethers_1 = require("ethers");
|
|
10
|
+
const contracts_1 = require("./contracts");
|
|
11
|
+
const AdapterFactory_1 = require("./adapters/AdapterFactory");
|
|
12
|
+
const ContractVersionRegistry_1 = require("./ContractVersionRegistry");
|
|
13
|
+
/**
|
|
14
|
+
* Blockchain Token Verifier for ZIP files
|
|
15
|
+
*/
|
|
16
|
+
class ZipkitVerifier {
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.debug = options.debug || false;
|
|
19
|
+
this.maxRetries = options.maxRetries || 3;
|
|
20
|
+
this.retryDelay = options.retryDelay || 1000;
|
|
21
|
+
this.batchDelay = options.batchDelay || 500;
|
|
22
|
+
if (this.debug) {
|
|
23
|
+
console.log(`[DEBUG] ZipkitVerifier initialized`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Extracts and normalizes token metadata from a tokenized ZIP archive.
|
|
28
|
+
*
|
|
29
|
+
* Handles field name variations across different token versions:
|
|
30
|
+
* - chainId vs networkChainId
|
|
31
|
+
* - timestamp vs mintedAt
|
|
32
|
+
* - Validates required fields
|
|
33
|
+
*
|
|
34
|
+
* @param zipkit - ZipkitNode instance with loaded archive
|
|
35
|
+
* @returns Normalized TokenMetadata or null if not tokenized
|
|
36
|
+
* @throws Error if TOKEN file exists but is invalid
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { ZipkitNode } from 'neozipkit/node';
|
|
41
|
+
* import { ZipkitVerifier } from 'neozipkit/blockchain';
|
|
42
|
+
*
|
|
43
|
+
* const zipkit = new ZipkitNode();
|
|
44
|
+
* await zipkit.loadZipFile('archive.nzip');
|
|
45
|
+
* const metadata = await ZipkitVerifier.extractTokenMetadata(zipkit);
|
|
46
|
+
* if (metadata) {
|
|
47
|
+
* console.log(`Token ID: ${metadata.tokenId}`);
|
|
48
|
+
* console.log(`Network: ${metadata.network}`);
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
static async extractTokenMetadata(zipkit) {
|
|
53
|
+
try {
|
|
54
|
+
// Check for META-INF/TOKEN.NZIP entry (new standard)
|
|
55
|
+
let tokenEntry = zipkit.getZipEntry(contracts_1.TOKENIZED_METADATA);
|
|
56
|
+
// Fallback to legacy META-INF/NZIP.TOKEN for backward compatibility
|
|
57
|
+
if (!tokenEntry) {
|
|
58
|
+
tokenEntry = zipkit.getZipEntry(contracts_1.TOKENIZED_METADATA_LEGACY);
|
|
59
|
+
if (tokenEntry) {
|
|
60
|
+
console.warn('[ZipkitVerifier] Reading legacy NZIP.TOKEN. Write new files as TOKEN.NZIP.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!tokenEntry) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
// Extract token data to buffer
|
|
67
|
+
const tokenDataBuffer = await zipkit.extractToBuffer(tokenEntry, {
|
|
68
|
+
skipHashCheck: false
|
|
69
|
+
});
|
|
70
|
+
const tokenData = tokenDataBuffer.toString('utf-8');
|
|
71
|
+
const rawMetadata = JSON.parse(tokenData);
|
|
72
|
+
// Normalize field names for compatibility across versions
|
|
73
|
+
const chainId = rawMetadata.chainId ?? rawMetadata.networkChainId;
|
|
74
|
+
const timestamp = rawMetadata.timestamp ?? rawMetadata.mintedAt;
|
|
75
|
+
// SECURITY: networkChainId is REQUIRED - cannot infer from network name (ambiguous)
|
|
76
|
+
// Network names like "sepolia" could match multiple networks (Base Sepolia, Ethereum Sepolia, Arbitrum Sepolia)
|
|
77
|
+
let networkChainId = chainId;
|
|
78
|
+
let contractVersion = rawMetadata.contractVersion;
|
|
79
|
+
// Validate required fields - chainId is mandatory for security
|
|
80
|
+
if (!networkChainId) {
|
|
81
|
+
throw new Error('Invalid token metadata: missing required field "networkChainId". Network names are ambiguous and cannot be used for verification. Please ensure token metadata includes networkChainId.');
|
|
82
|
+
}
|
|
83
|
+
// contractVersion: Do NOT infer from network config because:
|
|
84
|
+
// 1. Old v2.10 files never had contractVersion in TOKEN file
|
|
85
|
+
// 2. Network configs may have been updated to newer contract versions
|
|
86
|
+
// 3. Verification will try all versions automatically if version is 'unknown'
|
|
87
|
+
if (!contractVersion) {
|
|
88
|
+
console.warn(`[WARNING] TOKEN file missing contractVersion (common for v2.10 files) - verification will try all versions automatically`);
|
|
89
|
+
contractVersion = 'unknown';
|
|
90
|
+
}
|
|
91
|
+
// Create normalized metadata object
|
|
92
|
+
const metadata = {
|
|
93
|
+
tokenId: rawMetadata.tokenId,
|
|
94
|
+
contractAddress: rawMetadata.contractAddress,
|
|
95
|
+
network: rawMetadata.network,
|
|
96
|
+
networkChainId: networkChainId, // Required - now guaranteed to be set
|
|
97
|
+
contractVersion: contractVersion, // Required - now guaranteed to be set
|
|
98
|
+
merkleRoot: rawMetadata.merkleRoot,
|
|
99
|
+
creationTimestamp: rawMetadata.blockNumber || rawMetadata.creationTimestamp,
|
|
100
|
+
transactionHash: rawMetadata.transactionHash,
|
|
101
|
+
owner: rawMetadata.owner || rawMetadata.ownerAddress,
|
|
102
|
+
encryptedHash: rawMetadata.encryptedHash,
|
|
103
|
+
mintedAt: rawMetadata.mintedAt || rawMetadata.mintDate,
|
|
104
|
+
mintDate: rawMetadata.mintDate || rawMetadata.mintedAt,
|
|
105
|
+
ipfsHash: rawMetadata.ipfsHash,
|
|
106
|
+
blockNumber: rawMetadata.blockNumber
|
|
107
|
+
};
|
|
108
|
+
// Validate other required fields exist
|
|
109
|
+
if (!metadata.tokenId || !metadata.contractAddress || !metadata.merkleRoot) {
|
|
110
|
+
throw new Error('Invalid token metadata: missing required fields (tokenId, contractAddress, or merkleRoot)');
|
|
111
|
+
}
|
|
112
|
+
// SECURITY: Validate input formats to prevent injection attacks and invalid data processing
|
|
113
|
+
// Validate token ID format
|
|
114
|
+
const tokenIdValidation = (0, contracts_1.validateTokenId)(metadata.tokenId);
|
|
115
|
+
if (!tokenIdValidation.success) {
|
|
116
|
+
throw new Error(`Invalid token ID: ${tokenIdValidation.error}`);
|
|
117
|
+
}
|
|
118
|
+
// Validate contract address format
|
|
119
|
+
const addressValidation = (0, contracts_1.validateEthereumAddress)(metadata.contractAddress);
|
|
120
|
+
if (!addressValidation.success) {
|
|
121
|
+
throw new Error(`Invalid contract address: ${addressValidation.error}`);
|
|
122
|
+
}
|
|
123
|
+
// Use normalized address (checksum format)
|
|
124
|
+
if (addressValidation.normalizedAddress) {
|
|
125
|
+
metadata.contractAddress = addressValidation.normalizedAddress;
|
|
126
|
+
}
|
|
127
|
+
// Sanitize network name
|
|
128
|
+
const networkValidation = (0, contracts_1.sanitizeNetworkName)(metadata.network);
|
|
129
|
+
if (!networkValidation.success) {
|
|
130
|
+
throw new Error(`Invalid network name: ${networkValidation.error}`);
|
|
131
|
+
}
|
|
132
|
+
if (networkValidation.sanitized) {
|
|
133
|
+
metadata.network = networkValidation.sanitized;
|
|
134
|
+
}
|
|
135
|
+
// Validate merkle root format
|
|
136
|
+
const merkleRootValidation = (0, contracts_1.validateMerkleRootFormat)(metadata.merkleRoot);
|
|
137
|
+
if (!merkleRootValidation.success) {
|
|
138
|
+
throw new Error(`Invalid merkle root format: ${merkleRootValidation.error}`);
|
|
139
|
+
}
|
|
140
|
+
// Use normalized merkle root (consistent format)
|
|
141
|
+
if (merkleRootValidation.normalized) {
|
|
142
|
+
metadata.merkleRoot = merkleRootValidation.normalized;
|
|
143
|
+
}
|
|
144
|
+
return metadata;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error instanceof Error && error.message.includes('Invalid token metadata')) {
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
// Return null for non-tokenized archives or extraction errors
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Instance method for convenience - extracts token metadata from loaded archive
|
|
156
|
+
* @param zipkit - ZipkitNode instance with loaded archive
|
|
157
|
+
* @returns Normalized TokenMetadata or null if not tokenized
|
|
158
|
+
*/
|
|
159
|
+
async getTokenMetadata(zipkit) {
|
|
160
|
+
return ZipkitVerifier.extractTokenMetadata(zipkit);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Query contract version from blockchain
|
|
164
|
+
* @param contractAddress - Contract address
|
|
165
|
+
* @param networkConfig - Network configuration with RPC URLs
|
|
166
|
+
* @param rpcUrlIndex - Optional RPC URL index (default: 0)
|
|
167
|
+
* @returns Contract version string or null if query fails
|
|
168
|
+
*/
|
|
169
|
+
async queryContractVersion(contractAddress, networkConfig, rpcUrlIndex = 0) {
|
|
170
|
+
const rpcUrls = networkConfig.rpcUrls.length > 0 ? networkConfig.rpcUrls : [];
|
|
171
|
+
if (rpcUrls.length === 0 || rpcUrlIndex >= rpcUrls.length) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const rpcUrl = rpcUrls[rpcUrlIndex];
|
|
175
|
+
let provider = null;
|
|
176
|
+
try {
|
|
177
|
+
provider = new ethers_1.ethers.JsonRpcProvider(rpcUrl);
|
|
178
|
+
const contract = new ethers_1.ethers.Contract(contractAddress, contracts_1.NZIP_CONTRACT_ABI, provider);
|
|
179
|
+
// Query getVersion() function with timeout
|
|
180
|
+
let timeoutId = null;
|
|
181
|
+
const version = await Promise.race([
|
|
182
|
+
contract.getVersion(),
|
|
183
|
+
new Promise((_, reject) => {
|
|
184
|
+
timeoutId = setTimeout(() => reject(new Error('getVersion timeout after 10 seconds')), 10000);
|
|
185
|
+
})
|
|
186
|
+
]).finally(() => {
|
|
187
|
+
// Clear the timeout if it's still pending
|
|
188
|
+
if (timeoutId !== null) {
|
|
189
|
+
clearTimeout(timeoutId);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
if (this.debug) {
|
|
193
|
+
console.log(`[DEBUG] Contract version queried from blockchain: ${version}`);
|
|
194
|
+
}
|
|
195
|
+
return version;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (this.debug) {
|
|
199
|
+
console.log(`[DEBUG] Failed to query contract version: ${error.message || String(error)}`);
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
try {
|
|
205
|
+
if (provider) {
|
|
206
|
+
provider.destroy();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (cleanupError) {
|
|
210
|
+
// Ignore cleanup errors
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Extract and validate token metadata from ZIP
|
|
216
|
+
*/
|
|
217
|
+
async extractTokenMetadata(tokenBuffer) {
|
|
218
|
+
try {
|
|
219
|
+
if (this.debug) {
|
|
220
|
+
console.log(`[DEBUG] Extracting token metadata from buffer (${tokenBuffer.length} bytes)`);
|
|
221
|
+
}
|
|
222
|
+
const tokenData = JSON.parse(tokenBuffer.toString('utf8'));
|
|
223
|
+
// Validate basic required fields exist
|
|
224
|
+
const requiredFields = ['tokenId', 'contractAddress', 'network', 'merkleRoot'];
|
|
225
|
+
for (const field of requiredFields) {
|
|
226
|
+
if (!tokenData[field]) {
|
|
227
|
+
if (this.debug) {
|
|
228
|
+
console.log(`[DEBUG] Missing required field: ${field}`);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
error: `Missing required field: ${field}`
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// SECURITY: Validate input formats to prevent injection attacks and invalid data processing
|
|
237
|
+
// Validate token ID format
|
|
238
|
+
const tokenIdValidation = (0, contracts_1.validateTokenId)(tokenData.tokenId);
|
|
239
|
+
if (!tokenIdValidation.success) {
|
|
240
|
+
if (this.debug) {
|
|
241
|
+
console.log(`[DEBUG] Token ID validation failed: ${tokenIdValidation.error}`);
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
success: false,
|
|
245
|
+
error: `Invalid token ID format: ${tokenIdValidation.error}`
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// Validate contract address format
|
|
249
|
+
const addressValidation = (0, contracts_1.validateEthereumAddress)(tokenData.contractAddress);
|
|
250
|
+
if (!addressValidation.success) {
|
|
251
|
+
if (this.debug) {
|
|
252
|
+
console.log(`[DEBUG] Contract address validation failed: ${addressValidation.error}`);
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
error: `Invalid contract address format: ${addressValidation.error}`
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// Use normalized address (checksum format)
|
|
260
|
+
if (addressValidation.normalizedAddress) {
|
|
261
|
+
tokenData.contractAddress = addressValidation.normalizedAddress;
|
|
262
|
+
}
|
|
263
|
+
// Sanitize network name
|
|
264
|
+
const networkValidation = (0, contracts_1.sanitizeNetworkName)(tokenData.network);
|
|
265
|
+
if (!networkValidation.success) {
|
|
266
|
+
if (this.debug) {
|
|
267
|
+
console.log(`[DEBUG] Network name validation failed: ${networkValidation.error}`);
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
success: false,
|
|
271
|
+
error: `Invalid network name: ${networkValidation.error}`
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (networkValidation.sanitized) {
|
|
275
|
+
tokenData.network = networkValidation.sanitized;
|
|
276
|
+
}
|
|
277
|
+
// Validate merkle root format
|
|
278
|
+
const merkleRootValidation = (0, contracts_1.validateMerkleRootFormat)(tokenData.merkleRoot);
|
|
279
|
+
if (!merkleRootValidation.success) {
|
|
280
|
+
if (this.debug) {
|
|
281
|
+
console.log(`[DEBUG] Merkle root validation failed: ${merkleRootValidation.error}`);
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
success: false,
|
|
285
|
+
error: `Invalid merkle root format: ${merkleRootValidation.error}`
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// Use normalized merkle root (consistent format)
|
|
289
|
+
if (merkleRootValidation.normalized) {
|
|
290
|
+
tokenData.merkleRoot = merkleRootValidation.normalized;
|
|
291
|
+
}
|
|
292
|
+
// SECURITY: networkChainId is REQUIRED - cannot infer from network name (ambiguous)
|
|
293
|
+
// Network names like "sepolia" could match multiple networks (Base Sepolia, Ethereum Sepolia, Arbitrum Sepolia)
|
|
294
|
+
let networkChainId = tokenData.networkChainId ?? tokenData.chainId;
|
|
295
|
+
let contractVersion = tokenData.contractVersion;
|
|
296
|
+
// Validate networkChainId (required for blockchain queries)
|
|
297
|
+
if (!networkChainId) {
|
|
298
|
+
return {
|
|
299
|
+
success: false,
|
|
300
|
+
error: 'Missing required field: networkChainId. Network names are ambiguous and cannot be used for verification. Please ensure token metadata includes networkChainId.'
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// contractVersion: Do NOT infer from network config because:
|
|
304
|
+
// 1. Old v2.10 files never had contractVersion in TOKEN file
|
|
305
|
+
// 2. Network configs may have been updated to newer contract versions
|
|
306
|
+
// 3. Query from blockchain first, then let verification try all versions if needed
|
|
307
|
+
// If contractVersion is missing, try to query from blockchain
|
|
308
|
+
if (!contractVersion && networkChainId && tokenData.contractAddress) {
|
|
309
|
+
const networkConfig = (0, contracts_1.getContractConfig)(networkChainId);
|
|
310
|
+
if (networkConfig) {
|
|
311
|
+
if (this.debug) {
|
|
312
|
+
console.log(`[DEBUG] TOKEN file missing contractVersion - attempting to query from blockchain...`);
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
const blockchainVersion = await this.queryContractVersion(tokenData.contractAddress, networkConfig, 0);
|
|
316
|
+
if (blockchainVersion) {
|
|
317
|
+
const normalized = (0, ContractVersionRegistry_1.normalizeVersion)(blockchainVersion);
|
|
318
|
+
if (normalized && (0, ContractVersionRegistry_1.isVersionSupported)(normalized)) {
|
|
319
|
+
contractVersion = normalized;
|
|
320
|
+
if (this.debug) {
|
|
321
|
+
console.log(`[DEBUG] Successfully queried contractVersion from blockchain: ${contractVersion}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
if (this.debug) {
|
|
326
|
+
console.log(`[DEBUG] Queried version ${blockchainVersion} (normalized: ${normalized}) is not supported`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch (queryError) {
|
|
332
|
+
if (this.debug) {
|
|
333
|
+
console.log(`[DEBUG] Failed to query contract version from blockchain: ${queryError.message || String(queryError)}`);
|
|
334
|
+
}
|
|
335
|
+
// Continue - we'll try multiple versions during verification
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Final validation - contractVersion is preferred but not strictly required
|
|
340
|
+
// (we can try multiple versions during verification)
|
|
341
|
+
if (!contractVersion) {
|
|
342
|
+
if (this.debug) {
|
|
343
|
+
console.log(`[DEBUG] ⚠️ Contract version not found in TOKEN file or blockchain. Will try all versions during verification.`);
|
|
344
|
+
}
|
|
345
|
+
// Don't fail here - let verification try multiple versions
|
|
346
|
+
// But we still need a version for the metadata object, so use a placeholder
|
|
347
|
+
contractVersion = 'unknown';
|
|
348
|
+
}
|
|
349
|
+
// Create normalized metadata object
|
|
350
|
+
const metadata = {
|
|
351
|
+
tokenId: tokenData.tokenId,
|
|
352
|
+
contractAddress: tokenData.contractAddress,
|
|
353
|
+
network: tokenData.network,
|
|
354
|
+
networkChainId: networkChainId, // Required - now guaranteed to be set
|
|
355
|
+
contractVersion: contractVersion, // Required - now guaranteed to be set
|
|
356
|
+
merkleRoot: tokenData.merkleRoot,
|
|
357
|
+
transactionHash: tokenData.transactionHash,
|
|
358
|
+
blockNumber: tokenData.blockNumber,
|
|
359
|
+
owner: tokenData.owner || tokenData.ownerAddress,
|
|
360
|
+
encryptedHash: tokenData.encryptedHash,
|
|
361
|
+
mintedAt: tokenData.mintedAt || tokenData.mintDate,
|
|
362
|
+
mintDate: tokenData.mintDate || tokenData.mintedAt,
|
|
363
|
+
creationTimestamp: tokenData.creationTimestamp || tokenData.blockNumber,
|
|
364
|
+
ipfsHash: tokenData.ipfsHash
|
|
365
|
+
};
|
|
366
|
+
if (this.debug) {
|
|
367
|
+
console.log(`[DEBUG] Token metadata extracted successfully`);
|
|
368
|
+
console.log(`[DEBUG] Token ID: ${metadata.tokenId}, Network: ${metadata.network}, ChainId: ${metadata.networkChainId}, Version: ${metadata.contractVersion}`);
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
success: true,
|
|
372
|
+
metadata
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
const errorMessage = error instanceof Error ? error.message : 'Invalid JSON format';
|
|
377
|
+
if (this.debug) {
|
|
378
|
+
console.log(`[DEBUG] Failed to parse token metadata: ${errorMessage}`);
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
success: false,
|
|
382
|
+
error: `Invalid token metadata format: ${errorMessage}`
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Map network name to RPC configuration using contracts.ts (single source of truth)
|
|
388
|
+
* REQUIRES chainId for security - network names are ambiguous and can match wrong networks
|
|
389
|
+
*
|
|
390
|
+
* @param networkName Network name (for logging/display purposes only)
|
|
391
|
+
* @param chainId Chain ID (REQUIRED - must be provided)
|
|
392
|
+
* @returns Network configuration or error if chainId is missing or invalid
|
|
393
|
+
*/
|
|
394
|
+
getNetworkConfig(networkName, chainId) {
|
|
395
|
+
if (this.debug) {
|
|
396
|
+
if (chainId) {
|
|
397
|
+
console.log(`[DEBUG] Looking up network config by chainId: ${chainId} (network: ${networkName})`);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
console.log(`[DEBUG] SECURITY WARNING: chainId not provided for network: ${networkName}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// SECURITY: chainId is REQUIRED - network names are ambiguous and can match wrong networks
|
|
404
|
+
// For example, "sepolia" could match Base Sepolia, Ethereum Sepolia, or Arbitrum Sepolia
|
|
405
|
+
if (!chainId) {
|
|
406
|
+
return {
|
|
407
|
+
success: false,
|
|
408
|
+
error: `chainId is required for network identification. Network name "${networkName}" is ambiguous and cannot be used for verification. Please ensure token metadata includes networkChainId.`
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
// Use chainId directly (most reliable and secure)
|
|
412
|
+
const config = (0, contracts_1.getContractConfig)(chainId);
|
|
413
|
+
if (config && config.chainId === chainId) {
|
|
414
|
+
if (this.debug) {
|
|
415
|
+
console.log(`[DEBUG] Found config by chainId: ${config.network} (${config.chainId})`);
|
|
416
|
+
}
|
|
417
|
+
return { success: true, config };
|
|
418
|
+
}
|
|
419
|
+
// ChainId not found in contract configs
|
|
420
|
+
if (this.debug) {
|
|
421
|
+
console.log(`[DEBUG] ChainId ${chainId} not found in contract configs`);
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
success: false,
|
|
425
|
+
error: `Unsupported chainId: ${chainId}. Network "${networkName}" with chainId ${chainId} is not configured.`
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Perform merkle root comparison
|
|
430
|
+
* Uses direct string comparison - merkle roots are hex strings and must match exactly
|
|
431
|
+
* Case-sensitive comparison ensures no encoding issues or false positives
|
|
432
|
+
*/
|
|
433
|
+
compareMerkleRoots(calculated, declared) {
|
|
434
|
+
if (this.debug) {
|
|
435
|
+
console.log(`[DEBUG] Comparing merkle roots (case-sensitive)`);
|
|
436
|
+
console.log(`[DEBUG] Calculated: ${calculated || 'null'}`);
|
|
437
|
+
console.log(`[DEBUG] Declared: ${declared || 'null'}`);
|
|
438
|
+
}
|
|
439
|
+
if (!calculated) {
|
|
440
|
+
return {
|
|
441
|
+
success: false,
|
|
442
|
+
match: false,
|
|
443
|
+
error: 'Cannot calculate merkle root from archive contents'
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
if (!declared) {
|
|
447
|
+
return {
|
|
448
|
+
success: false,
|
|
449
|
+
match: false,
|
|
450
|
+
calculated,
|
|
451
|
+
error: 'No merkle root found in token metadata'
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
// Direct string comparison - merkle roots are hex strings and must match exactly
|
|
455
|
+
// Case-sensitive comparison prevents encoding issues and false positives
|
|
456
|
+
const match = calculated === declared;
|
|
457
|
+
if (this.debug) {
|
|
458
|
+
console.log(`[DEBUG] Merkle root match: ${match}`);
|
|
459
|
+
if (!match) {
|
|
460
|
+
console.log(`[DEBUG] Mismatch details:`);
|
|
461
|
+
console.log(`[DEBUG] Calculated length: ${calculated.length}, Declared length: ${declared.length}`);
|
|
462
|
+
console.log(`[DEBUG] Calculated: ${calculated}`);
|
|
463
|
+
console.log(`[DEBUG] Declared: ${declared}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
success: true,
|
|
468
|
+
match,
|
|
469
|
+
calculated,
|
|
470
|
+
declared
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Query multiple RPC endpoints with consensus mechanism
|
|
475
|
+
* Returns data only if majority of endpoints agree
|
|
476
|
+
*/
|
|
477
|
+
async queryMultipleRPCsWithConsensus(tokenId, contractAddress, networkConfig, merkleRoot, minConsensus = 2, metadataContractVersion // Optional: version from metadata for validation
|
|
478
|
+
) {
|
|
479
|
+
const rpcUrls = networkConfig.rpcUrls.length > 0 ? networkConfig.rpcUrls : [];
|
|
480
|
+
if (rpcUrls.length < minConsensus) {
|
|
481
|
+
return {
|
|
482
|
+
success: false,
|
|
483
|
+
error: `Not enough RPC endpoints (${rpcUrls.length}) for consensus (minimum: ${minConsensus})`
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
// Query up to 3 RPC endpoints (or all if less than 3)
|
|
487
|
+
const endpointsToQuery = rpcUrls.slice(0, Math.min(3, rpcUrls.length));
|
|
488
|
+
const results = [];
|
|
489
|
+
if (this.debug) {
|
|
490
|
+
console.log(`[DEBUG] Querying ${endpointsToQuery.length} RPC endpoints for consensus...`);
|
|
491
|
+
}
|
|
492
|
+
// Query all endpoints in parallel
|
|
493
|
+
const queryPromises = endpointsToQuery.map(async (rpcUrl) => {
|
|
494
|
+
try {
|
|
495
|
+
// First validate chainId from RPC
|
|
496
|
+
const provider = new ethers_1.ethers.JsonRpcProvider(rpcUrl);
|
|
497
|
+
const actualChainId = await Promise.race([
|
|
498
|
+
provider.getNetwork().then(n => Number(n.chainId)),
|
|
499
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('ChainId query timeout')), 5000))
|
|
500
|
+
]);
|
|
501
|
+
if (actualChainId !== networkConfig.chainId) {
|
|
502
|
+
if (this.debug) {
|
|
503
|
+
console.log(`[DEBUG] RPC ${rpcUrl} chainId mismatch: expected ${networkConfig.chainId}, got ${actualChainId}`);
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
success: false,
|
|
507
|
+
error: `ChainId mismatch: expected ${networkConfig.chainId}, got ${actualChainId}`,
|
|
508
|
+
rpcUrl
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
// Query token info
|
|
512
|
+
const contract = new ethers_1.ethers.Contract(contractAddress, contracts_1.NZIP_CONTRACT_ABI, provider);
|
|
513
|
+
const adapter = (0, AdapterFactory_1.getAdapter)(networkConfig.version || contracts_1.DEFAULT_CONTRACT_VERSION);
|
|
514
|
+
const zipFileInfo = await Promise.race([
|
|
515
|
+
adapter.getZipFileInfo(contract, BigInt(tokenId)),
|
|
516
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('getZipFileInfo timeout')), 10000))
|
|
517
|
+
]);
|
|
518
|
+
provider.destroy();
|
|
519
|
+
return {
|
|
520
|
+
success: true,
|
|
521
|
+
onChainMerkleRoot: zipFileInfo.merkleRootHash,
|
|
522
|
+
onChainTokenizationTime: Number(zipFileInfo.tokenizationTime),
|
|
523
|
+
onChainCreator: zipFileInfo.creator,
|
|
524
|
+
onChainBlockNumber: Number(zipFileInfo.blockNumber),
|
|
525
|
+
chainId: actualChainId,
|
|
526
|
+
rpcUrl
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
const errorMsg = error?.message || String(error);
|
|
531
|
+
if (this.debug) {
|
|
532
|
+
console.log(`[DEBUG] RPC ${rpcUrl} query failed: ${errorMsg}`);
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
success: false,
|
|
536
|
+
error: errorMsg,
|
|
537
|
+
rpcUrl
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
const queryResults = await Promise.all(queryPromises);
|
|
542
|
+
results.push(...queryResults);
|
|
543
|
+
// Filter successful results
|
|
544
|
+
const successfulResults = results.filter(r => r.success);
|
|
545
|
+
if (successfulResults.length < minConsensus) {
|
|
546
|
+
return {
|
|
547
|
+
success: false,
|
|
548
|
+
error: `Consensus failed: only ${successfulResults.length} of ${endpointsToQuery.length} endpoints succeeded (minimum: ${minConsensus})`,
|
|
549
|
+
consensusCount: successfulResults.length,
|
|
550
|
+
totalQueried: endpointsToQuery.length
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
// Check if all successful results agree on the merkle root
|
|
554
|
+
// Use direct comparison - merkle roots are hex strings and must match exactly
|
|
555
|
+
const merkleRoots = successfulResults.map(r => r.onChainMerkleRoot).filter(Boolean);
|
|
556
|
+
const uniqueRoots = new Set(merkleRoots);
|
|
557
|
+
if (uniqueRoots.size > 1) {
|
|
558
|
+
if (this.debug) {
|
|
559
|
+
console.log(`[DEBUG] RPC consensus mismatch: different merkle roots returned`);
|
|
560
|
+
successfulResults.forEach((r, i) => {
|
|
561
|
+
console.log(`[DEBUG] RPC ${i + 1}: ${r.onChainMerkleRoot}`);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
success: false,
|
|
566
|
+
error: `RPC consensus failed: endpoints returned different merkle roots`,
|
|
567
|
+
consensusCount: successfulResults.length,
|
|
568
|
+
totalQueried: endpointsToQuery.length
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
// Use the first successful result (all should be the same)
|
|
572
|
+
const consensusResult = successfulResults[0];
|
|
573
|
+
if (this.debug) {
|
|
574
|
+
console.log(`[DEBUG] RPC consensus achieved: ${successfulResults.length} of ${endpointsToQuery.length} endpoints agreed`);
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
success: true,
|
|
578
|
+
onChainMerkleRoot: consensusResult.onChainMerkleRoot,
|
|
579
|
+
onChainTokenizationTime: consensusResult.onChainTokenizationTime,
|
|
580
|
+
onChainCreator: consensusResult.onChainCreator,
|
|
581
|
+
onChainBlockNumber: consensusResult.onChainBlockNumber,
|
|
582
|
+
consensusCount: successfulResults.length,
|
|
583
|
+
totalQueried: endpointsToQuery.length
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Verify token against blockchain
|
|
588
|
+
* @param rpcUrlIndex Optional index of RPC URL to use (default: 0)
|
|
589
|
+
* @param options Optional verification options including RPC consensus settings
|
|
590
|
+
*/
|
|
591
|
+
async verifyOnChain(tokenId, contractAddress, networkConfig, merkleRoot, rpcUrlIndex = 0, options = {}, metadataContractVersion // Optional: version from metadata for validation
|
|
592
|
+
) {
|
|
593
|
+
// If RPC consensus is enabled, use consensus mechanism
|
|
594
|
+
if (options.rpcConsensus) {
|
|
595
|
+
const minConsensus = options.minRpcConsensus ?? 2;
|
|
596
|
+
const consensusResult = await this.queryMultipleRPCsWithConsensus(tokenId, contractAddress, networkConfig, merkleRoot, minConsensus, metadataContractVersion // Pass metadata version for validation
|
|
597
|
+
);
|
|
598
|
+
if (!consensusResult.success) {
|
|
599
|
+
return {
|
|
600
|
+
success: false,
|
|
601
|
+
error: consensusResult.error || 'RPC consensus failed'
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
// SECURITY: Validate version matches on-chain version (if enabled)
|
|
605
|
+
if (options.validateContractVersion !== false) {
|
|
606
|
+
let onChainVersion = null;
|
|
607
|
+
try {
|
|
608
|
+
const rpcUrl = networkConfig.rpcUrls[0];
|
|
609
|
+
const provider = new ethers_1.ethers.JsonRpcProvider(rpcUrl);
|
|
610
|
+
const contract = new ethers_1.ethers.Contract(contractAddress, contracts_1.NZIP_CONTRACT_ABI, provider);
|
|
611
|
+
onChainVersion = await Promise.race([
|
|
612
|
+
contract.getVersion(),
|
|
613
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('getVersion timeout')), 5000))
|
|
614
|
+
]);
|
|
615
|
+
provider.destroy();
|
|
616
|
+
}
|
|
617
|
+
catch (versionError) {
|
|
618
|
+
if (this.debug) {
|
|
619
|
+
console.log(`[DEBUG] Could not query on-chain version in consensus mode: ${versionError.message}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// Validate version if we have both metadata version and on-chain version
|
|
623
|
+
if (metadataContractVersion && metadataContractVersion !== 'unknown' && onChainVersion) {
|
|
624
|
+
const normalizedMetadata = (0, ContractVersionRegistry_1.normalizeVersion)(metadataContractVersion);
|
|
625
|
+
const normalizedOnChain = (0, ContractVersionRegistry_1.normalizeVersion)(onChainVersion);
|
|
626
|
+
if (normalizedMetadata && normalizedOnChain && normalizedMetadata !== normalizedOnChain) {
|
|
627
|
+
return {
|
|
628
|
+
success: false,
|
|
629
|
+
error: `Contract version mismatch: metadata version "${metadataContractVersion}" does not match on-chain version "${onChainVersion}"`
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
if (this.debug && normalizedMetadata === normalizedOnChain) {
|
|
633
|
+
console.log(`[DEBUG] ✅ Version validation passed (consensus mode): metadata version "${metadataContractVersion}" matches on-chain version "${onChainVersion}"`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
if (this.debug) {
|
|
639
|
+
console.log(`[DEBUG] Version validation disabled (consensus mode) - skipping on-chain version check`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// Verify merkle root match - direct comparison (case-sensitive)
|
|
643
|
+
// Merkle roots are hex strings and must match exactly
|
|
644
|
+
const merkleRootMatch = consensusResult.onChainMerkleRoot &&
|
|
645
|
+
consensusResult.onChainMerkleRoot === merkleRoot;
|
|
646
|
+
// Also call verifyZipFile for additional validation
|
|
647
|
+
let verifyResult = false;
|
|
648
|
+
try {
|
|
649
|
+
// Use first available RPC for verifyZipFile call
|
|
650
|
+
const rpcUrl = networkConfig.rpcUrls[0];
|
|
651
|
+
const provider = new ethers_1.ethers.JsonRpcProvider(rpcUrl);
|
|
652
|
+
const contract = new ethers_1.ethers.Contract(contractAddress, contracts_1.NZIP_CONTRACT_ABI, provider);
|
|
653
|
+
verifyResult = await Promise.race([
|
|
654
|
+
contract.verifyZipFile(tokenId, merkleRoot),
|
|
655
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Contract call timeout')), 30000))
|
|
656
|
+
]);
|
|
657
|
+
provider.destroy();
|
|
658
|
+
}
|
|
659
|
+
catch (verifyError) {
|
|
660
|
+
if (this.debug) {
|
|
661
|
+
console.log(`[DEBUG] verifyZipFile call failed: ${verifyError.message}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const isValid = merkleRootMatch === true;
|
|
665
|
+
return {
|
|
666
|
+
success: true,
|
|
667
|
+
isValid,
|
|
668
|
+
onChainMerkleRoot: consensusResult.onChainMerkleRoot,
|
|
669
|
+
onChainTokenizationTime: consensusResult.onChainTokenizationTime,
|
|
670
|
+
onChainCreator: consensusResult.onChainCreator,
|
|
671
|
+
onChainBlockNumber: consensusResult.onChainBlockNumber
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
// Standard single RPC query (quick validation - default behavior)
|
|
675
|
+
const rpcUrls = networkConfig.rpcUrls.length > 0 ? networkConfig.rpcUrls : [];
|
|
676
|
+
if (rpcUrls.length === 0) {
|
|
677
|
+
return {
|
|
678
|
+
success: false,
|
|
679
|
+
error: 'No RPC URLs configured for this network'
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
if (rpcUrlIndex >= rpcUrls.length) {
|
|
683
|
+
return {
|
|
684
|
+
success: false,
|
|
685
|
+
error: `RPC URL index ${rpcUrlIndex} is out of range (${rpcUrls.length} RPCs available)`
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
const rpcUrl = rpcUrls[rpcUrlIndex];
|
|
689
|
+
let provider = null;
|
|
690
|
+
try {
|
|
691
|
+
if (this.debug) {
|
|
692
|
+
console.log(`[DEBUG] Starting on-chain verification (quick mode - single RPC)`);
|
|
693
|
+
console.log(`[DEBUG] Token ID: ${tokenId}, Contract: ${contractAddress}`);
|
|
694
|
+
console.log(`[DEBUG] Network: ${networkConfig.network}, Chain ID: ${networkConfig.chainId}`);
|
|
695
|
+
console.log(`[DEBUG] Using RPC: ${rpcUrl}`);
|
|
696
|
+
}
|
|
697
|
+
provider = new ethers_1.ethers.JsonRpcProvider(rpcUrl);
|
|
698
|
+
// Validate chainId from RPC if option is enabled (default: true)
|
|
699
|
+
if (options.validateRpcChainId !== false) {
|
|
700
|
+
try {
|
|
701
|
+
const actualChainId = await Promise.race([
|
|
702
|
+
provider.getNetwork().then(n => Number(n.chainId)),
|
|
703
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('ChainId validation timeout')), 5000))
|
|
704
|
+
]);
|
|
705
|
+
if (actualChainId !== networkConfig.chainId) {
|
|
706
|
+
provider.destroy();
|
|
707
|
+
return {
|
|
708
|
+
success: false,
|
|
709
|
+
error: `RPC chainId validation failed: expected ${networkConfig.chainId}, got ${actualChainId}. RPC may be pointing to wrong network.`,
|
|
710
|
+
rpcUrl
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
if (this.debug) {
|
|
714
|
+
console.log(`[DEBUG] RPC chainId validated: ${actualChainId}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
catch (chainIdError) {
|
|
718
|
+
if (this.debug) {
|
|
719
|
+
console.log(`[DEBUG] ChainId validation failed: ${chainIdError.message}`);
|
|
720
|
+
}
|
|
721
|
+
// Continue with verification even if chainId validation fails (non-critical)
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const contract = new ethers_1.ethers.Contract(contractAddress, contracts_1.NZIP_CONTRACT_ABI, provider);
|
|
725
|
+
if (this.debug) {
|
|
726
|
+
console.log(`[DEBUG] Calling contract.verifyZipFile(${tokenId}, ${merkleRoot.substring(0, 10)}...)`);
|
|
727
|
+
}
|
|
728
|
+
// If contractVersion is missing/unknown, use DEFAULT_CONTRACT_VERSION (v2.50) first, then older versions for legacy files
|
|
729
|
+
// No need to query blockchain - just use the version from networkConfig or default order
|
|
730
|
+
// Determine which version to try
|
|
731
|
+
const versionsToTry = [];
|
|
732
|
+
if (networkConfig.version && networkConfig.version !== 'unknown') {
|
|
733
|
+
// Use the version from network config if available
|
|
734
|
+
versionsToTry.push(networkConfig.version);
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
// If version is missing/unknown, try default (v2.50) first, then 2.11, 2.10 for legacy files
|
|
738
|
+
versionsToTry.push(contracts_1.DEFAULT_CONTRACT_VERSION, '2.11', '2.10');
|
|
739
|
+
}
|
|
740
|
+
if (versionsToTry.length === 0) {
|
|
741
|
+
return {
|
|
742
|
+
success: false,
|
|
743
|
+
error: 'No contract versions available to try. No versions implemented or detected.'
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
// First check if token exists and get ALL actual on-chain data (not from metadata)
|
|
747
|
+
// Use adapter to handle version-specific differences
|
|
748
|
+
let onChainMerkleRoot;
|
|
749
|
+
let onChainTokenizationTime;
|
|
750
|
+
let onChainCreator;
|
|
751
|
+
let onChainBlockNumber;
|
|
752
|
+
let onChainEncryptedHash;
|
|
753
|
+
let lastError;
|
|
754
|
+
let successfulVersion;
|
|
755
|
+
// Try each version until one works
|
|
756
|
+
for (const version of versionsToTry) {
|
|
757
|
+
try {
|
|
758
|
+
if (this.debug) {
|
|
759
|
+
console.log(`[DEBUG] Trying contract version: ${version}`);
|
|
760
|
+
}
|
|
761
|
+
const adapter = (0, AdapterFactory_1.getAdapter)(version);
|
|
762
|
+
// Get token info using adapter (handles version-specific differences)
|
|
763
|
+
// CRITICAL: We trust ONLY blockchain data, not metadata file which can be tampered with
|
|
764
|
+
const zipFileInfo = await Promise.race([
|
|
765
|
+
adapter.getZipFileInfo(contract, BigInt(tokenId)),
|
|
766
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('getZipFileInfo timeout after 10 seconds')), 10000))
|
|
767
|
+
]);
|
|
768
|
+
// Extract all on-chain data (adapter returns full structure for this version)
|
|
769
|
+
onChainMerkleRoot = zipFileInfo.merkleRootHash;
|
|
770
|
+
onChainTokenizationTime = Number(zipFileInfo.tokenizationTime);
|
|
771
|
+
onChainCreator = zipFileInfo.creator;
|
|
772
|
+
onChainBlockNumber = Number(zipFileInfo.blockNumber);
|
|
773
|
+
// encryptedHash is available if contract version supports it (v2.11+)
|
|
774
|
+
onChainEncryptedHash = zipFileInfo.encryptedHash || undefined;
|
|
775
|
+
successfulVersion = version;
|
|
776
|
+
if (this.debug) {
|
|
777
|
+
console.log(`[DEBUG] Successfully queried token info using version ${version}`);
|
|
778
|
+
console.log(`[DEBUG] Token ${tokenId} exists`);
|
|
779
|
+
console.log(`[DEBUG] On-chain merkle root: ${onChainMerkleRoot}`);
|
|
780
|
+
console.log(`[DEBUG] On-chain tokenization time: ${onChainTokenizationTime} (${new Date(onChainTokenizationTime * 1000).toLocaleString()})`);
|
|
781
|
+
console.log(`[DEBUG] On-chain creator: ${onChainCreator}`);
|
|
782
|
+
console.log(`[DEBUG] On-chain block number: ${onChainBlockNumber}`);
|
|
783
|
+
console.log(`[DEBUG] Calculated merkle root: ${merkleRoot}`);
|
|
784
|
+
}
|
|
785
|
+
// Success! Break out of version loop
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
catch (infoError) {
|
|
789
|
+
const errorMsg = infoError.message || String(infoError);
|
|
790
|
+
lastError = errorMsg;
|
|
791
|
+
if (this.debug) {
|
|
792
|
+
console.log(`[DEBUG] Version ${version} failed: ${errorMsg}`);
|
|
793
|
+
}
|
|
794
|
+
// If token doesn't exist, don't try other versions
|
|
795
|
+
if (errorMsg.includes('nonexistent token') || errorMsg.includes('ERC721: invalid token ID')) {
|
|
796
|
+
return {
|
|
797
|
+
success: false,
|
|
798
|
+
error: `Token ${tokenId} does not exist on contract ${contractAddress}`
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
// If it's an "out of result range" error, try next version
|
|
802
|
+
if (errorMsg.includes('out of result range') || errorMsg.includes('data out-of-bounds')) {
|
|
803
|
+
if (this.debug) {
|
|
804
|
+
console.log(`[DEBUG] Version ${version} incompatible (wrong return structure), trying next version...`);
|
|
805
|
+
}
|
|
806
|
+
continue; // Try next version
|
|
807
|
+
}
|
|
808
|
+
// For other errors, continue to next version
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
// If we tried all versions and none worked
|
|
813
|
+
if (!onChainMerkleRoot) {
|
|
814
|
+
const triedVersions = versionsToTry.join(', ');
|
|
815
|
+
return {
|
|
816
|
+
success: false,
|
|
817
|
+
error: `Failed to get token info after trying all versions (${triedVersions}). Last error: ${lastError || 'Unknown error'}`
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
// SECURITY: Validate version matches on-chain version
|
|
821
|
+
// Query actual contract version from blockchain and compare
|
|
822
|
+
let onChainVersion = null;
|
|
823
|
+
try {
|
|
824
|
+
onChainVersion = await this.queryContractVersion(contractAddress, networkConfig, rpcUrlIndex);
|
|
825
|
+
}
|
|
826
|
+
catch (versionError) {
|
|
827
|
+
if (this.debug) {
|
|
828
|
+
console.log(`[DEBUG] Could not query on-chain version: ${versionError.message}`);
|
|
829
|
+
}
|
|
830
|
+
// Continue - version validation is important but not critical if query fails
|
|
831
|
+
}
|
|
832
|
+
if (onChainVersion) {
|
|
833
|
+
const normalizedOnChain = (0, ContractVersionRegistry_1.normalizeVersion)(onChainVersion);
|
|
834
|
+
const normalizedSuccessful = (0, ContractVersionRegistry_1.normalizeVersion)(successfulVersion || '');
|
|
835
|
+
// SECURITY: Validate version
|
|
836
|
+
// 1. If metadata has explicit version, it MUST match on-chain version (strict validation)
|
|
837
|
+
// 2. If metadata version is missing (legacy), validate that the version we used matches on-chain
|
|
838
|
+
if (metadataContractVersion && metadataContractVersion !== 'unknown') {
|
|
839
|
+
// New file with explicit version - strict validation
|
|
840
|
+
const normalizedMetadata = (0, ContractVersionRegistry_1.normalizeVersion)(metadataContractVersion);
|
|
841
|
+
if (normalizedMetadata && normalizedOnChain && normalizedMetadata !== normalizedOnChain) {
|
|
842
|
+
const errorMsg = `Contract version mismatch: metadata version "${metadataContractVersion}" does not match on-chain version "${onChainVersion}" (normalized: ${normalizedMetadata} vs ${normalizedOnChain})`;
|
|
843
|
+
if (this.debug) {
|
|
844
|
+
console.log(`[DEBUG] ❌ ${errorMsg}`);
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
success: false,
|
|
848
|
+
error: errorMsg,
|
|
849
|
+
rpcUrl
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
if (this.debug && normalizedMetadata === normalizedOnChain) {
|
|
853
|
+
console.log(`[DEBUG] ✅ Version validation passed: metadata version "${metadataContractVersion}" matches on-chain version "${onChainVersion}"`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
// Legacy file - validate that the version we used matches on-chain
|
|
858
|
+
if (normalizedOnChain && normalizedSuccessful && normalizedOnChain !== normalizedSuccessful) {
|
|
859
|
+
const errorMsg = `Contract version mismatch: used version "${successfulVersion}" does not match on-chain version "${onChainVersion}" (normalized: ${normalizedSuccessful} vs ${normalizedOnChain})`;
|
|
860
|
+
if (this.debug) {
|
|
861
|
+
console.log(`[DEBUG] ⚠️ ${errorMsg}`);
|
|
862
|
+
}
|
|
863
|
+
return {
|
|
864
|
+
success: false,
|
|
865
|
+
error: errorMsg,
|
|
866
|
+
rpcUrl
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
if (this.debug && normalizedOnChain === normalizedSuccessful) {
|
|
870
|
+
console.log(`[DEBUG] ✅ Version validation passed (legacy file): on-chain version "${onChainVersion}" matches used version "${successfulVersion}"`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
if (this.debug) {
|
|
876
|
+
console.log(`[DEBUG] ⚠️ Could not query on-chain version for validation - continuing with used version "${successfulVersion}"`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (this.debug && successfulVersion && successfulVersion !== networkConfig.version) {
|
|
880
|
+
console.log(`[DEBUG] ⚠️ Contract version mismatch: network config says "${networkConfig.version || 'unknown'}", but "${successfulVersion}" worked. Consider updating network config.`);
|
|
881
|
+
}
|
|
882
|
+
// Compare the calculated merkle root with the on-chain merkle root
|
|
883
|
+
// This is the critical security check - we trust the blockchain, not the metadata file
|
|
884
|
+
// Direct string comparison (case-sensitive) - merkle roots are hex strings and must match exactly
|
|
885
|
+
const merkleRootMatch = onChainMerkleRoot && onChainMerkleRoot === merkleRoot;
|
|
886
|
+
if (this.debug) {
|
|
887
|
+
console.log(`[DEBUG] Merkle root match: ${merkleRootMatch}`);
|
|
888
|
+
}
|
|
889
|
+
// Also call verifyZipFile for additional validation (though we already have the root)
|
|
890
|
+
let verifyResult = false;
|
|
891
|
+
try {
|
|
892
|
+
verifyResult = await Promise.race([
|
|
893
|
+
contract.verifyZipFile(tokenId, merkleRoot),
|
|
894
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Contract call timeout after 30 seconds')), 30000))
|
|
895
|
+
]);
|
|
896
|
+
}
|
|
897
|
+
catch (verifyError) {
|
|
898
|
+
// If verifyZipFile fails but we have the root, we can still validate
|
|
899
|
+
if (this.debug) {
|
|
900
|
+
console.log(`[DEBUG] verifyZipFile call failed: ${verifyError.message}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
// The merkle root comparison is the primary check - if roots match, verification passes
|
|
904
|
+
// verifyZipFile is a secondary check for additional validation
|
|
905
|
+
const isValid = merkleRootMatch === true;
|
|
906
|
+
if (this.debug) {
|
|
907
|
+
console.log(`[DEBUG] On-chain verification result: ${isValid} (merkle root match: ${merkleRootMatch}, verifyZipFile: ${verifyResult})`);
|
|
908
|
+
}
|
|
909
|
+
// Clean up provider to allow process to exit
|
|
910
|
+
try {
|
|
911
|
+
if (provider) {
|
|
912
|
+
provider.destroy();
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
catch (cleanupError) {
|
|
916
|
+
// Ignore cleanup errors
|
|
917
|
+
if (this.debug) {
|
|
918
|
+
console.log(`[DEBUG] Error destroying provider: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
success: true,
|
|
923
|
+
isValid,
|
|
924
|
+
onChainMerkleRoot,
|
|
925
|
+
onChainTokenizationTime,
|
|
926
|
+
onChainCreator,
|
|
927
|
+
onChainBlockNumber
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
const errorMessage = error instanceof Error ? error.message : 'Network error';
|
|
932
|
+
if (this.debug) {
|
|
933
|
+
console.log(`[DEBUG] On-chain verification failed: ${errorMessage}`);
|
|
934
|
+
}
|
|
935
|
+
// Clean up provider on error
|
|
936
|
+
try {
|
|
937
|
+
if (provider) {
|
|
938
|
+
provider.destroy();
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
catch (cleanupError) {
|
|
942
|
+
// Ignore cleanup errors
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
success: false,
|
|
946
|
+
error: errorMessage,
|
|
947
|
+
rpcUrl: rpcUrl
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Complete verification process
|
|
953
|
+
* @param rpcUrlIndex Optional index of RPC URL to use (default: 0)
|
|
954
|
+
*/
|
|
955
|
+
async verifyToken(tokenMetadata, calculatedMerkleRoot, options = {}, rpcUrlIndex = 0) {
|
|
956
|
+
try {
|
|
957
|
+
if (this.debug) {
|
|
958
|
+
console.log(`[DEBUG] Starting complete token verification`);
|
|
959
|
+
}
|
|
960
|
+
// SECURITY: Validate input formats before processing
|
|
961
|
+
const tokenIdValidation = (0, contracts_1.validateTokenId)(tokenMetadata.tokenId);
|
|
962
|
+
if (!tokenIdValidation.success) {
|
|
963
|
+
return {
|
|
964
|
+
success: false,
|
|
965
|
+
message: `Invalid token ID: ${tokenIdValidation.error}`,
|
|
966
|
+
errorDetails: {
|
|
967
|
+
errorType: 'METADATA_ERROR',
|
|
968
|
+
tokenId: tokenMetadata.tokenId
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
const addressValidation = (0, contracts_1.validateEthereumAddress)(tokenMetadata.contractAddress);
|
|
973
|
+
if (!addressValidation.success) {
|
|
974
|
+
return {
|
|
975
|
+
success: false,
|
|
976
|
+
message: `Invalid contract address: ${addressValidation.error}`,
|
|
977
|
+
errorDetails: {
|
|
978
|
+
errorType: 'METADATA_ERROR',
|
|
979
|
+
contractAddress: tokenMetadata.contractAddress
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
const networkValidation = (0, contracts_1.sanitizeNetworkName)(tokenMetadata.network);
|
|
984
|
+
if (!networkValidation.success) {
|
|
985
|
+
return {
|
|
986
|
+
success: false,
|
|
987
|
+
message: `Invalid network name: ${networkValidation.error}`,
|
|
988
|
+
errorDetails: {
|
|
989
|
+
errorType: 'NETWORK_ERROR',
|
|
990
|
+
networkName: tokenMetadata.network
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
if (calculatedMerkleRoot) {
|
|
995
|
+
const merkleRootValidation = (0, contracts_1.validateMerkleRootFormat)(calculatedMerkleRoot);
|
|
996
|
+
if (!merkleRootValidation.success) {
|
|
997
|
+
return {
|
|
998
|
+
success: false,
|
|
999
|
+
message: `Invalid calculated merkle root format: ${merkleRootValidation.error}`,
|
|
1000
|
+
errorDetails: {
|
|
1001
|
+
errorType: 'MERKLE_ERROR',
|
|
1002
|
+
calculatedMerkleRoot
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
// NOTE: tokenMetadata (tokenId, contractAddress, network) is used ONLY to locate the token on blockchain
|
|
1008
|
+
// All verification data (merkle root, tokenization time, creator, block number) comes from blockchain contract
|
|
1009
|
+
// If metadata is tampered with, the blockchain query will fail or return mismatched data, causing verification to fail
|
|
1010
|
+
// Get network configuration - use chainId if available (most reliable)
|
|
1011
|
+
const networkResult = this.getNetworkConfig(tokenMetadata.network, tokenMetadata.networkChainId);
|
|
1012
|
+
if (!networkResult.success || !networkResult.config) {
|
|
1013
|
+
return {
|
|
1014
|
+
success: false,
|
|
1015
|
+
message: networkResult.error || 'Unknown network error',
|
|
1016
|
+
errorDetails: {
|
|
1017
|
+
errorType: 'NETWORK_ERROR',
|
|
1018
|
+
networkName: tokenMetadata.network
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
const networkConfig = networkResult.config;
|
|
1023
|
+
// CRITICAL SECURITY CHECK: Validate contract address matches expected contract for network
|
|
1024
|
+
// This prevents attackers from creating fake metadata pointing to malicious contracts
|
|
1025
|
+
const contractValidation = (0, contracts_1.validateContractAddress)(tokenMetadata.contractAddress, networkConfig.chainId, this.debug);
|
|
1026
|
+
if (!contractValidation.success) {
|
|
1027
|
+
return {
|
|
1028
|
+
success: false,
|
|
1029
|
+
message: `Contract address validation failed: ${contractValidation.error}`,
|
|
1030
|
+
errorDetails: {
|
|
1031
|
+
errorType: 'CONTRACT_ERROR',
|
|
1032
|
+
networkName: tokenMetadata.network,
|
|
1033
|
+
contractAddress: tokenMetadata.contractAddress,
|
|
1034
|
+
tokenId: tokenMetadata.tokenId
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
// CRITICAL: We must calculate the merkle root from the actual files and compare with on-chain value
|
|
1039
|
+
// We do NOT trust the merkle root stored in the metadata file as it could be tampered with
|
|
1040
|
+
if (!calculatedMerkleRoot) {
|
|
1041
|
+
return {
|
|
1042
|
+
success: false,
|
|
1043
|
+
message: 'No merkle root calculated from archive contents - cannot verify',
|
|
1044
|
+
errorDetails: {
|
|
1045
|
+
errorType: 'MERKLE_ERROR'
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
// SECURITY: Version validation with backward compatibility for legacy files
|
|
1050
|
+
// Strategy:
|
|
1051
|
+
// 1. If contractVersion is provided in metadata: Validate it matches on-chain version (strict)
|
|
1052
|
+
// 2. If contractVersion is missing (legacy files): Query on-chain version and validate it matches what we use
|
|
1053
|
+
const configToUse = { ...networkConfig };
|
|
1054
|
+
const isLegacyFile = !tokenMetadata.contractVersion || tokenMetadata.contractVersion === 'unknown';
|
|
1055
|
+
if (isLegacyFile) {
|
|
1056
|
+
if (this.debug) {
|
|
1057
|
+
console.log(`[DEBUG] Legacy file detected (missing contractVersion) - will query on-chain version for validation`);
|
|
1058
|
+
}
|
|
1059
|
+
configToUse.version = 'unknown'; // This will trigger fallback logic in verifyOnChain
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
// New file with explicit version - validate it matches on-chain version
|
|
1063
|
+
if (this.debug) {
|
|
1064
|
+
console.log(`[DEBUG] File has explicit contractVersion: ${tokenMetadata.contractVersion} - will validate against on-chain version`);
|
|
1065
|
+
}
|
|
1066
|
+
// Keep the version from metadata for validation
|
|
1067
|
+
}
|
|
1068
|
+
// Perform on-chain verification - this will fetch the actual merkle root from the blockchain
|
|
1069
|
+
// This also validates version matches on-chain version
|
|
1070
|
+
const onChainResult = await this.verifyOnChain(tokenMetadata.tokenId, tokenMetadata.contractAddress, configToUse, calculatedMerkleRoot, rpcUrlIndex, options, // Pass options for RPC consensus settings
|
|
1071
|
+
tokenMetadata.contractVersion // Pass metadata version for validation
|
|
1072
|
+
);
|
|
1073
|
+
if (!onChainResult.success) {
|
|
1074
|
+
return {
|
|
1075
|
+
success: false,
|
|
1076
|
+
message: `Blockchain verification failed: ${onChainResult.error}`,
|
|
1077
|
+
errorDetails: {
|
|
1078
|
+
errorType: 'CONTRACT_ERROR',
|
|
1079
|
+
networkName: tokenMetadata.network,
|
|
1080
|
+
rpcUrl: onChainResult.rpcUrl || networkConfig.rpcUrls[rpcUrlIndex] || 'unknown',
|
|
1081
|
+
contractAddress: tokenMetadata.contractAddress,
|
|
1082
|
+
tokenId: tokenMetadata.tokenId
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
// Compare calculated merkle root with on-chain merkle root
|
|
1087
|
+
if (!onChainResult.onChainMerkleRoot) {
|
|
1088
|
+
return {
|
|
1089
|
+
success: false,
|
|
1090
|
+
message: 'Blockchain verification failed - could not retrieve on-chain merkle root',
|
|
1091
|
+
errorDetails: {
|
|
1092
|
+
errorType: 'CONTRACT_ERROR',
|
|
1093
|
+
tokenId: tokenMetadata.tokenId,
|
|
1094
|
+
contractAddress: tokenMetadata.contractAddress
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
// This is the critical security check: calculated root must match on-chain root
|
|
1099
|
+
// Direct string comparison (case-sensitive) - merkle roots are hex strings and must match exactly
|
|
1100
|
+
const onChainRoot = onChainResult.onChainMerkleRoot;
|
|
1101
|
+
const calculatedRoot = calculatedMerkleRoot;
|
|
1102
|
+
if (onChainRoot !== calculatedRoot) {
|
|
1103
|
+
return {
|
|
1104
|
+
success: false,
|
|
1105
|
+
message: 'Archive integrity verification FAILED - merkle root mismatch',
|
|
1106
|
+
errorDetails: {
|
|
1107
|
+
errorType: 'MERKLE_ERROR',
|
|
1108
|
+
calculatedMerkleRoot: calculatedMerkleRoot,
|
|
1109
|
+
onChainMerkleRoot: onChainResult.onChainMerkleRoot,
|
|
1110
|
+
merkleRoot: `calculated: ${calculatedMerkleRoot}, on-chain: ${onChainResult.onChainMerkleRoot}` // Keep for backward compatibility
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
if (!onChainResult.isValid) {
|
|
1115
|
+
return {
|
|
1116
|
+
success: false,
|
|
1117
|
+
message: 'Blockchain verification FAILED - contract verification returned false',
|
|
1118
|
+
errorDetails: {
|
|
1119
|
+
errorType: 'CONTRACT_ERROR',
|
|
1120
|
+
tokenId: tokenMetadata.tokenId,
|
|
1121
|
+
contractAddress: tokenMetadata.contractAddress,
|
|
1122
|
+
merkleRoot: calculatedMerkleRoot
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
// Success - create verification details
|
|
1127
|
+
// CRITICAL: Use ONLY blockchain data for display, not metadata which can be tampered with
|
|
1128
|
+
const mintDate = onChainResult.onChainTokenizationTime
|
|
1129
|
+
? new Date(onChainResult.onChainTokenizationTime * 1000).toLocaleString()
|
|
1130
|
+
: (tokenMetadata.mintDate ||
|
|
1131
|
+
(tokenMetadata.creationTimestamp ? new Date(tokenMetadata.creationTimestamp * 1000).toLocaleString() : '') ||
|
|
1132
|
+
'Unknown'); // Use on-chain tokenization time as source of truth
|
|
1133
|
+
return {
|
|
1134
|
+
success: true,
|
|
1135
|
+
message: 'Archive integrity VERIFIED against blockchain records',
|
|
1136
|
+
tokenMetadata,
|
|
1137
|
+
verificationDetails: {
|
|
1138
|
+
// These identifiers come from metadata (needed to locate token), but are validated by blockchain query
|
|
1139
|
+
tokenId: tokenMetadata.tokenId,
|
|
1140
|
+
network: tokenMetadata.network,
|
|
1141
|
+
contractAddress: tokenMetadata.contractAddress,
|
|
1142
|
+
// CRITICAL: All verification data comes from blockchain, not metadata
|
|
1143
|
+
merkleRoot: onChainResult.onChainMerkleRoot || calculatedMerkleRoot, // On-chain value (source of truth)
|
|
1144
|
+
mintDate, // On-chain tokenizationTime converted to date string (source of truth)
|
|
1145
|
+
calculatedMerkleRoot: calculatedMerkleRoot || undefined,
|
|
1146
|
+
declaredMerkleRoot: onChainResult.onChainMerkleRoot || undefined, // On-chain value, not metadata
|
|
1147
|
+
merkleRootMatch: true, // Verified: calculated root matches on-chain root
|
|
1148
|
+
onChainValid: true
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
catch (error) {
|
|
1153
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown verification error';
|
|
1154
|
+
if (this.debug) {
|
|
1155
|
+
console.log(`[DEBUG] Verification process failed: ${errorMessage}`);
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
success: false,
|
|
1159
|
+
message: `Tokenization verification failed: ${errorMessage}`,
|
|
1160
|
+
errorDetails: {
|
|
1161
|
+
errorType: 'METADATA_ERROR'
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Verify token with retry logic and exponential backoff
|
|
1168
|
+
* @param tokenMetadata Token metadata to verify
|
|
1169
|
+
* @param calculatedMerkleRoot Calculated merkle root from archive
|
|
1170
|
+
* @param maxRetries Maximum number of retry attempts (default: uses instance maxRetries)
|
|
1171
|
+
* @returns Enhanced verification result with retry information
|
|
1172
|
+
*/
|
|
1173
|
+
async verifyTokenWithRetry(tokenMetadata, calculatedMerkleRoot, maxRetries) {
|
|
1174
|
+
const startTime = Date.now();
|
|
1175
|
+
const retries = maxRetries || this.maxRetries;
|
|
1176
|
+
let lastError = null;
|
|
1177
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
1178
|
+
try {
|
|
1179
|
+
console.log(`🔄 Verification attempt ${attempt}/${retries} for token ${tokenMetadata.tokenId}`);
|
|
1180
|
+
const result = await this.verifyToken(tokenMetadata, calculatedMerkleRoot);
|
|
1181
|
+
if (result.success) {
|
|
1182
|
+
console.log(`✅ Verification successful on attempt ${attempt} for token ${tokenMetadata.tokenId}`);
|
|
1183
|
+
return {
|
|
1184
|
+
...result,
|
|
1185
|
+
retryAttempts: attempt,
|
|
1186
|
+
processingTime: Date.now() - startTime,
|
|
1187
|
+
timestamp: Date.now()
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
lastError = new Error(result.message || 'Verification failed');
|
|
1191
|
+
}
|
|
1192
|
+
catch (error) {
|
|
1193
|
+
lastError = error instanceof Error ? error : new Error('Unknown verification error');
|
|
1194
|
+
console.warn(`❌ Verification attempt ${attempt} failed for token ${tokenMetadata.tokenId}:`, lastError.message);
|
|
1195
|
+
if (attempt < retries) {
|
|
1196
|
+
const delay = this.calculateRetryDelay(attempt);
|
|
1197
|
+
console.log(`⏳ Waiting ${delay}ms before retry...`);
|
|
1198
|
+
await this.delay(delay);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
success: false,
|
|
1204
|
+
message: `Verification failed after ${retries} attempts: ${lastError?.message || 'Unknown error'}`,
|
|
1205
|
+
retryAttempts: retries,
|
|
1206
|
+
processingTime: Date.now() - startTime,
|
|
1207
|
+
timestamp: Date.now()
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Batch verification for multiple tokens with rate limiting
|
|
1212
|
+
* @param tokens Array of token metadata and merkle roots to verify
|
|
1213
|
+
* @param options Batch processing options
|
|
1214
|
+
* @returns Batch verification result
|
|
1215
|
+
*/
|
|
1216
|
+
async verifyBatch(tokens, options = {}) {
|
|
1217
|
+
const startTime = Date.now();
|
|
1218
|
+
const { parallel = false, maxConcurrent = 3 } = options;
|
|
1219
|
+
console.log(`🔄 Starting batch verification for ${tokens.length} tokens (parallel: ${parallel})`);
|
|
1220
|
+
let results = [];
|
|
1221
|
+
if (parallel) {
|
|
1222
|
+
// Parallel processing with concurrency limit
|
|
1223
|
+
results = await this.verifyBatchParallel(tokens, maxConcurrent);
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
// Sequential processing with rate limiting
|
|
1227
|
+
results = await this.verifyBatchSequential(tokens);
|
|
1228
|
+
}
|
|
1229
|
+
const successfulVerifications = results.filter(r => r.success).length;
|
|
1230
|
+
const failedVerifications = results.length - successfulVerifications;
|
|
1231
|
+
console.log(`✅ Batch verification completed: ${successfulVerifications}/${tokens.length} successful`);
|
|
1232
|
+
return {
|
|
1233
|
+
success: failedVerifications === 0,
|
|
1234
|
+
totalTokens: tokens.length,
|
|
1235
|
+
successfulVerifications,
|
|
1236
|
+
failedVerifications,
|
|
1237
|
+
results,
|
|
1238
|
+
processingTime: Date.now() - startTime
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Verify tokens in parallel with concurrency control
|
|
1243
|
+
*/
|
|
1244
|
+
async verifyBatchParallel(tokens, maxConcurrent) {
|
|
1245
|
+
const results = [];
|
|
1246
|
+
const chunks = [];
|
|
1247
|
+
// Split tokens into chunks for parallel processing
|
|
1248
|
+
for (let i = 0; i < tokens.length; i += maxConcurrent) {
|
|
1249
|
+
chunks.push(tokens.slice(i, i + maxConcurrent));
|
|
1250
|
+
}
|
|
1251
|
+
// Process each chunk in parallel
|
|
1252
|
+
for (const chunk of chunks) {
|
|
1253
|
+
const chunkPromises = chunk.map(token => this.verifyTokenWithRetry(token.tokenMetadata, token.calculatedMerkleRoot));
|
|
1254
|
+
const chunkResults = await Promise.all(chunkPromises);
|
|
1255
|
+
results.push(...chunkResults);
|
|
1256
|
+
// Add delay between chunks to avoid overwhelming the network
|
|
1257
|
+
if (chunks.indexOf(chunk) < chunks.length - 1) {
|
|
1258
|
+
await this.delay(this.batchDelay);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return results;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Verify tokens sequentially with rate limiting
|
|
1265
|
+
*/
|
|
1266
|
+
async verifyBatchSequential(tokens) {
|
|
1267
|
+
const results = [];
|
|
1268
|
+
for (const token of tokens) {
|
|
1269
|
+
const result = await this.verifyTokenWithRetry(token.tokenMetadata, token.calculatedMerkleRoot);
|
|
1270
|
+
results.push(result);
|
|
1271
|
+
// Add delay between verifications to avoid rate limiting
|
|
1272
|
+
if (tokens.indexOf(token) < tokens.length - 1) {
|
|
1273
|
+
await this.delay(this.batchDelay);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return results;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Process verification jobs with priority queue
|
|
1280
|
+
* @param jobs Array of verification jobs
|
|
1281
|
+
* @param options Processing options
|
|
1282
|
+
* @returns Batch verification result
|
|
1283
|
+
*/
|
|
1284
|
+
async processVerificationJobs(jobs, options = {}) {
|
|
1285
|
+
const { maxConcurrent = 3, priorityOrder = true, getTokenMetadata, getCalculatedMerkleRoot } = options;
|
|
1286
|
+
if (!getTokenMetadata || !getCalculatedMerkleRoot) {
|
|
1287
|
+
return {
|
|
1288
|
+
success: false,
|
|
1289
|
+
totalTokens: jobs.length,
|
|
1290
|
+
successfulVerifications: 0,
|
|
1291
|
+
failedVerifications: jobs.length,
|
|
1292
|
+
results: [],
|
|
1293
|
+
processingTime: 0
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
// Sort jobs by priority if requested
|
|
1297
|
+
const sortedJobs = priorityOrder
|
|
1298
|
+
? this.sortJobsByPriority(jobs)
|
|
1299
|
+
: jobs;
|
|
1300
|
+
// Convert jobs to token format
|
|
1301
|
+
const tokens = [];
|
|
1302
|
+
for (const job of sortedJobs) {
|
|
1303
|
+
const tokenMetadata = await getTokenMetadata(job.tokenId);
|
|
1304
|
+
const calculatedMerkleRoot = await getCalculatedMerkleRoot(job.tokenId);
|
|
1305
|
+
if (tokenMetadata) {
|
|
1306
|
+
tokens.push({
|
|
1307
|
+
tokenMetadata,
|
|
1308
|
+
calculatedMerkleRoot: calculatedMerkleRoot || null
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return this.verifyBatch(tokens, { parallel: true, maxConcurrent });
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Sort verification jobs by priority
|
|
1316
|
+
*/
|
|
1317
|
+
sortJobsByPriority(jobs) {
|
|
1318
|
+
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
|
1319
|
+
return jobs.sort((a, b) => {
|
|
1320
|
+
const aPriority = priorityOrder[a.priority || 'normal'];
|
|
1321
|
+
const bPriority = priorityOrder[b.priority || 'normal'];
|
|
1322
|
+
if (aPriority !== bPriority) {
|
|
1323
|
+
return aPriority - bPriority;
|
|
1324
|
+
}
|
|
1325
|
+
// If same priority, sort by retry count (fewer retries first)
|
|
1326
|
+
return (a.retryCount || 0) - (b.retryCount || 0);
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Calculate retry delay with exponential backoff
|
|
1331
|
+
*/
|
|
1332
|
+
calculateRetryDelay(attempt) {
|
|
1333
|
+
return Math.min(this.retryDelay * Math.pow(2, attempt - 1), 30000); // Max 30 seconds
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Utility method for delays
|
|
1337
|
+
*/
|
|
1338
|
+
async delay(ms) {
|
|
1339
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Get verification statistics
|
|
1343
|
+
*/
|
|
1344
|
+
getVerificationStats() {
|
|
1345
|
+
return {
|
|
1346
|
+
maxRetries: this.maxRetries,
|
|
1347
|
+
retryDelay: this.retryDelay,
|
|
1348
|
+
batchDelay: this.batchDelay
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Update verification configuration
|
|
1353
|
+
*/
|
|
1354
|
+
updateConfig(config) {
|
|
1355
|
+
if (config.maxRetries !== undefined) {
|
|
1356
|
+
this.maxRetries = config.maxRetries;
|
|
1357
|
+
}
|
|
1358
|
+
if (config.retryDelay !== undefined) {
|
|
1359
|
+
this.retryDelay = config.retryDelay;
|
|
1360
|
+
}
|
|
1361
|
+
if (config.batchDelay !== undefined) {
|
|
1362
|
+
this.batchDelay = config.batchDelay;
|
|
1363
|
+
}
|
|
1364
|
+
if (this.debug) {
|
|
1365
|
+
console.log('🔧 ZipkitVerifier configuration updated:', this.getVerificationStats());
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
exports.ZipkitVerifier = ZipkitVerifier;
|
|
1370
|
+
//# sourceMappingURL=ZipkitVerifier.js.map
|