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.

dependencies {
    implementation("tech.indicio:holdr:1.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.

  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.

"dependencies": {
    "@holdr/react-native": "file:holdr-react-native-v1.1.0.tgz",
    "@holdr/core": "file:holdr-core-v1.1.0.tgz",
}
  1. Call yarn install.

Android

  1. Add repositories to android/build.gradle and confirm minSdkVersion 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

  1. 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 -->
  1. Reinstall the pods.

Android

  1. 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')
  1. 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'
+    }
}
  1. 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;
        }
  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

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

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

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.

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

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.

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.

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

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

  // 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)
  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

  // 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()
              }
    })
  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

// 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)

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

// 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}`)
            }
        }
    )

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

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

  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?