Skip to content

Commit

Permalink
feat: remote proof bundles (#1022)
Browse files Browse the repository at this point in the history
Signed-off-by: wadeking98 <[email protected]>
  • Loading branch information
wadeking98 authored Nov 8, 2023
1 parent 6c7a4b3 commit bf5cf8a
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 24 deletions.
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) => {
if (template) {
setProofRequestTemplate(applyTemplateMarkers(template))
}
})
}, [])
return proofRequestTemplate
}
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 @@ const ProofRequestDetails: React.FC<ProofRequestDetailsProps> = ({ 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) => {
Expand All @@ -282,7 +282,7 @@ const ProofRequestDetails: React.FC<ProofRequestDetailsProps> = ({ route, naviga
setMeta(metaOverlay)
setAttributes(attributes)
})
}, [templateId])
}, [templateId, template])

const onlyNumberRegex = /^\d+$/

Expand All @@ -305,6 +305,10 @@ const ProofRequestDetails: React.FC<ProofRequestDetailsProps> = ({ route, naviga
)

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

if (invalidPredicate) {
setInvalidPredicate({ visible: true, predicate: invalidPredicate.predicate })
return
Expand All @@ -323,7 +327,7 @@ const ProofRequestDetails: React.FC<ProofRequestDetailsProps> = ({ 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 })
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 @@ const ProofRequesting: React.FC<ProofRequestingProps> = ({ route, navigation })
},
})

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 @@ const ProofRequesting: React.FC<ProofRequestingProps> = ({ 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
Expand All @@ -181,7 +180,7 @@ const ProofRequesting: React.FC<ProofRequestingProps> = ({ route, navigation })
}
}
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)]

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
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<ProofRequestTemplate | undefined> {
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<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)
)
}
}
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

0 comments on commit bf5cf8a

Please sign in to comment.