├── README.md ├── arduino_esp32 ├── README.md └── arduino_esp32.ino ├── doc ├── osx_screenshot.png └── usbchargerscreen1.png ├── osx_client └── osx_client.py ├── server └── server.py └── windows_linux_client ├── nano.ico └── nano_iot_client.py /README.md: -------------------------------------------------------------------------------- 1 | # Nano_Callback_System 2 | ## Introduction 3 | This is a simple system for detecting new blocks being sent to a Nano address, this is achieved by a server script which listens for new blocks via the Nano node callback and then can be setup to forward the block details over a websocket connection to listening clients. By using the node callback you can trigger an event as soon as a block is seen (and voted on) on devices and systems that don't have the capacity or the need to run a full node for example embedded systems. 4 | The system works in this order: 5 | * The 'client' system connects to the 'server' via a websockets connection and sends a json containing a Nano address to track. 6 | * The server adds this Nano address to an internal dictionary 7 | * The server monitors the callback and if it sees a transaction being sent to any of the addresses in its internal dictionary it then forwards this block to the client over the websockets connection. 8 | 9 | Currently a test server is running: `ws://yapraiwallet.space/call` (currently does not require an API key, just use "0") 10 | 11 | ## Why and Why Nano? 12 | You don't need to hold a wallet (either full or light) on a device that just needs to initiate an action based on a Nano transaction, just need to detect the incoming block and trigger the action. 13 | Nano is ideal for this as its feeless and fast enough to do this all in real time, the user will get a very quick response back from the device/system. In theory you could do the same with any crypto however you won't get the same advantages. 14 | 15 | ## Use Cases 16 | ### Embedded Hardware 17 | An internet connected embedded hardware device can connect via websockets to the callback server and monitor for activity of a particular address, this allows a low powered device not to need a full node to function. 18 | 19 | Example 1 - a Nano controlled parking meter: 20 | * The local council could create a Nano address assigned to a particular device on their Nano node (they hold the private key securely) 21 | * A internet connected parking meter could connect to the callback server and monitor for send transactions to this address 22 | * A user would scan a QR code on the parking meter and send a fixed amount to the parking meters address 23 | * The callback server would detect the incoming send block and push this block to the parking meter 24 | * The parking meter checks the block and amount and if correct will trigger the parking meter to turn on 25 | * The council's Nano node processes the pending blocks in their own time. 26 | 27 | Example 2 - USB charger in an airport paid for with Nano 28 | * Prototype is based on ESP32 Heltec WiFi board with SSD1306 OLED screen 29 | * User connects a device to the USB port 30 | * User sends a set amount of Nano to the displayed address (scan the QR code) 31 | * The callback server detects the incoming send block and pushes the data over the websockets connection to the ESP32 32 | * The device checks the block and turns on the USB port (time depends on amount of Nano sent) 33 | * This has been implemented, see [arduino_esp32](https://github.com/jamescoxon/Nano_Callback_System/tree/master/arduino_esp32) for more infomation. 34 | 35 | ![usbcharger1](https://github.com/jamescoxon/Nano_Callback_System/blob/master/doc/usbchargerscreen1.png) 36 | 37 | * [Youtube - Simple example of triggering with a send block](https://youtu.be/LOb4rLssOxY) 38 | * [Youtube - Example of Hardware](https://www.youtube.com/watch?v=FJB87_jbJ6k&feature=youtu.be) (please note this is an old implementation of the concept.) 39 | 40 | ### Desktop Notifications 41 | Requirements - python3.7 42 | 43 | #### OS X 44 | 45 | `pip3 install pync asyncio websockets json decimal` 46 | 47 | `python3 osx_client.py ws://yapraiwallet.space/call 0 xrb_1w4h9mk7nyzjdy8bt3o5u46uwrpdt6e4xaosja7yduz55jfiecmirdxhnz9z` 48 | 49 | This will run a python script that connects to the callback server and waits for a transaction to the address you've inputed. 50 | 51 | ![osx_screenshot](https://github.com/jamescoxon/Nano_Callback_System/blob/master/doc/osx_screenshot.png) 52 | 53 | 54 | #### Windows 55 | On Windows if you install `win10toast` you can have pop up notifications of transactions. Installation depends on how you have installed python on your windows system (may need to execute the script using powershell). 56 | 57 | `python3 nano_iot_client.py ws://yapraiwallet.space/call 0 xrb_1w4h9mk7nyzjdy8bt3o5u46uwrpdt6e4xaosja7yduz55jfiecmirdxhnz9z` 58 | 59 | #### Linux 60 | The linux python client will only show blocks in the terminal. 61 | `python3 nano_iot_client.py ws://yapraiwallet.space/call 0 xrb_1w4h9mk7nyzjdy8bt3o5u46uwrpdt6e4xaosja7yduz55jfiecmirdxhnz9z` 62 | 63 | ## Status 64 | Currently the system lacks any true authentication or security however this is something that will be implemented, it will be up to the service provider to decided how much they want to implement (it may be acceptable to have less security if your transactions are very small). Authentication will likely include the use of an api key but also will add the ability to actually track the account and check the hash/state blocks themselves. 65 | -------------------------------------------------------------------------------- /arduino_esp32/README.md: -------------------------------------------------------------------------------- 1 | # ARDUINO 2 | -------------------------------------------------------------------------------- /arduino_esp32/arduino_esp32.ino: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Example of Nano Callback Server Interface 4 | * Creates a USB charger, pay for charging with Nano 5 | * 0.1Nano = 10mins 6 | * 0.2Nano = 20mins 7 | * 0.5Nano = 60mins 8 | * 9 | * Based originally on 10 | * WebSocketClient.ino 11 | * 12 | * Created on: 24.05.2015 13 | * 14 | */ 15 | 16 | #include 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #define SSID_NAME "" 27 | #define SSID_PASS "" 28 | #define TRACKING_ADDRESS "" 29 | 30 | WiFiMulti WiFiMulti; 31 | WebSocketsClient webSocket; 32 | 33 | StaticJsonDocument<200> doc; 34 | StaticJsonDocument<1024> rx_doc; 35 | 36 | //U8X8_SSD1306_128X64_NONAME_SW_I2C u8x8(/* clock=*/ 15, /* data=*/ 4, /* reset=*/ 16); 37 | U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2 (U8G2_R0,/* clock=*/ 15, /* data=*/ 4, /* reset=*/ 16); 38 | uint64_t chipid; 39 | int count = 0; 40 | 41 | void hexdump(const void *mem, uint32_t len, uint8_t cols = 16) { 42 | const uint8_t* src = (const uint8_t*) mem; 43 | Serial.printf("\n[HEXDUMP] Address: 0x%08X len: 0x%X (%d)", (ptrdiff_t)src, len, len); 44 | for(uint32_t i = 0; i < len; i++) { 45 | if(i % cols == 0) { 46 | Serial.printf("\n[0x%08X] 0x%08X: ", (ptrdiff_t)src, i); 47 | } 48 | Serial.printf("%02X ", *src); 49 | src++; 50 | } 51 | Serial.printf("\n"); 52 | } 53 | 54 | void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { 55 | 56 | 57 | switch(type) { 58 | case WStype_DISCONNECTED: 59 | Serial.printf("[WSc] Disconnected!\n"); 60 | break; 61 | case WStype_CONNECTED: 62 | Serial.printf("[WSc] Connected to url: %s\n", payload); 63 | 64 | // send message to server when Connected 65 | 66 | doc["address"] = TRACKING_ADDRESS; 67 | doc["api_key"] = "0"; 68 | char output[512]; 69 | serializeJson(doc, output); 70 | Serial.println(output); 71 | webSocket.sendTXT(output); 72 | break; 73 | 74 | case WStype_TEXT: 75 | { 76 | Serial.printf("[WSc] get text: %s\n", payload); 77 | deserializeJson(rx_doc, payload); 78 | String block_amount = rx_doc["amount"]; 79 | 80 | long int noughtPointone = 100000; 81 | long int noughtPointtwo = 200000; 82 | Serial.println(block_amount); 83 | 84 | //Convert to nano 85 | int lastIndex = block_amount.length() - 24; 86 | block_amount.remove(lastIndex); 87 | 88 | Serial.println(block_amount); 89 | 90 | u8g2.clearBuffer(); 91 | u8g2.setFont(u8g2_font_helvB14_tf); 92 | u8g2.drawStr(0, 40, "Block Detected!"); 93 | u8g2.sendBuffer(); 94 | int count = 0; 95 | // Check amount 96 | if (block_amount.toInt() >= 100000){ 97 | if (block_amount.toInt() >= 500000){ 98 | count = 3600; 99 | } 100 | else if (block_amount.toInt() >= 200000){ 101 | count = 1200; 102 | } 103 | else { 104 | count = 600; 105 | } 106 | // Turn on Switch 107 | pinMode(18, OUTPUT); 108 | digitalWrite(18, HIGH); 109 | //Start Timer 110 | char current_time[40]; 111 | 112 | while(count > 0){ 113 | int seconds; 114 | int minutes; 115 | 116 | seconds = count % 60; 117 | minutes = (count - seconds) / 60; 118 | 119 | sprintf(current_time, "%02d:%02d", minutes, seconds); 120 | u8g2.clearBuffer(); 121 | u8g2.setFont(u8g2_font_helvB14_tf); 122 | u8g2.drawStr(30, 40, current_time); 123 | u8g2.sendBuffer(); 124 | count--; 125 | delay(1000); 126 | } 127 | 128 | digitalWrite(18, LOW); 129 | } 130 | else { 131 | u8g2.clearBuffer(); 132 | u8g2.setFont(u8g2_font_helvB14_tf); 133 | u8g2.drawStr(0, 40, "Not Enough Funds"); 134 | u8g2.sendBuffer(); 135 | delay(3000); 136 | } 137 | 138 | u8g2.clearBuffer(); 139 | u8g2.setFont(u8g2_font_helvB10_tf); 140 | u8g2.drawStr(0, 20, "0.1Nano = 10mins"); 141 | u8g2.drawStr(0, 40, "0.2Nano = 20mins"); 142 | u8g2.drawStr(0, 60, "0.5Nano = 60mins"); 143 | u8g2.sendBuffer(); 144 | 145 | // send message to server 146 | // webSocket.sendTXT("message here"); 147 | break; 148 | } 149 | case WStype_BIN: 150 | Serial.printf("[WSc] get binary length: %u\n", length); 151 | hexdump(payload, length); 152 | 153 | // send data to server 154 | // webSocket.sendBIN(payload, length); 155 | break; 156 | case WStype_ERROR: 157 | case WStype_FRAGMENT_TEXT_START: 158 | case WStype_FRAGMENT_BIN_START: 159 | case WStype_FRAGMENT: 160 | case WStype_FRAGMENT_FIN: 161 | break; 162 | } 163 | 164 | } 165 | 166 | void setup() { 167 | 168 | // Serial.begin(921600); 169 | Serial.begin(115200); 170 | 171 | //Serial.setDebugOutput(true); 172 | Serial.setDebugOutput(true); 173 | 174 | Serial.println(); 175 | Serial.println(); 176 | Serial.println(); 177 | 178 | delay(5000); 179 | 180 | for(uint8_t t = 4; t > 0; t--) { 181 | Serial.printf("[SETUP] BOOT WAIT %d...\n", t); 182 | Serial.flush(); 183 | delay(1000); 184 | } 185 | 186 | u8g2.begin(); 187 | u8g2.setFont(u8g2_font_helvB14_tf); 188 | 189 | chipid=ESP.getEfuseMac();//The chip ID is essentially its MAC address(length: 6 bytes). 190 | Serial.printf("ESP32 Chip ID = %04X",(uint16_t)(chipid>>32));//print High 2 bytes 191 | Serial.printf("%08X\n",(uint32_t)chipid);//print Low 4bytes. 192 | 193 | u8g2.clearBuffer(); 194 | u8g2.setFont(u8g2_font_helvB14_tf); 195 | u8g2.drawStr(0, 40, "Booting..."); 196 | u8g2.sendBuffer(); 197 | 198 | WiFiMulti.addAP(SSID_NAME, SSID_PASS); 199 | 200 | //WiFi.disconnect(); 201 | while(WiFiMulti.run() != WL_CONNECTED) { 202 | Serial.println("WiFi not connected!"); 203 | delay(100); 204 | } 205 | 206 | Serial.println(""); 207 | Serial.println("WiFi connected"); 208 | Serial.println("IP address: "); 209 | Serial.println(WiFi.localIP()); 210 | 211 | // server address, port and URL 212 | webSocket.begin("yapraiwallet.space", 80, "/call"); 213 | 214 | // event handler 215 | webSocket.onEvent(webSocketEvent); 216 | 217 | // try ever 5000 again if connection has failed 218 | webSocket.setReconnectInterval(5000); 219 | 220 | u8g2.clearBuffer(); 221 | u8g2.setFont(u8g2_font_helvB10_tf); 222 | u8g2.drawStr(0, 20, "0.1Nano = 10mins"); 223 | u8g2.drawStr(0, 40, "0.2Nano = 20mins"); 224 | u8g2.drawStr(0, 60, "0.5Nano = 60mins"); 225 | u8g2.sendBuffer(); 226 | 227 | } 228 | 229 | void loop() { 230 | webSocket.loop(); 231 | 232 | } 233 | -------------------------------------------------------------------------------- /doc/osx_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamescoxon/Nano_Callback_System/b59fd7ba919d3bca0f746c28a5e8c04e83464a9a/doc/osx_screenshot.png -------------------------------------------------------------------------------- /doc/usbchargerscreen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamescoxon/Nano_Callback_System/b59fd7ba919d3bca0f746c28a5e8c04e83464a9a/doc/usbchargerscreen1.png -------------------------------------------------------------------------------- /osx_client/osx_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import websockets 3 | from sys import argv, exit 4 | import json, pync, time 5 | from decimal import Decimal 6 | 7 | def get_details(block): 8 | try: 9 | amount_raw = block["amount"] 10 | # block contents need another json format 11 | contents = json.loads(block["block"]) 12 | destination = contents["link_as_account"] 13 | return amount_raw, destination 14 | except Exception as e: 15 | print(f"Failed to get block info: {e}") 16 | return None, None 17 | 18 | def convert_amount(raw): 19 | return str(float(raw)/1e30) 20 | 21 | async def test(server, api_key, account): 22 | 23 | async with websockets.connect(server) as websocket: 24 | 25 | await websocket.send(json.dumps({"address": account, "api_key": api_key})) 26 | 27 | while True: 28 | response = await websocket.recv() 29 | block = json.loads(response) 30 | print("Block received! Printing contents after JSON format...\n\n{}\n\n".format(block)) 31 | 32 | amount_raw, destination = get_details(block) 33 | if amount_raw is None or destination is None: 34 | continue 35 | 36 | amount_NANO = convert_amount(amount_raw) 37 | print("{}: {:.3} NANO received on {}".format(time.strftime("%d/%m/%Y %H:%M:%S"), Decimal(amount_NANO), destination)) 38 | pync.notify("{:.3} NANO received on {}".format(Decimal(amount_NANO), destination), title='Nano Notifier') 39 | 40 | def main(): 41 | 42 | argc = len(argv) 43 | if argc < 4: 44 | print("Usage: {} SERVER API_KEY ACCOUNT".format(argv[0])) 45 | exit(0) 46 | 47 | server = argv[1] 48 | key = argv[2] 49 | account = argv[3] 50 | 51 | asyncio.get_event_loop().run_until_complete(test(server, key, account)) 52 | 53 | if __name__ == "__main__": 54 | main() 55 | -------------------------------------------------------------------------------- /server/server.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2019 James Coxon 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import socket, time, json 24 | import tornado.gen 25 | import tornado.ioloop 26 | import tornado.iostream 27 | import tornado.tcpserver 28 | import tornado.web 29 | import tornado.websocket 30 | from collections import defaultdict 31 | 32 | client_addresses = defaultdict(list) 33 | client_accounts = defaultdict(list) 34 | 35 | class Data_Callback(tornado.web.RequestHandler): 36 | @tornado.gen.coroutine 37 | def post(self): 38 | receive_time = time.strftime("%d/%m/%Y %H:%M:%S") 39 | post_data = json.loads(self.request.body.decode('utf-8')) 40 | # print("{}: {}".format(receive_time, post_data)) 41 | block_data = json.loads(post_data['block']) 42 | if block_data['link_as_account'] in client_addresses: 43 | tracking_address = block_data['link_as_account'] 44 | clients = client_addresses[tracking_address] 45 | for client in clients: 46 | print("{}: {}".format(receive_time, client, post_data)) 47 | client.write_message(post_data) 48 | print("Sent data") 49 | 50 | class WSHandler(tornado.websocket.WebSocketHandler): 51 | def check_origin(self, origin): 52 | return True 53 | 54 | def open(self): 55 | print("WebSocket opened: {}".format(self)) 56 | 57 | @tornado.gen.coroutine 58 | def on_message(self, message): 59 | print('Message from client {}: {}'.format(self, message)) 60 | if message != "Connected": 61 | try: 62 | ws_data = json.loads(message) 63 | if 'address' not in ws_data: 64 | raise Exception('Incorrect data from client: {}'.format(ws_data)) 65 | print(ws_data['address']) 66 | client_addresses[ws_data['address']].append(self) 67 | print(client_addresses) 68 | client_accounts[self].append(ws_data['address']) 69 | 70 | except Exception as e: 71 | print("Error {}".format(e)) 72 | 73 | def on_close(self): 74 | print('Client disconnected - {}'.format(self)) 75 | accounts = client_accounts[self] 76 | for account in accounts: 77 | client_addresses[account].remove(self) 78 | if len(client_addresses[account]) == 0: 79 | del client_addresses[account] 80 | del client_accounts[self] 81 | print(client_addresses) 82 | print(client_accounts) 83 | 84 | application = tornado.web.Application([ 85 | (r"/callback/", Data_Callback), 86 | (r"/call", WSHandler), 87 | ]) 88 | 89 | 90 | def main(): 91 | 92 | # websocket server 93 | myIP = socket.gethostbyname(socket.gethostname()) 94 | print('*** Websocket Server Started at %s***' % myIP) 95 | 96 | # callback server 97 | application.listen(7090) 98 | 99 | # infinite loop 100 | tornado.ioloop.IOLoop.instance().start() 101 | 102 | if __name__ == "__main__": 103 | main() 104 | -------------------------------------------------------------------------------- /windows_linux_client/nano.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamescoxon/Nano_Callback_System/b59fd7ba919d3bca0f746c28a5e8c04e83464a9a/windows_linux_client/nano.ico -------------------------------------------------------------------------------- /windows_linux_client/nano_iot_client.py: -------------------------------------------------------------------------------- 1 | # python 3.6, windows 10 for toast notification 2 | 3 | from sys import argv, exit 4 | import json 5 | import websocket 6 | import time 7 | try: 8 | from win10toast import ToastNotifier 9 | toaster = ToastNotifier() 10 | except ImportError: 11 | print("If on Windows 10, pip install win10toast for toast notifications") 12 | toaster = None 13 | 14 | # examples for arguments: 15 | # SERVER = 'ws://yapraiwallet.space/callsocket/' 16 | # API_KEY = '123456' 17 | # ACCOUNT = 'xrb_1dgt79j1bwjbhf9raywnwq51nut988c99dj7suf56yfpgxayaj9efw1hjdkb' 18 | 19 | def get_socket(server): 20 | try: 21 | ws = websocket.create_connection(server) 22 | ws.settimeout(2.0) 23 | print(f"Connected to {server}") 24 | return ws 25 | except Exception as e: 26 | print(f"Unable to connect to {server}: {e}") 27 | return None 28 | 29 | 30 | def setup(ws, account, api_key): 31 | try: 32 | ws.send(json.dumps({"address": account, "api_key": api_key})) 33 | return True 34 | except Exception as e: 35 | print("Unable to send account") 36 | return False 37 | 38 | 39 | def get_details(block): 40 | try: 41 | amount_raw = block["amount"] 42 | # block contents need another json format 43 | contents = json.loads(block["block"]) 44 | destination = contents["link_as_account"] 45 | return amount_raw, destination 46 | except Exception as e: 47 | print(f"Failed to get block info: {e}") 48 | return None, None 49 | 50 | 51 | def convert_amount(raw): 52 | return str(float(raw)/1e30) 53 | 54 | 55 | def main(): 56 | 57 | argc = len(argv) 58 | if argc < 4: 59 | print(f"Usage: {argv[0]} SERVER API_KEY ACCOUNT") 60 | exit(0) 61 | 62 | server = argv[1] 63 | key = argv[2] 64 | account = argv[3] 65 | 66 | ws = get_socket(server) 67 | if not ws: exit(1) 68 | if not setup(ws, account, key): exit(1) 69 | time_last_msg = time.time() 70 | 71 | while 1: 72 | try: 73 | rec = ws.recv() 74 | time_last_msg = time.time() 75 | block = json.loads(rec) 76 | print(f"Block received! Printing contents after JSON format...\n\n{block}\n\n") 77 | 78 | amount_raw, destination = get_details(block) 79 | if amount_raw is None or destination is None: 80 | continue 81 | 82 | amount_NANO = convert_amount(amount_raw) 83 | 84 | if toaster: 85 | toaster.show_toast( 86 | "Received some Nano!", f"{amount_NANO} NANO received on {destination}", 87 | icon_path="nano.ico", 88 | duration=5 89 | ) 90 | else: 91 | print(f"{amount_NANO} NANO received on {destination}") 92 | continue 93 | 94 | except websocket.WebSocketTimeoutException as e: 95 | # timeout 96 | # prevent shadow disconnects by reconnecting every now and then 97 | if time.time() - time_last_msg > 300: 98 | ws.close() 99 | ws = None 100 | try: 101 | while ws is None: 102 | ws = get_socket(server) 103 | if not ws: 104 | time.sleep(2) 105 | print("Reconnecting...") 106 | if not setup(ws, account, key): exit(1) 107 | except KeyboardInterrupt: 108 | print("\nCtrl-C detected, closing\n") 109 | break 110 | continue 111 | except KeyboardInterrupt: 112 | print("\nCtrl-C detected, closing\n") 113 | break 114 | except Exception as e: 115 | print(f"Unexpected error, reconnecting...: {e}") 116 | raise 117 | ws.close() 118 | ws = None 119 | try: 120 | while ws is None: 121 | ws = get_socket(server) 122 | if not ws: 123 | time.sleep(2) 124 | print("Reconnecting...") 125 | if not setup(ws, account, key): exit(1) 126 | except KeyboardInterrupt: 127 | print("\nCtrl-C detected, closing\n") 128 | break 129 | continue 130 | 131 | 132 | if __name__ == "__main__": 133 | main() --------------------------------------------------------------------------------