We handle hundreds of tokens and coins.

Lucky Framework Tutorial - Let's build a Bitcoin wallet.

Build stunning web applications in less time. A Crystal web framework that catches bugs for you, runs incredibly fast, and helps you write code that lasts.

Installing Crystal and the Lucky Framework.

First you have to add the repository to your APT configuration. For easy setup just run in your command line:

curl -sSL https://dist.crystal-lang.org/apt/setup.sh | sudo bash

Once the repository is configured you're ready to install Crystal:

sudo apt install crystal

Call crystal -v to check everything installed correctly.

$ crystal -v
Crystal 0.25.1 [b782738ff] (2018-06-27)

LLVM: 4.0.0
Default target: x86_64-unknown-linux-gnu

Install Lucky

git clone https://github.com/luckyframework/lucky_cli
cd lucky_cli
git checkout v0.11.0
shards install
crystal build src/lucky.cr --release --no-debug
sudo cp lucky /usr/local/bin

Call lucky -v and you should get the version number you specified in the git checkout above.

Create a project

First, you'll need to make sure you postgres installed and running. For most Linux install the following is enough.

$ sudo service postgresql start
 * Starting PostgreSQL 9.3 database server

Now go ahead and create a lucky project.

$ lucky init bitcoin_wallet
Generating crystal project for bitcoin_wallet
Adding Lucky dependencies to shards.yml
Done generating your Lucky project

  ▸ cd into bitcoin_wallet
  ▸ run bin/setup
  ▸ run lucky dev to start the server

Lucky tries to be as friendly as possible so as you can see if gives you the next steps. Let's follow those.

$ cd bitcoin_wallet
$ bin/setup
Yarn is not installed.
See https://yarnpkg.com/lang/en/docs/install/ for install instructions.

Lucky does come with a list of requirements before installation. I've kind of skipped that so far, so if you don't have Yarn imnstalled do the following.

$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
OK
$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
deb https://dl.yarnpkg.com/debian/ stable main

Then you can simply:

sudo apt-get update && sudo apt-get install yarn

Running bin/setup one more time should give you a long list of package install then eventuall a command prompt hopefully with no errors.

$ bin/setup

It's also worth setting a password in postgres for your user id and adding that to the database config.

$ psql
psql (9.3.18)
Type "help" for help.

ubuntu=# password
Enter new password: 
Enter it again: 
ubuntu=# q

Your config/database.cr should now have the following section

LuckyRecord::Repo.configure do
  if Lucky::Env.production?
    settings.url = ENV.fetch("DATABASE_URL")
  else
    settings.url = ENV["DATABASE_URL"]? ||
      LuckyRecord::PostgresURL.build(
        hostname: "localhost",
        database: database,
        username: "ubuntu",
        password: "password")
  end
  # In development and test, raise an error
  # if you forget to preload associations
  settings.lazy_load_enabled = Lucky::Env.production?
end

Integrated web and build server catches compile errors

Run the Lucky server in development and it will look for changes to any of your source files. Changes trigger asset compilation for JavaScript and Sass. For changes to Crystal source files the compiler will run and you will see any errors in the console.

$ lucky dev

The lucky combined web and build server.

Database Migrations

Database migration allow us to easily add and modify tables in our application. We first generate a migration file and then add our columns to the crystal source code.

$ lucky gen.migration CreateKeyPair
Created CreateKeyPair::V20180810125258 in ./db/migrations/20180810125258_create_key_pair.cr

Edit the newly generated migration file and add the following code.

class CreateKeyPair::V20180810125258 < LuckyMigrator::Migration::V1
  def migrate
    create :key_pairs do
      add user_id : Int32
      add public_key : String
      add private_key : String
    end
  end

  def rollback
    drop :key_pairs
  end
end

The we can migrate the database to add our new columns.

$ lucky db.migrate
Migrated CreateKeyPair::V20180810125258

Type Safe Pages and Layouts (And how to style with SCSS)

Out of the box lucky already comes with authentification code and a user model. It also generates a bunch of pages to get you started.

Lucky doesn't use templating to generate pages. Lucky pages are pure crystal code. That means they are checked by the compiler and the rendering code is crystal. In my opinion this is a huge time saver and benefit over other frameworks. Here are the pages lucky generates for you.

$ tree src/pages/
src/pages/
├── errors
│   └── show_page.cr
├── guest_layout.cr
├── main_layout.cr
├── me
│   └── show_page.cr
├── password_reset_requests
│   └── new_page.cr
├── password_resets
│   └── new_page.cr
├── sign_ins
│   └── new_page.cr
└── sign_ups
    └── new_page.cr

Example Code for Sign Up form rendering

Below is a code sample for one of the generated pages. HTML elements such as H1 are method calls which take parameters or blocks of more elements.

Parts of the page can be split into functions to be re-used or to make the code easier to maintain.

class SignIns::NewPage < GuestLayout
  needs form : SignInForm

  def content
    h1 "Sign In"
    render_sign_in_form(@form)
  end

  private def render_sign_in_form(f)
    form_for SignIns::Create do
      sign_in_fields(f)
      submit "Sign In", flow_id: "sign-in-button"
    end
    link "Reset password", to: PasswordResetRequests::New
    text " | "
    link "Sign up", to: SignUps::New
  end

  private def sign_in_fields(f)
    field(f.email) { |i| email_input i, autofocus: "true" }
    field(f.password) { |i| password_input i }
  end
end

Things to note..

  • Links are using type safe destinations. i.e. PasswordResetRequests::New
  • To use a layout we just inherit from it. < GuestLayout
  • No need to learn a templating language we are using pure crystal code.

The default unstyled sign up process

Here's how the signup process currently looks.

Out of the box lucky sign up and sign in.

The authentication pages are unstyled so this is a great opportunity to dive in and look at how lucky handles layouts, pages and CSS in the asset pipeline.

Altering the provide GuestLayout

It would be tempting at this point to bring in a CSS framework like bootstrap and alter the generated pages by adding classes and so on.

However, as we're learning a new framwork now is also a good time to revisit CSS and see how much styling we can do without adding too much markup into our crystal pages. All of the authentication pages inherit from GuestLayout so let's add a little bit of markup there.

abstract class GuestLayout
  # Edit shared layout code in src/components/shared/layout.cr
  include Shared::Layout

  def render
    html_doctype

    html lang: "en" do
      shared_layout_head

      # Add a class to the body element.
      body class: "authentication" do
        # surround the content with a main element.
        main do
          render_flash
          content
        end
      end
    end
  end
end

Using the built in CSS processing.

Lucky uses SCSS as a CSS extension framework. This gives us two main benefits. Firstly we can structure our css into seperate files and lucky already provides the folders for this. Secondly, we can leverage some CSS extension provided by SCSS tsuch as nested CSS and mixins.

$ tree src/css/
src/css//
├── app.scss/
├── components/
├── mixins/
└── variables/

So let's create 2 new files. Firtly color.scss in the variables folder. Here we can define colours and use them throughout our style sheets.

$color-white: white;
$color-grey: #fafafa;
$color-dark-grey: #333;

Secondly, we'll create components/authentication.scss which is the main styling to apply to our authentication pages.

We're making use here of CSS Flexbox which in my opinion is well worth learning if you don't know it already.

.authentication {

    background-color: $color-grey;

    h1 {
        text-align: center;
        text-transform: uppercase;
        color: $color-dark-grey;
    }

    form {
      display: flex;
      flex-direction: column;
    }

    form > * {
      margin-bottom: 1em;
    }

    main {
      background-color: $color-white;
      width: 500px;
      margin: 3em auto 0 auto;
      padding: 2em;
      border-radius: 8px;
      flex-wrap: wrap;
      flex-direction: column;
      justify-content: center;
      box-shadow: 0 10px 40px -14px rgba(0, 0, 0, 0.25);
    }

    input {
        font-size: 16px;
        padding: 15px;
        border-radius: 2px;
        border-width: 1px;
        border-style: solid;
        border-color: rgba(40, 47, 55, 0.1);
    }
    input[type="submit"] {
        font-size: 12px;
        font-weight: 600;
        text-align: center;
        text-transform: uppercase;
        letter-spacing: 1px;
        cursor: pointer;
        background-color: rgb(14, 125, 255);
        color: rgb(255, 255, 255);
        padding: 14px 20px;
        border-width: 0px;
        border-radius: 1px;
    }
}
@media (max-width: 600px) {
  .authentication main {
    width: 95%;
    margin-top: 1em;
  }
}

Finally we import our new files into app.css

// Lucky generates 3 folders to help you organize your CSS:
@import "~normalize-scss/sass/normalize/import-now";
@import "variables/colors";
@import "components/authentication";

body {
  font-family: Sans-Serif;
}

After changing the layout adding our CSS and updating our me page you should be able to register and be presented with something that looks like the screenshot below.

The final result

The newly styled sign up and sign in forms.

Generating Actions and Pages

Rather than having potentially bloated controllers handling many incoming requests Lucky handles each request in a seperate class. Let's create an action.

class Bitcoins::Index < BrowserAction
  get "/bitcoins" do
    addresses = Array(Tuple(String, String, String)).new
    render IndexPage, addresses: addresses
  end
end

The get "bitcoins" method tells lucky to create a get route that goes to this action. We'll need to add a page that correspons to the IndexPage we've asked the action to render.

class Bitcoins::IndexPage < MainLayout
  needs addresses : Array(Tuple(String, String, String))

  def content

    h1 "Your Bitcoin Addresses"

    table class: "table" do

      thead do
        tr do
          th "Label"
          th "Public Address"  
          th "Balance"  
        end
      end

      tbody do
        @addresses.each do |label, public_key, balance|
          tr do
            td label
            td public_key
            td balance
          end
        end
      end 

    end
  end

end

Our new page extends MainLayout which is provided by lucky. This layout requires the user to be logged in.

Redirecting login to our new page

So now we need to direct users to our bitcoins page when they log in. Edit your src/actions/sign_ins/create.cr and chnage the redirect so it looks like the one below.

Authentic.redirect_to_originally_requested_path(self, fallback: Bitcoins::Index)

It's now time to log into your new Lucky application. You'll see something like the following.

The newly styled sign up and sign in forms.

You can run lucky routes to see what routes are created for you and the ones you added.

$ lucky routes
╔════════╦════════════════════════════════╦═══════════════════════════════╗
║ Verb   | URI                            | Action                        ║
╠────────┼────────────────────────────────┼───────────────────────────────╣
║ GET    | /bitcoins                      | Bitcoins::Index               ║
╠────────┼────────────────────────────────┼───────────────────────────────╣
║ GET    | /                              | Home::Index                   ║
╠────────┼────────────────────────────────┼───────────────────────────────╣
║ GET    | /me                            | Me::Show                      ║
╠────────┼────────────────────────────────┼───────────────────────────────╣
║ POST   | /password_reset_requests       | PasswordResetRequests::Create ║
╠────────┼────────────────────────────────┼───────────────────────────────╣
║ GET    | /password_reset_requests/new   | PasswordResetRequests::New    ║
╠────────┼────────────────────────────────┼───────────────────────────────╣
║ POST   | /password_resets/:user_id      | PasswordResets::Create        ║
╠────────┼────────────────────────────────┼───────────────────────────────╣
║ GET    | /password_resets/:user_id/edit | PasswordResets::Edit          ║
╠────────┼────────────────────────────────┼───────────────────────────────╣
║ GET    | /password_resets/:user_id      | PasswordResets::New           ║
╠────────┼────────────────────────────────┼───────────────────────────────╣
║ POST   | /sign_ins                      | SignIns::Create               ║
╠────────┼────────────────────────────────┼───────────────────────────────╣
║ DELETE | /sign_out                      | SignIns::Delete               ║
╠────────┼────────────────────────────────┼───────────────────────────────╣
║ GET    | /sign_in                       | SignIns::New                  ║
╠────────┼────────────────────────────────┼───────────────────────────────╣
║ POST   | /sign_ups                      | SignUps::Create               ║
╠────────┼────────────────────────────────┼───────────────────────────────╣
║ GET    | /sign_up                       | SignUps::New                  ║
╚════════╩════════════════════════════════╩═══════════════════════════════╝

Installing Javascript dependencies with Yarn

We'll create our Bitcoin private keys in the browser and encrypt them before they are sent to the server and stored in the database. This adds a level of security to our wallet.

To do this we'll use BitcoinJS to handle browser side key creation. We install BitcoinJS with Yarn a tool for managing javascript package dependencies. This is the recommended way of using javascript libraries in your application.

$ yarn add bitcoinjs-lib --ignore-engines

And you should get the following result.

yarn add v1.9.4
[1/4] Resolving packages...
[2/4] Fetching packages...
info [email protected]: The platform "linux" is incompatible with this module.
info "[email protected]" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 14 new dependencies.
info Direct dependencies
└─ [email protected]
info All dependencies
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
└─ [email protected]
Done in 21.36s.

To test this add the following code to your src/js/app.js. Then refresh the page and you should see a Bitocin Testnet address generated in your console.

import Bitcoin from "bitcoinjs-lib"
const testnet = Bitcoin.networks.testnet
const keyPair = Bitcoin.ECPair.makeRandom({ network: testnet })
console.log(Bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey, network: testnet }).address)

Lucky data persistance and validation

We already migrated our database and created a keypair table. So let's connect to this table and start to create key pair records.

In lucky we need to create a model.

require "./user"
class KeyPair < BaseModel
  table :key_pairs do
    column label : String
    column public_key : String
    column private_key : String
    belongs_to user : User
  end
end

When you define a model, Lucky creates a corresponding form class. This is a way to encapsulate validation logic. We need to create our form and tell lucky which fields we allow to be defined.

class KeyPairForm < KeyPair::BaseForm
  fillable label
  fillable public_key
  fillable private_key
end

Updating the page and actions for our new form

We'll add a method to generate a HTML form and call it form our page.

class Bitcoins::IndexPage < MainLayout
  needs addresses : Array(Tuple(String, String, String))
  needs form : KeyPairForm

  def content

    render_key_pair_form(@form)  

    h1 "Your Bitcoin Addresses"

    table class: "table" do

      thead do
        tr do
          th "Label"
          th "Public Address"  
          th "Balance"  
        end
      end

      tbody do
        @addresses.each do |label, public_key, balance|
          tr do
            td label
            td public_key
            td balance
          end
        end
      end 

    end
  end

  private def render_key_pair_form(f)
    form_for Bitcoins::Create do
      label_for f.label
      text_input f.label
      hidden_input f.public_key
      hidden_input f.private_key
      submit "Create Bitcoin Address", id: "create-address"
    end
  end

end

We need to create a new action called Create that will store the KeyPair into the database for us.

class Bitcoins::Create < BrowserAction
  route do
    KeyPairForm.create(params, user_id: current_user.id) do |form, key_pair|

      if key_pair
        flash.info = "Bitcoin address successfully generated"
        redirect to: Bitcoins::Index
      else
        flash.failure = "Unable to generate Bitcoin Address, Please try again."
        addresses = KeyPairQuery.new.user_id(current_user.id).map{ |keypair|
          { keypair.label, keypair.public_key, "" }
        }
        render IndexPage, form: form, addresses: addresses
      end

    end
  end
end

And we need to update our IndexAction as the compiler will complain unless we start passing a form into our page.

class Bitcoins::Index < BrowserAction
  get "/bitcoins" do
    addresses = Array(Tuple(String, String, String)).new
    render IndexPage, addresses: addresses, form: KeyPairForm.new
  end
end

Creating keys in the browser

We want to add a handler to the button that creates the public and private keys and populates the hidden fields of the form. We can do that quite simply by adding the following code into your src/js/app.js

function $(id) { return document.getElementById(id) }

import Bitcoin from "bitcoinjs-lib"

document.addEventListener("DOMContentLoaded", ()=>{

    const btn = $('create-address');
    if(! btn)
        return

    btn.onclick = ()=>{
        const testnet = Bitcoin.networks.testnet
        const keyPair = Bitcoin.ECPair.makeRandom({ network: testnet })

        $('key_pair_public_key').value = 
            Bitcoin.payments.p2pkh(
                { pubkey: keyPair.publicKey, network: testnet }).address

        $('key_pair_private_key').value = 
            keyPair.toWIF()

        return true
    }
});

If you're used to using JS libraries like JQuery, now's a good time to rethink that. Here we can write ES6 style JavaScript and Lucky will transpile this into something most browsers understand via Babel.

Additional styling

The design is looking a bit sad at the moment so let's add some SCSS to make it look a bit nicer. You can add the SCSS from the codepen below into your CSS folder.

Querying the database and showing results

When you define a model, Lucky creates a query class that you can use to access the database. In our case it's called called KeyPair::BaseQuery. To use it, let's create a query object that inherits from the one generated by Lucky.

class Bitcoins::Index < BrowserAction
  get "/bitcoins" do
    addresses = KeyPairQuery.new.user_id(current_user.id).map { |key_pair|
      { key_pair.label, key_pair.public_key, "0" }
    }
    render IndexPage, addresses: addresses
  end
end

We can add our own method to the query but Lucky generates lots of methods for us. And in most cases this would be enough to get the data you need.

The final result

Succesfully adding data to the database.

Installing dependencies with Shards and retrieving balances

Dependency management is very easy in Crystal using shards. This is similar to Bundler that comes with rails with the added security benefit of also using a lock file. Add the following to your shards.yml

github: onchain/onchain-shard

And then run shards to install your new dependency.

$ shards install
...
Fetching https://github.com/onchain/onchain-shard.git
...

You'll need to register with https://onchain.io and set the API key into your environment. When you've done that we can call out to onchain and view the results.

class Bitcoins::Index < BrowserAction

  get "/bitcoins" do

    addresses = populate_balances(KeyPairQuery.new.user_id(current_user.id))

    render IndexPage, addresses: addresses, form: KeyPairForm.new
  end

  private def populate_balances(key_pair_query)

    just_addresses = key_pair_query.map{ |keypair| keypair.public_key }

    balances = OnChain::API::Address.get_balances(
      "testnet3", just_addresses.join(","))

    case balances
    when OnChain::API::Balances
      return key_pair_query.map{ |keypair|
        bal = balances.addresses.find { |a| a.address == keypair.public_key }
        human_bal = "0"
        human_bal = bal.human_balance.to_s if bal
        { keypair.label, keypair.public_key, human_bal }
      }
    else
      puts balances
      return key_pair_query.map{ |keypair|
        { keypair.label, keypair.public_key, "" }
      }
    end
  end
end

Conclusion

There's more we can do with the wallet like giving the user the ability to spend. However I think we've covered enough to give a good overview of how Lucky works.

With the lucky framework we have all the power of Rails but with the added benefit of the compiler. The code footprint is small and the quality to me feels higher.