Using IBM Cloud Functions for Github Hooks
Tags: showdev, serverless, payid, webmonetization
Published: 2020-07-06 14:38:50

So you want to get Github to "do something" on a commit? Well, serverless technologies like IBM Cloud Functions (based on Apache Openwhisk) could be the answer for you.

[A 1080p version of this video is on Cinnamon here.](https://www.cinnamon.video/watch?v=356335029560804720)

This is part two of a live coding session in which I show how to create an IBM Cloud Function in Python to look up a user's PayID and resolve their payment address to pay them some XRP on commit. Part one is here:

{% post https://dev.to/hammertoe/payid-hackathon-live-coding-monetizing-github-commits-585f %}

Once again, I was joined by my colleague, [Si Metson](https://twitter.com/drsm79/), for the show and we were working on this together with a view to submit it to the [PayID hackathon currently running](https://payid.devpost.com/).

## Recap of the session

I'm going to run through the basic steps we covered in both part I and II of this session. I will be taking examples from the code we wrote during the session, although may omit or simplify some bits for focus in this post.

### Setting up Cloud Functions

Firstly you will need an account on IBM Cloud, which you can get [here](http://ibm.biz/BdqptL).

You will need to [download the IBM Cloud CLI](https://cloud.ibm.com/functions/learn/cli), login, install the cloud functions plugin, create and target a namespace:

```bash
$ ibmcloud login -a  
$ ibmcloud plugin install cloud-functions
$ ibmcloud namespace create mynamespace
$ ibmcloud fn property set --namespace mynamespace
```

### Code For Our Webhook

We then need to create our code to be called. I've simplified this a bit from the video to make it a bit easier to blog about. But there are three main functions:

A function `pay_xrp_testnet` that takes a wallet seed (secret key) for a wallet on the XRP Ledger testnet and will make a payment to a destination address of a certain amount using the [Xpring Python API](https://pypi.org/project/xpring/). For the sake of brevity this function is not waiting to confirm that the transaction was successful.

```python
def pay_xrp_testnet(wallet_seed, address, amount):
    # Create a wallet instance using the seed
    wallet = xpring.Wallet.from_seed(wallet_seed)

    # The XRP testnet
    url = 'test.xrp.xpring.io:50051'
    client = xpring.Client.from_url(url)

    # Create a transaction
    txn = client.send(wallet, address, str(amount))

    # Submit txn to the network
    res = client.submit(txn)

    return res
```

We then have a function, `get_address_from_payid`, that given a [PayID](https://payid.org/) will form an HTTP request to fetch the contents of the PayID and parse it for the address for the network and environment we want:

```python
def get_address_from_payid(payid, network, environment):
    # Convert the PayID into a URL e.g.
    # pay$username.github.io -> https://username.github.io/pay
    local_part, domain = payid.split('$')
    url = f'https://{domain}/{local_part}'
    response = requests.get(url)
    response.raise_for_status()

    data = response.json()

    # Look for an address that matches the network
    # and environment we want to use
    for address in data['addresses']:
        if address['paymentNetwork'] == network and \
           address['environment'] == environment:
            return address['addressDetails']['address']
```

Again for the sake of brevity here, we are not handling errors if we can't resolve or parse the PayID.

And finally, we have our `main` function that is actually called by the webhook:

```python
def main(args):
    # The wallet seed (secret key) is passed in, as a bound
    # parameter to this function
    xrp_wallet_seed = args['xrp_wallet_seed']

    # Extract the username of the pusher from the
    # Github hook payload
    pusher = args['pusher']['name']

    # Assume a PayID on Github of this form
    payid = f'pay${pusher}.github.io'

    # Calculate the amount based on number of commits
    # this is just an example and could be any metric
    try:
        num_commits = len(args['commits'])
    except KeyError:
        num_commits = 1
    amount = 1000 * num_commits
    
    # Get the address from the PayID and make payment
    address = get_address_from_payid(payid, 'XRPL', 'TESTNET')
    res = pay_xrp_testnet(xrp_wallet_seed, address, amount)

    return {
        'address': address,
        'amount': amount,
    }
```

In this version we are just assuming that a Github user with username `foo` will have a PayID setup of `pay$foo.github.io`. But in the future we'd like to parse the PayID from the commit message itself.

We calculate how much to pay the contributor based on the number of commits in this push multiplied by 1000 drops. There are 1e6 drops in 1 XRP.

I've detailed in another post how to setup a PayID using Github Pages:

{% post https://dev.to/hammertoe/hosting-a-payid-on-github-pages-5fp0 %}

### Creating a Cloud Function

Now how to get create the Cloud function? Well this was slightly complicated by the fact we need to have access to the `xpring` library, which is not included by default. So we created our own Docker image, the Dockerfile being:

```
FROM ibmfunctions/action-python-v3.7

RUN pip install --upgrade pip
RUN pip install -r requirements.txt

RUN pip install xpring[py] requests
```

This bases off the original docker image that IBM Cloud Functions uses for Python functions and installs the `xpring` library to it.

I then built and pushed the Docker image to Dockerhub:

```bash
$ docker build -t hammertoe/twitch_payid:0.1 . 
$ docker push hammertoe/twitch_payid:0.1
```

I can then refer to it when I create my cloud function. I also bind a value of `xrp_wallet_seed` to the function with the secret key of my wallet. Obviously take care with this secret as with it anyone can empty your account (in this case it is just the testnet, so no issue). By passing `--web true` we allow this function to be accessed from the web.

```bash
$ ic fn action create webhook webhook.py --docker hammertoe/twitch_payid:0.1 --param xrp_wallet_seed snzBUmvTTAzCCRwGvGfKeA6Zqn4Yf --web true 
ok: created action github_pay
```

### Configure the webhook in Github

To configure the webhook in Github, you need to first get the URL of the function we just created:

```bash
$ ic fn action get github_pay --url
ok: got action github_pay
https://eu-gb.functions.cloud.ibm.com/api/v1/web/2155dac7-8c22-4c44-8241-987ae2557df6/default/github_pay
```

The can then put that URL *with `.json` on the end* into our Github hook settings. Also note we are not setting a secret here, which we would want to configure in a real world system to ensure our Cloud Function can only be called by Github and not someone else randomly.

![Screenshot of setting up Github hook](https://dev-to-uploads.s3.amazonaws.com/i/rek53u3me32bpeh35p26.png)
 
### Results

Here is an animated gif showing a commit being pushed and the payment arriving a few seconds later in my XRP wallet:

![Animated gif of github commit being paid in XRP](https://dev-to-uploads.s3.amazonaws.com/i/782eth9x1ke7fhe1qmz6.gif)

The next steps will be to take this and expand it to be a bit more generic and secure to run in the real world.

The full code to this session is, as always, in the following Github Repo:

{% github https://github.com/IBMDeveloperUK/ML-For-Everyone %}

I hope you enjoyed the video, if you want to catch them live, I stream each week at 2pm UK time on the IBM Developer Twitch channel:

https://developer.ibm.com/livestream