For the complete documentation index, see llms.txt. This page is also available as Markdown.
Share KYC Attributes
This guide shows how to use the Anchor SDK to share KYC attributes with an Anchor that requires KYC using an existing on-chain KYC certificate.
1
Identify Share KYC Is Required
When an Anchor requires KYC it will return an error of type KeetaAssetMovementAnchorKYCShareNeededError and will include the required KYC attributes and which Keeta account they should be shared with. Sharing KYC attributes uses encrypted containers that only grant the specific principals access to decrypt the container so it remains encrypted in transit.
KeetaAssetMovementAnchorKYCShareNeededError members shown below.
2
Select KYC Certificate
Full example below shows more in depth details on identifying a KYC certificate on the users account, for example an Anchor may require a certificate from a specific issuer. At a high level it follows these steps to get the certificates for the account from the network and build the Certificate.
3
Build the Sharable Certificate Attributes
Create the encrypted container to share certificate attributes, this selects only the attributes requested by the Anchor.
4
Grant Access for the Anchor
Granting access to the encrypted container allows the principal to decrypt the attributes. This enables the sensitive attributes to be encrypted in transit and only viewable by the recipient.
5
Share the Attributes with the Anchor
Anchor providers that require KYC have an endpoint to share the attributes. The Anchor Client SDK handles the request details.
6
Handle Additional Onboarding Steps
An Anchor may require some additional onboarding steps. Like adding it's own certificate to the account or granting permissions. These will be identified with an error of type KeetaAssetMovementAnchorUserActionNeededError
The actionsNeeded attribute of the error can be used to complete the actions and the UserClientBuilder can be used to construct a Keeta block to publish the actions.
// Get account certificates
const records = await userClient.client.getAllCertificates(account);
// Build the intermediates from the first result
const intermediateSet = records[0].intermediates
? new Set(records[0].intermediates.getCertificates())
: undefined;
// Construct the certificate from the first result
const certificate = new KeetaAnchor.lib.Certificates.Certificate(records[0].certificate.toPEM(), {
subjectKey: userAccount,
store: {
root: trustedRoots,
intermediate: intermediateSet ?? new Set()
}
});
IReadOnlyList<OnChainCertificate> records = await nodeClient.GetAllCertificates(userAccount, cancellationToken);
const intermediateSet = record.intermediates
? new Set(record.intermediates.getCertificates())
: undefined;
if (trustedRoots.size > 0) {
const cert = new KeetaAnchor.lib.Certificates.Certificate(record.certificate.toPEM(), {
subjectKey: account,
store: {
root: trustedRoots,
intermediate: intermediateSet ?? new Set()
}
});
}
for (const principal of shareWithPrincipals) {
await sharable.grantAccess(principal);
}
foreach (string principalAddress in shareWithPrincipals)
{
using Account principal = runtime.Accounts.FromPublicKeyString(principalAddress);
sharable.GrantAccess([principal]);
}
// C#: translate the required actions into ledger operations and publish them in one block.
await UserActions.Execute(runtime, userClient, userActionNeeded, cancellationToken);
#! /usr/bin/env ts-node
/*
* Description: Use the Keeta Anchor Client to share KYC attributes to an Anchor and Onboard
*/
import * as KeetaAnchor from "@keetanetwork/anchor";
import type { CertificateAttributeNames } from "@keetanetwork/anchor/lib/certificates.js";
import type { KeetaAssetMovementAnchorProvider } from "@keetanetwork/anchor/services/asset-movement/client.js";
import { Errors } from "@keetanetwork/anchor/services/asset-movement/common.js";
import { debugPrintableObject as DPO, promptUser } from "../helper.js";
import * as util from "util";
const Account = KeetaAnchor.KeetaNet.lib.Account;
const DEBUG = false;
const logger = DEBUG ? { logger: console } : {};
const network = "test";
const KEETA_USD_ASSET = Account.fromPublicKeyString('keeta_any4zllibya6fum3lsoimxmnmeo57nklxlh4c6d6xosfacarfaa3knkiprkmm');
const US_BANK_SOURCE = { type: 'bank-account', account: { type: 'us' }} as const;
/** Keeta Test Network KYC Root CA â used to verify on-chain KYC certificate chains */
const KYC_ROOT_CA_PEM = `-----BEGIN CERTIFICATE-----
MIIBiDCCAS2gAwIBAgIGAZhi7awAMAsGCWCGSAFlAwQDCjApMScwJQYDVQQDEx5L
ZWV0YSBUZXN0IE5ldHdvcmsgS1lDIFJvb3QgQ0EwHhcNMjUwODAxMDAwMDAwWhcN
MjgwODAxMDAwMDAwWjApMScwJQYDVQQDEx5LZWV0YSBUZXN0IE5ldHdvcmsgS1lD
IFJvb3QgQ0EwNjAQBgcqhkjOPQIBBgUrgQQACgMiAAKK1O9NiYvu2sBYNRPfjOpp
sNSMZ1lOVn+psFdk3Ugq2qNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E
BAMCAMYwHwYDVR0jBBgwFoAUap82oKFjJ2jhIj2CGABULiX4h3owHQYDVR0OBBYE
FGqfNqChYydo4SI9ghgAVC4l+Id6MAsGCWCGSAFlAwQDCgNIADBFAiEAqnl85S6v
bw8HLO+YXhnwqq6GmnY+7tCcnwYtoyDzYTMCIEw7ALqHJp0kO9AExm5sSoC7rPOd
GlX42GsZQW3AJ7Jc
-----END CERTIFICATE-----`;
const KYC_ROOT_CA = new KeetaAnchor.lib.Certificates.Certificate(KYC_ROOT_CA_PEM);
type BaseCertificate = InstanceType<typeof KeetaAnchor.KeetaNet.lib.Utils.Certificate.Certificate>;
type AnchorCertificate = InstanceType<typeof KeetaAnchor.lib.Certificates.Certificate>;
type SharableCertificateAttributes = InstanceType<typeof KeetaAnchor.lib.Certificates.SharableCertificateAttributes>;
/**
* Map the attribute names requested by the provider to the typed names expected by
* `SharableCertificateAttributes.fromCertificate`.
*
* `neededAttributes` lists attributes the provider can accept; not all are required.
* Attributes absent on the certificate are skipped by `fromCertificate`.
*/
function resolveAttributeNames(neededAttributes: string[] | undefined): CertificateAttributeNames[] | undefined {
if (!neededAttributes || neededAttributes.length === 0) {
return(undefined);
}
return(neededAttributes.map(function(name): CertificateAttributeNames {
KeetaAnchor.lib.Certificates.SharableCertificateAttributes.assertCertificateAttributeName(name);
return(name);
}));
}
/**
* Select an on-chain KYC leaf certificate whose chain terminates at {@link KYC_ROOT_CA}.
*
* When the provider supplies `acceptedIssuers`, the leaf must chain to the known
* test-network root. Opening with `{ subjectKey, store: { root, intermediate } }`
* verifies the chain; `subjectKey` alone is used only when no issuer filter applies.
*/
async function selectOnChainKYCCertificate(
userClient: InstanceType<typeof KeetaAnchor.KeetaNet.UserClient>,
account: InstanceType<typeof Account>,
requireTrustedChain: boolean
): Promise<{ certificate: AnchorCertificate; intermediates: Set<BaseCertificate> | undefined }> {
const records = await userClient.client.getAllCertificates(account);
if (records.length === 0) {
throw(new Error('No on-chain KYC certificates found for this account'));
}
const trustedRoots = requireTrustedChain ? new Set<BaseCertificate>([ KYC_ROOT_CA ]) : new Set<BaseCertificate>();
const rejections: string[] = [];
for (const record of records) {
const intermediateSet = record.intermediates
? new Set(record.intermediates.getCertificates())
: undefined;
try {
if (trustedRoots.size > 0) {
const cert = new KeetaAnchor.lib.Certificates.Certificate(record.certificate.toPEM(), {
subjectKey: account,
store: {
root: trustedRoots,
intermediate: intermediateSet ?? new Set()
}
});
if (!cert.checkValid() || !cert.trusted) {
rejections.push(`chain not trusted (issuer DN: ${JSON.stringify(cert.issuerDN)})`);
continue;
}
return({ certificate: cert, intermediates: intermediateSet });
}
const cert = new KeetaAnchor.lib.Certificates.Certificate(record.certificate.toPEM(), {
subjectKey: account
});
if (!cert.checkValid()) {
rejections.push('certificate not valid at current time');
continue;
}
return({ certificate: cert, intermediates: intermediateSet });
} catch (error) {
rejections.push(error instanceof Error ? error.message : String(error));
}
}
throw(new Error([
requireTrustedChain
? 'No on-chain KYC certificate chains to KYC_ROOT_CA'
: 'No valid on-chain KYC certificate found',
requireTrustedChain ? 'Ensure kyc-client.ts attached the certificate with intermediates.' : undefined,
...rejections.map(function(reason) { return(` - ${reason}`); })
].filter(function(line): line is string { return(line !== undefined); }).join('\n')));
}
/**
* Load the on-chain KYC certificate and build a sharable attributes container.
*
* Selects a leaf whose chain terminates at {@link KYC_ROOT_CA}, packages the
* requested attributes, and grants access to each principal from the KYC share
* instructions so the container can be posted to the provider.
*/
async function buildSharableKYCAttributes(
userClient: InstanceType<typeof KeetaAnchor.KeetaNet.UserClient>,
account: InstanceType<typeof Account>,
kycShareNeeded: InstanceType<typeof Errors.KYCShareNeeded>
): Promise<SharableCertificateAttributes> {
const { certificate: selectedCertificate, intermediates } = await selectOnChainKYCCertificate(
userClient,
account,
kycShareNeeded.acceptedIssuers.length > 0
);
const attributeNames = resolveAttributeNames(kycShareNeeded.neededAttributes);
const sharable = await KeetaAnchor.lib.Certificates.SharableCertificateAttributes.fromCertificate(
selectedCertificate,
intermediates,
attributeNames
);
for (const principal of kycShareNeeded.shareWithPrincipals) {
await sharable.grantAccess(principal);
}
return(sharable);
}
/**
* Publish on-chain blocks for provider onboarding steps (add certificate, grant permission).
*/
async function executeUserActionNeeded(
userClient: InstanceType<typeof KeetaAnchor.KeetaNet.UserClient>,
userActionNeeded: InstanceType<typeof Errors.UserActionNeeded>
): Promise<void> {
const builder = userClient.initBuilder();
Errors.UserActionNeeded.addOperationsToBuilder(userActionNeeded.actionsNeeded, builder);
await userClient.publishBuilder(builder);
console.log('Onboarding steps completed.\n');
}
/**
* Request a persistent forwarding address, running any onboarding steps the provider
* returns via {@link Errors.UserActionNeeded} before retrying.
*/
async function createPersistentForwardingAddressWithOnboarding(
provider: KeetaAssetMovementAnchorProvider,
userClient: InstanceType<typeof KeetaAnchor.KeetaNet.UserClient>,
request: Parameters<KeetaAssetMovementAnchorProvider['createPersistentForwardingAddress']>[0],
promptBeforeOnboarding: boolean
): Promise<Awaited<ReturnType<KeetaAssetMovementAnchorProvider['createPersistentForwardingAddress']>>> {
for (;;) {
try {
return(await provider.createPersistentForwardingAddress(request));
} catch (error) {
if (!Errors.UserActionNeeded.isInstance(error)) {
throw(error);
}
console.log('Onboarding steps required:');
console.log(util.inspect(DPO(error.actionsNeeded), { depth: 6, colors: true }));
if (promptBeforeOnboarding) {
const proceed = await promptUser('\nComplete onboarding steps and retry? (y/n): ');
if (proceed.trim().toLowerCase() !== 'y') {
throw(error);
}
}
await executeUserActionNeeded(userClient, error);
}
}
}
async function main() {
console.log("Keeta KYC Example: Request USD Bank Deposit Address");
console.log("====================================================\n");
const seed = await promptUser('Enter your Keeta SEED with KYC completed: ');
if (!seed.trim()) {
throw(new Error('Invalid seed'));
}
const account = Account.fromSeed(seed.trim(), 0);
console.log(`Keeta Account: ${account.publicKeyString.get()}\n`);
await using userClient = KeetaAnchor.KeetaNet.UserClient.fromNetwork(network, account);
const assetMovementClient = new KeetaAnchor.AssetMovement.Client(userClient, {
root: userClient.networkAddress,
...logger
});
const keetaDestination = {
type: 'chain',
chain: { type: 'keeta', networkId: userClient.network }
} as const;
const assetPair = { from: 'USD' as const, to: KEETA_USD_ASSET };
// Get the providers for the transfer to trigger KYC share
const providers = await assetMovementClient.getProvidersForTransfer({
asset: assetPair,
from: US_BANK_SOURCE,
to: keetaDestination
});
if (!providers || providers.length === 0) {
throw(new Error('No asset movement providers found for USD bank deposit to Keeta'));
}
const provider = providers[0];
if (!provider) {
throw(new Error('Provider is undefined'));
}
console.log(`Using provider: ${String(provider.providerID)}`);
const persistentAddressRequest = {
account,
asset: assetPair,
sourceLocation: US_BANK_SOURCE,
destinationLocation: keetaDestination,
destinationAddress: account.publicKeyString.get()
} as const;
try {
const persistentAddress = await createPersistentForwardingAddressWithOnboarding(
provider,
userClient,
persistentAddressRequest,
true
);
console.log('Persistent address created (KYC share was not required):');
console.log(util.inspect(DPO(persistentAddress), { depth: 6, colors: true }));
} catch (error) {
if (!Errors.KYCShareNeeded.isInstance(error)) {
throw(error);
}
console.log('KYC Share Instructions:');
console.log(util.inspect(DPO({
message: error.message,
neededAttributes: error.neededAttributes,
shareWithPrincipals: error.shareWithPrincipals.map(function(principal) {
return(principal.publicKeyString.get());
}),
acceptedIssuers: error.acceptedIssuers,
tosFlow: error.tosFlow
}), { depth: 6, colors: true }));
const proceed = await promptUser('\nShare KYC attributes and retry? (y/n): ');
if (proceed.trim().toLowerCase() !== 'y') {
console.log('Exiting without sharing KYC attributes.');
return;
}
const sharable = await buildSharableKYCAttributes(userClient, account, error);
const sharedAttributeNames = await sharable.getAttributeNames();
console.log(`Sharing ${sharedAttributeNames.length} KYC attributes: ${sharedAttributeNames.join(', ')}\n`);
// Optional: some providers include `tosFlow` when TOS must be accepted out-of-band
// before KYC sharing succeeds. Extend here to pass `tosAgreement: { id }` once the
// flow returns an ID.
if (error.tosFlow?.type === 'url-flow') {
console.log(`\nAccept Terms of Service:\n ${error.tosFlow.url}\n`);
await promptUser('Press Enter after accepting TOS: ');
}
await provider.shareKYCAttributes({
account,
attributes: sharable
});
console.log('KYC attributes shared.\n');
// Create a persistent forwarding address to confirm KYC share was successful
const persistentAddress = await createPersistentForwardingAddressWithOnboarding(
provider,
userClient,
persistentAddressRequest,
true
);
console.log('Persistent address created:');
console.log(util.inspect(DPO(persistentAddress), { depth: 6, colors: true }));
}
}
main().then(function() {
process.exit(0);
}, function(err: unknown) {
console.error(err);
process.exit(1);
});
using System.Text.Json;
using KeetaNet.Anchor;
using KeetaNet.Anchor.Crypto;
using KeetaNet.Examples.Anchor.AssetMovement;
using KeetaNet.Examples.Common;
using CryptoCertificate = KeetaNet.Anchor.Crypto.Certificate;
using OnChainCertificate = KeetaNet.Anchor.Certificate;
using UserClient = KeetaNet.Examples.Network.UserClient;
namespace KeetaNet.Examples.Anchor;
public sealed class KycClientShareKycExample : IKeetaExample
{
private const string Network = "test";
public string Id => "anchor/kyc-client-sharekyc";
public string Description =>
"Use the Keeta Anchor Client to share KYC attributes to an Anchor and onboard";
public async Task<int> Run(string[] args, CancellationToken cancellationToken = default)
{
Console.WriteLine("""
Keeta KYC Example: Request USD Bank Deposit Address
====================================================
""");
using var runtime = WasmRuntime.Load();
string seed = Helper.ReadLine("Enter your Keeta SEED with KYC completed: ").Trim();
if (seed.Length == 0)
{
throw new InvalidOperationException("Invalid seed");
}
using Account userAccount = runtime.Accounts.FromSeed(seed, 0, "ecdsa_secp256k1");
Console.WriteLine($"Keeta Account: {userAccount.PublicKeyString}\n");
UserClient userClient = UserClient.FromNetwork(Network, userAccount);
using AssetMovementClient assetMovementClient = runtime.CreateAssetMovementClient(
Constants.NodeApi,
userClient.NetworkAddress,
userAccount);
using NodeClient nodeClient = runtime.CreateNodeClient(Constants.NodeApi);
string keetaDestination = $"chain:keeta:{userClient.Network}";
AssetOrPair assetPair = AssetOrPair.Pair("USD", Constants.KeetaUsdAsset);
IReadOnlyList<AssetProvider> providers = await assetMovementClient.GetProvidersForTransfer(
new AssetProviderSearch(
Asset: assetPair,
From: Constants.BankAccountUsLocation,
To: keetaDestination),
cancellationToken);
if (providers.Count == 0)
{
throw new InvalidOperationException("No asset movement providers found for USD bank deposit to Keeta");
}
AssetProvider provider = providers[0];
Console.WriteLine($"Using provider: {provider.Id}");
AssetCreateAddressRequest persistentAddressRequest = new(
SourceLocation: Constants.BankAccountUsLocation,
Asset: assetPair,
DestinationLocation: keetaDestination,
DestinationAddress: userAccount.PublicKeyString);
try
{
JsonElement persistentAddress = await CreatePersistentForwardingAddressWithOnboarding(
runtime,
assetMovementClient,
provider,
userClient,
persistentAddressRequest,
promptBeforeOnboarding: true,
cancellationToken);
Console.WriteLine("Persistent address created (KYC share was not required):");
Console.WriteLine(JsonSerializer.Serialize(persistentAddress, new JsonSerializerOptions { WriteIndented = true }));
return 0;
}
catch (KeetaBlockerException refusal) when (refusal.Blocker is AssetKycShareNeededBlocker shareBlocker)
{
Console.WriteLine("KYC Share Instructions:");
Console.WriteLine(JsonSerializer.Serialize(new
{
neededAttributes = shareBlocker.NeededAttributes,
shareWithPrincipals = shareBlocker.ShareWithPrincipals,
acceptedIssuers = shareBlocker.AcceptedIssuers,
tosFlow = shareBlocker.TosFlow,
}, new JsonSerializerOptions { WriteIndented = true }));
string proceed = Helper.ReadLine("\nShare KYC attributes and retry? (y/n): ");
if (!proceed.Trim().Equals("y", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Exiting without sharing KYC attributes.");
return 0;
}
using SharableCertificateAttributes sharable = await BuildSharableKycAttributes(
runtime,
nodeClient,
userAccount,
shareBlocker,
cancellationToken);
IReadOnlyList<string> sharedAttributeNames = sharable.GetAttributeNames();
Console.WriteLine(
$"Sharing {sharedAttributeNames.Count} KYC attributes: {string.Join(", ", sharedAttributeNames)}\n");
if (shareBlocker.TosFlow is { ValueKind: JsonValueKind.Object } tosFlow
&& tosFlow.TryGetProperty("type", out JsonElement tosType)
&& tosType.GetString() == "url-flow"
&& tosFlow.TryGetProperty("url", out JsonElement tosUrl))
{
Console.WriteLine($"\nAccept Terms of Service:\n {tosUrl.GetString()}\n");
Helper.ReadLine("Press Enter after accepting TOS: ");
}
await assetMovementClient.ShareKycAttributesAndWait(
provider,
new AssetShareKycRequest(sharable.ToPem()),
cancellationToken: cancellationToken);
Console.WriteLine("KYC attributes shared.\n");
JsonElement createdAddress = await CreatePersistentForwardingAddressWithOnboarding(
runtime,
assetMovementClient,
provider,
userClient,
persistentAddressRequest,
promptBeforeOnboarding: true,
cancellationToken);
Console.WriteLine("Persistent address created:");
Console.WriteLine(JsonSerializer.Serialize(createdAddress, new JsonSerializerOptions { WriteIndented = true }));
return 0;
}
}
private static async Task<JsonElement> CreatePersistentForwardingAddressWithOnboarding(
WasmRuntime runtime,
AssetMovementClient assetMovementClient,
AssetProvider provider,
UserClient userClient,
AssetCreateAddressRequest request,
bool promptBeforeOnboarding,
CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
return await assetMovementClient.CreatePersistentForwardingAddress(
provider,
request,
cancellationToken);
}
catch (KeetaBlockerException refusal) when (refusal.Blocker is AssetKycShareNeededBlocker)
{
throw;
}
catch (KeetaBlockerException refusal) when (refusal.Blocker is AssetUserActionNeededBlocker userActionNeeded)
{
Console.WriteLine("Onboarding steps required:");
Console.WriteLine(JsonSerializer.Serialize(userActionNeeded.ActionsNeeded, new JsonSerializerOptions { WriteIndented = true }));
if (promptBeforeOnboarding)
{
string proceed = Helper.ReadLine("\nComplete onboarding steps and retry? (y/n): ");
if (!proceed.Trim().Equals("y", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Onboarding cancelled", refusal);
}
}
await UserActions.Execute(runtime, userClient, userActionNeeded, cancellationToken);
Console.WriteLine("Onboarding steps completed.\n");
}
}
throw new OperationCanceledException(cancellationToken);
}
private static async Task<SharableCertificateAttributes> BuildSharableKycAttributes(
WasmRuntime runtime,
NodeClient nodeClient,
Account userAccount,
AssetKycShareNeededBlocker blocker,
CancellationToken cancellationToken)
{
bool requiresTrustedChain = blocker.AcceptedIssuers.ValueKind == JsonValueKind.Array
&& blocker.AcceptedIssuers.GetArrayLength() > 0;
(KycCertificate selected, IReadOnlyList<CryptoCertificate> intermediates) = await SelectOnChainKycCertificate(
runtime,
nodeClient,
userAccount,
requiresTrustedChain,
cancellationToken);
try
{
using HttpClient httpClient = new();
SharableCertificateAttributes sharable = await runtime.Sharables.FromCertificate(
selected,
userAccount,
httpClient,
intermediates,
blocker.NeededAttributes ?? [],
cancellationToken);
foreach (string principalAddress in blocker.ShareWithPrincipals)
{
using Account principal = runtime.Accounts.FromPublicKeyString(principalAddress);
sharable.GrantAccess([principal]);
}
return sharable;
}
finally
{
selected.Dispose();
foreach (CryptoCertificate intermediate in intermediates)
{
intermediate.Dispose();
}
}
}
private static async Task<(KycCertificate Certificate, IReadOnlyList<CryptoCertificate> Intermediates)> SelectOnChainKycCertificate(
WasmRuntime runtime,
NodeClient nodeClient,
Account userAccount,
bool requireTrustedChain,
CancellationToken cancellationToken)
{
IReadOnlyList<OnChainCertificate> records =
await nodeClient.GetAllCertificates(userAccount, cancellationToken);
if (records.Count == 0)
{
throw new InvalidOperationException("No on-chain KYC certificates found for this account");
}
using CryptoCertificate? trustedRoot = requireTrustedChain
? runtime.Certificates.Parse(Constants.KycRootCaPem)
: null;
List<string> rejections = new();
foreach (OnChainCertificate record in records)
{
List<CryptoCertificate> intermediateCertificates = new();
foreach (string intermediatePem in record.Intermediates)
{
intermediateCertificates.Add(runtime.Certificates.Parse(intermediatePem));
}
KycCertificate certificate = runtime.KycCertificates.Parse(record.Value);
try
{
if (requireTrustedChain && trustedRoot is not null)
{
if (!certificate.Verify(
new[] { trustedRoot },
intermediateCertificates,
DateTimeOffset.UtcNow))
{
using CryptoCertificate baseCertificate = certificate.Base();
rejections.Add($"chain not trusted (issuer DN: {baseCertificate.Issuer})");
certificate.Dispose();
foreach (CryptoCertificate intermediate in intermediateCertificates)
{
intermediate.Dispose();
}
continue;
}
}
else if (!certificate.IsValidAt(DateTimeOffset.UtcNow))
{
rejections.Add("certificate not valid at current time");
certificate.Dispose();
foreach (CryptoCertificate intermediate in intermediateCertificates)
{
intermediate.Dispose();
}
continue;
}
return (certificate, intermediateCertificates);
}
catch (Exception error)
{
rejections.Add(error.Message);
certificate.Dispose();
foreach (CryptoCertificate intermediate in intermediateCertificates)
{
intermediate.Dispose();
}
}
}
string message = requireTrustedChain
? "No on-chain KYC certificate chains to the test-network KYC root CA"
: "No valid on-chain KYC certificate found";
throw new InvalidOperationException(
string.Join('\n', new List<string> { message }.Concat(rejections.Select(reason => $" - {reason}"))));
}
}