-
Notifications
You must be signed in to change notification settings - Fork 50
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
Changes from all commits
a6ff5b9
89f0c08
141b713
c463419
6fd7ac9
71825f0
e179647
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 on Rootstock." |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| --- | ||
| sidebar_label: Architecture & Data Flow | ||
| sidebar_position: 2 | ||
| title: USSD-Rootstock Architecture & Data Flow | ||
| tags: [rsk, rootstock, developers, architecture, ussd] | ||
| description: Understand the complete USSD session lifecycle, relay server architecture, and how the Rootstock JSON-RPC layer connects feature phone interactions to on-chain smart contract execution. | ||
| --- | ||
|
|
||
| ## Architecture & Data Flow | ||
|
|
||
| This page describes the full architecture of the USSD-Rootstock bridge how a user interaction on a feature phone translates into a signed blockchain transaction on the Rootstock network. | ||
|
|
||
| ## Components Overview | ||
|
|
||
| Four core components work together in a linear pipeline to convert user input into an on-chain state change: | ||
|
|
||
| ### 1. GSM Network & USSD Gateway | ||
|
|
||
| USSD sessions are stateful, real-time text sessions established over a GSM network. When a user dials a shortcode (e.g., `*384#`), the telecom carrier routes the session to a configured callback URL via **HTTP POST**. In this architecture, [Africa's Talking](https://developers.africastalking.com/docs/ussd/overview) provides that gateway. | ||
|
|
||
| Each HTTP POST from the gateway includes: | ||
|
|
||
| - `sessionId`: A unique string identifying the current USSD session | ||
| - `phoneNumber`: The caller's MSISDN (phone number) | ||
| - `text`: A `*`-delimited string of all user inputs in the current session | ||
|
|
||
| **Session state is fully encoded in the `text` field.** No server-side session storage is required. For example, if a user selected option `2` then entered a recipient address, the `text` field arrives as: | ||
|
|
||
| ```plaintext | ||
| 2*0xRecipientAddress | ||
| ``` | ||
|
|
||
| ### 2. Node.js Relay Server | ||
|
|
||
| The Express server is the core bridge component. It is responsible for: | ||
|
|
||
| - Parsing the incoming USSD payload and determining which menu level the user is at | ||
| - Routing the request to either a read call (balance check) or a write transaction (transfer, loan) | ||
| - Signing and broadcasting transactions to the Rootstock network using **ethers.js** | ||
| - Returning a correctly formatted USSD response string | ||
|
|
||
| USSD responses must begin with either `CON` (continue → show next menu) or `END` (terminate the session and display a final message). No other prefixes are valid. | ||
|
|
||
| ### 3. Ethers.js + Rootstock JSON-RPC | ||
|
|
||
| The relay server connects to the Rootstock network using a standard JSON-RPC provider pointed at the Rootstock public node: | ||
|
|
||
| ```plaintext | ||
| https://dev.rootstock.io/developers/rpc-api/rootstock/ | ||
| ``` | ||
|
|
||
| A single **relayer wallet** signs all transactions, with its private key loaded from an environment variable. This wallet holds tRBTC to pay gas fees on behalf of users. The wallet interacts with the deployed `InclusiveDeFi` contract through an ABI-defined interface. | ||
|
|
||
| ### 4. InclusiveDeFi Smart Contract | ||
|
|
||
| The on-chain component is a Solidity contract that maintains internal balance and loan mappings. It does not use ERC-20 or any external token standard, it uses native tRBTC via `msg.value` deposits and internal accounting via `mapping(address => uint256)`. | ||
|
|
||
| ## USSD Session Lifecycle | ||
|
|
||
| The following diagram shows the full data flow for a **balance check** request: | ||
|
|
||
| ```plaintext | ||
| User dials *384# | ||
| ↓ | ||
| GSM Network routes session to Africa's Talking gateway | ||
| ↓ | ||
| Africa's Talking sends HTTP POST to relay server: | ||
| { | ||
| text: "", | ||
| phoneNumber: "+2348012345678", | ||
| sessionId: "ATsession_xyz" | ||
| } | ||
| ↓ | ||
| Relay server parses text === "" → serves main menu: | ||
| CON Rootstock DeFi (+2348012345678) | ||
| 1. My Balance | ||
| 2. Send Money (P2P) | ||
| 3. Request Micro-Loan | ||
| ↓ | ||
| User selects 1 | ||
| ↓ | ||
| Africa's Talking sends HTTP POST: | ||
| { text: "1", phoneNumber: "...", sessionId: "..." } | ||
| ↓ | ||
| Relay server calls contract.getBalance(wallet.address) | ||
| → Rootstock JSON-RPC read call (no gas, no transaction) | ||
| ↓ | ||
| Relay server formats and returns: | ||
| END Your Balance: 0.01 tRBTC | ||
| ↓ | ||
| Session terminates. User sees final balance on screen. | ||
| ``` | ||
|
|
||
| ## USSD Session Lifecycle and P2P Transfer | ||
|
|
||
| A transfer requires **three sequential interactions** within one USSD session, which is why stateless `text` accumulation is critical: | ||
|
|
||
| ```plaintext | ||
| Round 1: text = "" → Main menu shown | ||
| Round 2: text = "2" → Prompt: "Enter Recipient Address:" | ||
| Round 3: text = "2*0xABC" → Prompt: "Enter Amount:" | ||
| Round 4: text = "2*0xABC*0.005" | ||
| → Relay server: | ||
| 1. Parses input[1] = "0xABC" (recipient) | ||
| 2. Parses input[2] = "0.005" (amount) | ||
| 3. Calls contract.transfer("0xABC", parseEther("0.005")) | ||
| 4. Signs & broadcasts tx via Rootstock JSON-RPC | ||
| 5. Awaits tx confirmation | ||
| → Returns: END Transfer Sent! Hash: 0xa924c40e... | ||
| ``` | ||
|
|
||
| :::note | ||
| Each round trip has a latency budget imposed by the telecom network. Typical USSD gateways require a response within **5–10 seconds**. For write operations (transfer, loan), the relay server calls `tx.wait()` which blocks until the transaction is mined on Rootstock. Ensure your Rootstock node connection is reliable and monitor average block confirmation time on the testnet. | ||
| ::: | ||
|
|
||
| ## Relay Server Architecture | ||
|
|
||
| The relay server is a minimal Express application. Its request handler implements a single `/ussd` POST route that acts as the USSD state machine: | ||
|
|
||
| ```plaintext | ||
| POST /ussd | ||
| │ | ||
| ├── text === "" → Serve main menu (CON) | ||
| │ | ||
| ├── input[0] === "1" → Read: getBalance() → END | ||
| │ | ||
| ├── input[0] === "2" | ||
| │ ├── !input[1] → Prompt: Enter recipient (CON) | ||
| │ ├── !input[2] → Prompt: Enter amount (CON) | ||
| │ └── input[1] & [2] → Write: transfer() → END | ||
| │ | ||
| └── input[0] === "3" → Write: applyForLoan() → END | ||
| ``` | ||
|
|
||
| This pattern is intentionally flat and stateless. Every USSD payload contains the complete session history in the `text` field no database or session store is needed. | ||
|
|
||
| ## Security Considerations | ||
|
|
||
| The current proof-of-concept architecture has deliberate simplifications that are acceptable for a guidance demo but must be addressed in production: | ||
|
|
||
| **Single relayer wallet:** All users share one relayer wallet address. This means `getBalance()` always returns the relay wallet's balance, not the individual user's. In production, map each `phoneNumber` to a unique wallet address stored in a database. | ||
|
|
||
| **Private key in environment variables:** The relayer's private key is stored in `.env`. This is standard practice for local development but is not acceptable for production. Use a secrets manager (e.g., AWS Secrets Manager, HashiCorp Vault) or an HSM-backed signing service. | ||
|
|
||
| **No input sanitization:** Recipient addresses and amounts from USSD text are passed directly to ethers.js. Validate Ethereum address format and parse amounts safely before calling contract functions. | ||
|
|
||
| **No loan repayment logic:** The current `applyForLoan()` function issues a loan with no repayment mechanism. This is a demo limitation. Implement a separate repayment flow and time-based interest logic for production. | ||
|
|
||
| ## Gas & Fee Model | ||
|
|
||
| The relayer wallet pays all gas fees on behalf of users. This is a **meta-transaction pattern** (without formal EIP-2771 implementation). The implications are: | ||
|
|
||
| Keep the relayer wallet funded with sufficient tRBTC for gas. Users never need to hold tRBTC to interact with the system. Gas costs on Rootstock are denominated in RBTC and are significantly cheaper than Ethereum mainnet. Build a fee recovery model (e.g., charging a small service fee per transaction or operating as a subsidized financial inclusion service). | ||
|
Check failure on line 153 in docs/02-developers/09-use-cases/ussd-rootstock-defi/architecture.md
|
||
|
|
||
| ## Network Configuration | ||
|
|
||
| | Parameter | Value | | ||
| |---|---| | ||
| | Network | RSK Testnet | | ||
|
Check failure on line 159 in docs/02-developers/09-use-cases/ussd-rootstock-defi/architecture.md
|
||
| | Chain ID | 31 | | ||
| | RPC URL | `https://dev.rootstock.io/developers/rpc-api/rootstock/` | | ||
| | Block Explorer | `https://explorer.testnet.rootstock.io` | | ||
| | Native Currency | tRBTC | | ||
| | Consensus | Merge-mined with Bitcoin (PoW) | | ||
|
|
||
| Rootstock is fully EVM-compatible. Use standard Ethereum tooling (ethers.js, Hardhat, Ignition) without modification by pointing it at the Rootstock RPC endpoint. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| --- | ||
| sidebar_label: Demo & Testing | ||
| sidebar_position: 5 | ||
| title: Demo, Testing & Limitations | ||
| tags: [rsk, rootstock, developers, defi, ussd] | ||
| description: Validate the USSD-RSK bridge end-to-end using the Africa's Talking simulator and curl. Verify on-chain transactions via the Rootstock Testnet Explorer and understand current limitations. | ||
|
Check failure on line 6 in docs/02-developers/09-use-cases/ussd-rootstock-defi/demo-and-testing.md
|
||
| --- | ||
|
|
||
| This page walks through end-to-end validation of the USSD-RSK bridge using the Africa's Talking sandbox simulator, raw `curl` commands, and on-chain verification via the Roostock Testnet Explorer. It also documents the current known limitations of the proof-of-concept and recommended improvements required before any production deployment. | ||
|
Check failure on line 9 in docs/02-developers/09-use-cases/ussd-rootstock-defi/demo-and-testing.md
|
||
|
|
||
| ## Live Demo Overview | ||
|
|
||
| The following demo shows the complete USSD flow running against the deployed `InclusiveDeFi` contract on Roostock Testnet. A user on a feature phone dials a shortcode, navigates the text menu, and triggers real blockchain transactions without a smartphone or internet connection. | ||
|
|
||
| ### Main Menu | ||
|
|
||
| When a user dials the shortcode (e.g., `*384#`), the relay server responds with the main menu. The phone number is echoed back to confirm the active session. | ||
|
|
||
| <img src="/img/developers/use-cases/ussd/2-main-menu.png"/> | ||
|
|
||
| ### Balance Check | ||
|
|
||
| Selecting option `1` triggers a read call to `getBalance(wallet.address)` on the Roostock node. No transaction is broadcast and no gas is consumed. The result is returned immediately and the session ends. | ||
|
|
||
| <img src="/img/developers/use-cases/ussd/3-balance-check.png"/> | ||
|
|
||
| ### P2P Transfer Flow | ||
|
|
||
| Selecting option `2` opens a three-round collection flow. The user enters the recipient address in round 2 and the amount in round 3. The relay server signs and broadcasts the transaction on round 4. | ||
| <img src="/img/developers/use-cases/ussd/6-transfer-01.png"/> | ||
| <img src="/img/developers/use-cases/ussd/6-transfer-02.png"/> | ||
| <img src="/img/developers/use-cases/ussd/6-transfer-03.png"/> | ||
| <img src="/img/developers/use-cases/ussd/6-transfer-04.png"/> | ||
|
|
||
| ### Micro-Loan | ||
|
|
||
| Selecting option `3` immediately calls `applyForLoan()`. If the address has no existing active loan, the contract credits `0.01 tRBTC` to its internal balance and emits a `LoanIssued` event. | ||
|
|
||
| <img src="/img/developers/use-cases/ussd/5-loan-creadited.png"/> | ||
|
|
||
| ### On-Chain Transaction Confirmation | ||
|
|
||
| Every write operation (transfer, loan) produces a transaction that can be verified on the Rootstock Testnet Explorer. The screenshot below shows a confirmed `applyForLoan` transaction. | ||
|
|
||
| ## Testing Without a Phone Using curl | ||
|
|
||
| You can simulate the full USSD session lifecycle against your local relay server using `curl`. This allows you to test all branches of the state machine without the Africa's Talking gateway or a physical device. | ||
|
|
||
| Ensure your relay server is running (`npm run start-bridge`) before executing these commands. | ||
|
|
||
| ### Main Menu | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3000/ussd \ | ||
| -d "text=&phoneNumber=%2B2348012345678&sessionId=test001" | ||
| ``` | ||
|
|
||
| Expected response: | ||
|
|
||
| ```plaintext | ||
| CON Rootstock DeFi (+2348012345678) | ||
| 1. My Balance | ||
| 2. Send Money (P2P) | ||
| 3. Request Micro-Loan | ||
| ``` | ||
|
|
||
| ### Balance Check | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3000/ussd \ | ||
| -d "text=1&phoneNumber=%2B2348012345678&sessionId=test001" | ||
| ``` | ||
|
|
||
| Expected response: | ||
|
|
||
| ```plaintext | ||
| END Your Balance: 0.01 tRBTC | ||
| ``` | ||
|
|
||
| ### Transfer Recipient Prompt | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3000/ussd \ | ||
| -d "text=2&phoneNumber=%2B2348012345678&sessionId=test002" | ||
| ``` | ||
|
|
||
| Expected response: | ||
|
|
||
| ```plaintext | ||
| CON Enter Recipient Address:_________ | ||
| ``` | ||
|
|
||
| ### Transfer Amount Prompt | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3000/ussd \ | ||
| -d "text=2*0xSomeRecipientAddress&phoneNumber=%2B2348012345678&sessionId=test002" | ||
| ``` | ||
|
|
||
| Expected response: | ||
|
|
||
| ```plaintext | ||
| CON Enter Amount:_____ | ||
| ``` | ||
|
|
||
| ### Transfer Execute | ||
|
|
||
| Replace `0xSomeRecipientAddress` with a valid Roostock address and `0.005` with your desired amount. | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3000/ussd \ | ||
| -d "text=2*0xSomeRecipientAddress*0.005&phoneNumber=%2B2348012345678&sessionId=test002" | ||
| ``` | ||
|
|
||
| Expected response (address and hash will differ): | ||
|
|
||
| ```plaintext | ||
| END Transfer Submitted | ||
| To: 0xSomeRecip... | ||
| Tx: 0xa924c40ed1... | ||
| Funds will arrive shortly. | ||
| ``` | ||
|
|
||
| :::note | ||
| The transaction enters the Rootstock mempool immediately and the session ends. The server terminal will log `Transfer confirmed: 0x...` once the block is mined. The full transaction hash is available in your server logs if you need to look it up on the explorer. | ||
| ::: | ||
|
|
||
| ### Micro Loan | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3000/ussd \ | ||
| -d "text=3&phoneNumber=%2B2348012345678&sessionId=test003" | ||
| ``` | ||
|
|
||
| Expected response (hash will differ): | ||
|
|
||
| ```plaintext | ||
| END Loan Requested | ||
| Tx: 0xa924c40ed1... | ||
| 0.01 tRBTC credited shortly. | ||
| ``` | ||
|
|
||
| :::note | ||
| Like the transfer, the loan transaction is submitted to the mempool and the USSD session ends immediately. Watch your server terminal for the background confirmation log `Loan confirmed: 0x...`. If the contract reverts on chain (e.g., because a loan is already active), it will appear as `Loan failed on-chain: Existing loan active` in the server logs. The user will not see this message since the session is already closed. | ||
| ::: | ||
|
|
||
| ## Verifying Transactions On-Chain | ||
|
|
||
| After any write operation through the relay server, verify the transaction on the Roostock Testnet Explorer by searching for your deployed contract address. | ||
|
|
||
| ``` | ||
| https://explorer.testnet.rootstock.io/address/0xYourDeployedContractAddress | ||
| ``` | ||
|
|
||
| You can also search by transaction hash. The truncated hash is returned in the `END` response string from the relay server (e.g., `Tx: 0xa924c40ed1...`). Copy the full hash from your server terminal logs. A successful transaction will show the following fields. | ||
|
|
||
| - **Status:** Success | ||
| - **To:** Your deployed contract address | ||
| - **Input Data:** The encoded function call (`applyForLoan`, `transfer`, etc.) | ||
| - **Block:** The block number it was included in | ||
|
|
||
| You can also inspect `ignition/deployments/chain-31/journal.jsonl` to review all deployment transactions, including their block hashes and receipt status. | ||
|
|
||
| ## Africa's Talking Simulator | ||
|
|
||
| For end-to-end testing with the real USSD protocol (including session ID management and proper `CON`/`END` handling), use the built-in **USSD Simulator** in the Africa's Talking sandbox dashboard. | ||
|
|
||
| 1. Open your sandbox dashboard at [account.africastalking.com/apps/sandbox](https://account.africastalking.com/apps/sandbox). | ||
| 2. Navigate to **USSD > Simulator**. | ||
| 3. Enter a test phone number and dial your shortcode. | ||
| 4. Navigate the menu and observe the responses. | ||
|
|
||
| The ngrok inspector at `http://localhost:4040` will show you every raw HTTP POST from Africa's Talking and every response your server returns. This is essential for debugging unexpected session terminations or malformed responses. | ||
|
|
||
| ## Known Limitations and Recommended Improvements | ||
|
|
||
| The current implementation is a working proof of concept. The following limitations must be addressed before any production or mainnet deployment. | ||
|
|
||
| **Single relayer wallet for all users.** The `getBalance()` call always returns the relay wallet's balance, not the individual user's balance, because all transactions are signed by the same wallet. Map each `phoneNumber` to a unique wallet address stored in a database (e.g., PostgreSQL or Redis), and derive or assign wallets per user at first dial. | ||
|
|
||
| **In-memory session guard.** The `processedSessions` Set that prevents duplicate transactions is held in memory and resets on every server restart. Under horizontal scaling, two server instances will have separate Sets, which breaks the duplicate-prevention guarantee. Move session state to a shared Redis store with a matching TTL. | ||
|
|
||
| **No input validation.** Recipient addresses and amounts from the USSD `text` field are passed to ethers.js without explicit format checks. A non-numeric amount string passed to `ethers.parseEther()` will throw a non-descriptive error. Add address and numeric format validation before calling contract functions. | ||
|
|
||
| **No loan repayment mechanism.** `applyForLoan()` issues a loan with no way to repay it. The `require(loans[msg.sender] == 0)` guard prevents a second loan, but there is no `repayLoan()` function. Add a repayment flow to `InclusiveDeFi.sol` that accepts tRBTC and clears the `loans` mapping entry, then expose it as option `4` in the USSD menu. | ||
|
|
||
| **Post-broadcast reverts are not visible to the user.** Because `tx.wait()` runs in the background after the USSD session has ended, any on-chain revert that occurs after broadcast cannot be communicated back to the user. The user will see the `END Transfer Submitted` screen regardless of the final on-chain outcome. Monitor server logs actively and consider adding a secondary USSD option that lets users check transaction status by hash. | ||
|
|
||
| **No formal meta-transaction standard.** The relayer pattern used here is informal. The smart contract cannot distinguish between the relay wallet acting on behalf of different users, since all calls appear to originate from the same `msg.sender`. Implement [EIP-2771](https://eips.ethereum.org/EIPS/eip-2771) trusted forwarder to allow the contract to identify the true originating user via their phone-mapped wallet address. | ||
|
|
||
| **Private key stored in `.env`.** The relayer private key is stored in a plaintext `.env` file. For any deployment beyond local development, move secrets to a secrets manager such as AWS Secrets Manager or HashiCorp Vault, or use an HSM-backed signing service. | ||
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.