-
Notifications
You must be signed in to change notification settings - Fork 49
docs: added ussd use case demo #470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
a6ff5b9
docs: added ussd use case demo
jayzalani 89f0c08
docs(ussd-rootstock): address review feedback
jayzalani 141b713
docs: remove Implementation file
jayzalani c463419
Apply suggestion from @owans
jayzalani 6fd7ac9
Apply suggestions from code review
jayzalani 71825f0
Apply suggestions from code review
jayzalani e179647
Added some corrected commands, Versions, and dash lines
jayzalani File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
356 changes: 356 additions & 0 deletions
356
docs/02-developers/09-use-cases/ussd-rootstock-defi/Implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,356 @@ | ||
| --- | ||
| sidebar_label: Implementation Guide | ||
| sidebar_position: 3 | ||
| title: USSD-RSK Implementation Guide | ||
| tags: [rsk, rootstock, developers, solidity, nodejs] | ||
| description: Step-by-step implementation guide for deploying the InclusiveDeFi smart contract on RSK Testnet and building the USSD relay server with Africa's Talking and ethers.js. | ||
| --- | ||
|
|
||
| # Implementation Guide | ||
|
|
||
| This guide walks through the complete implementation writing and deploying the `InclusiveDeFi` smart contract on RSK Testnet, building the USSD relay server, and wiring it to the Africa's Talking USSD gateway. | ||
| :::note | ||
| `InclusiveDeFi` is just name a file name here, We encourage to name your smart contract anything! | ||
| ::: | ||
| ## Prerequisites | ||
|
|
||
| Before starting, ensure you have the following installed: | ||
|
|
||
| - Node.js v18 or later | ||
| - `pnpm` or `npm` | ||
| - A funded RSK Testnet wallet (get tRBTC from the [RSK Testnet Faucet](https://faucet.rootstock.io)) | ||
| - An [Africa's Talking](https://africastalking.com) sandbox account | ||
|
|
||
| ## Project Structure | ||
|
|
||
| ``` | ||
| ussd-rsk/ | ||
| ├── contracts/ | ||
| │ └── InclusiveDeFi.sol | ||
| ├── ignition/ | ||
| │ └── modules/ | ||
| │ └── InclusiveDeFi.ts | ||
| ├── index.ts | ||
| ├── hardhat.config.ts | ||
| ├── .env | ||
| └── package.json | ||
| ``` | ||
|
|
||
| ## Step 1: Smart Contract | ||
|
|
||
| ### InclusiveDeFi.sol | ||
|
|
||
| The contract maintains two mappings: one for balances and one for active loans. It exposes four functions covering deposit, balance check, P2P transfer, and micro-loan issuance. | ||
|
|
||
| ```solidity | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.19; | ||
|
|
||
| contract InclusiveDeFi { | ||
| mapping(address => uint256) public balances; | ||
| mapping(address => uint256) public loans; | ||
|
|
||
| event Transfer(address indexed from, address indexed to, uint256 amount); | ||
| event LoanIssued(address indexed user, uint256 amount); | ||
|
|
||
| // P2P Transfer | ||
| function transfer(address to, uint256 amount) public { | ||
| require(balances[msg.sender] >= amount, "Insufficient balance"); | ||
| balances[msg.sender] -= amount; | ||
| balances[to] += amount; | ||
| emit Transfer(msg.sender, to, amount); | ||
| } | ||
|
|
||
| // Micro-Loan — fixed 0.01 tRBTC, one active loan per address | ||
| function applyForLoan() public { | ||
| require(loans[msg.sender] == 0, "Existing loan active"); | ||
| uint256 loanAmount = 0.01 ether; | ||
| loans[msg.sender] = loanAmount; | ||
| balances[msg.sender] += loanAmount; | ||
| emit LoanIssued(msg.sender, loanAmount); | ||
| } | ||
|
|
||
| // Balance query | ||
| function getBalance(address user) public view returns (uint256) { | ||
| return balances[user]; | ||
| } | ||
|
|
||
| // Deposit tRBTC into the contract | ||
| function deposit() public payable { | ||
| balances[msg.sender] += msg.value; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| :::note | ||
| The `applyForLoan()` function credits the loan amount directly to the caller's internal balance without requiring any on-chain collateral. This is intentional for the demo. In production, implement a collateral or credit-scoring mechanism before issuing loans. | ||
| ::: | ||
|
|
||
| ## Step 2: Hardhat Configuration | ||
|
|
||
| Install dependencies and configure Hardhat to target the RSK Testnet alongside the default local networks. | ||
|
|
||
| ```bash | ||
| npm install | ||
| ``` | ||
|
|
||
| ### hardhat.config.ts | ||
|
|
||
| ```typescript | ||
| import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers"; | ||
| import { configVariable, defineConfig } from "hardhat/config"; | ||
| import * as dotenv from "dotenv"; | ||
|
|
||
| dotenv.config(); | ||
|
|
||
| export default defineConfig({ | ||
| plugins: [hardhatToolboxMochaEthersPlugin], | ||
| solidity: { | ||
| profiles: { | ||
| default: { version: "0.8.28" }, | ||
| production: { | ||
| version: "0.8.28", | ||
| settings: { optimizer: { enabled: true, runs: 200 } }, | ||
| }, | ||
| }, | ||
| }, | ||
| networks: { | ||
| rskTestnet: { | ||
| type: "http", | ||
| chainType: "l1", | ||
| url: "https://public-node.testnet.rsk.co", | ||
| chainId: 31, | ||
| accounts: [process.env.PRIVATE_KEY!], | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| ## Step 3: Environment Variables | ||
|
|
||
| Create a `.env` file in the project root. **Never commit this file to version control.** | ||
|
|
||
| ```bash | ||
| PRIVATE_KEY=your_relayer_wallet_private_key_here | ||
| RSK_TESTNET_RPC=https://public-node.testnet.rsk.co | ||
| ``` | ||
|
|
||
| :::warning | ||
| The private key stored here controls the relayer wallet. This wallet signs all transactions on behalf of users. Keep it funded with tRBTC and treat the key as a production secret. | ||
| ::: | ||
|
|
||
| Ensure `.env` is in your `.gitignore`: | ||
|
|
||
| ``` | ||
| # .gitignore | ||
| node_modules/ | ||
| dist/ | ||
| artifacts/ | ||
| cache/ | ||
| .env | ||
| ``` | ||
|
|
||
| ## Step 4: Deploy the Contract | ||
|
|
||
| ### Ignition Module | ||
|
|
||
| Create the Hardhat Ignition deployment module: | ||
|
|
||
| ```typescript | ||
| // ignition/modules/InclusiveDeFi.ts | ||
| import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; | ||
|
|
||
| const InclusiveDeFiModule = buildModule("InclusiveDeFiModule", (m) => { | ||
| const inclusiveDeFi = m.contract("InclusiveDeFi"); | ||
| return { inclusiveDeFi }; | ||
| }); | ||
|
|
||
| export default InclusiveDeFiModule; | ||
| ``` | ||
|
|
||
| ### Deploy to RSK Testnet | ||
|
|
||
| ```bash | ||
| npx hardhat ignition deploy --network rskTestnet ignition/modules/InclusiveDeFi.ts | ||
| ``` | ||
|
|
||
| On successful deployment, Hardhat Ignition will output the deployed contract address. Record this address — it is needed in the relay server configuration. | ||
|
|
||
| Example output: | ||
|
|
||
| ``` | ||
| Deployed Addresses | ||
| InclusiveDeFiModule#InclusiveDeFi: 0x636Cd39bE612d78019C534aA168B9c5969c0c966 | ||
| ``` | ||
|
|
||
| You can verify the deployment on the [RSK Testnet Explorer](https://explorer.testnet.rootstock.io). | ||
|
|
||
| ## Step 5: USSD Relay Server | ||
|
|
||
| The relay server is an Express application with a single `/ussd` POST endpoint. It connects to RSK via ethers.js, parses the USSD session state from the `text` field, and routes each request to the appropriate contract function. | ||
|
|
||
| ```typescript | ||
| // index.ts | ||
| import express, { Request, Response } from 'express'; | ||
| import { ethers } from 'ethers'; | ||
| import * as dotenv from 'dotenv'; | ||
|
|
||
| dotenv.config(); | ||
|
|
||
| const app = express(); | ||
| app.use(express.urlencoded({ extended: false })); | ||
|
|
||
| const provider = new ethers.JsonRpcProvider(process.env.RSK_TESTNET_RPC); | ||
| const wallet = new ethers.Wallet(process.env.PRIVATE_KEY as string, provider); | ||
|
|
||
| const CONTRACT_ADDRESS = "0x636Cd39bE612d78019C534aA168B9c5969c0c966"; | ||
| const ABI = [ | ||
| "function getBalance(address user) view returns (uint256)", | ||
| "function transfer(address to, uint256 amount)", | ||
| "function applyForLoan()" | ||
| ]; | ||
|
|
||
| const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, wallet); | ||
|
|
||
| app.post('/ussd', async (req: Request, res: Response) => { | ||
| const { text, phoneNumber } = req.body; | ||
| let response = ""; | ||
| const input = text.split('*'); | ||
|
|
||
| if (text === "") { | ||
| // Entry point — show main menu | ||
| response = `CON Rootstock DeFi (${phoneNumber}) | ||
| 1. My Balance | ||
| 2. Send Money (P2P) | ||
| 3. Request Micro-Loan`; | ||
|
|
||
| } else if (input[0] === "1") { | ||
| // Balance check — read call, no gas required | ||
| const bal = await contract.getBalance(wallet.address); | ||
| response = `END Your Balance: ${ethers.formatEther(bal)} tRBTC`; | ||
|
|
||
| } else if (input[0] === "2") { | ||
| if (!input[1]) { | ||
| response = "CON Enter Recipient Address:"; | ||
| } else if (!input[2]) { | ||
| response = "CON Enter Amount:"; | ||
| } else { | ||
| // Execute transfer — signed transaction broadcasted to RSK | ||
| const tx = await contract.transfer(input[1], ethers.parseEther(input[2])); | ||
| await tx.wait(); | ||
| response = `END Transfer Sent! Hash: ${tx.hash.substring(0, 10)}`; | ||
| } | ||
|
|
||
| } else if (input[0] === "3") { | ||
| // Micro-loan — signed transaction | ||
| const tx = await contract.applyForLoan(); | ||
| await tx.wait(); | ||
| response = `END Loan of 0.01 tRBTC approved and deposited!`; | ||
| } | ||
|
|
||
| res.header('Content-Type', 'text/plain'); | ||
| res.send(response); | ||
| }); | ||
|
|
||
| app.listen(3000, () => console.log("RSK-USSD Bridge running on port 3000")); | ||
| ``` | ||
|
|
||
| ### Start the Server | ||
|
|
||
| ```bash | ||
| npm run start-bridge | ||
| ``` | ||
|
|
||
| The server starts on port `3000`. To expose it to the Africa's Talking gateway, use a tunnelling tool like [ngrok](https://ngrok.com) during local development: | ||
|
|
||
| ```bash | ||
| ngrok http 3000 | ||
| ``` | ||
|
|
||
| Copy the generated HTTPS URL (e.g., `https://abc123.ngrok.io`) — you will register this as your USSD callback URL. | ||
|
|
||
| ## Step 6: Africa's Talking USSD Gateway Configuration | ||
|
|
||
| 1. Log into your [Africa's Talking sandbox](https://account.africastalking.com/apps/sandbox) | ||
| 2. Navigate to **USSD → Create Channel** | ||
| 3. Assign a shortcode (e.g., `*384#`) | ||
| 4. Set the **Callback URL** to your relay server endpoint: `https://your-ngrok-url.ngrok.io/ussd` | ||
| 5. Save and activate the channel | ||
|
|
||
| You can now test your full USSD flow using the Africa's Talking **USSD Simulator** in the sandbox dashboard. | ||
|
|
||
| ## Step 7: Testing the Flow | ||
|
|
||
| ### Simulated USSD Test (curl) | ||
|
|
||
| You can test the relay server directly without the Africa's Talking gateway using `curl`: | ||
|
|
||
| ```bash | ||
| # Simulate dialing — entry menu | ||
| curl -X POST http://localhost:3000/ussd \ | ||
| -d "text=&phoneNumber=%2B2348012345678&sessionId=test001" | ||
|
|
||
| # Simulate selecting balance check | ||
| curl -X POST http://localhost:3000/ussd \ | ||
| -d "text=1&phoneNumber=%2B2348012345678&sessionId=test001" | ||
|
|
||
| # Simulate requesting a micro-loan | ||
| curl -X POST http://localhost:3000/ussd \ | ||
| -d "text=3&phoneNumber=%2B2348012345678&sessionId=test001" | ||
| ``` | ||
|
|
||
| ### Hardhat Unit Tests | ||
|
|
||
| Run the included test suite against a local simulated network: | ||
|
|
||
| ```bash | ||
| npx hardhat test | ||
| ``` | ||
|
|
||
| ### Verify On-Chain | ||
|
|
||
| After running a transaction through the relay server, verify it on the RSK Testnet Explorer: | ||
|
|
||
| ``` | ||
| https://explorer.testnet.rootstock.io/address/0x636Cd39bE612d78019C534aA168B9c5969c0c966 | ||
| ``` | ||
|
|
||
| ## package.json Reference | ||
|
|
||
| ```json | ||
| { | ||
| "name": "ussd-rsk", | ||
| "version": "1.0.0", | ||
| "type": "module", | ||
| "scripts": { | ||
| "start-bridge": "tsx index.ts" | ||
| }, | ||
| "dependencies": { | ||
| "dotenv": "^17.3.1", | ||
| "express": "^5.2.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", | ||
| "ethers": "^6.16.0", | ||
| "hardhat": "^3.1.9", | ||
| "tsx": "^4.21.0", | ||
| "typescript": "~5.8.0" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Limitations & Future Work | ||
|
|
||
| The current implementation is a functional proof of concept. The following improvements are recommended before any production deployment: | ||
|
|
||
| **Per-user wallet mapping:** Map each `phoneNumber` to a dedicated wallet address. Store this mapping in a database (e.g., PostgreSQL or Redis) so that `getBalance()` returns the correct user's balance, not the relay wallet's. | ||
|
|
||
| **Loan repayment:** Add a `repayLoan()` function to `InclusiveDeFi.sol` that accepts tRBTC and clears the `loans[msg.sender]` entry, enabling reuse of the loan feature. | ||
|
|
||
| **Input validation:** Sanitize all user-supplied input (recipient addresses, amounts) in the relay server before passing to ethers.js. An invalid address or a non-numeric amount will cause an unhandled exception that crashes the USSD session. | ||
|
|
||
| **Error handling:** Wrap all `await` calls in try/catch blocks and return graceful `END` messages to the user on failure rather than timing out their session. | ||
|
|
||
| **Meta-transaction standard:** Consider implementing [EIP-2771](https://eips.ethereum.org/EIPS/eip-2771) to formalize the relayer pattern and allow the smart contract to identify the original user (by phone-mapped wallet) rather than the relay wallet address. | ||
|
|
||
| **Production secrets:** Move the relayer private key out of `.env` and into a secrets manager or HSM-backed signing service before any mainnet deployment. |
7 changes: 7 additions & 0 deletions
7
docs/02-developers/09-use-cases/ussd-rootstock-defi/_category_.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
|
|
||
| label: USSD Rootstock DeFi | ||
| position: 4 | ||
| link: | ||
| type: generated-index | ||
| slug: /developers/use-cases/ussd-rootstock-defi/ | ||
| description: "Use USSD to interact with DeFi protocols Rootstock." |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kindly update the title here as well