Using 21 with Django and Heroku

Posted by Jeremy Kun

How to make a 21 app with Django and Heroku from scratch

In this tutorial we'll see how to set up a Django project for accepting bitcoin-payable HTTP requests using 21. Then we'll deploy the app to Heroku and purchase from it.

This tutorial has a Github repository, which you can use to follow along. Each major section of the article has an associated snapshot.

Starting the Django project

A blank slate

Let's start from a blank Django project, and do the minimum required to get a "hello world" bitcoin-payable endpoint up and running on your local machine.

I'll start by calling my project hello.

mkdir hello
cd hello

From here, we'll create and activate a fresh virtual environment.

virtualenv -p python3 venv
source venv/bin/activate
## You are now in the virtual environment (venv)

The virtual environment allows us to avoid mixing up which python packages are installed on our system and which are installed for this particular project. This is particularly nice when we want to deploy a service to Heroku, because we can explicitly describe which packages we want using a requirements.txt file. For example, you can see which pip is currently being used by running the which pip command:

## In the venv
which pip

Or we can inspect which third-party python packages are installed by running the pip freeze command:

## In the venv
pip freeze

In a fresh virtual environment, the above pip freeze command should produce an empty list.

Currently the 21 library only works with Django 1.8, so we can specify that in our requirements file and install it. By specifying it with the special ~= operator, we tell pip to install either 1.8.0 or the newest version that is compatible with it:

## In the venv
echo 'django~=1.8.0' > requirements.txt
pip install -r requirements.txt

Now if we relist the current requirements with pip freeze, we will see the specific version that pip installed (currently 1.8.13 as of this writing):

## In the venv
pip freeze

Open requirements.txt in a text editor and paste in the following content, which are all the requirements we need for this particular project:

django~=1.8.0
django-extensions==1.5.7
djangorestframework==3.2.3
dj-database-url==0.4.1
gunicorn==19.3.0
python-dotenv==0.1.3
hashids==1.1.0
psycopg2==2.6.1
two1==3.4.1
  • django-extensions is a nice set of extra apps like shell_plus that make life easier when developing.

  • djangorestframework is what the 21 library uses to handle payments.

  • dj-database-url will allow us to easily configure our database from an environment variable.

  • gunicorn is the wsgi HTTP server

  • python-dotenv will allow us to easily load configuration variables and secrets like API keys from environment variables.

  • The last line installs the most recent version of the 21 library. See our PyPI page for more detailed information about versions.

Now we can run the installation command again.

## In the venv
pip install -r requirements.txt

Now we can create our Django project. Be sure to include the trailing . argument in the next command, so that the Django project is initialized in the current directory.

django-admin startproject hello .

After initialization, you should see the following output if you run ls in the current directory:

hello            manage.py        requirements.txt venv
(venv) ~/hello $ ls hello/ 
__init__.py settings.py urls.py     wsgi.py

Interlude on how Django logically organizes projects

Django projects run with a wsgi server that directs urls to python functions called "views." Views are python functions that process requests and produce responses (raw HTTP, or Json, or rendered webpages).

The name we used to create the project (hello) is hard coded into a file called manage.py that tells Django where to look for the main project settings file.

#!/usr/bin/env python
import os
import sys

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hello.settings")

    from django.core.management import execute_from_command_line

    execute_from_command_line(sys.argv)

So hello.settings is where Django will look for information about where to direct urls, configuring the database, etc. This is where we'll configure project-wide settings that rarely change between deployments, and load deployment-specific configuration variables from the environment.

But before we do that, let's talk about apps. Django further allows you to logically subdivide your project into "apps." Each app has its own settings.py, url redirection, and views. A complicated Django project needs modularization into many apps, but most 21 apps probably don't, and having multiple apps is beyond the scope of this tutorial.

Either way, Django apps are just glorified python modules, so if you want you can make the modules from scratch yourself. If you want to make an app, check out the django-admin startapp command. But we'll instead make ours a single-app project with the following structure.

.
├── hello
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   ├── views.py
│   └── wsgi.py
├── manage.py
└── requirements.txt

Currently we're missing views.py, the file that actually processes requests. Django didn't make this file for us because complex Django projects usually use the default app for redirection only. But, again, we'll be doing a single-app style.

Back to our project

Open hello/settings.py in a text editor. Before we make any changes, notice a few lines that implement the organization described in the previous section:

WSGI_APPLICATION = 'hello.wsgi.application'
ROOT_URLCONF = 'hello.urls'

The first line tells Django to use hello/wsgi.py as the primary starting point for the project. The second line points to the base url router.

In order to tell Django that the hello directory is also an app (which we would need if we wanted to run Django management commands, etc.), we need to add hello to the INSTALLED_APPS list in settings.py. We'll also add django-extensions to this list, and shortly we'll add two1.bitserv.django to integrate 21. The former installs some extra tools we'll use shortly, and the latter includes all the tools needed for the two1 library to interact with Django.

At this point INSTALLED_APPS list looks like this

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_extensions',
    'hello',
)

Before we do anything else project-specific, let's configure Django to load environment variables and the database. This amounts to the following few lines in settings.py

import dotenv
import dj_database_url

# this line is already in your settings.py
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# load environment variables from .env
dotenv_file = os.path.join(BASE_DIR, ".env")
if os.path.isfile(dotenv_file):
    dotenv.load_dotenv(dotenv_file)

# load database from the DATABASE_URL environment variable
DATABASES = {}
DATABASES['default'] = dj_database_url.config(conn_max_age=600)

Also, delete the following lines that hard-code the database configuration

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

Now let's add a .env file to the project base directory which includes our database URL. We'll add more environment variables shortly.

## In the venv
echo 'DATABASE_URL=sqlite:///db.sqlite3' > .env

The dj-database-url app parses the string and determines which protocol to load. To create a fresh (empty) database, run python manage.py migrate

## In the venv
python manage.py migrate

The output will look similar to the following:

Operations to perform:
  Synchronize unmigrated apps: staticfiles, hello, django_extensions, messages
  Apply all migrations: contenttypes, auth, admin, sessions
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying sessions.0001_initial... OK

Also, if you run python manage.py with no arguments, you'll see that the [django_extensions] family of commands pops up in the list, meaning Django successfully detected the django_extensions app.

To test our database, run python manage.py shell_plus. This will open a python Read-Eval-Print Loop (REPL), load all the models from the database (we'll talk about models more shortly), and allow you to run commands that tinker with the database.

## In the venv
python manage.py shell_plus

The output will look similar to the following, concluding with the python REPL:

# Shell Plus Model Imports
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import Group, Permission, User
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session
# Shell Plus Django Imports
from django.db import transaction
from django.core.urlresolvers import reverse
from django.db.models import Avg, Count, F, Max, Min, Sum, Q, Prefetch
from django.utils import timezone
from django.core.cache import cache
from django.conf import settings
Python 3.5.1 (default, Jan 22 2016, 08:54:32) 
[GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

To check that your environment variables were loaded properly, run

>>> ## In the REPL
>>> import os
>>> os.environ['DATABASE_URL']
'sqlite:///db.sqlite3'

To check that your database was created properly, try loading the Session model.

>>> ## In the RELP
>>> Session.objects.all()
[]

That means there are no sessions to load, but Django was able to access the database. Quit the REPL by typing exit() or pressing Ctrl-D. Finally, try running the server

python manage.py runserver

This should print the following output, which includes the IP address and port number on which it started your server:

Performing system checks...

System check identified no issues (0 silenced).
May 20, 2016 - 19:04:35
Django version 1.8.13, using settings 'hello.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Browse to http://127.0.0.1:8000/ and you'll see an "It worked!" message. Quit the server with Ctrl-C.

Note that the dj_database_url module supports most commonly used databases. See the package's documentation for information on how to configure a different database backend.

Integrating the 21 library

The 21 library includes a Django app you can install by adding 'two1.bitserv.django' to your INSTALLED_APPS in hello/settings.py.

If you try to run the server now your project will fail, because the 21 library expects a 21 wallet to be available in the project's main settings file with the variable name WALLET. For example, if you run python manage.py without setting up the wallet first, you will see the following error:

Traceback (most recent call last):
  ...
  File "/Users/jeremy/hello/venv/lib/python3.5/site-packages/two1/bitserv/django/__init__.py", line 5, in <module>
    payment = Payment(settings.WALLET)
  File "/Users/jeremy/hello/venv/lib/python3.5/site-packages/django/conf/__init__.py", line 49, in __getattr__
    return getattr(self._wrapped, name)
AttributeError: 'Settings' object has no attribute 'WALLET'

To make this appropriate for deployment to Heroku, we will load our wallet mnemonic as an environment variable, and create the wallet using import_from_mnemonic. The reason we need to do it this way is because we don't want to perform interactive 21 CLI commands on the deployed server. All of that should be automated in our build and deploy workflow.

If you already have a wallet mnemonic you know you want to use, you can add it to your .env with the variable name TWO1_WALLET_MNEMONIC (also add an environment variable for TWO1_USERNAME), and add the following lines to your settings.py.

from two1.wallet import Two1Wallet

# ... prior code that loads env vars ...

'''
    The left hand side will be referenced in python via, e.g.

    from hello import settings
    settings.TWO1_WALLET_MNEMONIC

    The quoted string on the right hand side is the name of the environment
    variable that is loaded by the dotenv package.
'''
TWO1_WALLET_MNEMONIC = os.environ.get("TWO1_WALLET_MNEMONIC")
TWO1_USERNAME = os.environ.get("TWO1_USERNAME")
WALLET = Two1Wallet.import_from_mnemonic(mnemonic=TWO1_WALLET_MNEMONIC)

At this point my .env file looks like this

DATABASE_URL=sqlite:///db.sqlite3

TWO1_USERNAME=jeremy31415
TWO1_WALLET_MNEMONIC=runway cloud toaster toward crack vessel metal erase climb holiday amateur cube

If you can't remember your mnemonic, you can find one in your machine's default wallet.

cat ~/.two1/wallet/default_wallet.json

Now run the migrate command again to create the extra database tables needed by 21 and the runserver command to verify there are no errors.

## In the venv
python manage.py migrate
python manage.py runserver

A first view

Recall, a view is a python function that accepts as input a request and returns some sort of response, whether it's a plain HTTP response, a Json response, or a rendered webpage. Here is the simplest possible bitcoin-payable view: it accepts a bitcoin payment from a GET request, and returns an HTTP response with a message.

from django.http import HttpResponse

from rest_framework.decorators import api_view
from two1.bitserv.django import payment


@api_view(['GET'])
@payment.required(100)
def buy(request):
    return HttpResponse('Hello 21!', status=200)

Put the above in the file hello/views.py. In it, we're including a payment.required(100) decorator to the view to charge 100 satoshis for a request. Later in this tutorial we'll see how to do dynamic pricing.

To direct requests to our new view, open hello/urls.py and add the following line to the urlpatterns list.

import hello.views

urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^buy$', hello.views.buy),
]

This will make it so that any request to our web server that hits the url /buy will forward the request to the function hello.views.buy. The name in the url and the name of the view function do not need to the same.

Testing our hello world app locally

Start your server by running python manage.py runserver.

If you try to visit http://127.0.0.1:8000 in a browser, you'll see a TemplateDoesNotExist error, which is expected. However, if you curl the endpoint from a terminal, you'll see the "Payment Required" response.

curl "http://127.0.0.1:8000/buy"

Now if you open a new terminal (i.e. exit the virtual environment) and you have the 21 CLI tools installed, you can buy from the endpoint.

21 buy "http://127.0.0.1:8000/buy"

This should print the following output:

Hello 21!

You spent: 100 satoshis. Remaining 21.co balance: 143615 satoshis.

At this point, I saved a snapshot of the repository you can clone and experiment with.

Deploying to Heroku

Next, we'll deploy our hello world app to Heroku. We'll start by setting up a Heroku account. Browse to https://dashboard.heroku.com and sign up for a free account. Then install the heroku CLI tools. Run heroku login to associate your machine with your heroku account.

Once you've logged in, return to the heroku dashboard, click new and Create new app. Give it a name (this will be the prefix of your free subdomain, YOUR_APP_NAME.herokuapp.com). I called mine django-heroku-21-tutorial. Choose your region appropriately.

Configuring the build/deploy system with Github

In the Heroku dashboard, browse to the "Deploy" tab and you'll see a bunch of options for configuring your deployment. In this tutorial we'll deploy from Github. This means you have to create a Github repository (and account, if you don't have one) and push your source code to github. If you don't want to use this, you can use Heroku's command-line "toolbelt" to deploy.

When creating your repository, be sure not to push your .env file, your virtual environment, any __pycache__ files, or your database file. These are supposed to be local to each system. You can tell git to ignore these files with a .gitignore file.

Here is a minimal .gitignore file

__pycache__
*.py[cod]
venv/
*.sqlite3
.env

And here is an example runthrough of creating a new git repository and committing your changes. First create a repository on github.

new repository screen

On the next screen, copy down the git remote command.

blank repository

Now run the following commands from your Django app directory.

git init .
git add .
git commit -m "First commit"
git remote add origin https://github.com/j2kun/two1-django-heroku.git
git push origin master

Continuing with the Github deployment, select "GitHub" under "Deployment Method" and then click "Connect to GitHub", following the steps to authorize your application.

heroku github deployment option

Next, input your repository name, hit search, and click "Connect" next to your repository. We won't deploy just yet, but when we're ready we'll click "Deploy Branch" under "Manual Deploy."

Now browse to the "Settings" tab and under "Buildpacks" add a new buildpack called "heroku/python". This tells Heroku to install the python tools and run pip install -r requirements.txt when it deploys our server.

buildpack menu

Next, under "Config Variables" click "Reveal Config Vars". This will show which environment variables are configured for your app's deployment. It starts empty, so you should add a variable for each line in your .env file, except for the DATABSE_URL variable, which Heroku will add for us momentarily.

config variables menu

There are two more special environment variables we need to add to satisfy Heroku's build system. The first is DISABLE_COLLECTSTATIC=1. Django has special facilities for organizing and caching static files that aren't dynamically generated based on user requests. These might be icons, header images, or videos that are served for most requests. Since our endpoint won't be serving static files (a more complicated app might), we are temporarily disabling this feature.

The second is the database. Heroku doesn't allow you to have a database stored on the local filesystem, because Heroku machines are stopped and restarted very often, and all local data is wiped. Heroku stores databases in a separate place. To set this up, run the following at the command line.

heroku addons:create heroku-postgresql:hobby-dev -a YOUR_APP_NAME

This will provision a new postgres database for your app. Next we need to add the database url that was just provisioned for us to the Heroku config variables. Copy the output of the following command:

heroku config -a YOUR_APP_NAME

It should look like

=== tutorial-testing Config Vars
DATABASE_URL:         postgres://user3123:passkja83kd8@ec2-117-21-174-214.compute-1.amazonaws.com:6212/db982398
TWO1_USERNAME:        jeremy31415
TWO1_WALLET_MNEMONIC: runway cloud toaster toward crack vessel metal erase climb holiday amateur cube

If you didn't already have a DATABASE_URL config variable in the Heroku dashboard, Heroku will automatically add this one for you. Otherwise you have to manually copy the url.

In a more advanced deployments, you can set up a pipeline via Heroku, which allows one to connect a large project to continuous integration tools and configure a staging pipeline. This is beyond the scope of this tutorial.

The Procfile

Heroku deploys your project to a number of abstract servers they call "dynos." Heroku allows you to abstractly define a dyno type via a Procfile, and to seamlessly scale the number of dynos running your app if you have an unexpected increase in demand.

Here's what a template Procfile looks like, where the hello.wsgi string is specific to our project. It refers to the python module hello/wsgi.py.

web: gunicorn hello.wsgi --log-level=info --log-file -

Save this to a file called Procfile in the base directory of your project.

Next, we need to add a file called runtime.txt that tells Heroku what version of python to install. Put the following line in runtime.txt in the base directory of your project.

python-3.5.1

Now our entire directory structure looks like this.

.
├── .env               # local
├── .gitignore
├── Procfile
├── db.sqlite3         # local
├── hello
│   ├── __init__.py
│   ├── __pycache__    # local
│   ├── settings.py
│   ├── urls.py
│   ├── views.py
│   └── wsgi.py
├── manage.py
├── requirements.txt
├── runtime.txt
└── venv/              # local

Now push to the github repository and let's deploy.

git add Procfile runtime.txt
git commit -m "added Procfile and runtime.txt"
git push origin master

Deploying and testing

On the Heroku dashboard, browse to the "Deploy" tab and under "Manual Deploy," select the master branch of your git repository, and hit "Deploy Branch." You can watch the build log, and fix any errors that arise.

First heroku deployment

If it's successful, you'll see a green "Deployed to Heroku" with a checkmark, and you can click "view." This will give an error (as we saw locally). But again you can curl the /buy endpoint to see a payment required.

curl "https://YOUR_APP_NAME.herokuapp.com/buy"

And we can buy from it.

21 buy "https://YOUR_APP_NAME.herokuapp.com/buy"

Now we have a nice development/deployment routine:

  • Work on your code
  • Test locally
  • Push to github
  • Deploy on heroku

Going public

In this section we'll submit our app to the marketplace as part of our deployment cycle. This will add an extra step to our deployment cycle, in the form of an extra CLI command we have to run whenever our app's metadata changes.

The manifest

We'll start by writing our app's manifest, which is a structured .yaml metadata file. We need to serve the manifest at the /manifest endpoint to allow the 21 marketplace to check the status of our app. This also allows the 21 marketplace to populate your 21 profile with your app's details.

Here's a simple manifest file. Note that we're hard-coding our heroku app's url. Store this file at hello/manifest.yaml.

swagger: '2.0'
info:
  title: Hello 21!
  description: A hello world app hosted on heroku using Django. 
  termsOfService: https://opensource.org/licenses/MIT
  x-21-usage: ''
  x-21-quick-buy: |
    $ 21 buy "https://YOUR_APP_NAME.herokuapp.com/buy"
  x-21-category: utilities 
  x-21-app-image: '<your image url>'
  x-21-total-price:
    min: 100 
    max: 100
  contact:
    name: Jeremy Kun
    email: jeremy@21.co
    url: https://21.co
  license:
    name: MIT LICENSE
    url: https://opensource.org/licenses/MIT
  x-21-keywords:
    - tutorial
    - heroku
    - django
  version: '0.3'
host: <your project name>.herokuapp.com
schemes:
  - https
basePath: /
paths:
  /buy:
    get:
      summary: buy a hello world message 
      produces:
        - text/plain
      responses:
        200:
          description: a hello world message 
          schema:
            type: text

You can get a url to use for your x-21-app-image (which is displayed in the 21 marketplace and on your 21 profile), by using the 21 image uploader.

Submitting the app to the 21 marketplace

Dry run

Now, outside the virtual environment, run 21 status to make sure you're logged in to the 21 CLI, and then submit the app with the following commands.

First join the 21 marketplace.

21 market join 
21 market status

Next submit your manifest. As a safety measure, the 21 CLI will ask you to make sure you want to submit an app hosted somewhere that's not your 21 marketplace virtual ip, so say yes and continue.

21 publish submit ~/hello/hello/manifest.yaml

Now browse to your profile to verify that the app shows up.

21 profile

My app looks like this

my profile listing

Now you can remove the listing by looking up the id:

21 publish list

Which will display output similar to the following:

Listing all the published apps by jeremy31415: 

Page 1/1
id    Title      Url                                              Rating         Is up    Is healthy    Average Uptime    Last Update
----  ---------  -----------------------------------------------  -------------  -------  ------------  ----------------  -----------------------------
q1D   Hello 21!  https://django-heroku-21-tutorial.herokuapp.com  Not yet Rated  True     True          100.00%           2016-05-20 15:17:53 UTC-07:00

Enter the id of the app for more info, n for next page, p for the previous page, q to stop search.: q

The number you want is q1D, and you can remove the listing from the marketplace by running

21 publish remove q1D

Now the listing will no longer show up on your profile.

It's important that we tested app submission manually before we automate it, so we can ensure the manifest is correct and that our user has joined the 21 market.

During deployment

Publishing an app is an idempotent operation, so you can safely republish without unpublishing first. We'll take advantage of this and fold the publish step into our deployment workflow, which will now be

  • Work on your code
  • Test locally
  • Push to github
  • Deploy on heroku
  • Run a heroku CLI command to publish the app

We'll make the last step possible by adding a Django command to our project. A Django command is an argument to python manage.py like runserver or migrate. We can make special commands that Django automatically adds to the list of available commands.

Back inside your project, set up some special directories that Django recognizes for commands.

## In the venv
mkdir -p hello/management/commands
touch hello/management/__init__.py
touch hello/management/commands/__init__.py

Now create a file hello/management/commands/publish.py. Fill this file with the following.

import logging
import sys
from datetime import datetime

from django.core.management.base import BaseCommand

from hello import settings
from two1.commands import publish
from two1.server import rest_client


class Command(BaseCommand):
    help = 'Publish your app to the marketplace'

    def __init__(self):
        super().__init__()
        self._logger = logging.getLogger('hello.publish')
        logging.basicConfig(stream=sys.stdout, level=logging.INFO) 
        self._username = settings.TWO1_USERNAME
        self._client = rest_client.TwentyOneRestClient(
            username=self._username, wallet=settings.WALLET
        )

    def handle(self, *args, **options):
        manifest_path = 'hello/manifest.yaml'
        app_name = 'Hello 21'
        try:
            publish._publish(self._client, manifest_path, '21market', True, {})
            self._logger.info(
                '%s publishing %s - published: True, Timestamp: %s' %
                (self._username, app_name, datetime.now())
            )
        except Exception as e:
            self._logger.error(
                '%s publishing %s - published: False, error: %s, Timestamp: %s' %
                (self._username, app_name, e, datetime.now())
            )

Now you can test your publish command with python manage.py publish.

python manage.py publish

The output should look similar to the following:

Publishing Hello 21! at https://django-heroku-21-tutorial.herokuapp.com/ to 21market.
INFO:two1.commands.publish:Publishing Hello 21! at https://django-heroku-21-tutorial.herokuapp.com/ to 21market.
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): api.21.co
Hello 21! successfully published to 21market. It may take a couple of minutes for your app to show up in the marketplace.
You can view your app at https://21.co/mkt.
INFO:two1.commands.publish:Hello 21! successfully published to 21market. It may take a couple of minutes for your app to show up in the marketplace.
You can view your app at https://21.co/mkt.
INFO:hello.publish:jeremy31415 publishing Hello 21 - published: True, Timestamp: 2016-06-01 18:24:22.003739

If you receive any errors at this step, it's likely because you did not associate the wallet corresponding to your wallet mnemonic with your 21 account. 21 authenticates requests like publish by requiring you to sign a request-specific message with your wallet's private key, and then it verifies the signature using the public key for that wallet stored on the 21 servers. If you've never performed a 21 login with this specific wallet on your machine, the requests will be rejected by the server.

If you're experiencing this problem, you should back up your current machine wallet, restore from the mnemonic in your .env using wallet restore, login as the user in your .env. Then you can safely restore your original wallet and submit.

mv ~/.two1/wallet/default_wallet.json ~/.two1/wallet/default_wallet.json.bak
wallet restore

This will prompt you for your mnemonic:

Please enter the wallet's 12 word mnemonic: YOUR MNEMONIC GOES HERE 

Restoring...
Wallet successfully restored. Run '21 login' to connect this wallet to your 21 account.

Then run login and restore your wallet:

21 login -u YOUR_USERNAME
mv ~/.two1/wallet/default_wallet.json.bak ~/.two1/wallet/default_wallet.json
21 login -u YOUR_USERNAME

You can unpublish from any location, but now that we've got this Django command, we can commit it to Github, deploy to Heroku, and publish with the following heroku command. First install the heroku CLI tools. Then run the following with your Heroku app name.

## In the venv
heroku run python manage.py publish -a YOUR_APP_NAME

You could also run this command locally, but in many cases deployment commands will depend on environment variables that differ on your dev machine and your Heroku deployment. For example, you might have a development wallet that differs from your production wallet. So it's best to let Heroku run the command.

Now you can buy from the app yet again.

21 buy "https://YOUR_APP_NAME.herokuapp.com/buy"

I've made another snapshot of the project at this stage, which you can clone.

Adding complexity to our app

Now we're going to make our app stateful by adding a very simple database model for a "token."

The Token model

Create a file hello/models.py which has the following.

from django.db import models
from django.utils import timezone


class Token(models.Model):
    '''
        A model representing a token given to the user.
    '''

    created = models.DateTimeField(default=timezone.now)

    '''
        The value of the token, given to the user, which is a hashid of the
        database id.
    '''
    value = models.CharField(max_length=100, null=True, default=None)

    '''
        True when the token has successfully been redeemed, False otherwise.
    '''
    redeemed = models.BooleanField(default=False)

This is a simple Django model that represents a token that can be given to the user and later redeemed. The details of the Django object-relational mapping is beyond the scope of this tutorial, but it suffices to say that Django allows you to treat database objects more or less like regular python objects.

This value of the token will be a hash of the database id. It is considered bad to expose your database ids to the public, and using hashids will allow us to provide unique tokens without exposing any database ids.

Make sure hashids is in your your requirements.txt:

hashids==1.1.0

If not, add it and pip install -r requirements.txt again.

Then add a randomly chosen salt to your .env:

# in .env
HASHIDS_SALT=32020312999310397602

And add the corresponding line to your settings.py:

# in settings.py
HASHIDS_SALT = os.environ.get("HASHIDS_SALT")

Be sure to add this new environment variable to your Heroku config variables.

And finally, run python manage.py migrate to tell Django to create a database table for the new model.

The redeem view

Remove the buy view from your views.py and replace it with two views for buy and redeem. For the sake of variety, we'll implement the buy as a GET request and the redeem as a POST request. Our complete views.py is shown below.

import sys
import yaml
import hashids
import logging

from django.core.exceptions import ObjectDoesNotExist
from django.http import JsonResponse
from rest_framework.decorators import api_view
from two1.bitserv.django import payment

from hello import settings
from hello.models import Token

hasher = hashids.Hashids(salt=settings.HASHIDS_SALT, min_length=5)

logger = logging.getLogger(__name__)
logging.basicConfig(stream=sys.stdout, level=logging.INFO)


@api_view(['GET'])
@payment.required(100)
def buy(request):
    new_token = Token.objects.create()
    new_token.value = hasher.encode(new_token.id)
    new_token.save()

    return JsonResponse({'token': new_token.value}, status=200)


def _redeem(token):
    try:
        requested_token = Token.objects.get(value=token)
        if requested_token.redeemed:
            raise ValueError()
    except ObjectDoesNotExist:
        logger.error('User requested token {} that does not exist'.format(token))
        return JsonResponse({'success': False, 'error': 'Invalid or redeemed token.'}, status=400)
    except ValueError:
        logger.error('User requested token {} that was already redeemed'.format(token))
        return JsonResponse({'success': False, 'error': 'Invalid or redeemed token.'}, status=400)

    requested_token.redeemed = True
    requested_token.save()

    return JsonResponse({'success': True, "message": "Thanks!"}, status=200)


@api_view(['POST'])
def redeem(request):
    try:
        token = request.data['token']
    except KeyError:
        return JsonResponse({'error': 'POST data must include "token"'}, status=400)

    return _redeem(token)

And add the new redeem endpoint to urls.py.

urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^buy$', hello.views.buy),
    url(r'^redeem$', hello.views.redeem),
]

Now we can pay for a token, and redeem it. Start the server locally, and then (from outside the virtual environment) run the buy command:

21 buy "http://127.0.0.1:8000/buy"

Which should print output similar to the following:

{
    "token": "Wa7M8"
}

You spent: 100 satoshis. Remaining 21.co balance: 128315 satoshis.

And redeem it similarly with a --data flag to make a POST request.

21 buy "http://127.0.0.1:8000/redeem" --data '{"token": "Wa7M8"}'

Which should print the following output:

{
    "message": "Thanks!",
    "success": true
}

As expected, trying to redeem the same token again fails.

21 buy "http://127.0.0.1:8000/redeem" --data '{"token": "Wa7M8"}'

Giving you the following error message:

{
    "error": "Invalid or redeemed token.",
    "success": false
}

Next we'll see how to add dynamic pricing to our app.

Dynamic pricing

Adding dynamic pricing involves replacing the integer value for payment.required to a callable. Lets change our token example from the previous section to charge users who try to redeem an invalid token.

Update your redeem view as follows, with the new get_redeem_price function.

def get_redeem_price(request):
    try:
        token = request.data['token']
    except:
        return JsonResponse({'error': 'POST data must include "token"'}, status=400)

    try:
        requested_token = Token.objects.get(value=token)
        if requested_token.redeemed:
            raise ValueError()
    except:
        return 100

    return 0


@api_view(['POST'])
@payment.required(get_redeem_price)
def redeem(request):
    try:
        token = request.data['token']
    except KeyError:
        return JsonResponse({'error': 'POST data must include "token"'}, status=400)

    return _redeem(token)

Notice that we're passing get_redeem_price to the payment.required decorator instead of a number. It's important to note that payments are charged before the body of the redeem function is run. So if your server raises an error during its processing, it will still charge the user. This makes it very useful to do request validation in the dynamic pricing function.

One more deployment step

Before our deployment process was the following:

  • Work on your code
  • Test locally
  • Push to github
  • Deploy on heroku
  • Tell heroku to publish

Now that we have a database, we need to add one more step to this list. We need to tell heroku to update the database with any changes we made since the last deployment. To do this, execute a heroku run command

heroku run "python manage.py migrate" -a YOUR_APP_NAME

So now our deployment process is

  • Work on your code
  • Test locally
  • Push to github
  • Deploy on heroku
  • Tell heroku to migrate the database
  • Tell heroku to publish

Once you deploy and migrate, you can run the same buy and redeem commands from the previous section against your production deployment.

Here is the final snapshot of the repository.

Final remarks

Be sure to set DEBUG=False before deploying to a production server. This is basic security so that attackers can't see what kinds of errors they're causing. If you do this, you'll need to additionally add your host server url to the ALLOWED_HOSTS list in your project's global settings.py. For example, I use the following:

# in settings.py
ALLOWED_HOSTS = [
    'django-heroku-21-tutorial.herokuapp.com'
]

DEBUG = os.environ.get("DEBUG", "False").lower() in ['true', '1', 't']

# in .env
DEBUG=True

How to send your Bitcoin to the Blockchain

Just as a reminder, you can send bitcoin mined or earned in your 21.co balance to the blockchain at any time by running 21 flush . A transaction will be created within 10 minutes, and you can view the transaction id with 21 log. Once the transaction has been confirmed, you can check the balance in your bitcoin wallet from the command line with wallet balance, and you can send bitcoin from your wallet to another address with wallet sendto $BITCOIN_ADDRESS --satoshis $SATOSHI_AMOUNT --use-unconfirmed. The --satoshis flag allows you to specify the amount in satoshis; without it the sendto amount is in BTC, but this behavior is deprecated and will be removed soon. The --use-unconfirmed flag ensures that you can send even if you have unconfirmed transactions in your wallet.


Ready to sell your endpoint? Go to slack.21.co

Ready to try out your bitcoin-payable server in the wild? Or simply want to browse and purchase from other bitcoin-enabled servers? Head over to the 21 Developer Community at slack.21.co to join the bitcoin machine-payable marketplace hosted on the 21 peer-to-peer network.