diff --git a/integration-tests/verification-flow/compound-presentation.test.ts b/integration-tests/verification-flow/compound-presentation.test.ts new file mode 100644 index 00000000..eeffe524 --- /dev/null +++ b/integration-tests/verification-flow/compound-presentation.test.ts @@ -0,0 +1,195 @@ +import {IWallet} from '@docknetwork/wallet-sdk-core/lib/types'; +import { + closeWallet, + getWallet, + getDIDProvider, + getCredentialProvider, +} from '../helpers/wallet-helpers'; +import {createVerificationController} from '@docknetwork/wallet-sdk-core/src/verification-controller'; +import {setWitnessCacheTTL} from '@docknetwork/wallet-sdk-wasm/src/services/credential/bbs-revocation'; + +const allCredentials = [ + { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://ld.truvera.io/credentials/extensions-v1', + 'https://ld.truvera.io/security/bbs23/v1', + { + QuotientCustomerIdentity: 'dk:QuotientCustomerIdentity', + address: 'dk:address', + age: 'dk:age', + customerID: 'dk:customerID', + dateOfBirth: 'dk:dateOfBirth', + dk: 'https://ld.truvera.io/credentials#', + fullName: 'dk:fullName', + phoneNumber: 'dk:phoneNumber', + }, + ], + credentialStatus: { + id: 'accumulator:cheqd:testnet:59459883-4e4d-40a0-bb4f-2b7c42405dd3:aac15524-4aff-43bd-b132-1779eb7dcf36', + type: 'DockVBAccumulator2022', + revocationCheck: 'membership', + revocationId: '79', + }, + id: 'https://creds-testnet.truvera.io/76dd0f26a04b4253ac29faf08555e43a5d6329bdc6659dc35a35ee6645750b91', + type: ['VerifiableCredential', 'QuotientCustomerIdentity'], + credentialSubject: { + id: 'did:key:z6MkqEFT8w27x18qRqf7SBnd882pNWsBZPLnk5hVZfVP9AqG', + age: 51, + address: '654 Cedar Ln, Austin, TX 78701', + fullName: 'Grace Lee', + customerID: 'CIDT2CQ0E', + dateOfBirth: '1974-10-17', + phoneNumber: '+14519037960', + }, + issuanceDate: '2026-03-31T13:05:53.563Z', + issuer: { + name: 'Quotient', + logo: 'https://img.truvera.io/d689827d822056c6da742080ac6c80ba', + id: 'did:cheqd:testnet:59459883-4e4d-40a0-bb4f-2b7c42405dd3', + }, + credentialSchema: { + id: 'https://schema.truvera.io/QuotientCustomerIdentity-V1-1768398916249.json', + type: 'JsonSchemaValidator2018', + details: + '{"jsonSchema":{"$id":"https://schema.truvera.io/QuotientCustomerIdentity-V1-1768398916249.json","$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":true,"description":"Schema to issue identity credentials for Quotient customers","name":"Quotient Customer Identity","properties":{"@context":{"type":"string"},"credentialSchema":{"properties":{"details":{"type":"string"},"id":{"type":"string"},"type":{"type":"string"},"version":{"type":"string"}},"type":"object"},"credentialStatus":{"properties":{"id":{"type":"string"},"revocationCheck":{"type":"string"},"revocationId":{"type":"string"},"type":{"type":"string"}},"type":"object"},"credentialSubject":{"properties":{"address":{"title":"Address","type":"string"},"age":{"title":"Age","type":"number"},"customerID":{"title":"Customer ID","type":"string"},"dateOfBirth":{"format":"date","title":"Date of birth","type":"string"},"fullName":{"description":"","title":"Full Name","type":"string"},"id":{"description":"A unique identifier of the recipient. Example: DID, email address, national ID number, employee ID, student ID etc.","title":"ID","type":"string"},"phoneNumber":{"title":"Phone number","type":"string"}},"required":["fullName","age","phoneNumber","customerID"],"type":"object"},"cryptoVersion":{"type":"string"},"id":{"type":"string"},"issuanceDate":{"format":"date-time","type":"string"},"issuer":{"properties":{"id":{"type":"string"},"logo":{"type":"string"},"name":{"type":"string"}},"type":"object"},"name":{"type":"string"},"proof":{"properties":{"@context":{"items":[{"properties":{"proof":{"properties":{"@container":{"type":"string"},"@id":{"type":"string"},"@type":{"type":"string"}},"type":"object"},"sec":{"type":"string"}},"type":"object"},{"type":"string"}],"type":"array"},"created":{"format":"date-time","type":"string"},"proofPurpose":{"type":"string"},"type":{"type":"string"},"verificationMethod":{"type":"string"}},"type":"object"},"type":{"type":"string"}},"type":"object"},"parsingOptions":{"defaultDecimalPlaces":4,"defaultMinimumDate":-17592186044415,"defaultMinimumInteger":-4294967295,"useDefaults":true}}', + version: '0.4.0', + }, + name: 'Quotient Customer Identity', + cryptoVersion: '0.6.0', + proof: { + '@context': [ + { + sec: 'https://w3id.org/security#', + proof: { + '@id': 'sec:proof', + '@type': '@id', + '@container': '@graph', + }, + }, + 'https://ld.truvera.io/security/bbs23/v1', + ], + type: 'Bls12381BBSSignatureDock2023', + created: '2026-03-31T13:05:55Z', + verificationMethod: + 'did:cheqd:testnet:59459883-4e4d-40a0-bb4f-2b7c42405dd3#keys-2', + proofPurpose: 'assertionMethod', + proofValue: + 'z2mhJL8VK9cvvRuBqEkscjqUtXG2nCQ2oLH1NvuUT6uejKMha61Zz7ux61nui1X3cYsjc8nTmQLMgH7T5y4L8PH29eEVykbGtAT5rnxQAUjoxra', + }, + '$$accum__witness$$': '{"blockNo":"d10f73a8-8aed-4e5c-be8d-24105cb494ac","witness":"0xb8ff9c2afe1753a7b455315127e402ec7357110dfda15460f8df55b474220ec5f29de9177dd60dc409751ede1d1f66fb"}', + }, + { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://ld.truvera.io/credentials/extensions-v1', + 'https://ld.truvera.io/security/bbs23/v1', + { + QuotientLoyaltyCard: 'dk:QuotientLoyaltyCard', + customerID: 'dk:customerID', + customerName: 'dk:customerName', + dk: 'https://ld.truvera.io/credentials#', + loyaltyCardNumber: 'dk:loyaltyCardNumber', + }, + ], + credentialStatus: { + id: 'accumulator:cheqd:testnet:59459883-4e4d-40a0-bb4f-2b7c42405dd3:aac15524-4aff-43bd-b132-1779eb7dcf36', + type: 'DockVBAccumulator2022', + revocationCheck: 'membership', + revocationId: '80', + }, + id: 'https://creds-testnet.truvera.io/e66ee7ff343b74248d80da9a56e32fb38b6956cdee5534fe738ef6fe44a6913a', + type: ['VerifiableCredential', 'QuotientLoyaltyCard'], + credentialSubject: { + id: 'did:key:z6MkqEFT8w27x18qRqf7SBnd882pNWsBZPLnk5hVZfVP9AqG', + customerID: 'CIDT2CQ0E', + customerName: 'Grace Lee', + loyaltyCardNumber: '6048443734212353276', + }, + issuanceDate: '2026-03-31T13:05:53.563Z', + issuer: { + name: 'Quotient', + logo: 'https://img.truvera.io/d689827d822056c6da742080ac6c80ba', + id: 'did:cheqd:testnet:59459883-4e4d-40a0-bb4f-2b7c42405dd3', + }, + credentialSchema: { + id: 'https://schema.truvera.io/QuotientLoyaltyCard-V1-1768398982554.json', + type: 'JsonSchemaValidator2018', + details: + '{"jsonSchema":{"$id":"https://schema.truvera.io/QuotientLoyaltyCard-V1-1768398982554.json","$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":true,"description":"Schema to issue loyalty card credentials to Quotient customers","name":"Quotient Loyalty Card","properties":{"@context":{"type":"string"},"credentialSchema":{"properties":{"details":{"type":"string"},"id":{"type":"string"},"type":{"type":"string"},"version":{"type":"string"}},"type":"object"},"credentialStatus":{"properties":{"id":{"type":"string"},"revocationCheck":{"type":"string"},"revocationId":{"type":"string"},"type":{"type":"string"}},"type":"object"},"credentialSubject":{"properties":{"customerID":{"description":"","title":"Customer ID","type":"string"},"customerName":{"title":"Customer Name","type":"string"},"id":{"description":"A unique identifier of the recipient. Example: DID, email address, national ID number, employee ID, student ID etc.","title":"ID","type":"string"},"loyaltyCardNumber":{"title":"Loyalty card number","type":"string"}},"required":["loyaltyCardNumber","customerID","customerName"],"type":"object"},"cryptoVersion":{"type":"string"},"id":{"type":"string"},"issuanceDate":{"format":"date-time","type":"string"},"issuer":{"properties":{"id":{"type":"string"},"logo":{"type":"string"},"name":{"type":"string"}},"type":"object"},"name":{"type":"string"},"proof":{"properties":{"@context":{"items":[{"properties":{"proof":{"properties":{"@container":{"type":"string"},"@id":{"type":"string"},"@type":{"type":"string"}},"type":"object"},"sec":{"type":"string"}},"type":"object"},{"type":"string"}],"type":"array"},"created":{"format":"date-time","type":"string"},"proofPurpose":{"type":"string"},"type":{"type":"string"},"verificationMethod":{"type":"string"}},"type":"object"},"type":{"type":"string"}},"type":"object"},"parsingOptions":{"defaultDecimalPlaces":4,"defaultMinimumDate":-17592186044415,"defaultMinimumInteger":-4294967295,"useDefaults":true}}', + version: '0.4.0', + }, + name: 'Quotient Loyalty Card', + cryptoVersion: '0.6.0', + proof: { + '@context': [ + { + sec: 'https://w3id.org/security#', + proof: { + '@id': 'sec:proof', + '@type': '@id', + '@container': '@graph', + }, + }, + 'https://ld.truvera.io/security/bbs23/v1', + ], + type: 'Bls12381BBSSignatureDock2023', + created: '2026-03-31T13:05:57Z', + verificationMethod: + 'did:cheqd:testnet:59459883-4e4d-40a0-bb4f-2b7c42405dd3#keys-2', + proofPurpose: 'assertionMethod', + proofValue: + 'z2TZXgpEqiRkAmoB8XRvZZj8KJBpgFv5M9vVj4Nm5uYAj5nw5XwDRN2gYwyVyWtyrwyBtWHbbfhfahWWZN49RwHeedJ7kEuzCrxoSU5HQAKxHPG', + }, + '$$accum__witness$$': '{"blockNo":"d10f73a8-8aed-4e5c-be8d-24105cb494ac","witness":"0x935dd2dcf49a488a9aafda317abc477a8cd8c0edcfdf7f79a6fe4f51b0b352cf4d050822e8d44ecb001a8d98c7a618f2"}', + } +]; + +let wallet: IWallet; +let didProvider; +let credentialProvider; + +describe('Default presentation', () => { + beforeAll(async () => { + setWitnessCacheTTL(0); // Disable witness cache for testing + wallet = await getWallet(); + didProvider = getDIDProvider(); + credentialProvider = getCredentialProvider(); + + await didProvider.ensureDID(); + for (const credential of allCredentials) { + await credentialProvider.addCredential(credential); + } + }); + + afterAll(() => closeWallet()); + + it('should create a compound presentation with 2 credentials', async () => { + const controller = createVerificationController({ + wallet, + didProvider, + credentialProvider, + }); + + await controller.start({ + template: 'https://creds-testnet.truvera.io/proof/62f832bd-d7af-4da9-9e4f-bc7288a77d69', + }); + + controller.selectedCredentials.set(allCredentials[0].id, { + credential: allCredentials[0], + attributesToReveal: ['credentialSubject.age', 'credentialSubject.address'], + }); + + controller.selectedCredentials.set(allCredentials[1].id, { + credential: allCredentials[1], + attributesToReveal: ['credentialSubject.loyaltyCardNumber'], + }); + + const presentation = await controller.createPresentation(); + + expect(presentation).toBeDefined(); + expect(presentation.type).toEqual(['VerifiablePresentation']); + expect(presentation.verifiableCredential).toBeDefined(); + expect(presentation.verifiableCredential.length).toBe(2); + }); +}); diff --git a/integration-tests/verification-flow/default-presentation.test.ts b/integration-tests/verification-flow/default-presentation.test.ts index 49f1ecf5..c212339b 100644 --- a/integration-tests/verification-flow/default-presentation.test.ts +++ b/integration-tests/verification-flow/default-presentation.test.ts @@ -10,6 +10,7 @@ import universityDegree from '../data/default-presentation-tests/university-degr import universityDegree2 from '../data/default-presentation-tests/university-degree-2.json'; import equinetCreditScore from '../data/default-presentation-tests/equinet-credit-score.json'; import {createProofRequest} from '../helpers/certs-helpers'; +import {setWitnessCacheTTL} from '@docknetwork/wallet-sdk-wasm/src/services/credential/bbs-revocation'; jest.retryTimes(0); @@ -27,6 +28,7 @@ let didProvider; describe('Default presentation', () => { beforeAll(async () => { + setWitnessCacheTTL(0); // Disable witness cache for testing wallet = await getWallet(); didProvider = getDIDProvider(); const credentialProvider = getCredentialProvider(); @@ -207,11 +209,12 @@ describe('Default presentation', () => { const originalCredentialId = descriptors[0].selected.id; const replacementCredentialId = descriptors[0].alternatives[0].id; - const newPresentation = await controller.switchCredential( + await controller.switchCredential( originalCredentialId, replacementCredentialId, ); + const newPresentation = await controller.createPresentation(); expect(newPresentation).toBeDefined(); expect(newPresentation.type).toEqual(['VerifiablePresentation']); diff --git a/package-lock.json b/package-lock.json index 413f00c7..d99003ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48265,7 +48265,7 @@ }, "packages/web": { "name": "@docknetwork/wallet-sdk-web", - "version": "1.7.6", + "version": "1.7.8", "license": "https://github.com/docknetwork/wallet-sdk/LICENSE", "devDependencies": { "@docknetwork/wallet-sdk-core": "^1.7.6", @@ -48435,20 +48435,6 @@ "android" ] }, - "packages/web/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", - "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, "packages/web/node_modules/@rollup/rollup-darwin-x64": { "version": "4.57.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", @@ -48904,6 +48890,20 @@ "fsevents": "~2.3.2" } }, + "packages/web/node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", + "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, "packages/web/node_modules/semver": { "version": "6.3.1", "dev": true, diff --git a/packages/core/src/credential-provider.ts b/packages/core/src/credential-provider.ts index a29ea842..9381656d 100644 --- a/packages/core/src/credential-provider.ts +++ b/packages/core/src/credential-provider.ts @@ -5,6 +5,7 @@ */ import {credentialServiceRPC} from '@docknetwork/wallet-sdk-wasm/src/services/credential'; +import {prefetchWitnessCache} from '@docknetwork/wallet-sdk-wasm/src/services/credential/bbs-revocation'; import {IWallet, ICredentialProvider} from './types'; export type {ICredentialProvider}; import assert from 'assert'; @@ -203,6 +204,9 @@ export async function addCredential({wallet, credential}) { value: acummWitness, initialWitness: acummWitness, }); + + // Pre-fetch and cache the witness blockchain data in the background + prefetchWitnessCache(credential, acummWitness); } syncCredentialStatus({wallet, credentialIds: [credential.id]}); diff --git a/packages/core/src/verification-controller.ts b/packages/core/src/verification-controller.ts index f4aa5be8..905f2534 100644 --- a/packages/core/src/verification-controller.ts +++ b/packages/core/src/verification-controller.ts @@ -53,6 +53,20 @@ export interface IVerificationController { errors: any[]; warnings: any[]; }; + getRequirementGroups: () => Array<{ + descriptorKey: string; + descriptorName: string; + candidates: any[]; + }>; + getSelectedCredentialsByDescriptor: () => any[]; + getCredentialOptionsForDescriptor: (credentialId: string) => any; + switchCredential: ( + currentCredentialId: string, + replacementCredentialId: string, + ) => Promise; + getRequestedAttributes: (credentialId: string) => any[]; + getCredentialStatus: (credentialId: string) => Promise; + canSwitchCredential: (credentialId: string) => boolean; getTemplateJSON: () => any; } @@ -197,15 +211,22 @@ export function createVerificationController({ function getRequirementGroups() { if (filteredMatches.length === 0) { - return [{ - descriptorKey: 'default', - descriptorName: 'default', - candidates: [...filteredCredentials], - }]; + return [ + { + descriptorKey: 'default', + descriptorName: 'default', + candidates: [...filteredCredentials], + }, + ]; } - const groups: Array<{descriptorKey: string; descriptorName: string; candidates: any[]}> = []; - const groupKeyFn = (match) => match.from ? JSON.stringify(match.from) : (match.name || match.id || ''); + const groups: Array<{ + descriptorKey: string; + descriptorName: string; + candidates: any[]; + }> = []; + const groupKeyFn = match => + match.from ? JSON.stringify(match.from) : match.name || match.id || ''; const seen = new Map(); for (const match of filteredMatches) { @@ -248,13 +269,18 @@ export function createVerificationController({ const groups = getRequirementGroups(); for (const group of groups) { - const chosen = group.candidates.find(cred => !selectedCredentials.has(cred.id)) || group.candidates[0]; + const chosen = + group.candidates.find(cred => !selectedCredentials.has(cred.id)) || + group.candidates[0]; if (chosen) { - selectedCredentials.set(chosen.id, { credential: chosen }); + selectedCredentials.set(chosen.id, {credential: chosen}); } } - assert(selectedCredentials.size > 0, 'No credentials could be selected for the presentation'); + assert( + selectedCredentials.size > 0, + 'No credentials could be selected for the presentation', + ); return createPresentation(); } @@ -263,8 +289,11 @@ export function createVerificationController({ const groups = getRequirementGroups(); return groups.map(group => { - const selected = group.candidates.find(cred => selectedCredentials.has(cred.id)) || null; - const alternatives = group.candidates.filter(cred => !selected || cred.id !== selected.id); + const selected = + group.candidates.find(cred => selectedCredentials.has(cred.id)) || null; + const alternatives = group.candidates.filter( + cred => !selected || cred.id !== selected.id, + ); return { descriptorId: group.descriptorKey, @@ -277,9 +306,14 @@ export function createVerificationController({ function getCredentialOptionsForDescriptor(credentialId: string) { const groups = getRequirementGroups(); - const group = groups.find(g => g.candidates.some(c => c.id === credentialId)); + const group = groups.find(g => + g.candidates.some(c => c.id === credentialId), + ); - assert(group, `Credential ${credentialId} not found in any descriptor group`); + assert( + group, + `Credential ${credentialId} not found in any descriptor group`, + ); const selected = group.candidates.find(c => c.id === credentialId); const alternatives = group.candidates.filter(c => c.id !== credentialId); @@ -292,14 +326,19 @@ export function createVerificationController({ }; } - async function switchCredential(currentCredentialId: string, replacementCredentialId: string) { + async function switchCredential( + currentCredentialId: string, + replacementCredentialId: string, + ) { assert( selectedCredentials.has(currentCredentialId), `Credential ${currentCredentialId} is not currently selected`, ); const options = getCredentialOptionsForDescriptor(currentCredentialId); - const replacement = options.alternatives.find(c => c.id === replacementCredentialId); + const replacement = options.alternatives.find( + c => c.id === replacementCredentialId, + ); assert( replacement, @@ -307,7 +346,7 @@ export function createVerificationController({ ); selectedCredentials.delete(currentCredentialId); - selectedCredentials.set(replacementCredentialId, { credential: replacement }); + selectedCredentials.set(replacementCredentialId, {credential: replacement}); return createPresentation(); } @@ -322,16 +361,26 @@ export function createVerificationController({ for (const descriptor of definition.input_descriptors) { const fields = descriptor.constraints?.fields || []; for (const field of fields) { - if (!field.path) continue; + if (!field.path) { + continue; + } const paths = Array.isArray(field.path) ? field.path : [field.path]; for (const p of paths) { const attr = p.replace('$.', ''); - if (attr && !attributes.includes(attr) && !attr.startsWith('type') && - !attr.startsWith('issuer') && !attr.startsWith('@context') && - !attr.startsWith('proof') && !attr.startsWith('credentialSchema') && - !attr.startsWith('issuanceDate')) { + if ( + attr && + !attributes.includes(attr) && + !attr.startsWith('type') && + !attr.startsWith('issuer') && + !attr.startsWith('@context') && + !attr.startsWith('proof') && + !attr.startsWith('credentialSchema') && + !attr.startsWith('issuanceDate') + ) { // Only include if the credential actually has this attribute - const value = attr.split('.').reduce((obj, key) => obj?.[key], credential); + const value = attr + .split('.') + .reduce((obj, key) => obj?.[key], credential); if (value !== undefined) { attributes.push(attr); } @@ -345,9 +394,7 @@ export function createVerificationController({ } function ensureDescriptorMap(presentation) { - if ( - presentation?.presentation_submission?.descriptor_map?.length > 0 - ) { + if (presentation?.presentation_submission?.descriptor_map?.length > 0) { return presentation; } @@ -356,11 +403,13 @@ export function createVerificationController({ return presentation; } - const descriptorMap = definition.input_descriptors.map((descriptor, idx) => ({ - id: descriptor.id, - format: 'ldp_vp', - path: `$.verifiableCredential[${idx}]`, - })); + const descriptorMap = definition.input_descriptors.map( + (descriptor, idx) => ({ + id: descriptor.id, + format: 'ldp_vp', + path: `$.verifiableCredential[${idx}]`, + }), + ); presentation.presentation_submission = { ...presentation.presentation_submission, @@ -405,8 +454,12 @@ export function createVerificationController({ bbsKvacSelections.map(async sel => { return { credential: sel.credential, - witness: await credentialProvider.getMembershipWitness(sel.credential.id), - attributesToReveal: sel.attributesToReveal || getAttributesToRevealFromTemplate(sel.credential), + witness: await credentialProvider.getMembershipWitness( + sel.credential.id, + ), + attributesToReveal: + sel.attributesToReveal || + getAttributesToRevealFromTemplate(sel.credential), }; }), ); @@ -418,17 +471,22 @@ export function createVerificationController({ credentialsWithWitness.map(c => credentialServiceRPC.deriveVCFromPresentation({ proofRequest: templateJSON, - credentials: [{ - credential: c.credential, - witness: c.witness, - attributesToReveal: c.attributesToReveal, - }], + credentials: [ + { + credential: c.credential, + witness: c.witness, + attributesToReveal: c.attributesToReveal, + }, + ], }), ), ); const derivedCredentials = derivedResults.flat(); - const nonBbsCredentials = await deriveNonBbsCredentials(sdJwtSelections, regularSelections); + const nonBbsCredentials = await deriveNonBbsCredentials( + sdJwtSelections, + regularSelections, + ); const presentation = await assembleSignedPresentation( [...derivedCredentials, ...nonBbsCredentials], keyDoc, @@ -437,7 +495,10 @@ export function createVerificationController({ } // No BBS+/KVAC: handle SD-JWT and regular only - const credentials = await deriveNonBbsCredentials(sdJwtSelections, regularSelections); + const credentials = await deriveNonBbsCredentials( + sdJwtSelections, + regularSelections, + ); const presentation = await assembleSignedPresentation(credentials, keyDoc); return ensureDescriptorMap(presentation); } @@ -487,7 +548,9 @@ export function createVerificationController({ } const groups = getRequirementGroups(); - const group = groups.find(g => g.candidates.some(c => c.id === credentialId)); + const group = groups.find(g => + g.candidates.some(c => c.id === credentialId), + ); if (!group) { return []; } @@ -519,7 +582,7 @@ export function createVerificationController({ .map(field => { const paths = Array.isArray(field.path) ? field.path : [field.path]; let resolvedPath = null; - let value = undefined; + let value; for (const p of paths) { const cleanPath = p.replace('$.', ''); @@ -565,8 +628,14 @@ export function createVerificationController({ isRangeProof, isOptional: field.optional === true, ...(isRangeProof && { - min: field.filter.minimum ?? field.filter.exclusiveMinimum ?? field.filter.formatMinimum, - max: field.filter.maximum ?? field.filter.exclusiveMaximum ?? field.filter.formatMaximum, + min: + field.filter.minimum ?? + field.filter.exclusiveMinimum ?? + field.filter.formatMinimum, + max: + field.filter.maximum ?? + field.filter.exclusiveMaximum ?? + field.filter.formatMaximum, }), }; }) @@ -579,10 +648,15 @@ export function createVerificationController({ for (const group of groups) { credential = group.candidates.find(c => c.id === credentialId); - if (credential) break; + if (credential) { + break; + } } - assert(credential, `Credential ${credentialId} not found in any descriptor group`); + assert( + credential, + `Credential ${credentialId} not found in any descriptor group`, + ); return credentialProvider.isValid(credential); } @@ -596,10 +670,23 @@ export function createVerificationController({ } } - function submitPresentation(presentation) { - return axios - .post(templateJSON.response_url, presentation) - .then(res => res.data); + async function submitPresentation(presentation, maxRetries = 3) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const res = await axios.post(templateJSON.response_url, presentation); + return res.data; + } catch (err) { + const httpStatus = err?.response?.status; + const isRetryable = + !httpStatus || httpStatus === 429 || httpStatus >= 500; + + if (!isRetryable || attempt === maxRetries - 1) { + throw err; + } + const delay = Math.min(1000 * 2 ** attempt, 8000); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } } return { diff --git a/packages/wasm/src/services/credential/bbs-revocation.test.js b/packages/wasm/src/services/credential/bbs-revocation.test.js new file mode 100644 index 00000000..9d422aae --- /dev/null +++ b/packages/wasm/src/services/credential/bbs-revocation.test.js @@ -0,0 +1,147 @@ +import {initializeWasm} from '@docknetwork/crypto-wasm-ts/lib/index'; + +const {blockchainService} = require('../blockchain/service'); +const { + getWitnessDetails, + clearWitnessCache, + getWitnessCacheTTL, + setWitnessCacheTTL, +} = require('./bbs-revocation'); + +const mockAccumulatorResult = { + accumulated: {bytes: new Uint8Array([1, 2, 3])}, + keyRef: ['key1', 1], + lastModified: 100, +}; + +const mockPublicKeyResult = { + bytes: new Uint8Array([4, 5, 6]), +}; + +const mockHistoryResult = { + updates: [{id: {toString: () => '1'}}], +}; + +const createCredential = (statusId = 'dock:accumulator:test-registry-1') => ({ + credentialStatus: { + id: statusId, + revocationId: '42', + }, +}); + +const membershipWitness = JSON.stringify({ + witness: '0xaabbcc', + blockNo: '1', +}); + +describe('bbs-revocation witness cache', () => { + let originalModules; + let originalGetTypes; + + beforeAll(async () => { + await initializeWasm(); + originalModules = blockchainService.modules; + originalGetTypes = blockchainService.getTypesForDIDOrAccumulator; + }); + + afterAll(() => { + blockchainService.modules = originalModules; + blockchainService.getTypesForDIDOrAccumulator = originalGetTypes; + }); + + beforeEach(() => { + clearWitnessCache(); + setWitnessCacheTTL(120_000); // Enable cache for unit tests + + blockchainService.modules = { + accumulator: { + getAccumulator: jest.fn().mockResolvedValue(mockAccumulatorResult), + getPublicKey: jest.fn().mockResolvedValue(mockPublicKeyResult), + accumulatorHistory: jest.fn().mockResolvedValue(mockHistoryResult), + }, + }; + + blockchainService.getTypesForDIDOrAccumulator = jest.fn().mockReturnValue({ + AccumulatorId: {from: jest.fn().mockReturnValue('acc-id')}, + }); + }); + + it('should call blockchain on first request', async () => { + const credential = createCredential(); + await getWitnessDetails(credential, membershipWitness); + + expect( + blockchainService.modules.accumulator.getAccumulator, + ).toHaveBeenCalledTimes(1); + expect( + blockchainService.modules.accumulator.getPublicKey, + ).toHaveBeenCalledTimes(1); + }); + + it('should return cached result on second call with same credential', async () => { + const credential = createCredential(); + + const result1 = await getWitnessDetails(credential, membershipWitness); + const result2 = await getWitnessDetails(credential, membershipWitness); + + expect(result1).toEqual(result2); + expect( + blockchainService.modules.accumulator.getAccumulator, + ).toHaveBeenCalledTimes(1); + expect( + blockchainService.modules.accumulator.getPublicKey, + ).toHaveBeenCalledTimes(1); + }); + + it('should not share cache between different credentials', async () => { + const credential1 = createCredential('dock:accumulator:registry-1'); + const credential2 = createCredential('dock:accumulator:registry-2'); + + await getWitnessDetails(credential1, membershipWitness); + await getWitnessDetails(credential2, membershipWitness); + + expect( + blockchainService.modules.accumulator.getAccumulator, + ).toHaveBeenCalledTimes(2); + }); + + it('should refresh cache after TTL expires', async () => { + const credential = createCredential(); + const originalTTL = getWitnessCacheTTL(); + + await getWitnessDetails(credential, membershipWitness); + + setWitnessCacheTTL(0); // expire immediately + + await getWitnessDetails(credential, membershipWitness); + + expect( + blockchainService.modules.accumulator.getAccumulator, + ).toHaveBeenCalledTimes(2); + + setWitnessCacheTTL(originalTTL); + }); + + it('should clear cache when clearWitnessCache is called', async () => { + const credential = createCredential(); + + await getWitnessDetails(credential, membershipWitness); + clearWitnessCache(); + await getWitnessDetails(credential, membershipWitness); + + expect( + blockchainService.modules.accumulator.getAccumulator, + ).toHaveBeenCalledTimes(2); + }); + + it('should bypass cache when credential has no credentialStatus.id', async () => { + const credential = {credentialStatus: {id: undefined, revocationId: '42'}}; + + await getWitnessDetails(credential, membershipWitness); + await getWitnessDetails(credential, membershipWitness); + + expect( + blockchainService.modules.accumulator.getAccumulator, + ).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/wasm/src/services/credential/bbs-revocation.ts b/packages/wasm/src/services/credential/bbs-revocation.ts index 3d37f5e6..dc93b740 100644 --- a/packages/wasm/src/services/credential/bbs-revocation.ts +++ b/packages/wasm/src/services/credential/bbs-revocation.ts @@ -22,7 +22,14 @@ const trimHexID = id => { return id.substr(2); }; -export const getWitnessDetails = async (credential, _membershipWitness) => { +const blockchainCache = new Map(); +let WITNESS_CACHE_TTL = 0; // Disabled — accumulator can change between cache time and verification + +export const getWitnessCacheTTL = () => WITNESS_CACHE_TTL; +export const setWitnessCacheTTL = (ms: number) => { WITNESS_CACHE_TTL = ms; }; +export const clearWitnessCache = () => blockchainCache.clear(); + +async function fetchBlockchainData(credential, _membershipWitness) { let witness = _membershipWitness; let blockNo; @@ -34,7 +41,6 @@ export const getWitnessDetails = async (credential, _membershipWitness) => { const {credentialStatus} = credential; const registryId = credentialStatus?.id; - const revocationIndex = credentialStatus.revocationId; const queriedAccumulator = await blockchainService.modules.accumulator.getAccumulator( @@ -46,22 +52,12 @@ export const getWitnessDetails = async (credential, _membershipWitness) => { throw new Error('Accumulator not found'); } - const accumulator = PositiveAccumulator.fromAccumulated( - queriedAccumulator.accumulated.bytes, - ); - - const encodedRevId = Encoder.defaultEncodeFunc()(revocationIndex.toString()); - const publicKey = await blockchainService.modules.accumulator.getPublicKey( queriedAccumulator.keyRef[0], queriedAccumulator.keyRef[1], ); - const params = dockAccumulatorParams(); - const pk = new AccumulatorPublicKey(publicKey.bytes); - - const membershipWitness = new VBMembershipWitness(hexToU8a(witness)); - + let updatedWitness = witness; try { const credentialStatusId = credential.credentialStatus.id; const accumulatorId = blockchainService @@ -80,6 +76,10 @@ export const getWitnessDetails = async (credential, _membershipWitness) => { const nextBlockNo = history.updates[blockNoIndex + 1]?.id?.toString(); if (nextBlockNo) { + const revocationIndex = credentialStatus.revocationId; + const encodedRevId = Encoder.defaultEncodeFunc()(revocationIndex.toString()); + const membershipWitness = new VBMembershipWitness(hexToU8a(witness)); + await blockchainService.modules.accumulator.updateWitness( registryId, encodedRevId, @@ -87,20 +87,67 @@ export const getWitnessDetails = async (credential, _membershipWitness) => { nextBlockNo, queriedAccumulator.lastModified, ); + updatedWitness = witness; } } catch (err) { console.error(err); } return { - encodedRevId, - membershipWitness, - pk, - params, - accumulator, + accumulatedBytes: queriedAccumulator.accumulated.bytes, + publicKeyBytes: publicKey.bytes, + witness: updatedWitness, + }; +} + +export const getWitnessDetails = async (credential, _membershipWitness) => { + const cacheKey = credential?.credentialStatus?.id; + let rawData; + + if (cacheKey) { + const cached = blockchainCache.get(cacheKey); + if (cached) { + if (Date.now() - cached.timestamp < WITNESS_CACHE_TTL) { + rawData = cached.data; + } else { + blockchainCache.delete(cacheKey); + } + } + } + + if (!rawData) { + rawData = await fetchBlockchainData(credential, _membershipWitness); + if (cacheKey) { + blockchainCache.set(cacheKey, {data: rawData, timestamp: Date.now()}); + } + } + + const {credentialStatus} = credential; + const revocationIndex = credentialStatus.revocationId; + + return { + encodedRevId: Encoder.defaultEncodeFunc()(revocationIndex.toString()), + membershipWitness: new VBMembershipWitness(hexToU8a(rawData.witness)), + pk: new AccumulatorPublicKey(rawData.publicKeyBytes), + params: dockAccumulatorParams(), + accumulator: PositiveAccumulator.fromAccumulated(rawData.accumulatedBytes), }; }; +export const prefetchWitnessCache = async (credential, _membershipWitness) => { + const cacheKey = credential?.credentialStatus?.id; + if (!cacheKey || !_membershipWitness) { + return; + } + + try { + const rawData = await fetchBlockchainData(credential, _membershipWitness); + blockchainCache.set(cacheKey, {data: rawData, timestamp: Date.now()}); + } catch (err) { + console.warn('Failed to prefetch witness cache:', err.message); + } +}; + export const getIsRevoked = async (credential, _membershipWitness) => { const {encodedRevId, membershipWitness, pk, params, accumulator} = await getWitnessDetails(credential, _membershipWitness); diff --git a/packages/wasm/src/services/credential/pex-helpers.js b/packages/wasm/src/services/credential/pex-helpers.js index 7a9508be..cd6f5b0d 100644 --- a/packages/wasm/src/services/credential/pex-helpers.js +++ b/packages/wasm/src/services/credential/pex-helpers.js @@ -283,35 +283,81 @@ export const shouldSkipAttribute = attributeName => attributesToSkip.some(regex => regex.test(attributeName)); export function getPexRequiredAttributes(pexRequest, selectedCredentials = []) { - return pexRequest.input_descriptors - .map((inputDescriptor, index) => { - return inputDescriptor.constraints.fields - .filter(field => { - if (field.filter || field.optional) { - return false; - } + // Match each credential to its best-fitting descriptor by checking which + // descriptor's fields exist in the credential, rather than relying on + // positional index. This allows credentials to be provided in any order. + return selectedCredentials.map((credential, credIdx) => { + const matchedDescriptor = findMatchingDescriptor( + pexRequest.input_descriptors, + credential, + ); + if (!matchedDescriptor) { + return []; + } + return matchedDescriptor.constraints.fields + .filter(field => { + if (field.filter || field.optional) { + return false; + } - try { - if (!selectedCredentials[index]) { - return false; - } - - const paths = Array.isArray(field.path) - ? field.path.flatMap(singlePath => - JSONPath.paths(selectedCredentials[index], singlePath), - ) - : JSONPath.paths(selectedCredentials[index], field.path); - - return paths.length !== 0; - } catch (error) { - console.error(`Error in field ${field.path}: ${error.message}`); - return false; + try { + const paths = Array.isArray(field.path) + ? field.path.flatMap(singlePath => + JSONPath.paths(credential, singlePath), + ) + : JSONPath.paths(credential, field.path); + + return paths.length !== 0; + } catch (error) { + console.error(`Error in field ${field.path}: ${error.message}`); + return false; + } + }) + .map(field => + getAttributeName({field, selectedCredentials, index: credIdx}), + ) + .filter(attributeName => { + return !shouldSkipAttribute(attributeName); + }); + }); +} + +export function findMatchingDescriptor(inputDescriptors, credential) { + let bestMatch = null; + let bestScore = -1; + + for (const descriptor of inputDescriptors) { + const fields = descriptor.constraints?.fields || []; + if (fields.length === 0) { + continue; + } + + let matched = 0; + for (const field of fields) { + try { + const fieldPaths = Array.isArray(field.path) + ? field.path + : [field.path]; + for (const p of fieldPaths) { + const paths = JSONPath.paths(credential, p); + if (paths.length > 0) { + matched++; + break; } - }) - .map(field => getAttributeName({field, selectedCredentials, index})) - .filter(attributeName => { - return !shouldSkipAttribute(attributeName); - }); - }) - .filter(requiredAttributes => requiredAttributes.length > 0); + } + } catch { + // ignore + } + } + + // Use match ratio so descriptors where all fields match score higher + // than descriptors where only generic fields (e.g. issuer) match + const score = matched / fields.length; + if (score > bestScore) { + bestScore = score; + bestMatch = descriptor; + } + } + + return bestMatch; } diff --git a/packages/wasm/src/services/credential/pex-helpers.test.js b/packages/wasm/src/services/credential/pex-helpers.test.js index 8c0a16cf..8ba948f4 100644 --- a/packages/wasm/src/services/credential/pex-helpers.test.js +++ b/packages/wasm/src/services/credential/pex-helpers.test.js @@ -253,7 +253,7 @@ describe('pex helpers', () => { }, ], ); - expect(result).toEqual([]); + expect(result).toEqual([[]]); }); }); diff --git a/packages/wasm/src/services/credential/service.ts b/packages/wasm/src/services/credential/service.ts index f2e42944..144d24f6 100644 --- a/packages/wasm/src/services/credential/service.ts +++ b/packages/wasm/src/services/credential/service.ts @@ -45,7 +45,7 @@ import {LegoProvingKey} from '@docknetwork/crypto-wasm-ts/lib/legosnark'; import assert from 'assert'; import axios from 'axios'; import {getIsRevoked, getWitnessDetails} from './bbs-revocation'; -import {getPexRequiredAttributes, shouldSkipAttribute} from './pex-helpers'; +import {getPexRequiredAttributes, shouldSkipAttribute, findMatchingDescriptor} from './pex-helpers'; import {didService} from '../dids/service'; import {isSDJWTCredential as checkIsSDJWT, credentialToW3C as convertCredentialToW3C, verifySDJWT, createSDJWTPresentation} from './sd-jwt'; @@ -742,11 +742,31 @@ class CredentialService { }); } - if (proofRequest && hasProvingKey(proofRequest)) { - const {provingKey, provingKeyId} = await fetchProvingKey(proofRequest); + // Filter proof request descriptors to only those matching the provided + // credentials. This ensures correct mapping when credentials are provided + // in a different order than the proof request's input_descriptors. + let filteredProofRequest = proofRequest; + if (proofRequest?.request?.input_descriptors?.length > 1) { + const matchedDescriptors = selectedCredentials.map(credential => + findMatchingDescriptor(proofRequest.request.input_descriptors, credential), + ).filter(Boolean); + + if (matchedDescriptors.length > 0) { + filteredProofRequest = { + ...proofRequest, + request: { + ...proofRequest.request, + input_descriptors: matchedDescriptors, + }, + }; + } + } + + if (filteredProofRequest && hasProvingKey(filteredProofRequest)) { + const {provingKey, provingKeyId} = await fetchProvingKey(filteredProofRequest); descriptorBounds = applyEnforceBounds({ builder: presentation.presBuilder, - proofRequest, + proofRequest: filteredProofRequest, provingKey, provingKeyId, selectedCredentials, @@ -754,9 +774,9 @@ class CredentialService { } let pexRequiredAttributes = []; - if (proofRequest?.request) { + if (filteredProofRequest?.request) { pexRequiredAttributes = getPexRequiredAttributes( - proofRequest.request, + filteredProofRequest.request, selectedCredentials, ); } diff --git a/packages/web/.gitignore b/packages/web/.gitignore index 40ce8b2a..c6d6f270 100644 --- a/packages/web/.gitignore +++ b/packages/web/.gitignore @@ -1,3 +1,4 @@ dist test-results -playwright-report \ No newline at end of file +playwright-report +perf-report.json \ No newline at end of file diff --git a/packages/web/README.md b/packages/web/README.md index 886913c1..866a1bef 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -113,6 +113,21 @@ const credential = await wallet.addCredential('openid-credential-offer://...'); --- +### `removeCredential` + +Remove a credential from the wallet by its ID. + +```javascript +await wallet.removeCredential('https://creds-testnet.truvera.io/credential-id'); +``` + +**Parameters**: +- `credentialId` (string): The ID of the credential to remove. + +**Returns**: `Promise` + +--- + ### `getDID` Get the default Decentralized Identifier (DID) associated with the wallet. @@ -125,26 +140,53 @@ const did = await wallet.getDID(); --- -### `submitPresentation` +### `createPresentation` + +Create a verifiable presentation for a given proof request. When called without `credentials`, the SDK automatically selects the best matching credentials from the wallet (default presentation). When called with `credentials`, uses the specified credentials and attributes (selective disclosure). -Submit a presentation for specific credentials to a proof request URL. +#### Default presentation (auto-selects credentials) ```javascript -const response = await wallet.submitPresentation({ +// Using a proof request URL +const result = await wallet.createPresentation({ + proofRequest: 'https://creds-staging.truvera.io/proof/77ae2c67-678e-4cb6-8c5d-a4dd4a1a19f1' +}); + +// Or using a proof request object +const result = await wallet.createPresentation({ + proofRequest: proofRequestObject, +}); + +// Inspect the presentation +console.log(result.presentation); + +// Submit when ready +const response = await result.submit(); +``` + +#### Selective disclosure (specify credentials and attributes) + +```javascript +const result = await wallet.createPresentation({ + proofRequest: 'https://creds-staging.truvera.io/proof/77ae2c67-678e-4cb6-8c5d-a4dd4a1a19f1', credentials: [ { - id: 'https://creds-testnet.truvera.io/c7f3e722287d1ea98c136ad5df8066209c5e9b44c6251af0860d62e9a3a21a76', + id: 'https://creds-testnet.truvera.io/credential-id', attributesToReveal: ['credentialSubject.fullName', 'credentialSubject.age'] }, ], - proofRequestUrl: 'https://creds-staging.truvera.io/proof/77ae2c67-678e-4cb6-8c5d-a4dd4a1a19f1' }); + +const response = await result.submit(); ``` **Parameters**: -- `credentials` (Array): Array of credential objects to include in the presentation. +- `proofRequest` (string | Object): The proof request — either a URL string or a proof request object. +- `credentials` (Array, optional): Array of credentials to include. When omitted, credentials are auto-selected. - `credentials[].id` (string): The credential ID. - `credentials[].attributesToReveal` (Array): Array of attribute names to reveal from this credential. -- `proofRequestUrl` (string): The URL of the proof request template from the verifier. -**Returns**: `Promise` - The submission response from the verifier. +**Returns**: `Promise` - Result object containing: +- `presentation` (Object): The generated verifiable presentation. +- `verificationController` (Object): The verification controller instance. +- `submit` (Function): Convenience function to submit the presentation to the Certs API. Returns a `Promise` with the submission response. diff --git a/packages/web/package.json b/packages/web/package.json index 02b1f3c5..dafd37ea 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@docknetwork/wallet-sdk-web", - "version": "1.7.6", + "version": "1.7.8", "license": "https://github.com/docknetwork/wallet-sdk/LICENSE", "repository": { "type": "git", @@ -57,5 +57,6 @@ "wasm-loader": "^1.3.0", "webpack": "^5.67.0", "webpack-cli": "^4.10.0" - } + }, + "dependencies": {} } diff --git a/packages/web/src/index.js b/packages/web/src/index.js index 41374f75..c8561ce5 100644 --- a/packages/web/src/index.js +++ b/packages/web/src/index.js @@ -46,7 +46,7 @@ import {blockchainService} from '@docknetwork/wallet-sdk-wasm/src/services/block * @returns {Function} returns.getCredentials - Function to retrieve all credentials * @returns {Function} returns.addCredential - Function to import a credential from an offer URI * @returns {Function} returns.getDID - Function to get the default DID - * @returns {Function} returns.submitPresentation - Function to create and submit a presentation + * @returns {Function} returns.createPresentation - Function to create a presentation with optional auto-selection * * @throws {Error} When neither masterKey nor mnemonic is provided * @throws {Error} When both masterKey and mnemonic are provided @@ -255,122 +255,137 @@ async function initialize({ } }, /** - * Creates and submits a verifiable presentation for the given credentials and proof request. + * Removes a credential from the wallet. * - * This function processes a proof request from a verifier, selects the specified credentials, - * creates a presentation with the requested attributes, and submits it to the verifier. + * @async + * @param {string} credentialId - The ID of the credential to remove + * @returns {Promise} + */ + removeCredential: async credentialId => { + if (!credentialId || typeof credentialId !== 'string') { + throw new Error('Invalid credentialId: Must be a non-empty string'); + } + return await credentialProvider.removeCredential(credentialId); + }, + + /** + * Creates a verifiable presentation for a given proof request. + * + * When called without credentials, automatically filters wallet credentials + * against the proof request template, selects the best matches, and generates + * a presentation with the required attributes revealed (default presentation). + * + * When called with credentials, uses the specified credentials and attributes + * to generate the presentation (selective disclosure). * * @async - * @param {Object} config - Configuration object for presentation submission - * @param {Array} config.credentials - Array of credential objects to include in the presentation + * @param {Object} config - Configuration object + * @param {string|Object} config.proofRequest - Proof request URL string or proof request object + * @param {Array} [config.credentials] - Optional array of credentials to include. + * When omitted, a default presentation is created by auto-selecting credentials. * @param {string} config.credentials[].id - The credential ID - * @param {Object} config.credentials[].attributesToReveal - Map of attribute names to reveal * @param {Array} config.credentials[].attributesToReveal - Array of attribute names to reveal from this credential - * @param {string} config.proofRequestUrl - URL to the proof request template from the verifier * - * @returns {Promise} The submission response from the verifier - * @throws {Error} When credentials array is empty or invalid - * @throws {Error} When proofRequestUrl is invalid - * @throws {Error} When verification controller fails to start + * @returns {Promise} Result object containing: + * @returns {Object} returns.presentation - The generated verifiable presentation + * @returns {Object} returns.verificationController - The verification controller instance + * @returns {Function} returns.submit - Convenience function to submit the presentation to the Certs API + * + * @throws {Error} When proofRequest is not provided + * @throws {Error} When no matching credentials are found in the wallet * @throws {Error} When presentation creation fails - * @throws {Error} When presentation submission fails * * @example - * // Submit a presentation with selective disclosure - * const result = await wallet.submitPresentation({ + * // Default presentation with URL (auto-selects credentials) + * const result = await wallet.createPresentation({ + * proofRequest: 'https://creds-staging.truvera.io/proof/77ae2c67-678e-4cb6-8c5d-a4dd4a1a19f1' + * }); + * + * console.log(result.presentation); + * const response = await result.submit(); + * + * @example + * // Default presentation with proof request object + * const result = await wallet.createPresentation({ + * proofRequest: proofRequestObject, + * }); + * + * @example + * // Selective disclosure (specify credentials and attributes) + * const result = await wallet.createPresentation({ + * proofRequest: 'https://creds-staging.truvera.io/proof/77ae2c67-678e-4cb6-8c5d-a4dd4a1a19f1', * credentials: [ * { - * id: 'https://creds-testnet.truvera.io/c7f3e722287d1ea98c136ad5df8066209c5e9b44c6251af0860d62e9a3a21a76', + * id: 'https://creds-testnet.truvera.io/credential-id', * attributesToReveal: ['credentialSubject.fullName', 'credentialSubject.age'] * }, * ], - * proofRequestUrl: 'https://creds-staging.truvera.io/proof/77ae2c67-678e-4cb6-8c5d-a4dd4a1a19f1' * }); + * + * const response = await result.submit(); */ - submitPresentation: async ({credentials, proofRequestUrl}) => { - // ensure blockchain is connected + createPresentation: async ({credentials, proofRequest}) => { await blockchainService.ensureBlockchainReady(); - // Validate inputs - if ( - !credentials || - !Array.isArray(credentials) || - credentials.length === 0 - ) { + if (!proofRequest) { throw new Error( - 'Invalid credentials: Must provide a non-empty array of credentials', + 'Invalid input: proofRequest is required (URL string or proof request object)', ); } - if (!proofRequestUrl || typeof proofRequestUrl !== 'string') { - throw new Error('Invalid proofRequestUrl: Must be a valid URL string'); - } + const verificationController = createVerificationController({ + wallet, + credentialProvider, + didProvider, + }); - // Validate each credential - for (const credential of credentials) { - if (!credential.id) { - throw new Error( - 'Invalid credential: Each credential must have an id property', - ); - } - if (!credential.attributesToReveal) { + await verificationController.start({template: proofRequest}); + + let presentation; + + if (!credentials) { + presentation = await verificationController.createDefaultPresentation(); + } else { + if (!Array.isArray(credentials) || credentials.length === 0) { throw new Error( - `Invalid credential ${credential.id}: Missing attributesToReveal property`, + 'Invalid credentials: Must provide a non-empty array of credentials', ); } - } - let verificationController; - try { - verificationController = createVerificationController({ - wallet, - credentialProvider, - didProvider, - }); - } catch (error) { - throw new Error( - `Failed to create verification controller: ${error.message}`, - ); - } - - try { - await verificationController.start({template: proofRequestUrl}); - } catch (error) { - throw new Error( - `Failed to start verification with proof request: ${error.message}`, - ); - } + for (const credential of credentials) { + if (!credential.id) { + throw new Error( + 'Invalid credential: Each credential must have an id property', + ); + } + if (!credential.attributesToReveal) { + throw new Error( + `Invalid credential ${credential.id}: Missing attributesToReveal property`, + ); + } + } - // Select credentials and attributes to reveal - try { for (const credentialToPresent of credentials) { - const credential = await credentialProvider.getById( - credentialToPresent.id, - ); + const cred = await credentialProvider.getById(credentialToPresent.id); verificationController.selectedCredentials.set( credentialToPresent.id, { - credential, + credential: cred, attributesToReveal: credentialToPresent.attributesToReveal, }, ); } - } catch (error) { - throw new Error(`Failed to select credentials: ${error.message}`); - } - let presentation; - try { presentation = await verificationController.createPresentation(); - } catch (error) { - throw new Error(`Failed to create presentation: ${error.message}`); } - try { - return await verificationController.submitPresentation(presentation); - } catch (error) { - throw new Error(`Failed to submit presentation: ${error.message}`); - } + return { + presentation, + verificationController, + submit: async () => { + return await verificationController.submitPresentation(presentation); + }, + }; }, }; } diff --git a/packages/web/test/compound-presentation.e2e.test.js b/packages/web/test/compound-presentation.e2e.test.js new file mode 100644 index 00000000..9f1740bd --- /dev/null +++ b/packages/web/test/compound-presentation.e2e.test.js @@ -0,0 +1,309 @@ +const {test, expect} = require('@playwright/test'); + +const allCredentials = [ + { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://ld.truvera.io/credentials/extensions-v1', + 'https://ld.truvera.io/security/bbs23/v1', + { + QuotientLoyaltyCard: 'dk:QuotientLoyaltyCard', + customerID: 'dk:customerID', + customerName: 'dk:customerName', + dk: 'https://ld.truvera.io/credentials#', + loyaltyCardNumber: 'dk:loyaltyCardNumber', + }, + ], + credentialStatus: { + id: 'accumulator:cheqd:testnet:59459883-4e4d-40a0-bb4f-2b7c42405dd3:aac15524-4aff-43bd-b132-1779eb7dcf36', + type: 'DockVBAccumulator2022', + revocationCheck: 'membership', + revocationId: '80', + }, + id: 'https://creds-testnet.truvera.io/e66ee7ff343b74248d80da9a56e32fb38b6956cdee5534fe738ef6fe44a6913a', + type: ['VerifiableCredential', 'QuotientLoyaltyCard'], + credentialSubject: { + id: 'did:key:z6MkqEFT8w27x18qRqf7SBnd882pNWsBZPLnk5hVZfVP9AqG', + customerID: 'CIDT2CQ0E', + customerName: 'Grace Lee', + loyaltyCardNumber: '6048443734212353276', + }, + issuanceDate: '2026-03-31T13:05:53.563Z', + issuer: { + name: 'Quotient', + logo: 'https://img.truvera.io/d689827d822056c6da742080ac6c80ba', + id: 'did:cheqd:testnet:59459883-4e4d-40a0-bb4f-2b7c42405dd3', + }, + credentialSchema: { + id: 'https://schema.truvera.io/QuotientLoyaltyCard-V1-1768398982554.json', + type: 'JsonSchemaValidator2018', + details: + '{"jsonSchema":{"$id":"https://schema.truvera.io/QuotientLoyaltyCard-V1-1768398982554.json","$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":true,"description":"Schema to issue loyalty card credentials to Quotient customers","name":"Quotient Loyalty Card","properties":{"@context":{"type":"string"},"credentialSchema":{"properties":{"details":{"type":"string"},"id":{"type":"string"},"type":{"type":"string"},"version":{"type":"string"}},"type":"object"},"credentialStatus":{"properties":{"id":{"type":"string"},"revocationCheck":{"type":"string"},"revocationId":{"type":"string"},"type":{"type":"string"}},"type":"object"},"credentialSubject":{"properties":{"customerID":{"description":"","title":"Customer ID","type":"string"},"customerName":{"title":"Customer Name","type":"string"},"id":{"description":"A unique identifier of the recipient. Example: DID, email address, national ID number, employee ID, student ID etc.","title":"ID","type":"string"},"loyaltyCardNumber":{"title":"Loyalty card number","type":"string"}},"required":["loyaltyCardNumber","customerID","customerName"],"type":"object"},"cryptoVersion":{"type":"string"},"id":{"type":"string"},"issuanceDate":{"format":"date-time","type":"string"},"issuer":{"properties":{"id":{"type":"string"},"logo":{"type":"string"},"name":{"type":"string"}},"type":"object"},"name":{"type":"string"},"proof":{"properties":{"@context":{"items":[{"properties":{"proof":{"properties":{"@container":{"type":"string"},"@id":{"type":"string"},"@type":{"type":"string"}},"type":"object"},"sec":{"type":"string"}},"type":"object"},{"type":"string"}],"type":"array"},"created":{"format":"date-time","type":"string"},"proofPurpose":{"type":"string"},"type":{"type":"string"},"verificationMethod":{"type":"string"}},"type":"object"},"type":{"type":"string"}},"type":"object"},"parsingOptions":{"defaultDecimalPlaces":4,"defaultMinimumDate":-17592186044415,"defaultMinimumInteger":-4294967295,"useDefaults":true}}', + version: '0.4.0', + }, + name: 'Quotient Loyalty Card', + cryptoVersion: '0.6.0', + proof: { + '@context': [ + { + sec: 'https://w3id.org/security#', + proof: { + '@id': 'sec:proof', + '@type': '@id', + '@container': '@graph', + }, + }, + 'https://ld.truvera.io/security/bbs23/v1', + ], + type: 'Bls12381BBSSignatureDock2023', + created: '2026-03-31T13:05:57Z', + verificationMethod: + 'did:cheqd:testnet:59459883-4e4d-40a0-bb4f-2b7c42405dd3#keys-2', + proofPurpose: 'assertionMethod', + proofValue: + 'z2TZXgpEqiRkAmoB8XRvZZj8KJBpgFv5M9vVj4Nm5uYAj5nw5XwDRN2gYwyVyWtyrwyBtWHbbfhfahWWZN49RwHeedJ7kEuzCrxoSU5HQAKxHPG', + }, + $$accum__witness$$: + '{"blockNo":"d10f73a8-8aed-4e5c-be8d-24105cb494ac","witness":"0x935dd2dcf49a488a9aafda317abc477a8cd8c0edcfdf7f79a6fe4f51b0b352cf4d050822e8d44ecb001a8d98c7a618f2"}', + }, + { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://ld.truvera.io/credentials/extensions-v1', + 'https://ld.truvera.io/security/bbs23/v1', + { + QuotientCustomerIdentity: 'dk:QuotientCustomerIdentity', + address: 'dk:address', + age: 'dk:age', + customerID: 'dk:customerID', + dateOfBirth: 'dk:dateOfBirth', + dk: 'https://ld.truvera.io/credentials#', + fullName: 'dk:fullName', + phoneNumber: 'dk:phoneNumber', + }, + ], + credentialStatus: { + id: 'accumulator:cheqd:testnet:59459883-4e4d-40a0-bb4f-2b7c42405dd3:aac15524-4aff-43bd-b132-1779eb7dcf36', + type: 'DockVBAccumulator2022', + revocationCheck: 'membership', + revocationId: '79', + }, + id: 'https://creds-testnet.truvera.io/76dd0f26a04b4253ac29faf08555e43a5d6329bdc6659dc35a35ee6645750b91', + type: ['VerifiableCredential', 'QuotientCustomerIdentity'], + credentialSubject: { + id: 'did:key:z6MkqEFT8w27x18qRqf7SBnd882pNWsBZPLnk5hVZfVP9AqG', + age: 51, + address: '654 Cedar Ln, Austin, TX 78701', + fullName: 'Grace Lee', + customerID: 'CIDT2CQ0E', + dateOfBirth: '1974-10-17', + phoneNumber: '+14519037960', + }, + issuanceDate: '2026-03-31T13:05:53.563Z', + issuer: { + name: 'Quotient', + logo: 'https://img.truvera.io/d689827d822056c6da742080ac6c80ba', + id: 'did:cheqd:testnet:59459883-4e4d-40a0-bb4f-2b7c42405dd3', + }, + credentialSchema: { + id: 'https://schema.truvera.io/QuotientCustomerIdentity-V1-1768398916249.json', + type: 'JsonSchemaValidator2018', + details: + '{"jsonSchema":{"$id":"https://schema.truvera.io/QuotientCustomerIdentity-V1-1768398916249.json","$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":true,"description":"Schema to issue identity credentials for Quotient customers","name":"Quotient Customer Identity","properties":{"@context":{"type":"string"},"credentialSchema":{"properties":{"details":{"type":"string"},"id":{"type":"string"},"type":{"type":"string"},"version":{"type":"string"}},"type":"object"},"credentialStatus":{"properties":{"id":{"type":"string"},"revocationCheck":{"type":"string"},"revocationId":{"type":"string"},"type":{"type":"string"}},"type":"object"},"credentialSubject":{"properties":{"address":{"title":"Address","type":"string"},"age":{"title":"Age","type":"number"},"customerID":{"title":"Customer ID","type":"string"},"dateOfBirth":{"format":"date","title":"Date of birth","type":"string"},"fullName":{"description":"","title":"Full Name","type":"string"},"id":{"description":"A unique identifier of the recipient. Example: DID, email address, national ID number, employee ID, student ID etc.","title":"ID","type":"string"},"phoneNumber":{"title":"Phone number","type":"string"}},"required":["fullName","age","phoneNumber","customerID"],"type":"object"},"cryptoVersion":{"type":"string"},"id":{"type":"string"},"issuanceDate":{"format":"date-time","type":"string"},"issuer":{"properties":{"id":{"type":"string"},"logo":{"type":"string"},"name":{"type":"string"}},"type":"object"},"name":{"type":"string"},"proof":{"properties":{"@context":{"items":[{"properties":{"proof":{"properties":{"@container":{"type":"string"},"@id":{"type":"string"},"@type":{"type":"string"}},"type":"object"},"sec":{"type":"string"}},"type":"object"},{"type":"string"}],"type":"array"},"created":{"format":"date-time","type":"string"},"proofPurpose":{"type":"string"},"type":{"type":"string"},"verificationMethod":{"type":"string"}},"type":"object"},"type":{"type":"string"}},"type":"object"},"parsingOptions":{"defaultDecimalPlaces":4,"defaultMinimumDate":-17592186044415,"defaultMinimumInteger":-4294967295,"useDefaults":true}}', + version: '0.4.0', + }, + name: 'Quotient Customer Identity', + cryptoVersion: '0.6.0', + proof: { + '@context': [ + { + sec: 'https://w3id.org/security#', + proof: { + '@id': 'sec:proof', + '@type': '@id', + '@container': '@graph', + }, + }, + 'https://ld.truvera.io/security/bbs23/v1', + ], + type: 'Bls12381BBSSignatureDock2023', + created: '2026-03-31T13:05:55Z', + verificationMethod: + 'did:cheqd:testnet:59459883-4e4d-40a0-bb4f-2b7c42405dd3#keys-2', + proofPurpose: 'assertionMethod', + proofValue: + 'z2mhJL8VK9cvvRuBqEkscjqUtXG2nCQ2oLH1NvuUT6uejKMha61Zz7ux61nui1X3cYsjc8nTmQLMgH7T5y4L8PH29eEVykbGtAT5rnxQAUjoxra', + }, + $$accum__witness$$: + '{"blockNo":"d10f73a8-8aed-4e5c-be8d-24105cb494ac","witness":"0xb8ff9c2afe1753a7b455315127e402ec7357110dfda15460f8df55b474220ec5f29de9177dd60dc409751ede1d1f66fb"}', + }, +]; + +const TEST_CONFIG = { + edvUrl: 'https://edv.dock.io', + edvAuthKey: process.env.TEST_EDV_AUTH_KEY, + networkId: 'testnet', +}; + +const certsApiUrl = + process.env.CERTS_API_URL || + process.env.TESTING_API_URL || + 'https://api-staging.dock.io'; +const certsApiKey = process.env.CERTS_API_KEY; + +// Compound proof template: requires both QuotientLoyaltyCard + QuotientCustomerIdentity +const compoundTemplateId = '0dd6f2bc-a035-4d97-b9de-bfa7ccb72b56'; + +async function createProofRequest(templateId, retries = 3) { + for (let attempt = 1; attempt <= retries; attempt++) { + const response = await fetch( + `${certsApiUrl}/proof-templates/${templateId}/request`, + { + method: 'POST', + headers: { + 'DOCK-API-TOKEN': certsApiKey, + 'Content-Type': 'application/json', + }, + body: '{}', + }, + ); + + if (response.status === 429 || response.status >= 500) { + if (attempt < retries) { + await new Promise(r => setTimeout(r, 3000 * attempt)); + continue; + } + } + + const data = await response.json(); + + if (!response.ok) { + throw new Error( + `Proof request failed (${response.status}): ${JSON.stringify(data)}`, + ); + } + + if (data.request && !data.request.id) { + data.request.id = data.id; + } + return data; + } +} + +test.describe('Compound presentation e2e', () => { + test.setTimeout(180_000); + test.describe.configure({retries: 2}); + + let walletHandle; + + test.beforeAll(async ({browser}) => { + const page = await browser.newPage(); + await page.goto('http://localhost:8686/test/test-page.html'); + await page.waitForSelector('#status:has-text("Ready")', {timeout: 60_000}); + + const result = await page.evaluate( + async ({config, credentials}) => { + try { + const initStart = Date.now(); + const {mnemonic} = + await window.TruveraWebWallet.generateCloudWalletMasterKey(); + + const wallet = await window.TruveraWebWallet.initialize({ + edvUrl: config.edvUrl, + edvAuthKey: config.edvAuthKey, + mnemonic, + networkId: config.networkId, + databasePath: 'test-compound-presentation-' + Date.now(), + }); + const initDuration = Date.now() - initStart; + + const addStart = Date.now(); + for (const cred of credentials) { + await wallet.credentialProvider.addCredential(cred); + } + const addDuration = Date.now() - addStart; + + const allCreds = await wallet.getCredentials(); + + window.__testWallet = wallet; + + return { + success: true, + credentialsCount: allCreds.length, + perf: {initDuration, addDuration}, + }; + } catch (error) { + return {success: false, error: error.message, stack: error.stack}; + } + }, + { + config: TEST_CONFIG, + credentials: allCredentials, + }, + ); + + expect(result.success).toBe(true); + expect(result.credentialsCount).toBe(2); + + console.log( + `[PERF] Wallet init: ${result.perf.initDuration}ms | Add 2 credentials: ${result.perf.addDuration}ms`, + ); + + walletHandle = page; + }); + + test.afterAll(async () => { + if (walletHandle) { + await walletHandle.close(); + } + }); + + test('compound: QuotientLoyaltyCard + QuotientCustomerIdentity', async () => { + const proofRequest = await createProofRequest(compoundTemplateId); + + const result = await walletHandle.evaluate(async proofReq => { + try { + const wallet = window.__testWallet; + + const createStart = Date.now(); + const {presentation, submit} = await wallet.createPresentation({ + proofRequest: proofReq, + }); + const createMs = Date.now() - createStart; + + const submitStart = Date.now(); + const submitResult = await submit(); + const submitMs = Date.now() - submitStart; + + return { + success: true, + hasPresentation: !!presentation, + type: presentation?.type, + credentialCount: presentation?.verifiableCredential?.length, + verified: submitResult?.verified, + createMs, + submitMs, + }; + } catch (error) { + return {success: false, error: error.message, stack: error.stack}; + } + }, proofRequest); + + if (!result.success) { + console.error('Compound test error:', result.error); + } + if (result.createMs) { + console.log( + `[PERF] Compound | Create: ${result.createMs}ms | Submit: ${ + result.submitMs + }ms | Total: ${result.createMs + result.submitMs}ms`, + ); + } + + expect(result.success).toBe(true); + expect(result.hasPresentation).toBe(true); + expect(result.type).toEqual(['VerifiablePresentation']); + expect(result.credentialCount).toBe(2); + expect(result.verified).toBe(true); + }); +}); diff --git a/packages/web/test/default-presentation.e2e.test.js b/packages/web/test/default-presentation.e2e.test.js new file mode 100644 index 00000000..6dd5f5e8 --- /dev/null +++ b/packages/web/test/default-presentation.e2e.test.js @@ -0,0 +1,374 @@ +const {test, expect} = require('@playwright/test'); +const fs = require('fs'); +const path = require('path'); + +const universityDegree = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, + '../../../integration-tests/data/default-presentation-tests/university-degree.json', + ), + 'utf-8', + ), +); +const universityDegree2 = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, + '../../../integration-tests/data/default-presentation-tests/university-degree-2.json', + ), + 'utf-8', + ), +); +const equinetCreditScore = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, + '../../../integration-tests/data/default-presentation-tests/equinet-credit-score.json', + ), + 'utf-8', + ), +); + +const TEST_CONFIG = { + edvUrl: 'https://edv.dock.io', + edvAuthKey: process.env.TEST_EDV_AUTH_KEY, + networkId: 'testnet', +}; + +const certsApiUrl = + process.env.TESTING_API_URL || 'https://api-staging.dock.io'; +const certsApiKey = process.env.CERTS_API_KEY; + +// Template IDs (same as integration tests) +const template1 = '91138d81-1b54-4955-a5bc-4e2d90d8f7b1'; // university degree +const template2 = 'a7786f42-03ee-4f49-80b8-ce3027b5a903'; // 1 range proof (creditScore > 50) +const template3 = 'a3e775bb-aaab-4489-b31b-746dc74f76c5'; // any credential with dateOfBirth +const template4 = '9b434ed1-3b65-4b7c-b678-afc7e218f063'; // 2 range proofs + +// Performance report collected across all tests +const perfReport = []; + +async function createProofRequest(templateId, retries = 3) { + for (let attempt = 1; attempt <= retries; attempt++) { + const response = await fetch( + `${certsApiUrl}/proof-templates/${templateId}/request`, + { + method: 'POST', + headers: { + 'DOCK-API-TOKEN': certsApiKey, + 'Content-Type': 'application/json', + }, + body: '{}', + }, + ); + + if (response.status === 429 || response.status >= 500) { + if (attempt < retries) { + await new Promise(r => setTimeout(r, 3000 * attempt)); + continue; + } + } + + const data = await response.json(); + + if (!response.ok) { + throw new Error( + `Proof request failed (${response.status}): ${JSON.stringify(data)}`, + ); + } + + if (data.request && !data.request.id) { + data.request.id = data.id; + } + return data; + } +} + +test.describe('Default presentation e2e', () => { + // Increase timeout for blockchain operations + // Retries handle occasional accumulator proof flakiness when tests run in quick succession + test.setTimeout(180_000); + test.describe.configure({retries: 2}); + + let walletHandle; + + test.beforeAll(async ({browser}) => { + const page = await browser.newPage(); + await page.goto('http://localhost:8686/test/test-page.html'); + await page.waitForSelector('#status:has-text("Ready")', {timeout: 60_000}); + + // Initialize wallet and add credentials in the browser + const result = await page.evaluate( + async ({config, credentials}) => { + try { + const initStart = Date.now(); + const {mnemonic} = + await window.TruveraWebWallet.generateCloudWalletMasterKey(); + + const wallet = await window.TruveraWebWallet.initialize({ + edvUrl: config.edvUrl, + edvAuthKey: config.edvAuthKey, + mnemonic, + networkId: config.networkId, + databasePath: 'test-default-presentation-' + Date.now(), + }); + const initDuration = Date.now() - initStart; + + // Add test credentials via credentialProvider + const addStart = Date.now(); + for (const cred of credentials) { + await wallet.credentialProvider.addCredential(cred); + } + const addDuration = Date.now() - addStart; + + const allCredentials = await wallet.getCredentials(); + + // Store wallet on window for subsequent tests + window.__testWallet = wallet; + + return { + success: true, + credentialsCount: allCredentials.length, + perf: {initDuration, addDuration}, + }; + } catch (error) { + return {success: false, error: error.message, stack: error.stack}; + } + }, + { + config: TEST_CONFIG, + credentials: [universityDegree, universityDegree2, equinetCreditScore], + }, + ); + + expect(result.success).toBe(true); + expect(result.credentialsCount).toBe(3); + + perfReport.push({ + step: 'Setup', + walletInit: `${result.perf.initDuration}ms`, + addCredentials: `${result.perf.addDuration}ms`, + }); + console.log( + `[PERF] Wallet init: ${result.perf.initDuration}ms | Add 3 credentials: ${result.perf.addDuration}ms`, + ); + + // Keep the page alive for all tests + walletHandle = page; + }); + + test.afterAll(async () => { + // Print performance report + console.log('\n========== PERFORMANCE REPORT =========='); + console.log('| Template | Create Presentation | Submit | Total |'); + console.log('|----------|-------------------|--------|-------|'); + for (const entry of perfReport) { + if (entry.template) { + console.log( + `| ${entry.template} | ${entry.createMs}ms | ${entry.submitMs}ms | ${entry.totalMs}ms |`, + ); + } + } + console.log('=========================================\n'); + + // Write report to file + const reportPath = path.resolve(__dirname, '../perf-report.json'); + fs.writeFileSync(reportPath, JSON.stringify(perfReport, null, 2)); + console.log(`Performance report saved to: ${reportPath}`); + + if (walletHandle) { + await walletHandle.close(); + } + }); + + test('template 1: university degree', async () => { + const proofRequest = await createProofRequest(template1); + + const result = await walletHandle.evaluate(async proofReq => { + try { + const wallet = window.__testWallet; + + const createStart = Date.now(); + const {presentation, submit} = await wallet.createPresentation({ + proofRequest: proofReq, + }); + const createMs = Date.now() - createStart; + + const submitStart = Date.now(); + const submitResult = await submit(); + const submitMs = Date.now() - submitStart; + + return { + success: true, + hasPresentation: !!presentation, + type: presentation?.type, + credentialCount: presentation?.verifiableCredential?.length, + verified: submitResult?.verified, + perf: {createMs, submitMs, totalMs: createMs + submitMs}, + }; + } catch (error) { + return {success: false, error: error.message, stack: error.stack}; + } + }, proofRequest); + + if (result.perf) { + perfReport.push({template: 'T1: University degree', ...result.perf}); + console.log( + `[PERF] T1 | Create: ${result.perf.createMs}ms | Submit: ${result.perf.submitMs}ms | Total: ${result.perf.totalMs}ms`, + ); + } + + expect(result.success).toBe(true); + expect(result.hasPresentation).toBe(true); + expect(result.type).toEqual(['VerifiablePresentation']); + expect(result.credentialCount).toBe(1); + expect(result.verified).toBe(true); + }); + + test('template 2: range proof (creditScore > 50)', async () => { + const proofRequest = await createProofRequest(template2); + + const result = await walletHandle.evaluate(async proofReq => { + try { + const wallet = window.__testWallet; + + const createStart = Date.now(); + const {presentation, submit} = await wallet.createPresentation({ + proofRequest: proofReq, + }); + const createMs = Date.now() - createStart; + + const submitStart = Date.now(); + const submitResult = await submit(); + const submitMs = Date.now() - submitStart; + + return { + success: true, + hasPresentation: !!presentation, + type: presentation?.type, + credentialCount: presentation?.verifiableCredential?.length, + verified: submitResult?.verified, + perf: {createMs, submitMs, totalMs: createMs + submitMs}, + }; + } catch (error) { + return {success: false, error: error.message, stack: error.stack}; + } + }, proofRequest); + + if (result.perf) { + perfReport.push({ + template: 'T2: Range proof (creditScore)', + ...result.perf, + }); + console.log( + `[PERF] T2 | Create: ${result.perf.createMs}ms | Submit: ${result.perf.submitMs}ms | Total: ${result.perf.totalMs}ms`, + ); + } + + expect(result.success).toBe(true); + expect(result.hasPresentation).toBe(true); + expect(result.type).toEqual(['VerifiablePresentation']); + expect(result.credentialCount).toBe(1); + expect(result.verified).toBe(true); + }); + + test('template 3: any credential with dateOfBirth', async () => { + const proofRequest = await createProofRequest(template3); + + const result = await walletHandle.evaluate(async proofReq => { + try { + const wallet = window.__testWallet; + + const createStart = Date.now(); + const {presentation, submit} = await wallet.createPresentation({ + proofRequest: proofReq, + }); + const createMs = Date.now() - createStart; + + const submitStart = Date.now(); + const submitResult = await submit(); + const submitMs = Date.now() - submitStart; + + return { + success: true, + hasPresentation: !!presentation, + type: presentation?.type, + credentialCount: presentation?.verifiableCredential?.length, + verified: submitResult?.verified, + perf: {createMs, submitMs, totalMs: createMs + submitMs}, + }; + } catch (error) { + return { + success: false, + error: error.message, + stack: error.stack, + responseData: error.response?.data, + }; + } + }, proofRequest); + + if (!result.success) { + console.error('Template 3 error:', result.error, result.responseData); + } + if (result.perf) { + perfReport.push({ + template: 'T3: Any cred with dateOfBirth', + ...result.perf, + }); + console.log( + `[PERF] T3 | Create: ${result.perf.createMs}ms | Submit: ${result.perf.submitMs}ms | Total: ${result.perf.totalMs}ms`, + ); + } + + expect(result.success).toBe(true); + expect(result.hasPresentation).toBe(true); + expect(result.type).toEqual(['VerifiablePresentation']); + expect(result.credentialCount).toBe(1); + expect(result.verified).toBe(true); + }); + + test('template 4: 2 range proofs', async () => { + const proofRequest = await createProofRequest(template4); + + const result = await walletHandle.evaluate(async proofReq => { + try { + const wallet = window.__testWallet; + + const createStart = Date.now(); + const {presentation, submit} = await wallet.createPresentation({ + proofRequest: proofReq, + }); + const createMs = Date.now() - createStart; + + const submitStart = Date.now(); + const submitResult = await submit(); + const submitMs = Date.now() - submitStart; + + return { + success: true, + hasPresentation: !!presentation, + type: presentation?.type, + credentialCount: presentation?.verifiableCredential?.length, + verified: submitResult?.verified, + perf: {createMs, submitMs, totalMs: createMs + submitMs}, + }; + } catch (error) { + return {success: false, error: error.message, stack: error.stack}; + } + }, proofRequest); + + if (result.perf) { + perfReport.push({template: 'T4: 2 range proofs', ...result.perf}); + console.log( + `[PERF] T4 | Create: ${result.perf.createMs}ms | Submit: ${result.perf.submitMs}ms | Total: ${result.perf.totalMs}ms`, + ); + } + + expect(result.success).toBe(true); + expect(result.hasPresentation).toBe(true); + expect(result.type).toEqual(['VerifiablePresentation']); + expect(result.credentialCount).toBeGreaterThanOrEqual(1); + expect(result.verified).toBe(true); + }); +});