How to create a Stellar wallet?

blockchain in vaccine distribution

Ever since the introduction of Bitcoin in 2009, Blockchain has always been in highlights and has attracted everyone worldwide. Today, Bitcoin is valued at over $31,000. Apart from bitcoin, various other cryptocurrencies have been developed that have also gained huge profits. Released in 2014, Stellar is a decentralized payment network that gained popularity due to its unique approach and appeals to cross border payments. It is a blockchain-based open-source database dedicated to making transactions faster, safer and easier. It takes international payments to a whole new level by providing secure, real-time and low-cost transfers.

The cryptocurrency issued by Stellar is known as Stellar Lumen or XLM.

Stellar’s Coinmarketcap rank is #11 with a market cap of around $6 billion. The Stellar coin is presently valued at $0.278 with a 24-hour trading volume of $877.01 million. Founder of Stellar, Jed McCaleb, developed Stellar to provide people a way to move their fiat currency into crypto and remove the friction involved in transferring money worldwide. The not-for-profit organization, Stellar Development Foundation, aims to “unlock the world’s economic potential by making money more fluid, markets more open and people more empowered.”

Stellar is unique since the fee for every transaction is just 0.00001 XLM. Such minimal transaction cost attracts more users and ensures that users keep most of their money.

This article aims to deliver a clear explanation of Stellar wallets, discuss the detailed steps to create a basic wallet and integrate Stellar wallets into an existing application.

  1. What’s a Stellar wallet and what does it hold?
  2. How to manage keys?
  3. How to create a Stellar wallet?

What’s a Stellar wallet and what does it hold?

A stellar wallet is an application component built to handle basic functionalities like account creation, key storage, queries and transaction signing to the Stellar database.

Unlike real-world wallets, Stellar wallets do not hold digital cash, at least not directly. Stellar wallets are used to sign and submit the transactions and view the Stellar ledger’s past and current state. The ledger stores data, including offers to buy and sell, accounts and balances shared by all nodes that make up a network. Wallet stores caches or references to the Stellar database, but the actual data is stored on the blockchain.

The key takeaway here is that the Stellar wallet, rather than holding or storing something, is an interface or interactive layer on top of Stellar. Therefore, Stellar wallet’s discussion revolves around accessing and surfacing data in the network rather than storing something on our end.

Wallets operate on the client-side deal with user’s secret keys, giving direct access to the user’s accounts. Therefore, for the wallet’s security, it is necessary to flow all the web traffic over strong TLS methods. Moreover, key management is an inevitable part of security. Before moving on to the creation of wallets, first, we discuss some basics of key management in a Stellar wallet.

How to manage keys?

The first step for any app is to sort out user onboarding. Since secret keys control the access of the user’s account, deciding how to handle keys and how to append the Stellar account to a user object becomes the priority.

An important question might occur to your mind here: who will “own” the account? The answer involves three possibilities:

  • The service provider is the owner, stores the secret keys and represents the usage rights to the user. It is a custodial service.
  • The user is the owner, will have self-custody of their account credentials and delegate transaction signing. It is a non-custodial service.
  • A blend of both via multi-sig. This method helps maintain non-custodial status while still allowing for account recovery.

While going with the first or third approach, excessive carefulness is required to store and take control of the user’s secret keys. It is easy to get wrong and reach a devastating situation. Although, developers can choose any of the options depending on their requirements. Here, our focus is to present to you how to build a non-custodial service. Our goal is to take you to a place where user can create, store and access Stellar account using intuitive encryption method.

How to create a Stellar wallet?

We’ll use a toolchain called StencilJS. It provides the best of modern frontend frameworks and pares everything back to small, fast and completely standard-based web components that work on every browser. It provides an easy way to create web applications and allows you to see all the ins and outs of creating a Stellar wallet from start to end.

Project Setup

To set up the project, open the terminal and initialize a new project.

npm init stencil

A prompt will appear to choose the type of project. Choose components as we are dealing with modular components, not the entire application. Now run:

$ npm run generate

This step will initialize a component generation script. Enter stellar-wallet.

% npm run generate
> stellar-wallet generate
> stencil generate
$ stencil generate stellar-wallet

The following files have been generated

– src/components/wallet/wallet.tsx
– src/components/wallet/wallet.css

Now, for styling, we’re using SCSS rather than CSS

npm i -D @stencil/postcss @stencil/sass autoprefixer @types/autoprefixer rollup-plugin-node-polyfills

After the style packages have installed, go to stencil.config.ts and modify it to this:

import { Config } from “@stencil/core”;
import { sass } from “@stencil/sass”;
import { postcss } from “@stencil/postcss”;
import autoprefixer from “autoprefixer”;
import nodePolyfills from “rollup-plugin-node-polyfills”;
export const config: Config = {
namespace: “stellar-wallet”,
outputTargets: [
{
type: “dist”,
esmLoaderPath: “../loader”,
},
{
type: “docs-readme”,
},
{
type: “www”,
serviceWorker: null, // disable service workers
},
],
globalStyle: “src/global/style.scss”,
commonjs: {
namedExports: {
“stellar-sdk”: [
“StrKey”,
“xdr”,
“Transaction”,
“Keypair”,
“Networks”,
“Account”, “TransactionBuilder”,
“BASE_FEE”,
“Operation”,
“Asset”,
“Memo”,
“MemoHash”,
],
“@stellar/wallet-sdk”: [“KeyManager”, “KeyManagerPlugins”, “KeyType”],
},
},
plugins: [
nodePolyfills(),
sass(),
postcss({
plugins: [autoprefixer()],
}),
],
nodeResolve: {
browser: true,
preferBuiltins: true,
},
};

Save all the style files and update wallet.tsx.

Non-custodial wallets do not need to communicate with servers or databases, and every action is performed locally on the user’s device. The basic user flow is expected to work as: “Create account UI modal popup asking for pincode enter pincode app encrypts a new secret Stellar keypair with pincode save encrypted key to local storage. For every page reload, ‘public key’ is fetched to allow the user to login into the account. For any protected action like “CopySecret,” the modal will popup again and ask for the original pincode.

Create Popup Modal

For the popup modal, the browser’s prompt functionality will be implemented with our new component. First, generate a new component:

npm run generate

Name it as stellar-prompt. Open src/components/prompt/ and change the .css file to .scss. In that style file write this:

@import “../../global/style.scss”;
:host {
display: block;
font-family: $font-family;
font-size: 15px;
.prompt-wrapper {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
display:
flex;
align-items: center;
justify-content: center;
align-content: center;
min-height: 100vh;
min-width: 100vw;
background-color: rgba(black, 0.2);
z-index: 1;
}
.prompt {
background-color: white;
padding: 20px;
max-width: 350px;
width: 100%;
position: relative;
p {
margin-bottom: 10px;
}
input {
width: 100%;
margin: 0;
padding: 5px;
outline: none;
border: 1px solid black;
text-transform: uppercase;
&:focus {
border-color: blue;
}
}
}
.select-wrapper {
position: relative
display: inline-flex;
select {
border-color: blue;
padding: 0 10px;
min-width: 100px;
}
&:after,
&:before {
font-size: 12px;
position: absolute;
right: 10px;
color: blue;
}
&:after {
content: “<<“; top: calc(50% – 5px); transform: translate(0, -50%) rotate(90deg); } &:before { content: “>>”;
top: calc(50% + 5px);
transform: translate(0, -50%) rotate(90deg);
}
}
.actions {
display: flex;
justify-content: flex-end;
margin-top: 10px;
button {
margin: 0;
min-width: 50px;
}
.cancel {
background: none;
border: 1px solid blue;
color: blue;
}
.submit {
margin-left: 10px;
}
}
}

Now replace the content of prompt.tsx with this:

import { Component, Prop, Element, Watch, h, State } from “@stencil/core”;
import { defer as loDefer } from “lodash-es”;
export interface Prompter {
show: boolean;
message?: string;
placeholder?: string;
options?: Array<any>;
resolve?: Function;
reject?: Function;
}
@Component({
tag: “stellar-prompt”,
styleUrl: “prompt.scss”,
shadow: true,
})
export class Prompt {
@Element() private element: HTMLElement;
@Prop({ mutable: true }) prompter: Prompter;
@State() private input: string;
@Watch(“prompter”)
watchHandler(newValue: Prompter, oldValue: Prompter) {
if (newValue.show === oldValue.show) return;
if (newValue.show) {
this.input = null;
if (newValue.options)
this.input =
this.input ||
`${newValue.options[0].code}:${newValue.options[0].issuer}`;
else
loDefer(() => this.element.shadowRoot.querySelector(“input”).focus());
} else {
this.prompter.message = null;
this.prompter.placeholder = null;
this.prompter.options = null;
}
}
componentDidLoad() {
addEventListener(“keyup”, (e: KeyboardEvent) => {
if (this.prompter.show)
e.keyCode === 13
? this.submit(e)
: e.keyCode === 27
? this.cancel(e)
: null;
});
}
cancel(e: Event) {
e.preventDefault();
this.prompter = {
…this.prompter,
show: false,
};
this.prompter.reject(null);
}
submit(e: Event) {
e.preventDefault();
this.prompter = {
…this.prompter,
show: false,
};
this.prompter.resolve(this.input);
}
update(e) {
this.input = e.target.value.toUpperCase();
}
render() {
return this.prompter.show ? (
<div class=”prompt-wrapper”>
<div class=”prompt”>
{this.prompter.message ? <p>{this.prompter.message}</p> : null}
{this.prompter.options ? (
<div class=”select-wrapper”>
<select onInput={(e) => this.update(e)}>
{” “}
{this.prompter.options.map((option) => (
<option
value={`${option.code}:${option.issuer}`}
selected={this.input === `${option.code}:${option.issuer}`}
>
{option.code}
</option>
))}
</select>
</div>
) : (
<input
type=”text”
placeholder={this.prompter.placeholder}
value={this.input}
onInput={(e) => this.update(e)}
></input>
)}
<div class=”actions”>
<button
class=”cancel”
type=”button”
onClick={(e) => this.cancel(e)}
>
Cancel
</button>
<button
class=”submit”
type=”button”
onClick={(e) => this.submit(e)}
>
OK
</button>
</div>
</div>
</div>
) : null;
}
}

 

Make sure to import lodash-es before moving ahead:

npm i -D lodash-es

Create Stellar Account Class

interface StellarAccount {
publicKey: string;
keystore: string;
}

StellarAccount is a class that includes the public key. Set up account state with StellarAccount class and prompter state with Prompter class.

@Component({
tag: ‘stellar-wallet’,
styleUrl: ‘wallet.scss’,
shadow: true
})
export class Wallet {
@State() account: StellarAccount
@State() prompter: Prompter = {show: false}
@State() error: any = null
…}

After this step, the assignment of imported events and methods needs to be done.

import { handleError } from “@services/error”;
import { get } from “@services/storage”;
export default async function componentWillLoad() {
try {
let keystore = await get(“keyStore”);
this.error = null;
if (keystore) {
keystore = atob(keystore);
const { publicKey } = JSON.parse(atob(JSON.parse(keystore).adata));
this.account = {
publicKey,
keystore,
};
}
} catch (err) {
this.error = handleError(err);
}
}

componentWillLoad prefills the state and props values before actually rendering the component. Now, create the following two files and add them to the src/services directory.

mkdir -p src/services
touch src/services/{error,storage}.ts

error.ts will hold the following:

import { get as loGet } from “lodash-es”;
export function handleError(err: any) {
return loGet(err, “response.data”, loGet(err, “message”, err));
}

It is a simple error handler used while processing API requests.

Set up key storage

Modify storage.ts as:

import { Plugins } from “@capacitor/core”;
const { Storage } = Plugins;
export async function set(key: string, value: any): Promise {
await Storage.set({
key,
value,
});
}
export async function get(key: string): Promise {
const item = await Storage.get({ key });
return item.value;
}
export async function remove(key: string): Promise {
await Storage.remove({ key });
}

Install and set up a new package @capacitor/core.

# Install dependencies
npm i -D @capacitor/core @capacitor/cli
# Initialize Capacitor
npx cap init
? App name Stellar Wallet
? App Package ID (in Java package format, no dashes) com.wallet.stellar
? Which npm client would you like to use? npm

Initializing Capacitor project in
/Users/tylervanderhoeven/Desktop/Web/Clients/Stellar/stellar-demowallet in 1.91ms

Your Capacitor project is ready to launch.

Add platforms using “npx cap add”:
npx cap add android
npx cap add ios
npx cap add electron

Set up event handling

On ./events/render.tsx file:

import { h } from “@stencil/core”;
export default function render() {
return [
,
this.account ? (
[
<div class=”account-key”>

{this.account.publicKey}

<button class=”small” type=”button”> this.copyAddress(e)}
&gt;
Copy Address
</button>
<button class=”small” type=”button”> this.copySecret(e)}
&gt;
Copy Secret
</button>

</div>
,
]
) : (
<button type=”button”> this.createAccount(e)}&gt;
Create Account
</button>
),
this.error ? (
<pre class=”error”>{JSON.stringify(this.error, null, 2)}</pre>
) : null,
this.account ? (
<button type=”button”> this.signOut(e)}&gt;
Sign Out
</button>
) : null,
]
}

It is a simple .tsx file rendering out DOM based on a series of conditional values. A ternary operation toggles between Create account button and basic account UI. If the value for this.account is true, print the account’s public key along with some interaction buttons; if the value is false, print the singular Create Account button connected to the createAccount method. When an error encounters, print an error message and finally a Sign out button if there is an account to sign out.

Create Methods

In ./methods/createAccount.ts file:
import sjcl from “@tinyanvil/sjcl”;
import { Keypair } from “stellar-sdk”;
import { handleError } from “@services/error”;
import { set } from “@services/storage”;
export default async function createAccount(e: Event) {
try {
e.preventDefault();
const pincode_1 = await this.setPrompt(“Enter a keystore pincode”);
const pincode_2 = await this.setPrompt(“Enter keystore pincode again”);
if (!pincode_1 || !pincode_2 || pincode_1 !== pincode_2)
throw “Invalid pincode”;
this.error = null;
const keypair = Keypair.random();
this.account = {
publicKey: keypair.publicKey(),
keystore: sjcl.encrypt(pincode_1, keypair.secret(), {
adata: JSON.stringify({
publicKey: keypair.publicKey(),
}),
}),
};
await set(“keyStore”, btoa(this.account.keystore));
} catch (err) {
this.error = handleError(err);
}
}

Create an account

Everything we’ve done till now was requesting to create an account that triggers prompt modal to ask for a pincode. The method sjcl.encrypt uses this pincode to encrypt the secret key from keypair. random() function. Set the this.account with the public key, which encrypted keystore cipher and store that cipher in base64 format in local storage via set(‘keyStore’). Moreover, the cipher can also be encoded into a QR code or a link shareable with other devices.

Copy Address

Once the account is created, three more actions need to be enabled: copyAddress, copySecret and signOut.

In ./methods/copyAddress.ts :

import copy from “copy-to-clipboard”;
export default async function copyAddress(e: Event) {
e.preventDefault();
copy(this.account.publicKey);
}
npm i -D copy-to-clipboard

Copy Secret

In ./methods/copySecret.ts

import sjcl from “@tinyanvil/sjcl”;
import copy from “copy-to-clipboard”;
import { handleError } from “@services/error”;
export default async function copySecret(e: Event) {
try {
e.preventDefault();
const pincode = await this.setPrompt(“Enter your keystore pincode”);
if (!pincode) return;
this.error = null;
const secret = sjcl.decrypt(pincode, this.account.keystore);
copy(secret);
} catch (err) {
this.error = handleError(err);
}
}

Sign Out

And finally, ./methods/signOut.ts

import { remove } from “@services/storage”;
import { handleError } from “@services/error”;
export default async function signOut(e: Event) {
try {
e.preventDefault();
const confirmNuke = await this.setPrompt(
“Are you sure? This will nuke your account”,
“Enter NUKE to confirm”,
);
if (!confirm || !/nuke/gi.test(confirmNuke)) return;
this.error = null;
await remove(“keyStore”);
location.reload();
} catch (err) {
this.error = handleError(err);
}
}

Set Prompt

The last method in wallet.ts file is ./methods/setPrompt.ts.

export default function setPrompt(
message: string,
placeholder?: string,
options?: Array,
): Promise {
this.prompter = {
…this.prompter,
show: true,
message,
placeholder,
options,
};
return new Promise((resolve, reject) => {
this.prompter.resolve = resolve
this.prompter.reject = reject;
});
}

We’re done with our job! Restart the server with npm start and a legitimate, minimal Stellar wallet web component is ready. It’s a solid foundation for Pincode reliant simple non-custodial wallet.

 

Conclusion

Stellar is increasingly gaining popularity worldwide because of its built-in order books, unique consensus protocol and connection to existing financial infrastructure. It offers benefits of cheap fees, fast transaction speed, international reach, universal asset exchange and easy fiat on and off-ramps within the network. Offering such beneficial facilities, Stellar aims to become the standard method of money transfer around the world. The team is working to their full potential to make it possible.

If you’re searching for help to create a Stellar wallet or integrate Stellar wallet into your existing system, we’re ready to help you. Connect with our team of Stellar Blockchain Experts and get your idea converted into reality.

Author’s Bio

Akash Takyar
Akash Takyar
CEO LeewayHertz
Akash Takyar is the founder and CEO at LeewayHertz. With the experience of building over 100+ platforms for startups and enterprise allows Akash to rapidly architect and design solutions that are scalable and beautiful.
Akash's ability to build enterprise-grade technology solutions has attracted over 30 Fortune 500 companies, including Siemens, 3M, P&G and Hershey’s. Akash is an early adopter of new technology, a passionate technology enthusiast, and an investor in AI and IoT startups.
Start a conversation by filling the form
Once you let us know your requirement, our technical expert will schedule a call and discuss your idea in detail post sign of an NDA.

All information will be kept confidential.

 Send me the signed Non-Disclosure Agreement (NDA)

Insights