Creating TrustWeave Plugins
This guide explains how to create custom plugins for TrustWeave by implementing the various plugin interfaces.
Overview
TrustWeave is designed with a plugin architecture that allows you to extend functionality by implementing specific interfaces. Plugins can be registered manually or discovered automatically via the Service Provider Interface (SPI).
Plugin Types
TrustWeave supports the following plugin interfaces:
- DidMethod - Implement custom DID methods (e.g., did:web, did:key, did:ion)
- BlockchainAnchorClient - Add support for new blockchain networks
- ProofGenerator - Implement custom proof types (e.g., Ed25519, JWT, BBS+)
- KeyManagementService - Integrate with different key management backends
- CredentialService - Add credential issuance/verification providers
- WalletFactory - Create custom wallet storage backends
Prerequisites
Add the necessary dependencies to your project:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dependencies {
// Core interfaces
implementation("org.trustweave:did-did-core:0.6.0")
implementation("org.trustweave:anchors-anchor-core:0.6.0")
implementation("org.trustweave:kms-kms-core:0.6.0")
implementation("org.trustweave:common:0.6.0")
// Credential SPI (proof engines, exchange protocols, status checkers)
implementation("org.trustweave:credentials-credential-api:0.6.0")
// Wallet SPI
implementation("org.trustweave:wallet-wallet-core:0.6.0")
// Test doubles (in-memory KMS, mock DID method, etc.)
testImplementation("org.trustweave:testkit:0.6.0")
}
1. Implementing a DID Method
The DidMethod interface allows you to implement custom DID methods.
Interface Definition
Implement org.trustweave.did.DidMethod in did-core (it extends DidMethodResolver). Resolution uses the type-safe Did identifier and returns sealed org.trustweave.did.resolver.DidResolutionResult — not a nullable document on a single data class.
1
2
3
4
5
6
7
8
9
// Abbreviated — see did-core for full Javadoc
interface DidMethod : DidMethodResolver {
val method: String
val capabilities: MethodCapabilities? get() = null
suspend fun createDid(options: DidCreationOptions = DidCreationOptions()): DidDocument
override suspend fun resolveDid(did: Did): DidResolutionResult
suspend fun updateDid(did: Did, updater: (DidDocument) -> DidDocument): DidDocument
suspend fun deactivateDid(did: Did): Boolean
}
Example Implementation
Reference: Copy from org.trustweave.testkit.did.DidKeyMockMethod — it shows GenerateKeyResult, VerificationMethodId, Did, kotlinx.datetime.Clock, and correct DidDocument construction.
Resolution must return DidResolutionResult.Success or a Failure subtype (e.g. NotFound, InvalidFormat, MethodNotRegistered, ResolutionError):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.trustweave.did.identifiers.Did
import org.trustweave.did.model.DidDocument
import org.trustweave.did.model.DidDocumentMetadata
import org.trustweave.did.resolver.DidResolutionMetadata
import org.trustweave.did.resolver.DidResolutionResult
import kotlinx.datetime.Clock
// private val documents: MutableMap<String, DidDocument> = ...
override suspend fun resolveDid(did: Did): DidResolutionResult {
val document = documents[did.value]
val now = Clock.System.now()
return if (document != null) {
DidResolutionResult.Success(
document = document,
documentMetadata = DidDocumentMetadata(created = now, updated = now),
resolutionMetadata = DidResolutionMetadata(pattern = method)
)
} else {
DidResolutionResult.Failure.NotFound(did = did, reason = "DID not found")
}
}
Wrong method prefix or malformed DID string should use DidResolutionResult.Failure.InvalidFormat (or validate earlier with DidValidator). Unregistered method names are Failure.MethodNotRegistered at the registry layer.
Registration
Typical approach: ship a DidMethodProvider (and META-INF/services/org.trustweave.did.spi.DidMethodProvider (see existing plugins under did/plugins/)) so TrustWeave.build { } can resolve your method when the JAR is on the classpath.
Smoke test in a coroutine:
1
2
3
4
5
6
7
import kotlinx.coroutines.runBlocking
import org.trustweave.trust.TrustWeave
fun main() = runBlocking {
val trustWeave = TrustWeave.quickStart()
// Exercise createDid { method("yourMethod") } once SPI + classpath are wired.
}
There is no TrustWeave.registerDidMethod(...) on the facade; configuration is applied when the instance is built.
2. Implementing a Blockchain Anchor Client
The BlockchainAnchorClient interface allows you to add support for new blockchain networks.
Interface Definition
1
2
3
4
5
6
7
8
interface BlockchainAnchorClient {
suspend fun writePayload(
payload: JsonElement,
mediaType: String = "application/json"
): AnchorResult
suspend fun readPayload(ref: AnchorRef): AnchorResult
}
Example Implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.example.TrustWeave.plugins
import org.trustweave.anchor.*
import org.trustweave.core.exception.NotFoundException
import kotlinx.serialization.json.JsonElement
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong
/**
* Example blockchain anchor client implementation.
* This stores anchors in memory (use actual blockchain SDK in production).
*/
class ExampleBlockchainAnchorClient(
private val chainId: String,
private val contract: String? = null
) : BlockchainAnchorClient {
private val storage = ConcurrentHashMap<String, AnchorResult>()
private val txCounter = AtomicLong(0)
override suspend fun writePayload(
payload: JsonElement,
mediaType: String
): AnchorResult {
// Generate transaction hash
val txHash = "tx_${txCounter.incrementAndGet()}_${System.currentTimeMillis()}"
// Create anchor reference
val ref = AnchorRef(
chainId = chainId,
txHash = txHash,
contract = contract
)
// Create anchor result
val result = AnchorResult(
ref = ref,
payload = payload,
mediaType = mediaType,
timestamp = System.currentTimeMillis() / 1000
)
// Store (in production, submit to blockchain)
storage[txHash] = result
return result
}
override suspend fun readPayload(ref: AnchorRef): AnchorResult {
if (ref.chainId != chainId) {
throw IllegalArgumentException("Chain ID mismatch")
}
return storage[ref.txHash]
?: throw NotFoundException("Anchor not found: ${ref.txHash}")
}
}
Using AbstractBlockchainAnchorClient
For production implementations, extend AbstractBlockchainAnchorClient which provides fallback storage and common patterns:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import org.trustweave.anchor.AbstractBlockchainAnchorClient
class MyBlockchainClient(
chainId: String,
options: Map<String, Any?>
) : AbstractBlockchainAnchorClient(chainId, options) {
override protected fun canSubmitTransaction(): Boolean {
// Check if credentials are configured
return options["privateKey"] != null
}
override protected suspend fun submitTransactionToBlockchain(
payloadBytes: ByteArray
): String {
// Submit to actual blockchain
// Return transaction hash
return "0x..."
}
override protected suspend fun readTransactionFromBlockchain(
txHash: String
): AnchorResult {
// Read from actual blockchain
// Return AnchorResult
}
override protected fun buildExtraMetadata(mediaType: String): Map<String, String> {
return mapOf("network" to "mainnet", "mediaType" to mediaType)
}
override protected fun generateTestTxHash(): String {
return "test_${System.currentTimeMillis()}"
}
}
Registration
Expose a BlockchainAnchorClientProvider (SPI) named for your adapter, then reference it from TrustWeave.build:
1
2
3
4
5
6
7
8
val trustWeave = TrustWeave.build {
anchor {
chain("example:mainnet") {
provider("example")
options { /* passed to provider.create(chainId, options) */ }
}
}
}
3. Implementing a Proof Engine
The legacy ProofGenerator interface has been replaced by org.trustweave.credential.spi.proof.ProofEngine, which covers issuance, verification, and presentation creation for a given proof suite (VC-LD, VC-JWT, SD-JWT-VC, etc.) keyed by ProofSuiteId.
Reference
A full ProofEngine walkthrough is out of scope for this page — see the canonical
guide at Proof Engine Implementation Guide.
Quick pointers for orientation:
- SPI interface:
credentials/credential-api/src/main/kotlin/org/trustweave/credential/spi/proof/ProofEngine.kt - Built-in reference engines:
credentials/credential-api/.../proof/internal/engines/(VcLdProofEngine,SdJwtProofEngine) - External plugin examples:
credentials/plugins/bbs/(Bbs2023ProofEngine),credentials/plugins/mdl/(MdocProofEngine) - Registration: ship a
ProofEngineProviderand aMETA-INF/services/org.trustweave.credential.spi.proof.ProofEngineProviderfile. The defaultCredentialServicepicks it up automatically viaServiceLoader.
Registration
Proof engines are not registered on a separate proofGenerators { } builder. Ship a ProofEngineProvider with a META-INF/services/org.trustweave.credential.spi.proof.ProofEngineProvider entry, or compose them into a custom CredentialService (see below).
4. Implementing a Key Management Service
The KeyManagementService interface allows you to integrate with different key management backends.
Interface Definition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Real signatures return Result types (Generate/GetPublicKey/Sign/DeleteKeyResult).
interface KeyManagementService {
suspend fun getSupportedAlgorithms(): Set<Algorithm>
suspend fun generateKey(
algorithm: Algorithm,
options: Map<String, Any?> = emptyMap()
): GenerateKeyResult
suspend fun getPublicKey(keyId: KeyId): GetPublicKeyResult
suspend fun sign(
keyId: KeyId,
data: ByteArray,
algorithm: Algorithm? = null
): SignResult
suspend fun deleteKey(keyId: KeyId): DeleteKeyResult
}
Example Implementation
Reference: Copy from org.trustweave.testkit.kms.InMemoryKeyManagementService for a working implementation. Key points:
- Algorithms are the
Algorithmsealed class (e.g.Algorithm.Ed25519,Algorithm.Secp256k1) — not free-form strings. - All operations return
Resultsealed classes (GenerateKeyResult,GetPublicKeyResult,SignResult,DeleteKeyResult) — never throw cross-module exceptions or return rawKeyHandle/ByteArray. KeyIdis a value class wrapping the key identifier string.- Providers must implement
getSupportedAlgorithms()so the SPI layer can match capabilities.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Sketch only — see InMemoryKeyManagementService for full code.
class ExampleKeyManagementService : KeyManagementService {
override suspend fun getSupportedAlgorithms(): Set<Algorithm> =
setOf(Algorithm.Ed25519, Algorithm.Secp256k1)
override suspend fun generateKey(
algorithm: Algorithm,
options: Map<String, Any?>
): GenerateKeyResult = TODO()
override suspend fun getPublicKey(keyId: KeyId): GetPublicKeyResult = TODO()
override suspend fun sign(
keyId: KeyId,
data: ByteArray,
algorithm: Algorithm?
): SignResult = TODO()
override suspend fun deleteKey(keyId: KeyId): DeleteKeyResult = TODO()
}
Registration
1
2
3
4
val trustWeave = TrustWeave.build {
customKms(ExampleKeyManagementService())
did { method("key") { algorithm("Ed25519") } }
}
5. Extending credential issuance / verification
The public CredentialService API is issue(IssuanceRequest), verify(...), createPresentation, verifyPresentation, and format helpers (supports, supportedFormats). The default implementation composes built-in proof engines (VC-LD, SD-JWT-VC, etc.).
Typical extensions (instead of reimplementing the whole service):
- Implement or configure
ProofEngine/ SPI types undercredentials/credential-api(seespi.proofand internal engines). - Supply a custom
credentialService(...)instance only if you truly replace orchestration; register it on the facade:
1
2
3
4
5
6
7
8
9
import org.trustweave.trust.dsl.credential.KmsProviders
import org.trustweave.trust.dsl.credential.DidMethods
import org.trustweave.trust.dsl.credential.KeyAlgorithms
val trustWeave = TrustWeave.build {
keys { provider(KmsProviders.IN_MEMORY); algorithm(KeyAlgorithms.ED25519) }
did { method(DidMethods.KEY) { algorithm(KeyAlgorithms.ED25519) } }
credentialService(myCustomCredentialService)
}
See Credential Service API Reference for the current method list and CredentialServices / credentialService factories.
6. Implementing a Wallet Factory
The WalletFactory interface allows you to create custom wallet storage backends.
Interface Definition
1
2
3
4
5
6
7
8
9
interface WalletFactory {
suspend fun create(
providerName: String,
walletId: String? = null,
walletDid: String? = null,
holderDid: String? = null,
options: WalletCreationOptions = WalletCreationOptions()
): Wallet
}
Example Implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.example.TrustWeave.plugins
import org.trustweave.testkit.credential.InMemoryWallet
import org.trustweave.wallet.Wallet
import org.trustweave.wallet.services.WalletCreationOptions
import org.trustweave.wallet.services.WalletFactory
/**
* Example wallet factory implementation.
*/
class ExampleWalletFactory : WalletFactory {
override suspend fun create(
providerName: String,
walletId: String?,
walletDid: String?,
holderDid: String?,
options: WalletCreationOptions
): Wallet {
return when (providerName) {
"inMemory" -> {
// Create in-memory wallet
InMemoryWallet(
walletId = walletId ?: generateWalletId(),
holderDid = holderDid ?: throw IllegalArgumentException("holderDid required")
)
}
"database" -> {
throw UnsupportedOperationException(
"Replace with your own Wallet implementation (e.g. JDBC-backed) wired to options.storagePath"
)
}
else -> throw IllegalArgumentException("Unknown provider: $providerName")
}
}
private fun generateWalletId(): String {
return "wallet_${System.currentTimeMillis()}"
}
}
Registration
1
2
3
4
5
6
7
8
9
import kotlinx.coroutines.runBlocking
import org.trustweave.trust.TrustWeave
val trustWeave = runBlocking {
TrustWeave.build {
factories(walletFactory = ExampleWalletFactory())
did { method("key") { algorithm("Ed25519") } }
}
}
Plugin Registration Methods
Manual Registration
Configure plugins when building the facade (SPI-discovered providers are merged automatically):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import kotlinx.coroutines.runBlocking
import org.trustweave.trust.TrustWeave
val trustWeave = runBlocking {
TrustWeave.build {
customKms(MyKms())
factories(walletFactory = MyWalletFactory())
did {
method("mymethod") { /* options consumed by your DidMethodProvider, if any */ }
}
anchor {
chain("myChain:mainnet") {
provider("myProvider") // BlockchainAnchorClientProvider name
options { /* passed to provider.create(chainId, options) */ }
}
}
credentialService(MyCredentialService())
}
}
Runtime reconfiguration
The TrustWeave facade does not support adding DID methods or anchor clients after construction. Build a new instance with an updated TrustWeave.build { } block (or manage long-lived clients yourself and call TrustWeave.from(config) only from code that can obtain a TrustWeaveConfig legally).
SPI Auto-Discovery (Advanced)
For automatic discovery via Java ServiceLoader, implement provider interfaces:
- Create a provider class: ```kotlin import org.trustweave.did.DidCreationOptions import org.trustweave.did.DidMethod import org.trustweave.did.spi.DidMethodProvider
class MyDidMethodProvider : DidMethodProvider {
override val name = “mymethod”
override val supportedMethods: List
1
2
3
4
override fun create(methodName: String, options: DidCreationOptions): DidMethod? {
if (methodName !in supportedMethods) return null
return MyDidMethod(/* construct from options.additionalProperties */)
} } ```
- Create service file:
META-INF/services/org.trustweave.did.spi.DidMethodProvider1
com.example.MyDidMethodProvider
Plugin Lifecycle
Plugins can optionally implement PluginLifecycle for initialization and cleanup:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.trustweave.core.plugin.PluginLifecycle
class MyBlockchainClient : BlockchainAnchorClient, PluginLifecycle {
override suspend fun initialize(config: Map<String, Any?>): Boolean {
// Initialize connections, load configuration
return true
}
override suspend fun start(): Boolean {
// Start background processes
return true
}
override suspend fun stop(): Boolean {
// Stop accepting new operations
return true
}
override suspend fun cleanup() {
// Clean up resources
}
}
Call trustWeave.close() when you discard the facade so Closeable collaborators (KMS, anchor clients, etc.) can shut down. If your plugin implements PluginLifecycle, invoke those hooks from your own wiring; the facade does not expose initialize() / start() / stop().
See Plugin lifecycle for patterns.
Testing Your Plugin
Unit Testing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class ExampleDidMethodTest {
@Test
fun `test create DID`() = runTest {
val kms = InMemoryKeyManagementService()
val method = ExampleDidMethod(kms)
val document = method.createDid()
assertNotNull(document)
assertTrue(document.id.startsWith("did:example:"))
assertFalse(document.verificationMethod.isEmpty())
}
@Test
fun `test resolve DID`() = runTest {
val kms = InMemoryKeyManagementService()
val method = ExampleDidMethod(kms)
val document = method.createDid()
val result = method.resolveDid(document.id) as DidResolutionResult.Success
assertNotNull(result.document)
assertEquals(document.id, result.document.id)
}
}
Integration Testing
1
2
3
4
5
6
7
8
9
10
11
@Test
fun `test plugin with TrustWeave`() = runTest {
val trustWeave = TrustWeave.build {
customKms(InMemoryKeyManagementService())
did { method("example") { algorithm("Ed25519") } }
}
val did = trustWeave.createDid { method("example") }.getOrThrowDid()
// Did is a value class — use `.value` for the raw string.
assertTrue(did.value.startsWith("did:example:"))
}
Best Practices
- Error Handling: Always throw appropriate exceptions (
KeyNotFoundException,NotFoundException, etc.) - Thread Safety: Use concurrent collections for in-memory storage
- Resource Management: Implement
PluginLifecyclefor plugins that need initialization/cleanup - Documentation: Document any method-specific options or requirements
- Testing: Provide both unit tests and integration tests
- Type Safety: Use type-safe options classes when available
- Idempotency: Make operations idempotent where possible
- Validation: Validate inputs early and provide clear error messages
Next Steps
- Review existing implementations in
trustweave-testkitfor reference - See Integration Modules for production examples
- Check Plugin Lifecycle for lifecycle management
- Review Architecture Overview for design patterns