diff --git a/docs/pages/verifiable-presentations.md b/docs/pages/verifiable-presentations.md new file mode 100644 index 000000000..3aa46ec25 --- /dev/null +++ b/docs/pages/verifiable-presentations.md @@ -0,0 +1,289 @@ +# Verifiable Presentations (V1) + +This document describes how to create v1 verifiable presentations and how to verify them. + +**Table of Contents:** + +- [Build Statement](#build-statement) + - [Identity/account credential statements](#identityaccount-credential-statements) + - [Web3 ID credential statements](#web3-id-credential-statements) +- [JSON representation](#json-representation) +- [Verifiable Presentation Request (proof request)](#verifiable-presentation-request-proof-request) +- [Verifiable Presentation (proof)](#verifiable-presentation-proof) +- [Verifiable Audit Record](#verifiable-audit-record) + + +## Build Statement + +The SDK contains a helper to create statements about identities, which can +then be proven. + +To do so, use the CredentialStatementBuilder, to build a statement: + +{@codeblock ~~:nodejs/common/verifiable-credential-statements.ts#documentation-snippet} + +### Identity/account credential statements + +To build a statement against an identity credential, the builder has two different entrypoints, which +have an identical function signature, which consists of + +1. A list of identity providers that the identity must be created from +2. A callback function which should be used to add statements for the credential + +```ts +// used for proofs which are not tied to a specific account +builder.forIdentityCredentials([0,2], (build) => ...) +// used for proofs tied to an account created from the identity credential. +builder.forAccountCredentials([0,2], (build) => ... +``` + +Below are a set of functions accessible for the `build` object passed in the callback + +#### Minimum Age + +There is a helper function for specifying the prover must have some minimum +age. + +Example: add the statement that the prover must be born at least 18 years old: + +```ts + build.addMinimumAge(18); +``` + +#### Eu membership + +There are helpers for specifying the country of residency or nationality to +be one of the EU member states. + +```ts + build.addEUNationality(); + build.addEUResidency(); +``` + +#### Reveal statement + +State that a given attribute should be revealed as part of the proof. + +```ts + build.revealAttribute(AttributesKeys.nationality); +``` + +#### Range statement + +State that a given attribute should be between 2 given values. + +Example: add the statement that the prover must be born between January 1, +1941 and Februar 2, 2005. + +```ts + build.addRange(AttributesKeys.dob, 19410101, 20050202); +``` + +Note that this type of statement is only allowed for the following attributes: + +- dob (date of birth) +- idDocIssuedAt +- idDocExpiresAt + +#### Membership statement + +Example: add the statement that the prover's country of residency is France or Spain: + +```ts + build.addMembership(AttributesKeys.CountryOfResidency, ['FR', 'ES']); +``` + +Note that this type of statement is only allowed for the following attributes: + +- Nationality +- CountryOfResidency +- IdDocIssuer +- IdDocType + +#### Non membership statement + +Example: add the statement that the prover's country of residency not Germany +nor Portugal: + +```ts + build.addNonMembership(AttributesKeys.CountryOfResidency, ['DE', 'PT']); +``` + +Note that this type of statement is only allowed for the following attributes: + +- Nationality +- CountryOfResidency +- IdDocIssuer +- IdDocType + +### Web3 ID credential statements + +To build a statement against a Web3 ID, the builder has exposes an entrypoint `forWeb3IdCredentials`, +which has a function signature similar to those used for [identity/account statements](#identityaccount-credential-statements) + +1. A list smart contract addresses the Web3 ID must be created from +2. A callback function which should be used to add statements for the credential + +#### Reveal statement + +State that a given attribute should be revealed as part of the proof. + +Example: reveal the education degree of an education ID. + +```ts + build.revealAttribute('degree'); +``` + +#### Range statement + +State that a given attribute should be between 2 given values. + +Example: add the statement that the prover must be hired between January 1, +2015 and Februar 2, 2005. + +```ts + build.addRange('hired', 20150101, 20050202); +``` + +#### Membership statement + +Example: add the statement that the prover's position in a company is either "engineer" or "designer" + +```ts + build.addMembership('position', ['engineer', 'designer']); +``` + +#### Non membership statement + +Example: add the statement that the prover's position in a company is _not_ "manager": + +```ts + build.addNonMembership('position', ['manager']); +``` + +## JSON representation + +The `VerifiablePresentationRequestV1`, `VerifiablePresentationV1`, and `VerifiableAuditRecord` can be represented as +JSON by calling the associated `.toJSON` method (will also be called implicitly with `JSON.stringify`). Correspondingly, +parsing the JSON values can be done with the `.fromJSON` function exposed for each type. + +> bigints are used internally in the types described above and need to be handled with something like `json-bigint` + +Example: service serializes presentation request in response to frontend; frontend deserializes and parses the JSON. + +```ts +const json = JSON.stringify(presentationRequest); // service sends back presentation request to frontend +... +const presentationRequest = VerifiablePresentationRequestV1.fromJSON(JSON.parse(json)); // frontend parses the JSON. +``` + +## Verifiable Presentation Request (proof request) + +To get a _verifiable presentation_ of one or more _verifiable credentials_ owned by a user, the entity requesting +the information must first build a _verifiable presentation request_. In the V1 protocol, this is done in the following +sequence: + +1. Make the _request context_, consisting of + a. a unique 32-byte "nonce" + b. a "connection ID" which identifies the connection between prover and requester + c. a "context string" which describes the context of the proof request, e.g. which store is being accessed + d. a set of requested context values, identified by their labels. For now the defaults here are: the block hash of the + anchor transaction and the resource ID (i.e. an identifier of the requester, e.g. a url of the website) +2. [Build the statement](#build-statement) to be proven by the user + +Once this is done, the request must be _anchored_ on chain with a transaction. This can be achieved by calling + +```ts +const nonce = Uint8Array.from(...) // randomly generated 32-byte value +const connectionID = ... // e.g. a wallet-connect ID +const contextString = 'My compliant web3 wine shop' +const context = VerifiablePresentationRequestV1.createSimpleContext(nonce, connectionID, contextString) + +const statement = new CredentialStatementBuilder()... + +// a GRPC client connected a node on the network the anchor should be registered on +const grpcClient: ConcordiumGRPCClient = ...; +// the sender of the anchor transaction +const sender: AccountAddress.Type = ...; +// the keys for the account to sign the anchor transaction +const signer: Signer = ...; + +// create the presentation request with an on-chain anchor, which can be checked by the owner of the credentials. +const presentationRequest = await VerifiablePresentationRequestV1.createAndAchor( + grpcClient, + sender, + signer, + context, + statement +); +``` + +## Verifiable Presentation (proof) + +Computing a _verifiable presentation_ from a _verifiable presentation request_ is a process of the following sequence +for each credential statement in the request: + +1. Identify valid credentials for the statement by looking at the ID qualifier of the statement. +2. Validate the attributes of the credential in the context of the statement. +3. Construct a `SpecifiedCredentialStatement` corresponding to the credential. This is is _not_ the same as the + `CredentialStatement` we built for the `VerfiablePresentationRequest` previously; here we're working with + a specific credential, e.g. from the users wallet. + +When this is done for all credential statements in the request, we construct the _proof context_ corresponding to the +_request context_ of the request, specifying values for each requested context value in +`VerifiablePresentationRequestV1.Context.requested`. + +```ts +// specify the resource ID from the connection to the requester of the proof +// the block hash is automatically derived from the request +const contextValues: GivenContext[] = [{label: 'ResourceID', context: ...}]; + +// The application holding the credentials selects the credentials to use and creates a DIDString from them. +// This will be a combination of the below, i.e. probably not all at once +const selectedCredentialIds: DIDString[] = [ + createIdentityDID(...), + createAccountDID(...), + createWeb3IdDID(...), +]; + +// These are then paired with the statements from the verifiable presentation request to form the statements +// required for the verifiable presentation input: +const statements: SpecifiedCredentialStatement[] = selectedCredentialIds.map((id, i) => ({ + id, + statement: presentationRequest.credentialStatements[i].statement +})); + +// the inputs for the credential owned by the user, i.e. credential attribute values. For each +// `SpecifiedCredentialStatement`, there should be a corresponding input +const inputs: CommitmentInput[] = [ + createIdentityCommitmentInputWithHdWallet(...), + createAccountCommitmentInputWithHdWallet(...), + createWeb3CommitmentInputWithHdWallet(...) +]; + +const presentation = await VerifiablePresentationV1.createFromAnchor( + grpcClient, + presentationRequest, + statements, + inputs, + contextValues +); + +// verify the presentation elsewhere +const result = VerifiablePresentationV1.verifyWithNode(presentation, presentationRequest, grpcClient, network); +``` + +## Verifiable Audit Record + +Services can opt in to create a _verifiable audit record_ from the _verifiable presentation request_ and corresponding +_verifiable presentation_. This exists in a private and public pair. The private should be stored by the application, +and the public should be registered on chain. + +```ts +const uuid: string = ...; +const private = PrivateVerificationAuditRecord.create(uuid, presentationRequest, presentation); +const { + publicRecord, + transactionHash +} = await PrivateVerificationAuditRecord.registerPublicRecord(private, grpcClient, sender, signer); +``` diff --git a/docs/typedoc.config.cjs b/docs/typedoc.config.cjs index 76051e1d6..3c2022e7a 100644 --- a/docs/typedoc.config.cjs +++ b/docs/typedoc.config.cjs @@ -31,6 +31,10 @@ module.exports = { name: 'Identity Proofs', source: 'identity-proofs.md', }, + { + name: 'Verifiable presentations', + source: 'verifiable-presentations.md', + }, { name: 'Runnable Examples', source: 'runnable-examples.md', diff --git a/examples/nodejs/common/verifiable-credential-statements.ts b/examples/nodejs/common/verifiable-credential-statements.ts new file mode 100644 index 000000000..02365efb6 --- /dev/null +++ b/examples/nodejs/common/verifiable-credential-statements.ts @@ -0,0 +1,21 @@ +import { ContractAddress, CredentialStatementBuilder } from '@concordium/web-sdk'; + +// #region documentation-snippet +let builder = new CredentialStatementBuilder(); + +// Add a web3 ID credential statements +builder = builder.forWeb3IdCredentials([ContractAddress.create(123)], (b) => b.addMembership('position', ['engineer'])); + +// Add an identity credential statement. Alternatively, if the proof produced from the +// statement should be tied to an account, use `builder.forAccountCredentials`. +builder = builder.forIdentityCredentials([0, 1], (b) => { + b.addMinimumAge(18); + b.addEUResidency(); + b.revealAttribute('firstName'); +}); + +// Get the complete statement to request a proof of. +const statement = builder.getStatements(); + +console.log('successfully constructed statement', statement); +// #endregion documentation-snippet diff --git a/examples/nodejs/package.json b/examples/nodejs/package.json index ce500b093..a1d2b9222 100644 --- a/examples/nodejs/package.json +++ b/examples/nodejs/package.json @@ -13,7 +13,9 @@ "@concordium/web-sdk": "workspace:^", "@grpc/grpc-js": "^1.14.0", "@noble/ed25519": "^2.0.0", + "@types/json-bigint": "^1.0.4", "buffer": "^6.0.3", + "json-bigint": "^1.0.0", "meow": "11.0", "node-fetch": "^3.3.2" }, diff --git a/examples/nodejs/proofs/resources/id-object.json b/examples/nodejs/proofs/resources/id-object.json new file mode 100644 index 000000000..e4493c4a0 --- /dev/null +++ b/examples/nodejs/proofs/resources/id-object.json @@ -0,0 +1,50 @@ +{ + "attributeList": { + "chosenAttributes": { + "countryOfResidence": "DK", + "dob": "19700101", + "firstName": "John", + "idDocExpiresAt": "20211231", + "idDocIssuedAt": "20200101", + "idDocIssuer": "DK", + "idDocNo": "12345", + "idDocType": "1", + "lastName": "Doe", + "nationalIdNo": "N-1234", + "nationality": "DK", + "sex": "0", + "taxIdNo": "T-1234" + }, + "createdAt": "202208", + "maxAccounts": 200, + "validTo": "202308" + }, + "preIdentityObject": { + "choiceArData": { + "arIdentities": [1, 2, 3], + "threshold": 1 + }, + "idCredPub": "86bdc35aa086f3407e89220ebd4ea89840a53f37a8cdb26d917779984a751919ef7e4d2d98798038f441265c398a703e", + "idCredSecCommitment": "971637bd25c2cfeb8b0f7eaf321a72ada6fff1c6180d459120c0c331c56097f0d3acfe42dd314f7e3fb6ac657d37ed8d", + "ipArData": { + "1": { + "encPrfKeyShare": "b8c0e1131b1fb3d1a608384ac7df8108f0af82a701c00dcfc435916963deb0edc030e8ddefd9dcf07a783ceae29810e1b70bbc88e343bb49a19fda60ea6b1177a8782f9d0835deff0a722b67680b7023b07a248d3a932a13001851bb36e9ef239540d9fcd1d10d0373cf965e44d8f6f396107816c9c9643b49308f58aa357cb11126cd3c156a8ff8308b73d7ebf0fc7b8e0ac2c86b9acc3e3f06b4dc4856c9bb46754efe6beafe688dab5e857958c61d748599d635cf179163bee878d8ca10aea5cf0db530e64836a169aac4fd1e70b8551f2044cf0a7106487908b3135e65571e7ef1a2967496a9d78028f61d7c87bfab6ca1880f8f17e27c863fb15fae48127768f7a1ca1ce5b5041c57ab4bc8f52e19ea152e639930f04a22b11ff54bb0f9b1c60fdd20aaea9ec6af7a2f257e1597b29398436f2bf63088d7088c64daab9ad6b5c4ff833b7f7439cf27db9bc87b5c8a5678712bdc8e43a85efd0c8e6c72c4090d595c6c53e7461b78e5c81b168bbff9326bc2cb9030e91ecd12ff360c7cdb958571fe1395046c7575f5ec4819f4dca6ea189ab8822700212c7c28df8b1d582cf512847182b85b5335363d7293c5d294354971a6f5472dbe8007ba281c5976206cd51c28ac267bb03551723ad27bb2ff98a835802b18987e09363f6cbe2a8aa7f184f9aad6edf0e0fedfb3358a51a53b5331887fe8bd2f7ee9ded244d0a39a20650b0dc1e0d29f4ae83ff88c3a1240b2f8173a728c4cc835ad2141a14e47dd58c10bf2756042b005ebf5d06e1c0a6f13d6ec0148be09e0f3aff477a6509ccc84892523ac88688dd057c5801b3e85613a73cc1e3f2e19c8af921c6107ab1ab5765ca835454c76e24aa95a038cbce92e911abd2dafc4a48b776b7bb1291e20157c2f40c93360a577c12fcd703ecd98ef7a47f4bc246942990e98e152bc965182a853b67405c3e4a864a2932ae4bd2c03e02beceb138442e3f888f27a947cbf75bf77663d78893ba5acbaf517a6480928b043ca1024b553cca7eb1f184d15cea22c859d359b7ca4266505a362997a29f55ff9b986129b3a58ec6d881453118b8c", + "proofComEncEq": "4854f537c2e050b1951dde8c1200cf9fe8d12729bd6119d9181e6a2d7718071f0f126589462379d22f16785b75d8006266e78677cce00dca23b564fa32b8c890702af22dff95de8e5657c120488839e2ac9bee6ce7fcbab1480661dbfd6d8519" + }, + "2": { + "encPrfKeyShare": "a8a6324b0573f190498989fa002cc295076fa08950bbba609b04a76bce2bc163cd1c0c677a2c3c19bfd3f3ac89b5b05b824cd8b1b218b103f8cf82075f172b5da2ad93b5f0203c95c780c317b601eacb653dc985d9d4002b594aca8e809f2f7bafcf2e107aec433eb5c29341653b92975eaac637fd5c1551f72574d765a8251f62b33dc0630aecb19f42cef80c2215efafc5c33233262fc1bbdb35e81bfd719bcdf5bb3f27e3bc8e73e9ad2c790b247d5ce1ab359210a2763727480ded96c890b19de1e423e588de7ce1c32edc24b23cb5fef5e20badce79ab6cb5c6d3ede0e285820df028dd142073d772cace33847d862504796fabc2e10716bf383fdb1c3834dab182b182ffc570b056d8483b175fa2af4ecb225db3f72a4494a4231f1de6a006bd23fe0414c8434dbacda740213f21568ef43803fa966f7ce9193d11530994651983911790585539cf38ae568d569882d4352abb5a263f019d7e03d734e7725c60cadab4f72c436601e16292771c52cd4bf89f9ea16ad85cfcc32620e57c90830de15f6e7b3013af8832ccf9c9029ffdfed4b7b41cd4685cf6d5e99dd4c76a1c81a2b6664f0bfffe603edb413f0b8faa5d2659e6b282c00d693c3bb1bbe5a38c08009433bfa4efa4b2591b032c77bb0d69260cefe03d42dbb0d91a6620dea488cb7d92d844957e2519ac984ad78ce312ebfca1e07806c71cae189923e9291babbeaf540692701bae207488541a68b81203cce9bd3b4f62cec70af972746bf40f31043694ad8feb4789050f501873f95d482ab8bdb6e27ee624a5d1c5b47aa14b6c0738bb2a89b041f038818b39a391efdc7de04be31a971bb8a331cf50e8b998e61e2e330a4087241440a9dbf566870a0cfc61abe183fc797981480af89f6676c9a4a13f9c191ed861c59687068b6b41217b80e8cdc70e4967525288fc2ba916b51ed57fd2a476f6087e4e43f3688cd3d8621bb9c076a7e021c9c7faae35266264394cb830d69e954d00285e5760a666b904d77035b676d7c81674c6cb12fc241f7c4147756c4dfd9cb808429adf81087dd1289b3afa638e894aa0279740", + "proofComEncEq": "1700b45adbb0ef8679d6c0500a890bbabcd8691fde7f74641760220130195891506af09003fd1c8ff3ba0a44750fd1d93fcf2ebbf4bf40bdfba160d375bb954034a3ef03a0598b7018a2d80e2637ccbb51714efd9688842842431d2f01ab489e" + }, + "3": { + "encPrfKeyShare": "aced178c5a3ecd4cbb80a60761f21fbf3174309641815ab2af0557274026db47377af74a11e2ab2a2808f8b92353e18ba15f062ecbce8066510c477ec27a84bab1d5c2b7b2d81222119dbba27d3174158d34c3f3fdf04cfbe55f027b22780c74a8b1a1794be16e0ef2bf2468d4035054c95c42d65c4ac204714a632283b47e6d04f3d7bd9b13a220778345c1646053bdb305e75d8d1f1e3d0c6c0a06860fcf8e990de681ab698f9758618c4922631d07aca57b7d00ef9cd822f1797bf3fa9075b38873ab2f3e8de346176eb73f17b2131c5990299bb5cda19f1ea91a0f27d68670e2580f928ac427830787a8f6a1d8dc964b1c1ea4554df35d8807a795171ed216944194b38b1b7cbc608c254bd9be5ee39e96b0f7469ea596497ae330e4d9fa8f0ebc75c2ddc69f5f35e1bc7977bc3c643cbf2f7ea8bfadeca7bee6ddeaccc8c0d40f9be53382cb5e8b06151be0854f8935ce37c25448b13a3fd7abf5505fd3a608d7b78f37456357d17b8bb2d643c5841724df1b210d884627603d90a8e40c935bacf3ec1fc7dfc46c6962960d12852697cab088eec5a9c36eb2890be20394c9a65c26b58c67f976765b7ac67468019100e50344083d7315f0d65b56cef4fa3088a82dea3961a18e848de02dc1c9252188cc02707caee1ee794b4769a223d894a7599f6e3ebe28766c3ad488289c8c839a9a16aab8e0df8944a0a99241f83b89f6918541065b49bae46e65999183988a5d1084e276262b5673810616a0983d21083a01e9423f1793ee5ca9240fe8f33c09176156a398c2e8d51af49807a550ab2d42f5dd4a82cc5b8e134fd89b4866d564ca82c4005cf07fca195ba51893f575a47ac8b2ddeb6cd189c31ee258ef3a86b66df847ea4b70a8bd35c85f9d5a77b96f32a421e489f44d8fc2aff5d9dfb3a363ec44319b0f202b6b448ae831f59e9780dded5403d108167a506a7ca1312ec377b4b2d02461c05bbaf7109afb816d3dec1c8834230f8e056c4b20b63fce2aaac95f0bdf49b24f61265a660fcddd6b217590195638d6d28f8129da978f6d1ce7fd9530a2ccef4195ef9c06abce1a0a", + "proofComEncEq": "729c7e53986746acbbba2d9a262ffb8dd1ad4e02cc58e877514f1fb7b776cb7707101dde4136b20c94e33d812702871e36f1012ca4d708eb598fb5d2ba7ee8623828d001559b227f5f01a6e4c3eaba5c6607bf9c008ec7738aa4801d06a0e4e9" + } + }, + "prfKeyCommitmentWithIP": "80eee95dd029f4077e4de5b7c469fea70a0e20ae05f60ab6784a227b5cd277381ef96cff3565816f5fe226c3866161ed", + "prfKeySharingCoeffCommitments": [ + "a146057e158d734526b6f438403991e60f46a0035cfb95a2e94d052c1dbc576a28c40d952d531d22949c68cbb9b6801e" + ], + "proofsOfKnowledge": "2870ecb6ce1c4e129baa76ab195fe4a1d0805da7bafa8a060d372615ef46b6102e7c020fd49a3b418687314a738424d8d2befc823e36b465f3eb6996274450bd35326037538f7f468d5fd8100d4b3afa245a3df6fd7d2ec829e24023acb7ca0d5b4eb756133722770cf984acda9c641e778869f3a589e061a9c9095429c946924986032647257485aed2f6bbf976063510f4d9d665b52cbde5a6ba64895b48544ec4e04f937a788bd33f4b58ed125971abcf3ca06155440ecc0a61792becaf593cea6e338be7159c5742c3489376ce40878d95cfe058757aa49097bc23bd39c10000000000000003b662981ae0b14982db58eef3bbd05bdd33bdbed9fac29dbd46b4fb54211613c613026e08d8f0e1f82137770669ce4f528d0adb346c3f9d391c4b4f4c9740c8409e8486846404b305f98ecc4d07919fe89bc93bf091c8f7cec78428665152a52da66402ae5fd7b3626e6e2def9f46f77372a47d161a339368e08900c0d04290ba590b649a69371e2a74e5332d80a4b81e819e81643db327028deb236ef388c2265f63fce236a64eb4702a48b4ed579f15df721e3206c0a9dfe34f1232d20290fa6e7db531a4476f48186656dfc77d4d0fce25086198d26a7fdd42a7007275d75a0679d7a39ecae3f8ba5142f5b73d0c9f535cb3add6d44eb411a38f9c60e4bb761ad1eb2a4ddb50fe72af7ae54ecc80a58d5b43d04cbb0a727040c41262c0fa13000000088fcb41f45d5209bbc45774b13971bb1dc558a011fac0a433042eff86fd67d837b00cff2776cdf80d2f222b2a636be8658df78844ae1e04a1a27ef369d3d40f02e97cb65660d70a8dcd4e928f576d7d7902f42afaf9b5d88110c392b031d64125a5a32b588cf32a26f97a36459864b57620a92a0d9540dae0d541ee40fae0e5be2714cadc1ac09982b53f0961fc54a02d8be1e471502f1f2c1cc909a6104b451099f72cb7ef6a180f22be59192bf8df679cca7a8cad0646a188d26255cf06b7c6ae2d393427df1adcefa1483177928eb453be356f30d9e60eeac6513cea9733fca04e0cd6530b4523a0cdfe84ac558e73b642a4c1d01982dd0ba35e449655b5244e820fd2d53f1d7bcde75e8f20bdc967e296f1ef239d0ab97859bf56bfc8a2d9832ccdc70d7e05710a916e29f10e3dbb71f5232749cd06c32c23718d04c52bb58623e09931c0201fdce1f5f21c18c64aa4c634446185925271ec9c334eac4dfc5f56bf791a336ab4688ae3a7ee9160d372818bc4b70d0953768e1519d6fd813ba5bee23702a81160f0f4d630ccd45f15859f61e7303a343ba86f73b3ea4a03c8a642e91de4b9e17d209cd05edc753a35a26f845712949a257b534f762216c76d8d8007f36392f5c716498c99719ca9afe70c9db60084e5f1cc6b2f0434885e24ac7013096ab113ebcb9d1874260dedcec15e5689cd50f298d079dcfeb2f8c24243b828786b8f3c3ef38a43e22ef26fdd88b5b80595c97fb9c6db4d93729b88d70f7a1294b3f9f50d80f8a962ba1283afeb270c429580dcca094dc67f4a564ebbb6e007ad25eddbe84f801d7f1fc8d3dd24266769bcfc6318f9fadf3c3ec8c3b3c2638351488a2fa0bb2ebc0a7d30edea9487c41bba352262427cccf0692f96ca6e73d1164da3f5a4b82e17f67d291b75ca1e1f0fdb758858acd7e4d1902d5b9888184bf4ed0425aa17098eea167f8d1c5f2c7e87158a85bb455d4bf9b5e262a9c82cd94157a701d870783d4c5075359ca63d1ed71c596df46f1f5ae9b4c2b9b2d702b2a300508986be4905d298ed0787045c4d444981fa44c3687ed64a0eefcc3834c5c52fb60cd973a7e3a41f7500d2adbaddfa6d29c83bbf0e7c5299eae45d128efe868cde0faa4dff4782fca9e296eccf1e5de079cd0ec999449c37c27a9da152b157a3dce9def3eaa50b30d6b526e16eec99fc7549fe62e08c58ac559e421a7d2bcad91b23038833aad21764fde7b1a13446b254c2457963cd3be7881b2953022868c266e85b6ba6d30aaa3112d35c58075d1dbef0dcce17813d203220718793f4361e6a32c3100f517ec71dd652fa608ec907cf7088778288938fa5053b9ad07fef029ca8b3ed0da24575b251c9807ead7fdf7b45f1b59aed03cdff45c9872a1b7d22b2892dc505ffa639c8a3f79793dcf488ac3c8feaf90b7719061396536ebc576492f4aec4ccf1ee79899de97f29dde294a05540eee80adbc74eb32f32af94524953390d46cf8299436ade6efbb149344aa55cf1a494ca6811b42fc80ae64a86a3ebab380e056eb9bd471042678d21faf33438599a501f0c6a698b5b00000008b518c5117e704c0dc391a868d0edd1a9f46e2edcf6eeed52c436d99b547d2d22e8f0a3f8b404895f881145cd5cd761c6a354f379d514e1cd883eb57ea67ae13fbc41e502dfeeb75910271cd7525fb8eb1a05ad9723dca4021330d38d13ab5656a60a362062af95df90027f1aaa79d42dffcb98cfa60e5068bfea1e51d0688328f1ebede7083fe68abdad38c11c9d9f7793f5b64bbe8ca9f4f1e2aed5759c5d239aafe211b8bbd4c1583ae2346d01089c230089aea5b3b774df69ce2758a8c151aaec9ad782917315c8033329b702a0610d6f37e4b541d0970d47dbda0b395a9a860510512434549056d219b1d3526dbb81c04d2f0e2e353bc5b7ecf98df448f9891ad5e72a809c29e0faa25722f66926f9bd61eca67d71ad35b02f54467aa85d8a72421130f6907a12a1a4449f6a020c20d0d80970c6751ed66dd6654a1863bd3429aa0936ee13e1d7479e9f2413df4f89b3d6ad4ac7576f30deb9b24e9f770cbca1d381595d54916347307df61b57825779fed1d6e601d47611edc48deb2bbc8785d06a2df7e045af541979cc8a8d1d988b1c80e00ae503fdeafc0eefbbf33c03ecb12e96f438505bf85a3ec16129aeb4149b98f828bc9dda74101c9ce101a0b39a722f0723e19e863f087d8915ea13184fcd99d5f2d6908006ac1247883dbca72dcb66ec816987f5b719174297c97ff93f036a5841dab4a7234de9c3589a3a75205b997df6a439e402b1d2ce8180db89989b3bdf77eee93ee3920ed7120d62d05f53bf24980db20914983ac34d475a1ed88090f3fbc35374a4d9417503197295324eab24bf2cb0d368eaec2edcec5726757f18f7ac2c0b48b545fefb1e42e7a9372feb3cdb546bb41283d50bf88bb6994074e02c3e19b2e9769c3a854b503c15fa0de79fbb680c809d279b429f8dea3d232711807cde212b63735538a38590936df61fc406f67eee35432c595a596545f9889968de8321e25f6e6374b0bc76a652970076d1d0a7fa53e943a0be443f882bf65e19200f379c2ea03cdb17f20c99b4a0b8d8094ea8f3b238af176a9f2398b9482c9c4b1c10e5caba2ed8f12c6263a1c771bb3120c0f8c107d43acde378e9d22aa5f4efbe2ab38a4130c5795c1b6f959a3b902b8a1780c2fcef8fa5b45c6ae4a35c7ae26bc1c678394ada08f391a0b8bc6fb74408e3bd2b0f1b7f111e8672019fffb8cbe7e93c79750e4f5e83709f7d19cc21bd91752e7f197e4b294c25b2fa6f62739220861d820ef5cad15172f5b3bd6f278e5799eaf029b4b7a50e2f4a103043bc24221fbb480ae849d9432398263caf2375bd5354a74b81375d16d6f926b66408815a3e357832c19035dc5405fc8a66be6476697a88b58ef7b13a98a384c284578369e56046414b61b17cf417d81497dda0c863842491e474c678cb1cf4684dc1f3b49852b254da93b74574191b50100dab3c0f74c5c5203832b56030918280c72b8dab33ce5872136f734d253928d0c8ff00f0010db0ae4ff67679c87b2b57633ad7c69ac3ad744cf8218c11062f058ea9d00744ec14e4e632be9cc98eeb58991e9a00bb373b7852b53b1b00000008a193f17a557298ebe18b2a0e13048aea185522f506948f0bc269bc4f46c4ea506df698f0b35a0a04fb64f10ba038ac948064b3932ea4e0c1cc7d57b857f19fa0f43a10f1bb6b3e67ac8406626b0162da78965f6a3cf31b37ca311a8c4b62480b8efc320711da568f8c6aa04de44add0680eb9da0a0e6724d2728ed01fb8422d4107ee530340999f3eb4a14af4b449c488ada1f02445787aeed6bd6b91ecee7f0357b3968c804f1445af64b0657b77140d4183b1509dc203287716ba5d412500e926fb46d826a3f1dce9cea11bcfd6722fab6e4ad63b9af64ece36589fe65945afcd7e2f41a480a51480e91c3d714a05c8348cf6a83c2aba1dc6ddb3d790eeb063845f18c540f53ecdec27362850389b23003f27b2ed3fd109d9df7660963a146af787b8b47f006913760a16655ba32198144ace981650cd47f52627b6455d5fb21445b1e6723aec1a1512a02d369058db60b2d79b5c34ab14b6d4e719bcf1bddd89f49c338260460c918d736a5d8d28b5a777e7460cb593230206a76a7e01f42ac0d3b954e9e5e73455f2c1e753ca3c96d2250989daf7333deee1c295fa35d4540a37a4bbec67254db5444a1848125d4a353ffc74f1b0f2a76d315a400da55ca1060de08e978c41ac23199fbe2b3d3da182ea6fd008e21543374e1af80a43da6b26bcd67d524c0a816108312bc709291f4ff3726af131efc81f8f632a45eb5c0128a688935828fa638bad9935684ed548f30cd5ebdbbfe80b67ea75a160200abe746ee19f4f9437d75b72679f909ca9ea6f28f32ecc421715211f9f16c475c7e9134bb5b70a5f96f385a6b10d4e440bf9f14aad9efb99076b306c8c31798a4130aee5b4eb52b9a710f36aa8fa5e08307a19bbd996d1892015a664cc2003102847bf9c4c56738e25dea985c99af1a53d84b4b222361a4095c969ea3b30f340db6a43926121f63f57b439b70bbabb90d9ca91c1a92ce8910a5c33ee2435dd190b604ee1c78ef451c5b08efb8864109ff328af2b2666a1cc2bdb47b739cfb687721492be730f6a9c96f4d62041329ade6cc1e29dd533565d680e909b248831774782bd73fc2825aa3213ed278cf4d96438ab710257a1f0c27fbf2b9021f842af62970e441177ee8dd15eacd2ab843183e07ff8fd01850bed7e5b232f1a8a0e3ba2a" + }, + "signature": "828e17e41937bacb9e7a1bbc3a7e5178cd49abf2e5255479037671d0b2db03bb717bd67a8e873f593622e4f2acd3defba6389bc67b7559ee8d7fac54e22f110251089f0ba6ccd02e0a625b6225b6eb256d8643c647e6389bfcf305a1eb0289e5" +} diff --git a/examples/nodejs/proofs/resources/seed.hex b/examples/nodejs/proofs/resources/seed.hex new file mode 100644 index 000000000..771c34dae --- /dev/null +++ b/examples/nodejs/proofs/resources/seed.hex @@ -0,0 +1 @@ +efa5e27326f8fa0902e647b52449bf335b7b605adc387015ec903f41d95080eb71361cbc7fb78721dcd4f3926a337340aa1406df83332c44c1cdcfe100603860 diff --git a/examples/nodejs/proofs/verifiable-presentation-id.ts b/examples/nodejs/proofs/verifiable-presentation-id.ts new file mode 100644 index 000000000..9cde5b778 --- /dev/null +++ b/examples/nodejs/proofs/verifiable-presentation-id.ts @@ -0,0 +1,226 @@ +import { + ArInfo, + ConcordiumHdWallet, + CredentialStatementBuilder, + IdentityObjectV1, + IdentityProvider, + PrivateVerificationAuditRecord, + SpecifiedIdentityCredentialStatement, + VerifiablePresentationRequestV1, + VerifiablePresentationV1, + createIdentityCommitmentInputWithHdWallet, + createIdentityDID, + isIdentityCredentialStatement, + sha256, + streamToList, +} from '@concordium/web-sdk'; +import { ConcordiumGRPCNodeClient, credentials } from '@concordium/web-sdk/nodejs'; +import _JB from 'json-bigint'; +import meow from 'meow'; +import { randomUUID } from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { parseEndpoint, parseKeysFile, validateNetwork } from '../shared/util.js'; + +const JSONBig = _JB({ alwaysParseAsBig: true, useNativeBigInt: true }); + +const cli = meow( + ` + Usage + $ yarn run-example [options] + + Required + --wallet-file, -w A path to a wallet export file from a Concordium wallet, used to register anchor and audit report. + --seed-phrase-hex, -p Hex representation of a seed phrase to use to retrieve identity secrets. + --id-object, -o A path to a JSON file holding an identity object. Defaults to './proofs/resources/id-object.json'. + --identity-provider-index, -idp The index of the identity provider used for the id-object. Defaults to 0. + --identity-index, -id The index of the identity. Defaults to 0. + + Options + --help, -h Displays this message + --endpoint, -e Specify endpoint of a grpc2 interface of a Concordium node in the format "address:port". Defaults to 'localhost:20000' + --network, -n The network corresponding to the node ('Testnet' or 'Mainnet'). Defaults to 'Testnet'. + --secure, -s Whether to use tls or not. Defaults to false. +`, + { + importMeta: import.meta, + flags: { + walletFile: { + type: 'string', + alias: 'w', + isRequired: true, + }, + seedPhraseHex: { + type: 'string', + alias: 'p', + isRequired: true, + default: + 'efa5e27326f8fa0902e647b52449bf335b7b605adc387015ec903f41d95080eb71361cbc7fb78721dcd4f3926a337340aa1406df83332c44c1cdcfe100603860', + }, + idObject: { + type: 'string', + alias: 'o', + isRequired: true, + default: './proofs/resources/id-object.json', + }, + identityProviderIndex: { + type: 'number', + alias: 'idp', + isRequired: true, + default: 0, + }, + identityIndex: { + type: 'number', + alias: 'id', + isRequired: true, + default: 0, + }, + + // optional + endpoint: { + type: 'string', + alias: 'e', + default: 'localhost:20000', + }, + network: { + type: 'string', + alias: 'n', + default: 'Testnet', + choices: ['Testnet', 'Mainnet'], + }, + secure: { + type: 'boolean', + alias: 's', + default: false, + }, + }, + } +); + +const { + seedPhraseHex, + network, + secure, + idObject: idObjectFile, + identityIndex, + identityProviderIndex, + walletFile, + endpoint, +} = cli.flags; + +if (!validateNetwork(network)) throw new Error('Invalid netowrk'); + +const [addr, port] = parseEndpoint(endpoint); +const grpc = new ConcordiumGRPCNodeClient( + addr, + Number(port), + secure ? credentials.createSsl() : credentials.createInsecure() +); + +const [sender, signer] = parseKeysFile(walletFile); + +// First we generate the presentation request. +// +// This will normally happen server-side. +const context = VerifiablePresentationRequestV1.createSimpleContext( + sha256([Buffer.from(Date.now().toString())]), + randomUUID(), + 'Example VP' +); +const statements = new CredentialStatementBuilder() + .forIdentityCredentials([identityProviderIndex], (b) => { + b.addEUResidency(); + b.addMinimumAge(18); + }) + .getStatements(); +const presentationRequest = await VerifiablePresentationRequestV1.createAndAchor( + grpc, + sender, + signer, + context, + statements, + { info: 'Example VP anchor' } +); + +console.log('PRESENTATION REQUEST: \n', JSONBig.stringify(presentationRequest, null, 2), '\n'); + +// simulate sending a response to the client requesting the presentation request +const requestJson = JSONBig.stringify(presentationRequest); + +// Then we create the presentation. +// +// This will normally happen in an application that holds the user credentials. In this example, the information +// normally held by said application, i.e. the credential used, the idp index, and the id index, is passed as program +// input. + +// First, we get an interface to retrieve the cryptographic values required by the VP protocol +const wallet = ConcordiumHdWallet.fromHex(seedPhraseHex, network); +// Get the ID object - this will normally be stored inside the application constructing the proof +const idObject: IdentityObjectV1 = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), idObjectFile)).toString()); +// The `IdentityProvider` information is normally stored alongside the ID object +const ipInfo = (await streamToList(grpc.getIdentityProviders()))[identityProviderIndex]; +const ars = await streamToList(grpc.getAnonymityRevokers()); +const arsInfos: Record = ars.reduce((acc, ar) => { + if (idObject.preIdentityObject.choiceArData.arIdentities.includes(ar.arIdentity)) { + return { ...acc, [ar.arIdentity]: ar }; + } + return acc; +}, {}); +const idp: IdentityProvider = { ipInfo, arsInfos }; + +// At this point, we have all the values held inside the application. +// From the above, we retreive the secret input which is at the core of creating the verifiable presentation (proof) +const credentialInput = createIdentityCommitmentInputWithHdWallet(idObject, idp, identityIndex, wallet); + +// we select the identity to prove the statement for +const selectedIdentity = createIdentityDID(network, identityProviderIndex, identityIndex); +// we unwrap here, as we know the statement exists (we created it just above) +const idStatement = presentationRequest.credentialStatements.find(isIdentityCredentialStatement)!; +const specifiedStatement: SpecifiedIdentityCredentialStatement = { + id: selectedIdentity, + statement: idStatement.statement, +}; + +// simulate receiving presentation request +const requestParsed = VerifiablePresentationRequestV1.fromJSON(JSONBig.parse(requestJson)); + +console.log('Waiting for anchor transaction to finalize:', requestParsed.transactionRef.toString()); + +// wait for the anchor transaction to finalize +await grpc.waitForTransactionFinalization(requestParsed.transactionRef); + +const presentation = await VerifiablePresentationV1.createFromAnchor( + grpc, + requestParsed, + [specifiedStatement], + [credentialInput], + [{ label: 'ResourceID', context: 'Example VP use-case' }] +); + +// simulate sending a response from the application to the client requesting the presentation +const presentationJson = JSONBig.stringify(presentation); + +console.log('PRESENTATION:\n', JSONBig.stringify(presentation, null, 2), '\n'); + +// simulate receiving presentation to be verified +const presentationParsed = VerifiablePresentationV1.fromJSON(JSONBig.parse(presentationJson)); + +if (!(await VerifiablePresentationV1.verifyWithNode(presentationParsed, presentationRequest, grpc, network))) + throw new Error('Failed to verify the presentation'); + +// Finally, the entity requesting the proof stores the audit report and registers a pulic version on chain +const report = PrivateVerificationAuditRecord.create(randomUUID(), presentationRequest, presentation); +const { publicRecord, transactionHash: auditTransaction } = await PrivateVerificationAuditRecord.registerPublicRecord( + report, + grpc, + sender, + signer, + 'Some public info' +); + +console.log('AUDIT REPORT:\n', JSON.stringify(publicRecord, null, 2), '\n'); + +console.log('Waiting for audit report registration to finalize:', auditTransaction.toString()); +await grpc.waitForTransactionFinalization(auditTransaction); +console.log('Audit report successfully registered.'); diff --git a/examples/nodejs/shared/util.ts b/examples/nodejs/shared/util.ts index 455f564d5..1f024f60b 100644 --- a/examples/nodejs/shared/util.ts +++ b/examples/nodejs/shared/util.ts @@ -2,6 +2,7 @@ import { AccountAddress, AccountSigner, ContractAddress, + Network, buildAccountSigner, parseSimpleWallet, parseWallet, @@ -63,3 +64,7 @@ export const parseKeysFile = (path: string): [AccountAddress.Type, AccountSigner return [sender, signer]; }; + +export const validateNetwork = (value: string): value is Network => { + return ['Testnet', 'Mainnet'].includes(value); +}; diff --git a/examples/wallet-connection/proofs/src/App.tsx b/examples/wallet-connection/proofs/src/App.tsx index 3c0222c84..d5b7c9687 100644 --- a/examples/wallet-connection/proofs/src/App.tsx +++ b/examples/wallet-connection/proofs/src/App.tsx @@ -7,9 +7,9 @@ import { } from '@concordium/react-components'; import { AttributeKeyString, + CredentialStatementBuilder, MIN_DATE, VerifiablePresentation, - Web3StatementBuilder, getPastDate, } from '@concordium/web-sdk'; import React, { useCallback, useState } from 'react'; @@ -41,7 +41,7 @@ function Main(props: WalletConnectionProps) { setError(''); setIsWaiting(true); - const statementBuilder = new Web3StatementBuilder().addForIdentityCredentials([0, 1, 2, 3, 4, 5], (b) => + const statementBuilder = new CredentialStatementBuilder().forAccountCredentials([0, 1, 2, 3, 4, 5], (b) => b.addRange(AttributeKeyString.dob, MIN_DATE, getPastDate(18, 1)) ); const statement = statementBuilder.getStatements(); diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index e881b05d6..d62291e36 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -2,6 +2,61 @@ ## Unreleased +## 12.0.0-alpha.1 + +### Fixes + +- An issue where anchor computation would sometimes be different when serializing and deserializing a presentation +request + +### Changes + +- `VerifiablePresentationV1.verify`, and the corresponding GRPC helper `VerifiablePresentationV1.verifyWithNode` now + return a `VerifiablePresentationV1.VerificationResult` instead of `true | Error`. + +## 12.0.0-alpha.0 + +### Breaking changes + +- `AccountStatementBuild` has been renamed to `IdentityStatementBuilder` +- `Web3StatementBuilder` has been renamed to `CredentialStatementBuilder` with the following method renames + - `addForWeb3IdCredentials` -> `forWeb3IdCredentials`, used for statements for web3 ID credentials + - `addForIdentityCredentials` -> `forAccountCredentials`, used for statements for the identity credentials tied to an account + +- Type renames to align with the target credentials + - renamed `VerifiableCredentialQualifier` to `Web3IdCredentialQualifier` + - renamed `VerifiableCredentialStatement` to `Web3IdCredentialStatement` + - renamed `IdentityQualifier` to `AccountCredentialQualifier` + - renamed `RequestStatement` to `SpecifiedCredentialStatement`, which is now a union of + `SpecifiedAccountCredentialStatement | SpecifiedWeb3IdCredentialStatement | SpecifiedIdentityCredentialStatement` + instead of the corresponding old dynamic version. + +### Added + +- `Web3StatementBuilder.forIdentityCredentials`, used for statements for identity credentials without tying it to an account +- types `IdentityCredentialQualifier`, `IdentityCommitmentInput`, and `CredentialsInputsIdentity` which have been added + to the respective union types that reflect the possible variants. +- helper functions `createIdentityCommitmentInput` and `createIdentityCommitmentInputWithHdWallet` +- helper function `createIdentityDID` + +- types `VerifiablePresentationV1` and `VerifiablePresentationRequestV1` to be used with the new zero-knowledge proof + protocol + - `VerifiablePresentationRequestV1.createContext` and `VerifiablePresentationRequestV1.createSimpleContext` for + creating the presentation request context. + - `VerifiablePresentationRequestV1.createAndAchor` which is a GRPC helper function for creating a verifiable presentation + request from a minimal set of values. + - `VerifiablePresentationV1.createFromAnchor` which is a GRPC helper function for creating a verifiable presentation + from a minimal set of values. This also creates the `VerifiablePresentation.Context` from the corresponding + `VerifiablePresentationRequestV1.Context`. + - `VerifiablePresentationV1.verify`, and the corresponding GRPC helper `VerifiablePresentationV1.verifyWithNode` for + verification of the proof. + - **Please note**: some functionality related to this is currently either stubbed or routed into the old verifiable + presentation computation functions for now. + +- types `PrivateVerificationAuditRecord` and `VerificationAuditRecord` for creating audit records. + - `PrivateVerificationAuditRecord.registerPublicRecord` is a GRPC helper function for registering a creating a public + record and registering it on chain. + ## 11.0.0 ### Fixed diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 1e5379769..b74301544 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/web-sdk", - "version": "11.0.0", + "version": "12.0.0-alpha.1", "license": "Apache-2.0", "engines": { "node": ">=16" diff --git a/packages/sdk/src/commonProofTypes.ts b/packages/sdk/src/commonProofTypes.ts index 69c9ee222..b3ebae6e5 100644 --- a/packages/sdk/src/commonProofTypes.ts +++ b/packages/sdk/src/commonProofTypes.ts @@ -1,4 +1,4 @@ -export interface StatementBuilder { +export interface StatementBuilder { addRange(attribute: AttributeType, lower: ValueType, upper: ValueType): this; addMembership(attribute: AttributeType, set: ValueType[]): this; diff --git a/packages/sdk/src/types/DataBlob.ts b/packages/sdk/src/types/DataBlob.ts index 1826d3359..b960c8086 100644 --- a/packages/sdk/src/types/DataBlob.ts +++ b/packages/sdk/src/types/DataBlob.ts @@ -36,6 +36,14 @@ export class DataBlob { return packBufferWithWord16Length(this.data).toString('hex'); } + /** + * Get a string representation of the data blob in hex format. + * @returns {string} The string representation. + */ + public toString(): HexString { + return this.data.toString('hex'); + } + /** * Takes a hex-string and converts it to an instance of type {@linkcode DataBlob}. * The method expects the string to be prefixed with a 2-byte length like the one returned by {@linkcode toJSON}. diff --git a/packages/sdk/src/types/TransactionHash.ts b/packages/sdk/src/types/TransactionHash.ts index 6cc64c927..f937c90b9 100644 --- a/packages/sdk/src/types/TransactionHash.ts +++ b/packages/sdk/src/types/TransactionHash.ts @@ -40,7 +40,7 @@ class TransactionHash { * Get a JSON-serializable representation of the transaction hash. * @returns {HexString} The JSON-serializable representation. */ - public toJSON(): HexString { + public toJSON(): JSON { return toHexString(this); } } @@ -50,7 +50,7 @@ class TransactionHash { * @param {HexString} json The JSON representation of the transaction hash. * @returns {TransactionHash} The transaction hash. */ -export function fromJSON(json: HexString): TransactionHash { +export function fromJSON(json: JSON): TransactionHash { return fromHexString(json); } @@ -66,6 +66,7 @@ export function toUnwrappedJSON(value: Type): Serializable { /** Hash of a transaction. */ export type Type = TransactionHash; +export type JSON = HexString; /** * Type predicate for {@linkcode Type} diff --git a/packages/sdk/src/types/VerifiablePresentation.ts b/packages/sdk/src/types/VerifiablePresentation.ts index 242e1b80b..7d59d3f5d 100644 --- a/packages/sdk/src/types/VerifiablePresentation.ts +++ b/packages/sdk/src/types/VerifiablePresentation.ts @@ -2,13 +2,12 @@ import JSONBigInt from 'json-bigint'; import { AtomicProof, GenericAtomicStatement } from '../commonProofTypes.js'; import { HexString } from '../types.js'; -import { AttributeType } from '../web3-id/types.js'; +import { AttributeType, DIDString } from '../web3-id/types.js'; /** - * The "Distributed Identifier" string. + * A proof that establishes that the owner of the credential has indeed created + * the presentation. At present this is a list of signatures. */ -type DIDString = string; - export type ConcordiumWeakLinkingProofV1 = { /** When the statement was created, serialized as an ISO string */ created: string; @@ -18,9 +17,12 @@ export type ConcordiumWeakLinkingProofV1 = { type: 'ConcordiumWeakLinkingProofV1'; }; +/** + * A proof corresponding to a {@linkcode GenericAtomicStatement} + */ export type AtomicProofV2 = AtomicProof; -export type StatementProofAccount = { +type ZKProofV3Base = { /** When the statement was created, serialized as an ISO string */ created: string; /** The proof value */ @@ -29,6 +31,11 @@ export type StatementProofAccount = { type: 'ConcordiumZKProofV3'; }; +/** + * A zero-knowledge proof for an account credential + */ +export type StatementProofAccount = ZKProofV3Base; + /** The signed commitments of a Web3 ID credential proof */ export type SignedCommitments = { /** A signature of the commitments */ @@ -37,12 +44,28 @@ export type SignedCommitments = { commitments: Record; }; -export type StatementProofWeb3Id = StatementProofAccount & { +/** + * A zero-knowledge proof for a Web3 ID credential + */ +export type StatementProofWeb3Id = ZKProofV3Base & { /** The signed commitments of the proof needed to verify the proof */ commitments: SignedCommitments; }; -export type CredentialSubjectProof

= { +/** + * A zero-knowledge proof for an identity credential + */ +export type StatementProofIdentity = ZKProofV3Base & { + /** Commitments to attribute values and their proofs */ + // TODO: need to model this after + // https://github.com/Concordium/concordium-base/blob/c4a5917309b168e4d69f883bc23f92ea62ec97df/rust-src/concordium_base/src/id/types.rs#L2812 + identityAttributesInfo: unknown; +}; + +/** + * Describes a credential subject of verifiable credential + */ +export type CredentialSubjectProof

= { /** The credential proof ID */ id: DIDString; /** The credential proof data */ @@ -52,6 +75,9 @@ export type CredentialSubjectProof

= { }; /** + * A proof corresponding to one account credential statement. This contains almost + * all the information needed to verify it, except the public commitments. + * * Matches the serialization of `CredentialProof::Account` from concordium-base */ export type VerifiableCredentialProofAccount = { @@ -64,6 +90,9 @@ export type VerifiableCredentialProofAccount = { }; /** + * A proof corresponding to one Web3 ID credential statement. This contains almost + * all the information needed to verify it, except the issuer's public key. + * * Matches the serialization of `CredentialProof::Web3Id` from concordium-base */ export type VerifiableCredentialProofWeb3Id = { @@ -75,10 +104,34 @@ export type VerifiableCredentialProofWeb3Id = { type: ['VerifiableCredential', 'ConcordiumVerifiableCredential', ...string[]]; }; +// TODO: not implemented in base yet... might need to revise +/** + * A proof corresponding to one identity credential statement. This contains almost + * all the information needed to verify it, except TODO: ... + * + * Matches the serialization of `CredentialProof::Identity`from concordium-base + */ +export type VerifiableCredentialProofIdentity = { + /** The credential proof */ + credentialSubject: CredentialSubjectProof; + /** The issuer DID */ + issuer: DIDString; + /** The credential type */ + type: ['VerifiableCredential', 'ConcordiumVerifiableCredential']; +}; + /** + * A proof corresponding to one credential statement. This contains almost + * all the information needed to verify it, except the issuer's public key in + * case of the `Web3Id` proof, and the public commitments in case of the + * `Account` proof. + * * Matches the serialization of `CredentialProof` enum from concordium-base. */ -export type VerifiableCredentialProof = VerifiableCredentialProofAccount | VerifiableCredentialProofWeb3Id; +export type VerifiableCredentialProof = + | VerifiableCredentialProofAccount + | VerifiableCredentialProofWeb3Id + | VerifiableCredentialProofIdentity; /** * Type predicate to check if the proof is a {@linkcode VerifiableCredentialProofWeb3Id}, or consequently a {@linkcode VerifiableCredentialProofAccount} @@ -110,6 +163,11 @@ export function reviveDateFromTimeStampAttribute(this: any, _key: string, value: return value; } +/** + * A presentation is the response to a corresponding request containing a set of statements about an identity. + * It contains proofs for statements, ownership proof for all credentials, and a context. The + * only missing part to verify the proof are the public commitments. + */ export class VerifiablePresentation { presentationContext: string; proof: ConcordiumWeakLinkingProofV1; diff --git a/packages/sdk/src/wasm/VerifiablePresentationV1/audit/private.ts b/packages/sdk/src/wasm/VerifiablePresentationV1/audit/private.ts new file mode 100644 index 000000000..ec8ce17e8 --- /dev/null +++ b/packages/sdk/src/wasm/VerifiablePresentationV1/audit/private.ts @@ -0,0 +1,169 @@ +import { Buffer } from 'buffer/index.js'; +import _JB from 'json-bigint'; + +import { ConcordiumGRPCClient } from '../../../grpc/index.js'; +import { sha256 } from '../../../hash.js'; +import { AccountSigner, signTransaction } from '../../../signHelpers.js'; +import { + AccountTransaction, + AccountTransactionHeader, + AccountTransactionType, + NextAccountNonce, + RegisterDataPayload, +} from '../../../types.js'; +import { AccountAddress, DataBlob, TransactionExpiry, TransactionHash } from '../../../types/index.js'; +import { VerifiablePresentationRequestV1, VerifiablePresentationV1, VerificationAuditRecord } from '../index.js'; + +const JSONBig = _JB({ alwaysParseAsBig: true, useNativeBigInt: true }); + +/** + * A private verification audit record that contains the complete verifiable presentation + * request and response data. This record maintains the full audit trail of a verification + * interaction, including all sensitive data that should be kept private. + * + * Private audit records are used internally by verifiers to maintain complete records + * of verification interactions, while only publishing hash-based public records on-chain + * to preserve privacy. + */ +class PrivateVerificationAuditRecord { + /** The type identifier for this audit record */ + public readonly type = 'ConcordiumVerificationAuditRecord'; + /** The version of the audit record format */ + public readonly version = 1; + + /** + * Creates a new private verification audit record. + * + * @param id - Unique identifier for this audit record + * @param request - The verifiable presentation request that was made + * @param presentation - The verifiable presentation that was provided in response + */ + constructor( + public readonly id: string, + public request: VerifiablePresentationRequestV1.Type, + public presentation: VerifiablePresentationV1.Type + ) {} + + /** + * Serializes the private audit record to a JSON representation. + * + * @returns The JSON representation of this audit record + */ + public toJSON(): JSON { + return { ...this, request: this.request.toJSON(), presentation: this.presentation.toJSON() }; + } +} + +/** + * A private verification audit record that contains the complete verifiable presentation + * request and response data. This record maintains the full audit trail of a verification + * interaction, including all sensitive data that should be kept private. + * + * Private audit records are used internally by verifiers to maintain complete records + * of verification interactions, while only publishing hash-based public records on-chain + * to preserve privacy. + */ +export type Type = PrivateVerificationAuditRecord; + +/** + * JSON representation of a private verification audit record. + * Contains the serialized forms of the request and presentation data. + */ +export type JSON = Pick & { + /** The serialized verifiable presentation request */ + request: VerifiablePresentationRequestV1.JSON; + /** The serialized verifiable presentation */ + presentation: VerifiablePresentationV1.JSON; +}; + +/** + * Creates a new private verification audit record. + * + * @param id - Unique identifier for the audit record + * @param request - The verifiable presentation request + * @param presentation - The verifiable presentation response + * + * @returns A new private verification audit record instance + */ +export function create( + id: string, + request: VerifiablePresentationRequestV1.Type, + presentation: VerifiablePresentationV1.Type +): PrivateVerificationAuditRecord { + return new PrivateVerificationAuditRecord(id, request, presentation); +} + +/** + * Deserializes a private verification audit record from its JSON representation. + * + * @param json - The JSON representation to deserialize + * @returns The deserialized private verification audit record + */ +export function fromJSON(json: JSON): PrivateVerificationAuditRecord { + return new PrivateVerificationAuditRecord( + json.id, + VerifiablePresentationRequestV1.fromJSON(json.request), + VerifiablePresentationV1.fromJSON(json.presentation) + ); +} + +/** + * Converts a private verification audit record to a public audit record. + * + * This function creates a privacy-preserving public record that contains only + * a hash of the private record data, suitable for publishing on-chain while + * maintaining the privacy of the original verification interaction. + * + * @param record - The private audit record to convert + * @param info - Optional additional public information to include + * + * @returns A public verification audit record containing only the hash + */ +export function toPublic(record: PrivateVerificationAuditRecord, info?: string): VerificationAuditRecord.Type { + const message = Buffer.from(JSONBig.stringify(record)); // TODO: replace this with proper hashing.. properly from @concordium/rust-bindings + const hash = Uint8Array.from(sha256([message])); + return VerificationAuditRecord.create(hash, info); +} + +/** + * Registers a public verification audit record on the Concordium blockchain. + * + * This function converts a private audit record to a public one and registers + * it as transaction data on the blockchain. This provides a verifiable timestamp + * and immutable record of the verification event while preserving privacy. + * + * @param privateRecord - The private audit record to register publicly + * @param grpc - The Concordium GRPC client for blockchain interaction + * @param sender - The account address that will send the transaction + * @param signer - The signer for the transaction + * @param info - Optional additional public information to include + * + * @returns Promise resolving to the public record and transaction hash + * @throws Error if the transaction fails or network issues occur + */ +export async function registerPublicRecord( + privateRecord: PrivateVerificationAuditRecord, + grpc: ConcordiumGRPCClient, + sender: AccountAddress.Type, + signer: AccountSigner, + info?: string +): Promise<{ publicRecord: VerificationAuditRecord.Type; transactionHash: TransactionHash.Type }> { + const nextNonce: NextAccountNonce = await grpc.getNextAccountNonce(sender); + const header: AccountTransactionHeader = { + expiry: TransactionExpiry.futureMinutes(60), + nonce: nextNonce.nonce, + sender, + }; + + const publicRecord = toPublic(privateRecord, info); + const payload: RegisterDataPayload = { data: new DataBlob(VerificationAuditRecord.createAnchor(publicRecord)) }; + const accountTransaction: AccountTransaction = { + header: header, + payload, + type: AccountTransactionType.RegisterData, + }; + const signature = await signTransaction(accountTransaction, signer); + const transactionHash = await grpc.sendAccountTransaction(accountTransaction, signature); + + return { publicRecord, transactionHash }; +} diff --git a/packages/sdk/src/wasm/VerifiablePresentationV1/audit/public.ts b/packages/sdk/src/wasm/VerifiablePresentationV1/audit/public.ts new file mode 100644 index 000000000..129a767e3 --- /dev/null +++ b/packages/sdk/src/wasm/VerifiablePresentationV1/audit/public.ts @@ -0,0 +1,145 @@ +import { Buffer } from 'buffer/index.js'; + +import { HexString } from '../../../types.js'; +import { cborDecode, cborEncode } from '../../../types/cbor.js'; + +/** + * A public verification audit record that contains only a hash of the private + * verification data. This record is designed to be published on-chain to provide + * a verifiable timestamp and immutable proof of a verification event while + * preserving the privacy of the underlying verification data. + * + * Public audit records serve as privacy-preserving anchors that can be used to + * prove that a verification occurred at a specific time without revealing the + * contents of the verification. + */ +class VerificationAuditRecord { + /** + * Creates a new public verification audit record. + * + * @param hash - SHA-256 hash of the private verification audit record + * @param info - Optional additional public information about the verification + */ + constructor( + // TODO: possibly add a specialized type for sha256 hashes + public readonly hash: Uint8Array, + public readonly info?: string + ) {} + + /** + * Serializes the public audit record to a JSON representation. + * + * @returns The JSON representation of this audit record + */ + public toJSON(): JSON { + let json: JSON = { hash: Buffer.from(this.hash).toString('hex') }; + if (this.info !== undefined) json.info = this.info; + return json; + } +} + +/** + * A public verification audit record that contains only a hash of the private + * verification data. This record is designed to be published on-chain to provide + * a verifiable timestamp and immutable proof of a verification event while + * preserving the privacy of the underlying verification data. + * + * Public audit records serve as privacy-preserving anchors that can be used to + * prove that a verification occurred at a specific time without revealing the + * contents of the verification. + */ +export type Type = VerificationAuditRecord; + +/** + * JSON representation of a public verification audit record. + * Contains the hash as a hex string and optional public information. + */ +export type JSON = { + /** The SHA-256 hash of the private audit record, encoded as a hex string */ + hash: HexString; + /** Optional additional public information about the verification */ + info?: string; +}; + +/** + * Deserializes a public verification audit record from its JSON representation. + * + * @param json - The JSON representation to deserialize + * @returns The deserialized public verification audit record + */ +export function fromJSON(json: JSON): VerificationAuditRecord { + return new VerificationAuditRecord(Uint8Array.from(Buffer.from(json.hash, 'hex')), json.info); +} + +/** + * Creates a new public verification audit record. + * + * @param hash - SHA-256 hash of the private verification data + * @param info - Optional additional public information + * @returns A new public verification audit record instance + */ +export function create(hash: Uint8Array, info?: string): VerificationAuditRecord { + return new VerificationAuditRecord(hash, info); +} + +/** + * Data structure for CBOR-encoded verification audit anchors. + * This format is used when storing audit records on the Concordium blockchain. + */ +export type AnchorData = { + /** Type identifier for _Concordium Verification Audit Anchor_ */ + type: 'CCDVAA'; + /** Version of the anchor data format */ + version: number; + /** SHA-256 hash of the private audit record */ + hash: Uint8Array; // TODO: possibly add a specialized type for sha256 hashes + /** Optional public information that can be included in the anchor */ + public?: Record; +}; + +/** + * Creates a CBOR-encoded anchor for a verification audit record. + * + * This function creates a standardized CBOR-encoded representation of the + * audit record that can be stored on the Concordium blockchain as transaction data. + * The anchor includes the audit record hash and optional public metadata. + * + * @param value - The verification audit record to create an anchor for + * @param publicInfo - Optional public information to include in the anchor + * + * @returns CBOR-encoded anchor data suitable for blockchain storage + */ +export function createAnchor(value: VerificationAuditRecord, publicInfo?: Record): Uint8Array { + const data: AnchorData = { + type: 'CCDVAA', + version: 1, + hash: value.hash, + public: publicInfo, + }; + return cborEncode(data); +} + +/** + * Decodes a CBOR-encoded verification audit anchor. + * + * This function parses and validates a CBOR-encoded anchor that was previously + * created with `createAnchor`. It ensures the anchor has the correct format + * and contains all required fields. + * + * @param cbor - The CBOR-encoded anchor data to decode + * @returns The decoded anchor data structure + * @throws Error if the CBOR data is invalid or doesn't match expected format + */ +export function decodeAnchor(cbor: Uint8Array): AnchorData { + const value = cborDecode(cbor); + if (typeof value !== 'object' || value === null) throw new Error('Expected a cbor encoded object'); + // required fields + if (!('type' in value) || value.type !== 'CCDVAA') throw new Error('Expected "type" to be "CCDVAA"'); + if (!('version' in value) || typeof value.version !== 'number') + throw new Error('Expected "version" to be a number'); + if (!('hash' in value) || !(value.hash instanceof Uint8Array)) + throw new Error('Expected "hash" to be a Uint8Array'); + // optional fields + if ('public' in value && typeof value.public !== 'object') throw new Error('Expected "public" to be an object'); + return value as AnchorData; +} diff --git a/packages/sdk/src/wasm/VerifiablePresentationV1/index.ts b/packages/sdk/src/wasm/VerifiablePresentationV1/index.ts new file mode 100644 index 000000000..b96cf6f18 --- /dev/null +++ b/packages/sdk/src/wasm/VerifiablePresentationV1/index.ts @@ -0,0 +1,31 @@ +/** + * @fileoverview Concordium Verifiable Presentation V1 API + * + * This module provides types and functions for working with verifiable presentations + * on the Concordium blockchain. It includes support for creating presentation requests, + * generating zero-knowledge proofs, and managing audit records for verification events. + * + * Key components: + * - VerifiablePresentationRequestV1: For creating and managing presentation requests + * - VerifiablePresentationV1: For creating and verifying presentations with ZK proofs + * - VerificationAuditRecord: For public audit trails of verification events + * - PrivateVerificationAuditRecord: For private audit records with full data + */ +import * as PrivateVerificationAuditRecord from './audit/private.js'; +import * as VerificationAuditRecord from './audit/public.js'; +import * as VerifiablePresentationV1 from './proof.js'; +import * as VerifiablePresentationRequestV1 from './request.js'; + +export { + /** Namespace for verifiable presentation request operations */ + VerifiablePresentationRequestV1, + /** Namespace for verifiable presentation proof operations */ + VerifiablePresentationV1, + /** Namespace for private verification audit record operations */ + PrivateVerificationAuditRecord, + /** Namespace for public verification audit record operations */ + VerificationAuditRecord, +}; + +/** Export all common types used across the verifiable presentation system */ +export * from './types.js'; diff --git a/packages/sdk/src/wasm/VerifiablePresentationV1/internal.ts b/packages/sdk/src/wasm/VerifiablePresentationV1/internal.ts new file mode 100644 index 000000000..5e7044480 --- /dev/null +++ b/packages/sdk/src/wasm/VerifiablePresentationV1/internal.ts @@ -0,0 +1,65 @@ +import { Buffer } from 'buffer/index.js'; + +import { BlockHash } from '../../types/index.js'; +import { CredentialContextLabel, GivenContext } from './types.js'; + +/** + * JSON representation of given context information. + * All context data is serialized to strings for transport and storage. + */ +export type GivenContextJSON = { + /** The label identifying the type of context */ + label: CredentialContextLabel | string; + /** The context data serialized as a string */ + context: string; +}; + +/** + * Serializes given context information to its JSON representation. + * + * This function handles the conversion of different context types to their + * string representations for JSON serialization. Binary data is converted + * to hex strings, while other data types are handled appropriately. + * + * @param context - The context information to serialize + * @returns The JSON representation of the context + */ +export function givenContextToJSON(context: GivenContext): GivenContextJSON { + switch (context.label) { + case 'Nonce': + case 'PaymentHash': + return { ...context, context: new Buffer(context.context as Uint8Array).toString('hex') }; + case 'BlockHash': + return { ...context, context: context.context.toJSON() }; + case 'ConnectionID': + case 'ResourceID': + case 'ContextString': + default: + return context; + } +} + +/** + * Deserializes given context information from its JSON representation. + * + * This function handles the conversion of JSON string representations back + * to their proper types. Hex strings are converted back to Uint8Arrays, + * and other types are handled appropriately. + * + * @param context - The JSON representation to deserialize + * @returns The deserialized context information + */ +export function givenContextFromJSON(context: GivenContextJSON): GivenContext { + switch (context.label) { + case 'Nonce': + case 'PaymentHash': + return { label: 'Nonce', context: new Uint8Array(Buffer.from(context.context, 'hex')) }; + case 'BlockHash': + return { label: 'BlockHash', context: BlockHash.fromJSON(context.context) }; + case 'ConnectionID': + case 'ResourceID': + case 'ContextString': + default: + return context as GivenContext; + } +} diff --git a/packages/sdk/src/wasm/VerifiablePresentationV1/proof.ts b/packages/sdk/src/wasm/VerifiablePresentationV1/proof.ts new file mode 100644 index 000000000..a297d5b92 --- /dev/null +++ b/packages/sdk/src/wasm/VerifiablePresentationV1/proof.ts @@ -0,0 +1,435 @@ +// TODO: remove any eslint disable once fully implemented + +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Buffer } from 'buffer/index.js'; + +import { + AttributeKey, + ConcordiumGRPCClient, + CryptographicParameters, + HexString, + Network, + TransactionKindString, + TransactionStatusEnum, + TransactionSummaryType, + VerifiablePresentationRequestV1, + isKnown, + sha256, +} from '../../index.js'; +import { ConcordiumWeakLinkingProofV1 } from '../../types/VerifiablePresentation.js'; +import { bail } from '../../util.js'; +import { + AtomicStatementV2, + CommitmentInput, + CredentialsInputs, + DIDString, + IdentityCommitmentInput, + SpecifiedCredentialStatement, + SpecifiedIdentityCredentialStatement, + isSpecifiedAccountCredentialStatement, + isSpecifiedIdentityCredentialStatement, +} from '../../web3-id/index.js'; +import { Web3IdProofRequest, getVerifiablePresentation } from '../web3Id.js'; +import { GivenContextJSON, givenContextFromJSON, givenContextToJSON } from './internal.js'; +import * as Request from './request.js'; +import { GivenContext, ZKProofV4 } from './types.js'; + +/** + * Context information for a verifiable presentation proof. + * This extends the request context by filling in the requested context + * information with actual values provided by the presenter. + */ +export type Context = { + /** Type identifier for the context format */ + type: 'ConcordiumContextInformationV1'; + /** Context information that was already provided in the request */ + given: GivenContext[]; + /** Context information that was requested and is now filled with actual values */ + requested: GivenContext[]; +}; + +/** + * Creates a proof context by filling in the requested context from a presentation request. + * + * This function validates that all requested context information has been provided + * and creates a complete context for generating the verifiable presentation proof. + * + * @param requestContext - The original request context with labels for requested data + * @param filledRequestedContext - The actual context data filling the requested labels + * + * @returns A complete context for proof generation + * @throws Error if not all requested context is provided or if there's a mismatch + */ +// Fails if not given the full amount of requested context. +export function createContext(requestContext: Request.Context, filledRequestedContext: GivenContext[]): Context { + // First we validate that the requested context is filled in `filledRequestedContext`. + if (requestContext.requested.length !== filledRequestedContext.length) + throw new Error('Mismatch between amount of requested context and filled context'); + requestContext.requested.every( + (requestedLabel) => + filledRequestedContext.some((requestedData) => requestedData.label === requestedLabel) || + bail(`No data for requested context ${requestedLabel} found`) + ); + return { type: 'ConcordiumContextInformationV1', given: requestContext.given, requested: filledRequestedContext }; +} + +/** + * A verifiable credential based on identity information from an identity provider. + * This credential type contains zero-knowledge proofs about identity attributes + * without revealing the actual identity information. + */ +export type IdentityBasedCredential = { + /** Type identifiers for this credential format */ + type: ['VerifiableCredential', 'ConcordiumVerifiableCredentialV1', 'ConcordiumIDBasedCredential']; + /** The credential subject containing identity-based statements */ + credentialSubject: { + /** The identity disclosure information also acts as ephemeral ID */ + id: HexString; + /** Statements about identity attributes (should match request) */ + statement: AtomicStatementV2[]; + }; + /** The zero-knowledge proof for attestation */ + proof: ZKProofV4; + /** Issuer of the original ID credential */ + issuer: DIDString; +}; + +function createIdentityCredentialStub( + { id, statement }: SpecifiedIdentityCredentialStatement, + ipIndex: number +): IdentityBasedCredential { + const network = id.split(':')[1] as Network; + const proof: ZKProofV4 = { + type: 'ConcordiumZKProofV4', + createdAt: new Date().toISOString(), + proofValue: '0102'.repeat(32), + }; + const credentialSubject: IdentityBasedCredential['credentialSubject'] = { + statement: statement, + id: '123456'.repeat(8), + }; + return { + type: ['VerifiableCredential', 'ConcordiumVerifiableCredentialV1', 'ConcordiumIDBasedCredential'], + proof, + issuer: `ccd:${network.toLowerCase()}:idp:${ipIndex}`, + credentialSubject, + }; +} + +/** + * A verifiable credential based on an account credential on the Concordium blockchain. + * This credential type contains zero-knowledge proofs about account credentials + * and their associated identity attributes. + */ +export type AccountBasedCredential = { + /** Type identifiers for this credential format */ + type: ['VerifiableCredential', 'ConcordiumVerifiableCredentialV1', 'ConcordiumAccountBasedCredential']; + /** The credential subject containing account-based statements */ + credentialSubject: { + /** The account credential identifier as a DID */ + id: DIDString; + /** Statements about account attributes (should match request) */ + statement: AtomicStatementV2[]; + }; + /** The zero-knowledge proof for attestation */ + proof: ZKProofV4; + /** The issuer of the ID credential used to open the account credential */ + issuer: DIDString; +}; + +/** + * A verifiable credential based on Web3 ID smart contract information. + * This credential type contains zero-knowledge proofs about smart contract + * issued credentials and their attributes. + */ +export type Web3BasedCredential = { + /** Type identifiers for this credential format */ + type: ['VerifiableCredential', 'ConcordiumVerifiableCredentialV1', 'ConcordiumWeb3BasedCredential']; + /** The credential subject containing Web3 ID statements */ + credentialSubject: { + /** The account credential identifier as a DID */ + id: DIDString; + /** Statements about Web3 ID attributes (should match request) */ + statement: AtomicStatementV2[]; + }; + /** The zero-knowledge proof for attestation */ + proof: ZKProofV4; + /** The issuer of the ID credential used to open the account credential */ + issuer: DIDString; +}; + +/** + * Union type representing all supported verifiable credential formats + * in Concordium verifiable presentations. + * + * The structure is reminiscent of a w3c verifiable credential + */ +export type Credential = IdentityBasedCredential | AccountBasedCredential | Web3BasedCredential; + +/** + * A verifiable presentation containing zero-knowledge proofs of credential statements. + * This class represents a complete response to a verifiable presentation request, + * including the context, credentials, and cryptographic proofs. + */ +class VerifiablePresentationV1 { + /** + * Creates a new verifiable presentation. + * + * @param presentationContext - The complete context for this presentation + * @param verifiableCredential - Array of verifiable credentials with proofs + * @param proof - Optional weak linking proof (required for account-based credentials) + */ + constructor( + public readonly presentationContext: Context, + public readonly verifiableCredential: Credential[], + // only present if the verifiable credential includes an account based credential + public readonly proof?: ConcordiumWeakLinkingProofV1 + ) {} + + /** + * Serializes the verifiable presentation to a JSON representation. + * + * @returns The JSON representation of this presentation + */ + public toJSON(): JSON { + let json: JSON = { + presentationContext: { + type: this.presentationContext.type, + given: this.presentationContext.given.map(givenContextToJSON), + requested: this.presentationContext.requested.map(givenContextToJSON), + }, + verifiableCredential: this.verifiableCredential, + }; + + if (this.proof !== undefined) json.proof = this.proof; + return json; + } +} + +/** + * A verifiable presentation containing zero-knowledge proofs of credential statements. + * This class represents a complete response to a verifiable presentation request, + * including the context, credentials, and cryptographic proofs. + */ +export type Type = VerifiablePresentationV1; + +/** + * JSON representation of a verifiable presentation. + * Used for serialization and network transmission of presentation data. + */ +export type JSON = { + /** The presentation context with serialized context information */ + presentationContext: Pick & { given: GivenContextJSON[]; requested: GivenContextJSON[] }; + /** Array of verifiable credentials with their proofs */ + verifiableCredential: Credential[]; + /** Optional weak linking proof for account-based credentials */ + proof?: ConcordiumWeakLinkingProofV1; +}; + +/** + * Deserializes a verifiable presentation from its JSON representation. + * + * This function reconstructs the presentation object from JSON data, handling + * the conversion of serialized context information back to proper types. + * + * @param value - The JSON representation to deserialize + * @returns The deserialized verifiable presentation + */ +export function fromJSON(value: JSON): VerifiablePresentationV1 { + const presentationContext: Context = { + type: value.presentationContext.type, + given: value.presentationContext.given.map(givenContextFromJSON), + requested: value.presentationContext.requested.map(givenContextFromJSON), + }; + return new VerifiablePresentationV1(presentationContext, value.verifiableCredential, value.proof); +} + +/** + * Creates a verifiable presentation from an anchored presentation request. + * + * This function retrieves the presentation request anchor from the blockchain, + * verifies its integrity, and creates a verifiable presentation with the + * requested statements and proofs. It automatically includes blockchain + * context (block hash) in the presentation. + * + * @param grpc - Concordium GRPC client for blockchain interaction + * @param presentationRequest - The anchored presentation request + * @param statements - The credential statements to prove + * @param inputs - The commitment inputs for generating proofs + * @param additionalContext - Additional context information beyond block hash + * + * @returns Promise resolving to the created verifiable presentation + * @throws Error if anchor verification fails or blockchain interaction errors + */ +export async function createFromAnchor( + grpc: ConcordiumGRPCClient, + presentationRequest: Request.Type, + statements: SpecifiedCredentialStatement[], + inputs: CommitmentInput[], + additionalContext: GivenContext[] +): Promise { + const globalContext = await grpc.getCryptographicParameters(); + const transaction = await grpc.getBlockItemStatus(presentationRequest.transactionRef); + if (transaction.status !== TransactionStatusEnum.Finalized) { + throw new Error('presentation request anchor transaction not finalized'); + } + const { summary, blockHash } = transaction.outcome; + if ( + !isKnown(summary) || + summary.type !== TransactionSummaryType.AccountTransaction || + summary.transactionType !== TransactionKindString.RegisterData + ) { + throw new Error('Unexpected transaction type found for presentation request anchor transaction'); + } + + const expectedAnchor = VerifiablePresentationRequestV1.computeAnchorHash( + presentationRequest.requestContext, + presentationRequest.credentialStatements + ); + const transactionAnchor = VerifiablePresentationRequestV1.decodeAnchor( + Buffer.from(summary.dataRegistered.data, 'hex') + ); + if (Buffer.from(expectedAnchor).toString('hex') !== Buffer.from(transactionAnchor.hash).toString('hex')) { + throw new Error('presentation anchor verification failed.'); + } + + const blockContext: GivenContext = { label: 'BlockHash', context: blockHash }; + const proofContext = createContext(presentationRequest.requestContext, [...additionalContext, blockContext]); + return create(statements, inputs, proofContext, globalContext); +} + +/** + * Creates a verifiable presentation with the specified statements, inputs, and context. + * + * This function generates zero-knowledge proofs for the requested credential statements + * using the provided commitment inputs and proof context. It handles different types + * of credentials (identity-based, account-based, Web3-based) and creates appropriate + * proofs for each. + * + * @param requestStatements - The credential statements to prove + * @param inputs - The commitment inputs for generating proofs + * @param proofContext - The complete context for proof generation + * @param globalContext - Concordium network cryptographic parameters + * + * @returns The created verifiable presentation with all proofs + */ +// TODO: this entire function should call a function in @concordium/rust-bindings to create the verifiable +// presentation from the function arguments. For now, we hack something together from the old protocol which +// means filtering and mapping the input/output. +export function create( + requestStatements: SpecifiedCredentialStatement[], + inputs: CommitmentInput[], + proofContext: Context, + globalContext: CryptographicParameters +): VerifiablePresentationV1 { + // first we filter out the id statements, as they're not compatible with the current implementation + // in concordium-base + const idStatements: [number, SpecifiedIdentityCredentialStatement][] = []; + const compatibleStatements: Exclude[] = []; + requestStatements.forEach((s, i) => { + if (isSpecifiedIdentityCredentialStatement(s)) idStatements.push([i, s]); + else compatibleStatements.push(s); + }); + + // correspondingly, filter out the the inputs for identity credentials + const idInputs = inputs.filter((ci) => ci.type === 'identity') as IdentityCommitmentInput[]; + const compatibleInputs = inputs.filter((ci) => ci.type !== 'identity'); + + if (idStatements.length !== idInputs.length) throw new Error('Mismatch between provided statements and inputs'); + + const challenge = sha256([Buffer.from(JSON.stringify([compatibleStatements, proofContext]))]).toString('hex'); + const request: Web3IdProofRequest = { challenge, credentialStatements: compatibleStatements }; + + const { verifiableCredential, proof } = getVerifiablePresentation({ + commitmentInputs: compatibleInputs, + globalContext, + request, + }); + // Map the output to match the format of the V1 protocol. + const compatibleCredentials: Credential[] = verifiableCredential.map((c, i) => { + const { proof, ...credentialSubject } = c.credentialSubject; + const { created, type: _type, ...proofValues } = proof; + const type = isSpecifiedAccountCredentialStatement(compatibleStatements[i]) + ? 'ConcordiumAccountBasedCredential' + : 'ConcordiumWeb3BasedCredential'; + return { + proof: { createdAt: created, type: 'ConcordiumZKProofV4', proofValue: JSON.stringify(proofValues) }, + issuer: c.issuer, + type: ['VerifiableCredential', 'ConcordiumVerifiableCredentialV1', type] as any, + credentialSubject, + }; + }); + // and add stubbed ID credentials in + const idCredentials: [number, IdentityBasedCredential][] = idStatements.map(([originalIndex, statement], i) => [ + originalIndex, + createIdentityCredentialStub(statement, idInputs[i].context.ipInfo.ipIdentity), + ]); + + const credentials: Credential[] = []; + let compatibleCounter = 0; + for (let i = 0; i < requestStatements.length; i += 1) { + const idCred = idCredentials.find((entry) => entry[0] === i); + if (idCred !== undefined) { + credentials.push(idCred[1]); + } else { + credentials.push(compatibleCredentials[compatibleCounter]); + compatibleCounter += 1; + } + } + + return new VerifiablePresentationV1(proofContext, credentials, proof); +} + +/** + * Describes the result of a verifiable presentation verification, which can either succeed + * or fail with an associated {@linkcode Error} + */ +export type VerificationResult = { type: 'success' } | { type: 'failed'; error: Error }; + +/** + * Verifies a verifiable presentation against its corresponding request. + * + * This function validates the cryptographic proofs in the presentation, + * checks that they match the original request, and verifies them against + * the provided public data and cryptographic parameters. + * + * @param presentation - The verifiable presentation to verify + * @param request - The original presentation request + * @param cryptographicParameters - Concordium network cryptographic parameters + * @param publicData - Public credential data for verification + * + * @returns a {@linkcode VerificationResult} + */ +// TODO: for now this just returns true, but this should be replaced with call to the coresponding function in +// @concordium/rust-bindings that verifies the presentation in the context of the request. +export function verify( + presentation: VerifiablePresentationV1, + request: Request.Type, + cryptographicParameters: CryptographicParameters, + publicData: CredentialsInputs[] +): VerificationResult { + return { type: 'success' }; +} + +/** + * Verifies a verifiable presentation using a Concordium node. + * + * This function performs verification by querying a Concordium node for + * the necessary cryptographic parameters and public data, then validates + * the presentation proofs against the blockchain state. + * + * @param presentation - The verifiable presentation to verify + * @param request - The original presentation request + * @param grpc - Concordium GRPC client for node communication + * @param network - The Concordium network to verify against + * + * @returns Promise resolving to a {@linkcode VerificationResult} + */ +export async function verifyWithNode( + presentation: VerifiablePresentationV1, + request: Request.Type, + grpc: ConcordiumGRPCClient, + network: Network +): Promise { + return { type: 'success' }; +} diff --git a/packages/sdk/src/wasm/VerifiablePresentationV1/request.ts b/packages/sdk/src/wasm/VerifiablePresentationV1/request.ts new file mode 100644 index 000000000..fbf96e182 --- /dev/null +++ b/packages/sdk/src/wasm/VerifiablePresentationV1/request.ts @@ -0,0 +1,326 @@ +import { sha256 } from '../../hash.js'; +import { + AccountAddress, + AccountSigner, + AccountTransaction, + AccountTransactionHeader, + AccountTransactionType, + ConcordiumGRPCClient, + NextAccountNonce, + RegisterDataPayload, + cborDecode, + cborEncode, + signTransaction, +} from '../../index.js'; +import { ContractAddress, DataBlob, TransactionExpiry, TransactionHash } from '../../types/index.js'; +import { CredentialStatement, StatementProverQualifier } from '../../web3-id/types.js'; +import { GivenContextJSON, givenContextFromJSON, givenContextToJSON } from './internal.js'; +import { CredentialContextLabel, GivenContext } from './types.js'; + +/** + * Context information for a verifiable presentation request. + * Contains both the context data that is already known (given) and + * the context data that needs to be provided by the presenter (requested). + */ +export type Context = { + /** Type identifier for the context format */ + type: 'ConcordiumContextInformationV1'; + /** Context information that is already provided */ + given: GivenContext[]; + /** Context information that must be provided by the presenter */ + requested: CredentialContextLabel[]; +}; + +/** + * Creates a new context object with the proper type identifier. + * + * @param context - The context data without the type field + * @returns A complete context object with type identifier + */ +export function createContext(context: Omit): Context { + return { type: 'ConcordiumContextInformationV1', ...context }; +} + +/** + * Creates a simple context with commonly used parameters for basic verification scenarios. + * + * This is a convenience function that creates a context with a nonce for freshness, + * a connection ID for session tracking, and a context string for additional information. + * It requests BlockHash and ResourceID to be provided by the presenter. + * + * @param nonce - Cryptographic nonce for preventing replay attacks + * @param connectionId - Identifier for the verification session + * @param contextString - Additional context information + * + * @returns A context object configured for basic verification + */ +export function createSimpleContext(nonce: Uint8Array, connectionId: string, contextString: string): Context { + return createContext({ + given: [ + { label: 'Nonce', context: nonce }, + { label: 'ConnectionID', context: connectionId }, + { label: 'ContextString', context: contextString }, + ], + requested: ['BlockHash', 'ResourceID'], + }); +} + +/** + * Data structure for CBOR-encoded verifiable presentation request anchors. + * This format is used when storing presentation requests on the Concordium blockchain. + */ +export type AnchorData = { + /** Type identifier for Concordium Verifiable Request Anchor */ + type: 'CCDVRA'; + /** Version of the anchor data format */ + version: number; + /** Hash of the presentation request */ + // TODO: maybe use a specific type for sha256 hash + hash: Uint8Array; + /** Optional public information that can be included in the anchor */ + public?: Record; +}; + +/** + * Creates a CBOR-encoded anchor for a verifiable presentation request. + * + * This function creates a standardized CBOR-encoded representation of the + * presentation request that can be stored on the Concordium blockchain as + * transaction data. The anchor includes a hash of the request and optional + * public metadata. + * + * @param context - The context information for the request + * @param credentialStatements - The credential statements being requested + * @param publicInfo - Optional public information to include in the anchor + * + * @returns CBOR-encoded anchor data suitable for blockchain storage + */ +export function createAnchor( + context: Context, + credentialStatements: CredentialStatement[], + publicInfo?: Record +): Uint8Array { + const hash = computeAnchorHash(context, credentialStatements); + const data: AnchorData = { type: 'CCDVRA', version: 1, hash, public: publicInfo }; + return cborEncode(data); +} + +/** + * Computes a hash of the presentation request context and statements. + * + * This hash is used to create a tamper-evident anchor that can be stored + * on-chain to prove the request was made at a specific time and with + * specific parameters. + * + * @param context - The context information for the request + * @param credentialStatements - The credential statements being requested + * + * @returns SHA-256 hash of the serialized request data + */ +export function computeAnchorHash(context: Context, credentialStatements: CredentialStatement[]): Uint8Array { + // TODO: this is a quick and dirty anchor implementation that needs to be replaced with + // proper serialization, which is TBD. + const sanitizedContext: Context = { + ...context, + given: context.given.map( + (c) => + ({ + ...c, + // convert any potential `Buffer` instances to raw Uint8Array to avoid discrepancies when decoding + context: c.context instanceof Uint8Array ? Uint8Array.from(c.context) : c.context, + }) as GivenContext + ), + }; + const contextDigest = cborEncode(sanitizedContext); + const statementsDigest = cborEncode(credentialStatements); + return Uint8Array.from(sha256([contextDigest, statementsDigest])); +} + +/** + * Decodes a CBOR-encoded verifiable presentation request anchor. + * + * This function parses and validates a CBOR-encoded anchor that was previously + * created with `createAnchor`. It ensures the anchor has the correct format + * and contains all required fields. + * + * @param cbor - The CBOR-encoded anchor data to decode + * @returns The decoded anchor data structure + * @throws Error if the CBOR data is invalid or doesn't match expected format + */ +export function decodeAnchor(cbor: Uint8Array): AnchorData { + const value = cborDecode(cbor); + if (typeof value !== 'object' || value === null) throw new Error('Expected a cbor encoded object'); + // required fields + if (!('type' in value) || value.type !== 'CCDVRA') throw new Error('Expected "type" to be "CCDVRA"'); + if (!('version' in value) || typeof value.version !== 'number') + throw new Error('Expected "version" to be a number'); + if (!('hash' in value) || !(value.hash instanceof Uint8Array)) + throw new Error('Expected "hash" to be a Uint8Array'); + // optional fields + if ('public' in value && typeof value.public !== 'object') throw new Error('Expected "public" to be an object'); + return value as AnchorData; +} + +/** + * JSON representation of a verifiable presentation request. + * Used for serialization and network transmission of request data. + * + * The structure is reminiscent of a w3c verifiable presentation + */ +export type JSON = { + /** The request context with serialized given contexts */ + requestContext: Pick & { given: GivenContextJSON[] }; + /** The credential statements being requested */ + credentialStatements: CredentialStatement[]; + /** Reference to the blockchain transaction containing the request anchor */ + transactionRef: TransactionHash.JSON; +}; + +/** + * A verifiable presentation request that specifies what credentials and proofs + * are being requested from a credential holder. This class encapsulates the + * request context, the specific credential statements needed, and a reference + * to the blockchain transaction that anchors the request. + */ +class VerifiablePresentationRequestV1 { + /** + * Creates a new verifiable presentation request. + * + * @param requestContext - The context information for this request + * @param credentialStatements - The specific credential statements being requested + * @param transactionRef - Reference to the blockchain transaction anchoring this request + */ + constructor( + public readonly requestContext: Context, + public readonly credentialStatements: CredentialStatement[], + public readonly transactionRef: TransactionHash.Type // NOTE: renamed from requestTX in ADR + ) {} + + /** + * Serializes the presentation request to a JSON representation. + * + * @returns The JSON representation of this presentation request + */ + public toJSON(): JSON { + return { + requestContext: { ...this.requestContext, given: this.requestContext.given.map(givenContextToJSON) }, + credentialStatements: this.credentialStatements, + transactionRef: this.transactionRef.toJSON(), + }; + } +} + +/** + * A verifiable presentation request that specifies what credentials and proofs + * are being requested from a credential holder. This class encapsulates the + * request context, the specific credential statements needed, and a reference + * to the blockchain transaction that anchors the request. + */ +export type Type = VerifiablePresentationRequestV1; + +/** + * Deserializes a verifiable presentation request from its JSON representation. + * + * This function reconstructs the request object from JSON data, handling + * the conversion of serialized context information and credential statements + * back to their proper types. + * + * @param json - The JSON representation to deserialize + * @returns The deserialized verifiable presentation request + */ +export function fromJSON(json: JSON): VerifiablePresentationRequestV1 { + const requestContext = { ...json.requestContext, given: json.requestContext.given.map(givenContextFromJSON) }; + const statements: CredentialStatement[] = json.credentialStatements.map(({ statement, idQualifier }) => { + let mappedQualifier: StatementProverQualifier; + switch (idQualifier.type) { + case 'id': + mappedQualifier = { type: 'id', issuers: idQualifier.issuers.map(Number) }; + break; + case 'cred': + mappedQualifier = { type: 'cred', issuers: idQualifier.issuers.map(Number) }; + break; + case 'sci': + mappedQualifier = { + type: 'sci', + issuers: idQualifier.issuers.map((c) => ContractAddress.create(c.index, c.subindex)), + }; + break; + default: + mappedQualifier = idQualifier; + } + return { statement, idQualifier: mappedQualifier } as CredentialStatement; + }); + + return new VerifiablePresentationRequestV1( + requestContext, + statements, + TransactionHash.fromJSON(json.transactionRef) + ); +} + +/** + * Creates a verifiable presentation request and anchors it to the Concordium blockchain. + * + * This function creates a presentation request with the specified context and credential + * statements, then stores an anchor of the request on the blockchain as a data registration + * transaction. The blockchain anchor provides a tamper-evident timestamp and immutable + * record of the request. + * + * @param grpc - The Concordium GRPC client for blockchain interaction + * @param sender - The account address that will send the anchoring transaction + * @param signer - The signer for the anchoring transaction + * @param context - The context information for the request (without type field) + * @param credentialStatements - The credential statements being requested + * @param anchorPublicInfo - Optional public information to include in the anchor + * + * @returns Promise resolving to the created presentation request + * @throws Error if the transaction fails or network issues occur + */ +export async function createAndAchor( + grpc: ConcordiumGRPCClient, + sender: AccountAddress.Type, + signer: AccountSigner, + context: Omit, + credentialStatements: CredentialStatement[], + anchorPublicInfo?: Record +): Promise { + const requestContext = createContext(context); + const anchor = createAnchor(requestContext, credentialStatements, anchorPublicInfo); + + const nextNonce: NextAccountNonce = await grpc.getNextAccountNonce(sender); + const header: AccountTransactionHeader = { + expiry: TransactionExpiry.futureMinutes(60), + nonce: nextNonce.nonce, + sender, + }; + const payload: RegisterDataPayload = { data: new DataBlob(anchor) }; + const accountTransaction: AccountTransaction = { + header: header, + payload, + type: AccountTransactionType.RegisterData, + }; + const signature = await signTransaction(accountTransaction, signer); + const transactionHash = await grpc.sendAccountTransaction(accountTransaction, signature); + + return create(requestContext, credentialStatements, transactionHash); +} + +/** + * Creates a new verifiable presentation request. + * + * This is a factory function that creates a request with the specified + * context, credential statements, and transaction reference. + * + * @param context - The context information for the request + * @param credentialStatements - The credential statements being requested + * @param transactionRef - Reference to the blockchain transaction anchoring this request + * + * @returns A new verifiable presentation request instance + */ +export function create( + context: Context, + credentialStatements: CredentialStatement[], + transactionRef: TransactionHash.Type +): VerifiablePresentationRequestV1 { + return new VerifiablePresentationRequestV1(context, credentialStatements, transactionRef); +} diff --git a/packages/sdk/src/wasm/VerifiablePresentationV1/types.ts b/packages/sdk/src/wasm/VerifiablePresentationV1/types.ts new file mode 100644 index 000000000..68a3eca9a --- /dev/null +++ b/packages/sdk/src/wasm/VerifiablePresentationV1/types.ts @@ -0,0 +1,74 @@ +import { HexString } from '../../types.js'; +import { BlockHash } from '../../types/index.js'; + +/** + * Labels for different types of context information that can be provided + * in verifiable presentation requests and proofs. + */ +export type CredentialContextLabel = + | 'ContextString' + | 'ResourceID' + | 'BlockHash' + | 'PaymentHash' + | 'ConnectionID' + | 'Nonce'; + +/** + * Generic type for context information with a specific label and context data. + * + * @template L - The label type for this context + * @template C - The context data type + */ +type GivenContextGen = { + /** The label identifying the type of context */ + label: L; + /** The actual context data */ + context: C; // TODO: make explicit variants with unknown represented as hex string. +}; + +/** Context information containing a string value for general purposes */ +type GivenContextContextString = GivenContextGen<'ContextString', string>; + +/** Context information containing a resource identifier */ +type GivenContextResourceID = GivenContextGen<'ResourceID', string>; + +/** Context information containing a Concordium block hash */ +type GivenContextBlockHash = GivenContextGen<'BlockHash', BlockHash.Type>; + +/** Context information containing a payment-related hash */ +type GivenContextPaymentHash = GivenContextGen<'PaymentHash', Uint8Array>; // TODO: what's this hash? + +/** Context information containing a connection identifier */ +type GivenContextConnectionID = GivenContextGen<'ConnectionID', string>; + +/** Context information containing a cryptographic nonce */ +type GivenContextNonce = GivenContextGen<'Nonce', Uint8Array>; // TODO: we should probably enforce some specific length here, e.g. sha256. + +/** + * Union type representing all possible context information that can be provided + * in verifiable presentation interactions. Each variant contains specific data + * relevant to the verification process. + */ +export type GivenContext = + | GivenContextContextString + | GivenContextResourceID + | GivenContextBlockHash + | GivenContextPaymentHash + | GivenContextConnectionID + | GivenContextNonce; + +/** + * Zero-knowledge proof data structure for Concordium verifiable presentations. + * Contains the cryptographic proof that validates the statements made in a + * verifiable credential without revealing the underlying private information. + */ +export type ZKProofV4 = { + /** The type identifier for this proof format */ + type: 'ConcordiumZKProofV4'; + /** ISO formatted datetime when the proof was created */ + createdAt: string; + /** Serialized cryptographic proof data as hex string */ + // TODO: what's going on here? why is this not a `AtomicProof[]` corresponding to the `AtomicStatement[]` + // in the statement being proven? + proofValue: HexString; // a serialization of the proof corresponding to the credential it's contained in. +}; diff --git a/packages/sdk/src/wasm/index.ts b/packages/sdk/src/wasm/index.ts index 16a688cb1..5f1f0e9a4 100644 --- a/packages/sdk/src/wasm/index.ts +++ b/packages/sdk/src/wasm/index.ts @@ -9,3 +9,4 @@ export * from './HdWallet.js'; export * from './identity.js'; export * from './credentialDeploymentTransactions.js'; export * from './web3Id.js'; +export * from './VerifiablePresentationV1/index.js'; diff --git a/packages/sdk/src/wasm/web3Id.ts b/packages/sdk/src/wasm/web3Id.ts index 7e85215f6..c3a822ff0 100644 --- a/packages/sdk/src/wasm/web3Id.ts +++ b/packages/sdk/src/wasm/web3Id.ts @@ -4,7 +4,13 @@ import { stringify } from 'json-bigint'; import { CryptographicParameters } from '../types.js'; import { VerifiablePresentation } from '../types/VerifiablePresentation.js'; import { VerifyWeb3IdCredentialSignatureInput } from '../web3-id/helpers.js'; -import { CredentialsInputs, Web3IdProofInput, Web3IdProofRequest } from '../web3-id/types.js'; +import { + CommitmentInput, + CredentialsInputs, + SpecifiedCredentialStatement, + isSpecifiedAccountCredentialStatement, + isSpecifiedWeb3IdCredentialStatement, +} from '../web3-id/types.js'; /** * Verifies that the given signature is correct for the given values/randomness/holder/issuerPublicKey/issuerContract @@ -14,10 +20,38 @@ export function verifyWeb3IdCredentialSignature(input: VerifyWeb3IdCredentialSig return wasm.verifyWeb3IdCredentialSignature(stringify(input)); } +/** + * Describes a proof request which is at the core of computing the corresponding proof. + */ +export type Web3IdProofRequest = { + /** The challenge of the proof */ + challenge: string; + /** The statements paired with the credential IDs to prove them for */ + credentialStatements: SpecifiedCredentialStatement[]; +}; + +/** + * The input to {@linkcode getVerifiablePresentation} + */ +export type Web3IdProofInput = { + request: Web3IdProofRequest; + globalContext: CryptographicParameters; + commitmentInputs: CommitmentInput[]; +}; + /** * Given a statement about an identity and the inputs necessary to prove the statement, produces a proof that the associated identity fulfills the statement. */ export function getVerifiablePresentation(input: Web3IdProofInput): VerifiablePresentation { + // validate that we don't pass any unsupported credentials in + if ( + input.request.credentialStatements.some( + (statement) => + !isSpecifiedWeb3IdCredentialStatement(statement) && !isSpecifiedAccountCredentialStatement(statement) + ) + ) + throw new Error('Identity proofs are not supported for this verifiable presentation protocol'); + try { const s: VerifiablePresentation = VerifiablePresentation.fromString( // Use json-bigint stringify to ensure we can handle bigints diff --git a/packages/sdk/src/web3-id/proofs.ts b/packages/sdk/src/web3-id/proofs.ts index 80ac9feb0..693ce42fe 100644 --- a/packages/sdk/src/web3-id/proofs.ts +++ b/packages/sdk/src/web3-id/proofs.ts @@ -3,7 +3,17 @@ import { Buffer } from 'buffer/index.js'; import { EU_MEMBERS, MAX_DATE, MIN_DATE, StatementBuilder, StatementTypes } from '../commonProofTypes.js'; import { MAX_U64 } from '../constants.js'; import { getPastDate } from '../id/idProofs.js'; -import { AttributeKey, AttributeKeyString, AttributeList, AttributesKeys, HexString, Network } from '../types.js'; +import { + AttributeKey, + AttributeKeyString, + AttributeList, + AttributesKeys, + HexString, + IdentityObjectV1, + IdentityProvider, + Network, + Policy, +} from '../types.js'; import type * as ContractAddress from '../types/ContractAddress.js'; import { ConcordiumHdWallet } from '../wasm/HdWallet.js'; import { @@ -14,6 +24,8 @@ import { } from './helpers.js'; import { AccountCommitmentInput, + AccountCredentialQualifier, + AccountCredentialStatement, AtomicStatementV2, AttributeType, CredentialSchemaProperty, @@ -21,28 +33,40 @@ import { CredentialStatement, CredentialStatements, CredentialSubject, + DIDString, IDENTITY_SUBJECT_SCHEMA, - IdentityQualifier, + IdObjectUseData, + IdentityCommitmentInput, + IdentityCredentialQualifier, + IdentityCredentialStatement, MembershipStatementV2, NonMembershipStatementV2, RangeStatementV2, StatementAttributeType, - StatementProverQualifier, - VerifiableCredentialQualifier, + Web3IdCredentialQualifier, Web3IssuerCommitmentInput, isTimestampAttribute, } from './types.js'; +/** Maximum byte length allowed for string attributes. */ export const MAX_STRING_BYTE_LENGTH = 31; +/** Minimum date in ISO format supported by the system. */ export const MIN_DATE_ISO = '-262144-01-01T00:00:00Z'; +/** Maximum date in ISO format supported by the system. */ export const MAX_DATE_ISO = '+262143-12-31T23:59:59.999999999Z'; +/** Minimum date timestamp value supported by the system. */ export const MIN_DATE_TIMESTAMP = Date.parse(MIN_DATE_ISO); +/** Maximum date timestamp value supported by the system. */ export const MAX_DATE_TIMESTAMP = Date.parse(MAX_DATE_ISO); +/** Valid timestamp range string for error messages. */ const TIMESTAMP_VALID_VALUES = MIN_DATE_ISO + 'to ' + MAX_DATE_ISO; +/** Valid string range description for error messages. */ const STRING_VALID_VALUES = '0 to ' + MAX_STRING_BYTE_LENGTH + ' bytes as UTF-8'; +/** Valid integer range description for error messages. */ const INTEGER_VALID_VALUES = '0 to ' + MAX_U64; +/** Throws a standardized range error for statement validation. */ const throwRangeError = (title: string, property: string, end: string, mustBe: string, validRange: string) => { throw new Error( title + @@ -56,6 +80,7 @@ const throwRangeError = (title: string, property: string, end: string, mustBe: s validRange ); }; +/** Throws a standardized set error for statement validation. */ const throwSetError = (title: string, property: string, mustBe: string, validRange: string) => { throw new Error( title + @@ -68,35 +93,46 @@ const throwSetError = (title: string, property: string, mustBe: string, validRan ); }; +/** Checks if a credential schema property represents a timestamp attribute. */ function isTimestampAttributeSchemaProperty(properties?: CredentialSchemaProperty) { return properties && properties.type === 'object' && properties.properties.type.const === 'date-time'; } +/** Validates that a string attribute value is within allowed byte length. */ function isValidStringAttribute(attributeValue: string): boolean { return Buffer.from(attributeValue, 'utf-8').length <= MAX_STRING_BYTE_LENGTH; } +/** Validates that an integer attribute value is within allowed range. */ function isValidIntegerAttribute(attributeValue: bigint) { return attributeValue >= 0 && attributeValue <= MAX_U64; } +/** Validates that a timestamp attribute value is within allowed date range. */ function isValidTimestampAttribute(attributeValue: Date) { return attributeValue.getTime() >= MIN_DATE_TIMESTAMP && attributeValue.getTime() <= MAX_DATE_TIMESTAMP; } +/** Validates a timestamp attribute type and value. */ function validateTimestampAttribute(value: AttributeType) { return isTimestampAttribute(value) && isValidTimestampAttribute(timestampToDate(value)); } +/** Validates a string attribute type and value. */ function validateStringAttribute(value: AttributeType) { return typeof value === 'string' && isValidStringAttribute(value); } +/** Validates an integer attribute type and value. */ function validateIntegerAttribute(value: AttributeType) { return typeof value === 'bigint' && isValidIntegerAttribute(value); } -function verifyRangeStatement(statement: RangeStatementV2, properties?: CredentialSchemaProperty) { +/** Verifies that a range statement is valid according to schema constraints. */ +function verifyRangeStatement( + statement: RangeStatementV2, + properties?: CredentialSchemaProperty +) { if (statement.lower === undefined) { throw new Error('Range statements must contain a lower field'); } @@ -142,8 +178,9 @@ function verifyRangeStatement(statement: RangeStatementV2, properties?: Credenti } } -function verifySetStatement( - statement: MembershipStatementV2 | NonMembershipStatementV2, +/** Verifies that a set statement (membership or non-membership) is valid according to schema constraints. */ +function verifySetStatement( + statement: MembershipStatementV2 | NonMembershipStatementV2, statementTypeName: string, properties?: CredentialSchemaProperty ) { @@ -176,7 +213,11 @@ function verifySetStatement( } } -function verifyAtomicStatement(statement: AtomicStatementV2, schema?: CredentialSchemaSubject) { +/** Verifies that an atomic statement is valid according to schema constraints. */ +function verifyAtomicStatement( + statement: AtomicStatementV2, + schema?: CredentialSchemaSubject +) { if (statement.type === undefined) { throw new Error('Statements must contain a type field'); } @@ -184,11 +225,11 @@ function verifyAtomicStatement(statement: AtomicStatementV2, schema?: Credential throw new Error('Statements must contain an attributeTag field'); } - if (schema && !Object.keys(schema.properties.attributes.properties).includes(statement.attributeTag)) { + if (schema && !Object.keys(schema.properties.attributes.properties).includes(statement.attributeTag as string)) { throw new Error('Unknown attributeTag: ' + statement.attributeTag); } - const property = schema && schema.properties.attributes.properties[statement.attributeTag]; + const property = schema && schema.properties.attributes.properties[statement.attributeTag as string]; switch (statement.type) { case StatementTypes.AttributeInRange: @@ -210,9 +251,9 @@ function verifyAtomicStatement(statement: AtomicStatementV2, schema?: Credential /** * Verify that the atomicStatement is valid, and check it doesn't break any "composite" rules in the context of the existing statements. */ -function verifyAtomicStatementInContext( - statement: AtomicStatementV2, - existingStatements: AtomicStatementV2[], +function verifyAtomicStatementInContext( + statement: AtomicStatementV2, + existingStatements: AtomicStatementV2[], schema?: CredentialSchemaSubject ) { verifyAtomicStatement(statement, schema); @@ -237,24 +278,40 @@ export function verifyAtomicStatements(statements: AtomicStatementV2[], schema?: return true; } -function getWeb3IdCredentialQualifier(validContractAddresses: ContractAddress.Type[]): VerifiableCredentialQualifier { +/** Creates a Web3 ID credential qualifier with valid contract addresses. */ +function getWeb3IdCredentialQualifier(validContractAddresses: ContractAddress.Type[]): Web3IdCredentialQualifier { return { type: 'sci', issuers: validContractAddresses, }; } -function getAccountCredentialQualifier(validIdentityProviders: number[]): IdentityQualifier { +/** Creates an account credential qualifier with valid identity providers. */ +function getAccountCredentialQualifier(validIdentityProviders: number[]): AccountCredentialQualifier { return { type: 'cred', issuers: validIdentityProviders, }; } -export class AtomicStatementBuilder implements InternalBuilder { - statements: AtomicStatementV2[]; +/** Creates an identity credential qualifier with valid identity providers. */ +function getIdentityCredentialQualifier(validIdentityProviders: number[]): IdentityCredentialQualifier { + return { + type: 'id', + issuers: validIdentityProviders, + }; +} + +export class AtomicStatementBuilder implements InternalBuilder { + /** Array of atomic statements being built. */ + statements: AtomicStatementV2[]; + /** Optional schema for validating statements against credential schema. */ schema: CredentialSchemaSubject | undefined; + /** + * Creates a new AtomicStatementBuilder. + * @param schema Optional credential schema for validation + */ constructor(schema?: CredentialSchemaSubject) { this.statements = []; this.schema = schema; @@ -263,15 +320,16 @@ export class AtomicStatementBuilder implements InternalBuilder { /** * Outputs the built statement. */ - getStatement(): AtomicStatementV2[] { + getStatement(): AtomicStatementV2[] { return this.statements; } /** * This checks whether the given statement may be added to the statement being built. * If the statement breaks any rules, this will throw an error. + * @param statement The atomic statement to validate */ - private check(statement: AtomicStatementV2) { + private check(statement: AtomicStatementV2) { if (this.schema) { verifyAtomicStatementInContext(statement, this.statements, this.schema); } @@ -284,8 +342,8 @@ export class AtomicStatementBuilder implements InternalBuilder { * @param upper: the upper end of the range, exclusive. * @returns the updated builder */ - addRange(attribute: string, lower: StatementAttributeType, upper: StatementAttributeType): this { - const statement: AtomicStatementV2 = { + addRange(attribute: AttributeKey, lower: StatementAttributeType, upper: StatementAttributeType): this { + const statement: AtomicStatementV2 = { type: StatementTypes.AttributeInRange, attributeTag: attribute, lower: statementAttributeTypeToAttributeType(lower), @@ -302,8 +360,8 @@ export class AtomicStatementBuilder implements InternalBuilder { * @param set: the set of values that the attribute must be included in. * @returns the updated builder */ - addMembership(attribute: string, set: StatementAttributeType[]): this { - const statement: AtomicStatementV2 = { + addMembership(attribute: AttributeKey, set: StatementAttributeType[]): this { + const statement: AtomicStatementV2 = { type: StatementTypes.AttributeInSet, attributeTag: attribute, set: set.map(statementAttributeTypeToAttributeType), @@ -319,8 +377,8 @@ export class AtomicStatementBuilder implements InternalBuilder { * @param set: the set of values that the attribute must be included in. * @returns the updated builder */ - addNonMembership(attribute: string, set: StatementAttributeType[]): this { - const statement: AtomicStatementV2 = { + addNonMembership(attribute: AttributeKey, set: StatementAttributeType[]): this { + const statement: AtomicStatementV2 = { type: StatementTypes.AttributeNotInSet, attributeTag: attribute, set: set.map(statementAttributeTypeToAttributeType), @@ -336,8 +394,8 @@ export class AtomicStatementBuilder implements InternalBuilder { * @param attribute the attribute that should be revealed * @returns the updated builder */ - revealAttribute(attribute: string): this { - const statement: AtomicStatementV2 = { + revealAttribute(attribute: AttributeKey): this { + const statement: AtomicStatementV2 = { type: StatementTypes.RevealAttribute, attributeTag: attribute, }; @@ -347,14 +405,14 @@ export class AtomicStatementBuilder implements InternalBuilder { } } -export class AccountStatementBuild extends AtomicStatementBuilder { +export class IdentityStatementBuilder extends AtomicStatementBuilder { /** * Add to the statement that the age is at minimum the given value. * This adds a range statement that the date of birth is between 1st of january 1800 and years ago. * @param age: the minimum age allowed. * @returns the updated builder */ - addMinimumAge(age: number): AtomicStatementBuilder { + addMinimumAge(age: number): IdentityStatementBuilder { return this.addRange(AttributeKeyString.dob, MIN_DATE, getPastDate(age, 1)); } @@ -364,7 +422,7 @@ export class AccountStatementBuild extends AtomicStatementBuilder { * @param age: the maximum age allowed. * @returns the updated builder */ - addMaximumAge(age: number): AtomicStatementBuilder { + addMaximumAge(age: number): IdentityStatementBuilder { return this.addRange(AttributeKeyString.dob, getPastDate(age + 1, 1), MAX_DATE); } @@ -375,7 +433,7 @@ export class AccountStatementBuild extends AtomicStatementBuilder { * @param maxAge: the maximum age allowed. * @returns the updated builder */ - addAgeInRange(minAge: number, maxAge: number): AtomicStatementBuilder { + addAgeInRange(minAge: number, maxAge: number): IdentityStatementBuilder { return this.addRange(AttributeKeyString.dob, getPastDate(maxAge + 1, 1), getPastDate(minAge)); } @@ -385,7 +443,7 @@ export class AccountStatementBuild extends AtomicStatementBuilder { * @param earliestDate: the earliest the document is allow to be expired at, should be a string in YYYYMMDD format. * @returns the updated builder */ - documentExpiryNoEarlierThan(earliestDate: string): AtomicStatementBuilder { + documentExpiryNoEarlierThan(earliestDate: string): IdentityStatementBuilder { return this.addRange(AttributeKeyString.idDocExpiresAt, earliestDate, MAX_DATE); } @@ -393,7 +451,7 @@ export class AccountStatementBuild extends AtomicStatementBuilder { * Add to the statement that the country of residence is one of the EU countries * @returns the updated builder */ - addEUResidency(): AtomicStatementBuilder { + addEUResidency(): IdentityStatementBuilder { return this.addMembership(AttributeKeyString.countryOfResidence, EU_MEMBERS); } @@ -401,48 +459,89 @@ export class AccountStatementBuild extends AtomicStatementBuilder { * Add to the statement that the nationality is one of the EU countries * @returns the updated builder */ - addEUNationality(): AtomicStatementBuilder { + addEUNationality(): IdentityStatementBuilder { return this.addMembership(AttributeKeyString.nationality, EU_MEMBERS); } } -type InternalBuilder = StatementBuilder; -export class Web3StatementBuilder { +/** Internal type alias for statement builders. */ +type InternalBuilder = StatementBuilder; +/** Builder class for constructing credential statements with different credential types. */ +export class CredentialStatementBuilder { + /** Array of credential statements being built. */ private statements: CredentialStatements = []; - private add( - idQualifier: StatementProverQualifier, - builderCallback: (builder: InternalBuilder) => void, + /** + * Add statements for Web3 ID credentials. + * + * @param validContractAddresses Array of contract addresses that are valid issuers + * @param builderCallback Callback function to build the statements using the provided builder + * @param schema Optional credential schema for validation + * + * @returns The updated builder instance + */ + forWeb3IdCredentials( + validContractAddresses: ContractAddress.Type[], + builderCallback: (builder: InternalBuilder) => void, schema?: CredentialSchemaSubject - ): this { - const builder = new AtomicStatementBuilder(schema); + ): CredentialStatementBuilder { + const builder = new AtomicStatementBuilder(schema); builderCallback(builder); this.statements.push({ - idQualifier, + idQualifier: getWeb3IdCredentialQualifier(validContractAddresses), statement: builder.getStatement(), }); return this; } - addForVerifiableCredentials( - validContractAddresses: ContractAddress.Type[], - builderCallback: (builder: InternalBuilder) => void, - schema?: CredentialSchemaSubject - ): this { - return this.add(getWeb3IdCredentialQualifier(validContractAddresses), builderCallback, schema); + /** + * Add statements for account credentials. + * + * @param validIdentityProviders Array of identity provider indices that are valid issuers + * @param builderCallback Callback function to build the statements using the provided identity builder + * + * @returns The updated builder instance + */ + forAccountCredentials( + validIdentityProviders: number[], + builderCallback: (builder: IdentityStatementBuilder) => void + ): CredentialStatementBuilder { + const builder = new IdentityStatementBuilder(IDENTITY_SUBJECT_SCHEMA); + builderCallback(builder); + const statement: AccountCredentialStatement = { + idQualifier: getAccountCredentialQualifier(validIdentityProviders), + statement: builder.getStatement(), + }; + this.statements.push(statement); + return this; } - addForIdentityCredentials( + /** + * Add statements for identity credentials. + * + * @param validIdentityProviders Array of identity provider indices that are valid issuers + * @param builderCallback Callback function to build the statements using the provided identity builder + * + * @returns The updated builder instance + */ + forIdentityCredentials( validIdentityProviders: number[], - builderCallback: (builder: InternalBuilder) => void - ): this { - return this.add( - getAccountCredentialQualifier(validIdentityProviders), - builderCallback, - IDENTITY_SUBJECT_SCHEMA - ); + builderCallback: (builder: IdentityStatementBuilder) => void + ): CredentialStatementBuilder { + const builder = new IdentityStatementBuilder(IDENTITY_SUBJECT_SCHEMA); + builderCallback(builder); + const statement: IdentityCredentialStatement = { + idQualifier: getIdentityCredentialQualifier(validIdentityProviders), + statement: builder.getStatement(), + }; + this.statements.push(statement); + return this; } + /** + * Get the built credential statements. + * @returns Array of credential statements + */ getStatements(): CredentialStatements { return this.statements; } @@ -471,6 +570,13 @@ export function createAccountDID(network: Network, credId: string): string { return 'did:ccd:' + network.toLowerCase() + ':cred:' + credId; } +/** + * Create a DID string for an identity credential. Used to build a request for a verifiable credential. + */ +export function createIdentityDID(network: Network, identityProviderIndex: number, identityIndex: number): DIDString { + return 'did:ccd:' + network.toLowerCase() + ':id:' + identityProviderIndex + ':' + identityIndex; +} + /** * Create the commitment input required to create a proof for the given statements, using an account credential. */ @@ -559,6 +665,48 @@ export function createWeb3CommitmentInputWithHdWallet( ); } +/** + * Create the commitment input required to create a proof for the given statements, using an identity credential. + */ +export function createIdentityCommitmentInput( + identityProvider: IdentityProvider, + idObject: IdentityObjectV1, + idObjectUseData: IdObjectUseData +): IdentityCommitmentInput { + const policy: Policy = { + createdAt: idObject.attributeList.createdAt, + validTo: idObject.attributeList.validTo, + revealedAttributes: {}, // TODO: this is temporary until we figure out if it's needed or not + }; + return { + type: 'identity', + context: identityProvider, + idObject, + idObjectUseData, + policy, + }; +} + +/** + * Create the commitment input required to create a proof for the given statements, using an identity credential. + * Uses a ConcordiumHdWallet to get values for the {@linkcode IdObjectUseData}. + */ +export function createIdentityCommitmentInputWithHdWallet( + idObject: IdentityObjectV1, + identityProvider: IdentityProvider, + identityIndex: number, + wallet: ConcordiumHdWallet +): IdentityCommitmentInput { + const prfKey = wallet.getPrfKey(identityProvider.ipInfo.ipIdentity, identityIndex); + const idCredSecret = wallet.getIdCredSec(identityProvider.ipInfo.ipIdentity, identityIndex); + const randomness = wallet.getSignatureBlindingRandomness(identityProvider.ipInfo.ipIdentity, identityIndex); + const idObjectUseData: IdObjectUseData = { + randomness, + aci: { prfKey, credentialHolderInformation: { idCredSecret } }, + }; + return createIdentityCommitmentInput(identityProvider, idObject, idObjectUseData); +} + /** * Helper to check if an attribute value is in the given range. */ diff --git a/packages/sdk/src/web3-id/types.ts b/packages/sdk/src/web3-id/types.ts index a6a057180..7ca5978ff 100644 --- a/packages/sdk/src/web3-id/types.ts +++ b/packages/sdk/src/web3-id/types.ts @@ -6,16 +6,27 @@ import { GenericRangeStatement, GenericRevealStatement, } from '../commonProofTypes.js'; -import type { AttributeKey, CryptographicParameters, HexString } from '../types.js'; +import type { ArInfo, AttributeKey, HexString, IdentityObjectV1, IdentityProvider, IpInfo, Policy } from '../types.js'; import type * as ContractAddress from '../types/ContractAddress.js'; +/** + * The "Distributed Identifier" string. + */ +export type DIDString = string; + +/** Represents a timestamp attribute with type information. */ export type TimestampAttribute = { + /** Identifies this as a date-time type attribute. */ type: 'date-time'; + /** The timestamp value as a string. */ timestamp: string; }; +/** Union type representing different attribute value types. */ export type AttributeType = string | bigint | TimestampAttribute; +/** Union type for statement attribute types, including Date objects. */ export type StatementAttributeType = AttributeType | Date; +/** Type guard to check if an attribute is a timestamp attribute. */ export function isTimestampAttribute(attribute: AttributeType): attribute is TimestampAttribute { return ( (attribute as TimestampAttribute).type === 'date-time' && @@ -23,80 +34,152 @@ export function isTimestampAttribute(attribute: AttributeType): attribute is Tim ); } +/** Commitment input for account credentials containing issuer and attribute information. */ +// NOTE: **MUST** match the serialiation of CommitmentInput::Account in concordium-base export type AccountCommitmentInput = { + /** Identifies this as an account commitment input. */ type: 'account'; + /** The identity provider index that issued the credential. */ issuer: number; + /** Attribute values mapped by attribute name. */ values: Record; + /** Randomness values used for commitments mapped by attribute name. */ randomness: Record; }; +/** Commitment input for Web3 issuer credentials containing signature and signer information. */ +// NOTE: **MUST** match the serialiation of CommitmentInput::Web3Id in concordium-base export type Web3IssuerCommitmentInput = { + /** Identifies this as a Web3 issuer commitment input. */ type: 'web3Issuer'; + /** The credential signature. */ signature: string; + /** The signer's identifier/key. */ signer: string; + /** Attribute values mapped by attribute name. */ values: Record; + /** Randomness values used for commitments mapped by attribute name. */ randomness: Record; }; -export type CommitmentInput = AccountCommitmentInput | Web3IssuerCommitmentInput; - -export type Web3IdProofRequest = { - challenge: string; - credentialStatements: RequestStatement[]; +/** + * Can be computed with a seed phrase through the use of {@linkcode createIdentityCommitmentInputWithHdWallet}. + * The seed phrase must be the once used during the identity issuance process with the identity provider. + */ +export type IdObjectUseData = { + /** Account credential information including secrets and keys. */ + aci: { + /** Information held by the credential holder. */ + credentialHolderInformation: { + /** The identity credential secret. */ + idCredSecret: Uint8Array; + }; + /** The pseudorandom function key. */ + prfKey: Uint8Array; + }; + /** Randomness used for signature blinding. */ + randomness: Uint8Array; }; -export type Web3IdProofInput = { - request: Web3IdProofRequest; - globalContext: CryptographicParameters; - commitmentInputs: CommitmentInput[]; +/** Commitment input for identity credentials containing context and identity object data. */ +// NOTE: **MUST** match the serialiation of CommitmentInput::Identity in concordium-base +export type IdentityCommitmentInput = { + /** Identifies this as an identity credentials commitment input. */ + // TODO: make sure this aligns with the chosen representation in concordium-base + type: 'identity'; + /** The identity provider context. */ + context: IdentityProvider; + /** The identity object containing identity information. */ + idObject: IdentityObjectV1; + /** Additional data required for using the identity object. */ + idObjectUseData: IdObjectUseData; + /** The policy associated with the credential. */ + policy: Policy; }; +/** Union type of all commitment input types. */ +export type CommitmentInput = AccountCommitmentInput | Web3IssuerCommitmentInput | IdentityCommitmentInput; + +/** Represents timestamp property schema for credential attributes. */ export type TimestampProperty = { + /** The title of the property. */ title: string; + /** Indicates this is an object type property. */ type: 'object'; + /** Schema properties for the timestamp object. */ properties: { + /** Schema for the type field. */ type: { + /** The type field is a string. */ type: 'string'; + /** The type field must be 'date-time'. */ const: 'date-time'; }; + /** Schema for the timestamp field. */ timestamp: { + /** The timestamp field is a string. */ type: 'string'; + /** Optional format specification. */ format?: 'date-time'; }; }; + /** Required fields for the timestamp property. */ required: ['type', 'timestamp']; + /** Optional description of the property. */ description?: string; }; +/** Represents simple property schema for string or integer credential attributes. */ export type SimpleProperty = { + /** The title of the property. */ title: string; + /** Optional description of the property. */ description?: string; + /** The type of the property value. */ type: 'string' | 'integer'; + /** Optional format specification for the property. */ format?: string; }; +/** Union type for credential schema property types. */ export type CredentialSchemaProperty = SimpleProperty | TimestampProperty; +/** Schema for credential subject identifier details. */ type IdDetails = { + /** The title of the identifier field. */ title: string; + /** Optional description of the identifier field. */ description?: string; + /** The identifier field is a string. */ type: 'string'; }; +/** Schema for credential attributes collection. */ type CredentialSchemaAttributes = { + /** Optional title for the attributes collection. */ title?: string; + /** Optional description of the attributes collection. */ description?: string; + /** Attributes are represented as an object. */ type: 'object'; + /** Schema properties for individual attributes mapped by attribute name. */ properties: Record; + /** List of required attribute names. */ required: string[]; }; +/** Complete schema definition for credential subjects. */ export type CredentialSchemaSubject = { + /** The credential subject is represented as an object. */ type: 'object'; + /** Schema properties for the credential subject. */ properties: { + /** Schema for the identifier field. */ id: IdDetails; + /** Schema for the attributes collection. */ attributes: CredentialSchemaAttributes; }; + /** List of required fields in the credential subject. */ required: string[]; }; @@ -190,67 +273,159 @@ export const IDENTITY_SUBJECT_SCHEMA: CredentialSchemaSubject = { required: [], }; -export type RangeStatementV2 = GenericRangeStatement; -export type NonMembershipStatementV2 = GenericNonMembershipStatement; -export type MembershipStatementV2 = GenericMembershipStatement; -export type RevealStatementV2 = GenericRevealStatement; +/** Type aliases for different statement types with specific attribute key constraints. */ +export type RangeStatementV2 = GenericRangeStatement; +export type NonMembershipStatementV2 = GenericNonMembershipStatement< + AttributeKey, + AttributeType +>; +export type MembershipStatementV2 = GenericMembershipStatement; +export type RevealStatementV2 = GenericRevealStatement; -export type AtomicStatementV2 = GenericAtomicStatement; +export type AtomicStatementV2 = GenericAtomicStatement; -export type VerifiableCredentialQualifier = { +/** Qualifier for Web3 ID credentials issued by smart contracts. */ +export type Web3IdCredentialQualifier = { + /** Identifies this as a smart contract issuer qualifier. */ type: 'sci'; + /** Array of valid contract addresses that can issue these credentials. */ issuers: ContractAddress.Type[]; }; +/** Index type for identity providers. */ type IdentityProviderIndex = number; -export type IdentityQualifier = { +/** Qualifier for account credentials issued by identity providers. */ +export type AccountCredentialQualifier = { + /** Identifies this as an account credential issuer qualifier. */ type: 'cred'; + /** Array of valid identity provider indices that can issue these credentials. */ + issuers: IdentityProviderIndex[]; +}; + +/** Qualifier for identity credentials issued by identity providers. */ +export type IdentityCredentialQualifier = { + /** Identifies this as an identity issuer qualifier. */ + // TODO: align with the corresponding DID defined in concordium-base + type: 'id'; + /** Array of valid identity provider indices that can issue these credentials. */ issuers: IdentityProviderIndex[]; }; -export type StatementProverQualifier = VerifiableCredentialQualifier | IdentityQualifier; +/** Union type for all statement prover qualifiers. */ +export type StatementProverQualifier = + | Web3IdCredentialQualifier + | AccountCredentialQualifier + | IdentityCredentialQualifier; +/** + * Type predicate to identifying {@linkcode AccountCredentialStatement}s from a {@linkcode CredentialStatement} + */ export function isAccountCredentialStatement(statement: CredentialStatement): statement is AccountCredentialStatement { return statement.idQualifier.type === 'cred'; } -export function isVerifiableCredentialStatement( - statement: CredentialStatement -): statement is VerifiableCredentialStatement { +/** + * Type predicate to identifying {@linkcode Web3IdCredentialStatement}s from a {@linkcode CredentialStatement} + */ +export function isWeb3IdCredentialStatement(statement: CredentialStatement): statement is Web3IdCredentialStatement { return statement.idQualifier.type === 'sci'; } -export interface AccountCredentialStatement extends CredentialStatement { - idQualifier: IdentityQualifier; - statement: AtomicStatementV2[]; +/** + * Type predicate to identifying {@linkcode IdentityCredentialStatement}s from a {@linkcode CredentialStatement} + */ +export function isIdentityCredentialStatement( + statement: CredentialStatement +): statement is IdentityCredentialStatement { + return statement.idQualifier.type === 'id'; } -export interface VerifiableCredentialStatement extends CredentialStatement { - idQualifier: VerifiableCredentialQualifier; - statement: AtomicStatementV2[]; -} +/** Statement type for account credentials with attribute key constraints. */ +export type AccountCredentialStatement = { + /** Qualifier specifying which account credential issuers are valid. */ + idQualifier: AccountCredentialQualifier; + /** Array of atomic statements to prove about the account credential. */ + statement: AtomicStatementV2[]; +}; -export type CredentialStatement = { - idQualifier: StatementProverQualifier; - statement: AtomicStatementV2[]; +/** Statement type for Web3 ID credentials with string attribute keys. */ +export type Web3IdCredentialStatement = { + /** Qualifier specifying which Web3 ID credential issuers are valid. */ + idQualifier: Web3IdCredentialQualifier; + /** Array of atomic statements to prove about the Web3 ID credential. */ + statement: AtomicStatementV2[]; }; -export type RequestStatement = { - id: string; - statement: AtomicStatementV2[]; - /** The type field is present iff the request is for a verifiable credential */ - type?: string[]; +/** Statement type for identity credentials with attribute key constraints. */ +export type IdentityCredentialStatement = { + /** Qualifier specifying which identity credential issuers are valid. */ + idQualifier: IdentityCredentialQualifier; + /** Array of atomic statements to prove about the identity credential. */ + statement: AtomicStatementV2[]; }; -export function isVerifiableCredentialRequestStatement(statement: RequestStatement): boolean { - return Boolean(statement.type); +/** Union type for all credential statement types. */ +export type CredentialStatement = AccountCredentialStatement | Web3IdCredentialStatement | IdentityCredentialStatement; + +/** Specified account credential statement with explicit DID. */ +export type SpecifiedAccountCredentialStatement = { + /** The distributed identifier for the account credential. */ + id: DIDString; + /** Array of atomic statements to prove about the account credential. */ + statement: AtomicStatementV2[]; +}; + +/** Specified Web3 ID credential statement with explicit DID and type information. */ +export type SpecifiedWeb3IdCredentialStatement = { + /** The distributed identifier for the Web3 ID credential. */ + id: DIDString; + /** Array of atomic statements to prove about the Web3 ID credential. */ + statement: AtomicStatementV2[]; + /** Array of type strings associated with the credential. */ + type: string[]; +}; + +/** Specified identity credential statement with explicit DID. */ +export type SpecifiedIdentityCredentialStatement = { + /** The distributed identifier for the identity credential. */ + id: DIDString; + /** Array of atomic statements to prove about the identity credential. */ + statement: AtomicStatementV2[]; +}; + +/** Union type for all specified credential statement types. */ +export type SpecifiedCredentialStatement = + | SpecifiedAccountCredentialStatement + | SpecifiedWeb3IdCredentialStatement + | SpecifiedIdentityCredentialStatement; + +export function isSpecifiedAccountCredentialStatement( + statement: SpecifiedCredentialStatement +): statement is SpecifiedAccountCredentialStatement { + return statement.id.includes(':cred:'); } +export function isSpecifiedWeb3IdCredentialStatement( + statement: SpecifiedCredentialStatement +): statement is SpecifiedWeb3IdCredentialStatement { + return statement.id.includes(':sci:'); +} + +export function isSpecifiedIdentityCredentialStatement( + statement: SpecifiedCredentialStatement +): statement is SpecifiedIdentityCredentialStatement { + return statement.id.includes(':id:'); // TODO: figure out if this matches the identifier. +} + +/** Array type for credential statements. */ export type CredentialStatements = CredentialStatement[]; +/** Represents a credential subject with identifier and attributes. */ export type CredentialSubject = { + /** The identifier of the credential subject. */ id: string; + /** Attributes of the credential subject mapped by attribute name. */ attributes: Record; }; @@ -270,8 +445,18 @@ export type CredentialsInputsWeb3 = { issuerPk: HexString; }; +/** Credentials inputs for identity credential proofs. */ +export type CredentialsInputsIdentity = { + /** Identifies this as identity credentials input. */ + type: 'identityCredentials'; // TODO: or maybe just 'id'? + /** Information about the identity provider. */ + ipInfo: IpInfo; + /** Known anonymity revokers mapped by their index. */ + knownArs: Record; +}; + /** Union of the different inputs required to verify corresponding proofs */ -export type CredentialsInputs = CredentialsInputsAccount | CredentialsInputsWeb3; +export type CredentialsInputs = CredentialsInputsAccount | CredentialsInputsWeb3 | CredentialsInputsIdentity; /** Contains the credential status and inputs required to verify a corresponding credential proof */ export type CredentialWithMetadata = { diff --git a/packages/sdk/test/ci/VerifiablePresentation.test.ts b/packages/sdk/test/ci/VerifiablePresentation.test.ts index 25ca3c930..345ab3065 100644 --- a/packages/sdk/test/ci/VerifiablePresentation.test.ts +++ b/packages/sdk/test/ci/VerifiablePresentation.test.ts @@ -1,4 +1,8 @@ +import { CIS4 } from '../../src/pub/cis4.ts'; +import { Web3IdProofRequest, verifyPresentation } from '../../src/pub/wasm.ts'; +import { CredentialWithMetadata } from '../../src/pub/web3-id.ts'; import { + VerifiablePresentation, replaceDateWithTimeStampAttribute, reviveDateFromTimeStampAttribute, } from '../../src/types/VerifiablePresentation.js'; @@ -19,3 +23,177 @@ test('reviveDateFromTimeStampAttribute', () => { const parsed = JSON.parse(withTimestamp, reviveDateFromTimeStampAttribute); expect(parsed.date).toStrictEqual(new Date(timestamp)); }); + +const TESTNET_PRESENTATION = VerifiablePresentation.fromString(`{ + "presentationContext": "d7bce30c25cad255a30b8bc72a7d4ee654d2d1e0fa4342fde1e453c034e9afa7", + "proof": { + "created": "2024-05-10T06:57:45.005Z", + "proofValue": [ + "08a6bc326070d685fcd11f9ccf81a50c75f76a787bdf3dc1e9246c46783e249f2e539338ac5d1c511fb98e6e14b5174f19e3dbf9a477398868d7d74482ad9f05" + ], + "type": "ConcordiumWeakLinkingProofV1" + }, + "type": "VerifiablePresentation", + "verifiableCredential": [ + { + "credentialSubject": { + "id": "did:ccd:testnet:pkc:70159fe625e369b05c09294692088174dbc5df15b78ebd6722e5cac6f6b93052", + "proof": { + "commitments": { + "commitments": { + "userId": "8b98cc0f223b69c63d10ca4edd2021c83a092893407652ad095aa65e4ffcbd9738448f64c3b0a62f7e7adfa57ba7e641", + "username": "8ae284303ca77dda87cc17bed0b6b4aaba28fe6f1783909a22d887624f3502a42affd545a7cf15f5c3c06f6734b158d5" + }, + "signature": "a32b1e81611650cebfcb1da1cb08ddadf817f33bef7379ec8cf694cf36fc585315d96f7c8d0efe62cd7f69ec6f824fa421a81747f3bdf9a61af9d15bcfdbba0f" + }, + "created": "2024-05-10T06:57:45.001Z", + "proofValue": [ + { + "attribute": "6521470895", + "proof": "d72775bcd3a7aad3e8cd021c913acd21bb171d8e20e6252f99bbca7c39724ab02ea90dd4b8d4795c64ea7490a37426cca7088698f22964b84f35cb80fabc6a2b", + "type": "RevealAttribute" + }, + { + "attribute": "sorenbz", + "proof": "2e7e599dfa5eba3ed58dd8bb98f1bcd3f390059a6426a757cb079c0594dca86428191e4f60ed20bb4c498750af17ca621e1401c0ecd195f5ee4ecc52cc9f6dc5", + "type": "RevealAttribute" + } + ], + "type": "ConcordiumZKProofV3" + }, + "statement": [ + { + "attributeTag": "userId", + "type": "RevealAttribute" + }, + { + "attributeTag": "username", + "type": "RevealAttribute" + } + ] + }, + "issuer": "did:ccd:testnet:sci:6260:0/issuer", + "type": [ + "ConcordiumVerifiableCredential", + "SoMeCredential", + "VerifiableCredential" + ] + }, + { + "credentialSubject": { + "id": "did:ccd:testnet:cred:9549a7e0894fe888a68019e31db5a99a21c9b14cca0513934e9951057c96434a86dd54f90ff2bf18b99dad5ec64d7563", + "proof": { + "created": "2024-05-10T06:57:45.005Z", + "proofValue": [ + { + "attribute": "John", + "proof": "3008e5d80469197adb9537cd6f0390351b9151fb3be57cfeceafdb9969d88da647141e3e24d1d9c821559d63aec12195512b4b2704460e152a3887828b77744c", + "type": "RevealAttribute" + }, + { + "attribute": "Doe", + "proof": "91cebc7d0466b5c569b1c014f86f82cd3a637aa8debeaaf95a8a098cc3d585c43881998a252288761127325278ac275a77b844a3775edc159cf2d90cf0c96a71", + "type": "RevealAttribute" + } + ], + "type": "ConcordiumZKProofV3" + }, + "statement": [ + { + "attributeTag": "firstName", + "type": "RevealAttribute" + }, + { + "attributeTag": "lastName", + "type": "RevealAttribute" + } + ] + }, + "issuer": "did:ccd:testnet:idp:0", + "type": [ + "VerifiableCredential", + "ConcordiumVerifiableCredential" + ] + } + ] +}`); + +const TESTNET_PRESENTATION_DATA: CredentialWithMetadata[] = [ + { + status: CIS4.CredentialStatus.Active, + inputs: { + type: 'web3', + issuerPk: '400d2cd6bc50a29168c02d4e7897a3659d8874f3f2cb349dbbeff37cc58469c1', + }, + }, + { + status: CIS4.CredentialStatus.Expired, + inputs: { + type: 'account', + commitments: { + firstName: + '89be3003492dadd443a25fa497d0501390639e275c9ede84b7d6124c839540a00a5a5bb7c9ca73a69021e3fe97eefd63', + lastName: + '99b1173bdfb4b2c807cbfc5007f91d535cba365d299fc177d355df1f5800ff40a74ca1a2567dcd00b061309599f91dd9', + sex: 'a145554049184f6112d3e4a3fa44ed6bda56445a574aa780a06d09882d8519fe8ceb39b33ac8f6303b610a80c8d1c50a', + dob: 'a7d8bb1eb0afb6cf2f391d29b563ed4e7ad9f029b55a159abe37414ad667ed471be3991eb0bfc405d391828301671853', + countryOfResidence: + '82321ee934060686b4cdfd3718deb45840417dce240da171a39141f709f5c4a6645a3fbf266eef25e7a8237931a24727', + nationality: + 'a2b1c1f0422a5904c66f31bdb6bcdb736f4ed0267d165b3df76533ffd4b5d7edaff63b6c4fdc81db0c090322f5107a8e', + idDocType: + 'b244ed7528c11bc110a9641e505e28002b437ca9df4fa6a2072d7eedca40a9a0a835791a486e8ffc3d6bab60bc0a4066', + idDocNo: + 'b796dce42bce7a0591971fa5e1def8bfe6516b7a9806f5b0287df73ba59d31faaf32e38ddb31fd71b763236cdc47210b', + idDocIssuer: + '8c3d2810ad35a5405e191b2bd5719cb03f1b459a9f8c52d6b4fde84cd7c90b52753e025d245718170438be5ac533e201', + idDocIssuedAt: + '84bdce8c8bc7c867e114468d9fca2a9eee3dfabd4a266a2944d23b2c19f051e894e4e53661d74a7e0ead052be3e838ad', + idDocExpiresAt: + '97e13c07f3e3959d2eae8c74ce95c3f677545b1eb25fe71924c9381426abfac37ce3dd82096b78aa9791695c87992594', + nationalIdNo: + '945a97cac996d42cb1a3548971b2a4c88bcd7683668a51854dbc9e3e4f655cba06b51c29fe6e5077f60eccbe32eea0d0', + taxIdNo: + '81d427defe8fc313590fa3126c211f9c1614f574150be3e279c45a4a5a98ac4c9097158534021ccb28718d0696bf6f48', + }, + }, + }, +]; + +const TESTNET_GLOBAL_CONTEXT = { + onChainCommitmentKey: + 'b14cbfe44a02c6b1f78711176d5f437295367aa4f2a8c2551ee10d25a03adc69d61a332a058971919dad7312e1fc94c5a8d45e64b6f917c540eee16c970c3d4b7f3caf48a7746284878e2ace21c82ea44bf84609834625be1f309988ac523fac', + bulletproofGenerators: + '0000010098855a650637f2086157d32536f646b758a3c45a2f299a4dad7ea3dbd1c4cfb4ba42aca5461f8e45aab9112984572cdf8ba9381c58d98d196b1c03e149ec0c8de86098f25d23d32288c695dc7ae015f6506b1c74c218f080aaadaf25bb8f31539510c87e8fed74e784b63b88afba4953cacc94bceb060f2ad22e555cffe6f0131c027429826dd3a4358fd75a06e8f7c5878791f70384a7f3a90f4b7afa45fae6e0fa7153b840f6fc37aed121d6c51225c56d1ce6bbc88096aa3f86e6b3517daa90baffc69b9eb27aaebf87f04ed091412547ec94aa7bf0c8dd826e3cdd621e11dfef6fc667c53a5abc82c163efa51435b3be74f47073511db07c1af7229c78ec38554a3f7e9fa38499201d4fe6c80e867df75f2c540e57d2e15f9f5e18f73c0a804567494aba85653f57f5f990111d680400bdbee2d0678271ce93a119c107458b8d4e557fc870de9cf0ccce6d34a216a6ffbec9581349366706377b436261200834fb2654ad5cd34f5bfe3f3658ae94838ed38033886844008143d823f7e49db43017655c53b983a883948d7401f26ed14daef0dc46da30f57e187ca8e027063174a412cbfc8a7b606b41fcf7f75b698ce9115afe5124e8d72f4b34ce839742a46885ed60af26f24f8d10d46621d78b5772b0314311ed3bb627e93bac47e7eda5ff4d2ef5f98ef7265677a382a1f7b8f9a43d1e563b66b6de94c52c3c87169bd19b6e884c6a28dd6f51ba64bc36ac07926a8d91a64e88a4e19b4c0fe0db8b76c9c99bfcddc05fd5630c54bff85c041fea34a630ffe1e6313bf039477994696db0596f9e2522e04ffdb530147780099d918ee47031db2870c00b25ba6d201b00ef2459e73f6a8219d7a7ff96833bd1c8a3c24285d93b85a321d1b6e175f2d9ef111a7304438b5f5874f064539d27bf8bc46d8e2473d458f957fa206bc417ab5b8ca4dd5f0595e21063c1bbae14f07bec6f12d95a05166b90896f33aa941e0a057989dcc1ed77430a3e6f93ab16f62598b9816dc203db6ecc7fd7989d2763d4ef72abae3c4aa1e1b9c9965f8772a1be640a18486dc0804a2efdd7175316af3220a6c10db0ac0e2913de42c44f2f54a20687d2b22c9636e0ba8d09747f2229ee6ba0fa49c4a328f2ffcc5ff462ddca0de8b1d13cf85ad590183767a9ae4179b3c617ed776cf14abad1964d362747492547359c4b65e78f6dfb452d5a559ac0c24b8affdf13256e43d3213bdda0c056172771cda382f3f31fecb82ee8dc81a48fe74467665c4e34e07b1954cae4ee97270b94a336779d27646348ffa0eb52663aa60bd8b7fb9311d38a7fc1f0f2842081cb81b594dfcdefb44db1caeeac7527c5a48e8a8ecb83de0514ef184f2a82c4110f9f64ea655fb42218f95e27c81a0adb3fa7b441b71cb15113aef7c318d73f911688d34111928fdff181dbc014be35cc7e3e5bd75bf767686933931bab272eac577558b1314206d97611152ae82c1bd56ab92a4378dbe73e76a9fbd90154bd1a8c73c681f70298d15f1574d7038521b4e9b9cfc0d900c2d74cad594b68923607a1b913cb9484e093ab8ef2020ddbbd77d041581487f41e2dea13832e566325f4fb0f9934df94ee068c676e475251659770b93e0e32c7739d3c430db581308817ba091c56a5087c6248ee79baddc3c233e77b3fdbbd73f1a4298a8277055ee32a96b7e83f76e0070ea65c4a1784e9e281fa70fd9771c762a91c7ea014f60c87d09687768c9d0c8277d84e8af2eee505fda2967631a999619dc332d98bd45d40458923de655769aac754bf9081dbe9318b4880bd325e6be713f3aafb65d8e44b9143acdc7f2ba7f15290c84225c77111e0629d0f82f2f367d2641d0c3658fd54e538286787d379252876f71ec4a96ca72fdf8b156059ec50131317b9243eb71c5e84ab6791303ee0ebacd790bb27767701083afa616413b1d5bf0347cc4199943c938d74eda87660dd40405aedafbaf54b1e177117f632deeb6a9bd5227d9b8ba5693625ac68ed7d1604ccffe73e97a45411b99c666dd5eaa90cd021d05f12f84acf60bafe0c98e8ae213fa65c189256c79afb4b3eba970a8ad4e94374dd6f9d258b8c86335f36481affdcc9eb074f1ace01fae937a7d1a92279bdfb1081e029103e48877e4442199593c1b946aaed494885b0bd9d67fb7abc6f41c0f7574c3cc007d29a4ddc0a966270f5432a47e3e5563061b158eedef8bae522cb1f800c6d5f0a85a4e24a68a9bddb99e408d204e56cd994796bd004cdc5871798df2bf068ac48271290a2b3b1f0eba346f8746c79ce2389ed3ced67bb962fcb50876b7a32afca94216adadc0aed990dfd2b558aa5d8f35aa773e710f14f1641ff7eaa9ecab43ca3dbf32e846a90c8a3771d378aeb9277ef864377f7ca79ec7bc9e4b373ac890cbcbcf7c33df157e0cab0fcc7fc4daa2599177bd9d6bfc4be694a8e834be081bc7fccee530fbf6bac5be0cd1656b4c79ba6e121c39640cda83f81d17324da795da00f9a05317224ba827cecea6cbba1459df4a5bf6b84cd009d5864d44e747e3f30866a7c261004f861359e717c414a699461086e9136e29f9973e5b3f008a77fedb184e6835bc50934178b4bbd8bc9ecbb10865789d063ee7781fb2dc7b9b641b73fb172c3aba915c420c1c72cdc999d1fd224bf4d3d8a5ce26fa36100533682964abc2e99368402b0c518df1b6af245837eadb31a5616884c5474d25f57a228ed2bbef64d5a80e37cb20385a0283072a3b7a11b68bef323ea4804608c0eed5ea213caf6feb183558d56666f472a9abd10ddbf49650c5318109f9c5ed8fac7847644e153638e29e5f45dd00da06c9b9aab4af18eda0d0bfdc48ae42efe79d4e7196ef07c4319efdf042724d9aeb2c13b089b1fde77467c32cdf778749370e2c3b84a466bdfd1aa00ca8ac85c0eb29f1ccba6953a8bf877802ca3e3c31ceba8f9ff0a8684b3b118ab11a27182b033847baa372fb6b4c88e9177b8b131cd69f6ff5ec347db17215fb9890fab1c4d265d405ca74e249fafda004f86f2a5c3fbec7cae6afe6c507e089bb2d47a9e421512c016b99cb1cb15a3b8e75f2d8ff4866731585c686871691b5ca89a1351f6a79b159530fb5825f87131b0bcee2573d3932d223a358fcf08c8d4bae92f26d7fe35255c7486484bcb26693174ec2688abf39db5f091c8c09ad804f4ae42ca8b645200f27629ece81c6b5d94ef7646cbbc4b62a716ac6d2ad21f5a7e2297ebed15f2e085859a1002a9e7658b0d0249446ce9e769caba71536b9329a5fea3a15d52344d42d868b0d92a423fa44f8a6c4aebf50fb496c62d03598de39f1fbdb29e098c6a2e2ca59cbb5e37eb021594463a72471683117b957890f8307828168b1a8f2066a5e5f2c6822589272f259b012822b8be2b15c004a2a69b66c2f5ee5d81aa9a7bbdd44d059587bea89d021c734f62828382c9f13210185482694f8b30b46111d426927ec5f10b155f825d852fd995b9c7ff27861f7fee8dd2b654473198504cdcb53154b29f323ac1adde8510beabb8cef6d1a927f2dc844f746edeef9b256ace54dd8e3219b265ae958ced6593ea154b8188bf58b72ff05c036630b6d55141c4ac227a4d966fde1d0ff1ed9748c66127feec86e7839d77c1c56b46e29d2033a7871b4b0e3c53b3cc4ac49dc8eacd0555b338265be1234b81259b3746eae71600c16b7f4bc692f0a1d3dc2ed855c9fb11b2ff5fd9e6361a82d42bd4ba00635d974fa1214aa8da9180cb158347608e0c44bb5b77397d8b233df7be91e7047d28d18ee66472b98c78091eb95760c5b381d6050779ce65fd1508af8df81a3385f185f7941f0b7aa9bcd6023c6e4db7ac3045aee58f3b2d307a5be14e2b59d05333949d9e8dafbe15b671034b7c629c1de1bf560415cf72761d4d7abcbe3b762d9a66b1b630b75c2af751f6a5d37b43e84139aeb418e0b10dec012a32095cf7305bda79bb3853c3cb1fb3c8447ee4306923b6b693020880bb11df8c78f3411f176b2393776d0b9b3ea7eb0cecfaadbcd8112c4c4ad9960001f4d4df210860470a4c89a0fb9a07229ec3476e1221f490ff80314da398ffbefac3a376c53c0db82d6785bc3d3b0cf3ce4bb54cb9b6fdc29b01bca2894debb7de518a61cc953d4f92a9c52d427e2858db633d042ca14028ef771d9c8c62014676a30ed9d5f9f51036a823a388ed0f72321a13dad31bb28fe383e456361159490942382ed3e49f1c420968877a42b7bbd8c3c9e78723017400dd178f0f89065072d7ef5f5e9baffafb2ce55fe7b35a7865957260b1789e3fddc5bebdfc3301e79b5077a92f6048121c557099c329bb270cb90ad47818398bbdddefdfa6b9d9256b8194f51b45c69176d5d7deb6440c9a81bc5ad0c8b2f31720f071a8ddb7907fb161ed658f890540ac56c506b7b98df3a45f6fc062698bc86d3cc1150db00d702b2d1e3c8eae552eab55a7afd5a5a3be2610867cfff07545c8227dd26c502039240cf00ce2e91b7bd7ed6495980639eaf919635c82c5073acdc21aa275ba87cb7280d0c6321c6a4021b1f0ae7bce23c32238b3f486ce27434721df436446f9ddfb9c66bb1f4b2337803850b2606d8954268eaa0ae060f8a6da2a0e7cc315c1f8cdad4e11bdf5165a2d72ad34f976bfc41000afff158f31e9632b898f2e9886b8b92c81bf916861c62f07ea326e94ddc4ff9af51fc0420b0d3c57fe0d14109ec90a72de74147d8ae1eeebe6b565c4ed81ac01d49b0e6a9551e9fdb7a9e040b4bcbad61fe627983462106e1c0b59ce762ce89b4b1100444048e2fe84850db19d63522d83993067cf025ddaf796e2f11d76da5a2b6ef4a409720ac4e97f7c75973584d935ab0e723f415a5622b03ece90173ea58da495f402f5ecc33526d251da5d2eb5558cbabc1634e8aced49d7c3bfb514c20f7284e8a902855b2296ae90ffb84bd0ea96aac846b6c9dd8ce92c0f9d457ab44a381046e7cd50b4958bf4b854bff340be4d41452ae1e3e19c82b99908c0a45968e580823450ffcc67561dfcdf446a57ed8d032ca94ec3e8dc0a181b9faf478275fc353ba104d5c31cb7338cc4c2cd445ffed7025f576ae84e9b08311b77490b6111dacd7e3afa920193cded42246276cb999d5d85f594352592002efa2d34c387fa1901cc0f63c0f6ed141c7d2a48d5bed031a204cca98c4e26ab7210d4ca5ba3e20df6d1b0e1daa69216b3e1ed98f20ad9f527efe75a0e7e2067c85383b7a614945669b442e1f0d7ee07160902ba9ac292fa0e9c62bc1b0e848856aa680898c4769d965441a16cec21ffc2eff717267cf0f9d4e7139b764c72826309e8e0ce8965c6c96953c3ebedb4ab4c9b35c32b7b59f8715f7c790afe8a3c7a09965192baa75262a16650cfa1706f4cdb4d34043ad1d6a53898164d049647dba7be95b7c620c750e3146155f57ccaeb283936ba6b9fe6a6c6fc5fec11b59b2d12da76075e022a2308fea5865ab5cbc12a4898de2204bb218cb6757d1d3d380b488a22e4dca9c9b1d1e2d01e7a9e707a6c09c323973dd639c7d48f25e4b5b5e95b4394a1221484d337a032b34cf77998acf78efe6d7c8c1ca4f83970bd62c0a790f38e405827a71baf31a9c89892f6745e2d29bc13b5b2873c0581df064a96bf64eecdec5504a75c7e03369662e0ad9f6c2d38c4da79b82f62aeebb69392a78a2cf6fe5a1ea8627b77d5ec828f4d445fbf3a51987023218ce4f944f03041d01976a2780fe632238a828414364b8b98aa8d709bab9c2202b144f9edaab822c33d5d8ec5f62517567711cf25c890036700b7f39b130335b4b8d14d4e9ebdb6c4fc015027907b931fe5cd99ad87976e8181fa284a53185f1e982ae24842abf28f23de420376b205222e5067a2ec53814d659485327eec748b821d70f114fae08ba181f320717dc462c88a15370cf8acba5516c093e13102d92c681bda73f3ab192976179dea08875abdc054e389b309d62a9737e05edd489469f63497da850450e1f299aea08649a9ed1982a4f8a3b251fb8cfb4737ce650d125199d07f7acbbd50dd65ab973be9684ee248269ff200e2f36118cb0eb9104b94149f9c91ebb76f9d46ef5c11b4b78bf6331116110dbe6bebb7169d3c637839bcf89b114ed9d765cd6211405e2dbbe78a4ad608a960a051097a65dc0620a3d1143439c4e04cc6eb1453e43904dc9f9db084d52b2d251dcb1d476141b74c627fbff849eda5a0abc85416b97d9888dd3cd88d33104e8f0fe03ade560dbe4baee030af797fe68e45196a2137d79de2ced8e6c2771d6b16a74b2c1044e9d381a0107adb870af01329c413c0a29b88d92b3a3c4a117424e613704917bb2aeb343dc22d96b0831db78b63063f10527cafc6731b32d50e4b6e39f45595cabd413f62117244ffc1d69122b5b1c9bd864303c46cdaa1e76a97448ba5ed266859a744411e29d1ce4b78a037d99d337a7b2f74907b591da8103613a19bfa6cc1f87b9698912b85ddb0ad0d86569f813ed4dfd3a9edfc0803427c648ef49724149e49aaaa98279b2ea7d25e3463be263b6cf518e9bfc456e993c2a3806335f13deaecd3c052e8604a08c045175d044f7544291dd4acce9db398fef80ffd0fc75b28886903d2d39c133a1869b8429c9ec58069384d8639e6d268849bc02f863f431f574ca988b84ae59427e446891cdcbed74a72e2b60601ba707b2c839d85b33320a32c1b1f7514510f70d08c474a7b6155a0ed004a23fee27c6aad072c0c751474948a1397ced8f04c5b50b4be2001f4f52ada7731781b69ed93b7602441344dedcc359c385a897532f3475764e0ed3c80959cd95d47f958d237f39694cdad607b966b0d2b690844646b614fa9fd4f169fb4de7c2ff3b4805402e4408c5ba5be95cff6bb68c79104a58c11aac0f999ec307a230eba3869f9078c453af1539a9cf5f07dd18a10c2f2104a2007d52d55b8ba7e24d97002c461c2f21796055179f403a012554b2eb503bd7f62aa3542df13ce9b520c81f364b7bea0deda55e430ff52e0f7408e43e4f7289d93b2431ef21d80e04ca57538dc17b6a7b2f1729cd52ccf7f8b4d58fb2fbaef5c9b3cb38b34b179742e686b9fcb884286cbb9b35d70083d8dc6e6e9f374d473a79ffddf8547bcfc05d5da6250ff982c8692b51e6b219406d9eb1b39dd2adc8de3dd86510da7c383041ed1e87e5104d33478bb8bd7891e16c72eb23029dfb8b90c98c15377d0928a36a65b01b4e83e5c93e5a804a37513447ca0e64a945c0807ffb5f2ff06f270e9080eb0fe471b4241a9f66235b8066d425ef09e6d46e19717747d80f5a16964690b949b335b4c20d3d15a7eabd7e815856f3463565654eaa243f7a4c619289212014d1e1effd99390d1feb0d9a9a0b101a298aefbfa703582c5c2f6470c673a180cd6bd9adab2e6d044ba3f130e47b7340ba9a358b0600d1a8862a9c330e6ef1782ef1c912a4a60db9522cd3f1b941832abdce920a6bebc950d29e057d6a71e75f9056d601079109bbbb3b7bbc46b413934b902ea7bd0ed115553de7c51ce60ca100272221a880d3d2bb0b49f5b36a246df30b7fcfa3e924b5e0db2c55038289b1e266f91d504c14cc3722881721f46d864c9f5ca7c736ade9d7196c9fc59a4740d59d0bc1cc3845ab33c8ee7e539f2bcd8d7295e88803b1a66552fbe133a585962edc24ef8b8b3ee88d005c5817c9e65249c554a678b71fa1e6dabdea92b1ffcd004e16f289ff1758a491a2ebed7c070423b4dbbd61141d3357ee08bb9be71663d0d2ea59f4322ee39348fe8af117fb424ac49e362ccdeec78e99f7f2eef955f7454ce38c8dd963bc6a43bcf476514a5951637c64a71dcb65351b05c557d7485494cbbd0c206acc04002ca1545d1ad3f70cff600aeffa021fcc63d46b5128d466c87475a6b420af9e4648b3218449886a5f3448d2993609e7055566430b2e3f3d09a22633e19a1dd0418be9f99cee8be8336f575d3a55dc090deabc6786fd0d210b7e9577d69ef79beafbb541f96c6a01c9344653d7f84cef645989c9928b1738b4fb901dc9b9c29f4e0940fadc8403262e953ed8f6d772f1040b6ed52df64f7bddcab26bd6f5575e325fc81ede2c931f46c8cac6beb889961d6b2395d711261f9ddfd6be95e17f1aafa506e7d14bddb132b0821e7b06cc8597c8133878ca983bedabf66d0d2abadf4dfa38015d3e0afe9323c792dfccda5544c17624162ef0554b5cc6d77362208018717a53f44c2bffec644467d6b2bdddfd1ac55973d05edebd6e24c41223b8beceef1ea63410eed001c947429b115b864d785de238fa0e68411e9c5de226bfee699ab981ea86a67935faeb6f571f04d6dd1b73920a4afacea51c89a5561532cdb94a08da8e0bce1da552da943efdaf4a9549984337f0f5c4b42fd81a4f545e00d86f41ac6d54eb554efeba20896f3e1234f41c285636524b666b69a21bf19318a618c95a05bd4754318f696e0671662e9b37cd2393fb709e687a8ff171155d71e6ba436857d522aad38d34382c5b9f2c8e5b94464156cfbbefb3e390a935dfce72b9dbd2197e241b68cd9ef87de8165eb0515ecf534d5bdcd2b6a1540c092e26d99985085a95e31cff50452e89a3de519a0bc9494c66e77434242ef17bb0f4719b65b10cdead9b2e9072c59d334cb43827482fb92876e6fdb66dded5245e0f4a8aa6ac7bea417f3feddba61ee20b092fda1502551fe6cd0f14023fefe4d08767a0eaae297faf7352badf5ff9e58cf4d1fecb857ac3b9e83c90d19345474f82c074da1600172969606801ec567df6dbaa2df8a4380d3d500570b8461b2cc8511e5d3a45495b3bbe014440846892b11c3248decfa722419cd30aa94d9209872872820ad1b02b8abbce3b26e31aae5bcc816795d0bb3810c1e4f6ad22ff0a00ad709f93c3f63a9dafc2eb7cc6c8d986808e165b25dbed2af7ff5620ab09b3aae0928ea163fab6315b7885448f5579c40c35ed50340bef058ab56186940cb5fadc5b1d093c1a82ddd172a4d5cf3efd5c5297db8dba62e1a302127b7faac192a1de5f8225c49607768a7561dc138b0c4d9888fe68a5e38c125dcf23c7d9066c34c7536226cf68e8106c9eecf4925e48c6fb8286c5e4d48c136550d179a30daf27017aaa890abdcab582f67f4b7117257ffefc0bb8a8b6bab8c68894955bc13c185b64b5e790483145915881643222c50fc024002a7c619ac5561bb72704c2d46745ca5ee77311a3c22750ee8cadcc77dd037db697b939ce0e8be03f83c82948d491b38289483c8da893ebd9384f83f1ce0148a43065ea4e041c22aaac15a1a82dc6688c3f73f86f97c97ca7e259cf588f8ddf2de829970a0cc527df4d9514032a4161e3175c34dc46065c3a1f4c70a07734fa9bf76a3744d2764df6fb49dd8a97d85c62890d6cc094862a34ca78558c47c263fdeb1dd5dfa9b6c0ae2b3cc43b5fb742049ab38e87f9d50ef256fe3187506f8b462ac07b85f2395089291336d8233db5631cb1afc311f6b1bda3a2b8cbeecdf46085c5f9c0727f40c64681ff0adadfbc742a8a70d717a00080277f8fb67074c98ab4cdd2dba5b9a6d3defa9b6024bb7c452f19b74dc8363cc33b62ff9d5653f7f069723f1d6d7f7fdc38980bcd01d1867c2060dbc75a615933c2830e5a39885185d74f980c733afd19a830b12dadd006530b1514b5ef61de8b5b27ca58b9471029edf360e8bc4cc962d6ab28a1dc2083c5b98510f4c22f4577b9c416b4072f36b298ee1ce8dd8111c254340123e19509c179e4b71cb268381af7290196666e055a7de2715e6cc173bfa5af642a3fa942c85b07fac2f9970bacfa03e09d36da2f80b4f1a379e8e5b43d9e2890d551e6f37df150fe561212a3b0b714d13b46765dcbbac3f6ea01c976cd366da0de6bf601eb17230e6f3032f9a476ceb75d7d25ceab9f65bf93102daed84af1c7e336daf571bae47f98f74214d93403f55adb50693c9cdc1a2144f1544bcd1e441a34a8d2a2f48387130e569a9f3053d510e2bf379df90ed2064ebd503b44a2a729a19ae1af257508dee6133bc8cb0af76690be0b41248f3f447adb4679cbc91e93532513a9cac6dd4a587ffef01c202f6e46c3129567c833b5db32fb36054cef0ed13530c48cd3bb887137f41902fde6e4e472bd6e38f6a0992d363a0470a1f8f98ee17d528a0b4b187d6b0a931f2a0b123d50827d6b50797da8029a868f97e144f7586afb6b394dec7ca89d85be8a0ecec25863eb9215704eaafab2e277c3256dffadc6a99fb3bc94b4483a3fce40f29ecbe39c223a9d99adab90973c65ec64b9b9ba9f1d68ca967d87df7793323d898fd09065da2c6051308cd4e535634e235e6284fdb3697bd285eae40d57b976d7ac690e6a96b193a9c3ab3dfd771efce5cfaf12d48abd69023d7773ae998e7433600924435bc89163b7a18f7bada60f52ced09e50dd8401198fd48ad6741f545fa1b53ddd0b55e71baa55112e01f42731148ad13f696b63e307f3f9878c74bf513fb03b3bbcedd2234f8d299286add8e0f7aa4c7511b9cd080e293b0067b4bed86869a814fe4b2a5b5f2a9eb543795c7139211a61de2d5dad8c61969a0a460552589867d544b3561b8234e7d16ef4fe6590d141e3b29a77e967caaa25bfd1eed4537e5d57372e621f4f99825f99ff7016193038ad5f975fa64497d20f2d05ba4fd2d015ce6ae8d65d536584157d0a4269f5421c308818415235d15c2063bdd645329e270c4c6e180575ba5b20e98812520a41d415df1b7acbaa29d6600b797160d9e4b7f96cffbadc623456aaf9aeaad3fbdfebaed9b7725f4c5027d7acbdf36c3aa77ba6d2f834b5b45b2e7f1538cfc109a4817c913846a4d5a479d5f29e9b377144bb76265d1747644dda7465b498b3ebc4d361ac8817071b60ee23d0803d19a9da55619b430997ebc41c59d8feda62b9e1f287634b1b8b22f415c89444c01af938c50e0fc828996a044d5dc514479fde095d6e91c2e75e74af4dadd159e4e84d9dfb8e6e4e3fffd7f9a1dfebeb14816857dd1b6e197fe33d03f1835f91d2764d3cf8eea43ed03e9d4486cf3dc90f34a4f4e7f1e6b62dec4936284054322904ec54c8e3d618b4c8fea8c5985624b3625b01a904512da4cf3a5bb5d23d99fc6f72f636e08c23d435de96b0d52f16b68932e2a03b0ae8de987e2b9a324c3c1028236ffbbc85399dd8e441ce740ab549ed1547591469251aebad65b00fd4d60abf9e3072213e3ab5ecfa9ea0c1dd98141b6f58d244a6979af521e1d1c04b2de3db33a15b7e4845046a4f749b9a08fac86c32b958079a48a13743777aa000581a58151b3bd18f5b397a786d3323bce206a91013fa451a2e8c0c56cd428497e959b728e88a9707a80230850f82388c5f653adcedfee27ef1d045703dfa12952f9ab8859c610f3a9eddcfa6f3c470b7ec1cad5c06f69a65fa5e8641eff14a5378fdb63099cffdecb4816f5fe8fe64f715b432301523d2280be3e3bb2ff3374c5fe07f1342a37fef2b46a2726d308f9e678246448528a219ddde24baf73cbf4459791f564d9fc06b22635a0580b3ff21c9d5f6ec4433fac3eaddb055714aaceb48ac87af41f00fe3482a9888afa7a0811cbecbe64b264197ae22c1e17baae62116e79948771731bce85068dfa4016f3978e39ddb6373b398939642824f763dbbfdc002af17846352ea59cffbb3b07fd526936f313a9f36c818b9c8935d3535e05a7f8ad66541a202b3770e7b5b9ee000fc03f87a67ec1668c270bb6dbb46e1f58e999887cdaf5bfd08a899afbd1b1712a474fe6bdc0e3c6540072ede5b4fd2eb4da9b93bbda506a634eae7f9b0855c40609011911e765c5bf92c7ef9876d3898d1b9f74ae44dd10de3cc31b5056f219ccb522f6c48d8e32893118f6fdef1441251f5f1ec351b2ee7b95a2ae8d08edc4ce2d5749b2f07321ea546805fc4d46c27392fb66ec9ee04c41c8f77d4920dcbe5937cb5507795210b0acb300b3a9625fc4edb076cfd36c5f8f0dd519025af2eaccbc13747e1c7a848a5deafa9caddd682356e21d7d24d7b3458f8c2351c20a864a152fffa4d049b331de77b2de9490bbede98488158108571cb80c5e3eff895ba2a31a1d70aa1bc3b38cdf5610495f432a9f9369b9ceacbf45da63cf533e42df66df7c7e108146bd107fd890e32252e5e1125db9e5e938fc86a3ad8547951fcaacb69789e99bff56fb9e0350ec742665cae725faba2dd08de44a16d5779fd5c0a3624db0bd265d933494020efe58b64b6da26b0a78377fe8d55a2576e245791efbd9c2811c66374f1d95121ba4ee1b85b637fcbad348cc8b089936520a1c19391ec9186429689919c0692b665e95450bb37c8e88306f1d367cdde245b2174277da0cd799788d6ee9ae8453adcfccc3a5dc9832cdc59946ffbce28be4a4c0630691f5e9bb4f9277df568182fe478a4eac8b6576909c6780a7bcb0ce8f7d60f89e2d76d1fbff45d6390c1ac743129754aa2b7320101e0e7da7327d6c5e545feb5a0c48c6cacaf245503f87d7b7ad7ddb24522df40f94cea9a9044ee7de7216d533e16823ee8599181975253e5841c4a88f71b04d9465eee909c6b1e220d11da5bcfd01ab009ddda154828467adfa4c49cde3991a2062e10e74da1a6535ced9fed0b6d9f4c4f507d711d8879b883c43e7c3588dfc02a3218925feabe7dfc619d110bd5455cb79ad9cea0cbb5d2220c5d83c9a2169af546b260c4eb936a74db8d1538470c73fba41010963314b1ab662230d2bf126102eca5a8e02cf3fc9c3e406110d44c483b5a1770c0c97657b251ebfc59d2db5eac1cb2715aa3c5b65c1f4eae95b700bc4815b663b61d0cc7b5bc2b8270d7cd558edfbe3086cac8eff197ed25b4ca98546735f433db5ccf9d0b01f56400f9eae1656bfdaf81137c306211011e09b15dfbae35c8df774a3bba8e40cd3d4fb00a6880e445324f26d61a612acbdd0737cae1179bf26bc29b7cb0b23a907ea011402947c0cebd8aaa82620a36ab9b6d281470d6f05a42c99b87981f7c45adcd01b2b47026eac614e0930976ba5b0ca56e81fc5277d80a9c7ab49d670ed03dd760af98a47e495a3fe92c0f97c4138958044faf55c5f4ab7833f1db1245522924609028ce1c234bae58d99e377eb82856c7a8fbfc6a21f3f2c21001badd0aab473cfdecd9a89d230ff477d43739debadf3c7d03de3987d4715ac54ccfbc271ef5796428faee43cb46feea3a73ca2acd73685d6be66e86a958a8f29ddf8a7d3f9f444b399dfab5a6e46aff3d3ae8d25a4c737e4c2bd1660b0302d2381ee4dc9980fe75e39da89af19310d61a4042f56d22fa2eab5499eb2865197ef07226c05600fd904f67feabb07a7b6f5fcd31302755a19c28f3fd8fb34849afc27a79c226e91645642229a0415ecb6279770561ea342e043baf8907a6e6a31914bfd8295ba6926bf4cfa11a369e10895d5d4ced4c426b69f6084a304804083020e812d01b0b6e6591dea9145ae40fe4fc0e820c2a3dc813bd6b1e960adb54628f1fc5b66df7e33cc9cfefcefaf14a510ecc1ee5a7bd5656a34b14d3bbd6e20034b421233e24d7d8d80b51aab12a1d55408e3679bb3c68997729b2488594ba5d8ac0deab6b93fc38ee6db89eaa263d34822bc71ca45234db23ec5a6826540781358a9f9f360b863204e81d07046a093dcb0c9bc3c0415b7c45722eee2ac88627823b78421d11f0be2d32590c2380f32d355229e34e33e44ea3ad47be9021aa521b3832b8edde4a37368ee10cc09141822d5dba7c5b50705b4af3e94e3f9ca32840146fd4f5f6910de5004fd23c3089556a0fa92e2645b9a3b9f4ac9e850a3d3e7b5b8d32fc8bd9ebd30d9d5230b04c9f21073dec23366748560c6fc67d741a822c54ebe373e676e515959056a805f09852a111a6beedec8c1ed6d8133e24d04fafeab7b877b71bd107db03e7a47219111a98d8967405396fa2a12e30e5c02b0235d6ab4c414d14393e6bafe123ba93f1c24fa303313ecd123af3c816a898e8c015c977550012256d83c8f96da6129de1b2bd857afee09a2122c264f5535ee17207e07250e756051b665c5130945b98cba09141f4175a1dbfc6fc3440f6a2692f0141a980b856131e75a7f0072305c0065b5b0b32d0a8a406f136a67d8bb51974a46cc1fcaebeede69c8f76e9cddc5c040ef380555ade3492dde6f9caa0d4c18d02106389c3334dd85e21e5d1198bd95e32a97fcb59c85de999cb078d9d2982408cae15ae6fe9fa95b4b5443e8d6effcf4b3e903f021f42644d1605bd52861a520f413bcf9c5fba40195bd534ced9088357fc3155c7606ec5b857eed52106d4472dfd4220c46c88a2e444b0f0ffee688cd472ff90304853bacbe594e7e9eec897b6f6fb0757a658f76ac8a36f995f228db232ff365c842a95ce4dedeccc7d6adcc66f9723b413de9f410bb3a62b338b3bae5e9115fa66d581a5d459ec0bcb09a013e63303e89073cb8473896915f1593c9a62c060a64c8d164cae46388ce6751a7d11e1246af906e1fac401016bc7b8ba49be563af3fe509c1efc2771bb2fab8827c60cb79de9a1ad7485cbf6f145c4117a93b2f476300c403a1fbc48af19666556697857a6422675ee39868e4d694b538a5d90a741829ea1eb7f16a4cb74dd92eb71cfa84b5ed5eef1f5a52232b53d8791b48fb98cb0bf41a881f11d0d202a40d5031ac03961c345750b8d0846f28f76e663b931c0aed8b5dab097d144826ebe16b8e4362868b4b105ae28e4c458982b0861c0c5ee362ca4a714dff9ce082629e21c612546e4cc50fc78dec5ff9bb6cfb7477499e90b3d799fcd26bc460448ae038f4d7dfae99e1ff63d34285a15592955e50691548ec56edf4493f54e90abe856c933b365953c683767622c31bd2b18c50eab5e1e8ae29cba3dbad2199a4fedeac65f4fcb0094524ef23a1d0001147cf902c092e82eb5e2b80340ca64adda9d479af18673875eb661b3f085a016b7fd21bee7789755ddb38555338d36bf6277947fc8debe80d3ca65934882e6b03ae5426dcbfb40efe0de27d5dd33ffe91d8151327b8d49232495a021c7d52b1a70810d270f473b9c0a9481744b0b51ba5a3b7f1f8f86c2f23da41f61c2bb60b545bafb0f7294aa9867e2d1c2c5a1b4bcad31373e8fcd29118f455d0334c9db169a438fc9ef021aac8d8efcedd2fa51750706d4a4fb95d7bb5d91e86d6b8b10b87bd82aaa18c2074e54f9ebfd46c1659a988015c86b7ce187b83fa89e27ba3e91b6e1c1bad902723f0f1a05a9d016e0e6b587ac9b3f1c9985d1ab1975a5427bf34b812e87130b44812cd2e4c5dffc0752c13c523bfc86baaf12a256141f12f33a727abe44c948d58aa76f91df64f063c36b9a6d04dcdf7201dd7c3ffa1d45eaee5ed72b85dc589bf8147ef5fef4ae4b949c2f9add64cc0b45b05723ee3701165b4a5eb8a442dc0af576512b0272338702afaaf13c3fdcaf4854a010733c4c56d1abfabb061cc40b0dab4b6c5861fffe5e3b8294d393a368c38196036b257f70d227b32140708516a1b97c9c50ddba5ab246eaacb193129cff1a51ba69b4744b712ad3477c6fe36a940b6aa5ced1615fd0f9e25ae89812eb682b8a7a376c1e8d08e589cd9e3d29b25ac51e3e8e3ce7d4b3f8b7fe1ac105c7ee26e3f467cddfd77a5da729943a14ce537cf7f54170293996834a4206521a03da2e14bf28ff11ec8288912d13a08631e01278eb5fd631c7ed1f2a2c72f22f7d7b96cb27c27da9ee304f70753287d4e288e53b2781b8dc405379154da26904290850422ff4b396a1352ebe08c7136efe738fe6a38d39b4275127b9f230f437cea7df223b3b31bbd88aeaae1f7ab5e93d39b7f4efc5f2bcb6fafa8ab485f9c121cf9526fca23f3213c5ac138190c9458236557618eb79c0227678f9fa5557a9c214530bf6b858cdb46cb259092425ce63de420298a7ec7ee62b8b5eb61a3df4a479b68e0ccf7ae1cf4908c0b5ff2f34383fbf155c94288614ae02b8f1c4d30fe332ac7a87d45bf55c9fe2fd593d6160b92795ce24cebac2ee6c2803087515b8d44c9a5953bcef88124af2e82f01e66a2ed8e3d68260ab9dfa887036954d3bb9d9b43860bd482a5c8ef96aac05909de00e73d96063256dc875c08327725ae2c71259001b49acb348cc3e2db38372a67cb2fbcb136be5597a6207e801b08430bb5c33e9d1880621a7508960cd3df0c36ccfc9d0bf417fb43239a72490b1d536beb736ee74d37e4745d15b1a860c7c43532c51621414ea153daf68fb09f0eeb8915f0e973013dffdba60c8d7bc807651ec4227c0aa4fab2b04001fa8d27b3743a75e91579551d1c45d1b47994783e5b9b79eba1245f2c779e856343fb34b15b9ef2bc2fc7db25df53f0880184b6afbd88813ca38930d58c6bc371e1e0c6729baff00def2818eaa51ba356d6baddab556c59f198695d58621ca609d187d9937bd4dbaf0a55dfe855d2c6f42df4906eec6565c283684b51f36d642cfc88841292b899f4ffbe6c7bbed34db11ba1646de299d752cfaf3ffb98c163ba8affa22b4b96221991b48a9e51dd5135042e2e90298088da604b8cf7756f6c9904a55ab157f508a028de1cfb65e81ea8065f20b7ed44c15a8687351bdf200aceeadd5542e9e33dd85079a46b54ee44712d8cdf2b7a672d1607171a9c00221cb04c256f0a355d7c98cecc9035ad45032fa6a053333e419250f4b57a026d72b341fd90c4ee43808c70569058a7f391962f39158d84d0c684fa4213551d76fca21d56836a64eb638b1a1b8b9f9d22b2bcaf3bafd6d4ac633dff5556f622ee96f4512aade6f336de2ceafa3efe78d20a4e5a66549a503a44fcc91704b5e865f451fafd8ecddda1d09315f2f2938df845c1f4bbc55615f1d3fff74cd263b6a60b5ca1ed8fdc3e1d69f792ed688d95cd679873ce958458990f2aff5dc86292744cb698d93a37da64965572f663cd4b2203a80685bfb55936bba99b9a6c2e0d6cad1e84b6b62632110dce3b4995f481cdb308b9b9c13f2cee0c92f552b148eb2493c63ff3c8520c4b6660522f7d314f6e5615b81e97c4febd1ddd66df9617814b0f84324d683dbfd082d803ae2e6fbd76ef66fd9a24f1e63c3f3335a2f61554f8be4dd0cc888afa85645372da0ee6d6730d32e5b5c7637564e7590d73f3a86d5a7cfc3fb0079faec67f8f6891d70936993d0b3dd6a385c98b45830b3ec76864347b3fbacc90779cc5fde43af74b5c327502fc6e2f0c4e0533d4d30aeca71273a54aeb815699d38781fdb6871992591104a2617fe6dfb5c628d95d1f6caa29194305f5fe15cfc76d1d18509fba3a09c208aada3edb8fbfadcec634f30866727260bdbd3bfd751700bf9f78a3e896ad23db343877e7ca7b8f0af80cd97ce764ef93c8f5eeae86cbfc196c4137bd0d5134888ec2247e62297928ffe4105c1e40d865264d1d046ae2e785dc369daa550c4861ef870913916694bd0baf4257662b413356ea72cfba8872eb522c6e1f7db5604916d6ecf9d4c8f74999f45af1b486a886c8cf6cc7b8cb8de8b6f9a7e1a6d445a1a0cd69cc3047f4a49a78cb9f0af584d2508c1708c912de3e9567c2d93a5dfa595199aeea8e9600e9193bec7f61b7bc5032b59c653f654b1a2385b5f11266ae2b7cc9ee7154a2ec5b82be245f37aa82b7af41d11e827f965f7819937d2cfafb34e2d67f8afcd63a49c68c2882937378e7d5b1d89bb72f4eb341ba6feac61fab677d42feb8899f763db4c1dba15014c852edeae1ebb9218b36fc465a4f29d6bad08821f7838777559953dfb9b80a164de1f0b757f193be8a8572231c799bffa981a89afde411e7789a567a462d7d0fcd351695f09bbbc46fd96fe4b875135110c740062992867616e920a078148dee0d87a47161c84de9b4061480cf6c184bdfd8b2dfc0d668287d7206b52ea84abe8692c9bda5318fddcc59555cc856fa88795cf981f987fe6a2ec5382bc9bfd9e5c61a9bd29d6c30b72509911980ed85e5f1a2baf1d630b59ed80eae8b7bb8811c1fb43115bf0255a5608345e00a401b82938c5def0a553b921d4a33998dab73aef8dcc94a84629876be7ca1222bb03623c2a684f95acd7cf0829ecd1cb1323a3c2837476b9563f9d3deef4898bc50b48d062eb55f77fba9b929805a612b004e4d67ed7dc3db2de2ff23e696b525334e3645015e3a5926ba734e3a615c037d5795d180fe269ede800d6d505c92b08a35331e49d8e91fd41fadad67480489a1fc165ff593700bab7b98daaf6f47dae85c36e267e8517f7992517da7f35c4a741128b08484d34d9468fe66977bfd06500e286cf246d65a8121be3a6392ea1c011f110882c2c2b6ea8ac13858864fb1d8ec8d3b00238bdf7f17b0e1719b13642424337a35e188503dc4a5439876e201158a082c2035e2c39cac23abf0966a44c659bff7580e063fc565db8d0b319afc7e16062b3bf5f840cbaa07b589d0a68dfcfe75b78d67b2a76e97dcd72a5450dad5ee69b61e4e7717afe68e92d03f0d992a3568590d028d1b4382644b383756541dd7562216671b936ba8fcb01243f03ed4b540fd2a3ab2834583419899b72f5912b4a77e39b638db313eac03b17de82f4576f1578762b83948a7f86ec118a1cfb545d3bbeb33b9f54e2171b880fa9f4a312b721cea7d58b8626cf3ec13c1f1e9d0ad59682400d18beb96b512e5f6d5564926565afe7586162180f6dffbce79600eeb81cfc7465c5820d40993e3b1cc69cb79ee6342dde0c93b89a4b0b57e907ecbcef47f5332fc0fe265d7f4a813cde04f2e32f24b541f6d0f28ba53d632a9317bca045afb660ff41986b421ec5693a0b8c78279f636118e618f192330ecf34f533d3af8f6529bcf5919cb2d4222e45b546ad303f27f655f4d93a867fd929cba132a460b7063146633826809f051392d0c9362408c6e21182108e72538a04b6bf8c80745b3d1446e15b80c26cd25fca5a2f06e2afa0b73041d5e020d8114aed6dcfa8f3692aa08fae2cdc0fa49630bc4520b1c70dc4ff22b9ab64d589a1c8b9d9b15a87c8e1514f7f328db92b0ff5b1b56a95c136b0302697309bc92a0dde9f69db9527ee1d5b42aa2ae05d31d5f84ae266219c8ad8de036bec43d3b65b326645430abf28f3a578167c2b2abe0f20eaad0cc78735bfc0159598070ec2ee2a448a52492bedc6f5743cdaac00a5e0b493a82f16ded5a7760b4ba7db565a96aaa2629caed538d23999286b0395a59b6a32419413b3e3ef5e52c504faae2f54f3801389ff2aba485af2c46cd1218d94814d46dc6733590be1e559da9e71f45a60faffa607d39b790a4c14545b18f6aa9f38f9149451273ddf464501dfd67a55c0f95b9bcb7d378d523c52d80b1066ef5e51baa34bc5575589586ff31c8beec4473cf78072ad7bbb3191abc15c701d3d8f390e91dd0d99d02dcc7129358895ef6b31544fa071e56b8f9bbd74e023b6358f4c7452bc6c22400029f53fff4819f986c287fc4f26c8f071565faa3e1da2b6438745512949996c88b54e39c0dc72f389f7aac0e69158e10906f9ad41db237c507b8e7b0f24f1815c23ed0a9b2da7618196b0b6f50bf8a9b941112b2264709ed2797791d63ac0ea0257aa37a8e3353a2dfb0e90000ea384459a465b6cff2f0671046bac98042c3b9ea9f4432808c6354ab0adf0063448e7d9cc9bdc53963d183411dbfb7209b4ece4b6250a5861364347b4f4bad8de949d5413dd4651071db6eb77c6fa730d312754f3541ccd81c29f24464cc4d370fc6504e6710902538eb75fbad327d6e74c90b2b346dfeb9234522f5e4735259222088b92e1402b4b9e9a2d5ddbd6aab5e47c8abd3f7905d52a832d701e44f8c03b85410206cb5432fdc6b925182f119663f312ec175806c6ade1fa07ae9a43cfa2f7495775ca947d66c3faa0a087620f78cbeaff99955f762e8d82a2ec20c2a5282a87f716c96a5d6a5de2043713e197399bfcaafdaa75d5579ec6514708e60cd6c7a52b9ff57efbe70ab973a5a5ddb7e23cbf19906de5ece03fd1b7c58436ae574bb51b467ac65a0b5684c4d625a3776b10590fac1a9ce8fbabac4bbc369f58492a4734bc57644fb4ff3bdf090fd4db40afb5fd39d8419b3db20b090e766ef4d24fd28a698e12bd94c1961a6d0d26404865e920b5a08dcbb9dda04391b4bb9c4262522139195ace73343bcd2ab056167ed0ff0b98299dec424970d87d325a2bf52cd3ad2641e3459996dcfa0003dd17de89f1c457dac80e3beed0ce574491848762e1163e6d3fc7e876f0209fe6cd101e45b99f7943cab69f6186d9bf6d8e520d7539763f5aa2fb4e43491e6c2142608dbed4c91fc1db473b3576b8e3c1fff7432112e99988ca7a6831a9242eb3b411f03d215e1748b4d15d8188a7e631acc38d59c3d144c1335816a22e0cb5ba99d7be75310a893bb59dff8fc1689c64088723aebe4da7da792ac20e8ff5d4dad89a5ad69f29f596c73bc3a4313eb3bc25312e255f4c895ace26cca38bdf7afa9698ad4a410dadfa286aeb8d5f10c467b6df332127c682b989308b9ac6d77866cf9c9cab9c7148338a4ce3b9e39ebc4d7e1dd5abcd5ecdbb25dcd41a74eb40afa638f6ee1ebbaa31598d5e533147dbed0f4121b9759f32444935800ff21b23f1a4f53daf22d45e680637bc82eaff186540ffbd82831965377fc5374ecc057fab5b606d45df49d1e46b7ea15c42b77740348d3deba6eb02784f09443e251be7beec1728db61c8e6b7a46d9f61c25854c8cc8262afd14a20a37de4cd8bfec05d5a3d603b3f0665c5fa21be03054cd70773fe92364fbc2776b591a279fe000bc52ff0de9b5995ffefa0ab11324ecb4d7848761bf855fd8149a896c9ba8263cc27a90ebcd2f9480cba5ff5d4726cacbbb55562692c74645a2df6e0c965bcc907a7a243e5e6699af637fb252ad071c47e2d5f5538c4d4301c9c176bff0abea559b6c42bf1d0ce04873b99350605bad00e0577967414820a8e5f5b2316d868c9dd5cfd40b2ef35ca7bfec53aeb32a94dd9341b8d629200518776874aae6c8e3281196458645fd97f223da99034b4b3324bcd1a4c51538d363eb2f3b46713831f58fc5411939b6bfde6e0cdc72417090fb4e0905831ba58fd8ea4ea0b9039757e5c83bf635977770bdc802554b54ecdca275dbd921aeeefc84c934f19960f7125a926ef85e30f87b045c2faa23639860a5611d3b4b88deb464d8ae37d53c0125892e226024fb7b1282819f282531dd36ba9f1d6abdba037acf1f43a3fc65f793a858991289a2d00417f8b09fec15c7c0a3d863e583069b93a5b0789150ecaa8be5d54cb88625487eda5dc5996a95c2e15a46fe4ac3ffaee66c0cdb16bbc7ef90894bc427aedc5beb2144c02f6fa9ed8d42a51229eabab54ba7792dadbc0f11b73c18fe5a15610f4530e54610104d7b8ccc14108b8fc4a19a0573250200b070f41a8e5fea8826f0173d8965adcbe90d3810e2e35e9ab038f2631bbcc18af94177a0f1c0af02566971af4ffd6f5bff94a3d5548f9fd9ed9bdee151a4a3f6202a9271fcb4635dd915e0a512df7ca7d60047cb6dc5c128df391875ef3b09701ce7165c9798f2604fd25eb078f2992ce671cadd4313054d80462090290e1a62164dc5ac96f45c0f12654c797f6269a37b12ec4e3b1e5e4d9e5dd17f5262ab1a7ebb873f035aa268ef9e374d6521aae6d7af2d6d65d836e79d6461c5aafe2c91ec35374a038b9ec89748c1693122ac6028d391d6f5143b74b842f53493d5bf6956d26a33c63c9b10994034a36001ddde26e5b8b6c1b8ee76bb94748b1fbe8d2e08fcfc31e2fc459f518282a609c1f97296185ae280c4318137bc411b8d097c40af0a11c2685e1ea3b37f425bb4c78766a235d219531d2cacfc12404ce3de0ddd4712f84671acae9bb139816bbcffedc1a6c273f4d86457af7aff6132e94cb4165e90b16491df1e67ffc0fca1c227a10be9c46447cfd0005a0153fb3b535858ee2cce73ec76ee4aecb5b73cfe4565a1a0ed1f5d582b1e7d244a413e93f5ada361a58443fbe0edc99c1ee3368c7583b70c21bab896491c86aea126b6779294ad1b36fa07ea2b780d4a19c5b0ecef284d444537bbe741c4cfd6987ce20f6c6765ee90d38fc7f2d587d3a40cfd79f312ac44ac734adcd418741d2bb793a468ae43c854ba982e9f5667a488e40b4aeba0eaf81e17b0fd0c76a3326591ce94be7290f30bb23307dc78d6b69975455d6cd925b09bfcbb43a591f9b509508138f6f04aa7680111ea6a280195cf3af9ca06958cc7954db0c53545f473207520acf19153c1219c25b14bd52d9e8ef2efe8fd44716de55f4f3316970ce80076bbc4ca37b93ae358389c5b72f0eaaabd6b500ce63e26fea094dc76e4e7efc0adac93388f5b501f572b9b482d6586a9f9a678fd5f90491eb94dc7359bcb27308b74bfd4541e13c76c29ed6b425beb4a2b68ceeb49503fc4226e980d0fdc1dcaa97f962b2dae3fa4628e1161be82d56b87234ed079daf0cdb95b8eff084f5dc693ab6bc906befb0323eac3974cea284647cf84c3398d1289a4c798daf996d8a1a77b0e326c8241fdc35aae4112f0d306691b533fea134585d93f86239d4aec0907c0476779b9f0255f7ba0c48463c0728904f3f49da65745ba82c6d43a1f69fe0a3939a9dbd0a1a326c8f122467f7a474a1f34db3eae662385e671c45d9d764ee7480f9951044ba58d7202bc749b80f7560df0636cb567dde48d4a40e05b06a02492da43408c7eee67e830692bd56dfa83f2a83925fab3341fd88abc0bc5c912ffd3e989a4809410bc7a88a586a81bf6b7790acd86b4afb10c88cbadcde886424d5bea51773cdd5e3e5cd4b48e61f50fff2d9633fb0ab1dd76df3de572066b9659b18c94c682993a1b63577a8591032e346201c08c821c1515ca4521bdbaf1be924092eadeeb685fec9b2e94e5e7a8129068000c508d07d1059ecb2c44cbc0b5c35e209d04ebde92d82388a6a40bfa63e6d9e185c09857aa0d56047e118771a7801f72d31d91b2a9213d9211a49b49273ce519ac51236030d485f42f08ac720bc1c29c97648784d183cd4cba06b51f55eff0bed608b9a9c05f21e9c6243ef9f294fb23b41028bb9e41fc3e9564916659b5f890ef7a3e49f22981e008a83210566c9a1b6d29a341a12d947601bbbe20e8a2ff2f4bfbb0b7856b4a330eeea7e24643a853884ffeaa0c104d27a4b31f9082506eccfa428d821731e2e62f7af51165e0665b07e26048e6fcbe046bded2bf5d295b29d3b59f119d4b16a8aaa926fa044a9c4caf678041708cf383e5c494f9285471a32e0ec1eb9d363305d9b6645f719d6209d1a7cbb8facb0e40f305ba618c4d36a07289a0e44c957ae861fca29399d576c3a35c1e587381078a7b1ef99155dfd7f981d352b88ccb039d344f5d04f0cec3e14906aa1fed6c5a05853ea70c1dd06aa50266b8f687478e1e2e68a029024fa4a8072a586e336f9b47b947d7594c284a42daa1b2da26091c61f80cb82f7f22b3aaeff341999d1b8af2dc496bb674de530041925e34b039a70a5bd4df33e99cafed534296c8bfc593cd48eaa757e57fb8483e42d98335571272fd84eb0bdd4dc77e4e78d631b7b61c769809bbaba76bc6d5f38884569b2fa542b71054a3f4e5ae1808e8022c1f02024c6144a890b9c37aca0905745d2cd0ac292cf0f91dc20e81dea89a8be98bdde3e2898c02e52fcf8d6193b9a3c7ce2b311a4e764e3980a902f016a1a4fb41267ddc2966f82b8324f967f0b2a3cdca7835491360c23deeae74171489e78ca496d04235576266b834e5971b9fcf2c2cba6bd4803caa4675d6efe7811a90233012d1078c5f7f037e403770ab5d7e4f3171f299547cc45ad7a02c453314f5f2cc40519895f0b4d9bd6bfb70d6a69609979435c41d7dc816d9a8ef9b1ae1ac7f27a281cc93ab680cb6ccb9bef638381308afb37be762f2da5229025f2746abc1dd54732bd8b831994c669ec07b3110ec7cbc00753ed4e40fd0c7284842092aa1636963cad046c97ebc5dd3d732dc94a30b3c3ebcfe9bdf40e550cbd1d229b1907d49832d5d321903db8037743da1ac98e6357f8b65d1add1c2c37573c5ee8843fdac1a938565609f3bd5a76c92bb8dd1418fa12f74048bf1495007a60b1f015a469bbc47bfabd1fe65e6c67459a6a1aa218b747b52c3bf8b51b582332f2004875c7a0357a87b0a04ad01678b107f9a2b9f3039801e7d204462df86096aef837f6d71f9ab6a08d17e91c05de652507879b27e7dc6870c2618b4227aa50f2c3efb8c1568610e179bf578c13a162e54a59cff0e08157198b56957a89e421e9dfba696b3f42ed0b9ebf0cc70608463cc4c376120087bfae5900a5d3c9ddb097aad563b5af2f6abd112323b349d89702c313b0e4002df7a3664c188821c8c7f0571007570c6215c9bd603c53435a77da0b228642d30eb6e84eeb3d0e4564764544edafa987a78def852d72bf838b244a29c1065bf24afdf69f683ea11c7127efd9635a1aa05cc6970f9d312287950a6c4d2117cc2ae844642ffe1d2b004cdf93f2da62081f3957be5995dcd97b590a997b9a093f64db2f861659dbc80c8587044db8247ed16db8eda9bc32fe068d20790725412c66ab95ea879a373a607e9cdb5a62a693c01f7a0a4031ad8de040d28ca5448cc9982fe7304cdf6c6deb9b4782b3cdff0f8cdc00ef05bf3c6ed3258eb90b01e6ab32652c35f8133223755d7ebd73c24989e225aaa168afcde29289b4771596e885005b3d6d65c73a5509202b68f96fedfec2121326488ef60b101e8e60d149d69ede2b08782be1c7aaa9e0c5255dfcab36e5459a25d8f447f868ebfc9ce939471fc807b77eb5a00c653331872c774fb68c212c343a8bd3fc9bc52f2ecbbde2fb2ea9605c0fe6a7dc02b5cdd0f84974cade1832939d2dc0f383626e1bcc3d9193d96d8fec338617ae0c5ad75466d9050a4a7730dd61a87ca62a2143926d7ae8cef2333268d2274155e297d499f7730685c2fd9c863b97c7a3dd01fc18fc3bf42354fe6246d6b78987dcfbd06269081f8975c515d351b1cb8cbfa6c43925d51666a279ce13bc79b6136f195cddc998a18d1f0ad5c9997dbf8c0643280a775a829e7138b5051207e1b2e57f680f82cb3a3484bdf7626dd3267bde824c3ee7995f6f8cff35e71de6c612a06dbe344fc8745adee861dd7959c2b5b3aed5fbc22e65bba308932dc5ee90071ad7b920c5698683fbe7de1e980303df957881b54f1b750654ea3f00aad73efd82b6084dbc646f38b2ac19269d213a0444584e7bd49a30f011f5a2f6bc158c4ffb52aba4e34b212c01368e2ceb565df8cae2dd4b2e1a30cd06e019807104d08238b53ad30e231c4f8d1440e14c21c842ff709648e78b3ea80a29f726113a19918f1f74e39f5052d63d98e66005341688f57ec438b7c6cb90d21a2a86e695ffd48f242e6f642b55194a40fbafd0198f8914c6fe5930fd152d0286ae1bd23848e27fe3446836b2d76b960caa119421fe7f4e7df092e00a7019cd58301882e878600a3970c970c33118d43d13c33af57ddb66085dbf02a9ef4d09b5acd5c6e9eb48fa140713b7987ede4fff7ee835263d99db9df706abad165ff5c56debeb5791cd5b0bb1e0477d139c24d66944093d2a5743fd72b8960b4d602836c536a942ca45a2fbd29ab9d8bd7ae7d65e64f292f26fc96d7088909420db9b303ac963c427a4d05681c4006afb48aac617368da5661c5278a30cfa002268cb3195505b2ace8c8cae1f1fd5b34186798f56ecababaf0b790781e04d08eddd1da8af98950ced6dfee3caa49a78b16ca7049ae0e378da7c9ccd216a06f98cec591879d8ef1ce2ea2d1fd01f55eb92f606a70baf40c999583829a895d74645255202e2daaa498c77f193c06e563a9b3561db8983448cd6d0c9d06a148ceb58082e362864290f38139c148c3d802a3aff13a2e9b9cd5486d89f68478ad22eb6c68f199726e4788cfc2d3ef9d4ede892a99989b5d06b31bc9b76b9374e8ba0b46723ca16bc42097b1666cb762af7d04d78547d7cc036bc211a3ac9b1ea706a2946730b67b50a34fcd135bde01515004b70a7c5c7b588bddb0984b2ba516add041585d78328cf8bc39d081727dd794b7460002fe136f0f2c4005d70a0ea40f11a74857ea18b78dc79a04552d7929b1a4a279aa6ff8cfaf1dbed8c2417f4f86adb54707366df2185f97e5b58ef83bbfa513f0dc53da5cda1b83d8449e3c7868b9a194f962a99b567743ed416fc4eeb390b729ce72db98b7e37690a4ef0578aad7d3975d57e4c982f74e2f1c7902096d2fb31f58512acbfbd480dce54b1d7b5ea1b2493d610f03d3bff2153dbf2a2a4887eb6bd335192cfaa6c3f48a6a83044dc24ca5c3324197bb0ef6c126e4ef9129b6113758c075a0e9afca077e8c92940836b71c1ab34f35a4345b96f437d735be8aef40f202f281e6bc178c65b38aa65da2e8ca183d14aef4c86cc88cb6d506882cbde320ae5a77547f52ef997c3a40f173464e1060380e5a4900e29d081cc34191945c1a7dc69a40e55ede68636c6673752e3adf9e5a217384dd240af5d1cf28b37c7559b964d41ecbdb6e70b9f209fe818c9cbee404298e4863ce600dab3716abf797ad951fa831df1a85a3752e31ec7a1cc1115732e7d537990ba7ac238d14862dc7c256d21e1af90009479a93c28c86d3077931118be2d5bd79640187ac12722e0da3e0582e86efa6e2f5f7b463049311fa0af24b043167b4ac57cc6d0e4058d5e46d7533be851c397c646fa0cde2220235c305ca8729affb67ef3dc3c5a7a1e0dc58e2cc5ed92e4da36072cd55244ea5a2ca061ffb1ecc83b0953fc527c2321acc14df92f9f650c581152315728da425b6e117a3f089897482a5394d363a9943e1160bf113652a510381e9c194bf52fb1986a098e17d8b8594a923b2f7beaa095ecb50ae11d94591f4f4e2332dd85695e588d1fa66611c7cc47202605027fb2512f587355f78e193332ff6b98af8b48a30ed56b498c2676e06921ec6bd322d00064e5d2417fe5f7af871773a80ce8234b5a2b178be151bc79776191bab61a480874edd3740945fc8da656cbfd22d5bae2ca28cedc420876445dc02d3d5d987e7389a972ae7a8aad24c77e97bda20aee0ef68b1014afc1533422728108eb6c21417784ff97bf88493404aa3d485d4d738ccbf5f99188b4723b13739ca1b158341cc62a222e1e5e7fa225ad60dd8152ae44975642622aa4ed6c5615bd309d4e2c793dcfe5a089b58045307fef21892b76244db2e788693899e21348de1c7262ad54a6e9acc2308b71457b725507f175da4497be1b835d3f483eff576d8ec13aa3c57b7b11a0afd8a477b270813eea4600f6c919162e2aa2764f13335b917e13e85f71d4b5aa672389caba88cb547cc81e7cc8c31e88a64038534027af918723e9ae5e7ec32c5b68b8fe6a803b19775648faaeba5d36e7486b9c68041485e8598701748201b31e8245bddb180b17e2230954cef73d3815afabf991dfab9071fadbdfb2b153f75ce519f8f7b479e752790c64d18f32ec7ca29359c994a75206a5c23d5f341d784c1dd6bb6df3caa4ec39dee34e8b65dab6cf22e599bd08b72f686a91807284a7976ca613b5f3969ae4236027b0cc2f34018bb0485bf2222b5ee8cd9b74d60b9e4923e151e29f208268ca9b22514e8e2f8cdd1f1957d68056def464f63fb35836688bb3c444afea76c355639f3b43c0f14832b5313892bcd073da802b1c58433aff0d6d8712e375f6f9c6defc9a6d7d26e66d066809924ab0fc3ad227af43acd3f67fb9a2aab0c2a0a6399154bc988a3fe4983e841b9c79d89ad26c6a3abf912b43c16c58e23d84798856d5e802c4fadaabd41b7084d2d20a7e8a775b5b41f27f9fa7318129957b4e3b6261812c0db74f8ab21f9a810f2c2f979147c31d310b502893ea9bf3e62ad2bd6b088ae4cd3d268883918ff230a958cfb6009fde57defa7745d156795063b92a27aa598efd559db04d336414fcf059876b389ae2a9843e91f22b181dfb2c31f76fd3acdd15d3050ea7e91d4e2978899d0cb53689f687f2a2eef9191e55de8d397b21f9dcbabc170b5cf930b192780b07cbe27720dc2cb66773dd467b7715cb278beb1e3c7392fa3801248a1e848ae3136a371a172d6db3c5e4221492b011c336c47654d0923448d1905661944d58d83fcf968e6b7e09d28f957f6a7ab52b7554d938440a4107a272186f4679d31b1c1d8656f07cb452a2fec1abbd28a3efdb5bc25945563958d6f38faab7dbc0a54b44ba71db2169316f1642f111e69aa0d7126851f264a85c0451f799d8ab080b3528cdd9c1ebebd987e2d660f845313faf4e493b74d93b9839a556bfdbbc2346906e766a5465a8307f232ac0040a56dc969abc2e7519c86815a6d2481944d77175f8f8f6f666f82504f99b5a3368884d07cddc4c92e81ba68ec91ad05f3b9ed1aae4506e3974018c95665ad249f21c6882048854973c71090afb6f8222df5175952b9eaed98f90886b43d36bae4bfc9c8060786309a5cb1c5a47ac26e78f9cabdd84d870041ed102553e8331c06e81fc6646c851d60fbfc4f4df5be47347ee07d3257f923b02331ef9602c566bc2e008b6bc48d9586ce9cf9d3adde5e67cda07655822e00f66b95c32676e22d0ba262d04045a3e6cfa9e2336af3301cc5e23def47e595355271d08650389c4dc7cc25993a84c62b6a95cc700f0eb83231af1bafca03da774c0d8cd4531ee4e3e4a5ef11a5c4819eac5a00fe960da5e4f50c5a9bfc92ad7686ea7d652c3ea9643bfb829bc2932c255b87171d9d3198ed794cb2887291913ae84bc27b5142ca6a2de9eb3a302ff3d8f817d8d7dc2bc9f6bca071599446cfc78cdb58837119f200d44cd16160b4afed472139c34ab8d8f76df4070bf1c520019de28f486e5516fcbb9af1a1d015987fbceae835c0fc165e35b73b9d9608963c0cdd0808cf7ff680ca3bffd60ecdc63ad7e345263253c01df836540bba7319dafcef0f63431899f1c7f58ea4b37ea3d8d4e9a5698c5437a391220707e34af9017e37867bc1ec766c6a24108ef4dbf67130d23eec4f3299a127be649f5d48b478a83d8c81ab29e91e1157327a3f6d2f36957ee21a1f6207a586f3d5a463974967c13e5a219f6d3d3e688587eb8b4d8d914cbf341378d0f25c681026c39fe65403d8d78c764458334141c46098a899d565ab8c8ab1a7464039f72beeb4f0d3b43a69ee063f31881917167fe6e884b681bfe7cc8c03f916b0bbb345ac36c1fc924decfa8ad613bcb3579fa1515ad96495625e259119149aecae93661d32db0d26b051a1cec2e16d984373c5b83ce79b28bb431097894ce01537dbb30c4ad642b7e817b4d4736321504967b70f5c4c8e74fb9c483822668362650dc143325123ce4701502e550fc9c5141534b23240e092287ced7dacac60cd97c01f2d71516c22845b39a943b11264a4f98ac5864ed4e05da850f9336eaa8aab668bce4beef982358d24edb6639d6baeb68c1c37534dcadec3666b18c8440f6eda6cd3045b7f068c193be24b54372a97391a3f143f4b96fbe4a19acb20694c3bc69c2814abe2ffac3514ab6e7371c22025fee43add653981c9fa37af6bb30f27821e4dabdd6d847dff803decc77febda719cdbc1abd4f1002b81db9eb3494f20c1c561100d6babf1e29127a681474a8deb6c44db25bd82fbe9565a1c53918545e07c12e1e9cde07243d5d6e0336134d216dd5b29709cab0057549133fb6a9e95fcdbebe08eeaa5486275870c09a205ca5c32eb7322b20b9bb81a2064ad4d9c8f14c723c97968e319b844a47e81944fcb682cd8edf1d597a4b98a3aeeb029249772b9bb57d5156acb1d1036b3ed700c9495ea285ae5a82d5c788b172eee610a6a1cddb31d9c27a77614b8939f9e014b017580cd6857531bb6d1099e2f171e4a52c3d119addb6816cef1a05f4ec5d9a662dc3d3147effe9391cfe59bda97baa89abd69ad986edf4eaed759ec0913972558a840a9b4fb87dfc5148d26340b61039c1741f87a5e4f858f03c316ac6d5a24e9e42841410adf128d25503a9c3334086a1263eed865c50b7336fdd52f76d72e2a07fec314b353af032bffea9d6a8b74a7b1d3a943c5ffb733dee16bbc179f5192264b5fcd5424a10c3c3e2fc281cca8084b9572c48c8d93e312e3806dd6e7652eb6d2a2a615c34fd018876287aefa6d40f06d321f0b0fa476f31f327a2d80e28162b14e49059b8e6b77577778f61cd60e57099e2629bfda09b44aaa624e9e85e9ce8f2bc9a8b85db28ad19c890f630ca358e4882a1485b0564ccde5028573bb92180ee736ad0fb66e5d02785564c817845bb4dca29444fd7a3b162bfd3bc01b658df1eb35bc8aaa13dd7eff92d4c66a7bebffbae5e7b52331631c0eac829d3b7722949011fa7398d973cf1e6bd863851452a7663ab3da869f6daea5b15973226a4f23daab4d66235078366a3532a1b4285c93c2d072bf1e9ad9b256a3e4bebf2ce55b46c885e78a6c9342e5cb19307b51b5428633300aaf7949570fa37aec730fb0d8e7aea7bfee1d54e13370c9da2e51d7a81c67556a9221175a975dca9728668573e1bf6f519554def0d12cb3a1495f36320cdeacf75cd9f6321c963e372b5b65e7650f158488dfbbd40dcd438d824fdd1ef62318614b64480a822c21197080d90be39520d1b5b59ca1f555d683174371776d7ee08a99679ef2f2a55af7d4e77e08b701ea0c9faf28ebb801067cbc708e951f2621385762f00df797e802f14366ed9aabbed3b5ee3ec0b2b74b566803619a6282de0bb0dbfa16faa4113f90d317ef331f2be8baebc5cc7f301068ed1e7a32f514948c9219618797004b89b4aace04714072ab0e5d57f0a9d06bdd6211811eaa2bbbf76d4ee6200a8afab8c5dd92d331c123b8839cf39cc7cf457f0bf04e35928533145c0a39c562b52f0fdf8abcbffb8bfeb8215fa58f797c0348c8868c876552aa3894eb34807bc62accd2b3f30758b081298bc4c3d4c9ff927efedb828bc629ef622e2edbe178ad70532d081833ab8686f4b5a3e2f6046725d33adc021060962a6be6e410d6e651f75ae03b870a609200c11a549233164ee4b5782c1c6c88ecf8d7a96ef581a4a0c0580cc5c441962ba84ebe9a415f159aef0f78574ec2fb5f0ab295b792b89aeac2df77b3c743c62584b9a119afe81d960c43711436eda67435288f1581727586a29d9c1c98ea945c64626aa7122fc420dc9ecc80d5e36fc8d71f8311b25acd3d11d7bedfa85796d4f02a5e9476845c8806ceb18900902fba4cb1caf338330e465c966120258486c4b3e0aa32b78e88e9502b7da265e804f1f881f81fd948209ca06554af60b631991cf8225759d84d453be06e479a0f8d13186ab504e4de94986f469b97fee6cccbbb54e68336177fdbe678ba93b5bb503516474cb98e4827d20b60266439bcc65b70ed829b45b05b0d7d60da7b099dfdc3cfe5236794c2365e1f655c5e7eb736f761eaa671371687f99e213616a0445dc53ec49144a26cf8041a9ae74772089090fb27e0349b5bce7cb9d9f054e9d5e72a8d1d4581edfecca0dbbdaa3a1cc8fee8b2fb90e25467ec448fa6df4b05fdfd427825fba29d4c718203593f9259c0c593c71a00b901ab3675402f096a748b2d6d66d3b642a9c9f12691108578ea4360001376315c56b99a87f1f7d9401537052532e5b4ba0778aa98f8fe599c4aee56f9e5b882d64e33f101488e10f5502ddeb7f0bd6cdcc6be6c72412ba4e28920bd20cf3d72b805c76b6bff2895bd13150b163587a152c1e1664ec2db9956a87ddbd4471a3c1bb83583b2f93ab7e8eaae739663e45b2354d127346c08191f006b1e67d1b5a7f6714c5abc4bc3a22c5fa11d892a7f1d5fd12655145478c42216c66401172a196bf689181cf37439f738cdf0cbd738b03b7be21966a8a95626eef8de15ed1c6490392090eecfe9930c247eec4ef964d4686ba58a401dd97d1ec2a23b02919a8772acad453c899cbfb88182df74c91b6cdb5265b96b2f4c7ef9ab0635f57803b2b5dbea2d3592c70b2c4fa88a9127c780845d0e7d930565dff12e9eee8094dec44d0b7af5b4e841d5f1da0be424993eac8d405159c1168fb98fef0928ef4a94806661d37e16417de158ce9f52282af51791e78e8c2e0e935fe5151b508a612720eeabe36bfdf7c04c34bf1bd35331088697e53e6b554dc080a1739d55faec48ab7430901c5a37b5c61ab189fe3deb0f2c781d231eb57dfe457b280a2aaac141a27841a4e43973050538062513f15edf39deccb4d6c87791f0475092bd3c5e91384984a0ebedc37daf8e79c727c2f8bf8ef9a79a0c8144dfce77336ad85750e54e9e71bd7cba0ed50866c1d3bedeff85f7b1262be6d8cb2045ce905861dc5fb3b5a113b4253dcff5e9b44f94bbf79af7c82fcf096fe1ccc48a6b96cd5e7fdd3796b87f14c0a55f05e04cf890fbfd4a6388e54d97c622dcd7dd84dd9c1ca288a3fc1654f7d33d192b464c9ed3edb1610bbc7ec142f861c925ba05dbb3e1808320a66553ea320368ec8bdc5f757aa890825673e9d0e80b6b60a0eefad5bdcee3c03ee70db608d24f5e0eb1c64b6ba92f10b58be246655b984b58bf5268504801b4c1cd40c633503d87b907452c332ea16d7be122fdfcf5ffc2fbc950afe5c65f9fad7db166ea7dfe250aef83f2a4da341aead9f40d780fa4562e3e06a47a1b271027add92ffd595638952d101332245d0b94d1ca6bd726d29ae1b495f01868fb79e8505e1caeb7f8379e689add14e2f5efbc750688f4b2f76245efb8addd2458a7957f3829f87138e61370cde7ee72eeee9a0e76cfa79e6d869c3f2e33ad0ef7f6945af98c8659c936ad10c9f66a639c33965a25eb68289e473243c4a8b58132eeecf7fca073b341c6f8e51d88e955ea4b59c5993d57c0d52929d03d7d24504aa6a8865d3766ca9fb619bceac8214c4c6b9bf464493c534c5eef688bcf9538860cf14fc83c4471baf632eb4f8f7ed163fe80966de5f5e7598c495a0d62849ff611a6f308aa6e787752dafe0ea3df7826488ef9040d0b4f05c199ef9e7e2bc39740830cec27e59925a2922f89ebe65089eca31c1f366e300cbc6c239871d7df51975b91a4330850dbde552ef715793df19e8875eb8b9427ee44527a2fb8da0aa347d4305549866d16ff7c608e63781d80e5ab607ff8472bc9d315b8ec6ef1d3e027b9a177a3ff565d21eaa9931aec272136a3c70dbd2335b1bc735f93fc29dd3bba5c5d32f4868f9296436d057756530db0a65b632ed0cb64da888454c5322db6aea62ab1441c8c21bf406782e5b95ba75b6d8e2ceb6cd6c23b9a75de6936a6c3afaddddb0309ea7713a5be55522aacfbf7e07cb5f2e45081f5bf4869fcad36f09a4cd2643a0145972276f7b017c3ea656cb3263c73da1c1ea1c3908ace4a8a0f630a73f0e0fb8db2de4b85c650d9c36edc0d893a0f0bd3c354bba131bafeb8898ba1ec24974d0d5d75b82dcb02fb2f29359fb37d57fed7a9114bdde0e4ece9ee73d4317faef53babfe386399555e41f7dbb19f1b1d076a4373506414e254fd6f2985026ede5774425f82749765337f9cbb20d034778484e0f99cb08abffda6a779adf6097c61ec22ed7a2c1d028ecebf4894db684804ff822fd4efbcdaf5dbf276061faca150e8b9947bb7bd849c10fa8181551706b21421f5acda536144c6b8daf2d3e7ec8c3b5b2b858915b7fae0e188e7df5e6e90d5bdc8cfce90987e01b2bd945c8b2770beca31eb77f393bd6a334ac7a40eaae47e0a675036e591c873fd2b8a78332faead69673db15307e7e23b6eaa55b444571fd402d873e445983c501e9638039fc8c5a7368e7aac86c75fc0396a559e563e733fab6301059931aaed3d97a22000c30ea9323a9d7f000de74d1e578204ccd73f26762def3a8edfff9b5c631ae52a37f4a96346be9ffc2f2da8a9a2ac20fa7509d005a633fd76b7512194a4b8a5076b2f01809ee0543678015475ee4144772b8d2b920f52eeafccd2a7aa8e9b6a6584fe2ceb571abb8f0c9ff0117e6a3e322024718ff2b73f501c4520f1a43820e8b3ee29332275ef68aa602da6a5cc127870a773217642d56cfa1e90e433ce8f30c173f20893c005cae2e2c6e1985974cedd7ef6c07622a0a1d92bf6c1ac7cca43a0c25aeee2f17e3f99d275c6cf7c510bd34cc6a20f0273e3a89a98ebb23322d7fd1ef2c6bd3fc8e4b4e6b130b38c7b343009750608bda2ceef3f764a28fa2f00576b41d2e79d34ed312fbc5351df796f79e7625066cff9a4b1b739df8cacfa7df141ee7086b64dc342808b77f4bfa4f5f20aa1726d085cabe5c2a76a6c000118ea867928e07418600087b0c7a8765a8cf0aa1c844ca5adf6a109cbc8c33502a03b2e339a87473862c09b749af5e83bae5eb411f33b3558072891bac0ab41a2ca027ec82c00d71bab18a2f0b4c53bac438b48f6ed0084516e21bc9497adae3415d180c25b6e9286e58f8697cb83540b3f3cea1ab20159d3c5b0d4f69b888562096639e4483971c5d46631939de3320892ad2e28f567d84cb7127cb2ac86e2cbf80dfbce06b8df580d28c574a4eb82f5de6778cdd8fbc7bb79c4a3bac618c0b0abb1ead50221572d9a60333a26', + genesisString: 'Concordium Testnet Version 5', +}; + +test('testnet presentation', () => { + const request = verifyPresentation( + TESTNET_PRESENTATION, + TESTNET_GLOBAL_CONTEXT, + TESTNET_PRESENTATION_DATA.map((d) => d.inputs) + ); + const expected: Web3IdProofRequest = { + challenge: 'd7bce30c25cad255a30b8bc72a7d4ee654d2d1e0fa4342fde1e453c034e9afa7', + credentialStatements: [ + { + id: 'did:ccd:testnet:sci:6260:0/credentialEntry/70159fe625e369b05c09294692088174dbc5df15b78ebd6722e5cac6f6b93052', + statement: [ + { attributeTag: 'userId', type: 'RevealAttribute' }, + { attributeTag: 'username', type: 'RevealAttribute' }, + ], + type: ['ConcordiumVerifiableCredential', 'SoMeCredential', 'VerifiableCredential'], + }, + { + id: 'did:ccd:testnet:cred:9549a7e0894fe888a68019e31db5a99a21c9b14cca0513934e9951057c96434a86dd54f90ff2bf18b99dad5ec64d7563', + statement: [ + { attributeTag: 'firstName', type: 'RevealAttribute' }, + { attributeTag: 'lastName', type: 'RevealAttribute' }, + ], + }, + ], + }; + + expect(request).toEqual(expected); +}); diff --git a/packages/sdk/test/ci/wasm/VerifiablePresentationRequestV1.test.ts b/packages/sdk/test/ci/wasm/VerifiablePresentationRequestV1.test.ts new file mode 100644 index 000000000..7aeffc607 --- /dev/null +++ b/packages/sdk/test/ci/wasm/VerifiablePresentationRequestV1.test.ts @@ -0,0 +1,60 @@ +import _JB from 'json-bigint'; + +import { AttributeKeyString, ContractAddress, TransactionHash } from '../../../src/pub/types.ts'; +import { VerifiablePresentationRequestV1 } from '../../../src/pub/wasm.ts'; +import { CredentialStatementBuilder } from '../../../src/pub/web3-id.ts'; + +const JSONBig = _JB({ alwaysParseAsBig: true, useNativeBigInt: true }); + +describe('VerifiablePresentationRequestV1', () => { + it('should perform successful JSON roundtrip', () => { + const context = VerifiablePresentationRequestV1.createSimpleContext( + Uint8Array.from([0, 1, 2, 3]), + '0102'.repeat(16), + 'Wine payment' + ); + const builder = new CredentialStatementBuilder(); + const statement = builder + .forWeb3IdCredentials([ContractAddress.create(2101), ContractAddress.create(1337, 42)], (b) => + b.addRange('b', 80n, 1237n).addMembership('c', ['aa', 'ff', 'zz']) + ) + .forIdentityCredentials([0, 1, 2], (b) => b.revealAttribute(AttributeKeyString.firstName)) + .getStatements(); + const transactionRef = TransactionHash.fromHexString('01020304'.repeat(8)); + const presentationRequest = VerifiablePresentationRequestV1.create(context, statement, transactionRef); + + const json = JSONBig.stringify(presentationRequest); + const roundtrip = VerifiablePresentationRequestV1.fromJSON(JSONBig.parse(json)); + expect(presentationRequest).toEqual(roundtrip); + }); + + it('should compute the correct anchor', () => { + const context = VerifiablePresentationRequestV1.createSimpleContext( + Uint8Array.from([0, 1, 2, 3]), + '0102'.repeat(16), + 'Wine payment' + ); + const builder = new CredentialStatementBuilder(); + const statement = builder + .forWeb3IdCredentials([ContractAddress.create(2101), ContractAddress.create(1337, 42)], (b) => + b.addRange('b', 80n, 1237n).addMembership('c', ['aa', 'ff', 'zz']) + ) + .forIdentityCredentials([0, 1, 2], (b) => b.revealAttribute(AttributeKeyString.firstName)) + .getStatements(); + const anchor = VerifiablePresentationRequestV1.createAnchor(context, statement, { + somePulicInfo: 'public info', + }); + const expectedAnchor = + 'a4646861736858203e594af5733889ea6433d851f167f28ac41a12ef5236bd977bdf83815bf0d5d7647479706566434344565241667075626c6963a16d736f6d6550756c6963496e666f6b7075626c696320696e666f6776657273696f6e01'; + expect(Buffer.from(anchor).toString('hex')).toEqual(expectedAnchor); + + const roundtrip = VerifiablePresentationRequestV1.decodeAnchor(anchor); + const expectedData: VerifiablePresentationRequestV1.AnchorData = { + type: 'CCDVRA', + version: 1, + hash: VerifiablePresentationRequestV1.computeAnchorHash(context, statement), + public: { somePulicInfo: 'public info' }, + }; + expect(roundtrip).toEqual(expectedData); + }); +}); diff --git a/packages/sdk/test/ci/wasm/VerifiablePresentationV1.test.ts b/packages/sdk/test/ci/wasm/VerifiablePresentationV1.test.ts new file mode 100644 index 000000000..e5bf1c7f7 --- /dev/null +++ b/packages/sdk/test/ci/wasm/VerifiablePresentationV1.test.ts @@ -0,0 +1,188 @@ +import fs from 'node:fs'; + +import { + AttributeKeyString, + ContractAddress, + IdentityObjectV1, + IdentityProvider, + IpInfo, +} from '../../../src/pub/types.ts'; +import { + ConcordiumHdWallet, + VerifiablePresentationRequestV1, + VerifiablePresentationV1, +} from '../../../src/pub/wasm.ts'; +import { + CommitmentInput, + SpecifiedCredentialStatement, + SpecifiedIdentityCredentialStatement, + createAccountDID, + createIdentityCommitmentInputWithHdWallet, + createWeb3IdDID, +} from '../../../src/pub/web3-id.ts'; +import { BlockHash } from '../../../src/types/index.ts'; +import { TESTNET_GLOBAL_CONTEXT, TEST_SEED_1 } from './constants.ts'; + +test('create testnet account-based presentation v1', () => { + const requestContext = VerifiablePresentationRequestV1.createContext({ + given: [{ label: 'Nonce', context: Uint8Array.from([0, 1, 2]) }], + requested: ['BlockHash'], + }); + const context = VerifiablePresentationV1.createContext(requestContext, [ + { label: 'BlockHash', context: BlockHash.fromHexString('01'.repeat(32)) }, + ]); + + const values: Record = {}; + values.dob = '0'; + values.firstName = 'a'; + + const statements: SpecifiedCredentialStatement[] = [ + { + id: createAccountDID( + 'Testnet', + '94d3e85bbc8ff0091e562ad8ef6c30d57f29b19f17c98ce155df2a30100df4cac5e161fb81aebe3a04300e63f086d0d8' + ), + statement: [ + { + attributeTag: AttributeKeyString.dob, + lower: '81', + type: 'AttributeInRange', + upper: '1231', + }, + { + attributeTag: AttributeKeyString.firstName, + type: 'RevealAttribute', + }, + ], + }, + ]; + const inputs: CommitmentInput[] = [ + { + type: 'account', + issuer: 1, + values, + randomness: { + dob: '575851a4e0558d589a57544a4a9f5ad1bd8467126c1b6767d32f633ea03380e6', + firstName: '575851a4e0558d589a57544a4a9f5ad1bd8467126c1b6767d32f633ea03380e6', + }, + }, + ]; + + const presentation = VerifiablePresentationV1.create(statements, inputs, context, TESTNET_GLOBAL_CONTEXT); + + const json = JSON.stringify(presentation); + const roundtrip = VerifiablePresentationV1.fromJSON(JSON.parse(json)); + expect(presentation).toEqual(roundtrip); + // TODO: for now we just check that it does not fail - later we need to check the actual values +}); + +test('create testnet web3Id-based presentation v1', () => { + const requestContext = VerifiablePresentationRequestV1.createContext({ + given: [{ label: 'Nonce', context: Uint8Array.from([0, 1, 2]) }], + requested: ['BlockHash'], + }); + const context = VerifiablePresentationV1.createContext(requestContext, [ + { label: 'BlockHash', context: BlockHash.fromHexString('01'.repeat(32)) }, + ]); + + const randomness: Record = {}; + randomness.degreeType = '53573aac0039a54affd939be0ad0c49df6e5a854ce448a73abb2b0534a0a62ba'; + randomness.degreeName = '3917917065f8178e99c954017886f83984247ca16a22b065286de89b54d04610'; + randomness.graduationDate = '0f5a299aeba0cdc16fbaa98f21cab57cfa6dd50f0a2b039393686df7c7ae1561'; + + const wallet = ConcordiumHdWallet.fromHex(TEST_SEED_1, 'Testnet'); + const publicKey = wallet.getVerifiableCredentialPublicKey(ContractAddress.create(1), 1).toString('hex'); + + const values: Record = { + degreeName: 'Bachelor of Science and Arts', + degreeType: 'BachelorDegree', + graduationDate: '2010-06-01T00:00:00Z', + }; + const statements: SpecifiedCredentialStatement[] = [ + { + id: createWeb3IdDID('Testnet', publicKey, 1n, 0n), + statement: [ + { + attributeTag: 'degreeType', + type: 'AttributeInSet', + set: ['BachelorDegree', 'MasterDegree'], + }, + { + attributeTag: 'degreeName', + type: 'RevealAttribute', + }, + ], + type: [], + }, + ]; + const inputs: CommitmentInput[] = [ + { + type: 'web3Issuer', + signer: wallet.getVerifiableCredentialSigningKey(ContractAddress.create(1), 1).toString('hex'), + values, + randomness, + signature: + '40ced1f01109c7a307fffabdbea7eb37ac015226939eddc05562b7e8a29d4a2cf32ab33b2f76dd879ce69fab7ff3752a73800c9ce41da6d38b189dccffa45906', + }, + ]; + + const presentation = VerifiablePresentationV1.create(statements, inputs, context, TESTNET_GLOBAL_CONTEXT); + + const json = JSON.stringify(presentation); + const roundtrip = VerifiablePresentationV1.fromJSON(JSON.parse(json)); + expect(presentation).toEqual(roundtrip); + // TODO: for now we just check that it does not fail - later we need to check the actual values +}); + +test('create testnet id-based presentation v1', () => { + const requestContext = VerifiablePresentationRequestV1.createContext({ + given: [{ label: 'Nonce', context: Uint8Array.from([0, 1, 2]) }], + requested: ['BlockHash'], + }); + const context = VerifiablePresentationV1.createContext(requestContext, [ + { label: 'BlockHash', context: BlockHash.fromHexString('01'.repeat(32)) }, + ]); + + const wallet = ConcordiumHdWallet.fromHex(TEST_SEED_1, 'Testnet'); + const idObject: IdentityObjectV1 = JSON.parse( + fs.readFileSync('./test/ci/resources/identity-object.json').toString() + ).value; + const ipInfo: IpInfo = JSON.parse(fs.readFileSync('./test/ci/resources/ip_info.json').toString()).value; + + const inputContext: IdentityProvider = { + ipInfo, + arsInfos: { + [1]: { + arPublicKey: '0102', + arIdentity: 0, + arDescription: { description: 'test', name: 'test', url: 'https://ar.com' }, + }, + }, + }; + const input = createIdentityCommitmentInputWithHdWallet(idObject, inputContext, 0, wallet); + + const statements: SpecifiedIdentityCredentialStatement[] = [ + { + id: 'ccd:testnet:id:0:0', + statement: [ + { + attributeTag: AttributeKeyString.dob, + lower: '81', + type: 'AttributeInRange', + upper: '1231', + }, + { + attributeTag: AttributeKeyString.firstName, + type: 'RevealAttribute', + }, + ], + }, + ]; + + const presentation = VerifiablePresentationV1.create(statements, [input], context, TESTNET_GLOBAL_CONTEXT); + + const json = JSON.stringify(presentation); + const roundtrip = VerifiablePresentationV1.fromJSON(JSON.parse(json)); + expect(presentation).toEqual(roundtrip); + // TODO: for now we just check that it does not fail - later we need to check the actual values +}); diff --git a/packages/sdk/test/ci/wasm/VerificationAuditRecord.test.ts b/packages/sdk/test/ci/wasm/VerificationAuditRecord.test.ts new file mode 100644 index 000000000..fd67a285d --- /dev/null +++ b/packages/sdk/test/ci/wasm/VerificationAuditRecord.test.ts @@ -0,0 +1,117 @@ +import _JB from 'json-bigint'; + +import { + PrivateVerificationAuditRecord, + VerifiablePresentationRequestV1, + VerifiablePresentationV1, + VerificationAuditRecord, +} from '../../../src/index.ts'; + +const JSONBig = _JB({ alwaysParseAsBig: true, useNativeBigInt: true }); + +const PRESENTATION_REQUEST = VerifiablePresentationRequestV1.fromJSON({ + requestContext: { + type: 'ConcordiumContextInformationV1', + given: [ + { label: 'Nonce', context: '00010203' }, + { label: 'ConnectionID', context: '0102010201020102010201020102010201020102010201020102010201020102' }, + { label: 'ContextString', context: 'Wine payment' }, + ], + requested: ['BlockHash', 'ResourceID'], + }, + credentialStatements: [ + { + idQualifier: { + type: 'sci', + issuers: [ + { index: 2101n, subindex: 0n }, + { index: 1337n, subindex: 42n }, + ] as any, + }, + statement: [ + { type: 'AttributeInRange', attributeTag: 'b', lower: 80n, upper: 1237n } as any, + { type: 'AttributeInSet', attributeTag: 'c', set: ['aa', 'ff', 'zz'] }, + ], + }, + { + idQualifier: { type: 'id', issuers: [0, 1, 2] }, + statement: [{ type: 'RevealAttribute', attributeTag: 'firstName' }], + }, + ], + transactionRef: '0102030401020304010203040102030401020304010203040102030401020304', +}); + +const PRESENTATION = VerifiablePresentationV1.fromJSON({ + presentationContext: { + type: 'ConcordiumContextInformationV1', + given: [ + { label: 'Nonce', context: '00010203' }, + { label: 'ConnectionID', context: '0102010201020102010201020102010201020102010201020102010201020102' }, + { label: 'ContextString', context: 'Wine payment' }, + ], + requested: [ + { label: 'BlockHash', context: '0101010101010101010101010101010101010101010101010101010101010101' }, + { label: 'ResourceID', context: 'https://compliant.shop' }, + ], + }, + verifiableCredential: [ + { + type: ['VerifiableCredential', 'ConcordiumVerifiableCredentialV1', 'ConcordiumIDBasedCredential'], + proof: { + type: 'ConcordiumZKProofV4', + createdAt: '2025-10-17T13:14:14.292Z', + proofValue: + '01020102010201020102010201020102010201020102010201020102010201020102010201020102010201020102010201020102010201020102010201020102', + }, + issuer: 'ccd:testnet:idp:0', + credentialSubject: { + statement: [ + { attributeTag: 'dob', lower: '81', type: 'AttributeInRange', upper: '1231' }, + { attributeTag: 'firstName', type: 'RevealAttribute' }, + ], + id: '123456123456123456123456123456123456123456123456', + }, + }, + ], + proof: { created: '2025-10-17T13:14:14.290Z', proofValue: [], type: 'ConcordiumWeakLinkingProofV1' }, +}); + +const PRIVATE_RECORD = PrivateVerificationAuditRecord.create('VERY unique ID', PRESENTATION_REQUEST, PRESENTATION); +const PUBLIC_RECORD = PrivateVerificationAuditRecord.toPublic(PRIVATE_RECORD, 'some public info?'); + +describe('PrivateVerificationAuditRecord', () => { + it('completes JSON roundtrip', () => { + const json = JSONBig.stringify(PRIVATE_RECORD); + const roundtrip = PrivateVerificationAuditRecord.fromJSON(JSONBig.parse(json)); + expect(roundtrip).toEqual(PRIVATE_RECORD); + }); + + it('creates expected public record', () => { + const publicAuditRecord = PrivateVerificationAuditRecord.toPublic(PRIVATE_RECORD, 'some public info?'); + const expected: VerificationAuditRecord.Type = VerificationAuditRecord.fromJSON({ + hash: 'fcce3a7222e09bc86f0b4e0186501ff360c5a0abce88b8d1df2aaf7aa3ef8d78', + info: 'some public info?', + }); + expect(publicAuditRecord).toEqual(expected); + }); +}); + +describe('VerificationAuditRecord', () => { + it('completes JSON roundtrip', () => { + const json = JSONBig.stringify(PUBLIC_RECORD); + const roundtrip = VerificationAuditRecord.fromJSON(JSONBig.parse(json)); + expect(roundtrip).toEqual(PUBLIC_RECORD); + }); + + it('computes the anchor as expected', () => { + const anchor = VerificationAuditRecord.createAnchor(PUBLIC_RECORD, { pub: 'anchor info' }); + const decoded = VerificationAuditRecord.decodeAnchor(anchor); + const expected: VerificationAuditRecord.AnchorData = { + type: 'CCDVAA', + version: 1, + hash: PUBLIC_RECORD.hash, + public: { pub: 'anchor info' }, + }; + expect(decoded).toEqual(expected); + }); +}); diff --git a/packages/sdk/test/ci/wasm/constants.ts b/packages/sdk/test/ci/wasm/constants.ts new file mode 100644 index 000000000..2d0d6c52b --- /dev/null +++ b/packages/sdk/test/ci/wasm/constants.ts @@ -0,0 +1,10 @@ +export const TEST_SEED_1 = + 'efa5e27326f8fa0902e647b52449bf335b7b605adc387015ec903f41d95080eb71361cbc7fb78721dcd4f3926a337340aa1406df83332c44c1cdcfe100603860'; + +export const TESTNET_GLOBAL_CONTEXT = { + onChainCommitmentKey: + 'b14cbfe44a02c6b1f78711176d5f437295367aa4f2a8c2551ee10d25a03adc69d61a332a058971919dad7312e1fc94c5a8d45e64b6f917c540eee16c970c3d4b7f3caf48a7746284878e2ace21c82ea44bf84609834625be1f309988ac523fac', + bulletproofGenerators: + '0000010098855a650637f2086157d32536f646b758a3c45a2f299a4dad7ea3dbd1c4cfb4ba42aca5461f8e45aab9112984572cdf8ba9381c58d98d196b1c03e149ec0c8de86098f25d23d32288c695dc7ae015f6506b1c74c218f080aaadaf25bb8f31539510c87e8fed74e784b63b88afba4953cacc94bceb060f2ad22e555cffe6f0131c027429826dd3a4358fd75a06e8f7c5878791f70384a7f3a90f4b7afa45fae6e0fa7153b840f6fc37aed121d6c51225c56d1ce6bbc88096aa3f86e6b3517daa90baffc69b9eb27aaebf87f04ed091412547ec94aa7bf0c8dd826e3cdd621e11dfef6fc667c53a5abc82c163efa51435b3be74f47073511db07c1af7229c78ec38554a3f7e9fa38499201d4fe6c80e867df75f2c540e57d2e15f9f5e18f73c0a804567494aba85653f57f5f990111d680400bdbee2d0678271ce93a119c107458b8d4e557fc870de9cf0ccce6d34a216a6ffbec9581349366706377b436261200834fb2654ad5cd34f5bfe3f3658ae94838ed38033886844008143d823f7e49db43017655c53b983a883948d7401f26ed14daef0dc46da30f57e187ca8e027063174a412cbfc8a7b606b41fcf7f75b698ce9115afe5124e8d72f4b34ce839742a46885ed60af26f24f8d10d46621d78b5772b0314311ed3bb627e93bac47e7eda5ff4d2ef5f98ef7265677a382a1f7b8f9a43d1e563b66b6de94c52c3c87169bd19b6e884c6a28dd6f51ba64bc36ac07926a8d91a64e88a4e19b4c0fe0db8b76c9c99bfcddc05fd5630c54bff85c041fea34a630ffe1e6313bf039477994696db0596f9e2522e04ffdb530147780099d918ee47031db2870c00b25ba6d201b00ef2459e73f6a8219d7a7ff96833bd1c8a3c24285d93b85a321d1b6e175f2d9ef111a7304438b5f5874f064539d27bf8bc46d8e2473d458f957fa206bc417ab5b8ca4dd5f0595e21063c1bbae14f07bec6f12d95a05166b90896f33aa941e0a057989dcc1ed77430a3e6f93ab16f62598b9816dc203db6ecc7fd7989d2763d4ef72abae3c4aa1e1b9c9965f8772a1be640a18486dc0804a2efdd7175316af3220a6c10db0ac0e2913de42c44f2f54a20687d2b22c9636e0ba8d09747f2229ee6ba0fa49c4a328f2ffcc5ff462ddca0de8b1d13cf85ad590183767a9ae4179b3c617ed776cf14abad1964d362747492547359c4b65e78f6dfb452d5a559ac0c24b8affdf13256e43d3213bdda0c056172771cda382f3f31fecb82ee8dc81a48fe74467665c4e34e07b1954cae4ee97270b94a336779d27646348ffa0eb52663aa60bd8b7fb9311d38a7fc1f0f2842081cb81b594dfcdefb44db1caeeac7527c5a48e8a8ecb83de0514ef184f2a82c4110f9f64ea655fb42218f95e27c81a0adb3fa7b441b71cb15113aef7c318d73f911688d34111928fdff181dbc014be35cc7e3e5bd75bf767686933931bab272eac577558b1314206d97611152ae82c1bd56ab92a4378dbe73e76a9fbd90154bd1a8c73c681f70298d15f1574d7038521b4e9b9cfc0d900c2d74cad594b68923607a1b913cb9484e093ab8ef2020ddbbd77d041581487f41e2dea13832e566325f4fb0f9934df94ee068c676e475251659770b93e0e32c7739d3c430db581308817ba091c56a5087c6248ee79baddc3c233e77b3fdbbd73f1a4298a8277055ee32a96b7e83f76e0070ea65c4a1784e9e281fa70fd9771c762a91c7ea014f60c87d09687768c9d0c8277d84e8af2eee505fda2967631a999619dc332d98bd45d40458923de655769aac754bf9081dbe9318b4880bd325e6be713f3aafb65d8e44b9143acdc7f2ba7f15290c84225c77111e0629d0f82f2f367d2641d0c3658fd54e538286787d379252876f71ec4a96ca72fdf8b156059ec50131317b9243eb71c5e84ab6791303ee0ebacd790bb27767701083afa616413b1d5bf0347cc4199943c938d74eda87660dd40405aedafbaf54b1e177117f632deeb6a9bd5227d9b8ba5693625ac68ed7d1604ccffe73e97a45411b99c666dd5eaa90cd021d05f12f84acf60bafe0c98e8ae213fa65c189256c79afb4b3eba970a8ad4e94374dd6f9d258b8c86335f36481affdcc9eb074f1ace01fae937a7d1a92279bdfb1081e029103e48877e4442199593c1b946aaed494885b0bd9d67fb7abc6f41c0f7574c3cc007d29a4ddc0a966270f5432a47e3e5563061b158eedef8bae522cb1f800c6d5f0a85a4e24a68a9bddb99e408d204e56cd994796bd004cdc5871798df2bf068ac48271290a2b3b1f0eba346f8746c79ce2389ed3ced67bb962fcb50876b7a32afca94216adadc0aed990dfd2b558aa5d8f35aa773e710f14f1641ff7eaa9ecab43ca3dbf32e846a90c8a3771d378aeb9277ef864377f7ca79ec7bc9e4b373ac890cbcbcf7c33df157e0cab0fcc7fc4daa2599177bd9d6bfc4be694a8e834be081bc7fccee530fbf6bac5be0cd1656b4c79ba6e121c39640cda83f81d17324da795da00f9a05317224ba827cecea6cbba1459df4a5bf6b84cd009d5864d44e747e3f30866a7c261004f861359e717c414a699461086e9136e29f9973e5b3f008a77fedb184e6835bc50934178b4bbd8bc9ecbb10865789d063ee7781fb2dc7b9b641b73fb172c3aba915c420c1c72cdc999d1fd224bf4d3d8a5ce26fa36100533682964abc2e99368402b0c518df1b6af245837eadb31a5616884c5474d25f57a228ed2bbef64d5a80e37cb20385a0283072a3b7a11b68bef323ea4804608c0eed5ea213caf6feb183558d56666f472a9abd10ddbf49650c5318109f9c5ed8fac7847644e153638e29e5f45dd00da06c9b9aab4af18eda0d0bfdc48ae42efe79d4e7196ef07c4319efdf042724d9aeb2c13b089b1fde77467c32cdf778749370e2c3b84a466bdfd1aa00ca8ac85c0eb29f1ccba6953a8bf877802ca3e3c31ceba8f9ff0a8684b3b118ab11a27182b033847baa372fb6b4c88e9177b8b131cd69f6ff5ec347db17215fb9890fab1c4d265d405ca74e249fafda004f86f2a5c3fbec7cae6afe6c507e089bb2d47a9e421512c016b99cb1cb15a3b8e75f2d8ff4866731585c686871691b5ca89a1351f6a79b159530fb5825f87131b0bcee2573d3932d223a358fcf08c8d4bae92f26d7fe35255c7486484bcb26693174ec2688abf39db5f091c8c09ad804f4ae42ca8b645200f27629ece81c6b5d94ef7646cbbc4b62a716ac6d2ad21f5a7e2297ebed15f2e085859a1002a9e7658b0d0249446ce9e769caba71536b9329a5fea3a15d52344d42d868b0d92a423fa44f8a6c4aebf50fb496c62d03598de39f1fbdb29e098c6a2e2ca59cbb5e37eb021594463a72471683117b957890f8307828168b1a8f2066a5e5f2c6822589272f259b012822b8be2b15c004a2a69b66c2f5ee5d81aa9a7bbdd44d059587bea89d021c734f62828382c9f13210185482694f8b30b46111d426927ec5f10b155f825d852fd995b9c7ff27861f7fee8dd2b654473198504cdcb53154b29f323ac1adde8510beabb8cef6d1a927f2dc844f746edeef9b256ace54dd8e3219b265ae958ced6593ea154b8188bf58b72ff05c036630b6d55141c4ac227a4d966fde1d0ff1ed9748c66127feec86e7839d77c1c56b46e29d2033a7871b4b0e3c53b3cc4ac49dc8eacd0555b338265be1234b81259b3746eae71600c16b7f4bc692f0a1d3dc2ed855c9fb11b2ff5fd9e6361a82d42bd4ba00635d974fa1214aa8da9180cb158347608e0c44bb5b77397d8b233df7be91e7047d28d18ee66472b98c78091eb95760c5b381d6050779ce65fd1508af8df81a3385f185f7941f0b7aa9bcd6023c6e4db7ac3045aee58f3b2d307a5be14e2b59d05333949d9e8dafbe15b671034b7c629c1de1bf560415cf72761d4d7abcbe3b762d9a66b1b630b75c2af751f6a5d37b43e84139aeb418e0b10dec012a32095cf7305bda79bb3853c3cb1fb3c8447ee4306923b6b693020880bb11df8c78f3411f176b2393776d0b9b3ea7eb0cecfaadbcd8112c4c4ad9960001f4d4df210860470a4c89a0fb9a07229ec3476e1221f490ff80314da398ffbefac3a376c53c0db82d6785bc3d3b0cf3ce4bb54cb9b6fdc29b01bca2894debb7de518a61cc953d4f92a9c52d427e2858db633d042ca14028ef771d9c8c62014676a30ed9d5f9f51036a823a388ed0f72321a13dad31bb28fe383e456361159490942382ed3e49f1c420968877a42b7bbd8c3c9e78723017400dd178f0f89065072d7ef5f5e9baffafb2ce55fe7b35a7865957260b1789e3fddc5bebdfc3301e79b5077a92f6048121c557099c329bb270cb90ad47818398bbdddefdfa6b9d9256b8194f51b45c69176d5d7deb6440c9a81bc5ad0c8b2f31720f071a8ddb7907fb161ed658f890540ac56c506b7b98df3a45f6fc062698bc86d3cc1150db00d702b2d1e3c8eae552eab55a7afd5a5a3be2610867cfff07545c8227dd26c502039240cf00ce2e91b7bd7ed6495980639eaf919635c82c5073acdc21aa275ba87cb7280d0c6321c6a4021b1f0ae7bce23c32238b3f486ce27434721df436446f9ddfb9c66bb1f4b2337803850b2606d8954268eaa0ae060f8a6da2a0e7cc315c1f8cdad4e11bdf5165a2d72ad34f976bfc41000afff158f31e9632b898f2e9886b8b92c81bf916861c62f07ea326e94ddc4ff9af51fc0420b0d3c57fe0d14109ec90a72de74147d8ae1eeebe6b565c4ed81ac01d49b0e6a9551e9fdb7a9e040b4bcbad61fe627983462106e1c0b59ce762ce89b4b1100444048e2fe84850db19d63522d83993067cf025ddaf796e2f11d76da5a2b6ef4a409720ac4e97f7c75973584d935ab0e723f415a5622b03ece90173ea58da495f402f5ecc33526d251da5d2eb5558cbabc1634e8aced49d7c3bfb514c20f7284e8a902855b2296ae90ffb84bd0ea96aac846b6c9dd8ce92c0f9d457ab44a381046e7cd50b4958bf4b854bff340be4d41452ae1e3e19c82b99908c0a45968e580823450ffcc67561dfcdf446a57ed8d032ca94ec3e8dc0a181b9faf478275fc353ba104d5c31cb7338cc4c2cd445ffed7025f576ae84e9b08311b77490b6111dacd7e3afa920193cded42246276cb999d5d85f594352592002efa2d34c387fa1901cc0f63c0f6ed141c7d2a48d5bed031a204cca98c4e26ab7210d4ca5ba3e20df6d1b0e1daa69216b3e1ed98f20ad9f527efe75a0e7e2067c85383b7a614945669b442e1f0d7ee07160902ba9ac292fa0e9c62bc1b0e848856aa680898c4769d965441a16cec21ffc2eff717267cf0f9d4e7139b764c72826309e8e0ce8965c6c96953c3ebedb4ab4c9b35c32b7b59f8715f7c790afe8a3c7a09965192baa75262a16650cfa1706f4cdb4d34043ad1d6a53898164d049647dba7be95b7c620c750e3146155f57ccaeb283936ba6b9fe6a6c6fc5fec11b59b2d12da76075e022a2308fea5865ab5cbc12a4898de2204bb218cb6757d1d3d380b488a22e4dca9c9b1d1e2d01e7a9e707a6c09c323973dd639c7d48f25e4b5b5e95b4394a1221484d337a032b34cf77998acf78efe6d7c8c1ca4f83970bd62c0a790f38e405827a71baf31a9c89892f6745e2d29bc13b5b2873c0581df064a96bf64eecdec5504a75c7e03369662e0ad9f6c2d38c4da79b82f62aeebb69392a78a2cf6fe5a1ea8627b77d5ec828f4d445fbf3a51987023218ce4f944f03041d01976a2780fe632238a828414364b8b98aa8d709bab9c2202b144f9edaab822c33d5d8ec5f62517567711cf25c890036700b7f39b130335b4b8d14d4e9ebdb6c4fc015027907b931fe5cd99ad87976e8181fa284a53185f1e982ae24842abf28f23de420376b205222e5067a2ec53814d659485327eec748b821d70f114fae08ba181f320717dc462c88a15370cf8acba5516c093e13102d92c681bda73f3ab192976179dea08875abdc054e389b309d62a9737e05edd489469f63497da850450e1f299aea08649a9ed1982a4f8a3b251fb8cfb4737ce650d125199d07f7acbbd50dd65ab973be9684ee248269ff200e2f36118cb0eb9104b94149f9c91ebb76f9d46ef5c11b4b78bf6331116110dbe6bebb7169d3c637839bcf89b114ed9d765cd6211405e2dbbe78a4ad608a960a051097a65dc0620a3d1143439c4e04cc6eb1453e43904dc9f9db084d52b2d251dcb1d476141b74c627fbff849eda5a0abc85416b97d9888dd3cd88d33104e8f0fe03ade560dbe4baee030af797fe68e45196a2137d79de2ced8e6c2771d6b16a74b2c1044e9d381a0107adb870af01329c413c0a29b88d92b3a3c4a117424e613704917bb2aeb343dc22d96b0831db78b63063f10527cafc6731b32d50e4b6e39f45595cabd413f62117244ffc1d69122b5b1c9bd864303c46cdaa1e76a97448ba5ed266859a744411e29d1ce4b78a037d99d337a7b2f74907b591da8103613a19bfa6cc1f87b9698912b85ddb0ad0d86569f813ed4dfd3a9edfc0803427c648ef49724149e49aaaa98279b2ea7d25e3463be263b6cf518e9bfc456e993c2a3806335f13deaecd3c052e8604a08c045175d044f7544291dd4acce9db398fef80ffd0fc75b28886903d2d39c133a1869b8429c9ec58069384d8639e6d268849bc02f863f431f574ca988b84ae59427e446891cdcbed74a72e2b60601ba707b2c839d85b33320a32c1b1f7514510f70d08c474a7b6155a0ed004a23fee27c6aad072c0c751474948a1397ced8f04c5b50b4be2001f4f52ada7731781b69ed93b7602441344dedcc359c385a897532f3475764e0ed3c80959cd95d47f958d237f39694cdad607b966b0d2b690844646b614fa9fd4f169fb4de7c2ff3b4805402e4408c5ba5be95cff6bb68c79104a58c11aac0f999ec307a230eba3869f9078c453af1539a9cf5f07dd18a10c2f2104a2007d52d55b8ba7e24d97002c461c2f21796055179f403a012554b2eb503bd7f62aa3542df13ce9b520c81f364b7bea0deda55e430ff52e0f7408e43e4f7289d93b2431ef21d80e04ca57538dc17b6a7b2f1729cd52ccf7f8b4d58fb2fbaef5c9b3cb38b34b179742e686b9fcb884286cbb9b35d70083d8dc6e6e9f374d473a79ffddf8547bcfc05d5da6250ff982c8692b51e6b219406d9eb1b39dd2adc8de3dd86510da7c383041ed1e87e5104d33478bb8bd7891e16c72eb23029dfb8b90c98c15377d0928a36a65b01b4e83e5c93e5a804a37513447ca0e64a945c0807ffb5f2ff06f270e9080eb0fe471b4241a9f66235b8066d425ef09e6d46e19717747d80f5a16964690b949b335b4c20d3d15a7eabd7e815856f3463565654eaa243f7a4c619289212014d1e1effd99390d1feb0d9a9a0b101a298aefbfa703582c5c2f6470c673a180cd6bd9adab2e6d044ba3f130e47b7340ba9a358b0600d1a8862a9c330e6ef1782ef1c912a4a60db9522cd3f1b941832abdce920a6bebc950d29e057d6a71e75f9056d601079109bbbb3b7bbc46b413934b902ea7bd0ed115553de7c51ce60ca100272221a880d3d2bb0b49f5b36a246df30b7fcfa3e924b5e0db2c55038289b1e266f91d504c14cc3722881721f46d864c9f5ca7c736ade9d7196c9fc59a4740d59d0bc1cc3845ab33c8ee7e539f2bcd8d7295e88803b1a66552fbe133a585962edc24ef8b8b3ee88d005c5817c9e65249c554a678b71fa1e6dabdea92b1ffcd004e16f289ff1758a491a2ebed7c070423b4dbbd61141d3357ee08bb9be71663d0d2ea59f4322ee39348fe8af117fb424ac49e362ccdeec78e99f7f2eef955f7454ce38c8dd963bc6a43bcf476514a5951637c64a71dcb65351b05c557d7485494cbbd0c206acc04002ca1545d1ad3f70cff600aeffa021fcc63d46b5128d466c87475a6b420af9e4648b3218449886a5f3448d2993609e7055566430b2e3f3d09a22633e19a1dd0418be9f99cee8be8336f575d3a55dc090deabc6786fd0d210b7e9577d69ef79beafbb541f96c6a01c9344653d7f84cef645989c9928b1738b4fb901dc9b9c29f4e0940fadc8403262e953ed8f6d772f1040b6ed52df64f7bddcab26bd6f5575e325fc81ede2c931f46c8cac6beb889961d6b2395d711261f9ddfd6be95e17f1aafa506e7d14bddb132b0821e7b06cc8597c8133878ca983bedabf66d0d2abadf4dfa38015d3e0afe9323c792dfccda5544c17624162ef0554b5cc6d77362208018717a53f44c2bffec644467d6b2bdddfd1ac55973d05edebd6e24c41223b8beceef1ea63410eed001c947429b115b864d785de238fa0e68411e9c5de226bfee699ab981ea86a67935faeb6f571f04d6dd1b73920a4afacea51c89a5561532cdb94a08da8e0bce1da552da943efdaf4a9549984337f0f5c4b42fd81a4f545e00d86f41ac6d54eb554efeba20896f3e1234f41c285636524b666b69a21bf19318a618c95a05bd4754318f696e0671662e9b37cd2393fb709e687a8ff171155d71e6ba436857d522aad38d34382c5b9f2c8e5b94464156cfbbefb3e390a935dfce72b9dbd2197e241b68cd9ef87de8165eb0515ecf534d5bdcd2b6a1540c092e26d99985085a95e31cff50452e89a3de519a0bc9494c66e77434242ef17bb0f4719b65b10cdead9b2e9072c59d334cb43827482fb92876e6fdb66dded5245e0f4a8aa6ac7bea417f3feddba61ee20b092fda1502551fe6cd0f14023fefe4d08767a0eaae297faf7352badf5ff9e58cf4d1fecb857ac3b9e83c90d19345474f82c074da1600172969606801ec567df6dbaa2df8a4380d3d500570b8461b2cc8511e5d3a45495b3bbe014440846892b11c3248decfa722419cd30aa94d9209872872820ad1b02b8abbce3b26e31aae5bcc816795d0bb3810c1e4f6ad22ff0a00ad709f93c3f63a9dafc2eb7cc6c8d986808e165b25dbed2af7ff5620ab09b3aae0928ea163fab6315b7885448f5579c40c35ed50340bef058ab56186940cb5fadc5b1d093c1a82ddd172a4d5cf3efd5c5297db8dba62e1a302127b7faac192a1de5f8225c49607768a7561dc138b0c4d9888fe68a5e38c125dcf23c7d9066c34c7536226cf68e8106c9eecf4925e48c6fb8286c5e4d48c136550d179a30daf27017aaa890abdcab582f67f4b7117257ffefc0bb8a8b6bab8c68894955bc13c185b64b5e790483145915881643222c50fc024002a7c619ac5561bb72704c2d46745ca5ee77311a3c22750ee8cadcc77dd037db697b939ce0e8be03f83c82948d491b38289483c8da893ebd9384f83f1ce0148a43065ea4e041c22aaac15a1a82dc6688c3f73f86f97c97ca7e259cf588f8ddf2de829970a0cc527df4d9514032a4161e3175c34dc46065c3a1f4c70a07734fa9bf76a3744d2764df6fb49dd8a97d85c62890d6cc094862a34ca78558c47c263fdeb1dd5dfa9b6c0ae2b3cc43b5fb742049ab38e87f9d50ef256fe3187506f8b462ac07b85f2395089291336d8233db5631cb1afc311f6b1bda3a2b8cbeecdf46085c5f9c0727f40c64681ff0adadfbc742a8a70d717a00080277f8fb67074c98ab4cdd2dba5b9a6d3defa9b6024bb7c452f19b74dc8363cc33b62ff9d5653f7f069723f1d6d7f7fdc38980bcd01d1867c2060dbc75a615933c2830e5a39885185d74f980c733afd19a830b12dadd006530b1514b5ef61de8b5b27ca58b9471029edf360e8bc4cc962d6ab28a1dc2083c5b98510f4c22f4577b9c416b4072f36b298ee1ce8dd8111c254340123e19509c179e4b71cb268381af7290196666e055a7de2715e6cc173bfa5af642a3fa942c85b07fac2f9970bacfa03e09d36da2f80b4f1a379e8e5b43d9e2890d551e6f37df150fe561212a3b0b714d13b46765dcbbac3f6ea01c976cd366da0de6bf601eb17230e6f3032f9a476ceb75d7d25ceab9f65bf93102daed84af1c7e336daf571bae47f98f74214d93403f55adb50693c9cdc1a2144f1544bcd1e441a34a8d2a2f48387130e569a9f3053d510e2bf379df90ed2064ebd503b44a2a729a19ae1af257508dee6133bc8cb0af76690be0b41248f3f447adb4679cbc91e93532513a9cac6dd4a587ffef01c202f6e46c3129567c833b5db32fb36054cef0ed13530c48cd3bb887137f41902fde6e4e472bd6e38f6a0992d363a0470a1f8f98ee17d528a0b4b187d6b0a931f2a0b123d50827d6b50797da8029a868f97e144f7586afb6b394dec7ca89d85be8a0ecec25863eb9215704eaafab2e277c3256dffadc6a99fb3bc94b4483a3fce40f29ecbe39c223a9d99adab90973c65ec64b9b9ba9f1d68ca967d87df7793323d898fd09065da2c6051308cd4e535634e235e6284fdb3697bd285eae40d57b976d7ac690e6a96b193a9c3ab3dfd771efce5cfaf12d48abd69023d7773ae998e7433600924435bc89163b7a18f7bada60f52ced09e50dd8401198fd48ad6741f545fa1b53ddd0b55e71baa55112e01f42731148ad13f696b63e307f3f9878c74bf513fb03b3bbcedd2234f8d299286add8e0f7aa4c7511b9cd080e293b0067b4bed86869a814fe4b2a5b5f2a9eb543795c7139211a61de2d5dad8c61969a0a460552589867d544b3561b8234e7d16ef4fe6590d141e3b29a77e967caaa25bfd1eed4537e5d57372e621f4f99825f99ff7016193038ad5f975fa64497d20f2d05ba4fd2d015ce6ae8d65d536584157d0a4269f5421c308818415235d15c2063bdd645329e270c4c6e180575ba5b20e98812520a41d415df1b7acbaa29d6600b797160d9e4b7f96cffbadc623456aaf9aeaad3fbdfebaed9b7725f4c5027d7acbdf36c3aa77ba6d2f834b5b45b2e7f1538cfc109a4817c913846a4d5a479d5f29e9b377144bb76265d1747644dda7465b498b3ebc4d361ac8817071b60ee23d0803d19a9da55619b430997ebc41c59d8feda62b9e1f287634b1b8b22f415c89444c01af938c50e0fc828996a044d5dc514479fde095d6e91c2e75e74af4dadd159e4e84d9dfb8e6e4e3fffd7f9a1dfebeb14816857dd1b6e197fe33d03f1835f91d2764d3cf8eea43ed03e9d4486cf3dc90f34a4f4e7f1e6b62dec4936284054322904ec54c8e3d618b4c8fea8c5985624b3625b01a904512da4cf3a5bb5d23d99fc6f72f636e08c23d435de96b0d52f16b68932e2a03b0ae8de987e2b9a324c3c1028236ffbbc85399dd8e441ce740ab549ed1547591469251aebad65b00fd4d60abf9e3072213e3ab5ecfa9ea0c1dd98141b6f58d244a6979af521e1d1c04b2de3db33a15b7e4845046a4f749b9a08fac86c32b958079a48a13743777aa000581a58151b3bd18f5b397a786d3323bce206a91013fa451a2e8c0c56cd428497e959b728e88a9707a80230850f82388c5f653adcedfee27ef1d045703dfa12952f9ab8859c610f3a9eddcfa6f3c470b7ec1cad5c06f69a65fa5e8641eff14a5378fdb63099cffdecb4816f5fe8fe64f715b432301523d2280be3e3bb2ff3374c5fe07f1342a37fef2b46a2726d308f9e678246448528a219ddde24baf73cbf4459791f564d9fc06b22635a0580b3ff21c9d5f6ec4433fac3eaddb055714aaceb48ac87af41f00fe3482a9888afa7a0811cbecbe64b264197ae22c1e17baae62116e79948771731bce85068dfa4016f3978e39ddb6373b398939642824f763dbbfdc002af17846352ea59cffbb3b07fd526936f313a9f36c818b9c8935d3535e05a7f8ad66541a202b3770e7b5b9ee000fc03f87a67ec1668c270bb6dbb46e1f58e999887cdaf5bfd08a899afbd1b1712a474fe6bdc0e3c6540072ede5b4fd2eb4da9b93bbda506a634eae7f9b0855c40609011911e765c5bf92c7ef9876d3898d1b9f74ae44dd10de3cc31b5056f219ccb522f6c48d8e32893118f6fdef1441251f5f1ec351b2ee7b95a2ae8d08edc4ce2d5749b2f07321ea546805fc4d46c27392fb66ec9ee04c41c8f77d4920dcbe5937cb5507795210b0acb300b3a9625fc4edb076cfd36c5f8f0dd519025af2eaccbc13747e1c7a848a5deafa9caddd682356e21d7d24d7b3458f8c2351c20a864a152fffa4d049b331de77b2de9490bbede98488158108571cb80c5e3eff895ba2a31a1d70aa1bc3b38cdf5610495f432a9f9369b9ceacbf45da63cf533e42df66df7c7e108146bd107fd890e32252e5e1125db9e5e938fc86a3ad8547951fcaacb69789e99bff56fb9e0350ec742665cae725faba2dd08de44a16d5779fd5c0a3624db0bd265d933494020efe58b64b6da26b0a78377fe8d55a2576e245791efbd9c2811c66374f1d95121ba4ee1b85b637fcbad348cc8b089936520a1c19391ec9186429689919c0692b665e95450bb37c8e88306f1d367cdde245b2174277da0cd799788d6ee9ae8453adcfccc3a5dc9832cdc59946ffbce28be4a4c0630691f5e9bb4f9277df568182fe478a4eac8b6576909c6780a7bcb0ce8f7d60f89e2d76d1fbff45d6390c1ac743129754aa2b7320101e0e7da7327d6c5e545feb5a0c48c6cacaf245503f87d7b7ad7ddb24522df40f94cea9a9044ee7de7216d533e16823ee8599181975253e5841c4a88f71b04d9465eee909c6b1e220d11da5bcfd01ab009ddda154828467adfa4c49cde3991a2062e10e74da1a6535ced9fed0b6d9f4c4f507d711d8879b883c43e7c3588dfc02a3218925feabe7dfc619d110bd5455cb79ad9cea0cbb5d2220c5d83c9a2169af546b260c4eb936a74db8d1538470c73fba41010963314b1ab662230d2bf126102eca5a8e02cf3fc9c3e406110d44c483b5a1770c0c97657b251ebfc59d2db5eac1cb2715aa3c5b65c1f4eae95b700bc4815b663b61d0cc7b5bc2b8270d7cd558edfbe3086cac8eff197ed25b4ca98546735f433db5ccf9d0b01f56400f9eae1656bfdaf81137c306211011e09b15dfbae35c8df774a3bba8e40cd3d4fb00a6880e445324f26d61a612acbdd0737cae1179bf26bc29b7cb0b23a907ea011402947c0cebd8aaa82620a36ab9b6d281470d6f05a42c99b87981f7c45adcd01b2b47026eac614e0930976ba5b0ca56e81fc5277d80a9c7ab49d670ed03dd760af98a47e495a3fe92c0f97c4138958044faf55c5f4ab7833f1db1245522924609028ce1c234bae58d99e377eb82856c7a8fbfc6a21f3f2c21001badd0aab473cfdecd9a89d230ff477d43739debadf3c7d03de3987d4715ac54ccfbc271ef5796428faee43cb46feea3a73ca2acd73685d6be66e86a958a8f29ddf8a7d3f9f444b399dfab5a6e46aff3d3ae8d25a4c737e4c2bd1660b0302d2381ee4dc9980fe75e39da89af19310d61a4042f56d22fa2eab5499eb2865197ef07226c05600fd904f67feabb07a7b6f5fcd31302755a19c28f3fd8fb34849afc27a79c226e91645642229a0415ecb6279770561ea342e043baf8907a6e6a31914bfd8295ba6926bf4cfa11a369e10895d5d4ced4c426b69f6084a304804083020e812d01b0b6e6591dea9145ae40fe4fc0e820c2a3dc813bd6b1e960adb54628f1fc5b66df7e33cc9cfefcefaf14a510ecc1ee5a7bd5656a34b14d3bbd6e20034b421233e24d7d8d80b51aab12a1d55408e3679bb3c68997729b2488594ba5d8ac0deab6b93fc38ee6db89eaa263d34822bc71ca45234db23ec5a6826540781358a9f9f360b863204e81d07046a093dcb0c9bc3c0415b7c45722eee2ac88627823b78421d11f0be2d32590c2380f32d355229e34e33e44ea3ad47be9021aa521b3832b8edde4a37368ee10cc09141822d5dba7c5b50705b4af3e94e3f9ca32840146fd4f5f6910de5004fd23c3089556a0fa92e2645b9a3b9f4ac9e850a3d3e7b5b8d32fc8bd9ebd30d9d5230b04c9f21073dec23366748560c6fc67d741a822c54ebe373e676e515959056a805f09852a111a6beedec8c1ed6d8133e24d04fafeab7b877b71bd107db03e7a47219111a98d8967405396fa2a12e30e5c02b0235d6ab4c414d14393e6bafe123ba93f1c24fa303313ecd123af3c816a898e8c015c977550012256d83c8f96da6129de1b2bd857afee09a2122c264f5535ee17207e07250e756051b665c5130945b98cba09141f4175a1dbfc6fc3440f6a2692f0141a980b856131e75a7f0072305c0065b5b0b32d0a8a406f136a67d8bb51974a46cc1fcaebeede69c8f76e9cddc5c040ef380555ade3492dde6f9caa0d4c18d02106389c3334dd85e21e5d1198bd95e32a97fcb59c85de999cb078d9d2982408cae15ae6fe9fa95b4b5443e8d6effcf4b3e903f021f42644d1605bd52861a520f413bcf9c5fba40195bd534ced9088357fc3155c7606ec5b857eed52106d4472dfd4220c46c88a2e444b0f0ffee688cd472ff90304853bacbe594e7e9eec897b6f6fb0757a658f76ac8a36f995f228db232ff365c842a95ce4dedeccc7d6adcc66f9723b413de9f410bb3a62b338b3bae5e9115fa66d581a5d459ec0bcb09a013e63303e89073cb8473896915f1593c9a62c060a64c8d164cae46388ce6751a7d11e1246af906e1fac401016bc7b8ba49be563af3fe509c1efc2771bb2fab8827c60cb79de9a1ad7485cbf6f145c4117a93b2f476300c403a1fbc48af19666556697857a6422675ee39868e4d694b538a5d90a741829ea1eb7f16a4cb74dd92eb71cfa84b5ed5eef1f5a52232b53d8791b48fb98cb0bf41a881f11d0d202a40d5031ac03961c345750b8d0846f28f76e663b931c0aed8b5dab097d144826ebe16b8e4362868b4b105ae28e4c458982b0861c0c5ee362ca4a714dff9ce082629e21c612546e4cc50fc78dec5ff9bb6cfb7477499e90b3d799fcd26bc460448ae038f4d7dfae99e1ff63d34285a15592955e50691548ec56edf4493f54e90abe856c933b365953c683767622c31bd2b18c50eab5e1e8ae29cba3dbad2199a4fedeac65f4fcb0094524ef23a1d0001147cf902c092e82eb5e2b80340ca64adda9d479af18673875eb661b3f085a016b7fd21bee7789755ddb38555338d36bf6277947fc8debe80d3ca65934882e6b03ae5426dcbfb40efe0de27d5dd33ffe91d8151327b8d49232495a021c7d52b1a70810d270f473b9c0a9481744b0b51ba5a3b7f1f8f86c2f23da41f61c2bb60b545bafb0f7294aa9867e2d1c2c5a1b4bcad31373e8fcd29118f455d0334c9db169a438fc9ef021aac8d8efcedd2fa51750706d4a4fb95d7bb5d91e86d6b8b10b87bd82aaa18c2074e54f9ebfd46c1659a988015c86b7ce187b83fa89e27ba3e91b6e1c1bad902723f0f1a05a9d016e0e6b587ac9b3f1c9985d1ab1975a5427bf34b812e87130b44812cd2e4c5dffc0752c13c523bfc86baaf12a256141f12f33a727abe44c948d58aa76f91df64f063c36b9a6d04dcdf7201dd7c3ffa1d45eaee5ed72b85dc589bf8147ef5fef4ae4b949c2f9add64cc0b45b05723ee3701165b4a5eb8a442dc0af576512b0272338702afaaf13c3fdcaf4854a010733c4c56d1abfabb061cc40b0dab4b6c5861fffe5e3b8294d393a368c38196036b257f70d227b32140708516a1b97c9c50ddba5ab246eaacb193129cff1a51ba69b4744b712ad3477c6fe36a940b6aa5ced1615fd0f9e25ae89812eb682b8a7a376c1e8d08e589cd9e3d29b25ac51e3e8e3ce7d4b3f8b7fe1ac105c7ee26e3f467cddfd77a5da729943a14ce537cf7f54170293996834a4206521a03da2e14bf28ff11ec8288912d13a08631e01278eb5fd631c7ed1f2a2c72f22f7d7b96cb27c27da9ee304f70753287d4e288e53b2781b8dc405379154da26904290850422ff4b396a1352ebe08c7136efe738fe6a38d39b4275127b9f230f437cea7df223b3b31bbd88aeaae1f7ab5e93d39b7f4efc5f2bcb6fafa8ab485f9c121cf9526fca23f3213c5ac138190c9458236557618eb79c0227678f9fa5557a9c214530bf6b858cdb46cb259092425ce63de420298a7ec7ee62b8b5eb61a3df4a479b68e0ccf7ae1cf4908c0b5ff2f34383fbf155c94288614ae02b8f1c4d30fe332ac7a87d45bf55c9fe2fd593d6160b92795ce24cebac2ee6c2803087515b8d44c9a5953bcef88124af2e82f01e66a2ed8e3d68260ab9dfa887036954d3bb9d9b43860bd482a5c8ef96aac05909de00e73d96063256dc875c08327725ae2c71259001b49acb348cc3e2db38372a67cb2fbcb136be5597a6207e801b08430bb5c33e9d1880621a7508960cd3df0c36ccfc9d0bf417fb43239a72490b1d536beb736ee74d37e4745d15b1a860c7c43532c51621414ea153daf68fb09f0eeb8915f0e973013dffdba60c8d7bc807651ec4227c0aa4fab2b04001fa8d27b3743a75e91579551d1c45d1b47994783e5b9b79eba1245f2c779e856343fb34b15b9ef2bc2fc7db25df53f0880184b6afbd88813ca38930d58c6bc371e1e0c6729baff00def2818eaa51ba356d6baddab556c59f198695d58621ca609d187d9937bd4dbaf0a55dfe855d2c6f42df4906eec6565c283684b51f36d642cfc88841292b899f4ffbe6c7bbed34db11ba1646de299d752cfaf3ffb98c163ba8affa22b4b96221991b48a9e51dd5135042e2e90298088da604b8cf7756f6c9904a55ab157f508a028de1cfb65e81ea8065f20b7ed44c15a8687351bdf200aceeadd5542e9e33dd85079a46b54ee44712d8cdf2b7a672d1607171a9c00221cb04c256f0a355d7c98cecc9035ad45032fa6a053333e419250f4b57a026d72b341fd90c4ee43808c70569058a7f391962f39158d84d0c684fa4213551d76fca21d56836a64eb638b1a1b8b9f9d22b2bcaf3bafd6d4ac633dff5556f622ee96f4512aade6f336de2ceafa3efe78d20a4e5a66549a503a44fcc91704b5e865f451fafd8ecddda1d09315f2f2938df845c1f4bbc55615f1d3fff74cd263b6a60b5ca1ed8fdc3e1d69f792ed688d95cd679873ce958458990f2aff5dc86292744cb698d93a37da64965572f663cd4b2203a80685bfb55936bba99b9a6c2e0d6cad1e84b6b62632110dce3b4995f481cdb308b9b9c13f2cee0c92f552b148eb2493c63ff3c8520c4b6660522f7d314f6e5615b81e97c4febd1ddd66df9617814b0f84324d683dbfd082d803ae2e6fbd76ef66fd9a24f1e63c3f3335a2f61554f8be4dd0cc888afa85645372da0ee6d6730d32e5b5c7637564e7590d73f3a86d5a7cfc3fb0079faec67f8f6891d70936993d0b3dd6a385c98b45830b3ec76864347b3fbacc90779cc5fde43af74b5c327502fc6e2f0c4e0533d4d30aeca71273a54aeb815699d38781fdb6871992591104a2617fe6dfb5c628d95d1f6caa29194305f5fe15cfc76d1d18509fba3a09c208aada3edb8fbfadcec634f30866727260bdbd3bfd751700bf9f78a3e896ad23db343877e7ca7b8f0af80cd97ce764ef93c8f5eeae86cbfc196c4137bd0d5134888ec2247e62297928ffe4105c1e40d865264d1d046ae2e785dc369daa550c4861ef870913916694bd0baf4257662b413356ea72cfba8872eb522c6e1f7db5604916d6ecf9d4c8f74999f45af1b486a886c8cf6cc7b8cb8de8b6f9a7e1a6d445a1a0cd69cc3047f4a49a78cb9f0af584d2508c1708c912de3e9567c2d93a5dfa595199aeea8e9600e9193bec7f61b7bc5032b59c653f654b1a2385b5f11266ae2b7cc9ee7154a2ec5b82be245f37aa82b7af41d11e827f965f7819937d2cfafb34e2d67f8afcd63a49c68c2882937378e7d5b1d89bb72f4eb341ba6feac61fab677d42feb8899f763db4c1dba15014c852edeae1ebb9218b36fc465a4f29d6bad08821f7838777559953dfb9b80a164de1f0b757f193be8a8572231c799bffa981a89afde411e7789a567a462d7d0fcd351695f09bbbc46fd96fe4b875135110c740062992867616e920a078148dee0d87a47161c84de9b4061480cf6c184bdfd8b2dfc0d668287d7206b52ea84abe8692c9bda5318fddcc59555cc856fa88795cf981f987fe6a2ec5382bc9bfd9e5c61a9bd29d6c30b72509911980ed85e5f1a2baf1d630b59ed80eae8b7bb8811c1fb43115bf0255a5608345e00a401b82938c5def0a553b921d4a33998dab73aef8dcc94a84629876be7ca1222bb03623c2a684f95acd7cf0829ecd1cb1323a3c2837476b9563f9d3deef4898bc50b48d062eb55f77fba9b929805a612b004e4d67ed7dc3db2de2ff23e696b525334e3645015e3a5926ba734e3a615c037d5795d180fe269ede800d6d505c92b08a35331e49d8e91fd41fadad67480489a1fc165ff593700bab7b98daaf6f47dae85c36e267e8517f7992517da7f35c4a741128b08484d34d9468fe66977bfd06500e286cf246d65a8121be3a6392ea1c011f110882c2c2b6ea8ac13858864fb1d8ec8d3b00238bdf7f17b0e1719b13642424337a35e188503dc4a5439876e201158a082c2035e2c39cac23abf0966a44c659bff7580e063fc565db8d0b319afc7e16062b3bf5f840cbaa07b589d0a68dfcfe75b78d67b2a76e97dcd72a5450dad5ee69b61e4e7717afe68e92d03f0d992a3568590d028d1b4382644b383756541dd7562216671b936ba8fcb01243f03ed4b540fd2a3ab2834583419899b72f5912b4a77e39b638db313eac03b17de82f4576f1578762b83948a7f86ec118a1cfb545d3bbeb33b9f54e2171b880fa9f4a312b721cea7d58b8626cf3ec13c1f1e9d0ad59682400d18beb96b512e5f6d5564926565afe7586162180f6dffbce79600eeb81cfc7465c5820d40993e3b1cc69cb79ee6342dde0c93b89a4b0b57e907ecbcef47f5332fc0fe265d7f4a813cde04f2e32f24b541f6d0f28ba53d632a9317bca045afb660ff41986b421ec5693a0b8c78279f636118e618f192330ecf34f533d3af8f6529bcf5919cb2d4222e45b546ad303f27f655f4d93a867fd929cba132a460b7063146633826809f051392d0c9362408c6e21182108e72538a04b6bf8c80745b3d1446e15b80c26cd25fca5a2f06e2afa0b73041d5e020d8114aed6dcfa8f3692aa08fae2cdc0fa49630bc4520b1c70dc4ff22b9ab64d589a1c8b9d9b15a87c8e1514f7f328db92b0ff5b1b56a95c136b0302697309bc92a0dde9f69db9527ee1d5b42aa2ae05d31d5f84ae266219c8ad8de036bec43d3b65b326645430abf28f3a578167c2b2abe0f20eaad0cc78735bfc0159598070ec2ee2a448a52492bedc6f5743cdaac00a5e0b493a82f16ded5a7760b4ba7db565a96aaa2629caed538d23999286b0395a59b6a32419413b3e3ef5e52c504faae2f54f3801389ff2aba485af2c46cd1218d94814d46dc6733590be1e559da9e71f45a60faffa607d39b790a4c14545b18f6aa9f38f9149451273ddf464501dfd67a55c0f95b9bcb7d378d523c52d80b1066ef5e51baa34bc5575589586ff31c8beec4473cf78072ad7bbb3191abc15c701d3d8f390e91dd0d99d02dcc7129358895ef6b31544fa071e56b8f9bbd74e023b6358f4c7452bc6c22400029f53fff4819f986c287fc4f26c8f071565faa3e1da2b6438745512949996c88b54e39c0dc72f389f7aac0e69158e10906f9ad41db237c507b8e7b0f24f1815c23ed0a9b2da7618196b0b6f50bf8a9b941112b2264709ed2797791d63ac0ea0257aa37a8e3353a2dfb0e90000ea384459a465b6cff2f0671046bac98042c3b9ea9f4432808c6354ab0adf0063448e7d9cc9bdc53963d183411dbfb7209b4ece4b6250a5861364347b4f4bad8de949d5413dd4651071db6eb77c6fa730d312754f3541ccd81c29f24464cc4d370fc6504e6710902538eb75fbad327d6e74c90b2b346dfeb9234522f5e4735259222088b92e1402b4b9e9a2d5ddbd6aab5e47c8abd3f7905d52a832d701e44f8c03b85410206cb5432fdc6b925182f119663f312ec175806c6ade1fa07ae9a43cfa2f7495775ca947d66c3faa0a087620f78cbeaff99955f762e8d82a2ec20c2a5282a87f716c96a5d6a5de2043713e197399bfcaafdaa75d5579ec6514708e60cd6c7a52b9ff57efbe70ab973a5a5ddb7e23cbf19906de5ece03fd1b7c58436ae574bb51b467ac65a0b5684c4d625a3776b10590fac1a9ce8fbabac4bbc369f58492a4734bc57644fb4ff3bdf090fd4db40afb5fd39d8419b3db20b090e766ef4d24fd28a698e12bd94c1961a6d0d26404865e920b5a08dcbb9dda04391b4bb9c4262522139195ace73343bcd2ab056167ed0ff0b98299dec424970d87d325a2bf52cd3ad2641e3459996dcfa0003dd17de89f1c457dac80e3beed0ce574491848762e1163e6d3fc7e876f0209fe6cd101e45b99f7943cab69f6186d9bf6d8e520d7539763f5aa2fb4e43491e6c2142608dbed4c91fc1db473b3576b8e3c1fff7432112e99988ca7a6831a9242eb3b411f03d215e1748b4d15d8188a7e631acc38d59c3d144c1335816a22e0cb5ba99d7be75310a893bb59dff8fc1689c64088723aebe4da7da792ac20e8ff5d4dad89a5ad69f29f596c73bc3a4313eb3bc25312e255f4c895ace26cca38bdf7afa9698ad4a410dadfa286aeb8d5f10c467b6df332127c682b989308b9ac6d77866cf9c9cab9c7148338a4ce3b9e39ebc4d7e1dd5abcd5ecdbb25dcd41a74eb40afa638f6ee1ebbaa31598d5e533147dbed0f4121b9759f32444935800ff21b23f1a4f53daf22d45e680637bc82eaff186540ffbd82831965377fc5374ecc057fab5b606d45df49d1e46b7ea15c42b77740348d3deba6eb02784f09443e251be7beec1728db61c8e6b7a46d9f61c25854c8cc8262afd14a20a37de4cd8bfec05d5a3d603b3f0665c5fa21be03054cd70773fe92364fbc2776b591a279fe000bc52ff0de9b5995ffefa0ab11324ecb4d7848761bf855fd8149a896c9ba8263cc27a90ebcd2f9480cba5ff5d4726cacbbb55562692c74645a2df6e0c965bcc907a7a243e5e6699af637fb252ad071c47e2d5f5538c4d4301c9c176bff0abea559b6c42bf1d0ce04873b99350605bad00e0577967414820a8e5f5b2316d868c9dd5cfd40b2ef35ca7bfec53aeb32a94dd9341b8d629200518776874aae6c8e3281196458645fd97f223da99034b4b3324bcd1a4c51538d363eb2f3b46713831f58fc5411939b6bfde6e0cdc72417090fb4e0905831ba58fd8ea4ea0b9039757e5c83bf635977770bdc802554b54ecdca275dbd921aeeefc84c934f19960f7125a926ef85e30f87b045c2faa23639860a5611d3b4b88deb464d8ae37d53c0125892e226024fb7b1282819f282531dd36ba9f1d6abdba037acf1f43a3fc65f793a858991289a2d00417f8b09fec15c7c0a3d863e583069b93a5b0789150ecaa8be5d54cb88625487eda5dc5996a95c2e15a46fe4ac3ffaee66c0cdb16bbc7ef90894bc427aedc5beb2144c02f6fa9ed8d42a51229eabab54ba7792dadbc0f11b73c18fe5a15610f4530e54610104d7b8ccc14108b8fc4a19a0573250200b070f41a8e5fea8826f0173d8965adcbe90d3810e2e35e9ab038f2631bbcc18af94177a0f1c0af02566971af4ffd6f5bff94a3d5548f9fd9ed9bdee151a4a3f6202a9271fcb4635dd915e0a512df7ca7d60047cb6dc5c128df391875ef3b09701ce7165c9798f2604fd25eb078f2992ce671cadd4313054d80462090290e1a62164dc5ac96f45c0f12654c797f6269a37b12ec4e3b1e5e4d9e5dd17f5262ab1a7ebb873f035aa268ef9e374d6521aae6d7af2d6d65d836e79d6461c5aafe2c91ec35374a038b9ec89748c1693122ac6028d391d6f5143b74b842f53493d5bf6956d26a33c63c9b10994034a36001ddde26e5b8b6c1b8ee76bb94748b1fbe8d2e08fcfc31e2fc459f518282a609c1f97296185ae280c4318137bc411b8d097c40af0a11c2685e1ea3b37f425bb4c78766a235d219531d2cacfc12404ce3de0ddd4712f84671acae9bb139816bbcffedc1a6c273f4d86457af7aff6132e94cb4165e90b16491df1e67ffc0fca1c227a10be9c46447cfd0005a0153fb3b535858ee2cce73ec76ee4aecb5b73cfe4565a1a0ed1f5d582b1e7d244a413e93f5ada361a58443fbe0edc99c1ee3368c7583b70c21bab896491c86aea126b6779294ad1b36fa07ea2b780d4a19c5b0ecef284d444537bbe741c4cfd6987ce20f6c6765ee90d38fc7f2d587d3a40cfd79f312ac44ac734adcd418741d2bb793a468ae43c854ba982e9f5667a488e40b4aeba0eaf81e17b0fd0c76a3326591ce94be7290f30bb23307dc78d6b69975455d6cd925b09bfcbb43a591f9b509508138f6f04aa7680111ea6a280195cf3af9ca06958cc7954db0c53545f473207520acf19153c1219c25b14bd52d9e8ef2efe8fd44716de55f4f3316970ce80076bbc4ca37b93ae358389c5b72f0eaaabd6b500ce63e26fea094dc76e4e7efc0adac93388f5b501f572b9b482d6586a9f9a678fd5f90491eb94dc7359bcb27308b74bfd4541e13c76c29ed6b425beb4a2b68ceeb49503fc4226e980d0fdc1dcaa97f962b2dae3fa4628e1161be82d56b87234ed079daf0cdb95b8eff084f5dc693ab6bc906befb0323eac3974cea284647cf84c3398d1289a4c798daf996d8a1a77b0e326c8241fdc35aae4112f0d306691b533fea134585d93f86239d4aec0907c0476779b9f0255f7ba0c48463c0728904f3f49da65745ba82c6d43a1f69fe0a3939a9dbd0a1a326c8f122467f7a474a1f34db3eae662385e671c45d9d764ee7480f9951044ba58d7202bc749b80f7560df0636cb567dde48d4a40e05b06a02492da43408c7eee67e830692bd56dfa83f2a83925fab3341fd88abc0bc5c912ffd3e989a4809410bc7a88a586a81bf6b7790acd86b4afb10c88cbadcde886424d5bea51773cdd5e3e5cd4b48e61f50fff2d9633fb0ab1dd76df3de572066b9659b18c94c682993a1b63577a8591032e346201c08c821c1515ca4521bdbaf1be924092eadeeb685fec9b2e94e5e7a8129068000c508d07d1059ecb2c44cbc0b5c35e209d04ebde92d82388a6a40bfa63e6d9e185c09857aa0d56047e118771a7801f72d31d91b2a9213d9211a49b49273ce519ac51236030d485f42f08ac720bc1c29c97648784d183cd4cba06b51f55eff0bed608b9a9c05f21e9c6243ef9f294fb23b41028bb9e41fc3e9564916659b5f890ef7a3e49f22981e008a83210566c9a1b6d29a341a12d947601bbbe20e8a2ff2f4bfbb0b7856b4a330eeea7e24643a853884ffeaa0c104d27a4b31f9082506eccfa428d821731e2e62f7af51165e0665b07e26048e6fcbe046bded2bf5d295b29d3b59f119d4b16a8aaa926fa044a9c4caf678041708cf383e5c494f9285471a32e0ec1eb9d363305d9b6645f719d6209d1a7cbb8facb0e40f305ba618c4d36a07289a0e44c957ae861fca29399d576c3a35c1e587381078a7b1ef99155dfd7f981d352b88ccb039d344f5d04f0cec3e14906aa1fed6c5a05853ea70c1dd06aa50266b8f687478e1e2e68a029024fa4a8072a586e336f9b47b947d7594c284a42daa1b2da26091c61f80cb82f7f22b3aaeff341999d1b8af2dc496bb674de530041925e34b039a70a5bd4df33e99cafed534296c8bfc593cd48eaa757e57fb8483e42d98335571272fd84eb0bdd4dc77e4e78d631b7b61c769809bbaba76bc6d5f38884569b2fa542b71054a3f4e5ae1808e8022c1f02024c6144a890b9c37aca0905745d2cd0ac292cf0f91dc20e81dea89a8be98bdde3e2898c02e52fcf8d6193b9a3c7ce2b311a4e764e3980a902f016a1a4fb41267ddc2966f82b8324f967f0b2a3cdca7835491360c23deeae74171489e78ca496d04235576266b834e5971b9fcf2c2cba6bd4803caa4675d6efe7811a90233012d1078c5f7f037e403770ab5d7e4f3171f299547cc45ad7a02c453314f5f2cc40519895f0b4d9bd6bfb70d6a69609979435c41d7dc816d9a8ef9b1ae1ac7f27a281cc93ab680cb6ccb9bef638381308afb37be762f2da5229025f2746abc1dd54732bd8b831994c669ec07b3110ec7cbc00753ed4e40fd0c7284842092aa1636963cad046c97ebc5dd3d732dc94a30b3c3ebcfe9bdf40e550cbd1d229b1907d49832d5d321903db8037743da1ac98e6357f8b65d1add1c2c37573c5ee8843fdac1a938565609f3bd5a76c92bb8dd1418fa12f74048bf1495007a60b1f015a469bbc47bfabd1fe65e6c67459a6a1aa218b747b52c3bf8b51b582332f2004875c7a0357a87b0a04ad01678b107f9a2b9f3039801e7d204462df86096aef837f6d71f9ab6a08d17e91c05de652507879b27e7dc6870c2618b4227aa50f2c3efb8c1568610e179bf578c13a162e54a59cff0e08157198b56957a89e421e9dfba696b3f42ed0b9ebf0cc70608463cc4c376120087bfae5900a5d3c9ddb097aad563b5af2f6abd112323b349d89702c313b0e4002df7a3664c188821c8c7f0571007570c6215c9bd603c53435a77da0b228642d30eb6e84eeb3d0e4564764544edafa987a78def852d72bf838b244a29c1065bf24afdf69f683ea11c7127efd9635a1aa05cc6970f9d312287950a6c4d2117cc2ae844642ffe1d2b004cdf93f2da62081f3957be5995dcd97b590a997b9a093f64db2f861659dbc80c8587044db8247ed16db8eda9bc32fe068d20790725412c66ab95ea879a373a607e9cdb5a62a693c01f7a0a4031ad8de040d28ca5448cc9982fe7304cdf6c6deb9b4782b3cdff0f8cdc00ef05bf3c6ed3258eb90b01e6ab32652c35f8133223755d7ebd73c24989e225aaa168afcde29289b4771596e885005b3d6d65c73a5509202b68f96fedfec2121326488ef60b101e8e60d149d69ede2b08782be1c7aaa9e0c5255dfcab36e5459a25d8f447f868ebfc9ce939471fc807b77eb5a00c653331872c774fb68c212c343a8bd3fc9bc52f2ecbbde2fb2ea9605c0fe6a7dc02b5cdd0f84974cade1832939d2dc0f383626e1bcc3d9193d96d8fec338617ae0c5ad75466d9050a4a7730dd61a87ca62a2143926d7ae8cef2333268d2274155e297d499f7730685c2fd9c863b97c7a3dd01fc18fc3bf42354fe6246d6b78987dcfbd06269081f8975c515d351b1cb8cbfa6c43925d51666a279ce13bc79b6136f195cddc998a18d1f0ad5c9997dbf8c0643280a775a829e7138b5051207e1b2e57f680f82cb3a3484bdf7626dd3267bde824c3ee7995f6f8cff35e71de6c612a06dbe344fc8745adee861dd7959c2b5b3aed5fbc22e65bba308932dc5ee90071ad7b920c5698683fbe7de1e980303df957881b54f1b750654ea3f00aad73efd82b6084dbc646f38b2ac19269d213a0444584e7bd49a30f011f5a2f6bc158c4ffb52aba4e34b212c01368e2ceb565df8cae2dd4b2e1a30cd06e019807104d08238b53ad30e231c4f8d1440e14c21c842ff709648e78b3ea80a29f726113a19918f1f74e39f5052d63d98e66005341688f57ec438b7c6cb90d21a2a86e695ffd48f242e6f642b55194a40fbafd0198f8914c6fe5930fd152d0286ae1bd23848e27fe3446836b2d76b960caa119421fe7f4e7df092e00a7019cd58301882e878600a3970c970c33118d43d13c33af57ddb66085dbf02a9ef4d09b5acd5c6e9eb48fa140713b7987ede4fff7ee835263d99db9df706abad165ff5c56debeb5791cd5b0bb1e0477d139c24d66944093d2a5743fd72b8960b4d602836c536a942ca45a2fbd29ab9d8bd7ae7d65e64f292f26fc96d7088909420db9b303ac963c427a4d05681c4006afb48aac617368da5661c5278a30cfa002268cb3195505b2ace8c8cae1f1fd5b34186798f56ecababaf0b790781e04d08eddd1da8af98950ced6dfee3caa49a78b16ca7049ae0e378da7c9ccd216a06f98cec591879d8ef1ce2ea2d1fd01f55eb92f606a70baf40c999583829a895d74645255202e2daaa498c77f193c06e563a9b3561db8983448cd6d0c9d06a148ceb58082e362864290f38139c148c3d802a3aff13a2e9b9cd5486d89f68478ad22eb6c68f199726e4788cfc2d3ef9d4ede892a99989b5d06b31bc9b76b9374e8ba0b46723ca16bc42097b1666cb762af7d04d78547d7cc036bc211a3ac9b1ea706a2946730b67b50a34fcd135bde01515004b70a7c5c7b588bddb0984b2ba516add041585d78328cf8bc39d081727dd794b7460002fe136f0f2c4005d70a0ea40f11a74857ea18b78dc79a04552d7929b1a4a279aa6ff8cfaf1dbed8c2417f4f86adb54707366df2185f97e5b58ef83bbfa513f0dc53da5cda1b83d8449e3c7868b9a194f962a99b567743ed416fc4eeb390b729ce72db98b7e37690a4ef0578aad7d3975d57e4c982f74e2f1c7902096d2fb31f58512acbfbd480dce54b1d7b5ea1b2493d610f03d3bff2153dbf2a2a4887eb6bd335192cfaa6c3f48a6a83044dc24ca5c3324197bb0ef6c126e4ef9129b6113758c075a0e9afca077e8c92940836b71c1ab34f35a4345b96f437d735be8aef40f202f281e6bc178c65b38aa65da2e8ca183d14aef4c86cc88cb6d506882cbde320ae5a77547f52ef997c3a40f173464e1060380e5a4900e29d081cc34191945c1a7dc69a40e55ede68636c6673752e3adf9e5a217384dd240af5d1cf28b37c7559b964d41ecbdb6e70b9f209fe818c9cbee404298e4863ce600dab3716abf797ad951fa831df1a85a3752e31ec7a1cc1115732e7d537990ba7ac238d14862dc7c256d21e1af90009479a93c28c86d3077931118be2d5bd79640187ac12722e0da3e0582e86efa6e2f5f7b463049311fa0af24b043167b4ac57cc6d0e4058d5e46d7533be851c397c646fa0cde2220235c305ca8729affb67ef3dc3c5a7a1e0dc58e2cc5ed92e4da36072cd55244ea5a2ca061ffb1ecc83b0953fc527c2321acc14df92f9f650c581152315728da425b6e117a3f089897482a5394d363a9943e1160bf113652a510381e9c194bf52fb1986a098e17d8b8594a923b2f7beaa095ecb50ae11d94591f4f4e2332dd85695e588d1fa66611c7cc47202605027fb2512f587355f78e193332ff6b98af8b48a30ed56b498c2676e06921ec6bd322d00064e5d2417fe5f7af871773a80ce8234b5a2b178be151bc79776191bab61a480874edd3740945fc8da656cbfd22d5bae2ca28cedc420876445dc02d3d5d987e7389a972ae7a8aad24c77e97bda20aee0ef68b1014afc1533422728108eb6c21417784ff97bf88493404aa3d485d4d738ccbf5f99188b4723b13739ca1b158341cc62a222e1e5e7fa225ad60dd8152ae44975642622aa4ed6c5615bd309d4e2c793dcfe5a089b58045307fef21892b76244db2e788693899e21348de1c7262ad54a6e9acc2308b71457b725507f175da4497be1b835d3f483eff576d8ec13aa3c57b7b11a0afd8a477b270813eea4600f6c919162e2aa2764f13335b917e13e85f71d4b5aa672389caba88cb547cc81e7cc8c31e88a64038534027af918723e9ae5e7ec32c5b68b8fe6a803b19775648faaeba5d36e7486b9c68041485e8598701748201b31e8245bddb180b17e2230954cef73d3815afabf991dfab9071fadbdfb2b153f75ce519f8f7b479e752790c64d18f32ec7ca29359c994a75206a5c23d5f341d784c1dd6bb6df3caa4ec39dee34e8b65dab6cf22e599bd08b72f686a91807284a7976ca613b5f3969ae4236027b0cc2f34018bb0485bf2222b5ee8cd9b74d60b9e4923e151e29f208268ca9b22514e8e2f8cdd1f1957d68056def464f63fb35836688bb3c444afea76c355639f3b43c0f14832b5313892bcd073da802b1c58433aff0d6d8712e375f6f9c6defc9a6d7d26e66d066809924ab0fc3ad227af43acd3f67fb9a2aab0c2a0a6399154bc988a3fe4983e841b9c79d89ad26c6a3abf912b43c16c58e23d84798856d5e802c4fadaabd41b7084d2d20a7e8a775b5b41f27f9fa7318129957b4e3b6261812c0db74f8ab21f9a810f2c2f979147c31d310b502893ea9bf3e62ad2bd6b088ae4cd3d268883918ff230a958cfb6009fde57defa7745d156795063b92a27aa598efd559db04d336414fcf059876b389ae2a9843e91f22b181dfb2c31f76fd3acdd15d3050ea7e91d4e2978899d0cb53689f687f2a2eef9191e55de8d397b21f9dcbabc170b5cf930b192780b07cbe27720dc2cb66773dd467b7715cb278beb1e3c7392fa3801248a1e848ae3136a371a172d6db3c5e4221492b011c336c47654d0923448d1905661944d58d83fcf968e6b7e09d28f957f6a7ab52b7554d938440a4107a272186f4679d31b1c1d8656f07cb452a2fec1abbd28a3efdb5bc25945563958d6f38faab7dbc0a54b44ba71db2169316f1642f111e69aa0d7126851f264a85c0451f799d8ab080b3528cdd9c1ebebd987e2d660f845313faf4e493b74d93b9839a556bfdbbc2346906e766a5465a8307f232ac0040a56dc969abc2e7519c86815a6d2481944d77175f8f8f6f666f82504f99b5a3368884d07cddc4c92e81ba68ec91ad05f3b9ed1aae4506e3974018c95665ad249f21c6882048854973c71090afb6f8222df5175952b9eaed98f90886b43d36bae4bfc9c8060786309a5cb1c5a47ac26e78f9cabdd84d870041ed102553e8331c06e81fc6646c851d60fbfc4f4df5be47347ee07d3257f923b02331ef9602c566bc2e008b6bc48d9586ce9cf9d3adde5e67cda07655822e00f66b95c32676e22d0ba262d04045a3e6cfa9e2336af3301cc5e23def47e595355271d08650389c4dc7cc25993a84c62b6a95cc700f0eb83231af1bafca03da774c0d8cd4531ee4e3e4a5ef11a5c4819eac5a00fe960da5e4f50c5a9bfc92ad7686ea7d652c3ea9643bfb829bc2932c255b87171d9d3198ed794cb2887291913ae84bc27b5142ca6a2de9eb3a302ff3d8f817d8d7dc2bc9f6bca071599446cfc78cdb58837119f200d44cd16160b4afed472139c34ab8d8f76df4070bf1c520019de28f486e5516fcbb9af1a1d015987fbceae835c0fc165e35b73b9d9608963c0cdd0808cf7ff680ca3bffd60ecdc63ad7e345263253c01df836540bba7319dafcef0f63431899f1c7f58ea4b37ea3d8d4e9a5698c5437a391220707e34af9017e37867bc1ec766c6a24108ef4dbf67130d23eec4f3299a127be649f5d48b478a83d8c81ab29e91e1157327a3f6d2f36957ee21a1f6207a586f3d5a463974967c13e5a219f6d3d3e688587eb8b4d8d914cbf341378d0f25c681026c39fe65403d8d78c764458334141c46098a899d565ab8c8ab1a7464039f72beeb4f0d3b43a69ee063f31881917167fe6e884b681bfe7cc8c03f916b0bbb345ac36c1fc924decfa8ad613bcb3579fa1515ad96495625e259119149aecae93661d32db0d26b051a1cec2e16d984373c5b83ce79b28bb431097894ce01537dbb30c4ad642b7e817b4d4736321504967b70f5c4c8e74fb9c483822668362650dc143325123ce4701502e550fc9c5141534b23240e092287ced7dacac60cd97c01f2d71516c22845b39a943b11264a4f98ac5864ed4e05da850f9336eaa8aab668bce4beef982358d24edb6639d6baeb68c1c37534dcadec3666b18c8440f6eda6cd3045b7f068c193be24b54372a97391a3f143f4b96fbe4a19acb20694c3bc69c2814abe2ffac3514ab6e7371c22025fee43add653981c9fa37af6bb30f27821e4dabdd6d847dff803decc77febda719cdbc1abd4f1002b81db9eb3494f20c1c561100d6babf1e29127a681474a8deb6c44db25bd82fbe9565a1c53918545e07c12e1e9cde07243d5d6e0336134d216dd5b29709cab0057549133fb6a9e95fcdbebe08eeaa5486275870c09a205ca5c32eb7322b20b9bb81a2064ad4d9c8f14c723c97968e319b844a47e81944fcb682cd8edf1d597a4b98a3aeeb029249772b9bb57d5156acb1d1036b3ed700c9495ea285ae5a82d5c788b172eee610a6a1cddb31d9c27a77614b8939f9e014b017580cd6857531bb6d1099e2f171e4a52c3d119addb6816cef1a05f4ec5d9a662dc3d3147effe9391cfe59bda97baa89abd69ad986edf4eaed759ec0913972558a840a9b4fb87dfc5148d26340b61039c1741f87a5e4f858f03c316ac6d5a24e9e42841410adf128d25503a9c3334086a1263eed865c50b7336fdd52f76d72e2a07fec314b353af032bffea9d6a8b74a7b1d3a943c5ffb733dee16bbc179f5192264b5fcd5424a10c3c3e2fc281cca8084b9572c48c8d93e312e3806dd6e7652eb6d2a2a615c34fd018876287aefa6d40f06d321f0b0fa476f31f327a2d80e28162b14e49059b8e6b77577778f61cd60e57099e2629bfda09b44aaa624e9e85e9ce8f2bc9a8b85db28ad19c890f630ca358e4882a1485b0564ccde5028573bb92180ee736ad0fb66e5d02785564c817845bb4dca29444fd7a3b162bfd3bc01b658df1eb35bc8aaa13dd7eff92d4c66a7bebffbae5e7b52331631c0eac829d3b7722949011fa7398d973cf1e6bd863851452a7663ab3da869f6daea5b15973226a4f23daab4d66235078366a3532a1b4285c93c2d072bf1e9ad9b256a3e4bebf2ce55b46c885e78a6c9342e5cb19307b51b5428633300aaf7949570fa37aec730fb0d8e7aea7bfee1d54e13370c9da2e51d7a81c67556a9221175a975dca9728668573e1bf6f519554def0d12cb3a1495f36320cdeacf75cd9f6321c963e372b5b65e7650f158488dfbbd40dcd438d824fdd1ef62318614b64480a822c21197080d90be39520d1b5b59ca1f555d683174371776d7ee08a99679ef2f2a55af7d4e77e08b701ea0c9faf28ebb801067cbc708e951f2621385762f00df797e802f14366ed9aabbed3b5ee3ec0b2b74b566803619a6282de0bb0dbfa16faa4113f90d317ef331f2be8baebc5cc7f301068ed1e7a32f514948c9219618797004b89b4aace04714072ab0e5d57f0a9d06bdd6211811eaa2bbbf76d4ee6200a8afab8c5dd92d331c123b8839cf39cc7cf457f0bf04e35928533145c0a39c562b52f0fdf8abcbffb8bfeb8215fa58f797c0348c8868c876552aa3894eb34807bc62accd2b3f30758b081298bc4c3d4c9ff927efedb828bc629ef622e2edbe178ad70532d081833ab8686f4b5a3e2f6046725d33adc021060962a6be6e410d6e651f75ae03b870a609200c11a549233164ee4b5782c1c6c88ecf8d7a96ef581a4a0c0580cc5c441962ba84ebe9a415f159aef0f78574ec2fb5f0ab295b792b89aeac2df77b3c743c62584b9a119afe81d960c43711436eda67435288f1581727586a29d9c1c98ea945c64626aa7122fc420dc9ecc80d5e36fc8d71f8311b25acd3d11d7bedfa85796d4f02a5e9476845c8806ceb18900902fba4cb1caf338330e465c966120258486c4b3e0aa32b78e88e9502b7da265e804f1f881f81fd948209ca06554af60b631991cf8225759d84d453be06e479a0f8d13186ab504e4de94986f469b97fee6cccbbb54e68336177fdbe678ba93b5bb503516474cb98e4827d20b60266439bcc65b70ed829b45b05b0d7d60da7b099dfdc3cfe5236794c2365e1f655c5e7eb736f761eaa671371687f99e213616a0445dc53ec49144a26cf8041a9ae74772089090fb27e0349b5bce7cb9d9f054e9d5e72a8d1d4581edfecca0dbbdaa3a1cc8fee8b2fb90e25467ec448fa6df4b05fdfd427825fba29d4c718203593f9259c0c593c71a00b901ab3675402f096a748b2d6d66d3b642a9c9f12691108578ea4360001376315c56b99a87f1f7d9401537052532e5b4ba0778aa98f8fe599c4aee56f9e5b882d64e33f101488e10f5502ddeb7f0bd6cdcc6be6c72412ba4e28920bd20cf3d72b805c76b6bff2895bd13150b163587a152c1e1664ec2db9956a87ddbd4471a3c1bb83583b2f93ab7e8eaae739663e45b2354d127346c08191f006b1e67d1b5a7f6714c5abc4bc3a22c5fa11d892a7f1d5fd12655145478c42216c66401172a196bf689181cf37439f738cdf0cbd738b03b7be21966a8a95626eef8de15ed1c6490392090eecfe9930c247eec4ef964d4686ba58a401dd97d1ec2a23b02919a8772acad453c899cbfb88182df74c91b6cdb5265b96b2f4c7ef9ab0635f57803b2b5dbea2d3592c70b2c4fa88a9127c780845d0e7d930565dff12e9eee8094dec44d0b7af5b4e841d5f1da0be424993eac8d405159c1168fb98fef0928ef4a94806661d37e16417de158ce9f52282af51791e78e8c2e0e935fe5151b508a612720eeabe36bfdf7c04c34bf1bd35331088697e53e6b554dc080a1739d55faec48ab7430901c5a37b5c61ab189fe3deb0f2c781d231eb57dfe457b280a2aaac141a27841a4e43973050538062513f15edf39deccb4d6c87791f0475092bd3c5e91384984a0ebedc37daf8e79c727c2f8bf8ef9a79a0c8144dfce77336ad85750e54e9e71bd7cba0ed50866c1d3bedeff85f7b1262be6d8cb2045ce905861dc5fb3b5a113b4253dcff5e9b44f94bbf79af7c82fcf096fe1ccc48a6b96cd5e7fdd3796b87f14c0a55f05e04cf890fbfd4a6388e54d97c622dcd7dd84dd9c1ca288a3fc1654f7d33d192b464c9ed3edb1610bbc7ec142f861c925ba05dbb3e1808320a66553ea320368ec8bdc5f757aa890825673e9d0e80b6b60a0eefad5bdcee3c03ee70db608d24f5e0eb1c64b6ba92f10b58be246655b984b58bf5268504801b4c1cd40c633503d87b907452c332ea16d7be122fdfcf5ffc2fbc950afe5c65f9fad7db166ea7dfe250aef83f2a4da341aead9f40d780fa4562e3e06a47a1b271027add92ffd595638952d101332245d0b94d1ca6bd726d29ae1b495f01868fb79e8505e1caeb7f8379e689add14e2f5efbc750688f4b2f76245efb8addd2458a7957f3829f87138e61370cde7ee72eeee9a0e76cfa79e6d869c3f2e33ad0ef7f6945af98c8659c936ad10c9f66a639c33965a25eb68289e473243c4a8b58132eeecf7fca073b341c6f8e51d88e955ea4b59c5993d57c0d52929d03d7d24504aa6a8865d3766ca9fb619bceac8214c4c6b9bf464493c534c5eef688bcf9538860cf14fc83c4471baf632eb4f8f7ed163fe80966de5f5e7598c495a0d62849ff611a6f308aa6e787752dafe0ea3df7826488ef9040d0b4f05c199ef9e7e2bc39740830cec27e59925a2922f89ebe65089eca31c1f366e300cbc6c239871d7df51975b91a4330850dbde552ef715793df19e8875eb8b9427ee44527a2fb8da0aa347d4305549866d16ff7c608e63781d80e5ab607ff8472bc9d315b8ec6ef1d3e027b9a177a3ff565d21eaa9931aec272136a3c70dbd2335b1bc735f93fc29dd3bba5c5d32f4868f9296436d057756530db0a65b632ed0cb64da888454c5322db6aea62ab1441c8c21bf406782e5b95ba75b6d8e2ceb6cd6c23b9a75de6936a6c3afaddddb0309ea7713a5be55522aacfbf7e07cb5f2e45081f5bf4869fcad36f09a4cd2643a0145972276f7b017c3ea656cb3263c73da1c1ea1c3908ace4a8a0f630a73f0e0fb8db2de4b85c650d9c36edc0d893a0f0bd3c354bba131bafeb8898ba1ec24974d0d5d75b82dcb02fb2f29359fb37d57fed7a9114bdde0e4ece9ee73d4317faef53babfe386399555e41f7dbb19f1b1d076a4373506414e254fd6f2985026ede5774425f82749765337f9cbb20d034778484e0f99cb08abffda6a779adf6097c61ec22ed7a2c1d028ecebf4894db684804ff822fd4efbcdaf5dbf276061faca150e8b9947bb7bd849c10fa8181551706b21421f5acda536144c6b8daf2d3e7ec8c3b5b2b858915b7fae0e188e7df5e6e90d5bdc8cfce90987e01b2bd945c8b2770beca31eb77f393bd6a334ac7a40eaae47e0a675036e591c873fd2b8a78332faead69673db15307e7e23b6eaa55b444571fd402d873e445983c501e9638039fc8c5a7368e7aac86c75fc0396a559e563e733fab6301059931aaed3d97a22000c30ea9323a9d7f000de74d1e578204ccd73f26762def3a8edfff9b5c631ae52a37f4a96346be9ffc2f2da8a9a2ac20fa7509d005a633fd76b7512194a4b8a5076b2f01809ee0543678015475ee4144772b8d2b920f52eeafccd2a7aa8e9b6a6584fe2ceb571abb8f0c9ff0117e6a3e322024718ff2b73f501c4520f1a43820e8b3ee29332275ef68aa602da6a5cc127870a773217642d56cfa1e90e433ce8f30c173f20893c005cae2e2c6e1985974cedd7ef6c07622a0a1d92bf6c1ac7cca43a0c25aeee2f17e3f99d275c6cf7c510bd34cc6a20f0273e3a89a98ebb23322d7fd1ef2c6bd3fc8e4b4e6b130b38c7b343009750608bda2ceef3f764a28fa2f00576b41d2e79d34ed312fbc5351df796f79e7625066cff9a4b1b739df8cacfa7df141ee7086b64dc342808b77f4bfa4f5f20aa1726d085cabe5c2a76a6c000118ea867928e07418600087b0c7a8765a8cf0aa1c844ca5adf6a109cbc8c33502a03b2e339a87473862c09b749af5e83bae5eb411f33b3558072891bac0ab41a2ca027ec82c00d71bab18a2f0b4c53bac438b48f6ed0084516e21bc9497adae3415d180c25b6e9286e58f8697cb83540b3f3cea1ab20159d3c5b0d4f69b888562096639e4483971c5d46631939de3320892ad2e28f567d84cb7127cb2ac86e2cbf80dfbce06b8df580d28c574a4eb82f5de6778cdd8fbc7bb79c4a3bac618c0b0abb1ead50221572d9a60333a26', + genesisString: 'Concordium Testnet Version 5', +}; diff --git a/packages/sdk/test/ci/web3Proofs.test.ts b/packages/sdk/test/ci/web3Proofs.test.ts index 15085e689..0b241ede5 100644 --- a/packages/sdk/test/ci/web3Proofs.test.ts +++ b/packages/sdk/test/ci/web3Proofs.test.ts @@ -8,48 +8,41 @@ import { import { MAX_U64 } from '../../src/constants.js'; import { AttributeKeyString, - CIS4, ConcordiumHdWallet, ContractAddress, + CredentialStatementBuilder, MAX_DATE_TIMESTAMP, MIN_DATE_TIMESTAMP, - RequestStatement, + SpecifiedCredentialStatement, StatementTypes, VerifiablePresentation, - Web3StatementBuilder, createAccountDID, createWeb3IdDID, dateToTimestampAttribute, getVerifiablePresentation, verifyAtomicStatements, - verifyPresentation, } from '../../src/index.js'; -import { - CommitmentInput, - CredentialSchemaSubject, - CredentialWithMetadata, - TimestampAttribute, - Web3IdProofRequest, -} from '../../src/web3-id/types.js'; -import { TEST_SEED_1 } from './HdWallet.test.js'; +import { CommitmentInput, CredentialSchemaSubject, TimestampAttribute } from '../../src/web3-id/types.js'; import { expectedAccountCredentialPresentation, expectedWeb3IdCredentialPresentation, } from './resources/expectedPresentation.js'; import { expectedStatementMixed } from './resources/expectedStatements.js'; +const TEST_SEED_1 = + 'efa5e27326f8fa0902e647b52449bf335b7b605adc387015ec903f41d95080eb71361cbc7fb78721dcd4f3926a337340aa1406df83332c44c1cdcfe100603860'; const GLOBAL_CONTEXT = JSON.parse(fs.readFileSync('./test/ci/resources/global.json').toString()).value; test('Generate V2 statement', () => { - const builder = new Web3StatementBuilder(); + const builder = new CredentialStatementBuilder(); const statement = builder - .addForVerifiableCredentials([ContractAddress.create(2101), ContractAddress.create(1337, 42)], (b) => + .forWeb3IdCredentials([ContractAddress.create(2101), ContractAddress.create(1337, 42)], (b) => b.addRange('b', 80n, 1237n).addMembership('c', ['aa', 'ff', 'zz']) ) - .addForVerifiableCredentials([ContractAddress.create(1338)], (b) => + .forWeb3IdCredentials([ContractAddress.create(1338)], (b) => b.addRange('a', 80n, 1237n).addNonMembership('d', ['aa', 'ff', 'zz']) ) - .addForIdentityCredentials([0, 1, 2], (b) => b.revealAttribute(AttributeKeyString.firstName)) + .forAccountCredentials([0, 1, 2], (b) => b.revealAttribute(AttributeKeyString.firstName)) .getStatements(); expect(statement).toStrictEqual(expectedStatementMixed); }); @@ -59,7 +52,7 @@ test('create Web3Id proof with account credentials', () => { values.dob = '0'; values.firstName = 'a'; - const credentialStatements: RequestStatement[] = [ + const credentialStatements: SpecifiedCredentialStatement[] = [ { id: createAccountDID( 'Testnet', @@ -138,7 +131,7 @@ test('create Web3Id proof with Web3Id Credentials', () => { graduationDate: '2010-06-01T00:00:00Z', }; - const credentialStatements: RequestStatement[] = [ + const credentialStatements: SpecifiedCredentialStatement[] = [ { id: createWeb3IdDID('Testnet', publicKey, 1n, 0n), statement: [ @@ -245,13 +238,13 @@ const schemaWithTimeStamp: CredentialSchemaSubject = { }; test('Generate statement with timestamp', () => { - const builder = new Web3StatementBuilder(); + const builder = new CredentialStatementBuilder(); const lower = new Date(); const upper = new Date(new Date().getTime() + 24 * 60 * 60 * 10000); const statement = builder - .addForVerifiableCredentials( + .forWeb3IdCredentials( [ContractAddress.create(0)], (b) => b.addRange('graduationDate', lower, upper), schemaWithTimeStamp @@ -266,13 +259,13 @@ test('Generate statement with timestamp', () => { }); test('Generate statement with timestamp fails if not timestamp attribute', () => { - const builder = new Web3StatementBuilder(); + const builder = new CredentialStatementBuilder(); const lower = new Date(); const upper = new Date(new Date().getTime() + 24 * 60 * 60 * 10000); expect(() => - builder.addForVerifiableCredentials( + builder.forWeb3IdCredentials( [ContractAddress.create(0)], (b) => b @@ -579,177 +572,3 @@ test('A timestamp not in set statement with valid items succeeds', () => { expect(verifyAtomicStatements([statement], schemaWithTimeStamp)).toBeTruthy(); }); - -const TESTNET_PRESENTATION = VerifiablePresentation.fromString(`{ - "presentationContext": "d7bce30c25cad255a30b8bc72a7d4ee654d2d1e0fa4342fde1e453c034e9afa7", - "proof": { - "created": "2024-05-10T06:57:45.005Z", - "proofValue": [ - "08a6bc326070d685fcd11f9ccf81a50c75f76a787bdf3dc1e9246c46783e249f2e539338ac5d1c511fb98e6e14b5174f19e3dbf9a477398868d7d74482ad9f05" - ], - "type": "ConcordiumWeakLinkingProofV1" - }, - "type": "VerifiablePresentation", - "verifiableCredential": [ - { - "credentialSubject": { - "id": "did:ccd:testnet:pkc:70159fe625e369b05c09294692088174dbc5df15b78ebd6722e5cac6f6b93052", - "proof": { - "commitments": { - "commitments": { - "userId": "8b98cc0f223b69c63d10ca4edd2021c83a092893407652ad095aa65e4ffcbd9738448f64c3b0a62f7e7adfa57ba7e641", - "username": "8ae284303ca77dda87cc17bed0b6b4aaba28fe6f1783909a22d887624f3502a42affd545a7cf15f5c3c06f6734b158d5" - }, - "signature": "a32b1e81611650cebfcb1da1cb08ddadf817f33bef7379ec8cf694cf36fc585315d96f7c8d0efe62cd7f69ec6f824fa421a81747f3bdf9a61af9d15bcfdbba0f" - }, - "created": "2024-05-10T06:57:45.001Z", - "proofValue": [ - { - "attribute": "6521470895", - "proof": "d72775bcd3a7aad3e8cd021c913acd21bb171d8e20e6252f99bbca7c39724ab02ea90dd4b8d4795c64ea7490a37426cca7088698f22964b84f35cb80fabc6a2b", - "type": "RevealAttribute" - }, - { - "attribute": "sorenbz", - "proof": "2e7e599dfa5eba3ed58dd8bb98f1bcd3f390059a6426a757cb079c0594dca86428191e4f60ed20bb4c498750af17ca621e1401c0ecd195f5ee4ecc52cc9f6dc5", - "type": "RevealAttribute" - } - ], - "type": "ConcordiumZKProofV3" - }, - "statement": [ - { - "attributeTag": "userId", - "type": "RevealAttribute" - }, - { - "attributeTag": "username", - "type": "RevealAttribute" - } - ] - }, - "issuer": "did:ccd:testnet:sci:6260:0/issuer", - "type": [ - "ConcordiumVerifiableCredential", - "SoMeCredential", - "VerifiableCredential" - ] - }, - { - "credentialSubject": { - "id": "did:ccd:testnet:cred:9549a7e0894fe888a68019e31db5a99a21c9b14cca0513934e9951057c96434a86dd54f90ff2bf18b99dad5ec64d7563", - "proof": { - "created": "2024-05-10T06:57:45.005Z", - "proofValue": [ - { - "attribute": "John", - "proof": "3008e5d80469197adb9537cd6f0390351b9151fb3be57cfeceafdb9969d88da647141e3e24d1d9c821559d63aec12195512b4b2704460e152a3887828b77744c", - "type": "RevealAttribute" - }, - { - "attribute": "Doe", - "proof": "91cebc7d0466b5c569b1c014f86f82cd3a637aa8debeaaf95a8a098cc3d585c43881998a252288761127325278ac275a77b844a3775edc159cf2d90cf0c96a71", - "type": "RevealAttribute" - } - ], - "type": "ConcordiumZKProofV3" - }, - "statement": [ - { - "attributeTag": "firstName", - "type": "RevealAttribute" - }, - { - "attributeTag": "lastName", - "type": "RevealAttribute" - } - ] - }, - "issuer": "did:ccd:testnet:idp:0", - "type": [ - "VerifiableCredential", - "ConcordiumVerifiableCredential" - ] - } - ] -}`); - -const TESTNET_PRESENTATION_DATA: CredentialWithMetadata[] = [ - { - status: CIS4.CredentialStatus.Active, - inputs: { - type: 'web3', - issuerPk: '400d2cd6bc50a29168c02d4e7897a3659d8874f3f2cb349dbbeff37cc58469c1', - }, - }, - { - status: CIS4.CredentialStatus.Expired, - inputs: { - type: 'account', - commitments: { - firstName: - '89be3003492dadd443a25fa497d0501390639e275c9ede84b7d6124c839540a00a5a5bb7c9ca73a69021e3fe97eefd63', - lastName: - '99b1173bdfb4b2c807cbfc5007f91d535cba365d299fc177d355df1f5800ff40a74ca1a2567dcd00b061309599f91dd9', - sex: 'a145554049184f6112d3e4a3fa44ed6bda56445a574aa780a06d09882d8519fe8ceb39b33ac8f6303b610a80c8d1c50a', - dob: 'a7d8bb1eb0afb6cf2f391d29b563ed4e7ad9f029b55a159abe37414ad667ed471be3991eb0bfc405d391828301671853', - countryOfResidence: - '82321ee934060686b4cdfd3718deb45840417dce240da171a39141f709f5c4a6645a3fbf266eef25e7a8237931a24727', - nationality: - 'a2b1c1f0422a5904c66f31bdb6bcdb736f4ed0267d165b3df76533ffd4b5d7edaff63b6c4fdc81db0c090322f5107a8e', - idDocType: - 'b244ed7528c11bc110a9641e505e28002b437ca9df4fa6a2072d7eedca40a9a0a835791a486e8ffc3d6bab60bc0a4066', - idDocNo: - 'b796dce42bce7a0591971fa5e1def8bfe6516b7a9806f5b0287df73ba59d31faaf32e38ddb31fd71b763236cdc47210b', - idDocIssuer: - '8c3d2810ad35a5405e191b2bd5719cb03f1b459a9f8c52d6b4fde84cd7c90b52753e025d245718170438be5ac533e201', - idDocIssuedAt: - '84bdce8c8bc7c867e114468d9fca2a9eee3dfabd4a266a2944d23b2c19f051e894e4e53661d74a7e0ead052be3e838ad', - idDocExpiresAt: - '97e13c07f3e3959d2eae8c74ce95c3f677545b1eb25fe71924c9381426abfac37ce3dd82096b78aa9791695c87992594', - nationalIdNo: - '945a97cac996d42cb1a3548971b2a4c88bcd7683668a51854dbc9e3e4f655cba06b51c29fe6e5077f60eccbe32eea0d0', - taxIdNo: - '81d427defe8fc313590fa3126c211f9c1614f574150be3e279c45a4a5a98ac4c9097158534021ccb28718d0696bf6f48', - }, - }, - }, -]; - -const TESTNET_GLOBAL_CONTEXT = { - onChainCommitmentKey: - 'b14cbfe44a02c6b1f78711176d5f437295367aa4f2a8c2551ee10d25a03adc69d61a332a058971919dad7312e1fc94c5a8d45e64b6f917c540eee16c970c3d4b7f3caf48a7746284878e2ace21c82ea44bf84609834625be1f309988ac523fac', - bulletproofGenerators: - '0000010098855a650637f2086157d32536f646b758a3c45a2f299a4dad7ea3dbd1c4cfb4ba42aca5461f8e45aab9112984572cdf8ba9381c58d98d196b1c03e149ec0c8de86098f25d23d32288c695dc7ae015f6506b1c74c218f080aaadaf25bb8f31539510c87e8fed74e784b63b88afba4953cacc94bceb060f2ad22e555cffe6f0131c027429826dd3a4358fd75a06e8f7c5878791f70384a7f3a90f4b7afa45fae6e0fa7153b840f6fc37aed121d6c51225c56d1ce6bbc88096aa3f86e6b3517daa90baffc69b9eb27aaebf87f04ed091412547ec94aa7bf0c8dd826e3cdd621e11dfef6fc667c53a5abc82c163efa51435b3be74f47073511db07c1af7229c78ec38554a3f7e9fa38499201d4fe6c80e867df75f2c540e57d2e15f9f5e18f73c0a804567494aba85653f57f5f990111d680400bdbee2d0678271ce93a119c107458b8d4e557fc870de9cf0ccce6d34a216a6ffbec9581349366706377b436261200834fb2654ad5cd34f5bfe3f3658ae94838ed38033886844008143d823f7e49db43017655c53b983a883948d7401f26ed14daef0dc46da30f57e187ca8e027063174a412cbfc8a7b606b41fcf7f75b698ce9115afe5124e8d72f4b34ce839742a46885ed60af26f24f8d10d46621d78b5772b0314311ed3bb627e93bac47e7eda5ff4d2ef5f98ef7265677a382a1f7b8f9a43d1e563b66b6de94c52c3c87169bd19b6e884c6a28dd6f51ba64bc36ac07926a8d91a64e88a4e19b4c0fe0db8b76c9c99bfcddc05fd5630c54bff85c041fea34a630ffe1e6313bf039477994696db0596f9e2522e04ffdb530147780099d918ee47031db2870c00b25ba6d201b00ef2459e73f6a8219d7a7ff96833bd1c8a3c24285d93b85a321d1b6e175f2d9ef111a7304438b5f5874f064539d27bf8bc46d8e2473d458f957fa206bc417ab5b8ca4dd5f0595e21063c1bbae14f07bec6f12d95a05166b90896f33aa941e0a057989dcc1ed77430a3e6f93ab16f62598b9816dc203db6ecc7fd7989d2763d4ef72abae3c4aa1e1b9c9965f8772a1be640a18486dc0804a2efdd7175316af3220a6c10db0ac0e2913de42c44f2f54a20687d2b22c9636e0ba8d09747f2229ee6ba0fa49c4a328f2ffcc5ff462ddca0de8b1d13cf85ad590183767a9ae4179b3c617ed776cf14abad1964d362747492547359c4b65e78f6dfb452d5a559ac0c24b8affdf13256e43d3213bdda0c056172771cda382f3f31fecb82ee8dc81a48fe74467665c4e34e07b1954cae4ee97270b94a336779d27646348ffa0eb52663aa60bd8b7fb9311d38a7fc1f0f2842081cb81b594dfcdefb44db1caeeac7527c5a48e8a8ecb83de0514ef184f2a82c4110f9f64ea655fb42218f95e27c81a0adb3fa7b441b71cb15113aef7c318d73f911688d34111928fdff181dbc014be35cc7e3e5bd75bf767686933931bab272eac577558b1314206d97611152ae82c1bd56ab92a4378dbe73e76a9fbd90154bd1a8c73c681f70298d15f1574d7038521b4e9b9cfc0d900c2d74cad594b68923607a1b913cb9484e093ab8ef2020ddbbd77d041581487f41e2dea13832e566325f4fb0f9934df94ee068c676e475251659770b93e0e32c7739d3c430db581308817ba091c56a5087c6248ee79baddc3c233e77b3fdbbd73f1a4298a8277055ee32a96b7e83f76e0070ea65c4a1784e9e281fa70fd9771c762a91c7ea014f60c87d09687768c9d0c8277d84e8af2eee505fda2967631a999619dc332d98bd45d40458923de655769aac754bf9081dbe9318b4880bd325e6be713f3aafb65d8e44b9143acdc7f2ba7f15290c84225c77111e0629d0f82f2f367d2641d0c3658fd54e538286787d379252876f71ec4a96ca72fdf8b156059ec50131317b9243eb71c5e84ab6791303ee0ebacd790bb27767701083afa616413b1d5bf0347cc4199943c938d74eda87660dd40405aedafbaf54b1e177117f632deeb6a9bd5227d9b8ba5693625ac68ed7d1604ccffe73e97a45411b99c666dd5eaa90cd021d05f12f84acf60bafe0c98e8ae213fa65c189256c79afb4b3eba970a8ad4e94374dd6f9d258b8c86335f36481affdcc9eb074f1ace01fae937a7d1a92279bdfb1081e029103e48877e4442199593c1b946aaed494885b0bd9d67fb7abc6f41c0f7574c3cc007d29a4ddc0a966270f5432a47e3e5563061b158eedef8bae522cb1f800c6d5f0a85a4e24a68a9bddb99e408d204e56cd994796bd004cdc5871798df2bf068ac48271290a2b3b1f0eba346f8746c79ce2389ed3ced67bb962fcb50876b7a32afca94216adadc0aed990dfd2b558aa5d8f35aa773e710f14f1641ff7eaa9ecab43ca3dbf32e846a90c8a3771d378aeb9277ef864377f7ca79ec7bc9e4b373ac890cbcbcf7c33df157e0cab0fcc7fc4daa2599177bd9d6bfc4be694a8e834be081bc7fccee530fbf6bac5be0cd1656b4c79ba6e121c39640cda83f81d17324da795da00f9a05317224ba827cecea6cbba1459df4a5bf6b84cd009d5864d44e747e3f30866a7c261004f861359e717c414a699461086e9136e29f9973e5b3f008a77fedb184e6835bc50934178b4bbd8bc9ecbb10865789d063ee7781fb2dc7b9b641b73fb172c3aba915c420c1c72cdc999d1fd224bf4d3d8a5ce26fa36100533682964abc2e99368402b0c518df1b6af245837eadb31a5616884c5474d25f57a228ed2bbef64d5a80e37cb20385a0283072a3b7a11b68bef323ea4804608c0eed5ea213caf6feb183558d56666f472a9abd10ddbf49650c5318109f9c5ed8fac7847644e153638e29e5f45dd00da06c9b9aab4af18eda0d0bfdc48ae42efe79d4e7196ef07c4319efdf042724d9aeb2c13b089b1fde77467c32cdf778749370e2c3b84a466bdfd1aa00ca8ac85c0eb29f1ccba6953a8bf877802ca3e3c31ceba8f9ff0a8684b3b118ab11a27182b033847baa372fb6b4c88e9177b8b131cd69f6ff5ec347db17215fb9890fab1c4d265d405ca74e249fafda004f86f2a5c3fbec7cae6afe6c507e089bb2d47a9e421512c016b99cb1cb15a3b8e75f2d8ff4866731585c686871691b5ca89a1351f6a79b159530fb5825f87131b0bcee2573d3932d223a358fcf08c8d4bae92f26d7fe35255c7486484bcb26693174ec2688abf39db5f091c8c09ad804f4ae42ca8b645200f27629ece81c6b5d94ef7646cbbc4b62a716ac6d2ad21f5a7e2297ebed15f2e085859a1002a9e7658b0d0249446ce9e769caba71536b9329a5fea3a15d52344d42d868b0d92a423fa44f8a6c4aebf50fb496c62d03598de39f1fbdb29e098c6a2e2ca59cbb5e37eb021594463a72471683117b957890f8307828168b1a8f2066a5e5f2c6822589272f259b012822b8be2b15c004a2a69b66c2f5ee5d81aa9a7bbdd44d059587bea89d021c734f62828382c9f13210185482694f8b30b46111d426927ec5f10b155f825d852fd995b9c7ff27861f7fee8dd2b654473198504cdcb53154b29f323ac1adde8510beabb8cef6d1a927f2dc844f746edeef9b256ace54dd8e3219b265ae958ced6593ea154b8188bf58b72ff05c036630b6d55141c4ac227a4d966fde1d0ff1ed9748c66127feec86e7839d77c1c56b46e29d2033a7871b4b0e3c53b3cc4ac49dc8eacd0555b338265be1234b81259b3746eae71600c16b7f4bc692f0a1d3dc2ed855c9fb11b2ff5fd9e6361a82d42bd4ba00635d974fa1214aa8da9180cb158347608e0c44bb5b77397d8b233df7be91e7047d28d18ee66472b98c78091eb95760c5b381d6050779ce65fd1508af8df81a3385f185f7941f0b7aa9bcd6023c6e4db7ac3045aee58f3b2d307a5be14e2b59d05333949d9e8dafbe15b671034b7c629c1de1bf560415cf72761d4d7abcbe3b762d9a66b1b630b75c2af751f6a5d37b43e84139aeb418e0b10dec012a32095cf7305bda79bb3853c3cb1fb3c8447ee4306923b6b693020880bb11df8c78f3411f176b2393776d0b9b3ea7eb0cecfaadbcd8112c4c4ad9960001f4d4df210860470a4c89a0fb9a07229ec3476e1221f490ff80314da398ffbefac3a376c53c0db82d6785bc3d3b0cf3ce4bb54cb9b6fdc29b01bca2894debb7de518a61cc953d4f92a9c52d427e2858db633d042ca14028ef771d9c8c62014676a30ed9d5f9f51036a823a388ed0f72321a13dad31bb28fe383e456361159490942382ed3e49f1c420968877a42b7bbd8c3c9e78723017400dd178f0f89065072d7ef5f5e9baffafb2ce55fe7b35a7865957260b1789e3fddc5bebdfc3301e79b5077a92f6048121c557099c329bb270cb90ad47818398bbdddefdfa6b9d9256b8194f51b45c69176d5d7deb6440c9a81bc5ad0c8b2f31720f071a8ddb7907fb161ed658f890540ac56c506b7b98df3a45f6fc062698bc86d3cc1150db00d702b2d1e3c8eae552eab55a7afd5a5a3be2610867cfff07545c8227dd26c502039240cf00ce2e91b7bd7ed6495980639eaf919635c82c5073acdc21aa275ba87cb7280d0c6321c6a4021b1f0ae7bce23c32238b3f486ce27434721df436446f9ddfb9c66bb1f4b2337803850b2606d8954268eaa0ae060f8a6da2a0e7cc315c1f8cdad4e11bdf5165a2d72ad34f976bfc41000afff158f31e9632b898f2e9886b8b92c81bf916861c62f07ea326e94ddc4ff9af51fc0420b0d3c57fe0d14109ec90a72de74147d8ae1eeebe6b565c4ed81ac01d49b0e6a9551e9fdb7a9e040b4bcbad61fe627983462106e1c0b59ce762ce89b4b1100444048e2fe84850db19d63522d83993067cf025ddaf796e2f11d76da5a2b6ef4a409720ac4e97f7c75973584d935ab0e723f415a5622b03ece90173ea58da495f402f5ecc33526d251da5d2eb5558cbabc1634e8aced49d7c3bfb514c20f7284e8a902855b2296ae90ffb84bd0ea96aac846b6c9dd8ce92c0f9d457ab44a381046e7cd50b4958bf4b854bff340be4d41452ae1e3e19c82b99908c0a45968e580823450ffcc67561dfcdf446a57ed8d032ca94ec3e8dc0a181b9faf478275fc353ba104d5c31cb7338cc4c2cd445ffed7025f576ae84e9b08311b77490b6111dacd7e3afa920193cded42246276cb999d5d85f594352592002efa2d34c387fa1901cc0f63c0f6ed141c7d2a48d5bed031a204cca98c4e26ab7210d4ca5ba3e20df6d1b0e1daa69216b3e1ed98f20ad9f527efe75a0e7e2067c85383b7a614945669b442e1f0d7ee07160902ba9ac292fa0e9c62bc1b0e848856aa680898c4769d965441a16cec21ffc2eff717267cf0f9d4e7139b764c72826309e8e0ce8965c6c96953c3ebedb4ab4c9b35c32b7b59f8715f7c790afe8a3c7a09965192baa75262a16650cfa1706f4cdb4d34043ad1d6a53898164d049647dba7be95b7c620c750e3146155f57ccaeb283936ba6b9fe6a6c6fc5fec11b59b2d12da76075e022a2308fea5865ab5cbc12a4898de2204bb218cb6757d1d3d380b488a22e4dca9c9b1d1e2d01e7a9e707a6c09c323973dd639c7d48f25e4b5b5e95b4394a1221484d337a032b34cf77998acf78efe6d7c8c1ca4f83970bd62c0a790f38e405827a71baf31a9c89892f6745e2d29bc13b5b2873c0581df064a96bf64eecdec5504a75c7e03369662e0ad9f6c2d38c4da79b82f62aeebb69392a78a2cf6fe5a1ea8627b77d5ec828f4d445fbf3a51987023218ce4f944f03041d01976a2780fe632238a828414364b8b98aa8d709bab9c2202b144f9edaab822c33d5d8ec5f62517567711cf25c890036700b7f39b130335b4b8d14d4e9ebdb6c4fc015027907b931fe5cd99ad87976e8181fa284a53185f1e982ae24842abf28f23de420376b205222e5067a2ec53814d659485327eec748b821d70f114fae08ba181f320717dc462c88a15370cf8acba5516c093e13102d92c681bda73f3ab192976179dea08875abdc054e389b309d62a9737e05edd489469f63497da850450e1f299aea08649a9ed1982a4f8a3b251fb8cfb4737ce650d125199d07f7acbbd50dd65ab973be9684ee248269ff200e2f36118cb0eb9104b94149f9c91ebb76f9d46ef5c11b4b78bf6331116110dbe6bebb7169d3c637839bcf89b114ed9d765cd6211405e2dbbe78a4ad608a960a051097a65dc0620a3d1143439c4e04cc6eb1453e43904dc9f9db084d52b2d251dcb1d476141b74c627fbff849eda5a0abc85416b97d9888dd3cd88d33104e8f0fe03ade560dbe4baee030af797fe68e45196a2137d79de2ced8e6c2771d6b16a74b2c1044e9d381a0107adb870af01329c413c0a29b88d92b3a3c4a117424e613704917bb2aeb343dc22d96b0831db78b63063f10527cafc6731b32d50e4b6e39f45595cabd413f62117244ffc1d69122b5b1c9bd864303c46cdaa1e76a97448ba5ed266859a744411e29d1ce4b78a037d99d337a7b2f74907b591da8103613a19bfa6cc1f87b9698912b85ddb0ad0d86569f813ed4dfd3a9edfc0803427c648ef49724149e49aaaa98279b2ea7d25e3463be263b6cf518e9bfc456e993c2a3806335f13deaecd3c052e8604a08c045175d044f7544291dd4acce9db398fef80ffd0fc75b28886903d2d39c133a1869b8429c9ec58069384d8639e6d268849bc02f863f431f574ca988b84ae59427e446891cdcbed74a72e2b60601ba707b2c839d85b33320a32c1b1f7514510f70d08c474a7b6155a0ed004a23fee27c6aad072c0c751474948a1397ced8f04c5b50b4be2001f4f52ada7731781b69ed93b7602441344dedcc359c385a897532f3475764e0ed3c80959cd95d47f958d237f39694cdad607b966b0d2b690844646b614fa9fd4f169fb4de7c2ff3b4805402e4408c5ba5be95cff6bb68c79104a58c11aac0f999ec307a230eba3869f9078c453af1539a9cf5f07dd18a10c2f2104a2007d52d55b8ba7e24d97002c461c2f21796055179f403a012554b2eb503bd7f62aa3542df13ce9b520c81f364b7bea0deda55e430ff52e0f7408e43e4f7289d93b2431ef21d80e04ca57538dc17b6a7b2f1729cd52ccf7f8b4d58fb2fbaef5c9b3cb38b34b179742e686b9fcb884286cbb9b35d70083d8dc6e6e9f374d473a79ffddf8547bcfc05d5da6250ff982c8692b51e6b219406d9eb1b39dd2adc8de3dd86510da7c383041ed1e87e5104d33478bb8bd7891e16c72eb23029dfb8b90c98c15377d0928a36a65b01b4e83e5c93e5a804a37513447ca0e64a945c0807ffb5f2ff06f270e9080eb0fe471b4241a9f66235b8066d425ef09e6d46e19717747d80f5a16964690b949b335b4c20d3d15a7eabd7e815856f3463565654eaa243f7a4c619289212014d1e1effd99390d1feb0d9a9a0b101a298aefbfa703582c5c2f6470c673a180cd6bd9adab2e6d044ba3f130e47b7340ba9a358b0600d1a8862a9c330e6ef1782ef1c912a4a60db9522cd3f1b941832abdce920a6bebc950d29e057d6a71e75f9056d601079109bbbb3b7bbc46b413934b902ea7bd0ed115553de7c51ce60ca100272221a880d3d2bb0b49f5b36a246df30b7fcfa3e924b5e0db2c55038289b1e266f91d504c14cc3722881721f46d864c9f5ca7c736ade9d7196c9fc59a4740d59d0bc1cc3845ab33c8ee7e539f2bcd8d7295e88803b1a66552fbe133a585962edc24ef8b8b3ee88d005c5817c9e65249c554a678b71fa1e6dabdea92b1ffcd004e16f289ff1758a491a2ebed7c070423b4dbbd61141d3357ee08bb9be71663d0d2ea59f4322ee39348fe8af117fb424ac49e362ccdeec78e99f7f2eef955f7454ce38c8dd963bc6a43bcf476514a5951637c64a71dcb65351b05c557d7485494cbbd0c206acc04002ca1545d1ad3f70cff600aeffa021fcc63d46b5128d466c87475a6b420af9e4648b3218449886a5f3448d2993609e7055566430b2e3f3d09a22633e19a1dd0418be9f99cee8be8336f575d3a55dc090deabc6786fd0d210b7e9577d69ef79beafbb541f96c6a01c9344653d7f84cef645989c9928b1738b4fb901dc9b9c29f4e0940fadc8403262e953ed8f6d772f1040b6ed52df64f7bddcab26bd6f5575e325fc81ede2c931f46c8cac6beb889961d6b2395d711261f9ddfd6be95e17f1aafa506e7d14bddb132b0821e7b06cc8597c8133878ca983bedabf66d0d2abadf4dfa38015d3e0afe9323c792dfccda5544c17624162ef0554b5cc6d77362208018717a53f44c2bffec644467d6b2bdddfd1ac55973d05edebd6e24c41223b8beceef1ea63410eed001c947429b115b864d785de238fa0e68411e9c5de226bfee699ab981ea86a67935faeb6f571f04d6dd1b73920a4afacea51c89a5561532cdb94a08da8e0bce1da552da943efdaf4a9549984337f0f5c4b42fd81a4f545e00d86f41ac6d54eb554efeba20896f3e1234f41c285636524b666b69a21bf19318a618c95a05bd4754318f696e0671662e9b37cd2393fb709e687a8ff171155d71e6ba436857d522aad38d34382c5b9f2c8e5b94464156cfbbefb3e390a935dfce72b9dbd2197e241b68cd9ef87de8165eb0515ecf534d5bdcd2b6a1540c092e26d99985085a95e31cff50452e89a3de519a0bc9494c66e77434242ef17bb0f4719b65b10cdead9b2e9072c59d334cb43827482fb92876e6fdb66dded5245e0f4a8aa6ac7bea417f3feddba61ee20b092fda1502551fe6cd0f14023fefe4d08767a0eaae297faf7352badf5ff9e58cf4d1fecb857ac3b9e83c90d19345474f82c074da1600172969606801ec567df6dbaa2df8a4380d3d500570b8461b2cc8511e5d3a45495b3bbe014440846892b11c3248decfa722419cd30aa94d9209872872820ad1b02b8abbce3b26e31aae5bcc816795d0bb3810c1e4f6ad22ff0a00ad709f93c3f63a9dafc2eb7cc6c8d986808e165b25dbed2af7ff5620ab09b3aae0928ea163fab6315b7885448f5579c40c35ed50340bef058ab56186940cb5fadc5b1d093c1a82ddd172a4d5cf3efd5c5297db8dba62e1a302127b7faac192a1de5f8225c49607768a7561dc138b0c4d9888fe68a5e38c125dcf23c7d9066c34c7536226cf68e8106c9eecf4925e48c6fb8286c5e4d48c136550d179a30daf27017aaa890abdcab582f67f4b7117257ffefc0bb8a8b6bab8c68894955bc13c185b64b5e790483145915881643222c50fc024002a7c619ac5561bb72704c2d46745ca5ee77311a3c22750ee8cadcc77dd037db697b939ce0e8be03f83c82948d491b38289483c8da893ebd9384f83f1ce0148a43065ea4e041c22aaac15a1a82dc6688c3f73f86f97c97ca7e259cf588f8ddf2de829970a0cc527df4d9514032a4161e3175c34dc46065c3a1f4c70a07734fa9bf76a3744d2764df6fb49dd8a97d85c62890d6cc094862a34ca78558c47c263fdeb1dd5dfa9b6c0ae2b3cc43b5fb742049ab38e87f9d50ef256fe3187506f8b462ac07b85f2395089291336d8233db5631cb1afc311f6b1bda3a2b8cbeecdf46085c5f9c0727f40c64681ff0adadfbc742a8a70d717a00080277f8fb67074c98ab4cdd2dba5b9a6d3defa9b6024bb7c452f19b74dc8363cc33b62ff9d5653f7f069723f1d6d7f7fdc38980bcd01d1867c2060dbc75a615933c2830e5a39885185d74f980c733afd19a830b12dadd006530b1514b5ef61de8b5b27ca58b9471029edf360e8bc4cc962d6ab28a1dc2083c5b98510f4c22f4577b9c416b4072f36b298ee1ce8dd8111c254340123e19509c179e4b71cb268381af7290196666e055a7de2715e6cc173bfa5af642a3fa942c85b07fac2f9970bacfa03e09d36da2f80b4f1a379e8e5b43d9e2890d551e6f37df150fe561212a3b0b714d13b46765dcbbac3f6ea01c976cd366da0de6bf601eb17230e6f3032f9a476ceb75d7d25ceab9f65bf93102daed84af1c7e336daf571bae47f98f74214d93403f55adb50693c9cdc1a2144f1544bcd1e441a34a8d2a2f48387130e569a9f3053d510e2bf379df90ed2064ebd503b44a2a729a19ae1af257508dee6133bc8cb0af76690be0b41248f3f447adb4679cbc91e93532513a9cac6dd4a587ffef01c202f6e46c3129567c833b5db32fb36054cef0ed13530c48cd3bb887137f41902fde6e4e472bd6e38f6a0992d363a0470a1f8f98ee17d528a0b4b187d6b0a931f2a0b123d50827d6b50797da8029a868f97e144f7586afb6b394dec7ca89d85be8a0ecec25863eb9215704eaafab2e277c3256dffadc6a99fb3bc94b4483a3fce40f29ecbe39c223a9d99adab90973c65ec64b9b9ba9f1d68ca967d87df7793323d898fd09065da2c6051308cd4e535634e235e6284fdb3697bd285eae40d57b976d7ac690e6a96b193a9c3ab3dfd771efce5cfaf12d48abd69023d7773ae998e7433600924435bc89163b7a18f7bada60f52ced09e50dd8401198fd48ad6741f545fa1b53ddd0b55e71baa55112e01f42731148ad13f696b63e307f3f9878c74bf513fb03b3bbcedd2234f8d299286add8e0f7aa4c7511b9cd080e293b0067b4bed86869a814fe4b2a5b5f2a9eb543795c7139211a61de2d5dad8c61969a0a460552589867d544b3561b8234e7d16ef4fe6590d141e3b29a77e967caaa25bfd1eed4537e5d57372e621f4f99825f99ff7016193038ad5f975fa64497d20f2d05ba4fd2d015ce6ae8d65d536584157d0a4269f5421c308818415235d15c2063bdd645329e270c4c6e180575ba5b20e98812520a41d415df1b7acbaa29d6600b797160d9e4b7f96cffbadc623456aaf9aeaad3fbdfebaed9b7725f4c5027d7acbdf36c3aa77ba6d2f834b5b45b2e7f1538cfc109a4817c913846a4d5a479d5f29e9b377144bb76265d1747644dda7465b498b3ebc4d361ac8817071b60ee23d0803d19a9da55619b430997ebc41c59d8feda62b9e1f287634b1b8b22f415c89444c01af938c50e0fc828996a044d5dc514479fde095d6e91c2e75e74af4dadd159e4e84d9dfb8e6e4e3fffd7f9a1dfebeb14816857dd1b6e197fe33d03f1835f91d2764d3cf8eea43ed03e9d4486cf3dc90f34a4f4e7f1e6b62dec4936284054322904ec54c8e3d618b4c8fea8c5985624b3625b01a904512da4cf3a5bb5d23d99fc6f72f636e08c23d435de96b0d52f16b68932e2a03b0ae8de987e2b9a324c3c1028236ffbbc85399dd8e441ce740ab549ed1547591469251aebad65b00fd4d60abf9e3072213e3ab5ecfa9ea0c1dd98141b6f58d244a6979af521e1d1c04b2de3db33a15b7e4845046a4f749b9a08fac86c32b958079a48a13743777aa000581a58151b3bd18f5b397a786d3323bce206a91013fa451a2e8c0c56cd428497e959b728e88a9707a80230850f82388c5f653adcedfee27ef1d045703dfa12952f9ab8859c610f3a9eddcfa6f3c470b7ec1cad5c06f69a65fa5e8641eff14a5378fdb63099cffdecb4816f5fe8fe64f715b432301523d2280be3e3bb2ff3374c5fe07f1342a37fef2b46a2726d308f9e678246448528a219ddde24baf73cbf4459791f564d9fc06b22635a0580b3ff21c9d5f6ec4433fac3eaddb055714aaceb48ac87af41f00fe3482a9888afa7a0811cbecbe64b264197ae22c1e17baae62116e79948771731bce85068dfa4016f3978e39ddb6373b398939642824f763dbbfdc002af17846352ea59cffbb3b07fd526936f313a9f36c818b9c8935d3535e05a7f8ad66541a202b3770e7b5b9ee000fc03f87a67ec1668c270bb6dbb46e1f58e999887cdaf5bfd08a899afbd1b1712a474fe6bdc0e3c6540072ede5b4fd2eb4da9b93bbda506a634eae7f9b0855c40609011911e765c5bf92c7ef9876d3898d1b9f74ae44dd10de3cc31b5056f219ccb522f6c48d8e32893118f6fdef1441251f5f1ec351b2ee7b95a2ae8d08edc4ce2d5749b2f07321ea546805fc4d46c27392fb66ec9ee04c41c8f77d4920dcbe5937cb5507795210b0acb300b3a9625fc4edb076cfd36c5f8f0dd519025af2eaccbc13747e1c7a848a5deafa9caddd682356e21d7d24d7b3458f8c2351c20a864a152fffa4d049b331de77b2de9490bbede98488158108571cb80c5e3eff895ba2a31a1d70aa1bc3b38cdf5610495f432a9f9369b9ceacbf45da63cf533e42df66df7c7e108146bd107fd890e32252e5e1125db9e5e938fc86a3ad8547951fcaacb69789e99bff56fb9e0350ec742665cae725faba2dd08de44a16d5779fd5c0a3624db0bd265d933494020efe58b64b6da26b0a78377fe8d55a2576e245791efbd9c2811c66374f1d95121ba4ee1b85b637fcbad348cc8b089936520a1c19391ec9186429689919c0692b665e95450bb37c8e88306f1d367cdde245b2174277da0cd799788d6ee9ae8453adcfccc3a5dc9832cdc59946ffbce28be4a4c0630691f5e9bb4f9277df568182fe478a4eac8b6576909c6780a7bcb0ce8f7d60f89e2d76d1fbff45d6390c1ac743129754aa2b7320101e0e7da7327d6c5e545feb5a0c48c6cacaf245503f87d7b7ad7ddb24522df40f94cea9a9044ee7de7216d533e16823ee8599181975253e5841c4a88f71b04d9465eee909c6b1e220d11da5bcfd01ab009ddda154828467adfa4c49cde3991a2062e10e74da1a6535ced9fed0b6d9f4c4f507d711d8879b883c43e7c3588dfc02a3218925feabe7dfc619d110bd5455cb79ad9cea0cbb5d2220c5d83c9a2169af546b260c4eb936a74db8d1538470c73fba41010963314b1ab662230d2bf126102eca5a8e02cf3fc9c3e406110d44c483b5a1770c0c97657b251ebfc59d2db5eac1cb2715aa3c5b65c1f4eae95b700bc4815b663b61d0cc7b5bc2b8270d7cd558edfbe3086cac8eff197ed25b4ca98546735f433db5ccf9d0b01f56400f9eae1656bfdaf81137c306211011e09b15dfbae35c8df774a3bba8e40cd3d4fb00a6880e445324f26d61a612acbdd0737cae1179bf26bc29b7cb0b23a907ea011402947c0cebd8aaa82620a36ab9b6d281470d6f05a42c99b87981f7c45adcd01b2b47026eac614e0930976ba5b0ca56e81fc5277d80a9c7ab49d670ed03dd760af98a47e495a3fe92c0f97c4138958044faf55c5f4ab7833f1db1245522924609028ce1c234bae58d99e377eb82856c7a8fbfc6a21f3f2c21001badd0aab473cfdecd9a89d230ff477d43739debadf3c7d03de3987d4715ac54ccfbc271ef5796428faee43cb46feea3a73ca2acd73685d6be66e86a958a8f29ddf8a7d3f9f444b399dfab5a6e46aff3d3ae8d25a4c737e4c2bd1660b0302d2381ee4dc9980fe75e39da89af19310d61a4042f56d22fa2eab5499eb2865197ef07226c05600fd904f67feabb07a7b6f5fcd31302755a19c28f3fd8fb34849afc27a79c226e91645642229a0415ecb6279770561ea342e043baf8907a6e6a31914bfd8295ba6926bf4cfa11a369e10895d5d4ced4c426b69f6084a304804083020e812d01b0b6e6591dea9145ae40fe4fc0e820c2a3dc813bd6b1e960adb54628f1fc5b66df7e33cc9cfefcefaf14a510ecc1ee5a7bd5656a34b14d3bbd6e20034b421233e24d7d8d80b51aab12a1d55408e3679bb3c68997729b2488594ba5d8ac0deab6b93fc38ee6db89eaa263d34822bc71ca45234db23ec5a6826540781358a9f9f360b863204e81d07046a093dcb0c9bc3c0415b7c45722eee2ac88627823b78421d11f0be2d32590c2380f32d355229e34e33e44ea3ad47be9021aa521b3832b8edde4a37368ee10cc09141822d5dba7c5b50705b4af3e94e3f9ca32840146fd4f5f6910de5004fd23c3089556a0fa92e2645b9a3b9f4ac9e850a3d3e7b5b8d32fc8bd9ebd30d9d5230b04c9f21073dec23366748560c6fc67d741a822c54ebe373e676e515959056a805f09852a111a6beedec8c1ed6d8133e24d04fafeab7b877b71bd107db03e7a47219111a98d8967405396fa2a12e30e5c02b0235d6ab4c414d14393e6bafe123ba93f1c24fa303313ecd123af3c816a898e8c015c977550012256d83c8f96da6129de1b2bd857afee09a2122c264f5535ee17207e07250e756051b665c5130945b98cba09141f4175a1dbfc6fc3440f6a2692f0141a980b856131e75a7f0072305c0065b5b0b32d0a8a406f136a67d8bb51974a46cc1fcaebeede69c8f76e9cddc5c040ef380555ade3492dde6f9caa0d4c18d02106389c3334dd85e21e5d1198bd95e32a97fcb59c85de999cb078d9d2982408cae15ae6fe9fa95b4b5443e8d6effcf4b3e903f021f42644d1605bd52861a520f413bcf9c5fba40195bd534ced9088357fc3155c7606ec5b857eed52106d4472dfd4220c46c88a2e444b0f0ffee688cd472ff90304853bacbe594e7e9eec897b6f6fb0757a658f76ac8a36f995f228db232ff365c842a95ce4dedeccc7d6adcc66f9723b413de9f410bb3a62b338b3bae5e9115fa66d581a5d459ec0bcb09a013e63303e89073cb8473896915f1593c9a62c060a64c8d164cae46388ce6751a7d11e1246af906e1fac401016bc7b8ba49be563af3fe509c1efc2771bb2fab8827c60cb79de9a1ad7485cbf6f145c4117a93b2f476300c403a1fbc48af19666556697857a6422675ee39868e4d694b538a5d90a741829ea1eb7f16a4cb74dd92eb71cfa84b5ed5eef1f5a52232b53d8791b48fb98cb0bf41a881f11d0d202a40d5031ac03961c345750b8d0846f28f76e663b931c0aed8b5dab097d144826ebe16b8e4362868b4b105ae28e4c458982b0861c0c5ee362ca4a714dff9ce082629e21c612546e4cc50fc78dec5ff9bb6cfb7477499e90b3d799fcd26bc460448ae038f4d7dfae99e1ff63d34285a15592955e50691548ec56edf4493f54e90abe856c933b365953c683767622c31bd2b18c50eab5e1e8ae29cba3dbad2199a4fedeac65f4fcb0094524ef23a1d0001147cf902c092e82eb5e2b80340ca64adda9d479af18673875eb661b3f085a016b7fd21bee7789755ddb38555338d36bf6277947fc8debe80d3ca65934882e6b03ae5426dcbfb40efe0de27d5dd33ffe91d8151327b8d49232495a021c7d52b1a70810d270f473b9c0a9481744b0b51ba5a3b7f1f8f86c2f23da41f61c2bb60b545bafb0f7294aa9867e2d1c2c5a1b4bcad31373e8fcd29118f455d0334c9db169a438fc9ef021aac8d8efcedd2fa51750706d4a4fb95d7bb5d91e86d6b8b10b87bd82aaa18c2074e54f9ebfd46c1659a988015c86b7ce187b83fa89e27ba3e91b6e1c1bad902723f0f1a05a9d016e0e6b587ac9b3f1c9985d1ab1975a5427bf34b812e87130b44812cd2e4c5dffc0752c13c523bfc86baaf12a256141f12f33a727abe44c948d58aa76f91df64f063c36b9a6d04dcdf7201dd7c3ffa1d45eaee5ed72b85dc589bf8147ef5fef4ae4b949c2f9add64cc0b45b05723ee3701165b4a5eb8a442dc0af576512b0272338702afaaf13c3fdcaf4854a010733c4c56d1abfabb061cc40b0dab4b6c5861fffe5e3b8294d393a368c38196036b257f70d227b32140708516a1b97c9c50ddba5ab246eaacb193129cff1a51ba69b4744b712ad3477c6fe36a940b6aa5ced1615fd0f9e25ae89812eb682b8a7a376c1e8d08e589cd9e3d29b25ac51e3e8e3ce7d4b3f8b7fe1ac105c7ee26e3f467cddfd77a5da729943a14ce537cf7f54170293996834a4206521a03da2e14bf28ff11ec8288912d13a08631e01278eb5fd631c7ed1f2a2c72f22f7d7b96cb27c27da9ee304f70753287d4e288e53b2781b8dc405379154da26904290850422ff4b396a1352ebe08c7136efe738fe6a38d39b4275127b9f230f437cea7df223b3b31bbd88aeaae1f7ab5e93d39b7f4efc5f2bcb6fafa8ab485f9c121cf9526fca23f3213c5ac138190c9458236557618eb79c0227678f9fa5557a9c214530bf6b858cdb46cb259092425ce63de420298a7ec7ee62b8b5eb61a3df4a479b68e0ccf7ae1cf4908c0b5ff2f34383fbf155c94288614ae02b8f1c4d30fe332ac7a87d45bf55c9fe2fd593d6160b92795ce24cebac2ee6c2803087515b8d44c9a5953bcef88124af2e82f01e66a2ed8e3d68260ab9dfa887036954d3bb9d9b43860bd482a5c8ef96aac05909de00e73d96063256dc875c08327725ae2c71259001b49acb348cc3e2db38372a67cb2fbcb136be5597a6207e801b08430bb5c33e9d1880621a7508960cd3df0c36ccfc9d0bf417fb43239a72490b1d536beb736ee74d37e4745d15b1a860c7c43532c51621414ea153daf68fb09f0eeb8915f0e973013dffdba60c8d7bc807651ec4227c0aa4fab2b04001fa8d27b3743a75e91579551d1c45d1b47994783e5b9b79eba1245f2c779e856343fb34b15b9ef2bc2fc7db25df53f0880184b6afbd88813ca38930d58c6bc371e1e0c6729baff00def2818eaa51ba356d6baddab556c59f198695d58621ca609d187d9937bd4dbaf0a55dfe855d2c6f42df4906eec6565c283684b51f36d642cfc88841292b899f4ffbe6c7bbed34db11ba1646de299d752cfaf3ffb98c163ba8affa22b4b96221991b48a9e51dd5135042e2e90298088da604b8cf7756f6c9904a55ab157f508a028de1cfb65e81ea8065f20b7ed44c15a8687351bdf200aceeadd5542e9e33dd85079a46b54ee44712d8cdf2b7a672d1607171a9c00221cb04c256f0a355d7c98cecc9035ad45032fa6a053333e419250f4b57a026d72b341fd90c4ee43808c70569058a7f391962f39158d84d0c684fa4213551d76fca21d56836a64eb638b1a1b8b9f9d22b2bcaf3bafd6d4ac633dff5556f622ee96f4512aade6f336de2ceafa3efe78d20a4e5a66549a503a44fcc91704b5e865f451fafd8ecddda1d09315f2f2938df845c1f4bbc55615f1d3fff74cd263b6a60b5ca1ed8fdc3e1d69f792ed688d95cd679873ce958458990f2aff5dc86292744cb698d93a37da64965572f663cd4b2203a80685bfb55936bba99b9a6c2e0d6cad1e84b6b62632110dce3b4995f481cdb308b9b9c13f2cee0c92f552b148eb2493c63ff3c8520c4b6660522f7d314f6e5615b81e97c4febd1ddd66df9617814b0f84324d683dbfd082d803ae2e6fbd76ef66fd9a24f1e63c3f3335a2f61554f8be4dd0cc888afa85645372da0ee6d6730d32e5b5c7637564e7590d73f3a86d5a7cfc3fb0079faec67f8f6891d70936993d0b3dd6a385c98b45830b3ec76864347b3fbacc90779cc5fde43af74b5c327502fc6e2f0c4e0533d4d30aeca71273a54aeb815699d38781fdb6871992591104a2617fe6dfb5c628d95d1f6caa29194305f5fe15cfc76d1d18509fba3a09c208aada3edb8fbfadcec634f30866727260bdbd3bfd751700bf9f78a3e896ad23db343877e7ca7b8f0af80cd97ce764ef93c8f5eeae86cbfc196c4137bd0d5134888ec2247e62297928ffe4105c1e40d865264d1d046ae2e785dc369daa550c4861ef870913916694bd0baf4257662b413356ea72cfba8872eb522c6e1f7db5604916d6ecf9d4c8f74999f45af1b486a886c8cf6cc7b8cb8de8b6f9a7e1a6d445a1a0cd69cc3047f4a49a78cb9f0af584d2508c1708c912de3e9567c2d93a5dfa595199aeea8e9600e9193bec7f61b7bc5032b59c653f654b1a2385b5f11266ae2b7cc9ee7154a2ec5b82be245f37aa82b7af41d11e827f965f7819937d2cfafb34e2d67f8afcd63a49c68c2882937378e7d5b1d89bb72f4eb341ba6feac61fab677d42feb8899f763db4c1dba15014c852edeae1ebb9218b36fc465a4f29d6bad08821f7838777559953dfb9b80a164de1f0b757f193be8a8572231c799bffa981a89afde411e7789a567a462d7d0fcd351695f09bbbc46fd96fe4b875135110c740062992867616e920a078148dee0d87a47161c84de9b4061480cf6c184bdfd8b2dfc0d668287d7206b52ea84abe8692c9bda5318fddcc59555cc856fa88795cf981f987fe6a2ec5382bc9bfd9e5c61a9bd29d6c30b72509911980ed85e5f1a2baf1d630b59ed80eae8b7bb8811c1fb43115bf0255a5608345e00a401b82938c5def0a553b921d4a33998dab73aef8dcc94a84629876be7ca1222bb03623c2a684f95acd7cf0829ecd1cb1323a3c2837476b9563f9d3deef4898bc50b48d062eb55f77fba9b929805a612b004e4d67ed7dc3db2de2ff23e696b525334e3645015e3a5926ba734e3a615c037d5795d180fe269ede800d6d505c92b08a35331e49d8e91fd41fadad67480489a1fc165ff593700bab7b98daaf6f47dae85c36e267e8517f7992517da7f35c4a741128b08484d34d9468fe66977bfd06500e286cf246d65a8121be3a6392ea1c011f110882c2c2b6ea8ac13858864fb1d8ec8d3b00238bdf7f17b0e1719b13642424337a35e188503dc4a5439876e201158a082c2035e2c39cac23abf0966a44c659bff7580e063fc565db8d0b319afc7e16062b3bf5f840cbaa07b589d0a68dfcfe75b78d67b2a76e97dcd72a5450dad5ee69b61e4e7717afe68e92d03f0d992a3568590d028d1b4382644b383756541dd7562216671b936ba8fcb01243f03ed4b540fd2a3ab2834583419899b72f5912b4a77e39b638db313eac03b17de82f4576f1578762b83948a7f86ec118a1cfb545d3bbeb33b9f54e2171b880fa9f4a312b721cea7d58b8626cf3ec13c1f1e9d0ad59682400d18beb96b512e5f6d5564926565afe7586162180f6dffbce79600eeb81cfc7465c5820d40993e3b1cc69cb79ee6342dde0c93b89a4b0b57e907ecbcef47f5332fc0fe265d7f4a813cde04f2e32f24b541f6d0f28ba53d632a9317bca045afb660ff41986b421ec5693a0b8c78279f636118e618f192330ecf34f533d3af8f6529bcf5919cb2d4222e45b546ad303f27f655f4d93a867fd929cba132a460b7063146633826809f051392d0c9362408c6e21182108e72538a04b6bf8c80745b3d1446e15b80c26cd25fca5a2f06e2afa0b73041d5e020d8114aed6dcfa8f3692aa08fae2cdc0fa49630bc4520b1c70dc4ff22b9ab64d589a1c8b9d9b15a87c8e1514f7f328db92b0ff5b1b56a95c136b0302697309bc92a0dde9f69db9527ee1d5b42aa2ae05d31d5f84ae266219c8ad8de036bec43d3b65b326645430abf28f3a578167c2b2abe0f20eaad0cc78735bfc0159598070ec2ee2a448a52492bedc6f5743cdaac00a5e0b493a82f16ded5a7760b4ba7db565a96aaa2629caed538d23999286b0395a59b6a32419413b3e3ef5e52c504faae2f54f3801389ff2aba485af2c46cd1218d94814d46dc6733590be1e559da9e71f45a60faffa607d39b790a4c14545b18f6aa9f38f9149451273ddf464501dfd67a55c0f95b9bcb7d378d523c52d80b1066ef5e51baa34bc5575589586ff31c8beec4473cf78072ad7bbb3191abc15c701d3d8f390e91dd0d99d02dcc7129358895ef6b31544fa071e56b8f9bbd74e023b6358f4c7452bc6c22400029f53fff4819f986c287fc4f26c8f071565faa3e1da2b6438745512949996c88b54e39c0dc72f389f7aac0e69158e10906f9ad41db237c507b8e7b0f24f1815c23ed0a9b2da7618196b0b6f50bf8a9b941112b2264709ed2797791d63ac0ea0257aa37a8e3353a2dfb0e90000ea384459a465b6cff2f0671046bac98042c3b9ea9f4432808c6354ab0adf0063448e7d9cc9bdc53963d183411dbfb7209b4ece4b6250a5861364347b4f4bad8de949d5413dd4651071db6eb77c6fa730d312754f3541ccd81c29f24464cc4d370fc6504e6710902538eb75fbad327d6e74c90b2b346dfeb9234522f5e4735259222088b92e1402b4b9e9a2d5ddbd6aab5e47c8abd3f7905d52a832d701e44f8c03b85410206cb5432fdc6b925182f119663f312ec175806c6ade1fa07ae9a43cfa2f7495775ca947d66c3faa0a087620f78cbeaff99955f762e8d82a2ec20c2a5282a87f716c96a5d6a5de2043713e197399bfcaafdaa75d5579ec6514708e60cd6c7a52b9ff57efbe70ab973a5a5ddb7e23cbf19906de5ece03fd1b7c58436ae574bb51b467ac65a0b5684c4d625a3776b10590fac1a9ce8fbabac4bbc369f58492a4734bc57644fb4ff3bdf090fd4db40afb5fd39d8419b3db20b090e766ef4d24fd28a698e12bd94c1961a6d0d26404865e920b5a08dcbb9dda04391b4bb9c4262522139195ace73343bcd2ab056167ed0ff0b98299dec424970d87d325a2bf52cd3ad2641e3459996dcfa0003dd17de89f1c457dac80e3beed0ce574491848762e1163e6d3fc7e876f0209fe6cd101e45b99f7943cab69f6186d9bf6d8e520d7539763f5aa2fb4e43491e6c2142608dbed4c91fc1db473b3576b8e3c1fff7432112e99988ca7a6831a9242eb3b411f03d215e1748b4d15d8188a7e631acc38d59c3d144c1335816a22e0cb5ba99d7be75310a893bb59dff8fc1689c64088723aebe4da7da792ac20e8ff5d4dad89a5ad69f29f596c73bc3a4313eb3bc25312e255f4c895ace26cca38bdf7afa9698ad4a410dadfa286aeb8d5f10c467b6df332127c682b989308b9ac6d77866cf9c9cab9c7148338a4ce3b9e39ebc4d7e1dd5abcd5ecdbb25dcd41a74eb40afa638f6ee1ebbaa31598d5e533147dbed0f4121b9759f32444935800ff21b23f1a4f53daf22d45e680637bc82eaff186540ffbd82831965377fc5374ecc057fab5b606d45df49d1e46b7ea15c42b77740348d3deba6eb02784f09443e251be7beec1728db61c8e6b7a46d9f61c25854c8cc8262afd14a20a37de4cd8bfec05d5a3d603b3f0665c5fa21be03054cd70773fe92364fbc2776b591a279fe000bc52ff0de9b5995ffefa0ab11324ecb4d7848761bf855fd8149a896c9ba8263cc27a90ebcd2f9480cba5ff5d4726cacbbb55562692c74645a2df6e0c965bcc907a7a243e5e6699af637fb252ad071c47e2d5f5538c4d4301c9c176bff0abea559b6c42bf1d0ce04873b99350605bad00e0577967414820a8e5f5b2316d868c9dd5cfd40b2ef35ca7bfec53aeb32a94dd9341b8d629200518776874aae6c8e3281196458645fd97f223da99034b4b3324bcd1a4c51538d363eb2f3b46713831f58fc5411939b6bfde6e0cdc72417090fb4e0905831ba58fd8ea4ea0b9039757e5c83bf635977770bdc802554b54ecdca275dbd921aeeefc84c934f19960f7125a926ef85e30f87b045c2faa23639860a5611d3b4b88deb464d8ae37d53c0125892e226024fb7b1282819f282531dd36ba9f1d6abdba037acf1f43a3fc65f793a858991289a2d00417f8b09fec15c7c0a3d863e583069b93a5b0789150ecaa8be5d54cb88625487eda5dc5996a95c2e15a46fe4ac3ffaee66c0cdb16bbc7ef90894bc427aedc5beb2144c02f6fa9ed8d42a51229eabab54ba7792dadbc0f11b73c18fe5a15610f4530e54610104d7b8ccc14108b8fc4a19a0573250200b070f41a8e5fea8826f0173d8965adcbe90d3810e2e35e9ab038f2631bbcc18af94177a0f1c0af02566971af4ffd6f5bff94a3d5548f9fd9ed9bdee151a4a3f6202a9271fcb4635dd915e0a512df7ca7d60047cb6dc5c128df391875ef3b09701ce7165c9798f2604fd25eb078f2992ce671cadd4313054d80462090290e1a62164dc5ac96f45c0f12654c797f6269a37b12ec4e3b1e5e4d9e5dd17f5262ab1a7ebb873f035aa268ef9e374d6521aae6d7af2d6d65d836e79d6461c5aafe2c91ec35374a038b9ec89748c1693122ac6028d391d6f5143b74b842f53493d5bf6956d26a33c63c9b10994034a36001ddde26e5b8b6c1b8ee76bb94748b1fbe8d2e08fcfc31e2fc459f518282a609c1f97296185ae280c4318137bc411b8d097c40af0a11c2685e1ea3b37f425bb4c78766a235d219531d2cacfc12404ce3de0ddd4712f84671acae9bb139816bbcffedc1a6c273f4d86457af7aff6132e94cb4165e90b16491df1e67ffc0fca1c227a10be9c46447cfd0005a0153fb3b535858ee2cce73ec76ee4aecb5b73cfe4565a1a0ed1f5d582b1e7d244a413e93f5ada361a58443fbe0edc99c1ee3368c7583b70c21bab896491c86aea126b6779294ad1b36fa07ea2b780d4a19c5b0ecef284d444537bbe741c4cfd6987ce20f6c6765ee90d38fc7f2d587d3a40cfd79f312ac44ac734adcd418741d2bb793a468ae43c854ba982e9f5667a488e40b4aeba0eaf81e17b0fd0c76a3326591ce94be7290f30bb23307dc78d6b69975455d6cd925b09bfcbb43a591f9b509508138f6f04aa7680111ea6a280195cf3af9ca06958cc7954db0c53545f473207520acf19153c1219c25b14bd52d9e8ef2efe8fd44716de55f4f3316970ce80076bbc4ca37b93ae358389c5b72f0eaaabd6b500ce63e26fea094dc76e4e7efc0adac93388f5b501f572b9b482d6586a9f9a678fd5f90491eb94dc7359bcb27308b74bfd4541e13c76c29ed6b425beb4a2b68ceeb49503fc4226e980d0fdc1dcaa97f962b2dae3fa4628e1161be82d56b87234ed079daf0cdb95b8eff084f5dc693ab6bc906befb0323eac3974cea284647cf84c3398d1289a4c798daf996d8a1a77b0e326c8241fdc35aae4112f0d306691b533fea134585d93f86239d4aec0907c0476779b9f0255f7ba0c48463c0728904f3f49da65745ba82c6d43a1f69fe0a3939a9dbd0a1a326c8f122467f7a474a1f34db3eae662385e671c45d9d764ee7480f9951044ba58d7202bc749b80f7560df0636cb567dde48d4a40e05b06a02492da43408c7eee67e830692bd56dfa83f2a83925fab3341fd88abc0bc5c912ffd3e989a4809410bc7a88a586a81bf6b7790acd86b4afb10c88cbadcde886424d5bea51773cdd5e3e5cd4b48e61f50fff2d9633fb0ab1dd76df3de572066b9659b18c94c682993a1b63577a8591032e346201c08c821c1515ca4521bdbaf1be924092eadeeb685fec9b2e94e5e7a8129068000c508d07d1059ecb2c44cbc0b5c35e209d04ebde92d82388a6a40bfa63e6d9e185c09857aa0d56047e118771a7801f72d31d91b2a9213d9211a49b49273ce519ac51236030d485f42f08ac720bc1c29c97648784d183cd4cba06b51f55eff0bed608b9a9c05f21e9c6243ef9f294fb23b41028bb9e41fc3e9564916659b5f890ef7a3e49f22981e008a83210566c9a1b6d29a341a12d947601bbbe20e8a2ff2f4bfbb0b7856b4a330eeea7e24643a853884ffeaa0c104d27a4b31f9082506eccfa428d821731e2e62f7af51165e0665b07e26048e6fcbe046bded2bf5d295b29d3b59f119d4b16a8aaa926fa044a9c4caf678041708cf383e5c494f9285471a32e0ec1eb9d363305d9b6645f719d6209d1a7cbb8facb0e40f305ba618c4d36a07289a0e44c957ae861fca29399d576c3a35c1e587381078a7b1ef99155dfd7f981d352b88ccb039d344f5d04f0cec3e14906aa1fed6c5a05853ea70c1dd06aa50266b8f687478e1e2e68a029024fa4a8072a586e336f9b47b947d7594c284a42daa1b2da26091c61f80cb82f7f22b3aaeff341999d1b8af2dc496bb674de530041925e34b039a70a5bd4df33e99cafed534296c8bfc593cd48eaa757e57fb8483e42d98335571272fd84eb0bdd4dc77e4e78d631b7b61c769809bbaba76bc6d5f38884569b2fa542b71054a3f4e5ae1808e8022c1f02024c6144a890b9c37aca0905745d2cd0ac292cf0f91dc20e81dea89a8be98bdde3e2898c02e52fcf8d6193b9a3c7ce2b311a4e764e3980a902f016a1a4fb41267ddc2966f82b8324f967f0b2a3cdca7835491360c23deeae74171489e78ca496d04235576266b834e5971b9fcf2c2cba6bd4803caa4675d6efe7811a90233012d1078c5f7f037e403770ab5d7e4f3171f299547cc45ad7a02c453314f5f2cc40519895f0b4d9bd6bfb70d6a69609979435c41d7dc816d9a8ef9b1ae1ac7f27a281cc93ab680cb6ccb9bef638381308afb37be762f2da5229025f2746abc1dd54732bd8b831994c669ec07b3110ec7cbc00753ed4e40fd0c7284842092aa1636963cad046c97ebc5dd3d732dc94a30b3c3ebcfe9bdf40e550cbd1d229b1907d49832d5d321903db8037743da1ac98e6357f8b65d1add1c2c37573c5ee8843fdac1a938565609f3bd5a76c92bb8dd1418fa12f74048bf1495007a60b1f015a469bbc47bfabd1fe65e6c67459a6a1aa218b747b52c3bf8b51b582332f2004875c7a0357a87b0a04ad01678b107f9a2b9f3039801e7d204462df86096aef837f6d71f9ab6a08d17e91c05de652507879b27e7dc6870c2618b4227aa50f2c3efb8c1568610e179bf578c13a162e54a59cff0e08157198b56957a89e421e9dfba696b3f42ed0b9ebf0cc70608463cc4c376120087bfae5900a5d3c9ddb097aad563b5af2f6abd112323b349d89702c313b0e4002df7a3664c188821c8c7f0571007570c6215c9bd603c53435a77da0b228642d30eb6e84eeb3d0e4564764544edafa987a78def852d72bf838b244a29c1065bf24afdf69f683ea11c7127efd9635a1aa05cc6970f9d312287950a6c4d2117cc2ae844642ffe1d2b004cdf93f2da62081f3957be5995dcd97b590a997b9a093f64db2f861659dbc80c8587044db8247ed16db8eda9bc32fe068d20790725412c66ab95ea879a373a607e9cdb5a62a693c01f7a0a4031ad8de040d28ca5448cc9982fe7304cdf6c6deb9b4782b3cdff0f8cdc00ef05bf3c6ed3258eb90b01e6ab32652c35f8133223755d7ebd73c24989e225aaa168afcde29289b4771596e885005b3d6d65c73a5509202b68f96fedfec2121326488ef60b101e8e60d149d69ede2b08782be1c7aaa9e0c5255dfcab36e5459a25d8f447f868ebfc9ce939471fc807b77eb5a00c653331872c774fb68c212c343a8bd3fc9bc52f2ecbbde2fb2ea9605c0fe6a7dc02b5cdd0f84974cade1832939d2dc0f383626e1bcc3d9193d96d8fec338617ae0c5ad75466d9050a4a7730dd61a87ca62a2143926d7ae8cef2333268d2274155e297d499f7730685c2fd9c863b97c7a3dd01fc18fc3bf42354fe6246d6b78987dcfbd06269081f8975c515d351b1cb8cbfa6c43925d51666a279ce13bc79b6136f195cddc998a18d1f0ad5c9997dbf8c0643280a775a829e7138b5051207e1b2e57f680f82cb3a3484bdf7626dd3267bde824c3ee7995f6f8cff35e71de6c612a06dbe344fc8745adee861dd7959c2b5b3aed5fbc22e65bba308932dc5ee90071ad7b920c5698683fbe7de1e980303df957881b54f1b750654ea3f00aad73efd82b6084dbc646f38b2ac19269d213a0444584e7bd49a30f011f5a2f6bc158c4ffb52aba4e34b212c01368e2ceb565df8cae2dd4b2e1a30cd06e019807104d08238b53ad30e231c4f8d1440e14c21c842ff709648e78b3ea80a29f726113a19918f1f74e39f5052d63d98e66005341688f57ec438b7c6cb90d21a2a86e695ffd48f242e6f642b55194a40fbafd0198f8914c6fe5930fd152d0286ae1bd23848e27fe3446836b2d76b960caa119421fe7f4e7df092e00a7019cd58301882e878600a3970c970c33118d43d13c33af57ddb66085dbf02a9ef4d09b5acd5c6e9eb48fa140713b7987ede4fff7ee835263d99db9df706abad165ff5c56debeb5791cd5b0bb1e0477d139c24d66944093d2a5743fd72b8960b4d602836c536a942ca45a2fbd29ab9d8bd7ae7d65e64f292f26fc96d7088909420db9b303ac963c427a4d05681c4006afb48aac617368da5661c5278a30cfa002268cb3195505b2ace8c8cae1f1fd5b34186798f56ecababaf0b790781e04d08eddd1da8af98950ced6dfee3caa49a78b16ca7049ae0e378da7c9ccd216a06f98cec591879d8ef1ce2ea2d1fd01f55eb92f606a70baf40c999583829a895d74645255202e2daaa498c77f193c06e563a9b3561db8983448cd6d0c9d06a148ceb58082e362864290f38139c148c3d802a3aff13a2e9b9cd5486d89f68478ad22eb6c68f199726e4788cfc2d3ef9d4ede892a99989b5d06b31bc9b76b9374e8ba0b46723ca16bc42097b1666cb762af7d04d78547d7cc036bc211a3ac9b1ea706a2946730b67b50a34fcd135bde01515004b70a7c5c7b588bddb0984b2ba516add041585d78328cf8bc39d081727dd794b7460002fe136f0f2c4005d70a0ea40f11a74857ea18b78dc79a04552d7929b1a4a279aa6ff8cfaf1dbed8c2417f4f86adb54707366df2185f97e5b58ef83bbfa513f0dc53da5cda1b83d8449e3c7868b9a194f962a99b567743ed416fc4eeb390b729ce72db98b7e37690a4ef0578aad7d3975d57e4c982f74e2f1c7902096d2fb31f58512acbfbd480dce54b1d7b5ea1b2493d610f03d3bff2153dbf2a2a4887eb6bd335192cfaa6c3f48a6a83044dc24ca5c3324197bb0ef6c126e4ef9129b6113758c075a0e9afca077e8c92940836b71c1ab34f35a4345b96f437d735be8aef40f202f281e6bc178c65b38aa65da2e8ca183d14aef4c86cc88cb6d506882cbde320ae5a77547f52ef997c3a40f173464e1060380e5a4900e29d081cc34191945c1a7dc69a40e55ede68636c6673752e3adf9e5a217384dd240af5d1cf28b37c7559b964d41ecbdb6e70b9f209fe818c9cbee404298e4863ce600dab3716abf797ad951fa831df1a85a3752e31ec7a1cc1115732e7d537990ba7ac238d14862dc7c256d21e1af90009479a93c28c86d3077931118be2d5bd79640187ac12722e0da3e0582e86efa6e2f5f7b463049311fa0af24b043167b4ac57cc6d0e4058d5e46d7533be851c397c646fa0cde2220235c305ca8729affb67ef3dc3c5a7a1e0dc58e2cc5ed92e4da36072cd55244ea5a2ca061ffb1ecc83b0953fc527c2321acc14df92f9f650c581152315728da425b6e117a3f089897482a5394d363a9943e1160bf113652a510381e9c194bf52fb1986a098e17d8b8594a923b2f7beaa095ecb50ae11d94591f4f4e2332dd85695e588d1fa66611c7cc47202605027fb2512f587355f78e193332ff6b98af8b48a30ed56b498c2676e06921ec6bd322d00064e5d2417fe5f7af871773a80ce8234b5a2b178be151bc79776191bab61a480874edd3740945fc8da656cbfd22d5bae2ca28cedc420876445dc02d3d5d987e7389a972ae7a8aad24c77e97bda20aee0ef68b1014afc1533422728108eb6c21417784ff97bf88493404aa3d485d4d738ccbf5f99188b4723b13739ca1b158341cc62a222e1e5e7fa225ad60dd8152ae44975642622aa4ed6c5615bd309d4e2c793dcfe5a089b58045307fef21892b76244db2e788693899e21348de1c7262ad54a6e9acc2308b71457b725507f175da4497be1b835d3f483eff576d8ec13aa3c57b7b11a0afd8a477b270813eea4600f6c919162e2aa2764f13335b917e13e85f71d4b5aa672389caba88cb547cc81e7cc8c31e88a64038534027af918723e9ae5e7ec32c5b68b8fe6a803b19775648faaeba5d36e7486b9c68041485e8598701748201b31e8245bddb180b17e2230954cef73d3815afabf991dfab9071fadbdfb2b153f75ce519f8f7b479e752790c64d18f32ec7ca29359c994a75206a5c23d5f341d784c1dd6bb6df3caa4ec39dee34e8b65dab6cf22e599bd08b72f686a91807284a7976ca613b5f3969ae4236027b0cc2f34018bb0485bf2222b5ee8cd9b74d60b9e4923e151e29f208268ca9b22514e8e2f8cdd1f1957d68056def464f63fb35836688bb3c444afea76c355639f3b43c0f14832b5313892bcd073da802b1c58433aff0d6d8712e375f6f9c6defc9a6d7d26e66d066809924ab0fc3ad227af43acd3f67fb9a2aab0c2a0a6399154bc988a3fe4983e841b9c79d89ad26c6a3abf912b43c16c58e23d84798856d5e802c4fadaabd41b7084d2d20a7e8a775b5b41f27f9fa7318129957b4e3b6261812c0db74f8ab21f9a810f2c2f979147c31d310b502893ea9bf3e62ad2bd6b088ae4cd3d268883918ff230a958cfb6009fde57defa7745d156795063b92a27aa598efd559db04d336414fcf059876b389ae2a9843e91f22b181dfb2c31f76fd3acdd15d3050ea7e91d4e2978899d0cb53689f687f2a2eef9191e55de8d397b21f9dcbabc170b5cf930b192780b07cbe27720dc2cb66773dd467b7715cb278beb1e3c7392fa3801248a1e848ae3136a371a172d6db3c5e4221492b011c336c47654d0923448d1905661944d58d83fcf968e6b7e09d28f957f6a7ab52b7554d938440a4107a272186f4679d31b1c1d8656f07cb452a2fec1abbd28a3efdb5bc25945563958d6f38faab7dbc0a54b44ba71db2169316f1642f111e69aa0d7126851f264a85c0451f799d8ab080b3528cdd9c1ebebd987e2d660f845313faf4e493b74d93b9839a556bfdbbc2346906e766a5465a8307f232ac0040a56dc969abc2e7519c86815a6d2481944d77175f8f8f6f666f82504f99b5a3368884d07cddc4c92e81ba68ec91ad05f3b9ed1aae4506e3974018c95665ad249f21c6882048854973c71090afb6f8222df5175952b9eaed98f90886b43d36bae4bfc9c8060786309a5cb1c5a47ac26e78f9cabdd84d870041ed102553e8331c06e81fc6646c851d60fbfc4f4df5be47347ee07d3257f923b02331ef9602c566bc2e008b6bc48d9586ce9cf9d3adde5e67cda07655822e00f66b95c32676e22d0ba262d04045a3e6cfa9e2336af3301cc5e23def47e595355271d08650389c4dc7cc25993a84c62b6a95cc700f0eb83231af1bafca03da774c0d8cd4531ee4e3e4a5ef11a5c4819eac5a00fe960da5e4f50c5a9bfc92ad7686ea7d652c3ea9643bfb829bc2932c255b87171d9d3198ed794cb2887291913ae84bc27b5142ca6a2de9eb3a302ff3d8f817d8d7dc2bc9f6bca071599446cfc78cdb58837119f200d44cd16160b4afed472139c34ab8d8f76df4070bf1c520019de28f486e5516fcbb9af1a1d015987fbceae835c0fc165e35b73b9d9608963c0cdd0808cf7ff680ca3bffd60ecdc63ad7e345263253c01df836540bba7319dafcef0f63431899f1c7f58ea4b37ea3d8d4e9a5698c5437a391220707e34af9017e37867bc1ec766c6a24108ef4dbf67130d23eec4f3299a127be649f5d48b478a83d8c81ab29e91e1157327a3f6d2f36957ee21a1f6207a586f3d5a463974967c13e5a219f6d3d3e688587eb8b4d8d914cbf341378d0f25c681026c39fe65403d8d78c764458334141c46098a899d565ab8c8ab1a7464039f72beeb4f0d3b43a69ee063f31881917167fe6e884b681bfe7cc8c03f916b0bbb345ac36c1fc924decfa8ad613bcb3579fa1515ad96495625e259119149aecae93661d32db0d26b051a1cec2e16d984373c5b83ce79b28bb431097894ce01537dbb30c4ad642b7e817b4d4736321504967b70f5c4c8e74fb9c483822668362650dc143325123ce4701502e550fc9c5141534b23240e092287ced7dacac60cd97c01f2d71516c22845b39a943b11264a4f98ac5864ed4e05da850f9336eaa8aab668bce4beef982358d24edb6639d6baeb68c1c37534dcadec3666b18c8440f6eda6cd3045b7f068c193be24b54372a97391a3f143f4b96fbe4a19acb20694c3bc69c2814abe2ffac3514ab6e7371c22025fee43add653981c9fa37af6bb30f27821e4dabdd6d847dff803decc77febda719cdbc1abd4f1002b81db9eb3494f20c1c561100d6babf1e29127a681474a8deb6c44db25bd82fbe9565a1c53918545e07c12e1e9cde07243d5d6e0336134d216dd5b29709cab0057549133fb6a9e95fcdbebe08eeaa5486275870c09a205ca5c32eb7322b20b9bb81a2064ad4d9c8f14c723c97968e319b844a47e81944fcb682cd8edf1d597a4b98a3aeeb029249772b9bb57d5156acb1d1036b3ed700c9495ea285ae5a82d5c788b172eee610a6a1cddb31d9c27a77614b8939f9e014b017580cd6857531bb6d1099e2f171e4a52c3d119addb6816cef1a05f4ec5d9a662dc3d3147effe9391cfe59bda97baa89abd69ad986edf4eaed759ec0913972558a840a9b4fb87dfc5148d26340b61039c1741f87a5e4f858f03c316ac6d5a24e9e42841410adf128d25503a9c3334086a1263eed865c50b7336fdd52f76d72e2a07fec314b353af032bffea9d6a8b74a7b1d3a943c5ffb733dee16bbc179f5192264b5fcd5424a10c3c3e2fc281cca8084b9572c48c8d93e312e3806dd6e7652eb6d2a2a615c34fd018876287aefa6d40f06d321f0b0fa476f31f327a2d80e28162b14e49059b8e6b77577778f61cd60e57099e2629bfda09b44aaa624e9e85e9ce8f2bc9a8b85db28ad19c890f630ca358e4882a1485b0564ccde5028573bb92180ee736ad0fb66e5d02785564c817845bb4dca29444fd7a3b162bfd3bc01b658df1eb35bc8aaa13dd7eff92d4c66a7bebffbae5e7b52331631c0eac829d3b7722949011fa7398d973cf1e6bd863851452a7663ab3da869f6daea5b15973226a4f23daab4d66235078366a3532a1b4285c93c2d072bf1e9ad9b256a3e4bebf2ce55b46c885e78a6c9342e5cb19307b51b5428633300aaf7949570fa37aec730fb0d8e7aea7bfee1d54e13370c9da2e51d7a81c67556a9221175a975dca9728668573e1bf6f519554def0d12cb3a1495f36320cdeacf75cd9f6321c963e372b5b65e7650f158488dfbbd40dcd438d824fdd1ef62318614b64480a822c21197080d90be39520d1b5b59ca1f555d683174371776d7ee08a99679ef2f2a55af7d4e77e08b701ea0c9faf28ebb801067cbc708e951f2621385762f00df797e802f14366ed9aabbed3b5ee3ec0b2b74b566803619a6282de0bb0dbfa16faa4113f90d317ef331f2be8baebc5cc7f301068ed1e7a32f514948c9219618797004b89b4aace04714072ab0e5d57f0a9d06bdd6211811eaa2bbbf76d4ee6200a8afab8c5dd92d331c123b8839cf39cc7cf457f0bf04e35928533145c0a39c562b52f0fdf8abcbffb8bfeb8215fa58f797c0348c8868c876552aa3894eb34807bc62accd2b3f30758b081298bc4c3d4c9ff927efedb828bc629ef622e2edbe178ad70532d081833ab8686f4b5a3e2f6046725d33adc021060962a6be6e410d6e651f75ae03b870a609200c11a549233164ee4b5782c1c6c88ecf8d7a96ef581a4a0c0580cc5c441962ba84ebe9a415f159aef0f78574ec2fb5f0ab295b792b89aeac2df77b3c743c62584b9a119afe81d960c43711436eda67435288f1581727586a29d9c1c98ea945c64626aa7122fc420dc9ecc80d5e36fc8d71f8311b25acd3d11d7bedfa85796d4f02a5e9476845c8806ceb18900902fba4cb1caf338330e465c966120258486c4b3e0aa32b78e88e9502b7da265e804f1f881f81fd948209ca06554af60b631991cf8225759d84d453be06e479a0f8d13186ab504e4de94986f469b97fee6cccbbb54e68336177fdbe678ba93b5bb503516474cb98e4827d20b60266439bcc65b70ed829b45b05b0d7d60da7b099dfdc3cfe5236794c2365e1f655c5e7eb736f761eaa671371687f99e213616a0445dc53ec49144a26cf8041a9ae74772089090fb27e0349b5bce7cb9d9f054e9d5e72a8d1d4581edfecca0dbbdaa3a1cc8fee8b2fb90e25467ec448fa6df4b05fdfd427825fba29d4c718203593f9259c0c593c71a00b901ab3675402f096a748b2d6d66d3b642a9c9f12691108578ea4360001376315c56b99a87f1f7d9401537052532e5b4ba0778aa98f8fe599c4aee56f9e5b882d64e33f101488e10f5502ddeb7f0bd6cdcc6be6c72412ba4e28920bd20cf3d72b805c76b6bff2895bd13150b163587a152c1e1664ec2db9956a87ddbd4471a3c1bb83583b2f93ab7e8eaae739663e45b2354d127346c08191f006b1e67d1b5a7f6714c5abc4bc3a22c5fa11d892a7f1d5fd12655145478c42216c66401172a196bf689181cf37439f738cdf0cbd738b03b7be21966a8a95626eef8de15ed1c6490392090eecfe9930c247eec4ef964d4686ba58a401dd97d1ec2a23b02919a8772acad453c899cbfb88182df74c91b6cdb5265b96b2f4c7ef9ab0635f57803b2b5dbea2d3592c70b2c4fa88a9127c780845d0e7d930565dff12e9eee8094dec44d0b7af5b4e841d5f1da0be424993eac8d405159c1168fb98fef0928ef4a94806661d37e16417de158ce9f52282af51791e78e8c2e0e935fe5151b508a612720eeabe36bfdf7c04c34bf1bd35331088697e53e6b554dc080a1739d55faec48ab7430901c5a37b5c61ab189fe3deb0f2c781d231eb57dfe457b280a2aaac141a27841a4e43973050538062513f15edf39deccb4d6c87791f0475092bd3c5e91384984a0ebedc37daf8e79c727c2f8bf8ef9a79a0c8144dfce77336ad85750e54e9e71bd7cba0ed50866c1d3bedeff85f7b1262be6d8cb2045ce905861dc5fb3b5a113b4253dcff5e9b44f94bbf79af7c82fcf096fe1ccc48a6b96cd5e7fdd3796b87f14c0a55f05e04cf890fbfd4a6388e54d97c622dcd7dd84dd9c1ca288a3fc1654f7d33d192b464c9ed3edb1610bbc7ec142f861c925ba05dbb3e1808320a66553ea320368ec8bdc5f757aa890825673e9d0e80b6b60a0eefad5bdcee3c03ee70db608d24f5e0eb1c64b6ba92f10b58be246655b984b58bf5268504801b4c1cd40c633503d87b907452c332ea16d7be122fdfcf5ffc2fbc950afe5c65f9fad7db166ea7dfe250aef83f2a4da341aead9f40d780fa4562e3e06a47a1b271027add92ffd595638952d101332245d0b94d1ca6bd726d29ae1b495f01868fb79e8505e1caeb7f8379e689add14e2f5efbc750688f4b2f76245efb8addd2458a7957f3829f87138e61370cde7ee72eeee9a0e76cfa79e6d869c3f2e33ad0ef7f6945af98c8659c936ad10c9f66a639c33965a25eb68289e473243c4a8b58132eeecf7fca073b341c6f8e51d88e955ea4b59c5993d57c0d52929d03d7d24504aa6a8865d3766ca9fb619bceac8214c4c6b9bf464493c534c5eef688bcf9538860cf14fc83c4471baf632eb4f8f7ed163fe80966de5f5e7598c495a0d62849ff611a6f308aa6e787752dafe0ea3df7826488ef9040d0b4f05c199ef9e7e2bc39740830cec27e59925a2922f89ebe65089eca31c1f366e300cbc6c239871d7df51975b91a4330850dbde552ef715793df19e8875eb8b9427ee44527a2fb8da0aa347d4305549866d16ff7c608e63781d80e5ab607ff8472bc9d315b8ec6ef1d3e027b9a177a3ff565d21eaa9931aec272136a3c70dbd2335b1bc735f93fc29dd3bba5c5d32f4868f9296436d057756530db0a65b632ed0cb64da888454c5322db6aea62ab1441c8c21bf406782e5b95ba75b6d8e2ceb6cd6c23b9a75de6936a6c3afaddddb0309ea7713a5be55522aacfbf7e07cb5f2e45081f5bf4869fcad36f09a4cd2643a0145972276f7b017c3ea656cb3263c73da1c1ea1c3908ace4a8a0f630a73f0e0fb8db2de4b85c650d9c36edc0d893a0f0bd3c354bba131bafeb8898ba1ec24974d0d5d75b82dcb02fb2f29359fb37d57fed7a9114bdde0e4ece9ee73d4317faef53babfe386399555e41f7dbb19f1b1d076a4373506414e254fd6f2985026ede5774425f82749765337f9cbb20d034778484e0f99cb08abffda6a779adf6097c61ec22ed7a2c1d028ecebf4894db684804ff822fd4efbcdaf5dbf276061faca150e8b9947bb7bd849c10fa8181551706b21421f5acda536144c6b8daf2d3e7ec8c3b5b2b858915b7fae0e188e7df5e6e90d5bdc8cfce90987e01b2bd945c8b2770beca31eb77f393bd6a334ac7a40eaae47e0a675036e591c873fd2b8a78332faead69673db15307e7e23b6eaa55b444571fd402d873e445983c501e9638039fc8c5a7368e7aac86c75fc0396a559e563e733fab6301059931aaed3d97a22000c30ea9323a9d7f000de74d1e578204ccd73f26762def3a8edfff9b5c631ae52a37f4a96346be9ffc2f2da8a9a2ac20fa7509d005a633fd76b7512194a4b8a5076b2f01809ee0543678015475ee4144772b8d2b920f52eeafccd2a7aa8e9b6a6584fe2ceb571abb8f0c9ff0117e6a3e322024718ff2b73f501c4520f1a43820e8b3ee29332275ef68aa602da6a5cc127870a773217642d56cfa1e90e433ce8f30c173f20893c005cae2e2c6e1985974cedd7ef6c07622a0a1d92bf6c1ac7cca43a0c25aeee2f17e3f99d275c6cf7c510bd34cc6a20f0273e3a89a98ebb23322d7fd1ef2c6bd3fc8e4b4e6b130b38c7b343009750608bda2ceef3f764a28fa2f00576b41d2e79d34ed312fbc5351df796f79e7625066cff9a4b1b739df8cacfa7df141ee7086b64dc342808b77f4bfa4f5f20aa1726d085cabe5c2a76a6c000118ea867928e07418600087b0c7a8765a8cf0aa1c844ca5adf6a109cbc8c33502a03b2e339a87473862c09b749af5e83bae5eb411f33b3558072891bac0ab41a2ca027ec82c00d71bab18a2f0b4c53bac438b48f6ed0084516e21bc9497adae3415d180c25b6e9286e58f8697cb83540b3f3cea1ab20159d3c5b0d4f69b888562096639e4483971c5d46631939de3320892ad2e28f567d84cb7127cb2ac86e2cbf80dfbce06b8df580d28c574a4eb82f5de6778cdd8fbc7bb79c4a3bac618c0b0abb1ead50221572d9a60333a26', - genesisString: 'Concordium Testnet Version 5', -}; - -test('testnet presentation', () => { - const request = verifyPresentation( - TESTNET_PRESENTATION, - TESTNET_GLOBAL_CONTEXT, - TESTNET_PRESENTATION_DATA.map((d) => d.inputs) - ); - const expected: Web3IdProofRequest = { - challenge: 'd7bce30c25cad255a30b8bc72a7d4ee654d2d1e0fa4342fde1e453c034e9afa7', - credentialStatements: [ - { - id: 'did:ccd:testnet:sci:6260:0/credentialEntry/70159fe625e369b05c09294692088174dbc5df15b78ebd6722e5cac6f6b93052', - statement: [ - { attributeTag: 'userId', type: 'RevealAttribute' }, - { attributeTag: 'username', type: 'RevealAttribute' }, - ], - type: ['ConcordiumVerifiableCredential', 'SoMeCredential', 'VerifiableCredential'], - }, - { - id: 'did:ccd:testnet:cred:9549a7e0894fe888a68019e31db5a99a21c9b14cca0513934e9951057c96434a86dd54f90ff2bf18b99dad5ec64d7563', - statement: [ - { attributeTag: 'firstName', type: 'RevealAttribute' }, - { attributeTag: 'lastName', type: 'RevealAttribute' }, - ], - }, - ], - }; - - expect(request).toEqual(expected); -}); diff --git a/yarn.lock b/yarn.lock index 785ef610e..1102313e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3686,9 +3686,11 @@ __metadata: "@concordium/web-sdk": "workspace:^" "@grpc/grpc-js": ^1.14.0 "@noble/ed25519": ^2.0.0 + "@types/json-bigint": ^1.0.4 "@types/node": ^20.12.13 buffer: ^6.0.3 eslint: 8 + json-bigint: ^1.0.0 meow: 11.0 node-fetch: ^3.3.2 tsx: ^4.19.2 @@ -8180,6 +8182,13 @@ __metadata: languageName: node linkType: hard +"@types/json-bigint@npm:^1.0.4": + version: 1.0.4 + resolution: "@types/json-bigint@npm:1.0.4" + checksum: 7aee137b3796121cec755247ea56577611c8e5df89224c567fa71999764d01e12199c4089c6be8a9e4e6a6a8f5c6f309eee2965cc3dd8d576f4c4ba432566041 + languageName: node + linkType: hard + "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.8": version: 7.0.11 resolution: "@types/json-schema@npm:7.0.11"