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

  1. Open your iOS project in XCode and click on your project name under the “TARGETS” section.

  2. Scroll down in the “General” tab until you see the “Frameworks, Libraries, and Embedded Content” section of the project.

  3. Click on the + button under “Frameworks, Libraries, and Embedded Content.”

  4. Select “Add Files…” in the bottom left dropdown and select “Proven SDK XCFramework” to add it to your project.

Android

We recommend you add Github Authentication to local.properties or other secrets file.

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

  1. 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")
        }
    }
}
  1. Add the following implementation to the project dependencies in the build.gradle.kts file.

  1. Make sure your AndroidManifest.xml file includes the following permissions.

React Native

Installation

You will want to add @holdr/core and @holdr/react-native to your project.

  1. Place holdr-core-v1.1.0.tgz and holdr-react-native-v1.1.0.tgz in a directory parallel to your package.json.

  2. In package.json, add them both as dependencies.

  1. Call yarn install.

Android

  1. Add repositories to android/build.gradle and confirm minSdkVersion is greater than or equal to 24.

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

  1. Add the pod to ios/Podfile.

  1. Reinstall the pods.

Android

  1. Add the project to android/settings.gradle.

  1. Add module to dependency in android/app/build.gradle.

  1. Import and link package in android/app/src/main/java/com/example/MainApplication.java.

  1. 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 upgrade com.android.tools.build:gradle (declared in android/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

Swift

Mediation Config

If you are not using DIDcomm, you can pass null for the mediation config because you do not need a mediator.

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

Swift

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

Swift

Configuring an Agent

Kotlin

Swift

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.

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.

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.

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.

The following examples of event flows do not include the code needed for launching them in a background coroutine scope. That code needs to be added in your Kotlin code to take the handlers off of the main thread.

Kotlin

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.

Performing certain actions that wait for completion of a protocol inside of an event handler can cause a deadlock situation. It is not advised to wait for protocol completion inside a continuous event handler.

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

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.

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

Swift

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.

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.

Registered events will not persist after the app has been closed.

Creating a Connection

All connections are made through the out-of-band protocol using the out-of-band module on the Agent.

  1. 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

React Native

  1. 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

React Native

  1. While it is not recommended to disable auto acceptance, manually accepting a connection is an option as shown in the following code example:

Kotlin

React Native

In the above example you technically have two instances of the didExchange record; one in the records variable, and one in the exchangeRecord variable. Because records can be updated asynchronously, always use the most recently returned or fetch the record by ID in case any operations have been completed that could affect the Agent's records.

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

React Native

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

React Native

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

React Native

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

React Native

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

React Native

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

React Native

This only works if there are not self-attested attributes for the request and the wallet contains sufficient credentials.

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

React Native

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 remaining proofReferent that do not have a selection.

  • isReady: returns a Boolean that indicates if a valid selection has been made for all proofReferents.

  • 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 not

  • requiredSelfAttest: indicates if the value must be self attested

  • selfAttesdedValue: 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 the proofReferent. It can be set using the selectCredential function which takes a credentialMatch object or an index corresponding to a value in the credentials array.

  • credentials: is an array of all the credentials that satisfy the proofReferent.

  • 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

React Native

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.

React Native

Last updated

Was this helpful?