@0xpolygonid/js-sdk 1.30.0 → 1.31.3
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/browser/esm/index.js +972 -367
- package/dist/browser/esm/index.js.map +1 -1
- package/dist/browser/umd/index.js +975 -365
- package/dist/browser/umd/index.js.map +1 -1
- package/dist/node/cjs/credentials/credential-wallet.js +1 -1
- package/dist/node/cjs/credentials/status/on-chain-revocation.js +3 -10
- package/dist/node/cjs/iden3comm/handlers/common.js +51 -24
- package/dist/node/cjs/identity/identity-wallet.js +29 -16
- package/dist/node/cjs/proof/proof-service.js +5 -4
- package/dist/node/cjs/proof/verifiers/query.js +25 -1
- package/dist/node/cjs/schema-processor/json/validator.js +11 -0
- package/dist/node/cjs/storage/blockchain/onchain-zkp-verifier.js +0 -1
- package/dist/node/cjs/storage/blockchain/state.js +120 -3
- package/dist/node/cjs/storage/memory/cache-lru.js +25 -0
- package/dist/node/cjs/storage/memory/index.js +1 -0
- package/dist/node/cjs/storage/shared/identity-storage.js +7 -0
- package/dist/node/cjs/verifiable/constants.js +26 -2
- package/dist/node/cjs/verifiable/presentation.js +3 -3
- package/dist/node/esm/credentials/credential-wallet.js +1 -1
- package/dist/node/esm/credentials/status/on-chain-revocation.js +4 -11
- package/dist/node/esm/iden3comm/handlers/common.js +51 -24
- package/dist/node/esm/identity/identity-wallet.js +29 -16
- package/dist/node/esm/proof/proof-service.js +6 -5
- package/dist/node/esm/proof/verifiers/query.js +21 -0
- package/dist/node/esm/schema-processor/json/validator.js +11 -0
- package/dist/node/esm/storage/blockchain/onchain-zkp-verifier.js +0 -1
- package/dist/node/esm/storage/blockchain/state.js +120 -3
- package/dist/node/esm/storage/memory/cache-lru.js +18 -0
- package/dist/node/esm/storage/memory/index.js +1 -0
- package/dist/node/esm/storage/shared/identity-storage.js +7 -0
- package/dist/node/esm/verifiable/constants.js +25 -1
- package/dist/node/esm/verifiable/presentation.js +3 -3
- package/dist/types/credentials/status/on-chain-revocation.d.ts +6 -2
- package/dist/types/iden3comm/types/protocol/auth.d.ts +2 -2
- package/dist/types/identity/identity-wallet.d.ts +18 -3
- package/dist/types/proof/verifiers/query.d.ts +3 -0
- package/dist/types/storage/blockchain/state.d.ts +65 -1
- package/dist/types/storage/entities/identity.d.ts +1 -0
- package/dist/types/storage/interfaces/identity.d.ts +8 -1
- package/dist/types/storage/memory/cache-lru.d.ts +10 -0
- package/dist/types/storage/memory/index.d.ts +1 -0
- package/dist/types/storage/shared/identity-storage.d.ts +5 -0
- package/dist/types/verifiable/constants.d.ts +21 -0
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DisplayMethodType = exports.PaymentFeatures = exports.SupportedCurrencies = exports.SupportedPaymentProofType = exports.PaymentType = exports.PaymentRequestDataType = exports.RefreshServiceType = exports.SubjectPosition = exports.MerklizedRootPosition = exports.ProofPurpose = exports.CredentialStatusType = exports.ProofType = exports.VerifiableConstants = void 0;
|
|
3
|
+
exports.DEFAULT_CACHE_MAX_SIZE = exports.DisplayMethodType = exports.PaymentFeatures = exports.SupportedCurrencies = exports.SupportedPaymentProofType = exports.PaymentType = exports.PaymentRequestDataType = exports.RefreshServiceType = exports.SubjectPosition = exports.MerklizedRootPosition = exports.ProofPurpose = exports.CredentialStatusType = exports.ProofType = exports.VerifiableConstants = void 0;
|
|
4
4
|
/** Verifiable constants */
|
|
5
5
|
exports.VerifiableConstants = Object.freeze({
|
|
6
6
|
ERRORS: {
|
|
@@ -8,7 +8,27 @@ exports.VerifiableConstants = Object.freeze({
|
|
|
8
8
|
CONTEXT_TYPE_IS_EMPTY: 'ctxType is empty',
|
|
9
9
|
// ErrStateNotFound issuer state is genesis state.
|
|
10
10
|
IDENTITY_DOES_NOT_EXIST: 'Identity does not exist',
|
|
11
|
-
NO_AUTH_CRED_FOUND: 'no auth credentials found'
|
|
11
|
+
NO_AUTH_CRED_FOUND: 'no auth credentials found',
|
|
12
|
+
// identity wallet
|
|
13
|
+
ID_WALLET_NO_CREDENTIAL_SATISFIED_QUERY: 'no credential satisfied query',
|
|
14
|
+
ID_WALLET_SIGNER_IS_REQUIRED: 'Ethereum signer is required to create Ethereum identities in order to transit state',
|
|
15
|
+
ID_WALLET_PROVER_IS_REQUIRED: 'prover is required to generate proofs for non ethereum identities',
|
|
16
|
+
ID_WALLET_CORE_CLAIM_REQUIRED_IN_SIG_PROOF: 'credential must have coreClaim representation in the signature proof',
|
|
17
|
+
ID_WALLET_CORE_CLAIM_REQUIRED_IN_ANY_PROOF: 'credential must have coreClaim representation in proofs',
|
|
18
|
+
ID_WALLET_CORE_CLAIM_MISMATCH: 'core claim representations is set in both proofs but they are not equal',
|
|
19
|
+
ID_WALLET_CORE_CLAIM_IS_NOT_SET: 'core claim is not set in credential proofs',
|
|
20
|
+
ID_WALLET_PROFILE_OR_IDENTITY_NOT_FOUND: 'profile or identity not found',
|
|
21
|
+
ID_WALLET_PROFILE_ALREADY_EXISTS: 'profile with given nonce or verifier already exists',
|
|
22
|
+
ID_WALLET_PROFILE_ALREADY_EXISTS_VERIFIER_TAGS: 'profile with given verifier and tags already exists',
|
|
23
|
+
ID_WALLET_ISSUER_AUTH_BJJ_CRED_MUST_HAVE_ANY_PROOF: 'issuer auth credential must have proof',
|
|
24
|
+
ID_WALLET_ISSUER_AUTH_BJJ_CRED_MUST_HAVE_MTP_PROOF: 'mtp is required for auth bjj key to issue new credentials',
|
|
25
|
+
// proof service
|
|
26
|
+
PROOF_SERVICE_NO_CREDENTIAL_FOR_IDENTITY_OR_PROFILE: 'no credentials belong to did or its profiles',
|
|
27
|
+
PROOF_SERVICE_NO_CREDENTIAL_FOR_QUERY: 'credential not found for query',
|
|
28
|
+
PROOF_SERVICE_PROFILE_GENESIS_DID_MISMATCH: 'subject and auth profiles are not derived from the same did',
|
|
29
|
+
PROOF_SERVICE_NO_QUERIES_IN_ZKP_REQUEST: 'no queries in zkp request',
|
|
30
|
+
// credential wallet
|
|
31
|
+
CREDENTIAL_WALLET_ALL_CREDENTIALS_ARE_REVOKED: 'all claims are revoked'
|
|
12
32
|
},
|
|
13
33
|
CREDENTIAL_TYPE: {
|
|
14
34
|
// VerifiableCredential is a W3C verifiable credential type
|
|
@@ -181,3 +201,7 @@ var DisplayMethodType;
|
|
|
181
201
|
(function (DisplayMethodType) {
|
|
182
202
|
DisplayMethodType["Iden3BasicDisplayMethodV1"] = "Iden3BasicDisplayMethodV1";
|
|
183
203
|
})(DisplayMethodType = exports.DisplayMethodType || (exports.DisplayMethodType = {}));
|
|
204
|
+
/**
|
|
205
|
+
* Default cache max size for in-memory cache
|
|
206
|
+
*/
|
|
207
|
+
exports.DEFAULT_CACHE_MAX_SIZE = 10000;
|
|
@@ -47,12 +47,12 @@ const createVerifiablePresentation = (context, tp, credential, queries) => {
|
|
|
47
47
|
}
|
|
48
48
|
const skeleton = {
|
|
49
49
|
'@context': baseContext,
|
|
50
|
-
|
|
50
|
+
type: constants_1.VerifiableConstants.CREDENTIAL_TYPE.W3C_VERIFIABLE_PRESENTATION,
|
|
51
51
|
verifiableCredential: {
|
|
52
52
|
'@context': ldContext,
|
|
53
|
-
|
|
53
|
+
type: vcTypes,
|
|
54
54
|
credentialSubject: {
|
|
55
|
-
|
|
55
|
+
type: tp
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OnChainRevocationStorage } from '../../storage/blockchain/onchain-revocation';
|
|
2
|
-
import { DID
|
|
2
|
+
import { DID } from '@iden3/js-iden3-core';
|
|
3
3
|
import { VerifiableConstants } from '../../verifiable/constants';
|
|
4
4
|
import { isGenesisState } from '../../utils';
|
|
5
5
|
import { EthStateStorage } from '../../storage/blockchain/state';
|
|
@@ -18,8 +18,9 @@ export class OnChainResolver {
|
|
|
18
18
|
* @public
|
|
19
19
|
* @param {Array<EthConnectionConfig>} _configs - list of ethereum network connections
|
|
20
20
|
*/
|
|
21
|
-
constructor(_configs) {
|
|
21
|
+
constructor(_configs, _opts) {
|
|
22
22
|
this._configs = _configs;
|
|
23
|
+
this._stateStorage = new EthStateStorage(_configs, _opts?.stateStorageOptions);
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
26
|
* resolve is a method to resolve a credential status from the blockchain.
|
|
@@ -50,8 +51,7 @@ export class OnChainResolver {
|
|
|
50
51
|
const issuerId = DID.idFromDID(issuer);
|
|
51
52
|
let latestIssuerState;
|
|
52
53
|
try {
|
|
53
|
-
const
|
|
54
|
-
const latestStateInfo = await ethStorage.getLatestStateById(issuerId.bigInt());
|
|
54
|
+
const latestStateInfo = await this._stateStorage.getLatestStateById(issuerId.bigInt());
|
|
55
55
|
if (!latestStateInfo.state) {
|
|
56
56
|
throw new Error('state contract returned empty state');
|
|
57
57
|
}
|
|
@@ -117,13 +117,6 @@ export class OnChainResolver {
|
|
|
117
117
|
}
|
|
118
118
|
return network;
|
|
119
119
|
}
|
|
120
|
-
// TODO: is dirty hack for mock in tests.
|
|
121
|
-
// need to pass to constructor list of state stores not list of network configs
|
|
122
|
-
_getStateStorageForIssuer(issuerId) {
|
|
123
|
-
const issuerChainId = getChainId(DID.blockchainFromId(issuerId), DID.networkIdFromId(issuerId));
|
|
124
|
-
const ethStorage = new EthStateStorage(this.networkByChainId(issuerChainId));
|
|
125
|
-
return ethStorage;
|
|
126
|
-
}
|
|
127
120
|
_getOnChainRevocationStorageForIssuer(chainId, contractAddress) {
|
|
128
121
|
const networkConfig = this.networkByChainId(chainId);
|
|
129
122
|
const onChainCaller = new OnChainRevocationStorage(networkConfig, contractAddress);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getRandomBytes } from '@iden3/js-crypto';
|
|
2
2
|
import { AuthMethod } from '../types';
|
|
3
3
|
import { mergeObjects } from '../../utils';
|
|
4
|
+
import { VerifiableConstants } from '../../verifiable';
|
|
4
5
|
import { getUnixTimestamp } from '@iden3/js-iden3-core';
|
|
5
6
|
import { AcceptJwsAlgorithms, defaultAcceptProfile, MediaType } from '../constants';
|
|
6
7
|
import { ethers } from 'ethers';
|
|
@@ -52,34 +53,60 @@ export const processZeroKnowledgeProofRequests = async (to, requests, from, proo
|
|
|
52
53
|
const groupedCredentialsCache = new Map();
|
|
53
54
|
const zkpResponses = [];
|
|
54
55
|
for (const proofReq of requestScope) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
throw new Error(`Invalid group id ${query.groupId}`);
|
|
56
|
+
let zkpRes;
|
|
57
|
+
try {
|
|
58
|
+
const isCircuitSupported = opts.supportedCircuits.includes(proofReq.circuitId);
|
|
59
|
+
if (!isCircuitSupported) {
|
|
60
|
+
if (proofReq.optional) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`Circuit ${proofReq.circuitId} is not allowed`);
|
|
64
64
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
const query = proofReq.query;
|
|
66
|
+
const groupId = query.groupId;
|
|
67
|
+
const combinedQueryData = combinedQueries.get(groupId);
|
|
68
|
+
if (groupId) {
|
|
69
|
+
if (!combinedQueryData) {
|
|
70
|
+
throw new Error(`Invalid group id ${query.groupId}`);
|
|
71
|
+
}
|
|
72
|
+
const combinedQuery = combinedQueryData.query;
|
|
73
|
+
if (!groupedCredentialsCache.has(groupId)) {
|
|
74
|
+
const credWithRevStatus = await proofService.findCredentialByProofQuery(to, combinedQueryData.query);
|
|
75
|
+
if (!credWithRevStatus.cred) {
|
|
76
|
+
if (proofReq.optional) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(VerifiableConstants.ERRORS.PROOF_SERVICE_NO_CREDENTIAL_FOR_QUERY +
|
|
80
|
+
`${JSON.stringify(combinedQuery)}`);
|
|
81
|
+
}
|
|
82
|
+
groupedCredentialsCache.set(groupId, credWithRevStatus);
|
|
70
83
|
}
|
|
71
|
-
groupedCredentialsCache.set(groupId, credWithRevStatus);
|
|
72
84
|
}
|
|
85
|
+
const credWithRevStatus = groupedCredentialsCache.get(groupId);
|
|
86
|
+
zkpRes = await proofService.generateProof(proofReq, to, {
|
|
87
|
+
verifierDid: from,
|
|
88
|
+
challenge: opts.challenge,
|
|
89
|
+
skipRevocation: Boolean(query.skipClaimRevocationCheck),
|
|
90
|
+
credential: credWithRevStatus?.cred,
|
|
91
|
+
credentialRevocationStatus: credWithRevStatus?.revStatus,
|
|
92
|
+
linkNonce: combinedQueryData?.linkNonce ? BigInt(combinedQueryData.linkNonce) : undefined
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const expectedErrors = [
|
|
97
|
+
VerifiableConstants.ERRORS.PROOF_SERVICE_NO_CREDENTIAL_FOR_IDENTITY_OR_PROFILE,
|
|
98
|
+
VerifiableConstants.ERRORS.ID_WALLET_NO_CREDENTIAL_SATISFIED_QUERY,
|
|
99
|
+
VerifiableConstants.ERRORS.CREDENTIAL_WALLET_ALL_CREDENTIALS_ARE_REVOKED
|
|
100
|
+
];
|
|
101
|
+
// handle only errors in case credential is not found and it is optional proof request - otherwise throw
|
|
102
|
+
if (error instanceof Error &&
|
|
103
|
+
(expectedErrors.includes(error.message) ||
|
|
104
|
+
error.message.includes(VerifiableConstants.ERRORS.PROOF_SERVICE_NO_CREDENTIAL_FOR_QUERY)) &&
|
|
105
|
+
proofReq.optional) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
73
109
|
}
|
|
74
|
-
const credWithRevStatus = groupedCredentialsCache.get(groupId);
|
|
75
|
-
const zkpRes = await proofService.generateProof(proofReq, to, {
|
|
76
|
-
verifierDid: from,
|
|
77
|
-
challenge: opts.challenge,
|
|
78
|
-
skipRevocation: Boolean(query.skipClaimRevocationCheck),
|
|
79
|
-
credential: credWithRevStatus?.cred,
|
|
80
|
-
credentialRevocationStatus: credWithRevStatus?.revStatus,
|
|
81
|
-
linkNonce: combinedQueryData?.linkNonce ? BigInt(combinedQueryData.linkNonce) : undefined
|
|
82
|
-
});
|
|
83
110
|
zkpResponses.push(zkpRes);
|
|
84
111
|
}
|
|
85
112
|
return zkpResponses;
|
|
@@ -203,7 +203,7 @@ export class IdentityWallet {
|
|
|
203
203
|
let credential;
|
|
204
204
|
const ethSigner = opts.ethSigner;
|
|
205
205
|
if (opts.createBjjCredential && !ethSigner) {
|
|
206
|
-
throw new Error(
|
|
206
|
+
throw new Error(VerifiableConstants.ERRORS.ID_WALLET_SIGNER_IS_REQUIRED);
|
|
207
207
|
}
|
|
208
208
|
const currentState = ZERO_HASH; // In Ethereum identities we don't have an initial state with the auth credential
|
|
209
209
|
const didType = buildDIDType(opts.method || DidMethod.Iden3, opts.blockchain || Blockchain.Polygon, opts.networkId || NetworkId.Amoy);
|
|
@@ -241,23 +241,28 @@ export class IdentityWallet {
|
|
|
241
241
|
}
|
|
242
242
|
const profile = await this._storage.identity.getProfileById(did.string());
|
|
243
243
|
if (!profile) {
|
|
244
|
-
throw new Error(
|
|
244
|
+
throw new Error(VerifiableConstants.ERRORS.ID_WALLET_PROFILE_OR_IDENTITY_NOT_FOUND);
|
|
245
245
|
}
|
|
246
246
|
return { nonce: profile.nonce, genesisDID: DID.parse(profile.genesisIdentifier) };
|
|
247
247
|
}
|
|
248
248
|
/** {@inheritDoc IIdentityWallet.createProfile} */
|
|
249
|
-
async createProfile(did, nonce, verifier) {
|
|
249
|
+
async createProfile(did, nonce, verifier, tags) {
|
|
250
250
|
const profileDID = generateProfileDID(did, nonce);
|
|
251
251
|
const identityProfiles = await this._storage.identity.getProfilesByGenesisIdentifier(did.string());
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
254
|
-
throw new Error(
|
|
252
|
+
const profilesForTagAndVerifier = await this._storage.identity.getProfilesByVerifier(verifier, tags);
|
|
253
|
+
if (profilesForTagAndVerifier.length) {
|
|
254
|
+
throw new Error(VerifiableConstants.ERRORS.ID_WALLET_PROFILE_ALREADY_EXISTS_VERIFIER_TAGS);
|
|
255
|
+
}
|
|
256
|
+
const existingProfileWithNonce = identityProfiles.find((p) => p.nonce == nonce);
|
|
257
|
+
if (existingProfileWithNonce) {
|
|
258
|
+
throw new Error(VerifiableConstants.ERRORS.ID_WALLET_PROFILE_ALREADY_EXISTS);
|
|
255
259
|
}
|
|
256
260
|
await this._storage.identity.saveProfile({
|
|
257
261
|
id: profileDID.string(),
|
|
258
262
|
nonce,
|
|
259
263
|
genesisIdentifier: did.string(),
|
|
260
|
-
verifier
|
|
264
|
+
verifier,
|
|
265
|
+
tags
|
|
261
266
|
});
|
|
262
267
|
return profileDID;
|
|
263
268
|
}
|
|
@@ -276,9 +281,17 @@ export class IdentityWallet {
|
|
|
276
281
|
const key = await this._kms.createKeyFromSeed(keyType, getRandomBytes(32));
|
|
277
282
|
return key;
|
|
278
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* @deprecated The method should not be used. It returns only one profile per verifier, which can potentially restrict business use cases
|
|
286
|
+
* {@inheritDoc IIdentityWallet.getProfileByVerifier}
|
|
287
|
+
*/
|
|
279
288
|
async getProfileByVerifier(verifier) {
|
|
280
289
|
return this._storage.identity.getProfileByVerifier(verifier);
|
|
281
290
|
}
|
|
291
|
+
/** {@inheritDoc IIdentityWallet.getProfilesByVerifier} */
|
|
292
|
+
async getProfilesByVerifier(verifier, tags) {
|
|
293
|
+
return this._storage.identity.getProfilesByVerifier(verifier, tags);
|
|
294
|
+
}
|
|
282
295
|
/** {@inheritDoc IIdentityWallet.getDIDTreeModel} */
|
|
283
296
|
async getDIDTreeModel(did) {
|
|
284
297
|
const didStr = did.string();
|
|
@@ -400,11 +413,11 @@ export class IdentityWallet {
|
|
|
400
413
|
const coreClaimHash = poseidon.hash([hi, hv]);
|
|
401
414
|
const signature = await this.signChallenge(coreClaimHash, issuerAuthBJJCredential);
|
|
402
415
|
if (!issuerAuthBJJCredential.proof) {
|
|
403
|
-
throw new Error(
|
|
416
|
+
throw new Error(VerifiableConstants.ERRORS.ID_WALLET_ISSUER_AUTH_BJJ_CRED_MUST_HAVE_ANY_PROOF);
|
|
404
417
|
}
|
|
405
418
|
const mtpAuthBJJProof = issuerAuthBJJCredential.getIden3SparseMerkleTreeProof();
|
|
406
419
|
if (!mtpAuthBJJProof) {
|
|
407
|
-
throw new Error(
|
|
420
|
+
throw new Error(VerifiableConstants.ERRORS.ID_WALLET_ISSUER_AUTH_BJJ_CRED_MUST_HAVE_MTP_PROOF);
|
|
408
421
|
}
|
|
409
422
|
const sigProof = new BJJSignatureProof2021({
|
|
410
423
|
issuerData: {
|
|
@@ -444,7 +457,7 @@ export class IdentityWallet {
|
|
|
444
457
|
const issuerTree = await this.getDIDTreeModel(issuerDID);
|
|
445
458
|
const coreClaim = await this.getCoreClaimFromCredential(credential);
|
|
446
459
|
if (!coreClaim) {
|
|
447
|
-
throw new Error(
|
|
460
|
+
throw new Error(VerifiableConstants.ERRORS.ID_WALLET_CORE_CLAIM_REQUIRED_IN_ANY_PROOF);
|
|
448
461
|
}
|
|
449
462
|
const nonce = coreClaim.getRevocationNonce();
|
|
450
463
|
await issuerTree.revocationTree.add(nonce, BigInt(0));
|
|
@@ -467,7 +480,7 @@ export class IdentityWallet {
|
|
|
467
480
|
// credential must have a bjj signature proof
|
|
468
481
|
const coreClaim = credential.getCoreClaimFromProof(ProofType.BJJSignature);
|
|
469
482
|
if (!coreClaim) {
|
|
470
|
-
throw new Error(
|
|
483
|
+
throw new Error(VerifiableConstants.ERRORS.ID_WALLET_CORE_CLAIM_REQUIRED_IN_SIG_PROOF);
|
|
471
484
|
}
|
|
472
485
|
await this._storage.mt.addToMerkleTree(issuerDID.string(), MerkleTreeType.Claims, coreClaim.hIndex(), coreClaim.hValue());
|
|
473
486
|
}
|
|
@@ -499,7 +512,7 @@ export class IdentityWallet {
|
|
|
499
512
|
const coreClaim = credential.getCoreClaimFromProof(ProofType.BJJSignature) ||
|
|
500
513
|
(await credential.toCoreClaim(opts));
|
|
501
514
|
if (!coreClaim) {
|
|
502
|
-
throw new Error(
|
|
515
|
+
throw new Error(VerifiableConstants.ERRORS.ID_WALLET_CORE_CLAIM_REQUIRED_IN_SIG_PROOF);
|
|
503
516
|
}
|
|
504
517
|
const mtpWithProof = await this.generateCoreClaimMtp(issuerDID, coreClaim, treeState);
|
|
505
518
|
const mtpProof = new Iden3SparseMerkleTreeProof({
|
|
@@ -569,10 +582,10 @@ export class IdentityWallet {
|
|
|
569
582
|
if (coreClaimFromMtpProof &&
|
|
570
583
|
coreClaimFromSigProof &&
|
|
571
584
|
coreClaimFromMtpProof.hex() !== coreClaimFromSigProof.hex()) {
|
|
572
|
-
throw new Error(
|
|
585
|
+
throw new Error(VerifiableConstants.ERRORS.ID_WALLET_CORE_CLAIM_MISMATCH);
|
|
573
586
|
}
|
|
574
587
|
if (!coreClaimFromMtpProof && !coreClaimFromSigProof) {
|
|
575
|
-
throw new Error(
|
|
588
|
+
throw new Error(VerifiableConstants.ERRORS.ID_WALLET_CORE_CLAIM_IS_NOT_SET);
|
|
576
589
|
}
|
|
577
590
|
//eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
578
591
|
const coreClaim = coreClaimFromMtpProof ?? coreClaimFromSigProof;
|
|
@@ -581,7 +594,7 @@ export class IdentityWallet {
|
|
|
581
594
|
async findOwnedCredentialsByDID(did, query) {
|
|
582
595
|
const credentials = await this._credentialWallet.findByQuery(query);
|
|
583
596
|
if (!credentials.length) {
|
|
584
|
-
throw new Error(
|
|
597
|
+
throw new Error(VerifiableConstants.ERRORS.ID_WALLET_NO_CREDENTIAL_SATISFIED_QUERY);
|
|
585
598
|
}
|
|
586
599
|
const { genesisDID } = await this.getGenesisDIDMetadata(did);
|
|
587
600
|
const profiles = await this.getProfilesByDID(genesisDID);
|
|
@@ -621,7 +634,7 @@ export class IdentityWallet {
|
|
|
621
634
|
let txId;
|
|
622
635
|
if (!isEthIdentity) {
|
|
623
636
|
if (!prover) {
|
|
624
|
-
throw new Error(
|
|
637
|
+
throw new Error(VerifiableConstants.ERRORS.ID_WALLET_PROVER_IS_REQUIRED);
|
|
625
638
|
}
|
|
626
639
|
// generate the proof
|
|
627
640
|
const authInfo = await this._inputsGenerator.prepareAuthBJJCredential(did, oldTreeState);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BytesHelper, DID, MerklizedRootPosition, getDateFromUnixTimestamp } from '@iden3/js-iden3-core';
|
|
2
2
|
import { AuthV2Inputs, AuthV2PubSignals, CircuitId, Operators, Query, ValueProof } from '../circuits';
|
|
3
|
-
import { createVerifiablePresentation } from '../verifiable';
|
|
3
|
+
import { createVerifiablePresentation, VerifiableConstants } from '../verifiable';
|
|
4
4
|
import { parseCredentialSubject, parseQueryMetadata, toGISTProof, transformQueryValueToBigInts } from './common';
|
|
5
5
|
import { NativeProver } from './provers/prover';
|
|
6
6
|
import { getDocumentLoader } from '@iden3/js-jsonld-merklization';
|
|
@@ -73,7 +73,8 @@ export class ProofService {
|
|
|
73
73
|
credentialWithRevStatus = { cred: opts.credential, revStatus };
|
|
74
74
|
}
|
|
75
75
|
if (!credentialWithRevStatus.cred) {
|
|
76
|
-
throw new Error(
|
|
76
|
+
throw new Error(VerifiableConstants.ERRORS.PROOF_SERVICE_NO_CREDENTIAL_FOR_QUERY +
|
|
77
|
+
` ${JSON.stringify(proofReq.query)}`);
|
|
77
78
|
}
|
|
78
79
|
const credentialCoreClaim = await this._identityWallet.getCoreClaimFromCredential(credentialWithRevStatus.cred);
|
|
79
80
|
const { nonce: authProfileNonce, genesisDID } = await this._identityWallet.getGenesisDIDMetadata(identifier);
|
|
@@ -85,11 +86,11 @@ export class ProofService {
|
|
|
85
86
|
const subjectDID = DID.parse(preparedCredential.credential.credentialSubject['id']);
|
|
86
87
|
const { nonce: credentialSubjectProfileNonce, genesisDID: subjectGenesisDID } = await this._identityWallet.getGenesisDIDMetadata(subjectDID);
|
|
87
88
|
if (subjectGenesisDID.string() !== genesisDID.string()) {
|
|
88
|
-
throw new Error(
|
|
89
|
+
throw new Error(VerifiableConstants.ERRORS.PROOF_SERVICE_PROFILE_GENESIS_DID_MISMATCH);
|
|
89
90
|
}
|
|
90
91
|
const propertiesMetadata = parseCredentialSubject(proofReq.query.credentialSubject);
|
|
91
92
|
if (!propertiesMetadata.length) {
|
|
92
|
-
throw new Error(
|
|
93
|
+
throw new Error(VerifiableConstants.ERRORS.PROOF_SERVICE_NO_QUERIES_IN_ZKP_REQUEST);
|
|
93
94
|
}
|
|
94
95
|
const mtPosition = preparedCredential.credentialCoreClaim.getMerklizedPosition();
|
|
95
96
|
let mk;
|
|
@@ -273,7 +274,7 @@ export class ProofService {
|
|
|
273
274
|
async findCredentialByProofQuery(did, query) {
|
|
274
275
|
const credentials = await this._identityWallet.findOwnedCredentialsByDID(did, query);
|
|
275
276
|
if (!credentials.length) {
|
|
276
|
-
throw new Error(
|
|
277
|
+
throw new Error(VerifiableConstants.ERRORS.PROOF_SERVICE_NO_CREDENTIAL_FOR_IDENTITY_OR_PROFILE);
|
|
277
278
|
}
|
|
278
279
|
// For EQ / IN / NIN / LT / GT operations selective if credential satisfies query - we can get any.
|
|
279
280
|
// TODO: choose credential for selective credentials
|
|
@@ -4,6 +4,7 @@ import { byteEncoder } from '../../utils';
|
|
|
4
4
|
import { getOperatorNameByValue, Operators, QueryOperators } from '../../circuits/comparer';
|
|
5
5
|
import { calculateCoreSchemaHash, VerifiableConstants } from '../../verifiable';
|
|
6
6
|
import { circuitValidator } from '../provers';
|
|
7
|
+
import { ethers } from 'ethers';
|
|
7
8
|
const defaultProofGenerationDelayOpts = 24 * 60 * 60 * 1000; // 24 hours
|
|
8
9
|
export async function checkQueryRequest(query, queriesMetadata, ldContext, outputs, circuitId, schemaLoader, opts) {
|
|
9
10
|
// validate issuer
|
|
@@ -197,3 +198,23 @@ export const fieldValueFromVerifiablePresentation = async (fieldName, verifiable
|
|
|
197
198
|
}
|
|
198
199
|
return await value.mtEntry();
|
|
199
200
|
};
|
|
201
|
+
export function calculateGroupId(requestIds) {
|
|
202
|
+
const types = Array(requestIds.length).fill('uint256');
|
|
203
|
+
const groupID = BigInt(ethers.keccak256(ethers.solidityPacked(types, requestIds))) &
|
|
204
|
+
// It should fit in a field number in the circuit (max 253 bits). With this we truncate to 252 bits for the group ID
|
|
205
|
+
BigInt('0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF');
|
|
206
|
+
return groupID;
|
|
207
|
+
}
|
|
208
|
+
export function calculateRequestId(requestParams, creatorAddress) {
|
|
209
|
+
// 0x0000000000000000FFFF...FF. Reserved first 8 bytes for the request Id type and future use
|
|
210
|
+
// 0x00010000000000000000...00. First 2 bytes for the request Id type
|
|
211
|
+
// - 0x0000... for old request Ids with uint64
|
|
212
|
+
// - 0x0001... for new request Ids with uint256
|
|
213
|
+
const requestId = (BigInt(ethers.keccak256(ethers.solidityPacked(['bytes', 'address'], [requestParams, creatorAddress]))) &
|
|
214
|
+
BigInt('0x0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF')) +
|
|
215
|
+
BigInt('0x0001000000000000000000000000000000000000000000000000000000000000');
|
|
216
|
+
return requestId;
|
|
217
|
+
}
|
|
218
|
+
export function calculateMultiRequestId(requestIds, groupIds, creatorAddress) {
|
|
219
|
+
return BigInt(ethers.keccak256(ethers.solidityPacked(['uint256[]', 'uint256[]', 'address'], [requestIds, groupIds, creatorAddress])));
|
|
220
|
+
}
|
|
@@ -38,6 +38,7 @@ export class JsonSchemaValidator {
|
|
|
38
38
|
validator = ajv ?? defaultJSONSchemaValidator;
|
|
39
39
|
if (validator.formats && !Object.keys(validator.formats).length) {
|
|
40
40
|
addFormats(validator);
|
|
41
|
+
addCustomFormats(validator);
|
|
41
42
|
}
|
|
42
43
|
const validate = (schema.$id ? validator.getSchema(schema.$id) : undefined) || validator.compile(schema);
|
|
43
44
|
const valid = validate(data);
|
|
@@ -48,3 +49,13 @@ export class JsonSchemaValidator {
|
|
|
48
49
|
return true;
|
|
49
50
|
}
|
|
50
51
|
}
|
|
52
|
+
function addCustomFormats(validator) {
|
|
53
|
+
validator.addFormat('positive-integer', {
|
|
54
|
+
type: 'string',
|
|
55
|
+
validate: (positiveIntegerStr) => /^[1-9]\d*$/.test(positiveIntegerStr)
|
|
56
|
+
});
|
|
57
|
+
validator.addFormat('non-negative-integer', {
|
|
58
|
+
type: 'string',
|
|
59
|
+
validate: (nonNegativeIntegerStr) => /^(0|[1-9]\d*)$/.test(nonNegativeIntegerStr)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { JsonRpcProvider, Contract, ethers } from 'ethers';
|
|
2
2
|
import { AuthMethod, processProofResponse } from '../../iden3comm';
|
|
3
3
|
import abi from './abi/ZkpVerifier.json';
|
|
4
|
-
//import { UniversalVerifier } from '@iden3/universal-verifier-v2-abi';
|
|
5
4
|
import { IVerifierABI } from '@iden3/universal-verifier-v2-abi';
|
|
6
5
|
import { TransactionService } from '../../blockchain';
|
|
7
6
|
import { BytesHelper, chainIDfromDID, DID, Id } from '@iden3/js-iden3-core';
|
|
@@ -5,6 +5,9 @@ import abi from './abi/State.json';
|
|
|
5
5
|
import { DID, getChainId, Id } from '@iden3/js-iden3-core';
|
|
6
6
|
import { TransactionService } from '../../blockchain';
|
|
7
7
|
import { prepareZkpProof } from './common';
|
|
8
|
+
import { createInMemoryCache } from '../memory';
|
|
9
|
+
import { PROTOCOL_CONSTANTS } from '../../iden3comm';
|
|
10
|
+
import { DEFAULT_CACHE_MAX_SIZE } from '../../verifiable';
|
|
8
11
|
export /** @type {EthConnectionConfig} - default configuration for EthConnectionConfig */ const defaultEthConnectionConfig = {
|
|
9
12
|
url: 'http://localhost:8545',
|
|
10
13
|
defaultGasLimit: 600000,
|
|
@@ -30,15 +33,71 @@ export class EthStateStorage {
|
|
|
30
33
|
* Creates an instance of EthStateStorage.
|
|
31
34
|
* @param {EthConnectionConfig} [ethConfig=defaultEthConnectionConfig]
|
|
32
35
|
*/
|
|
33
|
-
constructor(ethConfig) {
|
|
36
|
+
constructor(ethConfig, options) {
|
|
34
37
|
this.ethConfig = ethConfig;
|
|
38
|
+
this._disableCache = false;
|
|
35
39
|
const config = Array.isArray(ethConfig) ? ethConfig[0] : ethConfig;
|
|
36
40
|
this.provider = new JsonRpcProvider(config.url);
|
|
37
41
|
this.stateContract = new Contract(config.contractAddress, abi, this.provider);
|
|
38
42
|
this._transactionService = new TransactionService(this.getRpcProvider());
|
|
43
|
+
// Store cache options for later use
|
|
44
|
+
this._latestStateCacheOptions = {
|
|
45
|
+
ttl: options?.latestStateCacheOptions?.ttl ?? PROTOCOL_CONSTANTS.DEFAULT_PROOF_VERIFY_DELAY / 2,
|
|
46
|
+
maxSize: options?.latestStateCacheOptions?.maxSize ?? DEFAULT_CACHE_MAX_SIZE
|
|
47
|
+
};
|
|
48
|
+
this._stateCacheOptions = {
|
|
49
|
+
notReplacedTtl: options?.stateCacheOptions?.notReplacedTtl ??
|
|
50
|
+
PROTOCOL_CONSTANTS.DEFAULT_PROOF_VERIFY_DELAY / 2,
|
|
51
|
+
replacedTtl: options?.stateCacheOptions?.replacedTtl ?? PROTOCOL_CONSTANTS.DEFAULT_PROOF_VERIFY_DELAY,
|
|
52
|
+
maxSize: options?.stateCacheOptions?.maxSize ?? DEFAULT_CACHE_MAX_SIZE
|
|
53
|
+
};
|
|
54
|
+
this._rootCacheOptions = {
|
|
55
|
+
replacedTtl: options?.rootCacheOptions?.replacedTtl ?? PROTOCOL_CONSTANTS.DEFAULT_AUTH_VERIFY_DELAY,
|
|
56
|
+
notReplacedTtl: options?.rootCacheOptions?.notReplacedTtl ??
|
|
57
|
+
PROTOCOL_CONSTANTS.DEFAULT_AUTH_VERIFY_DELAY / 2,
|
|
58
|
+
maxSize: options?.rootCacheOptions?.maxSize ?? DEFAULT_CACHE_MAX_SIZE
|
|
59
|
+
};
|
|
60
|
+
this._gistProofCacheOptions = {
|
|
61
|
+
ttl: PROTOCOL_CONSTANTS.DEFAULT_AUTH_VERIFY_DELAY / 2,
|
|
62
|
+
maxSize: options?.gistProofCacheOptions?.maxSize ?? DEFAULT_CACHE_MAX_SIZE
|
|
63
|
+
};
|
|
64
|
+
// Initialize cache instances
|
|
65
|
+
this._latestStateResolveCache =
|
|
66
|
+
options?.latestStateCacheOptions?.cache ??
|
|
67
|
+
createInMemoryCache({
|
|
68
|
+
maxSize: this._latestStateCacheOptions.maxSize,
|
|
69
|
+
ttl: this._latestStateCacheOptions.ttl
|
|
70
|
+
});
|
|
71
|
+
this._stateResolveCache =
|
|
72
|
+
options?.stateCacheOptions?.cache ??
|
|
73
|
+
createInMemoryCache({
|
|
74
|
+
maxSize: this._stateCacheOptions.maxSize,
|
|
75
|
+
ttl: this._stateCacheOptions.replacedTtl
|
|
76
|
+
});
|
|
77
|
+
this._rootResolveCache =
|
|
78
|
+
options?.rootCacheOptions?.cache ??
|
|
79
|
+
createInMemoryCache({
|
|
80
|
+
maxSize: this._rootCacheOptions.maxSize,
|
|
81
|
+
ttl: this._rootCacheOptions.replacedTtl
|
|
82
|
+
});
|
|
83
|
+
this._gistProofResolveCache =
|
|
84
|
+
options?.gistProofCacheOptions?.cache ??
|
|
85
|
+
createInMemoryCache({
|
|
86
|
+
maxSize: this._gistProofCacheOptions.maxSize,
|
|
87
|
+
ttl: this._gistProofCacheOptions.ttl
|
|
88
|
+
});
|
|
89
|
+
this._disableCache = options?.disableCache ?? false;
|
|
39
90
|
}
|
|
40
91
|
/** {@inheritdoc IStateStorage.getLatestStateById} */
|
|
41
92
|
async getLatestStateById(id) {
|
|
93
|
+
const cacheKey = this.getLatestStateCacheKey(id);
|
|
94
|
+
if (!this._disableCache) {
|
|
95
|
+
// Check cache first
|
|
96
|
+
const cachedResult = await this._latestStateResolveCache?.get(cacheKey);
|
|
97
|
+
if (cachedResult) {
|
|
98
|
+
return cachedResult;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
42
101
|
const { stateContract } = this.getStateContractAndProviderForId(id);
|
|
43
102
|
const rawData = await stateContract.getStateInfoById(id);
|
|
44
103
|
const stateInfo = {
|
|
@@ -50,10 +109,20 @@ export class EthStateStorage {
|
|
|
50
109
|
createdAtBlock: BigInt(rawData[5]),
|
|
51
110
|
replacedAtBlock: BigInt(rawData[6])
|
|
52
111
|
};
|
|
112
|
+
!this._disableCache &&
|
|
113
|
+
(await this._latestStateResolveCache?.set(cacheKey, stateInfo, this._latestStateCacheOptions.ttl));
|
|
53
114
|
return stateInfo;
|
|
54
115
|
}
|
|
55
116
|
/** {@inheritdoc IStateStorage.getStateInfoByIdAndState} */
|
|
56
117
|
async getStateInfoByIdAndState(id, state) {
|
|
118
|
+
const cacheKey = this.getStateCacheKey(id, state);
|
|
119
|
+
if (!this._disableCache) {
|
|
120
|
+
// Check cache first
|
|
121
|
+
const cachedResult = await this._stateResolveCache?.get(cacheKey);
|
|
122
|
+
if (cachedResult) {
|
|
123
|
+
return cachedResult;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
57
126
|
const { stateContract } = this.getStateContractAndProviderForId(id);
|
|
58
127
|
const rawData = await stateContract.getStateInfoByIdAndState(id, state);
|
|
59
128
|
const stateInfo = {
|
|
@@ -65,6 +134,10 @@ export class EthStateStorage {
|
|
|
65
134
|
createdAtBlock: BigInt(rawData[5]),
|
|
66
135
|
replacedAtBlock: BigInt(rawData[6])
|
|
67
136
|
};
|
|
137
|
+
const ttl = stateInfo.replacedAtTimestamp === 0n
|
|
138
|
+
? this._stateCacheOptions.notReplacedTtl
|
|
139
|
+
: this._stateCacheOptions.replacedTtl;
|
|
140
|
+
!this._disableCache && (await this._stateResolveCache?.set(cacheKey, stateInfo, ttl));
|
|
68
141
|
return stateInfo;
|
|
69
142
|
}
|
|
70
143
|
/** {@inheritdoc IStateStorage.publishState} */
|
|
@@ -137,9 +210,17 @@ export class EthStateStorage {
|
|
|
137
210
|
}
|
|
138
211
|
/** {@inheritdoc IStateStorage.getGISTProof} */
|
|
139
212
|
async getGISTProof(id) {
|
|
213
|
+
const cacheKey = this.getGistProofCacheKey(id);
|
|
214
|
+
if (!this._disableCache) {
|
|
215
|
+
// Check cache first
|
|
216
|
+
const cachedResult = await this._gistProofResolveCache?.get(cacheKey);
|
|
217
|
+
if (cachedResult) {
|
|
218
|
+
return cachedResult;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
140
221
|
const { stateContract } = this.getStateContractAndProviderForId(id);
|
|
141
222
|
const data = await stateContract.getGISTProof(id);
|
|
142
|
-
|
|
223
|
+
const stateProof = {
|
|
143
224
|
root: BigInt(data.root.toString()),
|
|
144
225
|
existence: data.existence,
|
|
145
226
|
siblings: data.siblings?.map((sibling) => BigInt(sibling.toString())),
|
|
@@ -149,12 +230,23 @@ export class EthStateStorage {
|
|
|
149
230
|
auxIndex: BigInt(data.auxIndex.toString()),
|
|
150
231
|
auxValue: BigInt(data.auxValue.toString())
|
|
151
232
|
};
|
|
233
|
+
!this._disableCache &&
|
|
234
|
+
(await this._gistProofResolveCache?.set(cacheKey, stateProof, this._gistProofCacheOptions.ttl));
|
|
235
|
+
return stateProof;
|
|
152
236
|
}
|
|
153
237
|
/** {@inheritdoc IStateStorage.getGISTRootInfo} */
|
|
154
238
|
async getGISTRootInfo(root, id) {
|
|
239
|
+
const cacheKey = this.getRootCacheKey(root);
|
|
240
|
+
if (!this._disableCache) {
|
|
241
|
+
// Check cache first
|
|
242
|
+
const cachedResult = await this._rootResolveCache?.get(cacheKey);
|
|
243
|
+
if (cachedResult) {
|
|
244
|
+
return cachedResult;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
155
247
|
const { stateContract } = this.getStateContractAndProviderForId(id);
|
|
156
248
|
const data = await stateContract.getGISTRootInfo(root);
|
|
157
|
-
|
|
249
|
+
const rootInfo = {
|
|
158
250
|
root: BigInt(data.root.toString()),
|
|
159
251
|
replacedByRoot: BigInt(data.replacedByRoot.toString()),
|
|
160
252
|
createdAtTimestamp: BigInt(data.createdAtTimestamp.toString()),
|
|
@@ -162,11 +254,24 @@ export class EthStateStorage {
|
|
|
162
254
|
createdAtBlock: BigInt(data.createdAtBlock.toString()),
|
|
163
255
|
replacedAtBlock: BigInt(data.replacedAtBlock.toString())
|
|
164
256
|
};
|
|
257
|
+
const ttl = rootInfo.replacedAtTimestamp == 0n
|
|
258
|
+
? this._rootCacheOptions.notReplacedTtl
|
|
259
|
+
: this._rootCacheOptions.replacedTtl;
|
|
260
|
+
!this._disableCache && (await this._rootResolveCache?.set(cacheKey, rootInfo, ttl));
|
|
261
|
+
return rootInfo;
|
|
165
262
|
}
|
|
166
263
|
/** {@inheritdoc IStateStorage.getRpcProvider} */
|
|
167
264
|
getRpcProvider() {
|
|
168
265
|
return this.provider;
|
|
169
266
|
}
|
|
267
|
+
/** enable caching */
|
|
268
|
+
enableCache() {
|
|
269
|
+
this._disableCache = false;
|
|
270
|
+
}
|
|
271
|
+
/** disable caching */
|
|
272
|
+
disableCache() {
|
|
273
|
+
this._disableCache = true;
|
|
274
|
+
}
|
|
170
275
|
getStateContractAndProviderForId(id) {
|
|
171
276
|
const idTyped = Id.fromBigInt(id);
|
|
172
277
|
const chainId = getChainId(DID.blockchainFromId(idTyped), DID.networkIdFromId(idTyped));
|
|
@@ -183,4 +288,16 @@ export class EthStateStorage {
|
|
|
183
288
|
}
|
|
184
289
|
return network;
|
|
185
290
|
}
|
|
291
|
+
getGistProofCacheKey(id) {
|
|
292
|
+
return `gist-${id.toString()}`;
|
|
293
|
+
}
|
|
294
|
+
getLatestStateCacheKey(id) {
|
|
295
|
+
return `latest-${id.toString()}`;
|
|
296
|
+
}
|
|
297
|
+
getStateCacheKey(id, state) {
|
|
298
|
+
return `${id.toString()}-${state.toString()}`;
|
|
299
|
+
}
|
|
300
|
+
getRootCacheKey(root) {
|
|
301
|
+
return root.toString();
|
|
302
|
+
}
|
|
186
303
|
}
|