In this tutorial, the goal is to get to a place where a user can create, store, and access their Bantu account using an intuitive pincode encryption method.
User Flow
Because we've decided to build a non-custodial wallet, we don’t need to communicate with a server or database at all: everything can happen locally right on a user’s device. We’ll be doing all our work inside of src/components/wallet/, so head over there now. We’re going to use the StellarSdk.Keypair.random() from the StellarSdk to generate a new valid Bantu keypair. That's the easy part. The hard work will be storing that vital information in a secure yet accessible manner.
The user flow we're building will work like this: Click “Create Account” → UI modal popup asking for a pincode → Enter pincode, click “OK” → App encrypts a new Bantu keypair secret key with pincode → App saves encrypted secret to localStorage. On page reload, we’ll fetch the publicKey to “login” the user, but for any protected action such as “Copy Secret”, the modal will pop back up asking for the original pincode.
Create a Popup Modal
To start, let's look at at the popup modal. We’ll be mimicking the browser’s prompt functionality with our own new, more powerful component. First things first we should generate a new component:
npmrungenerate
Call it bantu-prompt, and deselect both test files leaving only the styling. Once you have that, open src/components/prompt/ and rename the .css file to .scss. Fill that style file with this:
One of the first things you’ll notice is the use of lodash-es. Let’s make sure we’ve got that imported before moving forward:
npmi-Dlodash-es
There’s a lot going on in this file, but since this isn’t a Stencil tutorial we’ll skip the details. What this allows us to do it to use a <bantu-prompt prompter={this.prompter} /> component elsewhere in our project. It's worth noting the variables available to us in the prompter property.
The values we’ll be making most use of are those first three: show, message and placeholder. The last two—resolve and reject—are for promisifying the prompt so we can await a response before continuing with further logic. Don't worry: that statement will make more sense in a moment once we include this component in src/components/wallet/. Speaking of, let’s swing over to that component now.
We’ve got a lot of work to do in here so I’ll just paste the code in all its glory and we’ll walk through it block by block:
Just one import from a library we should already have installed.
The other relative path imports are all the events and methods we’ll create here in a moment. For now, just generate all those files in their appropriate directories. Ensure your console is at the root of the bantu-wallet project before running this string of commands:
Next we have this funky line which may seem like an npm team import, but is actually a fancy typescript module alias path.
import { Prompter } from"@prompt/prompt";
This allows us to avoid long error prone ../../../ paths and just use @{alias}/{path?}/{module}. In order to get this past both the linter and compiler we’ll need to modify a couple files.
First, modify the tsconfig.json file to include these values in the compilerOptions object.
interface is just the TypeScript way of setting up a tidy typed class. BantuAccount will be our account class. It includes the publicKey for easy reference later in Horizon or Astrograph calls and the Top Secret keystore key containing the encrypted account secret cipher.
Pretty standard boring bits, setting up the @Component with its defining values and initializing with some @State and @Prop data. You can see we’re setting up an account state with our BantuAccount class as well as a prompter state with that Prompter class from the bantu-prompt we imported earlier. We’re initializing that prompter state with a show value of false so the prompt modal rendereth not initially.
Everything after this is the assignment of our imported events and methods from up above. Let’s begin with the ./events/componentWillLoad.ts
componentWillLoad is the Stencil way of pre-filling the state and prop values before actually rendering the component. In our case we’ll use this method to populate the account@State with the saved storage keyStore value if there is one. At first there won’t be, so we’ll come back to this once we’ve actually gone over how to create and save accounts. For now just know it’s here, and since you’re smart, I imagine you can already kind of see how it works.
“But wait!” you say, “What are the @services/error and @services/storage packages?” Fine, yes, we should go over those. Remember the module alias stuff from earlier? Well one was for @prompt and the other was for @services. Go ahead and create these two files and add them to the src/services directory.
? App name Bantu Wallet? App Package ID (in Java package format, no dashes) com.wallet.stellar? Which npm client would you like to use? npm✔InitializingCapacitorprojectin/Users/Desktop/Web/Clients/bantu/bantu-demo-walletin1.91ms🎉YourCapacitorprojectisreadytogo!🎉Addplatformsusing"npx cap add":npxcapaddandroidnpxcapaddiosnpxcapaddelectronFollowtheDeveloperWorkflowguidetogetbuilding:https://capacitor.ionicframework.com/docs/basics/workflow
We’re not really making full use of Capacitor , but it is an amazing service so be sure and check it out! For now we just need it to make storing and retrieving our data a bit more stable.
This storage service is simply a key setter and getter helper for storing and retrieving data. We’ll use this for any persistent data we want to store. For now, that's our Bantu account data.
Set Up Event Handling
That’s everything we need for the componentWillLoad event. On to the ./events/render.tsx file.
It looks messy, but it’s actually a pretty simple .tsx file rendering out our DOM based off a series of conditional values. You can see we’re including the bantu-prompt component, and setting the prompter prop to our this.prompter state. We then have a ternary operation toggling between a Create Account button and a basic account UI. If this.account has a truthy value, we’ll print out the account’s publicKey along with some interaction buttons. If this.account is falsey, we’ll print out a singular Create Account button connected to, you guessed it, the createAccount method. After that logic, we print out an error if there is one, and finally a Sign Out button if there’s an account to sign out of. Those are the two Wallet@Component events.
Create Methods
Let’s look at the methods now beginning with the ./methods/createAccount.ts file.
Aha! Finally something interesting. This method forms the meat of our component. Before we dive into it, though let’s install the missing @tinyanvil/sjcl package.
npmi-D@tinyanvil/sjcl
Create an Account
Essentially all we’re doing is making a request to create an account, which triggers the Prompt modal to ask for a pincode. That pincode will be used in the sjcl.encrypt method to encrypt the secret key from the Keypair.random() method. We set the this.account with the publicKey, which encrypted the keystore cipher, and now we're storing that cipher in base64 format in localStorage via our set('keyStore') method for easy retrieval when the browser reloads. We could also encode that cipher into a QR code or a link to share with other devices. Since it requires the pincode that encrypted cipher, it's as secure as the pincode you encrypt it with.
Copy Address
Now that we’ve created an account, there are three more actions we'll enable: copyAddress, copySecret, and signOut.
Well there you go, the easiest code you’ll see all day. Just copy the publicKey from the this.account object to the clipboard. Before we jump though don’t forget to install that copy-to-clipboard package.
You may not actually include this in your production wallet, but for now it's a simple demonstration of how to programmatically gain access to the secret key at a later date for making payments, creating trustlines, etc. It’s essentially the createAccount in reverse: it asks for the pincode to decrypt the keystore which, once decrypted, we copy into the clipboard.
Sign Out
Finally ./methods/signOut.ts
import { remove } from"@services/storage";import { handleError } from"@services/error";exportdefaultasyncfunctionsignOut(e:Event) {try {e.preventDefault();constconfirmNuke=awaitthis.setPrompt("Are you sure? This will nuke your account","Enter NUKE to confirm", );if (!confirm ||!/nuke/gi.test(confirmNuke)) return;this.error =null;awaitremove("keyStore");location.reload(); } catch (err) {this.error =handleError(err); }}
It’s important to allow users to nuke their account, but we need to be careful to confirm that action with our faithful setPrompt. Once they opt to “NUKE” the account we can remove the keyStore and reload the app.
Set Prompt
Speaking of setPrompt the last method in our wallet.ts file is ./methods/setPrompt.ts.
In setPrompt, we see how the prompt state is set, and how the Promise is set up to allow us to wait on the prompt whenever we call this method. It’s actually pretty slick, and it might be worth looking back at the src/components/prompt/prompt.tsx to see how the resolve and reject functions get called. It’s not central to our wallet creation, but it’s a pretty handy little component that will serve us well in the future as we continue to request input from the user.
That’s it folks! Restart the server with npm start and you’ve got a perfectly legitimate, minimal Bantu wallet key creation and storage Web Component! It's a solid foundation for a non-custodial wallet that relies on a simple pincode.