⭐CHAPI Wallet Setup Guide
Learn how to use LearnCard to build a CHAPI compliant wallet application
In order to make a CHAPI compliant wallet, there are six things that your site will need to do:
Install/run the web-credential-polyfill
Run the
installHandlermethodHost a public manifest.json file
Host a public wallet service worker
[Optional] Host a storage endpoint for users to visit when storing a credential via CHAPI
[Optional] Host a get endpoint for users to visit when retrieving a credential via CHAPI
Install/run the web-credential-polyfill
Installing and running the web-credential-polyfill could not be easier with LearnCard! It is automatically run for you when constructing a wallet!
const learnCard = await initLearnCard();Run the installHandler method
After initializing a wallet, prompt the user to use your application as a CHAPI wallet by calling the installChapiHandler method:
await learnCard.invoke.installChapiHandler();Host a public manifest.json file
This step is a bit trickier, and is deeply intertwined with the next step. The simple answer here is to add a manifest.json file to your site that is hosted at /manifest.json with contents similar to the following:
{
"name": "LearnCard Demo CHAPI Wallet",
"short_name": "LearnCard Demo CHAPI Wallet",
"icons": [
{
"sizes": "64x64",
"src": "icon.png",
"type": "image/png"
}
],
"credential_handler": {
"url": "/wallet-worker",
"enabledTypes": ["VerifiablePresentation"]
}
}Replacing the url with the path to the service worker you set up in the next step, and pointing src to an image file that you would like to appear in the CHAPI menu.
In most bundlers/webapp setups, you will simply place this file inside the public directory. However, if you are not using a bundler and instead just hosting static files, you will want to place this file right next to your index.html file.
Host a public wallet service worker
Using the url defined in the manifest.json above, add a public endpoint to your site that can be used to instantiate an empty wallet and run the following code:
import { initLearnCard } from '@learncard/init';
const learnCard = await initLearnCard();
try {
// This will ask the user if they'd like to use your application as a CHAPI
// compliant wallet
await learnCard.invoke.installChapiHandler();
} catch (error) {
console.error('Error installing Chapi Handler:', error);
}
learnCard.invoke.activateChapiHandler({
get: async () => {
// Return an arbitrary route to display to users when requesting a credential
// or using DIDAuth with your application
return { type: 'redirect', url: `${window.location.origin}/get` };
},
store: async () => {
// Return an arbitrary route to display to users when storing a credential
// with your application
return { type: 'redirect', url: `${window.location.origin}/store` };
},
});There's a lot going on in this short amount of code, so let's break it down step-by-step:
A user visits your site and does something which ultimately instantiates a wallet and calls
installHandlerA pop-up appears, asking the user if they'd like to use your site as a CHAPI wallet. Let's assume they say yes!
CHAPI looks at the hosted
manifest.jsonto get some basic information about your wallet, such as what it's called and where to find this service workerThe same user then uses a website that asks to store a credential using CHAPI
A pop-up appears, asking the user what CHAPI wallet they'd like to use. Let's assume they say yours!
The
storefunction passed intoactivateChapiHandleris called, and the result is used to determine what to display to the user. In the above example, we have specified that we would like the/storeroute to be displayed.The user is shown your site's
/storeroute via an iframe.
As you might have been able to tell, the operative code here is the function passed into activateChapiHandler. This is what determines what will be shown to users when asking to store a credential.
[Optional] Host a storage endpoint for users to visit when storing a credential via CHAPI
After the above flow finishes, a user will land on your sites /store route. In order to actually display and store the sent credential, you will need to call wallet.receiveChapiEvent:
const event = await learnCard.invoke.receiveChapiEvent();
const vp = event.credential.data;
const vc = Array.isArray(vp.verifiableCredential)
? vp.verifiableCredential[0]
: vp.verifiableCredential;After displaying the credential to the user, you may prompt the user for a title and store it with the following code:
const uri = await learnCard.invoke.publishCredential(vc);
await learnCard.invoke.addCredential({ id, uri });Once the credential is stored, you may inform the calling code that you have successfully stored the credential with the following code:
event.respontWith(Promise.resolve({ dataType: 'VerifiablePresentation', data: vp }););If you would instead prefer to reject the credential, you may do so with the following code:
event.respondWith(Promise.resolve(null));[Optional] Host a get endpoint for users to visit when retrieving a credential via CHAPI
DIDAuth
One reason why you might want to have a get route setup is for DID-Auth. Including support for DID-Auth allows issuers to seemlessly request your user's did and verify that they actually control that did.
The code to do this looks very similar to the code used for hosting a storage endpoint, however you will want to display different information to the user!
To start, grab the event:
const event = await learnCard.invoke.receiveChapiEvent();Next, grab the request origin and display it to the user:
const origin = event.credentialRequestOrigin;A good prompt might be "{origin} would like to send you a credential".
If the user accepts, you will need to create a new VP that is signed using the challenge and domain in the DID-Auth request:
const presentation = event.credentialRequestOptions.web.VerifiablePresentation;
const { challenge, domain } = presentation;
const didAuthVp = {
'@context': [
'https://www.w3.org/2018/credentials/v1',
'https://w3id.org/security/suites/ed25519-2020/v1',
],
type: 'VerifiablePresentation',
holder: learnCard.id.did(),
};
const data = await learnCard.invoke.issuePresentation(didAuthVp, {
challenge,
domain,
proofPurpose: 'authentication',
});
event.respondWith(
Promise.resolve({
dataType: 'VerifiablePresentation',
data,
})
);If the user rejects, simply respond with null!
event.respondWith(Promise.resolve(null));Testing
The easiest way to test out your new CHAPI software is by visiting https://playground.chapi.io/issuer. Once there, you can easily generate and attempt to store different test credentials into your wallet software.
Troubleshooting
If you find yourself totally stuck, it can be really helpful to use the official CHAPI docs to help get you totally unstuck! Because we are simply wrapping the exposed CHAPI methods, it is very easy to translate the CHAPI docs to the relevant LearnCard functions!
↔️Translating to CHAPI documentationLast updated
Was this helpful?