Functions: 12 Core: 4 Auth: 2 Query: 4 Admin: 1 Utility: 1

System Overview

Architecture Flow

ClientLogin Page
→
AuthCognito
→
GatewayAPI Gateway
→
ComputeLambda
→
ClientDashboard
→
DatabaseDynamoDB

Blockchain-style immutable hash chain for grain commodity ownership tracking

Function Index

FunctionCategoryMethodEndpointDescription
grainApp-create-title Core POST /titles Creates new grain title with initial cryptographic hash
grainApp-transfer-title Core POST /transfer-title Transfers ownership and creates new blockchain record
relist_title Core POST /relist-title Relists a transferred title for sale at a new price
grainApp-validate-hash Core POST /validate-hash Validates hash chain integrity for tamper detection
grainApp-login Auth GET / Entry point that redirects to Cognito login
grainApp-callback Auth GET /callback OAuth callback that exchanges code for tokens
grainApp-sales Query GET /sales Returns all titles currently for sale in the marketplace
get_my_titles Query GET /my-titles Returns all titles owned by the authenticated user
grainApp-get-ownership-history Query POST /ownership-history Returns complete ownership chain for a title
grainApp-dashboard Query GET /dashboard Serves the main application dashboard HTML
GetAdminStats Admin GET /admin-stats Returns system-wide statistics for administrators
grainApp-backup Utility - EventBridge/CloudWatch Backs up DynamoDB table to S3

Core Functions

grainApp-create-title
Core

Creates new grain title with initial cryptographic hash

POST /titles
Cognito JWT
grainApp-create-title.py

Detailed Operation

This function serves as the genesis point for all grain titles in the system. When a user wants to list grain for sale, this function creates the initial record that will form the foundation of an immutable ownership chain.

Authentication & Security:
The function extracts user identity from Cognito JWT claims in the request context. The seller identity is FORCED to be the authenticated user - the function explicitly ignores any seller_id passed in the request body. This prevents impersonation attacks where someone might try to create listings on behalf of another user.

Input Validation:
Three fields are required: grain_type (Corn, Wheat, Soybean, or Oats), quantity (positive integer representing bushels), and price (decimal value per bushel). The price is normalized to exactly 2 decimal places to ensure consistent hash generation.

Hash Generation Process:
The initial hash is created using SHA-256 with the following concatenated string:
hash_input = f"{grain_type}{quantity}{seller_id}{price_normalized}{timestamp_iso}"
This hash serves multiple purposes: it becomes the primary key (TitleHash), the chain identifier (InitialHash), and the starting point for the hash chain. All future transfers will reference this InitialHash to identify records belonging to the same title.

Record Structure:
The DynamoDB item includes blockchain-style fields: TitleHash (primary key), InitialHash (chain ID), CurrentHash, PreviousHash, HashChain (array of all hashes), plus business fields like GrainType, Quantity, Price, SellerID, Status ('ForSale'), and TransferCount (0 for new titles).

Response:
Returns the generated hash which serves as the unique title identifier. The frontend uses this to display the listing and enable purchases.
grainApp-transfer-title
Core

Transfers ownership and creates new blockchain record

POST /transfer-title
Cognito JWT
grainApp-transfer-title.py

Detailed Operation

This is the core transaction function that implements blockchain-style ownership transfer. Rather than simply updating a record, it creates a NEW record with a new hash, preserving the complete history.

Authentication & Security:
Like create-title, the buyer identity is extracted from Cognito claims and cannot be spoofed. The function also prevents self-purchase by comparing the buyer's email against the current SellerID.

Pre-Transfer Validation:
The function performs several checks: (1) Title must exist, (2) Status must be 'ForSale', (3) Buyer cannot be the same as seller. It uses ConsistentRead=True to ensure it's working with the latest data.

Hash Chain Integrity:
The new hash incorporates the previous hash, creating an unbreakable chain:
hash_input = f"{current_hash}{grain_type}{quantity}{buyer_id}{price_string}{timestamp_iso}{new_transfer_count}"
If anyone tampers with historical records, the hash chain breaks and validation fails.

Two-Step Database Update:
Step 1: The OLD record is updated - Status changes to 'Transferred' and BuyerID is set to the buyer's email. This record remains in the database as historical evidence.
Step 2: A NEW record is created with: new TitleHash (the calculated hash), same InitialHash (maintains chain identity), new CurrentHash, PreviousHash pointing to the old record, incremented TransferCount, and crucially - the buyer becomes the new SellerID (they now own it).

Ownership Model:
In this system, SellerID doesn't mean "person selling" - it means "current owner." When you buy a title, you become the SellerID of the new record. The new record has Status='Transferred' (not for sale) until the owner explicitly relists it.

Response:
Returns the new hash, previous hash, and transfer count for the frontend to update its display.
relist_title
Core

Relists a transferred title for sale at a new price

POST /relist-title
Cognito JWT
relist_title.py

Detailed Operation

After purchasing a title, the owner may want to sell it. This function allows them to relist it on the marketplace at a price of their choosing.

Ownership Verification:
The function performs strict ownership checking by comparing the authenticated user's email (lowercase) against the SellerID of the title record. Only the current owner can relist a title.

Status Validation:
Prevents duplicate listings by checking that the current Status is NOT 'ForSale'. If someone tries to relist an already-listed title, they receive an error.

Price Update:
Unlike transfers where price stays the same, relisting allows the owner to set a new price. This enables market dynamics - buy low, sell high. The new price is normalized to 2 decimal places for consistent hashing.

Blockchain Record Creation:
Like transfer, relisting creates a NEW record rather than updating the existing one:
hash_input = f"{current_hash}{grain_type}{quantity}{user_email}{price_string}{timestamp_iso}{new_transfer_count}"
The old record's Status is changed to 'Listed' (historical marker), and a new record is created with Status='ForSale'.

TransferCount Increment:
Even though ownership doesn't change, TransferCount is incremented. This maintains proper chain ordering - each record in a chain has a unique TransferCount, allowing the system to identify the "head" (current state) by finding the highest TransferCount for a given InitialHash.

Chain Head Concept:
The marketplace and my-titles queries find chain heads by grouping records by InitialHash and selecting the one with the highest TransferCount. This is why TransferCount must always increment.
grainApp-validate-hash
Core

Validates hash chain integrity for tamper detection

POST /validate-hash
Cognito JWT
grainApp-validate-hash.py

Detailed Operation

This function is the integrity checker of the entire system. It recalculates what a record's hash SHOULD be based on its stored fields, then compares against the stored hash. Any mismatch indicates tampering.

Three Hash Formats:
The system uses different hash formulas depending on record type:

1. Original Creation (TransferCount=0, RelistCount=0):
hash_input = f"{grain}{qty}{seller_id}{price}{timestamp}"
This matches what grainApp-create-title generates.

2. Transfer Records:
hash_input = f"{prev_hash}{grain}{qty}{seller_id}{price}{timestamp}{transfer_count}"
Includes PreviousHash to chain records together.

3. Relist Records (RelistCount > 0, Status='ForSale'):
hash_input = f"{prev_hash}{grain}{qty}{seller_id}{price}{timestamp}{transfer_count}R{relist_count}"
Includes relist marker to differentiate from transfers.

Validation Process:
The function fetches the record, determines which hash format applies based on TransferCount/RelistCount/Status, reconstructs the hash input string using stored field values, calculates SHA-256, and compares against CurrentHash.

Tamper Detection:
If someone directly modifies the DynamoDB record (changing quantity, price, seller, etc.), the recalculated hash won't match. The dashboard calls this function for every listing and displays a red "Invalid" badge if validation fails.

Logging:
The function prints the hash input, calculated hash, and stored hash for debugging. This is invaluable when troubleshooting validation failures - you can see exactly which component differs.

Response:
Returns chain_valid (boolean) and validationStatus ('Valid' or 'Invalid') for the frontend.

Authentication Functions

grainApp-login
Auth

Entry point that redirects to Cognito login

GET /
Cognito JWT
grainApp-login.py

Detailed Operation

This is the simplest function in the system - it constructs the Cognito Hosted UI URL and returns an HTML page that automatically redirects to it.

Environment Variables:
Three values come from Lambda environment configuration:
- COGNITO_DOMAIN: The Cognito user pool domain (e.g., 'grain-app.auth.us-east-2.amazoncognito.com')
- CLIENT_ID: The Cognito app client ID
- REDIRECT_URI: Where Cognito should send users after login (the callback Lambda)

OAuth URL Construction:
login_url = f"https://{cognito_domain}/login?client_id={client_id}&response_type=code&scope=email+openid+profile&redirect_uri={redirect_uri}"

The response_type=code indicates Authorization Code flow (most secure). Scopes request email, OpenID identity, and profile information.

HTML Response:
Rather than a 302 redirect, the function returns HTML with a meta-refresh tag. This provides a brief "Redirecting to login..." message before the browser navigates to Cognito.

User Experience:
When users visit the application root URL, they see a brief loading message, then the Cognito Hosted UI appears where they can sign in, sign up, or reset their password. After successful authentication, Cognito redirects to the callback URL with an authorization code.
grainApp-callback
Auth

OAuth callback that exchanges code for tokens

GET /callback
Cognito JWT
grainApp-callback.py

Detailed Operation

This function completes the OAuth 2.0 Authorization Code flow by exchanging the authorization code for tokens and establishing the user session.

Authorization Code Extraction:
Cognito redirects here with ?code=XXXXX in the query string. The function extracts this code from event['queryStringParameters'].

Token Exchange:
The function makes a POST request to Cognito's /oauth2/token endpoint with:
- grant_type: 'authorization_code'
- client_id: The app client ID
- code: The authorization code from Cognito
- redirect_uri: Must match exactly what was used in the login request

Cognito responds with id_token (JWT with user info), access_token (for API calls), and refresh_token.

JWT Decoding:
The id_token is a JWT with three parts separated by dots. The function base64-decodes the middle part (payload) to extract user claims like cognito:username and email.

Group Lookup:
The function calls Cognito's admin_list_groups_for_user API to get the user's group memberships. This determines their role in the application.

Role Determination:
Groups are checked in priority order: Admins → Admin role, Sellers → Seller role, Buyers → Buyer role, otherwise User. The Admin role enables the admin statistics button in the dashboard.

Session Establishment:
The function returns HTML with JavaScript that stores tokens in sessionStorage:
- id_token: Used as Authorization header for API calls
- access_token: Available for future use
- user_email: Displayed in the UI
- user_role: Controls UI elements and permissions

Finally, it redirects to /dashboard where the main application loads.

Query Functions

grainApp-sales
Query

Returns all titles currently for sale in the marketplace

GET /sales
Cognito JWT
grainApp-sales.py

Detailed Operation

This function powers the marketplace view by returning all titles that are currently available for purchase.

Full Table Scan:
The function scans the entire GrainTitles table with ConsistentRead=True. While scans are expensive at scale, they're necessary here because we need to group records by InitialHash, which isn't the primary key.

Pagination Handling:
DynamoDB limits scan results to 1MB. The function handles this by checking for LastEvaluatedKey and continuing to scan until all records are retrieved.

Chain Grouping:
Records are grouped into a dictionary keyed by InitialHash. Each grain title (original creation) generates a unique InitialHash, and all subsequent transfers/relists share that InitialHash. This groups all records belonging to the same "title" together.

Finding Chain Heads:
For each InitialHash group, records are sorted by TransferCount in descending order. The first record (highest TransferCount) is the "chain head" - the current state of that title. This is the fundamental query pattern of the blockchain model.

ForSale Filter:
Only chain heads with Status='ForSale' are included in results. Titles that have been purchased but not relisted (Status='Transferred') don't appear in the marketplace.

Response Format:
Returns {items: [...], count: N} where items contains the full DynamoDB records for display. The DecimalEncoder class converts Decimal types to floats for JSON serialization (DynamoDB uses Decimal for numbers).

Frontend Usage:
The dashboard calls this endpoint on load and displays each item as a card showing grain type, quantity, price, seller, and validation status. Users can click "Buy This Title" on any listing they don't own.
get_my_titles
Query

Returns all titles owned by the authenticated user

GET /my-titles
Cognito JWT
get_my_titles.py

Detailed Operation

This function shows users what grain titles they currently own, enabling them to manage their portfolio and relist titles for sale.

User Identification:
The authenticated user's email is extracted from Cognito claims and normalized to lowercase. This will be matched against SellerID fields (remember: SellerID means "current owner").

Same Scan Pattern:
Like grainApp-sales, this function scans the full table, groups by InitialHash, and finds chain heads.

Ownership Matching:
For each chain head, the function compares SellerID (lowercase) against the user's email. A match means the user currently owns that title. The comparison is case-insensitive to handle email capitalization variations.

All Statuses Included:
Unlike the sales endpoint which filters for 'ForSale', this returns all chain heads owned by the user regardless of status.
This includes:
- 'ForSale': Titles the user is currently selling
- 'Transferred': Titles the user purchased but hasn't relisted

Response Contents:
Returns the matching chain heads with full record details. The frontend displays these with "Relist for Sale" buttons on transferred titles and "Already For Sale" indicators on listed ones.

Portfolio View:
Users can see their total holdings, track which titles they've listed vs. holding, and make decisions about when to relist based on current market conditions.
grainApp-get-ownership-history
Query

Returns complete ownership chain for a title

POST /ownership-history
Cognito JWT
grainApp-get-ownership-history.py

Detailed Operation

This function provides full transparency into a title's history - every creation, transfer, and relist event from genesis to current state. This Lambda function endpoint is POST rather than GET because the function expects titleId from event['body'], which only works with POST/PUT/PATCH requests.

Input:
Takes a title_hash which can be any hash in the chain (not just the current head). This allows users to click on any record and see the full history.

Finding the Chain:
First, the function fetches the clicked record to get its InitialHash. Then it scans for ALL records with that InitialHash. This retrieves the complete chain regardless of which record was clicked.

Chronological Sorting:
Records are sorted by TransferCount ascending, showing the chronological order: original creation (TransferCount=0), first transfer (1), second transfer (2), etc.

Record Formatting:
Each record is transformed by format_record() which extracts display-friendly fields:
- Owner determination: For TransferCount=0, owner is CreatedBy; for transfers, owner is SellerID
- Price and total value calculation
- Timestamp formatting for human readability
- Sold_to field showing who purchased from this owner

Current Record Marking:
The record matching the originally clicked hash is marked with is_current=True. The frontend highlights this record in the history modal.

Visual Display:
The dashboard shows this in a modal with each record as a card. The origin record has a yellow border, the current record has a green border, and intermediate transfers are gray. Each shows the hash, owner, buyer (if sold), price, status, and timestamp.

Audit Trail:
This provides a complete audit trail for any title. Inspectors, auditors, or buyers can verify the complete provenance of any grain title in the system.
grainApp-dashboard
Query

Serves the main application dashboard HTML

GET /dashboard
Cognito JWT
grainApp-dashboard.py

Detailed Operation

This function returns the complete single-page application HTML, CSS, and JavaScript for the grain trading dashboard.

Lambda-Served SPA:
Rather than hosting static files in S3, the entire frontend is embedded in this Lambda function. This simplifies deployment - updating the dashboard is just a Lambda code update.

Authentication Check:
The JavaScript immediately checks for id_token in sessionStorage. If missing, it redirects to the login page. This client-side check is supplemented by server-side JWT validation on API calls.

User Interface Sections:
- Header: Shows user email, role, and navigation buttons
- Marketplace: Grid of ForSale titles with buy buttons
- My Titles: User's owned titles with relist buttons
- List New Sale: Form to create new grain listings
- Admin Stats: Statistics panel (visible only to admins)

API Integration:
JavaScript functions make fetch() calls to the various Lambda endpoints:
- loadSales() → GET /sales
- loadMyTitles() → GET /my-titles
- submitNewSale() → POST /titles
- submitPurchase() → POST /transfer-title
- submitRelist() → POST /relist-title
- validateTitle() → POST /validate-hash
- showHistoryModal() → POST /ownership-history
- loadAdminStats() → GET /admin-stats

All requests include the Authorization header with the id_token.

Real-Time Validation:
When listings load, the dashboard calls validateTitle() for each one and displays a colored badge (green=Valid, red=Invalid) next to each listing.

Ownership Indicators:
The UI marks listings with "Your Listing" (yellow) or "You Own This" (blue) badges by comparing SellerID/BuyerID against the logged-in user's email.

Modal System:
A modal dialog shows ownership history when users click on a hash.

Admin Functions

GetAdminStats
Admin

Returns system-wide statistics for administrators

GET /admin-stats
Cognito JWT
GetAdminStats.py

Detailed Operation

This function provides administrators with a bird's-eye view of the entire system through aggregated statistics.

Admin Authorization:
Before processing, the function verifies the user belongs to the 'Admin' or 'Admins' Cognito group. Non-admin users receive a 403 Forbidden response. This is enforced server-side regardless of frontend UI restrictions.

Data Collection:
The function scans the entire GrainTitles table (with pagination handling) to collect all records. This provides the raw data for statistical analysis.

Statistics Calculated:
System Stats: - total_titles: Count of all records in the database
- original_titles: Records with TransferCount=0 (genesis blocks)
- total_transfers: Sum of all TransferCount values
- unique_users: Distinct emails in CreatedBy/SellerID fields

Status Breakdown:
- for_sale: Count where Status='ForSale'
- transferred: Count where Status='Transferred'

Grain Type Distribution:
Dictionary counting records by GrainType (Corn, Wheat, Soybean, Oats)

Volume Statistics: - total_bushels: Sum of all Quantity fields
- total_value_usd: Sum of (Price × Quantity) for all records
- average_price_per_bushel: total_value / total_bushels

Decimal Handling:
DynamoDB stores numbers as Decimal types. The DecimalEncoder class converts these to floats for JSON serialization.

Audit Logging:
The function logs who requested statistics and the results, providing an audit trail of admin activity.

Dashboard Display:
The admin stats panel in the dashboard shows these metrics in organized cards: System Overview, Status Breakdown, Grain Distribution, and Volume Statistics.

Utility Functions

grainApp-backup
Utility

Backs up DynamoDB table to S3

- EventBridge/CloudWatch
None
grainApp-backup.py

Detailed Operation

This utility function creates point-in-time backups of the GrainTitles table by exporting all records to an S3 bucket.

Trigger:
Unlike API-triggered functions, this is designed to run on a schedule via EventBridge (CloudWatch Events). A typical schedule might be daily at midnight: cron(0 0 * * * *).

Implementation:
The function performs:
1. Scan entire GrainTitles table
2. Generate filename with current date: backup-YYYYMMDD.json
3. Convert records to JSON (using DecimalEncoder for numeric types)
4. Upload to S3 bucket specified in BUCKET environment variable

S3 Configuration: The BUCKET environment variable must be set to an S3 bucket name. The Lambda execution role needs s3:PutObject permission on this bucket.

Backup Contents: The JSON file contains an array of all DynamoDB items with their full attributes. This provides a complete snapshot that can be used for disaster recovery or data analysis.

Retention: Each backup has a unique date-based filename, so historical backups accumulate. S3 lifecycle policies can be configured to automatically delete backups older than a retention period (e.g., 30 days) or transition them to cheaper storage classes.

Recovery: To restore from backup: download the JSON file, parse it, and use DynamoDB BatchWriteItem to re-insert records.