From bf5cf8aacff7acdaacee06e7c6755bf42c3f31de Mon Sep 17 00:00:00 2001 From: Wade King Date: Wed, 8 Nov 2023 15:18:44 -0800 Subject: [PATCH] feat: remote proof bundles (#1022) Signed-off-by: wadeking98 --- .../core/App/contexts/configuration.tsx | 1 + .../core/App/hooks/proof-request-templates.ts | 32 ++++- .../core/App/screens/ProofRequestDetails.tsx | 14 +- .../core/App/screens/ProofRequesting.tsx | 9 +- packages/legacy/core/App/utils/proofBundle.ts | 136 ++++++++++++++++++ .../screens/ProofRequestDetails.test.tsx | 73 +++++++++- .../ProofRequestDetails.test.tsx.snap | 24 +++- 7 files changed, 265 insertions(+), 24 deletions(-) create mode 100644 packages/legacy/core/App/utils/proofBundle.ts diff --git a/packages/legacy/core/App/contexts/configuration.tsx b/packages/legacy/core/App/contexts/configuration.tsx index 74ce77b1a7..b9ac8e8ed5 100644 --- a/packages/legacy/core/App/contexts/configuration.tsx +++ b/packages/legacy/core/App/contexts/configuration.tsx @@ -32,6 +32,7 @@ export interface ConfigurationContext { credentialEmptyList: React.FC developer: React.FC OCABundleResolver: OCABundleResolverType + proofTemplateBaseUrl?: string scan: React.FC useBiometry: React.FC record: React.FC diff --git a/packages/legacy/core/App/hooks/proof-request-templates.ts b/packages/legacy/core/App/hooks/proof-request-templates.ts index 33a2bd94a9..f2156d53d3 100644 --- a/packages/legacy/core/App/hooks/proof-request-templates.ts +++ b/packages/legacy/core/App/hooks/proof-request-templates.ts @@ -1,18 +1,36 @@ +import { useEffect, useState } from 'react' + import { ProofRequestTemplate } from '../../verifier' import { useConfiguration } from '../contexts/configuration' import { useStore } from '../contexts/store' +import { applyTemplateMarkers, useRemoteProofBundleResolver } from '../utils/proofBundle' export const useTemplates = (): Array => { const [store] = useStore() - const { proofRequestTemplates } = useConfiguration() - return (proofRequestTemplates && proofRequestTemplates(store.preferences.acceptDevCredentials)) || [] + const [proofRequestTemplates, setProofRequestTemplates] = useState([]) + const { proofTemplateBaseUrl } = useConfiguration() + const resolver = useRemoteProofBundleResolver(proofTemplateBaseUrl) + useEffect(() => { + resolver.resolve(store.preferences.acceptDevCredentials).then((templates) => { + if (templates) { + setProofRequestTemplates(applyTemplateMarkers(templates)) + } + }) + }, []) + return proofRequestTemplates } export const useTemplate = (templateId: string): ProofRequestTemplate | undefined => { - const { proofRequestTemplates } = useConfiguration() const [store] = useStore() - return ( - proofRequestTemplates && - proofRequestTemplates(store.preferences.acceptDevCredentials).find((template) => template.id === templateId) - ) + const [proofRequestTemplate, setProofRequestTemplate] = useState(undefined) + const { proofTemplateBaseUrl } = useConfiguration() + const resolver = useRemoteProofBundleResolver(proofTemplateBaseUrl) + useEffect(() => { + resolver.resolveById(templateId, store.preferences.acceptDevCredentials).then((template) => { + if (template) { + setProofRequestTemplate(applyTemplateMarkers(template)) + } + }) + }, []) + return proofRequestTemplate } diff --git a/packages/legacy/core/App/screens/ProofRequestDetails.tsx b/packages/legacy/core/App/screens/ProofRequestDetails.tsx index 0c79492a7f..1d361cb162 100644 --- a/packages/legacy/core/App/screens/ProofRequestDetails.tsx +++ b/packages/legacy/core/App/screens/ProofRequestDetails.tsx @@ -257,11 +257,11 @@ const ProofRequestDetails: React.FC = ({ route, naviga >(undefined) const template = useTemplate(templateId) - if (!template) { - throw new Error('Unable to find proof request template') - } useEffect(() => { + if (!template) { + return + } const attributes = template.payload.type === ProofRequestType.AnonCreds ? template.payload.data : [] OCABundleResolver.resolve({ identifiers: { templateId }, language: i18n.language }).then((bundle) => { @@ -282,7 +282,7 @@ const ProofRequestDetails: React.FC = ({ route, naviga setMeta(metaOverlay) setAttributes(attributes) }) - }, [templateId]) + }, [templateId, template]) const onlyNumberRegex = /^\d+$/ @@ -305,6 +305,10 @@ const ProofRequestDetails: React.FC = ({ route, naviga ) const useProofRequest = useCallback(async () => { + if (!template) { + return + } + if (invalidPredicate) { setInvalidPredicate({ visible: true, predicate: invalidPredicate.predicate }) return @@ -323,7 +327,7 @@ const ProofRequestDetails: React.FC = ({ route, naviga // Else redirect to the screen with connectionless request navigation.navigate(Screens.ProofRequesting, { templateId, predicateValues: customPredicateValues }) } - }, [agent, templateId, connectionId, customPredicateValues, invalidPredicate]) + }, [agent, template, templateId, connectionId, customPredicateValues, invalidPredicate]) const showTemplateUsageHistory = useCallback(async () => { navigation.navigate(Screens.ProofRequestUsageHistory, { templateId }) diff --git a/packages/legacy/core/App/screens/ProofRequesting.tsx b/packages/legacy/core/App/screens/ProofRequesting.tsx index 47b2fa05fd..5278f82009 100644 --- a/packages/legacy/core/App/screens/ProofRequesting.tsx +++ b/packages/legacy/core/App/screens/ProofRequesting.tsx @@ -123,10 +123,6 @@ const ProofRequesting: React.FC = ({ route, navigation }) }, }) - if (!template) { - throw new Error('Unable to find proof request template') - } - const createProofRequest = useCallback(async () => { try { setMessage(undefined) @@ -165,6 +161,9 @@ const ProofRequesting: React.FC = ({ route, navigation }) }, [isFocused]) useEffect(() => { + if (!template) { + return + } const sendAsyncProof = async () => { if (record && record.state === DidExchangeState.Completed) { //send haptic feedback to verifier that connection is completed @@ -181,7 +180,7 @@ const ProofRequesting: React.FC = ({ route, navigation }) } } sendAsyncProof() - }, [record]) + }, [record, template]) useEffect(() => { if (proofRecord && (isPresentationReceived(proofRecord) || isPresentationFailed(proofRecord))) { diff --git a/packages/legacy/core/App/utils/proofBundle.ts b/packages/legacy/core/App/utils/proofBundle.ts new file mode 100644 index 0000000000..2c27b8460b --- /dev/null +++ b/packages/legacy/core/App/utils/proofBundle.ts @@ -0,0 +1,136 @@ +import axios from 'axios' + +import { AnonCredsProofRequestTemplatePayload, ProofRequestTemplate, useProofRequestTemplates } from '../../verifier' +import { useConfiguration } from '../contexts/configuration' + +const calculatePreviousYear = (yearOffset: number) => { + const pastDate = new Date() + pastDate.setFullYear(pastDate.getFullYear() + yearOffset) + return parseInt(pastDate.toISOString().split('T')[0].replace(/-/g, '')) +} + +export const applyTemplateMarkers = (templates: any): any => { + if (!templates) return templates + const markerActions: { [key: string]: (param: string) => string } = { + now: () => Math.floor(new Date().getTime() / 1000).toString(), + currentDate: (offset: string) => calculatePreviousYear(parseInt(offset)).toString(), + } + let templateString = JSON.stringify(templates) + // regex to find all markers in the template so we can replace them with computed values + const markers = [...templateString.matchAll(/"@\{(\w+)(?:\((\S*)\))?\}"/gm)] + + markers.forEach((marker) => { + const markerValue = markerActions[marker[1] as string](marker[2]) + templateString = templateString.replace(marker[0], markerValue) + }) + return JSON.parse(templateString) +} + +export const applyDevRestrictions = (templates: ProofRequestTemplate[]): ProofRequestTemplate[] => { + return templates.map((temp) => { + return { + ...temp, + payload: { + ...temp.payload, + data: (temp.payload as AnonCredsProofRequestTemplatePayload).data.map((data) => { + return { + ...data, + requestedAttributes: data.requestedAttributes?.map((attr) => { + return { + ...attr, + restrictions: [...(attr.restrictions ?? []), ...(attr.devRestrictions ?? [])], + devRestrictions: [], + } + }), + requestedPredicates: data.requestedPredicates?.map((pred) => { + return { + ...pred, + restrictions: [...(pred.restrictions ?? []), ...(pred.devRestrictions ?? [])], + devRestrictions: [], + } + }), + } + }), + }, + } + }) +} + +export interface ProofBundleResolverType { + resolve: (acceptDevRestrictions: boolean) => Promise + resolveById: (templateId: string, acceptDevRestrictions: boolean) => Promise +} + +export const useRemoteProofBundleResolver = (indexFileBaseUrl: string | undefined): ProofBundleResolverType => { + if (indexFileBaseUrl) { + return new RemoteProofBundleResolver(indexFileBaseUrl) + } else { + return new DefaultProofBundleResolver() + } +} + +export class RemoteProofBundleResolver implements ProofBundleResolverType { + private remoteServer + private templateData: ProofRequestTemplate[] | undefined + + public constructor(indexFileBaseUrl: string) { + this.remoteServer = axios.create({ + baseURL: indexFileBaseUrl, + }) + } + public async resolve(acceptDevRestrictions: boolean): Promise { + if (this.templateData) { + let templateData = this.templateData + if (acceptDevRestrictions) { + templateData = applyDevRestrictions(templateData) + } + return Promise.resolve(templateData) + } + return this.remoteServer.get('proof-templates.json').then((response) => { + try { + let templateData: ProofRequestTemplate[] = response.data + this.templateData = templateData + if (acceptDevRestrictions) { + templateData = applyDevRestrictions(templateData) + } + return templateData + } catch (error) { + return undefined + } + }) + } + public async resolveById( + templateId: string, + acceptDevRestrictions: boolean + ): Promise { + if (!this.templateData) { + return (await this.resolve(acceptDevRestrictions))?.find((template) => template.id === templateId) + } else { + let templateData = this.templateData + if (acceptDevRestrictions) { + templateData = applyDevRestrictions(templateData) + } + const template = templateData.find((template) => template.id === templateId) + return template + } + } +} + +export class DefaultProofBundleResolver implements ProofBundleResolverType { + private proofRequestTemplates + public constructor() { + const { proofRequestTemplates } = useConfiguration() + this.proofRequestTemplates = proofRequestTemplates ?? useProofRequestTemplates + } + public async resolve(acceptDevRestrictions: boolean): Promise { + return Promise.resolve(this.proofRequestTemplates(acceptDevRestrictions)) + } + public async resolveById( + templateId: string, + acceptDevRestrictions: boolean + ): Promise { + return Promise.resolve( + this.proofRequestTemplates(acceptDevRestrictions).find((template) => template.id === templateId) + ) + } +} diff --git a/packages/legacy/core/__tests__/screens/ProofRequestDetails.test.tsx b/packages/legacy/core/__tests__/screens/ProofRequestDetails.test.tsx index 9186f298f4..0e47d8c1be 100644 --- a/packages/legacy/core/__tests__/screens/ProofRequestDetails.test.tsx +++ b/packages/legacy/core/__tests__/screens/ProofRequestDetails.test.tsx @@ -6,9 +6,11 @@ import React from 'react' import { ConfigurationContext } from '../../App/contexts/configuration' import { NetworkProvider } from '../../App/contexts/network' import configurationContext from '../contexts/configuration' -import { useProofRequestTemplates } from '../../verifier/request-templates' import ProofRequestDetails from '../../App/screens/ProofRequestDetails' -import { testIdWithKey } from '../../App' +import { ProofRequestType, testIdWithKey } from '../../App' +import { useTemplates, useTemplate } from '../../App/hooks/proof-request-templates' +import axios from 'axios' +import { applyTemplateMarkers, useRemoteProofBundleResolver } from '../../App/utils/proofBundle' jest.mock('react-native-permissions', () => require('react-native-permissions/mock')) jest.mock('@react-native-community/netinfo', () => mockRNCNetInfo) @@ -23,12 +25,60 @@ jest.mock('@react-navigation/native', () => { return require('../../__mocks__/custom/@react-navigation/native') }) // eslint-disable-next-line @typescript-eslint/no-empty-function -jest.mock('react-native-localize', () => {}) +jest.mock('react-native-localize', () => { }) jest.mock('react-native-device-info', () => () => jest.fn()) jest.useFakeTimers({ legacyFakeTimers: true }) jest.spyOn(global, 'setTimeout') -const templates = useProofRequestTemplates(false) + +jest.mock('../../App/hooks/proof-request-templates', () => ({ + useTemplates: jest.fn(), + useTemplate: jest.fn(), +})) + +jest.mock('axios', () => ({ create: jest.fn() })) + + +const templates = [ + { + id: 'Aries:5:StudentFullName:0.0.1:indy', + name: 'Student full name', + description: 'Verify the full name of a student', + version: '0.0.1', + payload: { + type: ProofRequestType.AnonCreds, + data: [ + { + schema: 'XUxBrVSALWHLeycAUhrNr9:3:CL:26293:Student Card', + requestedAttributes: [ + { + names: ['student_first_name', 'student_last_name'], + restrictions: [{ cred_def_id: 'XUxBrVSALWHLeycAUhrNr9:3:CL:26293:student_card' }], + devRestrictions: [{ schema_name: 'student_card' }], + non_revoked: { to: "@{now}" }, + }, + ], + requestedPredicates: [ + { + name: 'expiry_date', + predicateType: '>=', + predicateValue: "@{currentDate(0)}", + restrictions: [{ cred_def_id: 'XUxBrVSALWHLeycAUhrNr9:3:CL:26293:student_card' }], + devRestrictions: [{ schema_name: 'student_card' }], + }, + ], + }, + ], + }, + } +] + +// @ts-ignore +axios.create.mockImplementation(() => ({ get: () => Promise.resolve({ data: templates }) })) +// @ts-ignore +useTemplates.mockImplementation(() => templates) +// @ts-ignore +useTemplate.mockImplementation((id) => templates[0]) const templateId = templates[0].id const connectionId = 'test' const navigation = useNavigation() @@ -48,9 +98,22 @@ describe('ProofRequestDetails Component', () => { ) } + test('Proof bundle resolver works correctly', async () => { + const resolver = useRemoteProofBundleResolver("http://localhost:3000") + const bundle = await resolver.resolve(true) + expect((bundle?.[0].payload.data[0] as any).requestedAttributes[0].restrictions.length).toBe(2) + }) + + test("Template is parsed correctly", async () => { + const template = templates[0] + const parsedTemplate = applyTemplateMarkers(template) + expect(parsedTemplate.payload.data[0].requestedAttributes[0].non_revoked.to).not.toBe("@{now}") + expect(parsedTemplate.payload.data[0].requestedPredicates[0].predicateValue.to).not.toBe("@{currentDate(0)}") + }) + test('Renders correctly', async () => { const tree = renderView({ templateId }) - await act(async () => {}) + await act(async () => { }) expect(tree).toMatchSnapshot() }) diff --git a/packages/legacy/core/__tests__/screens/__snapshots__/ProofRequestDetails.test.tsx.snap b/packages/legacy/core/__tests__/screens/__snapshots__/ProofRequestDetails.test.tsx.snap index a90a71d98d..5f416cad60 100644 --- a/packages/legacy/core/__tests__/screens/__snapshots__/ProofRequestDetails.test.tsx.snap +++ b/packages/legacy/core/__tests__/screens/__snapshots__/ProofRequestDetails.test.tsx.snap @@ -35,15 +35,35 @@ exports[`ProofRequestDetails Component Renders correctly 1`] = ` Object { "requestedAttributes": Array [ Object { - "name": "student_first_name", + "devRestrictions": Array [ + Object { + "schema_name": "student_card", + }, + ], + "names": Array [ + "student_first_name", + "student_last_name", + ], + "non_revoked": Object { + "to": "@{now}", + }, "restrictions": Array [ Object { "cred_def_id": "XUxBrVSALWHLeycAUhrNr9:3:CL:26293:student_card", }, ], }, + ], + "requestedPredicates": Array [ Object { - "name": "student_last_name", + "devRestrictions": Array [ + Object { + "schema_name": "student_card", + }, + ], + "name": "expiry_date", + "predicateType": ">=", + "predicateValue": "@{currentDate(0)}", "restrictions": Array [ Object { "cred_def_id": "XUxBrVSALWHLeycAUhrNr9:3:CL:26293:student_card",