Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remote proof bundles #1022

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/legacy/core/App/contexts/configuration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface ConfigurationContext {
credentialEmptyList: React.FC<EmptyListProps>
developer: React.FC
OCABundleResolver: OCABundleResolverType
proofTemplateBaseUrl?: string
scan: React.FC<ScanProps>
useBiometry: React.FC
record: React.FC<RecordProps>
Expand Down
32 changes: 25 additions & 7 deletions packages/legacy/core/App/hooks/proof-request-templates.ts
Original file line number Diff line number Diff line change
@@ -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<ProofRequestTemplate> => {
const [store] = useStore()
const { proofRequestTemplates } = useConfiguration()
return (proofRequestTemplates && proofRequestTemplates(store.preferences.acceptDevCredentials)) || []
const [proofRequestTemplates, setProofRequestTemplates] = useState<ProofRequestTemplate[]>([])
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<ProofRequestTemplate | undefined>(undefined)
const { proofTemplateBaseUrl } = useConfiguration()
const resolver = useRemoteProofBundleResolver(proofTemplateBaseUrl)
useEffect(() => {
resolver.resolveById(templateId, store.preferences.acceptDevCredentials).then((template) => {

Check warning on line 29 in packages/legacy/core/App/hooks/proof-request-templates.ts

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/hooks/proof-request-templates.ts#L25-L29

Added lines #L25 - L29 were not covered by tests
if (template) {
setProofRequestTemplate(applyTemplateMarkers(template))

Check warning on line 31 in packages/legacy/core/App/hooks/proof-request-templates.ts

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/hooks/proof-request-templates.ts#L31

Added line #L31 was not covered by tests
}
})
}, [])
return proofRequestTemplate

Check warning on line 35 in packages/legacy/core/App/hooks/proof-request-templates.ts

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/hooks/proof-request-templates.ts#L35

Added line #L35 was not covered by tests
}
14 changes: 9 additions & 5 deletions packages/legacy/core/App/screens/ProofRequestDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,11 @@
>(undefined)

const template = useTemplate(templateId)
if (!template) {
throw new Error('Unable to find proof request template')
}

useEffect(() => {
if (!template) {
return

Check warning on line 263 in packages/legacy/core/App/screens/ProofRequestDetails.tsx

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/screens/ProofRequestDetails.tsx#L263

Added line #L263 was not covered by tests
}
const attributes = template.payload.type === ProofRequestType.AnonCreds ? template.payload.data : []

OCABundleResolver.resolve({ identifiers: { templateId }, language: i18n.language }).then((bundle) => {
Expand All @@ -282,7 +282,7 @@
setMeta(metaOverlay)
setAttributes(attributes)
})
}, [templateId])
}, [templateId, template])

const onlyNumberRegex = /^\d+$/

Expand All @@ -305,6 +305,10 @@
)

const useProofRequest = useCallback(async () => {
if (!template) {
return

Check warning on line 309 in packages/legacy/core/App/screens/ProofRequestDetails.tsx

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/screens/ProofRequestDetails.tsx#L309

Added line #L309 was not covered by tests
}

if (invalidPredicate) {
setInvalidPredicate({ visible: true, predicate: invalidPredicate.predicate })
return
Expand All @@ -323,7 +327,7 @@
// 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 })
Expand Down
9 changes: 4 additions & 5 deletions packages/legacy/core/App/screens/ProofRequesting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,6 @@
},
})

if (!template) {
throw new Error('Unable to find proof request template')
}

const createProofRequest = useCallback(async () => {
try {
setMessage(undefined)
Expand Down Expand Up @@ -165,6 +161,9 @@
}, [isFocused])

useEffect(() => {
if (!template) {
return

Check warning on line 165 in packages/legacy/core/App/screens/ProofRequesting.tsx

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/screens/ProofRequesting.tsx#L165

Added line #L165 was not covered by tests
}
const sendAsyncProof = async () => {
if (record && record.state === DidExchangeState.Completed) {
//send haptic feedback to verifier that connection is completed
Expand All @@ -181,7 +180,7 @@
}
}
sendAsyncProof()
}, [record])
}, [record, template])

useEffect(() => {
if (proofRecord && (isPresentationReceived(proofRecord) || isPresentationFailed(proofRecord))) {
Expand Down
136 changes: 136 additions & 0 deletions packages/legacy/core/App/utils/proofBundle.ts
Original file line number Diff line number Diff line change
@@ -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)]
wadeking98 marked this conversation as resolved.
Show resolved Hide resolved

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<ProofRequestTemplate[] | undefined>
resolveById: (templateId: string, acceptDevRestrictions: boolean) => Promise<ProofRequestTemplate | undefined>
}

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<ProofRequestTemplate[] | undefined> {
if (this.templateData) {
let templateData = this.templateData

Check warning on line 83 in packages/legacy/core/App/utils/proofBundle.ts

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/utils/proofBundle.ts#L83

Added line #L83 was not covered by tests
if (acceptDevRestrictions) {
templateData = applyDevRestrictions(templateData)

Check warning on line 85 in packages/legacy/core/App/utils/proofBundle.ts

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/utils/proofBundle.ts#L85

Added line #L85 was not covered by tests
}
return Promise.resolve(templateData)

Check warning on line 87 in packages/legacy/core/App/utils/proofBundle.ts

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/utils/proofBundle.ts#L87

Added line #L87 was not covered by tests
}
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

Check warning on line 98 in packages/legacy/core/App/utils/proofBundle.ts

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/utils/proofBundle.ts#L98

Added line #L98 was not covered by tests
}
})
}
public async resolveById(
templateId: string,
acceptDevRestrictions: boolean
): Promise<ProofRequestTemplate | undefined> {

Check warning on line 105 in packages/legacy/core/App/utils/proofBundle.ts

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/utils/proofBundle.ts#L105

Added line #L105 was not covered by tests
if (!this.templateData) {
return (await this.resolve(acceptDevRestrictions))?.find((template) => template.id === templateId)
} else {
let templateData = this.templateData

Check warning on line 109 in packages/legacy/core/App/utils/proofBundle.ts

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/utils/proofBundle.ts#L107-L109

Added lines #L107 - L109 were not covered by tests
if (acceptDevRestrictions) {
templateData = applyDevRestrictions(templateData)

Check warning on line 111 in packages/legacy/core/App/utils/proofBundle.ts

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/utils/proofBundle.ts#L111

Added line #L111 was not covered by tests
}
const template = templateData.find((template) => template.id === templateId)
return template

Check warning on line 114 in packages/legacy/core/App/utils/proofBundle.ts

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/utils/proofBundle.ts#L113-L114

Added lines #L113 - L114 were not covered by tests
}
}
}

export class DefaultProofBundleResolver implements ProofBundleResolverType {
private proofRequestTemplates
public constructor() {
const { proofRequestTemplates } = useConfiguration()
this.proofRequestTemplates = proofRequestTemplates ?? useProofRequestTemplates
}
public async resolve(acceptDevRestrictions: boolean): Promise<ProofRequestTemplate[]> {
return Promise.resolve(this.proofRequestTemplates(acceptDevRestrictions))
}
public async resolveById(
templateId: string,
acceptDevRestrictions: boolean
): Promise<ProofRequestTemplate | undefined> {
return Promise.resolve(
this.proofRequestTemplates(acceptDevRestrictions).find((template) => template.id === templateId)

Check warning on line 133 in packages/legacy/core/App/utils/proofBundle.ts

View check run for this annotation

Codecov / codecov/patch

packages/legacy/core/App/utils/proofBundle.ts#L131-L133

Added lines #L131 - L133 were not covered by tests
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -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()
})

Expand Down
Loading
Loading