Skip to content

Commit

Permalink
feat: VC Wallet Query By PresentationExchange
Browse files Browse the repository at this point in the history
- PresentationExchange query support
- GetAll by content type support
- Closes hyperledger-archives#2712
- Closes hyperledger-archives#2713

Signed-off-by: sudesh.shetty <sudesh.shetty@securekey.com>
  • Loading branch information
sudeshrshetty committed Apr 5, 2021
1 parent 17bb393 commit 761ed11
Show file tree
Hide file tree
Showing 10 changed files with 765 additions and 35 deletions.
46 changes: 33 additions & 13 deletions pkg/client/vcwallet/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,11 @@ func (c *Client) Close() bool {
// Returns exported locked wallet.
//
// Supported data models:
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Profile
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Collection
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Credential
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#DIDResolutionResponse
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#meta-data
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection
//
func (c *Client) Export(auth string) (json.RawMessage, error) {
// TODO to be added #2433
Expand All @@ -144,11 +147,12 @@ func (c *Client) Export(auth string) (json.RawMessage, error) {
// - auth: token used while exporting the wallet.
//
// Supported data models:
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Profile
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Collection
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Credential
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#CachedDIDDocument
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#DIDResolutionResponse
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#meta-data
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Key
//
func (c *Client) Import(auth string, contents json.RawMessage) error {
// TODO to be added #2433
Expand All @@ -158,11 +162,12 @@ func (c *Client) Import(auth string, contents json.RawMessage) error {
// Add adds given data model to wallet contents store.
//
// Supported data models:
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Profile
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Collection
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Credential
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#CachedDIDDocument
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#DIDResolutionResponse
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#meta-data
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Key
//
// TODO: (#2433) support for correlation between wallet contents (ex: credentials to a profile/collection).
func (c *Client) Add(contentType wallet.ContentType, content json.RawMessage) error {
Expand All @@ -177,9 +182,9 @@ func (c *Client) Add(contentType wallet.ContentType, content json.RawMessage) er
// Remove removes wallet content by content ID.
//
// Supported data models:
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Profile
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Collection
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Credential
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#CachedDIDDocument
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#DIDResolutionResponse
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#meta-data
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection
//
Expand All @@ -190,25 +195,40 @@ func (c *Client) Remove(contentType wallet.ContentType, contentID string) error
// Get fetches a wallet content by content ID.
//
// Supported data models:
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Profile
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Collection
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Credential
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#CachedDIDDocument
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#DIDResolutionResponse
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#meta-data
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection
//
func (c *Client) Get(contentType wallet.ContentType, contentID string) (json.RawMessage, error) {
return c.wallet.Get(contentType, contentID)
}

// Query returns a collection of results based on current wallet contents.
// GetAll fetches all wallet contents of given type.
//
// Supported data models:
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Collection
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Credential
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#DIDResolutionResponse
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#meta-data
// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection
//
func (c *Client) GetAll(contentType wallet.ContentType) ([]json.RawMessage, error) {
return c.wallet.GetAll(contentType)
}

// Query runs query against wallet credential contents and returns presentation containing credential results.
//
// https://w3c-ccg.github.io/universal-wallet-interop-spec/#query
//
// Supported Query Types:
// - https://www.w3.org/TR/json-ld11-framing
// - https://identity.foundation/presentation-exchange
// - https://w3c-ccg.github.io/vp-request-spec/#query-by-example
//
func (c *Client) Query(query *wallet.QueryParams) ([]json.RawMessage, error) {
// TODO to be added #2433
return nil, fmt.Errorf("to be implemented")
func (c *Client) Query(params *wallet.QueryParams) (*verifiable.Presentation, error) {
return c.wallet.Query(params)
}

// Issue adds proof to a Verifiable Credential.
Expand Down
46 changes: 45 additions & 1 deletion pkg/client/vcwallet/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,42 @@ func TestClient_Get(t *testing.T) {
require.Equal(t, sampleContentValid, string(content))
}

func TestClient_GetAll(t *testing.T) {
const vcContent = `{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "%s",
"issuer": {
"id": "did:example:76e12ec712ebc6f1c221ebfeb1f"
},
"type": [
"VerifiableCredential",
"UniversityDegreeCredential"
]
}`

mockctx := newMockProvider()
err := CreateProfile(sampleUserID, mockctx, wallet.WithPassphrase(samplePassPhrase))
require.NoError(t, err)

vcWalletClient, err := New(sampleUserID, mockctx, wallet.WithUnlockByPassphrase(samplePassPhrase))
require.NotEmpty(t, vcWalletClient)
require.NoError(t, err)

// save test data
const count = 5

for i := 0; i < count; i++ {
require.NoError(t, vcWalletClient.Add(wallet.Credential, []byte(fmt.Sprintf(vcContent, uuid.New().String()))))
}

vcs, err := vcWalletClient.GetAll(wallet.Credential)
require.NoError(t, err)
require.Len(t, vcs, count)
}

func TestClient_Remove(t *testing.T) {
mockctx := newMockProvider()
err := CreateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL))
Expand Down Expand Up @@ -714,7 +750,15 @@ func TestClient_Query(t *testing.T) {
require.NotEmpty(t, vcWalletClient)
require.NoError(t, err)

results, err := vcWalletClient.Query(&wallet.QueryParams{})
results, err := vcWalletClient.Query(&wallet.QueryParams{Type: "QueryByExample"})
require.Empty(t, results)
require.Error(t, err)
require.EqualError(t, err, "no result found")

require.NoError(t, vcWalletClient.Open(wallet.WithUnlockByPassphrase(samplePassPhrase)))
require.NoError(t, vcWalletClient.Add(wallet.Credential, []byte(sampleUDCVC)))

results, err = vcWalletClient.Query(&wallet.QueryParams{Type: "QueryByExample"})
require.Empty(t, results)
require.Error(t, err)
require.EqualError(t, err, toBeImplementedErr)
Expand Down
21 changes: 19 additions & 2 deletions pkg/mock/storage/mock_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ type MockStore struct {
ErrPut error
ErrGet error
ErrDelete error
ErrQuery error
ErrNext error
ErrValue error
}

// Put stores the key and the record.
Expand Down Expand Up @@ -152,6 +155,10 @@ func (s *MockStore) GetBulk(keys ...string) ([][]byte, error) {
// If TagValue is not provided, then all data associated with the TagName will be returned.
// For now, expression can only be a single tag Name + Value pair.
func (s *MockStore) Query(expression string, _ ...storage.QueryOption) (storage.Iterator, error) {
if s.ErrQuery != nil {
return nil, s.ErrQuery
}

if expression == "" {
return nil, errInvalidQueryExpressionFormat
}
Expand All @@ -166,7 +173,7 @@ func (s *MockStore) Query(expression string, _ ...storage.QueryOption) (storage.

keys, dbEntries := s.getMatchingKeysAndDBEntries(expressionTagName, "")

return &iterator{keys: keys, dbEntries: dbEntries}, nil
return &iterator{keys: keys, dbEntries: dbEntries, errNext: s.ErrNext, errValue: s.ErrValue}, nil
case expressionTagNameAndValueLength:
expressionTagName := expressionSplit[0]
expressionTagValue := expressionSplit[1]
Expand All @@ -176,7 +183,7 @@ func (s *MockStore) Query(expression string, _ ...storage.QueryOption) (storage.

keys, dbEntries := s.getMatchingKeysAndDBEntries(expressionTagName, expressionTagValue)

return &iterator{keys: keys, dbEntries: dbEntries}, nil
return &iterator{keys: keys, dbEntries: dbEntries, errNext: s.ErrNext, errValue: s.ErrValue}, nil
default:
return nil, errInvalidQueryExpressionFormat
}
Expand Down Expand Up @@ -236,9 +243,15 @@ type iterator struct {
currentDBEntry DBEntry
keys []string
dbEntries []DBEntry
errNext error
errValue error
}

func (m *iterator) Next() (bool, error) {
if m.errNext != nil {
return false, m.errNext
}

if len(m.dbEntries) == m.currentIndex || len(m.dbEntries) == 0 {
m.dbEntries = nil
return false, nil
Expand All @@ -260,6 +273,10 @@ func (m *iterator) Key() (string, error) {
}

func (m *iterator) Value() ([]byte, error) {
if m.errValue != nil {
return nil, m.errValue
}

if len(m.dbEntries) == 0 {
return nil, errIteratorExhausted
}
Expand Down
33 changes: 32 additions & 1 deletion pkg/wallet/contents.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type ContentType string

const (
// Collection content type which can be used to group wallet contents together.
// https://w3c-ccg.github.io/universal-wallet-interop-spec/#Profile
// https://w3c-ccg.github.io/universal-wallet-interop-spec/#Collection
Collection ContentType = "collection"

// Credential content type for handling credential data models.
Expand Down Expand Up @@ -176,6 +176,37 @@ func (cs *contentStore) Get(ct ContentType, key string) ([]byte, error) {
return cs.store.Get(getContentKeyPrefix(ct, key))
}

// GetAll returns all wallet contents of give type.
// returns empty result when no data found.
func (cs *contentStore) GetAll(ct ContentType) ([]json.RawMessage, error) {
iter, err := cs.store.Query(ct.Name())
if err != nil {
return nil, err
}

var result []json.RawMessage

for {
ok, err := iter.Next()
if err != nil {
return nil, err
}

if !ok {
break
}

val, err := iter.Value()
if err != nil {
return nil, err
}

result = append(result, val)
}

return result, nil
}

func getContentID(content []byte) (string, error) {
var cid contentID
if err := json.Unmarshal(content, &cid); err != nil {
Expand Down
105 changes: 105 additions & 0 deletions pkg/wallet/contents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package wallet

import (
"errors"
"fmt"
"testing"

"github.com/google/uuid"
Expand Down Expand Up @@ -458,6 +459,110 @@ func TestContentStores(t *testing.T) {
})
}

func TestContentStore_GetAll(t *testing.T) {
const vcContent = `{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"credentialSchema": [],
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"name": "Jayden Doe"
},
"id": "%s",
"issuanceDate": "2010-01-01T19:23:24Z",
"issuer": {
"id": "did:example:76e12ec712ebc6f1c221ebfeb1f",
"name": "Example University"
},
"type": [
"VerifiableCredential",
"UniversityDegreeCredential"
]
}`

const testMetadata = `{
"@context": ["https://w3id.org/wallet/v1"],
"id": "%s",
"type": "Person",
"name": "John Smith",
"image": "https://via.placeholder.com/150",
"description" : "Professional software developer for Acme Corp."
}`

t.Run("get all content from store for credential type - success", func(t *testing.T) {
sp := getMockStorageProvider()

contentStore, err := newContentStore(sp, &profile{ID: uuid.New().String()})
require.NoError(t, err)
require.NotEmpty(t, contentStore)

// save test data
const count = 5

for i := 0; i < count; i++ {
require.NoError(t, contentStore.Save(sampleFakeTkn,
Credential, []byte(fmt.Sprintf(vcContent, uuid.New().String()))))
require.NoError(t, contentStore.Save(sampleFakeTkn,
Metadata, []byte(fmt.Sprintf(testMetadata, uuid.New().String()))))
}

allVcs, err := contentStore.GetAll(Credential)
require.NoError(t, err)
require.Len(t, allVcs, count)

allMetadata, err := contentStore.GetAll(Metadata)
require.NoError(t, err)
require.Len(t, allMetadata, count)

allDIDs, err := contentStore.GetAll(DIDResolutionResponse)
require.NoError(t, err)
require.Empty(t, allDIDs)
})

t.Run("get all content from store for credential type - errors", func(t *testing.T) {
sp := getMockStorageProvider()

// iterator value error
sp.MockStoreProvider.Store.ErrValue = errors.New(sampleContenttErr + uuid.New().String())

contentStore, err := newContentStore(sp, &profile{ID: uuid.New().String()})
require.NoError(t, err)
require.NotEmpty(t, contentStore)

require.NoError(t, contentStore.Save(sampleFakeTkn, Credential, []byte(fmt.Sprintf(vcContent, uuid.New().String()))))

allVcs, err := contentStore.GetAll(Credential)
require.True(t, errors.Is(err, sp.MockStoreProvider.Store.ErrValue))
require.Empty(t, allVcs)

// iterator next error
sp.MockStoreProvider.Store.ErrNext = errors.New(sampleContenttErr + uuid.New().String())

contentStore, err = newContentStore(sp, &profile{ID: uuid.New().String()})
require.NoError(t, err)
require.NotEmpty(t, contentStore)

require.NoError(t, contentStore.Save(sampleFakeTkn, Credential, []byte(fmt.Sprintf(vcContent, uuid.New().String()))))

allVcs, err = contentStore.GetAll(Credential)
require.True(t, errors.Is(err, sp.MockStoreProvider.Store.ErrNext))
require.Empty(t, allVcs)

// iterator next error
sp.MockStoreProvider.Store.ErrQuery = errors.New(sampleContenttErr + uuid.New().String())

contentStore, err = newContentStore(sp, &profile{ID: uuid.New().String()})
require.NoError(t, err)
require.NotEmpty(t, contentStore)

allVcs, err = contentStore.GetAll(Credential)
require.True(t, errors.Is(err, sp.MockStoreProvider.Store.ErrQuery))
require.Empty(t, allVcs)
})
}

func TestContentDIDResolver(t *testing.T) {
t.Run("create new content store - success", func(t *testing.T) {
sp := getMockStorageProvider()
Expand Down
Loading

0 comments on commit 761ed11

Please sign in to comment.