Redatium Developer Documentation

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

api
API Reference
12 controllers, 52 endpoints
route
Application Flows
8 user journeys
token
Smart Contracts
4 Solidity contracts
computer
Web App Guide
React, embedded wallet
smartphone
Mobile Guide
React Native, embedded wallet

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 (Base Sepolia)

ContractAddress
RedatiumRegistry0xabE1d17925519290c68c68eBDdbF3B96329EE5d6
SolidusDataMarketplace0xc0E4605CC9bD6768B785121c214B039946922082
DataTransferAdjudicator0x66345886cD27fD1a88F65a5704899F68af59cbAb
DatasetRating0xA0DBB5504cA81163d937C9c2374d778A4fD7a7bF

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 builds a SIWE (Sign-In With Ethereum) message and signs it with the user's wallet (MetaMask removed in the webapp; WalletConnect / ethers signer in webapp and mobile).
  3. Client POSTs the (message, signature) pair to POST /api/auth/siwe-verify (anonymous). The backend (AuthService) re-parses the SIWE message, verifies the signature server-side, and issues a JWT bound to the recovered wallet address.
  4. Client sends the JWT as Authorization: Bearer <token> on subsequent calls; JWT bearer auth validates the token and populates HttpContext.User. Note: CurrentUserService currently resolves the caller from the X-User-Id request header (wallet address or user GUID), not from the JWT claim — migrating it to read the JWT wallet/sub claim is pending.

Note: A handful of endpoints are intentionally anonymous ([AllowAnonymous]): the SIWE verify itself, the wallet-registration check, the public earnings read, /events/since, and /health/indexer. State-channel endpoints (/api/statechannel/*) are not [Authorize]-gated at all — wallet identity is taken from URL/body params and matched against the channel record server-side; tightening this to the JWT claim is on the auth-hardening backlog.

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
api

Auth

/api/auth
C# Code
public sealed class SiweVerifyRequest
{
    public string Message { get; set; }    // The unsigned SIWE message (EIP-4361 text)
    public string Signature { get; set; }  // 0x-prefixed signature hex
}

public sealed class SiweVerifyResponse
{
    public string Token { get; set; }              // JWT — send as Bearer on subsequent requests
    public string WalletAddress { get; set; }      // Lowercase recovered wallet address
    public DateTimeOffset ExpiresAt { get; set; }  // JWT expiry
}
POST/api/authSIWE Verify

The backend:

1. Parses the SIWE message (domain, address, nonce, chain ID).

2. Re-derives the EIP-191 personal_sign hash and recovers the signer.

3. Asserts the recovered address matches the address field in the message.

4. Issues a JWT with claims for the wallet address; lifetime per JwtSettings.

Failures surface as a Response.Exception with category Business (e.g. invalid signature, mismatched address, expired or stale Issued At).

---

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/earnings/{walletAddress}Get Earnings

Returns the wallet's lifetime "Earned via Redatium" total. Public — no auth required. Returns zero for wallets that have never earned.

The total is the running sum of FundsWithdrawn event amounts indexed off the DataTransferAdjudicator contract, credited idempotently per (blockNumber, logIndex) into the user_earnings table.

Response UserEarningsDto

FieldTypeDescription
walletAddressstringLowercased address echoed back
totalEarnedWeistringCumulative wei. **String** (not number) — JS would lose precision at 2^53.
totalEarnedEthdecimaltotalEarnedWei / 1e18, convenience field
updatedAtdatetime?Last credit timestamp (null if never credited)

Live updates also push via SignalR EarningsUpdated on the wallet group whenever a new FundsWithdrawn is indexed — see §6.

---

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; }

    // Owner-reported availability — see /dataset/owner-unavailable
    public string AvailabilityState { get; set; }      // "available" | "unavailable_owner_deleted"
    public DateTime? AvailabilityCheckedAt { get; set; }
    public string? AvailabilityReason { 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 string? TypeMachineName { 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; }

    // Owner-reported availability
    public string AvailabilityState { get; set; }      // "available" | "unavailable_owner_deleted"
    public string? AvailabilityReason { get; set; }
    public DateTime? AvailabilityCheckedAt { 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/owner-unavailableMark Owner-Unavailable

Owner-only: flag a dataset as no-longer-on-device. The mobile client calls this when the underlying source file (health export, bank statement, etc.) has been deleted locally. The catalog (/dataset/browse) surfaces the resulting availabilityState so other buyers see a disabled "Request" button and the reason verbatim. Existing in-flight requests against the dataset show the owner-supplied reason.

The dataset is identified by its blockchain data id (the value embedded in Dataset.SourceInfo), not the backend GUID — this lets the mobile client signal availability without needing to know the server-side primary key.

Request Body MarkOwnerUnavailableDto

FieldTypeRequiredDescription
blockchainDataIdstringYesSource id embedded in Dataset.SourceInfo (e.g. health_1234567, nbg-combined-1234567)
reasonstringNoFree-form reason (max 500 chars, truncated server-side). Defaults to a generic message.

Response DatasetDto — with availabilityState = "unavailable_owner_deleted", availabilityReason set, and availabilityCheckedAt stamped.

---

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

---

api

Dataset Feedback

/dataset-feedback
C# Types: EligibilityRequestDto, EligibilityResponseDto, DatasetEligibilityDto
public class EligibilityRequestDto
{
    public List<Guid> DatasetIds { get; set; }
}

public class EligibilityResponseDto
{
    public List<DatasetEligibilityDto> Items { get; set; }
}

public class DatasetEligibilityDto
{
    public Guid DatasetId { get; set; }
    public bool Eligible { get; set; }       // true ⇔ DataSharingRequests.Status='completed' for this buyer
    public int? CurrentRating { get; set; }  // 1-5 if previously rated, null otherwise
}
POST/dataset-feedback/eligibilityBatch Eligibility Check

Batched: pass every datasetId visible on the page, get one row back per id. Eligibility is satisfied by a single completed purchase — re-rating an already-rated dataset is allowed (the on-chain contract overwrites). The UI should render an interactive star input where eligible: true and a read-only badge otherwise.

Request Body EligibilityRequestDto

FieldTypeRequiredDescription
datasetIdsuuid[]YesDatasets to check (no hard upper bound — the backend resolves with a single SQL query)

Response EligibilityResponseDto

---

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

---

POST/notification/clear-allClear All Notifications

Deletes every notification row for the current user. Drives the "Clear" button on the mobile notifications screen. Returns the count of rows deleted.

Request Body: none.

Response

---

POST/notification/device-tokensRegister Device Token (Push Notifications)

Registers an FCM (Firebase Cloud Messaging) token for a wallet so the backend can send push notifications when relevant on-chain events occur. Idempotent: re-registering the same (walletAddress, fcmToken) pair updates the existing row's timestamp instead of creating a duplicate.

Request Body RegisterDeviceTokenRequestDto

FieldTypeRequiredDescription
walletAddressstringYesLowercase wallet address (e.g. 0xabc...)
fcmTokenstringYesFCM registration token from messaging().getToken()
platformstringNo"android" or "ios" (default: "android")

The mobile app calls this automatically from WalletProvider whenever a wallet connects or switches.

---

DELETE/notification/device-tokensUnregister Device Token

Removes a single (walletAddress, fcmToken) row. Called on wallet disconnect or when the FCM SDK reports that the token has been invalidated. The IPushNotificationService also auto-deletes rows when FCM responds with UNREGISTERED or INVALID_ARGUMENT during a push send, so manual unregistration is mostly defensive.

Request Body UnregisterDeviceTokenRequestDto

FieldTypeRequired
walletAddressstringYes
fcmTokenstringYes

(No platform field — only the wallet/token pair is needed to delete the row.)

---

POST/notification/admin/push-testAdmin Push Test (Dev only)

Smoke-test endpoint that fires an arbitrary push to a wallet. Compiled into Debug builds only (#if DEBUG); returns 404 if hit on a Release build because the controller method does not exist.

HeaderRequiredDescription
X-Dev-AuthYesMust equal Notifications:AdminPushTestSecret from appsettings.Development.json

Request Body AdminPushTestRequestDto

FieldTypeRequired
walletAddressstringYes
messagestringNo (default: "Test message from /admin/push-test")

If no FCM service-account credentials are configured, the underlying IPushNotificationService is a silent no-op and the endpoint returns 200 even though no push fires. Check the backend logs for [Push] No Firebase credentials configured.

---

Push Notification Trigger (internal, not an endpoint)

The backend never exposes "send push" as a public endpoint. Pushes are emitted internally from three call sites — all fire-and-forget, all wrapped so failures never roll back the underlying DB write:

Call siteWhenWhy two API paths plus the indexer
DataSharingRequestService.CreateAsync (declared at DataSharingRequestService.cs:154, push call at line 270)Buyer hits POST /request/createPush fires the moment the request row is inserted from the API path — no need to wait for chain confirmation
DataSharingRequestService.CreateBulkAsync (declared at DataSharingRequestService.cs:407, push call at line 519)Buyer hits POST /request/create-bulkSame as above, per-request inside the bulk loop
BlockchainEventListenerServiceIndexer decodes a RedatiumStored event with JSON type: "data-request-ask"Catches requests created on-chain (out-of-band of the API path), or replays missed pushes if the API call's push attempt failed

Note on contract events: there is no Solidity event called DataSharingRequested. The on-chain event is RedatiumStored; the indexer dispatches on the inner JSON type field — "data-request-ask" becomes a DataSharingRequest row.

Payload format:

The notification block makes Android draw the system tray automatically when the app is backgrounded or killed. The data block drives in-app routing (mobile reads data.type and navigates to Requests with the highlight ID).

---

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"; // SETUP, GAME, 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/stuck-balances/{walletAddress}Get Stuck Balances

Returns balances locked in channels that have been finalized on-chain but not yet withdrawn by the wallet — used by the mobile / webapp "stuck funds" recovery UI.

If the blockchain RPC is unreachable, the endpoint returns HTTP 503 with a bestEffort: true response payload built from cached database state, so the UI can still surface stuck channels even during chain outages.

Path Parameters

FieldTypeDescription
walletAddressstringLowercase wallet address

Response Response> — one entry per channel with non-zero withdrawable allocation.

---

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
channelIdstring—Channel ID
turnNumlong—Turn number
allocation0string"0"Seller allocation (wei)
allocation1string"0"Buyer allocation (wei)
isFinalbool—Is this a final state
appDataJsonstringnullApp-specific JSON
signature0string—Seller signature
signature1string—Buyer signature
chunkIndexint—Chunk index
chunkHashstringnullHash of delivered chunk
stateTypestring"GAME"SETUP, GAME, 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

GET /events/since?wallet=0x...&sinceTimestamp=2026-05-18T09:00:00Z

GET /health/indexer

{

"payload": {

"lastProcessedBlock": 12534218,

"headBlock": 12534221,

"lagBlocks": 3,

"lastUpdatedAt": "2026-05-18T09:42:11Z",

"perContract": [

{ "contractType": "RedatiumRegistry", "lastProcessedBlock": 12534218, "lastConfirmedBlock": 12534216, "lastUpdated": "2026-05-18T09:42:11Z" },

{ "contractType": "DataTransferAdjudicator", "lastProcessedBlock": 12534218, "lastConfirmedBlock": 12534216, "lastUpdated": "2026-05-18T09:42:11Z" },

{ "contractType": "DatasetRating", "lastProcessedBlock": 12534215, "lastConfirmedBlock": 12534213, "lastUpdated": "2026-05-18T09:42:10Z" }

]

},

"exception": null

}

public sealed class TransakSignUrlRequest {

public string ProductsAvailed { get; set; } // "BUY" or "SELL"

public string WalletAddress { get; set; } // Authenticated user's EIP-55 address

public decimal? CryptoAmount { get; set; } // Optional preset

public decimal? FiatAmount { get; set; } // Optional preset

public string? DefaultFiatCurrency { get; set; } // "EUR" | "USD" | "GBP"

public string? PartnerOrderId { get; set; } // Optional idempotency id

}

public sealed class TransakSignUrlResponse {

public string WidgetUrl { get; set; } // Single-use widget URL (5-min TTL)

public DateTimeOffset ExpiresAt { get; set; } // Server-translated expiry

}

POST /api/transak/sign-url

Authorization: Bearer

{

"header": { ... },

"payload": { "productsAvailed": "BUY", "walletAddress": "0x...", "defaultFiatCurrency": "EUR" }

}

{

"payload": {

"widgetUrl": "https://global-stg.transak.com?apiKey=...&sessionId=...",

"expiresAt": "2026-05-21T09:47:11Z"

}

}

{

"Blockchain": {

"Enabled": true,

"Network": "Base",

"ChainId": 8453,

"RpcUrl": "https://base-mainnet.g.alchemy.com/v2/",

"WebSocketRpcUrl": "wss://base-mainnet.g.alchemy.com/v2/",

"EnableWebSocket": true,

"ContractAddress": "0xabE1d17925519290c68c68eBDdbF3B96329EE5d6",

"RegistryContractAddress": "0xabE1d17925519290c68c68eBDdbF3B96329EE5d6",

"TransactionContractAddress": "0xc0E4605CC9bD6768B785121c214B039946922082",

"DataTransferAdjudicatorAddress": "0x66345886cD27fD1a88F65a5704899F68af59cbAb",

"DatasetRatingContractAddress": "0xA0DBB5504cA81163d937C9c2374d778A4fD7a7bF",

"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 Source Description
StateProposed { channelId, signedStateJson } Peer relay via ProposeState State broadcast to channel group
RequestStatusChanged { requestId, status, datasetName, ... } DataSharingRequestService.RespondAsync / StartSeedingAsync Drives webapp toast + bell + Requests auto-refresh
UserRegistered { walletAddress, ... } Indexer On-chain user-registration event indexed
ChannelFunded { channelId, ... } Indexer On-chain ChannelFunded event indexed
ChallengeDetected { channelId, turnNum, expiry } Indexer On-chain ChallengePlayed event. Known bug: Clients listen for ChallengeRaised but backend emits ChallengeDetected — event never received; verify on next client review
ChallengeRefuted { channelId, turnNum } Indexer On-chain ChallengeRefuted event indexed
ChallengeResponded { channelId, turnNum } Indexer On-chain ChallengeResponded event indexed
ChannelConcluded { channelId, sellerBalance, buyerBalance } Indexer On-chain ChannelConcluded event. Known bug: Clients listen for ChannelFinalized but backend emits ChannelConcluded — event never received
EarningsUpdated { walletAddress, totalEarnedWei, updatedAt } Indexer — on FundsWithdrawn after UserEarningsRepository.CreditAsync succeeds Live update for the seller's lifetime "Earned via Redatium" total. Fires only when the credit is idempotently new (skipped on replay).

All wallet-scoped on-chain events are emitted to BlockchainHub wallet groups via IHubContext<BlockchainHub>. The legacy NotifyChallengeRaised / NotifyChannelFinalized static helpers on TransferHub exist but are never called.

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

(Dispute / channel-lifecycle events are NOT on TransferHub — see BlockchainHub above.)

Blockchain Event Indexing Configuration

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

{
  "Blockchain": {
    "Enabled": true,
    "Network": "Base",
    "ChainId": 8453,
    "RpcUrl": "https://base-mainnet.g.alchemy.com/v2/<key>",
    "WebSocketRpcUrl": "wss://base-mainnet.g.alchemy.com/v2/<key>",
    "EnableWebSocket": true,
    "ContractAddress": "0xabE1d17925519290c68c68eBDdbF3B96329EE5d6",
    "RegistryContractAddress": "0xabE1d17925519290c68c68eBDdbF3B96329EE5d6",
    "TransactionContractAddress": "0xc0E4605CC9bD6768B785121c214B039946922082",
    "DataTransferAdjudicatorAddress": "0x66345886cD27fD1a88F65a5704899F68af59cbAb",
    "DatasetRatingContractAddress": "0xA0DBB5504cA81163d937C9c2374d778A4fD7a7bF",
    "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).

Polling strategy (current): the indexer collapses all tracked contracts into a single getLogs call per cycle rather than per-contract polling, then dispatches to per-event handlers. After 10 consecutive empty polls the loop backs off to a 60-second cycle (instead of EventPollingIntervalSeconds) and snaps back to the configured cadence on the first non-empty cycle. This dramatically reduces RPC compute-unit usage during quiet periods without sacrificing responsiveness.

Tracked contracts (current):

Contract Address Events indexed
RedatiumRegistry 0xabE1d17925519290c68c68eBDdbF3B96329EE5d6 RedatiumStored (user registrations, data-request-ask)
SolidusDataMarketplace 0xc0E4605CC9bD6768B785121c214B039946922082 DataTransactionRecorded
DataTransferAdjudicator 0x66345886cD27fD1a88F65a5704899F68af59cbAb ChannelFunded, ChannelConcluded, ChallengePlayed, ChallengeRefuted, ChallengeResponded, FundsWithdrawn
DatasetRating 0xA0DBB5504cA81163d937C9c2374d778A4fD7a7bF DatasetRated (upserts DatasetFeedbacks)

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 33_UserDeviceTokens.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 create or import an embedded on-device wallet (encrypted in IndexedDB, no MetaMask, no WalletConnect), browse datasets, request data, and download via P2P transfer.

Registration - Web App

User Experience

  1. User opens the app — <WalletGate> checks for an existing wallet envelope in IndexedDB
  2. First visit (no envelope): the gate shows OnboardingWalletScreen — choice of "Create a new wallet" or "Import from mnemonic"
  3. User picks Create — enters a password and confirms the 12-word recovery phrase shown by RegisterWalletScreen
  4. Wallet is generated client-side, encrypted with the password, and persisted to IndexedDB
  5. The gate flips to unlocked; if the wallet isn't yet registered on-chain, RegistrationGate shows a one-shot "Register on-chain" screen
  6. User approves via the Approval modal — the embedded wallet signs a storeRedatium(registrationJSON) tx and pays the small gas fee
  7. App polls /user/wallet/check/{address} until the backend indexer confirms the RedatiumStored event
  8. App builds a SIWE message (domain, address, nonce, chainId), signs it via the embedded wallet, and POSTs (message, signature) to /api/auth/siwe-verify
  9. Backend re-parses + recovers signer + issues a JWT — stored in localStorage (key redatium_auth, 7-day max age)
  10. User is redirected to the Dashboard (/)

On returning visits, <WalletGate> shows UnlockWalletScreen (password unlock) and the SIWE step re-runs to get a fresh JWT.

Technical Flow

sequenceDiagram
    participant User
    participant Gate as WalletGate
    participant Store as walletStore
    participant IDB as IndexedDB
    participant API as Backend API
    participant Chain as "Base mainnet"

    User->>Gate: Open webapp
    Gate->>Store: getAddress() / hasWallet()
    Note over Gate: First visit — no envelope

    User->>Gate: Create wallet, enter password
    Gate->>Store: createWallet(password)
    Store->>IDB: encrypt + persist envelope
    Store-->>Gate: status='unlocked', address

    Gate->>API: GET /user/wallet/check/{address}
    API-->>Gate: { isRegistered: false }

    Note over Gate, Chain: RegistrationGate one-shot
    Gate->>Store: requireSigner()
    User->>Gate: Approve registration tx (ApprovalProvider modal)
    Gate->>Chain: storeRedatium(registrationJSON)
    Chain-->>API: RedatiumStored event (indexed)
    Gate->>API: Poll GET /user/wallet/check/{address}
    API-->>Gate: { isRegistered: true }

    Gate->>Gate: Build SIWE message (nonce, chainId)
    Gate->>Store: signer.signMessage(siwe)
    Gate->>API: POST /api/auth/siwe-verify { message, signature }
    API->>API: Re-parse + recover signer + mint JWT
    API-->>Gate: { token, walletAddress, expiresAt }
    Gate->>Gate: Persist JWT to localStorage
    Gate->>Gate: Render Dashboard

API Calls

Step Method Endpoint Purpose
1 — walletStore.createWallet() Generate + encrypt embedded wallet (no backend call)
2 GET /user/wallet/check/{address} Check on-chain registration status
3 — storeRedatium(registrationJSON) On-chain registration tx (from embedded wallet)
4 GET /user/wallet/check/{address} Poll until indexer confirms
5 POST /api/auth/siwe-verify Backend-verified SIWE → JWT

Key Files

webapp/src/wallet/components/WalletGate.jsx, webapp/src/wallet/walletStore.js, webapp/src/wallet/components/RegisterWalletScreen.jsx, webapp/src/contexts/AuthContext.jsx (SIWE session)


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 — SignalR RequestStatusChanged (BlockchainHub, wallet group) Real-time push when seller accepts/declines/starts-seeding. Replaces polling for the request lifecycle.
3 POST /notification/list Persistent history (used by the bell panel for past notifications when the user wasn't connected)

Real-Time Behavior

When the seller acts on the request, the backend broadcasts RequestStatusChanged on the BlockchainHub wallet group. The webapp:

  1. Pushes a brand-purple notification toast with a View button — clicking routes to /requests?highlight=<id> and the matching row glows briefly.
  2. Adds the toast to the bell dropdown (TopBar) with an unread-count badge.
  3. If the user is already on /requests, that page bumps a refreshTrigger and auto-refetches the list.

Key Files

webapp/src/components/pages/BrowseUsers.jsx, webapp/src/services/DataSharingRequestService.js, webapp/src/contexts/ChannelContext.jsx (SignalR subscriber + toast), webapp/src/components/pages/Requests.jsx (auto-refresh + highlight)


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..." — the embedded wallet's ApprovalProvider modal pops up for the SETUP signature + ETH deposit
  4. User confirms in the approval modal (pays gas + dataset price in ETH from the embedded wallet's balance — top up via Transak if needed)
  5. Progress shows "Downloading..." with a percentage bar: "Piece 5/30 (16.7%)"
  6. Each piece is paid for automatically via state channel — ephemeral keys mean no further approval modals
  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. The backend indexer pushes ChannelConcluded via SignalR — no client-side polling.
  10. A toast: "Payment of 0.03 ETH sent to seller"

Technical Flow

sequenceDiagram
    participant User
    participant Webapp
    participant Wallet as Embedded Wallet
    participant API as Backend API
    participant Tracker as WebTorrent Tracker
    participant Seller as Seller - Mobile
    participant Chain as "Base mainnet"

    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->>Wallet: requireSigner()
    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->>Wallet: Sign deposit tx (ApprovalProvider modal)
    User->>Wallet: Approve
    Webapp->>Chain: deposit + SETUP sigs turnNum 0 and 1 + delegations
    Chain-->>API: ChannelFunded event (indexed)
    API-->>Webapp: SignalR ChannelFunded
    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 (no modal)
        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
    Chain-->>API: ChannelConcluded event
    API-->>Webapp: SignalR ChannelConcluded
    API-->>Seller: SignalR ChannelConcluded
    Webapp->>API: POST /request/{id}/complete-download

    Seller->>Chain: withdraw - receive payment
    Chain-->>API: FundsWithdrawn event
    API->>API: Credit user_earnings (idempotent per (block, logIndex))
    API-->>Seller: SignalR EarningsUpdated (lifetime total)
    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 — Embedded wallet signTypedData (EIP-712) + sendTransaction SETUP sigs + deposit; one user approval via the ApprovalProvider modal
4 — Ephemeral key signing Per-chunk ACK states - no approval modals
5 — Blockchain conclude Finalize channel
6 POST /request/{id}/complete-download Mark transfer complete
7 — Blockchain withdraw Claim funds
8 — SignalR ChannelFunded / ChannelConcluded / EarningsUpdated Indexer-driven push to wallet group (replaces client-side polling)

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 create or import an embedded on-device wallet (encrypted in the OS Keychain, biometric unlock), collect health/banking data, create datasets, and serve files via P2P. A legacy WalletConnect shim is retained for backward compatibility but is not used for active signing.

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 embedded-wallet setup
  3. Chooses "Create a new wallet" (or "Import from mnemonic" if returning) — EmbeddedWalletService.createWallet
  4. Sets a PIN/password, confirms the 12-word recovery phrase
  5. The app prompts for biometric unlock setup (react-native-biometrics) — fingerprint / face if available
  6. Wallet is encrypted and stored in the OS Keychain via react-native-keychain. The unlocked ethers.Wallet signer lives in EmbeddedWalletService._signer at module scope.
  7. RegistrationGate checks /user/wallet/check/{address} — if not registered, shows a one-shot "Register on-chain" screen
  8. ApprovalProvider modal appears for the storeRedatium(...) tx — user confirms
  9. The app shows "Registering on blockchain..." then polls (RegistrationPollingService) until the backend indexer confirms the RedatiumStored event
  10. App builds a SIWE message, signs it with the embedded wallet, POSTs (message, signature) to /api/auth/siwe-verify — backend returns a JWT
  11. JWT is stored in encrypted Keychain storage; "Welcome to Redatium!" — navigates to Home

Technical Flow

sequenceDiagram
    participant User
    participant App as Mobile App
    participant Wallet as EmbeddedWalletService
    participant Keychain as OS Keychain
    participant API as Backend API
    participant Chain as "Base mainnet"

    User->>App: Tap "Get Started" → Create wallet
    App->>Wallet: createWallet(password)
    Wallet->>Wallet: Generate BIP-39 mnemonic + HDNode
    Wallet->>Keychain: encrypt + persist envelope
    Wallet-->>App: status='unlocked', address

    User->>App: (optional) Enable biometric unlock
    App->>Wallet: configure biometric

    App->>API: GET /user/wallet/check/{address}
    API-->>App: { isRegistered: false }

    Note over App, Chain: RegistrationGate one-shot
    User->>App: Approve registration tx (ApprovalProvider modal)
    App->>Wallet: requireSigner()
    App->>Chain: storeRedatium(registrationJSON)
    Chain-->>API: RedatiumStored event

    App->>API: Poll GET /user/wallet/check/{address}
    API-->>App: { isRegistered: true }

    App->>App: Build SIWE message
    App->>Wallet: signer.signMessage(siwe)
    App->>API: POST /api/auth/siwe-verify
    API-->>App: { token, walletAddress, expiresAt }
    App->>Keychain: persist JWT

    App->>App: Navigate to Home

API Calls

Step Method Endpoint Purpose
1 — EmbeddedWalletService.createWallet() Generate + encrypt embedded wallet (no backend call)
2 GET /user/wallet/check/{address} Check on-chain registration status
3 — storeRedatium(registrationJSON) On-chain registration tx (from embedded wallet)
4 GET /user/wallet/check/{address} Poll until indexer confirms
5 POST /api/auth/siwe-verify Backend-verified SIWE → JWT

Key Files

mobile/src/wallet/EmbeddedWalletService.ts, mobile/src/wallet/WalletProvider.tsx, mobile/src/wallet/screens/RegistrationGate.tsx, mobile/src/auth/siwe.ts, mobile/src/services/RegistrationPollingService.ts


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 an Android system-tray push notification ("New data request"). The notification works even if the app is backgrounded or fully killed — it's delivered by Firebase Cloud Messaging (FCM).
  2. Tapping the notification opens the app directly to the Requests tab with the new row highlighted (foreground / background / cold-start handled by usePushNavigation).
  3. The Requests tab shows the request with status "Pending"
  4. The request card shows: buyer's address, dataset name, offered price, purpose, and a relative timestamp (e.g. "3m ago" — formatRelativeTime parses naive UTC timestamps from the backend)
  5. Seller taps "Accept" — status changes to "Approved"
  6. A "Transfer Now" button appears on the request card
  7. Seller taps it — the app shows "Preparing torrent..." with a spinner
  8. The embedded wallet's ApprovalProvider modal opens for the channel SETUP signature (one-time confirmation for this transfer)
  9. Seller confirms — returns to the app
  10. The card shows "Seeding..." with a progress indicator: "Piece 5/30 sent"
  11. Each piece is served automatically — ephemeral keys sign GAME states with no further approval modals
  12. When all pieces are delivered: "Transfer complete!"
  13. Backend indexer pushes ChannelConcluded via SignalR → the WalletScreen lights up the Check-withdrawals button. Seller taps it → ApprovalProvider modal for the withdraw(...) tx
  14. Seller confirms — embedded wallet sends the tx
  15. Backend indexes FundsWithdrawn, credits user_earnings idempotently, and pushes EarningsUpdated on the wallet's SignalR group → Home screen's "Earned via Redatium" total ticks up in real time
  16. Toast: "0.03 ETH received!"

Technical Flow

sequenceDiagram
    participant Seller as Seller - Mobile
    participant Wallet as Embedded Wallet
    participant API as Backend API
    participant Tracker as WebTorrent Tracker
    participant Buyer as Buyer - Webapp
    participant Chain as "Base mainnet"

    Note over Seller: FCM push 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->>Wallet: requireSigner() + sign SETUP turnNum 0 (ApprovalProvider modal)
    Seller->>Buyer: accept_channel - signed turnNum 0
    Buyer->>Buyer: forceSetupAgreed - skip turnNum 2/3

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

    loop Each piece - ACK gated
        Seller->>Buyer: chunk_delivery + signed GAME state (ephemeral key, no modal)
        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
    Chain-->>API: ChannelConcluded event
    API-->>Seller: SignalR ChannelConcluded

    Seller->>Wallet: Approve withdraw (Check-withdrawals button)
    Seller->>Chain: withdraw - receive payment
    Chain-->>API: FundsWithdrawn event
    API->>API: Credit user_earnings (idempotent per block + logIndex)
    API-->>Seller: SignalR EarningsUpdated → Home "Earned via Redatium" updates

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 — Embedded wallet signTypedData (EIP-712, ethers v5) Sign SETUP state — one ApprovalProvider modal
6 — Ephemeral key signing Per-chunk GAME states - no modals
7 — Blockchain conclude Finalize channel
8 — Blockchain withdraw (from embedded wallet) Claim seller funds
9 GET /user/earnings/{address} Initial fetch for the lifetime "Earned via Redatium" total
10 — SignalR EarningsUpdated Live push on every successful FundsWithdrawn credit

Key Files

mobile/src/modules/requests/screens/RequestsScreen.tsx, mobile/src/services/WebTorrentSeeder.ts, mobile/src/modules/wallet/screens/ChannelScreen.tsx (Check-withdrawals), mobile/src/modules/home/hooks/useEarnings.ts


Buy / Sell ETH (Transak) - Web App & Mobile

User Experience

  1. User opens the embedded wallet panel (Wallet Settings, or the funding step of Wallet Registration when balance is too low to register) — sees current ETH balance and the "Buy ETH" + "Sell ETH" buttons (visible only when TRANSAK_ENABLED=true)
  2. First-time tap shows a one-shot disclosure interstitial (KYC is performed by Transak, independent of Redatium); on continue the dismissal is remembered in localStorage
  3. App calls POST /api/transak/sign-url with productsAvailed: "BUY" | "SELL" and the authenticated user's walletAddress; backend mints a single-use, 5-minute Transak widgetUrl via the Transak Partner API and returns { widgetUrl, expiresAt }
  4. The widget URL opens the Transak hosted widget (in-app WebView on mobile, embedded modal on web) with network=base, cryptoCurrencyCode=ETH, and the wallet address pre-bound by the backend
  5. BUY: user completes KYC + fiat payment on Transak; Transak sends ETH directly to the user's embedded wallet address on Base mainnet
  6. SELL: user completes KYC on Transak and is redirected to /transak/sell-deposit (Transak also emits the TRANSAK_WALLET_REDIRECTION postMessage event); the embedded wallet then signs an ETH transfer from the user's wallet to the Transak-provided deposit address, and Transak pays out fiat to the user's bank/card
  7. New balance is reflected after the next on-chain confirmation (mobile uses useWalletBalance polling; webapp's BlockchainService.getBalance() is called on-demand by screens that need it)

Technical Flow

sequenceDiagram
    participant User
    participant App as Webapp / Mobile
    participant API as Backend API
    participant Transak as Transak Widget
    participant Chain as "Base mainnet"

    User->>App: Tap "Buy ETH" / "Sell ETH"
    App->>API: POST /api/transak/sign-url { productsAvailed, walletAddress }
    API->>API: Get cached Partner Access Token (refresh via api-secret if missing/401)
    API->>Transak: POST /api/v2/auth/session  (access-token header + widgetParams)
    Transak-->>API: { data: { widgetUrl } }
    API-->>App: { widgetUrl, expiresAt }

    App->>Transak: Open widget with single-use widgetUrl
    User->>Transak: Complete KYC + payment (BUY) / KYC + payout details (SELL)
    alt BUY
        Transak->>Chain: Send ETH to walletAddress
    else SELL
        Transak-->>App: Redirect to /transak/sell-deposit + postMessage
        App->>Chain: Embedded wallet signs ETH transfer to Transak deposit address
    end
    Chain-->>App: Balance updates on next read

API Calls

Step Method Endpoint Purpose
1 POST /api/transak/sign-url Mints a single-use 5-minute Transak widget URL. Backend rejects walletAddress values that don't match the authenticated wallet claim on the JWT (403 Transak.WalletAddress.Mismatch) and rejects productsAvailed outside {BUY, SELL}.
2 — Transak widget User completes KYC + purchase (BUY) or KYC + payout (SELL) out-of-band

The Transak API secret never leaves the backend — it is only used server-side to exchange for a Partner Access Token (cached in IMemoryCache, refreshed on expiry or on a 401 from /api/v2/auth/session). Only the public partner API key ships in the client bundle (VITE_TRANSAK_API_KEY / REACT_APP_TRANSAK_API_KEY). If *_ENABLED=true but the partner API key is blank, the app refuses to boot (design D-TX-3).

Key Files

webapp/src/services/TransakService.js, webapp/src/hooks/useTransakFlow.js, webapp/src/components/transak/* (TransakBuyButton, TransakSellButton, TransakInterstitialModal, TransakSellConfirmModal), webapp/src/components/pages/TransakSellDeposit.jsx, mobile/src/services/TransakService.ts, mobile/src/hooks/useTransakFlow.ts, mobile/src/screens/transak/*, mobile/src/components/transak/*, backend/Redatium.Api/Controllers/TransakController.cs, backend/Redatium.Api/Services/TransakService.cs


Rate a Dataset - Web App

User Experience

  1. Buyer completes a purchase + download — the dataset appears in their Dashboard's "My Purchases" section
  2. Each completed-purchase row shows an interactive star input (1–5 stars). Hovering shows live state; clicking commits.
  3. Behind the scenes the Dashboard already called POST /dataset-feedback/eligibility for the visible datasets; rows where eligible: false (no completed purchase) show a read-only badge instead.
  4. Buyer clicks 4 stars — the Approval modal appears confirming a submitRating(datasetId, 4) tx on the DatasetRating contract
  5. Buyer confirms — embedded wallet sends the tx and pays the small gas fee (this is the anti-spam mechanism)
  6. Backend indexer picks up the DatasetRated event, verifies the buyer's purchase off-chain, and upserts into DatasetFeedbacks
  7. The new average rating shows on the dataset card next time the marketplace is browsed

Re-rating is allowed and overwrites both on-chain and in the DB.

Technical Flow

sequenceDiagram
    participant User
    participant Webapp
    participant Wallet as Embedded Wallet
    participant API as Backend API
    participant Chain as DatasetRating
    participant DB as DatasetFeedbacks

    Webapp->>API: POST /dataset-feedback/eligibility { datasetIds[] }
    API-->>Webapp: items[] with { eligible, currentRating }

    User->>Webapp: Click 4 stars
    Webapp->>Webapp: datasetId = ethers.id(datasetGuid)
    Webapp->>Wallet: requireSigner() + ApprovalProvider modal
    User->>Wallet: Confirm
    Webapp->>Chain: submitRating(datasetId, 4)
    Chain-->>Chain: ratings[datasetId][buyer] = 4
    Chain-->>API: DatasetRated event (indexed)
    API->>API: Verify buyer's purchase
    API->>DB: UPSERT DatasetFeedbacks row

API Calls

Step Method Endpoint Purpose
1 POST /dataset-feedback/eligibility Gate the star input — returns eligible + currentRating per dataset
2 — DatasetRating.submitRating(datasetId, stars) On-chain attestation; signed by buyer's embedded wallet, gas paid by buyer

The mobile app doesn't currently expose a rating UI; ratings are submitted from the webapp Dashboard only.

Key Files

webapp/src/contexts/RatingsContext.jsx, webapp/src/services/DatasetFeedbackService.js, webapp/src/abi/DatasetRating.json, backend DatasetFeedbackController.cs + DatasetRatedEvent.cs

Smart Contracts

Redatium uses 4 Solidity smart contracts. The current deployment is Base Sepolia (chain id 84532, deployed 2026-05-11); the same deterministic-deploy script targets Base mainnet (chain id 8453) for production. All contracts use Solidity ^0.8.20 (the marketplace also pulls in OpenZeppelin's ECDSA + ReentrancyGuard; the others have no external dependencies).

Contract Overview

Current addresses (Base Sepolia, deployed via blockchain/scripts/deterministic-deploy.js):

Contract Address Pattern Purpose
RedatiumRegistry 0xabE1d17925519290c68c68eBDdbF3B96329EE5d6 Event-based storage User registration, data provenance, audit trails
SolidusDataMarketplace 0xc0E4605CC9bD6768B785121c214B039946922082 Meta-transactions Gasless data transaction recording
DataTransferAdjudicator 0x66345886cD27fD1a88F65a5704899F68af59cbAb ForceMove state channels Per-chunk payments for P2P data transfers
DatasetRating 0xA0DBB5504cA81163d937C9c2374d778A4fD7a7bF On-chain attestation Buyer star ratings (1–5) per dataset

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 the wallet doesn't have to authorize every chunk payment
  • EIP-712 Signing: All state signatures use typed data (domain: Redatium v1, chainId: read dynamically from block.chainid so the domain adapts per chain — 8453 on Base mainnet, 84532 on Base Sepolia)

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 their wallet (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
);

DatasetRating

Permissionless on-chain attestation of dataset star ratings (1–5). The buyer signs and pays gas to submit a rating; the cost is the anti-spam mechanism. Purchase eligibility is enforced off-chain by the backend (DatasetFeedbackService), which filters out events from non-buyers when indexing.

How It Works

  1. Backend exposes POST /dataset-feedback/eligibility so the UI can gate the star-input on completed purchases.
  2. Eligible buyer calls submitRating(datasetId, stars) from their wallet — datasetId is ethers.id(<dataset-guid>).
  3. Contract validates 1 <= stars <= 5, writes ratings[datasetId][buyer] = stars, emits DatasetRated(datasetId, buyer, stars, timestamp).
  4. Backend indexer ingests the event, verifies the buyer's purchase, and upserts into DatasetFeedbacks. Re-submission overwrites both on-chain and in the DB.

Functions

Function Gas Description
submitRating(bytes32 datasetId, uint8 stars) ~50K first / ~30K overwrite Record a buyer's rating
getRating(bytes32 datasetId, address buyer) Free (view) Read the buyer's current rating (0 = unrated)

Events

event DatasetRated(
    bytes32 indexed datasetId,
    address indexed buyer,
    uint8 stars,
    uint256 timestamp
);

Both datasetId and buyer are indexed, so the indexer can filter cheaply by either axis.

Integration Example

const datasetId = ethers.id(datasetGuid);
const rating = new ethers.Contract(addr, abi, signer);
const tx = await rating.submitRating(datasetId, 5);
await tx.wait();

Deployment

The canonical sequence is the deterministic-deploy.js script — it reproduces the historical Sepolia deployer's nonce sequence on a fresh chain so the four contracts land at their canonical addresses. Run on Base mainnet to bootstrap production; re-run against a clean Sepolia state to align staging.

# Full canonical deployment (all four contracts at known addresses)
npx hardhat run scripts/deterministic-deploy.js --network base
npx hardhat run scripts/deterministic-deploy.js --network base-sepolia

# Standalone deploy (e.g. testing a contract in isolation)
npx hardhat run scripts/deploy-rating.js --network base
forge script script/DeployDataTransfer.s.sol --rpc-url $BASE_RPC --broadcast

Prerequisites

  • .env with ALCHEMY_API_URL, DEPLOYER_PRIVATE_KEY (funded wallet) and SEPOLIA_PRIVATE_KEY (old deployer wallet — its nonce is replayed)
  • Deployer wallet funded with native ETH on the target network

Verification

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

View on Basescan:

  • Base mainnet: https://basescan.org/address/<ADDRESS>
  • Base Sepolia: https://sepolia.basescan.org/address/<ADDRESS>

Contract Source Files

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

Webapp -- Developer Guide

1. Getting Started

Prerequisites

  • Node.js 20+ and npm
  • A modern browser with crypto.subtle (Chrome, Firefox, Safari, Edge — used for the embedded-wallet key derivation; no MetaMask or other external wallet needed)
  • 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 (template at webapp/.env.example)

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 ¢
VITE_ALCHEMY_API_KEY Yes Alchemy API key — config/network.js builds the RPC URL from it yMM6Ie-wR74s...
VITE_CHAIN_ID Yes Target chain (hex). 0x14a34 = Base Sepolia (current default), 0x2105 = Base mainnet, 0xaa36a7 = Eth Sepolia 0x14a34
VITE_REDATIUM_CONTRACT_ADDRESS Yes RedatiumRegistry contract 0xabE1d17925519290c68c68eBDdbF3B96329EE5d6
VITE_REGISTRATION_CONTRACT_ADDRESS Yes RedatiumRegistry (same as above — user registration events) 0xabE1d17925519290c68c68eBDdbF3B96329EE5d6
VITE_TRANSFER_CONTRACT_ADDRESS Yes SolidusDataMarketplace 0xc0E4605CC9bD6768B785121c214B039946922082
VITE_STATE_CHANNEL_ADDRESS Yes DataTransferAdjudicator 0x66345886cD27fD1a88F65a5704899F68af59cbAb
VITE_DATASET_RATING_CONTRACT_ADDRESS Yes DatasetRating — buyer star ratings 0xA0DBB5504cA81163d937C9c2374d778A4fD7a7bF
VITE_DATA_EXCHANGE_APP_ADDRESS No App-routing contract address (reserved) —
VITE_TRANSAK_ENABLED No Master kill-switch for the fiat on/off-ramp. When true the partner API key is mandatory or boot fails. false
VITE_TRANSAK_API_KEY If Transak enabled Public Transak partner API key — safe to ship. The Transak API secret (used to mint a Partner Access Token) lives only in the backend. pk_test_...
VITE_TRANSAK_ENV No STAGING (default) or PRODUCTION STAGING
VITE_TRANSAK_NETWORK No Target network slug accepted by Transak. Single-network deployment: base. base
VITE_TRANSAK_CRYPTO_CURRENCY_CODE No Default crypto currency code. Single-asset deployment: ETH. ETH

All variables are accessed via webapp/src/config/env.js (and webapp/src/config/network.js for chain-related ones).

Important notes:

  • Switching VITE_CHAIN_ID also requires swapping the contract addresses to the deployment on that chain — each chain has its own deployed contracts.
  • When the ngrok tunnel restarts, update VITE_API_BASE_URL and restart the dev server.

3. Project Structure

webapp/src/
  App.jsx                       # Root component — provider hierarchy + routing
  main.jsx                      # Vite entry point
  index.css                     # Global styles + CSS variable design system
  App.css                       # App-level styles
  wallet/                       # Embedded-wallet stack (no MetaMask)
    WalletProvider.jsx          # Bridges walletStore module into React
    WalletGate.jsx              # Gates app behind unlocked wallet
    walletStore.js              # Module-scope signer + status
    crypto.js                   # WebCrypto key derivation / encryption
    keyStorage.js               # IndexedDB-backed encrypted envelope
    mnemonic.js                 # BIP-39 helpers
    autoLock.js                 # Idle-timeout auto-lock
    useWallet.js                # React hook over WalletProvider
    useWalletBalance.js         # On-chain balance hook
    approval/                   # User-confirmation dialog stack
    components/                 # Onboard / Unlock / Register / Settings screens
    walletMigration.js          # Legacy → embedded wallet migration
  components/
    Layout.jsx                  # Sidebar + content layout
    pages/                      # Page-level components
      Dashboard.jsx             # Home with dataset cards + activity + ratings
      BrowseUsers.jsx           # Marketplace browse with filters + pagination
      Requests.jsx              # Request lifecycle + magnet retrieval + download
      ChannelManagement.jsx     # State channel dashboard
      TransakSellDeposit.jsx    # Fiat off-ramp / withdrawal step (post-Transak redirect)
    transak/                    # Transak buy/sell buttons, interstitial + confirm modals
    ui/                         # Reusable UI components (TopBar, RatingStars, ...)
  contexts/
    AuthContext.jsx             # SIWE session layer over the embedded wallet
    ChannelContext.jsx          # State channel management + SignalR subscribe
    RatingsContext.jsx          # Rating eligibility + on-chain submission
    ThemeContext.jsx            # Light/dark theme
    ToastContext.jsx            # Toast notifications + notification bell
  hooks/
    useEthUsd.js                # USD price for native ETH
    useTransakFlow.js           # Transak buy/sell widget-URL flow + event handling
    useWebTorrentReceiver.js    # P2P download + payment protocol (buyer)
    useStateChannelProtocol.js  # State channel message builders
  services/
    BaseService.js              # HTTP fetch wrapper (singleton parent)
    DatasetService.js           # Dataset API
    DatasetFeedbackService.js   # Eligibility check (POST /dataset-feedback/eligibility)
    DataSharingRequestService.js
    DataTransactionService.js
    ChannelService.js           # State-channel HTTP endpoints
    BlockchainService.js        # On-chain reads + writes (via walletStore signer)
    TransakService.js           # /api/transak/sign-url client (returns { widgetUrl, expiresAt })
    PriceService.js
    SignalRService.js           # SignalR hub connections
    StateStorageService.js      # IndexedDB state persistence
    channel/
      channel-manager.js        # ForceMove state machine
      channel-store.js          # In-memory channel state
      scan-withdrawable.js      # Withdrawable balance scan
      index.js                  # Factory + singleton
    chain/                      # Chain adapter (ethers v6 contract calls)
    disputes/
      dispute-handler.js        # SignalR-driven auto-response
    signing/
      ethers6-signer.js         # ethers v6 signer adapters
      (EIP-712 typed-data helpers)
    transport/                  # WebRTC + tracker transport
  config/
    env.js                      # Environment config + contract ABI references
    network.js                  # Chain id + RPC URL builder + EXPECTED_CHAIN_ID_DEC
  abi/                          # Contract ABIs (RedatiumRegistry, DatasetRating, ...)
  utils/                        # Utility helpers (incl. network-guard utilities)
  locales/                      # i18next translations (en/, el/)

Removed: services/ChainWatcher.js (replaced by backend SignalR push), pages/WalletConnect.jsx (replaced by wallet/components/WalletGate.jsx + RegisterWalletScreen.jsx).

4. Key Pages and Routes

Route Page Component Key Features
/ Dashboard Dataset cards (Financial, Health, Other), recent activity, on-chain ratings via RatingsContext
/browse BrowseUsers Multi-filter (type, period, ratings), paginated 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
/transak/sell-deposit TransakSellDeposit Fiat off-ramp pre-deposit step (when Transak is enabled) — destination of the Transak walletRedirection for SELL flows

Authentication is enforced outside React Router: the <WalletGate> wrapper sits at the top of the provider tree and only renders the app once an embedded wallet exists and is unlocked. While locked or uninitialized, the gate shows the onboarding / unlock / register screens directly — there is no /connect route any more.

5. Embedded Wallet & SIWE

The webapp ships a browser-native, encrypted-on-device wallet — no MetaMask, no WalletConnect, no external extension. The unlocked ethers.Wallet signer lives in a module-scope variable in wallet/walletStore.js and is never copied into React state or context.

Wallet lifecycle

<WalletProvider> exposes a three-state status — uninitialized | locked | unlocked — plus the EIP-55 address. <WalletGate> reads this status and routes the user:

Status Shown screen
uninitialized OnboardingWalletScreen → choice of RegisterWalletScreen (create) or ImportWalletScreen (mnemonic)
locked UnlockWalletScreen (password unlock)
unlocked The actual app (Layout + routed page)

Creation flow:

  1. User sets a password and confirms a generated 12-word mnemonic.
  2. crypto.js derives an encryption key from the password (Argon2-style KDF via WebCrypto PBKDF2).
  3. The mnemonic-derived ethers.HDNodeWallet private key is encrypted and stored in IndexedDB (keyStorage.js).
  4. The unlocked signer is held in walletStore._signer at module scope; only { status, address } are surfaced through context.

On unlock, the same KDF re-derives the key, decrypts the envelope, and rehydrates the signer.

Auto-lock

autoLock.installAutoLock wires idle/visibility listeners on mount. After the configured idle threshold the store fires a { type: 'lock' } event; the provider flips status to locked and the gate redraws the unlock screen on the next render. The decrypted signer is zeroed from memory at that point.

Signing surface

Every signing call site fetches the signer on demand:

import * as walletStore from '../wallet/walletStore.js';

const signer = await walletStore.requireSigner();        // throws WalletLockedError if locked
const sig = await signer.signTypedData(domain, types, value);

This includes:

  • SIWE — AuthContext.signIn() builds a SiweMessage, calls signer.signMessage(msg.prepareMessage()), then POSTs (message, signature) to /api/auth/siwe-verify. The backend re-verifies server-side and issues a JWT (see API doc §2).
  • State channels — useWebTorrentReceiver / channel-manager use the signer for SETUP / CONCLUDE typed-data sigs.
  • DatasetRating — RatingsContext.submit(...) connects an ethers.Contract instance with the signer and calls submitRating(datasetId, stars).

Network guards

Before any wallet-signing action that hits the chain (SIWE, channel deposit, rating submission), utils/ensureCorrectNetwork.js asserts walletStore.getChainId() === EXPECTED_CHAIN_ID_DEC (from config/network.js). If a mismatch is detected, the embedded wallet's RPC provider is rewired to the expected network — there is no external "switch network" prompt because there is no external wallet.

Approval dialogs

For any user-visible signing event (SIWE login, rating submit, channel deposit), ApprovalProvider shows a modal describing exactly what is being signed. The user confirms or rejects; rejection throws an ApprovalRejectedError back to the caller.

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 + embedded-wallet signer (via walletStore.requireSigner())
  3. openChannel('buyer') -- buyer signs SETUP turnNum 1 via the embedded wallet (gated through ApprovalProvider)
  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 "Base"

    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. Since the chain-watcher refactor, all dispute signals arrive via backend SignalR push — there is no client-side chain polling fallback.

  1. Subscribes to dispute events on the BlockchainHub wallet group (ChallengeDetected, ChallengeRefuted, ChallengeResponded, ChannelConcluded). The backend's single getLogs polling loop owns chain reads and pushes the decoded events to subscribed wallets.
  2. On ChallengeDetected: 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().

Reconnect catch-up: if SignalR drops or the tab is backgrounded long enough to miss events, the handler hits GET /events/since?wallet=<addr>&sinceTimestamp=<last> on reconnect to backfill any chain events durably stored in ChainEvents.

Known event-name mismatch (still under review): clients still listen for ChallengeRaised and ChannelFinalized in some code paths, while the backend emits ChallengeDetected and ChannelConcluded. Verify your subscription names match the backend's SendAsync names (see API doc §6).

9. Real-Time UI (SignalR + Notification Bell)

When a seller accepts/declines a request or starts seeding, the buyer's webapp updates without a page reload. The plumbing:

  • Backend broadcasts RequestStatusChanged on the BlockchainHub wallet group from DataSharingRequestService.RespondAsync and StartSeedingAsync (see backend tech design).
  • ChannelContext subscribes via SignalRService.onRequestStatusChanged(...) — fires an info-variant toast (brand-purple, styled by .toast--info) with a View action button. There's no separate notification variant.
  • Notification bell in TopBar reads the toast history (ToastContext) and renders an unread-badge dropdown with one row per toast plus a View button each.
  • Requests page (/requests) subscribes too and bumps an internal refreshTrigger counter, so it auto-refetches when the user is sitting on it.
  • Highlight-on-arrival: View routes to /requests?highlight=<requestId>. The Requests page reads useSearchParams() and glows the matching row briefly.

Because ChannelContext is mounted outside BrowserRouter, the View action uses window.history.pushState(...) + dispatchEvent(new PopStateEvent('popstate')) (helper called navigateProgrammatic) instead of useNavigate() — react-router-dom listens to popstate and refreshes the URL.

10. 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).

11. Troubleshooting

Problem Solution
Wallet stuck on Unlock screen Wrong password. There is no recovery path other than re-import from the 12-word mnemonic via ImportWalletScreen. To start fresh, clear IndexedDB for the origin (DevTools → Application → IndexedDB → delete the redatium DB), then refresh.
"Failed to fetch" errors Verify VITE_API_BASE_URL in .env matches your running ngrok tunnel. Restart the 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 Confirm the backend /api/auth/siwe-verify is reachable and VITE_CHAIN_ID matches the chain the backend expects. Clear localStorage['redatium_auth'] and re-sign.
State channel deposit fails Ensure the embedded wallet has enough native ETH on the target chain (Base mainnet 0x2105 by default). Use the Transak buy flow or top up the address manually. Check that VITE_STATE_CHANNEL_ADDRESS matches the deployed contract on that chain.
Wrong network on signing The webapp uses an embedded wallet, so there is no external "switch network" prompt — utils/ensureCorrectNetwork.js rewires the RPC provider when VITE_CHAIN_ID and the active chain disagree. Confirm .env matches the chain you intend to use.
DisputeHandler not responding Check SignalR connection in console. Verify the backend BlockchainHub is reachable and you're subscribed via SubscribeToWallet. Confirm subscriber names match what the backend emits (see API doc §6).
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 don't have a working Keystore for the embedded-wallet biometric unlock)
  • Backend API running (see API.DeveloperGuide.md)
  • ADB configured for USB or wireless debugging

The app uses an on-device embedded wallet (encrypted key envelope in the OS Keychain, biometric unlock). No external wallet app is required.

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 the read-only RPC provider yMM6Ie-wR74s5OqdoO2zv
WALLETCONNECT_PROJECT_ID Legacy Project ID — kept for the legacy WalletConnectService shim; not used by the embedded-wallet flow 4f0d1018b810434d038f8b830821d13b
REACT_APP_API_BASE_URL Yes Backend API (ngrok tunnel in dev) https://xyz.ngrok-free.dev
REACT_APP_CURRENCY_SYMBOL No Display currency symbol ¢
REACT_APP_MOCK_USER_ID No Default wallet for dev 0x98ba...
REGISTRATION_CONTRACT_ADDRESS Yes RedatiumRegistry — user registration contract 0xabE1d17925519290c68c68eBDdbF3B96329EE5d6
REDATIUM_CONTRACT_ADDRESS Yes RedatiumRegistry — data sharing requests 0xabE1d17925519290c68c68eBDdbF3B96329EE5d6
REQUIRED_CHAIN_ID Yes Target chain (hex). 0x14a34 = Base Sepolia (current default), 0x2105 = Base mainnet 0x14a34
STATE_CHANNEL_ADDRESS Yes DataTransferAdjudicator address (read by config/contracts.ts, re-exported as DATA_TRANSFER_ADJUDICATOR_ADDRESS) 0x66345886cD27fD1a88F65a5704899F68af59cbAb
DATASET_RATING_CONTRACT_ADDRESS Yes DatasetRating contract — buyer star ratings 0xA0DBB5504cA81163d937C9c2374d778A4fD7a7bF
REACT_APP_PUSH_ENABLED No "true" to enable Firebase Cloud Messaging. Default "false" so the app boots without google-services.json configured. true
REACT_APP_TRANSAK_API_KEY If Transak enabled Public Transak partner API key — safe to ship. The Transak API secret stays on the backend. pk_test_...
REACT_APP_TRANSAK_ENVIRONMENT No Widget environment: STAGING (default) or PRODUCTION. STAGING
REACT_APP_TRANSAK_API_BASE_URL No Transak public REST API base. Staging https://api-stg.transak.com, Production https://api.transak.com. https://api-stg.transak.com
REACT_APP_TRANSAK_WIDGET_BASE_URL No Transak hosted widget base. Staging https://global-stg.transak.com, Production https://global.transak.com. https://global-stg.transak.com
REACT_APP_TRANSAK_REFERRER_DOMAIN No Referrer domain echoed back to Transak (Origin allow-list). Use the deployed mobile-deep-link domain or app.redatium.org. app.redatium.org
REACT_APP_TRANSAK_NETWORK No Target network slug accepted by Transak. Single-network deployment: base. base
REACT_APP_TRANSAK_DEFAULT_CRYPTO_CURRENCY No Default crypto currency code. Single-asset deployment: ETH. ETH

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/
  App.tsx                       # Root component (provider hierarchy)
  index.js                      # RN entry — registers FCM background handler
  src/
    auth/
      siwe.ts                   # SIWE message build + verify
    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
        notificationsSlice.ts
      providers/
        NotificationsProvider.tsx
    wallet/                     # Embedded-wallet stack (encrypted on-device keys)
      EmbeddedWalletService.ts  # Module-scope singleton — signer never escapes
      WalletProvider.tsx        # React context bridge over the service
      KeychainStore.ts          # OS Keychain envelope storage
      KeychainConfig.ts / keychainQueue.ts
      crypto.ts                 # KDF + AES encryption helpers
      mnemonic.ts               # BIP-39 helpers
      autoLock.ts               # Idle / background auto-lock
      useEmbeddedWallet.ts      # React hook over the provider
      walletAdapter.ts          # ethers.Signer adapter for legacy call sites
      walletMigration.ts        # Migration from legacy WalletConnect session
      screens/
        RegistrationGate.tsx    # On-chain wallet-registration gate
        WalletSettingsScreen.tsx
        (Onboard / Unlock / Import / Register screens)
      approval/                 # User-confirmation modal stack
    modules/
      auth/        screens/ (BiometricSetup, BiometricLogin) + slices/authSlice
      dataWallet/  screens/ (DataWallet, ConnectDataSource, ...)
      home/        screens/ (HomeScreen) + hooks/ (useEarnings, useRecentEarnings) + slices/earningsSlice
      notifications/ screens/ (NotificationsScreen)
      onboarding/  screens/ (OnboardingScreen 1-4) + navigation
      profile/     screens/ (Profile, Settings, LanguageSelector) + slices/userSlice
      requests/    screens/ (RequestsScreen) + slices/requestsSlice
      wallet/      screens/ (WalletScreen, ChannelScreen, RecentEarning) +
                   components/ (EarningsCard, TotalEarningsCard) + slices/walletSlice
    screens/                    # Standalone screens (Health, Banking, Torrent, etc.)
    services/
      WebTorrentSeeder.ts
      TorrentCreationService.ts
      StateChannelProtocol.ts
      StateChannelEngine.ts     # Legacy (deprecated)
      ChainWatcher.ts           # Legacy — kept for a few non-state-channel paths.
                                # State-channel events now arrive via backend SignalR push.
      EphemeralKeyManager.ts
      EscrowService.ts
      TransakService.ts         # /api/transak/sign-url client (returns { widgetUrl, expiresAt })
      PriceService.ts
      RegistrationPollingService.ts
      PushNotificationsService.ts
      SignalRService.ts
      channel/  channel-manager.ts / index.ts
      chain/    chain-adapter.ts
      signing/  local-signer.ts
      disputes/ dispute-handler.ts
    health/                     # Health Connect bridge
    config/
      contracts.ts              # Contract addresses + ABIs
      network.ts                # Chain id + RPC URL builder
      env.ts                    # Transak / feature flags (throws at boot if misconfigured)
    contexts/                   # TransferContext, AppDataProvider, etc.
    localization/               # i18next config + translations
    theme/
    shared/                     # Shared components

4. Embedded Wallet + SIWE

The mobile app ships an on-device embedded wallet — encrypted key envelope in the OS Keychain, biometric unlock. No external wallet app (MetaMask, Rainbow, Trust) is required. The unlocked ethers.Wallet signer lives in module-scope inside EmbeddedWalletService and is never exposed through React context.

Wallet Lifecycle

<WalletProvider> exposes a three-state status — uninitialized | locked | unlocked — plus the EIP-55 address. Mirrored into Redux (walletSlice) so persisted views like "Locked — 0xAbc…" keep working without forcing a biometric prompt.

Status Shown screen
uninitialized Onboarding → choice of "Create wallet" or "Import mnemonic"
locked UnlockWalletScreen (biometric or password)
unlocked Authenticated app (Tab navigator)

Creation flow:

  1. User sets a PIN/password; crypto.ts derives a KDF key.
  2. The mnemonic-derived private key is encrypted and stored in the OS Keychain via KeychainStore.ts (react-native-keychain).
  3. The unlocked signer lives in EmbeddedWalletService._signer at module scope; only { status, address } are exposed to React.
  4. Biometric unlock is configured if the device supports it (react-native-biometrics); on next launch, the user can unlock with a fingerprint / face scan.

Auto-lock

installAutoLock listens on AppState and an idle timer. After ~5 minutes in background, the in-memory signer is dropped and status flips to locked (per the OD-13 design constraint). The next signing call from any screen will trigger an unlock prompt.

Signing Surface

Every signing call site fetches the signer on demand via the service:

import { requireSigner } from '../wallet/EmbeddedWalletService';

const signer = await requireSigner();         // throws WalletLockedError if locked
const sig = await signer._signTypedData(domain, types, value);  // ethers v5 syntax

This applies to SIWE (auth/siwe.ts), state channel signatures, DatasetRating submissions, and on-chain calls from BlockchainService.

SIWE Flow

  1. WalletProvider mounts, wallet unlocked, address known.
  2. RegistrationGate checks GET /user/wallet/check/{address}; if not registered, routes to the on-chain registration screen.
  3. Once registered, auth/siwe.ts builds the SIWE message (domain, address, nonce, chain id) and signs it with the embedded wallet.
  4. Client POSTs (message, signature) to POST /api/auth/siwe-verify; backend re-verifies and issues a JWT.
  5. JWT is stored in encrypted storage (Keychain) and sent as Authorization: Bearer on subsequent calls.

Pre-SIWE Chain Switch

Before signing the SIWE message, the app asserts the embedded wallet's RPC provider is pointed at REQUIRED_CHAIN_ID. Since the wallet is local, "switching" means re-pointing the JsonRpcProvider URL — there is no user-facing network-switch prompt.

Legacy WalletConnect Shim

WalletConnectService.ts and useMetaMask.ts still exist as a compatibility shim so screens written before the embedded-wallet migration keep compiling. The shim's connect/disconnect are no-ops; getAccount returns the embedded wallet's address; getEthersProvider returns a plain JsonRpcProvider built from the configured Alchemy RPC. The Universal Provider relay is no longer used for signing. Treat the shim as deprecated — new code should call requireSigner() directly.

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's channelManager.openChannel(channel, 'seller') calls requireSigner() to fetch the embedded-wallet signer and signs SETUP (turnNum 0)
  4. ApprovalProvider shows a modal describing the channel terms; user confirms or rejects
  5. Caches accept_channel in _pendingAccept (data channel may die during the approval modal)
  6. When the data channel is available, sends accept_channel with ephemeral address
  7. 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 user-visible approval — that's the point of ephemeral keys)
  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. Backend SignalR pushes ChannelConcluded on the wallet group (the indexer's single getLogs cycle catches the on-chain finalization) — no client-side chain polling
  6. Calls withdraw() from the embedded-wallet signer to receive payment. The mobile WalletScreen also exposes a Check-withdrawals button that scans all the seller's channels for unclaimed balances and lists them.

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
EmbeddedWalletService wallet/EmbeddedWalletService.ts Yes (module) On-device encrypted wallet — owns _signer, requireSigner() is the only signing surface
WalletConnectService wallet/WalletConnectService.ts Yes (module) Deprecated shim — kept so legacy call sites compile; signing flows now go through requireSigner()
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 (state-channel updates)
HealthConnectService health/HealthConnectService.ts Yes (class) Android Health Connect bridge
EphemeralKeyManager services/EphemeralKeyManager.ts Yes (exported) Manage ephemeral keys
TransakService services/TransakService.ts Yes (exported) Calls backend /api/transak/sign-url and returns the single-use { widgetUrl, expiresAt } pair used by the in-app WebView
PushNotificationsService services/PushNotificationsService.ts Yes (module) FCM token registration; gated by REACT_APP_PUSH_ENABLED
SignalRService services/SignalRService.ts Yes (module) BlockchainHub + TransferHub connections, subscribes wallet groups
RegistrationPollingService services/RegistrationPollingService.ts Yes (module) Polls /user/wallet/check/{address} after on-chain registration tx

Earnings: modules/home/hooks/useEarnings.ts + modules/home/slices/earningsSlice.ts subscribe to EarningsUpdated on the wallet's SignalR group and mirror the running total into Redux. HomeScreen shows it as "Earned via Redatium". The initial fetch hits GET /user/earnings/{walletAddress}.

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 embedded wallet
    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
    Note over App: Backend SignalR pushes ChannelConcluded
    App->>App: withdraw via embedded wallet

9. Push Notifications (Android, FCM)

Files: mobile/src/services/PushNotificationsService.ts, mobile/src/hooks/usePushNavigation.ts, mobile/index.js, mobile/android/app/google-services.json.

When push fires

The seller's phone receives a system-tray notification when a buyer creates a DataSharingRequest for one of the seller's datasets. Backend triggers it from BlockchainEventListenerService after the chain event is indexed (see Backend technical design).

Setup

  1. Firebase Console (https://console.firebase.google.com):
    • Create a project (or reuse redatium). Free Spark plan, Analytics off.
    • Add an Android app, package name com.redatium (must match exactly). Download google-services.json.
    • Project Settings → Service accounts → Generate new private key. Download as JSON (used by backend, not the app).
  2. Drop google-services.json at mobile/android/app/google-services.json (gitignored — a redacted google-services.example.json is committed for shape reference).
  3. In mobile/.env, set REACT_APP_PUSH_ENABLED=true. Default is false so the app boots fine without Firebase configured.
  4. Rebuild — native deps and the Google services Gradle plugin need a clean sync:
    npm run android -- --active-arch-only
    
  5. After connect, watch Metro / adb logcat for [Push] Registered FCM token: 0x... to confirm the wallet → token row was registered with the backend.

Wiring

  • PushNotificationsService (singleton) handles getToken(), registration with the backend, and onTokenRefresh. Gated entirely by REACT_APP_PUSH_ENABLED.
  • WalletProvider calls pushNotifications.register(account) on connect and unregister(prev) on disconnect / wallet switch.
  • usePushNavigation (mounted inside RootNavigator) handles foreground / background / cold-start taps and routes data.type === 'datasharing-request' to MainTabs > Requests with params.highlightId. For cold-starts the hook waits via navigation.addListener('state') until MainTabs mounts before navigating.
  • index.js registers a no-op setBackgroundMessageHandler at module scope so Android can deliver while the JS context is dead. The payload includes a notification block which Android renders into the tray automatically without app code involvement.

Release builds

For a release APK, register the SHA-1 fingerprint of your release keystore in Firebase Console (Project Settings → your Android app → Add fingerprint) — otherwise FCM rejects token registrations from release-signed APKs. ProGuard rules ship with @react-native-firebase and merge automatically; verify after assembleRelease if you minify.

Smoke test (without going through the buyer flow)

In Debug builds the backend exposes a guarded test endpoint:

curl -X POST http://localhost:5032/notification/admin/push-test \
  -H "Content-Type: application/json" \
  -H "X-Dev-Auth: dev-only-change-me" \
  -d '{"payload":{"walletAddress":"0xYOUR_WALLET","message":"hello"}}'

The endpoint is compiled out of Release builds (#if DEBUG).

10. 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

Embedded Wallet

Problem Solution
Stuck on Unlock screen Forgotten password. There is no recovery path other than wiping the wallet (Wallet Settings → Wipe) and re-importing from the 12-word mnemonic.
Biometric prompt not appearing Device hasn't enrolled biometrics, or react-native-biometrics reports the secure enclave is unavailable. Fall back to PIN/password unlock.
Signing screen frozen Check Metro logs for [ApprovalProvider] events. The signing path is a Promise that resolves on user confirmation — if no modal is showing, ApprovalProvider likely isn't mounted at this point in the tree.
Wrong network Set REQUIRED_CHAIN_ID in .env and restart Metro with --reset-cache. There is no external wallet to switch — the embedded wallet rewires its JsonRpcProvider URL when the env changes.
Insufficient funds for state-channel deposit Embedded wallet needs native ETH on REQUIRED_CHAIN_ID. On Base mainnet (0x2105), use Transak (REACT_APP_TRANSAK_API_KEY set and the feature flag on) or top up the address manually. On Base Sepolia (0x14a34), use the Coinbase faucet at https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet.

Legacy WalletConnect (deprecated, but still wired)

Problem Solution
Deep link not opening Check redatium:// scheme in AndroidManifest.xml — still required for FCM push-notification taps
Old call site failing The WalletConnectService shim's connect/disconnect are no-ops; callers that expect a relay session will see no error but also no behavior. Migrate the call to requireSigner().

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.