|
| 1 | +# TokenX - FabricX Token Management Application |
| 2 | + |
| 3 | +A comprehensive token management system built on FabricX with privacy-preserving identities, multiple token types, and atomic swaps. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- **Three Roles**: Issuer, Auditor, Owner |
| 8 | +- **UTXO Token Model**: Tokens as discrete states with splitting support |
| 9 | +- **Idemix Privacy**: Anonymous identities for all token owners |
| 10 | +- **Multiple Token Types**: USD, EUR, GOLD, or any custom type |
| 11 | +- **Transfer Limits**: Configurable per-transaction limits |
| 12 | +- **Atomic Swaps**: Exchange tokens of different types atomically |
| 13 | +- **REST API**: Documented OpenAPI 3.0 specification |
| 14 | +- **Decimal Support**: 8 decimal places precision |
| 15 | + |
| 16 | +## Quick Start |
| 17 | + |
| 18 | +### Prerequisites |
| 19 | + |
| 20 | +- Go 1.19+ |
| 21 | +- Docker (for Fabric network) |
| 22 | +- Make |
| 23 | + |
| 24 | +### Run Tests |
| 25 | + |
| 26 | +```bash |
| 27 | +cd /path/to/fabric-smart-client/integration/fabricx/tokenx |
| 28 | + |
| 29 | +# Run all tests |
| 30 | +go test -v ./... |
| 31 | + |
| 32 | +# Or with Ginkgo |
| 33 | +ginkgo -v |
| 34 | +``` |
| 35 | + |
| 36 | +## Architecture |
| 37 | + |
| 38 | +``` |
| 39 | +┌─────────────────────────────────────────────────────────────────┐ |
| 40 | +│ TokenX Network │ |
| 41 | +├─────────────────────────────────────────────────────────────────┤ |
| 42 | +│ Fabric Topology │ |
| 43 | +│ - 3 Organizations: Org1 (Issuer), Org2 (Auditor), Org3 (Owners)│ |
| 44 | +│ - Idemix enabled for anonymous identities │ |
| 45 | +│ - Namespace: tokenx with Org1 endorsement │ |
| 46 | +├─────────────────────────────────────────────────────────────────┤ |
| 47 | +│ FSC Nodes │ |
| 48 | +│ ┌──────────┐ ┌──────────┐ ┌────────────────────────┐ │ |
| 49 | +│ │ Issuer │ │ Auditor │ │ Owners (Idemix) │ │ |
| 50 | +│ │ (Org1) │ │ (Org2) │ │ alice, bob, charlie │ │ |
| 51 | +│ │ - issue │ │ -balances│ │ - transfer │ │ |
| 52 | +│ │ - approve│ │ -history │ │ - redeem │ │ |
| 53 | +│ └──────────┘ └──────────┘ │ - swap │ │ |
| 54 | +│ └────────────────────────┘ │ |
| 55 | +└─────────────────────────────────────────────────────────────────┘ |
| 56 | +``` |
| 57 | + |
| 58 | +## Token Operations |
| 59 | + |
| 60 | +### Issue Tokens |
| 61 | + |
| 62 | +The issuer creates new tokens and assigns them to an owner: |
| 63 | + |
| 64 | +```go |
| 65 | +// Issue 1000 USD tokens to Alice |
| 66 | +result, _ := client.CallView("issue", &views.Issue{ |
| 67 | + TokenType: "USD", |
| 68 | + Amount: states.TokenFromFloat(1000), // 100000000000 |
| 69 | + Recipient: aliceIdentity, |
| 70 | +}) |
| 71 | +``` |
| 72 | + |
| 73 | +### Transfer Tokens |
| 74 | + |
| 75 | +Owners can transfer tokens to other owners: |
| 76 | + |
| 77 | +```go |
| 78 | +// Alice transfers 300 USD to Bob |
| 79 | +result, _ := client.CallView("transfer", &views.Transfer{ |
| 80 | + TokenLinearID: "TKN:abc123", |
| 81 | + Amount: states.TokenFromFloat(300), |
| 82 | + Recipient: bobIdentity, |
| 83 | + Approver: issuerIdentity, |
| 84 | +}) |
| 85 | +``` |
| 86 | + |
| 87 | +**Partial transfers** are supported - if you transfer less than the token amount, a "change" token is created for the sender. |
| 88 | + |
| 89 | +### Redeem Tokens |
| 90 | + |
| 91 | +Owners can burn tokens (with issuer approval): |
| 92 | + |
| 93 | +```go |
| 94 | +result, _ := client.CallView("redeem", &views.Redeem{ |
| 95 | + TokenLinearID: "TKN:abc123", |
| 96 | + Amount: states.TokenFromFloat(100), |
| 97 | + Approver: issuerIdentity, |
| 98 | +}) |
| 99 | +``` |
| 100 | + |
| 101 | +### Atomic Swap |
| 102 | + |
| 103 | +Exchange tokens of different types atomically: |
| 104 | + |
| 105 | +```go |
| 106 | +// Alice proposes: give 100 USD, want 80 EUR |
| 107 | +proposalID, _ := aliceClient.CallView("swap_propose", &views.SwapPropose{ |
| 108 | + OfferedTokenID: "TKN:usd123", |
| 109 | + RequestedType: "EUR", |
| 110 | + RequestedAmount: states.TokenFromFloat(80), |
| 111 | + ExpiryMinutes: 60, |
| 112 | +}) |
| 113 | + |
| 114 | +// Bob accepts with his EUR token |
| 115 | +txID, _ := bobClient.CallView("swap_accept", &views.SwapAccept{ |
| 116 | + ProposalID: proposalID, |
| 117 | + OfferedTokenID: "TKN:eur456", |
| 118 | + Approver: issuerIdentity, |
| 119 | +}) |
| 120 | +``` |
| 121 | + |
| 122 | +## Token Amounts |
| 123 | + |
| 124 | +All amounts use **8 decimal places** precision (similar to Bitcoin satoshis): |
| 125 | + |
| 126 | +| Display Amount | Internal Value | |
| 127 | +|----------------|----------------| |
| 128 | +| 1.00000000 | 100000000 | |
| 129 | +| 0.50000000 | 50000000 | |
| 130 | +| 0.00000001 | 1 | |
| 131 | + |
| 132 | +Use the helper functions: |
| 133 | +```go |
| 134 | +amount := states.TokenFromFloat(100.5) // 10050000000 |
| 135 | +display := token.AmountFloat() // 100.5 |
| 136 | +``` |
| 137 | + |
| 138 | +## Transfer Limits |
| 139 | + |
| 140 | +Default transfer limits are configured in `states/states.go`: |
| 141 | + |
| 142 | +| Limit | Default Value | |
| 143 | +|-------|---------------| |
| 144 | +| Max per transaction | 1,000,000 tokens | |
| 145 | +| Min amount | 0.00000001 tokens | |
| 146 | +| Daily limit | Unlimited | |
| 147 | + |
| 148 | +## REST API |
| 149 | + |
| 150 | +The API is documented in OpenAPI 3.0 format: `api/openapi.yaml` |
| 151 | + |
| 152 | +### Endpoints |
| 153 | + |
| 154 | +| Method | Endpoint | Description | |
| 155 | +|--------|----------|-------------| |
| 156 | +| POST | `/v1/tokens/issue` | Issue new tokens | |
| 157 | +| POST | `/v1/tokens/transfer` | Transfer tokens | |
| 158 | +| POST | `/v1/tokens/redeem` | Redeem/burn tokens | |
| 159 | +| GET | `/v1/tokens/balance` | Get token balance | |
| 160 | +| GET | `/v1/tokens/history` | Get transaction history | |
| 161 | +| POST | `/v1/tokens/swap/propose` | Propose atomic swap | |
| 162 | +| POST | `/v1/tokens/swap/accept` | Accept atomic swap | |
| 163 | +| GET | `/v1/audit/balances` | Auditor: all balances | |
| 164 | +| GET | `/v1/audit/history` | Auditor: all transactions | |
| 165 | + |
| 166 | +## Project Structure |
| 167 | + |
| 168 | +``` |
| 169 | +tokenx/ |
| 170 | +├── api/ |
| 171 | +│ ├── handlers.go # REST API handlers |
| 172 | +│ └── openapi.yaml # API documentation |
| 173 | +├── states/ |
| 174 | +│ └── states.go # Token, TransactionRecord, SwapProposal |
| 175 | +├── views/ |
| 176 | +│ ├── issue.go # Issue tokens |
| 177 | +│ ├── transfer.go # Transfer tokens |
| 178 | +│ ├── redeem.go # Burn tokens |
| 179 | +│ ├── balance.go # Query balances |
| 180 | +│ ├── auditor.go # Auditor views |
| 181 | +│ ├── swap.go # Atomic swaps |
| 182 | +│ ├── approver.go # Validation logic |
| 183 | +│ └── utils.go # Helpers |
| 184 | +├── sdk.go # SDK registration |
| 185 | +├── topology.go # Network topology |
| 186 | +├── tokenx_test.go # Integration tests |
| 187 | +├── tokenx_suite_test.go # Test suite |
| 188 | +└── README.md # This file |
| 189 | +``` |
| 190 | + |
| 191 | +## Privacy with Idemix |
| 192 | + |
| 193 | +All owner nodes use Idemix anonymous identities: |
| 194 | + |
| 195 | +- **Unlinkability**: Transactions from the same owner cannot be linked |
| 196 | +- **Privacy**: Owner identities are not revealed on-chain |
| 197 | +- **Multiple Accounts**: Each owner can have multiple Idemix credentials |
| 198 | + |
| 199 | +Enabled in topology: |
| 200 | +```go |
| 201 | +fscTopology.AddNodeByName("alice"). |
| 202 | + AddOptions(fabric.WithAnonymousIdentity()) // Idemix |
| 203 | +``` |
| 204 | + |
| 205 | +## Auditor Restrictions |
| 206 | + |
| 207 | +Auditors can view: |
| 208 | +- ✅ Token types and amounts |
| 209 | +- ✅ Transaction history |
| 210 | +- ✅ Aggregate supply |
| 211 | + |
| 212 | +Auditors **cannot** view: |
| 213 | +- ❌ Token metadata |
| 214 | +- ❌ Private properties |
| 215 | +- ❌ Detailed owner information (due to Idemix) |
| 216 | + |
| 217 | +## Development |
| 218 | + |
| 219 | +### Adding a New Token Type |
| 220 | + |
| 221 | +Simply issue tokens with a new type name: |
| 222 | +```go |
| 223 | +IssueTokens(ii, "MY_NEW_TOKEN", amount, "alice") |
| 224 | +``` |
| 225 | + |
| 226 | +### Extending Swap Functionality |
| 227 | + |
| 228 | +The swap implementation is designed for extension. Key areas: |
| 229 | +- `SwapProposal` struct in `states/states.go` - add new fields |
| 230 | +- `validateSwap` in `views/approver.go` - add new validations |
| 231 | +- Add new swap-related views as needed |
| 232 | + |
| 233 | +## Development Notes |
| 234 | + |
| 235 | +### Running Integration Tests |
| 236 | + |
| 237 | +**Important:** Always clean Docker before running tests: |
| 238 | + |
| 239 | +```bash |
| 240 | +# Clean up Docker environment |
| 241 | +docker stop $(docker ps -q) 2>/dev/null |
| 242 | +docker rm $(docker ps -aq) 2>/dev/null |
| 243 | +docker network prune -f |
| 244 | + |
| 245 | +# Verify port 7050 is free |
| 246 | +sudo lsof -i :7050 || echo "Port 7050 is free" |
| 247 | + |
| 248 | +# Run the test |
| 249 | +cd /path/to/fabric-smart-client |
| 250 | +make integration-tests-fabricx-tokenx |
| 251 | +``` |
| 252 | + |
| 253 | +### Known Issues & Fixes |
| 254 | + |
| 255 | +#### RWSet Endorsement Mismatch (FIXED) |
| 256 | + |
| 257 | +**Issue:** Endorsement collection failed with "received different results" error. |
| 258 | + |
| 259 | +**Root Cause:** FabricX's RWSet serialization includes namespace versions read from the local vault. When the approver re-serialized the RWSet, it used its own versions which differed from the issuer's. |
| 260 | + |
| 261 | +**Fix Applied:** Modified `platform/fabricx/core/transaction/transaction.go` to use received RWSet bytes directly instead of re-serializing. See [TASK.md](TASK.md) for details. |
| 262 | + |
| 263 | +#### Sidecar Port Mismatch (FIXED) |
| 264 | + |
| 265 | +**Issue:** Test hanged at "Post execution for FSC nodes...". |
| 266 | + |
| 267 | +**Root Cause:** The Sidecar container used a dynamic port (e.g., 5420), but the client configuration was hardcoded to `5411`. |
| 268 | + |
| 269 | +**Fix Applied:** Updated `integration/nwo/fabricx/extensions/scv2` to dynamically propagate the correct sidecar port to the client configuration. |
| 270 | + |
| 271 | +#### Docker Port Conflicts |
| 272 | + |
| 273 | +**Issue:** Test fails with "port 7050 already allocated" |
| 274 | + |
| 275 | +**Solution:** Clean Docker containers before running tests (see above). |
| 276 | + |
| 277 | +### Comparing with Simple Example |
| 278 | + |
| 279 | +The `integration/fabricx/simple/` project is a minimal working example of the same pattern. When debugging tokenx, compare with simple: |
| 280 | + |
| 281 | +| TokenX | Simple | |
| 282 | +|--------|--------| |
| 283 | +| `views/issue.go` | `views/create.go` | |
| 284 | +| `views/approver.go` | `views/approve.go` | |
| 285 | +| `states/states.go` | `views/state.go` | |
| 286 | +| `topology.go` | `topo.go` | |
| 287 | + |
| 288 | +### Debug Logging |
| 289 | + |
| 290 | +The codebase has extensive debug logging. Enable by checking `fsc.SetLogging()` in topology.go: |
| 291 | + |
| 292 | +```go |
| 293 | +fscTopology.SetLogging("grpc=error:fabricx=debug:info", "") |
| 294 | +``` |
| 295 | + |
| 296 | +### Documentation |
| 297 | + |
| 298 | +- [TASK.md](TASK.md) - Current development status and remaining work |
| 299 | +- [WALKTHROUGH.md](WALKTHROUGH.md) - Detailed code walkthrough |
| 300 | +- [SPECIFICATION.md](SPECIFICATION.md) - Full system specification |
| 301 | + |
| 302 | +## License |
| 303 | + |
| 304 | +Apache-2.0 |
0 commit comments