Redatium Developer Documentation

Everything you need to integrate with the Redatium decentralized health data marketplace.

api
API Reference
7 controllers, 40 endpoints
route
Application Flows
8 user journeys
token
Smart Contracts
3 Solidity contracts
computer
Web App Guide
React, MetaMask
smartphone
Mobile Guide
React Native, WC

Quick Start

Backend
docker-compose up -d
dotnet run --urls "http://0.0.0.0:5032"
ngrok http 5032
Webapp
cd webapp && npm install
npm run dev -- --host
Mobile
cd mobile && npm install
adb reverse tcp:8081 tcp:8081
npm run android

Deployed Contracts (Sepolia)

ContractAddress
RedatiumRegistry0xc0E4605CC9bD6768B785121c214B039946922082
Registration0xabE1d17925519290c68c68eBDdbF3B96329EE5d6
TransactionContract0xD1dCe36c968938953576BD57b3ECf62b6e9E8555
DataTransferAdjudicator0x66345886cD27fD1a88F65a5704899F68af59cbAb

Getting Started

Prerequisites

  • .NET 8 SDK (8.0+)
  • Docker (for PostgreSQL)
  • Node.js 20+ (for client-side tools, optional)
  • ngrok (to expose the API to mobile/webapp over HTTPS)

Running Locally

# 1. Start PostgreSQL
cd backend
docker-compose up -d
cd Redatium.Api

# 2. Initialize the database
psql -U postgres -h localhost -p 5432 -d SOLIDUS -f Database/recreate_database.sql
psql -U postgres -h localhost -p 5432 -d SOLIDUS -f Database/seed_database.sql

# 3. Run the API
dotnet run
# Listening on http://0.0.0.0:5032

# 4. Expose via ngrok (required for mobile app)
ngrok http 5032
# Copy the HTTPS URL to mobile/.env (REACT_APP_API_BASE_URL) and webapp/.env (VITE_API_BASE_URL)

Swagger

In development mode, Swagger UI is available at http://localhost:5032/swagger.

Authentication Flow

Wallet-Based Auth (SIWE)

  1. Client checks wallet registration via GET /user/wallet/check/{walletAddress}
  2. Client-side SIWE signing (MetaMask/WalletConnect) — verified locally in the browser/app, not on the backend
  3. Most endpoints read the userId from a Request<T> header field or from CurrentUserService, which extracts it from the HTTP context
  4. The backend trusts the wallet address sent by the client

Note: State channel endpoints (/api/statechannel/*) currently require no authentication — they use wallet address matching only.

Base URL and Conventions

Development base URL: https://<ngrok-subdomain>.ngrok-free.dev (changes on each ngrok restart)

Request Format

All POST endpoints accept:

{
  "header": {
    "userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  },
  "payload": {
    // endpoint-specific data
  }
}

Response Format

All endpoints return:

{
  "payload": { /* result data */ },
  "exception": null
}

On error:

{
  "payload": null,
  "exception": {
    "code": "DP-404",
    "description": "Resource not found"
  }
}

Error codes follow a prefix convention:

  • DP-4xx: Domain/business errors
  • TE-5xx: Technical/infrastructure errors
  • SC-4xx: State channel errors
person

Users

/user
C# Types: UserWalletDto
// DTOs
public class UserWalletDto
{
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public string WalletAddress { get; set; }
    public decimal Balance { get; set; }
}
GET/user/walletGet Wallet

Response UserWalletDto

FieldTypeDescription
iduuidWallet record ID
userIduuidOwner user ID
walletAddressstringEthereum address
balancedecimalCurrent balance

---

GET/user/wallet/check/{walletAddress}Check Wallet Registration

No authentication required.

Path Parameters

ParameterTypeDescription
walletAddressstringEthereum address to check

Response

---

database

Datasets

/dataset
C# Types: DatasetDto, CreateDatasetDto, BrowseDatasetRequestDto, BrowseDatasetDto
public class DatasetDto
{
    public Guid Id { get; set; }
    public Guid OwnerId { get; set; }
    public string Name { get; set; }
    public string? Description { get; set; }
    public Guid? DatasetTypeId { get; set; }
    public string? Type { get; set; }            // Joined from DatasetType
    public string? TypeMachineName { get; set; }
    public string? SourceInfo { get; set; }       // JSON metadata
    public decimal? QualityScore { get; set; }
    public DateTime Created { get; set; }
    public DateTime? Changed { get; set; }
}

public class CreateDatasetDto
{
    public Guid OwnerId { get; set; }
    public string Name { get; set; }
    public string? Description { get; set; }
    public Guid? DatasetTypeId { get; set; }
    public string? SourceInfo { get; set; }
    public decimal? QualityScore { get; set; }
}

public class BrowseDatasetRequestDto
{
    public int PageLimit { get; set; } = 20;
    public int PageOffset { get; set; } = 0;
    public Guid? DatasetTypeId { get; set; }
    public int? MinRating { get; set; }
    public DateTime? CreatedFrom { get; set; }
    public DateTime? CreatedTo { get; set; }
    public string? SearchTerm { get; set; }
    public string SortField { get; set; } = "created";
    public string SortOrder { get; set; } = "desc";
}

public class BrowseDatasetDto
{
    public Guid Id { get; set; }
    public Guid OwnerId { get; set; }
    public string? OwnerUsername { get; set; }
    public string Name { get; set; }
    public string? Description { get; set; }
    public string? Type { get; set; }
    public DateTime Created { get; set; }
    public decimal? QualityScore { get; set; }
    public decimal? AverageRating { get; set; }
    public decimal? Price { get; set; }
    public int TotalSales { get; set; }
}
GET/dataset/listList My Datasets

Returns all datasets owned by the authenticated user.

Response ReturnListDatasetDto

---

GET/dataset/{id}Get Dataset

Path Parameters

ParameterType
iduuid

Response DatasetDto

---

POST/dataset/createCreate Dataset

Request Body CreateDatasetDto

FieldTypeRequiredDescription
ownerIduuidYesOwner user ID
namestringYesDataset name
descriptionstringNoDescription
datasetTypeIduuidNoCategory type ID
sourceInfostringNoJSON with source metadata
qualityScoredecimalNoQuality score 0-5

Response DatasetDto

---

POST/dataset/updateUpdate Dataset

Request Body UpdateDatasetDto

FieldTypeRequired
iduuidYes
ownerIduuidYes
namestringNo
descriptionstringNo
datasetTypeIduuidNo
sourceInfostringNo
qualityScoredecimalNo

---

POST/dataset/deleteDelete Dataset

Request Body DeleteDatasetDto

FieldTypeRequired
datasetIduuidYes

---

POST/dataset/browseBrowse Marketplace

The main marketplace search endpoint. Returns datasets from all users.

Request Body BrowseDatasetRequestDto

FieldTypeDefaultDescription
pageLimitint20Max results
pageOffsetint0Skip count
datasetTypeIduuidnullFilter by type
minRatingintnullMin average rating
createdFromdatetimenullCreated after
createdTodatetimenullCreated before
searchTermstringnullFree text search
sortFieldstring"created"Sort column
sortOrderstring"desc"asc or desc

Response List

---

GET/dataset/typesList Dataset Types

Response List

---

share

Data Sharing Requests

/request
C# Types: DataSharingRequestDto, CreateDataSharingRequestDto, ListDataSharingRequestDto
public class DataSharingRequestDto
{
    public Guid Id { get; set; }
    public Guid RequestorId { get; set; }
    public Guid OwnerId { get; set; }
    public Guid DatasetId { get; set; }
    public string Status { get; set; }         // "pending", "approved", "declined", "completed"
    public string? Purpose { get; set; }
    public string? DatasetName { get; set; }
    public string? DatasetType { get; set; }
    public decimal? Price { get; set; }
    public string? OwnerUsername { get; set; }
    public string? RequestorUsername { get; set; }
    public string? MagnetUri { get; set; }
    public string? TransferStatus { get; set; } // "seeding", "downloading", "completed"
    public DateTime? SeedingStartedAt { get; set; }
    public DateTime? DownloadCompletedAt { get; set; }
    public string? HashLock { get; set; }
    public string? EscrowTxHash { get; set; }
    public string? EscrowSecret { get; set; }
    public string? EscrowState { get; set; }
    public DateTime? EscrowExpiry { get; set; }
    public DateTime? BuyerConfirmedAt { get; set; }
    public string? ClaimTxHash { get; set; }
    public DateTime Created { get; set; }
}

public class CreateDataSharingRequestDto
{
    public Guid? RequestorId { get; set; }  // Auto-set from auth if null
    public Guid OwnerId { get; set; }
    public Guid DatasetId { get; set; }
    public string? Purpose { get; set; }
}

public class ListDataSharingRequestDto
{
    public int PageLimit { get; set; } = 10000;
    public int PageOffset { get; set; } = 0;
    public string? SortField { get; set; }
    public string? SortOrder { get; set; }
    public string? Status { get; set; }
    public Guid? RequestorId { get; set; }
    public Guid? OwnerId { get; set; }
}
GET/request/{id}Get Request

Response DataSharingRequestDto

---

POST/request/listList Requests

Request Body ListDataSharingRequestDto

FieldTypeDefaultDescription
pageLimitint10000Max results
pageOffsetint0Skip count
sortFieldstring"Created"Sort column
sortOrderstring"desc"asc or desc
statusstringnullFilter: pending, approved, declined, completed
requestorIduuidnullFilter by requestor
ownerIduuidnullFilter by dataset owner

Response ReturnListDataSharingRequestDto{ data: DataSharingRequestDto[], metadata }

---

POST/request/createCreate Request

Request Body CreateDataSharingRequestDto

FieldTypeRequiredDescription
requestorIduuidNoAuto-set from auth if null
ownerIduuidYesDataset owner
datasetIduuidYesTarget dataset
purposestringNoReason for request

---

POST/request/respondRespond to Request

Request Body RespondDataSharingRequestDto

FieldTypeRequiredDescription
iduuidYesRequest ID
statusstringYesapproved or declined

---

POST/request/create-bulkBulk Create Requests

Request Body CreateBulkDataSharingRequestDto

FieldType
requestsCreateDataSharingRequestDto[]

Response BulkRequestResultDto{ successful: DataSharingRequestDto[], failed: FailedRequestDto[] }

---

POST/request/{id}/start-seedingStart Seeding

Called by the seller after creating a torrent.

Request Body StartSeedingDto

FieldTypeRequired
magnetUristringYes

---

GET/request/{id}/magnet-uriGet Magnet URI

Response { magnetUri: string }

---

POST/request/{id}/complete-downloadComplete Download

Marks the transfer as completed by the buyer. No request body.

---

POST/request/{id}/cancel-downloadCancel Download

Cancels an in-progress download. No request body.

---

POST/request/{id}/set-priceSet Price

Request Body SetPriceDto

FieldTypeRequired
pricedecimalYes

---

receipt_long

Transactions

/transaction
GET/transaction/listList Transactions

Query Parameters

ParameterTypeDefaultDescription
pageLimitint100Max results
pageOffsetint0Skip count
sortFieldstringnullSort column
sortOrderstringnullasc or desc
statusstringnullFilter by status
datasetIduuidnullFilter by dataset

Response ReturnListDataTransactionDto{ data: DataTransactionDto[], metadata }

DataTransactionDto: { id, buyerId, sellerId, datasetId, datasetName, datasetType, amount, status, blockchainTx, created, buyerUsername, sellerUsername }

---

POST/transaction/createCreate Transaction

Request Body CreateDataTransactionDto

FieldTypeRequired
buyerIduuidYes
sellerIduuidYes
datasetIduuidYes
amountdecimalYes

---

POST/transaction/create-bulkBulk Create Transactions

Request Body CreateBulkDataTransactionDto{ transactions: CreateDataTransactionDto[] }

Response BulkTransactionResultDto{ successful: [], failed: [] }

---

GET/transaction/status?transactionId={uuid}Get Transaction Status

Response DataTransactionDto

---

GET/transaction/tokenList Token Transactions

Query Parameters

ParameterTypeDescription
pageLimitintMax results
pageOffsetintSkip count
transactionTypestringFilter by type

Response ReturnListTokenTransactionDto

TokenTransactionDto: { id, userId, amount, type, relatedTxId, created }

---

GET/transaction/recent-activityRecent Activity

Returns the top 20 most recent transactions for the dashboard.

---

notifications

Notifications

/notification
POST/notification/listList Notifications

Request Body NotificationListRequestDto

FieldTypeDefault
pageLimitint100
pageOffsetint0
isReadboolnull

Response ReturnListNotificationDto

NotificationDto: { id, userId, type, message, status, created }

---

POST/notification/readMark as Read

Request Body NotificationReadRequestDto

FieldTypeRequired
notificationIduuidYes

---

hub

State Channels

/api/statechannel
C# Types: StateChannelDto, SignedStateDto, VirtualEscrowDto, OpenChannelRequest, SubmitSignedStateRequest
public class StateChannelDto
{
    public Guid Id { get; set; }
    public string ChannelId { get; set; }       // bytes32 hex
    public string Participant0 { get; set; }    // Seller address
    public string Participant1 { get; set; }    // Buyer address
    public long ChannelNonce { get; set; }
    public string Allocation0 { get; set; } = "0"; // Seller allocation (wei)
    public string Allocation1 { get; set; } = "0"; // Buyer allocation (wei)
    public long LatestTurnNum { get; set; }
    public string Status { get; set; }          // "Open", "Closed", "Disputed"
    public string ChannelType { get; set; }     // "DataTransfer"
    public string? OpenTxHash { get; set; }
    public string? CloseTxHash { get; set; }
    public DateTime Created { get; set; }
}

public class SignedStateDto
{
    public Guid Id { get; set; }
    public string ChannelId { get; set; }
    public long TurnNum { get; set; }
    public decimal Allocation0 { get; set; }
    public decimal Allocation1 { get; set; }
    public bool IsFinal { get; set; }
    public string? AppDataJson { get; set; }
    public string? Signature0 { get; set; }
    public string? Signature1 { get; set; }
    public int ChunkIndex { get; set; }
    public string? ChunkHash { get; set; }
    public string StateType { get; set; } = "GAME"; // GAME, PRE_FUND_SETUP, POST_FUND_SETUP, CONCLUDE
    public DateTime Created { get; set; }
}

public class VirtualEscrowDto
{
    public Guid Id { get; set; }
    public string PurchaseId { get; set; }
    public string BuyerChannelId { get; set; }
    public string SellerChannelId { get; set; }
    public Guid? DatasetId { get; set; }
    public string? DatasetName { get; set; }
    public string BuyerAddress { get; set; }
    public string SellerAddress { get; set; }
    public decimal Price { get; set; }
    public string HashLock { get; set; }
    public string? SecretPreimage { get; set; }
    public string Status { get; set; }
    public string? MagnetUri { get; set; }
    public DateTime? TimeoutAt { get; set; }
    public DateTime Created { get; set; }
}

public class OpenChannelRequest
{
    public string CounterpartyAddress { get; set; }
    public decimal DepositAmountEth { get; set; }
    public string ChannelType { get; set; }
}

public class SubmitSignedStateRequest
{
    public string ChannelId { get; set; }
    public long TurnNum { get; set; }
    public string Allocation0 { get; set; } = "0";
    public string Allocation1 { get; set; } = "0";
    public bool IsFinal { get; set; }
    public string? AppDataJson { get; set; }
    public string Signature0 { get; set; }
    public string Signature1 { get; set; }
    public int ChunkIndex { get; set; }
    public string? ChunkHash { get; set; }
    public string StateType { get; set; } = "GAME";
}
GET/api/statechannel/{channelId}Get Channel

Response StateChannelDto

---

GET/api/statechannel/user/{walletAddress}Get User Channels

Returns all channels where this wallet is a participant.

Response List

---

POST/api/statechannel/closeClose Channel

Request Body CooperativeCloseRequest

FieldTypeRequired
channelIdstringYes
finalAllocation0decimalYes
finalAllocation1decimalYes
signature0stringYes
signature1stringYes

---

GET/api/statechannel/{channelId}/statesGet Signed States

Response List

SignedStateDto: { id, channelId, turnNum, allocation0, allocation1, isFinal, appDataJson, signature0, signature1, chunkIndex, chunkHash, stateType, created }

---

POST/api/statechannel/{channelId}/statesSubmit Signed State

Request Body SubmitSignedStateRequest

FieldTypeDefaultDescription
channelIdstringChannel ID
turnNumlongTurn number
allocation0string"0"Seller allocation (wei)
allocation1string"0"Buyer allocation (wei)
isFinalboolIs this a final state
appDataJsonstringnullApp-specific JSON
signature0stringSeller signature
signature1stringBuyer signature
chunkIndexintChunk index
chunkHashstringnullHash of delivered chunk
stateTypestring"GAME"GAME, PRE_FUND_SETUP, POST_FUND_SETUP, CONCLUDE

---

GET/api/statechannel/purchases/seller/{address}Get Seller Purchases

Response List

VirtualEscrowDto: { id, purchaseId, buyerChannelId, sellerChannelId, datasetId, datasetName, buyerAddress, sellerAddress, price, hashLock, secretPreimage, status, magnetUri, timeoutAt, created }

---

GET/api/statechannel/purchases/buyer/{address}Get Buyer Purchases

Response List

---

POST/api/statechannel/purchase/proposePropose Purchase

Request Body ProposePurchaseRequest

FieldTypeRequired
buyerChannelIdstringYes
datasetIduuidYes
sellerAddressstringYes
ownerIduuidNo
pricedecimalYes
priceEthstringYes
hashLockstringYes

---

POST/api/statechannel/purchase/acceptAccept Purchase

Request Body AcceptPurchaseRequest

FieldTypeRequired
purchaseIdstringYes
hashLockstringYes
magnetUristringYes

---

POST/api/statechannel/settle-purchaseSettle Purchase

Request Body SettlePurchaseRequest

FieldTypeDefault
channelIdstring
buyerPayoutstring"0"
sellerPayoutstring"0"

---

GET/api/statechannel/purchase/{purchaseId}Get Purchase

Response VirtualEscrowDto

---

account_balance

PSD2 Banking

/adapters/psd2
POST/adapters/psd2/tokenExchange Token

Request Body ExchangeTokenRequestDto

FieldTypeRequired
codestringYes

Response TokenResponseDto

{

"Blockchain": {

"Enabled": true,

"Network": "Sepolia",

"ChainId": 11155111,

"RpcUrl": "https://eth-sepolia.g.alchemy.com/v2/",

"WebSocketRpcUrl": "wss://eth-sepolia.g.alchemy.com/v2/",

"EnableWebSocket": true,

"ContractAddress": "0xc0E4605CC9bD6768B785121c214B039946922082",

"RegistryContractAddress": "0xabE1d17925519290c68c68eBDdbF3B96329EE5d6",

"TransactionContractAddress": "0xD1dCe36c968938953576BD57b3ECf62b6e9E8555",

"EventPollingIntervalSeconds": 10,

"StartFromBlock": 10532939,

"BatchSize": 10,

"MaxBlockRetryThreshold": 100000,

"RequiredConfirmations": 2,

"MaxCatchupBlocks": 10000,

"ContractPollingDelayMs": 1000

}

}

var response = await SafeExecutor.ExecuteAsync(async () =>

{

return await _service.DoSomething(request.Payload!);

});

return Ok(response);

SELECT * FROM "BlockchainSyncState";

SELECT * FROM chain_events WHERE channel_id = '0x...' ORDER BY block_number;

SignalR Hub Reference

BlockchainHub (/hubs/blockchain)

Connection: new HubConnectionBuilder().withUrl("https://<api>/hubs/blockchain").build()

Client Method Parameters Description
SubscribeToWallet walletAddress: string Join wallet group for targeted events
UnsubscribeFromWallet walletAddress: string Leave wallet group
ProposeState channelId: string, signedStateJson: string Broadcast state to channel participants
Server Event Payload Description
StateProposed { channelId, signedStateJson } State broadcast to channel group

TransferHub (/hubs/transfer)

Connection: new HubConnectionBuilder().withUrl("https://<api>/hubs/transfer").build()

Client Method Parameters Description
JoinChannel channelId: string Join channel group for P2P signaling
LeaveChannel channelId: string Leave channel group
RelaySignaling channelId: string, signalingData: string WebRTC SDP/ICE relay
Server Event Payload Description
PeerJoined connectionId New peer in channel
PeerLeft connectionId Peer left channel
SignalingMessage connectionId, signalingData WebRTC signaling data
ChallengeDetected { channelId, turnNum, expiry } On-chain ChallengePlayed event. Known bug: Backend emits ChallengeDetected (line 2733) but clients listen for ChallengeRaised — event never received; only polling fallback works
ChallengeResponded { channelId, turnNum } Challenge response detected
ChannelConcluded { channelId, sellerBalance, buyerBalance } On-chain ChannelConcluded event. Known bug: Backend emits ChannelConcluded (line 2671) but clients listen for ChannelFinalized — event never received

Blockchain Event Indexing Configuration

File: backend/Redatium.Api/appsettings.Development.json section Blockchain

{
  "Blockchain": {
    "Enabled": true,
    "Network": "Sepolia",
    "ChainId": 11155111,
    "RpcUrl": "https://eth-sepolia.g.alchemy.com/v2/<key>",
    "WebSocketRpcUrl": "wss://eth-sepolia.g.alchemy.com/v2/<key>",
    "EnableWebSocket": true,
    "ContractAddress": "0xc0E4605CC9bD6768B785121c214B039946922082",
    "RegistryContractAddress": "0xabE1d17925519290c68c68eBDdbF3B96329EE5d6",
    "TransactionContractAddress": "0xD1dCe36c968938953576BD57b3ECf62b6e9E8555",
    "EventPollingIntervalSeconds": 10,
    "StartFromBlock": 10532939,
    "BatchSize": 10,
    "MaxBlockRetryThreshold": 100000,
    "RequiredConfirmations": 2,
    "MaxCatchupBlocks": 10000,
    "ContractPollingDelayMs": 1000
  }
}

Set Enabled: false to disable blockchain polling entirely (useful when working on UI-only features).

Configuration Reference

appsettings.Development.json

Section Key Description
ConnectionStrings.DefaultConnection Host=localhost;Port=5432;Database=SOLIDUS;Username=postgres;Password=postgres PostgreSQL connection
Cors.AllowedOrigins ["*"] CORS origins (dev: all)
JwtSettings.SecretKey (in appsettings.json) JWT signing key
JwtSettings.Issuer JWT issuer
JwtSettings.Audience JWT audience
Blockchain.* (see section 5) Event indexer config
Psd2Settings.* NBG sandbox OAuth credentials

Database Scripts

Scripts are in backend/Redatium.Api/Database/Scripts/, numbered 01_Users.sql through 32_ChainEvents.sql. To add a new table, create a new numbered script and add it to recreate_database.sql.

Common Patterns

SafeExecutor

All controller actions wrap service calls in SafeExecutor.ExecuteAsync():

var response = await SafeExecutor.ExecuteAsync(async () =>
{
    return await _service.DoSomething(request.Payload!);
});
return Ok(response);

This catches BusinessException (4xx), TechnicalException (5xx), and generic exceptions, mapping them to the standard Response<T> format.

Adding a New Endpoint

  1. Create DTO in DTOs/{Feature}/
  2. Define interface in Interfaces/I{Feature}Service.cs
  3. Implement in Services/{Feature}Service.cs
  4. Register in Program.cs: builder.Services.AddScoped<IFeatureService, FeatureService>()
  5. Create Controllers/{Feature}Controller.cs

Debugging Blockchain Events

Check the BlockchainSyncState table to see where the indexer is:

SELECT * FROM "BlockchainSyncState";

Check indexed chain events:

SELECT * FROM chain_events WHERE channel_id = '0x...' ORDER BY block_number;

Troubleshooting

Problem Solution
API not reachable from mobile Ensure ngrok is running and URL is updated in mobile .env
Blockchain polling not starting Check Blockchain:Enabled is true and RPC URL is valid
Events being missed Check RequiredConfirmations (increase if chain is congested)
Database connection errors Verify Docker container is running: docker ps
CORS errors from webapp In development, all origins are allowed. Check that AllowCredentials is set.
SignalR connection fails Ensure the API is running on 0.0.0.0:5032 (not localhost only)
Request / Response
Select an endpoint
Click any endpoint to see examples

Application Flows

Step-by-step user journeys for the Redatium platform — what the user sees, what happens behind the scenes, and which API endpoints are called.


Web App Flows

The Web App is the buyer/consumer side of the marketplace. Users connect via MetaMask browser extension, browse datasets, request data, and download via P2P transfer.

Registration - Web App

User Experience

  1. User opens the app and lands on the Wallet Connect page (/connect)
  2. A split-screen layout shows: preview image on the left, connection form on the right
  3. User clicks "Connect with MetaMask" — MetaMask extension pops up
  4. User approves the connection — the app receives their wallet address
  5. A SIWE signature request appears in MetaMask — "Sign in to Redatium" message (free, no gas)
  6. User signs — the app verifies the signature locally
  7. If the wallet is new, the user is registered automatically
  8. User is redirected to the Dashboard (/)

Technical Flow

sequenceDiagram
    participant User
    participant Webapp
    participant MetaMask
    participant API as Backend API

    User->>Webapp: Open /connect page
    Webapp->>MetaMask: sdk.connect
    MetaMask-->>User: Approve connection
    MetaMask-->>Webapp: accounts array

    Webapp->>Webapp: Build SIWE message locally
    Webapp->>MetaMask: personal_sign - message, address
    MetaMask-->>User: Sign message - FREE
    MetaMask-->>Webapp: signature

    Webapp->>Webapp: ethers.verifyMessage locally

    Webapp->>API: GET /user/wallet/check/0x...
    API-->>Webapp: isRegistered true/false

    alt New user - not yet registered
        Webapp->>Webapp: Store wallet as new user locally
        Note over Webapp: No backend register endpoint - auth is client-side SIWE only
    end

    Webapp->>Webapp: Store auth in localStorage
    Webapp->>Webapp: Redirect to Dashboard

API Calls

Step Method Endpoint Purpose
1 MetaMask sdk.connect Get wallet address
2 MetaMask personal_sign Sign SIWE message - free
3 ethers.verifyMessage() Verify signature locally (client-side only)
4 GET /user/wallet/check/{address} Check if wallet registered

Key Files

webapp/src/contexts/AuthContext.jsx, webapp/src/components/pages/WalletConnect.jsx


Browse Datasets - Web App

User Experience

  1. User navigates to Browse page from the sidebar
  2. The page loads with a grid of dataset cards showing: name, owner, type, rating, price
  3. Filter bar at the top: Type dropdown, Period selector, Rating filter, Search input
  4. User selects a Type (e.g., "Health") — the list filters instantly
  5. User types in search — results update on each keystroke (debounced)
  6. Pagination at the bottom: "Showing 1-20 of 142" with page navigation
  7. Each dataset card shows a star rating, price, and total sales count

Technical Flow

sequenceDiagram
    participant User
    participant Webapp
    participant API as Backend API

    User->>Webapp: Navigate to /browse
    Webapp->>API: GET /dataset/types
    API-->>Webapp: dataset type categories

    Webapp->>API: POST /dataset/browse
    Note over Webapp, API: pageLimit=20, sortField=created, sortOrder=desc
    API-->>Webapp: datasets + totalRecords

    User->>Webapp: Apply filters - type, period, rating
    Webapp->>API: POST /dataset/browse
    Note over Webapp, API: datasetTypeId, minRating, createdFrom, createdTo
    API-->>Webapp: filtered datasets

    User->>Webapp: Search "health steps"
    Webapp->>API: POST /dataset/browse
    Note over Webapp, API: searchTerm="health steps"
    API-->>Webapp: matching datasets

    User->>Webapp: Page 2
    Webapp->>API: POST /dataset/browse
    Note over Webapp, API: pageOffset=20
    API-->>Webapp: next page of results

API Calls

Step Method Endpoint Purpose
1 GET /dataset/types Fetch filter categories
2 POST /dataset/browse Search marketplace - paginated

Key Files

webapp/src/components/pages/BrowseUsers.jsx, webapp/src/services/DatasetService.js


Request Dataset - Web App

User Experience

  1. On the Browse page, user checks boxes next to datasets they want
  2. A "Request Data" button appears in the top bar with the count of selected items
  3. User clicks it — a modal opens showing the selected datasets
  4. User enters a purpose (e.g., "Research on physical activity patterns") and confirms
  5. A toast notification: "Requests sent successfully"
  6. The requests appear on the Requests page (/requests) with status "Pending"
  7. When the seller approves, the status changes to "Approved" (via SignalR real-time update)
  8. A "Download" button appears next to approved requests

Technical Flow

sequenceDiagram
    participant User
    participant Webapp
    participant API as Backend API
    participant Seller as Seller - Mobile

    User->>Webapp: Select datasets via checkboxes
    User->>Webapp: Click "Request Data"
    Webapp->>Webapp: Open RequestDataModal

    User->>Webapp: Enter purpose, confirm
    Webapp->>API: POST /request/create-bulk
    Note over Webapp, API: requests array with datasetId, ownerId, purpose
    API-->>Webapp: successful + failed arrays

    API-->>Seller: SignalR notification - new request

    Note over User, Webapp: Status: pending - waiting for seller

    Seller->>API: POST /request/respond - approved
    API-->>Webapp: SignalR notification - request approved

    Webapp->>Webapp: Show "Download" button

API Calls

Step Method Endpoint Purpose
1 POST /request/create-bulk Create requests for selected datasets
POST /request/create Alternative: single request
2 POST /notification/list Poll for approval notification (no SignalR event for request approval — uses NotificationService)

Key Files

webapp/src/components/pages/BrowseUsers.jsx, webapp/src/services/DataSharingRequestService.js


Transfer - Download Dataset - Web App

User Experience

  1. On the Requests page, user clicks "Download" on an approved request
  2. A progress indicator appears: "Connecting to seller..."
  3. The status changes to "Setting up payment channel..." — MetaMask pops up for ETH deposit
  4. User approves the deposit transaction in MetaMask (pays gas + dataset price in ETH)
  5. Progress shows "Downloading..." with a percentage bar: "Piece 5/30 (16.7%)"
  6. Each piece is paid for automatically via state channel (no more MetaMask popups)
  7. When all pieces arrive: "Assembling file..." then "Download complete!"
  8. The browser triggers a file download (e.g., health_data.json)
  9. The channel concludes automatically — seller receives payment, buyer gets refund of unused ETH
  10. A toast: "Payment of 0.03 ETH sent to seller"

Technical Flow

sequenceDiagram
    participant User
    participant Webapp
    participant MetaMask
    participant API as Backend API
    participant Tracker as WebTorrent Tracker
    participant Seller as Seller - Mobile
    participant Chain as Sepolia

    User->>Webapp: Click "Download" on approved request
    Webapp->>API: GET /request/{id}/magnet-uri
    API-->>Webapp: magnetUri

    Webapp->>Tracker: WebSocket announce - infoHash
    Seller->>Tracker: WebSocket announce - infoHash
    Tracker-->>Webapp: Seller's WebRTC offer
    Webapp->>Seller: WebRTC connection established

    Webapp->>Webapp: openChannel buyer - sign turnNum 1
    Webapp->>Seller: propose_channel - signed turnNum 1 + ephemeral key
    Seller-->>Webapp: accept_channel - signed turnNum 0 + ephemeral key
    Webapp->>Webapp: forceSetupAgreed - skip turnNum 2/3

    Webapp->>MetaMask: Sign deposit transaction
    MetaMask-->>User: Approve + send ETH
    Webapp->>Chain: deposit + SETUP sigs turnNum 0 and 1 + delegations
    Webapp->>Webapp: startTransferring - direct transition

    loop Each piece
        Seller->>Webapp: chunk_delivery + signed GAME state
        Seller->>Webapp: Binary piece data via BitTorrent
        Webapp->>Webapp: Verify chunk hash
        Webapp->>Webapp: Sign ACK with ephemeral key
        Webapp->>Seller: payment_ack
    end

    Webapp->>Webapp: Assemble file from pieces
    Webapp->>Webapp: Trigger browser download

    Webapp->>Seller: propose_conclude
    Seller-->>Webapp: accept_conclude
    Webapp->>Chain: conclude with both CONCLUDE sigs
    Webapp->>API: POST /request/{id}/complete-download

    Seller->>Chain: withdraw - receive payment
    Webapp->>Chain: withdraw - receive refund if any

API Calls

Step Method Endpoint Purpose
1 GET /request/{id}/magnet-uri Get torrent magnet link
2 WebSocket tracker Peer discovery
3 MetaMask eth_sendTransaction Deposit ETH into channel (turnNum 0+1 sigs)
4 Ephemeral key signing Per-chunk ACK states - no popups
5 Blockchain conclude Finalize channel
6 POST /request/{id}/complete-download Mark transfer complete
7 Blockchain withdraw Claim funds

Key Files

webapp/src/components/pages/Requests.jsx, webapp/src/hooks/useWebTorrentReceiver.js, webapp/src/contexts/ChannelContext.jsx


Mobile App Flows

The Mobile App is the seller/data owner side. Users connect via WalletConnect + MetaMask Mobile, collect health/banking data, create datasets, and serve files via P2P.

Registration - Mobile App

User Experience

  1. User opens the app for the first time — sees Onboarding screens (4 swipeable cards explaining the platform)
  2. Taps "Get Started" — navigates to the Registration screen
  3. Taps "Connect Wallet" — the app deep links to MetaMask Mobile
  4. MetaMask shows a connection request from "Redatium" — user approves
  5. Back in the app: MetaMask shows a signature request for SIWE — user signs (free)
  6. The app shows "Registering on blockchain..." with a spinner
  7. A blockchain transaction is sent (user may need to approve in MetaMask if not using delegation)
  8. The app polls for registration confirmation
  9. Once confirmed: "Welcome to Redatium!" — navigates to Home screen
  10. If biometrics enabled, the app prompts for fingerprint/face setup

Technical Flow

sequenceDiagram
    participant User
    participant App as Mobile App
    participant WC as WalletConnect Relay
    participant MetaMask as MetaMask Mobile
    participant API as Backend API
    participant Chain as Sepolia

    User->>App: Tap "Connect Wallet"
    App->>WC: UniversalProvider.init
    App->>MetaMask: Deep link metamask://
    MetaMask-->>User: Approve connection
    WC-->>App: Session with accounts

    App->>MetaMask: personal_sign SIWE message
    MetaMask-->>User: Sign - FREE
    MetaMask-->>App: signature

    App->>API: GET /user/wallet/check/0x...
    API-->>App: isRegistered false

    App->>MetaMask: sendTransaction - storeRedatium
    MetaMask-->>User: Approve tx
    MetaMask-->>Chain: storeRedatium - registration JSON

    App->>API: Poll GET /user/wallet/check/0x...
    Note over API, Chain: Backend detects RedatiumStored event
    API-->>App: isRegistered true

    App->>App: Navigate to Home

API Calls

Step Method Endpoint Purpose
1 WalletConnect relay Establish wallet session
2 MetaMask personal_sign Sign SIWE message
3 GET /user/wallet/check/{address} Check registration status
4 Blockchain storeRedatium On-chain registration tx
5 GET /user/wallet/check/{address} Poll until registered

Key Files

mobile/src/wallet/useMetaMask.ts, mobile/src/screens/RegistrationScreen.tsx


Create Health Data Dataset - Mobile

User Experience

  1. User navigates to Data Wallet tab from the bottom navigation
  2. Taps "Connect Health Data" — opens the Health Connect permission screen
  3. Android's Health Connect dialog appears — user selects which data types to share (Steps, Calories, Heart Rate, etc.)
  4. User taps "Allow" — permissions granted
  5. Back in the app: the Data Wallet shows connected health sources with a green checkmark
  6. User taps "Create Dataset" — a form appears: name, description, time range (last 7 days, 30 days, etc.)
  7. The app reads health records from Health Connect and packages them as JSON
  8. A price selector appears — user sets the price (e.g., 0.03 ETH / 3 cents)
  9. User taps "Publish" — the dataset is created on the backend
  10. The dataset appears in the Home screen's "My Datasets" section
  11. Other users can now see and request this dataset on the marketplace

Technical Flow

sequenceDiagram
    participant User
    participant App as Mobile App
    participant HC as Health Connect
    participant FS as File System
    participant API as Backend API

    User->>App: Navigate to Data Wallet
    User->>App: Tap "Connect Health Data"
    App->>HC: initialize
    App->>HC: requestPermission - Steps, Calories, etc.
    HC-->>User: Grant permissions
    HC-->>App: permissions granted

    User->>App: Select data types + time range
    App->>HC: readRecords - Steps, 7 days
    HC-->>App: health records array

    App->>App: Package as JSON with metadata
    App->>FS: saveHealthData - health_123.json
    FS-->>App: filePath, fileSize

    User->>App: Set name, description, price
    App->>API: POST /dataset/create
    API-->>App: dataset with ID

    App->>App: Dataset visible in marketplace

API Calls

Step Method Endpoint Purpose
1 Health Connect SDK Initialize + request permissions
2 Health Connect SDK Read health records
3 Local filesystem Save JSON to device
4 POST /dataset/create Publish dataset to marketplace

Supported Health Data Types

Steps, Distance, ActiveCaloriesBurned, TotalCaloriesBurned, HeartRate, RestingHeartRate, SleepSession, Weight, Height, BloodPressure, OxygenSaturation, BodyTemperature, BodyFat, Hydration, Nutrition, ExerciseSession, Speed, Vo2Max, and 15+ more.

Key Files

mobile/src/health/HealthConnectService.ts, mobile/src/services/HealthDataFileService.ts, mobile/src/services/DatasetService.ts


Create Banking Data - Mobile

User Experience

  1. User navigates to Data Wallet and taps "Connect Bank Account"
  2. Selects "National Bank of Greece" from the bank list
  3. The app opens a browser to the NBG login page (OAuth2 consent flow)
  4. User logs in with their banking credentials and authorizes access to account information
  5. The browser redirects back to the app with an authorization code
  6. The app fetches the user's bank accounts — shows list with IBANs and balances
  7. User selects which accounts to include in the dataset
  8. Sets a price and taps "Create Dataset"
  9. The banking dataset appears in their Data Wallet and marketplace

Technical Flow

sequenceDiagram
    participant User
    participant App as Mobile App
    participant API as Backend API
    participant NBG as NBG Bank Portal

    User->>App: Tap "Connect Bank Account"
    App->>API: POST /adapters/psd2/consents/initiate
    API-->>App: consentId + authorizationUrl

    App->>NBG: Open authorizationUrl in browser
    NBG-->>User: Login + authorize access
    NBG-->>App: Redirect with authorization code

    App->>API: POST /adapters/psd2/token
    API-->>App: access_token

    App->>NBG: GET /accounts with Bearer token
    NBG-->>App: accounts list with IBANs, balances

    User->>App: Select accounts, set price
    App->>API: POST /dataset/create
    API-->>App: dataset with ID

API Calls

Step Method Endpoint Purpose
1 POST /adapters/psd2/consents/initiate Start OAuth2 consent flow
2 NBG Bank portal User authorizes access
3 POST /adapters/psd2/token Exchange auth code for token
4 GET NBG API /accounts Fetch bank accounts
5 POST /dataset/create Publish banking dataset

Key Files

mobile/src/services/psd2Service.ts, mobile/src/services/nbgService.ts


Accept Request and Send File - Mobile

User Experience

  1. Seller receives a push notification: "New data request for October Steps"
  2. Opens the Requests tab — sees the request with status "Pending"
  3. The request card shows: buyer's address, dataset name, offered price, purpose
  4. Seller taps "Accept" — status changes to "Approved"
  5. A "Transfer Now" button appears on the request card
  6. Seller taps it — the app shows "Preparing torrent..." with a spinner
  7. MetaMask opens for a channel setup signature (one-time popup for this transfer)
  8. User approves in MetaMask — returns to the app
  9. The card shows "Seeding..." with a progress indicator: "Piece 5/30 sent"
  10. Each piece is served automatically — payments flow in via state channel (no more popups)
  11. When all pieces are delivered: "Transfer complete!"
  12. The channel concludes — seller taps "Withdraw" or it auto-withdraws
  13. MetaMask shows the withdraw transaction — seller approves
  14. Toast: "0.03 ETH received!"

Technical Flow

sequenceDiagram
    participant Seller as Seller - Mobile
    participant API as Backend API
    participant Tracker as WebTorrent Tracker
    participant Buyer as Buyer - Webapp
    participant Chain as Sepolia

    Note over Seller: Notification: new request received

    Seller->>API: POST /request/list
    API-->>Seller: pending requests

    Seller->>Seller: Tap "Accept"
    Seller->>API: POST /request/respond - approved
    API-->>Seller: request updated

    Seller->>Seller: Tap "Transfer Now"
    Seller->>Seller: Load dataset file from device
    Seller->>Seller: TorrentCreationService.createTorrent
    Seller->>API: POST /request/{id}/start-seeding - magnetUri

    Seller->>Tracker: WebSocket announce - infoHash
    Buyer->>Tracker: WebSocket announce - infoHash
    Tracker-->>Buyer: Seller's WebRTC offer
    Buyer->>Seller: WebRTC connection established

    Buyer->>Buyer: openChannel buyer - sign turnNum 1
    Buyer->>Seller: propose_channel - signed turnNum 1 + ephemeral key
    Seller->>Seller: Sign SETUP turnNum 0 via MetaMask/WalletConnect
    Seller->>Buyer: accept_channel - signed turnNum 0
    Buyer->>Buyer: forceSetupAgreed - skip turnNum 2/3

    Buyer->>Chain: deposit + SETUP sigs turnNum 0 and 1
    Note over Buyer, Seller: startTransferring - no confirm_funding

    loop Each piece - ACK gated
        Seller->>Buyer: chunk_delivery + signed GAME state
        Seller->>Buyer: Binary piece data
        Buyer->>Seller: payment_ack - co-signed state
        Note over Seller: Wait for ACK before next chunk
    end

    Seller->>Buyer: propose_conclude
    Buyer->>Seller: accept_conclude
    Buyer->>Chain: conclude with both signatures

    Seller->>Chain: withdraw - receive payment

API Calls

Step Method Endpoint Purpose
1 POST /request/list Fetch pending requests
2 POST /request/respond Approve request
3 POST /request/{id}/start-seeding Set magnet URI
4 WebSocket tracker Announce for peer discovery
5 MetaMask eth_signTypedData_v4 Sign SETUP state - 1 popup
6 Ephemeral key signing Per-chunk GAME states - no popups
7 Blockchain conclude Finalize channel
8 Blockchain withdraw Claim seller funds

Key Files

mobile/src/modules/requests/screens/RequestsScreen.tsx, mobile/src/services/WebTorrentSeeder.ts

Smart Contracts

Redatium uses 3 Solidity smart contracts deployed on the Ethereum Sepolia testnet. All contracts use Solidity ^0.8.20 and OpenZeppelin libraries.

Contract Overview

Contract Address Pattern Purpose
DataTransferAdjudicator 0x66345886cD27fD1a88F65a5704899F68af59cbAb ForceMove state channels Per-chunk payments for P2P data transfers
RedatiumRegistry 0xabE1d17925519290c68c68eBDdbF3B96329EE5d6 Event-based storage User registration, data provenance, audit trails
SolidusDataMarketplace 0xc0E4605CC9bD6768B785121c214B039946922082 Meta-transactions Gasless data transaction recording

DataTransferAdjudicator

The core payment contract. Manages state channels between buyers and sellers for per-chunk micropayments during P2P data transfers.

Key Concepts

  • State Channels: Off-chain bilateral agreements that enable instant, gas-less payments
  • ForceMove Protocol: Dispute resolution mechanism — if a party goes offline, the other can challenge on-chain
  • Ephemeral Key Delegation: Register temporary signing keys so MetaMask doesn't pop up for every chunk payment
  • EIP-712 Signing: All state signatures use typed data (domain: Redatium v1, chainId: 11155111)

Channel Lifecycle

  1. Deposit: Buyer calls deposit() with ETH + both parties' SETUP signatures
  2. Transfer: Off-chain chunk-by-chunk exchange (seller delivers data, buyer signs payment ACKs)
  3. Conclude: Both parties sign CONCLUDE states, either party calls conclude() on-chain
  4. Withdraw: Each party calls withdraw() to claim their share

Functions

Function Who Calls Gas Description
deposit Buyer ~200K Open channel + deposit ETH + register delegations
conclude Either ~120K Cooperative close with 2 CONCLUDE signatures
withdraw Either ~50K Claim funds after finalization
forceMove Either ~180K Start dispute challenge
respondWithMove Counterparty ~100K Answer a challenge
refute Counterparty ~90K Prove challenger signed a later state
timeout Anyone ~50K Finalize expired challenge
finalizeAndWithdraw Either ~70K Combined timeout + withdraw
registerDelegation Participant ~60K Register ephemeral signing key
rotateDelegation Participant ~70K Atomic key rotation
revokeDelegation Participant ~40K Revoke a delegate key

State Structure

struct ChannelState {
    bytes32 channelId;      // keccak256(seller, buyer, nonce)
    uint8   stateType;      // 0=SETUP, 1=GAME, 2=CONCLUDE
    uint48  turnNum;        // Monotonically increasing
    uint128 sellerBalance;  // Seller's allocation (wei)
    uint128 buyerBalance;   // Buyer's allocation (wei)
    bytes32 chunkHash;      // Hash of latest delivered chunk
    uint32  chunkIndex;     // 0-indexed chunk counter
    uint32  totalChunks;    // Total chunks (immutable)
    uint128 pricePerChunk;  // Price per chunk in wei (immutable)
    bytes32 datasetId;      // Dataset identifier (immutable)
}

Turn Number Convention

Even turnNum = seller's turn, odd = buyer's turn.

TurnNum Phase Mover Action
0 SETUP Seller Channel proposal
1 SETUP Buyer Channel acceptance
2-3 SETUP Alternating Post-fund confirmations
4 GAME Seller First chunk delivery
5 GAME Buyer First payment ACK
6+ GAME Alternating Chunk delivery / ACK
N CONCLUDE Either Propose close
N+1 CONCLUDE Either Accept close

Integration Example

// ethers.js v6 - Buyer deposits into channel
const adjudicator = new ethers.Contract(address, abi, signer);

const tx = await adjudicator.deposit(
  setupState0, sig0,    // Seller's SETUP signature
  setupState1, sig1,    // Buyer's SETUP signature
  86400,                // Challenge duration (24h)
  sellerDelegate,       // Seller's ephemeral key
  buyerDelegate,        // Buyer's ephemeral key
  delegateExpiry,       // Unix timestamp
  { value: ethers.parseEther("0.03") }
);

RedatiumRegistry

A simple permissionless registry for storing JSON data on-chain via event emission. Used for user registration and data provenance.

How It Works

  1. Anyone calls storeRedatium(jsonData) — pays gas
  2. Contract emits RedatiumStored(sender, timestamp, "redatium", jsonData) event
  3. Backend event listener indexes the event into the database
  4. Data is permanent and public — cannot be edited or deleted

Functions

Function Gas Description
storeRedatium(string jsonData) ~30K per KB Store JSON data on-chain
getEventCount() Free (view) Get total number of stored entries

Events

event RedatiumStored(
    address indexed sender,     // Who stored the data
    uint256 indexed timestamp,  // When it was stored
    string dataType,            // Always "redatium"
    string jsonData             // The JSON data
);

Integration Example

// Store user registration
const registry = new ethers.Contract(address, abi, signer);
const data = JSON.stringify({
  type: "register",
  address: "0x98ba...",
  timestamp: Date.now()
});
const tx = await registry.storeRedatium(data);

Backend Event Listener

The backend polls for RedatiumStored events and indexes them by type:

  • Registration events — from RegistryContractAddress
  • Data request events — from ContractAddress
  • Transaction events — from TransactionContractAddress

SolidusDataMarketplace

Records data marketplace transactions using a meta-transaction pattern. Users sign messages off-chain (free), the platform relayer submits them on-chain (platform pays gas).

How It Works

  1. User signs a message hash off-chain with MetaMask (free)
  2. Frontend sends the signature to the backend
  3. Backend calls recordDataTransaction() on-chain (backend pays gas)
  4. Contract verifies the ECDSA signature matches the buyer
  5. Event emitted for backend to index

Functions

Function Gas Description
recordDataTransaction(...) ~90K Record a transaction (signature verified)
getMessageHash(...) Free (pure) Get the hash to sign
getNonce(address) Free (view) Get current nonce for replay protection
isProcessed(bytes32) Free (view) Check if transaction already recorded

Security

  • Nonce system: Prevents replay attacks — each signature bound to a specific nonce
  • Idempotent: Same transaction ID cannot be processed twice
  • Signature verification: ECDSA recovery via OpenZeppelin
  • ReentrancyGuard: Prevents reentrancy attacks

Integration Example

// Frontend: create signature (FREE)
const nonce = await contract.getNonce(buyerAddress);
const messageHash = await contract.getMessageHash(
  transactionId, datasetHash, buyer, seller, amount, nonce
);
const signature = await signer.signMessage(ethers.getBytes(messageHash));

// Backend: submit transaction (PAYS GAS)
await contract.recordDataTransaction(
  transactionId, datasetHash, buyer, seller, amount, nonce, signature
);

Deployment

Hardhat (SolidusDataMarketplace, RedatiumRegistry)

npx hardhat run scripts/deploy.js --network sepolia
npx hardhat run scripts/deploy-redatium.js --network sepolia

Foundry (DataTransferAdjudicator)

forge script script/DeployDataTransfer.s.sol --rpc-url $SEPOLIA_RPC --broadcast

Prerequisites

  • .env with ALCHEMY_API_URL, SEPOLIA_PRIVATE_KEY
  • Deployer wallet with SepoliaETH

Verification

npx hardhat verify --network sepolia <CONTRACT_ADDRESS> [constructor args]

View on Etherscan: https://sepolia.etherscan.io/address/<ADDRESS>


Contract Source Files

File Lines Description
blockchain/contracts/DataTransferAdjudicator.sol 600 ForceMove adjudicator
blockchain/contracts/RedatiumRegistry.sol 220 Event-based registry
blockchain/contracts/SolidusDataMarketplace.sol 343 Meta-transaction marketplace
blockchain/test/DataTransferAdjudicator.t.sol Foundry tests
blockchain/test/SolidusDataMarketplace.test.js Hardhat tests
blockchain/test/RedatiumRegistry.test.js Hardhat tests

Webapp -- Developer Guide

1. Getting Started

Prerequisites

  • Node.js 20+ and npm
  • MetaMask browser extension (Chrome/Firefox)
  • Backend API running (see API.DeveloperGuide.md)
  • WSL2 (if developing on Windows -- see port forwarding section)

Setup and Run

cd webapp

# Install dependencies
npm install

# Create .env (if not present, copy from template)
# Update VITE_API_BASE_URL to your ngrok URL
cp .env.example .env  # or edit .env directly

# Start development server
npm run dev
# App runs on http://localhost:5173

The Vite dev server binds to 0.0.0.0:5173 by default, accessible from other devices on the network.

Build for Production

npm run build    # Output in dist/
npm run preview  # Preview the production build

2. Environment Variables

File: webapp/.env

Variable Required Description Example
VITE_API_BASE_URL Yes Backend API URL (ngrok tunnel in dev) https://xyz.ngrok-free.dev
VITE_CURRENCY_SYMBOL No Currency display symbol C
VITE_REDATIUM_CONTRACT_ADDRESS Yes RedatiumRegistry contract 0xc0E4605CC9bD6768B785121c214B039946922082
VITE_REGISTRATION_CONTRACT_ADDRESS Yes Registration contract 0xabE1d17925519290c68c68eBDdbF3B96329EE5d6
VITE_TRANSFER_CONTRACT_ADDRESS Yes TransactionContract 0xD1dCe36c968938953576BD57b3ECf62b6e9E8555
VITE_STATE_CHANNEL_ADDRESS Yes DataTransferAdjudicator 0x66345886cD27fD1a88F65a5704899F68af59cbAb
VITE_DATA_EXCHANGE_APP_ADDRESS No Legacy app (unused) 0xf45348c180b627E621B5cAe781F283F25de5d7De

All variables are accessed via webapp/src/config/env.js.

Important: When the ngrok tunnel restarts, you must update VITE_API_BASE_URL and restart the dev server.

3. Project Structure

webapp/src/
  App.jsx                   # Root component with routing
  main.jsx                  # Vite entry point
  index.css                 # Global styles + CSS variable design system
  App.css                   # App-level styles
  components/
    Layout.jsx              # Sidebar + content layout
    ui/                     # Reusable UI components
      TopBar.jsx            # Header with search + notifications
      FilterDropdown.jsx    # Dropdown filter control
      RatingStars.jsx       # Star rating display
      StatusBadge.jsx       # Colored status indicator
      Pager.jsx             # Pagination
      ThemeToggle.jsx       # Light/dark mode toggle
    hoc/                    # Higher-order components
    pages/
      Dashboard.jsx         # Home page with dataset cards + activity
      BrowseUsers.jsx       # Browse datasets with filters + pagination
      Requests.jsx          # Data sharing request management + download
      ChannelManagement.jsx # State channel dashboard
      WalletConnect.jsx     # MetaMask connection page
  contexts/
    AuthContext.jsx          # SIWE authentication + wallet state
    ChannelContext.jsx       # State channel management
    ThemeContext.jsx         # Light/dark theme
    ToastContext.jsx         # Toast notifications
  hooks/
    useWebTorrentReceiver.js  # P2P download + payment protocol
    useStateChannelProtocol.js # State channel message builders
  services/
    BaseService.js           # HTTP fetch wrapper
    DatasetService.js        # Dataset API
    DataSharingRequestService.js # Request API
    DataTransactionService.js    # Transaction API
    ChannelService.js        # State channel API
    SignalRService.js        # SignalR hub connections
    StateStorageService.js   # IndexedDB state persistence
    BlockchainService.js     # On-chain operations
    ChainWatcher.js          # Deprecated polling watcher
    channel/
      channel-manager.js     # ForceMove state machine
      channel-store.js       # In-memory channel state
      index.js               # Factory + singleton
    chain/                   # Chain adapter (ethers v6 contract calls)
    disputes/
      dispute-handler.js     # Hybrid SignalR + polling dispute auto-handler
    signing/                 # EIP-712 signing helpers
    transport/               # P2P transport abstractions
  config/
    env.js                   # Environment config + contract ABIs
  utils/                     # Utility functions
  locales/                   # i18next translations (en/, el/)

4. Key Pages and Routes

Route Page Component Key Features
/connect WalletConnect MetaMask connection, SIWE signing, loading/success states
/ Dashboard Dataset cards (Financial, Health, Other), recent activity table
/browse BrowseUsers Multi-filter (type, period, ratings), paginated data table, row selection, "Request Data"
/requests Requests Request lifecycle (pending/approved/seeding/downloading), magnet URI retrieval, P2P download trigger
/channel ChannelManagement Active channel status, balance display, challenge countdown, withdrawal

All routes except /connect are protected by ProtectedRoute which checks AuthContext.isConnected.

5. MetaMask Integration

The webapp uses @metamask/sdk-react for browser-based MetaMask interaction:

  1. MetaMaskProvider wraps the entire app in App.jsx
  2. AuthContext calls sdk.connect() to get the wallet address
  3. SIWE message is built and signed via personal_sign
  4. Signature is verified client-side with ethers.verifyMessage()
  5. Auth state is persisted to localStorage (7-day session)

For state channel operations:

const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// EIP-712 signing (ethers v6)
const sig = await signer.signTypedData(domain, types, value);

6. State Channel Buyer-Side Walkthrough

This traces the full buyer flow from requesting data to completing payment:

Step 1: Request and Approve

  1. User browses datasets on /browse
  2. Clicks "Request Data" -- calls POST /request/create
  3. Seller approves on mobile -- status changes to approved
  4. Seller creates torrent and starts seeding -- calls POST /request/{id}/start-seeding

Step 2: Download Initiation

  1. User navigates to /requests, finds approved request
  2. Clicks "Download" -- retrieves magnet URI via GET /request/{id}/magnet-uri
  3. useWebTorrentReceiver.downloadFromMagnet(magnetUri, options) is called

Step 3: P2P Connection

  1. Connect to wss://tracker.openwebtorrent.com
  2. Announce as leecher
  3. WebRTC offer/answer exchange via tracker
  4. BitTorrent handshake, bitfield, unchoke

Step 4: Channel Setup

  1. On BITFIELD received, determine piece count
  2. initChannelManager() -- create ephemeral key + MetaMask signer
  3. openChannel('buyer') -- buyer signs SETUP turnNum 1 via MetaMask
  4. Send propose_channel with signed turnNum 1 + ephemeral key
  5. Wait for accept_channel (seller's SETUP turnNum 0)
  6. forceSetupAgreed() -- skip turnNum 2/3
  7. Call deposit() on DataTransferAdjudicator with ETH + SETUP sigs (turnNum 0, 1)
  8. startTransferring() -- direct transition, no confirm_funding sent

Step 5: Data Transfer with Payments

  1. Request piece 0
  2. Receive chunk_delivery JSON (seller's signed GAME state)
  3. Receive BitTorrent piece (binary data)
  4. channelManager.acknowledgeChunk() -- validate + sign ACK
  5. Send payment_ack to seller
  6. Repeat for each piece (sequential, one at a time)

Step 6: Cooperative Close

  1. All pieces received -- assembleFinalFile()
  2. channelManager.initiateConclude() -- sign CONCLUDE
  3. Send propose_conclude to seller
  4. Wait for accept_conclude
  5. Call conclude() on-chain
  6. Both parties can withdraw()

7. Data Flow End-to-End

sequenceDiagram
    participant Buyer as Webapp - Buyer
    participant API as Backend API
    participant Seller as Mobile - Seller
    participant Chain as Sepolia

    Buyer->>API: POST /request/create
    API-->>Seller: SignalR notification
    Seller->>API: POST /request/respond - approved
    API-->>Buyer: SignalR notification

    Seller->>Seller: Create torrent
    Seller->>API: POST /request/{id}/start-seeding
    Buyer->>API: GET /request/{id}/magnet-uri

    Note over Buyer, Seller: P2P connection via WebTorrent tracker

    Buyer->>Buyer: openChannel buyer - sign turnNum 1
    Buyer->>Seller: propose_channel - signed turnNum 1
    Seller->>Buyer: accept_channel - signed turnNum 0
    Buyer->>Buyer: forceSetupAgreed - skip turnNum 2/3
    Buyer->>Chain: deposit + SETUP sigs turnNum 0 and 1
    Note over Buyer: startTransferring - no confirm_funding

    loop For each piece
        Seller->>Buyer: chunk_delivery + binary piece
        Buyer->>Seller: payment_ack
    end

    Buyer->>Seller: propose_conclude
    Seller->>Buyer: accept_conclude
    Buyer->>Chain: conclude
    Seller->>Chain: withdraw
    Buyer->>Chain: withdraw

8. Dispute Auto-Response

The DisputeHandler (created during download in useWebTorrentReceiver) automatically handles disputes:

  1. Subscribes to ChallengeRaised via SignalR TransferHub — known bug: backend emits ChallengeDetected not ChallengeRaised, so this listener is never triggered; only the polling fallback (every 15s) detects challenges
  2. On challenge: searches fmStateHistory for a state with higher turnNum signed by the challenger
  3. If found: calls refute() on-chain -- clears the challenge
  4. If not found: calls respondWithMove() with the next valid state
  5. On challenge expiry: waits EXPIRY_GRACE_MS (5s) then calls finalizeAndWithdraw()

If SignalR disconnects, falls back to chain polling every 15 seconds.

9. WSL2 Port Forwarding

If running on WSL2 (Windows), the Vite dev server is only accessible inside WSL. To access from the Windows host or other devices:

# Run in PowerShell as Administrator
netsh interface portproxy add v4tov4 listenport=5173 listenaddress=0.0.0.0 connectport=5173 connectaddress=$(wsl hostname -I | %{ $_.Trim() })

Similarly for the API (port 5032) and Metro bundler (port 8081).

10. Troubleshooting

Problem Solution
MetaMask not connecting Ensure MetaMask extension is installed and unlocked. Check browser console for errors.
"Failed to fetch" errors Verify VITE_API_BASE_URL in .env matches your running ngrok tunnel. Restart dev server after changing .env.
Torrent not downloading Check that the seller is actively seeding (mobile app on TorrentScreen). Verify WebSocket connection to tracker in DevTools Network tab.
SIWE verification fails Clear localStorage (redatium_auth) and reconnect. Ensure MetaMask is on Sepolia network.
State channel deposit fails Ensure MetaMask has enough Sepolia ETH. Check that VITE_STATE_CHANNEL_ADDRESS matches the deployed contract.
DisputeHandler not responding Check SignalR connection in console. Verify backend TransferHub is reachable. Check that channel was configured with disputeHandler.configure().
Pieces arriving but no payment Check console for [ForceMove] logs. Ensure fmReady is true before pieces are requested. Check acknowledgedPieces Set for duplicates.
Dark mode not working Check that data-theme attribute is set on <html> element. Verify ThemeContext is in the provider tree.

Mobile App -- Developer Guide

1. Getting Started

Prerequisites

  • Node.js 20+ and npm
  • Android Studio with Android SDK (API 34+)
  • Android device (physical device recommended; emulators have issues with WalletConnect deep links)
  • MetaMask mobile app installed on the device
  • Backend API running (see API.DeveloperGuide.md)
  • ADB configured for USB or wireless debugging

Setup

cd mobile

# Install dependencies
npm install

# Create .env from template (if not present)
# Update REACT_APP_API_BASE_URL to your ngrok tunnel URL
nano .env

# Verify ADB connection
adb devices
# Should show your device

# Set up ADB reverse for Metro bundler
adb reverse tcp:8081 tcp:8081

Running the App

# Terminal 1: Start Metro bundler
npm start
# or: npx react-native start --port 8081

# Terminal 2: Build and install on device
npm run android
# or: npx react-native run-android

# If Metro is running but app shows "Unable to load script":
adb reverse tcp:8081 tcp:8081

Resetting Cache

# Clear Metro cache
npm start -- --reset-cache

# Clear Android build
cd android && ./gradlew clean && cd ..

# Full clean
rm -rf node_modules && npm install

2. Environment Variables

File: mobile/.env

Variable Required Description Example
ALCHEMY_API_KEY Yes Alchemy API key for RPC yMM6Ie-wR74s5OqdoO2zv
WALLETCONNECT_PROJECT_ID Yes WalletConnect cloud project ID 4f0d1018b810434d038f8b830821d13b
REACT_APP_API_BASE_URL Yes Backend API (ngrok tunnel) https://xyz.ngrok-free.dev
REACT_APP_CURRENCY_SYMBOL No Display currency symbol C
REACT_APP_MOCK_USER_ID No Default wallet for dev 0x98ba...
REGISTRATION_CONTRACT_ADDRESS Yes User registration contract 0xabE1...E5d6
REDATIUM_CONTRACT_ADDRESS Yes Data sharing registry 0xc0E4...2082
REQUIRED_CHAIN_ID Yes Target chain (hex) 0xaa36a7 (Sepolia)
DATA_TRANSFER_ADJUDICATOR_ADDRESS Yes State channel contract 0x6634...cbAb
STATE_CHANNEL_ADDRESS No Deprecated alias for above Same as above
ESCROW_CONTRACT_ADDRESS No HTLC escrow (optional) 0x74d9...9BA7

Variables are accessed via react-native-dotenv (@env imports).

Important: After changing .env, restart Metro with cache clear: npm start -- --reset-cache

3. Project Structure

mobile/src/
  App.tsx                     # Root component (provider hierarchy)
  core/
    navigation/
      RootNavigator.tsx       # Stack navigator (all screens)
      TabNavigator.tsx        # Bottom tab navigator (main tabs)
    store/
      index.ts                # Redux store + persist config
      hooks.ts                # Typed useAppDispatch, useAppSelector
      actions.ts              # Shared actions (logout)
    slices/
      appSlice.ts             # Global app state
      notificationsSlice.ts   # Notification state
    providers/                # App-level providers
  modules/
    auth/
      screens/                # BiometricSetup, BiometricLogin
      slices/authSlice.ts     # Auth state
    dataWallet/
      screens/                # DataWallet, ConnectDataSource, etc. (7 screens)
    home/
      screens/HomeScreen.tsx  # Dashboard
    notifications/
      screens/                # NotificationsScreen
    onboarding/
      screens/                # OnboardingScreen 1-4
      navigation/             # OnboardingNavigator
    profile/
      screens/                # Profile, Settings, LanguageSelector
      slices/userSlice.ts     # User profile state
    requests/
      screens/                # RequestsScreen
      slices/requestsSlice.ts # Request state
    wallet/
      screens/                # WalletScreen, ChannelScreen, RecentEarning
      slices/walletSlice.ts   # Wallet state
  screens/                    # Standalone screens (Health, Banking, Torrent, etc.)
  services/
    WebTorrentSeeder.ts       # P2P file serving
    TorrentCreationService.ts # Torrent creation
    StateChannelProtocol.ts   # Protocol message types
    StateChannelEngine.ts     # Legacy state channel (deprecated)
    ChainWatcher.ts           # Legacy chain polling (deprecated)
    EphemeralKeyManager.ts    # Ephemeral signing keys
    EscrowService.ts          # Secret shard embedding
    channel/
      channel-manager.ts      # ForceMove state machine
      index.ts                # Factory + init
    chain/
      chain-adapter.ts        # On-chain contract calls
    signing/
      local-signer.ts         # Ephemeral EIP-712 signer
    disputes/
      dispute-handler.ts      # Seller-side dispute handling
  wallet/
    WalletConnectService.ts   # WC v2 Universal Provider
    useMetaMask.ts            # React hook for wallet
    WalletProvider.tsx         # Context provider
    walletConnectServiceInstance.ts # Singleton access
  health/
    HealthConnectService.ts   # Health Connect API bridge
    useHealthConnect.ts       # React hook
    index.ts                  # Re-exports
  config/
    contracts.ts              # Contract addresses + ABIs
  slices/
    healthDataSlice.ts        # Health data Redux slice
    bankingSlice.ts           # Banking Redux slice
  localization/               # i18next config + translations
  theme/                      # Theme config + hook
  shared/                     # Shared components (overlays, etc.)

4. WalletConnect Walkthrough

Initial Connection

  1. User opens WalletScreen and taps "Connect Wallet"
  2. useMetaMask.connect() is called
  3. WalletConnectService.connect(): a. Initializes UniversalProvider with project ID b. display_uri event fires with WC URI c. Deep-links to MetaMask: https://metamask.app.link/wc?uri=...
  4. User approves in MetaMask
  5. Session established with Sepolia namespace
  6. Account + chainId extracted from session
  7. Redux dispatched: setWalletConnected({ account, chainId })
  8. SIWE message built and signed

Signing a Transaction

ensureSession()    -> verify relay alive (4s timeout)
                   -> if dead: full reinit + reconnect
signTypedData()    -> eth_signTypedData_v4 via WC relay
                   -> Linking.openURL('metamask://') to bring MetaMask to foreground
                   -> wait for response via relay

Session Recovery

On app foreground (after >30s background):

  1. verifySession() checks provider.session exists
  2. If session lost: cleanupForReconnect() -- null everything, clear AsyncStorage
  3. Next operation triggers fresh connect()

5. State Channel Seller-Side Walkthrough

Step 1: Prepare for Transfer

  1. Buyer requests data, seller approves on RequestsScreen
  2. Seller navigates to TorrentScreen
  3. TorrentCreationService.createTorrent() creates magnet URI
  4. POST /request/{id}/start-seeding sends magnet URI to backend
  5. WebTorrentSeeder.startSeeding(torrentFile) starts announcing

Step 2: ForceMove Configuration

Before seeding starts, configureForceMove(signer, provider) is called:

  1. Creates ChannelManager + ChainAdapter for seller
  2. Generates ephemeral key via LocalSigner
  3. Sets _useNewProtocol = true

Step 3: Channel Acceptance

  1. Buyer connects via WebRTC and sends propose_channel
  2. _setupProtocolHandler() fires channelProposed event
  3. Seller calls ensureSession() to verify WalletConnect relay
  4. channelManager.openChannel(channel, 'seller') -- signs SETUP (turnNum 0) via MetaMask
  5. Deep-links to MetaMask for signing
  6. Caches accept_channel in _pendingAccept (data channel may die during MetaMask)
  7. When data channel is available, sends accept_channel with ephemeral address
  8. Transitions to TRANSFERRING

Step 4: Serving Pieces with Payment

For each piece request from buyer:

  1. Check channel is in TRANSFERRING phase
  2. Sign GAME state via ephemeral key (instant, no MetaMask)
  3. Send chunk_delivery JSON message
  4. Send binary piece data
  5. Wait for payment_ack (30s timeout)
  6. If ACK received: resolve, process next piece
  7. If timeout: ACK considered lost, subsequent chunks sent without state channel updates

Step 5: Cooperative Close

  1. After last ACK, seller detects chunkIndex >= totalChunks
  2. Signs CONCLUDE with ephemeral key
  3. Sends propose_conclude
  4. Receives accept_conclude from buyer
  5. Polls for on-chain finalization
  6. Calls withdraw() via MetaMask to receive payment

6. Health Data Collection

Connecting Health Connect

  1. Navigate to ConnectHealthScreen
  2. HealthConnectService.initialize() -- check SDK availability
  3. requestPermission(dataTypes) -- Android permission dialog
  4. On success: store connection in Redux healthData.connectedSources

Reading Health Data

  1. Navigate to HealthDataScreen
  2. Select data types (Steps, HeartRate, Sleep, etc.) and date range
  3. HealthConnectService.readRecords(type, timeRange) for each type
  4. Package records as JSON
  5. Store in Redux healthData.storedHealthData
  6. Create dataset via POST /dataset/create

Supported Data Types

~38 types across 6 categories: Activity, Body Measurements, Cycle Tracking, Nutrition, Sleep, Vitals. Full list in mobile/src/health/HealthConnectService.ts HealthRecordTypes const.

7. Key Services Reference

Service File Singleton? Description
WalletConnectService wallet/WalletConnectService.ts No (per instance) WC v2 relay communication
WebTorrentSeeder services/WebTorrentSeeder.ts Import-level P2P seeding + payment gating
TorrentCreationService services/TorrentCreationService.ts Yes (exported) Create torrents from buffers
ChannelManager services/channel/channel-manager.ts No (per channel) ForceMove state machine
ChainAdapter services/chain/chain-adapter.ts No (per session) On-chain contract calls
LocalSigner services/signing/local-signer.ts No (per channel) Ephemeral EIP-712 signer
HealthConnectService health/HealthConnectService.ts Yes (class) Android Health Connect bridge
EphemeralKeyManager services/EphemeralKeyManager.ts Yes (exported) Manage ephemeral keys

8. Data Flow End-to-End (Seller Perspective)

sequenceDiagram
    participant User as Seller
    participant App as Mobile App
    participant HC as Health Connect
    participant API as Backend
    participant Buyer as Webapp

    User->>App: Connect Health Connect
    App->>HC: requestPermission
    HC-->>User: Approve

    User->>App: Collect health data
    App->>HC: readRecords - Steps, 7 days
    HC-->>App: Health records
    App->>App: Store in Redux

    User->>App: Create dataset
    App->>API: POST /dataset/create

    Note over User, Buyer: Buyer requests purchase

    API-->>App: Notification - request received
    User->>App: Approve request
    App->>API: POST /request/respond - approved

    User->>App: Start transfer
    App->>App: createTorrent
    App->>API: POST /request/id/start-seeding

    App->>App: startSeeding
    Note over App, Buyer: P2P transfer with per-chunk payments

    Buyer->>App: propose_channel
    App->>App: Sign SETUP via MetaMask
    App->>Buyer: accept_channel

    loop Each piece
        Buyer->>App: BT request
        App->>Buyer: chunk_delivery + piece
        Buyer->>App: payment_ack
    end

    App->>Buyer: propose_conclude
    Buyer->>App: accept_conclude
    App->>App: Poll for finalization
    App->>App: withdraw via MetaMask

9. Troubleshooting

ADB Issues

Problem Solution
Device not detected Run adb devices. Enable USB debugging in Developer Options.
Metro not reachable Run adb reverse tcp:8081 tcp:8081
Multiple adb servers Kill extra: adb kill-server && adb start-server
Wireless debugging adb connect <ip>:5555 after enabling in Developer Options

Metro Bundler

Problem Solution
Cache issues npm start -- --reset-cache
Port conflict Kill process on 8081: `lsof -ti :8081
Module not found Delete node_modules and reinstall: rm -rf node_modules && npm install

WalletConnect

Problem Solution
Connection stuck Kill MetaMask app, restart mobile app, try again
"Unknown account #0" WC session stale. Disconnect and reconnect wallet.
Deep link not opening Check redatium:// scheme in AndroidManifest.xml
Relay timeout Check internet. ensureSession() should auto-recover. If not, force disconnect.
Session lost in background Android battery optimization. Disable for both Redatium and MetaMask in Settings.

MetaMask Signing

Problem Solution
Sign dialog not appearing App may be in foreground while MetaMask is in background. Manually switch to MetaMask.
Transaction rejected User cancelled. Will need to retry the operation.
Wrong network Call switchNetwork('0xaa36a7') before signing. NetworkSwitchScreen handles this.
Insufficient funds Need Sepolia ETH. Use a faucet: https://sepoliafaucet.com

Health Connect

Problem Solution
SDK not available Install Health Connect app from Play Store. Requires Android 14+.
Permissions denied Re-request via ConnectHealthScreen. User must explicitly grant each data type.
No data returned Verify another app (Google Fit, Samsung Health) has written data to Health Connect.

General

Problem Solution
API calls failing Check ngrok tunnel is running. Update .env and restart Metro with --reset-cache.
Redux state stale Clear app data on device, or AsyncStorage.clear() in debugger.
Crash on launch Check adb logcat for stack trace. Common: missing .env variables.