3ID Connect is a hosted 3ID account management and authentication system for browser-based Ceramic applications that allows users to onboard to applications and transact with Ceramic documents by signing authentication messages with their blockchain wallets. Try a demo of 3ID Connect here.

3ID Connect already supports Ethereum accounts, but this tutorial will show you how to add support for accounts from new blockchains to 3ID and 3ID Connect. This is the final part of a three-part series on 3ID Connect. Previous posts:

About 3id-blockchain-utils

All of the core functionality for supporting accounts from new blockchains/networks in 3ID and 3ID Connect is provided by the 3id-blockchain-utils package. This library allows the creation and validation of public links that cryptographically bind a blockchain account to a 3ID DID method using Ceramic documents. It also allows 3ID Connect and other similar 3ID account management/authentication libraries to create these public links as well as add the blockchain account as an authentication method capable of controlling a DID.

To add a new blockchain, you will need to implement a few functions in 3id-blockchain-utils. Before beginning, take a peek at the library to explore examples of current blockchain implementations such as Ethereum, Polkadot, and Filecoin. Then, continue with the process outlined below.

Fork 3id-blockchain-utils

Start out by forking the 3id-blockchain-utils library on Github. This will allow you to make changes locally and then push everything up in a Pull Request once you're done.

Create your files

First, create a file with the following path src/blockchains/<blockchain-name>.js to contain the JavaScript code that implements your blockchain. Then, create a file with the src/blockchains/__tests__/<blockchain-name>.test.js path to contain any tests for your blockchain.

Adhere to CAIP standards

In the interface seen below, note that  namespace and AccountID use ChainAgnostic (CAIP) standards. namespace uses CAIP-2 which specifies a standard id to represent any blockchain and/or network. For example, this is 'eip155' for Ethereum and 'fil' for Filecoin. AccountID uses CAIP-10 which defines a standardized way to represent any account on any blockchain.

If a CAIP standard for the blockchain or network you want to add support for does not already exist, follow directions in the CAIP Github repo to add it.

Implement the interface

You must create a BlockchainHandler module that exports the following interface which takes a CAIP-2 namespace and three functions: authenticate, createLink, and validateLink.

interface BlockchainHandler {
  namespace: string;
  authenticate(message: string, account: AccountID, provider: any): Promise<string>;
  validateLink (proof: LinkProof): Promise<LinkProof | null>;
  createLink (did: string, account: AccountID, provider: any, opts?: BlockchainHandlerOpts): Promise<LinkProof>;
} 

Now, let's implement the interface.

authenticate()

The authenticate function allows a blockchain account to be added as an authentication method (authMethod) to a 3ID. This means using your blockchain account you will always be able to access that 3ID and derive its 3ID Keychain for use, for example in 3ID Connect. This function consumes the following:

  • message: string, can be any string
  • AccountID: an instance of a CAIP-10 AccountID
  • provider: specific to your blockchain. This is any standard signer or provider defined for your blockchain. Ideally your ecosystem has a widely-accepted standard interface so that this module can support signing by most accounts.

The function should be implemented such that the given AccountID signs the given message using the given provider, and then returns a fixed length string deterministically derived from the signature.

The only strict requirements here are that:

  1. The string derivation is deterministic, and will return the same string for a given message + AccountID combination.
  2. Unique messages return unique strings.

Typically implementation looks as follows:

async function authenticate(message: string, account: AccountID, provider: any): Promise<string> {
  const address = account.address
  // convert message into proper input for your signer, ie object, encoding, etc
  const payload = toPayload(message, address)
  // use provider to sign message
  const signature = await provider.sign(payload, address)
  // typically good to hash and return, so that fixed length hex string is returned
  return sha256(signature)
}

The createLink function allows a blockchain account to create a verifiable link proof that publicly binds the blockchain account to a given DID. In Ceramic, these these link proofs can be used to create CAIP-10 Link Documents which allow anyone to look up the 3ID DID linked to your blockchain account, and then resolve any other public info linked to your 3ID. The DocIDs of your CAIP-10 Link documents can be stored in the IDX Crypto Accounts documents for simple lookup.

This function consumes similar arguments as described above. It also consumes the 3ID DID string that is being linked. This function is implemented such that when the given AccountID signs a message including the given DID with the given provider, a LinkProof is returned.

async function createLink (did: string, account: AccountID, provider: any, opts: BlockchainHandlerOpts): Promise<LinkProof> {
  const address = account.address
  // util function that returns standard message string including did 
  const linkMessage = getConsentMessage(did)
  // convert message into proper input for your signer, ie object, encoding, etc
  const payload = toPayload(linkMessage.message, address)
  // use provider to sign message
  const signature = await provider.sign(payload, address)
  const proof: LinkProof = {
    // version and type are implementation specific context used for verifying linkProofs later
    version: 2,
    type: 'eoa',
    message: linkMessage.message,
    signature: signature,
    account: account.toString()
  }
  return proof
}

Lastly the validateLink function validates a given LinkProof. This allows anyone to easily verify linkProofs and for Ceramic to validate  CAIP-10 Link Documents. The function consumes a LinkProof and returns the LinkProof if valid, otherwise it returns null. Valid typically means that the given signature in the LinkProof is valid over the given message and is created by the given account.

async function validateLink (proof: LinkProof): Promise<LinkProof | null> {
  const account = new AccountID(proof.account)
  const address = account.address
  const payload = toPayload(proof.message, address)
  // specific to how ever you verify signatures
  const res = await verifySignature(proof.signature, payload)
  return res
}

Open a pull request

Now that you have implemented the required interface and have included the necessary tests with adequate coverage, open a pull request in 3id-blockchain-utils. Once reviewed, it will be merged and released.

When opening a pull request, please be sure to minimize the size of new dependencies and/or try to use libraries that are likely shared across blockchain implementations. If you include large, one-off dependencies in your PR, support for the blockchain may not be included by default in other libraries like js-ceramic.

Usage in 3ID Connect

After adding support for the blockchain in 3id-blockchain-utils, it is easy to use these newly supported blockchain accounts with 3ID Connect. 3ID Connect consumes this functionality through an AuthProvider. You can implement an AuthProvider in your own codebase and/or also submit a pull request to 3ID Connect so that others can easily use it as well.

The interface for AuthProvider can be found below, and we will use the ethereumAuthProvider as an example. You can reference that code for more details, and find the AbstractAuthProvider interface to reference.

class EthereumAuthProvider extends AbstractAuthProvider {
  constructor(ethProvider: any, address:string ): string {
    //...
  }

  async authenticate(message: string, address:string): Promise<string> {
    // call authenticate from 3id-blockchain utils, encode any params as needed
    // ...
    return authenticate(message, accountId, this.provider)
  }

  async createLink(did: string, address: string): Promise<LinkProof> {
    // call createLink from 3id-blockchain utils, encode any params as needed
    // ...
    return createLink(did, accountId, this.provider, { type: 'eoa' })
  }
}

Once you have an authProvider you can now use your newly supported blockchain accounts to authenticate and link accounts to 3IDs in 3ID Connect. You can reference the "How to use 3ID Connect in browser applications" post for more details, but you simply have to pass your AuthProvider when connecting to 3ID Connect.

const threeIdConnect = new ThreeIdConnect()
const authProvider = new yourAuthProvider(yourProviderOrSigner, addressOrAccount)
await threeIdConnect.connect(authProvider)

That's it!

This tutorial covered how to implement support for any simple key pair account on any blockchain in 3ID Connect. However, these interfaces also allow support for more complex account implementations such as contract accounts that may support multi-sig accounts and other account or identity features. It may be possible to add support for other types of accounts in the future as well. If you have any interest in implementing or exploring these types, reach out to us on Discord.

Further Reading

Check out the other posts in this three-part series on 3ID Connect:

Questions?

Reach out in the Ceramic Discord.