Micropayments for games using LNP/BP (tutorial)

From time to time I choose a programming project to keep in touch with what’s important. This time I combined game development with cryptocurrency. I downloaded a Game Engine and created the World’s first Lightning Network (micro)payment solution for gamedevelopers to add to their Godot creations.

Because I’m fully aware being a Indie game developer is, from time to time, a harsh existence, I decided to Open Source this little project here and add this tutorial for those interested. It’s under MIT license but I appreciate attribution. Anyway: Have fun with it!


Now Godot game developers can monetize on their games using the Bitcoin Lightning Network.

Tutorial

This tutorial explains step-by-step how to implement payments in games built with the Godot 3.1 Game Engine. As micropayments are impossible to do online with traditional payment systems, the architecure chosen for this setup uses Bitcoin’s Lightning Network Protocol (LNP/BP). This setup offers payments for your game, even as small as $0.001 (plus tiny fees paid by the player).

This project is also available as open source on insert-coins.org.

Server setup

This tutorial focuses on the implementation of micropayments in games built with Godot. If you have no experience with running a Bitcoin node and/or the Lightning network it is good to know this setup can run extremely stable and cheap on a Raspberry pi with a 500GB external harddrive that has it’s own power source.

You’ll need Bitcoin (the Bitcoin daemon bitcoind) to connect to the Bitcoin network, the c-lightning implementation of the Lightning Network protocol (lightningd) and Lightning Charge, an API daemon (charged) for accepting Lightning payments. So let’s get going.

  • Install bitcoind, the bitcoin daemon. See: bitcoin.org and bitcoincore or raspnode. Make sure you set it up with txindex=1 and without pruning. When finished, setup RPC access for the lightning daemon. See: JSON-RPC.
  • Install lightningd and test the JSON-RPC Interface. See: ElementsProject
  • Setup Lightning Charge and bind it to all interfaces. See: lightning-charge (remember your server ip-address, charge’s port (usually 9112) and your login (api-token) and password, you’ll need it later)
  • Run the three services (bitcoind, lightningd and charge) and setup an inbound Lightning channel over the Lightning Network, for example a free one from lnbig.
  • Install a Lightning Network wallet on your mobile phone, for example bluewallet and add some funds.

Now your server is up and running, we’re ready for the fun (and easy) stuff.

Godot

If you have no experience with the Godot Game Engine that’s fine. It is extremely fast and easy to setup (only one click!).

  • Download version 3.1 or higher of this amazing Open Source game engine for free: godot (it’s only 50 MB).

Open Godot and get started with the tutorial. If you’re impatient, download the Godot ‘insertcoins’ project here, import it in Godot and follow along below, focusing on the bold and underlined bits of this tutorial.

Initial project setup

  • Create a new folder “insertcoins” using your OS file-explorer. Remember this location, you’ll need it later on.
  • In Godot create a new godot project on startup, name it “insertcoins”, select the directory you just created ‘insertcoins’.
  • Create your Root Node by clicking ‘2D Scene’ on the left hand side of the screen, under the Scene tab.
  • Rename the Root Node by double clicking on ‘Node2D’ in the top of the Scene tab. Name it “insertcoin” and press Enter.

Create a payment button

Add a button child node under your Root Node ‘insertcoin’ (Node2D) by right clicking it and selecting ‘Add Child Node’. Use the search field to search for the ‘Button’ node type. Double click it and rename this Node “insertcoin”. Enter it’s value in the Text field property using the inspector tab on the right hand side panel. It is the first property at the top of the Inspector tab. Enter “insert coin(s)”. Look for the ‘Rect’ Control in the Inspector of the Button and expand it’s properties by clicking on the ‘v’ icon. Change the values For Size. Change x to 300 and y to 100 (300×100). Then change the values for Position. x 366 and y 418 (366×418).

We’re working towards this Node tree for our Scene:

Use this image as a reference.

Create an invoice panel

  • Add a Panel child node under Node (Root Node). Rename it to “invoice”, set the properties under Rect to Position 337×67 and Size: 400×500.
  • Add a Button child node under invoice (Panel), enter it’s value in the Text field property: “generate invoice”, under rect: position 143×9.
  • Add a ColorRect child node under invoice (Panel), position 23×40, size 354×416, leave color white.
  • Add a Sprite child node under invoice (Panel), rename node to “qrcode”, position 200×240.
  • Add a Label child node under invoice (Panel), Text value: “click button to generate qr code”, position 25×470.
  • Hide the invoice (Panel) by clicking the eye icon (right from the Node (invoice) in the Scene tab).

Save the Scene you just created (short-cut: ctrl-s) and name it ‘insertcoin.tscn’

Code

  • Attach a new script to the ‘insertcoin’ Node (Root Node), name it ‘insertcoin.gd’
  • Click the ‘insertcoin’ node (Button) under the Node tab (this tab is next/behind the Inspector on the right hand side of Godot) and double click the ‘Pressed()’ signal, select the ‘insertcoin’ Node (root Node) and click ‘Connect’ to automatically create the ‘_on_insertcoin_pressed method’. The connection wil be created in the Script ‘insertcoin.gd’ of the ‘insertcoin’ Node.
  • Open the ‘insertcoin.gd’ script, remove all contents (ctrl-a, ctrl-x) and copy-paste gdscript: insertcoin.gd to replace everything (copy here from the turorial and paste inside godot script editor).

gdscript: insertcoin.gd

extends Node

func _ready():
     pass # Replace with function body.

func _on_insertcoin_pressed():
     get_node("invoice").show()
  • Attach a new script to the ‘qrcode’ Node (Sprite), name it ‘qrcode.gd’.
  • click the ‘Button’ Node (Button) under the Node tab (next/behind the Inspector) and double click the ‘Pressed()’ Signal, select the ‘qrcode’ Node (Sprite) and click ‘Connect’ to create the ‘_on_Button_Pressed’ Method.
  • Open the ‘qrcode.gd’ script by clicking on the scroll icon, next to the ‘qrcode’ Node (Sprite), delete all it’s contents (including the Method you just created) and copy-paste the following code:

gdscript: qrcode.gd

extends Sprite
const qrpng = "payreq_qrcode.png"
const path = "C:/Users/your/location/of/godot/insertcoins/"

func _ready():
    #unused
    pass

func generate_qrcode(payreq):
    var output = []
    #generate rcode
    OS.execute(path + 'zint.exe', ['-b', '58', '-o', path + qrpng, '--vers=15', '--scale', '2', '-d', payreq] ,true ,output)
    #to test the generation of the qrcode, uncomment the next line and check if zint is in this path
    #print(path)

func _on_Button_pressed():
    var httpr = load("res://HTTPRequest.gd").new()
    var jsonh = load("res://jsonhandler.gd").new()
    
    #generate new invoice
    var ni = httpr.new_invoice() 
    #test what happens by uncommenting the next line:
    #print(ni)

    #construct the new invoice data dictionary
    var invoice = jsonh.new_invoice(ni)
    
    #generate QR COde
    generate_qrcode(invoice['payreq'])
    #show qrcode
    var imageTexture = ImageTexture.new()
    var dynImage = Image.new()
    dynImage.load(path + qrpng)
    imageTexture.create_from_image(dynImage)
    self.texture = imageTexture

    #print(invoice['id'])
    #show invoice status
    get_node("../Button").disabled = true
    get_node("../Button").text = "Connecting..."
    get_node("../Label").text = "scan QR code with your LN wallet."
    
    #pause
    yield(get_tree().create_timer(1.1),"timeout")
    #var invoices = httpr.get_invoices()

    #start polling this invoice's status
    var invid = invoice['id']
    invoice_poller(httpr, jsonh, invid)

func invoice_poller(httpr, jsonh, id, count = 1023):
    #get status for this invoice (try 1023 times (max))
    count -= 1
    #request data and process json
    var invoices = httpr.get_invoices()
    var invoice = jsonh.poll_invoice(invoices, id)
    #print("status:" + invoice['status'] + " Expires: " + str(invoice['expires_at']) + " Conn timeout: " + str(count))
    if count == 0 or invoice['status'] == 'paid':
        #print(invoice)
        #print("status:" + invoice['status'] + " Expires: " + str(invoice['expires_at']) + " Conn timeout: " + str(count))
        if count == 0:
            print("LN server timeout")
        if invoice['status'] == 'paid':
            if int(invoice['msatoshi_received']) >= 10000:
                get_node("../Label").text = invoice['msatoshi_received'] + " sats received. Have fun!"
                get_tree().change_scene("res://game.tscn")
            else:
                get_node("../Label").text = "Payment received. Insufficient funds :-("
        #print("invoice expiry date: " + str(invoice['expires_at']))
        return
    else:
        yield(get_tree().create_timer(2.5),"timeout")
    get_node("../Label").text = "Scan QR code with your LN wallet. Conn timeout: " + str(count) + " (" + str(invoice['status']) + ")"
    #recurse: see if invoice is paid yet
    invoice_poller(httpr, jsonh, id, count)
  • Use zint.exe from this project or get a version for your system here: zint
  • In the ‘qrcode.gd’ Script set the Constant (const) on line 3 to the path of your godot project folder (you’ve remembered this in the first step of the initial setup).
  • Add a HTTPRrequest Child Node under ‘insertcoin’ (root Node).
  • Attach a script to the ‘HTTPRequest’ Node (HTTPRequest) and name it ‘HTTPRequest.gd’.
  • Attach a new script to this ‘HTTPRequest’ Node, delete it’s contents, and copy past the following code.

gdscript: HTTPRequest.gd

extends HTTPRequest
# This simple class can do HTTP requests
const host = "10.0.0.1"
const port = 9112

func _ready():
    #output = get_invoices()
    pass

func get_info():
    return request_data("info")
    
func get_invoices():
    return request_data("invoices")

func new_invoice():
    #return json
    return request_data("invoice")

func request_data(type):
    var err = 0
    var http = HTTPClient.new() # Create the Client.

    err = http.connect_to_host(host, port) # Connect to host/port.
    assert(err == OK) # Make sure connection was OK.

    # Wait until resolved and connected.
    while http.get_status() == HTTPClient.STATUS_CONNECTING or http.get_status() == HTTPClient.STATUS_RESOLVING:
        http.poll()
        #print("Connecting...")
        OS.delay_msec(500)

    assert(http.get_status() == HTTPClient.STATUS_CONNECTED) # Could not connect

    # Some headers
    var headers = [
        "User-Agent: Pirulo/1.0 (Godot)",
        "Content-Type: application/json",
        "Authorization: Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", #base64 encoded string "Token:Secret" ! DO NOT USE in shared compiled program (add hashing)
        "Accept: */*"
    ]
    var query = '{"msatoshi":10000,"metadata":{"customer_id":9999,"products":[1,1]},"description":"yourgame invoice"}'
    
    if type == "invoice":
        err = http.request(HTTPClient.METHOD_POST, "/" + type, headers, query) # Request a new invoice using POST
    else:
        err = http.request(HTTPClient.METHOD_GET, "/" + type, headers) # Request info or list of invoices using GET
    assert(err == OK) # Make sure all is OK.

    while http.get_status() == HTTPClient.STATUS_REQUESTING:
        # Keep polling for as long as the request is being processed.
        http.poll()
        #print("Requesting...")
        if not OS.has_feature("web"):
            OS.delay_msec(500)
        else:
            # Synchronous HTTP requests are not supported on the web,
            # so wait for the next main loop iteration.
            yield(Engine.get_main_loop(), "idle_frame")

    assert(http.get_status() == HTTPClient.STATUS_BODY or http.get_status() == HTTPClient.STATUS_CONNECTED) # Make sure request finished well.

    #print("response? ", http.has_response()) # Site might not have a response.
    if http.has_response():
        # If there is a response...
        headers = http.get_response_headers_as_dictionary() # Get response headers.
        #print("code: ", http.get_response_code()) # Show response code.
        #print("**headers:\\n", headers) # Show headers.

        # Getting the HTTP Body

        if http.is_response_chunked():
            # Does it use chunks?
            print("Response is Chunked")
        #else:
            # Or just plain Content-Length
            var bl = http.get_response_body_length()
            print("Response Length: ",bl)
        # This method works for both anyway

        var rb = PoolByteArray() # Array that will hold the data.

        while http.get_status() == HTTPClient.STATUS_BODY:
            # While there is body left to be read
            http.poll()
            var chunk = http.read_response_body_chunk() # Get a chunk.
            if chunk.size() == 0:
                # Got nothing, wait for buffers to fill a bit.
                OS.delay_usec(1000)
            else:
                rb = rb + chunk # Append to read buffer.
                
        var text = rb.get_string_from_ascii()
        #print("bytes got: ", rb.size())
        return text
  • On line 3 and 4 of ‘HTTPRequest.gd’ set your host (server-ip) and it’s port to what you have remembered earlier.
  • Generate a base64 encoded string from your ‘api-token’ and your ‘secretpassword’, concatenated using a semicolon like this: login:password. You remembered this during the installation of Lighting Charge. Use a handy online tool like this base64encode (choose ‘encode’) or a command-line if you know what you’re doing.
  • On line 39 replace the X-es with your base64 encoded access credentials. Be careful, do not use this in a production version of your game without understanding the security implications, people could decompile your executable, find these credentials, gain access to your Lightning Charge server and see your invoices.
  • Add a Child Node (Node) under ‘insertcoin’ (Root Node) and rename it “jsonhandler”. (You’ll find it in the node creation window when you remove your previous search string, it’s the top one)
  • Attach a new script, name it ‘jsonhandler.gd’, remove all it’s contents and copy-paste this code:

gdscript: jsonhandler.gd

extends Node
#class handles json

# Called when the node enters the scene tree for the first time.
func _ready():
    pass # Replace with function body.

func get_invoice(json, id):
    #get a specific invoice as array
    var invoices = JSON.parse(json).result
    
    #print("parsed json: ")
    #print(invoices)
    
    if typeof(invoices) == TYPE_ARRAY:
        #loop through list of invoices and find the one requested
        for i in range(invoices.size()):
            if invoices[i]['id'] == id:
                return invoices[i] #return dictionary
    else:
        print("unexpected result")

func new_invoice(json):
    #process json from new invoice
    var invoice = JSON.parse(json).result
    if typeof(invoice) == TYPE_DICTIONARY:
        return invoice #return dictonary
    else:
        print("not a dictionary")

func poll_invoice(json, id):
    var invoice = get_invoice(json, id)
    return invoice
  • From the Godot menu click ‘Scene > New Scene’, name it “game” and Save as ‘game.tscn’.
  • From the menu select ‘Project > Project settings’. In the project window under the ‘application’ category click the ‘Run’ item. Choose ‘insertcoins.tscn’ as your main Scene using the folder icon on the right.
  • Close the window.

Test

  • Click the play icon (top right) to run your game.

You’ll start off with your game’s splash screen with the ‘insert coin(s)’ button. After two clicks, you can open your previously installed Lightning wallet on your phone and pay your first invoice! If everything went well you will be presented with a gray ‘game’ screen. This is the part of your game behind your paywall.

  • If that didn’t work out right away, your QRcode might not generate. Fix this by making sure the ‘zint.exe’ file is in the folder you configured in line 3 of ‘qrcode.gd’. Fiddle around with line 14 as well. Uncomment this line in ‘qrcode.gd’ and do some testing (remove the “#” character).
  • If you do see a QRcode in the ‘invoice’ panel your server connection might need some additional love. Test if the server connection works properly. To do this uncomment line 23, 57, 59 or 60 of ‘qrcode.gd’.
  • More problems? Make sure your Base64 conversion is correct. Check if you used a UTF-8 destination charset by encoding your ‘api-token:password’ to UTF-8 before you encode to Base64. For example: ‘api-token:secretpassword’ should encode to ‘YXBpLXRva2VuOnNlY3JldHBhc3N3b3Jk’.

Still no success? Try these commands on your server and from another location. Did you start Lightning Charge with the –host 0.0.0.0 parameter as i mentioned in the beginning? This is required for remote access. Or try debugging using these commands with the credentials, host and port you configured with Lightning Charge:

CHARGE_URL=http://api-token:secretpassword@localhost:9112 
CHARGE_URL=http://api-token:secretpassword@10.0.0.1:9112 (if remote)
curl $CHARGE_URL/info
curl $CHARGE_URL/payment-stream
curl $CHARGE_URL/invoices

Once a payment came through you’re ready to build your game, starting with the ‘game.tscn’ Scene.

Happy game monetization!