├── README.md ├── Raspberry-LED-display ├── hello-solana.js ├── package-lock.json ├── package.json ├── readme.md └── solana-slot.js ├── helium-lorawan-chest ├── .gitignore ├── .prettierignore ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── app │ ├── .env │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── LorBisZjmXHAdUnAWKfBiVh84yaxGVF2WY6kjr7AQu5.json │ ├── README.md │ ├── app │ │ ├── globals.css │ │ ├── head.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ └── api │ │ │ ├── sensor-downlink.ts │ │ │ └── transaction.ts │ ├── postcss.config.js │ ├── public │ │ ├── bg.jpg │ │ ├── favicon.ico │ │ ├── icon.png │ │ ├── next.svg │ │ ├── thirteen.svg │ │ └── vercel.svg │ ├── src │ │ ├── Wallet.tsx │ │ ├── components │ │ │ └── PayQR.tsx │ │ └── util │ │ │ ├── const.ts │ │ │ └── lorawan_chest.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── package.json ├── programs │ └── lorawan-chest │ │ ├── Cargo.toml │ │ ├── Xargo.toml │ │ └── src │ │ └── lib.rs ├── raspberry │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── chest.ts │ │ └── lorawan_chest.ts │ └── tsconfig.json ├── tests │ └── LorawanChest.ts ├── tsconfig.json ├── yarn-error.log └── yarn.lock ├── led-switch ├── .gitignore ├── .prettierignore ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── app │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── README.md │ ├── app │ │ ├── globals.css │ │ ├── head.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ └── api │ │ │ └── transaction.ts │ ├── postcss.config.js │ ├── public │ │ ├── bg.jpg │ │ ├── favicon.ico │ │ ├── icon.png │ │ ├── next.svg │ │ ├── thirteen.svg │ │ └── vercel.svg │ ├── src │ │ ├── Wallet.tsx │ │ ├── components │ │ │ └── PayQR.tsx │ │ └── util │ │ │ ├── const.ts │ │ │ └── led_switch.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── yarn.lock ├── package.json ├── programs │ └── led-switch │ │ ├── Cargo.toml │ │ ├── Xargo.toml │ │ └── src │ │ └── lib.rs ├── raspberry │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── led.ts │ │ └── led_switch.json │ └── tsconfig.json ├── tests │ └── led-switch.ts ├── tsconfig.json └── yarn.lock ├── solana-bar ├── .gitignore ├── .prettierignore ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── app │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── README.md │ ├── app │ │ ├── globals.css │ │ ├── head.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ └── api │ │ │ └── transaction.ts │ ├── postcss.config.js │ ├── public │ │ ├── bg.jpg │ │ ├── favicon.ico │ │ ├── icon.png │ │ ├── next.svg │ │ ├── thirteen.svg │ │ └── vercel.svg │ ├── src │ │ ├── Wallet.tsx │ │ ├── components │ │ │ ├── PayQR.tsx │ │ │ └── Receipts.tsx │ │ └── util │ │ │ ├── const.ts │ │ │ └── solana_bar.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── package.json ├── programs │ └── solana-bar │ │ ├── Cargo.toml │ │ ├── Xargo.toml │ │ └── src │ │ └── lib.rs ├── raspberry │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── bar.ts │ │ ├── shoUzmg5H2zDdevxS6UdQCiY6JnP1qSn7fPtCP726pR.json │ │ └── solana_bar.ts │ └── tsconfig.json ├── tests │ ├── solana_bar.ts │ └── solana_bar_test.ts ├── tsconfig.json └── yarn.lock └── termina-data-anchor ├── .gitignore ├── README.md ├── rewards.json ├── upload_blob.sh └── verify_blob.sh /README.md: -------------------------------------------------------------------------------- 1 | # Solana DePin & IOT Examples 2 | 3 | This repository contains examples of how to use Solana to talk to hardware. 4 | DePin stands for decentralized physical infrastructure and is a term which describes the use of blockchain technology to manage physical infrastructure. 5 | 6 | ## Examples 7 | 8 | ### [Data Anchor by Termina (Blob Upload + Verification)](./termina-data-anchor/README.md) 9 | 10 | This example shows how DePIN teams can anchor large batches of device data (e.g., sensor readings and reward summaries) directly on Solana using Termina’s Data Anchor. 11 | It demonstrates a full flow using the CLI from uploading a structured JSON blob, to fetching it from chain, and verifying its contents using a local script. 12 | [Twitter Video](https://x.com/Terminaxyz/status/1909263420278394899) 13 | 14 | ### [Solana-bar V2](https://github.com/Woody4618/bar) 15 | 16 | This example shows how you can sell drinks to anyone anywhere using Solana Pay Qr codes direclty from a bottle. 17 | It uses the web app letmebuy.app, an anchor program and a raspberry pi with a connected pump. 18 | The examples comes with complete source code and tutorial on how to build it yourself for under 100 dollar. 19 | [Twitter Video](https://x.com/zCase_/status/1922321757693784198) 20 | 21 | ### [Unruggable ESP32 Solana Hardware Signer](https://github.com/hogyzen12/unruggable-rust-esp32) 22 | 23 | This example demonstrates how to build a low-cost hardware wallet using an ESP32 microcontroller that can securely sign Solana transactions. The ESP32 generates and stores a private key, requiring physical button confirmation for transaction signing. The project includes firmware for the ESP32 (written in Rust) and companion clis in Rust, Go for creating and submitting transactions. 24 | [Twitter Video](https://x.com/bill_papas_12/status/1903308186498596979) 25 | 26 | ### [Write Sensor Data on chain](https://x.com/priyansh_ptl18/status/1903940356070424825) 27 | 28 | This example shows how to write sensor data to the chain using the Solana Pay transaction request. It reads the sensor data and saves it into an anchor program: 29 | https://github.com/priyanshpatel18/aeroscan 30 | 31 | https://github.com/priyanshpatel18/aeroscan-esp32 32 | 33 | https://github.com/priyanshpatel18/aeroscan-ws 34 | 35 | ### [Helium-Lorawan-Sensor-Chest](./helium-lorawan-chest/README.md) 36 | 37 | This example shows how to use the helium network, which is powered by Solana, to create a chest which is only lootable via Solana Pay Transaction requests when the chest is physically open. 38 | It comes with a whole walkthrough of how to setup the sensor, create the api and the solana pay transaction requests. 39 | [Twitter Video](https://x.com/solana_devs/status/1707043184373637411) 40 | 41 | 42 | # IOT Examples 43 | 44 | ### [Rust based esp32 Solana tracker with display](https://github.com/Mantistc/esp32-ssd1306-solana) 45 | 46 | Show Solana real-time data in a little ssd1306 display using the microcontroller esp32 to manage wifi, http request and more. 47 | [Twitter Video](https://x.com/lich01_/status/1899208452167102621) 48 | 49 | ### [Solana slot LED-Display](https://github.com/solana-developers/solana-depin-examples/tree/main/Raspberry-LED-display) 50 | 51 | This example shows use an LED Display to show the current Solana slot using a Raspberry Pi and a I2C - SSD1306. 52 | 53 | ### [Led-Switch](./led-switch/README.md) 54 | 55 | This example shows how to use Solana Pay transaction requests to control a LED connected to a Raspberry Pi. 56 | It comes with a full walkthrough from start to finish. Setting up the Raspberry Pi, deploying a Solana program, creating the QR codes to change the account and finally running the client code to control the LED. 57 | [Twitter Video](https://twitter.com/solana_devs/status/1691563319457403301) 58 | 59 | ### [Solana-bar V1](./solana-bar/README.md) 60 | 61 | This example shows how to use Solana Pay transaction requests to sell liquids decentralized using a 5V pump. 62 | It comes with a full walkthrough from start to finish. Hardware requirements, how to attach the cables and the full source code. 63 | Take the liquid dispenser to the beach or a party and start selling holy water to your friends. 64 | [Twitter Video](https://twitter.com/solana_devs/status/1697023233789145421) 65 | 66 | 67 | -------------------------------------------------------------------------------- /Raspberry-LED-display/hello-solana.js: -------------------------------------------------------------------------------- 1 | const i2c = require("i2c-bus"); 2 | const Oled = require("oled-i2c-bus"); 3 | const font = require("oled-font-5x7"); 4 | 5 | const opts = { 6 | width: 128, 7 | height: 32, 8 | address: 0x3c, 9 | }; 10 | 11 | const i2cBus = i2c.openSync(1); 12 | const oled = new Oled(i2cBus, opts); 13 | 14 | // Clear and activate display 15 | oled.clearDisplay(); 16 | oled.turnOnDisplay(); 17 | 18 | // Animation variables 19 | let rotationAngle = 0; 20 | let textY = opts.height; 21 | let textDirection = -1; 22 | 23 | function drawSolLogo(x, y, size, angle) { 24 | const points = []; 25 | // Generate hexagon points 26 | for (let i = 0; i < 6; i++) { 27 | const pointAngle = angle + i * 60; 28 | const px = x + size * Math.cos((pointAngle * Math.PI) / 180); 29 | const py = y + size * Math.sin((pointAngle * Math.PI) / 180); 30 | points.push([Math.round(px), Math.round(py)]); 31 | } 32 | 33 | // Draw hexagon 34 | for (let i = 0; i < points.length; i++) { 35 | const start = points[i]; 36 | const end = points[(i + 1) % points.length]; 37 | oled.drawLine(start[0], start[1], end[0], end[1], 1); 38 | } 39 | 40 | // Draw center dot 41 | oled.drawPixel([ 42 | [x, y], 43 | [x + 1, y], 44 | [x - 1, y], 45 | [x, y + 1], 46 | [x, y - 1], 47 | ]); 48 | } 49 | 50 | function drawCirclePattern(centerX, centerY) { 51 | // Draw left circle 52 | for (let i = 0; i < 360; i += 45) { 53 | const x1 = Math.round(centerX - 50 + 3 * Math.cos((i * Math.PI) / 180)); 54 | const y1 = Math.round(centerY + 3 * Math.sin((i * Math.PI) / 180)); 55 | const x2 = Math.round( 56 | centerX - 50 + 3 * Math.cos(((i + 45) * Math.PI) / 180) 57 | ); 58 | const y2 = Math.round(centerY + 3 * Math.sin(((i + 45) * Math.PI) / 180)); 59 | oled.drawLine(x1, y1, x2, y2, 1); 60 | } 61 | 62 | // Draw right circle 63 | for (let i = 0; i < 360; i += 45) { 64 | const x1 = Math.round(centerX + 50 + 3 * Math.cos((i * Math.PI) / 180)); 65 | const y1 = Math.round(centerY + 3 * Math.sin((i * Math.PI) / 180)); 66 | const x2 = Math.round( 67 | centerX + 50 + 3 * Math.cos(((i + 45) * Math.PI) / 180) 68 | ); 69 | const y2 = Math.round(centerY + 3 * Math.sin(((i + 45) * Math.PI) / 180)); 70 | oled.drawLine(x1, y1, x2, y2, 1); 71 | } 72 | } 73 | 74 | function animate() { 75 | // Clear display 76 | oled.clearDisplay(); 77 | 78 | // Draw Solana logos on sides 79 | drawSolLogo(15, opts.height / 2, 6, rotationAngle); 80 | drawSolLogo(opts.width - 15, opts.height / 2, 6, -rotationAngle); 81 | 82 | // Draw circle pattern in center 83 | drawCirclePattern(opts.width / 2, opts.height / 2); 84 | 85 | // Draw text 86 | const text = "Hello Solana!"; 87 | const textX = 27; 88 | oled.setCursor(textX, textY); 89 | oled.writeString(font, 1, text, 1, true); 90 | 91 | // Update animation variables 92 | textY += textDirection * 2; 93 | 94 | if (textY <= 0) { 95 | textDirection = 1; 96 | } else if (textY >= opts.height) { 97 | textDirection = -1; 98 | } 99 | 100 | rotationAngle = (rotationAngle + 6) % 360; 101 | } 102 | 103 | // Handle cleanup on exit 104 | process.on("SIGINT", () => { 105 | oled.clearDisplay(); 106 | oled.turnOffDisplay(); 107 | process.exit(); 108 | }); 109 | 110 | // Start animation loop with faster interval 111 | setInterval(animate, 30); 112 | -------------------------------------------------------------------------------- /Raspberry-LED-display/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "led-display", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@solana/web3.js": "^1.98.0", 14 | "i2c-bus": "^5.2.3", 15 | "oled-font-5x7": "^1.0.3", 16 | "oled-i2c-bus": "^1.0.12" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Raspberry-LED-display/readme.md: -------------------------------------------------------------------------------- 1 | # Show current Solana Slot on OLED Display using Raspberry Pi 2 | 3 | This example shows how you can use a Raspberry Pi to display the current Solana slot on an OLED display. 4 | 5 | ## Bill of Materials 6 | 7 | - Raspberry Pi 5, 4 or zero 8 | - OLED Display I2C - SSD1306 (For example: https://www.amazon.de/dp/B01L9GC470) 9 | 10 | ## Connect the OLED Display 11 | 12 | ![RaspberryLEDDisplay](https://github.com/user-attachments/assets/863cb8b0-1ff3-411f-aa24-50d1dc607819) 13 | 14 | 15 | First you need to enable I2C on the Raspberry Pi: 16 | 17 | ```bash 18 | ssh yourUsername@raspberrypi.local 19 | sudo raspi-config 20 | ``` 21 | 22 | Select `5 Interfacing Options` and then `I2C` and then `Yes` to enable it. 23 | 24 | Connect the OLED Display to the Raspberry Pi using the following pins: 25 | 26 | - VCC to 3.3V (pin 1) 27 | - GND to GND (any ground pin will work. For example pin6 or pin9) 28 | - SDA to GPIO 2 (pin 3) 29 | - SCL to GPIO 3 (pin 5) 30 | 31 | Make sure it works using: 32 | 33 | ```bash 34 | sudo i2cdetect -y 1 35 | ``` 36 | 37 | That should show the address `0x3c` for the OLED Display somewhere in the list. 38 | 39 | ## Run the code 40 | 41 | Copy the files to your Raspberry Pi. 42 | Easiest is to use Cursor or VSCode with the Remote - SSH extension. 43 | See the [LED example](https://github.com/solana-developers/solana-depin-examples/tree/main/led-switch) for more details on the raspberry pi setup and a Video Walkthrough. 44 | 45 | Or use spc for copying the files (running on your machine): 46 | 47 | ```bash 48 | scp hello-solana.js solana-slot.js package.json package-lock.json jonas@raspberrypi.local:~/Documents/ 49 | ``` 50 | 51 | [Install node](https://github.com/solana-developers/solana-depin-examples/tree/main/led-switch#install-node-on-the-raspberry-pi) 52 | 53 | Running on the Raspberry Pi: 54 | 55 | ```bash 56 | npm install oled-i2c-bus i2c-bus oled-font-5x7 @solana/web3.js 57 | ``` 58 | 59 | ```bash 60 | sudo node solana-slot.js 61 | ``` 62 | 63 | This should not turn on the display and show the current slot. 64 | 65 | Congratulations! You have now a Solana Slot Display! 66 | Since this is just JS code you can also use it in all the other examples to show additional info. For example the price for a drink in the bar example or the progress bar for how long the Fan of the air blower will still run. 67 | Just experiment and have fun! 68 | -------------------------------------------------------------------------------- /Raspberry-LED-display/solana-slot.js: -------------------------------------------------------------------------------- 1 | const i2c = require("i2c-bus"); 2 | const Oled = require("oled-i2c-bus"); 3 | const font = require("oled-font-5x7"); 4 | const { Connection } = require("@solana/web3.js"); 5 | 6 | const opts = { 7 | width: 128, 8 | height: 32, 9 | address: 0x3c, 10 | }; 11 | 12 | const i2cBus = i2c.openSync(1); 13 | const oled = new Oled(i2cBus, opts); 14 | 15 | // Clear and activate display 16 | oled.clearDisplay(); 17 | oled.turnOnDisplay(); 18 | 19 | // Your websocket connection may not be working with the public end points. 20 | // Thats why there is some additional polling in the code in animate() that you can remove later 21 | // if you have a payed RPC URL. 22 | // Initialize Solana connection 23 | const connection = new Connection( 24 | "https://api.mainnet-beta.solana.com", 25 | "processed" 26 | ); 27 | // Or use devnet: 28 | // const connection = new Connection('https://api.devnet.solana.com', 'processed'); 29 | let currentSlot = 0; 30 | 31 | function drawSolanaLogo(x, y, size) { 32 | // Draw three horizontal lines with 2px thickness 33 | 34 | // Top line 35 | oled.drawLine( 36 | 1 + x - size / 2, 37 | y - size / 2, 38 | 1 + x + size / 2, 39 | y - size / 2, 40 | 1 41 | ); 42 | oled.drawLine( 43 | x - size / 2, 44 | y - size / 2 + 1, 45 | x + size / 2, 46 | y - size / 2 + 1, 47 | 1 48 | ); 49 | 50 | // Middle line 51 | oled.drawLine(x - size / 2, y, x + size / 2, y, 1); 52 | oled.drawLine(1 + x - size / 2, y + 1, 1 + x + size / 2, y + 1, 1); 53 | 54 | // Bottom line 55 | oled.drawLine( 56 | 1 + x - size / 2, 57 | y + size / 2, 58 | 1 + x + size / 2, 59 | y + size / 2, 60 | 1 61 | ); 62 | oled.drawLine( 63 | x - size / 2, 64 | y + size / 2 + 1, 65 | x + size / 2, 66 | y + size / 2 + 1, 67 | 1 68 | ); 69 | } 70 | 71 | function animate() { 72 | // This is not necessary if your websocket connection is working. 73 | // Just with the public end points its not always reliable so i added this polling here as well. 74 | // Feel free to remove if you have a proper RPC url. 75 | getSlot(); 76 | 77 | // Clear display 78 | oled.clearDisplay(); 79 | 80 | // Draw Solana logos on sides 81 | drawSolanaLogo(15, opts.height / 2, 6); // Left logo 82 | drawSolanaLogo(opts.width - 15, opts.height / 2, 6); // Right logo 83 | 84 | // Draw "SLOT" text 85 | const labelX = (opts.width - 30) / 2; 86 | oled.setCursor(labelX, 5); 87 | oled.writeString(font, 1, "SLOT", 1, true); 88 | 89 | // Draw slot number below 90 | const slotText = `${currentSlot}`; 91 | const slotX = (opts.width - slotText.length * 6) / 2; 92 | oled.setCursor(slotX, 20); 93 | oled.writeString(font, 1, slotText, 1, true); 94 | } 95 | 96 | // Subscribe to slot updates 97 | const slotSubscription = connection.onSlotChange((slotInfo) => { 98 | currentSlot = slotInfo.slot; 99 | console.log("New slot:", currentSlot); 100 | 101 | animate(); 102 | 103 | // Quick flash effect for new slot 104 | // oled.invertDisplay(true); 105 | // setTimeout(() => { 106 | // oled.invertDisplay(false); 107 | // }, 100); 108 | }); 109 | 110 | // Handle cleanup on exit 111 | process.on("SIGINT", () => { 112 | if (slotSubscription) { 113 | connection.removeSlotChangeListener(slotSubscription); 114 | } 115 | oled.clearDisplay(); 116 | oled.turnOffDisplay(); 117 | process.exit(); 118 | }); 119 | 120 | // Start animation loop. Can add some nice animations here. 121 | setInterval(animate, 20); 122 | 123 | function getSlot() { 124 | // Get initial slot 125 | connection.getSlot().then((slot) => { 126 | currentSlot = slot; 127 | console.log("Initial slot:", currentSlot); 128 | }); 129 | } 130 | 131 | getSlot(); 132 | -------------------------------------------------------------------------------- /helium-lorawan-chest/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | node_modules 7 | test-ledger 8 | .yarn 9 | -------------------------------------------------------------------------------- /helium-lorawan-chest/.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | node_modules 6 | dist 7 | build 8 | test-ledger 9 | -------------------------------------------------------------------------------- /helium-lorawan-chest/Anchor.toml: -------------------------------------------------------------------------------- 1 | [features] 2 | seeds = false 3 | skip-lint = false 4 | [programs.localnet] 5 | lorawan_chest = "2UYaB7aU7ZPA5LEQh3ZtWzC1MqgLkEJ3nBKebGUrFU3f" 6 | 7 | [registry] 8 | url = "https://api.apr.dev" 9 | 10 | [provider] 11 | cluster = "devnet" 12 | wallet = "~/.config/solana/id.json" 13 | 14 | [scripts] 15 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" 16 | -------------------------------------------------------------------------------- /helium-lorawan-chest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*" 4 | ] 5 | 6 | [profile.release] 7 | overflow-checks = true 8 | lto = "fat" 9 | codegen-units = 1 10 | [profile.release.build-override] 11 | opt-level = 3 12 | incremental = false 13 | codegen-units = 1 14 | -------------------------------------------------------------------------------- /helium-lorawan-chest/README.md: -------------------------------------------------------------------------------- 1 | # Helium Lorawan door sensor Chest 2 | 3 | This is an example of a chest with qr code which only works when the chest is open. 4 | It uses a Helium Lorawan door sensor which writes its state in the state of an anchor program. 5 | It can also be extended by connecting it to a raspberry pi and a LED. 6 | 7 | image 8 | 9 | 10 | This project consists of three parts: 11 | - The Helium Lorawan door sensor which sends the state of the chest to the Helium console. 12 | - An API which listens to the door sensor and updates the state of the anchor program. 13 | - The Solana Pay Transaction request api which creates a QR code which can be scanned by any mobile wallet that supports Solana Pay to loot the chest. 14 | - Optional: A Raspberry Pi with a LED which listens to the state of the anchor program and turns an LED on or off. 15 | 16 | ## Hardware Required 17 | 18 | A Lorawan magnetic Door sensor LDS02 (There are many resellers, just google it)): 19 | https://www.reichelt.de/lorawan-tuer-und-fenstersensor-dra-lds02-p311270.html 20 | 21 | If you want to attach a raspberry pi to the chest please follow the led-switch example: 22 | [LED-SWITCH-EXAMPLE 23 | ](https://github.com/solana-developers/solana-depin-examples/blob/main/led-switch/README.md) 24 | 25 | ## Setup Helium Lorawan door sensor 26 | 27 | Add betteries into your sensor. I little green LED will indicate that its trying to connect to the helium network. 28 | 29 | When you order a lorawan sensor it comes with a little paper with 3 ids on it. 30 | Device EUI, App EUI and App Key. 31 | 32 | These you need to add into the Helium console: 33 | [Devices](https://console.helium.com/devices) 34 | 35 | image 36 | 37 | Then you need to find the decoder for your sensor on its user manual. 38 | In our case you will find it here under uplink: 39 | [Sensor Data Sheet](http://wiki.dragino.com/xwiki/bin/view/Main/User%20Manual%20for%20LoRaWAN%20End%20Nodes/LDS02%20-%20LoRaWAN%20Door%20Sensor%20User%20Manual/#H4.3UplinkPayload) 40 | 41 | Which will lead you to: 42 | https://github.com/dragino/dragino-end-node-decoder/tree/main/LDS02 43 | 44 | There you can download the ttn (The Things Network) decoder. I used version 1.5 which is compatible. 45 | Then you can add the decoder to the Helium console under functions. 46 | 47 | image 48 | 49 | 50 | Last thing we need to do is to create an HTTP integration so that the Helium console sends the data to our API. 51 | For that you pick http integration and add the url of your api. The API link we will create in the next step. You can change it later. 52 | 53 | image 54 | 55 | 56 | Then under flows you add the device, then plug it into the decoder and then plug it into the http integration. Like that the api will be called every time the sensor sends data and the data will already arrive decoded. 57 | 58 | image 59 | 60 | Start your API using 61 | 62 | ```bash 63 | cd app 64 | yarn dev 65 | ``` 66 | 67 | Use ngrok to make your API publically available or deploy to vercel: 68 | https://ngrok.com/product 69 | 70 | ```bash 71 | ngrok http 3000 72 | ``` 73 | 74 | https://vercel.com/ 75 | 76 | Copy the url to your api into the http integration in the helium console. 77 | 78 | If you did the integration correctly you should be seeing an out put similar to this in your api: 79 | So you have all the information about the sensor. You can also for example use the door open duration or the amount of door open to add more logic to your chest. 80 | 81 | ```bash 82 | Door is open: { 83 | app_eui: '...', 84 | dc: { balance: 189, nonce: 1 }, 85 | decoded: { 86 | payload: { 87 | ALARM: 0, 88 | BAT_V: 3.174, 89 | DOOR_OPEN_STATUS: 0, 90 | DOOR_OPEN_TIMES: 28, 91 | LAST_DOOR_OPEN_DURATION: 0, 92 | MOD: 1 93 | }, 94 | status: 'success' 95 | }, 96 | dev_eui: '...', 97 | devaddr: '03000048', 98 | downlink_url: 'https://console.helium.com/api/v1/down/....', 99 | fcnt: 53, 100 | hotspots: [ 101 | { 102 | channel: 5, 103 | frequency: 868.1, 104 | hold_time: 0, 105 | id: '???????', 106 | lat: xxx, 107 | long: yyy, 108 | name: '????-????-????', 109 | reported_at: 1695491235713, 110 | rssi: -131, 111 | snr: -13.5, 112 | spreading: 'SF12BW125', 113 | status: 'success' 114 | } 115 | ], 116 | id: 'ID...', 117 | metadata: { 118 | adr_allowed: false, 119 | cf_list_enabled: false, 120 | multi_buy: 1, 121 | organization_id: 'ID...' 122 | preferred_hotspots: [], 123 | rx_delay: 1, 124 | rx_delay_actual: 1, 125 | rx_delay_state: 'rx_delay_established' 126 | }, 127 | name: 'Door Sensor', 128 | payload: 'DGYBAAAcAAAAAA==', 129 | payload_size: 10, 130 | port: 10, 131 | raw_packet: 'QAMAAEiANQAK5yx+wU+dOzSfMkpVL9c=', 132 | replay: false, 133 | reported_at: 1695491235713, 134 | type: 'uplink', 135 | uuid: 'id...' 136 | } 137 | ``` 138 | 139 | ## The sensor API 140 | 141 | The API changing the state in the program you can find here: 142 | [Downling api](https://github.com/solana-developers/solana-depin-examples/blob/main/helium-lorawan-chest/app/pages/api/sensor-downlink.ts) 143 | 144 | We use a local keypair which writes the current state of sensor in the state of the lorawan_chest anchor program. 145 | The program will only allow changs coming from this key pair. So if you want to chagne the key pair you may need to adjust also the admin key in the anchor program. 146 | 147 | ```js 148 | const post = async (req: NextApiRequest, res: NextApiResponse) => { 149 | 150 | console.log("Door is open:", req.body); 151 | console.log("Door is open:", req.body.decoded.payload.DOOR_OPEN_STATUS); 152 | 153 | const burner = JSON.parse(process.env.BURNER_KEY ?? "") as number[] 154 | const burnerKeypair = Keypair.fromSecretKey(Uint8Array.from(burner)) 155 | const sender = burnerKeypair.publicKey; 156 | 157 | const transaction = new Transaction(); 158 | const latestBlockhash = await CONNECTION.getLatestBlockhash(); 159 | transaction.feePayer = sender; 160 | transaction.recentBlockhash = latestBlockhash.blockhash; 161 | 162 | let message = ''; 163 | 164 | if (req.body.decoded.payload.DOOR_OPEN_STATUS == "1") { 165 | let ix = await LORAWAN_CHEST_PROGRAM.methods.switch(true).accounts( 166 | { 167 | lorawanChest: LORAWAN_CHEST_PDA, 168 | authority: sender 169 | }, 170 | ).instruction(); 171 | 172 | transaction.add(ix); 173 | 174 | message = 'Door open !'; 175 | } else { 176 | let ix = await LORAWAN_CHEST_PROGRAM.methods.switch(false).accounts( 177 | { 178 | lorawanChest: LORAWAN_CHEST_PDA, 179 | authority: sender 180 | }, 181 | ).instruction(); 182 | 183 | transaction.add(ix); 184 | message = 'Door Closed'; 185 | } 186 | 187 | var signature = await CONNECTION.sendTransaction(transaction, [burnerKeypair]); 188 | console.log("Transaction signature:", signature); 189 | 190 | res.status(200).send({ message }); 191 | }; 192 | ``` 193 | 194 | 195 | ## The program 196 | 197 | The program is a small anchor program which has a boolean which indicated if the chest is currently open or close. 198 | Note that the program is not yet connected to the LED. We will do that later. 199 | Also the seed for the chest account is just a string, so everyone can call this function and switch it on or off. You could add a public key or any other string here to have only certain wallets able to open the chest. 200 | 201 | ```rust 202 | use anchor_lang::prelude::*; 203 | use solana_program::pubkey; 204 | 205 | declare_id!("2UYaB7aU7ZPA5LEQh3ZtWzC1MqgLkEJ3nBKebGUrFU3f"); 206 | 207 | // Change this to what ever key you use in your API to make sure not everyone can just call the switch function. 208 | const ADMIN_PUBKEY: Pubkey = pubkey!("LorBisZjmXHAdUnAWKfBiVh84yaxGVF2WY6kjr7AQu5"); 209 | 210 | #[error_code] 211 | pub enum LorawanChestError { 212 | ChestIsClosed = 100, 213 | } 214 | 215 | #[program] 216 | pub mod lorawan_chest { 217 | use super::*; 218 | 219 | pub fn initialize(ctx: Context) -> Result<()> { 220 | ctx.accounts.lorawan_chest.is_open = false; 221 | Ok(()) 222 | } 223 | 224 | pub fn switch(ctx: Context, is_on: bool) -> Result<()> { 225 | // Note that the account which will be able to change the state if this account is only the admin account. 226 | ctx.accounts.lorawan_chest.is_open = is_on; 227 | Ok(()) 228 | } 229 | 230 | pub fn loot(ctx: Context) -> Result<()> { 231 | if !ctx.accounts.lorawan_chest.is_open { 232 | return Err(LorawanChestError::ChestIsClosed.into()); 233 | } 234 | 235 | // You can add any kind of loot action here. 236 | // In the next js api we add a transfer, but you could also mint an NFT for example. 237 | // Or you could save per user here which chests he already collected and build some real live adventure game. 238 | msg!("Looted!"); 239 | 240 | Ok(()) 241 | } 242 | } 243 | 244 | #[derive(Accounts)] 245 | pub struct Initialize<'info> { 246 | #[account(init, payer = authority, space = 8 + 8, seeds = [b"lorawan_chest"], bump)] 247 | pub lorawan_chest: Account<'info, LorawanChest>, 248 | #[account(mut)] 249 | pub authority: Signer<'info>, 250 | pub rent: Sysvar<'info, Rent>, 251 | pub system_program: Program<'info, System>, 252 | } 253 | 254 | #[derive(Accounts)] 255 | pub struct Switch<'info> { 256 | #[account(mut, seeds = [b"lorawan_chest"], bump)] 257 | pub lorawan_chest: Account<'info, LorawanChest>, 258 | #[account(mut, address = ADMIN_PUBKEY)] // <- Note that here we check for the admin account. 259 | pub authority: Signer<'info>, 260 | } 261 | 262 | #[derive(Accounts)] 263 | pub struct Loot<'info> { 264 | #[account(mut, seeds = [b"lorawan_chest"], bump)] 265 | pub lorawan_chest: Account<'info, LorawanChest>, 266 | #[account(mut)] 267 | pub authority: Signer<'info>, 268 | } 269 | 270 | #[account] 271 | pub struct LorawanChest { 272 | pub is_open: bool, 273 | } 274 | 275 | ``` 276 | 277 | 278 | ## Create a Solana Pay Transaction Request 279 | 280 | Solana Pay is not only for payments, but can request any transaction to be signed. 281 | The transaction can be signed by any wallet that supports Solana Pay. Especially useful on mobile. 282 | It consists of two parts. The api which can be found in lorawan-chest/app/pages/api/transaction.ts and the creation of the QR code which can be found in lorawan-chest/app/app/page.tsx and the QrCode in lorawan-chest/app/app/components/qr-code.tsx. 283 | 284 | Basically what is happening is that the wallet sends a get request to our API to get a name and icon and then the transaction is created in the nextJS api and send to the wallet. The wallet then signs it. When the transaction is confirmed the chest is looted. We add two instructions. One is a transfer of sol and one is the loot instruction to our anchor program. The Loot instruction will only work if the chest is open. 285 | 286 | To run the solana pay transaction request app use: 287 | 288 | ```console 289 | cd app 290 | yarn install 291 | yarn dev 292 | ``` 293 | 294 | open http://localhost:3000 in your browser. 295 | Notice that the QR code is not working yet. We need to be able to access is from the distance. 296 | For that we use ngrok to create a tunnel to our local server. 297 | Make an account and install ngrok https://ngrok.com/ 298 | open a terminal and type: 299 | ngrok http 3000 300 | Then copy the url from the terminal and open it in the browser. 301 | Now the QR code should work and switch the LED on and off. 302 | 303 | Now you can also copy and print the QR codes and glue them somewhere into the chest for example. 304 | 305 | 306 | ## Deploy 307 | 308 | Since you don't want to run ngrok every time it makes sense to deploy the app. You can deploy it to vercel and update the link to your api in the Helium console. 309 | 310 | 311 | ## Where to go from here 312 | 313 | There are many many different sensors that you can use for your depin projects. 314 | Here is a list of ready to use sensors. There are temperature, humidity and many other sensors. 315 | Some ideas that you could do with them: 316 | - A chest that only opens when the temperature is below 0 degrees of when its raining. 317 | - You could use distance senors to figure out if there are free parking spots in a city and have an app which shows you where you can park. 318 | - You could use a light sensor to figure out if a room is occupied or not. 319 | - You use downlinks and solana pay QR codes to control a robot car or a drone. 320 | - You could build a live stream where people can control robots via qr codes and let them fight against each other. 321 | - You could build a smart package by building a tiny lorawan sensor and put it in reusable packages. Like that I would not need to wait at home anymore when i am about to recieve a package, but could instead just get a notification when the package is delivered and then go and pick it up. kastzentracker.eu is building tiny sensors to track cats for example. 322 | - You could build a warning system for nature catastrophes like floods or fires. 323 | - You could build a vending machine using solana pay qr codes 324 | 325 | 326 | ## Optional step: Show the status of the chest with an LED using a raspberry pi 327 | 328 | Optional to show the current status of the chest with an LED: 329 | A Raspberry Pi 4B with WiFi connection, a LED and a 220 ohm resistor. 330 | A 32 Gb mini sd card for the raspberry OS. 331 | 332 | For example: 333 | https://www.amazon.de/dp/B0C7KXMP7W 334 | https://www.amazon.de/dp/B07WYX8M76 335 | 336 | There maybe cheaper and better options. This is an example, any Raspberry 4b and any starter kit with a LED and a resistor will do. Probably a raspberry nano/pico or similar would also work. 337 | 338 | 339 | ### Setup Raspberry 340 | 341 | Insert the SD card into your computer. 342 | 343 | Install the Raspberry OS from here: https://www.raspberrypi.com/software/ 344 | 345 | Make sure to add the correct wifi information and the user and password and enable SSH. Otherwise you will need a monitor to connect to it later. 346 | 347 | Write the os onto the sd card with the Raspberry Pi image and then put the sd card into the raspberry pi and connect it to a power source. 348 | 349 | Bildschirmfoto 2023-08-14 um 15 10 09 350 | 351 | #### optional switch wifi access via ethernet cable 352 | 353 | If you did not setup the wifi password in the setup you can also connect the raspberry pi via lan cable to your router and then ssh into it. This is also helpful if you want to connect to it from a different network if you move somewhere else. 354 | - Connect your computer via lan cable to the raspberry pi (Probably need a connector from usb-c to lan) 355 | - ssh jonas@raspberrypi.local 356 | - sudo raspi-config and change under system -> wifi to the new wifi network by adding the SSID and the password 357 | 358 | ### Quick blinking test 359 | 360 | Connect pin 18 to one side of the LED with a 220 Ohm resistor and pin 16 to the other side like so: 361 | Bildschirmfoto 2023-08-14 um 15 10 09 362 | Bildschirmfoto 2023-08-14 um 15 10 09 363 | 364 | open terminal 365 | 366 | ```console 367 | ping raspberrypi.local -> Copy ip address 368 | ssh yourUserName@TheCopiedIpAddress (ssh jonas@192.168.1.183) 369 | 370 | cd Documents 371 | nano LED.py 372 | ``` 373 | 374 | Copy this in the File: 375 | 376 | ```python 377 | import RPi.GPIO as GPIO 378 | import time 379 | GPIO.setmode(GPIO.BCM) 380 | GPIO.setwarnings(False) 381 | GPIO.setup(18,GPIO.OUT) 382 | print ("LED on") 383 | GPIO.output(18,GPIO.HIGH) 384 | time.sleep(3) 385 | print ("LED off") 386 | GPIO.output(18,GPIO.LOW) 387 | ``` 388 | 389 | use ctrl +x to exit and y to save 390 | 391 | sudo python LED.py 392 | 393 | If everything is set up correctly the LED should blink for 3 seconds. 394 | 395 | 396 | ### Install Node on the Raspberry Pi: 397 | 398 | We want to use js so we can easily use the Solana web3 library. 399 | 400 | 1. Type the command: 401 | ```console 402 | sudo apt update 403 | ``` 404 | 405 | 2. Then, install Node.js with the command: 406 | ```console 407 | sudo apt install nodejs 408 | ``` 409 | 410 | 3. Confirm that the installation was successful by checking the available version: 411 | ```console 412 | nodejs -v 413 | ``` 414 | 415 | 4. Install the Node.js package manager (npm): 416 | ```console 417 | sudo apt install npm 418 | ``` 419 | 420 | 5. Verify the installed version: 421 | ```console 422 | npm -v 423 | ``` 424 | 425 | Install nvm: (https://github.com/nvm-sh/nvm) 426 | 427 | ```console 428 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.4/install.sh | bash 429 | ``` 430 | 431 | ```console 432 | export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")" 433 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm 434 | ``` 435 | 436 | ```console 437 | nvm install 16.19.1 438 | nvm use 16.19.1 439 | node --version 440 | ``` 441 | 442 | ### Blink script in Node.js 443 | 444 | ```console 445 | mkdir led 446 | cd led 447 | npm install onoff 448 | nano blink.js 449 | ``` 450 | 451 | Paste the following code in the file: 452 | 453 | ```js 454 | var Gpio = require('onoff').Gpio; //include onoff to interact with the GPIO 455 | var LED = new Gpio(18, 'out'); //use GPIO pin 18, and specify that it is output 456 | var blinkInterval = setInterval(blinkLED, 250); //run the blinkLED function every 250ms 457 | 458 | function blinkLED() { //function to start blinking 459 | if (LED.readSync() === 0) { //check the pin state, if the state is 0 (or off) 460 | LED.writeSync(1); //set pin state to 1 (turn LED on) 461 | } else { 462 | LED.writeSync(0); //set pin state to 0 (turn LED off) 463 | } 464 | } 465 | 466 | function endBlink() { //function to stop blinking 467 | clearInterval(blinkInterval); // Stop blink intervals 468 | LED.writeSync(0); // Turn LED off 469 | LED.unexport(); // Unexport GPIO to free resources 470 | } 471 | setTimeout(endBlink, 5000); //stop blinking after 5 seconds 472 | ``` 473 | 474 | run it with: 475 | 476 | ```console 477 | sudo node blink.js 478 | ``` 479 | 480 | ## Now we want to listen to the account via websocket and trigger the LED 481 | 482 | Use scp or rsync to copy the files from the raspberry folder to the raspberry pi. 483 | (If you have problems coping files like i had you can also use VNC Viewer to copy the files.) 484 | Notice that you need to copy the anchor types from the target folder to the raspberry folder whenever you do changes. (I didn't manage to get it to work without copying the types file over next to the led.ts file.) 485 | 486 | Then maybe you need to install node types and type script. 487 | 488 | ```console 489 | npm install -D typescript 490 | npm install -D ts-node 491 | ``` 492 | 493 | Then you can run 494 | 495 | ```console 496 | npm i 497 | and then run the script led.ts 498 | npx ts-node led.ts 499 | ``` 500 | 501 | Don't run it with sudo. That gave me problems. 502 | You may need to change the rights of the directory to be able to write to it: 503 | ```console 504 | chmod -R 777 /directory 505 | ``` 506 | 507 | Now the LED will already have the correct state that is in the LED account. Next we gonna change it via Solana Pay Transaction requests. 508 | 509 | ```js 510 | import * as anchor from "@coral-xyz/anchor"; 511 | import { Program } from "@coral-xyz/anchor"; 512 | import { IDL, LorwawanChest } from "../target/types/lorwan_chest"; 513 | import { clusterApiUrl, Connection, Keypair, PublicKey } from "@solana/web3.js"; 514 | import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; 515 | 516 | var Gpio = require('onoff').Gpio; //include onoff to interact with the GPIO 517 | var LED = new Gpio(18, 'out'); //use GPIO pin 18, and specify that it is output 518 | 519 | let connection = new Connection(clusterApiUrl("devnet")); 520 | let wallet = new NodeWallet(new Keypair()); 521 | const provider = new anchor.AnchorProvider(connection, wallet, { 522 | commitment: "processed", 523 | }); 524 | anchor.setProvider(provider); 525 | 526 | const program = new Program(IDL, "2UYaB7aU7ZPA5LEQh3ZtWzC1MqgLkEJ3nBKebGUrFU3f", { connection }) 527 | 528 | console.log("Program ID", program.programId.toString()); 529 | 530 | startListeningToLedSwitchAccount(); 531 | 532 | async function startListeningToLedSwitchAccount() { 533 | const lorawanChestPDA = await anchor.web3.PublicKey.findProgramAddressSync( 534 | [ 535 | Buffer.from("lorawan_chest"), 536 | ], 537 | program.programId, 538 | )[0]; 539 | 540 | const lorawanChestAccount = await program.account.lorawanChest.fetch( 541 | lorawanChestPDA 542 | ) 543 | 544 | console.log(JSON.stringify(lorawanChestAccount)); 545 | console.log("Led is: ", lorawanChestAccount.isOpen); 546 | if (ledSwitchAccount.isOpen) { 547 | LED.writeSync(1); 548 | } else { 549 | LED.writeSync(0); 550 | } 551 | 552 | connection.onAccountChange(lorawanChestPDA, (account) => { 553 | const decoded = program.coder.accounts.decode( 554 | "lorawanChest", 555 | account.data 556 | ) 557 | 558 | if (decoded.isOpen) { 559 | LED.writeSync(1); 560 | } else { 561 | LED.writeSync(0); 562 | } 563 | console.log("Account changed. Chest is: ", decoded.isOpen); 564 | }, "processed") 565 | }; 566 | ``` 567 | 568 | When you run this script on your raspberry pi the LED will always show the state of the ChestAccount account. This is a nice indicator that the chest is currently open and the QR code can be scanned. 569 | 570 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/.env: -------------------------------------------------------------------------------- 1 | BURNER_KEY=[96,21,42,91,98,180,196,164,35,1,108,196,219,5,13,187,214,242,87,2,56,232,67,235,136,40,75,215,44,116,221,156,5,19,1,175,254,20,18,109,244,222,21,152,51,126,21,79,134,216,182,148,158,237,97,116,148,77,160,32,95,85,136,112] -------------------------------------------------------------------------------- /helium-lorawan-chest/app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /helium-lorawan-chest/app/LorBisZjmXHAdUnAWKfBiVh84yaxGVF2WY6kjr7AQu5.json: -------------------------------------------------------------------------------- 1 | [96,21,42,91,98,180,196,164,35,1,108,196,219,5,13,187,214,242,87,2,56,232,67,235,136,40,75,215,44,116,221,156,5,19,1,175,254,20,18,109,244,222,21,152,51,126,21,79,134,216,182,148,158,237,97,116,148,77,160,32,95,85,136,112] -------------------------------------------------------------------------------- /helium-lorawan-chest/app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/transaction](http://localhost:3000/api/transaction). This endpoint can be edited in `pages/api/transaction.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | To make the solana pay transaction requests work locally you need to make the page accessible from outside. Easiest way to do that is to use [`ngrok`](https://ngrok.com/). Run `ngrok http 3000` in the terminal and open the url provided in your browser instead of `localhost:3000`. 26 | 27 | ## Learn More 28 | 29 | To learn more about Next.js, take a look at the following resources: 30 | 31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 32 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 33 | 34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 35 | 36 | ## Deploy on Vercel 37 | 38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 39 | 40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 41 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /helium-lorawan-chest/app/app/head.tsx: -------------------------------------------------------------------------------- 1 | export default function Head() { 2 | return ( 3 | <> 4 | Lorawan Chest 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Wallet } from '@/src/Wallet' 2 | import './globals.css' 3 | 4 | export default function RootLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | return ( 10 | 11 | {/* 12 | will contain the components returned by the nearest parent 13 | head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head 14 | */} 15 | 16 | 17 | {children} 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; // this makes next know that this page should be rendered in the client 2 | import { useEffect, useState } from 'react'; 3 | import { CONNECTION, LORAWAN_CHEST_PROGRAM_ID, LORAWAN_CHEST_PROGRAM, LORAWAN_CHEST_PDA } from '@/src/util/const'; 4 | import PayQR from '@/src/components/PayQR'; 5 | import { Wallet } from '@/src/Wallet'; 6 | import { 7 | WalletModalProvider, 8 | WalletDisconnectButton, 9 | WalletMultiButton 10 | } from '@solana/wallet-adapter-react-ui'; 11 | import { Transaction, TransactionInstruction } from '@solana/web3.js'; 12 | import { useConnection, useWallet } from "@solana/wallet-adapter-react"; 13 | var sha256 = require('sha256') 14 | 15 | export default function Home() { 16 | const [chestState, setChestState] = useState() 17 | const [isOpen, SetIsOpen] = useState(); 18 | const { connection } = useConnection(); 19 | const { publicKey, sendTransaction } = useWallet(); 20 | 21 | useEffect(() => { 22 | 23 | CONNECTION.onAccountChange( 24 | LORAWAN_CHEST_PDA, 25 | (updatedAccountInfo, context) => { 26 | { 27 | console.log("updatedAccountInfo", updatedAccountInfo); 28 | const decoded = LORAWAN_CHEST_PROGRAM.coder.accounts.decode( 29 | "lorawanChest", 30 | updatedAccountInfo.data 31 | ) 32 | setChestState(decoded); 33 | SetIsOpen(decoded.isOpen); 34 | } 35 | }, 36 | "confirmed" 37 | ); 38 | 39 | const getState = async () => { 40 | const gameData = await LORAWAN_CHEST_PROGRAM.account.lorawanChest.fetch( 41 | LORAWAN_CHEST_PDA, 42 | ); 43 | setChestState(gameData); 44 | SetIsOpen(gameData.isOpen); 45 | }; 46 | 47 | getState(); 48 | 49 | }, []); 50 | 51 | return ( 52 |
53 | {
54 |
55 | 56 |
57 | 58 | {/* If you want to have wallet connector and call functions from the web page as well this is how you can do it. */ 59 | /*gameDataState && ( 60 | <> 61 | 62 | 63 | 64 | ) 65 | 68 | */} 69 | 70 |
71 |
72 |

73 | Lorawan Chest 74 |

75 | Scan the QR code to loot the chest. This will only work then the chest is open due to the lorawan sensor. 76 |

77 | 78 |

79 | { 80 | "Chest open: " + [chestState ? chestState.isOpen ? "Open" : "Closed" : "loading"] 81 | } 82 |

83 | 84 |
85 |
86 | 87 |
  • 88 | 89 | {isOpen != null && isOpen && ( 90 | 91 | )} 92 | 93 | {!chestState && ( 94 | 95 | )} 96 |
  • 97 |
    98 |
    99 |
    } 100 |
    101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | }, 6 | images: { 7 | domains: ['images.unsplash.com'] 8 | }, 9 | } 10 | 11 | module.exports = nextConfig 12 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solution", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "export": "next export", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "proxy": "local-ssl-proxy --source 3001 --target 3000" 12 | }, 13 | "dependencies": { 14 | "@next/font": "13.1.2", 15 | "@solana/pay": "^0.2.4", 16 | "@solana/spl-token": "^0.3.7", 17 | "@coral-xyz/anchor": "0.28.0", 18 | "@solana/wallet-adapter-react": "^0.15.28", 19 | "@solana/wallet-adapter-react-ui": "^0.9.27", 20 | "@solana/wallet-adapter-wallets": "^0.19.11", 21 | "@solana/web3.js": "^1.73.2", 22 | "@types/node": "18.11.18", 23 | "@types/qrcode": "^1.5.0", 24 | "@types/react": "18.0.27", 25 | "@types/react-dom": "18.0.10", 26 | "bigint-buffer": "^1.1.5", 27 | "bignumber.js": "^9.1.1", 28 | "borsh": "^0.7.0", 29 | "eslint": "8.32.0", 30 | "eslint-config-next": "13.1.2", 31 | "next": "13.1.2", 32 | "qrcode": "^1.5.1", 33 | "react": "^18.2.0", 34 | "react-dom": "18.2.0", 35 | "sha256": "^0.2.0" 36 | }, 37 | "devDependencies": { 38 | "autoprefixer": "^10.4.13", 39 | "encoding": "^0.1.13", 40 | "local-ssl-proxy": "^1.3.0", 41 | "postcss": "^8.4.21", 42 | "tailwindcss": "^3.2.4", 43 | "ts-loader": "^9.4.4", 44 | "typescript": "^5.1.6" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/pages/api/sensor-downlink.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import { Keypair, PublicKey, Transaction } from '@solana/web3.js'; 4 | import { CONNECTION, LORAWAN_CHEST_PDA, LORAWAN_CHEST_PROGRAM } from '@/src/util/const'; 5 | 6 | type POST = { 7 | message: string; 8 | }; 9 | 10 | type GET = { 11 | label: string; 12 | icon: string; 13 | }; 14 | 15 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 16 | if (req.method === 'GET') { 17 | return get(req, res); 18 | } 19 | 20 | if (req.method === 'POST') { 21 | return post(req, res); 22 | } 23 | } 24 | 25 | const get = async (req: NextApiRequest, res: NextApiResponse) => { 26 | const label = 'LED Switch'; 27 | const icon = 28 | 'https://media.discordapp.net/attachments/964525722301501477/978683590743302184/sol-logo1.png'; 29 | 30 | console.log("Sensor response:", req.body); 31 | console.log("Door is open:", req.body.decoded.payload.DOOR_OPEN_STATUS); 32 | 33 | var message = ''; 34 | if (req.body.decoded.payload.DOOR_OPEN_STATUS == "0") { 35 | message = 'Door open !'; 36 | } else { 37 | message = 'Door Closed'; 38 | } 39 | 40 | res.status(200).json({ 41 | label, 42 | icon, 43 | }); 44 | }; 45 | 46 | const post = async (req: NextApiRequest, res: NextApiResponse) => { 47 | 48 | console.log("Door is open:", req.body); 49 | console.log("Door is open:", req.body.decoded.payload.DOOR_OPEN_STATUS); 50 | 51 | const burner = JSON.parse(process.env.BURNER_KEY ?? "") as number[] 52 | const burnerKeypair = Keypair.fromSecretKey(Uint8Array.from(burner)) 53 | const sender = burnerKeypair.publicKey; 54 | 55 | const transaction = new Transaction(); 56 | const latestBlockhash = await CONNECTION.getLatestBlockhash(); 57 | transaction.feePayer = sender; 58 | transaction.recentBlockhash = latestBlockhash.blockhash; 59 | 60 | let message = ''; 61 | 62 | if (req.body.decoded.payload.DOOR_OPEN_STATUS == "1") { 63 | let ix = await LORAWAN_CHEST_PROGRAM.methods.switch(true).accounts( 64 | { 65 | lorawanChest: LORAWAN_CHEST_PDA, 66 | authority: sender 67 | }, 68 | ).instruction(); 69 | 70 | transaction.add(ix); 71 | 72 | message = 'Door open !'; 73 | } else { 74 | let ix = await LORAWAN_CHEST_PROGRAM.methods.switch(false).accounts( 75 | { 76 | lorawanChest: LORAWAN_CHEST_PDA, 77 | authority: sender 78 | }, 79 | ).instruction(); 80 | 81 | transaction.add(ix); 82 | message = 'Door Closed'; 83 | } 84 | 85 | var signature = await CONNECTION.sendTransaction(transaction, [burnerKeypair]); 86 | console.log("Transaction signature:", signature); 87 | 88 | res.status(200).send({ message }); 89 | }; 90 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/pages/api/transaction.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import { Keypair, PublicKey, SYSVAR_RENT_PUBKEY, SystemProgram, Transaction } from '@solana/web3.js'; 4 | import { CONNECTION, LORAWAN_CHEST_PDA, LORAWAN_CHEST_PROGRAM } from '@/src/util/const'; 5 | var web3 = require("@solana/web3.js"); 6 | 7 | type POST = { 8 | transaction: string; 9 | message: string; 10 | }; 11 | 12 | type GET = { 13 | label: string; 14 | icon: string; 15 | }; 16 | 17 | function getFromPayload(req: NextApiRequest, payload: string, field: string): string { 18 | function parseError() { throw new Error(`${payload} parse error: missing ${field}`) }; 19 | let value; 20 | if (payload === 'Query') { 21 | if (!(field in req.query)) parseError(); 22 | value = req.query[field]; 23 | } 24 | if (payload === 'Body') { 25 | if (!req.body || !(field in req.body)) parseError(); 26 | value = req.body[field]; 27 | } 28 | if (value === undefined || value.length === 0) parseError(); 29 | return typeof value === 'string' ? value : value[0]; 30 | } 31 | 32 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 33 | if (req.method === 'GET') { 34 | return get(req, res); 35 | } 36 | 37 | if (req.method === 'POST') { 38 | return post(req, res); 39 | } 40 | } 41 | 42 | const get = async (req: NextApiRequest, res: NextApiResponse) => { 43 | const label = 'Lorawan Chest'; 44 | const icon = 45 | 'https://media.discordapp.net/attachments/964525722301501477/978683590743302184/sol-logo1.png'; 46 | 47 | res.status(200).json({ 48 | label, 49 | icon, 50 | }); 51 | }; 52 | 53 | const post = async (req: NextApiRequest, res: NextApiResponse) => { 54 | 55 | const accountField = getFromPayload(req, 'Body', 'account'); 56 | const instructionField = getFromPayload(req, 'Query', 'instruction'); 57 | 58 | const burner = JSON.parse(process.env.BURNER_KEY ?? "") as number[] 59 | const burnerKeypair = Keypair.fromSecretKey(Uint8Array.from(burner)) 60 | 61 | const sender = new PublicKey(accountField); 62 | 63 | const transaction = new Transaction(); 64 | const latestBlockhash = await CONNECTION.getLatestBlockhash(); 65 | transaction.feePayer = sender; 66 | transaction.recentBlockhash = latestBlockhash.blockhash; 67 | 68 | let message; 69 | console.log("Instruction:", instructionField); 70 | if (instructionField == "loot") { 71 | console.log("inside:", instructionField); 72 | 73 | let tokenTransferIx = web3.SystemProgram.transfer({ 74 | fromPubkey: burnerKeypair.publicKey, 75 | toPubkey: sender, 76 | lamports: web3.LAMPORTS_PER_SOL / 10, 77 | }) 78 | 79 | transaction.add(tokenTransferIx); 80 | 81 | let lootInstruction = await LORAWAN_CHEST_PROGRAM.methods.loot().accounts( 82 | { 83 | lorawanChest: LORAWAN_CHEST_PDA, 84 | authority: sender, 85 | }, 86 | ).instruction(); 87 | 88 | transaction.add(lootInstruction); 89 | 90 | transaction.sign(burnerKeypair); 91 | 92 | message = 'Loot chest!'; 93 | } else if (instructionField == "initialize") { 94 | let ix = await LORAWAN_CHEST_PROGRAM.methods.initialize().accounts( 95 | { 96 | lorawanChest: LORAWAN_CHEST_PDA, 97 | authority: sender, 98 | systemProgram: SystemProgram.programId, 99 | rent: SYSVAR_RENT_PUBKEY, 100 | }, 101 | ).instruction(); 102 | 103 | transaction.add(ix); 104 | 105 | message = 'Initialize PDA !'; 106 | } else { 107 | message = 'Unknown instruction'; 108 | } 109 | 110 | console.log("message:", message); 111 | 112 | // Serialize and return the unsigned transaction. 113 | const serializedTransaction = transaction.serialize({ 114 | verifySignatures: false, 115 | requireAllSignatures: false, 116 | }); 117 | 118 | const base64Transaction = serializedTransaction.toString('base64'); 119 | 120 | res.status(200).send({ transaction: base64Transaction, message }); 121 | }; 122 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/public/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/solana-depin-examples/3f8cd5b1519efd9c44e3261a77ec576a7fedcb6c/helium-lorawan-chest/app/public/bg.jpg -------------------------------------------------------------------------------- /helium-lorawan-chest/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/solana-depin-examples/3f8cd5b1519efd9c44e3261a77ec576a7fedcb6c/helium-lorawan-chest/app/public/favicon.ico -------------------------------------------------------------------------------- /helium-lorawan-chest/app/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/solana-depin-examples/3f8cd5b1519efd9c44e3261a77ec576a7fedcb6c/helium-lorawan-chest/app/public/icon.png -------------------------------------------------------------------------------- /helium-lorawan-chest/app/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/src/Wallet.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { FC, useMemo } from 'react'; 3 | import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; 4 | import { WalletAdapterNetwork } from '@solana/wallet-adapter-base'; 5 | import { PhantomWalletAdapter } from '@solana/wallet-adapter-phantom'; 6 | import { 7 | WalletModalProvider, 8 | WalletDisconnectButton, 9 | WalletMultiButton 10 | } from '@solana/wallet-adapter-react-ui'; 11 | import { clusterApiUrl } from '@solana/web3.js'; 12 | 13 | // Default styles that can be overridden by your app 14 | require('@solana/wallet-adapter-react-ui/styles.css'); 15 | 16 | type Props = { 17 | children?: React.ReactNode 18 | }; 19 | 20 | export const Wallet: FC = ({children}) => { 21 | // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'. 22 | const network = WalletAdapterNetwork.Devnet; 23 | 24 | // You can also provide a custom RPC endpoint. 25 | const endpoint = useMemo(() => clusterApiUrl(network), [network]); 26 | 27 | const wallets = useMemo( 28 | () => [ 29 | 30 | new PhantomWalletAdapter(), 31 | ], 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | [network] 34 | ); 35 | 36 | return ( 37 | 38 | 39 | 40 | { children } 41 | 42 | 43 | 44 | ); 45 | }; -------------------------------------------------------------------------------- /helium-lorawan-chest/app/src/components/PayQR.tsx: -------------------------------------------------------------------------------- 1 | import { encodeURL, createQR } from '@solana/pay'; 2 | import { FC, useEffect, useRef } from 'react'; 3 | 4 | type TransactionRequestQRProps = { 5 | instruction: string; 6 | }; 7 | 8 | const queryBuilder = (baseUrl: string, params: string[][]) => { 9 | let url = baseUrl + '?'; 10 | params.forEach((p, i) => url += p[0] + '=' + p[1] + (i != params.length - 1 ? '&' : '')); 11 | console.log(url) 12 | return url; 13 | } 14 | 15 | const PayQR: FC = ( 16 | { instruction } 17 | ) => { 18 | const qrRef = useRef(null) 19 | 20 | useEffect(() => { 21 | const params = [ 22 | ['instruction', instruction], 23 | ]; 24 | 25 | const apiUrl = queryBuilder( 26 | `${window.location.protocol}//${window.location.host}/api/transaction`, 27 | params, 28 | ); 29 | 30 | const qr = createQR( 31 | encodeURL({ link: new URL(apiUrl) }), 32 | 360, 33 | 'transparent' 34 | ); 35 | 36 | qr.update({ backgroundOptions: { round: 1000 } }); 37 | qr.update({ type: 'canvas' }); 38 | 39 | if (qrRef.current != null) { 40 | qrRef.current.innerHTML = ''; 41 | qr.append(qrRef.current) 42 | } 43 | 44 | }, []) 45 | 46 | return ( 47 |
    48 | 49 |
    50 |

    {instruction}

    51 |
    52 | 53 |
    54 | 55 |
    56 | ); 57 | }; 58 | 59 | export default PayQR; 60 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/src/util/const.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from "@solana/web3.js"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import { IDL, LorawanChest } from "./lorawan_chest"; 4 | 5 | export const CONNECTION = new Connection(process.env.NEXT_PUBLIC_RPC ? process.env.NEXT_PUBLIC_RPC : 'https://api.devnet.solana.com', { 6 | wsEndpoint: process.env.NEXT_PUBLIC_WSS_RPC ? process.env.NEXT_PUBLIC_WSS_RPC : "wss://api.devnet.solana.com", 7 | commitment: 'confirmed' 8 | }); 9 | 10 | export const LORAWAN_CHEST_PROGRAM_ID = new PublicKey('2UYaB7aU7ZPA5LEQh3ZtWzC1MqgLkEJ3nBKebGUrFU3f'); 11 | 12 | export const LORAWAN_CHEST_PROGRAM = new Program(IDL, LORAWAN_CHEST_PROGRAM_ID, { connection: CONNECTION }) 13 | 14 | export const LORAWAN_CHEST_PDA = PublicKey.findProgramAddressSync( 15 | [ 16 | Buffer.from("lorawan_chest"), 17 | ], 18 | LORAWAN_CHEST_PROGRAM_ID, 19 | )[0]; -------------------------------------------------------------------------------- /helium-lorawan-chest/app/src/util/lorawan_chest.ts: -------------------------------------------------------------------------------- 1 | export type LorawanChest = { 2 | "version": "0.1.0", 3 | "name": "lorawan_chest", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [ 8 | { 9 | "name": "lorawanChest", 10 | "isMut": true, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "authority", 15 | "isMut": true, 16 | "isSigner": true 17 | }, 18 | { 19 | "name": "rent", 20 | "isMut": false, 21 | "isSigner": false 22 | }, 23 | { 24 | "name": "systemProgram", 25 | "isMut": false, 26 | "isSigner": false 27 | } 28 | ], 29 | "args": [] 30 | }, 31 | { 32 | "name": "switch", 33 | "accounts": [ 34 | { 35 | "name": "lorawanChest", 36 | "isMut": true, 37 | "isSigner": false 38 | }, 39 | { 40 | "name": "authority", 41 | "isMut": true, 42 | "isSigner": true 43 | } 44 | ], 45 | "args": [ 46 | { 47 | "name": "isOn", 48 | "type": "bool" 49 | } 50 | ] 51 | }, 52 | { 53 | "name": "loot", 54 | "accounts": [ 55 | { 56 | "name": "lorawanChest", 57 | "isMut": true, 58 | "isSigner": false 59 | }, 60 | { 61 | "name": "authority", 62 | "isMut": true, 63 | "isSigner": true 64 | } 65 | ], 66 | "args": [] 67 | } 68 | ], 69 | "accounts": [ 70 | { 71 | "name": "lorawanChest", 72 | "type": { 73 | "kind": "struct", 74 | "fields": [ 75 | { 76 | "name": "isOpen", 77 | "type": "bool" 78 | } 79 | ] 80 | } 81 | } 82 | ], 83 | "errors": [ 84 | { 85 | "code": 6100, 86 | "name": "ChestIsClosed" 87 | } 88 | ] 89 | }; 90 | 91 | export const IDL: LorawanChest = { 92 | "version": "0.1.0", 93 | "name": "lorawan_chest", 94 | "instructions": [ 95 | { 96 | "name": "initialize", 97 | "accounts": [ 98 | { 99 | "name": "lorawanChest", 100 | "isMut": true, 101 | "isSigner": false 102 | }, 103 | { 104 | "name": "authority", 105 | "isMut": true, 106 | "isSigner": true 107 | }, 108 | { 109 | "name": "rent", 110 | "isMut": false, 111 | "isSigner": false 112 | }, 113 | { 114 | "name": "systemProgram", 115 | "isMut": false, 116 | "isSigner": false 117 | } 118 | ], 119 | "args": [] 120 | }, 121 | { 122 | "name": "switch", 123 | "accounts": [ 124 | { 125 | "name": "lorawanChest", 126 | "isMut": true, 127 | "isSigner": false 128 | }, 129 | { 130 | "name": "authority", 131 | "isMut": true, 132 | "isSigner": true 133 | } 134 | ], 135 | "args": [ 136 | { 137 | "name": "isOn", 138 | "type": "bool" 139 | } 140 | ] 141 | }, 142 | { 143 | "name": "loot", 144 | "accounts": [ 145 | { 146 | "name": "lorawanChest", 147 | "isMut": true, 148 | "isSigner": false 149 | }, 150 | { 151 | "name": "authority", 152 | "isMut": true, 153 | "isSigner": true 154 | } 155 | ], 156 | "args": [] 157 | } 158 | ], 159 | "accounts": [ 160 | { 161 | "name": "lorawanChest", 162 | "type": { 163 | "kind": "struct", 164 | "fields": [ 165 | { 166 | "name": "isOpen", 167 | "type": "bool" 168 | } 169 | ] 170 | } 171 | } 172 | ], 173 | "errors": [ 174 | { 175 | "code": 6100, 176 | "name": "ChestIsClosed" 177 | } 178 | ] 179 | }; 180 | -------------------------------------------------------------------------------- /helium-lorawan-chest/app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx}", 5 | "./components/**/*.{js,ts,jsx,tsx}", 6 | 7 | // Or if using `src` directory: 8 | "./src/**/*.{js,ts,jsx,tsx}", 9 | 10 | // Or if using `app` directory: 11 | "./app/**/*.{js,ts,jsx,tsx}", 12 | ], 13 | theme: { 14 | extend: {}, 15 | }, 16 | plugins: [], 17 | } -------------------------------------------------------------------------------- /helium-lorawan-chest/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": ["./*"] 25 | } 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /helium-lorawan-chest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", 4 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check", 5 | "build": "webpack --mode production" 6 | }, 7 | "dependencies": { 8 | "@coral-xyz/anchor": "^0.28.0", 9 | "@solana/spl-token": "^0.3.7" 10 | }, 11 | "devDependencies": { 12 | "chai": "^4.3.4", 13 | "mocha": "^9.0.3", 14 | "ts-mocha": "^10.0.0", 15 | "@types/bn.js": "^5.1.0", 16 | "@types/chai": "^4.3.0", 17 | "@types/mocha": "^9.0.0", 18 | "typescript": "^4.3.5", 19 | "prettier": "^2.6.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /helium-lorawan-chest/programs/lorawan-chest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lorawan_chest" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "lorawan_chest" 10 | 11 | [features] 12 | no-entrypoint = [] 13 | no-idl = [] 14 | no-log-ix-name = [] 15 | cpi = ["no-entrypoint"] 16 | default = [] 17 | 18 | [dependencies] 19 | anchor-lang = {version = "0.28.0", features = ["init-if-needed"]} 20 | anchor-spl = "0.28.0" 21 | solana-program = "1.16.13" -------------------------------------------------------------------------------- /helium-lorawan-chest/programs/lorawan-chest/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /helium-lorawan-chest/programs/lorawan-chest/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use solana_program::pubkey; 3 | 4 | declare_id!("2UYaB7aU7ZPA5LEQh3ZtWzC1MqgLkEJ3nBKebGUrFU3f"); 5 | 6 | // Change this to what ever key you use in your API to make sure not everyone can just call the switch function. 7 | const ADMIN_PUBKEY: Pubkey = pubkey!("LorBisZjmXHAdUnAWKfBiVh84yaxGVF2WY6kjr7AQu5"); 8 | 9 | #[error_code] 10 | pub enum LorawanChestError { 11 | ChestIsClosed = 100, 12 | } 13 | 14 | #[program] 15 | pub mod lorawan_chest { 16 | use super::*; 17 | 18 | pub fn initialize(ctx: Context) -> Result<()> { 19 | ctx.accounts.lorawan_chest.is_open = false; 20 | Ok(()) 21 | } 22 | 23 | pub fn switch(ctx: Context, is_on: bool) -> Result<()> { 24 | // Add some check here that actually only your api which is triggered be the sensor is allowed to call this. 25 | ctx.accounts.lorawan_chest.is_open = is_on; 26 | Ok(()) 27 | } 28 | 29 | pub fn loot(ctx: Context) -> Result<()> { 30 | if !ctx.accounts.lorawan_chest.is_open { 31 | return Err(LorawanChestError::ChestIsClosed.into()); 32 | } 33 | 34 | // You can add any kind of loot action here. 35 | // In the next js api we add a transfer, but you could also mint an NFT for example. 36 | // Or you could save per user here which chests he already collected and build some real live adventure game. 37 | msg!("Looted!"); 38 | 39 | Ok(()) 40 | } 41 | } 42 | 43 | #[derive(Accounts)] 44 | pub struct Initialize<'info> { 45 | #[account(init, payer = authority, space = 8 + 8, seeds = [b"lorawan_chest"], bump)] 46 | pub lorawan_chest: Account<'info, LorawanChest>, 47 | #[account(mut)] 48 | pub authority: Signer<'info>, 49 | pub rent: Sysvar<'info, Rent>, 50 | pub system_program: Program<'info, System>, 51 | } 52 | 53 | #[derive(Accounts)] 54 | pub struct Switch<'info> { 55 | #[account(mut, seeds = [b"lorawan_chest"], bump)] 56 | pub lorawan_chest: Account<'info, LorawanChest>, 57 | #[account(mut, address = ADMIN_PUBKEY)] 58 | pub authority: Signer<'info>, 59 | } 60 | 61 | #[derive(Accounts)] 62 | pub struct Loot<'info> { 63 | #[account(mut, seeds = [b"lorawan_chest"], bump)] 64 | pub lorawan_chest: Account<'info, LorawanChest>, 65 | #[account(mut)] 66 | pub authority: Signer<'info>, 67 | } 68 | 69 | #[account] 70 | pub struct LorawanChest { 71 | pub is_open: bool, 72 | } 73 | -------------------------------------------------------------------------------- /helium-lorawan-chest/raspberry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", 4 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check", 5 | "build": "webpack" 6 | }, 7 | "dependencies": { 8 | "@coral-xyz/anchor": "^0.28.0", 9 | "onoff": "^6.0.3", 10 | "webpack": "^5.88.2" 11 | }, 12 | "devDependencies": { 13 | "@types/bn.js": "^5.1.0", 14 | "@types/chai": "^4.3.0", 15 | "@types/mocha": "^9.0.0", 16 | "chai": "^4.3.4", 17 | "mocha": "^9.0.3", 18 | "prettier": "^2.6.2", 19 | "ts-mocha": "^10.0.0", 20 | "typescript": "^4.3.5", 21 | "webpack-cli": "^5.1.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helium-lorawan-chest/raspberry/src/chest.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import { IDL, LorawanChest } from "./lorawan_chest"; 4 | import { clusterApiUrl, Connection, Keypair } from "@solana/web3.js"; 5 | import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; 6 | 7 | var Gpio = require('onoff').Gpio; //include onoff to interact with the GPIO 8 | var LED = new Gpio(18, 'out'); //use GPIO pin 18, and specify that it is output 9 | 10 | let connection = new Connection(clusterApiUrl("devnet")); 11 | let wallet = new NodeWallet(new Keypair()); 12 | const provider = new anchor.AnchorProvider(connection, wallet, { 13 | commitment: "processed", 14 | }); 15 | anchor.setProvider(provider); 16 | 17 | const program = new Program(IDL, "2UYaB7aU7ZPA5LEQh3ZtWzC1MqgLkEJ3nBKebGUrFU3f", { connection }) 18 | 19 | console.log("Program ID", program.programId.toString()); 20 | 21 | startListeningToLedSwitchAccount(); 22 | 23 | async function startListeningToLedSwitchAccount() { 24 | const ledSwitchPDA = await anchor.web3.PublicKey.findProgramAddressSync( 25 | [ 26 | Buffer.from("lorawan_chest"), 27 | ], 28 | program.programId, 29 | )[0]; 30 | 31 | const ledSwitchAccount = await program.account.lorawanChest.fetch( 32 | ledSwitchPDA 33 | ) 34 | 35 | console.log(JSON.stringify(ledSwitchAccount)); 36 | console.log("Chest is: ", ledSwitchAccount.isOpen); 37 | 38 | if (ledSwitchAccount.isOpen) { 39 | LED.writeSync(1); 40 | } else { 41 | LED.writeSync(0); 42 | } 43 | 44 | connection.onAccountChange(ledSwitchPDA, (account) => { 45 | const decoded = program.coder.accounts.decode( 46 | "lorawanChest", 47 | account.data 48 | ) 49 | 50 | if (decoded.isOn) { 51 | LED.writeSync(1); 52 | } else { 53 | LED.writeSync(0); 54 | } 55 | console.log("Account changed. Chest is: ", decoded.isOpen); 56 | }, "processed") 57 | }; 58 | -------------------------------------------------------------------------------- /helium-lorawan-chest/raspberry/src/lorawan_chest.ts: -------------------------------------------------------------------------------- 1 | export type LorawanChest = { 2 | "version": "0.1.0", 3 | "name": "lorawan_chest", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [ 8 | { 9 | "name": "lorawanChest", 10 | "isMut": true, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "authority", 15 | "isMut": true, 16 | "isSigner": true 17 | }, 18 | { 19 | "name": "rent", 20 | "isMut": false, 21 | "isSigner": false 22 | }, 23 | { 24 | "name": "systemProgram", 25 | "isMut": false, 26 | "isSigner": false 27 | } 28 | ], 29 | "args": [] 30 | }, 31 | { 32 | "name": "switch", 33 | "accounts": [ 34 | { 35 | "name": "lorawanChest", 36 | "isMut": true, 37 | "isSigner": false 38 | }, 39 | { 40 | "name": "authority", 41 | "isMut": true, 42 | "isSigner": true 43 | } 44 | ], 45 | "args": [ 46 | { 47 | "name": "isOn", 48 | "type": "bool" 49 | } 50 | ] 51 | }, 52 | { 53 | "name": "loot", 54 | "accounts": [ 55 | { 56 | "name": "lorawanChest", 57 | "isMut": true, 58 | "isSigner": false 59 | }, 60 | { 61 | "name": "authority", 62 | "isMut": true, 63 | "isSigner": true 64 | } 65 | ], 66 | "args": [] 67 | } 68 | ], 69 | "accounts": [ 70 | { 71 | "name": "lorawanChest", 72 | "type": { 73 | "kind": "struct", 74 | "fields": [ 75 | { 76 | "name": "isOpen", 77 | "type": "bool" 78 | } 79 | ] 80 | } 81 | } 82 | ], 83 | "errors": [ 84 | { 85 | "code": 6100, 86 | "name": "ChestIsClosed" 87 | } 88 | ] 89 | }; 90 | 91 | export const IDL: LorawanChest = { 92 | "version": "0.1.0", 93 | "name": "lorawan_chest", 94 | "instructions": [ 95 | { 96 | "name": "initialize", 97 | "accounts": [ 98 | { 99 | "name": "lorawanChest", 100 | "isMut": true, 101 | "isSigner": false 102 | }, 103 | { 104 | "name": "authority", 105 | "isMut": true, 106 | "isSigner": true 107 | }, 108 | { 109 | "name": "rent", 110 | "isMut": false, 111 | "isSigner": false 112 | }, 113 | { 114 | "name": "systemProgram", 115 | "isMut": false, 116 | "isSigner": false 117 | } 118 | ], 119 | "args": [] 120 | }, 121 | { 122 | "name": "switch", 123 | "accounts": [ 124 | { 125 | "name": "lorawanChest", 126 | "isMut": true, 127 | "isSigner": false 128 | }, 129 | { 130 | "name": "authority", 131 | "isMut": true, 132 | "isSigner": true 133 | } 134 | ], 135 | "args": [ 136 | { 137 | "name": "isOn", 138 | "type": "bool" 139 | } 140 | ] 141 | }, 142 | { 143 | "name": "loot", 144 | "accounts": [ 145 | { 146 | "name": "lorawanChest", 147 | "isMut": true, 148 | "isSigner": false 149 | }, 150 | { 151 | "name": "authority", 152 | "isMut": true, 153 | "isSigner": true 154 | } 155 | ], 156 | "args": [] 157 | } 158 | ], 159 | "accounts": [ 160 | { 161 | "name": "lorawanChest", 162 | "type": { 163 | "kind": "struct", 164 | "fields": [ 165 | { 166 | "name": "isOpen", 167 | "type": "bool" 168 | } 169 | ] 170 | } 171 | } 172 | ], 173 | "errors": [ 174 | { 175 | "code": 6100, 176 | "name": "ChestIsClosed" 177 | } 178 | ] 179 | }; 180 | -------------------------------------------------------------------------------- /helium-lorawan-chest/raspberry/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /helium-lorawan-chest/tests/LorawanChest.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import { LorawanChest } from "../target/types/lorawan_chest"; 4 | import { assert } from "chai"; 5 | 6 | describe("lorawan-chest-test", () => { 7 | anchor.setProvider(anchor.AnchorProvider.env()); 8 | 9 | const program = anchor.workspace.LorawanChest as Program; 10 | const wallet = anchor.workspace.LorawanChest.provider.wallet 11 | 12 | it("Is initialized!", async () => { 13 | 14 | const lorawanChestPDA = await anchor.web3.PublicKey.findProgramAddressSync( 15 | [ 16 | Buffer.from("lorawan_chest"), 17 | ], 18 | program.programId, 19 | )[0]; 20 | 21 | console.log("Lorawan Chest pda", lorawanChestPDA); 22 | 23 | try { 24 | const initializeTransaction = await program.methods.initialize().accounts( 25 | { 26 | lorawanChest: lorawanChestPDA, 27 | authority: wallet.publicKey, 28 | systemProgram: anchor.web3.SystemProgram.programId, 29 | }, 30 | ).rpc(); 31 | console.log("Initialize transaction signature: ", initializeTransaction); 32 | } catch (e) { 33 | console.log(e); 34 | } 35 | 36 | const switchOnTransaction = await program.methods.switch(true).accounts( 37 | { 38 | lorawanChest: lorawanChestPDA, 39 | authority: wallet.publicKey 40 | }, 41 | ).rpc(); 42 | 43 | const ledAccount = await program.account.lorawanChest.fetch( 44 | lorawanChestPDA 45 | ) 46 | console.log("Your switch on transaction signature", switchOnTransaction); 47 | 48 | assert(ledAccount.isOpen === true, "Chest account is not initialized correctly. Should be open/true") 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /helium-lorawan-chest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /led-switch/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | node_modules 7 | test-ledger 8 | .yarn 9 | -------------------------------------------------------------------------------- /led-switch/.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | node_modules 6 | dist 7 | build 8 | test-ledger 9 | -------------------------------------------------------------------------------- /led-switch/Anchor.toml: -------------------------------------------------------------------------------- 1 | [features] 2 | seeds = false 3 | skip-lint = false 4 | [programs.localnet] 5 | led_switch = "F7F5ZTEMU6d5Ac8CQEJKBGWXLbte1jK2Kodyu3tNtvaj" 6 | 7 | [registry] 8 | url = "https://api.apr.dev" 9 | 10 | [provider] 11 | cluster = "devnet" 12 | wallet = "~/.config/solana/id.json" 13 | 14 | [scripts] 15 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" 16 | -------------------------------------------------------------------------------- /led-switch/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*" 4 | ] 5 | 6 | [profile.release] 7 | overflow-checks = true 8 | lto = "fat" 9 | codegen-units = 1 10 | [profile.release.build-override] 11 | opt-level = 3 12 | incremental = false 13 | codegen-units = 1 14 | -------------------------------------------------------------------------------- /led-switch/README.md: -------------------------------------------------------------------------------- 1 | # Solana DePin Hello World 2 | 3 | In this example you will learn how to control a LED using Solana Pay Transaction Requests. 4 | 5 | 6 | 7 | https://github.com/solana-developers/solana-depin-examples/assets/5938789/784e851c-258d-4d5a-8508-dda90b22eb7f 8 | 9 | NOTE: 10 | If you use a raspberry 5 instead of 4 you need to use different GPIO pin addressed: 11 | You can find the addresses like this: 12 | 13 | ```bash 14 | cat /sys/kernel/debug/gpio 15 | ``` 16 | 17 | Here is the most important ones: 18 | 19 | So if it sais 18 in the tutorial you use 589 instead and so on: 20 | 21 | ``` 22 | gpio-573 (GPIO2 ) 23 | gpio-574 (GPIO3 ) 24 | gpio-575 (GPIO4 ) 25 | gpio-576 (GPIO5 ) 26 | gpio-577 (GPIO6 ) 27 | gpio-578 (GPIO7 ) 28 | gpio-579 (GPIO8 ) 29 | gpio-580 (GPIO9 ) 30 | gpio-581 (GPIO10 ) 31 | gpio-582 (GPIO11 ) 32 | gpio-583 (GPIO12 ) 33 | gpio-584 (GPIO13 ) 34 | gpio-585 (GPIO14 ) 35 | gpio-586 (GPIO15 ) 36 | gpio-587 (GPIO16 ) 37 | gpio-588 (GPIO17 ) 38 | gpio-589 (GPIO18 ) 39 | gpio-590 (GPIO19 ) 40 | gpio-591 (GPIO20 ) 41 | gpio-592 (GPIO21 ) 42 | gpio-593 (GPIO22 ) 43 | gpio-594 (GPIO23 ) 44 | gpio-595 (GPIO24 ) 45 | gpio-596 (GPIO25 ) 46 | gpio-597 (GPIO26 ) 47 | gpio-598 (GPIO27 ) 48 | ``` 49 | 50 | ## Hardware Required 51 | 52 | A Raspberry Pi 4B with WiFi connection, a LED and a 220 ohm resistor. 53 | A 32 Gb mini sd card for the raspberry OS. 54 | 55 | For example: 56 | 57 | https://www.amazon.de/dp/B0C7KXMP7W 58 | 59 | https://www.amazon.de/dp/B07WYX8M76 60 | 61 | Or if you need a case, sd card and power cable: 62 | https://www.amazon.de/RasTech-Raspberry-Active-Cooler-Readers/dp/B0D2WYFS23?th=1 63 | 64 | For american builders i would recommend: 65 | https://www.amazon.com/RasTech-Raspberry-Active-Cooler-Readers/dp/B0D2WYFS23 66 | and 67 | https://www.amazon.com/SunFounder-Raspberry-Compatible-Electronics-Programming/dp/B07WV2HYC6 68 | 69 | There may be cheaper and better options. This is an example, any Raspberry 4b and any starter kit with a LED and a resistor will do. Probably a raspberry nano/pico or similar would also work. 70 | 71 | ## Setup Raspberry 72 | 73 | Insert the SD card into your computer. 74 | 75 | Install the Raspberry OS from here: https://www.raspberrypi.com/software/ 76 | 77 | Make sure to add the correct wifi information and the user and password and enable SSH. Otherwise you will need a monitor to connect to it later. 78 | 79 | Write the os onto the sd card with the Raspberry Pi imager and then put the sd card into the raspberry pi and connect it to a power source. 80 | 81 | Bildschirmfoto 2023-08-14 um 15 10 09 82 | 83 | ### optional switch wifi access via ethernet cable 84 | 85 | If you did not setup the wifi password in the setup you can also connect the raspberry pi via lan cable to your router and then ssh into it. This is also helpful if you want to connect to it from a different network if you move somewhere else. 86 | - Connect your computer via lan cable to the raspberry pi (Probably need a connector from usb-c to lan) 87 | - ssh jonas@raspberrypi.local 88 | - sudo raspi-config and change under system -> wifi to the new wifi network by adding the SSID and the password 89 | 90 | ## Quick blinking test 91 | 92 | Connect pin 18 to one side of the LED with a 220 Ohm resistor and pin 16 to the other side like so: 93 | Bildschirmfoto 2023-08-14 um 15 10 09 94 | Bildschirmfoto 2023-08-14 um 15 10 09 95 | 96 | open terminal 97 | 98 | ```console 99 | ping raspberrypi.local -> Copy ip address 100 | ssh yourUserName@TheCopiedIpAddress (ssh jonas@192.168.1.183) 101 | 102 | cd Documents 103 | nano LED.py 104 | ``` 105 | 106 | Copy this in the File: 107 | 108 | ```python 109 | import RPi.GPIO as GPIO 110 | import time 111 | GPIO.setmode(GPIO.BCM) 112 | GPIO.setwarnings(False) 113 | GPIO.setup(18,GPIO.OUT) 114 | print ("LED on") 115 | GPIO.output(18,GPIO.HIGH) 116 | time.sleep(3) 117 | print ("LED off") 118 | GPIO.output(18,GPIO.LOW) 119 | ``` 120 | 121 | use ctrl +x to exit and y to save 122 | 123 | sudo python LED.py 124 | 125 | If everything is set up correctly the LED should blink for 3 seconds. 126 | 127 | 128 | ## Install Node on the Raspberry Pi: 129 | 130 | We want to use js so we can easily use the Solana web3 library. 131 | 132 | 1. Type the command: 133 | ```console 134 | sudo apt update 135 | ``` 136 | 137 | 2. Then, install Node.js with the command: 138 | ```console 139 | sudo apt install nodejs 140 | ``` 141 | 142 | 3. Confirm that the installation was successful by checking the available version: 143 | ```console 144 | nodejs -v 145 | ``` 146 | 147 | 4. Install the Node.js package manager (npm): 148 | ```console 149 | sudo apt install npm 150 | ``` 151 | 152 | 5. Verify the installed version: 153 | ```console 154 | npm -v 155 | ``` 156 | 157 | Install nvm: (https://github.com/nvm-sh/nvm) 158 | 159 | ```console 160 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.4/install.sh | bash 161 | ``` 162 | 163 | ```console 164 | export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")" 165 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm 166 | ``` 167 | 168 | ```console 169 | nvm install v18.19.0 170 | nvm use v18.19.0 171 | node --version 172 | ``` 173 | 174 | ## Blink script in Node.js 175 | 176 | ```console 177 | mkdir led 178 | cd led 179 | npm install onoff 180 | nano blink.js 181 | ``` 182 | 183 | Paste the following code in the file: 184 | 185 | ```js 186 | var Gpio = require('onoff').Gpio; //include onoff to interact with the GPIO 187 | var LED = new Gpio(18, 'out'); //use GPIO pin 18, and specify that it is output 188 | var blinkInterval = setInterval(blinkLED, 250); //run the blinkLED function every 250ms 189 | 190 | function blinkLED() { //function to start blinking 191 | if (LED.readSync() === 0) { //check the pin state, if the state is 0 (or off) 192 | LED.writeSync(1); //set pin state to 1 (turn LED on) 193 | } else { 194 | LED.writeSync(0); //set pin state to 0 (turn LED off) 195 | } 196 | } 197 | 198 | function endBlink() { //function to stop blinking 199 | clearInterval(blinkInterval); // Stop blink intervals 200 | LED.writeSync(0); // Turn LED off 201 | LED.unexport(); // Unexport GPIO to free resources 202 | } 203 | setTimeout(endBlink, 5000); //stop blinking after 5 seconds 204 | ``` 205 | 206 | run it with: 207 | 208 | ```console 209 | sudo node blink.js 210 | ``` 211 | 212 | ## The program 213 | 214 | The program is a small anchor program which has a boolean which indicated if the LED should be on or off. 215 | Not that the program is not yet connected to the LED. We will do that later. 216 | Also the seed for the LED account is just a string, so everyone can call this function and switch it on or off. You could add a public key or any other string here to have only certain wallets able to switch the LED on or off. 217 | 218 | ```rust 219 | use anchor_lang::prelude::*; 220 | 221 | declare_id!("F7F5ZTEMU6d5Ac8CQEJKBGWXLbte1jK2Kodyu3tNtvaj"); 222 | 223 | #[program] 224 | pub mod led_switch { 225 | use super::*; 226 | 227 | pub fn initialize(ctx: Context) -> Result<()> { 228 | ctx.accounts.led_switch.is_on = false; 229 | Ok(()) 230 | } 231 | 232 | pub fn switch(ctx: Context, is_on: bool) -> Result<()> { 233 | ctx.accounts.led_switch.is_on = is_on; 234 | Ok(()) 235 | } 236 | } 237 | 238 | #[derive(Accounts)] 239 | pub struct Initialize<'info> { 240 | #[account(init, payer = authority, space = 8 + 8, seeds = [b"led-switch"], bump)] 241 | pub led_switch: Account<'info, LedSwitch>, 242 | #[account(mut)] 243 | pub authority: Signer<'info>, 244 | pub system_program: Program<'info, System>, 245 | pub rent: Sysvar<'info, Rent>, 246 | } 247 | 248 | #[derive(Accounts)] 249 | pub struct Switch<'info> { 250 | #[account(mut, seeds = [b"led-switch"], bump)] 251 | pub led_switch: Account<'info, LedSwitch>, 252 | #[account(mut)] 253 | pub authority: Signer<'info>, 254 | } 255 | 256 | #[account] 257 | pub struct LedSwitch { 258 | pub is_on: bool, 259 | } 260 | ``` 261 | 262 | ## Now we want to listen to the account via websocket and trigger the LED 263 | 264 | The best way to code on the raspberry pi directly is to use the VS code SSH extension from Microsoft. 265 | Install it and click the little blue icon on the bottom left of VSCode to connect. Use the same command you would use in the terminal `ssh jonas@raspberrypi.local` and it will ask you for a password on the top. 266 | Then open the folder and start coding. 267 | 268 | You can also use scp or rsync to copy the files from the raspberry folder to the raspberry pi. 269 | (If you have problems coping files like I had you can also use VNC Viewer to copy the files.) 270 | Notice that you need to copy the anchor types from the target folder to the raspberry folder whenever you do changes. (I didn't manage to get it to work without copying the types file over next to the led.ts file.) 271 | 272 | Copy all the files from the raspberry folder in this repo on to the raspberry pi. When you deploy your own program donr forget to replace the anchor types as well as your program id. 273 | 274 | Then maybe you need to install node types and type script. 275 | 276 | ```console 277 | npm install -D typescript 278 | npm install -D ts-node 279 | ``` 280 | 281 | Then you can run 282 | 283 | ```console 284 | npm i 285 | and then run the script led.ts 286 | npx ts-node led.ts 287 | ``` 288 | 289 | Don't run it with sudo. That gave me problems. 290 | You may need to change the rights of the directory to be able to write to it: 291 | ```console 292 | chmod -R 777 /directory 293 | ``` 294 | 295 | Now the LED will already have the correct state that is in the LED account. Next we gonna change it via Solana Pay Transaction requests. 296 | 297 | ```js 298 | import * as anchor from "@coral-xyz/anchor"; 299 | import { Program } from "@coral-xyz/anchor"; 300 | import { IDL, LedSwitch } from "../target/types/led_switch"; 301 | import { clusterApiUrl, Connection, Keypair, PublicKey } from "@solana/web3.js"; 302 | import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; 303 | 304 | var Gpio = require('onoff').Gpio; //include onoff to interact with the GPIO 305 | var LED = new Gpio(18, 'out'); //use GPIO pin 18, and specify that it is output 306 | 307 | let connection = new Connection(clusterApiUrl("devnet")); 308 | let wallet = new NodeWallet(new Keypair()); 309 | const provider = new anchor.AnchorProvider(connection, wallet, { 310 | commitment: "processed", 311 | }); 312 | anchor.setProvider(provider); 313 | 314 | const program = new Program(IDL, "F7F5ZTEMU6d5Ac8CQEJKBGWXLbte1jK2Kodyu3tNtvaj", { connection }) 315 | 316 | console.log("Program ID", program.programId.toString()); 317 | 318 | startListeningToLedSwitchAccount(); 319 | 320 | async function startListeningToLedSwitchAccount() { 321 | const ledSwitchPDA = await anchor.web3.PublicKey.findProgramAddressSync( 322 | [ 323 | Buffer.from("led-switch"), 324 | ], 325 | program.programId, 326 | )[0]; 327 | 328 | const ledSwitchAccount = await program.account.ledSwitch.fetch( 329 | ledSwitchPDA 330 | ) 331 | 332 | console.log(JSON.stringify(ledSwitchAccount)); 333 | console.log("Led is: ", ledSwitchAccount.isOn); 334 | if (ledSwitchAccount.isOn) { 335 | LED.writeSync(1); 336 | } else { 337 | LED.writeSync(0); 338 | } 339 | 340 | connection.onAccountChange(ledSwitchPDA, (account) => { 341 | const decoded = program.coder.accounts.decode( 342 | "ledSwitch", 343 | account.data 344 | ) 345 | 346 | if (decoded.isOn) { 347 | LED.writeSync(1); 348 | } else { 349 | LED.writeSync(0); 350 | } 351 | console.log("Account changed. Led is: ", decoded.isOn); 352 | }, "processed") 353 | }; 354 | ``` 355 | 356 | Now the LED will always show the state of the LED account. Next we gonna change the state of the LED account via Solana Pay Transaction requests. 357 | 358 | ## Create a Solana Pay Transaction Request 359 | 360 | Solana Pay is not only for payments, but can request any transaction to be signed. 361 | The transaction can be signed by any wallet that supports Solana Pay. 362 | It consists of two parts. The api which can be found in led-switch/app/pages/api/transaction.ts and the creation of the QR code which can be found in led-switch/app/app/page.tsx and the QrCode in led-switch/app/app/components/qr-code.tsx. 363 | 364 | Basically what is happening is that the wallet sends a get request to our API to get a name and icon and then the transaction is created in the nextJS api and send to the wallet. The wallet then signs it. When the transaction is confirmed the LED account is updated and since on the raspberry pi we have a websocket connection to that account the LED turns on or off. 365 | 366 | Here is the example API already deployed to vercel for you to try out: 367 | https://solana-depin-examples-led.vercel.app/ 368 | 369 | To run the solana pay transaction request app use: 370 | 371 | ```console 372 | cd app 373 | yarn install 374 | yarn dev 375 | ``` 376 | 377 | open http://localhost:3000 in your browser. 378 | Notice that the QR code is not working yet. We need to be able to access is from the distance. 379 | For that we use ngrok to create a tunnel to our local server. 380 | Make an account and install ngrok https://ngrok.com/ 381 | open a terminal and type: 382 | ngrok http 3000 383 | Then copy the url from the terminal and open it in the browser. 384 | Now the QR code should work and switch the LED on and off. 385 | 386 | Now you can also copy and print the QR codes and glue them somewhere next to the LED for example. 387 | 388 | ## Deploy 389 | 390 | Since you don't want to run ngrok every time it makes sense to deploy the app. Either on the raspberry itself or on for example vercel.com. 391 | 392 | 393 | ## Where to go from here 394 | 395 | 396 | Now you just need imagination to think of what you can do with this. 397 | For example you could create a game where you have to scan the QR code to switch the LED on and off or control switches and ramps for marbles. 398 | Or you could use a lock to open a door. Or even only open it when there is certain NFT in that wallet. 399 | Or you could use it to water plants or feed a hamster during a live stream. 400 | Or attach it to a car which can be controlled by the audience. 401 | 402 | Have fun and let me know what you build with it! 403 | 404 | 405 | 406 | 407 | ### Optional step: auto start the script on boot (WIP) 408 | 409 | Here are three ways on how to start the script on boot: 410 | https://www.makeuseof.com/how-to-run-a-raspberry-pi-program-script-at-startup/ 411 | 412 | I went for step 2 the cron job since it didn't want to risk there being problems during the startup. 413 | 414 | First I created a start.sh file: 415 | ```bash 416 | nano /home/jonas/Documents/led-switch/led-switch/raspberry/start.sh 417 | ``` 418 | add 419 | ```bash 420 | npx ts-node /home/jonas/Documents/led-switch/led-switch/raspberry/led.ts 421 | ``` 422 | then run 423 | ```bash 424 | chmod +x /home/jonas/Documents/led-switch/led-switch/raspberry/start.sh 425 | ``` 426 | 427 | Then I added this line to the cron config: 428 | 429 | ```bash 430 | crontab -e 431 | @reboot sleep 10 && /home/jonas/Documents/led-switch/led-switch/raspberry/start.sh & 432 | ``` 433 | 434 | To enable cron logs 435 | ```bash 436 | sudo nano /etc/rsyslog.conf 437 | ``` 438 | and uncomment the line 439 | ```bash 440 | # cron.* /var/log/cron.log 441 | ``` 442 | Then restart the service 443 | ```bash 444 | sudo service rsyslog restart 445 | ``` 446 | Now you can check the logs with 447 | ```bash 448 | sudo cat /var/log/cron.log 449 | ``` 450 | Now reboot the raspberry and check if the LED is turning on. (Make sure the program state is set to true ;) ) 451 | ```bash 452 | sudo reboot 453 | ``` 454 | 455 | If you get error saying that the package crypto is not available it's probably because your sude node version is too low. 456 | You can check the node version with 457 | ```bash 458 | sudo node -v 459 | ``` 460 | Update it as described above just using sudo command. 461 | 462 | 463 | 464 | 465 | 466 | -------------------------------------------------------------------------------- /led-switch/app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /led-switch/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /led-switch/app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /led-switch/app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/transaction](http://localhost:3000/api/transaction). This endpoint can be edited in `pages/api/transaction.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | To make the solana pay transaction requests work locally you need to make the page accessible from outside. Easiest way to do that is to use [`ngrok`](https://ngrok.com/). Run `ngrok http 3000` in the terminal and open the url provided in your browser instead of `localhost:3000`. 26 | 27 | ## Learn More 28 | 29 | To learn more about Next.js, take a look at the following resources: 30 | 31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 32 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 33 | 34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 35 | 36 | ## Deploy on Vercel 37 | 38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 39 | 40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 41 | -------------------------------------------------------------------------------- /led-switch/app/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /led-switch/app/app/head.tsx: -------------------------------------------------------------------------------- 1 | export default function Head() { 2 | return ( 3 | <> 4 | Tug of War Solana Pay Game 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /led-switch/app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Wallet } from '@/src/Wallet' 2 | import './globals.css' 3 | 4 | export default function RootLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | return ( 10 | 11 | {/* 12 | will contain the components returned by the nearest parent 13 | head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head 14 | */} 15 | 16 | 17 | {children} 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /led-switch/app/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; // this makes next know that this page should be rendered in the client 2 | import { useEffect, useState } from 'react'; 3 | import { CONNECTION, LED_SWITCH_PROGRAM_ID, LED_SWITCH_PROGRAM, LED_SWITCH_PDA } from '@/src/util/const'; 4 | import PayQR from '@/src/components/PayQR'; 5 | import { Wallet } from '@/src/Wallet'; 6 | import { 7 | WalletModalProvider, 8 | WalletDisconnectButton, 9 | WalletMultiButton 10 | } from '@solana/wallet-adapter-react-ui'; 11 | import { Transaction, TransactionInstruction } from '@solana/web3.js'; 12 | import { useConnection, useWallet } from "@solana/wallet-adapter-react"; 13 | var sha256 = require('sha256') 14 | 15 | export default function Home() { 16 | const [ledState, setLedState] = useState() 17 | const [isOn, SetIsOn] = useState(); 18 | const { connection } = useConnection(); 19 | const { publicKey, sendTransaction } = useWallet(); 20 | 21 | useEffect(() => { 22 | 23 | CONNECTION.onAccountChange( 24 | LED_SWITCH_PDA, 25 | (updatedAccountInfo, context) => { 26 | { 27 | const decoded = LED_SWITCH_PROGRAM.coder.accounts.decode( 28 | "ledSwitch", 29 | updatedAccountInfo.data 30 | ) 31 | setLedState(decoded); 32 | SetIsOn(decoded.isOn); 33 | } 34 | }, 35 | "confirmed" 36 | ); 37 | 38 | const getState = async () => { 39 | const gameData = await LED_SWITCH_PROGRAM.account.ledSwitch.fetch( 40 | LED_SWITCH_PDA, 41 | ); 42 | setLedState(gameData); 43 | SetIsOn(gameData.isOn); 44 | }; 45 | 46 | getState(); 47 | 48 | }, []); 49 | 50 | return ( 51 |
    52 | {
    53 |
    54 | 55 |
    56 | 57 | {/* If you want to have wallet connector and call functions from the web page as well this is how you can do it. */ 58 | /*gameDataState && ( 59 | <> 60 | 61 | 62 | 63 | ) 64 | 67 | */} 68 | 69 |
    70 |
    71 |

    72 | Solana LED Switch 73 |

    74 | Scan the QR code to switch the LED on/off via a Solana pay transaction request. 75 |

    76 | 77 |

    78 | { 79 | "Led status: " + [ledState ? ledState.isOn ? "On" : "Off" : "loading"] 80 | } 81 |

    82 | 83 |
    84 |
    85 | 86 |
  • 87 | 88 | {isOn != null && isOn && ( 89 | 90 | )} 91 | 92 | {isOn != null && !isOn && ( 93 | 94 | )} 95 | 96 | {!ledState && ( 97 | 98 | )} 99 |
  • 100 |
    101 |
    102 |
    } 103 |
    104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /led-switch/app/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | }, 6 | images: { 7 | domains: ['images.unsplash.com'] 8 | }, 9 | } 10 | 11 | module.exports = nextConfig 12 | -------------------------------------------------------------------------------- /led-switch/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solution", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "export": "next export", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "proxy": "local-ssl-proxy --source 3001 --target 3000" 12 | }, 13 | "dependencies": { 14 | "@next/font": "13.1.2", 15 | "@solana/pay": "^0.2.4", 16 | "@solana/spl-token": "^0.3.7", 17 | "@coral-xyz/anchor": "0.30.1", 18 | "@solana/wallet-adapter-react": "^0.15.28", 19 | "@solana/wallet-adapter-react-ui": "^0.9.27", 20 | "@solana/wallet-adapter-wallets": "^0.19.11", 21 | "@solana/web3.js": "^1.73.2", 22 | "@types/node": "18.11.18", 23 | "@types/qrcode": "^1.5.0", 24 | "@types/react": "18.0.27", 25 | "@types/react-dom": "18.0.10", 26 | "bigint-buffer": "^1.1.5", 27 | "bignumber.js": "^9.1.1", 28 | "borsh": "^0.7.0", 29 | "eslint": "8.32.0", 30 | "eslint-config-next": "13.1.2", 31 | "next": "13.1.2", 32 | "qrcode": "^1.5.1", 33 | "react": "^18.2.0", 34 | "react-dom": "18.2.0", 35 | "sha256": "^0.2.0" 36 | }, 37 | "devDependencies": { 38 | "autoprefixer": "^10.4.13", 39 | "encoding": "^0.1.13", 40 | "local-ssl-proxy": "^1.3.0", 41 | "postcss": "^8.4.21", 42 | "tailwindcss": "^3.2.4", 43 | "ts-loader": "^9.4.4", 44 | "typescript": "^5.1.6" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /led-switch/app/pages/api/transaction.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import { PublicKey, Transaction } from '@solana/web3.js'; 4 | import { CONNECTION, LED_SWITCH_PDA, LED_SWITCH_PROGRAM } from '@/src/util/const'; 5 | 6 | type POST = { 7 | transaction: string; 8 | message: string; 9 | }; 10 | 11 | type GET = { 12 | label: string; 13 | icon: string; 14 | }; 15 | 16 | function getFromPayload(req: NextApiRequest, payload: string, field: string): string { 17 | function parseError() { throw new Error(`${payload} parse error: missing ${field}`) }; 18 | let value; 19 | if (payload === 'Query') { 20 | if (!(field in req.query)) parseError(); 21 | value = req.query[field]; 22 | } 23 | if (payload === 'Body') { 24 | if (!req.body || !(field in req.body)) parseError(); 25 | value = req.body[field]; 26 | } 27 | if (value === undefined || value.length === 0) parseError(); 28 | return typeof value === 'string' ? value : value[0]; 29 | } 30 | 31 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 32 | if (req.method === 'GET') { 33 | return get(req, res); 34 | } 35 | 36 | if (req.method === 'POST') { 37 | return post(req, res); 38 | } 39 | } 40 | 41 | const get = async (req: NextApiRequest, res: NextApiResponse) => { 42 | const label = 'LED Switch'; 43 | const icon = 44 | 'https://media.discordapp.net/attachments/964525722301501477/978683590743302184/sol-logo1.png'; 45 | 46 | res.status(200).json({ 47 | label, 48 | icon, 49 | }); 50 | }; 51 | 52 | const post = async (req: NextApiRequest, res: NextApiResponse) => { 53 | 54 | const accountField = getFromPayload(req, 'Body', 'account'); 55 | const instructionField = getFromPayload(req, 'Query', 'instruction'); 56 | 57 | const sender = new PublicKey(accountField); 58 | 59 | const transaction = new Transaction(); 60 | const latestBlockhash = await CONNECTION.getLatestBlockhash(); 61 | transaction.feePayer = sender; 62 | transaction.recentBlockhash = latestBlockhash.blockhash; 63 | 64 | let message; 65 | if (instructionField == "switch_on") { 66 | let ix = await LED_SWITCH_PROGRAM.methods.switch(true).accounts( 67 | { 68 | ledSwitch: LED_SWITCH_PDA, 69 | authority: sender 70 | }, 71 | ).instruction(); 72 | 73 | transaction.add(ix); 74 | 75 | message = 'Switch on!'; 76 | } else if (instructionField == "switch_off") { 77 | let ix = await LED_SWITCH_PROGRAM.methods.switch(false).accounts( 78 | { 79 | ledSwitch: LED_SWITCH_PDA, 80 | authority: sender 81 | }, 82 | ).instruction(); 83 | 84 | transaction.add(ix); 85 | message = 'Switch off !'; 86 | } else { 87 | message = 'Unknown instruction'; 88 | } 89 | 90 | // Serialize and return the unsigned transaction. 91 | const serializedTransaction = transaction.serialize({ 92 | verifySignatures: false, 93 | requireAllSignatures: false, 94 | }); 95 | 96 | const base64Transaction = serializedTransaction.toString('base64'); 97 | 98 | res.status(200).send({ transaction: base64Transaction, message }); 99 | }; 100 | -------------------------------------------------------------------------------- /led-switch/app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /led-switch/app/public/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/solana-depin-examples/3f8cd5b1519efd9c44e3261a77ec576a7fedcb6c/led-switch/app/public/bg.jpg -------------------------------------------------------------------------------- /led-switch/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/solana-depin-examples/3f8cd5b1519efd9c44e3261a77ec576a7fedcb6c/led-switch/app/public/favicon.ico -------------------------------------------------------------------------------- /led-switch/app/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/solana-depin-examples/3f8cd5b1519efd9c44e3261a77ec576a7fedcb6c/led-switch/app/public/icon.png -------------------------------------------------------------------------------- /led-switch/app/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /led-switch/app/public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /led-switch/app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /led-switch/app/src/Wallet.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { FC, useMemo } from 'react'; 3 | import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; 4 | import { WalletAdapterNetwork } from '@solana/wallet-adapter-base'; 5 | import { PhantomWalletAdapter } from '@solana/wallet-adapter-phantom'; 6 | import { 7 | WalletModalProvider, 8 | WalletDisconnectButton, 9 | WalletMultiButton 10 | } from '@solana/wallet-adapter-react-ui'; 11 | import { clusterApiUrl } from '@solana/web3.js'; 12 | 13 | // Default styles that can be overridden by your app 14 | require('@solana/wallet-adapter-react-ui/styles.css'); 15 | 16 | type Props = { 17 | children?: React.ReactNode 18 | }; 19 | 20 | export const Wallet: FC = ({children}) => { 21 | // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'. 22 | const network = WalletAdapterNetwork.Devnet; 23 | 24 | // You can also provide a custom RPC endpoint. 25 | const endpoint = useMemo(() => clusterApiUrl(network), [network]); 26 | 27 | const wallets = useMemo( 28 | () => [ 29 | 30 | new PhantomWalletAdapter(), 31 | ], 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | [network] 34 | ); 35 | 36 | return ( 37 | 38 | 39 | 40 | { children } 41 | 42 | 43 | 44 | ); 45 | }; -------------------------------------------------------------------------------- /led-switch/app/src/components/PayQR.tsx: -------------------------------------------------------------------------------- 1 | import { encodeURL, createQR } from '@solana/pay'; 2 | import { FC, useEffect, useRef } from 'react'; 3 | 4 | type TransactionRequestQRProps = { 5 | instruction: string; 6 | }; 7 | 8 | const queryBuilder = (baseUrl: string, params: string[][]) => { 9 | let url = baseUrl + '?'; 10 | params.forEach((p, i) => url += p[0] + '=' + p[1] + (i != params.length - 1 ? '&' : '')); 11 | console.log(url) 12 | return url; 13 | } 14 | 15 | const PayQR: FC = ( 16 | { instruction } 17 | ) => { 18 | const qrRef = useRef(null) 19 | 20 | useEffect(() => { 21 | const params = [ 22 | ['instruction', instruction], 23 | ]; 24 | 25 | const apiUrl = queryBuilder( 26 | `${window.location.protocol}//${window.location.host}/api/transaction`, 27 | params, 28 | ); 29 | 30 | const qr = createQR( 31 | encodeURL({ link: new URL(apiUrl) }), 32 | 360, 33 | 'transparent' 34 | ); 35 | 36 | qr.update({ backgroundOptions: { round: 1000 } }); 37 | qr.update({ type: 'canvas' }); 38 | 39 | if (qrRef.current != null) { 40 | qrRef.current.innerHTML = ''; 41 | qr.append(qrRef.current) 42 | } 43 | 44 | }, []) 45 | 46 | return ( 47 |
    48 | 49 |
    50 |

    {instruction}

    51 |
    52 | 53 |
    54 | 55 |
    56 | ); 57 | }; 58 | 59 | export default PayQR; 60 | -------------------------------------------------------------------------------- /led-switch/app/src/util/const.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from "@solana/web3.js"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import { IDL, LedSwitch } from "./led_switch"; 4 | 5 | export const CONNECTION = new Connection(process.env.NEXT_PUBLIC_RPC ? process.env.NEXT_PUBLIC_RPC : 'https://api.devnet.solana.com', { 6 | wsEndpoint: process.env.NEXT_PUBLIC_WSS_RPC ? process.env.NEXT_PUBLIC_WSS_RPC : "wss://api.devnet.solana.com", 7 | commitment: 'confirmed' 8 | }); 9 | 10 | export const LED_SWITCH_PROGRAM_ID = new PublicKey('F7F5ZTEMU6d5Ac8CQEJKBGWXLbte1jK2Kodyu3tNtvaj'); 11 | 12 | export const LED_SWITCH_PROGRAM = new Program(IDL, LED_SWITCH_PROGRAM_ID, { connection: CONNECTION }) 13 | 14 | export const LED_SWITCH_PDA = PublicKey.findProgramAddressSync( 15 | [ 16 | Buffer.from("led-switch"), 17 | ], 18 | LED_SWITCH_PROGRAM_ID, 19 | )[0]; -------------------------------------------------------------------------------- /led-switch/app/src/util/led_switch.ts: -------------------------------------------------------------------------------- 1 | export type LedSwitch = { 2 | "version": "0.1.0", 3 | "name": "led_switch", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [ 8 | { 9 | "name": "ledSwitch", 10 | "isMut": true, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "authority", 15 | "isMut": true, 16 | "isSigner": true 17 | }, 18 | { 19 | "name": "systemProgram", 20 | "isMut": false, 21 | "isSigner": false 22 | }, 23 | { 24 | "name": "rent", 25 | "isMut": false, 26 | "isSigner": false 27 | } 28 | ], 29 | "args": [] 30 | }, 31 | { 32 | "name": "switch", 33 | "accounts": [ 34 | { 35 | "name": "ledSwitch", 36 | "isMut": true, 37 | "isSigner": false 38 | }, 39 | { 40 | "name": "authority", 41 | "isMut": true, 42 | "isSigner": true 43 | } 44 | ], 45 | "args": [ 46 | { 47 | "name": "isOn", 48 | "type": "bool" 49 | } 50 | ] 51 | } 52 | ], 53 | "accounts": [ 54 | { 55 | "name": "ledSwitch", 56 | "type": { 57 | "kind": "struct", 58 | "fields": [ 59 | { 60 | "name": "isOn", 61 | "type": "bool" 62 | } 63 | ] 64 | } 65 | } 66 | ] 67 | }; 68 | 69 | export const IDL: LedSwitch = { 70 | "version": "0.1.0", 71 | "name": "led_switch", 72 | "instructions": [ 73 | { 74 | "name": "initialize", 75 | "accounts": [ 76 | { 77 | "name": "ledSwitch", 78 | "isMut": true, 79 | "isSigner": false 80 | }, 81 | { 82 | "name": "authority", 83 | "isMut": true, 84 | "isSigner": true 85 | }, 86 | { 87 | "name": "systemProgram", 88 | "isMut": false, 89 | "isSigner": false 90 | }, 91 | { 92 | "name": "rent", 93 | "isMut": false, 94 | "isSigner": false 95 | } 96 | ], 97 | "args": [] 98 | }, 99 | { 100 | "name": "switch", 101 | "accounts": [ 102 | { 103 | "name": "ledSwitch", 104 | "isMut": true, 105 | "isSigner": false 106 | }, 107 | { 108 | "name": "authority", 109 | "isMut": true, 110 | "isSigner": true 111 | } 112 | ], 113 | "args": [ 114 | { 115 | "name": "isOn", 116 | "type": "bool" 117 | } 118 | ] 119 | } 120 | ], 121 | "accounts": [ 122 | { 123 | "name": "ledSwitch", 124 | "type": { 125 | "kind": "struct", 126 | "fields": [ 127 | { 128 | "name": "isOn", 129 | "type": "bool" 130 | } 131 | ] 132 | } 133 | } 134 | ] 135 | }; 136 | -------------------------------------------------------------------------------- /led-switch/app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx}", 5 | "./components/**/*.{js,ts,jsx,tsx}", 6 | 7 | // Or if using `src` directory: 8 | "./src/**/*.{js,ts,jsx,tsx}", 9 | 10 | // Or if using `app` directory: 11 | "./app/**/*.{js,ts,jsx,tsx}", 12 | ], 13 | theme: { 14 | extend: {}, 15 | }, 16 | plugins: [], 17 | } -------------------------------------------------------------------------------- /led-switch/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": ["./*"] 25 | } 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /led-switch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", 4 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check", 5 | "build": "webpack --mode production" 6 | }, 7 | "dependencies": { 8 | "@coral-xyz/anchor": "^0.30.1" 9 | }, 10 | "devDependencies": { 11 | "chai": "^4.3.4", 12 | "mocha": "^9.0.3", 13 | "ts-mocha": "^10.0.0", 14 | "@types/bn.js": "^5.1.0", 15 | "@types/chai": "^4.3.0", 16 | "@types/mocha": "^9.0.0", 17 | "typescript": "^4.3.5", 18 | "prettier": "^2.6.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /led-switch/programs/led-switch/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "led-switch" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "led_switch" 10 | 11 | [features] 12 | no-entrypoint = [] 13 | no-idl = [] 14 | no-log-ix-name = [] 15 | cpi = ["no-entrypoint"] 16 | default = [] 17 | idl-build = ["anchor-lang/idl-build"] 18 | 19 | [dependencies] 20 | anchor-lang = "0.30.1" 21 | -------------------------------------------------------------------------------- /led-switch/programs/led-switch/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /led-switch/programs/led-switch/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | declare_id!("F7F5ZTEMU6d5Ac8CQEJKBGWXLbte1jK2Kodyu3tNtvaj"); 4 | 5 | #[program] 6 | pub mod led_switch { 7 | use super::*; 8 | 9 | pub fn initialize(ctx: Context) -> Result<()> { 10 | ctx.accounts.led_switch.is_on = false; 11 | Ok(()) 12 | } 13 | 14 | pub fn switch(ctx: Context, is_on: bool) -> Result<()> { 15 | ctx.accounts.led_switch.is_on = is_on; 16 | Ok(()) 17 | } 18 | } 19 | 20 | #[derive(Accounts)] 21 | pub struct Initialize<'info> { 22 | #[account(init, payer = authority, space = 8 + 8, seeds = [b"led-switch"], bump)] 23 | pub led_switch: Account<'info, LedSwitch>, 24 | #[account(mut)] 25 | pub authority: Signer<'info>, 26 | pub system_program: Program<'info, System>, 27 | pub rent: Sysvar<'info, Rent>, 28 | } 29 | 30 | #[derive(Accounts)] 31 | pub struct Switch<'info> { 32 | #[account(mut, seeds = [b"led-switch"], bump)] 33 | pub led_switch: Account<'info, LedSwitch>, 34 | #[account(mut)] 35 | pub authority: Signer<'info>, 36 | } 37 | 38 | #[account] 39 | pub struct LedSwitch { 40 | pub is_on: bool, 41 | } 42 | -------------------------------------------------------------------------------- /led-switch/raspberry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", 4 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check", 5 | "build": "webpack" 6 | }, 7 | "dependencies": { 8 | "@coral-xyz/anchor": "^0.30.1", 9 | "onoff": "^6.0.3", 10 | "webpack": "^5.88.2" 11 | }, 12 | "devDependencies": { 13 | "@types/bn.js": "^5.1.0", 14 | "@types/chai": "^4.3.0", 15 | "@types/mocha": "^9.0.0", 16 | "chai": "^4.3.4", 17 | "mocha": "^9.0.3", 18 | "prettier": "^2.6.2", 19 | "ts-mocha": "^10.0.0", 20 | "typescript": "^4.3.5", 21 | "webpack-cli": "^5.1.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /led-switch/raspberry/src/led.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import LedSwitch from "./led_switch"; 4 | import idl from "./led_switch.json"; 5 | 6 | import { clusterApiUrl, Connection, Keypair } from "@solana/web3.js"; 7 | import { 8 | Program, 9 | AnchorProvider, 10 | EventParser, 11 | Idl, 12 | Wallet, 13 | } from "@coral-xyz/anchor"; 14 | import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; 15 | 16 | var Gpio = require('onoff').Gpio; //include onoff to interact with the GPIO 17 | var LED = new Gpio(589, 'out'); //use GPIO pin 18, and specify that it is output 18 | 19 | let connection = new Connection(clusterApiUrl("devnet")); 20 | let wallet = new NodeWallet(new Keypair()); 21 | const provider = new anchor.AnchorProvider(connection, wallet, { 22 | commitment: "processed", 23 | }); 24 | anchor.setProvider(provider); 25 | 26 | const program = new Program(idl as Idl, { connection }) 27 | 28 | console.log("Program ID", program.programId.toString()); 29 | 30 | startListeningToLedSwitchAccount(); 31 | 32 | async function startListeningToLedSwitchAccount() { 33 | const ledSwitchPDA = await anchor.web3.PublicKey.findProgramAddressSync( 34 | [ 35 | Buffer.from("led-switch"), 36 | ], 37 | program.programId, 38 | )[0]; 39 | 40 | const ledSwitchAccount = await program.account.ledSwitch.fetch( 41 | ledSwitchPDA 42 | ) 43 | 44 | console.log(JSON.stringify(ledSwitchAccount)); 45 | console.log("Led is: ", ledSwitchAccount.isOn); 46 | if (ledSwitchAccount.isOn) { 47 | LED.writeSync(1); 48 | } else { 49 | LED.writeSync(0); 50 | } 51 | 52 | connection.onAccountChange(ledSwitchPDA, (account) => { 53 | const decoded = program.coder.accounts.decode( 54 | "ledSwitch", 55 | account.data 56 | ) 57 | 58 | if (decoded.isOn) { 59 | LED.writeSync(1); 60 | } else { 61 | LED.writeSync(0); 62 | } 63 | console.log("Account changed. Led is: ", decoded.isOn); 64 | }, "processed") 65 | }; 66 | -------------------------------------------------------------------------------- /led-switch/raspberry/src/led_switch.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "F7F5ZTEMU6d5Ac8CQEJKBGWXLbte1jK2Kodyu3tNtvaj", 3 | "metadata": { 4 | "name": "led_switch", 5 | "version": "0.1.0", 6 | "spec": "0.1.0", 7 | "description": "Created with Anchor" 8 | }, 9 | "instructions": [ 10 | { 11 | "name": "initialize", 12 | "discriminator": [ 13 | 175, 14 | 175, 15 | 109, 16 | 31, 17 | 13, 18 | 152, 19 | 155, 20 | 237 21 | ], 22 | "accounts": [ 23 | { 24 | "name": "led_switch", 25 | "writable": true, 26 | "pda": { 27 | "seeds": [ 28 | { 29 | "kind": "const", 30 | "value": [ 31 | 108, 32 | 101, 33 | 100, 34 | 45, 35 | 115, 36 | 119, 37 | 105, 38 | 116, 39 | 99, 40 | 104 41 | ] 42 | } 43 | ] 44 | } 45 | }, 46 | { 47 | "name": "authority", 48 | "writable": true, 49 | "signer": true 50 | }, 51 | { 52 | "name": "system_program", 53 | "address": "11111111111111111111111111111111" 54 | }, 55 | { 56 | "name": "rent", 57 | "address": "SysvarRent111111111111111111111111111111111" 58 | } 59 | ], 60 | "args": [] 61 | }, 62 | { 63 | "name": "switch", 64 | "discriminator": [ 65 | 74, 66 | 38, 67 | 37, 68 | 126, 69 | 251, 70 | 74, 71 | 86, 72 | 200 73 | ], 74 | "accounts": [ 75 | { 76 | "name": "led_switch", 77 | "writable": true, 78 | "pda": { 79 | "seeds": [ 80 | { 81 | "kind": "const", 82 | "value": [ 83 | 108, 84 | 101, 85 | 100, 86 | 45, 87 | 115, 88 | 119, 89 | 105, 90 | 116, 91 | 99, 92 | 104 93 | ] 94 | } 95 | ] 96 | } 97 | }, 98 | { 99 | "name": "authority", 100 | "writable": true, 101 | "signer": true 102 | } 103 | ], 104 | "args": [ 105 | { 106 | "name": "is_on", 107 | "type": "bool" 108 | } 109 | ] 110 | } 111 | ], 112 | "accounts": [ 113 | { 114 | "name": "LedSwitch", 115 | "discriminator": [ 116 | 247, 117 | 38, 118 | 144, 119 | 137, 120 | 230, 121 | 66, 122 | 103, 123 | 26 124 | ] 125 | } 126 | ], 127 | "types": [ 128 | { 129 | "name": "LedSwitch", 130 | "type": { 131 | "kind": "struct", 132 | "fields": [ 133 | { 134 | "name": "is_on", 135 | "type": "bool" 136 | } 137 | ] 138 | } 139 | } 140 | ] 141 | } -------------------------------------------------------------------------------- /led-switch/raspberry/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /led-switch/tests/led-switch.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import { LedSwitch } from "../target/types/led_switch"; 4 | import { assert } from "chai"; 5 | 6 | describe("led-switch", () => { 7 | anchor.setProvider(anchor.AnchorProvider.env()); 8 | 9 | const program = anchor.workspace.LedSwitch as Program; 10 | const wallet = anchor.workspace.LedSwitch.provider.wallet 11 | 12 | it("Is initialized!", async () => { 13 | 14 | const ledSwitchPDA = await anchor.web3.PublicKey.findProgramAddressSync( 15 | [ 16 | Buffer.from("led-switch"), 17 | ], 18 | program.programId, 19 | )[0]; 20 | 21 | console.log("Led switch pda", ledSwitchPDA); 22 | 23 | try { 24 | const initializeTransaction = await program.methods.initialize().accounts( 25 | { 26 | ledSwitch: ledSwitchPDA, 27 | authority: wallet.publicKey, 28 | systemProgram: anchor.web3.SystemProgram.programId, 29 | rent: anchor.web3.SYSVAR_RENT_PUBKEY, 30 | }, 31 | ).rpc(); 32 | console.log("Initialize transaction signature: ", initializeTransaction); 33 | } catch (e) { 34 | console.log(e); 35 | } 36 | 37 | const switchOnTransaction = await program.methods.switch(true).accounts( 38 | { 39 | ledSwitch: ledSwitchPDA, 40 | authority: wallet.publicKey 41 | }, 42 | ).rpc(); 43 | 44 | const ledAccount = await program.account.ledSwitch.fetch( 45 | ledSwitchPDA 46 | ) 47 | console.log("Your switch on transaction signature", switchOnTransaction); 48 | 49 | assert(ledAccount.isOn === true, "Game data account is not initialized correctly. Should be on/true") 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /led-switch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /solana-bar/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | node_modules 7 | test-ledger 8 | .yarn 9 | -------------------------------------------------------------------------------- /solana-bar/.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | node_modules 6 | dist 7 | build 8 | test-ledger 9 | -------------------------------------------------------------------------------- /solana-bar/Anchor.toml: -------------------------------------------------------------------------------- 1 | [features] 2 | seeds = false 3 | skip-lint = false 4 | [programs.localnet] 5 | led_switch = "GCgyx9JPNpqX97iWQh7rqPjaignahkS8DqQGdDdfXsPQ" 6 | 7 | [registry] 8 | url = "https://api.apr.dev" 9 | 10 | [provider] 11 | cluster = "devnet" 12 | wallet = "~/.config/solana/id.json" 13 | 14 | [scripts] 15 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" 16 | -------------------------------------------------------------------------------- /solana-bar/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*" 4 | ] 5 | 6 | [profile.release] 7 | overflow-checks = true 8 | lto = "fat" 9 | codegen-units = 1 10 | [profile.release.build-override] 11 | opt-level = 3 12 | incremental = false 13 | codegen-units = 1 14 | -------------------------------------------------------------------------------- /solana-bar/README.md: -------------------------------------------------------------------------------- 1 | # Solana Bar 2 | 3 | Project name Dionysos. 4 | Example that shows how you can sell wine or and other liquids via Solana Pay transaction requests qr codes. 5 | Dionysos was the god of wine, fertility, ritual madness, religious ecstasy, and theatre in ancient Greek religion and myth. 6 | He would have loved this project. 7 | 8 | ![IMG_2731](https://github.com/solana-developers/solana-depin-examples/assets/5938789/29bc21c9-1428-4252-93a6-a12c85b8260a) 9 | 10 | 11 | ## Prerequisites 12 | 13 | If you have not worked with raspberry pi before its highly recommended to do the LED-switch example first for the complete setup of the PI including node and typescript. 14 | Here is the link: 15 | [LED-SWITCH-EXAMPLE 16 | ](https://github.com/solana-developers/solana-depin-examples/blob/main/led-switch/README.md) 17 | 18 | 19 | ## Hardware Required 20 | 21 | A Raspberry Pi 4B with WiFi connection, a water pump and a 220 ohm resistor and a NPN transistor. 22 | A 32 Gb mini sd card for the raspberry OS. 23 | If you want to run the bar anywhere, you also need a power bank to power the raspberry pi. 24 | 25 | - Raspberry Pi 4B (or similar) with WiFi connection 26 | - 5V Water pump 27 | - 220 Ohm resistor (is part of the raspberry starter kit) 28 | - NPN transistor (S8050 D331, is part of the raspberry starter kit. It's the one with the little H on the back) 29 | - 32 Gb mini sd card 30 | - Power bank (optional) 31 | 32 | For example: 33 | 34 | https://www.amazon.de/dp/B0C7KXMP7W 35 | https://www.amazon.de/dp/B07WYX8M76 36 | There may be cheaper and better options for these two. This is an example, any Raspberry 4b and any starter kit with a LED and a resistor will do. Probably a raspberry nano/pico or similar would also work. 37 | 38 | Pump + moisture sensor (Sensor is not used in this example, but you could use it to check if there is still liquid in the container): 39 | https://www.amazon.de/dp/B07TQ6TP55 40 | 41 | ## Setup Raspberry 42 | 43 | See [LED-SWITCH-EXAMPLE 44 | ](https://github.com/solana-developers/solana-depin-examples/blob/main/led-switch/README.md) 45 | for the complete setup of the PI including node and typescript. 46 | 47 | 48 | ## Install Node on the Raspberry Pi: 49 | 50 | See [LED-SWITCH-EXAMPLE 51 | ](https://github.com/solana-developers/solana-depin-examples/blob/main/led-switch/README.md) 52 | for the complete setup of the PI including node and typescript. 53 | 54 | ## Power supply 55 | 56 | The water pump may have come with a relay. That is not really needed though. You can just use the power supply of the raspberry pi. 57 | We use a NPN Transistor from the Raspberry pi starter kit. (S8050 D331 the one with the little H on it) Attach the left side, the collector, to ground pin which is pin number three on the right side. Then the positive part to the collector of the transistor. Then you attach GPIO 23 via a resistor to the base of the transistor. 58 | What is happening here is that the GPIO pin will be loaded positive as soon as the GPIO pin is activated in our ts code. This basically makes the transistor conductive and the power can freely flow from plus to ground through the pump which makes it pump. 59 | 60 | ![IMG_2733](https://github.com/solana-developers/solana-depin-examples/assets/5938789/c2972057-4e68-47a3-aaf4-538686c5ae03) 61 | ![IMG_2735](https://github.com/solana-developers/solana-depin-examples/assets/5938789/d09fe43a-6792-432f-8483-bce2c36d4c5d) 62 | 63 | This is already the whole setup we need. Now just find a nice spot for your pump and attach the raspberry with a power bank so it looks like the wine is controlled by magic. 64 | 65 | ## The anchor program 66 | 67 | The program is written in Rust using the Anchor framework. 68 | It consists of two parts. The first part is the program that runs on the blockchain and the second part is the program that runs on the raspberry pi. 69 | The program has a function to buy a shot and a function to mark the shot as delivered. 70 | When scanning the QR code a transaction request is created that calls the buy shot function by signing a transaction on the users mobile wallet. 71 | When the raspberry pi receives the transaction request it will turn on the pump and wait for a certain amount of time. 72 | Then it will call the mark shot as delivered function. 73 | This program can be easily expanded to have multiple pumps and multiple drinks. The receipts can be used to track how much was sold for accounting. 74 | 75 | ```rust 76 | use anchor_lang::solana_program::native_token::LAMPORTS_PER_SOL; 77 | use anchor_lang::system_program; 78 | use anchor_lang::{prelude::*, solana_program::pubkey}; 79 | 80 | declare_id!("GCgyx9JPNpqX97iWQh7rqPjaignahkS8DqQGdDdfXsPQ"); 81 | 82 | // This is where the payments for drinks will be send to. 83 | const TREASURE_PUBKEY: Pubkey = pubkey!("GsfNSuZFrT2r4xzSndnCSs9tTXwt47etPqU8yFVnDcXd"); 84 | 85 | #[error_code] 86 | pub enum ShotErrorCode { 87 | #[msg("InvalidTreasury")] 88 | InvalidTreasury, 89 | } 90 | 91 | #[program] 92 | pub mod solana_bar { 93 | 94 | use super::*; 95 | const SHOT_PRICE: u64 = LAMPORTS_PER_SOL / 10; // 0.1 SOL 96 | 97 | pub fn initialize(_ctx: Context) -> Result<()> { 98 | Ok(()) 99 | } 100 | 101 | pub fn buy_shot(ctx: Context) -> Result<()> { 102 | if TREASURE_PUBKEY != *ctx.accounts.treasury.key { 103 | return Err(ShotErrorCode::InvalidTreasury.into()); 104 | } 105 | 106 | // Add a new receipt to the receipts account. 107 | let receipt_id = ctx.accounts.receipts.total_shots_sold; 108 | ctx.accounts.receipts.receipts.push(Receipt { 109 | buyer: *ctx.accounts.signer.key, 110 | was_delivered: false, 111 | price: 1, 112 | timestamp: Clock::get()?.unix_timestamp, 113 | receipt_id, 114 | }); 115 | 116 | // Change this number to how many receipts you want to save on chain. 117 | let len = ctx.accounts.receipts.receipts.len(); 118 | if len >= 10 { 119 | ctx.accounts.receipts.receipts.remove(0); 120 | } 121 | 122 | // Increment the total shots sold. 123 | ctx.accounts.receipts.total_shots_sold = ctx 124 | .accounts 125 | .receipts 126 | .total_shots_sold 127 | .checked_add(1) 128 | .unwrap(); 129 | 130 | // Transfer lamports to the treasury for payment. 131 | let cpi_context = CpiContext::new( 132 | ctx.accounts.system_program.to_account_info(), 133 | system_program::Transfer { 134 | from: ctx.accounts.signer.to_account_info().clone(), 135 | to: ctx.accounts.treasury.to_account_info().clone(), 136 | }, 137 | ); 138 | system_program::transfer(cpi_context, SHOT_PRICE)?; 139 | 140 | Ok(()) 141 | } 142 | 143 | // This instruction will be called from the raspberry pi as soon as he is done purring the drink. For the very unlikely case that an attacker wants to mark a shot as delivered here a signer check could be added. 144 | pub fn mark_shot_as_delivered(ctx: Context, recipe_id: u64) -> Result<()> { 145 | for i in 0..ctx.accounts.receipts.receipts.len() { 146 | if ctx.accounts.receipts.receipts[i].receipt_id == recipe_id { 147 | msg!("Marked shot as delivered {} {} ", recipe_id, i); 148 | ctx.accounts.receipts.receipts[i].was_delivered = true; 149 | } 150 | } 151 | Ok(()) 152 | } 153 | } 154 | 155 | #[derive(Accounts)] 156 | pub struct Initialize<'info> { 157 | #[account(init, payer = authority, space = 5000, seeds = [b"receipts"], bump)] 158 | pub receipts: Account<'info, Receipts>, 159 | #[account(mut)] 160 | pub authority: Signer<'info>, 161 | pub system_program: Program<'info, System>, 162 | pub rent: Sysvar<'info, Rent>, 163 | } 164 | 165 | #[derive(Accounts)] 166 | pub struct BuyShot<'info> { 167 | #[account(mut, seeds = [b"receipts"], bump)] 168 | pub receipts: Account<'info, Receipts>, 169 | #[account(mut)] 170 | pub signer: Signer<'info>, 171 | /// CHECK: checked against the treasury pubkey. 172 | #[account(mut)] 173 | pub treasury: AccountInfo<'info>, 174 | pub system_program: Program<'info, System>, 175 | } 176 | 177 | #[derive(Accounts)] 178 | pub struct MarkShotAsDelivered<'info> { 179 | #[account(mut, seeds = [b"receipts"], bump)] 180 | pub receipts: Account<'info, Receipts>, 181 | #[account(mut)] 182 | pub signer: Signer<'info>, 183 | } 184 | 185 | #[account()] 186 | pub struct Receipts { 187 | pub receipts: Vec, 188 | pub total_shots_sold: u64, 189 | } 190 | 191 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] 192 | pub struct Receipt { 193 | pub receipt_id: u64, 194 | pub buyer: Pubkey, 195 | pub was_delivered: bool, 196 | pub price: u64, 197 | pub timestamp: i64, 198 | } 199 | 200 | ``` 201 | 202 | ## Raspberry PI script setup 203 | 204 | See [LED-SWITCH-EXAMPLE 205 | ](https://github.com/solana-developers/solana-depin-examples/blob/main/led-switch/README.md) 206 | 207 | 208 | ## Raspberry PI script 209 | 210 | The script is written in typescript and uses the anchor framework to interact with the blockchain. Make sure you copy the solana_bar types script next to the bar.ts script to be able to interact with the anchor program. 211 | 212 | This script is loading the receipts account and starts purring all drinks that are not delivered yet. Then it starts listening to the receipts account and purrs drinks as they are being bought. 213 | 214 | To purr a drink it activates GPIO 23 which is connected to a transistor is connected to a 5V line which is connected to the pump. The time to purr could be adjusted by adding a field for purr time for different drinks into the receipt account for example. 215 | 216 | After the drink is purred the script uses a hardcoded keypair to mark the drink as delivered. 217 | 218 | 219 | ```js 220 | import * as anchor from "@coral-xyz/anchor"; 221 | import { Program } from "@coral-xyz/anchor"; 222 | import { IDL, SolanaBar } from "./solana_bar"; 223 | import { clusterApiUrl, Connection, Keypair, PublicKey } from "@solana/web3.js"; 224 | 225 | var Gpio = require('onoff').Gpio; //include onoff to interact with the GPIO 226 | var GPIO_23 = new Gpio(23, 'out'); //use GPIO pin 18, and specify that it is output 227 | 228 | let connection = new Connection(clusterApiUrl("devnet")); 229 | 230 | // Replace this with your own keypair to be able to pay for fees to mark drinks as delivered. 231 | const keypair = Keypair.fromSecretKey( 232 | Uint8Array.from([209,70,174,212,192,159,166,82,163,162,135,190,244,227,218,97,214,155,228,142,172,188,170,246,130,68,106,45,170,125,175,57,12,253,44,189,234,23,239,220,85,57,231,86,130,27,99,62,106,215,172,104,152,104,145,138,198,105,218,20,232,251,238,250]) 233 | ); 234 | 235 | // Or load from File: 236 | /*const keypair = new Uint8Array( 237 | JSON.parse( 238 | fs.readFileSync("shoUzmg5H2zDdevxS6UdQCiY6JnP1qSn7fPtCP726pR.json").toString()) 239 | ); 240 | let keyPair = Keypair.fromSecretKey(decodedKey); 241 | */ 242 | 243 | let wallet = new anchor.Wallet(keypair); 244 | const provider = new anchor.AnchorProvider(connection, wallet, { 245 | commitment: "confirmed", 246 | }); 247 | anchor.setProvider(provider); 248 | 249 | const program = new Program(IDL, "GCgyx9JPNpqX97iWQh7rqPjaignahkS8DqQGdDdfXsPQ", { connection }) 250 | 251 | console.log("Program ID", program.programId.toString()); 252 | 253 | startListeningToLedSwitchAccount(); 254 | 255 | async function startListeningToLedSwitchAccount() { 256 | const receiptsPDA = await anchor.web3.PublicKey.findProgramAddressSync( 257 | [ 258 | Buffer.from("receipts"), 259 | ], 260 | program.programId, 261 | )[0]; 262 | 263 | const receiptsAccount = await program.account.receipts.fetch( 264 | receiptsPDA 265 | ) 266 | 267 | console.log("Receipts account", JSON.stringify(receiptsAccount)); 268 | 269 | for (let i = 0; i < receiptsAccount.receipts.length; i++) { 270 | const receipt = receiptsAccount.receipts[i]; 271 | if (!receipt.wasDelivered) { 272 | await PourShotAndMarkAsDelivered(receipt); 273 | console.log("Receipt", JSON.stringify(receipt)); 274 | break; 275 | } 276 | } 277 | 278 | GPIO_23.writeSync(0); 279 | 280 | connection.onAccountChange(receiptsPDA, async (account) => { 281 | const decoded = program.coder.accounts.decode( 282 | "receipts", 283 | account.data 284 | ) 285 | 286 | for (let i = 0; i < decoded.receipts.length; i++) { 287 | const receipt = decoded.receipts[i]; 288 | if (!receipt.wasDelivered) { 289 | await PourShotAndMarkAsDelivered(receipt); 290 | break; 291 | } 292 | } 293 | 294 | console.log("Shot given out."); 295 | }, "confirmed") 296 | 297 | async function PourShotAndMarkAsDelivered(receipt: { receiptId: anchor.BN; buyer: anchor.web3.PublicKey; wasDelivered: boolean; price: anchor.BN; timestamp: anchor.BN; }) { 298 | console.log("start purring receipt id: " + receipt.receiptId.toString()); 299 | 300 | GPIO_23.writeSync(1); 301 | await sleep(3000); 302 | GPIO_23.writeSync(0); 303 | 304 | console.log("done purring: " + receipt.receiptId.toString()); 305 | 306 | let ix = await program.methods.markShotAsDelivered(receipt.receiptId).accounts( 307 | { 308 | receipts: receiptsPDA, 309 | signer: wallet.publicKey, 310 | }).transaction(); 311 | 312 | console.log("ix", JSON.stringify(ix)); 313 | ix.feePayer = wallet.publicKey; 314 | var signature = await connection.sendTransaction(ix, [keypair], {skipPreflight: true}); 315 | 316 | console.log("Sent receipt mark as delivered: ", signature); 317 | } 318 | 319 | function sleep(ms) { 320 | return new Promise(resolve => setTimeout(resolve, ms)); 321 | } 322 | }; 323 | 324 | ``` 325 | 326 | ## Create a Solana Pay Transaction Request 327 | 328 | Solana Pay is not only for payments, but can request any transaction to be signed. 329 | The transaction can be signed by any wallet that supports Solana Pay. 330 | It consists of two parts. 331 | The api which can be found in solana-bar/app/pages/api/transaction.ts 332 | 333 | ```js 334 | if (instructionField == "buy_shot") { 335 | let ix = await SOLANA_BAR_PROGRAM.methods.buyShot().accounts( 336 | { 337 | receipts: RECEIPTS_PDA, 338 | signer: sender, 339 | treasury: new PublicKey("BRWrkVaTTyq3eRJw4t8YjkJuH9EtnoVeyeQ4A3eDqU86"), 340 | systemProgram: PublicKey.default, 341 | }, 342 | ).instruction(); 343 | 344 | transaction.add(ix); 345 | 346 | message = 'Buy 4 cl drink!'; 347 | } else { 348 | message = 'Unknown instruction'; 349 | } 350 | ``` 351 | 352 | and the creation of the QR code which can be found in 353 | solana-ar/app/app/page.tsx 354 | 355 | ```js 356 | {receipts != null && ( 357 | 358 | )} 359 | ``` 360 | 361 | and the QrCode in solan-bar/app/app/components/qr-code.tsx. 362 | 363 | ```js 364 | const queryBuilder = (baseUrl: string, params: string[][]) => { 365 | let url = baseUrl + '?'; 366 | params.forEach((p, i) => url += p[0] + '=' + p[1] + (i != params.length - 1 ? '&' : '')); 367 | console.log(url) 368 | return url; 369 | } 370 | 371 | const PayQR: FC = ( 372 | { instruction } 373 | ) => { 374 | const qrRef = useRef(null) 375 | 376 | useEffect(() => { 377 | const params = [ 378 | ['instruction', instruction], 379 | ]; 380 | 381 | const apiUrl = queryBuilder( 382 | `${window.location.protocol}//${window.location.host}/api/transaction`, 383 | params, 384 | ); 385 | 386 | const qr = createQR( 387 | encodeURL({ link: new URL(apiUrl) }), 388 | 360, 389 | 'transparent' 390 | ); 391 | 392 | qr.update({ backgroundOptions: { round: 1000 } }); 393 | qr.update({ type: 'canvas' }); 394 | 395 | if (qrRef.current != null) { 396 | qrRef.current.innerHTML = ''; 397 | qr.append(qrRef.current) 398 | } 399 | 400 | }, []) 401 | ``` 402 | 403 | Basically what is happening is that the wallet sends a get request to our API to get a name and icon and then the transaction is created in the nextJS api and send to the wallet. The wallet then signs it. When the transaction is confirmed the new receipt is written into the receipt account is updated and since on the raspberry pi we have a websocket connection to that account the raspberry can start purring the drink. After he is done he will mark the drink as delivered. So like this we can make sure that the drink is only purred when the transaction is confirmed. In case of hardware errors we can issue a refund. 404 | 405 | To run the solana pay transaction request app use: 406 | 407 | ```console 408 | cd app 409 | yarn install 410 | yarn dev 411 | ``` 412 | 413 | open http://localhost:3000 in your browser. 414 | Notice that the QR code is not working yet. We need to be able to access is from the distance. 415 | For that we use ngrok to create a tunnel to our local server. 416 | Make an account and install ngrok https://ngrok.com/ 417 | open a terminal and type: 418 | ngrok http 3000 419 | Then copy the url from the terminal and open it in the browser. 420 | Now the QR code should work and scanning the QR code will start the pump. 421 | 422 | Now you can also copy and print the QR codes and glue them somewhere next to our Solana Bar for example. 423 | 424 | 425 | ## Deploy 426 | 427 | Since you don't want to run ngrok every time it makes sense to deploy the app. Either on the raspberry itself or on for example vercel.com which is very convenient since you can directly deploy it from the github repository. 428 | 429 | 430 | ## Where to go from here 431 | 432 | Good additions would be to add a field for purr duration on the receipt account and to add a moisture sensor to check if there is still liquid in the container. 433 | 434 | Build a nice case for it to hide the magic and shock your friends. 435 | 436 | You can now also power the raspberry pi with a power bank and take it with you to the beach or a party and sell drinks there. You may want to connect it to your phones hotspot in that case. 437 | 438 | 439 | ### Optional step: auto start the script on boot 440 | 441 | [LED-SWITCH-EXAMPLE 442 | ](https://github.com/solana-developers/solana-depin-examples/blob/main/led-switch/README.md) 443 | 444 | 445 | 446 | 447 | 448 | -------------------------------------------------------------------------------- /solana-bar/app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /solana-bar/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /solana-bar/app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /solana-bar/app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/transaction](http://localhost:3000/api/transaction). This endpoint can be edited in `pages/api/transaction.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | To make the solana pay transaction requests work locally you need to make the page accessible from outside. Easiest way to do that is to use [`ngrok`](https://ngrok.com/). Run `ngrok http 3000` in the terminal and open the url provided in your browser instead of `localhost:3000`. 26 | 27 | ## Learn More 28 | 29 | To learn more about Next.js, take a look at the following resources: 30 | 31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 32 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 33 | 34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 35 | 36 | ## Deploy on Vercel 37 | 38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 39 | 40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 41 | -------------------------------------------------------------------------------- /solana-bar/app/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /solana-bar/app/app/head.tsx: -------------------------------------------------------------------------------- 1 | export default function Head() { 2 | return ( 3 | <> 4 | Decentralized Solana Bar 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /solana-bar/app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Wallet } from '@/src/Wallet' 2 | import './globals.css' 3 | 4 | export default function RootLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | return ( 10 | 11 | {/* 12 | will contain the components returned by the nearest parent 13 | head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head 14 | */} 15 | 16 | 17 | {children} 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /solana-bar/app/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; // this makes next know that this page should be rendered in the client 2 | import { useEffect, useState } from 'react'; 3 | import { CONNECTION, SOLANA_BAR_PROGRAM, RECEIPTS_PDA } from '@/src/util/const'; 4 | import PayQR from '@/src/components/PayQR'; 5 | import Receipts from '@/src/components/Receipts'; 6 | 7 | export default function Home() { 8 | const [receipts, setReceipts] = useState() 9 | 10 | useEffect(() => { 11 | 12 | CONNECTION.onAccountChange( 13 | RECEIPTS_PDA, 14 | (updatedAccountInfo, context) => { 15 | { 16 | const decoded = SOLANA_BAR_PROGRAM.coder.accounts.decode( 17 | "receipts", 18 | updatedAccountInfo.data 19 | ) 20 | setReceipts(decoded); 21 | } 22 | }, 23 | "confirmed" 24 | ); 25 | 26 | const getState = async () => { 27 | const gameData = await SOLANA_BAR_PROGRAM.account.receipts.fetch( 28 | RECEIPTS_PDA, 29 | ); 30 | setReceipts(gameData); 31 | }; 32 | 33 | getState(); 34 | 35 | }, []); 36 | 37 | return ( 38 |
    39 | {
    40 |
    41 | 42 |
    43 | 44 |
    45 |
    46 |

    47 | Solana Bar 48 |

    49 | Scan the QR code to buy a shot via a Solana pay transaction request. 50 |

    51 | 52 |

    53 | 54 |

    55 | 56 |
    57 |
    58 | 59 |
  • 60 | 61 | {receipts != null && ( 62 | 63 | )} 64 | 65 | {!receipts && ( 66 | 67 | )} 68 | 69 | 70 | 71 |
  • 72 |
    73 |
    74 |
    } 75 |
    76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /solana-bar/app/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | }, 6 | images: { 7 | domains: ['images.unsplash.com'] 8 | }, 9 | } 10 | 11 | module.exports = nextConfig 12 | -------------------------------------------------------------------------------- /solana-bar/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solution", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "export": "next export", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "proxy": "local-ssl-proxy --source 3001 --target 3000" 12 | }, 13 | "dependencies": { 14 | "@next/font": "13.1.2", 15 | "@solana/pay": "^0.2.4", 16 | "@solana/spl-token": "^0.3.7", 17 | "@coral-xyz/anchor": "0.28.0", 18 | "@solana/wallet-adapter-react": "^0.15.28", 19 | "@solana/wallet-adapter-react-ui": "^0.9.27", 20 | "@solana/wallet-adapter-wallets": "^0.19.11", 21 | "@solana/web3.js": "^1.73.2", 22 | "@types/node": "18.11.18", 23 | "@types/qrcode": "^1.5.0", 24 | "@types/react": "18.0.27", 25 | "@types/react-dom": "18.0.10", 26 | "bigint-buffer": "^1.1.5", 27 | "bignumber.js": "^9.1.1", 28 | "borsh": "^0.7.0", 29 | "eslint": "8.32.0", 30 | "eslint-config-next": "13.1.2", 31 | "next": "13.1.2", 32 | "qrcode": "^1.5.1", 33 | "react": "^18.2.0", 34 | "react-dom": "18.2.0", 35 | "sha256": "^0.2.0" 36 | }, 37 | "devDependencies": { 38 | "autoprefixer": "^10.4.13", 39 | "encoding": "^0.1.13", 40 | "local-ssl-proxy": "^1.3.0", 41 | "postcss": "^8.4.21", 42 | "tailwindcss": "^3.2.4", 43 | "ts-loader": "^9.4.4", 44 | "typescript": "^5.1.6" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /solana-bar/app/pages/api/transaction.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import { PublicKey, Transaction } from '@solana/web3.js'; 4 | import { CONNECTION, RECEIPTS_PDA, SOLANA_BAR_PROGRAM } from '@/src/util/const'; 5 | 6 | type POST = { 7 | transaction: string; 8 | message: string; 9 | }; 10 | 11 | type GET = { 12 | label: string; 13 | icon: string; 14 | }; 15 | 16 | function getFromPayload(req: NextApiRequest, payload: string, field: string): string { 17 | function parseError() { throw new Error(`${payload} parse error: missing ${field}`) }; 18 | let value; 19 | if (payload === 'Query') { 20 | if (!(field in req.query)) parseError(); 21 | value = req.query[field]; 22 | } 23 | if (payload === 'Body') { 24 | if (!req.body || !(field in req.body)) parseError(); 25 | value = req.body[field]; 26 | } 27 | if (value === undefined || value.length === 0) parseError(); 28 | return typeof value === 'string' ? value : value[0]; 29 | } 30 | 31 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 32 | if (req.method === 'GET') { 33 | return get(req, res); 34 | } 35 | 36 | if (req.method === 'POST') { 37 | return post(req, res); 38 | } 39 | } 40 | 41 | const get = async (req: NextApiRequest, res: NextApiResponse) => { 42 | const label = 'Solana Shots'; 43 | const icon = 44 | 'https://media.discordapp.net/attachments/964525722301501477/978683590743302184/sol-logo1.png'; 45 | 46 | res.status(200).json({ 47 | label, 48 | icon, 49 | }); 50 | }; 51 | 52 | const post = async (req: NextApiRequest, res: NextApiResponse) => { 53 | 54 | const accountField = getFromPayload(req, 'Body', 'account'); 55 | const instructionField = getFromPayload(req, 'Query', 'instruction'); 56 | 57 | const sender = new PublicKey(accountField); 58 | 59 | const transaction = new Transaction(); 60 | const latestBlockhash = await CONNECTION.getLatestBlockhash(); 61 | transaction.feePayer = sender; 62 | transaction.recentBlockhash = latestBlockhash.blockhash; 63 | 64 | let message; 65 | if (instructionField == "buy_shot") { 66 | let ix = await SOLANA_BAR_PROGRAM.methods.buyShot().accounts( 67 | { 68 | receipts: RECEIPTS_PDA, 69 | signer: sender, 70 | treasury: new PublicKey("GsfNSuZFrT2r4xzSndnCSs9tTXwt47etPqU8yFVnDcXd"), 71 | systemProgram: PublicKey.default, 72 | }, 73 | ).instruction(); 74 | 75 | transaction.add(ix); 76 | 77 | message = 'Buy 4 cl drink!'; 78 | } else { 79 | message = 'Unknown instruction'; 80 | } 81 | 82 | // Serialize and return the unsigned transaction. 83 | const serializedTransaction = transaction.serialize({ 84 | verifySignatures: false, 85 | requireAllSignatures: false, 86 | }); 87 | 88 | const base64Transaction = serializedTransaction.toString('base64'); 89 | 90 | res.status(200).send({ transaction: base64Transaction, message }); 91 | }; 92 | -------------------------------------------------------------------------------- /solana-bar/app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /solana-bar/app/public/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/solana-depin-examples/3f8cd5b1519efd9c44e3261a77ec576a7fedcb6c/solana-bar/app/public/bg.jpg -------------------------------------------------------------------------------- /solana-bar/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/solana-depin-examples/3f8cd5b1519efd9c44e3261a77ec576a7fedcb6c/solana-bar/app/public/favicon.ico -------------------------------------------------------------------------------- /solana-bar/app/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/solana-depin-examples/3f8cd5b1519efd9c44e3261a77ec576a7fedcb6c/solana-bar/app/public/icon.png -------------------------------------------------------------------------------- /solana-bar/app/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /solana-bar/app/public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /solana-bar/app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /solana-bar/app/src/Wallet.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { FC, useMemo } from 'react'; 3 | import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; 4 | import { WalletAdapterNetwork } from '@solana/wallet-adapter-base'; 5 | import { PhantomWalletAdapter } from '@solana/wallet-adapter-phantom'; 6 | import { 7 | WalletModalProvider, 8 | WalletDisconnectButton, 9 | WalletMultiButton 10 | } from '@solana/wallet-adapter-react-ui'; 11 | import { clusterApiUrl } from '@solana/web3.js'; 12 | 13 | // Default styles that can be overridden by your app 14 | require('@solana/wallet-adapter-react-ui/styles.css'); 15 | 16 | type Props = { 17 | children?: React.ReactNode 18 | }; 19 | 20 | export const Wallet: FC = ({children}) => { 21 | // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'. 22 | const network = WalletAdapterNetwork.Devnet; 23 | 24 | // You can also provide a custom RPC endpoint. 25 | const endpoint = useMemo(() => clusterApiUrl(network), [network]); 26 | 27 | const wallets = useMemo( 28 | () => [ 29 | 30 | new PhantomWalletAdapter(), 31 | ], 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | [network] 34 | ); 35 | 36 | return ( 37 | 38 | 39 | 40 | { children } 41 | 42 | 43 | 44 | ); 45 | }; -------------------------------------------------------------------------------- /solana-bar/app/src/components/PayQR.tsx: -------------------------------------------------------------------------------- 1 | import { encodeURL, createQR } from '@solana/pay'; 2 | import { FC, useEffect, useRef } from 'react'; 3 | 4 | type TransactionRequestQRProps = { 5 | instruction: string; 6 | }; 7 | 8 | const queryBuilder = (baseUrl: string, params: string[][]) => { 9 | let url = baseUrl + '?'; 10 | params.forEach((p, i) => url += p[0] + '=' + p[1] + (i != params.length - 1 ? '&' : '')); 11 | console.log(url) 12 | return url; 13 | } 14 | 15 | const PayQR: FC = ( 16 | { instruction } 17 | ) => { 18 | const qrRef = useRef(null) 19 | 20 | useEffect(() => { 21 | const params = [ 22 | ['instruction', instruction], 23 | ]; 24 | 25 | const apiUrl = queryBuilder( 26 | `${window.location.protocol}//${window.location.host}/api/transaction`, 27 | params, 28 | ); 29 | 30 | const qr = createQR( 31 | encodeURL({ link: new URL(apiUrl) }), 32 | 360, 33 | 'transparent' 34 | ); 35 | 36 | qr.update({ backgroundOptions: { round: 1000 } }); 37 | qr.update({ type: 'canvas' }); 38 | 39 | if (qrRef.current != null) { 40 | qrRef.current.innerHTML = ''; 41 | qr.append(qrRef.current) 42 | } 43 | 44 | }, []) 45 | 46 | return ( 47 |
    48 | 49 |
    50 |

    {instruction}

    51 |
    52 | 53 |
    54 | 55 |
    56 | ); 57 | }; 58 | 59 | export default PayQR; 60 | -------------------------------------------------------------------------------- /solana-bar/app/src/components/Receipts.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useRef } from 'react'; 2 | 3 | const PayQR: FC = ( 4 | { receipts } 5 | ) => { 6 | 7 | console.log(JSON.stringify(receipts)); 8 | 9 | return ( 10 | 11 |
    12 |

    Receipts:

    13 | {receipts && receipts.receipts.map((receipt: any) => 14 | 15 | {receipt.receiptId.toString()} 16 | {receipt.wasDelivered ? "Done: " : "Pending: "} 17 | {receipt.buyer.toString()} 18 | 19 | )} 20 |
    21 | ); 22 | }; 23 | 24 | export default PayQR; 25 | -------------------------------------------------------------------------------- /solana-bar/app/src/util/const.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from "@solana/web3.js"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import { IDL, SolanaBar } from "./solana_bar"; 4 | 5 | export const CONNECTION = new Connection(process.env.NEXT_PUBLIC_RPC ? process.env.NEXT_PUBLIC_RPC : 'https://api.devnet.solana.com', { 6 | wsEndpoint: process.env.NEXT_PUBLIC_WSS_RPC ? process.env.NEXT_PUBLIC_WSS_RPC : "wss://api.devnet.solana.com", 7 | commitment: 'confirmed' 8 | }); 9 | 10 | export const SOLANA_BAR_PROGRAM_ID = new PublicKey('GCgyx9JPNpqX97iWQh7rqPjaignahkS8DqQGdDdfXsPQ'); 11 | 12 | export const SOLANA_BAR_PROGRAM = new Program(IDL, SOLANA_BAR_PROGRAM_ID, { connection: CONNECTION }) 13 | 14 | export const RECEIPTS_PDA = PublicKey.findProgramAddressSync( 15 | [ 16 | Buffer.from("receipts"), 17 | ], 18 | SOLANA_BAR_PROGRAM_ID, 19 | )[0]; -------------------------------------------------------------------------------- /solana-bar/app/src/util/solana_bar.ts: -------------------------------------------------------------------------------- 1 | export type SolanaBar = { 2 | "version": "0.1.0", 3 | "name": "solana_bar", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [ 8 | { 9 | "name": "receipts", 10 | "isMut": true, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "authority", 15 | "isMut": true, 16 | "isSigner": true 17 | }, 18 | { 19 | "name": "systemProgram", 20 | "isMut": false, 21 | "isSigner": false 22 | }, 23 | { 24 | "name": "rent", 25 | "isMut": false, 26 | "isSigner": false 27 | } 28 | ], 29 | "args": [] 30 | }, 31 | { 32 | "name": "buyShot", 33 | "accounts": [ 34 | { 35 | "name": "receipts", 36 | "isMut": true, 37 | "isSigner": false 38 | }, 39 | { 40 | "name": "signer", 41 | "isMut": true, 42 | "isSigner": true 43 | }, 44 | { 45 | "name": "treasury", 46 | "isMut": true, 47 | "isSigner": false 48 | }, 49 | { 50 | "name": "systemProgram", 51 | "isMut": false, 52 | "isSigner": false 53 | } 54 | ], 55 | "args": [] 56 | }, 57 | { 58 | "name": "markShotAsDelivered", 59 | "accounts": [ 60 | { 61 | "name": "receipts", 62 | "isMut": true, 63 | "isSigner": false 64 | }, 65 | { 66 | "name": "signer", 67 | "isMut": true, 68 | "isSigner": true 69 | } 70 | ], 71 | "args": [ 72 | { 73 | "name": "recipeId", 74 | "type": "u64" 75 | } 76 | ] 77 | } 78 | ], 79 | "accounts": [ 80 | { 81 | "name": "receipts", 82 | "type": { 83 | "kind": "struct", 84 | "fields": [ 85 | { 86 | "name": "receipts", 87 | "type": { 88 | "vec": { 89 | "defined": "Receipt" 90 | } 91 | } 92 | }, 93 | { 94 | "name": "totalShotsSold", 95 | "type": "u64" 96 | } 97 | ] 98 | } 99 | } 100 | ], 101 | "types": [ 102 | { 103 | "name": "Receipt", 104 | "type": { 105 | "kind": "struct", 106 | "fields": [ 107 | { 108 | "name": "receiptId", 109 | "type": "u64" 110 | }, 111 | { 112 | "name": "buyer", 113 | "type": "publicKey" 114 | }, 115 | { 116 | "name": "wasDelivered", 117 | "type": "bool" 118 | }, 119 | { 120 | "name": "price", 121 | "type": "u64" 122 | }, 123 | { 124 | "name": "timestamp", 125 | "type": "i64" 126 | } 127 | ] 128 | } 129 | } 130 | ], 131 | "errors": [ 132 | { 133 | "code": 6000, 134 | "name": "InvalidTreasury", 135 | "msg": "InvalidTreasury" 136 | } 137 | ] 138 | }; 139 | 140 | export const IDL: SolanaBar = { 141 | "version": "0.1.0", 142 | "name": "solana_bar", 143 | "instructions": [ 144 | { 145 | "name": "initialize", 146 | "accounts": [ 147 | { 148 | "name": "receipts", 149 | "isMut": true, 150 | "isSigner": false 151 | }, 152 | { 153 | "name": "authority", 154 | "isMut": true, 155 | "isSigner": true 156 | }, 157 | { 158 | "name": "systemProgram", 159 | "isMut": false, 160 | "isSigner": false 161 | }, 162 | { 163 | "name": "rent", 164 | "isMut": false, 165 | "isSigner": false 166 | } 167 | ], 168 | "args": [] 169 | }, 170 | { 171 | "name": "buyShot", 172 | "accounts": [ 173 | { 174 | "name": "receipts", 175 | "isMut": true, 176 | "isSigner": false 177 | }, 178 | { 179 | "name": "signer", 180 | "isMut": true, 181 | "isSigner": true 182 | }, 183 | { 184 | "name": "treasury", 185 | "isMut": true, 186 | "isSigner": false 187 | }, 188 | { 189 | "name": "systemProgram", 190 | "isMut": false, 191 | "isSigner": false 192 | } 193 | ], 194 | "args": [] 195 | }, 196 | { 197 | "name": "markShotAsDelivered", 198 | "accounts": [ 199 | { 200 | "name": "receipts", 201 | "isMut": true, 202 | "isSigner": false 203 | }, 204 | { 205 | "name": "signer", 206 | "isMut": true, 207 | "isSigner": true 208 | } 209 | ], 210 | "args": [ 211 | { 212 | "name": "recipeId", 213 | "type": "u64" 214 | } 215 | ] 216 | } 217 | ], 218 | "accounts": [ 219 | { 220 | "name": "receipts", 221 | "type": { 222 | "kind": "struct", 223 | "fields": [ 224 | { 225 | "name": "receipts", 226 | "type": { 227 | "vec": { 228 | "defined": "Receipt" 229 | } 230 | } 231 | }, 232 | { 233 | "name": "totalShotsSold", 234 | "type": "u64" 235 | } 236 | ] 237 | } 238 | } 239 | ], 240 | "types": [ 241 | { 242 | "name": "Receipt", 243 | "type": { 244 | "kind": "struct", 245 | "fields": [ 246 | { 247 | "name": "receiptId", 248 | "type": "u64" 249 | }, 250 | { 251 | "name": "buyer", 252 | "type": "publicKey" 253 | }, 254 | { 255 | "name": "wasDelivered", 256 | "type": "bool" 257 | }, 258 | { 259 | "name": "price", 260 | "type": "u64" 261 | }, 262 | { 263 | "name": "timestamp", 264 | "type": "i64" 265 | } 266 | ] 267 | } 268 | } 269 | ], 270 | "errors": [ 271 | { 272 | "code": 6000, 273 | "name": "InvalidTreasury", 274 | "msg": "InvalidTreasury" 275 | } 276 | ] 277 | }; 278 | -------------------------------------------------------------------------------- /solana-bar/app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx}", 5 | "./components/**/*.{js,ts,jsx,tsx}", 6 | 7 | // Or if using `src` directory: 8 | "./src/**/*.{js,ts,jsx,tsx}", 9 | 10 | // Or if using `app` directory: 11 | "./app/**/*.{js,ts,jsx,tsx}", 12 | ], 13 | theme: { 14 | extend: {}, 15 | }, 16 | plugins: [], 17 | } -------------------------------------------------------------------------------- /solana-bar/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": ["./*"] 25 | } 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /solana-bar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", 4 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check", 5 | "build": "webpack --mode production" 6 | }, 7 | "dependencies": { 8 | "@coral-xyz/anchor": "^0.28.0" 9 | }, 10 | "devDependencies": { 11 | "chai": "^4.3.4", 12 | "mocha": "^9.0.3", 13 | "ts-mocha": "^10.0.0", 14 | "@types/bn.js": "^5.1.0", 15 | "@types/chai": "^4.3.0", 16 | "@types/mocha": "^9.0.0", 17 | "typescript": "^4.3.5", 18 | "prettier": "^2.6.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /solana-bar/programs/solana-bar/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solana-bar" 3 | version = "0.1.0" 4 | description = "Buy shots with Solana Pay" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "solana_bar" 10 | 11 | [features] 12 | no-entrypoint = [] 13 | no-idl = [] 14 | no-log-ix-name = [] 15 | cpi = ["no-entrypoint"] 16 | default = [] 17 | 18 | [dependencies] 19 | anchor-lang = "0.28.0" 20 | solana-program = "*" 21 | -------------------------------------------------------------------------------- /solana-bar/programs/solana-bar/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /solana-bar/programs/solana-bar/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::solana_program::native_token::LAMPORTS_PER_SOL; 2 | use anchor_lang::system_program; 3 | use anchor_lang::{prelude::*, solana_program::pubkey}; 4 | 5 | declare_id!("GCgyx9JPNpqX97iWQh7rqPjaignahkS8DqQGdDdfXsPQ"); 6 | const TREASURE_PUBKEY: Pubkey = pubkey!("GsfNSuZFrT2r4xzSndnCSs9tTXwt47etPqU8yFVnDcXd"); 7 | 8 | #[error_code] 9 | pub enum ShotErrorCode { 10 | #[msg("InvalidTreasury")] 11 | InvalidTreasury, 12 | } 13 | 14 | #[program] 15 | pub mod solana_bar { 16 | 17 | use super::*; 18 | const SHOT_PRICE: u64 = LAMPORTS_PER_SOL / 100; // 0.01 SOL 19 | 20 | pub fn initialize(_ctx: Context) -> Result<()> { 21 | Ok(()) 22 | } 23 | 24 | pub fn buy_shot(ctx: Context) -> Result<()> { 25 | if TREASURE_PUBKEY != *ctx.accounts.treasury.key { 26 | return Err(ShotErrorCode::InvalidTreasury.into()); 27 | } 28 | 29 | // Add a new receipt to the receipts account. 30 | let receipt_id = ctx.accounts.receipts.total_shots_sold; 31 | ctx.accounts.receipts.receipts.push(Receipt { 32 | buyer: *ctx.accounts.signer.key, 33 | was_delivered: false, 34 | price: 1, 35 | timestamp: Clock::get()?.unix_timestamp, 36 | receipt_id, 37 | }); 38 | 39 | let len = ctx.accounts.receipts.receipts.len(); 40 | if len >= 10 { 41 | ctx.accounts.receipts.receipts.remove(0); 42 | } 43 | 44 | // Increment the total shots sold. 45 | ctx.accounts.receipts.total_shots_sold = ctx 46 | .accounts 47 | .receipts 48 | .total_shots_sold 49 | .checked_add(1) 50 | .unwrap(); 51 | 52 | // Transfer lamports to the treasury for payment. 53 | let cpi_context = CpiContext::new( 54 | ctx.accounts.system_program.to_account_info(), 55 | system_program::Transfer { 56 | from: ctx.accounts.signer.to_account_info().clone(), 57 | to: ctx.accounts.treasury.to_account_info().clone(), 58 | }, 59 | ); 60 | system_program::transfer(cpi_context, SHOT_PRICE)?; 61 | 62 | Ok(()) 63 | } 64 | 65 | pub fn mark_shot_as_delivered(ctx: Context, recipe_id: u64) -> Result<()> { 66 | msg!("Marked shot as delivered"); 67 | for i in 0..ctx.accounts.receipts.receipts.len() { 68 | msg!("Marked shot as delivered {}", i); 69 | if ctx.accounts.receipts.receipts[i].receipt_id == recipe_id { 70 | msg!("Marked shot as delivered {} {} ", recipe_id, i); 71 | ctx.accounts.receipts.receipts[i].was_delivered = true; 72 | } 73 | } 74 | Ok(()) 75 | } 76 | } 77 | 78 | #[derive(Accounts)] 79 | pub struct Initialize<'info> { 80 | #[account(init, payer = authority, space = 5000, seeds = [b"receipts"], bump)] 81 | pub receipts: Account<'info, Receipts>, 82 | #[account(mut)] 83 | pub authority: Signer<'info>, 84 | pub system_program: Program<'info, System>, 85 | pub rent: Sysvar<'info, Rent>, 86 | } 87 | 88 | #[derive(Accounts)] 89 | pub struct BuyShot<'info> { 90 | #[account(mut, seeds = [b"receipts"], bump)] 91 | pub receipts: Account<'info, Receipts>, 92 | #[account(mut)] 93 | pub signer: Signer<'info>, 94 | /// CHECK: checked against the treasury pubkey. 95 | #[account(mut)] 96 | pub treasury: AccountInfo<'info>, 97 | pub system_program: Program<'info, System>, 98 | } 99 | 100 | #[derive(Accounts)] 101 | pub struct MarkShotAsDelivered<'info> { 102 | #[account(mut, seeds = [b"receipts"], bump)] 103 | pub receipts: Account<'info, Receipts>, 104 | #[account(mut)] 105 | pub signer: Signer<'info>, 106 | } 107 | 108 | #[account()] 109 | pub struct Receipts { 110 | pub receipts: Vec, 111 | pub total_shots_sold: u64, 112 | } 113 | 114 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] 115 | pub struct Receipt { 116 | pub receipt_id: u64, 117 | pub buyer: Pubkey, 118 | pub was_delivered: bool, 119 | pub price: u64, 120 | pub timestamp: i64, 121 | } 122 | -------------------------------------------------------------------------------- /solana-bar/raspberry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", 4 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check", 5 | "build": "webpack" 6 | }, 7 | "dependencies": { 8 | "@coral-xyz/anchor": "^0.28.0", 9 | "onoff": "^6.0.3", 10 | "webpack": "^5.88.2" 11 | }, 12 | "devDependencies": { 13 | "@types/bn.js": "^5.1.0", 14 | "@types/chai": "^4.3.0", 15 | "@types/mocha": "^9.0.0", 16 | "chai": "^4.3.4", 17 | "mocha": "^9.0.3", 18 | "prettier": "^2.6.2", 19 | "ts-mocha": "^10.0.0", 20 | "typescript": "^4.3.5", 21 | "webpack-cli": "^5.1.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /solana-bar/raspberry/src/bar.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import { IDL, SolanaBar } from "./solana_bar"; 4 | import { clusterApiUrl, Connection, Keypair, PublicKey } from "@solana/web3.js"; 5 | 6 | var Gpio = require('onoff').Gpio; //include onoff to interact with the GPIO 7 | var GPIO_23 = new Gpio(23, 'out'); //use GPIO pin 18, and specify that it is output 8 | 9 | let connection = new Connection(clusterApiUrl("devnet")); 10 | 11 | // Replace this with your own keypair to be able to pay for fees 12 | const keypair = Keypair.fromSecretKey( 13 | Uint8Array.from([209,70,174,212,192,159,166,82,163,162,135,190,244,227,218,97,214,155,228,142,172,188,170,246,130,68,106,45,170,125,175,57,12,253,44,189,234,23,239,220,85,57,231,86,130,27,99,62,106,215,172,104,152,104,145,138,198,105,218,20,232,251,238,250]) 14 | ); 15 | 16 | // Or load from File: 17 | /*const keypair = new Uint8Array( 18 | JSON.parse( 19 | fs.readFileSync("shoUzmg5H2zDdevxS6UdQCiY6JnP1qSn7fPtCP726pR.json").toString()) 20 | ); 21 | let keyPair = Keypair.fromSecretKey(decodedKey); 22 | */ 23 | 24 | let wallet = new anchor.Wallet(keypair); 25 | const provider = new anchor.AnchorProvider(connection, wallet, { 26 | commitment: "confirmed", 27 | }); 28 | anchor.setProvider(provider); 29 | 30 | const program = new Program(IDL, "GCgyx9JPNpqX97iWQh7rqPjaignahkS8DqQGdDdfXsPQ", { connection }) 31 | 32 | console.log("Program ID", program.programId.toString()); 33 | 34 | startListeningToLedSwitchAccount(); 35 | 36 | async function startListeningToLedSwitchAccount() { 37 | const receiptsPDA = await anchor.web3.PublicKey.findProgramAddressSync( 38 | [ 39 | Buffer.from("receipts"), 40 | ], 41 | program.programId, 42 | )[0]; 43 | 44 | const receiptsAccount = await program.account.receipts.fetch( 45 | receiptsPDA 46 | ) 47 | 48 | console.log("Receipts account", JSON.stringify(receiptsAccount)); 49 | 50 | for (let i = 0; i < receiptsAccount.receipts.length; i++) { 51 | const receipt = receiptsAccount.receipts[i]; 52 | if (!receipt.wasDelivered) { 53 | await PourShotAndMarkAsDelivered(receipt); 54 | console.log("Receipt", JSON.stringify(receipt)); 55 | break; 56 | } 57 | } 58 | 59 | GPIO_23.writeSync(0); 60 | 61 | connection.onAccountChange(receiptsPDA, async (account) => { 62 | const decoded = program.coder.accounts.decode( 63 | "receipts", 64 | account.data 65 | ) 66 | 67 | for (let i = 0; i < decoded.receipts.length; i++) { 68 | const receipt = decoded.receipts[i]; 69 | if (!receipt.wasDelivered) { 70 | await PourShotAndMarkAsDelivered(receipt); 71 | break; 72 | } 73 | } 74 | 75 | console.log("Shot given out: "); 76 | }, "confirmed") 77 | 78 | async function PourShotAndMarkAsDelivered(receipt: { receiptId: anchor.BN; buyer: anchor.web3.PublicKey; wasDelivered: boolean; price: anchor.BN; timestamp: anchor.BN; }) { 79 | console.log("start puring receipt id: " + receipt.receiptId.toString()); 80 | 81 | GPIO_23.writeSync(1); 82 | await sleep(3000); 83 | GPIO_23.writeSync(0); 84 | 85 | console.log("done puring: " + receipt.receiptId.toString()); 86 | 87 | let ix = await program.methods.markShotAsDelivered(receipt.receiptId).accounts( 88 | { 89 | receipts: receiptsPDA, 90 | signer: wallet.publicKey, 91 | }).transaction(); 92 | 93 | console.log("ix", JSON.stringify(ix)); 94 | ix.feePayer = wallet.publicKey; 95 | var signature = await connection.sendTransaction(ix, [keypair], {skipPreflight: true}); 96 | 97 | console.log("Sent receipt mark as delivered: ", signature); 98 | } 99 | 100 | function sleep(ms) { 101 | return new Promise(resolve => setTimeout(resolve, ms)); 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /solana-bar/raspberry/src/shoUzmg5H2zDdevxS6UdQCiY6JnP1qSn7fPtCP726pR.json: -------------------------------------------------------------------------------- 1 | [209,70,174,212,192,159,166,82,163,162,135,190,244,227,218,97,214,155,228,142,172,188,170,246,130,68,106,45,170,125,175,57,12,253,44,189,234,23,239,220,85,57,231,86,130,27,99,62,106,215,172,104,152,104,145,138,198,105,218,20,232,251,238,250] -------------------------------------------------------------------------------- /solana-bar/raspberry/src/solana_bar.ts: -------------------------------------------------------------------------------- 1 | export type SolanaBar = { 2 | "version": "0.1.0", 3 | "name": "solana_bar", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [ 8 | { 9 | "name": "receipts", 10 | "isMut": true, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "authority", 15 | "isMut": true, 16 | "isSigner": true 17 | }, 18 | { 19 | "name": "systemProgram", 20 | "isMut": false, 21 | "isSigner": false 22 | }, 23 | { 24 | "name": "rent", 25 | "isMut": false, 26 | "isSigner": false 27 | } 28 | ], 29 | "args": [] 30 | }, 31 | { 32 | "name": "buyShot", 33 | "accounts": [ 34 | { 35 | "name": "receipts", 36 | "isMut": true, 37 | "isSigner": false 38 | }, 39 | { 40 | "name": "signer", 41 | "isMut": true, 42 | "isSigner": true 43 | }, 44 | { 45 | "name": "treasury", 46 | "isMut": true, 47 | "isSigner": false 48 | }, 49 | { 50 | "name": "systemProgram", 51 | "isMut": false, 52 | "isSigner": false 53 | } 54 | ], 55 | "args": [] 56 | }, 57 | { 58 | "name": "markShotAsDelivered", 59 | "accounts": [ 60 | { 61 | "name": "receipts", 62 | "isMut": true, 63 | "isSigner": false 64 | }, 65 | { 66 | "name": "signer", 67 | "isMut": true, 68 | "isSigner": true 69 | } 70 | ], 71 | "args": [ 72 | { 73 | "name": "recipeId", 74 | "type": "u64" 75 | } 76 | ] 77 | } 78 | ], 79 | "accounts": [ 80 | { 81 | "name": "receipts", 82 | "type": { 83 | "kind": "struct", 84 | "fields": [ 85 | { 86 | "name": "receipts", 87 | "type": { 88 | "vec": { 89 | "defined": "Receipt" 90 | } 91 | } 92 | }, 93 | { 94 | "name": "totalShotsSold", 95 | "type": "u64" 96 | } 97 | ] 98 | } 99 | } 100 | ], 101 | "types": [ 102 | { 103 | "name": "Receipt", 104 | "type": { 105 | "kind": "struct", 106 | "fields": [ 107 | { 108 | "name": "receiptId", 109 | "type": "u64" 110 | }, 111 | { 112 | "name": "buyer", 113 | "type": "publicKey" 114 | }, 115 | { 116 | "name": "wasDelivered", 117 | "type": "bool" 118 | }, 119 | { 120 | "name": "price", 121 | "type": "u64" 122 | }, 123 | { 124 | "name": "timestamp", 125 | "type": "i64" 126 | } 127 | ] 128 | } 129 | } 130 | ], 131 | "errors": [ 132 | { 133 | "code": 6000, 134 | "name": "InvalidTreasury", 135 | "msg": "InvalidTreasury" 136 | } 137 | ] 138 | }; 139 | 140 | export const IDL: SolanaBar = { 141 | "version": "0.1.0", 142 | "name": "solana_bar", 143 | "instructions": [ 144 | { 145 | "name": "initialize", 146 | "accounts": [ 147 | { 148 | "name": "receipts", 149 | "isMut": true, 150 | "isSigner": false 151 | }, 152 | { 153 | "name": "authority", 154 | "isMut": true, 155 | "isSigner": true 156 | }, 157 | { 158 | "name": "systemProgram", 159 | "isMut": false, 160 | "isSigner": false 161 | }, 162 | { 163 | "name": "rent", 164 | "isMut": false, 165 | "isSigner": false 166 | } 167 | ], 168 | "args": [] 169 | }, 170 | { 171 | "name": "buyShot", 172 | "accounts": [ 173 | { 174 | "name": "receipts", 175 | "isMut": true, 176 | "isSigner": false 177 | }, 178 | { 179 | "name": "signer", 180 | "isMut": true, 181 | "isSigner": true 182 | }, 183 | { 184 | "name": "treasury", 185 | "isMut": true, 186 | "isSigner": false 187 | }, 188 | { 189 | "name": "systemProgram", 190 | "isMut": false, 191 | "isSigner": false 192 | } 193 | ], 194 | "args": [] 195 | }, 196 | { 197 | "name": "markShotAsDelivered", 198 | "accounts": [ 199 | { 200 | "name": "receipts", 201 | "isMut": true, 202 | "isSigner": false 203 | }, 204 | { 205 | "name": "signer", 206 | "isMut": true, 207 | "isSigner": true 208 | } 209 | ], 210 | "args": [ 211 | { 212 | "name": "recipeId", 213 | "type": "u64" 214 | } 215 | ] 216 | } 217 | ], 218 | "accounts": [ 219 | { 220 | "name": "receipts", 221 | "type": { 222 | "kind": "struct", 223 | "fields": [ 224 | { 225 | "name": "receipts", 226 | "type": { 227 | "vec": { 228 | "defined": "Receipt" 229 | } 230 | } 231 | }, 232 | { 233 | "name": "totalShotsSold", 234 | "type": "u64" 235 | } 236 | ] 237 | } 238 | } 239 | ], 240 | "types": [ 241 | { 242 | "name": "Receipt", 243 | "type": { 244 | "kind": "struct", 245 | "fields": [ 246 | { 247 | "name": "receiptId", 248 | "type": "u64" 249 | }, 250 | { 251 | "name": "buyer", 252 | "type": "publicKey" 253 | }, 254 | { 255 | "name": "wasDelivered", 256 | "type": "bool" 257 | }, 258 | { 259 | "name": "price", 260 | "type": "u64" 261 | }, 262 | { 263 | "name": "timestamp", 264 | "type": "i64" 265 | } 266 | ] 267 | } 268 | } 269 | ], 270 | "errors": [ 271 | { 272 | "code": 6000, 273 | "name": "InvalidTreasury", 274 | "msg": "InvalidTreasury" 275 | } 276 | ] 277 | }; 278 | -------------------------------------------------------------------------------- /solana-bar/raspberry/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /solana-bar/tests/solana_bar.ts: -------------------------------------------------------------------------------- 1 | export type SolanaBar = { 2 | "version": "0.1.0", 3 | "name": "solana_bar", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [ 8 | { 9 | "name": "receipts", 10 | "isMut": true, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "authority", 15 | "isMut": true, 16 | "isSigner": true 17 | }, 18 | { 19 | "name": "systemProgram", 20 | "isMut": false, 21 | "isSigner": false 22 | }, 23 | { 24 | "name": "rent", 25 | "isMut": false, 26 | "isSigner": false 27 | } 28 | ], 29 | "args": [] 30 | }, 31 | { 32 | "name": "buyShot", 33 | "accounts": [ 34 | { 35 | "name": "receipts", 36 | "isMut": true, 37 | "isSigner": false 38 | }, 39 | { 40 | "name": "signer", 41 | "isMut": true, 42 | "isSigner": true 43 | }, 44 | { 45 | "name": "treasury", 46 | "isMut": true, 47 | "isSigner": false 48 | }, 49 | { 50 | "name": "systemProgram", 51 | "isMut": false, 52 | "isSigner": false 53 | } 54 | ], 55 | "args": [] 56 | }, 57 | { 58 | "name": "markShotAsDelivered", 59 | "accounts": [ 60 | { 61 | "name": "receipts", 62 | "isMut": true, 63 | "isSigner": false 64 | }, 65 | { 66 | "name": "signer", 67 | "isMut": true, 68 | "isSigner": true 69 | } 70 | ], 71 | "args": [ 72 | { 73 | "name": "recipeId", 74 | "type": "u64" 75 | } 76 | ] 77 | } 78 | ], 79 | "accounts": [ 80 | { 81 | "name": "receipts", 82 | "type": { 83 | "kind": "struct", 84 | "fields": [ 85 | { 86 | "name": "receipts", 87 | "type": { 88 | "vec": { 89 | "defined": "Receipt" 90 | } 91 | } 92 | }, 93 | { 94 | "name": "totalShotsSold", 95 | "type": "u64" 96 | } 97 | ] 98 | } 99 | } 100 | ], 101 | "types": [ 102 | { 103 | "name": "Receipt", 104 | "type": { 105 | "kind": "struct", 106 | "fields": [ 107 | { 108 | "name": "receiptId", 109 | "type": "u64" 110 | }, 111 | { 112 | "name": "buyer", 113 | "type": "publicKey" 114 | }, 115 | { 116 | "name": "wasDelivered", 117 | "type": "bool" 118 | }, 119 | { 120 | "name": "price", 121 | "type": "u64" 122 | }, 123 | { 124 | "name": "timestamp", 125 | "type": "i64" 126 | } 127 | ] 128 | } 129 | } 130 | ], 131 | "errors": [ 132 | { 133 | "code": 6000, 134 | "name": "InvalidTreasury", 135 | "msg": "InvalidTreasury" 136 | } 137 | ] 138 | }; 139 | 140 | export const IDL: SolanaBar = { 141 | "version": "0.1.0", 142 | "name": "solana_bar", 143 | "instructions": [ 144 | { 145 | "name": "initialize", 146 | "accounts": [ 147 | { 148 | "name": "receipts", 149 | "isMut": true, 150 | "isSigner": false 151 | }, 152 | { 153 | "name": "authority", 154 | "isMut": true, 155 | "isSigner": true 156 | }, 157 | { 158 | "name": "systemProgram", 159 | "isMut": false, 160 | "isSigner": false 161 | }, 162 | { 163 | "name": "rent", 164 | "isMut": false, 165 | "isSigner": false 166 | } 167 | ], 168 | "args": [] 169 | }, 170 | { 171 | "name": "buyShot", 172 | "accounts": [ 173 | { 174 | "name": "receipts", 175 | "isMut": true, 176 | "isSigner": false 177 | }, 178 | { 179 | "name": "signer", 180 | "isMut": true, 181 | "isSigner": true 182 | }, 183 | { 184 | "name": "treasury", 185 | "isMut": true, 186 | "isSigner": false 187 | }, 188 | { 189 | "name": "systemProgram", 190 | "isMut": false, 191 | "isSigner": false 192 | } 193 | ], 194 | "args": [] 195 | }, 196 | { 197 | "name": "markShotAsDelivered", 198 | "accounts": [ 199 | { 200 | "name": "receipts", 201 | "isMut": true, 202 | "isSigner": false 203 | }, 204 | { 205 | "name": "signer", 206 | "isMut": true, 207 | "isSigner": true 208 | } 209 | ], 210 | "args": [ 211 | { 212 | "name": "recipeId", 213 | "type": "u64" 214 | } 215 | ] 216 | } 217 | ], 218 | "accounts": [ 219 | { 220 | "name": "receipts", 221 | "type": { 222 | "kind": "struct", 223 | "fields": [ 224 | { 225 | "name": "receipts", 226 | "type": { 227 | "vec": { 228 | "defined": "Receipt" 229 | } 230 | } 231 | }, 232 | { 233 | "name": "totalShotsSold", 234 | "type": "u64" 235 | } 236 | ] 237 | } 238 | } 239 | ], 240 | "types": [ 241 | { 242 | "name": "Receipt", 243 | "type": { 244 | "kind": "struct", 245 | "fields": [ 246 | { 247 | "name": "receiptId", 248 | "type": "u64" 249 | }, 250 | { 251 | "name": "buyer", 252 | "type": "publicKey" 253 | }, 254 | { 255 | "name": "wasDelivered", 256 | "type": "bool" 257 | }, 258 | { 259 | "name": "price", 260 | "type": "u64" 261 | }, 262 | { 263 | "name": "timestamp", 264 | "type": "i64" 265 | } 266 | ] 267 | } 268 | } 269 | ], 270 | "errors": [ 271 | { 272 | "code": 6000, 273 | "name": "InvalidTreasury", 274 | "msg": "InvalidTreasury" 275 | } 276 | ] 277 | }; 278 | -------------------------------------------------------------------------------- /solana-bar/tests/solana_bar_test.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import { SolanaBar } from "../target/types/solana_bar"; 4 | import { assert } from "chai"; 5 | import { PublicKey } from "@solana/web3.js"; 6 | 7 | describe("SolanaBar", () => { 8 | anchor.setProvider(anchor.AnchorProvider.env()); 9 | 10 | const program = anchor.workspace.SolanaBar as Program; 11 | const wallet = anchor.workspace.SolanaBar.provider.wallet 12 | 13 | it("Is initialized!", async () => { 14 | 15 | const receiptsPDA = await anchor.web3.PublicKey.findProgramAddressSync( 16 | [ 17 | Buffer.from("receipts"), 18 | ], 19 | program.programId, 20 | )[0]; 21 | 22 | console.log("Receipts", receiptsPDA); 23 | 24 | try { 25 | const initializeTransaction = await program.methods.initialize().accounts( 26 | { 27 | receipts: receiptsPDA, 28 | authority: wallet.publicKey, 29 | systemProgram: anchor.web3.SystemProgram.programId, 30 | rent: anchor.web3.SYSVAR_RENT_PUBKEY, 31 | }, 32 | ).rpc(); 33 | console.log("Initialize transaction signature: ", initializeTransaction); 34 | } catch (e) { 35 | console.log(e); 36 | } 37 | 38 | const switchOnTransaction = await program.methods.buyShot().accounts( 39 | { 40 | receipts: receiptsPDA, 41 | signer: wallet.publicKey, 42 | treasury: new PublicKey("GsfNSuZFrT2r4xzSndnCSs9tTXwt47etPqU8yFVnDcXd"), 43 | systemProgram: anchor.web3.SystemProgram.programId, 44 | }, 45 | ).rpc(); 46 | 47 | const ledAccount = await program.account.receipts.fetch( 48 | receiptsPDA 49 | ) 50 | console.log("Your switch on transaction signature", switchOnTransaction); 51 | 52 | assert(ledAccount.receipts.length === 0, "Game data account is not initialized correctly. Should be on/true") 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /solana-bar/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /termina-data-anchor/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log -------------------------------------------------------------------------------- /termina-data-anchor/README.md: -------------------------------------------------------------------------------- 1 | # Termina Data Anchor Demo (CLI) 2 | 3 | Termina’s Data Anchor lets teams anchor structured data blobs directly on Solana without custom programs required. 4 | It’s especially useful for DePIN networks publishing verifiable device data like sensor readings or reward snapshots, in a cost-effective and high-throughput way. 5 | 6 | This example uses the CLI to walk through the full flow: upload a JSON reward file, fetch it from chain, and verify its contents locally. 7 | For production use, we recommend the Rust SDK for tighter integration. 8 | 9 | A quick look at the data flow of the Data Anchor: 10 | ![image](https://github.com/user-attachments/assets/2ea55114-fb30-4f16-a609-f69aa45368b1) 11 | 12 | --- 13 | 14 | ## What this demo shows 15 | 16 | This demo simulates a DePIN reward batch upload. 17 | 18 | - Two sensors report their data: IPs, CO₂ levels, data points submitted, and reward amounts 19 | - A Merkle root summarizes the batch, and a placeholder zk proof is attached 20 | - The file is uploaded as a blob via the CLI 21 | - The blob is fetched from Solana and decoded 22 | - A script verifies its structure 23 | 24 | > Think of this as a simplified version of how large DePIN networks might anchor thousands of device reports at once. 25 | 26 | --- 27 | 28 | ## Folder contents 29 | 30 | - `rewards.json` – Mock data file containing a reward batch from 2 devices 31 | - `upload_blob.sh` – Uploads the JSON to Solana using the Termina CLI 32 | - `verify_blob.sh` – Fetches and verifies the blob fields 33 | 34 | --- 35 | 36 | ## Prerequisites 37 | 38 | To run this demo, you just need: 39 | 40 | - A terminal (macOS, Linux, or WSL on Windows) 41 | - `jq` and `xxd` installed (used to parse and decode the blob output) 42 | 43 | --- 44 | 45 | ## 1. Prepare your data 46 | 47 | The file `rewards.json` simulates a real batch upload: 48 | 49 | ```json 50 | { 51 | "epoch": 1042, 52 | "location": "Zug, Switzerland", 53 | "devices": [ 54 | { 55 | "device_id": "sensor-001", 56 | "ip": "192.168.0.101", 57 | "data_points": 340, 58 | "co2_ppm": 417, 59 | "reward": "0.03" 60 | }, 61 | { 62 | "device_id": "sensor-002", 63 | "ip": "192.168.0.102", 64 | "data_points": 327, 65 | "co2_ppm": 419, 66 | "reward": "0.02" 67 | } 68 | ], 69 | "total_reward": "0.05", 70 | "merkle_root": "abc123xyz456", 71 | "proof": "mock_zk_proof_here" 72 | } 73 | ```` 74 | 75 | --- 76 | 77 | ## 2. Upload the blob 78 | 79 | Run the following to upload the file to Solana via the Data Anchor: 80 | 81 | ```bash 82 | ./upload_blob.sh 83 | ``` 84 | 85 | You’ll receive one or more blob signatures in response, depending on the file size. These signatures are needed to fetch the blob. 86 | 87 | --- 88 | 89 | ## 3. Fetch and decode the blob 90 | 91 | Use the CLI and signatures to retrieve the blob: 92 | 93 | ```bash 94 | nitro-da-cli \ 95 | --program-id "2RWsr92iL39YCLiZu7dZ5hron4oexEMbgWDg35v5U5tH" \ 96 | --namespace "nitro" \ 97 | -o json \ 98 | blob fetch ... \ 99 | | jq -r '.[].data' | xxd -r -p 100 | ``` 101 | 102 | Replace ` ...` with the signatures you received earlier. 103 | 104 | This will return the original JSON content. 105 | 106 | --- 107 | 108 | ## 4. Verify the blob contents 109 | 110 | To simulate how a verifier or client might validate the blob, run: 111 | 112 | ```bash 113 | ./verify_blob.sh ... 114 | ``` 115 | 116 | This script checks that the blob includes: 117 | 118 | * `device_id` 119 | * `reward` 120 | * `co2_ppm` 121 | * `merkle_root` 122 | * `proof` 123 | 124 | > This is an example verification logic. Actual validation requirements may vary based on the domain and use case. 125 | 126 | --- 127 | 128 | ## Link to official documentation 129 | 130 | This demo follows the steps from: 131 | 📖 [Using the Data Anchor](https://docs.termina.technology/documentation/network-extension-stack/ne-modules/data-anchor/using-the-data-anchor) 132 | 133 | It covers: 134 | 135 | * Blob upload 136 | * Retrieval and decoding 137 | * Basic verification workflow 138 | 139 | --- 140 | 141 | ## Crate Links (CLI and Module Components) 142 | 143 | The source code for the CLI and related components is published on [crates.io](https://crates.io), and visible to anyone who installs them: 144 | 145 | - [nitro-da-cli](https://crates.io/crates/nitro-da-cli) 146 | - [nitro-da-client](https://crates.io/crates/nitro-da-client) 147 | - [nitro-da-blober](https://crates.io/crates/nitro-da-blober) 148 | - [nitro-da-indexer-api](https://crates.io/crates/nitro-da-indexer-api) 149 | - [nitro-da-proofs](https://crates.io/crates/nitro-da-proofs) 150 | 151 | --- 152 | 153 | ## License 154 | 155 | MIT 156 | -------------------------------------------------------------------------------- /termina-data-anchor/rewards.json: -------------------------------------------------------------------------------- 1 | { 2 | "epoch": 1042, 3 | "location": "Zug, Switzerland", 4 | "devices": [ 5 | { 6 | "device_id": "sensor-001", 7 | "ip": "192.168.0.101", 8 | "data_points": 340, 9 | "co2_ppm": 417, 10 | "reward": "0.03" 11 | }, 12 | { 13 | "device_id": "sensor-002", 14 | "ip": "192.168.0.102", 15 | "data_points": 327, 16 | "co2_ppm": 419, 17 | "reward": "0.02" 18 | } 19 | ], 20 | "total_reward": "0.05", 21 | "merkle_root": "abc123xyz456", 22 | "proof": "mock_zk_proof_here" 23 | } -------------------------------------------------------------------------------- /termina-data-anchor/upload_blob.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # upload rewards.json using Termina's Data Anchor CLI 4 | # program ID below is the mainnet deployment of the Data Anchor 5 | # Mainnet Program ID: 9i2MEc7s38jLGoEkbFszuTJCL1w3Uorg7qjPjfN8Tv5Z 6 | # namespace used in this demo: "nitro" 7 | 8 | nitro-da-cli \ 9 | --program-id "9i2MEc7s38jLGoEkbFszuTJCL1w3Uorg7qjPjfN8Tv5Z" \ 10 | --namespace "nitro" \ 11 | blob upload --data-path "./rewards.json" 12 | -------------------------------------------------------------------------------- /termina-data-anchor/verify_blob.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # this is an example verification logic. Actual validation requirements may vary based on the domain and use case. 4 | # this script fetches a blob from the Termina Data Anchor and verifies expected fields exist 5 | 6 | PROGRAM_ID="2RWsr92iL39YCLiZu7dZ5hron4oexEMbgWDg35v5U5tH" 7 | NAMESPACE="nitro" 8 | SIGNATURES=("$@") # Accept blob signatures as arguments 9 | 10 | if [ ${#SIGNATURES[@]} -eq 0 ]; then 11 | echo "❌ Please provide blob signature(s)." 12 | exit 1 13 | fi 14 | 15 | # fetch the blob and decode from hex to plain JSON 16 | blob_data=$(nitro-da-cli \ 17 | --program-id "$PROGRAM_ID" \ 18 | --namespace "$NAMESPACE" \ 19 | -o json \ 20 | blob fetch "${SIGNATURES[@]}" \ 21 | | jq -r '.[].data' | xxd -r -p) 22 | 23 | # print the full decoded blob (for visibility) 24 | echo "---- Blob Content ----" 25 | echo "$blob_data" 26 | echo "----------------------" 27 | 28 | errors=0 29 | 30 | # check for required fields inside the blob 31 | for field in "device_id" "reward" "co2_ppm" "merkle_root" "proof"; do 32 | if ! echo "$blob_data" | grep -q "\"$field\""; then 33 | echo "❌ Missing field: $field" 34 | errors=$((errors + 1)) 35 | fi 36 | done 37 | 38 | # final result 39 | if [ "$errors" -eq 0 ]; then 40 | echo "✅ Blob passed verification." 41 | else 42 | echo "❌ Blob failed verification with $errors issue(s)." 43 | fi 44 | --------------------------------------------------------------------------------