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.
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:
PayID Hackathon live coding - Monetizing Github Commits
Matt Hamilton ・ Jul 2 '20 ・ 2 min read
Once again, I was joined by my colleague, Si Metson, for the show and we were working on this together with a view to submit it to the PayID hackathon currently running.
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.
You will need to download the IBM Cloud CLI, login, install the cloud functions plugin, create and target a namespace:
$ 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. For the sake of brevity this function is not waiting to confirm that the transaction was successful.
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 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:
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:
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:
Hosting a PayID on Github Pages
Matt Hamilton ・ Jun 20 '20 ・ 2 min read
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:
$ 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.
$ 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:
$ 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.
Results
Here is an animated gif showing a commit being pushed and the payment arriving a few seconds later in my XRP wallet:
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:
IBMDeveloperUK / ML-For-Everyone
Resources, notebooks, assets for ML for Everyone Twitch stream
ML for Everyone
ML For everyone is a live show broadcast on the IBM Developer Twitch channel every week at 2pm UK time.
The show is presented by Matt Hamilton and focusses on topics in and around machine learning for developers, with the odd smattering of random Python topics now and then.
Past shows
- 2020/05/26 Using Docker Images with IBM Cloud Functions
- 2020/05/19 Intro to IBM Cloud Functions
- 2020/05/12 Aligning Audio Files
- 2020/05/05 Finding Similar Images with an Autoencoder
- 2020/04/30 Creating an Autoencoder in Python with Keras
- 2020/04/15 Scraping web content from graphql with Python
- 2020/04/07 Intro to Watson Data Studio and Investigating Air Quaility Data
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:
Top comments (0)