Holdr Integration Tutorial
The Indicio Holdr SDK (Software Development Kit) supports Android and iOS platforms and provides holder capabilities that are compatible with many Aries protocols. Consumption of the Holdr SDK will allow an application to have its own Aries Askar wallet for holding digital credentials that conform to the Anoncreds specification, communicate with ledgers through IndyVDR, and generate zero trust proofs for information verification through Anoncreds-RS.
Adding the Holdr SDK to Your Project
iOS
Open your iOS project in XCode and click on your project name under the “TARGETS” section.
Scroll down in the “General” tab until you see the “Frameworks, Libraries, and Embedded Content” section of the project.
Click on the + button under “Frameworks, Libraries, and Embedded Content.”
Select “Add Files…” in the bottom left dropdown and select “Proven SDK XCFramework” to add it to your project.
Android
To consume the Holdr SDK in Android, you must have access to Maven repos hosted on GitHub. For this you must add your GitHub username and a GitHub personal access token with read permissions to your local.properties
. You will access those values in your build.gradle
.
Information for creating a personal access token can be found here.
Official documentation for reading values from the local.properties
can be found here.
Android Installation
Add the below repos to your Android repository list.
// Example function to read values from local.properties in a build.gradle.kts file
fun readLocalProperty(key: String): String? {
val localPropertiesFile = File(rootDir, "local.properties")
if (localPropertiesFile.exists()) {
val properties = Properties()
localPropertiesFile.inputStream().use { properties.load(it) }
return properties.getProperty(key)
}
return null
}
repositories {
maven {
setUrl("https://maven.pkg.github.com/indicio-tech/holdr-sdk-release")
credentials {
username = readLocalProperty("githubUsername")
password = readLocalProperty("githubToken")
}
}
maven {
setUrl("https://maven.pkg.github.com/LF-Decentralized-Trust-labs/aries-uniffi-wrappers")
credentials {
username = readLocalProperty("githubUsername")
password = readLocalProperty("githubToken")
}
}
}
Add the following implementation to the project dependencies in the
build.gradle.kts
file.
dependencies {
implementation("tech.indicio:holdr:1.1")
}
Make sure your AndroidManifest.xml file includes the following permissions.
<manifest>
<uses-permission android:name="android.permission. READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission. WRITE_EXTERNAL_STORAGE" />
<!-- For Android 10 (API level 29) and above -->
<uses-permission android:name="android.permission. MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
React Native
Installation
You will want to add @holdr/core
and @holdr/react-native
to your project.
Place
holdr-core-v1.1.0.tgz
andholdr-react-native-v1.1.0.tgz
in a directory parallel to yourpackage.json
.In
package.json
, add them both as dependencies.
"dependencies": {
"@holdr/react-native": "file:holdr-react-native-v1.1.0.tgz",
"@holdr/core": "file:holdr-core-v1.1.0.tgz",
}
Call
yarn install
.
Android
Add repositories to
android/build.gradle
and confirmminSdkVersion
is greater than or equal to 24.
buildscript {
ext {
minSdkVersion = 24
}
}
allprojects{
repositories{
maven {
setUrl("https://maven.pkg.github.com/indicio-tech/holdr-sdk-release")
credentials {
username = readLocalProperty("githubUsername")
password = readLocalProperty("githubToken")
}
}
maven {
setUrl("https://maven.pkg.github.com/LF-Decentralized-Trust-labs/aries-uniffi-wrappers")
credentials {
username = readLocalProperty("githubUsername")
password = readLocalProperty("githubToken")
}
}
}
}
Manually Link Modules (If They Are Not Getting Automatically Linked)
Older versions of React Native (0.67.5) may have trouble automatically linking the native modules. If you run into issues, we recommend linking the package manually. Follow the instructions for either iOS or Android:
iOS
Add the pod to
ios/Podfile
.
target 'ExampleApp' do
<!-- Other configs -->
+ pod 'rtn-proven-mobile-sdk', :path => '../node_modules/@holdr/react-native'
target 'ExampleAppTests' do
<!-- Other configs -->
Reinstall the pods.
Android
Add the project to
android/settings.gradle
.
<!-- Other configs -->
+include ':holdr-react-native
+project(':holdr-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/@holdr/react-native/android')
Add module to dependency in
android/app/build.gradle
.
dependencies{
<!-- Other dependencies -->
+ implementation project(":holdr-react-native")
}
// If you have other libraries that use libc++_shared.so or libfbjni.so
// you may need to add the following to your android configs
android{
<!-- Other configs -->
+ packagingOptions {
+ pickFirst '**/libc++_shared.so'
+ pickFirst '**/libfbjni.so'
+ }
}
Import and link package in
android/app/src/main/java/com/example/MainApplication.java
.
+ import com.rtnprovenmobilesdk.ProvenMobilePackage;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost =
new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
+ packages.add(new ProvenMobilePackage());
return packages;
}
Older React Native versions (0.67.5) may need to upgrade their gradle wrapper to 7.4 (declared in
android/gradle/wrapper/gradle-wrapper.properties
) and upgradecom.android.tools.build:gradle
(declared inandroid/build.gradle
) to 7.3.1.
Creating an Agent
Wallet Config
To create an Agent, you need to first create a wallet config object to tell the Agent how to initialize your wallet.
Kotlin
val walletConfig: WalletConfig = WalletConfig(
uri = "sqlite://pathWhereYouWantDataStored/local.db",
// options are raw(raw random key), kdf:argon2i(Encrypted Key), none(testing only)
keyMethod = "raw",
// Should be randomly generated or derived from a pin or password, must be able to be repeatedly accessed
passkey = "TheActualKeyThatUnlocksTheWallet",
id = "Unique id for this wallet"
)
Swift
let walletConfig = WalletConfig(
uri: "sqlite://pathWhereYouWantDataStored/local.db",
keyMethod: "raw",
passkey: "TheActualKeyThatUnlocksTheWallet",
id: "Unique id for this wallet"
)
Mediation Config
Next create a mediation config to pass to our Agent. The reconnection interval in the below section has two values that indicate the minimum and maximum amount of time in a back-off strategy on which to try to contact the mediator.
Kotlin
val mediationConfig: MediationConfig = MediationConfig(
mediatorInvitationUrl = "Mediation connection url for the default mediator you want to use",
// Optional parameter to indicate how often, in milliseconds,the Agent should try to reconnect to the mediator
baseMediatorReconnectionIntervalMS = 500,
// Optional parameter to indicate the max amount of time between attempts to contact the mediator
maximumMediatorReconnectionIntervalMS: Int = 10000
)
Swift
let mediationConfig = MediationConfig(
mediatorInvitationUrl: "Mediation connection URL for the default mediator you want to use",
// Optional parameter to indicate how often, in milliseconds, the Agent should try to reconnect to the mediator
baseMediatorReconnectionIntervalMS: 500,
// Optional parameter to indicate the max amount of time between attempts to contact the mediator
maximumMediatorReconnectionIntervalMS: 10000
)
Pool Config
The pool config is used to indicate what networks or ledgers the Agent can read from. Not all ledgers contain all schemas or credential definitions, and some are more volatile than others. The standard way to declare a pool config is shown in the examples below and again the React Native portion of this document.
The first value used to create a pool config is the genesisUrl
. This is a URL point to the genesis file in a raw string format. This file is used to establish connection and provide data about the network or ledger.
The second value is the isProduction
Boolean flag. This indicates to the agent whether to treat the network/ledger as a production environment or not.
The last value needed is the indyNamespace
. This string value is the name by which the network or ledger is known and is prepended to schemas and credential definitions to identify what network or ledger they are on. Misspelling this value or having an incorrect variation will cause errors when reading from the ledger.
Kotlin
val indicioDemoNet = IndyVDRPoolConfig.fromUrl(
genesisUrl = "https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_demonet_genesis",
isProduction = false,
indyNamespace = "indicio-demo-net"
)
Swift
let indicioDemoNet = IndyVDRPoolConfig.companion.fromUrl(
genesisUrl = "https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_demonet_genesis",
isProduction = false,
indyNamespace = "indicio-demo-net"
)
Configuring an Agent
Kotlin
val agent: Agent = Agent(
license: licenseString, // SDK license as a string
walletConfig = walletConfig,
mediationConfig = mediationConfig, // Set to null if you do not need didcomm
pools = ListOf(
indicioDemoNet
),
pickupBatchSize = 10, // max number of message that can be picked up at once from the mediator
defaultConnectionLabel = "Proven SDK", // The label the Agent will use in didComm communication
enableLogging = false,
autoAcceptConnections = true,
autoAcceptCredentials = false,
customLogger = null // (message: String)->Unit Callback to use for logging instead of `println()`
)
Swift
let agent = Agent(
license: licenseString, // SDK license as a string
walletConfig: walletConfig,
mediationConfig: mediationConfig, // Set to nil if not using didcomm
pools: [
indicioDemoNet
],
pickupBatchSize: 10, // max number of message that can be picked up at once from the mediator
defaultConnectionLabel: "Proven SDK", // The label the Agent will use in didComm communication
enableLogging: false,
autoAcceptConnections: true,
autoAcceptCredentials: false,
customLogger: nil // (message: String)->Void Callback to use for logging instead of `println()`
)
// Start agent (KMPNativeCoroutines)
try await asyncFunction(for: agent.start(startUpTimeout: 10000))
// Start agent (Completion handler)
agent.start(startUpTimeout: 10000){error in
if(error != nil){
// Handle error
}
// Code to be called when agent started
}
// Start agent (Without KMPNativeCoroutines)
let error: ProvenError? = await agent.start(startUpTimeout: 10000)
if(error != nil){
// Handle error
}
Once the Agent is created and configured, you will have to call agent.start()
before you can use the agent for anything. When the app is closing, agent.stop()
should be called to safely shut the agent down between sessions.
If you want to remove the Agent or reset the wallet you can call agent.delete()
to remove all data the agent has saved.
React Native
Creating an Agent follows similar steps as Kotlin and Swift but has some additional steps specific to React Native.
Similar to Kotlin and Swift, some config objects must be created to customize the Agent. Additionally, to create a pool config from a URL pointing to genesis files, you must pass the ProvenReactNative
object imported from the @holdr/react-native
package.
import RNFS from 'react-native-fs';
import ProvenReactNative, {
provenEventsFactory,
} from '@holdr/react-native';
import {
Agent,
IndyPoolConfig,
type MediationConfig,
type WalletConfig
} from '@holdr/core';
const walletConfig: WalletConfig = {
uri: `sqlite://${RNFS.DocumentDirectoryPath}/local.db`,
keyMethod: 'raw',
passkey: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb',
id: 'ReactNativeTest',
profile: 'test',
};
const mediationConfig: MediationConfig = {
mediatorInvitationUrl:
'url invite to the mediator you want to use',
baseMediatorReconnectionIntervalMS: 50,
maximumMediatorReconnectionIntervalMS: 10000,
};
const poolConfig: IndyVDRPoolConfig = await IndyPoolConfig.fromURL(
ProvenReactNative,
'https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_demonet_genesis',
false,
'indicio-demo-net',
);
The actual Agent is created by passing the created config objects along with some other options. Again, the ProvenReactNative
object must be included in the Agent constructor along with the provenEventsFactory
that facilitates getting events from the Agent to the React Native level.
import ProvenReactNative, {
provenEventsFactory,
} from '@holdr/react-native';
const agent = new Agent(
ProvenReactNative,
provenEventsFactory,
licenseString,
walletConfig,
mediationConfig,
[poolConfig],
'RN',
10,
true,
true,
true,
console.info // (message: string)->Void Callback to use for logging, defaults to console.log
);
Once the Agent has been constructed you can start the Agent with the following code. The React Native code allows the use of the Javascript Promise syntax for async operations.
await agent.start();
Similarly, the agent.stop()
and agent.delete()
functions exist on the agent object along with access to all other modules and their respective functions mirroring the Kotlin and Swift implementations.
Events
Events are handled as flows in Kotlin and can be retrieved from the agent.
Kotlin
val didExchangeEvents? = agent.events.getEventBus(DidExchangeEvents::class)
// or
val didExchangeEvents = agent.events.getDidExchangeEvents()?: throw Error("Events not initialized by agent")
The getEventBus
function will attempt to retrieve the event bus class passed to it. This exists because custom event flows can be registered and stored in the agent.events eventManager
object. There are eight base flows for different actions that the Agent manages, their functions are as follows:
getDidExchangeEvents()
getAgentEvents()
getCredentialEvents
getEventBusEvents()
getMessageEvents()
getProofEvents()
getTrustPingEvents()
getWebsocketEvents()
All of the listed functions exist on the events
property of the agent. The event bus events pertain to record updates. The message events pertain to all incoming messages regardless of the type. Agent events are emitted to indicate the state (Start, Running, Stop) of the Agent.
Both the named functions and the getEventBus
may return null
if, for some reason, the event bus has not yet been registered to the agent's eventManager
. The named function returns null
only if the Agent initializes with errors.
Once you have the events, you can perform operations on them to filter for certain IDs or states to complete processes or inform the user when things are occurring.
// Example of what NOT to do
val didExchangeEvents = agent.events.getDidExchangeEvents()?: throw Error("Events not initialized by agent")
didExchangeEvents.events.onEach{
// Causes deadlock because request connection will not return until the response message is processed
// This can be avoided by calling this function in a separate thread
agent.didExchange.requestConnection(it.didExchangeRecord.id, it.outOfBandRecord.id)
}.collect()
// Auto accept should be used instead of trying to do this. This is an intentionally BAD example
If, for some reason, you need to wait for the agent to complete an action you should use the Kotlin or Swift (using KMPN NativeCoroutines) directions following this paragraph.
Kotlin
val didExchangeEvents = agent.events.getDidExchangeEvents()?: throw Error("Events not initialized by agent")
// Waits for the first didExchangeStateChangedEvent to be emitted that is in the completed state
didExchangeEvents.events.first{
it.didExchangeRecord.state == DidExchangeState.Completed
}
// Additional attributes (such as ID) on the record can be used to wait for a specific record to reach a certain state
In Kotlin the events use flows, this allows the use of the Kotlin Asynchronous Flow on all events.
Swift (Using KMPNativeCoroutines)
Swift uses KMPNativeCoroutines (1.0.0-ALPHA-23) as a dependency that allows the Kotlin flows to be turned into native Swift observables, asyncSequences
and potentially more.
// Turns the didExchangeEvents into a swift asyncSequence
let credentialEvents = asyncSequence(for: agent.events.getCredentialEvents()!.events)
Swift/Objective-C (Without KMPNativeCoroutines)
We also wrapped events with the following methods so you can easily listen to events without a third party library or with Objective-C:
onDidExchangeStateChanged
onCredentialStateChanged
onTrustPingEvent
onAgentEvent
onRecordEvent
onProofEvent
onWebsocketEvent
Objective-C
void (^removeListener)(ProvenmobileKotlinCancellationException * _Nullable) =
[agent.events onDidExchangeStateChangedCallback:^(
ProvenmobileDidExchangeStateChangedEvent *_Nonnull event
) {
// Handle DidExchangeStateChangedEvent here
}
// Remove listener when no longer needed
removeListener(nil);
Swift
let removeListener = agent.events.onDidExchangeStateChanged { event in
// Handle DidExchangeStateChangedEvent here
}
// Remove listener when no longer needed
removeListener(nil)
React Native
Events in React Native are handled differently than in Kotlin and Swift. The format of events on the Agent is similar but is done in a simpler manner.
Instead of getting an events object, register a handler function that will be called on all events of the specified type.
// Registers a handler that is called on all Did Exchange events
const didExchangeRemove = agent.events.registerDidExchangeHandler(
event => {
if (event.didExchangeRecord.state === DidExchangeState.COMPLETED) {
console.log(
'didExchange completed',
event.didExchangeRecord.theirLabel,
)
}
})
didExchangeRemove()
The return of registering a handler is a function that will remove or unregister the handler. Calling the removal function stops the given handler from being called on any future events.
Creating a Connection
All connections are made through the out-of-band protocol using the out-of-band module on the Agent.
Get an out-of-band invitation. This is done using one of the two following methods: either through a QR code that resolves to a URL (most common) or from a JSON object in the form of a string.
Kotlin
// The url commonly comes from the result of scanning a QR code.
val invitation = OutOfBandInvitationMessage.fromUrl(url)
// Or if you have the message in json already
val invitation = OutOfBandInvitationMessage.fromJsonString(str)
React Native
// parses the URL to a json string
const invitation: string = agent.outOfBand.parseInvitation(invitationUrl)
Have the Agent accept the invitation. The
receiveInvitation
function has many options. To automatically make a connection with the provided invitation, supply just the invitation like the example below.
Kotlin
// Completed connection status may or may not be done after this call
val records = agent.outOfBand.receiveInvitation(invitation)
//Waits for the record to be in the completed state
agent.events.getDidExchangeEvents?.events?.first{ event ->
if(records.didExchangeRecord.id != event.didExchangeRecord.id){
event.didExchangeRecord.state == DidExchangeState.Completed
}
}
React Native
// Takes the json formatted string for the invitation
const records = await agent.outOfBand.receiveInvitation(invitation)
const removeListener = agent?.events.registerDidExchangeHandler((event) => {
if(event.didExchangeRecord.id === records.didExchangeRecord.id && event.didExchangeRecord.isReady){
console.log("Connection complete")
removeListener()
}
})
While it is not recommended to disable auto acceptance, manually accepting a connection is an option as shown in the following code example:
Kotlin
// returns references to records needed to complete connection
val records = agent.outOfBand.receiveInvitation(invitation, autoAcceptConnection = false)
// Connection guaranteed to be completed after this call
val exchangeRecord = agent.didExchange.requestConnection(records.didExchangeRecord!!.id, records.outOfBandRecord.id)
React Native
const record = await agent.outOfBand.receiveInvitation(invitation, false)
const exchangeRecord = await agent.didExchange.requestConnection(records.didExchangeRecord!!.id, records.outOfBandRecord.id)
Retrieving and Using the didExchange Records
Once a connection has been made through the didExchange
protocol, the agent keeps a record that holds information about the established connection. A list of all connections can be retrieved using the following code example. This list can be used to create a contact list.
Kotlin
// This is a suspend function in Kotlin
val contacts: List<DidExchangeRecord> = agent.didExchange.getAll()
React Native
const contacts = await agent.didExchange.getAll()
The didExchange
record itself has several properties that are derived when making a connection based on information provided from the connected agent. Likely properties are as follows:
Did
State
Role
theirDid
theirLabel
createdAt
updatedAt
Alias
threadId
mediatorId
outOfBandId
invitationDid
autoAccept
Some properties are found on all records and some are specific to didExchange
. The updatedAt
and createdAt
properties are on all records along with the id
property that is a UUID to identify the record. With didExchangeRecords
you would likely use theirLabel
or alias
to create a contact card item for displaying the connection.
Other properties like role
and state
can be used to filter the connections so that you can sort through them to find a specific connection or in rare cases find connections that failed or still need to be accepted after processing the invitation.
Removing a Connection
If you no longer want to have a connection with another agent, you would simply have to delete the didExchangeRecord
. If you need to reconnect, you will have to receive another invitation from the agent. Invitations can be reused in some instances (NOTE: single use invitations can only be used by one agent while multi-use can be used by multiple).
Deleting a connection is done as follows:
Kotlin
val contacts = agent.didExchange.getAll()
// Deleting the first contact in the list
agent.didExchange.deleteById(contacts[0].id)
React Native
const contacts = await agent.didExchange.getAll()
await agent.didExchange.deleteById(contacts[0].id)
Credentials
Agents can be configured to automatically or manually accept credentials. Credentials are generally offered by an external Agent upon connection or other business action. In some rare cases, the receiving Agent will request a credential.
If credentials are not auto accepted, use the credential events to determine when a credential has been offered.
The below block accepts the offer coming from an external issuing Agent. The issuer then sends a credential-issuance message that needs final confirmation before the credential is saved and the issuer is notified of completion of issuance.
Kotlin
val credEvents = agent.events.getCredentialEvents()!! // Assuming agent initialized normally
val offers = credEvents.events.filter {
it.credentialExchangeRecord.state == CredentialState.OfferReceived
}
offers.onEach{
// present to user
if(userAccept)
agent.credentials.acceptOffer(it.credentialExchangeRecord.id)
}.collect()
React Native
agent.events.registerCredentialHandler((event) => {
if(event.credentialExchangeRecord.state === CredentialState.OfferReceived)
// Present to user somehow
if(userAccept)
await agent.credentials.acceptOffer(event.credentialExchangeRecord.id)
})
The following code block has the Agent confirm that it accepts the issued credential. It also notifies the issuing Agent we have accepted and stored the credential, and the transaction should be recorded on the ledger. This second confirmation is done to ensure that the credential you were offered matches what you actually received.
Kotlin
val receivedCredentials = credEvents.events.filter{
it.credentialExchangeRecord.state == CredentialState.CredentialReceived
}
receivedCredentials.onEach{
val attributes = it.credentialExchangeRecord.attributes
// User reviews the attributes of the credential
if(userAccept)
agent.credentials.acceptCredential(it.credentialExchangeRecord.id)
else
agent.credentials.rejectCredential(it.credentialExchangeRecord.id)
}.collect()
React Native
agent.events.registerCredentialHandler((event) => {
const attributes = event.credentialExchangeRecord.attributes
// Have user review credential
if(userAccept)
await agent.credentials.acceptCredential(event.credentialExchangeRecord.id)
else
await agent.credentials.rejectCredential(event.credentialExchangeRecord.id)
})
Once the credential has been accepted, it is stored in the wallet. The credentials module on the Agent has several accessor functions to get credential exchange records. The records have the previewed attributes. After being accepted, they will have a populated credential attribute that contains information about the accepted credential. The credential attribute allows a developer using the SDK to fetch more detailed information about the credential, such as the credential definition ID or schema ID. (These values are not commonly used but may need to be checked in some rare use cases.)
Kotlin
val record = agent.credentials.findByRecordId("Some id")
React Native
const record = await agent.credential.findByRecordId("Some id")
Proofs
When a proof request is received, an event will be emitted. There are two options for handling proofs, attempt to auto accept and process the proof or manually select credentials. A proof request can be automatically accepted if the request does not have any attributes that need to be self attested (provided manually from the user) and there are sufficient credentials in the wallet.
A single proof request can contain multiple proofs that need to be satisfied and in turn can make manual selection complicated.
A proof event contains the proof record and the didExchangeId
of the contact that it came from. The initial state for the proof record is requestReceived
.
Auto Accepting
Kotlin
//Gets the first proof event received, blocks whatever thread it is ran on.
val proofEvent = agent.events.getProofEvents().first()
try {
agent.proofs.autoAcceptProof(proofEvent.proofRecord.id, proofEvent.didExchangeId)
} catch (e: Throwable) {
println("An error occurred auto-accepting proof, message: ${e.message}")
}
React Native
const removeProofHandler = agent.events.registerProofHandler(
(event) => {
try {
if (event.proofRecord.state === ProofState.REQUEST_RECEIVED) {
await agent.proofs.autoAcceptProof(
event.proofRecord.id,
event.exchangeId);
}
} catch (error) {
console.log(`An error ocurred auto-accepting proof, message ${error.message}`)
}
}
)
Manual Acceptance
Manual acceptance has two possible flows. One, the agent can auto select credentials, return them to you, and allow you to fill in any self-attested values. Or two, the agent returns all potential credentials that satisfy the proof. Both options will throw a ProvenError
if the wallet does not contain sufficient credentials for the proof request.
Kotlin
val proofEvent = agent.events.getProofEvents().first()
// Returns a list of pairs containing first the proof and then the credential(s) selected
val proofAttributes: SelectedCredentialsForProof = agent.proofs.autoSelectCredentialsForProof(
proofEvent.proofRecord.id,
proofEvent.didExchangeId,
nonRevoked = false
) // nonRevoked indicates if you care if the credentials selected are revoked or not
// Or
// Returns a similar object but can potentially contain multiple credentials that need to be selected from
val proofAttributes: SelectedCredentials = agent.proofs.getCredentialsForProofRequest(
proofEvent.proofRecord.id,
proofEvent.didExchangeId,
nonRevoked = false
)
React Native
const removeProofHandler = agent.events.registerProofHandler(async (event) => {
const proofAttributes = await agent.proofs.autoSelectCredentialsForProof(
event.proofRecord.id,
event.didExchangeId,
false
)
// Or
const proofAttributes = await agent.proofs.getCredentialsForProofRequest(
event.proofRecord.id,
event.didExchangeId,
false
)
})
The return from both of these functions are a PresentationData
object, with the auto select function having some data already filled in.
The PresentationData
object contains four properties: proofId
, exchangeId
, attributes
, and predicates
. The last two are arrays. It also contains some helper functions:
getRemaining
: returns an array of the remainingproofReferent
that do not have a selection.isReady
: returns a Boolean that indicates if a valid selection has been made for allproofReferents
.autoSelect
: does an in-place auto-selection of the provided credentials and returns a Boolean indicating if it successfully made a selection for all requests referents.getRequiredSelfAttested
: returns a list of all attributes required to be self-attested.
The attributes
and predicates
arrays contain two similar objects called attributeReferent
and predicateReferent
which are the two types of proofReferents
. The objects contain three properties that all pertain to self-attesting during a proof, which is only available with an attribute and not a predicate value:
canSelfAttest
: indicates if the value has to be self attested or notrequiredSelfAttest
: indicates if the value must be self attestedselfAttesdedValue
: is a mutable string that should be set to data not in a credential (ie. user input or from elsewhere in the app).
There are three object properties on the proofReferent
objects.
selection
: is the credential match that you use to satisfy theproofReferent
. It can be set using theselectCredential
function which takes acredentialMatch
object or an index corresponding to a value in thecredentials
array.credentials
: is an array of all the credentials that satisfy theproofReferent
.request
: is the actual object that indicates what data is being requested from the wallet.
The differences between attributeReferent
and predicateReferent
are that the predicateReferent
also has a requirement string that indicates the relationship the requested data needs to have to the requested value. Additionally predicates should not be self attested and as such should always return false self-attested values.
Self-attested values are intended to be entered in-place or as part of your written code. They are not automatically provided by the credential. If an optional self-attested attribute is supplied, it will be given priority over any supplied credential value.
The code below will go through processing the return from both functions starting with auto selection.
Auto selection
val proofData = agent.proofs.autoSelectCredentialsForProof(
proofEvent.proofRecord.id,
proofEvent.didExchangeId,
nonRevoked = false
)
// Check if there are any required self attested attributes
val selfAttested = proofData.getRequiredSelfAttested()
if(selftAttested.size != 0){
selfAttested.forEach{ referent ->
referent.selfAttestedValue = "Data from somewhere else"
}
}
// This code provides a self attested value for all values that can be self attested but not all values have to be.
// Supply the originally returned object that has been modified in place
agent.proofs.acceptProofs(proofData)
React Native
await const proofData = agent.proofs.autoSelectCredentialsForProof(
event.proofRecord.id,
event.didExchangeId,
false
)
const selfAttested = proofData.getRequiredSelfAttested()
if(selfAttested.size != 0) {
selfAttested.forEach((referent) => {
referent.selfAttestedValue = "Data from somewhere else"
})
}
await agent.proofs.acceptProofs(proofData)
Manual Selection
Self-attested attributes function the same way for manual selection.
Manual selection requires that a credential from the return list be selected for each referent in the proof. This allows the end user to select exactly what credentials are shared but is more complex to process.
val proofData = agent.proofs.getCredentialsForProofRequest(
proofEvent.proofRecord.id,
proofEvent.didExchangeId,
nonRevoked = false
)
// Goes through all attributes and picks the first credential that matches
proofData.attributes.forEach{ attribute =>
attribute.selectCredential(0)
}
proofData.predicates.forEach{ predicate =>
predicate.selectCredential(0)
}
agent.proofs.acceptProofs(proofData)
React Native
await const proofData = agent.proofs.getCredentialsForProofRequest(
proofEvent.proofRecord.id,
proofEvent.didExchangeId,
nonRevoked = false
)
proofData.attributes.forEach((attribute) => {
attribute.selectCredential(0)
})
proofData.predicates.forEach((predicate) => {
predicate.selectCredential(0)
})
await agent.proofs.acceptProofs(proofData)
Copyright 2025 Indicio PBC, All rights reserved
Last updated
Was this helpful?