How to create a Stellar wallet?
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.
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.
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; 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 ? ( ) : null; } }{this.prompter.message ?{this.prompter.message}
: null} {this.prompter.options ? () : ( this.update(e)} > )}
Make sure to import lodash-es before moving ahead:
npm i -D lodash-es
Facilitate secure transactions and cross-border payments with Stellar blockchain wallet
LeewayHertz Stellar Blockchain Wallet Development Services
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 ? ( [ , ] ) : ( ), this.error ? ( {JSON.stringify(this.error, null, 2)} ) : null, this.account ? ( ) : null, ] }{this.account.publicKey}
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.
Start a conversation by filling the form
All information will be kept confidential.
Insights
How to Issue and Anchor Assets on Stellar?
Stellar Distributed Network is used to hold, transfer and issue assets, including dollars, euros, stocks, gold and other tokens of value.
How to set up deposits and withdrawals on Stellar?
Supporting deposits and withdrawals of an asset on the Stellar blockchain requires interaction between anchor and wallet apps.
How to setup a Stellar Horizon server on AWS?
Learn how to set up Stellar Horizon server on AWS cloud. We have created a step-by-step guide to help you set up Stellar Horizon Server.